diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 28b225ea430d..47efda824e51 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -24,7 +24,7 @@ num-bigint.workspace = true itertools.workspace = true qiskit-circuit.workspace = true qiskit-transpiler.workspace = true -qiskit-quantum-info.workspace = true +qiskit-quantum-info = { workspace = true, features = ["python"]} qiskit-util.workspace = true nalgebra.workspace = true diff --git a/crates/accelerate/src/sampled_exp_val.rs b/crates/accelerate/src/sampled_exp_val.rs index 296ad59956cc..e034b6f62e98 100644 --- a/crates/accelerate/src/sampled_exp_val.rs +++ b/crates/accelerate/src/sampled_exp_val.rs @@ -19,8 +19,8 @@ use pyo3::prelude::*; use pyo3::wrap_pyfunction; use crate::pauli_exp_val::fast_sum; +use qiskit_quantum_info::python::sparse_observable::PySparseObservable; use qiskit_quantum_info::sparse_observable::BitTerm; -use qiskit_quantum_info::sparse_observable::PySparseObservable; use qiskit_quantum_info::sparse_observable::SparseTermView; use qiskit_util::complex::c64; diff --git a/crates/cext/Cargo.toml b/crates/cext/Cargo.toml index b02bfe10c59d..416a2963c59f 100644 --- a/crates/cext/Cargo.toml +++ b/crates/cext/Cargo.toml @@ -35,5 +35,5 @@ num-bigint.workspace = true num-traits.workspace = true [features] -python_binding = ["dep:pyo3"] +python_binding = ["dep:pyo3", "qiskit-quantum-info/python"] cache_pygates = ["qiskit-circuit/cache_pygates", "qiskit-transpiler/cache_pygates"] diff --git a/crates/cext/src/sparse_observable.rs b/crates/cext/src/sparse_observable.rs index 8894d3f5719d..cfdb0d639aa1 100644 --- a/crates/cext/src/sparse_observable.rs +++ b/crates/cext/src/sparse_observable.rs @@ -1145,7 +1145,8 @@ mod py { use crate::pointers::mut_ptr_as_ref; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; - use qiskit_quantum_info::sparse_observable::{PySparseObservable, SparseObservable}; + use qiskit_quantum_info::python::sparse_observable::PySparseObservable; + use qiskit_quantum_info::sparse_observable::SparseObservable; use std::sync; fn try_project_inner_observable<'a>( diff --git a/crates/circuit/src/operations.rs b/crates/circuit/src/operations.rs index 746bd9783e09..d30517511788 100644 --- a/crates/circuit/src/operations.rs +++ b/crates/circuit/src/operations.rs @@ -11,7 +11,7 @@ // that they have been altered from the originals. use approx::relative_eq; -use qiskit_quantum_info::sparse_pauli_op::MatrixCompressedPaulis; +use qiskit_quantum_info::python::sparse_pauli_op::MatrixCompressedPaulis; use std::any::Any; use std::fmt::Debug; use std::num::NonZero; diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index c8e8bcfa71e8..25e441a0ab59 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -72,7 +72,7 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, ::qiskit_accelerate::optimize_1q_gates::optimize_1q_gates, "optimize_1q_gates")?; add_submodule(m, ::qiskit_transpiler::passes::optimize_1q_gates_decomposition_mod, "optimize_1q_gates_decomposition")?; add_submodule(m, ::qiskit_accelerate::pauli_exp_val::pauli_expval, "pauli_expval")?; - add_submodule(m, ::qiskit_quantum_info::pauli_lindblad_map::pauli_lindblad_map, "pauli_lindblad_map")?; + add_submodule(m, ::qiskit_quantum_info::python::pauli_lindblad_map::pauli_lindblad_map, "pauli_lindblad_map")?; add_submodule(m, ::qiskit_transpiler::passes::high_level_synthesis_mod, "high_level_synthesis")?; add_submodule(m, ::qiskit_transpiler::passes::remove_diagonal_gates_before_measure_mod, "remove_diagonal_gates_before_measure")?; add_submodule(m, ::qiskit_transpiler::passes::remove_identity_equiv_mod, "remove_identity_equiv")?; @@ -84,8 +84,8 @@ fn _accelerate(m: &Bound) -> PyResult<()> { add_submodule(m, ::qiskit_accelerate::results::results, "results")?; add_submodule(m, ::qiskit_transpiler::passes::sabre::sabre, "sabre")?; add_submodule(m, ::qiskit_accelerate::sampled_exp_val::sampled_exp_val, "sampled_exp_val")?; - add_submodule(m, ::qiskit_quantum_info::sparse_observable::sparse_observable, "sparse_observable")?; - add_submodule(m, ::qiskit_quantum_info::sparse_pauli_op::sparse_pauli_op, "sparse_pauli_op")?; + add_submodule(m, ::qiskit_quantum_info::python::sparse_observable::sparse_observable, "sparse_observable")?; + add_submodule(m, ::qiskit_quantum_info::python::sparse_pauli_op::sparse_pauli_op, "sparse_pauli_op")?; add_submodule(m, ::qiskit_transpiler::passes::scheduling_mod, "scheduling")?; add_submodule(m, ::qiskit_synthesis::matrix::sim::unitary_sim, "unitary_sim")?; add_submodule(m, ::qiskit_transpiler::passes::split_2q_unitaries_mod, "split_2q_unitaries")?; diff --git a/crates/qpy/src/py_methods.rs b/crates/qpy/src/py_methods.rs index ad2fd8d3495d..bc614552aede 100644 --- a/crates/qpy/src/py_methods.rs +++ b/crates/qpy/src/py_methods.rs @@ -28,7 +28,7 @@ use qiskit_circuit::imports; use qiskit_circuit::operations::{Operation, OperationRef, PyInstruction, PyOpKind, PyRange}; use qiskit_circuit::packed_instruction::PackedOperation; use qiskit_circuit::parameter::parameter_expression::{PyParameter, PyParameterExpression}; -use qiskit_quantum_info::sparse_observable::PySparseObservable; +use qiskit_quantum_info::python::sparse_observable::PySparseObservable; use uuid::Uuid; use crate::bytes::Bytes; diff --git a/crates/quantum_info/src/lib.rs b/crates/quantum_info/src/lib.rs index d35c3d6e11d1..68569d082831 100644 --- a/crates/quantum_info/src/lib.rs +++ b/crates/quantum_info/src/lib.rs @@ -12,27 +12,12 @@ pub mod clifford; pub mod pauli_lindblad_map; -pub mod sparse_observable; #[cfg(feature = "python")] -pub mod sparse_pauli_op; +pub mod python; +pub mod sparse_observable; + pub mod unitary_compose; pub mod versor_u2; -#[cfg(feature = "python")] // Only currently used by python remove if needed from rust -mod rayon_ext; #[cfg(test)] mod test; - -#[cfg(feature = "python")] -use pyo3::import_exception; - -#[cfg(feature = "python")] -pub(crate) mod imports { - use qiskit_util::py::ImportOnceCell; - - pub static PAULI_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "Pauli"); - pub static PAULI_LIST_TYPE: ImportOnceCell = - ImportOnceCell::new("qiskit.quantum_info", "PauliList"); -} -#[cfg(feature = "python")] -import_exception!(qiskit.exceptions, QiskitError); diff --git a/crates/quantum_info/src/pauli_lindblad_map/mod.rs b/crates/quantum_info/src/pauli_lindblad_map/mod.rs index 38dd354ec362..da21cf592928 100644 --- a/crates/quantum_info/src/pauli_lindblad_map/mod.rs +++ b/crates/quantum_info/src/pauli_lindblad_map/mod.rs @@ -13,22 +13,3 @@ pub mod pauli_lindblad_map_class; pub mod phased_qubit_sparse_pauli; pub mod qubit_sparse_pauli; - -#[cfg(feature = "python")] -use pauli_lindblad_map_class::PyPauliLindbladMap; -#[cfg(feature = "python")] -use phased_qubit_sparse_pauli::{PyPhasedQubitSparsePauli, PyPhasedQubitSparsePauliList}; -#[cfg(feature = "python")] -use pyo3::prelude::*; -#[cfg(feature = "python")] -use qubit_sparse_pauli::{PyQubitSparsePauli, PyQubitSparsePauliList}; - -#[cfg(feature = "python")] -pub fn pauli_lindblad_map(m: &Bound) -> PyResult<()> { - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - Ok(()) -} diff --git a/crates/quantum_info/src/pauli_lindblad_map/pauli_lindblad_map_class.rs b/crates/quantum_info/src/pauli_lindblad_map/pauli_lindblad_map_class.rs index 0f0bee7e9a9c..bcd6a4bb104a 100644 --- a/crates/quantum_info/src/pauli_lindblad_map/pauli_lindblad_map_class.rs +++ b/crates/quantum_info/src/pauli_lindblad_map/pauli_lindblad_map_class.rs @@ -11,40 +11,18 @@ // that they have been altered from the originals. use hashbrown::HashSet; -#[cfg(feature = "python")] -use numpy::{PyArray1, PyArray2, PyArrayMethods, ToPyArray}; -#[cfg(feature = "python")] -use pyo3::{ - IntoPyObjectExt, PyErr, - exceptions::{PyTypeError, PyValueError}, - intern, - prelude::*, - types::{PyList, PyString, PyTuple, PyType}, -}; use std::collections::btree_map; -#[cfg(feature = "python")] -use std::sync::{Arc, RwLock}; - use rand::prelude::*; use rand::rngs::SysRng; use rand_distr::Bernoulli; use rand_pcg::Pcg64Mcg; -#[cfg(feature = "python")] -use qiskit_util::py::{PySequenceIndex, SequenceIndex}; - use super::qubit_sparse_pauli::{ ArithmeticError, CoherenceError, LabelError, Pauli, QubitSparsePauli, QubitSparsePauliList, QubitSparsePauliView, }; -#[cfg(feature = "python")] -use super::qubit_sparse_pauli::{ - InnerReadError, InnerWriteError, PyQubitSparsePauli, PyQubitSparsePauliList, - raw_parts_from_sparse_list, -}; - /// A Pauli Lindblad map that stores its data in a qubit-sparse format. Note that gamma, /// probabilities, and non_negative_rates are quantities derived from rates. /// @@ -53,16 +31,16 @@ use super::qubit_sparse_pauli::{ pub struct PauliLindbladMap { /// The rates of each abstract term in the generator sum. This has as many elements as /// terms in the sum. - rates: Vec, + pub(crate) rates: Vec, /// A list of qubit sparse Paulis corresponding to the rates - qubit_sparse_pauli_list: QubitSparsePauliList, + pub(crate) qubit_sparse_pauli_list: QubitSparsePauliList, /// The gamma parameter. - gamma: f64, + pub(crate) gamma: f64, /// Probability of application of a given Pauli operator in product form. Note that if the /// corresponding rate is less than 0, this is a quasi-probability. - probabilities: Vec, + pub(crate) probabilities: Vec, /// List of boolean values for the statement rate >= 0 for each rate in rates. - non_negative_rates: Vec, + pub(crate) non_negative_rates: Vec, } impl PauliLindbladMap { @@ -576,8 +554,8 @@ impl GeneratorTermView<'_> { #[derive(Clone, Debug, PartialEq)] pub struct GeneratorTerm { /// The real rate of the term. - rate: f64, - qubit_sparse_pauli: QubitSparsePauli, + pub(crate) rate: f64, + pub(crate) qubit_sparse_pauli: QubitSparsePauli, } impl GeneratorTerm { pub fn num_qubits(&self) -> u32 { @@ -611,1390 +589,3 @@ impl GeneratorTerm { ) } } - -/// A single term from a complete :class:`PauliLindbladMap`. -/// -/// These are typically created by indexing into or iterating through a :class:`PauliLindbladMap`. -#[cfg(feature = "python")] -#[pyclass( - name = "GeneratorTerm", - frozen, - module = "qiskit.quantum_info", - skip_from_py_object -)] -#[derive(Clone, Debug)] -struct PyGeneratorTerm { - inner: GeneratorTerm, -} - -#[cfg(feature = "python")] -#[pymethods] -impl PyGeneratorTerm { - // Mark the Python class as being defined "within" the `PauliLindbladMap` class namespace. - #[classattr] - #[pyo3(name = "__qualname__")] - fn type_qualname() -> &'static str { - "PauliLindbladMap.GeneratorTerm" - } - - #[new] - #[pyo3(signature = (/, rate, qubit_sparse_pauli))] - fn py_new(rate: f64, qubit_sparse_pauli: &PyQubitSparsePauli) -> PyResult { - let inner = GeneratorTerm { - rate, - qubit_sparse_pauli: qubit_sparse_pauli.inner().clone(), - }; - Ok(PyGeneratorTerm { inner }) - } - - /// Convert this term to a complete :class:`PauliLindbladMap`. - fn to_pauli_lindblad_map(&self) -> PyResult { - let pauli_lindblad_map = PauliLindbladMap::new( - vec![self.inner.rate()], - self.inner.qubit_sparse_pauli.to_qubit_sparse_pauli_list(), - )?; - Ok(pauli_lindblad_map.into()) - } - - fn __eq__(slf: Bound, other: Bound) -> PyResult { - if slf.is(&other) { - return Ok(true); - } - let Ok(other) = other.cast_into::() else { - return Ok(false); - }; - let slf = slf.borrow(); - let other = other.borrow(); - Ok(slf.inner.eq(&other.inner)) - } - - fn __repr__(&self) -> PyResult { - Ok(format!( - "<{} on {} qubit{}: {}>", - Self::type_qualname(), - self.inner.num_qubits(), - if self.inner.num_qubits() == 1 { - "" - } else { - "s" - }, - self.inner.view().to_sparse_str(), - )) - } - - /// Get a copy of this term. - fn copy(&self) -> Self { - self.clone() - } - - /// Read-only view onto the individual single-qubit terms. - /// - /// The only valid values in the array are those with a corresponding - /// :class:`~PauliLindbladMap.Pauli`. - #[getter] - fn get_paulis(slf_: Bound) -> Bound> { - let borrowed = slf_.borrow(); - let paulis = borrowed.inner.paulis(); - let arr = ::ndarray::aview1(::bytemuck::cast_slice::<_, u8>(paulis)); - // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. - // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the - // `Box<[Pauli]>` allocation (including dropping or reallocating it) other than the entire - // object getting dropped, which Python will keep safe. - let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; - out.readwrite().make_nonwriteable(); - out - } - - /// The number of qubits the term is defined on. - #[getter] - fn get_num_qubits(&self) -> u32 { - self.inner.num_qubits() - } - - /// The term's rate. - #[getter] - fn get_rate(&self) -> f64 { - self.inner.rate() - } - - /// The term's qubit_sparse_pauli. - #[getter] - fn get_qubit_sparse_pauli(&self) -> PyQubitSparsePauli { - self.inner.qubit_sparse_pauli.clone().into() - } - - /// Read-only view onto the indices of each non-identity single-qubit term. - /// - /// The indices will always be in sorted order. - #[getter] - fn get_indices(slf_: Bound) -> Bound> { - let borrowed = slf_.borrow(); - let indices = borrowed.inner.indices(); - let arr = ::ndarray::aview1(indices); - // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. - // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the - // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire - // object getting dropped, which Python will keep safe. - let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; - out.readwrite().make_nonwriteable(); - out - } - - fn __getnewargs__(slf_: Bound) -> PyResult> { - let py = slf_.py(); - let borrowed = slf_.borrow(); - ( - borrowed.inner.rate, - borrowed.inner.qubit_sparse_pauli.clone(), - ) - .into_pyobject(py) - } - - /// Return the pauli labels of the term as a string. - /// - /// The pauli labels will match the order of :attr:`.GeneratorTerm.indices`, such that the - /// i-th character in the string is applied to the qubit index at ``term.indices[i]``. E.g. the - /// term with operator ``X`` acting on qubit 0 and ``Y`` acting on qubit ``3`` will have - /// ``term.indices == np.array([0, 3])`` and ``term.pauli_labels == "XY"``. - /// - /// Returns: - /// The non-identity bit terms as a concatenated string. - fn pauli_labels<'py>(&self, py: Python<'py>) -> Bound<'py, PyString> { - let string: String = self - .inner - .paulis() - .iter() - .map(|bit| bit.py_label()) - .collect(); - PyString::new(py, string.as_str()) - } -} - -/// A Pauli Lindblad map stored in a qubit-sparse format. -/// -/// Mathematics -/// =========== -/// -/// A Pauli-Lindblad map is a linear map acting on density matrices on :math:`n`-qubits of the form: -/// -/// .. math:: -/// -/// \Lambda\bigl[\circ\bigr] = \exp\left(\sum_{P \in K} \lambda_P (P \circ P - \circ)\right) -/// -/// where :math:`K` is a subset of :math:`n`-qubit Pauli operators, and the rates, or coefficients, -/// :math:`\lambda_P` are real numbers. When all the rates :math:`\lambda_P` are non-negative, this -/// corresponds to a completely positive and trace preserving map. The sum in the exponential is -/// called the generator, and each individual term the generators. To simplify notation in the rest -/// of the documentation, we denote :math:`L(P)\bigl[\circ\bigr] = P \circ P - \circ`. -/// -/// Quasi-probability representation -/// ================================ -/// -/// The map :math:`\Lambda` can be written as a product: -/// -/// .. math:: -/// -/// \Lambda\bigl[\circ\bigr] = \prod_{P \in K}\exp\left(\lambda_P(P \circ P - \circ)\right). -/// -/// For each :math:`P`, it holds that -/// -/// .. math:: -/// -/// \exp\left(\lambda_P(P \circ P - \circ)\right) = \omega(\lambda_P) \circ + (1 - \omega(\lambda_P)) P \circ P, -/// -/// where :math:`\omega(x) = \frac{1}{2}(1 + e^{-2 x})`. Observe that if :math:`\lambda_P \geq 0`, -/// then :math:`\omega(\lambda_P) \in (\frac{1}{2}, 1]`, and this term is a completely-positive and -/// trace-preserving map. However, if :math:`\lambda_P < 0`, then :math:`\omega(\lambda_P) > 1` and -/// the map is not completely positive or trace preserving. Letting -/// :math:`\gamma_P = \omega(\lambda_P) + |1 - \omega(\lambda_P)|`, -/// :math:`p_P = \omega(\lambda_P) / \gamma_P` and :math:`b_P \in \{0, 1\}` be :math:`1` if -/// :math:`\lambda_P < 0` and :math:`0` otherwise, we rewrite the map as: -/// -/// .. math:: -/// -/// \omega(\lambda_P) \circ + (1 - \omega(\lambda_P)) P \circ P = \gamma_P \left(p_P \circ + (-1)^{b_P}(1 - p_P) P \circ P\right). -/// -/// If :math:`\lambda_P \geq 0`, :math:`\gamma_P = 1` and the expression reduces to the standard -/// mixture of the identity map and conjugation by :math:`P`. If :math:`\lambda_P < 0`, -/// :math:`\gamma_P > 1`, and the map is a scaled difference of the identity map and conjugation by -/// :math:`P`, with probability weights (hence "quasi-probability"). Note that this is a slightly -/// different presentation than in the literature, but this notation allows us to handle both -/// non-negative and negative rates simultaneously. The overall :math:`\gamma` of the channel is the -/// product :math:`\gamma = \prod_{P \in K} \gamma_P`. -/// -/// See the :meth:`.PauliLindbladMap.sample` method for the sampling procedure for this map. -/// -/// Representation -/// ============== -/// -/// Each individual Pauli operator in the generator is a tensor product of single-qubit Pauli -/// operators of the form :math:`P = \bigotimes_n A^{(n)}_i`, for :math:`A^{(n)}_i \in \{I, X, Y, -/// Z\}`. The internal representation of a :class:`PauliLindbladMap` stores only the non-identity -/// single-qubit Pauli operators. This makes it significantly more efficient to represent -/// generators such as :math:`\sum_{n\in \text{qubits}} c_n L(Z^{(n)})`; for which -/// :class:`PauliLindbladMap` requires an amount of memory linear in the total number of qubits. -/// -/// Internally, :class:`.PauliLindbladMap` stores an array of rates and a -/// :class:`.QubitSparsePauliList` containing the corresponding sparse Pauli operators. -/// Additionally, :class:`.PauliLindbladMap` can compute the overall channel :math:`\gamma` in the -/// :meth:`gamma` method, as well as the corresponding probabilities :math:`p_P` -/// via the :meth:`probabilities` method. -/// -/// Indexing -/// -------- -/// -/// :class:`PauliLindbladMap` behaves as `a Python sequence -/// `__ (the standard form, not the expanded -/// :class:`collections.abc.Sequence`). The generators of the map can be indexed by integers, and -/// iterated through to yield individual generator terms. -/// -/// Each generator term appears as an instance a self-contained class. The individual terms are -/// copied out of the base map; mutations to them will not affect the original map from which they -/// are indexed. -/// -/// .. autoclass:: qiskit.quantum_info::PauliLindbladMap.GeneratorTerm -/// :members: -/// -/// Construction -/// ============ -/// -/// :class:`PauliLindbladMap` defines several constructors. The default constructor will attempt to -/// delegate to one of the more specific constructors, based on the type of the input. You can -/// always use the specific constructors to have more control over the construction. -/// -/// .. _pauli-lindblad-map-convert-constructors: -/// .. table:: Construction from other objects -/// -/// ============================ ================================================================ -/// Method Summary -/// ============================ ================================================================ -/// :meth:`from_list` Generators given as a list of tuples of dense string labels and -/// the associated rates. -/// -/// :meth:`from_sparse_list` Generators given as a list of tuples of sparse string labels, -/// the qubits they apply to, and their rates. -/// -/// :meth:`from_terms` Sum explicit single :class:`GeneratorTerm` instances. -/// -/// :meth:`from_components` Build from an array of rates and a -/// :class:`.QubitSparsePauliList` instance. -/// ============================ ================================================================ -/// -/// .. py:function:: PauliLindbladMap.__new__(data, /, num_qubits=None) -/// -/// The default constructor of :class:`PauliLindbladMap`. -/// -/// This delegates to one of :ref:`the explicit conversion-constructor methods -/// `, based on the type of the ``data`` argument. If -/// ``num_qubits`` is supplied and constructor implied by the type of ``data`` does not accept a -/// number, the given integer must match the input. -/// -/// :param data: The data type of the input. This can be another :class:`PauliLindbladMap`, in -/// which case the input is copied, or it can be a list in a valid format for either -/// :meth:`from_list` or :meth:`from_sparse_list`. -/// :param int|None num_qubits: Optional number of qubits for the map. For most data -/// inputs, this can be inferred and need not be passed. It is only necessary for empty -/// lists or the sparse-list format. If given unnecessarily, it must match the data input. -/// -/// In addition to the conversion-based constructors, there are also helper methods that construct -/// special forms of maps. -/// -/// .. table:: Construction of special maps -/// -/// ============================ ================================================================ -/// Method Summary -/// ============================ ================================================================ -/// :meth:`identity` The identity map on a given number of qubits. -/// ============================ ================================================================ -/// -/// Conversions -/// =========== -/// -/// An existing :class:`PauliLindbladMap` can be converted into other formats. -/// -/// .. table:: Conversion methods to other forms. -/// -/// =========================== ================================================================= -/// Method Summary -/// =========================== ================================================================= -/// :meth:`to_sparse_list` Express the map in a sparse list format with elements -/// ``(paulis, indices, rate)``. -/// =========================== ================================================================= -#[cfg(feature = "python")] -#[pyclass(name = "PauliLindbladMap", module = "qiskit.quantum_info", sequence)] -#[derive(Debug)] -pub struct PyPauliLindbladMap { - // This class keeps a pointer to a pure Rust-GeneratorTerm and serves as interface from Python. - inner: Arc>, -} - -#[cfg(feature = "python")] -#[pymethods] -impl PyPauliLindbladMap { - #[pyo3(signature = (data, /, num_qubits=None))] - #[new] - fn py_new(data: &Bound, num_qubits: Option) -> PyResult { - let py = data.py(); - let check_num_qubits = |data: &Bound| -> PyResult<()> { - let Some(num_qubits) = num_qubits else { - return Ok(()); - }; - let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::()?; - if num_qubits == other_qubits { - return Ok(()); - } - Err(PyValueError::new_err(format!( - "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})" - ))) - }; - if let Ok(pauli_lindblad_map) = data.cast_exact::() { - check_num_qubits(data)?; - let borrowed = pauli_lindblad_map.borrow(); - let inner = borrowed.inner.read().map_err(|_| InnerReadError)?; - return Ok(inner.clone().into()); - } - // The type of `vec` is inferred from the subsequent calls to `Self::from_list` or - // `Self::from_sparse_list` to be either the two-tuple or the three-tuple form during the - // `extract`. The empty list will pass either, but it means the same to both functions. - if let Ok(vec) = data.extract() { - return Self::from_list(vec, num_qubits); - } - if let Ok(vec) = data.extract() { - let Some(num_qubits) = num_qubits else { - return Err(PyValueError::new_err( - "if using the sparse-list form, 'num_qubits' must be provided", - )); - }; - return Self::from_sparse_list(vec, num_qubits); - } - if let Ok(term) = data.cast_exact::() { - return term.borrow().to_pauli_lindblad_map(); - }; - if let Ok(pauli_lindblad_map) = Self::from_terms(data, num_qubits) { - return Ok(pauli_lindblad_map); - } - Err(PyTypeError::new_err(format!( - "unknown input format for 'PauliLindbladMap': {}", - data.get_type().repr()?, - ))) - } - - #[staticmethod] - fn from_components( - rates: Vec, - qubit_sparse_pauli_list: &PyQubitSparsePauliList, - ) -> PyResult { - let qubit_sparse_pauli_list = qubit_sparse_pauli_list - .inner - .read() - .map_err(|_| InnerReadError)?; - let inner = PauliLindbladMap::new(rates, qubit_sparse_pauli_list.clone())?; - - Ok(inner.into()) - } - - /// Construct a Pauli Lindblad map from a list of dense generator labels and rates. - /// - /// This is analogous to :meth:`.SparsePauliOp.from_list`. In this dense form, you must supply - /// all identities explicitly in each label. - /// - /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted - /// analogously to a bitstring. In other words, the right-most letter is associated with qubit - /// 0, and so on. This is the same as the labels for :class:`.Pauli` and - /// :class:`.SparsePauliOp`. - /// - /// Args: - /// iter (list[tuple[str, float]]): Pairs of labels and their associated rates in the - /// generator sum. - /// num_qubits (int | None): It is not necessary to specify this if you are sure that - /// ``iter`` is not an empty sequence, since it can be inferred from the label lengths. - /// If ``iter`` may be empty, you must specify this argument to disambiguate how many - /// qubits the map acts on. If this is given and ``iter`` is not empty, the value - /// must match the label lengths. - /// - /// Examples: - /// - /// Construct a Pauli Lindblad map from a list of labels:: - /// - /// >>> PauliLindbladMap.from_list([ - /// ... ("IIIXX", 1.0), - /// ... ("IIYYI", 1.0), - /// ... ("IXXII", -0.5), - /// ... ("ZZIII", -0.25), - /// ... ]) - /// - /// - /// Use ``num_qubits`` to disambiguate potentially empty inputs:: - /// - /// >>> PauliLindbladMap.from_list([], num_qubits=10) - /// - /// - /// This method is equivalent to calls to :meth:`from_sparse_list` with the explicit - /// qubit-arguments field set to decreasing integers:: - /// - /// >>> labels = ["XYXZ", "YYZZ", "XYXZ"] - /// >>> rates = [1.5, 2.0, -0.5] - /// >>> from_list = PauliLindbladMap.from_list(list(zip(labels, rates))) - /// >>> from_sparse_list = PauliLindbladMap.from_sparse_list([ - /// ... (label, (3, 2, 1, 0), rate) - /// ... for label, rate in zip(labels, rates) - /// ... ]) - /// >>> assert from_list == from_sparse_list - /// - /// See also: - /// :meth:`from_sparse_list` - /// Construct the map from a list of labels without explicit identities, but with - /// the qubits each single-qubit generator term applies to listed explicitly. - #[staticmethod] - #[pyo3(signature = (iter, /, *, num_qubits=None))] - fn from_list(iter: Vec<(String, f64)>, num_qubits: Option) -> PyResult { - if iter.is_empty() && num_qubits.is_none() { - return Err(PyValueError::new_err( - "cannot construct a PauliLindbladMap from an empty list without knowing `num_qubits`", - )); - } - let num_qubits = match num_qubits { - Some(num_qubits) => num_qubits, - None => iter[0].0.len() as u32, - }; - let mut inner = PauliLindbladMap::with_capacity(num_qubits, iter.len(), 0); - for (label, rate) in iter { - inner.add_dense_label(&label, rate)?; - } - Ok(inner.into()) - } - - /// Get the identity map on the given number of qubits. - /// - /// The identity map contains no generator terms, and is the identity element for composition of - /// two :class:`PauliLindbladMap` instances; anything composed with the identity map is equal to - /// itself. - /// - /// Examples: - /// - /// Get the identity map on 100 qubits:: - /// - /// >>> PauliLindbladMap.identity(100) - /// - #[pyo3(signature = (/, num_qubits))] - #[staticmethod] - pub fn identity(num_qubits: u32) -> Self { - PauliLindbladMap::identity(num_qubits).into() - } - - /// Construct a :class:`PauliLindbladMap` out of individual terms. - /// - /// All the terms must have the same number of qubits. If supplied, the ``num_qubits`` argument - /// must match the terms. - /// - /// No simplification is done as part of the map creation. - /// - /// Args: - /// obj (Iterable[Term]): Iterable of individual terms to build the map generator from. - /// num_qubits (int | None): The number of qubits the map should act on. This is - /// usually inferred from the input, but can be explicitly given to handle the case - /// of an empty iterable. - /// - /// Returns: - /// The corresponding map. - #[staticmethod] - #[pyo3(signature = (obj, /, num_qubits=None))] - fn from_terms(obj: &Bound, num_qubits: Option) -> PyResult { - let mut iter = obj.try_iter()?; - let mut inner = match num_qubits { - Some(num_qubits) => PauliLindbladMap::identity(num_qubits), - None => { - let Some(first) = iter.next() else { - return Err(PyValueError::new_err( - "cannot construct a PauliLindbladMap from an empty list without knowing `num_qubits`", - )); - }; - let py_term = first?.cast::()?.borrow(); - py_term.inner.to_pauli_lindblad_map()? - } - }; - for bound_py_term in iter { - let py_term = bound_py_term?.cast::()?.borrow(); - inner.add_term(py_term.inner.view())?; - } - Ok(inner.into()) - } - - /// Clear all the generator terms from this map, making it equal to the identity map again. - /// - /// This does not change the capacity of the internal allocations, so subsequent addition or - /// subtraction operations resulting from composition may not need to reallocate. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> pauli_lindblad_map = PauliLindbladMap.from_list([("IXXXYY", 2.0), ("ZZYZII", -1)]) - /// >>> pauli_lindblad_map.clear() - /// >>> assert pauli_lindblad_map == PauliLindbladMap.identity(pauli_lindblad_map.py_num_qubits()) - pub fn clear(&mut self) -> PyResult<()> { - let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; - inner.clear(); - Ok(()) - } - - /// Construct a Pauli Lindblad map from a list of labels, the qubits each item applies to, and - /// the rate of the whole term. - /// - /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`. - /// - /// The "labels" and "indices" fields of the triples are associated by zipping them together. - /// For example, this means that a call to :meth:`from_list` can be converted to the form used - /// by this method by setting the "indices" field of each triple to ``(num_qubits-1, ..., 1, - /// 0)``. - /// - /// Args: - /// iter (list[tuple[str, Sequence[int], float]]): triples of labels, the qubits - /// each single-qubit term applies to, and the rate of the entire term. - /// - /// num_qubits (int): the number of qubits the map acts on. - /// - /// Examples: - /// - /// Construct a simple map:: - /// - /// >>> PauliLindbladMap.from_sparse_list( - /// ... [("ZX", (1, 4), 1.0), ("YY", (0, 3), 2)], - /// ... num_qubits=5, - /// ... ) - /// - /// - /// This method can replicate the behavior of :meth:`from_list`, if the qubit-arguments - /// field of the triple is set to decreasing integers:: - /// - /// >>> labels = ["XYXZ", "YYZZ", "XYXZ"] - /// >>> rates = [1.5, 2.0, -0.5] - /// >>> from_list = PauliLindbladMap.from_list(list(zip(labels, rates))) - /// >>> from_sparse_list = PauliLindbladMap.from_sparse_list([ - /// ... (label, (3, 2, 1, 0), rate) - /// ... for label, rate in zip(labels, rates) - /// ... ]) - /// >>> assert from_list == from_sparse_list - /// - /// See also: - /// :meth:`to_sparse_list` - /// The reverse of this method. - #[staticmethod] - #[pyo3(signature = (iter, /, num_qubits))] - fn from_sparse_list(iter: Vec<(String, Vec, f64)>, num_qubits: u32) -> PyResult { - let rates = iter.iter().map(|(_, _, rate)| *rate).collect(); - let op_iter = iter - .iter() - .map(|(label, indices, _)| (label.clone(), indices.clone())) - .collect(); - let (paulis, indices, boundaries) = raw_parts_from_sparse_list(op_iter, num_qubits)?; - let qubit_sparse_pauli_list = - QubitSparsePauliList::new(num_qubits, paulis, indices, boundaries)?; - let inner: PauliLindbladMap = PauliLindbladMap::new(rates, qubit_sparse_pauli_list)?; - Ok(inner.into()) - } - - /// Get a copy of this Pauli Lindblad map. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> pauli_lindblad_map = PauliLindbladMap.from_list([("IXZXYYZZ", 2.5), ("ZXIXYYZZ", 0.5)]) - /// >>> assert pauli_lindblad_map == pauli_lindblad_map.copy() - /// >>> assert pauli_lindblad_map is not pauli_lindblad_map.copy() - fn copy(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.clone().into()) - } - - /// The number of qubits the map acts on. - /// - /// This is not inferable from any other shape or values, since identities are not stored - /// explicitly. - #[getter] - #[inline] - pub fn num_qubits(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.num_qubits()) - } - - /// The number of generator terms in the exponent for this map. - #[getter] - #[inline] - pub fn num_terms(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.num_terms()) - } - - /// Calculate the :math:`\gamma` for the map. - #[inline] - fn gamma(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.gamma) - } - - /// The rates for the map. - #[getter] - fn get_rates(slf_: Bound) -> Bound> { - let borrowed = &slf_.borrow(); - let inner = borrowed.inner.read().unwrap(); - let rates = inner.rates(); - let arr = ::ndarray::aview1(rates); - // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. - // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the - // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire - // object getting dropped, which Python will keep safe. - let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; - out.readwrite().make_nonwriteable(); - out - } - - /// Calculate the probabilities :math:`p_P` for the map. - /// These can be interpreted as the probabilities each generator is not applied, - /// and are defined to be independent of the sign of each Lindblad rate. - fn probabilities<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { - let inner = self.inner.read().unwrap(); - inner.probabilities().to_pyarray(py) - } - - /// Get a copy of the map's qubit sparse pauli list. - fn get_qubit_sparse_pauli_list_copy(&self) -> PyQubitSparsePauliList { - let inner = self.inner.read().unwrap(); - inner.qubit_sparse_pauli_list.clone().into() - } - - /// Get the generators of the map. - /// - /// This is an alias for :meth:`get_qubit_sparse_pauli_list_copy`, providing a more - /// convenient name that aligns with the naming conventions used in Aer and Runtime - /// for similar classes. - /// - /// Returns: - /// QubitSparsePauliList: A copy of the map's generator terms. - fn generators(&self) -> PyQubitSparsePauliList { - self.get_qubit_sparse_pauli_list_copy() - } - - /// Express the map in terms of a sparse list format. - /// - /// This can be seen as counter-operation of :meth:`.PauliLindbladMap.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. - /// - /// Examples: - /// - /// >>> pauli_lindblad_map = PauliLindbladMap.from_list([("IIXIZ", 2), ("IIZIX", 3)]) - /// >>> reconstructed = PauliLindbladMap.from_sparse_list(pauli_lindblad_map.to_sparse_list(), pauli_lindblad_map.num_qubits) - /// - /// See also: - /// :meth:`from_sparse_list` - /// The constructor that can interpret these lists. - #[pyo3(signature = ())] - fn to_sparse_list(&self, py: Python) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - - // turn a SparseView into a Python tuple of (bit terms, indices, rate) - let to_py_tuple = |view: GeneratorTermView| { - let mut pauli_string = String::with_capacity(view.qubit_sparse_pauli.paulis.len()); - - for bit in view.qubit_sparse_pauli.paulis.iter() { - pauli_string.push_str(bit.py_label()); - } - let py_string = PyString::new(py, &pauli_string).unbind(); - let py_indices = PyList::new(py, view.qubit_sparse_pauli.indices.iter())?.unbind(); - let py_rate = view.rate.into_py_any(py)?; - - PyTuple::new(py, vec![py_string.as_any(), py_indices.as_any(), &py_rate]) - }; - - let out = PyList::empty(py); - for view in inner.iter() { - out.append(to_py_tuple(view)?)?; - } - Ok(out.unbind()) - } - - /// Apply a transpiler layout to this Pauli Lindblad map. - /// - /// This enables remapping of qubit indices, e.g. if the map is defined in terms of virtual - /// qubit labels. - /// - /// Args: - /// layout (TranspileLayout | list[int] | None): The layout to apply. Most uses of this - /// function should pass the :attr:`.QuantumCircuit.layout` field from a circuit that - /// was transpiled for hardware. In addition, you can pass a list of new qubit indices. - /// If given as explicitly ``None``, no remapping is applied (but you can still use - /// ``num_qubits`` to expand the map). - /// num_qubits (int | None): The number of qubits to expand the map to. If not - /// supplied, the output will be as wide as the given :class:`.TranspileLayout`, or the - /// same width as the input if the ``layout`` is given in another form. - /// - /// Returns: - /// A new :class:`PauliLindbladMap` with the provided layout applied. - #[pyo3(signature = (/, layout, num_qubits=None))] - fn apply_layout(&self, layout: Bound, num_qubits: Option) -> PyResult { - let py = layout.py(); - let inner = self.inner.read().map_err(|_| InnerReadError)?; - - // A utility to check the number of qubits is compatible with the map. - let check_inferred_qubits = |inferred: u32| -> PyResult { - if inferred < inner.num_qubits() { - return Err(CoherenceError::NotEnoughQubits { - current: inner.num_qubits() as usize, - target: inferred as usize, - } - .into()); - } - Ok(inferred) - }; - - // Normalize the number of qubits in the layout and the layout itself, depending on the - // input types, before calling PauliLindbladMap.apply_layout to do the actual work. - let (num_qubits, layout): (u32, Option>) = if layout.is_none() { - (num_qubits.unwrap_or(inner.num_qubits()), None) - } else if layout.is_instance( - &py.import(intern!(py, "qiskit.transpiler"))? - .getattr(intern!(py, "TranspileLayout"))?, - )? { - ( - check_inferred_qubits( - layout.getattr(intern!(py, "_output_qubit_list"))?.len()? as u32 - )?, - Some( - layout - .call_method0(intern!(py, "final_index_layout"))? - .extract::>()?, - ), - ) - } else { - ( - check_inferred_qubits(num_qubits.unwrap_or(inner.num_qubits()))?, - Some(layout.extract()?), - ) - }; - - let out = inner.apply_layout(layout.as_deref(), num_qubits)?; - Ok(out.into()) - } - - /// Return a new :class:`PauliLindbladMap` with rates scaled by `scale_factor`. - /// - /// Args: - /// scale_factor (float): the scaling coefficient. - #[pyo3(signature = (scale_factor))] - fn scale_rates<'py>( - &self, - py: Python<'py>, - scale_factor: f64, - ) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let scaled = inner.clone().scale_rates(scale_factor); - scaled.into_pyobject(py) - } - - /// Return a new :class:`PauliLindbladMap` that is the mathematical inverse of `self`. - #[pyo3(signature = ())] - fn inverse<'py>(&self, py: Python<'py>) -> PyResult> { - self.scale_rates(py, -1.) - } - - /// Drop Paulis out of this Pauli Lindblad map. - /// - /// Drop every Pauli on the given `indices`, effectively replacing them with an identity. - /// - /// The resulting map may contain duplicates, which can be removed using the :meth:`PauliLindbladMap.simplify` - /// method. - /// - /// Args: - /// indices (Sequence[int]): The indices for which Paulis must be dropped. - /// - /// Returns: - /// A new Pauli Lindblad map where every Pauli on the given `indices` has been dropped. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> pauli_map_in = PauliLindbladMap.from_list([("XXIZI", 2.0), ("IIIYZ", 0.5), ("ZIIXY", -0.25)]) - /// >>> pauli_map_out = pauli_map_in.keep_paulis([1, 2, 4]) - /// >>> assert pauli_map_out == PauliLindbladMap.from_list([("IXIII", 2.0), ("IIIIZ", 0.5), ("IIIIY", -0.25)]) - #[pyo3(signature = (/, indices))] - pub fn drop_paulis(&self, indices: Vec) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - - let max_index = match indices.iter().max() { - Some(&index) => index, - None => 0, - }; - if max_index >= inner.num_qubits() { - let num_qubits = inner.num_qubits(); - return Err(PyValueError::new_err(format!( - "cannot drop Paulis for index {max_index} in a {num_qubits}-qubit PauliLindbladMap" - ))); - } - - Ok(inner.drop_paulis(indices.into_iter().collect())?.into()) - } - - /// Keep every Pauli on the given `indices` and drop all others. - /// - /// This is equivalent to using :meth:`PauliLindbladMap.drop_paulis` on the complement set of indices. - /// - /// Args: - /// indices (Sequence[int]): The indices for which Paulis must be kept. - /// - /// Returns: - /// A new Pauli Lindblad map where every Pauli on the given `indices` has been kept and all other - /// Paulis have been dropped. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> pauli_map_in = PauliLindbladMap.from_list([("XXIZI", 2.0), ("IIIYZ", 0.5), ("ZIIXY", -0.25)]) - /// >>> pauli_map_out = pauli_map_in.keep_paulis([0, 3]) - /// >>> assert pauli_map_out == PauliLindbladMap.from_list([("IXIII", 2.0), ("IIIIZ", 0.5), ("IIIIY", -0.25)]) - #[pyo3(signature = (/, indices))] - pub fn keep_paulis(&self, indices: Vec) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - - let max_index = match indices.iter().max() { - Some(&index) => index, - None => 0, - }; - if max_index >= inner.num_qubits() { - let num_qubits = inner.num_qubits(); - return Err(PyValueError::new_err(format!( - "cannot keep Paulis for index {max_index} in a {num_qubits}-qubit PauliLindbladMap" - ))); - } - - Ok(inner - .drop_paulis( - (0..self.num_qubits()?) - .filter(|index| !indices.contains(index)) - .collect(), - )? - .into()) - } - - /// Drop qubits out of this Pauli Lindblad map, effectively performing a trace-out operation. - /// - /// The resulting map may contain duplicates, which can be removed using the :meth:`PauliLindbladMap.simplify` - /// method. - /// - /// Args: - /// indices (Sequence[int]): The indices of the qubits to trace out. - /// - /// Returns: - /// A new Pauli Lindblad map where every Pauli on the given `indices` has been traced out. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> pauli_map_in = PauliLindbladMap.from_list([("XXIZI", 2.0), ("IIIYZ", 0.5), ("ZIIXY", -0.25)]) - /// >>> pauli_map_out = pauli_map_in.drop_qubits([1, 2, 4]) - /// >>> assert pauli_map_out == PauliLindbladMap.from_list([("XI", 2.0), ("IZ", 0.5), ("IY", -0.25)]) - #[pyo3(signature = (/, indices))] - pub fn drop_qubits(&self, indices: Vec) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - - let max_index = match indices.iter().max() { - Some(&index) => index, - None => 0, - }; - if max_index >= inner.num_qubits() { - let num_qubits = inner.num_qubits(); - return Err(PyValueError::new_err(format!( - "cannot drop qubits for index {max_index} in a {num_qubits}-qubit PauliLindbladMap" - ))); - } - - if (indices.len() as u32) == inner.num_qubits() { - return Err(PyValueError::new_err( - "cannot drop every qubit in the given PauliLindbladMap", - )); - } - - Ok(inner.drop_qubits(indices.into_iter().collect())?.into()) - } - - /// Keep every qubit on the given `indices` and trace out all other qubits. - /// - /// This is equivalent to using :meth:`PauliLindbladMap.drop_qubits` on the complement set of indices. - /// - /// Args: - /// indices (Sequence[int]): The indices for which qubits must be kept. - /// - /// Returns: - /// A new Pauli Lindblad map where every qubit on the given `indices` has been kept and all other - /// qubits have been traced out. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> pauli_map_in = PauliLindbladMap.from_list([("XXIZI", 2.0), ("IIIYZ", 0.5), ("ZIIXY", -0.25)]) - /// >>> pauli_map_out = pauli_map_in.keep_qubits([0, 3]) - /// >>> assert pauli_map_out == PauliLindbladMap.from_list([("XI", 2.0), ("IZ", 0.5), ("IY", -0.25)]) - #[pyo3(signature = (/, indices))] - pub fn keep_qubits(&self, indices: Vec) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - - let max_index = match indices.iter().max() { - Some(&index) => index, - None => 0, - }; - if max_index >= inner.num_qubits() { - let num_qubits = inner.num_qubits(); - return Err(PyValueError::new_err(format!( - "cannot keep qubits for index {max_index} in a {num_qubits}-qubit PauliLindbladMap" - ))); - } - - if indices.is_empty() { - return Err(PyValueError::new_err( - "cannot drop every qubit in the given PauliLindbladMap", - )); - } - - Ok(inner - .drop_qubits( - (0..self.num_qubits()?) - .filter(|index| !indices.contains(index)) - .collect(), - )? - .into()) - } - - /// Sample sign and Pauli operator pairs from the map. Note that the boolean sign convention in - /// this method is non-standard. The preferred method for this kind of sampling is - /// :meth:`.PauliLindbladMap.parity_sample`, which is also more featureful. - /// - /// Each sign is represented by a boolean, with ``True`` representing ``+1``, and ``False`` - /// representing ``-1``. - /// - /// Given the quasi-probability representation given in the class-level documentation, each - /// sample is drawn via the following process: - /// - /// * Initialize the sign boolean, and a :class:`~.QubitSparsePauli` instance to the identity - /// operator. - /// - /// * Iterate through each Pauli in the map. Using the pseudo-probability associated with - /// each operator, randomly choose between applying the operator or not. - /// - /// * If the operator is applied, update the :class`QubitSparsePauli` by multiplying it with - /// the Pauli. If the rate associated with the Pauli is negative, flip the sign boolean. - /// - /// The results are returned as a 1d array of booleans, and the corresponding sampled qubit - /// sparse Paulis in the form of a :class:`~.QubitSparsePauliList`. - /// - /// Args: - /// num_samples (int): Number of samples to draw. - /// seed (int): Random seed. - /// - /// Returns: - /// signs, qubit_sparse_pauli_list: The boolean array of signs and the list of qubit sparse - /// paulis. - #[pyo3(signature = (num_samples, seed=None))] - pub fn signed_sample<'py>( - &self, - py: Python<'py>, - num_samples: u64, - seed: Option, - ) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let (signs, paulis, _, _) = - py.detach(|| inner.parity_sample_with_history(num_samples, seed, None, None)); - - let signs = PyArray1::from_vec(py, signs.iter().map(|b| !b).collect()); - let paulis = paulis.into_pyobject(py).unwrap(); - - (signs, paulis).into_pyobject(py) - } - - /// Sample sign and Pauli operator pairs from the map. - /// - /// Each sign is represented by a boolean, with ``True`` representing ``-1``, and ``False`` - /// representing ``+1``. - /// - /// Given the quasi-probability representation given in the class-level documentation, each - /// sample is drawn via the following process: - /// - /// * Initialize the sign boolean, and a :class:`~.QubitSparsePauli` instance to the identity - /// operator. - /// - /// * Iterate through each Pauli in the map. Using the pseudo-probability associated with - /// each operator, randomly choose between applying the operator or not. - /// - /// * If the operator is applied, update the :class`QubitSparsePauli` by multiplying it with - /// the Pauli. If the rate associated with the Pauli is negative, flip the sign boolean. - /// - /// The results are returned as a 1d array of booleans, and the corresponding sampled qubit - /// sparse Paulis in the form of a :class:`~.QubitSparsePauliList`. - /// - /// The arguments ``scale`` and ``local_scale`` can be used to change the underlying rates used - /// in the sampling process without modifying current instance or requiring creating a new one. - /// The ``scale`` argument scales all rates by a fixed float, and ``local_scale`` scales rates - /// on a term-by-term basis. - /// - /// Args: - /// num_samples (int): Number of samples to draw. - /// seed (int): Random seed. - /// scale (float): Scale to apply to all rates. - /// local_scale (list[float]): Local scale to apply on a term-by-term basis. - /// - /// Returns: - /// signs, qubit_sparse_pauli_list: The boolean array of signs and the list of qubit sparse - /// paulis. - #[pyo3(signature = (num_samples, seed=None, scale=None, local_scale=None))] - pub fn parity_sample<'py>( - &self, - py: Python<'py>, - num_samples: u64, - seed: Option, - scale: Option, - local_scale: Option>, - ) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let (signs, paulis, _, _) = - py.detach(|| inner.parity_sample_with_history(num_samples, seed, scale, local_scale)); - - let signs = PyArray1::from_vec(py, signs); - let paulis = paulis.into_pyobject(py).unwrap(); - - (signs, paulis).into_pyobject(py) - } - - /// Sample sign and Pauli operator pairs from the map, preserving the sampled generators. - /// - /// This method is identical to :meth:`parity_sample` except for also returning the information - /// which :meth:`generators` were actually sampled to yield the final Pauli operator and sign. - /// - /// - /// Args: - /// num_samples (int): Number of samples to draw. - /// seed (int): Random seed. - /// scale (float): Scale to apply to all rates. - /// local_scale (list[float]): Local scale to apply on a term-by-term basis. - /// - /// Returns: - /// signs, qubit_sparse_pauli_list, pauli_history, signs_history: The boolean array of - /// signs, the list of qubit sparse paulis, the two dimensional boolean array - /// indicating which :meth:`generators` were sampled and the two dimensional boolean - /// array indicating their signs. - #[pyo3(signature = (num_samples, seed=None, scale=None, local_scale=None))] - pub fn parity_sample_with_history<'py>( - &self, - py: Python<'py>, - num_samples: u64, - seed: Option, - scale: Option, - local_scale: Option>, - ) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let (signs, paulis, pauli_history, signs_history) = - py.detach(|| inner.parity_sample_with_history(num_samples, seed, scale, local_scale)); - - let signs = PyArray1::from_vec(py, signs); - let paulis = paulis.into_pyobject(py).unwrap(); - let pauli_history = PyArray2::from_vec2(py, &pauli_history).unwrap(); - let signs_history = PyArray2::from_vec2(py, &signs_history).unwrap(); - - (signs, paulis, pauli_history, signs_history).into_pyobject(py) - } - - /// For :class:`.PauliLindbladMap` instances with purely non-negative rates, sample Pauli - /// operators from the map. If the map has negative rates, use - /// :meth:`.PauliLindbladMap.parity_sample`. - /// - /// Given the quasi-probability representation given in the class-level documentation, each - /// sample is drawn via the following process: - /// - /// * Initialize a :class`~.QubitSparsePauli` instance to the identity operator. - /// - /// * Iterate through each Pauli in the map. Using the pseudo-probability associated with - /// each operator, randomly choose between applying the operator or not. - /// - /// * If the operator is applied, update the :class`QubitSparsePauli` by multiplying it with - /// the Pauli. - /// - /// The sampled qubit sparse Paulis are returned in the form of a - /// :class:`~.QubitSparsePauliList`. - /// - /// Args: - /// num_samples (int): Number of samples to draw. - /// seed (int): Random seed. Defaults to ``None``. - /// - /// Returns: - /// qubit_sparse_pauli_list: The list of qubit sparse paulis. - /// - /// Raises: - /// ValueError: If any of the rates in the map are negative. - #[pyo3(signature = (num_samples, seed=None))] - pub fn sample<'py>( - &self, - py: Python<'py>, - num_samples: u64, - seed: Option, - ) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - - for non_negative in inner.non_negative_rates.iter() { - if !non_negative { - return Err(PyValueError::new_err( - "PauliLindbladMap.sample called for a map with negative rates. Use PauliLindbladMap.parity_sample", - )); - } - } - - let (_, paulis, _, _) = - py.detach(|| inner.parity_sample_with_history(num_samples, seed, None, None)); - - paulis.into_pyobject(py) - } - - /// Sum any like terms in the generator, removing them if the resulting rate has an absolute - /// value within tolerance of zero. This also removes terms whose Pauli operator is proportional - /// to the identity, as the corresponding generator is actually the zero map. - /// - /// As a side effect, this sorts the generators into a fixed canonical order. - /// - /// .. note:: - /// - /// When using this for equality comparisons, note that floating-point rounding and the - /// non-associativity of floating-point addition may cause non-zero coefficients of summed - /// terms to compare unequal. To compare two observables up to a tolerance, it is safest to - /// compare the canonicalized difference of the two observables to zero. - /// - /// Args: - /// tol (float): after summing like terms, any rates whose absolute value is less - /// than the given absolute tolerance will be suppressed from the output. - /// - /// Examples: - /// - /// Using :meth:`simplify` to compare two operators that represent the same map, but - /// would compare unequal due to the structural tests by default:: - /// - /// >>> base = PauliLindbladMap.from_sparse_list([ - /// ... ("XZ", (2, 1), 1e-10), # value too small - /// ... ("XX", (3, 1), 2), - /// ... ("XX", (3, 1), 2), # can be combined with the above - /// ... ("ZZ", (3, 1), 0.5), # out of order compared to `expected` - /// ... ], num_qubits=5) - /// >>> expected = PauliLindbladMap.from_list([("IZIZI", 0.5), ("IXIXI", 4)]) - /// >>> assert base != expected # non-canonical comparison - /// >>> assert base.simplify() == expected.simplify() - /// - /// Note that in the above example, the coefficients are chosen such that all floating-point - /// calculations are exact, and there are no intermediate rounding or associativity - /// concerns. If this cannot be guaranteed to be the case, the safer form is:: - /// - /// >>> left = PauliLindbladMap.from_list([("XYZ", 1.0/3.0)] * 3) # sums to 1.0 - /// >>> right = PauliLindbladMap.from_list([("XYZ", 1.0/7.0)] * 7) # doesn't sum to 1.0 - /// >>> assert left.simplify() != right.simplify() - /// >>> assert left.compose(right.inverse()).simplify() == PauliLindbladMap.identity(left.num_qubits) - #[pyo3( - signature = (/, tol=1e-8), - )] - fn simplify(&self, tol: f64) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let simplified = inner.simplify(tol); - Ok(simplified.into()) - } - - fn __len__(&self) -> PyResult { - self.num_terms() - } - - fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - ( - py.get_type::().getattr("from_sparse_list")?, - (self.to_sparse_list(py)?, inner.num_qubits()), - ) - .into_pyobject(py) - } - - /// Compose with another :class:`PauliLindbladMap`. - /// - /// This appends the internal arrays of self and other, and therefore results in a map with - /// whose enumerated terms are those of self followed by those of other. - /// - /// Args: - /// other (PauliLindbladMap): the Pauli Lindblad map to compose with. - fn compose<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { - let py = other.py(); - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let Some(other) = coerce_to_map(other)? else { - return Err(PyTypeError::new_err(format!( - "unknown type for compose: {}", - other.get_type().repr()? - ))); - }; - let other = other.borrow(); - let other_inner = other.inner.read().map_err(|_| InnerReadError)?; - let composed = inner.compose(&other_inner)?; - composed.into_pyobject(py) - } - - /// Compute the Pauli fidelity of this map for a qubit sparse Pauli. - /// - /// For a Pauli :math:`Q`, the fidelity with respect to the Pauli Lindblad map - /// :math:`\Lambda` is the real number :math:`f(Q)` for which :math:`\Lambda(Q) = f(Q) Q`. I.e. - /// every Pauli is an eigenvector of the linear map :math:`\Lambda`, and the fidelity is the - /// corresponding eigenvalue. For a Pauli Lindblad map with generator set :math:`K` and rate - /// function :math:`\lambda : K \rightarrow \mathbb{R}`, the pauli fidelity mathematically is - /// - /// .. math:: - /// - /// f(Q) = \exp\left(-2 \sum_{P \in K} \lambda(P) \langle P, Q\rangle_{sp}\right), - /// - /// where :math:`\langle P, Q\rangle_{sp}` is :math:`0` if :math:`P` and :math:`Q` commute, and - /// :math:`1` if they anti-commute. - /// - /// Args: qubit_sparse_pauli (QubitSparsePauli): the qubit sparse Pauli to compute the Pauli - /// fidelity of. - fn pauli_fidelity(&self, qubit_sparse_pauli: &PyQubitSparsePauli) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let result = inner.pauli_fidelity(qubit_sparse_pauli.inner())?; - Ok(result) - } - - fn __matmul__<'py>( - &self, - other: &Bound<'py, PyAny>, - ) -> PyResult> { - self.compose(other) - } - - fn __getitem__<'py>( - &self, - py: Python<'py>, - index: PySequenceIndex<'py>, - ) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let indices = match index.with_len(inner.num_terms())? { - SequenceIndex::Int(index) => { - return PyGeneratorTerm { - inner: inner.term(index).to_term(), - } - .into_bound_py_any(py); - } - indices => indices, - }; - let mut out = PauliLindbladMap::identity(inner.num_qubits()); - for index in indices.iter() { - out.add_term(inner.term(index))?; - } - out.into_bound_py_any(py) - } - - fn __eq__(slf: Bound, other: Bound) -> PyResult { - // this is also important to check before trying to read both slf and other - if slf.is(&other) { - return Ok(true); - } - let Ok(other) = other.cast_into::() else { - return Ok(false); - }; - let slf_borrowed = slf.borrow(); - let other_borrowed = other.borrow(); - let slf_inner = slf_borrowed.inner.read().map_err(|_| InnerReadError)?; - let other_inner = other_borrowed.inner.read().map_err(|_| InnerReadError)?; - Ok(slf_inner.eq(&other_inner)) - } - - // The documentation for this is inlined into the class-level documentation of - // `PauliLindbladMap`. - #[allow(non_snake_case)] - #[classattr] - fn GeneratorTerm(py: Python) -> Bound { - py.get_type::() - } - - fn __repr__(&self) -> PyResult { - let num_terms = self.num_terms()?; - let num_qubits = self.num_qubits()?; - - let str_num_terms = format!( - "{} term{}", - num_terms, - if num_terms == 1 { "" } else { "s" } - ); - let str_num_qubits = format!( - "{} qubit{}", - num_qubits, - if num_qubits == 1 { "" } else { "s" } - ); - - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let str_terms = if num_terms == 0 { - "0.0".to_owned() - } else { - inner - .iter() - .map(GeneratorTermView::to_sparse_str) - .collect::>() - .join(" + ") - }; - Ok(format!( - "" - )) - } -} - -#[cfg(feature = "python")] -impl From for PyPauliLindbladMap { - fn from(val: PauliLindbladMap) -> PyPauliLindbladMap { - PyPauliLindbladMap { - inner: Arc::new(RwLock::new(val)), - } - } -} - -#[cfg(feature = "python")] -impl<'py> IntoPyObject<'py> for PauliLindbladMap { - type Target = PyPauliLindbladMap; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> PyResult { - PyPauliLindbladMap::from(self).into_pyobject(py) - } -} - -/// Attempt to coerce an arbitrary Python object to a [PyPauliLindbladMap]. -/// -/// This returns: -/// -/// * `Ok(Some(obs))` if the coercion was completely successful. -/// * `Ok(None)` if the input value was just completely the wrong type and no coercion could be -/// attempted. -/// * `Err` if the input was a valid type for coercion, but the coercion failed with a Python -/// exception. -/// -/// The purpose of this is for conversion the arithmetic operations, which should return -/// [PyNotImplemented] if the type is not valid for coercion. -#[cfg(feature = "python")] -fn coerce_to_map<'py>( - value: &Bound<'py, PyAny>, -) -> PyResult>> { - let py = value.py(); - if let Ok(obs) = value.cast_exact::() { - return Ok(Some(obs.clone())); - } - match PyPauliLindbladMap::py_new(value, None) { - Ok(obs) => Ok(Some(Bound::new(py, obs)?)), - Err(e) => { - if e.is_instance_of::(py) { - Ok(None) - } else { - Err(e) - } - } - } -} diff --git a/crates/quantum_info/src/pauli_lindblad_map/phased_qubit_sparse_pauli.rs b/crates/quantum_info/src/pauli_lindblad_map/phased_qubit_sparse_pauli.rs index 71bbb3297f6b..44756a7c0e70 100644 --- a/crates/quantum_info/src/pauli_lindblad_map/phased_qubit_sparse_pauli.rs +++ b/crates/quantum_info/src/pauli_lindblad_map/phased_qubit_sparse_pauli.rs @@ -10,46 +10,18 @@ // copyright notice, and modified files need to carry a notice indicating // that they have been altered from the originals. -#[cfg(feature = "python")] -use numpy::{PyArray1, PyArrayMethods, PyReadonlyArray1}; -#[cfg(feature = "python")] -use pyo3::{ - IntoPyObjectExt, PyErr, - exceptions::{PyTypeError, PyValueError}, - intern, - prelude::*, - types::{PyInt, PyList, PyString, PyTuple, PyType}, -}; - -#[cfg(feature = "python")] -use std::{ - collections::btree_map, - sync::{Arc, RwLock}, -}; - -#[cfg(feature = "python")] -use qiskit_util::py::{PySequenceIndex, SequenceIndex}; - use super::qubit_sparse_pauli::{ ArithmeticError, CoherenceError, LabelError, Pauli, QubitSparsePauli, QubitSparsePauliList, QubitSparsePauliView, }; -#[cfg(feature = "python")] -use super::qubit_sparse_pauli::{ - InnerReadError, InnerWriteError, PyQubitSparsePauli, raw_parts_from_sparse_list, -}; - -#[cfg(feature = "python")] -use crate::imports; - /// A list of Pauli operators stored in a qubit-sparse format. /// /// See [PyQubitSparsePauliList] for detailed docs. #[derive(Clone, Debug, PartialEq)] pub struct PhasedQubitSparsePauliList { /// The paulis. - qubit_sparse_pauli_list: QubitSparsePauliList, + pub(crate) qubit_sparse_pauli_list: QubitSparsePauliList, /// Phases. phases: Vec, } @@ -177,8 +149,7 @@ impl PhasedQubitSparsePauliList { } // Check equality of operators - #[cfg(feature = "python")] // Only currently used by python remove if needed from rust - fn eq(&self, other: &PhasedQubitSparsePauliList) -> bool { + pub fn eq(&self, other: &PhasedQubitSparsePauliList) -> bool { if self.qubit_sparse_pauli_list != other.qubit_sparse_pauli_list { return false; } @@ -189,9 +160,9 @@ impl PhasedQubitSparsePauliList { return false; } } - true } + /// Apply a transpiler layout. pub fn apply_layout( &self, @@ -245,10 +216,10 @@ impl PhasedQubitSparsePauliView<'_> { #[derive(Clone, Debug, PartialEq)] pub struct PhasedQubitSparsePauli { /// The qubit sparse Pauli. - qubit_sparse_pauli: QubitSparsePauli, + pub(crate) qubit_sparse_pauli: QubitSparsePauli, /// ZX phase. Note that this is different from the "group phase" the user interacts with in the /// python interface. - phase: isize, + pub(crate) phase: isize, } impl PhasedQubitSparsePauli { @@ -399,1237 +370,8 @@ impl PhasedQubitSparsePauli { } // Check equality of operators - #[cfg(feature = "python")] // Only currently used by python remove if needed from rust - fn eq(&self, other: &PhasedQubitSparsePauli) -> bool { + pub fn eq(&self, other: &PhasedQubitSparsePauli) -> bool { ((self.phase - other.phase).rem_euclid(4) == 0) && self.qubit_sparse_pauli == other.qubit_sparse_pauli } } - -/// A Pauli operator stored in a qubit-sparse format. -/// -/// Representation -/// ============== -/// -/// A Pauli operator is a tensor product of single-qubit Pauli operators of the form :math:`P = -/// (-i)^m \bigotimes_n A^{(n)}_i`, for :math:`A^{(n)}_i \in \{I, X, Y, Z\}` and an integer -/// :math:`m` called the phase exponent. The internal representation of a -/// :class:`PhasedQubitSparsePauli` stores only the non-identity single-qubit Pauli operators. -/// -/// Internally, each single-qubit Pauli operator is stored with a numeric value. See the -/// documentation of :class:`QubitSparsePauli` for a description of the formatting of the numeric -/// value associated with each Pauli, as well as descriptions of the :attr:`paulis` and -/// :attr:`indices` attributes that store each Pauli and its associated qubit index. -/// -/// Additionally, the phase of the operator can be retrieved through the :attr:`phase` attribute, -/// which returns the group phase exponent, matching the behaviour of the same attribute in -/// :class:`.Pauli`. -/// -/// Construction -/// ============ -/// -/// :class:`PhasedQubitSparsePauli` defines several constructors. The default constructor will -/// attempt to delegate to one of the more specific constructors, based on the type of the input. -/// You can always use the specific constructors to have more control over the construction. -/// -/// .. _phased-qubit-sparse-pauli-convert-constructors: -/// .. table:: Construction from other objects -/// -/// ============================ ================================================================ -/// Method Summary -/// ============================ ================================================================ -/// :meth:`from_label` Convert a dense string label into a -/// :class:`~.PhasedQubitSparsePauli`. -/// -/// :meth:`from_sparse_label` Build a :class:`.PhasedQubitSparsePauli` from a tuple of a -/// phase, a sparse string label, and the qubits they apply to. -/// -/// :meth:`from_pauli` Raise a single :class:`~.quantum_info.Pauli` into a -/// :class:`.PhasedQubitSparsePauli`. -/// -/// :meth:`from_raw_parts` Build the list from :ref:`the raw data arrays -/// ` and the phase. -/// ============================ ================================================================ -/// -/// .. py:function:: PhasedQubitSparsePauli.__new__(data, /, num_qubits=None) -/// -/// The default constructor of :class:`QubitSparsePauli`. -/// -/// This delegates to one of :ref:`the explicit conversion-constructor methods -/// `, based on the type of the ``data`` -/// argument. If ``num_qubits`` is supplied and constructor implied by the type of ``data`` does -/// not accept a number, the given integer must match the input. -/// -/// :param data: The data type of the input. This can be another :class:`QubitSparsePauli`, -/// in which case the input is copied, or it can be a valid format for either -/// :meth:`from_label` or :meth:`from_sparse_label`. -/// :param int|None num_qubits: Optional number of qubits for the operator. For most data -/// inputs, this can be inferred and need not be passed. It is only necessary for the -/// sparse-label format. If given unnecessarily, it must match the data input. -#[cfg(feature = "python")] -#[pyclass( - name = "PhasedQubitSparsePauli", - frozen, - module = "qiskit.quantum_info", - skip_from_py_object -)] -#[derive(Clone, Debug)] -pub struct PyPhasedQubitSparsePauli { - inner: PhasedQubitSparsePauli, -} - -#[cfg(feature = "python")] -impl PyPhasedQubitSparsePauli { - pub fn inner(&self) -> &PhasedQubitSparsePauli { - &self.inner - } -} - -#[cfg(feature = "python")] -#[pymethods] -impl PyPhasedQubitSparsePauli { - #[new] - #[pyo3(signature = (data, /, num_qubits=None))] - fn py_new(data: &Bound, num_qubits: Option) -> PyResult { - let py = data.py(); - let check_num_qubits = |data: &Bound| -> PyResult<()> { - let Some(num_qubits) = num_qubits else { - return Ok(()); - }; - let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::()?; - if num_qubits == other_qubits { - return Ok(()); - } - Err(PyValueError::new_err(format!( - "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})" - ))) - }; - if data.is_instance(imports::PAULI_TYPE.get_bound(py))? { - check_num_qubits(data)?; - return Self::from_pauli(data); - } - if let Ok(label) = data.extract::() { - let num_qubits = num_qubits.unwrap_or(label.len() as u32); - if num_qubits as usize != label.len() { - return Err(PyValueError::new_err(format!( - "explicitly given 'num_qubits' ({}) does not match label ({})", - num_qubits, - label.len(), - ))); - } - return Self::from_label(&label); - } - if let Ok(sparse_label) = data.extract() { - let Some(num_qubits) = num_qubits else { - return Err(PyValueError::new_err( - "if using the sparse-label form, 'num_qubits' must be provided", - )); - }; - return Self::from_sparse_label(sparse_label, num_qubits); - } - Err(PyTypeError::new_err(format!( - "unknown input format for 'PhasedQubitSparsePauli': {}", - data.get_type().repr()?, - ))) - } - - /// Construct a :class:`.PhasedQubitSparsePauli` from raw Numpy arrays that match the - /// required data representation described in the class-level documentation. - /// - /// The data from each array is copied into fresh, growable Rust-space allocations. - /// - /// Args: - /// num_qubits: number of qubits the operator acts on. - /// paulis: list of the single-qubit terms. This should be a Numpy array with dtype - /// :attr:`~numpy.uint8` (which is compatible with :class:`.Pauli`). - /// indices: sorted list of the qubits each single-qubit term corresponds to. This should - /// be a Numpy array with dtype :attr:`~numpy.uint32`. - /// phase: The phase exponent of the operator. - /// - /// Examples: - /// - /// Construct a :math:`Z` operator acting on qubit 50 of 100 qubits. - /// - /// >>> num_qubits = 100 - /// >>> terms = np.array([PhasedQubitSparsePauli.Pauli.Z], dtype=np.uint8) - /// >>> indices = np.array([50], dtype=np.uint32) - /// >>> phase = 0 - /// >>> PhasedQubitSparsePauli.from_raw_parts(num_qubits, terms, indices, phase) - /// - #[staticmethod] - #[pyo3(signature = (/, num_qubits, paulis, indices, phase=0))] - fn from_raw_parts( - num_qubits: u32, - paulis: Vec, - indices: Vec, - phase: isize, - ) -> PyResult { - if paulis.len() != indices.len() { - return Err(CoherenceError::MismatchedItemCount { - paulis: paulis.len(), - indices: indices.len(), - } - .into()); - } - let mut order = (0..paulis.len()).collect::>(); - order.sort_unstable_by_key(|a| indices[*a]); - let paulis = order.iter().map(|i| paulis[*i]).collect(); - let mut sorted_indices = Vec::::with_capacity(order.len()); - for i in order { - let index = indices[i]; - if sorted_indices - .last() - .map(|prev| *prev >= index) - .unwrap_or(false) - { - return Err(CoherenceError::UnsortedIndices.into()); - } - sorted_indices.push(index) - } - let qubit_sparse_pauli = - QubitSparsePauli::new(num_qubits, paulis, sorted_indices.into_boxed_slice())?; - let num_ys = qubit_sparse_pauli.view().num_ys(); - let inner = PhasedQubitSparsePauli::new(qubit_sparse_pauli, (phase + num_ys).rem_euclid(4)); - Ok(PyPhasedQubitSparsePauli { inner }) - } - - /// Construct a :class:`.PhasedQubitSparsePauli` from a single :class:`~.quantum_info.Pauli` - /// instance. - /// - /// - /// Args: - /// pauli (:class:`~.quantum_info.Pauli`): the single Pauli to convert. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> label = "iIYXZI" - /// >>> pauli = Pauli(label) - /// >>> PhasedQubitSparsePauli.from_pauli(pauli) - /// - /// >>> assert PhasedQubitSparsePauli.from_label(label) == PhasedQubitSparsePauli.from_pauli(pauli) - #[staticmethod] - #[pyo3(signature = (pauli, /))] - fn from_pauli(pauli: &Bound) -> PyResult { - let py = pauli.py(); - let num_qubits = pauli.getattr(intern!(py, "num_qubits"))?.extract::()?; - let z = pauli - .getattr(intern!(py, "z"))? - .extract::>()?; - let x = pauli - .getattr(intern!(py, "x"))? - .extract::>()?; - let mut paulis = Vec::new(); - let mut indices = Vec::new(); - for (i, (x, z)) in x.as_array().iter().zip(z.as_array().iter()).enumerate() { - // The only failure case possible here is the identity, because of how we're - // constructing the value to convert. - let Ok(term) = ::bytemuck::checked::try_cast(((*x as u8) << 1) | (*z as u8)) else { - continue; - }; - indices.push(i as u32); - paulis.push(term); - } - let group_phase = pauli - // `Pauli`'s `_phase` is a Numpy array ... - .getattr(intern!(py, "_phase"))? - // ... that should have exactly 1 element ... - .call_method0(intern!(py, "item"))? - // ... which is some integral type. - .extract::()?; - - let inner = PhasedQubitSparsePauli::new( - QubitSparsePauli::new( - num_qubits, - paulis.into_boxed_slice(), - indices.into_boxed_slice(), - )?, - group_phase, - ); - Ok(inner.into()) - } - - /// Construct a phased qubit sparse Pauli from a sparse label, given as a tuple of an int for - /// the phase exponent, a string of Paulis, and the indices of the corresponding qubits. - /// - /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`. - /// - /// Args: - /// sparse_label (tuple[int, str, Sequence[int]]): labels and the qubits each single-qubit - /// term applies to. - /// - /// num_qubits (int): the number of qubits the operator acts on. - /// - /// Examples: - /// - /// Construct a simple Pauli:: - /// - /// >>> PhasedQubitSparsePauli.from_sparse_label( - /// ... (0, "ZX", (1, 4)), - /// ... num_qubits=5, - /// ... ) - /// - /// - /// This method can replicate the behavior of :meth:`from_label`, if the qubit-arguments - /// field of the tuple is set to decreasing integers:: - /// - /// >>> label = "XYXZ" - /// >>> from_label = PhasedQubitSparsePauli.from_label(label) - /// >>> from_sparse_label = PhasedQubitSparsePauli.from_sparse_label( - /// ... (0, label, (3, 2, 1, 0)), - /// ... num_qubits=4 - /// ... ) - /// >>> assert from_label == from_sparse_label - #[staticmethod] - #[pyo3(signature = (/, sparse_label, num_qubits))] - fn from_sparse_label( - sparse_label: (isize, String, Vec), - num_qubits: u32, - ) -> PyResult { - let phase = sparse_label.0; - let label = sparse_label.1; - let indices = sparse_label.2; - let mut paulis = Vec::new(); - let mut sorted_indices = Vec::new(); - - let label: &[u8] = label.as_ref(); - let mut sorted = btree_map::BTreeMap::new(); - if label.len() != indices.len() { - return Err(LabelError::WrongLengthIndices { - label: label.len(), - indices: indices.len(), - } - .into()); - } - for (letter, index) in label.iter().zip(indices) { - if index >= num_qubits { - return Err(LabelError::BadIndex { index, num_qubits }.into()); - } - let btree_map::Entry::Vacant(entry) = sorted.entry(index) else { - return Err(LabelError::DuplicateIndex { index }.into()); - }; - entry.insert(Pauli::try_from_u8(*letter).map_err(|_| LabelError::OutsideAlphabet)?); - } - for (index, term) in sorted.iter() { - let Some(term) = term else { - continue; - }; - sorted_indices.push(*index); - paulis.push(*term); - } - - let num_ys = isize::try_from(paulis.iter().filter(|&p| *p == Pauli::Y).count())?; - - let qubit_sparse_pauli = QubitSparsePauli::new( - num_qubits, - paulis.into_boxed_slice(), - sorted_indices.into_boxed_slice(), - )?; - let inner = PhasedQubitSparsePauli::new(qubit_sparse_pauli, phase + num_ys); - Ok(inner.into()) - } - - /// Construct from a dense string label. - /// - /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted - /// analogously to a bitstring. In other words, the right-most letter is associated with qubit - /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and - /// :class:`.SparsePauliOp`. - /// - /// Args: - /// label (str): the dense label. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> QubitSparsePauli.from_label("IIIIXZI") - /// - /// >>> label = "IYXZI" - /// >>> pauli = Pauli(label) - /// >>> assert QubitSparsePauli.from_label(label) == QubitSparsePauli.from_pauli(pauli) - #[staticmethod] - #[pyo3(signature = (label, /))] - fn from_label(label: &str) -> PyResult { - let qubit_sparse_pauli: QubitSparsePauli = QubitSparsePauli::from_dense_label(label)?; - let num_ys = qubit_sparse_pauli.view().num_ys(); - let inner = PhasedQubitSparsePauli { - qubit_sparse_pauli, - phase: num_ys.rem_euclid(4), - }; - Ok(inner.into()) - } - - /// Get the identity operator for a given number of qubits. - /// - /// Examples: - /// - /// Get the identity on 100 qubits:: - /// - /// >>> QubitSparsePauli.identity(100) - /// - #[pyo3(signature = (/, num_qubits))] - #[staticmethod] - pub fn identity(num_qubits: u32) -> Self { - PhasedQubitSparsePauli::identity(num_qubits).into() - } - - /// Read-only view onto the phase. - #[getter] - fn get_phase(slf_: Bound) -> isize { - let borrowed = slf_.borrow(); - let phase = borrowed.inner.phase; - - let num_ys = borrowed.inner.qubit_sparse_pauli.view().num_ys(); - (phase - num_ys).rem_euclid(4) - } - - /// Convert this Pauli into a single element :class:`PhasedQubitSparsePauliList`. - fn to_phased_qubit_sparse_pauli_list(&self) -> PyResult { - Ok(self.inner.to_phased_qubit_sparse_pauli_list().into()) - } - - /// Composition with another :class:`PhasedQubitSparsePauli`. - /// - /// Args: - /// other (PhasedQubitSparsePauli): the qubit sparse Pauli to compose with. - fn compose(&self, other: &PyPhasedQubitSparsePauli) -> PyResult { - Ok(PyPhasedQubitSparsePauli { - inner: self.inner.compose(&other.inner)?, - }) - } - - fn __matmul__(&self, other: &PyPhasedQubitSparsePauli) -> PyResult { - self.compose(other) - } - - /// Check if `self`` commutes with another phased qubit sparse pauli. - /// - /// Args: - /// other (PhasedQubitSparsePauli): the phased qubit sparse Pauli to check for commutation - /// with. - fn commutes(&self, other: &PyPhasedQubitSparsePauli) -> PyResult { - Ok(self.inner.commutes(&other.inner)?) - } - - fn __eq__(slf: Bound, other: Bound) -> PyResult { - if slf.is(&other) { - return Ok(true); - } - let Ok(other) = other.cast_into::() else { - return Ok(false); - }; - let slf = slf.borrow(); - let other = other.borrow(); - Ok(slf.inner.eq(&other.inner)) - } - - fn __repr__(&self) -> PyResult { - Ok(format!( - "<{} on {} qubit{}: {}>", - "PhasedQubitSparsePauli", - self.inner.num_qubits(), - if self.inner.num_qubits() == 1 { - "" - } else { - "s" - }, - self.inner.view().to_sparse_str(), - )) - } - - fn __getnewargs__(slf_: Bound) -> PyResult> { - let py = slf_.py(); - let borrowed = slf_.borrow(); - ( - borrowed.inner.num_qubits(), - Self::get_paulis(slf_.clone()), - Self::get_indices(slf_.clone()), - Self::get_phase(slf_), - ) - .into_pyobject(py) - } - - fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult> { - let paulis: &[u8] = ::bytemuck::cast_slice(self.inner.qubit_sparse_pauli.paulis()); - ( - py.get_type::().getattr("from_raw_parts")?, - ( - self.inner.num_qubits(), - PyArray1::from_slice(py, paulis), - PyArray1::from_slice(py, self.inner.qubit_sparse_pauli.indices()), - (self.inner.phase - self.inner.qubit_sparse_pauli.view().num_ys()).rem_euclid(4), - ), - ) - .into_pyobject(py) - } - - /// Return a :class:`~.quantum_info.Pauli` representing the same Pauli. - fn to_pauli<'py>(&self, py: Python<'py>) -> PyResult> { - let pauli = imports::PAULI_TYPE - .get_bound(py) - .call1((self.inner.qubit_sparse_pauli.to_dense_label(),))?; - pauli.setattr( - "phase", - self.inner.phase - self.inner.qubit_sparse_pauli.view().num_ys(), - )?; - Ok(pauli) - } - - /// Get a copy of this term. - fn copy(&self) -> Self { - self.clone() - } - - /// Read-only view onto the individual single-qubit terms. - /// - /// The only valid values in the array are those with a corresponding - /// :class:`~PhasedQubitSparsePauli.Pauli`. - #[getter] - fn get_paulis(slf_: Bound) -> Bound> { - let borrowed = slf_.borrow(); - let paulis = borrowed.inner.qubit_sparse_pauli.paulis(); - let arr = ::ndarray::aview1(::bytemuck::cast_slice::<_, u8>(paulis)); - // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. - // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the - // `Box<[Pauli]>` allocation (including dropping or reallocating it) other than the entire - // object getting dropped, which Python will keep safe. - let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; - out.readwrite().make_nonwriteable(); - out - } - - /// The number of qubits the term is defined on. - #[getter] - fn get_num_qubits(&self) -> u32 { - self.inner.num_qubits() - } - - /// Read-only view onto the indices of each non-identity single-qubit term. - /// - /// The indices will always be in sorted order. - #[getter] - fn get_indices(slf_: Bound) -> Bound> { - let borrowed = slf_.borrow(); - let indices = borrowed.inner.qubit_sparse_pauli.indices(); - let arr = ::ndarray::aview1(indices); - // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. - // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the - // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire - // object getting dropped, which Python will keep safe. - let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; - out.readwrite().make_nonwriteable(); - out - } - - // The documentation for this is inlined into the class-level documentation of - // :class:`PhasedQubitSparsePauliList`. - #[allow(non_snake_case)] - #[classattr] - fn Pauli(py: Python) -> PyResult> { - PyQubitSparsePauli::Pauli(py) - } -} - -/// A list of Pauli operators with phases stored in a qubit-sparse format. -/// -/// Representation -/// ============== -/// -/// Each individual Pauli operator in the list is a tensor product of single-qubit Pauli operators -/// of the form :math:`P = (-i)^n\bigotimes_n A^{(n)}_i`, for :math:`A^{(n)}_i \in \{I, X, Y, Z\}`, -/// and an integer :math:`n` called the phase exponent. The -/// internal representation of a :class:`PhasedQubitSparsePauliList` stores only the non-identity -/// single-qubit Pauli operators. -/// -/// Indexing -/// -------- -/// -/// :class:`PhasedQubitSparsePauliList` behaves as `a Python sequence -/// `__ (the standard form, not the expanded -/// :class:`collections.abc.Sequence`). The elements of the list can be indexed by integers, as -/// well as iterated through. Whether through indexing or iterating, elements of the list are -/// returned as :class:`PhasedQubitSparsePauli` instances. -/// -/// Construction -/// ============ -/// -/// :class:`PhasedQubitSparsePauliList` defines several constructors. The default constructor will -/// attempt to delegate to one of the more specific constructors, based on the type of the input. -/// You can always use the specific constructors to have more control over the construction. -/// -/// .. _phased-qubit-sparse-pauli-list-convert-constructors: -/// .. table:: Construction from other objects -/// -/// ======================================= ===================================================== -/// Method Summary -/// ======================================= ===================================================== -/// :meth:`from_label` Convert a dense string label into a single-element -/// :class:`.PhasedQubitSparsePauliList`. -/// -/// :meth:`from_list` Construct from a list of dense string labels. -/// -/// :meth:`from_sparse_list` Elements given as a list of tuples of the phase -/// exponent, sparse string labels, and the qubits they -/// apply to. -/// -/// :meth:`from_pauli` Raise a single :class:`~.quantum_info.Pauli` into a -/// single-element :class:`.PhasedQubitSparsePauliList`. -/// -/// :meth:`from_phased_qubit_sparse_paulis` Construct from a list of -/// :class:`.PhasedQubitSparsePauli`\s. -/// ======================================= ===================================================== -/// -/// .. py:function:: PhasedQubitSparsePauliList.__new__(data, /, num_qubits=None) -/// -/// The default constructor of :class:`PhasedQubitSparsePauliList`. -/// -/// This delegates to one of :ref:`the explicit conversion-constructor methods -/// `, based on the type of the ``data`` -/// argument. If ``num_qubits`` is supplied and constructor implied by the type of ``data`` does -/// not accept a number, the given integer must match the input. -/// -/// :param data: The data type of the input. This can be another -/// :class:`PhasedQubitSparsePauliList`, in which case the input is copied, or it can be a -/// list in a valid format for either :meth:`from_list` or :meth:`from_sparse_list`. -/// :param int|None num_qubits: Optional number of qubits for the list. For most data -/// inputs, this can be inferred and need not be passed. It is only necessary for empty -/// lists or the sparse-list format. If given unnecessarily, it must match the data input. -/// -/// In addition to the conversion-based constructors, the method :meth:`empty` can be used to -/// construct an empty list of phased qubit-sparse Paulis acting on a given number of qubits. -/// -/// Conversions -/// =========== -/// -/// An existing :class:`PhasedQubitSparsePauliList` can be converted into other formats. -/// -/// .. table:: Conversion methods to other observable forms. -/// -/// =========================== ================================================================= -/// Method Summary -/// =========================== ================================================================= -/// :meth:`to_sparse_list` Express the observable in a sparse list format with elements -/// ``(phase, paulis, indices)``. -/// =========================== ================================================================= -#[cfg(feature = "python")] -#[pyclass( - name = "PhasedQubitSparsePauliList", - module = "qiskit.quantum_info", - sequence -)] -#[derive(Debug)] -pub struct PyPhasedQubitSparsePauliList { - // This class keeps a pointer to a pure Rust-SparseTerm and serves as interface from Python. - pub inner: Arc>, -} - -#[cfg(feature = "python")] -#[pymethods] -impl PyPhasedQubitSparsePauliList { - #[pyo3(signature = (data, /, num_qubits=None))] - #[new] - fn py_new(data: &Bound, num_qubits: Option) -> PyResult { - let py = data.py(); - let check_num_qubits = |data: &Bound| -> PyResult<()> { - let Some(num_qubits) = num_qubits else { - return Ok(()); - }; - let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::()?; - if num_qubits == other_qubits { - return Ok(()); - } - Err(PyValueError::new_err(format!( - "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})" - ))) - }; - if data.is_instance(imports::PAULI_TYPE.get_bound(py))? { - check_num_qubits(data)?; - return Self::from_pauli(data); - } - if let Ok(label) = data.extract::() { - let num_qubits = num_qubits.unwrap_or(label.len() as u32); - if num_qubits as usize != label.len() { - return Err(PyValueError::new_err(format!( - "explicitly given 'num_qubits' ({}) does not match label ({})", - num_qubits, - label.len(), - ))); - } - return Self::from_label(&label); - } - if let Ok(pauli_list) = data.cast_exact::() { - check_num_qubits(data)?; - let borrowed = pauli_list.borrow(); - let inner = borrowed.inner.read().map_err(|_| InnerReadError)?; - return Ok(inner.clone().into()); - } - // The type of `vec` is inferred from the subsequent calls to `Self::from_list` or - // `Self::from_sparse_list` to be either the two-tuple or the three-tuple form during the - // `extract`. The empty list will pass either, but it means the same to both functions. - if let Ok(vec) = data.extract() { - return Self::from_list(vec, num_qubits); - } - if let Ok(vec) = data.extract() { - let Some(num_qubits) = num_qubits else { - return Err(PyValueError::new_err( - "if using the sparse-list form, 'num_qubits' must be provided", - )); - }; - return Self::from_sparse_list(vec, num_qubits); - } - if let Ok(term) = data.cast_exact::() { - return term.borrow().to_phased_qubit_sparse_pauli_list(); - }; - if let Ok(pauli_list) = Self::from_phased_qubit_sparse_paulis(data, num_qubits) { - return Ok(pauli_list); - } - Err(PyTypeError::new_err(format!( - "unknown input format for 'PhasedQubitSparsePauliList': {}", - data.get_type().repr()?, - ))) - } - - /// Get a copy of this qubit sparse Pauli list. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> phased_qubit_sparse_pauli_list = PhasedQubitSparsePauliList.from_list(["IXZXYYZZ", "ZXIXYYZZ"]) - /// >>> assert phased_qubit_sparse_pauli_list == phased_qubit_sparse_pauli_list.copy() - /// >>> assert phased_qubit_sparse_pauli_list is not phased_qubit_sparse_pauli_list.copy() - fn copy(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.clone().into()) - } - - /// The number of qubits the operators in the list act on. - /// - /// This is not inferable from any other shape or values, since identities are not stored - /// explicitly. - #[getter] - #[inline] - pub fn num_qubits(&self) -> PyResult { - 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 { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.num_terms()) - } - - /// Get the empty list for a given number of qubits. - /// - /// The empty list contains no elements, and is the identity element for joining two - /// :class:`PhasedQubitSparsePauliList` instances. - /// - /// Examples: - /// - /// Get the empty list on 100 qubits:: - /// - /// >>> PhasedQubitSparsePauliList.empty(100) - /// - #[pyo3(signature = (/, num_qubits))] - #[staticmethod] - pub fn empty(num_qubits: u32) -> Self { - PhasedQubitSparsePauliList::empty(num_qubits).into() - } - - /// Construct a :class:`.PhasedQubitSparsePauliList` from a single :class:`~.quantum_info.Pauli` - /// instance. - /// - /// The output list will have a single term. Note that the phase is dropped. - /// - /// Args: - /// pauli (:class:`~.quantum_info.Pauli`): the single Pauli to convert. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> label = "IYXZI" - /// >>> pauli = Pauli(label) - /// >>> PhasedQubitSparsePauliList.from_pauli(pauli) - /// - /// >>> assert PhasedQubitSparsePauliList.from_label(label) == PhasedQubitSparsePauliList.from_pauli(pauli) - #[staticmethod] - #[pyo3(signature = (pauli, /))] - fn from_pauli(pauli: &Bound) -> PyResult { - let x = PyPhasedQubitSparsePauli::from_pauli(pauli)?; - x.to_phased_qubit_sparse_pauli_list() - } - - /// Construct a list with a single-term from a dense string label. - /// - /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted - /// analogously to a bitstring. In other words, the right-most letter is associated with qubit - /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and - /// :class:`.SparsePauliOp`. - /// - /// Args: - /// label (str): the dense label. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> PhasedQubitSparsePauliList.from_label("IIIIXZI") - /// - /// >>> label = "IYXZI" - /// >>> pauli = Pauli(label) - /// >>> assert PhasedQubitSparsePauliList.from_label(label) == PhasedQubitSparsePauliList.from_pauli(pauli) - /// - /// See also: - /// :meth:`from_list` - /// A generalization of this method that constructs a list from multiple labels. - #[staticmethod] - #[pyo3(signature = (label, /))] - fn from_label(label: &str) -> PyResult { - let singleton = PyPhasedQubitSparsePauli::from_label(label)?; - singleton.to_phased_qubit_sparse_pauli_list() - } - - /// Construct a phased qubit-sparse Pauli list from a list of dense labels. - /// - /// This is analogous to :meth:`.SparsePauliOp.from_list`. In this dense form, you must supply - /// all identities explicitly in each label. - /// - /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted - /// analogously to a bitstring. In other words, the right-most letter is associated with qubit - /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and - /// :class:`.SparsePauliOp`. - /// - /// Args: - /// iter (list[str]): List of dense string labels. - /// num_qubits (int | None): It is not necessary to specify this if you are sure that - /// ``iter`` is not an empty sequence, since it can be inferred from the label lengths. - /// If ``iter`` may be empty, you must specify this argument to disambiguate how many - /// qubits the operators act on. If this is given and ``iter`` is not empty, the value - /// must match the label lengths. - /// - /// Examples: - /// - /// Construct a qubit sparse Pauli list from a list of labels:: - /// - /// >>> PhasedQubitSparsePauliList.from_list([ - /// ... "IIIXX", - /// ... "IIYYI", - /// ... "IXXII", - /// ... "ZZIII", - /// ... ]) - /// - /// - /// Use ``num_qubits`` to disambiguate potentially empty inputs:: - /// - /// >>> PhasedQubitSparsePauliList.from_list([], num_qubits=10) - /// - /// - /// This method is equivalent to calls to :meth:`from_sparse_list` with the explicit - /// qubit-arguments field set to decreasing integers:: - /// - /// >>> labels = ["XYXZ", "YYZZ", "XYXZ"] - /// >>> from_list = PhasedQubitSparsePauliList.from_list(labels) - /// >>> from_sparse_list = PhasedQubitSparsePauliList.from_sparse_list([ - /// ... (label, (3, 2, 1, 0)) - /// ... for label in labels - /// ... ]) - /// >>> assert from_list == from_sparse_list - /// - /// See also: - /// :meth:`from_sparse_list` - /// Construct the list from labels without explicit identities, but with the qubits each - /// single-qubit operator term applies to listed explicitly. - #[staticmethod] - #[pyo3(signature = (iter, /, *, num_qubits=None))] - fn from_list(iter: Vec, num_qubits: Option) -> PyResult { - if iter.is_empty() && num_qubits.is_none() { - return Err(PyValueError::new_err( - "cannot construct a PhasedQubitSparsePauliList from an empty list without knowing `num_qubits`", - )); - } - let num_qubits = match num_qubits { - Some(num_qubits) => num_qubits, - None => iter[0].len() as u32, - }; - let mut inner = PhasedQubitSparsePauliList::with_capacity(num_qubits, iter.len(), 0); - for label in iter { - inner.add_dense_label(&label)?; - } - Ok(inner.into()) - } - - /// Construct a :class:`PhasedQubitSparsePauliList` out of individual - /// :class:`PhasedQubitSparsePauli` instances. - /// - /// All the terms must have the same number of qubits. If supplied, the ``num_qubits`` argument - /// must match the terms. - /// - /// Args: - /// obj (Iterable[PhasedQubitSparsePauli]): Iterable of individual terms to build the list from. - /// num_qubits (int | None): The number of qubits the elements of the list should act on. - /// This is usually inferred from the input, but can be explicitly given to handle the - /// case of an empty iterable. - /// - /// Returns: - /// The corresponding list. - #[staticmethod] - #[pyo3(signature = (obj, /, num_qubits=None))] - fn from_phased_qubit_sparse_paulis( - obj: &Bound, - num_qubits: Option, - ) -> PyResult { - let mut iter = obj.try_iter()?; - let mut inner = match num_qubits { - Some(num_qubits) => PhasedQubitSparsePauliList::empty(num_qubits), - None => { - let Some(first) = iter.next() else { - return Err(PyValueError::new_err( - "cannot construct an empty PhasedQubitSparsePauliList without knowing `num_qubits`", - )); - }; - let py_term = first?.cast::()?.borrow(); - py_term.inner.to_phased_qubit_sparse_pauli_list() - } - }; - for bound_py_term in iter { - let py_term = bound_py_term?.cast::()?.borrow(); - inner.add_phased_qubit_sparse_pauli(py_term.inner.view())?; - } - Ok(inner.into()) - } - - /// Clear all the elements from the list, making it equal to the empty list again. - /// - /// This does not change the capacity of the internal allocations, so subsequent addition or - /// subtraction operations resulting from composition may not need to reallocate. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> pauli_list = PhasedQubitSparsePauliList.from_list(["IXXXYY", "ZZYZII"]) - /// >>> pauli_list.clear() - /// >>> assert pauli_list == PhasedQubitSparsePauliList.empty(pauli_list.num_qubits) - pub fn clear(&mut self) -> PyResult<()> { - let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; - inner.clear(); - Ok(()) - } - - /// Construct a phased qubit sparse Pauli list from a list of labels and the qubits each item - /// applies to. - /// - /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`. - /// - /// The "labels" and "indices" fields of the tuples are associated by zipping them together. - /// For example, this means that a call to :meth:`from_list` can be converted to the form used - /// by this method by setting the "indices" field of each triple to ``(num_qubits-1, ..., 1, - /// 0)``. - /// - /// Args: - /// iter (list[tuple[int, str, Sequence[int]]]): tuples of phase exponents, labels, and the - /// qubits each single-qubit term applies to. - /// - /// num_qubits (int): the number of qubits the operators in the list act on. - /// - /// Examples: - /// - /// Construct a simple list:: - /// - /// >>> PhasedQubitSparsePauliList.from_sparse_list( - /// ... [(0, "ZX", (1, 4)), (1, "YY", (0, 3))], - /// ... num_qubits=5, - /// ... ) - /// - /// - /// This method can replicate the behavior of :meth:`from_list`, if the qubit-arguments - /// field of the tuple is set to decreasing integers:: - /// - /// >>> labels = ["XYXZ", "YYZZ", "XYXZ"] - /// >>> from_list = PhasedQubitSparsePauliList.from_list(labels) - /// >>> from_sparse_list = PhasedQubitSparsePauliList.from_sparse_list([ - /// ... (label, (3, 2, 1, 0)) - /// ... for label in labels - /// ... ]) - /// >>> assert from_list == from_sparse_list - /// - /// See also: - /// :meth:`to_sparse_list` - /// The reverse of this method. - #[staticmethod] - #[pyo3(signature = (iter, /, num_qubits))] - fn from_sparse_list(iter: Vec<(isize, String, Vec)>, num_qubits: u32) -> PyResult { - // separate group phases and build QubitSparsePauliList - let mut group_phases = Vec::with_capacity(iter.len()); - let mut sub_iter = Vec::with_capacity(iter.len()); - for (phase, label, indices) in iter { - group_phases.push(phase); - sub_iter.push((label, indices)); - } - - let (paulis, indices, boundaries) = raw_parts_from_sparse_list(sub_iter, num_qubits)?; - let qubit_sparse_pauli_list = - QubitSparsePauliList::new(num_qubits, paulis, indices, boundaries)?; - - // Build zx phases - let mut phases = Vec::with_capacity(qubit_sparse_pauli_list.num_terms()); - for (group_phase, qubit_sparse_pauli) in - group_phases.iter().zip(qubit_sparse_pauli_list.iter()) - { - phases.push((group_phase + qubit_sparse_pauli.num_ys()).rem_euclid(4)) - } - - let inner = PhasedQubitSparsePauliList::new(qubit_sparse_pauli_list, phases)?; - Ok(inner.into()) - } - - /// Express the list in terms of a sparse list format. - /// - /// This can be seen as counter-operation of - /// :meth:`.PhasedQubitSparsePauliList.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. - /// - /// Examples: - /// - /// >>> phased_qubit_sparse_list = PhasedQubitSparsePauliList.from_list(["IIXIZ", "IIZIX"]) - /// >>> reconstructed = PhasedQubitSparsePauliList.from_sparse_list(phased_qubit_sparse_list.to_sparse_list(), qubit_sparse_list.num_qubits) - /// - /// See also: - /// :meth:`from_sparse_list` - /// The constructor that can interpret these lists. - #[pyo3(signature = ())] - fn to_sparse_list(&self, py: Python) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - - // turn a SparseView into a Python tuple of (phase, paulis, indices) - let to_py_tuple = |view: PhasedQubitSparsePauliView| { - let mut pauli_string = String::with_capacity(view.qubit_sparse_pauli_view.paulis.len()); - - for bit in view.qubit_sparse_pauli_view.paulis.iter() { - pauli_string.push_str(bit.py_label()); - } - let py_int = PyInt::new( - py, - (view.phase - view.qubit_sparse_pauli_view.num_ys()).rem_euclid(4), - ) - .unbind(); - let py_string = PyString::new(py, &pauli_string).unbind(); - let py_indices = PyList::new(py, view.qubit_sparse_pauli_view.indices.iter())?.unbind(); - - PyTuple::new( - py, - vec![py_int.as_any(), py_string.as_any(), py_indices.as_any()], - ) - }; - - let out = PyList::empty(py); - for view in inner.iter() { - out.append(to_py_tuple(view)?)?; - } - Ok(out.unbind()) - } - - /// Return a :class:`~.quantum_info.PauliList` representing the same list of Paulis. - fn to_pauli_list<'py>(&self, py: Python<'py>) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let pauli_list = imports::PAULI_LIST_TYPE - .get_bound(py) - .call1((inner.qubit_sparse_pauli_list.to_dense_label_list(),))?; - - let mut phases = Vec::with_capacity(inner.num_terms()); - for view in inner.iter() { - let ys = view.qubit_sparse_pauli_view.num_ys(); - phases.push(*view.phase - ys); - } - pauli_list.setattr("phase", phases)?; - Ok(pauli_list) - } - - /// Apply a transpiler layout to this phased qubit sparse Pauli list. - /// - /// This enables remapping of qubit indices, e.g. if the list is defined in terms of virtual - /// qubit labels. - /// - /// Args: - /// layout (TranspileLayout | list[int] | None): The layout to apply. Most uses of this - /// function should pass the :attr:`.QuantumCircuit.layout` field from a circuit that - /// was transpiled for hardware. In addition, you can pass a list of new qubit indices. - /// If given as explicitly ``None``, no remapping is applied (but you can still use - /// ``num_qubits`` to expand the qubits in the list). - /// num_qubits (int | None): The number of qubits to expand the list elements to. If not - /// supplied, the output will be as wide as the given :class:`.TranspileLayout`, or the - /// same width as the input if the ``layout`` is given in another form. - /// - /// Returns: - /// A new :class:`QubitSparsePauli` with the provided layout applied. - #[pyo3(signature = (/, layout, num_qubits=None))] - fn apply_layout(&self, layout: Bound, num_qubits: Option) -> PyResult { - let py = layout.py(); - let inner = self.inner.read().map_err(|_| InnerReadError)?; - - // A utility to check the number of qubits is compatible with the map. - let check_inferred_qubits = |inferred: u32| -> PyResult { - if inferred < inner.num_qubits() { - return Err(CoherenceError::NotEnoughQubits { - current: inner.num_qubits() as usize, - target: inferred as usize, - } - .into()); - } - Ok(inferred) - }; - - // Normalize the number of qubits in the layout and the layout itself, depending on the - // input types, before calling PhasedQubitSparsePauliList.apply_layout to do the actual work. - let (num_qubits, layout): (u32, Option>) = if layout.is_none() { - (num_qubits.unwrap_or(inner.num_qubits()), None) - } else if layout.is_instance( - &py.import(intern!(py, "qiskit.transpiler"))? - .getattr(intern!(py, "TranspileLayout"))?, - )? { - ( - check_inferred_qubits( - layout.getattr(intern!(py, "_output_qubit_list"))?.len()? as u32 - )?, - Some( - layout - .call_method0(intern!(py, "final_index_layout"))? - .extract::>()?, - ), - ) - } else { - ( - check_inferred_qubits(num_qubits.unwrap_or(inner.num_qubits()))?, - Some(layout.extract()?), - ) - }; - - let out = inner.apply_layout(layout.as_deref(), num_qubits)?; - Ok(out.into()) - } - - fn __len__(&self) -> PyResult { - self.num_terms() - } - - fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - ( - py.get_type::().getattr("from_sparse_list")?, - (self.to_sparse_list(py)?, inner.num_qubits()), - ) - .into_pyobject(py) - } - - fn __getitem__<'py>( - &self, - py: Python<'py>, - index: PySequenceIndex<'py>, - ) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let indices = match index.with_len(inner.num_terms())? { - SequenceIndex::Int(index) => { - return PyPhasedQubitSparsePauli { - inner: inner.term(index).to_term(), - } - .into_bound_py_any(py); - } - indices => indices, - }; - let mut out = PhasedQubitSparsePauliList::empty(inner.num_qubits()); - for index in indices.iter() { - out.add_phased_qubit_sparse_pauli(inner.term(index))?; - } - out.into_bound_py_any(py) - } - - fn __eq__(slf: Bound, other: Bound) -> PyResult { - // this is also important to check before trying to read both slf and other - if slf.is(&other) { - return Ok(true); - } - let Ok(other) = other.cast_into::() else { - return Ok(false); - }; - let slf_borrowed = slf.borrow(); - let other_borrowed = other.borrow(); - let slf_inner = slf_borrowed.inner.read().map_err(|_| InnerReadError)?; - let other_inner = other_borrowed.inner.read().map_err(|_| InnerReadError)?; - Ok(slf_inner.eq(&other_inner)) - } - - fn __repr__(&self) -> PyResult { - let num_terms = self.num_terms()?; - let num_qubits = self.num_qubits()?; - - let str_num_terms = format!( - "{} element{}", - num_terms, - if num_terms == 1 { "" } else { "s" } - ); - let str_num_qubits = format!( - "{} qubit{}", - num_qubits, - if num_qubits == 1 { "" } else { "s" } - ); - - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let str_terms = if num_terms == 0 { - "".to_owned() - } else { - inner - .iter() - .map(PhasedQubitSparsePauliView::to_sparse_str) - .collect::>() - .join(", ") - }; - Ok(format!( - "" - )) - } -} - -#[cfg(feature = "python")] -impl From for PyPhasedQubitSparsePauli { - fn from(val: PhasedQubitSparsePauli) -> PyPhasedQubitSparsePauli { - PyPhasedQubitSparsePauli { inner: val } - } -} - -#[cfg(feature = "python")] -impl<'py> IntoPyObject<'py> for PhasedQubitSparsePauli { - type Target = PyPhasedQubitSparsePauli; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> PyResult { - PyPhasedQubitSparsePauli::from(self).into_pyobject(py) - } -} - -#[cfg(feature = "python")] -impl From for PyPhasedQubitSparsePauliList { - fn from(val: PhasedQubitSparsePauliList) -> PyPhasedQubitSparsePauliList { - PyPhasedQubitSparsePauliList { - inner: Arc::new(RwLock::new(val)), - } - } -} - -#[cfg(feature = "python")] -impl<'py> IntoPyObject<'py> for PhasedQubitSparsePauliList { - type Target = PyPhasedQubitSparsePauliList; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> PyResult { - PyPhasedQubitSparsePauliList::from(self).into_pyobject(py) - } -} diff --git a/crates/quantum_info/src/pauli_lindblad_map/qubit_sparse_pauli.rs b/crates/quantum_info/src/pauli_lindblad_map/qubit_sparse_pauli.rs index 60613dcccad0..6a072d891e45 100644 --- a/crates/quantum_info/src/pauli_lindblad_map/qubit_sparse_pauli.rs +++ b/crates/quantum_info/src/pauli_lindblad_map/qubit_sparse_pauli.rs @@ -13,37 +13,10 @@ use hashbrown::HashSet; use ndarray::Array2; -#[cfg(feature = "python")] -use numpy::{IntoPyArray, PyArray1, PyArray2, PyArrayMethods, PyReadonlyArray1}; -#[cfg(feature = "python")] -use pyo3::{ - IntoPyObjectExt, PyErr, - exceptions::{PyRuntimeError, PyTypeError, PyValueError}, - intern, - prelude::*, - sync::PyOnceLock, - types::{IntoPyDict, PyList, PyString, PyTuple, PyType}, -}; use std::collections::btree_map; -#[cfg(feature = "python")] -use std::{ - iter::zip, - sync::{Arc, RwLock}, -}; use thiserror::Error; -#[cfg(feature = "python")] -use qiskit_util::py::{PySequenceIndex, SequenceIndex}; - -#[cfg(feature = "python")] -use crate::imports; - -#[cfg(feature = "python")] -static PAULI_PY_ENUM: PyOnceLock> = PyOnceLock::new(); -#[cfg(feature = "python")] -static PAULI_INTO_PY: PyOnceLock<[Option>; 16]> = PyOnceLock::new(); - /// Named handle to the alphabet of single-qubit terms. /// /// This is just the Rust-space representation. We make a separate Python-space `enum.IntEnum` to @@ -913,1442 +886,3 @@ impl ::std::fmt::Display for InnerWriteError { write!(f, "Failed acquiring lock for writing.") } } - -#[cfg(feature = "python")] -impl From for PyErr { - fn from(value: InnerReadError) -> PyErr { - PyRuntimeError::new_err(value.to_string()) - } -} - -#[cfg(feature = "python")] -impl From for PyErr { - fn from(value: InnerWriteError) -> PyErr { - PyRuntimeError::new_err(value.to_string()) - } -} - -#[cfg(feature = "python")] -impl From for PyErr { - fn from(value: PauliFromU8Error) -> PyErr { - PyValueError::new_err(value.to_string()) - } -} - -#[cfg(feature = "python")] -impl From for PyErr { - fn from(value: CoherenceError) -> PyErr { - PyValueError::new_err(value.to_string()) - } -} - -#[cfg(feature = "python")] -impl From for PyErr { - fn from(value: LabelError) -> PyErr { - PyValueError::new_err(value.to_string()) - } -} - -#[cfg(feature = "python")] -impl From for PyErr { - fn from(value: ArithmeticError) -> PyErr { - PyValueError::new_err(value.to_string()) - } -} - -/// The single-character string label used to represent this term in the -/// :class:`QubitSparsePauliList` alphabet. -#[cfg(feature = "python")] -#[pyfunction] -#[pyo3(name = "label")] -fn pauli_label(py: Python<'_>, slf: Pauli) -> &Bound<'_, PyString> { - // This doesn't use `py_label` so we can use `intern!`. - match slf { - Pauli::X => intern!(py, "X"), - Pauli::Y => intern!(py, "Y"), - Pauli::Z => intern!(py, "Z"), - } -} -/// Construct the Python-space `IntEnum` that represents the same values as the Rust-spce `Pauli`. -/// -/// We don't make `Pauli` a direct `pyclass` because we want the behaviour of `IntEnum`, which -/// specifically also makes its variants subclasses of the Python `int` type; we use a type-safe -/// enum in Rust, but from Python space we expect people to (carefully) deal with the raw ints in -/// Numpy arrays for efficiency. -/// -/// The resulting class is attached to `QubitSparsePauliList` as a class attribute, and its -/// `__qualname__` is set to reflect this. -#[cfg(feature = "python")] -fn make_py_pauli(py: Python) -> PyResult> { - let terms = [Pauli::X, Pauli::Y, Pauli::Z] - .into_iter() - .flat_map(|term| { - let mut out = vec![(term.py_label(), term as u8)]; - if term.py_label() != term.py_label() { - // Also ensure that the labels are created as aliases. These can't be (easily) accessed - // by attribute-getter (dot) syntax, but will work with the item-getter (square-bracket) - // syntax, or programmatically with `getattr`. - out.push((term.py_label(), term as u8)); - } - out - }) - .collect::>(); - let obj = py.import("enum")?.getattr("IntEnum")?.call( - ("Pauli", terms), - Some( - &[ - ("module", "qiskit.quantum_info"), - ("qualname", "QubitSparsePauliList.Pauli"), - ] - .into_py_dict(py)?, - ), - )?; - let label_property = py - .import("builtins")? - .getattr("property")? - .call1((wrap_pyfunction!(pauli_label, py)?,))?; - obj.setattr("label", label_property)?; - Ok(obj.cast_into::()?.unbind()) -} - -// Return the relevant value from the Python-space sister enumeration. These are Python-space -// singletons and subclasses of Python `int`. We only use this for interaction with "high level" -// Python space; the efficient Numpy-like array paths use `u8` directly so Numpy can act on it -// efficiently. -#[cfg(feature = "python")] -impl<'py> IntoPyObject<'py> for Pauli { - type Target = PyAny; - type Output = Bound<'py, PyAny>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> PyResult { - let terms = PAULI_INTO_PY.get_or_init(py, || { - let py_enum = PAULI_PY_ENUM - .get_or_try_init(py, || make_py_pauli(py)) - .expect("creating a simple Python enum class should be infallible") - .bind(py); - ::std::array::from_fn(|val| { - ::bytemuck::checked::try_cast(val as u8) - .ok() - .map(|term: Pauli| { - py_enum - .getattr(term.py_label()) - .expect("the created `Pauli` enum should have matching attribute names to the terms") - .unbind() - }) - }) - }); - Ok(terms[self as usize] - .as_ref() - .expect("the lookup table initializer populated a 'Some' in all valid locations") - .bind(py) - .clone()) - } -} - -#[cfg(feature = "python")] -impl<'a, 'py> FromPyObject<'a, 'py> for Pauli { - type Error = PyErr; - - fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { - let value = ob - .extract::() - .map_err(|_| match ob.get_type().repr() { - Ok(repr) => PyTypeError::new_err(format!("bad type for 'Pauli': {repr}")), - Err(err) => err, - })?; - let value_error = || { - PyValueError::new_err(format!( - "value {value} is not a valid letter of the single-qubit alphabet for 'Pauli'" - )) - }; - let value: u8 = value.try_into().map_err(|_| value_error())?; - value.try_into().map_err(|_| value_error()) - } -} - -/// A phase-less Pauli operator stored in a qubit-sparse format. -/// -/// Representation -/// ============== -/// -/// A Pauli operator is a tensor product of single-qubit Pauli operators of the form :math:`P = -/// \bigotimes_n A^{(n)}_i`, for :math:`A^{(n)}_i \in \{I, X, Y, Z\}`. The internal representation -/// of a :class:`QubitSparsePauli` stores only the non-identity single-qubit Pauli operators. -/// -/// Internally, each single-qubit Pauli operator is stored with a numeric value, explicitly: -/// -/// .. _qubit-sparse-pauli-alphabet: -/// .. table:: Alphabet of single-qubit Pauli operators used in :class:`QubitSparsePauliList` -/// -/// ======= ======================================= =============== =========================== -/// Label Operator Numeric value :class:`.Pauli` attribute -/// ======= ======================================= =============== =========================== -/// ``"I"`` :math:`I` (identity) Not stored. Not stored. -/// -/// ``"X"`` :math:`X` (Pauli X) ``0b10`` (2) :attr:`~.Pauli.X` -/// -/// ``"Y"`` :math:`Y` (Pauli Y) ``0b11`` (3) :attr:`~.Pauli.Y` -/// -/// ``"Z"`` :math:`Z` (Pauli Z) ``0b01`` (1) :attr:`~.Pauli.Z` -/// -/// ======= ======================================= =============== =========================== -/// -/// .. _qubit-sparse-pauli-arrays: -/// .. table:: Data arrays used to represent :class:`.QubitSparsePauli` -/// -/// ================== =========== ============================================================= -/// Attribute Length Description -/// ================== =========== ============================================================= -/// :attr:`paulis` :math:`s` Each of the non-identity single-qubit Pauli operators. These -/// correspond to the non-identity :math:`A^{(n)}_i` in the list, -/// where the entries are stored in order of increasing :math:`i` -/// first, and in order of increasing :math:`n` within each term. -/// -/// :attr:`indices` :math:`s` The corresponding qubit (:math:`n`) for each of the operators -/// in :attr:`paulis`. :class:`QubitSparsePauli` requires -/// that this list is term-wise sorted, and algorithms can rely -/// on this invariant being upheld. -/// ================== =========== ============================================================= -/// -/// The parameter :math:`s` is the total number of non-identity single-qubit terms. -/// -/// The scalar item of the :attr:`paulis` array is stored as a numeric byte. The numeric values -/// are related to the symplectic Pauli representation that :class:`.SparsePauliOp` uses, and are -/// accessible with named access by an enumeration: -/// -/// .. -/// This is documented manually here because the Python-space `Enum` is generated -/// programmatically from Rust - it'd be _more_ confusing to try and write a docstring somewhere -/// else in this source file. The use of `autoattribute` is because it pulls in the numeric -/// value. -/// -/// .. py:class:: QubitSparsePauli.Pauli -/// -/// -/// An :class:`~enum.IntEnum` that provides named access to the numerical values used to -/// represent each of the single-qubit alphabet terms enumerated in -/// :ref:`qubit-sparse-pauli-alphabet`. -/// -/// This class is attached to :class:`.QubitSparsePauli`. Access it as -/// :class:`.QubitSparsePauli.Pauli`. If this is too much typing, and you are solely -/// dealing with :class:`QubitSparsePauliList` objects and the :class:`Pauli` name is not -/// ambiguous, you might want to shorten it as:: -/// -/// >>> ops = QubitSparsePauli.Pauli -/// >>> assert ops.X is QubitSparsePauli.Pauli.X -/// -/// You can access all the values of the enumeration either with attribute access, or with -/// dictionary-like indexing by string:: -/// -/// >>> assert QubitSparsePauli.Pauli.X is QubitSparsePauli.Pauli["X"] -/// -/// The bits representing each single-qubit Pauli are the (phase-less) symplectic representation -/// of the Pauli operator. -/// -/// Values -/// ------ -/// -/// .. autoattribute:: qiskit.quantum_info::QubitSparsePauli.Pauli.X -/// -/// The Pauli :math:`X` operator. Uses the single-letter label ``"X"``. -/// -/// .. autoattribute:: qiskit.quantum_info::QubitSparsePauli.Pauli.Y -/// -/// The Pauli :math:`Y` operator. Uses the single-letter label ``"Y"``. -/// -/// .. autoattribute:: qiskit.quantum_info::QubitSparsePauli.Pauli.Z -/// -/// The Pauli :math:`Z` operator. Uses the single-letter label ``"Z"``. -/// -/// -/// Each of the array-like attributes behaves like a Python sequence. You can index and slice these -/// with standard :class:`list`-like semantics. Slicing an attribute returns a Numpy -/// :class:`~numpy.ndarray` containing a copy of the relevant data with the natural ``dtype`` of the -/// field; this lets you easily do mathematics on the results, like bitwise operations on -/// :attr:`paulis`. -/// -/// Construction -/// ============ -/// -/// :class:`QubitSparsePauli` defines several constructors. The default constructor will -/// attempt to delegate to one of the more specific constructors, based on the type of the input. -/// You can always use the specific constructors to have more control over the construction. -/// -/// .. _qubit-sparse-pauli-convert-constructors: -/// .. table:: Construction from other objects -/// -/// ============================ ================================================================ -/// Method Summary -/// ============================ ================================================================ -/// :meth:`from_label` Convert a dense string label into a :class:`~.QubitSparsePauli`. -/// -/// :meth:`from_sparse_label` Build a :class:`.QubitSparsePauli` from a tuple of a sparse -/// string label and the qubits they apply to. -/// -/// :meth:`from_pauli` Raise a single :class:`~.quantum_info.Pauli` into a -/// :class:`.QubitSparsePauli`. -/// -/// :meth:`from_raw_parts` Build the operator from :ref:`the raw data arrays -/// `. -/// ============================ ================================================================ -/// -/// .. py:function:: QubitSparsePauli.__new__(data, /, num_qubits=None) -/// -/// The default constructor of :class:`QubitSparsePauli`. -/// -/// This delegates to one of :ref:`the explicit conversion-constructor methods -/// `, based on the type of the ``data`` argument. -/// If ``num_qubits`` is supplied and constructor implied by the type of ``data`` does not -/// accept a number, the given integer must match the input. -/// -/// :param data: The data type of the input. This can be another :class:`QubitSparsePauli`, -/// in which case the input is copied, or it can be a valid format for either -/// :meth:`from_label` or :meth:`from_sparse_label`. -/// :param int|None num_qubits: Optional number of qubits for the operator. For most data -/// inputs, this can be inferred and need not be passed. It is only necessary for the -/// sparse-label format. If given unnecessarily, it must match the data input. -#[cfg(feature = "python")] -#[pyclass( - name = "QubitSparsePauli", - frozen, - module = "qiskit.quantum_info", - skip_from_py_object -)] -#[derive(Clone, Debug)] -pub struct PyQubitSparsePauli { - inner: QubitSparsePauli, -} - -#[cfg(feature = "python")] -impl PyQubitSparsePauli { - pub fn inner(&self) -> &QubitSparsePauli { - &self.inner - } -} - -#[cfg(feature = "python")] -#[pymethods] -impl PyQubitSparsePauli { - #[new] - #[pyo3(signature = (data, /, num_qubits=None))] - fn py_new(data: &Bound, num_qubits: Option) -> PyResult { - let py = data.py(); - let check_num_qubits = |data: &Bound| -> PyResult<()> { - let Some(num_qubits) = num_qubits else { - return Ok(()); - }; - let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::()?; - if num_qubits == other_qubits { - return Ok(()); - } - Err(PyValueError::new_err(format!( - "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})" - ))) - }; - if data.is_instance(imports::PAULI_TYPE.get_bound(py))? { - check_num_qubits(data)?; - return Self::from_pauli(data); - } - if let Ok(label) = data.extract::() { - let num_qubits = num_qubits.unwrap_or(label.len() as u32); - if num_qubits as usize != label.len() { - return Err(PyValueError::new_err(format!( - "explicitly given 'num_qubits' ({}) does not match label ({})", - num_qubits, - label.len(), - ))); - } - return Self::from_label(&label); - } - if let Ok(sparse_label) = data.extract() { - let Some(num_qubits) = num_qubits else { - return Err(PyValueError::new_err( - "if using the sparse-label form, 'num_qubits' must be provided", - )); - }; - return Self::from_sparse_label(sparse_label, num_qubits); - } - Err(PyTypeError::new_err(format!( - "unknown input format for 'QubitSparsePauli': {}", - data.get_type().repr()?, - ))) - } - - /// Construct a :class:`.QubitSparsePauli` from raw Numpy arrays that match :ref:`the required - /// data representation described in the class-level documentation - /// `. - /// - /// The data from each array is copied into fresh, growable Rust-space allocations. - /// - /// Args: - /// num_qubits: number of qubits the operator acts on. - /// paulis: list of the single-qubit terms. This should be a Numpy array with dtype - /// :attr:`~numpy.uint8` (which is compatible with :class:`.Pauli`). - /// indices: sorted list of the qubits each single-qubit term corresponds to. This should - /// be a Numpy array with dtype :attr:`~numpy.uint32`. - /// - /// Examples: - /// - /// Construct a :math:`Z` operator acting on qubit 50 of 100 qubits. - /// - /// >>> num_qubits = 100 - /// >>> terms = np.array([QubitSparsePauli.Pauli.Z], dtype=np.uint8) - /// >>> indices = np.array([50], dtype=np.uint32) - /// >>> QubitSparsePauli.from_raw_parts(num_qubits, terms, indices) - /// - #[staticmethod] - #[pyo3(signature = (/, num_qubits, paulis, indices))] - fn from_raw_parts(num_qubits: u32, paulis: Vec, indices: Vec) -> PyResult { - if paulis.len() != indices.len() { - return Err(CoherenceError::MismatchedItemCount { - paulis: paulis.len(), - indices: indices.len(), - } - .into()); - } - let mut order = (0..paulis.len()).collect::>(); - order.sort_unstable_by_key(|a| indices[*a]); - let paulis = order.iter().map(|i| paulis[*i]).collect(); - let mut sorted_indices = Vec::::with_capacity(order.len()); - for i in order { - let index = indices[i]; - if sorted_indices - .last() - .map(|prev| *prev >= index) - .unwrap_or(false) - { - return Err(CoherenceError::UnsortedIndices.into()); - } - sorted_indices.push(index) - } - let inner = QubitSparsePauli::new(num_qubits, paulis, sorted_indices.into_boxed_slice())?; - Ok(PyQubitSparsePauli { inner }) - } - - /// Construct from a dense string label. - /// - /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted - /// analogously to a bitstring. In other words, the right-most letter is associated with qubit - /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and - /// :class:`.SparsePauliOp`. - /// - /// Args: - /// label (str): the dense label. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> QubitSparsePauli.from_label("IIIIXZI") - /// - /// >>> label = "IYXZI" - /// >>> pauli = Pauli(label) - /// >>> assert QubitSparsePauli.from_label(label) == QubitSparsePauli.from_pauli(pauli) - #[staticmethod] - #[pyo3(signature = (label, /))] - fn from_label(label: &str) -> PyResult { - let inner = QubitSparsePauli::from_dense_label(label)?; - Ok(inner.into()) - } - - /// Construct a :class:`.QubitSparsePauli` from a single :class:`~.quantum_info.Pauli` instance. - /// - /// Note that the phase of the Pauli is dropped. - /// - /// Args: - /// pauli (:class:`~.quantum_info.Pauli`): the single Pauli to convert. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> label = "IYXZI" - /// >>> pauli = Pauli(label) - /// >>> QubitSparsePauli.from_pauli(pauli) - /// - /// >>> assert QubitSparsePauli.from_label(label) == QubitSparsePauli.from_pauli(pauli) - #[staticmethod] - #[pyo3(signature = (pauli, /))] - pub fn from_pauli(pauli: &Bound) -> PyResult { - let py = pauli.py(); - let num_qubits = pauli.getattr(intern!(py, "num_qubits"))?.extract::()?; - let z = pauli - .getattr(intern!(py, "z"))? - .extract::>()?; - let x = pauli - .getattr(intern!(py, "x"))? - .extract::>()?; - let mut paulis = Vec::new(); - let mut indices = Vec::new(); - for (i, (x, z)) in x.as_array().iter().zip(z.as_array().iter()).enumerate() { - // The only failure case possible here is the identity, because of how we're - // constructing the value to convert. - let Ok(term) = ::bytemuck::checked::try_cast(((*x as u8) << 1) | (*z as u8)) else { - continue; - }; - indices.push(i as u32); - paulis.push(term); - } - let inner = QubitSparsePauli::new( - num_qubits, - paulis.into_boxed_slice(), - indices.into_boxed_slice(), - )?; - Ok(inner.into()) - } - - /// Construct a qubit sparse Pauli from a sparse label, given as a tuple of a string of Paulis, - /// and the indices of the corresponding qubits. - /// - /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`. - /// - /// Args: - /// sparse_label (tuple[str, Sequence[int]]): labels and the qubits each single-qubit term - /// applies to. - /// - /// num_qubits (int): the number of qubits the operator acts on. - /// - /// Examples: - /// - /// Construct a simple Pauli:: - /// - /// >>> QubitSparsePauli.from_sparse_label( - /// ... ("ZX", (1, 4)), - /// ... num_qubits=5, - /// ... ) - /// - /// - /// This method can replicate the behavior of :meth:`from_label`, if the qubit-arguments - /// field of the tuple is set to decreasing integers:: - /// - /// >>> label = "XYXZ" - /// >>> from_label = QubitSparsePauli.from_label(label) - /// >>> from_sparse_label = QubitSparsePauli.from_sparse_label( - /// ... (label, (3, 2, 1, 0)), - /// ... num_qubits=4 - /// ... ) - /// >>> assert from_label == from_sparse_label - #[staticmethod] - #[pyo3(signature = (/, sparse_label, num_qubits))] - fn from_sparse_label(sparse_label: (String, Vec), num_qubits: u32) -> PyResult { - let label = sparse_label.0; - let indices = sparse_label.1; - let mut paulis = Vec::new(); - let mut sorted_indices = Vec::new(); - - let label: &[u8] = label.as_ref(); - let mut sorted = btree_map::BTreeMap::new(); - if label.len() != indices.len() { - return Err(LabelError::WrongLengthIndices { - label: label.len(), - indices: indices.len(), - } - .into()); - } - for (letter, index) in label.iter().zip(indices) { - if index >= num_qubits { - return Err(LabelError::BadIndex { index, num_qubits }.into()); - } - let btree_map::Entry::Vacant(entry) = sorted.entry(index) else { - return Err(LabelError::DuplicateIndex { index }.into()); - }; - entry.insert(Pauli::try_from_u8(*letter).map_err(|_| LabelError::OutsideAlphabet)?); - } - for (index, term) in sorted.iter() { - let Some(term) = term else { - continue; - }; - sorted_indices.push(*index); - paulis.push(*term); - } - - let inner = QubitSparsePauli::new( - num_qubits, - paulis.into_boxed_slice(), - sorted_indices.into_boxed_slice(), - )?; - Ok(inner.into()) - } - - /// Get the identity operator for a given number of qubits. - /// - /// Examples: - /// - /// Get the identity on 100 qubits:: - /// - /// >>> QubitSparsePauli.identity(100) - /// - #[pyo3(signature = (/, num_qubits))] - #[staticmethod] - pub fn identity(num_qubits: u32) -> Self { - QubitSparsePauli::identity(num_qubits).into() - } - - /// Convert this Pauli into a single element :class:`QubitSparsePauliList`. - fn to_qubit_sparse_pauli_list(&self) -> PyResult { - let qubit_sparse_pauli_list = QubitSparsePauliList::new( - self.inner.num_qubits(), - self.inner.paulis().to_vec(), - self.inner.indices().to_vec(), - vec![0, self.inner.paulis().len()], - )?; - Ok(qubit_sparse_pauli_list.into()) - } - - /// Phaseless composition with another :class:`QubitSparsePauli`. - /// - /// Args: - /// other (QubitSparsePauli): the qubit sparse Pauli to compose with. - fn compose(&self, other: &PyQubitSparsePauli) -> PyResult { - Ok(PyQubitSparsePauli { - inner: self.inner.compose(&other.inner)?, - }) - } - - fn __matmul__(&self, other: &PyQubitSparsePauli) -> PyResult { - self.compose(other) - } - - /// Check if `self`` commutes with another qubit sparse Pauli. - /// - /// Args: - /// other (QubitSparsePauli): the qubit sparse Pauli to check for commutation with. - fn commutes(&self, other: &PyQubitSparsePauli) -> PyResult { - Ok(self.inner.commutes(&other.inner)?) - } - - fn __eq__(slf: Bound, other: Bound) -> PyResult { - if slf.is(&other) { - return Ok(true); - } - let Ok(other) = other.cast_into::() else { - return Ok(false); - }; - let slf = slf.borrow(); - let other = other.borrow(); - Ok(slf.inner.eq(&other.inner)) - } - - fn __repr__(&self) -> PyResult { - Ok(format!( - "<{} on {} qubit{}: {}>", - "QubitSparsePauli", - self.inner.num_qubits(), - if self.inner.num_qubits() == 1 { - "" - } else { - "s" - }, - self.inner.view().to_sparse_str(), - )) - } - - fn __getnewargs__(slf_: Bound) -> PyResult> { - let py = slf_.py(); - let borrowed = slf_.borrow(); - ( - borrowed.inner.num_qubits(), - Self::get_paulis(slf_.clone()), - Self::get_indices(slf_), - ) - .into_pyobject(py) - } - - fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult> { - let paulis: &[u8] = ::bytemuck::cast_slice(self.inner.paulis()); - ( - py.get_type::().getattr("from_raw_parts")?, - ( - self.inner.num_qubits(), - PyArray1::from_slice(py, paulis), - PyArray1::from_slice(py, self.inner.indices()), - ), - ) - .into_pyobject(py) - } - - /// Return a :class:`~.quantum_info.Pauli` representing the same phaseless Pauli. - fn to_pauli<'py>(&self, py: Python<'py>) -> PyResult> { - imports::PAULI_TYPE - .get_bound(py) - .call1((self.inner.to_dense_label(),)) - } - - /// Get a copy of this term. - fn copy(&self) -> Self { - self.clone() - } - - /// Read-only view onto the individual single-qubit terms. - /// - /// The only valid values in the array are those with a corresponding - /// :class:`~QubitSparsePauli.Pauli`. - #[getter] - fn get_paulis(slf_: Bound) -> Bound> { - let borrowed = slf_.borrow(); - let paulis = borrowed.inner.paulis(); - let arr = ::ndarray::aview1(::bytemuck::cast_slice::<_, u8>(paulis)); - // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. - // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the - // `Box<[Pauli]>` allocation (including dropping or reallocating it) other than the entire - // object getting dropped, which Python will keep safe. - let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; - out.readwrite().make_nonwriteable(); - out - } - - /// The number of qubits the term is defined on. - #[getter] - fn get_num_qubits(&self) -> u32 { - self.inner.num_qubits() - } - - /// Read-only view onto the indices of each non-identity single-qubit term. - /// - /// The indices will always be in sorted order. - #[getter] - fn get_indices(slf_: Bound) -> Bound> { - let borrowed = slf_.borrow(); - let indices = borrowed.inner.indices(); - let arr = ::ndarray::aview1(indices); - // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. - // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the - // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire - // object getting dropped, which Python will keep safe. - let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; - out.readwrite().make_nonwriteable(); - out - } - - // The documentation for this is inlined into the class-level documentation of - // :class:`QubitSparsePauliList`. - #[allow(non_snake_case)] - #[classattr] - pub fn Pauli(py: Python) -> PyResult> { - PAULI_PY_ENUM - .get_or_try_init(py, || make_py_pauli(py)) - .map(|obj| obj.clone_ref(py)) - } -} - -/// A list of phase-less Pauli operators stored in a qubit-sparse format. -/// -/// Representation -/// ============== -/// -/// Each individual Pauli operator in the list is a tensor product of single-qubit Pauli operators -/// of the form :math:`P = \bigotimes_n A^{(n)}_i`, for :math:`A^{(n)}_i \in \{I, X, Y, Z\}`. The -/// internal representation of a :class:`QubitSparsePauliList` stores only the non-identity -/// single-qubit Pauli operators. This makes it significantly more efficient to represent lists of -/// Pauli operators with low weights on a large number of qubits. For example, the list of -/// :math`n`-qubit operators :math:`[Z^{(0)}, \dots Z^{(n-1)}]`, where :math:`Z^{(j)}` represents -/// The :math:`Z` operator on qubit :math:`j` and identity on all others, can be stored in -/// :class:`QubitSparsePauliList` with a linear amount of memory in the number of qubits. -/// -/// Indexing -/// -------- -/// -/// :class:`QubitSparsePauliList` behaves as `a Python sequence -/// `__ (the standard form, not the expanded -/// :class:`collections.abc.Sequence`). The elements of the list can be indexed by integers, as -/// well as iterated through. Whether through indexing or iterating, elements of the list are -/// returned as :class:`QubitSparsePauli` instances. -/// -/// Construction -/// ============ -/// -/// :class:`QubitSparsePauliList` defines several constructors. The default constructor will -/// attempt to delegate to one of the more specific constructors, based on the type of the input. -/// You can always use the specific constructors to have more control over the construction. -/// -/// .. _qubit-sparse-pauli-list-convert-constructors: -/// .. table:: Construction from other objects -/// -/// ================================ ============================================================ -/// Method Summary -/// ================================ ============================================================ -/// :meth:`from_label` Convert a dense string label into a single-element -/// :class:`.QubitSparsePauliList`. -/// -/// :meth:`from_list` Construct from a list of dense string labels. -/// -/// :meth:`from_sparse_list` Elements given as a list of tuples of sparse string labels -/// and the qubits they apply to. -/// -/// :meth:`from_pauli` Raise a single :class:`~.quantum_info.Pauli` into a -/// single-element :class:`.QubitSparsePauliList`. -/// -/// :meth:`from_qubit_sparse_paulis` Construct from a list of :class:`.QubitSparsePauli`\s. -/// ================================ ============================================================ -/// -/// .. py:function:: QubitSparsePauliList.__new__(data, /, num_qubits=None) -/// -/// The default constructor of :class:`QubitSparsePauliList`. -/// -/// This delegates to one of :ref:`the explicit conversion-constructor methods -/// `, based on the type of the ``data`` argument. -/// If ``num_qubits`` is supplied and constructor implied by the type of ``data`` does not -/// accept a number, the given integer must match the input. -/// -/// :param data: The data type of the input. This can be another :class:`QubitSparsePauliList`, -/// in which case the input is copied, or it can be a list in a valid format for either -/// :meth:`from_list` or :meth:`from_sparse_list`. -/// :param int|None num_qubits: Optional number of qubits for the list. For most data -/// inputs, this can be inferred and need not be passed. It is only necessary for empty -/// lists or the sparse-list format. If given unnecessarily, it must match the data input. -/// -/// In addition to the conversion-based constructors, the method :meth:`empty` can be used to -/// construct an empty list of qubit-sparse Paulis acting on a given number of qubits. -/// -/// Conversions -/// =========== -/// -/// An existing :class:`QubitSparsePauliList` can be converted into other formats. -/// -/// .. table:: Conversion methods to other observable forms. -/// -/// =========================== ================================================================= -/// Method Summary -/// =========================== ================================================================= -/// :meth:`to_sparse_list` Express the observable in a sparse list format with elements -/// ``(paulis, indices)``. -/// =========================== ================================================================= -#[cfg(feature = "python")] -#[pyclass( - name = "QubitSparsePauliList", - module = "qiskit.quantum_info", - sequence -)] -#[derive(Debug)] -pub struct PyQubitSparsePauliList { - // This class keeps a pointer to a pure Rust-SparseTerm and serves as interface from Python. - pub inner: Arc>, -} - -#[cfg(feature = "python")] -#[pymethods] -impl PyQubitSparsePauliList { - #[pyo3(signature = (data, /, num_qubits=None))] - #[new] - fn py_new(data: &Bound, num_qubits: Option) -> PyResult { - let py = data.py(); - let check_num_qubits = |data: &Bound| -> PyResult<()> { - let Some(num_qubits) = num_qubits else { - return Ok(()); - }; - let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::()?; - if num_qubits == other_qubits { - return Ok(()); - } - Err(PyValueError::new_err(format!( - "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})" - ))) - }; - if data.is_instance(imports::PAULI_TYPE.get_bound(py))? { - check_num_qubits(data)?; - return Self::from_pauli(data); - } - if let Ok(label) = data.extract::() { - let num_qubits = num_qubits.unwrap_or(label.len() as u32); - if num_qubits as usize != label.len() { - return Err(PyValueError::new_err(format!( - "explicitly given 'num_qubits' ({}) does not match label ({})", - num_qubits, - label.len(), - ))); - } - return Self::from_label(&label).map_err(PyErr::from); - } - if let Ok(pauli_list) = data.cast_exact::() { - check_num_qubits(data)?; - let borrowed = pauli_list.borrow(); - let inner = borrowed.inner.read().map_err(|_| InnerReadError)?; - return Ok(inner.clone().into()); - } - // The type of `vec` is inferred from the subsequent calls to `Self::from_list` or - // `Self::from_sparse_list` to be either the two-tuple or the three-tuple form during the - // `extract`. The empty list will pass either, but it means the same to both functions. - if let Ok(vec) = data.extract() { - return Self::from_list(vec, num_qubits); - } - if let Ok(vec) = data.extract() { - let Some(num_qubits) = num_qubits else { - return Err(PyValueError::new_err( - "if using the sparse-list form, 'num_qubits' must be provided", - )); - }; - return Self::from_sparse_list(vec, num_qubits); - } - if let Ok(term) = data.cast_exact::() { - return term.borrow().to_qubit_sparse_pauli_list(); - }; - if let Ok(pauli_list) = Self::from_qubit_sparse_paulis(data, num_qubits) { - return Ok(pauli_list); - } - Err(PyTypeError::new_err(format!( - "unknown input format for 'QubitSparsePauliList': {}", - data.get_type().repr()?, - ))) - } - - /// Get a copy of this qubit sparse Pauli list. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> qubit_sparse_pauli_list = QubitSparsePauliList.from_list(["IXZXYYZZ", "ZXIXYYZZ"]) - /// >>> assert qubit_sparse_pauli_list == qubit_sparse_pauli_list.copy() - /// >>> assert qubit_sparse_pauli_list is not qubit_sparse_pauli_list.copy() - fn copy(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.clone().into()) - } - - /// The number of qubits the operators in the list act on. - /// - /// This is not inferable from any other shape or values, since identities are not stored - /// explicitly. - #[getter] - #[inline] - pub fn num_qubits(&self) -> PyResult { - 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 { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.num_terms()) - } - - /// Get the empty list for a given number of qubits. - /// - /// The empty list contains no elements, and is the identity element for joining two - /// :class:`QubitSparsePauliList` instances. - /// - /// Examples: - /// - /// Get the empty list on 100 qubits:: - /// - /// >>> QubitSparsePauliList.empty(100) - /// - #[pyo3(signature = (/, num_qubits))] - #[staticmethod] - pub fn empty(num_qubits: u32) -> Self { - QubitSparsePauliList::empty(num_qubits).into() - } - - /// Construct a :class:`.QubitSparsePauliList` from a single :class:`~.quantum_info.Pauli` - /// instance. - /// - /// The output list will have a single term. Note that the phase is dropped. - /// - /// Args: - /// pauli (:class:`~.quantum_info.Pauli`): the single Pauli to convert. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> label = "IYXZI" - /// >>> pauli = Pauli(label) - /// >>> QubitSparsePauliList.from_pauli(pauli) - /// - /// >>> assert QubitSparsePauliList.from_label(label) == QubitSparsePauliList.from_pauli(pauli) - #[staticmethod] - #[pyo3(signature = (pauli, /))] - fn from_pauli(pauli: &Bound) -> PyResult { - let py = pauli.py(); - let num_qubits = pauli.getattr(intern!(py, "num_qubits"))?.extract::()?; - let z = pauli - .getattr(intern!(py, "z"))? - .extract::>()?; - let x = pauli - .getattr(intern!(py, "x"))? - .extract::>()?; - let mut paulis = Vec::new(); - let mut indices = Vec::new(); - for (i, (x, z)) in x.as_array().iter().zip(z.as_array().iter()).enumerate() { - // The only failure case possible here is the identity, because of how we're - // constructing the value to convert. - let Ok(term) = ::bytemuck::checked::try_cast(((*x as u8) << 1) | (*z as u8)) else { - continue; - }; - indices.push(i as u32); - paulis.push(term); - } - let boundaries = vec![0, indices.len()]; - let inner = QubitSparsePauliList::new(num_qubits, paulis, indices, boundaries)?; - Ok(inner.into()) - } - - /// Construct a list with a single-term from a dense string label. - /// - /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted - /// analogously to a bitstring. In other words, the right-most letter is associated with qubit - /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and - /// :class:`.SparsePauliOp`. - /// - /// Args: - /// label (str): the dense label. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> QubitSparsePauliList.from_label("IIIIXZI") - /// - /// >>> label = "IYXZI" - /// >>> pauli = Pauli(label) - /// >>> assert QubitSparsePauliList.from_label(label) == QubitSparsePauliList.from_pauli(pauli) - /// - /// See also: - /// :meth:`from_list` - /// A generalization of this method that constructs a list from multiple labels. - #[staticmethod] - #[pyo3(signature = (label, /))] - fn from_label(label: &str) -> Result { - let mut inner = QubitSparsePauliList::empty(label.len() as u32); - inner.add_dense_label(label)?; - Ok(inner.into()) - } - - /// Construct a qubit-sparse Pauli list from a list of dense labels. - /// - /// This is analogous to :meth:`.SparsePauliOp.from_list`. In this dense form, you must supply - /// all identities explicitly in each label. - /// - /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted - /// analogously to a bitstring. In other words, the right-most letter is associated with qubit - /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and - /// :class:`.SparsePauliOp`. - /// - /// Args: - /// iter (list[str]): List of dense string labels. - /// num_qubits (int | None): It is not necessary to specify this if you are sure that - /// ``iter`` is not an empty sequence, since it can be inferred from the label lengths. - /// If ``iter`` may be empty, you must specify this argument to disambiguate how many - /// qubits the operators act on. If this is given and ``iter`` is not empty, the value - /// must match the label lengths. - /// - /// Examples: - /// - /// Construct a qubit sparse Pauli list from a list of labels:: - /// - /// >>> QubitSparsePauliList.from_list([ - /// ... "IIIXX", - /// ... "IIYYI", - /// ... "IXXII", - /// ... "ZZIII", - /// ... ]) - /// - /// - /// Use ``num_qubits`` to disambiguate potentially empty inputs:: - /// - /// >>> QubitSparsePauliList.from_list([], num_qubits=10) - /// - /// - /// This method is equivalent to calls to :meth:`from_sparse_list` with the explicit - /// qubit-arguments field set to decreasing integers:: - /// - /// >>> labels = ["XYXZ", "YYZZ", "XYXZ"] - /// >>> from_list = QubitSparsePauliList.from_list(labels) - /// >>> from_sparse_list = QubitSparsePauliList.from_sparse_list([ - /// ... (label, (3, 2, 1, 0)) - /// ... for label in labels - /// ... ]) - /// >>> assert from_list == from_sparse_list - /// - /// See also: - /// :meth:`from_sparse_list` - /// Construct the list from labels without explicit identities, but with the qubits each - /// single-qubit operator term applies to listed explicitly. - #[staticmethod] - #[pyo3(signature = (iter, /, *, num_qubits=None))] - fn from_list(iter: Vec, num_qubits: Option) -> PyResult { - if iter.is_empty() && num_qubits.is_none() { - return Err(PyValueError::new_err( - "cannot construct a QubitSparsePauliList from an empty list without knowing `num_qubits`", - )); - } - let num_qubits = match num_qubits { - Some(num_qubits) => num_qubits, - None => iter[0].len() as u32, - }; - let mut inner = QubitSparsePauliList::with_capacity(num_qubits, iter.len(), 0); - for label in iter { - inner.add_dense_label(&label)?; - } - Ok(inner.into()) - } - - /// Construct a :class:`QubitSparsePauliList` out of individual :class:`QubitSparsePauli` - /// instances. - /// - /// All the terms must have the same number of qubits. If supplied, the ``num_qubits`` argument - /// must match the terms. - /// - /// Args: - /// obj (Iterable[QubitSparsePauli]): Iterable of individual terms to build the list from. - /// num_qubits (int | None): The number of qubits the elements of the list should act on. - /// This is usually inferred from the input, but can be explicitly given to handle the - /// case of an empty iterable. - /// - /// Returns: - /// The corresponding list. - #[staticmethod] - #[pyo3(signature = (obj, /, num_qubits=None))] - fn from_qubit_sparse_paulis(obj: &Bound, num_qubits: Option) -> PyResult { - let mut iter = obj.try_iter()?; - let mut inner = match num_qubits { - Some(num_qubits) => QubitSparsePauliList::empty(num_qubits), - None => { - let Some(first) = iter.next() else { - return Err(PyValueError::new_err( - "cannot construct an empty QubitSparsePauliList without knowing `num_qubits`", - )); - }; - let py_term = first?.cast::()?.borrow(); - py_term.inner.to_qubit_sparse_pauli_list() - } - }; - for bound_py_term in iter { - let py_term = bound_py_term?.cast::()?.borrow(); - inner.add_qubit_sparse_pauli(py_term.inner.view())?; - } - Ok(inner.into()) - } - - /// Clear all the elements from the list, making it equal to the empty list again. - /// - /// This does not change the capacity of the internal allocations, so subsequent addition or - /// subtraction operations resulting from composition may not need to reallocate. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> pauli_list = QubitSparsePauliList.from_list(["IXXXYY", "ZZYZII"]) - /// >>> pauli_list.clear() - /// >>> assert pauli_list == QubitSparsePauliList.empty(pauli_list.num_qubits) - pub fn clear(&mut self) -> PyResult<()> { - let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; - inner.clear(); - Ok(()) - } - - /// Construct a qubit sparse Pauli list from a list of labels and the qubits each item applies - /// to. - /// - /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`. - /// - /// The "labels" and "indices" fields of the tuples are associated by zipping them together. - /// For example, this means that a call to :meth:`from_list` can be converted to the form used - /// by this method by setting the "indices" field of each triple to ``(num_qubits-1, ..., 1, - /// 0)``. - /// - /// Args: - /// iter (list[tuple[str, Sequence[int]]]): tuples of labels and the qubits each - /// single-qubit term applies to. - /// - /// num_qubits (int): the number of qubits the operators in the list act on. - /// - /// Examples: - /// - /// Construct a simple list:: - /// - /// >>> QubitSparsePauliList.from_sparse_list( - /// ... [("ZX", (1, 4)), ("YY", (0, 3))], - /// ... num_qubits=5, - /// ... ) - /// - /// - /// This method can replicate the behavior of :meth:`from_list`, if the qubit-arguments - /// field of the tuple is set to decreasing integers:: - /// - /// >>> labels = ["XYXZ", "YYZZ", "XYXZ"] - /// >>> from_list = QubitSparsePauliList.from_list(labels) - /// >>> from_sparse_list = QubitSparsePauliList.from_sparse_list([ - /// ... (label, (3, 2, 1, 0)) - /// ... for label in labels - /// ... ]) - /// >>> assert from_list == from_sparse_list - /// - /// See also: - /// :meth:`to_sparse_list` - /// The reverse of this method. - #[staticmethod] - #[pyo3(signature = (iter, /, num_qubits))] - fn from_sparse_list(iter: Vec<(String, Vec)>, num_qubits: u32) -> PyResult { - let (paulis, indices, boundaries) = raw_parts_from_sparse_list(iter, num_qubits)?; - let inner = QubitSparsePauliList::new(num_qubits, paulis, indices, boundaries)?; - Ok(inner.into()) - } - - /// Express the list in terms of a sparse list format. - /// - /// This can be seen as counter-operation of :meth:`.QubitSparsePauliList.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. - /// - /// Examples: - /// - /// >>> qubit_sparse_list = QubitSparsePauliList.from_list(["IIXIZ", "IIZIX"]) - /// >>> reconstructed = QubitSparsePauliList.from_sparse_list(qubit_sparse_list.to_sparse_list(), qubit_sparse_list.num_qubits) - /// - /// See also: - /// :meth:`from_sparse_list` - /// The constructor that can interpret these lists. - #[pyo3(signature = ())] - fn to_sparse_list(&self, py: Python) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - - // turn a SparseView into a Python tuple of (paulis, indices, coeff) - let to_py_tuple = |view: QubitSparsePauliView| { - let mut pauli_string = String::with_capacity(view.paulis.len()); - - for bit in view.paulis.iter() { - pauli_string.push_str(bit.py_label()); - } - let py_string = PyString::new(py, &pauli_string).unbind(); - let py_indices = PyList::new(py, view.indices.iter())?.unbind(); - - PyTuple::new(py, vec![py_string.as_any(), py_indices.as_any()]) - }; - - let out = PyList::empty(py); - for view in inner.iter() { - out.append(to_py_tuple(view)?)?; - } - Ok(out.unbind()) - } - - /// Express the list in a dense array format. - /// - /// Each entry is a u8 following the :class:`Pauli` representation, while the rows index - /// distinct Paulis and the columns distinct qubits. - /// - /// Examples: - /// - /// >>> paulis = QubitSparsePauliList.from_sparse_list( - /// ... [("ZX", (1, 4)), ("YY", (0, 3)), ("XX", (0, 1))], - /// ... num_qubits=5, - /// ... ) - /// >>> paulis.to_dense_array() - #[pyo3(signature = ())] - fn to_dense_array(&self, py: Python) -> PyResult>> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let mut out = Array2::zeros((inner.num_terms(), inner.num_qubits.try_into().unwrap())); - for (idx, paulis) in inner.iter().enumerate() { - for (p, p_idx) in zip(paulis.paulis, paulis.indices) { - out[[idx, *p_idx as usize]] = *p as u8; - } - } - Ok(out.into_pyarray(py).unbind()) - } - - /// Check if the elements of `self`` commute with another qubit sparse Pauli list. - /// - /// Args: - /// other (QubitSparsePauliList): the qubit sparse Pauli list to check for commutation with. - #[pyo3(signature = (other))] - fn commutes(&self, py: Python, other: &PyQubitSparsePauliList) -> PyResult>> { - let slf_inner = self.inner.read().map_err(|_| InnerReadError)?; - let other_inner = other.inner.read().map_err(|_| InnerReadError)?; - Ok(slf_inner.commutes(&other_inner)?.into_pyarray(py).unbind()) - } - - /// Return a :class:`~.quantum_info.PauliList` representing the same phaseless list of Paulis. - fn to_pauli_list<'py>(&self, py: Python<'py>) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - imports::PAULI_LIST_TYPE - .get_bound(py) - .call1((inner.to_dense_label_list(),)) - } - - /// Apply a transpiler layout to this qubit sparse Pauli list. - /// - /// This enables remapping of qubit indices, e.g. if the list is defined in terms of virtual - /// qubit labels. - /// - /// Args: - /// layout (TranspileLayout | list[int] | None): The layout to apply. Most uses of this - /// function should pass the :attr:`.QuantumCircuit.layout` field from a circuit that - /// was transpiled for hardware. In addition, you can pass a list of new qubit indices. - /// If given as explicitly ``None``, no remapping is applied (but you can still use - /// ``num_qubits`` to expand the qubits in the list). - /// num_qubits (int | None): The number of qubits to expand the list elements to. If not - /// supplied, the output will be as wide as the given :class:`.TranspileLayout`, or the - /// same width as the input if the ``layout`` is given in another form. - /// - /// Returns: - /// A new :class:`QubitSparsePauli` with the provided layout applied. - #[pyo3(signature = (/, layout, num_qubits=None))] - fn apply_layout(&self, layout: Bound, num_qubits: Option) -> PyResult { - let py = layout.py(); - let inner = self.inner.read().map_err(|_| InnerReadError)?; - - // A utility to check the number of qubits is compatible with the map. - let check_inferred_qubits = |inferred: u32| -> PyResult { - if inferred < inner.num_qubits() { - return Err(CoherenceError::NotEnoughQubits { - current: inner.num_qubits() as usize, - target: inferred as usize, - } - .into()); - } - Ok(inferred) - }; - - // Normalize the number of qubits in the layout and the layout itself, depending on the - // input types, before calling QubitSparsePauliList.apply_layout to do the actual work. - let (num_qubits, layout): (u32, Option>) = if layout.is_none() { - (num_qubits.unwrap_or(inner.num_qubits()), None) - } else if layout.is_instance( - &py.import(intern!(py, "qiskit.transpiler"))? - .getattr(intern!(py, "TranspileLayout"))?, - )? { - ( - check_inferred_qubits( - layout.getattr(intern!(py, "_output_qubit_list"))?.len()? as u32 - )?, - Some( - layout - .call_method0(intern!(py, "final_index_layout"))? - .extract::>()?, - ), - ) - } else { - ( - check_inferred_qubits(num_qubits.unwrap_or(inner.num_qubits()))?, - Some(layout.extract()?), - ) - }; - - let out = inner.apply_layout(layout.as_deref(), num_qubits)?; - Ok(out.into()) - } - - fn __len__(&self) -> PyResult { - self.num_terms() - } - - fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - ( - py.get_type::().getattr("from_sparse_list")?, - (self.to_sparse_list(py)?, inner.num_qubits()), - ) - .into_pyobject(py) - } - - fn __getitem__<'py>( - &self, - py: Python<'py>, - index: PySequenceIndex<'py>, - ) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let indices = match index.with_len(inner.num_terms())? { - SequenceIndex::Int(index) => { - return PyQubitSparsePauli { - inner: inner.term(index).to_term(), - } - .into_bound_py_any(py); - } - indices => indices, - }; - let mut out = QubitSparsePauliList::empty(inner.num_qubits()); - for index in indices.iter() { - out.add_qubit_sparse_pauli(inner.term(index))?; - } - out.into_bound_py_any(py) - } - - fn __eq__(slf: Bound, other: Bound) -> PyResult { - // this is also important to check before trying to read both slf and other - if slf.is(&other) { - return Ok(true); - } - let Ok(other) = other.cast_into::() else { - return Ok(false); - }; - let slf_borrowed = slf.borrow(); - let other_borrowed = other.borrow(); - let slf_inner = slf_borrowed.inner.read().map_err(|_| InnerReadError)?; - let other_inner = other_borrowed.inner.read().map_err(|_| InnerReadError)?; - Ok(slf_inner.eq(&other_inner)) - } - - fn __repr__(&self) -> PyResult { - let num_terms = self.num_terms()?; - let num_qubits = self.num_qubits()?; - - let str_num_terms = format!( - "{} element{}", - num_terms, - if num_terms == 1 { "" } else { "s" } - ); - let str_num_qubits = format!( - "{} qubit{}", - num_qubits, - if num_qubits == 1 { "" } else { "s" } - ); - - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let str_terms = if num_terms == 0 { - "".to_owned() - } else { - inner - .iter() - .map(QubitSparsePauliView::to_sparse_str) - .collect::>() - .join(", ") - }; - Ok(format!( - "" - )) - } -} - -#[cfg(feature = "python")] -impl From for PyQubitSparsePauli { - fn from(val: QubitSparsePauli) -> PyQubitSparsePauli { - PyQubitSparsePauli { inner: val } - } -} - -#[cfg(feature = "python")] -impl<'py> IntoPyObject<'py> for QubitSparsePauli { - type Target = PyQubitSparsePauli; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> PyResult { - PyQubitSparsePauli::from(self).into_pyobject(py) - } -} - -#[cfg(feature = "python")] -impl From for PyQubitSparsePauliList { - fn from(val: QubitSparsePauliList) -> PyQubitSparsePauliList { - PyQubitSparsePauliList { - inner: Arc::new(RwLock::new(val)), - } - } -} - -#[cfg(feature = "python")] -impl<'py> IntoPyObject<'py> for QubitSparsePauliList { - type Target = PyQubitSparsePauliList; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> PyResult { - PyQubitSparsePauliList::from(self).into_pyobject(py) - } -} diff --git a/crates/quantum_info/src/python/mod.rs b/crates/quantum_info/src/python/mod.rs new file mode 100644 index 000000000000..23dee9e06fec --- /dev/null +++ b/crates/quantum_info/src/python/mod.rs @@ -0,0 +1,28 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +pub mod pauli_lindblad_map; +pub mod sparse_observable; +pub mod sparse_pauli_op; + +mod rayon_ext; + +use pyo3::import_exception; + +pub(crate) mod imports { + use qiskit_util::py::ImportOnceCell; + + pub static PAULI_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "Pauli"); + pub static PAULI_LIST_TYPE: ImportOnceCell = + ImportOnceCell::new("qiskit.quantum_info", "PauliList"); +} +import_exception!(qiskit.exceptions, QiskitError); diff --git a/crates/quantum_info/src/python/pauli_lindblad_map/mod.rs b/crates/quantum_info/src/python/pauli_lindblad_map/mod.rs new file mode 100644 index 000000000000..cac7423382fa --- /dev/null +++ b/crates/quantum_info/src/python/pauli_lindblad_map/mod.rs @@ -0,0 +1,29 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +mod pauli_lindblad_map_class; +mod phased_qubit_sparse_pauli; +mod qubit_sparse_pauli; + +use pauli_lindblad_map_class::PyPauliLindbladMap; +use phased_qubit_sparse_pauli::{PyPhasedQubitSparsePauli, PyPhasedQubitSparsePauliList}; +use pyo3::prelude::*; +use qubit_sparse_pauli::{PyQubitSparsePauli, PyQubitSparsePauliList}; + +pub fn pauli_lindblad_map(m: &Bound) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/crates/quantum_info/src/python/pauli_lindblad_map/pauli_lindblad_map_class.rs b/crates/quantum_info/src/python/pauli_lindblad_map/pauli_lindblad_map_class.rs new file mode 100644 index 000000000000..55756a684e36 --- /dev/null +++ b/crates/quantum_info/src/python/pauli_lindblad_map/pauli_lindblad_map_class.rs @@ -0,0 +1,1414 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use numpy::{PyArray1, PyArray2, PyArrayMethods, ToPyArray}; +use pyo3::{ + IntoPyObjectExt, PyErr, + exceptions::{PyTypeError, PyValueError}, + intern, + prelude::*, + types::{PyList, PyString, PyTuple, PyType}, +}; + +use std::sync::{Arc, RwLock}; + +use qiskit_util::py::{PySequenceIndex, SequenceIndex}; + +use crate::pauli_lindblad_map::pauli_lindblad_map_class::{ + GeneratorTerm, GeneratorTermView, PauliLindbladMap, +}; +use crate::pauli_lindblad_map::qubit_sparse_pauli::{ + CoherenceError, InnerReadError, InnerWriteError, QubitSparsePauliList, + raw_parts_from_sparse_list, +}; + +use super::qubit_sparse_pauli::{PyQubitSparsePauli, PyQubitSparsePauliList}; + +/// A single term from a complete :class:`PauliLindbladMap`. +/// +/// These are typically created by indexing into or iterating through a :class:`PauliLindbladMap`. +#[pyclass( + name = "GeneratorTerm", + frozen, + module = "qiskit.quantum_info", + skip_from_py_object +)] +#[derive(Clone, Debug)] +struct PyGeneratorTerm { + inner: GeneratorTerm, +} + +#[pymethods] +impl PyGeneratorTerm { + // Mark the Python class as being defined "within" the `PauliLindbladMap` class namespace. + #[classattr] + #[pyo3(name = "__qualname__")] + fn type_qualname() -> &'static str { + "PauliLindbladMap.GeneratorTerm" + } + + #[new] + #[pyo3(signature = (/, rate, qubit_sparse_pauli))] + fn py_new(rate: f64, qubit_sparse_pauli: &PyQubitSparsePauli) -> PyResult { + let inner = GeneratorTerm { + rate, + qubit_sparse_pauli: qubit_sparse_pauli.inner().clone(), + }; + Ok(PyGeneratorTerm { inner }) + } + + /// Convert this term to a complete :class:`PauliLindbladMap`. + fn to_pauli_lindblad_map(&self) -> PyResult { + let pauli_lindblad_map = PauliLindbladMap::new( + vec![self.inner.rate()], + self.inner.qubit_sparse_pauli.to_qubit_sparse_pauli_list(), + )?; + Ok(pauli_lindblad_map.into()) + } + + fn __eq__(slf: Bound, other: Bound) -> PyResult { + if slf.is(&other) { + return Ok(true); + } + let Ok(other) = other.cast_into::() else { + return Ok(false); + }; + let slf = slf.borrow(); + let other = other.borrow(); + Ok(slf.inner.eq(&other.inner)) + } + + fn __repr__(&self) -> PyResult { + Ok(format!( + "<{} on {} qubit{}: {}>", + Self::type_qualname(), + self.inner.num_qubits(), + if self.inner.num_qubits() == 1 { + "" + } else { + "s" + }, + self.inner.view().to_sparse_str(), + )) + } + + /// Get a copy of this term. + fn copy(&self) -> Self { + self.clone() + } + + /// Read-only view onto the individual single-qubit terms. + /// + /// The only valid values in the array are those with a corresponding + /// :class:`~PauliLindbladMap.Pauli`. + #[getter] + fn get_paulis(slf_: Bound) -> Bound> { + let borrowed = slf_.borrow(); + let paulis = borrowed.inner.paulis(); + let arr = ::ndarray::aview1(::bytemuck::cast_slice::<_, u8>(paulis)); + // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. + // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the + // `Box<[Pauli]>` allocation (including dropping or reallocating it) other than the entire + // object getting dropped, which Python will keep safe. + let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; + out.readwrite().make_nonwriteable(); + out + } + + /// The number of qubits the term is defined on. + #[getter] + fn get_num_qubits(&self) -> u32 { + self.inner.num_qubits() + } + + /// The term's rate. + #[getter] + fn get_rate(&self) -> f64 { + self.inner.rate() + } + + /// The term's qubit_sparse_pauli. + #[getter] + fn get_qubit_sparse_pauli(&self) -> PyQubitSparsePauli { + self.inner.qubit_sparse_pauli.clone().into() + } + + /// Read-only view onto the indices of each non-identity single-qubit term. + /// + /// The indices will always be in sorted order. + #[getter] + fn get_indices(slf_: Bound) -> Bound> { + let borrowed = slf_.borrow(); + let indices = borrowed.inner.indices(); + let arr = ::ndarray::aview1(indices); + // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. + // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the + // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire + // object getting dropped, which Python will keep safe. + let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; + out.readwrite().make_nonwriteable(); + out + } + + fn __getnewargs__(slf_: Bound) -> PyResult> { + let py = slf_.py(); + let borrowed = slf_.borrow(); + ( + borrowed.inner.rate, + borrowed.inner.qubit_sparse_pauli.clone(), + ) + .into_pyobject(py) + } + + /// Return the pauli labels of the term as a string. + /// + /// The pauli labels will match the order of :attr:`.GeneratorTerm.indices`, such that the + /// i-th character in the string is applied to the qubit index at ``term.indices[i]``. E.g. the + /// term with operator ``X`` acting on qubit 0 and ``Y`` acting on qubit ``3`` will have + /// ``term.indices == np.array([0, 3])`` and ``term.pauli_labels == "XY"``. + /// + /// Returns: + /// The non-identity bit terms as a concatenated string. + fn pauli_labels<'py>(&self, py: Python<'py>) -> Bound<'py, PyString> { + let string: String = self + .inner + .paulis() + .iter() + .map(|bit| bit.py_label()) + .collect(); + PyString::new(py, string.as_str()) + } +} + +/// A Pauli Lindblad map stored in a qubit-sparse format. +/// +/// Mathematics +/// =========== +/// +/// A Pauli-Lindblad map is a linear map acting on density matrices on :math:`n`-qubits of the form: +/// +/// .. math:: +/// +/// \Lambda\bigl[\circ\bigr] = \exp\left(\sum_{P \in K} \lambda_P (P \circ P - \circ)\right) +/// +/// where :math:`K` is a subset of :math:`n`-qubit Pauli operators, and the rates, or coefficients, +/// :math:`\lambda_P` are real numbers. When all the rates :math:`\lambda_P` are non-negative, this +/// corresponds to a completely positive and trace preserving map. The sum in the exponential is +/// called the generator, and each individual term the generators. To simplify notation in the rest +/// of the documentation, we denote :math:`L(P)\bigl[\circ\bigr] = P \circ P - \circ`. +/// +/// Quasi-probability representation +/// ================================ +/// +/// The map :math:`\Lambda` can be written as a product: +/// +/// .. math:: +/// +/// \Lambda\bigl[\circ\bigr] = \prod_{P \in K}\exp\left(\lambda_P(P \circ P - \circ)\right). +/// +/// For each :math:`P`, it holds that +/// +/// .. math:: +/// +/// \exp\left(\lambda_P(P \circ P - \circ)\right) = \omega(\lambda_P) \circ + (1 - \omega(\lambda_P)) P \circ P, +/// +/// where :math:`\omega(x) = \frac{1}{2}(1 + e^{-2 x})`. Observe that if :math:`\lambda_P \geq 0`, +/// then :math:`\omega(\lambda_P) \in (\frac{1}{2}, 1]`, and this term is a completely-positive and +/// trace-preserving map. However, if :math:`\lambda_P < 0`, then :math:`\omega(\lambda_P) > 1` and +/// the map is not completely positive or trace preserving. Letting +/// :math:`\gamma_P = \omega(\lambda_P) + |1 - \omega(\lambda_P)|`, +/// :math:`p_P = \omega(\lambda_P) / \gamma_P` and :math:`b_P \in \{0, 1\}` be :math:`1` if +/// :math:`\lambda_P < 0` and :math:`0` otherwise, we rewrite the map as: +/// +/// .. math:: +/// +/// \omega(\lambda_P) \circ + (1 - \omega(\lambda_P)) P \circ P = \gamma_P \left(p_P \circ + (-1)^{b_P}(1 - p_P) P \circ P\right). +/// +/// If :math:`\lambda_P \geq 0`, :math:`\gamma_P = 1` and the expression reduces to the standard +/// mixture of the identity map and conjugation by :math:`P`. If :math:`\lambda_P < 0`, +/// :math:`\gamma_P > 1`, and the map is a scaled difference of the identity map and conjugation by +/// :math:`P`, with probability weights (hence "quasi-probability"). Note that this is a slightly +/// different presentation than in the literature, but this notation allows us to handle both +/// non-negative and negative rates simultaneously. The overall :math:`\gamma` of the channel is the +/// product :math:`\gamma = \prod_{P \in K} \gamma_P`. +/// +/// See the :meth:`.PauliLindbladMap.sample` method for the sampling procedure for this map. +/// +/// Representation +/// ============== +/// +/// Each individual Pauli operator in the generator is a tensor product of single-qubit Pauli +/// operators of the form :math:`P = \bigotimes_n A^{(n)}_i`, for :math:`A^{(n)}_i \in \{I, X, Y, +/// Z\}`. The internal representation of a :class:`PauliLindbladMap` stores only the non-identity +/// single-qubit Pauli operators. This makes it significantly more efficient to represent +/// generators such as :math:`\sum_{n\in \text{qubits}} c_n L(Z^{(n)})`; for which +/// :class:`PauliLindbladMap` requires an amount of memory linear in the total number of qubits. +/// +/// Internally, :class:`.PauliLindbladMap` stores an array of rates and a +/// :class:`.QubitSparsePauliList` containing the corresponding sparse Pauli operators. +/// Additionally, :class:`.PauliLindbladMap` can compute the overall channel :math:`\gamma` in the +/// :meth:`gamma` method, as well as the corresponding probabilities :math:`p_P` +/// via the :meth:`probabilities` method. +/// +/// Indexing +/// -------- +/// +/// :class:`PauliLindbladMap` behaves as `a Python sequence +/// `__ (the standard form, not the expanded +/// :class:`collections.abc.Sequence`). The generators of the map can be indexed by integers, and +/// iterated through to yield individual generator terms. +/// +/// Each generator term appears as an instance a self-contained class. The individual terms are +/// copied out of the base map; mutations to them will not affect the original map from which they +/// are indexed. +/// +/// .. autoclass:: qiskit.quantum_info::PauliLindbladMap.GeneratorTerm +/// :members: +/// +/// Construction +/// ============ +/// +/// :class:`PauliLindbladMap` defines several constructors. The default constructor will attempt to +/// delegate to one of the more specific constructors, based on the type of the input. You can +/// always use the specific constructors to have more control over the construction. +/// +/// .. _pauli-lindblad-map-convert-constructors: +/// .. table:: Construction from other objects +/// +/// ============================ ================================================================ +/// Method Summary +/// ============================ ================================================================ +/// :meth:`from_list` Generators given as a list of tuples of dense string labels and +/// the associated rates. +/// +/// :meth:`from_sparse_list` Generators given as a list of tuples of sparse string labels, +/// the qubits they apply to, and their rates. +/// +/// :meth:`from_terms` Sum explicit single :class:`GeneratorTerm` instances. +/// +/// :meth:`from_components` Build from an array of rates and a +/// :class:`.QubitSparsePauliList` instance. +/// ============================ ================================================================ +/// +/// .. py:function:: PauliLindbladMap.__new__(data, /, num_qubits=None) +/// +/// The default constructor of :class:`PauliLindbladMap`. +/// +/// This delegates to one of :ref:`the explicit conversion-constructor methods +/// `, based on the type of the ``data`` argument. If +/// ``num_qubits`` is supplied and constructor implied by the type of ``data`` does not accept a +/// number, the given integer must match the input. +/// +/// :param data: The data type of the input. This can be another :class:`PauliLindbladMap`, in +/// which case the input is copied, or it can be a list in a valid format for either +/// :meth:`from_list` or :meth:`from_sparse_list`. +/// :param int|None num_qubits: Optional number of qubits for the map. For most data +/// inputs, this can be inferred and need not be passed. It is only necessary for empty +/// lists or the sparse-list format. If given unnecessarily, it must match the data input. +/// +/// In addition to the conversion-based constructors, there are also helper methods that construct +/// special forms of maps. +/// +/// .. table:: Construction of special maps +/// +/// ============================ ================================================================ +/// Method Summary +/// ============================ ================================================================ +/// :meth:`identity` The identity map on a given number of qubits. +/// ============================ ================================================================ +/// +/// Conversions +/// =========== +/// +/// An existing :class:`PauliLindbladMap` can be converted into other formats. +/// +/// .. table:: Conversion methods to other forms. +/// +/// =========================== ================================================================= +/// Method Summary +/// =========================== ================================================================= +/// :meth:`to_sparse_list` Express the map in a sparse list format with elements +/// ``(paulis, indices, rate)``. +/// =========================== ================================================================= +#[pyclass(name = "PauliLindbladMap", module = "qiskit.quantum_info", sequence)] +#[derive(Debug)] +pub struct PyPauliLindbladMap { + // This class keeps a pointer to a pure Rust-GeneratorTerm and serves as interface from Python. + inner: Arc>, +} + +#[pymethods] +impl PyPauliLindbladMap { + #[pyo3(signature = (data, /, num_qubits=None))] + #[new] + fn py_new(data: &Bound, num_qubits: Option) -> PyResult { + let py = data.py(); + let check_num_qubits = |data: &Bound| -> PyResult<()> { + let Some(num_qubits) = num_qubits else { + return Ok(()); + }; + let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::()?; + if num_qubits == other_qubits { + return Ok(()); + } + Err(PyValueError::new_err(format!( + "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})" + ))) + }; + if let Ok(pauli_lindblad_map) = data.cast_exact::() { + check_num_qubits(data)?; + let borrowed = pauli_lindblad_map.borrow(); + let inner = borrowed.inner.read().map_err(|_| InnerReadError)?; + return Ok(inner.clone().into()); + } + // The type of `vec` is inferred from the subsequent calls to `Self::from_list` or + // `Self::from_sparse_list` to be either the two-tuple or the three-tuple form during the + // `extract`. The empty list will pass either, but it means the same to both functions. + if let Ok(vec) = data.extract() { + return Self::from_list(vec, num_qubits); + } + if let Ok(vec) = data.extract() { + let Some(num_qubits) = num_qubits else { + return Err(PyValueError::new_err( + "if using the sparse-list form, 'num_qubits' must be provided", + )); + }; + return Self::from_sparse_list(vec, num_qubits); + } + if let Ok(term) = data.cast_exact::() { + return term.borrow().to_pauli_lindblad_map(); + }; + if let Ok(pauli_lindblad_map) = Self::from_terms(data, num_qubits) { + return Ok(pauli_lindblad_map); + } + Err(PyTypeError::new_err(format!( + "unknown input format for 'PauliLindbladMap': {}", + data.get_type().repr()?, + ))) + } + + #[staticmethod] + fn from_components( + rates: Vec, + qubit_sparse_pauli_list: &PyQubitSparsePauliList, + ) -> PyResult { + let qubit_sparse_pauli_list = qubit_sparse_pauli_list + .inner + .read() + .map_err(|_| InnerReadError)?; + let inner = PauliLindbladMap::new(rates, qubit_sparse_pauli_list.clone())?; + + Ok(inner.into()) + } + + /// Construct a Pauli Lindblad map from a list of dense generator labels and rates. + /// + /// This is analogous to :meth:`.SparsePauliOp.from_list`. In this dense form, you must supply + /// all identities explicitly in each label. + /// + /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted + /// analogously to a bitstring. In other words, the right-most letter is associated with qubit + /// 0, and so on. This is the same as the labels for :class:`.Pauli` and + /// :class:`.SparsePauliOp`. + /// + /// Args: + /// iter (list[tuple[str, float]]): Pairs of labels and their associated rates in the + /// generator sum. + /// num_qubits (int | None): It is not necessary to specify this if you are sure that + /// ``iter`` is not an empty sequence, since it can be inferred from the label lengths. + /// If ``iter`` may be empty, you must specify this argument to disambiguate how many + /// qubits the map acts on. If this is given and ``iter`` is not empty, the value + /// must match the label lengths. + /// + /// Examples: + /// + /// Construct a Pauli Lindblad map from a list of labels:: + /// + /// >>> PauliLindbladMap.from_list([ + /// ... ("IIIXX", 1.0), + /// ... ("IIYYI", 1.0), + /// ... ("IXXII", -0.5), + /// ... ("ZZIII", -0.25), + /// ... ]) + /// + /// + /// Use ``num_qubits`` to disambiguate potentially empty inputs:: + /// + /// >>> PauliLindbladMap.from_list([], num_qubits=10) + /// + /// + /// This method is equivalent to calls to :meth:`from_sparse_list` with the explicit + /// qubit-arguments field set to decreasing integers:: + /// + /// >>> labels = ["XYXZ", "YYZZ", "XYXZ"] + /// >>> rates = [1.5, 2.0, -0.5] + /// >>> from_list = PauliLindbladMap.from_list(list(zip(labels, rates))) + /// >>> from_sparse_list = PauliLindbladMap.from_sparse_list([ + /// ... (label, (3, 2, 1, 0), rate) + /// ... for label, rate in zip(labels, rates) + /// ... ]) + /// >>> assert from_list == from_sparse_list + /// + /// See also: + /// :meth:`from_sparse_list` + /// Construct the map from a list of labels without explicit identities, but with + /// the qubits each single-qubit generator term applies to listed explicitly. + #[staticmethod] + #[pyo3(signature = (iter, /, *, num_qubits=None))] + fn from_list(iter: Vec<(String, f64)>, num_qubits: Option) -> PyResult { + if iter.is_empty() && num_qubits.is_none() { + return Err(PyValueError::new_err( + "cannot construct a PauliLindbladMap from an empty list without knowing `num_qubits`", + )); + } + let num_qubits = match num_qubits { + Some(num_qubits) => num_qubits, + None => iter[0].0.len() as u32, + }; + let mut inner = PauliLindbladMap::with_capacity(num_qubits, iter.len(), 0); + for (label, rate) in iter { + inner.add_dense_label(&label, rate)?; + } + Ok(inner.into()) + } + + /// Get the identity map on the given number of qubits. + /// + /// The identity map contains no generator terms, and is the identity element for composition of + /// two :class:`PauliLindbladMap` instances; anything composed with the identity map is equal to + /// itself. + /// + /// Examples: + /// + /// Get the identity map on 100 qubits:: + /// + /// >>> PauliLindbladMap.identity(100) + /// + #[pyo3(signature = (/, num_qubits))] + #[staticmethod] + pub fn identity(num_qubits: u32) -> Self { + PauliLindbladMap::identity(num_qubits).into() + } + + /// Construct a :class:`PauliLindbladMap` out of individual terms. + /// + /// All the terms must have the same number of qubits. If supplied, the ``num_qubits`` argument + /// must match the terms. + /// + /// No simplification is done as part of the map creation. + /// + /// Args: + /// obj (Iterable[Term]): Iterable of individual terms to build the map generator from. + /// num_qubits (int | None): The number of qubits the map should act on. This is + /// usually inferred from the input, but can be explicitly given to handle the case + /// of an empty iterable. + /// + /// Returns: + /// The corresponding map. + #[staticmethod] + #[pyo3(signature = (obj, /, num_qubits=None))] + fn from_terms(obj: &Bound, num_qubits: Option) -> PyResult { + let mut iter = obj.try_iter()?; + let mut inner = match num_qubits { + Some(num_qubits) => PauliLindbladMap::identity(num_qubits), + None => { + let Some(first) = iter.next() else { + return Err(PyValueError::new_err( + "cannot construct a PauliLindbladMap from an empty list without knowing `num_qubits`", + )); + }; + let py_term = first?.cast::()?.borrow(); + py_term.inner.to_pauli_lindblad_map()? + } + }; + for bound_py_term in iter { + let py_term = bound_py_term?.cast::()?.borrow(); + inner.add_term(py_term.inner.view())?; + } + Ok(inner.into()) + } + + /// Clear all the generator terms from this map, making it equal to the identity map again. + /// + /// This does not change the capacity of the internal allocations, so subsequent addition or + /// subtraction operations resulting from composition may not need to reallocate. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> pauli_lindblad_map = PauliLindbladMap.from_list([("IXXXYY", 2.0), ("ZZYZII", -1)]) + /// >>> pauli_lindblad_map.clear() + /// >>> assert pauli_lindblad_map == PauliLindbladMap.identity(pauli_lindblad_map.py_num_qubits()) + pub fn clear(&mut self) -> PyResult<()> { + let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; + inner.clear(); + Ok(()) + } + + /// Construct a Pauli Lindblad map from a list of labels, the qubits each item applies to, and + /// the rate of the whole term. + /// + /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`. + /// + /// The "labels" and "indices" fields of the triples are associated by zipping them together. + /// For example, this means that a call to :meth:`from_list` can be converted to the form used + /// by this method by setting the "indices" field of each triple to ``(num_qubits-1, ..., 1, + /// 0)``. + /// + /// Args: + /// iter (list[tuple[str, Sequence[int], float]]): triples of labels, the qubits + /// each single-qubit term applies to, and the rate of the entire term. + /// + /// num_qubits (int): the number of qubits the map acts on. + /// + /// Examples: + /// + /// Construct a simple map:: + /// + /// >>> PauliLindbladMap.from_sparse_list( + /// ... [("ZX", (1, 4), 1.0), ("YY", (0, 3), 2)], + /// ... num_qubits=5, + /// ... ) + /// + /// + /// This method can replicate the behavior of :meth:`from_list`, if the qubit-arguments + /// field of the triple is set to decreasing integers:: + /// + /// >>> labels = ["XYXZ", "YYZZ", "XYXZ"] + /// >>> rates = [1.5, 2.0, -0.5] + /// >>> from_list = PauliLindbladMap.from_list(list(zip(labels, rates))) + /// >>> from_sparse_list = PauliLindbladMap.from_sparse_list([ + /// ... (label, (3, 2, 1, 0), rate) + /// ... for label, rate in zip(labels, rates) + /// ... ]) + /// >>> assert from_list == from_sparse_list + /// + /// See also: + /// :meth:`to_sparse_list` + /// The reverse of this method. + #[staticmethod] + #[pyo3(signature = (iter, /, num_qubits))] + fn from_sparse_list(iter: Vec<(String, Vec, f64)>, num_qubits: u32) -> PyResult { + let rates = iter.iter().map(|(_, _, rate)| *rate).collect(); + let op_iter = iter + .iter() + .map(|(label, indices, _)| (label.clone(), indices.clone())) + .collect(); + let (paulis, indices, boundaries) = raw_parts_from_sparse_list(op_iter, num_qubits)?; + let qubit_sparse_pauli_list = + QubitSparsePauliList::new(num_qubits, paulis, indices, boundaries)?; + let inner: PauliLindbladMap = PauliLindbladMap::new(rates, qubit_sparse_pauli_list)?; + Ok(inner.into()) + } + + /// Get a copy of this Pauli Lindblad map. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> pauli_lindblad_map = PauliLindbladMap.from_list([("IXZXYYZZ", 2.5), ("ZXIXYYZZ", 0.5)]) + /// >>> assert pauli_lindblad_map == pauli_lindblad_map.copy() + /// >>> assert pauli_lindblad_map is not pauli_lindblad_map.copy() + fn copy(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.clone().into()) + } + + /// The number of qubits the map acts on. + /// + /// This is not inferable from any other shape or values, since identities are not stored + /// explicitly. + #[getter] + #[inline] + pub fn num_qubits(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.num_qubits()) + } + + /// The number of generator terms in the exponent for this map. + #[getter] + #[inline] + pub fn num_terms(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.num_terms()) + } + + /// Calculate the :math:`\gamma` for the map. + #[inline] + fn gamma(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.gamma) + } + + /// The rates for the map. + #[getter] + fn get_rates(slf_: Bound) -> Bound> { + let borrowed = &slf_.borrow(); + let inner = borrowed.inner.read().unwrap(); + let rates = inner.rates(); + let arr = ::ndarray::aview1(rates); + // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. + // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the + // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire + // object getting dropped, which Python will keep safe. + let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; + out.readwrite().make_nonwriteable(); + out + } + + /// Calculate the probabilities :math:`p_P` for the map. + /// These can be interpreted as the probabilities each generator is not applied, + /// and are defined to be independent of the sign of each Lindblad rate. + fn probabilities<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1> { + let inner = self.inner.read().unwrap(); + inner.probabilities().to_pyarray(py) + } + + /// Get a copy of the map's qubit sparse pauli list. + fn get_qubit_sparse_pauli_list_copy(&self) -> PyQubitSparsePauliList { + let inner = self.inner.read().unwrap(); + inner.qubit_sparse_pauli_list.clone().into() + } + + /// Get the generators of the map. + /// + /// This is an alias for :meth:`get_qubit_sparse_pauli_list_copy`, providing a more + /// convenient name that aligns with the naming conventions used in Aer and Runtime + /// for similar classes. + /// + /// Returns: + /// QubitSparsePauliList: A copy of the map's generator terms. + fn generators(&self) -> PyQubitSparsePauliList { + self.get_qubit_sparse_pauli_list_copy() + } + + /// Express the map in terms of a sparse list format. + /// + /// This can be seen as counter-operation of :meth:`.PauliLindbladMap.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. + /// + /// Examples: + /// + /// >>> pauli_lindblad_map = PauliLindbladMap.from_list([("IIXIZ", 2), ("IIZIX", 3)]) + /// >>> reconstructed = PauliLindbladMap.from_sparse_list(pauli_lindblad_map.to_sparse_list(), pauli_lindblad_map.num_qubits) + /// + /// See also: + /// :meth:`from_sparse_list` + /// The constructor that can interpret these lists. + #[pyo3(signature = ())] + fn to_sparse_list(&self, py: Python) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + // turn a SparseView into a Python tuple of (bit terms, indices, rate) + let to_py_tuple = |view: GeneratorTermView| { + let mut pauli_string = String::with_capacity(view.qubit_sparse_pauli.paulis.len()); + + for bit in view.qubit_sparse_pauli.paulis.iter() { + pauli_string.push_str(bit.py_label()); + } + let py_string = PyString::new(py, &pauli_string).unbind(); + let py_indices = PyList::new(py, view.qubit_sparse_pauli.indices.iter())?.unbind(); + let py_rate = view.rate.into_py_any(py)?; + + PyTuple::new(py, vec![py_string.as_any(), py_indices.as_any(), &py_rate]) + }; + + let out = PyList::empty(py); + for view in inner.iter() { + out.append(to_py_tuple(view)?)?; + } + Ok(out.unbind()) + } + + /// Apply a transpiler layout to this Pauli Lindblad map. + /// + /// This enables remapping of qubit indices, e.g. if the map is defined in terms of virtual + /// qubit labels. + /// + /// Args: + /// layout (TranspileLayout | list[int] | None): The layout to apply. Most uses of this + /// function should pass the :attr:`.QuantumCircuit.layout` field from a circuit that + /// was transpiled for hardware. In addition, you can pass a list of new qubit indices. + /// If given as explicitly ``None``, no remapping is applied (but you can still use + /// ``num_qubits`` to expand the map). + /// num_qubits (int | None): The number of qubits to expand the map to. If not + /// supplied, the output will be as wide as the given :class:`.TranspileLayout`, or the + /// same width as the input if the ``layout`` is given in another form. + /// + /// Returns: + /// A new :class:`PauliLindbladMap` with the provided layout applied. + #[pyo3(signature = (/, layout, num_qubits=None))] + fn apply_layout(&self, layout: Bound, num_qubits: Option) -> PyResult { + let py = layout.py(); + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + // A utility to check the number of qubits is compatible with the map. + let check_inferred_qubits = |inferred: u32| -> PyResult { + if inferred < inner.num_qubits() { + return Err(CoherenceError::NotEnoughQubits { + current: inner.num_qubits() as usize, + target: inferred as usize, + } + .into()); + } + Ok(inferred) + }; + + // Normalize the number of qubits in the layout and the layout itself, depending on the + // input types, before calling PauliLindbladMap.apply_layout to do the actual work. + let (num_qubits, layout): (u32, Option>) = if layout.is_none() { + (num_qubits.unwrap_or(inner.num_qubits()), None) + } else if layout.is_instance( + &py.import(intern!(py, "qiskit.transpiler"))? + .getattr(intern!(py, "TranspileLayout"))?, + )? { + ( + check_inferred_qubits( + layout.getattr(intern!(py, "_output_qubit_list"))?.len()? as u32 + )?, + Some( + layout + .call_method0(intern!(py, "final_index_layout"))? + .extract::>()?, + ), + ) + } else { + ( + check_inferred_qubits(num_qubits.unwrap_or(inner.num_qubits()))?, + Some(layout.extract()?), + ) + }; + + let out = inner.apply_layout(layout.as_deref(), num_qubits)?; + Ok(out.into()) + } + + /// Return a new :class:`PauliLindbladMap` with rates scaled by `scale_factor`. + /// + /// Args: + /// scale_factor (float): the scaling coefficient. + #[pyo3(signature = (scale_factor))] + fn scale_rates<'py>( + &self, + py: Python<'py>, + scale_factor: f64, + ) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let scaled = inner.clone().scale_rates(scale_factor); + scaled.into_pyobject(py) + } + + /// Return a new :class:`PauliLindbladMap` that is the mathematical inverse of `self`. + #[pyo3(signature = ())] + fn inverse<'py>(&self, py: Python<'py>) -> PyResult> { + self.scale_rates(py, -1.) + } + + /// Drop Paulis out of this Pauli Lindblad map. + /// + /// Drop every Pauli on the given `indices`, effectively replacing them with an identity. + /// + /// The resulting map may contain duplicates, which can be removed using the :meth:`PauliLindbladMap.simplify` + /// method. + /// + /// Args: + /// indices (Sequence[int]): The indices for which Paulis must be dropped. + /// + /// Returns: + /// A new Pauli Lindblad map where every Pauli on the given `indices` has been dropped. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> pauli_map_in = PauliLindbladMap.from_list([("XXIZI", 2.0), ("IIIYZ", 0.5), ("ZIIXY", -0.25)]) + /// >>> pauli_map_out = pauli_map_in.keep_paulis([1, 2, 4]) + /// >>> assert pauli_map_out == PauliLindbladMap.from_list([("IXIII", 2.0), ("IIIIZ", 0.5), ("IIIIY", -0.25)]) + #[pyo3(signature = (/, indices))] + pub fn drop_paulis(&self, indices: Vec) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + let max_index = match indices.iter().max() { + Some(&index) => index, + None => 0, + }; + if max_index >= inner.num_qubits() { + let num_qubits = inner.num_qubits(); + return Err(PyValueError::new_err(format!( + "cannot drop Paulis for index {max_index} in a {num_qubits}-qubit PauliLindbladMap" + ))); + } + + Ok(inner.drop_paulis(indices.into_iter().collect())?.into()) + } + + /// Keep every Pauli on the given `indices` and drop all others. + /// + /// This is equivalent to using :meth:`PauliLindbladMap.drop_paulis` on the complement set of indices. + /// + /// Args: + /// indices (Sequence[int]): The indices for which Paulis must be kept. + /// + /// Returns: + /// A new Pauli Lindblad map where every Pauli on the given `indices` has been kept and all other + /// Paulis have been dropped. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> pauli_map_in = PauliLindbladMap.from_list([("XXIZI", 2.0), ("IIIYZ", 0.5), ("ZIIXY", -0.25)]) + /// >>> pauli_map_out = pauli_map_in.keep_paulis([0, 3]) + /// >>> assert pauli_map_out == PauliLindbladMap.from_list([("IXIII", 2.0), ("IIIIZ", 0.5), ("IIIIY", -0.25)]) + #[pyo3(signature = (/, indices))] + pub fn keep_paulis(&self, indices: Vec) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + let max_index = match indices.iter().max() { + Some(&index) => index, + None => 0, + }; + if max_index >= inner.num_qubits() { + let num_qubits = inner.num_qubits(); + return Err(PyValueError::new_err(format!( + "cannot keep Paulis for index {max_index} in a {num_qubits}-qubit PauliLindbladMap" + ))); + } + + Ok(inner + .drop_paulis( + (0..self.num_qubits()?) + .filter(|index| !indices.contains(index)) + .collect(), + )? + .into()) + } + + /// Drop qubits out of this Pauli Lindblad map, effectively performing a trace-out operation. + /// + /// The resulting map may contain duplicates, which can be removed using the :meth:`PauliLindbladMap.simplify` + /// method. + /// + /// Args: + /// indices (Sequence[int]): The indices of the qubits to trace out. + /// + /// Returns: + /// A new Pauli Lindblad map where every Pauli on the given `indices` has been traced out. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> pauli_map_in = PauliLindbladMap.from_list([("XXIZI", 2.0), ("IIIYZ", 0.5), ("ZIIXY", -0.25)]) + /// >>> pauli_map_out = pauli_map_in.drop_qubits([1, 2, 4]) + /// >>> assert pauli_map_out == PauliLindbladMap.from_list([("XI", 2.0), ("IZ", 0.5), ("IY", -0.25)]) + #[pyo3(signature = (/, indices))] + pub fn drop_qubits(&self, indices: Vec) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + let max_index = match indices.iter().max() { + Some(&index) => index, + None => 0, + }; + if max_index >= inner.num_qubits() { + let num_qubits = inner.num_qubits(); + return Err(PyValueError::new_err(format!( + "cannot drop qubits for index {max_index} in a {num_qubits}-qubit PauliLindbladMap" + ))); + } + + if (indices.len() as u32) == inner.num_qubits() { + return Err(PyValueError::new_err( + "cannot drop every qubit in the given PauliLindbladMap", + )); + } + + Ok(inner.drop_qubits(indices.into_iter().collect())?.into()) + } + + /// Keep every qubit on the given `indices` and trace out all other qubits. + /// + /// This is equivalent to using :meth:`PauliLindbladMap.drop_qubits` on the complement set of indices. + /// + /// Args: + /// indices (Sequence[int]): The indices for which qubits must be kept. + /// + /// Returns: + /// A new Pauli Lindblad map where every qubit on the given `indices` has been kept and all other + /// qubits have been traced out. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> pauli_map_in = PauliLindbladMap.from_list([("XXIZI", 2.0), ("IIIYZ", 0.5), ("ZIIXY", -0.25)]) + /// >>> pauli_map_out = pauli_map_in.keep_qubits([0, 3]) + /// >>> assert pauli_map_out == PauliLindbladMap.from_list([("XI", 2.0), ("IZ", 0.5), ("IY", -0.25)]) + #[pyo3(signature = (/, indices))] + pub fn keep_qubits(&self, indices: Vec) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + let max_index = match indices.iter().max() { + Some(&index) => index, + None => 0, + }; + if max_index >= inner.num_qubits() { + let num_qubits = inner.num_qubits(); + return Err(PyValueError::new_err(format!( + "cannot keep qubits for index {max_index} in a {num_qubits}-qubit PauliLindbladMap" + ))); + } + + if indices.is_empty() { + return Err(PyValueError::new_err( + "cannot drop every qubit in the given PauliLindbladMap", + )); + } + + Ok(inner + .drop_qubits( + (0..self.num_qubits()?) + .filter(|index| !indices.contains(index)) + .collect(), + )? + .into()) + } + + /// Sample sign and Pauli operator pairs from the map. Note that the boolean sign convention in + /// this method is non-standard. The preferred method for this kind of sampling is + /// :meth:`.PauliLindbladMap.parity_sample`, which is also more featureful. + /// + /// Each sign is represented by a boolean, with ``True`` representing ``+1``, and ``False`` + /// representing ``-1``. + /// + /// Given the quasi-probability representation given in the class-level documentation, each + /// sample is drawn via the following process: + /// + /// * Initialize the sign boolean, and a :class:`~.QubitSparsePauli` instance to the identity + /// operator. + /// + /// * Iterate through each Pauli in the map. Using the pseudo-probability associated with + /// each operator, randomly choose between applying the operator or not. + /// + /// * If the operator is applied, update the :class`QubitSparsePauli` by multiplying it with + /// the Pauli. If the rate associated with the Pauli is negative, flip the sign boolean. + /// + /// The results are returned as a 1d array of booleans, and the corresponding sampled qubit + /// sparse Paulis in the form of a :class:`~.QubitSparsePauliList`. + /// + /// Args: + /// num_samples (int): Number of samples to draw. + /// seed (int): Random seed. + /// + /// Returns: + /// signs, qubit_sparse_pauli_list: The boolean array of signs and the list of qubit sparse + /// paulis. + #[pyo3(signature = (num_samples, seed=None))] + pub fn signed_sample<'py>( + &self, + py: Python<'py>, + num_samples: u64, + seed: Option, + ) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let (signs, paulis, _, _) = + py.detach(|| inner.parity_sample_with_history(num_samples, seed, None, None)); + + let signs = PyArray1::from_vec(py, signs.iter().map(|b| !b).collect()); + let paulis = paulis.into_pyobject(py).unwrap(); + + (signs, paulis).into_pyobject(py) + } + + /// Sample sign and Pauli operator pairs from the map. + /// + /// Each sign is represented by a boolean, with ``True`` representing ``-1``, and ``False`` + /// representing ``+1``. + /// + /// Given the quasi-probability representation given in the class-level documentation, each + /// sample is drawn via the following process: + /// + /// * Initialize the sign boolean, and a :class:`~.QubitSparsePauli` instance to the identity + /// operator. + /// + /// * Iterate through each Pauli in the map. Using the pseudo-probability associated with + /// each operator, randomly choose between applying the operator or not. + /// + /// * If the operator is applied, update the :class`QubitSparsePauli` by multiplying it with + /// the Pauli. If the rate associated with the Pauli is negative, flip the sign boolean. + /// + /// The results are returned as a 1d array of booleans, and the corresponding sampled qubit + /// sparse Paulis in the form of a :class:`~.QubitSparsePauliList`. + /// + /// The arguments ``scale`` and ``local_scale`` can be used to change the underlying rates used + /// in the sampling process without modifying current instance or requiring creating a new one. + /// The ``scale`` argument scales all rates by a fixed float, and ``local_scale`` scales rates + /// on a term-by-term basis. + /// + /// Args: + /// num_samples (int): Number of samples to draw. + /// seed (int): Random seed. + /// scale (float): Scale to apply to all rates. + /// local_scale (list[float]): Local scale to apply on a term-by-term basis. + /// + /// Returns: + /// signs, qubit_sparse_pauli_list: The boolean array of signs and the list of qubit sparse + /// paulis. + #[pyo3(signature = (num_samples, seed=None, scale=None, local_scale=None))] + pub fn parity_sample<'py>( + &self, + py: Python<'py>, + num_samples: u64, + seed: Option, + scale: Option, + local_scale: Option>, + ) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let (signs, paulis, _, _) = + py.detach(|| inner.parity_sample_with_history(num_samples, seed, scale, local_scale)); + + let signs = PyArray1::from_vec(py, signs); + let paulis = paulis.into_pyobject(py).unwrap(); + + (signs, paulis).into_pyobject(py) + } + + /// Sample sign and Pauli operator pairs from the map, preserving the sampled generators. + /// + /// This method is identical to :meth:`parity_sample` except for also returning the information + /// which :meth:`generators` were actually sampled to yield the final Pauli operator and sign. + /// + /// + /// Args: + /// num_samples (int): Number of samples to draw. + /// seed (int): Random seed. + /// scale (float): Scale to apply to all rates. + /// local_scale (list[float]): Local scale to apply on a term-by-term basis. + /// + /// Returns: + /// signs, qubit_sparse_pauli_list, pauli_history, signs_history: The boolean array of + /// signs, the list of qubit sparse paulis, the two dimensional boolean array + /// indicating which :meth:`generators` were sampled and the two dimensional boolean + /// array indicating their signs. + #[pyo3(signature = (num_samples, seed=None, scale=None, local_scale=None))] + pub fn parity_sample_with_history<'py>( + &self, + py: Python<'py>, + num_samples: u64, + seed: Option, + scale: Option, + local_scale: Option>, + ) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let (signs, paulis, pauli_history, signs_history) = + py.detach(|| inner.parity_sample_with_history(num_samples, seed, scale, local_scale)); + + let signs = PyArray1::from_vec(py, signs); + let paulis = paulis.into_pyobject(py).unwrap(); + let pauli_history = PyArray2::from_vec2(py, &pauli_history).unwrap(); + let signs_history = PyArray2::from_vec2(py, &signs_history).unwrap(); + + (signs, paulis, pauli_history, signs_history).into_pyobject(py) + } + + /// For :class:`.PauliLindbladMap` instances with purely non-negative rates, sample Pauli + /// operators from the map. If the map has negative rates, use + /// :meth:`.PauliLindbladMap.parity_sample`. + /// + /// Given the quasi-probability representation given in the class-level documentation, each + /// sample is drawn via the following process: + /// + /// * Initialize a :class`~.QubitSparsePauli` instance to the identity operator. + /// + /// * Iterate through each Pauli in the map. Using the pseudo-probability associated with + /// each operator, randomly choose between applying the operator or not. + /// + /// * If the operator is applied, update the :class`QubitSparsePauli` by multiplying it with + /// the Pauli. + /// + /// The sampled qubit sparse Paulis are returned in the form of a + /// :class:`~.QubitSparsePauliList`. + /// + /// Args: + /// num_samples (int): Number of samples to draw. + /// seed (int): Random seed. Defaults to ``None``. + /// + /// Returns: + /// qubit_sparse_pauli_list: The list of qubit sparse paulis. + /// + /// Raises: + /// ValueError: If any of the rates in the map are negative. + #[pyo3(signature = (num_samples, seed=None))] + pub fn sample<'py>( + &self, + py: Python<'py>, + num_samples: u64, + seed: Option, + ) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + for non_negative in inner.non_negative_rates.iter() { + if !non_negative { + return Err(PyValueError::new_err( + "PauliLindbladMap.sample called for a map with negative rates. Use PauliLindbladMap.parity_sample", + )); + } + } + + let (_, paulis, _, _) = + py.detach(|| inner.parity_sample_with_history(num_samples, seed, None, None)); + + paulis.into_pyobject(py) + } + + /// Sum any like terms in the generator, removing them if the resulting rate has an absolute + /// value within tolerance of zero. This also removes terms whose Pauli operator is proportional + /// to the identity, as the corresponding generator is actually the zero map. + /// + /// As a side effect, this sorts the generators into a fixed canonical order. + /// + /// .. note:: + /// + /// When using this for equality comparisons, note that floating-point rounding and the + /// non-associativity of floating-point addition may cause non-zero coefficients of summed + /// terms to compare unequal. To compare two observables up to a tolerance, it is safest to + /// compare the canonicalized difference of the two observables to zero. + /// + /// Args: + /// tol (float): after summing like terms, any rates whose absolute value is less + /// than the given absolute tolerance will be suppressed from the output. + /// + /// Examples: + /// + /// Using :meth:`simplify` to compare two operators that represent the same map, but + /// would compare unequal due to the structural tests by default:: + /// + /// >>> base = PauliLindbladMap.from_sparse_list([ + /// ... ("XZ", (2, 1), 1e-10), # value too small + /// ... ("XX", (3, 1), 2), + /// ... ("XX", (3, 1), 2), # can be combined with the above + /// ... ("ZZ", (3, 1), 0.5), # out of order compared to `expected` + /// ... ], num_qubits=5) + /// >>> expected = PauliLindbladMap.from_list([("IZIZI", 0.5), ("IXIXI", 4)]) + /// >>> assert base != expected # non-canonical comparison + /// >>> assert base.simplify() == expected.simplify() + /// + /// Note that in the above example, the coefficients are chosen such that all floating-point + /// calculations are exact, and there are no intermediate rounding or associativity + /// concerns. If this cannot be guaranteed to be the case, the safer form is:: + /// + /// >>> left = PauliLindbladMap.from_list([("XYZ", 1.0/3.0)] * 3) # sums to 1.0 + /// >>> right = PauliLindbladMap.from_list([("XYZ", 1.0/7.0)] * 7) # doesn't sum to 1.0 + /// >>> assert left.simplify() != right.simplify() + /// >>> assert left.compose(right.inverse()).simplify() == PauliLindbladMap.identity(left.num_qubits) + #[pyo3( + signature = (/, tol=1e-8), + )] + fn simplify(&self, tol: f64) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let simplified = inner.simplify(tol); + Ok(simplified.into()) + } + + fn __len__(&self) -> PyResult { + self.num_terms() + } + + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + ( + py.get_type::().getattr("from_sparse_list")?, + (self.to_sparse_list(py)?, inner.num_qubits()), + ) + .into_pyobject(py) + } + + /// Compose with another :class:`PauliLindbladMap`. + /// + /// This appends the internal arrays of self and other, and therefore results in a map with + /// whose enumerated terms are those of self followed by those of other. + /// + /// Args: + /// other (PauliLindbladMap): the Pauli Lindblad map to compose with. + fn compose<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { + let py = other.py(); + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let Some(other) = coerce_to_map(other)? else { + return Err(PyTypeError::new_err(format!( + "unknown type for compose: {}", + other.get_type().repr()? + ))); + }; + let other = other.borrow(); + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + let composed = inner.compose(&other_inner)?; + composed.into_pyobject(py) + } + + /// Compute the Pauli fidelity of this map for a qubit sparse Pauli. + /// + /// For a Pauli :math:`Q`, the fidelity with respect to the Pauli Lindblad map + /// :math:`\Lambda` is the real number :math:`f(Q)` for which :math:`\Lambda(Q) = f(Q) Q`. I.e. + /// every Pauli is an eigenvector of the linear map :math:`\Lambda`, and the fidelity is the + /// corresponding eigenvalue. For a Pauli Lindblad map with generator set :math:`K` and rate + /// function :math:`\lambda : K \rightarrow \mathbb{R}`, the pauli fidelity mathematically is + /// + /// .. math:: + /// + /// f(Q) = \exp\left(-2 \sum_{P \in K} \lambda(P) \langle P, Q\rangle_{sp}\right), + /// + /// where :math:`\langle P, Q\rangle_{sp}` is :math:`0` if :math:`P` and :math:`Q` commute, and + /// :math:`1` if they anti-commute. + /// + /// Args: qubit_sparse_pauli (QubitSparsePauli): the qubit sparse Pauli to compute the Pauli + /// fidelity of. + fn pauli_fidelity(&self, qubit_sparse_pauli: &PyQubitSparsePauli) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let result = inner.pauli_fidelity(qubit_sparse_pauli.inner())?; + Ok(result) + } + + fn __matmul__<'py>( + &self, + other: &Bound<'py, PyAny>, + ) -> PyResult> { + self.compose(other) + } + + fn __getitem__<'py>( + &self, + py: Python<'py>, + index: PySequenceIndex<'py>, + ) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let indices = match index.with_len(inner.num_terms())? { + SequenceIndex::Int(index) => { + return PyGeneratorTerm { + inner: inner.term(index).to_term(), + } + .into_bound_py_any(py); + } + indices => indices, + }; + let mut out = PauliLindbladMap::identity(inner.num_qubits()); + for index in indices.iter() { + out.add_term(inner.term(index))?; + } + out.into_bound_py_any(py) + } + + fn __eq__(slf: Bound, other: Bound) -> PyResult { + // this is also important to check before trying to read both slf and other + if slf.is(&other) { + return Ok(true); + } + let Ok(other) = other.cast_into::() else { + return Ok(false); + }; + let slf_borrowed = slf.borrow(); + let other_borrowed = other.borrow(); + let slf_inner = slf_borrowed.inner.read().map_err(|_| InnerReadError)?; + let other_inner = other_borrowed.inner.read().map_err(|_| InnerReadError)?; + Ok(slf_inner.eq(&other_inner)) + } + + // The documentation for this is inlined into the class-level documentation of + // `PauliLindbladMap`. + #[allow(non_snake_case)] + #[classattr] + fn GeneratorTerm(py: Python) -> Bound { + py.get_type::() + } + + fn __repr__(&self) -> PyResult { + let num_terms = self.num_terms()?; + let num_qubits = self.num_qubits()?; + + let str_num_terms = format!( + "{} term{}", + num_terms, + if num_terms == 1 { "" } else { "s" } + ); + let str_num_qubits = format!( + "{} qubit{}", + num_qubits, + if num_qubits == 1 { "" } else { "s" } + ); + + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let str_terms = if num_terms == 0 { + "0.0".to_owned() + } else { + inner + .iter() + .map(GeneratorTermView::to_sparse_str) + .collect::>() + .join(" + ") + }; + Ok(format!( + "" + )) + } +} + +impl From for PyPauliLindbladMap { + fn from(val: PauliLindbladMap) -> PyPauliLindbladMap { + PyPauliLindbladMap { + inner: Arc::new(RwLock::new(val)), + } + } +} + +impl<'py> IntoPyObject<'py> for PauliLindbladMap { + type Target = PyPauliLindbladMap; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> PyResult { + PyPauliLindbladMap::from(self).into_pyobject(py) + } +} + +/// Attempt to coerce an arbitrary Python object to a [PyPauliLindbladMap]. +/// +/// This returns: +/// +/// * `Ok(Some(obs))` if the coercion was completely successful. +/// * `Ok(None)` if the input value was just completely the wrong type and no coercion could be +/// attempted. +/// * `Err` if the input was a valid type for coercion, but the coercion failed with a Python +/// exception. +/// +/// The purpose of this is for conversion the arithmetic operations, which should return +/// [PyNotImplemented] if the type is not valid for coercion. +fn coerce_to_map<'py>( + value: &Bound<'py, PyAny>, +) -> PyResult>> { + let py = value.py(); + if let Ok(obs) = value.cast_exact::() { + return Ok(Some(obs.clone())); + } + match PyPauliLindbladMap::py_new(value, None) { + Ok(obs) => Ok(Some(Bound::new(py, obs)?)), + Err(e) => { + if e.is_instance_of::(py) { + Ok(None) + } else { + Err(e) + } + } + } +} diff --git a/crates/quantum_info/src/python/pauli_lindblad_map/phased_qubit_sparse_pauli.rs b/crates/quantum_info/src/python/pauli_lindblad_map/phased_qubit_sparse_pauli.rs new file mode 100644 index 000000000000..1df4cb077069 --- /dev/null +++ b/crates/quantum_info/src/python/pauli_lindblad_map/phased_qubit_sparse_pauli.rs @@ -0,0 +1,1261 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use numpy::{PyArray1, PyArrayMethods, PyReadonlyArray1}; +use pyo3::{ + IntoPyObjectExt, PyErr, + exceptions::{PyTypeError, PyValueError}, + intern, + prelude::*, + types::{PyInt, PyList, PyString, PyTuple, PyType}, +}; + +use std::{ + collections::btree_map, + sync::{Arc, RwLock}, +}; + +use qiskit_util::py::{PySequenceIndex, SequenceIndex}; + +use crate::pauli_lindblad_map::qubit_sparse_pauli::{ + CoherenceError, LabelError, Pauli, QubitSparsePauli, QubitSparsePauliList, +}; + +use crate::pauli_lindblad_map::phased_qubit_sparse_pauli::{ + PhasedQubitSparsePauli, PhasedQubitSparsePauliList, PhasedQubitSparsePauliView, +}; +use crate::pauli_lindblad_map::qubit_sparse_pauli::{ + InnerReadError, InnerWriteError, raw_parts_from_sparse_list, +}; + +use super::PyQubitSparsePauli; + +use crate::python::imports; + +/// A Pauli operator stored in a qubit-sparse format. +/// +/// Representation +/// ============== +/// +/// A Pauli operator is a tensor product of single-qubit Pauli operators of the form :math:`P = +/// (-i)^m \bigotimes_n A^{(n)}_i`, for :math:`A^{(n)}_i \in \{I, X, Y, Z\}` and an integer +/// :math:`m` called the phase exponent. The internal representation of a +/// :class:`PhasedQubitSparsePauli` stores only the non-identity single-qubit Pauli operators. +/// +/// Internally, each single-qubit Pauli operator is stored with a numeric value. See the +/// documentation of :class:`QubitSparsePauli` for a description of the formatting of the numeric +/// value associated with each Pauli, as well as descriptions of the :attr:`paulis` and +/// :attr:`indices` attributes that store each Pauli and its associated qubit index. +/// +/// Additionally, the phase of the operator can be retrieved through the :attr:`phase` attribute, +/// which returns the group phase exponent, matching the behaviour of the same attribute in +/// :class:`.Pauli`. +/// +/// Construction +/// ============ +/// +/// :class:`PhasedQubitSparsePauli` defines several constructors. The default constructor will +/// attempt to delegate to one of the more specific constructors, based on the type of the input. +/// You can always use the specific constructors to have more control over the construction. +/// +/// .. _phased-qubit-sparse-pauli-convert-constructors: +/// .. table:: Construction from other objects +/// +/// ============================ ================================================================ +/// Method Summary +/// ============================ ================================================================ +/// :meth:`from_label` Convert a dense string label into a +/// :class:`~.PhasedQubitSparsePauli`. +/// +/// :meth:`from_sparse_label` Build a :class:`.PhasedQubitSparsePauli` from a tuple of a +/// phase, a sparse string label, and the qubits they apply to. +/// +/// :meth:`from_pauli` Raise a single :class:`~.quantum_info.Pauli` into a +/// :class:`.PhasedQubitSparsePauli`. +/// +/// :meth:`from_raw_parts` Build the list from :ref:`the raw data arrays +/// ` and the phase. +/// ============================ ================================================================ +/// +/// .. py:function:: PhasedQubitSparsePauli.__new__(data, /, num_qubits=None) +/// +/// The default constructor of :class:`QubitSparsePauli`. +/// +/// This delegates to one of :ref:`the explicit conversion-constructor methods +/// `, based on the type of the ``data`` +/// argument. If ``num_qubits`` is supplied and constructor implied by the type of ``data`` does +/// not accept a number, the given integer must match the input. +/// +/// :param data: The data type of the input. This can be another :class:`QubitSparsePauli`, +/// in which case the input is copied, or it can be a valid format for either +/// :meth:`from_label` or :meth:`from_sparse_label`. +/// :param int|None num_qubits: Optional number of qubits for the operator. For most data +/// inputs, this can be inferred and need not be passed. It is only necessary for the +/// sparse-label format. If given unnecessarily, it must match the data input. +#[pyclass( + name = "PhasedQubitSparsePauli", + frozen, + module = "qiskit.quantum_info", + skip_from_py_object +)] +#[derive(Clone, Debug)] +pub struct PyPhasedQubitSparsePauli { + inner: PhasedQubitSparsePauli, +} + +impl PyPhasedQubitSparsePauli { + pub fn inner(&self) -> &PhasedQubitSparsePauli { + &self.inner + } +} + +#[pymethods] +impl PyPhasedQubitSparsePauli { + #[new] + #[pyo3(signature = (data, /, num_qubits=None))] + fn py_new(data: &Bound, num_qubits: Option) -> PyResult { + let py = data.py(); + let check_num_qubits = |data: &Bound| -> PyResult<()> { + let Some(num_qubits) = num_qubits else { + return Ok(()); + }; + let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::()?; + if num_qubits == other_qubits { + return Ok(()); + } + Err(PyValueError::new_err(format!( + "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})" + ))) + }; + if data.is_instance(imports::PAULI_TYPE.get_bound(py))? { + check_num_qubits(data)?; + return Self::from_pauli(data); + } + if let Ok(label) = data.extract::() { + let num_qubits = num_qubits.unwrap_or(label.len() as u32); + if num_qubits as usize != label.len() { + return Err(PyValueError::new_err(format!( + "explicitly given 'num_qubits' ({}) does not match label ({})", + num_qubits, + label.len(), + ))); + } + return Self::from_label(&label); + } + if let Ok(sparse_label) = data.extract() { + let Some(num_qubits) = num_qubits else { + return Err(PyValueError::new_err( + "if using the sparse-label form, 'num_qubits' must be provided", + )); + }; + return Self::from_sparse_label(sparse_label, num_qubits); + } + Err(PyTypeError::new_err(format!( + "unknown input format for 'PhasedQubitSparsePauli': {}", + data.get_type().repr()?, + ))) + } + + /// Construct a :class:`.PhasedQubitSparsePauli` from raw Numpy arrays that match the + /// required data representation described in the class-level documentation. + /// + /// The data from each array is copied into fresh, growable Rust-space allocations. + /// + /// Args: + /// num_qubits: number of qubits the operator acts on. + /// paulis: list of the single-qubit terms. This should be a Numpy array with dtype + /// :attr:`~numpy.uint8` (which is compatible with :class:`.Pauli`). + /// indices: sorted list of the qubits each single-qubit term corresponds to. This should + /// be a Numpy array with dtype :attr:`~numpy.uint32`. + /// phase: The phase exponent of the operator. + /// + /// Examples: + /// + /// Construct a :math:`Z` operator acting on qubit 50 of 100 qubits. + /// + /// >>> num_qubits = 100 + /// >>> terms = np.array([PhasedQubitSparsePauli.Pauli.Z], dtype=np.uint8) + /// >>> indices = np.array([50], dtype=np.uint32) + /// >>> phase = 0 + /// >>> PhasedQubitSparsePauli.from_raw_parts(num_qubits, terms, indices, phase) + /// + #[staticmethod] + #[pyo3(signature = (/, num_qubits, paulis, indices, phase=0))] + fn from_raw_parts( + num_qubits: u32, + paulis: Vec, + indices: Vec, + phase: isize, + ) -> PyResult { + if paulis.len() != indices.len() { + return Err(CoherenceError::MismatchedItemCount { + paulis: paulis.len(), + indices: indices.len(), + } + .into()); + } + let mut order = (0..paulis.len()).collect::>(); + order.sort_unstable_by_key(|a| indices[*a]); + let paulis = order.iter().map(|i| paulis[*i]).collect(); + let mut sorted_indices = Vec::::with_capacity(order.len()); + for i in order { + let index = indices[i]; + if sorted_indices + .last() + .map(|prev| *prev >= index) + .unwrap_or(false) + { + return Err(CoherenceError::UnsortedIndices.into()); + } + sorted_indices.push(index) + } + let qubit_sparse_pauli = + QubitSparsePauli::new(num_qubits, paulis, sorted_indices.into_boxed_slice())?; + let num_ys = qubit_sparse_pauli.view().num_ys(); + let inner = PhasedQubitSparsePauli::new(qubit_sparse_pauli, (phase + num_ys).rem_euclid(4)); + Ok(PyPhasedQubitSparsePauli { inner }) + } + + /// Construct a :class:`.PhasedQubitSparsePauli` from a single :class:`~.quantum_info.Pauli` + /// instance. + /// + /// + /// Args: + /// pauli (:class:`~.quantum_info.Pauli`): the single Pauli to convert. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> label = "iIYXZI" + /// >>> pauli = Pauli(label) + /// >>> PhasedQubitSparsePauli.from_pauli(pauli) + /// + /// >>> assert PhasedQubitSparsePauli.from_label(label) == PhasedQubitSparsePauli.from_pauli(pauli) + #[staticmethod] + #[pyo3(signature = (pauli, /))] + fn from_pauli(pauli: &Bound) -> PyResult { + let py = pauli.py(); + let num_qubits = pauli.getattr(intern!(py, "num_qubits"))?.extract::()?; + let z = pauli + .getattr(intern!(py, "z"))? + .extract::>()?; + let x = pauli + .getattr(intern!(py, "x"))? + .extract::>()?; + let mut paulis = Vec::new(); + let mut indices = Vec::new(); + for (i, (x, z)) in x.as_array().iter().zip(z.as_array().iter()).enumerate() { + // The only failure case possible here is the identity, because of how we're + // constructing the value to convert. + let Ok(term) = ::bytemuck::checked::try_cast(((*x as u8) << 1) | (*z as u8)) else { + continue; + }; + indices.push(i as u32); + paulis.push(term); + } + let group_phase = pauli + // `Pauli`'s `_phase` is a Numpy array ... + .getattr(intern!(py, "_phase"))? + // ... that should have exactly 1 element ... + .call_method0(intern!(py, "item"))? + // ... which is some integral type. + .extract::()?; + + let inner = PhasedQubitSparsePauli::new( + QubitSparsePauli::new( + num_qubits, + paulis.into_boxed_slice(), + indices.into_boxed_slice(), + )?, + group_phase, + ); + Ok(inner.into()) + } + + /// Construct a phased qubit sparse Pauli from a sparse label, given as a tuple of an int for + /// the phase exponent, a string of Paulis, and the indices of the corresponding qubits. + /// + /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`. + /// + /// Args: + /// sparse_label (tuple[int, str, Sequence[int]]): labels and the qubits each single-qubit + /// term applies to. + /// + /// num_qubits (int): the number of qubits the operator acts on. + /// + /// Examples: + /// + /// Construct a simple Pauli:: + /// + /// >>> PhasedQubitSparsePauli.from_sparse_label( + /// ... (0, "ZX", (1, 4)), + /// ... num_qubits=5, + /// ... ) + /// + /// + /// This method can replicate the behavior of :meth:`from_label`, if the qubit-arguments + /// field of the tuple is set to decreasing integers:: + /// + /// >>> label = "XYXZ" + /// >>> from_label = PhasedQubitSparsePauli.from_label(label) + /// >>> from_sparse_label = PhasedQubitSparsePauli.from_sparse_label( + /// ... (0, label, (3, 2, 1, 0)), + /// ... num_qubits=4 + /// ... ) + /// >>> assert from_label == from_sparse_label + #[staticmethod] + #[pyo3(signature = (/, sparse_label, num_qubits))] + fn from_sparse_label( + sparse_label: (isize, String, Vec), + num_qubits: u32, + ) -> PyResult { + let phase = sparse_label.0; + let label = sparse_label.1; + let indices = sparse_label.2; + let mut paulis = Vec::new(); + let mut sorted_indices = Vec::new(); + + let label: &[u8] = label.as_ref(); + let mut sorted = btree_map::BTreeMap::new(); + if label.len() != indices.len() { + return Err(LabelError::WrongLengthIndices { + label: label.len(), + indices: indices.len(), + } + .into()); + } + for (letter, index) in label.iter().zip(indices) { + if index >= num_qubits { + return Err(LabelError::BadIndex { index, num_qubits }.into()); + } + let btree_map::Entry::Vacant(entry) = sorted.entry(index) else { + return Err(LabelError::DuplicateIndex { index }.into()); + }; + entry.insert(Pauli::try_from_u8(*letter).map_err(|_| LabelError::OutsideAlphabet)?); + } + for (index, term) in sorted.iter() { + let Some(term) = term else { + continue; + }; + sorted_indices.push(*index); + paulis.push(*term); + } + + let num_ys = isize::try_from(paulis.iter().filter(|&p| *p == Pauli::Y).count())?; + + let qubit_sparse_pauli = QubitSparsePauli::new( + num_qubits, + paulis.into_boxed_slice(), + sorted_indices.into_boxed_slice(), + )?; + let inner = PhasedQubitSparsePauli::new(qubit_sparse_pauli, phase + num_ys); + Ok(inner.into()) + } + + /// Construct from a dense string label. + /// + /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted + /// analogously to a bitstring. In other words, the right-most letter is associated with qubit + /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and + /// :class:`.SparsePauliOp`. + /// + /// Args: + /// label (str): the dense label. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> QubitSparsePauli.from_label("IIIIXZI") + /// + /// >>> label = "IYXZI" + /// >>> pauli = Pauli(label) + /// >>> assert QubitSparsePauli.from_label(label) == QubitSparsePauli.from_pauli(pauli) + #[staticmethod] + #[pyo3(signature = (label, /))] + fn from_label(label: &str) -> PyResult { + let qubit_sparse_pauli: QubitSparsePauli = QubitSparsePauli::from_dense_label(label)?; + let num_ys = qubit_sparse_pauli.view().num_ys(); + let inner = PhasedQubitSparsePauli { + qubit_sparse_pauli, + phase: num_ys.rem_euclid(4), + }; + Ok(inner.into()) + } + + /// Get the identity operator for a given number of qubits. + /// + /// Examples: + /// + /// Get the identity on 100 qubits:: + /// + /// >>> QubitSparsePauli.identity(100) + /// + #[pyo3(signature = (/, num_qubits))] + #[staticmethod] + pub fn identity(num_qubits: u32) -> Self { + PhasedQubitSparsePauli::identity(num_qubits).into() + } + + /// Read-only view onto the phase. + #[getter] + fn get_phase(slf_: Bound) -> isize { + let borrowed = slf_.borrow(); + let phase = borrowed.inner.phase; + + let num_ys = borrowed.inner.qubit_sparse_pauli.view().num_ys(); + (phase - num_ys).rem_euclid(4) + } + + /// Convert this Pauli into a single element :class:`PhasedQubitSparsePauliList`. + fn to_phased_qubit_sparse_pauli_list(&self) -> PyResult { + Ok(self.inner.to_phased_qubit_sparse_pauli_list().into()) + } + + /// Composition with another :class:`PhasedQubitSparsePauli`. + /// + /// Args: + /// other (PhasedQubitSparsePauli): the qubit sparse Pauli to compose with. + fn compose(&self, other: &PyPhasedQubitSparsePauli) -> PyResult { + Ok(PyPhasedQubitSparsePauli { + inner: self.inner.compose(&other.inner)?, + }) + } + + fn __matmul__(&self, other: &PyPhasedQubitSparsePauli) -> PyResult { + self.compose(other) + } + + /// Check if `self`` commutes with another phased qubit sparse pauli. + /// + /// Args: + /// other (PhasedQubitSparsePauli): the phased qubit sparse Pauli to check for commutation + /// with. + fn commutes(&self, other: &PyPhasedQubitSparsePauli) -> PyResult { + Ok(self.inner.commutes(&other.inner)?) + } + + fn __eq__(slf: Bound, other: Bound) -> PyResult { + if slf.is(&other) { + return Ok(true); + } + let Ok(other) = other.cast_into::() else { + return Ok(false); + }; + let slf = slf.borrow(); + let other = other.borrow(); + Ok(slf.inner.eq(&other.inner)) + } + + fn __repr__(&self) -> PyResult { + Ok(format!( + "<{} on {} qubit{}: {}>", + "PhasedQubitSparsePauli", + self.inner.num_qubits(), + if self.inner.num_qubits() == 1 { + "" + } else { + "s" + }, + self.inner.view().to_sparse_str(), + )) + } + + fn __getnewargs__(slf_: Bound) -> PyResult> { + let py = slf_.py(); + let borrowed = slf_.borrow(); + ( + borrowed.inner.num_qubits(), + Self::get_paulis(slf_.clone()), + Self::get_indices(slf_.clone()), + Self::get_phase(slf_), + ) + .into_pyobject(py) + } + + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult> { + let paulis: &[u8] = ::bytemuck::cast_slice(self.inner.qubit_sparse_pauli.paulis()); + ( + py.get_type::().getattr("from_raw_parts")?, + ( + self.inner.num_qubits(), + PyArray1::from_slice(py, paulis), + PyArray1::from_slice(py, self.inner.qubit_sparse_pauli.indices()), + (self.inner.phase - self.inner.qubit_sparse_pauli.view().num_ys()).rem_euclid(4), + ), + ) + .into_pyobject(py) + } + + /// Return a :class:`~.quantum_info.Pauli` representing the same Pauli. + fn to_pauli<'py>(&self, py: Python<'py>) -> PyResult> { + let pauli = imports::PAULI_TYPE + .get_bound(py) + .call1((self.inner.qubit_sparse_pauli.to_dense_label(),))?; + pauli.setattr( + "phase", + self.inner.phase - self.inner.qubit_sparse_pauli.view().num_ys(), + )?; + Ok(pauli) + } + + /// Get a copy of this term. + fn copy(&self) -> Self { + self.clone() + } + + /// Read-only view onto the individual single-qubit terms. + /// + /// The only valid values in the array are those with a corresponding + /// :class:`~PhasedQubitSparsePauli.Pauli`. + #[getter] + fn get_paulis(slf_: Bound) -> Bound> { + let borrowed = slf_.borrow(); + let paulis = borrowed.inner.qubit_sparse_pauli.paulis(); + let arr = ::ndarray::aview1(::bytemuck::cast_slice::<_, u8>(paulis)); + // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. + // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the + // `Box<[Pauli]>` allocation (including dropping or reallocating it) other than the entire + // object getting dropped, which Python will keep safe. + let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; + out.readwrite().make_nonwriteable(); + out + } + + /// The number of qubits the term is defined on. + #[getter] + fn get_num_qubits(&self) -> u32 { + self.inner.num_qubits() + } + + /// Read-only view onto the indices of each non-identity single-qubit term. + /// + /// The indices will always be in sorted order. + #[getter] + fn get_indices(slf_: Bound) -> Bound> { + let borrowed = slf_.borrow(); + let indices = borrowed.inner.qubit_sparse_pauli.indices(); + let arr = ::ndarray::aview1(indices); + // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. + // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the + // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire + // object getting dropped, which Python will keep safe. + let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; + out.readwrite().make_nonwriteable(); + out + } + + // The documentation for this is inlined into the class-level documentation of + // :class:`PhasedQubitSparsePauliList`. + #[allow(non_snake_case)] + #[classattr] + fn Pauli(py: Python) -> PyResult> { + PyQubitSparsePauli::Pauli(py) + } +} + +/// A list of Pauli operators with phases stored in a qubit-sparse format. +/// +/// Representation +/// ============== +/// +/// Each individual Pauli operator in the list is a tensor product of single-qubit Pauli operators +/// of the form :math:`P = (-i)^n\bigotimes_n A^{(n)}_i`, for :math:`A^{(n)}_i \in \{I, X, Y, Z\}`, +/// and an integer :math:`n` called the phase exponent. The +/// internal representation of a :class:`PhasedQubitSparsePauliList` stores only the non-identity +/// single-qubit Pauli operators. +/// +/// Indexing +/// -------- +/// +/// :class:`PhasedQubitSparsePauliList` behaves as `a Python sequence +/// `__ (the standard form, not the expanded +/// :class:`collections.abc.Sequence`). The elements of the list can be indexed by integers, as +/// well as iterated through. Whether through indexing or iterating, elements of the list are +/// returned as :class:`PhasedQubitSparsePauli` instances. +/// +/// Construction +/// ============ +/// +/// :class:`PhasedQubitSparsePauliList` defines several constructors. The default constructor will +/// attempt to delegate to one of the more specific constructors, based on the type of the input. +/// You can always use the specific constructors to have more control over the construction. +/// +/// .. _phased-qubit-sparse-pauli-list-convert-constructors: +/// .. table:: Construction from other objects +/// +/// ======================================= ===================================================== +/// Method Summary +/// ======================================= ===================================================== +/// :meth:`from_label` Convert a dense string label into a single-element +/// :class:`.PhasedQubitSparsePauliList`. +/// +/// :meth:`from_list` Construct from a list of dense string labels. +/// +/// :meth:`from_sparse_list` Elements given as a list of tuples of the phase +/// exponent, sparse string labels, and the qubits they +/// apply to. +/// +/// :meth:`from_pauli` Raise a single :class:`~.quantum_info.Pauli` into a +/// single-element :class:`.PhasedQubitSparsePauliList`. +/// +/// :meth:`from_phased_qubit_sparse_paulis` Construct from a list of +/// :class:`.PhasedQubitSparsePauli`\s. +/// ======================================= ===================================================== +/// +/// .. py:function:: PhasedQubitSparsePauliList.__new__(data, /, num_qubits=None) +/// +/// The default constructor of :class:`PhasedQubitSparsePauliList`. +/// +/// This delegates to one of :ref:`the explicit conversion-constructor methods +/// `, based on the type of the ``data`` +/// argument. If ``num_qubits`` is supplied and constructor implied by the type of ``data`` does +/// not accept a number, the given integer must match the input. +/// +/// :param data: The data type of the input. This can be another +/// :class:`PhasedQubitSparsePauliList`, in which case the input is copied, or it can be a +/// list in a valid format for either :meth:`from_list` or :meth:`from_sparse_list`. +/// :param int|None num_qubits: Optional number of qubits for the list. For most data +/// inputs, this can be inferred and need not be passed. It is only necessary for empty +/// lists or the sparse-list format. If given unnecessarily, it must match the data input. +/// +/// In addition to the conversion-based constructors, the method :meth:`empty` can be used to +/// construct an empty list of phased qubit-sparse Paulis acting on a given number of qubits. +/// +/// Conversions +/// =========== +/// +/// An existing :class:`PhasedQubitSparsePauliList` can be converted into other formats. +/// +/// .. table:: Conversion methods to other observable forms. +/// +/// =========================== ================================================================= +/// Method Summary +/// =========================== ================================================================= +/// :meth:`to_sparse_list` Express the observable in a sparse list format with elements +/// ``(phase, paulis, indices)``. +/// =========================== ================================================================= +#[pyclass( + name = "PhasedQubitSparsePauliList", + module = "qiskit.quantum_info", + sequence +)] +#[derive(Debug)] +pub struct PyPhasedQubitSparsePauliList { + // This class keeps a pointer to a pure Rust-SparseTerm and serves as interface from Python. + pub inner: Arc>, +} + +#[pymethods] +impl PyPhasedQubitSparsePauliList { + #[pyo3(signature = (data, /, num_qubits=None))] + #[new] + fn py_new(data: &Bound, num_qubits: Option) -> PyResult { + let py = data.py(); + let check_num_qubits = |data: &Bound| -> PyResult<()> { + let Some(num_qubits) = num_qubits else { + return Ok(()); + }; + let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::()?; + if num_qubits == other_qubits { + return Ok(()); + } + Err(PyValueError::new_err(format!( + "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})" + ))) + }; + if data.is_instance(imports::PAULI_TYPE.get_bound(py))? { + check_num_qubits(data)?; + return Self::from_pauli(data); + } + if let Ok(label) = data.extract::() { + let num_qubits = num_qubits.unwrap_or(label.len() as u32); + if num_qubits as usize != label.len() { + return Err(PyValueError::new_err(format!( + "explicitly given 'num_qubits' ({}) does not match label ({})", + num_qubits, + label.len(), + ))); + } + return Self::from_label(&label); + } + if let Ok(pauli_list) = data.cast_exact::() { + check_num_qubits(data)?; + let borrowed = pauli_list.borrow(); + let inner = borrowed.inner.read().map_err(|_| InnerReadError)?; + return Ok(inner.clone().into()); + } + // The type of `vec` is inferred from the subsequent calls to `Self::from_list` or + // `Self::from_sparse_list` to be either the two-tuple or the three-tuple form during the + // `extract`. The empty list will pass either, but it means the same to both functions. + if let Ok(vec) = data.extract() { + return Self::from_list(vec, num_qubits); + } + if let Ok(vec) = data.extract() { + let Some(num_qubits) = num_qubits else { + return Err(PyValueError::new_err( + "if using the sparse-list form, 'num_qubits' must be provided", + )); + }; + return Self::from_sparse_list(vec, num_qubits); + } + if let Ok(term) = data.cast_exact::() { + return term.borrow().to_phased_qubit_sparse_pauli_list(); + }; + if let Ok(pauli_list) = Self::from_phased_qubit_sparse_paulis(data, num_qubits) { + return Ok(pauli_list); + } + Err(PyTypeError::new_err(format!( + "unknown input format for 'PhasedQubitSparsePauliList': {}", + data.get_type().repr()?, + ))) + } + + /// Get a copy of this qubit sparse Pauli list. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> phased_qubit_sparse_pauli_list = PhasedQubitSparsePauliList.from_list(["IXZXYYZZ", "ZXIXYYZZ"]) + /// >>> assert phased_qubit_sparse_pauli_list == phased_qubit_sparse_pauli_list.copy() + /// >>> assert phased_qubit_sparse_pauli_list is not phased_qubit_sparse_pauli_list.copy() + fn copy(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.clone().into()) + } + + /// The number of qubits the operators in the list act on. + /// + /// This is not inferable from any other shape or values, since identities are not stored + /// explicitly. + #[getter] + #[inline] + pub fn num_qubits(&self) -> PyResult { + 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 { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.num_terms()) + } + + /// Get the empty list for a given number of qubits. + /// + /// The empty list contains no elements, and is the identity element for joining two + /// :class:`PhasedQubitSparsePauliList` instances. + /// + /// Examples: + /// + /// Get the empty list on 100 qubits:: + /// + /// >>> PhasedQubitSparsePauliList.empty(100) + /// + #[pyo3(signature = (/, num_qubits))] + #[staticmethod] + pub fn empty(num_qubits: u32) -> Self { + PhasedQubitSparsePauliList::empty(num_qubits).into() + } + + /// Construct a :class:`.PhasedQubitSparsePauliList` from a single :class:`~.quantum_info.Pauli` + /// instance. + /// + /// The output list will have a single term. Note that the phase is dropped. + /// + /// Args: + /// pauli (:class:`~.quantum_info.Pauli`): the single Pauli to convert. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> label = "IYXZI" + /// >>> pauli = Pauli(label) + /// >>> PhasedQubitSparsePauliList.from_pauli(pauli) + /// + /// >>> assert PhasedQubitSparsePauliList.from_label(label) == PhasedQubitSparsePauliList.from_pauli(pauli) + #[staticmethod] + #[pyo3(signature = (pauli, /))] + fn from_pauli(pauli: &Bound) -> PyResult { + let x = PyPhasedQubitSparsePauli::from_pauli(pauli)?; + x.to_phased_qubit_sparse_pauli_list() + } + + /// Construct a list with a single-term from a dense string label. + /// + /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted + /// analogously to a bitstring. In other words, the right-most letter is associated with qubit + /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and + /// :class:`.SparsePauliOp`. + /// + /// Args: + /// label (str): the dense label. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> PhasedQubitSparsePauliList.from_label("IIIIXZI") + /// + /// >>> label = "IYXZI" + /// >>> pauli = Pauli(label) + /// >>> assert PhasedQubitSparsePauliList.from_label(label) == PhasedQubitSparsePauliList.from_pauli(pauli) + /// + /// See also: + /// :meth:`from_list` + /// A generalization of this method that constructs a list from multiple labels. + #[staticmethod] + #[pyo3(signature = (label, /))] + fn from_label(label: &str) -> PyResult { + let singleton = PyPhasedQubitSparsePauli::from_label(label)?; + singleton.to_phased_qubit_sparse_pauli_list() + } + + /// Construct a phased qubit-sparse Pauli list from a list of dense labels. + /// + /// This is analogous to :meth:`.SparsePauliOp.from_list`. In this dense form, you must supply + /// all identities explicitly in each label. + /// + /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted + /// analogously to a bitstring. In other words, the right-most letter is associated with qubit + /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and + /// :class:`.SparsePauliOp`. + /// + /// Args: + /// iter (list[str]): List of dense string labels. + /// num_qubits (int | None): It is not necessary to specify this if you are sure that + /// ``iter`` is not an empty sequence, since it can be inferred from the label lengths. + /// If ``iter`` may be empty, you must specify this argument to disambiguate how many + /// qubits the operators act on. If this is given and ``iter`` is not empty, the value + /// must match the label lengths. + /// + /// Examples: + /// + /// Construct a qubit sparse Pauli list from a list of labels:: + /// + /// >>> PhasedQubitSparsePauliList.from_list([ + /// ... "IIIXX", + /// ... "IIYYI", + /// ... "IXXII", + /// ... "ZZIII", + /// ... ]) + /// + /// + /// Use ``num_qubits`` to disambiguate potentially empty inputs:: + /// + /// >>> PhasedQubitSparsePauliList.from_list([], num_qubits=10) + /// + /// + /// This method is equivalent to calls to :meth:`from_sparse_list` with the explicit + /// qubit-arguments field set to decreasing integers:: + /// + /// >>> labels = ["XYXZ", "YYZZ", "XYXZ"] + /// >>> from_list = PhasedQubitSparsePauliList.from_list(labels) + /// >>> from_sparse_list = PhasedQubitSparsePauliList.from_sparse_list([ + /// ... (label, (3, 2, 1, 0)) + /// ... for label in labels + /// ... ]) + /// >>> assert from_list == from_sparse_list + /// + /// See also: + /// :meth:`from_sparse_list` + /// Construct the list from labels without explicit identities, but with the qubits each + /// single-qubit operator term applies to listed explicitly. + #[staticmethod] + #[pyo3(signature = (iter, /, *, num_qubits=None))] + fn from_list(iter: Vec, num_qubits: Option) -> PyResult { + if iter.is_empty() && num_qubits.is_none() { + return Err(PyValueError::new_err( + "cannot construct a PhasedQubitSparsePauliList from an empty list without knowing `num_qubits`", + )); + } + let num_qubits = match num_qubits { + Some(num_qubits) => num_qubits, + None => iter[0].len() as u32, + }; + let mut inner = PhasedQubitSparsePauliList::with_capacity(num_qubits, iter.len(), 0); + for label in iter { + inner.add_dense_label(&label)?; + } + Ok(inner.into()) + } + + /// Construct a :class:`PhasedQubitSparsePauliList` out of individual + /// :class:`PhasedQubitSparsePauli` instances. + /// + /// All the terms must have the same number of qubits. If supplied, the ``num_qubits`` argument + /// must match the terms. + /// + /// Args: + /// obj (Iterable[PhasedQubitSparsePauli]): Iterable of individual terms to build the list from. + /// num_qubits (int | None): The number of qubits the elements of the list should act on. + /// This is usually inferred from the input, but can be explicitly given to handle the + /// case of an empty iterable. + /// + /// Returns: + /// The corresponding list. + #[staticmethod] + #[pyo3(signature = (obj, /, num_qubits=None))] + fn from_phased_qubit_sparse_paulis( + obj: &Bound, + num_qubits: Option, + ) -> PyResult { + let mut iter = obj.try_iter()?; + let mut inner = match num_qubits { + Some(num_qubits) => PhasedQubitSparsePauliList::empty(num_qubits), + None => { + let Some(first) = iter.next() else { + return Err(PyValueError::new_err( + "cannot construct an empty PhasedQubitSparsePauliList without knowing `num_qubits`", + )); + }; + let py_term = first?.cast::()?.borrow(); + py_term.inner.to_phased_qubit_sparse_pauli_list() + } + }; + for bound_py_term in iter { + let py_term = bound_py_term?.cast::()?.borrow(); + inner.add_phased_qubit_sparse_pauli(py_term.inner.view())?; + } + Ok(inner.into()) + } + + /// Clear all the elements from the list, making it equal to the empty list again. + /// + /// This does not change the capacity of the internal allocations, so subsequent addition or + /// subtraction operations resulting from composition may not need to reallocate. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> pauli_list = PhasedQubitSparsePauliList.from_list(["IXXXYY", "ZZYZII"]) + /// >>> pauli_list.clear() + /// >>> assert pauli_list == PhasedQubitSparsePauliList.empty(pauli_list.num_qubits) + pub fn clear(&mut self) -> PyResult<()> { + let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; + inner.clear(); + Ok(()) + } + + /// Construct a phased qubit sparse Pauli list from a list of labels and the qubits each item + /// applies to. + /// + /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`. + /// + /// The "labels" and "indices" fields of the tuples are associated by zipping them together. + /// For example, this means that a call to :meth:`from_list` can be converted to the form used + /// by this method by setting the "indices" field of each triple to ``(num_qubits-1, ..., 1, + /// 0)``. + /// + /// Args: + /// iter (list[tuple[int, str, Sequence[int]]]): tuples of phase exponents, labels, and the + /// qubits each single-qubit term applies to. + /// + /// num_qubits (int): the number of qubits the operators in the list act on. + /// + /// Examples: + /// + /// Construct a simple list:: + /// + /// >>> PhasedQubitSparsePauliList.from_sparse_list( + /// ... [(0, "ZX", (1, 4)), (1, "YY", (0, 3))], + /// ... num_qubits=5, + /// ... ) + /// + /// + /// This method can replicate the behavior of :meth:`from_list`, if the qubit-arguments + /// field of the tuple is set to decreasing integers:: + /// + /// >>> labels = ["XYXZ", "YYZZ", "XYXZ"] + /// >>> from_list = PhasedQubitSparsePauliList.from_list(labels) + /// >>> from_sparse_list = PhasedQubitSparsePauliList.from_sparse_list([ + /// ... (label, (3, 2, 1, 0)) + /// ... for label in labels + /// ... ]) + /// >>> assert from_list == from_sparse_list + /// + /// See also: + /// :meth:`to_sparse_list` + /// The reverse of this method. + #[staticmethod] + #[pyo3(signature = (iter, /, num_qubits))] + fn from_sparse_list(iter: Vec<(isize, String, Vec)>, num_qubits: u32) -> PyResult { + // separate group phases and build QubitSparsePauliList + let mut group_phases = Vec::with_capacity(iter.len()); + let mut sub_iter = Vec::with_capacity(iter.len()); + for (phase, label, indices) in iter { + group_phases.push(phase); + sub_iter.push((label, indices)); + } + + let (paulis, indices, boundaries) = raw_parts_from_sparse_list(sub_iter, num_qubits)?; + let qubit_sparse_pauli_list = + QubitSparsePauliList::new(num_qubits, paulis, indices, boundaries)?; + + // Build zx phases + let mut phases = Vec::with_capacity(qubit_sparse_pauli_list.num_terms()); + for (group_phase, qubit_sparse_pauli) in + group_phases.iter().zip(qubit_sparse_pauli_list.iter()) + { + phases.push((group_phase + qubit_sparse_pauli.num_ys()).rem_euclid(4)) + } + + let inner = PhasedQubitSparsePauliList::new(qubit_sparse_pauli_list, phases)?; + Ok(inner.into()) + } + + /// Express the list in terms of a sparse list format. + /// + /// This can be seen as counter-operation of + /// :meth:`.PhasedQubitSparsePauliList.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. + /// + /// Examples: + /// + /// >>> phased_qubit_sparse_list = PhasedQubitSparsePauliList.from_list(["IIXIZ", "IIZIX"]) + /// >>> reconstructed = PhasedQubitSparsePauliList.from_sparse_list(phased_qubit_sparse_list.to_sparse_list(), qubit_sparse_list.num_qubits) + /// + /// See also: + /// :meth:`from_sparse_list` + /// The constructor that can interpret these lists. + #[pyo3(signature = ())] + fn to_sparse_list(&self, py: Python) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + // turn a SparseView into a Python tuple of (phase, paulis, indices) + let to_py_tuple = |view: PhasedQubitSparsePauliView| { + let mut pauli_string = String::with_capacity(view.qubit_sparse_pauli_view.paulis.len()); + + for bit in view.qubit_sparse_pauli_view.paulis.iter() { + pauli_string.push_str(bit.py_label()); + } + let py_int = PyInt::new( + py, + (view.phase - view.qubit_sparse_pauli_view.num_ys()).rem_euclid(4), + ) + .unbind(); + let py_string = PyString::new(py, &pauli_string).unbind(); + let py_indices = PyList::new(py, view.qubit_sparse_pauli_view.indices.iter())?.unbind(); + + PyTuple::new( + py, + vec![py_int.as_any(), py_string.as_any(), py_indices.as_any()], + ) + }; + + let out = PyList::empty(py); + for view in inner.iter() { + out.append(to_py_tuple(view)?)?; + } + Ok(out.unbind()) + } + + /// Return a :class:`~.quantum_info.PauliList` representing the same list of Paulis. + fn to_pauli_list<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let pauli_list = imports::PAULI_LIST_TYPE + .get_bound(py) + .call1((inner.qubit_sparse_pauli_list.to_dense_label_list(),))?; + + let mut phases = Vec::with_capacity(inner.num_terms()); + for view in inner.iter() { + let ys = view.qubit_sparse_pauli_view.num_ys(); + phases.push(*view.phase - ys); + } + pauli_list.setattr("phase", phases)?; + Ok(pauli_list) + } + + /// Apply a transpiler layout to this phased qubit sparse Pauli list. + /// + /// This enables remapping of qubit indices, e.g. if the list is defined in terms of virtual + /// qubit labels. + /// + /// Args: + /// layout (TranspileLayout | list[int] | None): The layout to apply. Most uses of this + /// function should pass the :attr:`.QuantumCircuit.layout` field from a circuit that + /// was transpiled for hardware. In addition, you can pass a list of new qubit indices. + /// If given as explicitly ``None``, no remapping is applied (but you can still use + /// ``num_qubits`` to expand the qubits in the list). + /// num_qubits (int | None): The number of qubits to expand the list elements to. If not + /// supplied, the output will be as wide as the given :class:`.TranspileLayout`, or the + /// same width as the input if the ``layout`` is given in another form. + /// + /// Returns: + /// A new :class:`QubitSparsePauli` with the provided layout applied. + #[pyo3(signature = (/, layout, num_qubits=None))] + fn apply_layout(&self, layout: Bound, num_qubits: Option) -> PyResult { + let py = layout.py(); + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + // A utility to check the number of qubits is compatible with the map. + let check_inferred_qubits = |inferred: u32| -> PyResult { + if inferred < inner.num_qubits() { + return Err(CoherenceError::NotEnoughQubits { + current: inner.num_qubits() as usize, + target: inferred as usize, + } + .into()); + } + Ok(inferred) + }; + + // Normalize the number of qubits in the layout and the layout itself, depending on the + // input types, before calling PhasedQubitSparsePauliList.apply_layout to do the actual work. + let (num_qubits, layout): (u32, Option>) = if layout.is_none() { + (num_qubits.unwrap_or(inner.num_qubits()), None) + } else if layout.is_instance( + &py.import(intern!(py, "qiskit.transpiler"))? + .getattr(intern!(py, "TranspileLayout"))?, + )? { + ( + check_inferred_qubits( + layout.getattr(intern!(py, "_output_qubit_list"))?.len()? as u32 + )?, + Some( + layout + .call_method0(intern!(py, "final_index_layout"))? + .extract::>()?, + ), + ) + } else { + ( + check_inferred_qubits(num_qubits.unwrap_or(inner.num_qubits()))?, + Some(layout.extract()?), + ) + }; + + let out = inner.apply_layout(layout.as_deref(), num_qubits)?; + Ok(out.into()) + } + + fn __len__(&self) -> PyResult { + self.num_terms() + } + + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + ( + py.get_type::().getattr("from_sparse_list")?, + (self.to_sparse_list(py)?, inner.num_qubits()), + ) + .into_pyobject(py) + } + + fn __getitem__<'py>( + &self, + py: Python<'py>, + index: PySequenceIndex<'py>, + ) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let indices = match index.with_len(inner.num_terms())? { + SequenceIndex::Int(index) => { + return PyPhasedQubitSparsePauli { + inner: inner.term(index).to_term(), + } + .into_bound_py_any(py); + } + indices => indices, + }; + let mut out = PhasedQubitSparsePauliList::empty(inner.num_qubits()); + for index in indices.iter() { + out.add_phased_qubit_sparse_pauli(inner.term(index))?; + } + out.into_bound_py_any(py) + } + + fn __eq__(slf: Bound, other: Bound) -> PyResult { + // this is also important to check before trying to read both slf and other + if slf.is(&other) { + return Ok(true); + } + let Ok(other) = other.cast_into::() else { + return Ok(false); + }; + let slf_borrowed = slf.borrow(); + let other_borrowed = other.borrow(); + let slf_inner = slf_borrowed.inner.read().map_err(|_| InnerReadError)?; + let other_inner = other_borrowed.inner.read().map_err(|_| InnerReadError)?; + Ok(slf_inner.eq(&other_inner)) + } + + fn __repr__(&self) -> PyResult { + let num_terms = self.num_terms()?; + let num_qubits = self.num_qubits()?; + + let str_num_terms = format!( + "{} element{}", + num_terms, + if num_terms == 1 { "" } else { "s" } + ); + let str_num_qubits = format!( + "{} qubit{}", + num_qubits, + if num_qubits == 1 { "" } else { "s" } + ); + + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let str_terms = if num_terms == 0 { + "".to_owned() + } else { + inner + .iter() + .map(PhasedQubitSparsePauliView::to_sparse_str) + .collect::>() + .join(", ") + }; + Ok(format!( + "" + )) + } +} + +impl From for PyPhasedQubitSparsePauli { + fn from(val: PhasedQubitSparsePauli) -> PyPhasedQubitSparsePauli { + PyPhasedQubitSparsePauli { inner: val } + } +} + +impl<'py> IntoPyObject<'py> for PhasedQubitSparsePauli { + type Target = PyPhasedQubitSparsePauli; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> PyResult { + PyPhasedQubitSparsePauli::from(self).into_pyobject(py) + } +} + +impl From for PyPhasedQubitSparsePauliList { + fn from(val: PhasedQubitSparsePauliList) -> PyPhasedQubitSparsePauliList { + PyPhasedQubitSparsePauliList { + inner: Arc::new(RwLock::new(val)), + } + } +} + +impl<'py> IntoPyObject<'py> for PhasedQubitSparsePauliList { + type Target = PyPhasedQubitSparsePauliList; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> PyResult { + PyPhasedQubitSparsePauliList::from(self).into_pyobject(py) + } +} diff --git a/crates/quantum_info/src/python/pauli_lindblad_map/qubit_sparse_pauli.rs b/crates/quantum_info/src/python/pauli_lindblad_map/qubit_sparse_pauli.rs new file mode 100644 index 000000000000..9e339cc4c6c1 --- /dev/null +++ b/crates/quantum_info/src/python/pauli_lindblad_map/qubit_sparse_pauli.rs @@ -0,0 +1,1460 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2025 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use ndarray::Array2; +use numpy::{IntoPyArray, PyArray1, PyArray2, PyArrayMethods, PyReadonlyArray1}; +use pyo3::{ + IntoPyObjectExt, PyErr, + exceptions::{PyRuntimeError, PyTypeError, PyValueError}, + intern, + prelude::*, + sync::PyOnceLock, + types::{IntoPyDict, PyList, PyString, PyTuple, PyType}, +}; +use std::collections::btree_map; + +use std::{ + iter::zip, + sync::{Arc, RwLock}, +}; + +use qiskit_util::py::{PySequenceIndex, SequenceIndex}; + +use crate::pauli_lindblad_map::qubit_sparse_pauli::{ + ArithmeticError, CoherenceError, InnerReadError, InnerWriteError, LabelError, Pauli, + PauliFromU8Error, QubitSparsePauli, QubitSparsePauliList, QubitSparsePauliView, + raw_parts_from_sparse_list, +}; +use crate::python::imports; + +pub(super) static PAULI_PY_ENUM: PyOnceLock> = PyOnceLock::new(); +pub(super) static PAULI_INTO_PY: PyOnceLock<[Option>; 16]> = PyOnceLock::new(); + +impl From for PyErr { + fn from(value: InnerReadError) -> PyErr { + PyRuntimeError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: InnerWriteError) -> PyErr { + PyRuntimeError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: PauliFromU8Error) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: CoherenceError) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: LabelError) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: ArithmeticError) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + +/// The single-character string label used to represent this term in the +/// :class:`QubitSparsePauliList` alphabet. +#[pyfunction] +#[pyo3(name = "label")] +fn pauli_label(py: Python<'_>, slf: Pauli) -> &Bound<'_, PyString> { + // This doesn't use `py_label` so we can use `intern!`. + match slf { + Pauli::X => intern!(py, "X"), + Pauli::Y => intern!(py, "Y"), + Pauli::Z => intern!(py, "Z"), + } +} +/// Construct the Python-space `IntEnum` that represents the same values as the Rust-spce `Pauli`. +/// +/// We don't make `Pauli` a direct `pyclass` because we want the behaviour of `IntEnum`, which +/// specifically also makes its variants subclasses of the Python `int` type; we use a type-safe +/// enum in Rust, but from Python space we expect people to (carefully) deal with the raw ints in +/// Numpy arrays for efficiency. +/// +/// The resulting class is attached to `QubitSparsePauliList` as a class attribute, and its +/// `__qualname__` is set to reflect this. +fn make_py_pauli(py: Python) -> PyResult> { + let terms = [Pauli::X, Pauli::Y, Pauli::Z] + .into_iter() + .flat_map(|term| { + let mut out = vec![(term.py_label(), term as u8)]; + if term.py_label() != term.py_label() { + // Also ensure that the labels are created as aliases. These can't be (easily) accessed + // by attribute-getter (dot) syntax, but will work with the item-getter (square-bracket) + // syntax, or programmatically with `getattr`. + out.push((term.py_label(), term as u8)); + } + out + }) + .collect::>(); + let obj = py.import("enum")?.getattr("IntEnum")?.call( + ("Pauli", terms), + Some( + &[ + ("module", "qiskit.quantum_info"), + ("qualname", "QubitSparsePauliList.Pauli"), + ] + .into_py_dict(py)?, + ), + )?; + let label_property = py + .import("builtins")? + .getattr("property")? + .call1((wrap_pyfunction!(pauli_label, py)?,))?; + obj.setattr("label", label_property)?; + Ok(obj.cast_into::()?.unbind()) +} + +// Return the relevant value from the Python-space sister enumeration. These are Python-space +// singletons and subclasses of Python `int`. We only use this for interaction with "high level" +// Python space; the efficient Numpy-like array paths use `u8` directly so Numpy can act on it +// efficiently. +impl<'py> IntoPyObject<'py> for Pauli { + type Target = PyAny; + type Output = Bound<'py, PyAny>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> PyResult { + let terms = PAULI_INTO_PY.get_or_init(py, || { + let py_enum = PAULI_PY_ENUM + .get_or_try_init(py, || make_py_pauli(py)) + .expect("creating a simple Python enum class should be infallible") + .bind(py); + ::std::array::from_fn(|val| { + ::bytemuck::checked::try_cast(val as u8) + .ok() + .map(|term: Pauli| { + py_enum + .getattr(term.py_label()) + .expect("the created `Pauli` enum should have matching attribute names to the terms") + .unbind() + }) + }) + }); + Ok(terms[self as usize] + .as_ref() + .expect("the lookup table initializer populated a 'Some' in all valid locations") + .bind(py) + .clone()) + } +} + +impl<'a, 'py> FromPyObject<'a, 'py> for Pauli { + type Error = PyErr; + + fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { + let value = ob + .extract::() + .map_err(|_| match ob.get_type().repr() { + Ok(repr) => PyTypeError::new_err(format!("bad type for 'Pauli': {repr}")), + Err(err) => err, + })?; + let value_error = || { + PyValueError::new_err(format!( + "value {value} is not a valid letter of the single-qubit alphabet for 'Pauli'" + )) + }; + let value: u8 = value.try_into().map_err(|_| value_error())?; + value.try_into().map_err(|_| value_error()) + } +} + +/// A phase-less Pauli operator stored in a qubit-sparse format. +/// +/// Representation +/// ============== +/// +/// A Pauli operator is a tensor product of single-qubit Pauli operators of the form :math:`P = +/// \bigotimes_n A^{(n)}_i`, for :math:`A^{(n)}_i \in \{I, X, Y, Z\}`. The internal representation +/// of a :class:`QubitSparsePauli` stores only the non-identity single-qubit Pauli operators. +/// +/// Internally, each single-qubit Pauli operator is stored with a numeric value, explicitly: +/// +/// .. _qubit-sparse-pauli-alphabet: +/// .. table:: Alphabet of single-qubit Pauli operators used in :class:`QubitSparsePauliList` +/// +/// ======= ======================================= =============== =========================== +/// Label Operator Numeric value :class:`.Pauli` attribute +/// ======= ======================================= =============== =========================== +/// ``"I"`` :math:`I` (identity) Not stored. Not stored. +/// +/// ``"X"`` :math:`X` (Pauli X) ``0b10`` (2) :attr:`~.Pauli.X` +/// +/// ``"Y"`` :math:`Y` (Pauli Y) ``0b11`` (3) :attr:`~.Pauli.Y` +/// +/// ``"Z"`` :math:`Z` (Pauli Z) ``0b01`` (1) :attr:`~.Pauli.Z` +/// +/// ======= ======================================= =============== =========================== +/// +/// .. _qubit-sparse-pauli-arrays: +/// .. table:: Data arrays used to represent :class:`.QubitSparsePauli` +/// +/// ================== =========== ============================================================= +/// Attribute Length Description +/// ================== =========== ============================================================= +/// :attr:`paulis` :math:`s` Each of the non-identity single-qubit Pauli operators. These +/// correspond to the non-identity :math:`A^{(n)}_i` in the list, +/// where the entries are stored in order of increasing :math:`i` +/// first, and in order of increasing :math:`n` within each term. +/// +/// :attr:`indices` :math:`s` The corresponding qubit (:math:`n`) for each of the operators +/// in :attr:`paulis`. :class:`QubitSparsePauli` requires +/// that this list is term-wise sorted, and algorithms can rely +/// on this invariant being upheld. +/// ================== =========== ============================================================= +/// +/// The parameter :math:`s` is the total number of non-identity single-qubit terms. +/// +/// The scalar item of the :attr:`paulis` array is stored as a numeric byte. The numeric values +/// are related to the symplectic Pauli representation that :class:`.SparsePauliOp` uses, and are +/// accessible with named access by an enumeration: +/// +/// .. +/// This is documented manually here because the Python-space `Enum` is generated +/// programmatically from Rust - it'd be _more_ confusing to try and write a docstring somewhere +/// else in this source file. The use of `autoattribute` is because it pulls in the numeric +/// value. +/// +/// .. py:class:: QubitSparsePauli.Pauli +/// +/// +/// An :class:`~enum.IntEnum` that provides named access to the numerical values used to +/// represent each of the single-qubit alphabet terms enumerated in +/// :ref:`qubit-sparse-pauli-alphabet`. +/// +/// This class is attached to :class:`.QubitSparsePauli`. Access it as +/// :class:`.QubitSparsePauli.Pauli`. If this is too much typing, and you are solely +/// dealing with :class:`QubitSparsePauliList` objects and the :class:`Pauli` name is not +/// ambiguous, you might want to shorten it as:: +/// +/// >>> ops = QubitSparsePauli.Pauli +/// >>> assert ops.X is QubitSparsePauli.Pauli.X +/// +/// You can access all the values of the enumeration either with attribute access, or with +/// dictionary-like indexing by string:: +/// +/// >>> assert QubitSparsePauli.Pauli.X is QubitSparsePauli.Pauli["X"] +/// +/// The bits representing each single-qubit Pauli are the (phase-less) symplectic representation +/// of the Pauli operator. +/// +/// Values +/// ------ +/// +/// .. autoattribute:: qiskit.quantum_info::QubitSparsePauli.Pauli.X +/// +/// The Pauli :math:`X` operator. Uses the single-letter label ``"X"``. +/// +/// .. autoattribute:: qiskit.quantum_info::QubitSparsePauli.Pauli.Y +/// +/// The Pauli :math:`Y` operator. Uses the single-letter label ``"Y"``. +/// +/// .. autoattribute:: qiskit.quantum_info::QubitSparsePauli.Pauli.Z +/// +/// The Pauli :math:`Z` operator. Uses the single-letter label ``"Z"``. +/// +/// +/// Each of the array-like attributes behaves like a Python sequence. You can index and slice these +/// with standard :class:`list`-like semantics. Slicing an attribute returns a Numpy +/// :class:`~numpy.ndarray` containing a copy of the relevant data with the natural ``dtype`` of the +/// field; this lets you easily do mathematics on the results, like bitwise operations on +/// :attr:`paulis`. +/// +/// Construction +/// ============ +/// +/// :class:`QubitSparsePauli` defines several constructors. The default constructor will +/// attempt to delegate to one of the more specific constructors, based on the type of the input. +/// You can always use the specific constructors to have more control over the construction. +/// +/// .. _qubit-sparse-pauli-convert-constructors: +/// .. table:: Construction from other objects +/// +/// ============================ ================================================================ +/// Method Summary +/// ============================ ================================================================ +/// :meth:`from_label` Convert a dense string label into a :class:`~.QubitSparsePauli`. +/// +/// :meth:`from_sparse_label` Build a :class:`.QubitSparsePauli` from a tuple of a sparse +/// string label and the qubits they apply to. +/// +/// :meth:`from_pauli` Raise a single :class:`~.quantum_info.Pauli` into a +/// :class:`.QubitSparsePauli`. +/// +/// :meth:`from_raw_parts` Build the operator from :ref:`the raw data arrays +/// `. +/// ============================ ================================================================ +/// +/// .. py:function:: QubitSparsePauli.__new__(data, /, num_qubits=None) +/// +/// The default constructor of :class:`QubitSparsePauli`. +/// +/// This delegates to one of :ref:`the explicit conversion-constructor methods +/// `, based on the type of the ``data`` argument. +/// If ``num_qubits`` is supplied and constructor implied by the type of ``data`` does not +/// accept a number, the given integer must match the input. +/// +/// :param data: The data type of the input. This can be another :class:`QubitSparsePauli`, +/// in which case the input is copied, or it can be a valid format for either +/// :meth:`from_label` or :meth:`from_sparse_label`. +/// :param int|None num_qubits: Optional number of qubits for the operator. For most data +/// inputs, this can be inferred and need not be passed. It is only necessary for the +/// sparse-label format. If given unnecessarily, it must match the data input. +#[pyclass( + name = "QubitSparsePauli", + frozen, + module = "qiskit.quantum_info", + skip_from_py_object +)] +#[derive(Clone, Debug)] +pub struct PyQubitSparsePauli { + inner: QubitSparsePauli, +} + +impl PyQubitSparsePauli { + pub fn inner(&self) -> &QubitSparsePauli { + &self.inner + } +} + +#[pymethods] +impl PyQubitSparsePauli { + #[new] + #[pyo3(signature = (data, /, num_qubits=None))] + fn py_new(data: &Bound, num_qubits: Option) -> PyResult { + let py = data.py(); + let check_num_qubits = |data: &Bound| -> PyResult<()> { + let Some(num_qubits) = num_qubits else { + return Ok(()); + }; + let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::()?; + if num_qubits == other_qubits { + return Ok(()); + } + Err(PyValueError::new_err(format!( + "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})" + ))) + }; + if data.is_instance(imports::PAULI_TYPE.get_bound(py))? { + check_num_qubits(data)?; + return Self::from_pauli(data); + } + if let Ok(label) = data.extract::() { + let num_qubits = num_qubits.unwrap_or(label.len() as u32); + if num_qubits as usize != label.len() { + return Err(PyValueError::new_err(format!( + "explicitly given 'num_qubits' ({}) does not match label ({})", + num_qubits, + label.len(), + ))); + } + return Self::from_label(&label); + } + if let Ok(sparse_label) = data.extract() { + let Some(num_qubits) = num_qubits else { + return Err(PyValueError::new_err( + "if using the sparse-label form, 'num_qubits' must be provided", + )); + }; + return Self::from_sparse_label(sparse_label, num_qubits); + } + Err(PyTypeError::new_err(format!( + "unknown input format for 'QubitSparsePauli': {}", + data.get_type().repr()?, + ))) + } + + /// Construct a :class:`.QubitSparsePauli` from raw Numpy arrays that match :ref:`the required + /// data representation described in the class-level documentation + /// `. + /// + /// The data from each array is copied into fresh, growable Rust-space allocations. + /// + /// Args: + /// num_qubits: number of qubits the operator acts on. + /// paulis: list of the single-qubit terms. This should be a Numpy array with dtype + /// :attr:`~numpy.uint8` (which is compatible with :class:`.Pauli`). + /// indices: sorted list of the qubits each single-qubit term corresponds to. This should + /// be a Numpy array with dtype :attr:`~numpy.uint32`. + /// + /// Examples: + /// + /// Construct a :math:`Z` operator acting on qubit 50 of 100 qubits. + /// + /// >>> num_qubits = 100 + /// >>> terms = np.array([QubitSparsePauli.Pauli.Z], dtype=np.uint8) + /// >>> indices = np.array([50], dtype=np.uint32) + /// >>> QubitSparsePauli.from_raw_parts(num_qubits, terms, indices) + /// + #[staticmethod] + #[pyo3(signature = (/, num_qubits, paulis, indices))] + fn from_raw_parts(num_qubits: u32, paulis: Vec, indices: Vec) -> PyResult { + if paulis.len() != indices.len() { + return Err(CoherenceError::MismatchedItemCount { + paulis: paulis.len(), + indices: indices.len(), + } + .into()); + } + let mut order = (0..paulis.len()).collect::>(); + order.sort_unstable_by_key(|a| indices[*a]); + let paulis = order.iter().map(|i| paulis[*i]).collect(); + let mut sorted_indices = Vec::::with_capacity(order.len()); + for i in order { + let index = indices[i]; + if sorted_indices + .last() + .map(|prev| *prev >= index) + .unwrap_or(false) + { + return Err(CoherenceError::UnsortedIndices.into()); + } + sorted_indices.push(index) + } + let inner = QubitSparsePauli::new(num_qubits, paulis, sorted_indices.into_boxed_slice())?; + Ok(PyQubitSparsePauli { inner }) + } + + /// Construct from a dense string label. + /// + /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted + /// analogously to a bitstring. In other words, the right-most letter is associated with qubit + /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and + /// :class:`.SparsePauliOp`. + /// + /// Args: + /// label (str): the dense label. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> QubitSparsePauli.from_label("IIIIXZI") + /// + /// >>> label = "IYXZI" + /// >>> pauli = Pauli(label) + /// >>> assert QubitSparsePauli.from_label(label) == QubitSparsePauli.from_pauli(pauli) + #[staticmethod] + #[pyo3(signature = (label, /))] + fn from_label(label: &str) -> PyResult { + let inner = QubitSparsePauli::from_dense_label(label)?; + Ok(inner.into()) + } + + /// Construct a :class:`.QubitSparsePauli` from a single :class:`~.quantum_info.Pauli` instance. + /// + /// Note that the phase of the Pauli is dropped. + /// + /// Args: + /// pauli (:class:`~.quantum_info.Pauli`): the single Pauli to convert. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> label = "IYXZI" + /// >>> pauli = Pauli(label) + /// >>> QubitSparsePauli.from_pauli(pauli) + /// + /// >>> assert QubitSparsePauli.from_label(label) == QubitSparsePauli.from_pauli(pauli) + #[staticmethod] + #[pyo3(signature = (pauli, /))] + pub fn from_pauli(pauli: &Bound) -> PyResult { + let py = pauli.py(); + let num_qubits = pauli.getattr(intern!(py, "num_qubits"))?.extract::()?; + let z = pauli + .getattr(intern!(py, "z"))? + .extract::>()?; + let x = pauli + .getattr(intern!(py, "x"))? + .extract::>()?; + let mut paulis = Vec::new(); + let mut indices = Vec::new(); + for (i, (x, z)) in x.as_array().iter().zip(z.as_array().iter()).enumerate() { + // The only failure case possible here is the identity, because of how we're + // constructing the value to convert. + let Ok(term) = ::bytemuck::checked::try_cast(((*x as u8) << 1) | (*z as u8)) else { + continue; + }; + indices.push(i as u32); + paulis.push(term); + } + let inner = QubitSparsePauli::new( + num_qubits, + paulis.into_boxed_slice(), + indices.into_boxed_slice(), + )?; + Ok(inner.into()) + } + + /// Construct a qubit sparse Pauli from a sparse label, given as a tuple of a string of Paulis, + /// and the indices of the corresponding qubits. + /// + /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`. + /// + /// Args: + /// sparse_label (tuple[str, Sequence[int]]): labels and the qubits each single-qubit term + /// applies to. + /// + /// num_qubits (int): the number of qubits the operator acts on. + /// + /// Examples: + /// + /// Construct a simple Pauli:: + /// + /// >>> QubitSparsePauli.from_sparse_label( + /// ... ("ZX", (1, 4)), + /// ... num_qubits=5, + /// ... ) + /// + /// + /// This method can replicate the behavior of :meth:`from_label`, if the qubit-arguments + /// field of the tuple is set to decreasing integers:: + /// + /// >>> label = "XYXZ" + /// >>> from_label = QubitSparsePauli.from_label(label) + /// >>> from_sparse_label = QubitSparsePauli.from_sparse_label( + /// ... (label, (3, 2, 1, 0)), + /// ... num_qubits=4 + /// ... ) + /// >>> assert from_label == from_sparse_label + #[staticmethod] + #[pyo3(signature = (/, sparse_label, num_qubits))] + fn from_sparse_label(sparse_label: (String, Vec), num_qubits: u32) -> PyResult { + let label = sparse_label.0; + let indices = sparse_label.1; + let mut paulis = Vec::new(); + let mut sorted_indices = Vec::new(); + + let label: &[u8] = label.as_ref(); + let mut sorted = btree_map::BTreeMap::new(); + if label.len() != indices.len() { + return Err(LabelError::WrongLengthIndices { + label: label.len(), + indices: indices.len(), + } + .into()); + } + for (letter, index) in label.iter().zip(indices) { + if index >= num_qubits { + return Err(LabelError::BadIndex { index, num_qubits }.into()); + } + let btree_map::Entry::Vacant(entry) = sorted.entry(index) else { + return Err(LabelError::DuplicateIndex { index }.into()); + }; + entry.insert(Pauli::try_from_u8(*letter).map_err(|_| LabelError::OutsideAlphabet)?); + } + for (index, term) in sorted.iter() { + let Some(term) = term else { + continue; + }; + sorted_indices.push(*index); + paulis.push(*term); + } + + let inner = QubitSparsePauli::new( + num_qubits, + paulis.into_boxed_slice(), + sorted_indices.into_boxed_slice(), + )?; + Ok(inner.into()) + } + + /// Get the identity operator for a given number of qubits. + /// + /// Examples: + /// + /// Get the identity on 100 qubits:: + /// + /// >>> QubitSparsePauli.identity(100) + /// + #[pyo3(signature = (/, num_qubits))] + #[staticmethod] + pub fn identity(num_qubits: u32) -> Self { + QubitSparsePauli::identity(num_qubits).into() + } + + /// Convert this Pauli into a single element :class:`QubitSparsePauliList`. + fn to_qubit_sparse_pauli_list(&self) -> PyResult { + let qubit_sparse_pauli_list = QubitSparsePauliList::new( + self.inner.num_qubits(), + self.inner.paulis().to_vec(), + self.inner.indices().to_vec(), + vec![0, self.inner.paulis().len()], + )?; + Ok(qubit_sparse_pauli_list.into()) + } + + /// Phaseless composition with another :class:`QubitSparsePauli`. + /// + /// Args: + /// other (QubitSparsePauli): the qubit sparse Pauli to compose with. + fn compose(&self, other: &PyQubitSparsePauli) -> PyResult { + Ok(PyQubitSparsePauli { + inner: self.inner.compose(&other.inner)?, + }) + } + + fn __matmul__(&self, other: &PyQubitSparsePauli) -> PyResult { + self.compose(other) + } + + /// Check if `self`` commutes with another qubit sparse Pauli. + /// + /// Args: + /// other (QubitSparsePauli): the qubit sparse Pauli to check for commutation with. + fn commutes(&self, other: &PyQubitSparsePauli) -> PyResult { + Ok(self.inner.commutes(&other.inner)?) + } + + fn __eq__(slf: Bound, other: Bound) -> PyResult { + if slf.is(&other) { + return Ok(true); + } + let Ok(other) = other.cast_into::() else { + return Ok(false); + }; + let slf = slf.borrow(); + let other = other.borrow(); + Ok(slf.inner.eq(&other.inner)) + } + + fn __repr__(&self) -> PyResult { + Ok(format!( + "<{} on {} qubit{}: {}>", + "QubitSparsePauli", + self.inner.num_qubits(), + if self.inner.num_qubits() == 1 { + "" + } else { + "s" + }, + self.inner.view().to_sparse_str(), + )) + } + + fn __getnewargs__(slf_: Bound) -> PyResult> { + let py = slf_.py(); + let borrowed = slf_.borrow(); + ( + borrowed.inner.num_qubits(), + Self::get_paulis(slf_.clone()), + Self::get_indices(slf_), + ) + .into_pyobject(py) + } + + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult> { + let paulis: &[u8] = ::bytemuck::cast_slice(self.inner.paulis()); + ( + py.get_type::().getattr("from_raw_parts")?, + ( + self.inner.num_qubits(), + PyArray1::from_slice(py, paulis), + PyArray1::from_slice(py, self.inner.indices()), + ), + ) + .into_pyobject(py) + } + + /// Return a :class:`~.quantum_info.Pauli` representing the same phaseless Pauli. + fn to_pauli<'py>(&self, py: Python<'py>) -> PyResult> { + imports::PAULI_TYPE + .get_bound(py) + .call1((self.inner.to_dense_label(),)) + } + + /// Get a copy of this term. + fn copy(&self) -> Self { + self.clone() + } + + /// Read-only view onto the individual single-qubit terms. + /// + /// The only valid values in the array are those with a corresponding + /// :class:`~QubitSparsePauli.Pauli`. + #[getter] + fn get_paulis(slf_: Bound) -> Bound> { + let borrowed = slf_.borrow(); + let paulis = borrowed.inner.paulis(); + let arr = ::ndarray::aview1(::bytemuck::cast_slice::<_, u8>(paulis)); + // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. + // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the + // `Box<[Pauli]>` allocation (including dropping or reallocating it) other than the entire + // object getting dropped, which Python will keep safe. + let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; + out.readwrite().make_nonwriteable(); + out + } + + /// The number of qubits the term is defined on. + #[getter] + fn get_num_qubits(&self) -> u32 { + self.inner.num_qubits() + } + + /// Read-only view onto the indices of each non-identity single-qubit term. + /// + /// The indices will always be in sorted order. + #[getter] + fn get_indices(slf_: Bound) -> Bound> { + let borrowed = slf_.borrow(); + let indices = borrowed.inner.indices(); + let arr = ::ndarray::aview1(indices); + // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. + // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the + // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire + // object getting dropped, which Python will keep safe. + let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; + out.readwrite().make_nonwriteable(); + out + } + + // The documentation for this is inlined into the class-level documentation of + // :class:`QubitSparsePauliList`. + #[allow(non_snake_case)] + #[classattr] + pub fn Pauli(py: Python) -> PyResult> { + PAULI_PY_ENUM + .get_or_try_init(py, || make_py_pauli(py)) + .map(|obj| obj.clone_ref(py)) + } +} + +/// A list of phase-less Pauli operators stored in a qubit-sparse format. +/// +/// Representation +/// ============== +/// +/// Each individual Pauli operator in the list is a tensor product of single-qubit Pauli operators +/// of the form :math:`P = \bigotimes_n A^{(n)}_i`, for :math:`A^{(n)}_i \in \{I, X, Y, Z\}`. The +/// internal representation of a :class:`QubitSparsePauliList` stores only the non-identity +/// single-qubit Pauli operators. This makes it significantly more efficient to represent lists of +/// Pauli operators with low weights on a large number of qubits. For example, the list of +/// :math`n`-qubit operators :math:`[Z^{(0)}, \dots Z^{(n-1)}]`, where :math:`Z^{(j)}` represents +/// The :math:`Z` operator on qubit :math:`j` and identity on all others, can be stored in +/// :class:`QubitSparsePauliList` with a linear amount of memory in the number of qubits. +/// +/// Indexing +/// -------- +/// +/// :class:`QubitSparsePauliList` behaves as `a Python sequence +/// `__ (the standard form, not the expanded +/// :class:`collections.abc.Sequence`). The elements of the list can be indexed by integers, as +/// well as iterated through. Whether through indexing or iterating, elements of the list are +/// returned as :class:`QubitSparsePauli` instances. +/// +/// Construction +/// ============ +/// +/// :class:`QubitSparsePauliList` defines several constructors. The default constructor will +/// attempt to delegate to one of the more specific constructors, based on the type of the input. +/// You can always use the specific constructors to have more control over the construction. +/// +/// .. _qubit-sparse-pauli-list-convert-constructors: +/// .. table:: Construction from other objects +/// +/// ================================ ============================================================ +/// Method Summary +/// ================================ ============================================================ +/// :meth:`from_label` Convert a dense string label into a single-element +/// :class:`.QubitSparsePauliList`. +/// +/// :meth:`from_list` Construct from a list of dense string labels. +/// +/// :meth:`from_sparse_list` Elements given as a list of tuples of sparse string labels +/// and the qubits they apply to. +/// +/// :meth:`from_pauli` Raise a single :class:`~.quantum_info.Pauli` into a +/// single-element :class:`.QubitSparsePauliList`. +/// +/// :meth:`from_qubit_sparse_paulis` Construct from a list of :class:`.QubitSparsePauli`\s. +/// ================================ ============================================================ +/// +/// .. py:function:: QubitSparsePauliList.__new__(data, /, num_qubits=None) +/// +/// The default constructor of :class:`QubitSparsePauliList`. +/// +/// This delegates to one of :ref:`the explicit conversion-constructor methods +/// `, based on the type of the ``data`` argument. +/// If ``num_qubits`` is supplied and constructor implied by the type of ``data`` does not +/// accept a number, the given integer must match the input. +/// +/// :param data: The data type of the input. This can be another :class:`QubitSparsePauliList`, +/// in which case the input is copied, or it can be a list in a valid format for either +/// :meth:`from_list` or :meth:`from_sparse_list`. +/// :param int|None num_qubits: Optional number of qubits for the list. For most data +/// inputs, this can be inferred and need not be passed. It is only necessary for empty +/// lists or the sparse-list format. If given unnecessarily, it must match the data input. +/// +/// In addition to the conversion-based constructors, the method :meth:`empty` can be used to +/// construct an empty list of qubit-sparse Paulis acting on a given number of qubits. +/// +/// Conversions +/// =========== +/// +/// An existing :class:`QubitSparsePauliList` can be converted into other formats. +/// +/// .. table:: Conversion methods to other observable forms. +/// +/// =========================== ================================================================= +/// Method Summary +/// =========================== ================================================================= +/// :meth:`to_sparse_list` Express the observable in a sparse list format with elements +/// ``(paulis, indices)``. +/// =========================== ================================================================= +#[pyclass( + name = "QubitSparsePauliList", + module = "qiskit.quantum_info", + sequence +)] +#[derive(Debug)] +pub struct PyQubitSparsePauliList { + // This class keeps a pointer to a pure Rust-SparseTerm and serves as interface from Python. + pub inner: Arc>, +} + +#[pymethods] +impl PyQubitSparsePauliList { + #[pyo3(signature = (data, /, num_qubits=None))] + #[new] + fn py_new(data: &Bound, num_qubits: Option) -> PyResult { + let py = data.py(); + let check_num_qubits = |data: &Bound| -> PyResult<()> { + let Some(num_qubits) = num_qubits else { + return Ok(()); + }; + let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::()?; + if num_qubits == other_qubits { + return Ok(()); + } + Err(PyValueError::new_err(format!( + "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})" + ))) + }; + if data.is_instance(imports::PAULI_TYPE.get_bound(py))? { + check_num_qubits(data)?; + return Self::from_pauli(data); + } + if let Ok(label) = data.extract::() { + let num_qubits = num_qubits.unwrap_or(label.len() as u32); + if num_qubits as usize != label.len() { + return Err(PyValueError::new_err(format!( + "explicitly given 'num_qubits' ({}) does not match label ({})", + num_qubits, + label.len(), + ))); + } + return Self::from_label(&label).map_err(PyErr::from); + } + if let Ok(pauli_list) = data.cast_exact::() { + check_num_qubits(data)?; + let borrowed = pauli_list.borrow(); + let inner = borrowed.inner.read().map_err(|_| InnerReadError)?; + return Ok(inner.clone().into()); + } + // The type of `vec` is inferred from the subsequent calls to `Self::from_list` or + // `Self::from_sparse_list` to be either the two-tuple or the three-tuple form during the + // `extract`. The empty list will pass either, but it means the same to both functions. + if let Ok(vec) = data.extract() { + return Self::from_list(vec, num_qubits); + } + if let Ok(vec) = data.extract() { + let Some(num_qubits) = num_qubits else { + return Err(PyValueError::new_err( + "if using the sparse-list form, 'num_qubits' must be provided", + )); + }; + return Self::from_sparse_list(vec, num_qubits); + } + if let Ok(term) = data.cast_exact::() { + return term.borrow().to_qubit_sparse_pauli_list(); + }; + if let Ok(pauli_list) = Self::from_qubit_sparse_paulis(data, num_qubits) { + return Ok(pauli_list); + } + Err(PyTypeError::new_err(format!( + "unknown input format for 'QubitSparsePauliList': {}", + data.get_type().repr()?, + ))) + } + + /// Get a copy of this qubit sparse Pauli list. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> qubit_sparse_pauli_list = QubitSparsePauliList.from_list(["IXZXYYZZ", "ZXIXYYZZ"]) + /// >>> assert qubit_sparse_pauli_list == qubit_sparse_pauli_list.copy() + /// >>> assert qubit_sparse_pauli_list is not qubit_sparse_pauli_list.copy() + fn copy(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.clone().into()) + } + + /// The number of qubits the operators in the list act on. + /// + /// This is not inferable from any other shape or values, since identities are not stored + /// explicitly. + #[getter] + #[inline] + pub fn num_qubits(&self) -> PyResult { + 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 { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.num_terms()) + } + + /// Get the empty list for a given number of qubits. + /// + /// The empty list contains no elements, and is the identity element for joining two + /// :class:`QubitSparsePauliList` instances. + /// + /// Examples: + /// + /// Get the empty list on 100 qubits:: + /// + /// >>> QubitSparsePauliList.empty(100) + /// + #[pyo3(signature = (/, num_qubits))] + #[staticmethod] + pub fn empty(num_qubits: u32) -> Self { + QubitSparsePauliList::empty(num_qubits).into() + } + + /// Construct a :class:`.QubitSparsePauliList` from a single :class:`~.quantum_info.Pauli` + /// instance. + /// + /// The output list will have a single term. Note that the phase is dropped. + /// + /// Args: + /// pauli (:class:`~.quantum_info.Pauli`): the single Pauli to convert. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> label = "IYXZI" + /// >>> pauli = Pauli(label) + /// >>> QubitSparsePauliList.from_pauli(pauli) + /// + /// >>> assert QubitSparsePauliList.from_label(label) == QubitSparsePauliList.from_pauli(pauli) + #[staticmethod] + #[pyo3(signature = (pauli, /))] + fn from_pauli(pauli: &Bound) -> PyResult { + let py = pauli.py(); + let num_qubits = pauli.getattr(intern!(py, "num_qubits"))?.extract::()?; + let z = pauli + .getattr(intern!(py, "z"))? + .extract::>()?; + let x = pauli + .getattr(intern!(py, "x"))? + .extract::>()?; + let mut paulis = Vec::new(); + let mut indices = Vec::new(); + for (i, (x, z)) in x.as_array().iter().zip(z.as_array().iter()).enumerate() { + // The only failure case possible here is the identity, because of how we're + // constructing the value to convert. + let Ok(term) = ::bytemuck::checked::try_cast(((*x as u8) << 1) | (*z as u8)) else { + continue; + }; + indices.push(i as u32); + paulis.push(term); + } + let boundaries = vec![0, indices.len()]; + let inner = QubitSparsePauliList::new(num_qubits, paulis, indices, boundaries)?; + Ok(inner.into()) + } + + /// Construct a list with a single-term from a dense string label. + /// + /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted + /// analogously to a bitstring. In other words, the right-most letter is associated with qubit + /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and + /// :class:`.SparsePauliOp`. + /// + /// Args: + /// label (str): the dense label. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> QubitSparsePauliList.from_label("IIIIXZI") + /// + /// >>> label = "IYXZI" + /// >>> pauli = Pauli(label) + /// >>> assert QubitSparsePauliList.from_label(label) == QubitSparsePauliList.from_pauli(pauli) + /// + /// See also: + /// :meth:`from_list` + /// A generalization of this method that constructs a list from multiple labels. + #[staticmethod] + #[pyo3(signature = (label, /))] + fn from_label(label: &str) -> Result { + let mut inner = QubitSparsePauliList::empty(label.len() as u32); + inner.add_dense_label(label)?; + Ok(inner.into()) + } + + /// Construct a qubit-sparse Pauli list from a list of dense labels. + /// + /// This is analogous to :meth:`.SparsePauliOp.from_list`. In this dense form, you must supply + /// all identities explicitly in each label. + /// + /// The label must be a sequence of the alphabet ``'IXYZ'``. The label is interpreted + /// analogously to a bitstring. In other words, the right-most letter is associated with qubit + /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and + /// :class:`.SparsePauliOp`. + /// + /// Args: + /// iter (list[str]): List of dense string labels. + /// num_qubits (int | None): It is not necessary to specify this if you are sure that + /// ``iter`` is not an empty sequence, since it can be inferred from the label lengths. + /// If ``iter`` may be empty, you must specify this argument to disambiguate how many + /// qubits the operators act on. If this is given and ``iter`` is not empty, the value + /// must match the label lengths. + /// + /// Examples: + /// + /// Construct a qubit sparse Pauli list from a list of labels:: + /// + /// >>> QubitSparsePauliList.from_list([ + /// ... "IIIXX", + /// ... "IIYYI", + /// ... "IXXII", + /// ... "ZZIII", + /// ... ]) + /// + /// + /// Use ``num_qubits`` to disambiguate potentially empty inputs:: + /// + /// >>> QubitSparsePauliList.from_list([], num_qubits=10) + /// + /// + /// This method is equivalent to calls to :meth:`from_sparse_list` with the explicit + /// qubit-arguments field set to decreasing integers:: + /// + /// >>> labels = ["XYXZ", "YYZZ", "XYXZ"] + /// >>> from_list = QubitSparsePauliList.from_list(labels) + /// >>> from_sparse_list = QubitSparsePauliList.from_sparse_list([ + /// ... (label, (3, 2, 1, 0)) + /// ... for label in labels + /// ... ]) + /// >>> assert from_list == from_sparse_list + /// + /// See also: + /// :meth:`from_sparse_list` + /// Construct the list from labels without explicit identities, but with the qubits each + /// single-qubit operator term applies to listed explicitly. + #[staticmethod] + #[pyo3(signature = (iter, /, *, num_qubits=None))] + fn from_list(iter: Vec, num_qubits: Option) -> PyResult { + if iter.is_empty() && num_qubits.is_none() { + return Err(PyValueError::new_err( + "cannot construct a QubitSparsePauliList from an empty list without knowing `num_qubits`", + )); + } + let num_qubits = match num_qubits { + Some(num_qubits) => num_qubits, + None => iter[0].len() as u32, + }; + let mut inner = QubitSparsePauliList::with_capacity(num_qubits, iter.len(), 0); + for label in iter { + inner.add_dense_label(&label)?; + } + Ok(inner.into()) + } + + /// Construct a :class:`QubitSparsePauliList` out of individual :class:`QubitSparsePauli` + /// instances. + /// + /// All the terms must have the same number of qubits. If supplied, the ``num_qubits`` argument + /// must match the terms. + /// + /// Args: + /// obj (Iterable[QubitSparsePauli]): Iterable of individual terms to build the list from. + /// num_qubits (int | None): The number of qubits the elements of the list should act on. + /// This is usually inferred from the input, but can be explicitly given to handle the + /// case of an empty iterable. + /// + /// Returns: + /// The corresponding list. + #[staticmethod] + #[pyo3(signature = (obj, /, num_qubits=None))] + fn from_qubit_sparse_paulis(obj: &Bound, num_qubits: Option) -> PyResult { + let mut iter = obj.try_iter()?; + let mut inner = match num_qubits { + Some(num_qubits) => QubitSparsePauliList::empty(num_qubits), + None => { + let Some(first) = iter.next() else { + return Err(PyValueError::new_err( + "cannot construct an empty QubitSparsePauliList without knowing `num_qubits`", + )); + }; + let py_term = first?.cast::()?.borrow(); + py_term.inner.to_qubit_sparse_pauli_list() + } + }; + for bound_py_term in iter { + let py_term = bound_py_term?.cast::()?.borrow(); + inner.add_qubit_sparse_pauli(py_term.inner.view())?; + } + Ok(inner.into()) + } + + /// Clear all the elements from the list, making it equal to the empty list again. + /// + /// This does not change the capacity of the internal allocations, so subsequent addition or + /// subtraction operations resulting from composition may not need to reallocate. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> pauli_list = QubitSparsePauliList.from_list(["IXXXYY", "ZZYZII"]) + /// >>> pauli_list.clear() + /// >>> assert pauli_list == QubitSparsePauliList.empty(pauli_list.num_qubits) + pub fn clear(&mut self) -> PyResult<()> { + let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; + inner.clear(); + Ok(()) + } + + /// Construct a qubit sparse Pauli list from a list of labels and the qubits each item applies + /// to. + /// + /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`. + /// + /// The "labels" and "indices" fields of the tuples are associated by zipping them together. + /// For example, this means that a call to :meth:`from_list` can be converted to the form used + /// by this method by setting the "indices" field of each triple to ``(num_qubits-1, ..., 1, + /// 0)``. + /// + /// Args: + /// iter (list[tuple[str, Sequence[int]]]): tuples of labels and the qubits each + /// single-qubit term applies to. + /// + /// num_qubits (int): the number of qubits the operators in the list act on. + /// + /// Examples: + /// + /// Construct a simple list:: + /// + /// >>> QubitSparsePauliList.from_sparse_list( + /// ... [("ZX", (1, 4)), ("YY", (0, 3))], + /// ... num_qubits=5, + /// ... ) + /// + /// + /// This method can replicate the behavior of :meth:`from_list`, if the qubit-arguments + /// field of the tuple is set to decreasing integers:: + /// + /// >>> labels = ["XYXZ", "YYZZ", "XYXZ"] + /// >>> from_list = QubitSparsePauliList.from_list(labels) + /// >>> from_sparse_list = QubitSparsePauliList.from_sparse_list([ + /// ... (label, (3, 2, 1, 0)) + /// ... for label in labels + /// ... ]) + /// >>> assert from_list == from_sparse_list + /// + /// See also: + /// :meth:`to_sparse_list` + /// The reverse of this method. + #[staticmethod] + #[pyo3(signature = (iter, /, num_qubits))] + fn from_sparse_list(iter: Vec<(String, Vec)>, num_qubits: u32) -> PyResult { + let (paulis, indices, boundaries) = raw_parts_from_sparse_list(iter, num_qubits)?; + let inner = QubitSparsePauliList::new(num_qubits, paulis, indices, boundaries)?; + Ok(inner.into()) + } + + /// Express the list in terms of a sparse list format. + /// + /// This can be seen as counter-operation of :meth:`.QubitSparsePauliList.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. + /// + /// Examples: + /// + /// >>> qubit_sparse_list = QubitSparsePauliList.from_list(["IIXIZ", "IIZIX"]) + /// >>> reconstructed = QubitSparsePauliList.from_sparse_list(qubit_sparse_list.to_sparse_list(), qubit_sparse_list.num_qubits) + /// + /// See also: + /// :meth:`from_sparse_list` + /// The constructor that can interpret these lists. + #[pyo3(signature = ())] + fn to_sparse_list(&self, py: Python) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + // turn a SparseView into a Python tuple of (paulis, indices, coeff) + let to_py_tuple = |view: QubitSparsePauliView| { + let mut pauli_string = String::with_capacity(view.paulis.len()); + + for bit in view.paulis.iter() { + pauli_string.push_str(bit.py_label()); + } + let py_string = PyString::new(py, &pauli_string).unbind(); + let py_indices = PyList::new(py, view.indices.iter())?.unbind(); + + PyTuple::new(py, vec![py_string.as_any(), py_indices.as_any()]) + }; + + let out = PyList::empty(py); + for view in inner.iter() { + out.append(to_py_tuple(view)?)?; + } + Ok(out.unbind()) + } + + /// Express the list in a dense array format. + /// + /// Each entry is a u8 following the :class:`Pauli` representation, while the rows index + /// distinct Paulis and the columns distinct qubits. + /// + /// Examples: + /// + /// >>> paulis = QubitSparsePauliList.from_sparse_list( + /// ... [("ZX", (1, 4)), ("YY", (0, 3)), ("XX", (0, 1))], + /// ... num_qubits=5, + /// ... ) + /// >>> paulis.to_dense_array() + #[pyo3(signature = ())] + fn to_dense_array(&self, py: Python) -> PyResult>> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let mut out = Array2::zeros((inner.num_terms(), inner.num_qubits().try_into().unwrap())); + for (idx, paulis) in inner.iter().enumerate() { + for (p, p_idx) in zip(paulis.paulis, paulis.indices) { + out[[idx, *p_idx as usize]] = *p as u8; + } + } + Ok(out.into_pyarray(py).unbind()) + } + + /// Check if the elements of `self`` commute with another qubit sparse Pauli list. + /// + /// Args: + /// other (QubitSparsePauliList): the qubit sparse Pauli list to check for commutation with. + #[pyo3(signature = (other))] + fn commutes(&self, py: Python, other: &PyQubitSparsePauliList) -> PyResult>> { + let slf_inner = self.inner.read().map_err(|_| InnerReadError)?; + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + Ok(slf_inner.commutes(&other_inner)?.into_pyarray(py).unbind()) + } + + /// Return a :class:`~.quantum_info.PauliList` representing the same phaseless list of Paulis. + fn to_pauli_list<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + imports::PAULI_LIST_TYPE + .get_bound(py) + .call1((inner.to_dense_label_list(),)) + } + + /// Apply a transpiler layout to this qubit sparse Pauli list. + /// + /// This enables remapping of qubit indices, e.g. if the list is defined in terms of virtual + /// qubit labels. + /// + /// Args: + /// layout (TranspileLayout | list[int] | None): The layout to apply. Most uses of this + /// function should pass the :attr:`.QuantumCircuit.layout` field from a circuit that + /// was transpiled for hardware. In addition, you can pass a list of new qubit indices. + /// If given as explicitly ``None``, no remapping is applied (but you can still use + /// ``num_qubits`` to expand the qubits in the list). + /// num_qubits (int | None): The number of qubits to expand the list elements to. If not + /// supplied, the output will be as wide as the given :class:`.TranspileLayout`, or the + /// same width as the input if the ``layout`` is given in another form. + /// + /// Returns: + /// A new :class:`QubitSparsePauli` with the provided layout applied. + #[pyo3(signature = (/, layout, num_qubits=None))] + fn apply_layout(&self, layout: Bound, num_qubits: Option) -> PyResult { + let py = layout.py(); + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + // A utility to check the number of qubits is compatible with the map. + let check_inferred_qubits = |inferred: u32| -> PyResult { + if inferred < inner.num_qubits() { + return Err(CoherenceError::NotEnoughQubits { + current: inner.num_qubits() as usize, + target: inferred as usize, + } + .into()); + } + Ok(inferred) + }; + + // Normalize the number of qubits in the layout and the layout itself, depending on the + // input types, before calling QubitSparsePauliList.apply_layout to do the actual work. + let (num_qubits, layout): (u32, Option>) = if layout.is_none() { + (num_qubits.unwrap_or(inner.num_qubits()), None) + } else if layout.is_instance( + &py.import(intern!(py, "qiskit.transpiler"))? + .getattr(intern!(py, "TranspileLayout"))?, + )? { + ( + check_inferred_qubits( + layout.getattr(intern!(py, "_output_qubit_list"))?.len()? as u32 + )?, + Some( + layout + .call_method0(intern!(py, "final_index_layout"))? + .extract::>()?, + ), + ) + } else { + ( + check_inferred_qubits(num_qubits.unwrap_or(inner.num_qubits()))?, + Some(layout.extract()?), + ) + }; + + let out = inner.apply_layout(layout.as_deref(), num_qubits)?; + Ok(out.into()) + } + + fn __len__(&self) -> PyResult { + self.num_terms() + } + + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + ( + py.get_type::().getattr("from_sparse_list")?, + (self.to_sparse_list(py)?, inner.num_qubits()), + ) + .into_pyobject(py) + } + + fn __getitem__<'py>( + &self, + py: Python<'py>, + index: PySequenceIndex<'py>, + ) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let indices = match index.with_len(inner.num_terms())? { + SequenceIndex::Int(index) => { + return PyQubitSparsePauli { + inner: inner.term(index).to_term(), + } + .into_bound_py_any(py); + } + indices => indices, + }; + let mut out = QubitSparsePauliList::empty(inner.num_qubits()); + for index in indices.iter() { + out.add_qubit_sparse_pauli(inner.term(index))?; + } + out.into_bound_py_any(py) + } + + fn __eq__(slf: Bound, other: Bound) -> PyResult { + // this is also important to check before trying to read both slf and other + if slf.is(&other) { + return Ok(true); + } + let Ok(other) = other.cast_into::() else { + return Ok(false); + }; + let slf_borrowed = slf.borrow(); + let other_borrowed = other.borrow(); + let slf_inner = slf_borrowed.inner.read().map_err(|_| InnerReadError)?; + let other_inner = other_borrowed.inner.read().map_err(|_| InnerReadError)?; + Ok(slf_inner.eq(&other_inner)) + } + + fn __repr__(&self) -> PyResult { + let num_terms = self.num_terms()?; + let num_qubits = self.num_qubits()?; + + let str_num_terms = format!( + "{} element{}", + num_terms, + if num_terms == 1 { "" } else { "s" } + ); + let str_num_qubits = format!( + "{} qubit{}", + num_qubits, + if num_qubits == 1 { "" } else { "s" } + ); + + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let str_terms = if num_terms == 0 { + "".to_owned() + } else { + inner + .iter() + .map(QubitSparsePauliView::to_sparse_str) + .collect::>() + .join(", ") + }; + Ok(format!( + "" + )) + } +} + +impl From for PyQubitSparsePauli { + fn from(val: QubitSparsePauli) -> PyQubitSparsePauli { + PyQubitSparsePauli { inner: val } + } +} + +impl<'py> IntoPyObject<'py> for QubitSparsePauli { + type Target = PyQubitSparsePauli; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> PyResult { + PyQubitSparsePauli::from(self).into_pyobject(py) + } +} + +impl From for PyQubitSparsePauliList { + fn from(val: QubitSparsePauliList) -> PyQubitSparsePauliList { + PyQubitSparsePauliList { + inner: Arc::new(RwLock::new(val)), + } + } +} + +impl<'py> IntoPyObject<'py> for QubitSparsePauliList { + type Target = PyQubitSparsePauliList; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> PyResult { + PyQubitSparsePauliList::from(self).into_pyobject(py) + } +} diff --git a/crates/quantum_info/src/rayon_ext.rs b/crates/quantum_info/src/python/rayon_ext.rs similarity index 100% rename from crates/quantum_info/src/rayon_ext.rs rename to crates/quantum_info/src/python/rayon_ext.rs diff --git a/crates/quantum_info/src/python/sparse_observable.rs b/crates/quantum_info/src/python/sparse_observable.rs new file mode 100644 index 000000000000..8c0a5e4c5105 --- /dev/null +++ b/crates/quantum_info/src/python/sparse_observable.rs @@ -0,0 +1,2721 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use ndarray::Array2; +use num_complex::Complex64; +use num_traits::Zero; +use numpy::{ + PyArray1, PyArray2, PyArrayDescr, PyArrayDescrMethods, PyArrayLike1, PyArrayMethods, + PyReadonlyArray1, PyReadonlyArray2, PyUntypedArrayMethods, +}; +use pyo3::{ + IntoPyObjectExt, PyErr, + exceptions::{PyRuntimeError, PyTypeError, PyValueError, PyZeroDivisionError}, + intern, + prelude::*, + sync::PyOnceLock, + types::{IntoPyDict, PyList, PyString, PyTuple, PyType}, +}; +use qiskit_util::IndexSet; +use qiskit_util::py::{ImportOnceCell, PySequenceIndex, SequenceIndex}; +use std::collections::btree_map; +use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign}; +use std::sync::{Arc, RwLock, RwLockReadGuard}; +use thiserror::Error; + +use crate::sparse_observable::{ + ArithmeticError, BitTerm, BitTermFromU8Error, CoherenceError, InnerReadError, LabelError, + SparseObservable, SparseTerm, SparseTermView, +}; + +static PAULI_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "Pauli"); +static PAULI_LIST_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "PauliList"); +static SPARSE_PAULI_OP_TYPE: ImportOnceCell = + ImportOnceCell::new("qiskit.quantum_info", "SparsePauliOp"); +static BIT_TERM_PY_ENUM: PyOnceLock> = PyOnceLock::new(); +static BIT_TERM_INTO_PY: PyOnceLock<[Option>; 16]> = PyOnceLock::new(); + +#[derive(Error, Debug)] +struct InnerWriteError; + +impl ::std::fmt::Display for InnerWriteError { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "Failed acquiring lock for writing.") + } +} + +impl From for PyErr { + fn from(value: InnerReadError) -> PyErr { + PyRuntimeError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: InnerWriteError) -> PyErr { + PyRuntimeError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: BitTermFromU8Error) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: CoherenceError) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: LabelError) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + +impl From for PyErr { + fn from(value: ArithmeticError) -> PyErr { + PyValueError::new_err(value.to_string()) + } +} + +/// The single-character string label used to represent this term in the :class:`SparseObservable` +/// alphabet. +#[pyfunction] +#[pyo3(name = "label")] +fn bit_term_label(py: Python<'_>, slf: BitTerm) -> &Bound<'_, PyString> { + // This doesn't use `py_label` so we can use `intern!`. + match slf { + BitTerm::X => intern!(py, "X"), + BitTerm::Plus => intern!(py, "+"), + BitTerm::Minus => intern!(py, "-"), + BitTerm::Y => intern!(py, "Y"), + BitTerm::Right => intern!(py, "r"), + BitTerm::Left => intern!(py, "l"), + BitTerm::Z => intern!(py, "Z"), + BitTerm::Zero => intern!(py, "0"), + BitTerm::One => intern!(py, "1"), + } +} +/// Construct the Python-space `IntEnum` that represents the same values as the Rust-spce `BitTerm`. +/// +/// We don't make `BitTerm` a direct `pyclass` because we want the behaviour of `IntEnum`, which +/// specifically also makes its variants subclasses of the Python `int` type; we use a type-safe +/// enum in Rust, but from Python space we expect people to (carefully) deal with the raw ints in +/// Numpy arrays for efficiency. +/// +/// The resulting class is attached to `SparseObservable` as a class attribute, and its +/// `__qualname__` is set to reflect this. +fn make_py_bit_term(py: Python) -> PyResult> { + let terms = [ + BitTerm::X, + BitTerm::Plus, + BitTerm::Minus, + BitTerm::Y, + BitTerm::Right, + BitTerm::Left, + BitTerm::Z, + BitTerm::Zero, + BitTerm::One, + ] + .into_iter() + .flat_map(|term| { + let mut out = vec![(term.py_name(), term as u8)]; + if term.py_name() != term.py_label() { + // Also ensure that the labels are created as aliases. These can't be (easily) accessed + // by attribute-getter (dot) syntax, but will work with the item-getter (square-bracket) + // syntax, or programmatically with `getattr`. + out.push((term.py_label(), term as u8)); + } + out + }) + .collect::>(); + let obj = py.import("enum")?.getattr("IntEnum")?.call( + ("BitTerm", terms), + Some( + &[ + ("module", "qiskit.quantum_info"), + ("qualname", "SparseObservable.BitTerm"), + ] + .into_py_dict(py)?, + ), + )?; + let label_property = py + .import("builtins")? + .getattr("property")? + .call1((wrap_pyfunction!(bit_term_label, py)?,))?; + obj.setattr("label", label_property)?; + Ok(obj.cast_into::()?.unbind()) +} + +// Return the relevant value from the Python-space sister enumeration. These are Python-space +// singletons and subclasses of Python `int`. We only use this for interaction with "high level" +// Python space; the efficient Numpy-like array paths use `u8` directly so Numpy can act on it +// efficiently. +impl<'py> IntoPyObject<'py> for BitTerm { + type Target = PyAny; + type Output = Bound<'py, PyAny>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> PyResult { + let terms = BIT_TERM_INTO_PY.get_or_init(py, || { + let py_enum = BIT_TERM_PY_ENUM + .get_or_try_init(py, || make_py_bit_term(py)) + .expect("creating a simple Python enum class should be infallible") + .bind(py); + ::std::array::from_fn(|val| { + ::bytemuck::checked::try_cast(val as u8) + .ok() + .map(|term: BitTerm| { + py_enum + .getattr(term.py_name()) + .expect("the created `BitTerm` enum should have matching attribute names to the terms") + .unbind() + }) + }) + }); + Ok(terms[self as usize] + .as_ref() + .expect("the lookup table initializer populated a 'Some' in all valid locations") + .bind(py) + .clone()) + } +} + +impl<'a, 'py> FromPyObject<'a, 'py> for BitTerm { + type Error = PyErr; + + fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { + let value = ob + .extract::() + .map_err(|_| match ob.get_type().repr() { + Ok(repr) => PyTypeError::new_err(format!("bad type for 'BitTerm': {repr}")), + Err(err) => err, + })?; + let value_error = || { + PyValueError::new_err(format!( + "value {value} is not a valid letter of the single-qubit alphabet for 'BitTerm'" + )) + }; + let value: u8 = value.try_into().map_err(|_| value_error())?; + value.try_into().map_err(|_| value_error()) + } +} + +/// A single term from a complete :class:`SparseObservable`. +/// +/// These are typically created by indexing into or iterating through a :class:`SparseObservable`. +#[pyclass( + name = "Term", + frozen, + module = "qiskit.quantum_info", + skip_from_py_object +)] +#[derive(Clone, Debug)] +struct PySparseTerm { + inner: SparseTerm, +} + +#[pymethods] +impl PySparseTerm { + // Mark the Python class as being defined "within" the `SparseObservable` class namespace. + #[classattr] + #[pyo3(name = "__qualname__")] + fn type_qualname() -> &'static str { + "SparseObservable.Term" + } + + #[new] + #[pyo3(signature = (/, num_qubits, coeff, bit_terms, indices))] + fn py_new( + num_qubits: u32, + coeff: Complex64, + bit_terms: Vec, + indices: Vec, + ) -> PyResult { + if bit_terms.len() != indices.len() { + return Err(CoherenceError::MismatchedItemCount { + bit_terms: bit_terms.len(), + indices: indices.len(), + } + .into()); + } + let mut order = (0..bit_terms.len()).collect::>(); + order.sort_unstable_by_key(|a| indices[*a]); + let bit_terms = order.iter().map(|i| bit_terms[*i]).collect(); + let mut sorted_indices = Vec::::with_capacity(order.len()); + for i in order { + let index = indices[i]; + if sorted_indices + .last() + .map(|prev| *prev >= index) + .unwrap_or(false) + { + return Err(CoherenceError::UnsortedIndices.into()); + } + sorted_indices.push(index) + } + let inner = SparseTerm::new( + num_qubits, + coeff, + bit_terms, + sorted_indices.into_boxed_slice(), + )?; + Ok(PySparseTerm { inner }) + } + + /// Convert this term to a complete :class:`SparseObservable`. + fn to_observable(&self) -> PyResult { + let obs = SparseObservable::new( + self.inner.num_qubits(), + vec![self.inner.coeff()], + self.inner.bit_terms().to_vec(), + self.inner.indices().to_vec(), + vec![0, self.inner.bit_terms().len()], + )?; + Ok(obs.into()) + } + + fn to_label(&self) -> PyResult { + Ok(self.inner.view().to_sparse_str()) + } + + fn __eq__(slf: Bound, other: Bound) -> PyResult { + if slf.is(&other) { + return Ok(true); + } + let Ok(other) = other.cast_into::() else { + return Ok(false); + }; + let slf = slf.borrow(); + let other = other.borrow(); + Ok(slf.inner.eq(&other.inner)) + } + + fn __repr__(&self) -> PyResult { + Ok(format!( + "<{} on {} qubit{}: {}>", + Self::type_qualname(), + self.inner.num_qubits(), + if self.inner.num_qubits() == 1 { + "" + } else { + "s" + }, + self.inner.view().to_sparse_str(), + )) + } + + fn __getnewargs__(slf_: Bound) -> PyResult> { + let py = slf_.py(); + let borrowed = slf_.borrow(); + ( + borrowed.inner.num_qubits(), + borrowed.inner.coeff(), + Self::get_bit_terms(slf_.clone()), + Self::get_indices(slf_), + ) + .into_pyobject(py) + } + + /// Get a copy of this term. + fn copy(&self) -> Self { + self.clone() + } + + /// Read-only view onto the individual single-qubit terms. + /// + /// The only valid values in the array are those with a corresponding + /// :class:`~SparseObservable.BitTerm`. + #[getter] + fn get_bit_terms(slf_: Bound) -> Bound> { + let borrowed = slf_.borrow(); + let bit_terms = borrowed.inner.bit_terms(); + let arr = ::ndarray::aview1(::bytemuck::cast_slice::<_, u8>(bit_terms)); + // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. + // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the + // `Box<[BitTerm]>` allocation (including dropping or reallocating it) other than the entire + // object getting dropped, which Python will keep safe. + let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; + out.readwrite().make_nonwriteable(); + out + } + + /// The number of qubits the term is defined on. + #[getter] + fn get_num_qubits(&self) -> u32 { + self.inner.num_qubits() + } + + /// The term's coefficient. + #[getter] + fn get_coeff(&self) -> Complex64 { + self.inner.coeff() + } + + /// Read-only view onto the indices of each non-identity single-qubit term. + /// + /// The indices will always be in sorted order. + #[getter] + fn get_indices(slf_: Bound) -> Bound> { + let borrowed = slf_.borrow(); + let indices = borrowed.inner.indices(); + let arr = ::ndarray::aview1(indices); + // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. + // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the + // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire + // object getting dropped, which Python will keep safe. + let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; + out.readwrite().make_nonwriteable(); + out + } + + /// Get a :class:`~.quantum_info.Pauli` object that represents the measurement basis needed for + /// this term. + /// + /// For example, the projector ``0l+`` will return a Pauli ``ZYX``. The resulting + /// :class:`~.quantum_info.Pauli` is dense, in the sense that explicit identities are stored. + /// An identity in the Pauli output does not require a concrete measurement. + /// + /// Returns: :class:`~.quantum_info.Pauli`: the Pauli operator representing the necessary + /// measurement basis. + /// + /// See also: + /// :meth:`SparseObservable.pauli_bases` + /// A similar method for an entire observable at once. + fn pauli_base<'py>(&self, py: Python<'py>) -> PyResult> { + let mut x = vec![false; self.inner.num_qubits() as usize]; + let mut z = vec![false; self.inner.num_qubits() as usize]; + for (bit_term, index) in self + .inner + .bit_terms() + .iter() + .zip(self.inner.indices().iter()) + { + x[*index as usize] = bit_term.has_x_component(); + z[*index as usize] = bit_term.has_z_component(); + } + PAULI_TYPE + .get_bound(py) + .call1(((PyArray1::from_vec(py, z), PyArray1::from_vec(py, x)),)) + } + + /// Return the bit labels of the term as string. + /// + /// The bit labels will match the order of :attr:`.SparseTerm.indices`, such that the + /// i-th character in the string is applied to the qubit index at ``term.indices[i]``. + /// + /// Returns: + /// The non-identity bit terms as concatenated string. + fn bit_labels<'py>(&self, py: Python<'py>) -> Bound<'py, PyString> { + let string: String = self + .inner + .bit_terms() + .iter() + .map(|bit| bit.py_label()) + .collect(); + PyString::new(py, string.as_str()) + } +} + +/// An observable over Pauli bases that stores its data in a qubit-sparse format. +/// +/// Mathematics +/// =========== +/// +/// This observable represents a sum over strings of the Pauli operators and Pauli-eigenstate +/// projectors, with each term weighted by some complex number. That is, the full observable is +/// +/// .. math:: +/// +/// \text{\texttt{SparseObservable}} = \sum_i c_i \bigotimes_n A^{(n)}_i +/// +/// for complex numbers :math:`c_i` and single-qubit operators acting on qubit :math:`n` from a +/// restricted alphabet :math:`A^{(n)}_i`. The sum over :math:`i` is the sum of the individual +/// terms, and the tensor product produces the operator strings. +/// +/// The alphabet of allowed single-qubit operators that the :math:`A^{(n)}_i` are drawn from is the +/// Pauli operators and the Pauli-eigenstate projection operators. Explicitly, these are: +/// +/// .. _sparse-observable-alphabet: +/// .. table:: Alphabet of single-qubit terms used in :class:`SparseObservable` +/// +/// ======= ======================================= =============== =========================== +/// Label Operator Numeric value :class:`.BitTerm` attribute +/// ======= ======================================= =============== =========================== +/// ``"I"`` :math:`I` (identity) Not stored. Not stored. +/// +/// ``"X"`` :math:`X` (Pauli X) ``0b0010`` (2) :attr:`~.BitTerm.X` +/// +/// ``"Y"`` :math:`Y` (Pauli Y) ``0b0011`` (3) :attr:`~.BitTerm.Y` +/// +/// ``"Z"`` :math:`Z` (Pauli Z) ``0b0001`` (1) :attr:`~.BitTerm.Z` +/// +/// ``"+"`` :math:`\lvert+\rangle\langle+\rvert` ``0b1010`` (10) :attr:`~.BitTerm.PLUS` +/// (projector to positive eigenstate of X) +/// +/// ``"-"`` :math:`\lvert-\rangle\langle-\rvert` ``0b0110`` (6) :attr:`~.BitTerm.MINUS` +/// (projector to negative eigenstate of X) +/// +/// ``"r"`` :math:`\lvert r\rangle\langle r\rvert` ``0b1011`` (11) :attr:`~.BitTerm.RIGHT` +/// (projector to positive eigenstate of Y) +/// +/// ``"l"`` :math:`\lvert l\rangle\langle l\rvert` ``0b0111`` (7) :attr:`~.BitTerm.LEFT` +/// (projector to negative eigenstate of Y) +/// +/// ``"0"`` :math:`\lvert0\rangle\langle0\rvert` ``0b1001`` (9) :attr:`~.BitTerm.ZERO` +/// (projector to positive eigenstate of Z) +/// +/// ``"1"`` :math:`\lvert1\rangle\langle1\rvert` ``0b0101`` (5) :attr:`~.BitTerm.ONE` +/// (projector to negative eigenstate of Z) +/// ======= ======================================= =============== =========================== +/// +/// The allowed alphabet forms an overcomplete basis of the operator space. This means that there +/// is not a unique summation to represent a given observable. By comparison, +/// :class:`.SparsePauliOp` uses a precise basis of the operator space, so (after combining terms of +/// the same Pauli string, removing zeros, and sorting the terms to :ref:`some canonical order +/// `) there is only one representation of any operator. +/// +/// :class:`SparseObservable` uses its particular overcomplete basis with the aim of making +/// "efficiency of measurement" equivalent to "efficiency of representation". For example, the +/// observable :math:`{\lvert0\rangle\langle0\rvert}^{\otimes n}` can be efficiently measured on +/// hardware with simple :math:`Z` measurements, but can only be represented by +/// :class:`.SparsePauliOp` as :math:`{(I + Z)}^{\otimes n}/2^n`, which requires :math:`2^n` stored +/// terms. :class:`SparseObservable` requires only a single term to store this. +/// +/// The downside to this is that it is impractical to take an arbitrary matrix or +/// :class:`.SparsePauliOp` and find the *best* :class:`SparseObservable` representation. You +/// typically will want to construct a :class:`SparseObservable` directly, rather than trying to +/// decompose into one. +/// +/// +/// Representation +/// ============== +/// +/// The internal representation of a :class:`SparseObservable` stores only the non-identity qubit +/// operators. This makes it significantly more efficient to represent observables such as +/// :math:`\sum_{n\in \text{qubits}} Z^{(n)}`; :class:`SparseObservable` requires an amount of +/// memory linear in the total number of qubits, while :class:`.SparsePauliOp` scales quadratically. +/// +/// The terms are stored compressed, similar in spirit to the compressed sparse row format of sparse +/// matrices. In this analogy, the terms of the sum are the "rows", and the qubit terms are the +/// "columns", where an absent entry represents the identity rather than a zero. More explicitly, +/// the representation is made up of four contiguous arrays: +/// +/// .. _sparse-observable-arrays: +/// .. table:: Data arrays used to represent :class:`.SparseObservable` +/// +/// ================== =========== ============================================================= +/// Attribute Length Description +/// ================== =========== ============================================================= +/// :attr:`coeffs` :math:`t` The complex scalar multiplier for each term. +/// +/// :attr:`bit_terms` :math:`s` Each of the non-identity single-qubit terms for all of the +/// operators, in order. These correspond to the non-identity +/// :math:`A^{(n)}_i` in the sum description, where the entries +/// are stored in order of increasing :math:`i` first, and in +/// order of increasing :math:`n` within each term. +/// +/// :attr:`indices` :math:`s` The corresponding qubit (:math:`n`) for each of the operators +/// in :attr:`bit_terms`. :class:`SparseObservable` requires +/// that this list is term-wise sorted, and algorithms can rely +/// on this invariant being upheld. +/// +/// :attr:`boundaries` :math:`t+1` The indices that partition :attr:`bit_terms` and +/// :attr:`indices` into complete terms. For term number +/// :math:`i`, its complex coefficient is ``coeffs[i]``, and its +/// non-identity single-qubit operators and their corresponding +/// qubits are the slice ``boundaries[i] : boundaries[i+1]`` into +/// :attr:`bit_terms` and :attr:`indices` respectively. +/// :attr:`boundaries` always has an explicit 0 as its first +/// element. +/// ================== =========== ============================================================= +/// +/// The length parameter :math:`t` is the number of terms in the sum, and the parameter :math:`s` is +/// the total number of non-identity single-qubit terms. +/// +/// As illustrative examples: +/// +/// * in the case of a zero operator, :attr:`boundaries` is length 1 (a single 0) and all other +/// vectors are empty. +/// * in the case of a fully simplified identity operator, :attr:`boundaries` is ``[0, 0]``, +/// :attr:`coeffs` has a single entry, and :attr:`bit_terms` and :attr:`indices` are empty. +/// * for the operator :math:`Z_2 Z_0 - X_3 Y_1`, :attr:`boundaries` is ``[0, 2, 4]``, +/// :attr:`coeffs` is ``[1.0, -1.0]``, :attr:`bit_terms` is ``[BitTerm.Z, BitTerm.Z, BitTerm.Y, +/// BitTerm.X]`` and :attr:`indices` is ``[0, 2, 1, 3]``. The operator might act on more than +/// four qubits, depending on the :attr:`num_qubits` parameter. The :attr:`bit_terms` are integer +/// values, whose magic numbers can be accessed via the :class:`BitTerm` attribute class. Note +/// that the single-bit terms and indices are sorted into termwise sorted order. This is a +/// requirement of the class. +/// +/// These cases are not special, they're fully consistent with the rules and should not need special +/// handling. +/// +/// The scalar item of the :attr:`bit_terms` array is stored as a numeric byte. The numeric values +/// are related to the symplectic Pauli representation that :class:`.SparsePauliOp` uses, and are +/// accessible with named access by an enumeration: +/// +/// .. +/// This is documented manually here because the Python-space `Enum` is generated +/// programmatically from Rust - it'd be _more_ confusing to try and write a docstring somewhere +/// else in this source file. The use of `autoattribute` is because it pulls in the numeric +/// value. +/// +/// .. py:class:: SparseObservable.BitTerm +/// +/// An :class:`~enum.IntEnum` that provides named access to the numerical values used to +/// represent each of the single-qubit alphabet terms enumerated in +/// :ref:`sparse-observable-alphabet`. +/// +/// This class is attached to :class:`.SparseObservable`. Access it as +/// :class:`.SparseObservable.BitTerm`. If this is too much typing, and you are solely dealing +/// with :class:¬SparseObservable` objects and the :class:`BitTerm` name is not ambiguous, you +/// might want to shorten it as:: +/// +/// >>> ops = SparseObservable.BitTerm +/// >>> assert ops.X is SparseObservable.BitTerm.X +/// +/// You can access all the values of the enumeration by either their full all-capitals name, or +/// by their single-letter label. The single-letter labels are not generally valid Python +/// identifiers, so you must use indexing notation to access them:: +/// +/// >>> assert SparseObservable.BitTerm.ZERO is SparseObservable.BitTerm["0"] +/// +/// The numeric structure of these is that they are all four-bit values of which the low two +/// bits are the (phase-less) symplectic representation of the Pauli operator related to the +/// object, where the low bit denotes a contribution by :math:`Z` and the second lowest a +/// contribution by :math:`X`, while the upper two bits are ``00`` for a Pauli operator, ``01`` +/// for the negative-eigenstate projector, and ``10`` for the positive-eigenstate projector. +/// +/// Values +/// ------ +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.X +/// +/// The Pauli :math:`X` operator. Uses the single-letter label ``"X"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.PLUS +/// +/// The projector to the positive eigenstate of the :math:`X` operator: +/// :math:`\lvert+\rangle\langle+\rvert`. Uses the single-letter label ``"+"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.MINUS +/// +/// The projector to the negative eigenstate of the :math:`X` operator: +/// :math:`\lvert-\rangle\langle-\rvert`. Uses the single-letter label ``"-"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.Y +/// +/// The Pauli :math:`Y` operator. Uses the single-letter label ``"Y"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.RIGHT +/// +/// The projector to the positive eigenstate of the :math:`Y` operator: +/// :math:`\lvert r\rangle\langle r\rvert`. Uses the single-letter label ``"r"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.LEFT +/// +/// The projector to the negative eigenstate of the :math:`Y` operator: +/// :math:`\lvert l\rangle\langle l\rvert`. Uses the single-letter label ``"l"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.Z +/// +/// The Pauli :math:`Z` operator. Uses the single-letter label ``"Z"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.ZERO +/// +/// The projector to the positive eigenstate of the :math:`Z` operator: +/// :math:`\lvert0\rangle\langle0\rvert`. Uses the single-letter label ``"0"``. +/// +/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.ONE +/// +/// The projector to the negative eigenstate of the :math:`Z` operator: +/// :math:`\lvert1\rangle\langle1\rvert`. Uses the single-letter label ``"1"``. +/// +/// Attributes +/// ---------- +/// +/// .. autoproperty:: qiskit.quantum_info::SparseObservable.BitTerm.label +/// +/// Each of the array-like attributes behaves like a Python sequence. You can index and slice these +/// with standard :class:`list`-like semantics. Slicing an attribute returns a Numpy +/// :class:`~numpy.ndarray` containing a copy of the relevant data with the natural ``dtype`` of the +/// field; this lets you easily do mathematics on the results, like bitwise operations on +/// :attr:`bit_terms`. You can assign to indices or slices of each of the attributes, but beware +/// that you must uphold :ref:`the data coherence rules ` while doing +/// this. For example:: +/// +/// >>> obs = SparseObservable.from_list([("XZY", 1.5j), ("+1r", -0.5)]) +/// >>> assert isinstance(obs.coeffs[:], np.ndarray) +/// >>> # Reduce all single-qubit terms to the relevant Pauli operator, if they are a projector. +/// >>> obs.bit_terms[:] = obs.bit_terms[:] & 0b00_11 +/// >>> assert obs == SparseObservable.from_list([("XZY", 1.5j), ("XZY", -0.5)]) +/// +/// .. note:: +/// +/// The above reduction to the Pauli bases can also be achieved with :meth:`pauli_bases`. +/// +/// .. _sparse-observable-canonical-order: +/// +/// Canonical ordering +/// ------------------ +/// +/// For any given mathematical observable, there are several ways of representing it with +/// :class:`SparseObservable`. For example, the same set of single-bit terms and their +/// corresponding indices might appear multiple times in the observable. Mathematically, this is +/// equivalent to having only a single term with all the coefficients summed. Similarly, the terms +/// of the sum in a :class:`SparseObservable` can be in any order while representing the same +/// observable, since addition is commutative (although while floating-point addition is not +/// associative, :class:`SparseObservable` makes no guarantees about the summation order). +/// +/// These two categories of representation degeneracy can cause the ``==`` operator to claim that +/// two observables are not equal, despite representing the same object. In these cases, it can +/// be convenient to define some *canonical form*, which allows observables to be compared +/// structurally. +/// +/// You can put a :class:`SparseObservable` in canonical form by using the :meth:`simplify` method. +/// The precise ordering of terms in canonical ordering is not specified, and may change between +/// versions of Qiskit. Within the same version of Qiskit, however, you can compare two observables +/// structurally by comparing their simplified forms. +/// +/// .. note:: +/// +/// If you wish to account for floating-point tolerance in the comparison, it is safest to use +/// a recipe such as:: +/// +/// def equivalent(left, right, tol): +/// return (left - right).simplify(tol) == SparseObservable.zero(left.num_qubits) +/// +/// .. note:: +/// +/// The canonical form produced by :meth:`simplify` alone will not universally detect all +/// observables that are equivalent due to the over-complete basis alphabet. To obtain a +/// unique expression, you can first represent the observable using Pauli terms only by +/// calling :meth:`as_paulis`, followed by :meth:`simplify`. Note that the projector +/// expansion (e.g. ``+`` into ``I`` and ``X``) is not computationally feasible at scale. +/// +/// Indexing +/// -------- +/// +/// :class:`SparseObservable` behaves as `a Python sequence +/// `__ (the standard form, not the expanded +/// :class:`collections.abc.Sequence`). The observable can be indexed by integers, and iterated +/// through to yield individual terms. +/// +/// Each term appears as an instance a self-contained class. The individual terms are copied out of +/// the base observable; mutations to them will not affect the observable. +/// +/// .. autoclass:: qiskit.quantum_info::SparseObservable.Term +/// :members: +/// +/// Construction +/// ============ +/// +/// :class:`SparseObservable` defines several constructors. The default constructor will attempt to +/// delegate to one of the more specific constructors, based on the type of the input. You can +/// always use the specific constructors to have more control over the construction. +/// +/// .. _sparse-observable-convert-constructors: +/// .. table:: Construction from other objects +/// +/// ============================ ================================================================ +/// Method Summary +/// ============================ ================================================================ +/// :meth:`from_label` Convert a dense string label into a single-term +/// :class:`.SparseObservable`. +/// +/// :meth:`from_list` Sum a list of tuples of dense string labels and the associated +/// coefficients into an observable. +/// +/// :meth:`from_sparse_list` Sum a list of tuples of sparse string labels, the qubits they +/// apply to, and their coefficients into an observable. +/// +/// :meth:`from_pauli` Raise a single :class:`~.quantum_info.Pauli` into a single-term +/// :class:`.SparseObservable`. +/// +/// :meth:`from_sparse_pauli_op` Raise a :class:`.SparsePauliOp` into a +/// :class:`SparseObservable`. +/// +/// :meth:`from_terms` Sum explicit single :class:`Term` instances. +/// +/// :meth:`from_raw_parts` Build the observable from :ref:`the raw data arrays +/// `. +/// ============================ ================================================================ +/// +/// .. py:function:: SparseObservable.__new__(data, /, num_qubits=None) +/// +/// The default constructor of :class:`SparseObservable`. +/// +/// This delegates to one of :ref:`the explicit conversion-constructor methods +/// `, based on the type of the ``data`` argument. If +/// ``num_qubits`` is supplied and constructor implied by the type of ``data`` does not accept a +/// number, the given integer must match the input. +/// +/// :param data: The data type of the input. This can be another :class:`SparseObservable`, in +/// which case the input is copied, a :class:`~.quantum_info.Pauli` or :class:`.SparsePauliOp`, in which +/// case :meth:`from_pauli` or :meth:`from_sparse_pauli_op` are called as appropriate, or it +/// can be a list in a valid format for either :meth:`from_list` or +/// :meth:`from_sparse_list`. +/// :param int|None num_qubits: Optional number of qubits for the operator. For most data +/// inputs, this can be inferred and need not be passed. It is only necessary for empty +/// lists or the sparse-list format. If given unnecessarily, it must match the data input. +/// +/// In addition to the conversion-based constructors, there are also helper methods that construct +/// special forms of observables. +/// +/// .. table:: Construction of special observables +/// +/// ============================ ================================================================ +/// Method Summary +/// ============================ ================================================================ +/// :meth:`zero` The zero operator on a given number of qubits. +/// +/// :meth:`identity` The identity operator on a given number of qubits. +/// ============================ ================================================================ +/// +/// Conversions +/// =========== +/// +/// An existing :class:`SparseObservable` can be converted into other :mod:`~qiskit.quantum_info` +/// operators or generic formats. Beware that other objects may not be able to represent the same +/// observable as efficiently as :class:`SparseObservable`, including potentially needed +/// exponentially more memory. +/// +/// .. table:: Conversion methods to other observable forms. +/// +/// =========================== ================================================================= +/// Method Summary +/// =========================== ================================================================= +/// :meth:`as_paulis` Create a new :class:`SparseObservable`, expanding in terms +/// of Pauli operators only. +/// +/// :meth:`to_sparse_list` Express the observable in a sparse list format with elements +/// ``(bit_terms, indices, coeff)``. +/// =========================== ================================================================= +/// +/// In addition, :meth:`.SparsePauliOp.from_sparse_observable` is available for conversion from this +/// class to :class:`.SparsePauliOp`. Beware that this method suffers from the same +/// exponential-memory usage concerns as :meth:`as_paulis`. +/// +/// Mathematical manipulation +/// ========================= +/// +/// :class:`SparseObservable` supports the standard set of Python mathematical operators like other +/// :mod:`~qiskit.quantum_info` operators. +/// +/// In basic arithmetic, you can: +/// +/// * add two observables using ``+`` +/// * subtract two observables using ``-`` +/// * multiply or divide by an :class:`int`, :class:`float` or :class:`complex` using ``*`` and ``/`` +/// * negate all the coefficients in an observable with unary ``-`` +/// +/// Each of the basic binary arithmetic operators has a corresponding specialized in-place method, +/// which mutates the left-hand side in-place. Using these is typically more efficient than the +/// infix operators, especially for building an observable in a loop. +/// +/// The tensor product is calculated with :meth:`tensor` (for standard, juxtaposition ordering of +/// Pauli labels) or :meth:`expand` (for the reverse order). The ``^`` operator is overloaded to be +/// equivalent to :meth:`tensor`. +/// +/// .. note:: +/// +/// When using the binary operators ``^`` (:meth:`tensor`) and ``&`` (:meth:`compose`), beware +/// that `Python's operator-precedence rules +/// `__ may cause the +/// evaluation order to be different to your expectation. In particular, the operator ``+`` +/// binds more tightly than ``^`` or ``&``, just like ``*`` binds more tightly than ``+``. +/// +/// When using the operators in mixed expressions, it is safest to use parentheses to group the +/// operands of tensor products. +/// +/// A :class:`SparseObservable` has a well-defined :meth:`adjoint`. The notions of scalar complex +/// conjugation (:meth:`conjugate`) and real-value transposition (:meth:`transpose`) are defined +/// analogously to the matrix representation of other Pauli operators in Qiskit. +/// +/// +/// Efficiency notes +/// ---------------- +/// +/// Internally, :class:`SparseObservable` is in-place mutable, including using over-allocating +/// growable vectors for extending the number of terms. This means that the cost of appending to an +/// observable using ``+=`` is amortised linear in the total number of terms added, rather than +/// the quadratic complexity that the binary ``+`` would require. +/// +/// Additions and subtractions are implemented by a term-stacking operation; there is no automatic +/// "simplification" (summing of like terms), because the majority of additions to build up an +/// observable generate only a small number of duplications, and like-term detection has additional +/// costs. If this does not fit your use cases, you can either periodically call :meth:`simplify`, +/// or discuss further APIs with us for better building of observables. +#[pyclass(name = "SparseObservable", module = "qiskit.quantum_info", sequence)] +#[derive(Debug)] +pub struct PySparseObservable { + // This class keeps a pointer to a pure Rust-SparseTerm and serves as interface from Python. + pub inner: Arc>, +} + +#[pymethods] +impl PySparseObservable { + #[pyo3(signature = (data, /, num_qubits=None))] + #[new] + fn py_new(data: &Bound, num_qubits: Option) -> PyResult { + let py = data.py(); + let check_num_qubits = |data: &Bound| -> PyResult<()> { + let Some(num_qubits) = num_qubits else { + return Ok(()); + }; + let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::()?; + if num_qubits == other_qubits { + return Ok(()); + } + Err(PyValueError::new_err(format!( + "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})" + ))) + }; + + if data.is_instance(PAULI_TYPE.get_bound(py))? { + check_num_qubits(data)?; + return Self::from_pauli(data); + } + if data.is_instance(SPARSE_PAULI_OP_TYPE.get_bound(py))? { + check_num_qubits(data)?; + return Self::from_sparse_pauli_op(data); + } + if let Ok(label) = data.extract::() { + let num_qubits = num_qubits.unwrap_or(label.len() as u32); + if num_qubits as usize != label.len() { + return Err(PyValueError::new_err(format!( + "explicitly given 'num_qubits' ({}) does not match label ({})", + num_qubits, + label.len(), + ))); + } + return Self::from_label(&label).map_err(PyErr::from); + } + if let Ok(observable) = data.cast_exact::() { + check_num_qubits(data)?; + let borrowed = observable.borrow(); + let inner = borrowed.inner.read().map_err(|_| InnerReadError)?; + return Ok(inner.clone().into()); + } + // The type of `vec` is inferred from the subsequent calls to `Self::from_list` or + // `Self::from_sparse_list` to be either the two-tuple or the three-tuple form during the + // `extract`. The empty list will pass either, but it means the same to both functions. + if let Ok(vec) = data.extract() { + return Self::from_list(vec, num_qubits); + } + if let Ok(vec) = data.extract() { + let Some(num_qubits) = num_qubits else { + return Err(PyValueError::new_err( + "if using the sparse-list form, 'num_qubits' must be provided", + )); + }; + return Self::from_sparse_list(vec, num_qubits); + } + if let Ok(term) = data.cast_exact::() { + return term.borrow().to_observable(); + }; + if let Ok(observable) = Self::from_terms(data, num_qubits) { + return Ok(observable); + } + Err(PyTypeError::new_err(format!( + "unknown input format for 'SparseObservable': {}", + data.get_type().repr()?, + ))) + } + + /// Get a copy of this observable. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> obs = SparseObservable.from_list([("IXZ+lr01", 2.5), ("ZXI-rl10", 0.5j)]) + /// >>> assert obs == obs.copy() + /// >>> assert obs is not obs.copy() + fn copy(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.clone().into()) + } + + /// The number of qubits the operator acts on. + /// + /// This is not inferable from any other shape or values, since identities are not stored + /// explicitly. + #[getter] + #[inline] + pub fn num_qubits(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.num_qubits()) + } + + /// The number of terms in the sum this operator is tracking. + #[getter] + #[inline] + pub fn num_terms(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.num_terms()) + } + + /// The coefficients of each abstract term in in the sum. This has as many elements as terms in + /// the sum. + #[getter] + fn get_coeffs(slf_: &Bound) -> ArrayView { + let borrowed = slf_.borrow(); + ArrayView { + base: borrowed.inner.clone(), + slot: ArraySlot::Coeffs, + } + } + + /// A flat list of single-qubit terms. This is more naturally a list of lists, but is stored + /// flat for memory usage and locality reasons, with the sublists denoted by `boundaries.` + #[getter] + fn get_bit_terms(slf_: &Bound) -> ArrayView { + let borrowed = slf_.borrow(); + ArrayView { + base: borrowed.inner.clone(), + slot: ArraySlot::BitTerms, + } + } + + /// A flat list of the qubit indices that the corresponding entries in :attr:`bit_terms` act on. + /// This list must always be term-wise sorted, where a term is a sublist as denoted by + /// :attr:`boundaries`. + /// + /// .. warning:: + /// + /// If writing to this attribute from Python space, you *must* ensure that you only write in + /// indices that are term-wise sorted. + #[getter] + fn get_indices(slf_: &Bound) -> ArrayView { + let borrowed = slf_.borrow(); + ArrayView { + base: borrowed.inner.clone(), + slot: ArraySlot::Indices, + } + } + + /// Indices that partition :attr:`bit_terms` and :attr:`indices` into sublists for each + /// individual term in the sum. ``boundaries[0] : boundaries[1]`` is the range of indices into + /// :attr:`bit_terms` and :attr:`indices` that correspond to the first term of the sum. All + /// unspecified qubit indices are implicitly the identity. This is one item longer than + /// :attr:`coeffs`, since ``boundaries[0]`` is always an explicit zero (for algorithmic ease). + #[getter] + fn get_boundaries(slf_: &Bound) -> ArrayView { + let borrowed = slf_.borrow(); + ArrayView { + base: borrowed.inner.clone(), + slot: ArraySlot::Boundaries, + } + } + + /// Get the zero operator over the given number of qubits. + /// + /// The zero operator is the operator whose expectation value is zero for all quantum states. + /// It has no terms. It is the identity element for addition of two :class:`SparseObservable` + /// instances; anything added to the zero operator is equal to itself. + /// + /// If you want the projector onto the all zeros state, use:: + /// + /// >>> num_qubits = 10 + /// >>> all_zeros = SparseObservable.from_label("0" * num_qubits) + /// + /// Examples: + /// + /// Get the zero operator for 100 qubits:: + /// + /// >>> SparseObservable.zero(100) + /// + #[pyo3(signature = (/, num_qubits))] + #[staticmethod] + pub fn zero(num_qubits: u32) -> Self { + SparseObservable::zero(num_qubits).into() + } + + /// Get the identity operator over the given number of qubits. + /// + /// Examples: + /// + /// Get the identity operator for 100 qubits:: + /// + /// >>> SparseObservable.identity(100) + /// + #[pyo3(signature = (/, num_qubits))] + #[staticmethod] + pub fn identity(num_qubits: u32) -> Self { + SparseObservable::identity(num_qubits).into() + } + + /// Construct a :class:`.SparseObservable` from a single :class:`~.quantum_info.Pauli` instance. + /// + /// The output observable will have a single term, with a unitary coefficient dependent on the + /// phase. + /// + /// Args: + /// pauli (:class:`~.quantum_info.Pauli`): the single Pauli to convert. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> label = "IYXZI" + /// >>> pauli = Pauli(label) + /// >>> SparseObservable.from_pauli(pauli) + /// + /// >>> assert SparseObservable.from_label(label) == SparseObservable.from_pauli(pauli) + #[staticmethod] + #[pyo3(signature = (pauli, /))] + fn from_pauli(pauli: &Bound) -> PyResult { + let py = pauli.py(); + let num_qubits = pauli.getattr(intern!(py, "num_qubits"))?.extract::()?; + let z = pauli + .getattr(intern!(py, "z"))? + .extract::>()?; + let x = pauli + .getattr(intern!(py, "x"))? + .extract::>()?; + let mut bit_terms = Vec::new(); + let mut indices = Vec::new(); + let mut num_ys = 0; + for (i, (x, z)) in x.as_array().iter().zip(z.as_array().iter()).enumerate() { + // The only failure case possible here is the identity, because of how we're + // constructing the value to convert. + let Ok(term) = ::bytemuck::checked::try_cast(((*x as u8) << 1) | (*z as u8)) else { + continue; + }; + num_ys += (term == BitTerm::Y) as isize; + indices.push(i as u32); + bit_terms.push(term); + } + let boundaries = vec![0, indices.len()]; + // The "empty" state of a `Pauli` represents the identity, which isn't our empty state + // (that's zero), so we're always going to have a coefficient. + let group_phase = pauli + // `Pauli`'s `_phase` is a Numpy array ... + .getattr(intern!(py, "_phase"))? + // ... that should have exactly 1 element ... + .call_method0(intern!(py, "item"))? + // ... which is some integral type. + .extract::()?; + let phase = match (group_phase - num_ys).rem_euclid(4) { + 0 => Complex64::new(1.0, 0.0), + 1 => Complex64::new(0.0, -1.0), + 2 => Complex64::new(-1.0, 0.0), + 3 => Complex64::new(0.0, 1.0), + _ => unreachable!("`x % 4` has only four values"), + }; + let coeffs = vec![phase]; + let inner = SparseObservable::new(num_qubits, coeffs, bit_terms, indices, boundaries)?; + Ok(inner.into()) + } + + /// Construct a single-term observable from a dense string label. + /// + /// The resulting operator will have a coefficient of 1. The label must be a sequence of the + /// alphabet ``'IXYZ+-rl01'``. The label is interpreted analogously to a bitstring. In other + /// words, the right-most letter is associated with qubit 0, and so on. This is the same as the + /// labels for :class:`~.quantum_info.Pauli` and :class:`.SparsePauliOp`. + /// + /// Args: + /// label (str): the dense label. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> SparseObservable.from_label("IIII+ZI") + /// + /// >>> label = "IYXZI" + /// >>> pauli = Pauli(label) + /// >>> assert SparseObservable.from_label(label) == SparseObservable.from_pauli(pauli) + /// + /// See also: + /// :meth:`from_list` + /// A generalization of this method that constructs a sum operator from multiple labels + /// and their corresponding coefficients. + #[staticmethod] + #[pyo3(signature = (label, /))] + pub fn from_label(label: &str) -> Result { + let mut inner = SparseObservable::zero(label.len() as u32); + inner.add_dense_label(label, Complex64::new(1.0, 0.0))?; + Ok(inner.into()) + } + + /// Construct an observable from a list of dense labels and coefficients. + /// + /// This is analogous to :meth:`.SparsePauliOp.from_list`, except it uses + /// :ref:`the extended alphabet ` of :class:`.SparseObservable`. In + /// this dense form, you must supply all identities explicitly in each label. + /// + /// The label must be a sequence of the alphabet ``'IXYZ+-rl01'``. The label is interpreted + /// analogously to a bitstring. In other words, the right-most letter is associated with qubit + /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and + /// :class:`.SparsePauliOp`. + /// + /// Args: + /// iter (list[tuple[str, complex]]): Pairs of labels and their associated coefficients to + /// sum. The labels are interpreted the same way as in :meth:`from_label`. + /// num_qubits (int | None): It is not necessary to specify this if you are sure that + /// ``iter`` is not an empty sequence, since it can be inferred from the label lengths. + /// If ``iter`` may be empty, you must specify this argument to disambiguate how many + /// qubits the observable is for. If this is given and ``iter`` is not empty, the value + /// must match the label lengths. + /// + /// Examples: + /// + /// Construct an observable from a list of labels of the same length:: + /// + /// >>> SparseObservable.from_list([ + /// ... ("III++", 1.0), + /// ... ("II--I", 1.0j), + /// ... ("I++II", -0.5), + /// ... ("--III", -0.25j), + /// ... ]) + /// + /// + /// Use ``num_qubits`` to disambiguate potentially empty inputs:: + /// + /// >>> SparseObservable.from_list([], num_qubits=10) + /// + /// + /// This method is equivalent to calls to :meth:`from_sparse_list` with the explicit + /// qubit-arguments field set to decreasing integers:: + /// + /// >>> labels = ["XY+Z", "rl01", "-lXZ"] + /// >>> coeffs = [1.5j, 2.0, -0.5] + /// >>> from_list = SparseObservable.from_list(list(zip(labels, coeffs))) + /// >>> from_sparse_list = SparseObservable.from_sparse_list([ + /// ... (label, (3, 2, 1, 0), coeff) + /// ... for label, coeff in zip(labels, coeffs) + /// ... ]) + /// >>> assert from_list == from_sparse_list + /// + /// See also: + /// :meth:`from_label` + /// A similar constructor, but takes only a single label and always has its coefficient + /// set to ``1.0``. + /// + /// :meth:`from_sparse_list` + /// Construct the observable from a list of labels without explicit identities, but with + /// the qubits each single-qubit term applies to listed explicitly. + #[staticmethod] + #[pyo3(signature = (iter, /, *, num_qubits=None))] + fn from_list(iter: Vec<(String, Complex64)>, num_qubits: Option) -> PyResult { + if iter.is_empty() && num_qubits.is_none() { + return Err(PyValueError::new_err( + "cannot construct an observable from an empty list without knowing `num_qubits`", + )); + } + let num_qubits = match num_qubits { + Some(num_qubits) => num_qubits, + None => iter[0].0.len() as u32, + }; + let mut inner = SparseObservable::with_capacity(num_qubits, iter.len(), 0); + for (label, coeff) in iter { + inner.add_dense_label(&label, coeff)?; + } + Ok(inner.into()) + } + + /// Construct an observable from a list of labels, the qubits each item applies to, and the + /// coefficient of the whole term. + /// + /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`, except it uses + /// :ref:`the extended alphabet ` of :class:`.SparseObservable`. + /// + /// The "labels" and "indices" fields of the triples are associated by zipping them together. + /// For example, this means that a call to :meth:`from_list` can be converted to the form used + /// by this method by setting the "indices" field of each triple to ``(num_qubits-1, ..., 1, + /// 0)``. + /// + /// Args: + /// iter (list[tuple[str, Sequence[int], complex]]): triples of labels, the qubits + /// each single-qubit term applies to, and the coefficient of the entire term. + /// + /// num_qubits (int): the number of qubits in the operator. + /// + /// Examples: + /// + /// Construct a simple operator:: + /// + /// >>> SparseObservable.from_sparse_list( + /// ... [("ZX", (1, 4), 1.0), ("YY", (0, 3), 2j)], + /// ... num_qubits=5, + /// ... ) + /// + /// + /// Construct the identity observable (though really, just use :meth:`identity`):: + /// + /// >>> SparseObservable.from_sparse_list([("", (), 1.0)], num_qubits=100) + /// + /// + /// This method can replicate the behavior of :meth:`from_list`, if the qubit-arguments + /// field of the triple is set to decreasing integers:: + /// + /// >>> labels = ["XY+Z", "rl01", "-lXZ"] + /// >>> coeffs = [1.5j, 2.0, -0.5] + /// >>> from_list = SparseObservable.from_list(list(zip(labels, coeffs))) + /// >>> from_sparse_list = SparseObservable.from_sparse_list([ + /// ... (label, (3, 2, 1, 0), coeff) + /// ... for label, coeff in zip(labels, coeffs) + /// ... ]) + /// >>> assert from_list == from_sparse_list + /// + /// See also: + /// :meth:`to_sparse_list` + /// The reverse of this method. + #[staticmethod] + #[pyo3(signature = (iter, /, num_qubits))] + fn from_sparse_list( + iter: Vec<(String, Vec, Complex64)>, + num_qubits: u32, + ) -> PyResult { + let coeffs = iter.iter().map(|(_, _, coeff)| *coeff).collect(); + let mut boundaries = Vec::with_capacity(iter.len() + 1); + boundaries.push(0); + let mut indices = Vec::new(); + let mut bit_terms = Vec::new(); + // Insertions to the `BTreeMap` keep it sorted by keys, so we use this to do the termwise + // sorting on-the-fly. + let mut sorted = btree_map::BTreeMap::new(); + for (label, qubits, _) in iter { + sorted.clear(); + let label: &[u8] = label.as_ref(); + if label.len() != qubits.len() { + return Err(LabelError::WrongLengthIndices { + label: label.len(), + indices: indices.len(), + } + .into()); + } + for (letter, index) in label.iter().zip(qubits) { + if index >= num_qubits { + return Err(LabelError::BadIndex { index, num_qubits }.into()); + } + let btree_map::Entry::Vacant(entry) = sorted.entry(index) else { + return Err(LabelError::DuplicateIndex { index }.into()); + }; + entry.insert( + BitTerm::try_from_u8(*letter).map_err(|_| LabelError::OutsideAlphabet)?, + ); + } + for (index, term) in sorted.iter() { + let Some(term) = term else { + continue; + }; + indices.push(*index); + bit_terms.push(*term); + } + boundaries.push(bit_terms.len()); + } + let inner = SparseObservable::new(num_qubits, coeffs, bit_terms, indices, boundaries)?; + Ok(inner.into()) + } + + /// Express the observable in Pauli terms only, by writing each projector as sum of Pauli terms. + /// + /// Note that there is no guarantee of the order the resulting Pauli terms. Use + /// :meth:`SparseObservable.simplify` in addition to obtain a canonical representation. + /// + /// .. warning:: + /// + /// 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 exponential number of terms. + /// + /// Returns: + /// The same observable, but expressed in Pauli terms only. + /// + /// Examples: + /// + /// Rewrite an observable in terms of projectors into Pauli operators:: + /// + /// >>> obs = SparseObservable("+") + /// >>> obs.as_paulis() + /// + /// >>> direct = SparseObservable.from_list([("I", 0.5), ("Z", 0.5)]) + /// >>> assert direct.simplify() == obs.as_paulis().simplify() + /// + /// For small operators, this can be used with :meth:`simplify` as a unique canonical form:: + /// + /// >>> left = SparseObservable.from_list([("+", 0.5), ("-", 0.5)]) + /// >>> right = SparseObservable.from_list([("r", 0.5), ("l", 0.5)]) + /// >>> assert left.as_paulis().simplify() == right.as_paulis().simplify() + /// + /// See also: + /// :meth:`.SparsePauliOp.from_sparse_observable` + /// A constructor of :class:`.SparsePauliOp` that can convert a + /// :class:`SparseObservable` in the :class:`.SparsePauliOp` dense Pauli representation. + fn as_paulis(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.as_paulis().into()) + } + + /// Express the observable in terms of a sparse list format. + /// + /// 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. + /// + /// Examples: + /// + /// >>> obs = SparseObservable.from_list([("IIXIZ", 2j), ("IIZIX", 2j)]) + /// >>> reconstructed = SparseObservable.from_sparse_list(obs.to_sparse_list(), obs.num_qubits) + /// + /// See also: + /// :meth:`from_sparse_list` + /// The constructor that can interpret these lists. + #[pyo3(signature = ())] + fn to_sparse_list(&self, py: Python) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + // turn a SparseView into a Python tuple of (bit terms, indices, coeff) + let to_py_tuple = |view: SparseTermView| { + let mut pauli_string = String::with_capacity(view.bit_terms.len()); + + for bit in view.bit_terms.iter() { + pauli_string.push_str(bit.py_label()); + } + let py_string = PyString::new(py, &pauli_string).unbind(); + let py_indices = PyList::new(py, view.indices.iter())?.unbind(); + let py_coeff = view.coeff.into_py_any(py)?; + + PyTuple::new(py, vec![py_string.as_any(), py_indices.as_any(), &py_coeff]) + }; + + let out = PyList::empty(py); + for view in inner.iter() { + out.append(to_py_tuple(view)?)?; + } + Ok(out.unbind()) + } + + /// Construct a :class:`.SparseObservable` from a :class:`.SparsePauliOp` instance. + /// + /// This will be a largely direct translation of the :class:`.SparsePauliOp`; in particular, + /// there is no on-the-fly summing of like terms, nor any attempt to refactorize sums of Pauli + /// terms into equivalent projection operators. + /// + /// Args: + /// op (:class:`.SparsePauliOp`): the operator to convert. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> spo = SparsePauliOp.from_list([("III", 1.0), ("IIZ", 0.5), ("IZI", 0.5)]) + /// >>> SparseObservable.from_sparse_pauli_op(spo) + /// + #[staticmethod] + #[pyo3(signature = (op, /))] + fn from_sparse_pauli_op(op: &Bound) -> PyResult { + let py = op.py(); + let pauli_list_ob = op.getattr(intern!(py, "paulis"))?; + let coeffs = op + .getattr(intern!(py, "coeffs"))? + .extract::>() + .map_err(|_| PyTypeError::new_err("only 'SparsePauliOp' with complex-typed coefficients can be converted to 'SparseObservable'"))? + .as_array() + .to_vec(); + let op_z = pauli_list_ob + .getattr(intern!(py, "z"))? + .extract::>()?; + let op_x = pauli_list_ob + .getattr(intern!(py, "x"))? + .extract::>()?; + // We don't extract the `phase`, because that's supposed to be 0 for all `SparsePauliOp` + // instances - they use the symplectic convention in the representation with any phase term + // absorbed into the coefficients (like us). + let [num_terms, num_qubits] = *op_z.shape() else { + unreachable!("shape is statically known to be 2D") + }; + if op_x.shape() != [num_terms, num_qubits] { + return Err(PyValueError::new_err(format!( + "'x' and 'z' have different shapes ({:?} and {:?})", + op_x.shape(), + op_z.shape() + ))); + } + if num_terms != coeffs.len() { + return Err(PyValueError::new_err(format!( + "'x' and 'z' have a different number of operators to 'coeffs' ({} and {})", + num_terms, + coeffs.len(), + ))); + } + + let mut bit_terms = Vec::new(); + let mut indices = Vec::new(); + let mut boundaries = Vec::with_capacity(num_terms + 1); + boundaries.push(0); + for (term_x, term_z) in op_x + .as_array() + .rows() + .into_iter() + .zip(op_z.as_array().rows()) + { + for (i, (x, z)) in term_x.iter().zip(term_z.iter()).enumerate() { + // The only failure case possible here is the identity, because of how we're + // constructing the value to convert. + let Ok(term) = ::bytemuck::checked::try_cast(((*x as u8) << 1) | (*z as u8)) else { + continue; + }; + indices.push(i as u32); + bit_terms.push(term); + } + boundaries.push(indices.len()); + } + + let inner = + SparseObservable::new(num_qubits as u32, coeffs, bit_terms, indices, boundaries)?; + Ok(inner.into()) + } + + /// Construct a :class:`SparseObservable` out of individual terms. + /// + /// All the terms must have the same number of qubits. If supplied, the ``num_qubits`` argument + /// must match the terms. + /// + /// No simplification is done as part of the observable creation. + /// + /// Args: + /// obj (Iterable[Term]): Iterable of individual terms to build the observable from. + /// num_qubits (int | None): The number of qubits the observable should act on. This is + /// usually inferred from the input, but can be explicitly given to handle the case + /// of an empty iterable. + /// + /// Returns: + /// The corresponding observable. + #[staticmethod] + #[pyo3(signature = (obj, /, num_qubits=None))] + fn from_terms(obj: &Bound, num_qubits: Option) -> PyResult { + let mut iter = obj.try_iter()?; + let mut inner = match num_qubits { + Some(num_qubits) => SparseObservable::zero(num_qubits), + None => { + let Some(first) = iter.next() else { + return Err(PyValueError::new_err( + "cannot construct an observable from an empty list without knowing `num_qubits`", + )); + }; + let py_term = first?.cast::()?.borrow(); + py_term.inner.to_observable() + } + }; + for bound_py_term in iter { + let py_term = bound_py_term?.cast::()?.borrow(); + inner.add_term(py_term.inner.view())?; + } + Ok(inner.into()) + } + + // SAFETY: this cannot invoke undefined behaviour if `check = true`, but if `check = false` then + // the `bit_terms` must all be valid `BitTerm` representations. + /// Construct a :class:`.SparseObservable` from raw Numpy arrays that match :ref:`the required + /// data representation described in the class-level documentation `. + /// + /// The data from each array is copied into fresh, growable Rust-space allocations. + /// + /// Args: + /// num_qubits: number of qubits in the observable. + /// coeffs: complex coefficients of each term of the observable. This should be a Numpy + /// array with dtype :attr:`~numpy.complex128`. + /// bit_terms: flattened list of the single-qubit terms comprising all complete terms. This + /// should be a Numpy array with dtype :attr:`~numpy.uint8` (which is compatible with + /// :class:`.BitTerm`). + /// indices: flattened term-wise sorted list of the qubits each single-qubit term corresponds + /// to. This should be a Numpy array with dtype :attr:`~numpy.uint32`. + /// boundaries: the indices that partition ``bit_terms`` and ``indices`` into terms. This + /// should be a Numpy array with dtype :attr:`~numpy.uintp`. + /// check: if ``True`` (the default), validate that the data satisfies all coherence + /// guarantees. If ``False``, no checks are done. + /// + /// .. warning:: + /// + /// If ``check=False``, the ``bit_terms`` absolutely *must* be all be valid values + /// of :class:`.SparseObservable.BitTerm`. If they are not, Rust-space undefined + /// behavior may occur, entirely invalidating the program execution. + /// + /// Examples: + /// + /// Construct a sum of :math:`Z` on each individual qubit:: + /// + /// >>> num_qubits = 100 + /// >>> terms = np.full((num_qubits,), SparseObservable.BitTerm.Z, dtype=np.uint8) + /// >>> indices = np.arange(num_qubits, dtype=np.uint32) + /// >>> coeffs = np.ones((num_qubits,), dtype=complex) + /// >>> boundaries = np.arange(num_qubits + 1, dtype=np.uintp) + /// >>> SparseObservable.from_raw_parts(num_qubits, coeffs, terms, indices, boundaries) + /// + #[staticmethod] + #[pyo3( + signature = (/, num_qubits, coeffs, bit_terms, indices, boundaries, check=true), + )] + unsafe fn from_raw_parts<'py>( + num_qubits: u32, + coeffs: PyArrayLike1<'py, Complex64, numpy::AllowTypeChange>, + bit_terms: PyArrayLike1<'py, u8, numpy::AllowTypeChange>, + indices: PyArrayLike1<'py, u32, numpy::AllowTypeChange>, + boundaries: PyArrayLike1<'py, usize, numpy::AllowTypeChange>, + check: bool, + ) -> PyResult { + let coeffs = coeffs.as_array().to_vec(); + let bit_terms = if check { + bit_terms + .as_array() + .into_iter() + .copied() + .map(BitTerm::try_from) + .collect::>()? + } else { + let bit_terms_as_u8 = bit_terms.as_array().to_vec(); + // SAFETY: the caller enforced that each `u8` is a valid `BitTerm`, and `BitTerm` is be + // represented by a `u8`. We can't use `bytemuck` because we're casting a `Vec`. + unsafe { ::std::mem::transmute::, Vec>(bit_terms_as_u8) } + }; + let indices = indices.as_array().to_vec(); + let boundaries = boundaries.as_array().to_vec(); + + let inner = if check { + SparseObservable::new(num_qubits, coeffs, bit_terms, indices, boundaries) + .map_err(PyErr::from) + } else { + // SAFETY: the caller promised they have upheld the coherence guarantees. + Ok(unsafe { + SparseObservable::new_unchecked(num_qubits, coeffs, bit_terms, indices, boundaries) + }) + }?; + Ok(inner.into()) + } + + /// Clear all the terms from this operator, making it equal to the zero operator again. + /// + /// This does not change the capacity of the internal allocations, so subsequent addition or + /// subtraction operations may not need to reallocate. + /// + /// Examples: + /// + /// .. code-block:: python + /// + /// >>> obs = SparseObservable.from_list([("IX+-rl", 2.0), ("01YZII", -1j)]) + /// >>> obs.clear() + /// >>> assert obs == SparseObservable.zero(obs.py_num_qubits()) + pub fn clear(&mut self) -> PyResult<()> { + let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; + inner.clear(); + Ok(()) + } + + /// Sum any like terms in this operator, removing them if the resulting complex coefficient has + /// an absolute value within tolerance of zero. + /// + /// As a side effect, this sorts the operator into :ref:`canonical order + /// `. + /// + /// .. note:: + /// + /// When using this for equality comparisons, note that floating-point rounding and the + /// non-associativity of floating-point addition may cause non-zero coefficients of summed + /// terms to compare unequal. To compare two observables up to a tolerance, it is safest to + /// compare the canonicalized difference of the two observables to zero. + /// + /// Args: + /// tol (float): after summing like terms, any coefficients whose absolute value is less + /// than the given absolute tolerance will be suppressed from the output. + /// + /// Examples: + /// + /// Using :meth:`simplify` to compare two operators that represent the same observable, but + /// would compare unequal due to the structural tests by default:: + /// + /// >>> base = SparseObservable.from_sparse_list([ + /// ... ("XZ", (2, 1), 1e-10), # value too small + /// ... ("+-", (3, 1), 2j), + /// ... ("+-", (3, 1), 2j), # can be combined with the above + /// ... ("01", (3, 1), 0.5), # out of order compared to `expected` + /// ... ], num_qubits=5) + /// >>> expected = SparseObservable.from_list([("I0I1I", 0.5), ("I+I-I", 4j)]) + /// >>> assert base != expected # non-canonical comparison + /// >>> assert base.simplify() == expected.simplify() + /// + /// Note that in the above example, the coefficients are chosen such that all floating-point + /// calculations are exact, and there are no intermediate rounding or associativity + /// concerns. If this cannot be guaranteed to be the case, the safer form is:: + /// + /// >>> left = SparseObservable.from_list([("XYZ", 1.0/3.0)] * 3) # sums to 1.0 + /// >>> right = SparseObservable.from_list([("XYZ", 1.0/7.0)] * 7) # doesn't sum to 1.0 + /// >>> assert left.simplify() != right.simplify() + /// >>> assert (left - right).simplify() == SparseObservable.zero(left.num_qubits) + #[pyo3( + signature = (/, tol=1e-8), + )] + fn simplify(&self, tol: f64) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let simplified = inner.canonicalize(tol); + Ok(simplified.into()) + } + + /// Calculate the adjoint of this observable. + /// + /// + /// This is well defined in the abstract mathematical sense. All the terms of the single-qubit + /// alphabet are self-adjoint, so the result of this operation is the same observable, except + /// its coefficients are all their complex conjugates. + /// + /// Examples: + /// + /// .. code-block:: + /// + /// >>> left = SparseObservable.from_list([("XY+-", 1j)]) + /// >>> right = SparseObservable.from_list([("XY+-", -1j)]) + /// >>> assert left.adjoint() == right + fn adjoint(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.adjoint().into()) + } + + /// Calculate the matrix transposition of this observable. + /// + /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the + /// matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related + /// alphabet terms are unaffected by the transposition, but $Y$-related terms modify their + /// alphabet terms. Precisely: + /// + /// * :math:`Y` transposes to :math:`-Y` + /// * :math:`\lvert r\rangle\langle r\rvert` transposes to :math:`\lvert l\rangle\langle l\rvert` + /// * :math:`\lvert l\rangle\langle l\rvert` transposes to :math:`\lvert r\rangle\langle r\rvert` + /// + /// Examples: + /// + /// .. code-block:: + /// + /// >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)]) + /// >>> assert obs.transpose() == SparseObservable([("III", 1j), ("Ylr", -0.5)]) + fn transpose(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.transpose().into()) + } + + /// Calculate the complex conjugation of this observable. + /// + /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the + /// matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related + /// alphabet terms are unaffected by the complex conjugation, but $Y$-related terms modify their + /// alphabet terms. Precisely: + /// + /// * :math:`Y` conjguates to :math:`-Y` + /// * :math:`\lvert r\rangle\langle r\rvert` conjugates to :math:`\lvert l\rangle\langle l\rvert` + /// * :math:`\lvert l\rangle\langle l\rvert` conjugates to :math:`\lvert r\rangle\langle r\rvert` + /// + /// Additionally, all coefficients are conjugated. + /// + /// Examples: + /// + /// .. code-block:: + /// + /// >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)]) + /// >>> assert obs.conjugate() == SparseObservable([("III", -1j), ("Ylr", -0.5)]) + fn conjugate(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.conjugate().into()) + } + + /// Tensor product of two observables. + /// + /// The bit ordering is defined such that the qubit indices of the argument will remain the + /// same, and the indices of ``self`` will be offset by the number of qubits in ``other``. This + /// is the same convention as used by the rest of Qiskit's :mod:`~qiskit.quantum_info` + /// operators. + /// + /// This function is used for the infix ``^`` operator. If using this operator, beware that + /// `Python's operator-precedence rules + /// `__ may cause the + /// evaluation order to be different to your expectation. In particular, the operator ``+`` + /// binds more tightly than ``^``, just like ``*`` binds more tightly than ``+``. Use + /// parentheses to fix the evaluation order, if needed. + /// + /// The argument will be cast to :class:`SparseObservable` using its default constructor, if it + /// is not already in the correct form. + /// + /// Args: + /// + /// other: the observable to put on the right-hand side of the tensor product. + /// + /// Examples: + /// + /// The bit ordering of this is such that the tensor product of two observables made from a + /// single label "looks like" an observable made by concatenating the two strings:: + /// + /// >>> left = SparseObservable.from_label("XYZ") + /// >>> right = SparseObservable.from_label("+-IIrl") + /// >>> assert left.tensor(right) == SparseObservable.from_label("XYZ+-IIrl") + /// + /// You can also use the infix ``^`` operator for tensor products, which will similarly cast + /// the right-hand side of the operation if it is not already a :class:`SparseObservable`:: + /// + /// >>> assert SparseObservable("rl") ^ Pauli("XYZ") == SparseObservable("rlXYZ") + /// + /// See also: + /// :meth:`expand` + /// + /// The same function, but with the order of arguments flipped. This can be useful if + /// you like using the casting behavior for the argument, but you want your existing + /// :class:`SparseObservable` to be on the right-hand side of the tensor ordering. + #[pyo3(signature = (other, /))] + fn tensor(&self, other: &Bound) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Err(PyTypeError::new_err(format!( + "unknown type for tensor: {}", + other.get_type().repr()? + ))); + }; + + let other = other.borrow(); + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + inner.tensor(&other_inner).into_py_any(py) + } + + /// Reverse-order tensor product. + /// + /// This is equivalent to ``other.tensor(self)``, except that ``other`` will first be type-cast + /// to :class:`SparseObservable` if it isn't already one (by calling the default constructor). + /// + /// Args: + /// + /// other: the observable to put on the left-hand side of the tensor product. + /// + /// Examples: + /// + /// This is equivalent to :meth:`tensor` with the order of the arguments flipped:: + /// + /// >>> left = SparseObservable.from_label("XYZ") + /// >>> right = SparseObservable.from_label("+-IIrl") + /// >>> assert left.tensor(right) == right.expand(left) + /// + /// See also: + /// :meth:`tensor` + /// + /// The same function with the order of arguments flipped. :meth:`tensor` is the more + /// standard argument ordering, and matches Qiskit's other conventions. + #[pyo3(signature = (other, /))] + fn expand<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Err(PyTypeError::new_err(format!( + "unknown type for expand: {}", + other.get_type().repr()? + ))); + }; + + let other = other.borrow(); + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + other_inner.tensor(&inner).into_pyobject(py) + } + + /// Compose another :class:`SparseObservable` onto this one. + /// + /// In terms of operator algebras, composition corresponds to left-multiplication: + /// ``c = a.compose(b)`` corresponds to $C = B A$. In other words, ``a.compose(b)`` returns an + /// operator that "performs ``a``, and then performs ``b`` on the result". The argument + /// ``front=True`` instead makes this a right multiplication. + /// + /// ``self`` and ``other`` must be the same size, unless ``qargs`` is given, in which case + /// ``other`` can be smaller than ``self``, provided the number of qubits in ``other`` and the + /// length of ``qargs`` match. ``qargs`` can never contain duplicates or indices of qubits that + /// do not exist in ``self``. + /// + /// Beware that this function can cause exponential explosion of the memory usage of the + /// observable, as the alphabet of :class:`SparseObservable` is not closed under composition; + /// the composition of two single-bit terms can be a sum, which multiplies the total number of + /// terms. This memory usage is not _necessarily_ inherent to the resultant observable, but + /// finding an efficient re-factorization of the sum is generally equally computationally hard. + /// It's better to use domain knowledge of your observables to minimize the number of terms that + /// ever exist, rather than trying to simplify them after the fact. + /// + /// Args: + /// other: the observable used to left-multiply ``self``. + /// qargs: if given, the qubits in ``self`` to be associated with the qubits in ``other``. + /// Put another way: if this is given, it is similar to a more efficient implementation + /// of:: + /// + /// self.compose(other.apply_layout(qargs, self.num_qubits)) + /// + /// as no temporary observable is created to store the applied-layout form of ``other``. + /// front: if ``True``, then right-multiply by ``other`` instead of left-multiplying + /// (default ``False``). The ``qargs`` are still applied to ``other``. This is most + /// useful when ``qargs`` is set, or ``other`` might be an object that must be coerced + /// to :class:`SparseObservable`. + #[pyo3(signature = (other, /, qargs=None, *, front=false))] + fn compose<'py>( + &self, + other: &Bound<'py, PyAny>, + qargs: Option<&Bound<'py, PyAny>>, + front: bool, + ) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Err(PyTypeError::new_err(format!( + "unknown type for compose: {}", + other.get_type().repr()? + ))); + }; + let other = other.borrow(); + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + if let Some(order) = qargs { + if other_inner.num_qubits() > inner.num_qubits() { + return Err(PyValueError::new_err(format!( + "argument has more qubits ({}) than the base ({})", + other_inner.num_qubits(), + inner.num_qubits() + ))); + } + let in_length = order.len()?; + if other_inner.num_qubits() as usize != in_length { + return Err(PyValueError::new_err(format!( + "input qargs has length {}, but the observable is on {} qubit(s)", + in_length, + other_inner.num_qubits() + ))); + } + let order = order + .try_iter()? + .map(|obj| obj.and_then(|obj| obj.extract::())) + .collect::>>()?; + if order.len() != in_length { + return Err(PyValueError::new_err("duplicate indices in qargs")); + } + if order + .iter() + .max() + .is_some_and(|max| *max >= inner.num_qubits()) + { + return Err(PyValueError::new_err("qargs contains out-of-range qubits")); + } + if front { + // This implementation can be improved if it turns out to be needed a lot and the + // extra copy is a bottleneck. + let other_to_self = order.iter().copied().collect::>(); + other_inner + .apply_layout(Some(other_to_self.as_slice()), inner.num_qubits())? + .compose(&inner) + .into_pyobject(py) + } else { + inner + .compose_map(&other_inner, |bit| { + *order + .get_index(bit as usize) + .expect("order has the same length and no duplicates") + }) + .into_pyobject(py) + } + } else { + if other_inner.num_qubits() != inner.num_qubits() { + return Err(PyValueError::new_err(format!( + "mismatched numbers of qubits: {} (base) and {} (argument)", + inner.num_qubits(), + other_inner.num_qubits() + ))); + } + if front { + other_inner.compose(&inner).into_pyobject(py) + } else { + inner.compose(&other_inner).into_pyobject(py) + } + } + } + + /// Evolve this observable by a Pauli term. + /// + /// An evolution of the observable :math:`O` by a Pauli :math:`P` corresponds to + /// :math:`P^\dagger O P`. + /// + /// Unlike a literal implementation via two full compositions, this method + /// performs the conjugation directly at the single-qubit level using a fixed + /// lookup table. This avoids materializing any intermediate + /// :class:`SparseObservable` and computes the evolved observable in a single + /// pass over the terms. + /// ``self`` and ``other`` must have the same number of qubits, unless ``qargs`` is given, + /// in which case ``other`` can be smaller than ``self``, provided the number of qubits + /// in ``other`` and the length of ``qargs`` match. ``qargs`` specifies which qubits of + /// ``self`` are evolved by ``other``. + /// + /// Currently, this method supports evolution only by a *single-term* operator, meaning + /// that ``other`` must be a Pauli represented by :class:`~.quantum_info.Pauli`. + /// + /// Args: + /// other: the Pauli Operator used to conjugate ``self``. + /// qargs: if given, the qubits in ``self`` to be evolved by ``other``. + /// The length must match the number of qubits in ``other``. + /// + /// Returns: + /// A new evolved :class:`SparseObservable` with applied conjugations. + /// + /// Raises: + /// TypeError : if ``other`` is not of Type :class:`~.quantum_info.Pauli`. + /// ValueError: if ``self`` and ``other`` have different numbers of qubits (and ``qargs`` is not given). + /// ValueError: if ``qargs`` length doesn't match ``other`` number of qubits. + /// ValueError: if ``qargs`` contains duplicates or out-of-range indices. + /// ValueError: if ``other`` contains more than one term. + #[pyo3(signature = (other, /, qargs=None))] + fn evolve<'py>( + &self, + other: &Bound<'py, PyAny>, + qargs: Option<&Bound<'py, PyAny>>, + ) -> PyResult> { + let py = other.py(); + + if !other.is_instance(PAULI_TYPE.get_bound(py))? { + return Err(PyTypeError::new_err(format!( + "evolve only accepts Pauli instances, got: {}", + other.get_type().repr()? + ))); + } + + let base = self.as_inner()?; + let u_obs = Self::from_pauli(other)?; + let u_inner = u_obs.as_inner()?; + + let qargs_vec = if let Some(qargs) = qargs { + let vec = qargs + .try_iter()? + .map(|obj| obj.and_then(|obj| obj.extract::())) + .collect::>>()?; + Some(vec) + } else { + None + }; + + let out = base + .evolve(&u_inner, qargs_vec.as_deref()) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + + out.into_pyobject(py) + } + + /// Apply a transpiler layout to this :class:`SparseObservable`. + /// + /// Typically you will have defined your observable in terms of the virtual qubits of the + /// circuits you will use to prepare states. After transpilation, the virtual qubits are mapped + /// to particular physical qubits on a device, which may be wider than your circuit. That + /// mapping can also change over the course of the circuit. This method transforms the input + /// observable on virtual qubits to an observable that is suitable to apply immediately after + /// the fully transpiled *physical* circuit. + /// + /// Args: + /// layout (TranspileLayout | list[int] | None): The layout to apply. Most uses of this + /// function should pass the :attr:`.QuantumCircuit.layout` field from a circuit that + /// was transpiled for hardware. In addition, you can pass a list of new qubit indices. + /// If given as explicitly ``None``, no remapping is applied (but you can still use + /// ``num_qubits`` to expand the observable). + /// num_qubits (int | None): The number of qubits to expand the observable to. If not + /// supplied, the output will be as wide as the given :class:`.TranspileLayout`, or the + /// same width as the input if the ``layout`` is given in another form. + /// + /// Returns: + /// A new :class:`SparseObservable` with the provided layout applied. + #[pyo3(signature = (/, layout, num_qubits=None))] + fn apply_layout(&self, layout: Bound, num_qubits: Option) -> PyResult { + let py = layout.py(); + let inner = self.inner.read().map_err(|_| InnerReadError)?; + + // A utility to check the number of qubits is compatible with the observable. + let check_inferred_qubits = |inferred: u32| -> PyResult { + if inferred < inner.num_qubits() { + return Err(CoherenceError::NotEnoughQubits { + current: inner.num_qubits() as usize, + target: inferred as usize, + } + .into()); + } + Ok(inferred) + }; + + // Normalize the number of qubits in the layout and the layout itself, depending on the + // input types, before calling SparseObservable.apply_layout to do the actual work. + let (num_qubits, layout): (u32, Option>) = if layout.is_none() { + (num_qubits.unwrap_or(inner.num_qubits()), None) + } else if layout.is_instance( + &py.import(intern!(py, "qiskit.transpiler"))? + .getattr(intern!(py, "TranspileLayout"))?, + )? { + ( + check_inferred_qubits( + layout.getattr(intern!(py, "_output_qubit_list"))?.len()? as u32 + )?, + Some( + layout + .call_method0(intern!(py, "final_index_layout"))? + .extract::>()?, + ), + ) + } else { + ( + check_inferred_qubits(num_qubits.unwrap_or(inner.num_qubits()))?, + Some(layout.extract()?), + ) + }; + + let out = inner.apply_layout(layout.as_deref(), num_qubits)?; + Ok(out.into()) + } + + /// Get a :class:`.PauliList` object that represents the measurement basis needed for each term + /// (in order) in this observable. + /// + /// For example, the projector ``0l+`` will return a Pauli ``ZXY``. The resulting + /// :class:`~.quantum_info.Pauli` is dense, in the sense that explicit identities are stored. + /// An identity in the Pauli output does not require a concrete measurement. + /// + /// This will return an entry in the Pauli list for every term in the sum. + /// + /// Returns: + /// :class:`.PauliList`: the Pauli operator list representing the necessary measurement + /// bases. + fn pauli_bases<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let mut x = Array2::from_elem([inner.num_terms(), inner.num_qubits() as usize], false); + let mut z = Array2::from_elem([inner.num_terms(), inner.num_qubits() as usize], false); + for (loc, term) in inner.iter().enumerate() { + let mut x_row = x.row_mut(loc); + let mut z_row = z.row_mut(loc); + for (bit_term, index) in term.bit_terms.iter().zip(term.indices) { + x_row[*index as usize] = bit_term.has_x_component(); + z_row[*index as usize] = bit_term.has_z_component(); + } + } + PAULI_LIST_TYPE + .get_bound(py) + .getattr(intern!(py, "from_symplectic"))? + .call1(( + PyArray2::from_owned_array(py, z), + PyArray2::from_owned_array(py, x), + )) + } + + /// Check whether the observable commutes with another one. + /// + /// Args: + /// other (SparseObservable): The other observable to check commutation with. + /// tol (float): If coefficients in the product of `self` and `other` are below the + /// tolerance (in magnitude), the terms are ignored. + /// + /// Returns: + /// ``True`` if the terms commute, up to tolerance, ``False`` otherwise. + /// + /// Raises: + /// TypeError: If ``other`` could not be coerced to ``SparseObservable``. + #[pyo3(signature = (other, tol=1e-12))] + pub fn commutes(&self, other: &Bound, tol: f64) -> PyResult { + let Some(other) = coerce_to_observable(other)? else { + return Err(PyTypeError::new_err("Invalid type of other.")); + }; + + let self_inner = self.inner.read().map_err(|_| InnerReadError)?; + let other = other.borrow(); + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + + Ok(self_inner.commutes(&other_inner, tol)) + } + + fn __len__(&self) -> PyResult { + self.num_terms() + } + + fn __getitem__<'py>( + &self, + py: Python<'py>, + index: PySequenceIndex<'py>, + ) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let indices = match index.with_len(inner.num_terms())? { + SequenceIndex::Int(index) => { + return PySparseTerm { + inner: inner.term(index).to_term(), + } + .into_bound_py_any(py); + } + indices => indices, + }; + let mut out = SparseObservable::zero(inner.num_qubits()); + for index in indices.iter() { + out.add_term(inner.term(index))?; + } + out.into_bound_py_any(py) + } + + fn __eq__(slf: Bound, other: Bound) -> PyResult { + // this is also important to check before trying to read both slf and other + if slf.is(&other) { + return Ok(true); + } + let Ok(other) = other.cast_into::() else { + return Ok(false); + }; + let slf_borrowed = slf.borrow(); + let other_borrowed = other.borrow(); + let slf_inner = slf_borrowed.inner.read().map_err(|_| InnerReadError)?; + let other_inner = other_borrowed.inner.read().map_err(|_| InnerReadError)?; + Ok(slf_inner.eq(&other_inner)) + } + + fn __repr__(&self) -> PyResult { + let num_terms = self.num_terms()?; + let num_qubits = self.num_qubits()?; + + let str_num_terms = format!( + "{} term{}", + num_terms, + if num_terms == 1 { "" } else { "s" } + ); + let str_num_qubits = format!( + "{} qubit{}", + num_qubits, + if num_qubits == 1 { "" } else { "s" } + ); + + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let str_terms = if num_terms == 0 { + "0.0".to_owned() + } else { + inner + .iter() + .map(SparseTermView::to_sparse_str) + .collect::>() + .join(" + ") + }; + Ok(format!( + "" + )) + } + + fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult> { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let bit_terms: &[u8] = ::bytemuck::cast_slice(inner.bit_terms()); + ( + py.get_type::().getattr("from_raw_parts")?, + ( + inner.num_qubits(), + PyArray1::from_slice(py, inner.coeffs()), + PyArray1::from_slice(py, bit_terms), + PyArray1::from_slice(py, inner.indices()), + PyArray1::from_slice(py, inner.boundaries()), + false, + ), + ) + .into_pyobject(py) + } + + fn __add__<'py>( + slf_: &Bound<'py, Self>, + other: &Bound<'py, PyAny>, + ) -> PyResult> { + let py = slf_.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented().into_bound(py)); + }; + + let other = other.borrow(); + let slf_ = slf_.borrow(); + if Arc::ptr_eq(&slf_.inner, &other.inner) { + // This fast path is for consistency with the in-place `__iadd__`, which would otherwise + // struggle to do the addition to itself. + let inner = slf_.inner.read().map_err(|_| InnerReadError)?; + return <&SparseObservable as ::std::ops::Mul<_>>::mul( + &inner, + Complex64::new(2.0, 0.0), + ) + .into_bound_py_any(py); + } + let slf_inner = slf_.inner.read().map_err(|_| InnerReadError)?; + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + slf_inner.check_equal_qubits(&other_inner)?; + <&SparseObservable as ::std::ops::Add>::add(&slf_inner, &other_inner).into_bound_py_any(py) + } + + fn __radd__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { + // No need to handle the `self is other` case here, because `__add__` will get it. + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented().into_bound(py)); + }; + + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let other = other.borrow(); + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + inner.check_equal_qubits(&other_inner)?; + <&SparseObservable as ::std::ops::Add>::add(&other_inner, &inner).into_bound_py_any(py) + } + + fn __iadd__(slf_: Bound, other: &Bound) -> PyResult<()> { + let Some(other) = coerce_to_observable(other)? else { + // This is not well behaved - we _should_ return `NotImplemented` to Python space + // without an exception, but limitations in PyO3 prevent this at the moment. See + // https://github.com/PyO3/pyo3/issues/4605. + return Err(PyTypeError::new_err(format!( + "invalid object for in-place addition of 'SparseObservable': {}", + other.repr()? + ))); + }; + + let other = other.borrow(); + let slf_ = slf_.borrow(); + let mut slf_inner = slf_.inner.write().map_err(|_| InnerWriteError)?; + + // Check if slf_ and other point to the same SparseObservable object, in which case + // we just multiply it by 2 + if Arc::ptr_eq(&slf_.inner, &other.inner) { + *slf_inner *= Complex64::new(2.0, 0.0); + return Ok(()); + } + + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + slf_inner.check_equal_qubits(&other_inner)?; + slf_inner.add_assign(&other_inner); + Ok(()) + } + + fn __sub__<'py>( + slf_: &Bound<'py, Self>, + other: &Bound<'py, PyAny>, + ) -> PyResult> { + let py = slf_.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented().into_bound(py)); + }; + + let other = other.borrow(); + let slf_ = slf_.borrow(); + if Arc::ptr_eq(&slf_.inner, &other.inner) { + return PySparseObservable::zero(slf_.num_qubits()?).into_bound_py_any(py); + } + + let slf_inner = slf_.inner.read().map_err(|_| InnerReadError)?; + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + slf_inner.check_equal_qubits(&other_inner)?; + <&SparseObservable as ::std::ops::Sub>::sub(&slf_inner, &other_inner).into_bound_py_any(py) + } + + fn __rsub__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented().into_bound(py)); + }; + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let other = other.borrow(); + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + inner.check_equal_qubits(&other_inner)?; + <&SparseObservable as ::std::ops::Sub>::sub(&other_inner, &inner).into_bound_py_any(py) + } + + fn __isub__(slf_: Bound, other: &Bound) -> PyResult<()> { + let Some(other) = coerce_to_observable(other)? else { + // This is not well behaved - we _should_ return `NotImplemented` to Python space + // without an exception, but limitations in PyO3 prevent this at the moment. See + // https://github.com/PyO3/pyo3/issues/4605. + return Err(PyTypeError::new_err(format!( + "invalid object for in-place subtraction of 'SparseObservable': {}", + other.repr()? + ))); + }; + let other = other.borrow(); + let slf_ = slf_.borrow(); + let mut slf_inner = slf_.inner.write().map_err(|_| InnerWriteError)?; + + if Arc::ptr_eq(&slf_.inner, &other.inner) { + // This is not strictly the same thing as `a - a` if `a` contains non-finite + // floating-point values (`inf - inf` is `NaN`, for example); we don't really have a + // clear view on what floating-point guarantees we're going to make right now. + slf_inner.clear(); + return Ok(()); + } + + let other_inner = other.inner.read().map_err(|_| InnerReadError)?; + slf_inner.check_equal_qubits(&other_inner)?; + slf_inner.sub_assign(&other_inner); + Ok(()) + } + + fn __pos__(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + Ok(inner.clone().into()) + } + + fn __neg__(&self) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let neg = <&SparseObservable as ::std::ops::Neg>::neg(&inner); + Ok(neg.into()) + } + + fn __mul__(&self, other: Complex64) -> PyResult { + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let mult = <&SparseObservable as ::std::ops::Mul<_>>::mul(&inner, other); + Ok(mult.into()) + } + fn __rmul__(&self, other: Complex64) -> PyResult { + self.__mul__(other) + } + + fn __imul__(&mut self, other: Complex64) -> PyResult<()> { + let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; + inner.mul_assign(other); + Ok(()) + } + + fn __truediv__(&self, other: Complex64) -> PyResult { + if other.is_zero() { + return Err(PyZeroDivisionError::new_err("complex division by zero")); + } + let inner = self.inner.read().map_err(|_| InnerReadError)?; + let div = <&SparseObservable as ::std::ops::Div<_>>::div(&inner, other); + Ok(div.into()) + } + fn __itruediv__(&mut self, other: Complex64) -> PyResult<()> { + if other.is_zero() { + return Err(PyZeroDivisionError::new_err("complex division by zero")); + } + let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; + inner.div_assign(other); + Ok(()) + } + + fn __xor__(&self, other: &Bound) -> PyResult> { + // we cannot just delegate this to ``tensor`` since ``other`` might allow + // right-hand-side arithmetic and we have to try deferring to that object, + // which is done by returning ``NotImplemented`` + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented()); + }; + + self.tensor(&other) + } + + fn __rxor__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { + let py = other.py(); + let Some(other) = coerce_to_observable(other)? else { + return Ok(py.NotImplemented().into_bound(py)); + }; + self.expand(&other).map(|obj| obj.into_any()) + } + + // The documentation for this is inlined into the class-level documentation of + // `SparseObservable`. + #[allow(non_snake_case)] + #[classattr] + fn BitTerm(py: Python) -> PyResult> { + BIT_TERM_PY_ENUM + .get_or_try_init(py, || make_py_bit_term(py)) + .map(|obj| obj.clone_ref(py)) + } + + // The documentation for this is inlined into the class-level documentation of + // `SparseObservable`. + #[allow(non_snake_case)] + #[classattr] + fn Term(py: Python) -> Bound { + py.get_type::() + } +} + +impl PySparseObservable { + /// This is an immutable reference as opposed to a `copy`. + pub fn as_inner(&self) -> Result, InnerReadError> { + let data = self.inner.read().map_err(|_| InnerReadError)?; + Ok(data) + } +} + +impl From for PySparseObservable { + fn from(val: SparseObservable) -> PySparseObservable { + PySparseObservable { + inner: Arc::new(RwLock::new(val)), + } + } +} + +impl<'py> IntoPyObject<'py> for SparseObservable { + type Target = PySparseObservable; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> PyResult { + PySparseObservable::from(self).into_pyobject(py) + } +} + +/// Helper class of `ArrayView` that denotes the slot of the `SparseObservable` we're looking at. +#[derive(Clone, Copy, PartialEq, Eq)] +enum ArraySlot { + Coeffs, + BitTerms, + Indices, + Boundaries, +} + +/// Custom wrapper sequence class to get safe views onto the Rust-space data. We can't directly +/// expose Python-managed wrapped pointers without introducing some form of runtime exclusion on the +/// ability of `SparseObservable` to re-allocate in place; we can't leave dangling pointers for +/// Python space. +#[pyclass(frozen, sequence)] +struct ArrayView { + base: Arc>, + slot: ArraySlot, +} + +#[pymethods] +impl ArrayView { + fn __repr__(&self, py: Python) -> PyResult { + let obs = self.base.read().map_err(|_| InnerReadError)?; + let data = match self.slot { + // Simple integers look the same in Rust-space debug as Python. + ArraySlot::Indices => format!("{:?}", obs.indices()), + ArraySlot::Boundaries => format!("{:?}", obs.boundaries()), + // Complexes don't have a nice repr in Rust, so just delegate the whole load to Python + // and convert back. + ArraySlot::Coeffs => PyList::new(py, obs.coeffs())?.repr()?.to_string(), + ArraySlot::BitTerms => format!( + "[{}]", + obs.bit_terms() + .iter() + .map(BitTerm::py_label) + .collect::>() + .join(", ") + ), + }; + Ok(format!( + "", + match self.slot { + ArraySlot::Coeffs => "coeffs", + ArraySlot::BitTerms => "bit_terms", + ArraySlot::Indices => "indices", + ArraySlot::Boundaries => "boundaries", + }, + data, + )) + } + + fn __getitem__<'py>( + &self, + py: Python<'py>, + index: PySequenceIndex, + ) -> PyResult> { + // The slightly verbose generic setup here is to allow the type of a scalar return to be + // different to the type that gets put into the Numpy array, since the `BitTerm` enum can be + // a direct scalar, but for Numpy, we need it to be a raw `u8`. + fn get_from_slice<'py, T, S>( + py: Python<'py>, + slice: &[T], + index: PySequenceIndex, + ) -> PyResult> + where + T: IntoPyObject<'py> + Copy + Into, + S: ::numpy::Element, + { + match index.with_len(slice.len())? { + SequenceIndex::Int(index) => slice[index].into_bound_py_any(py), + indices => PyArray1::from_iter(py, indices.iter().map(|index| slice[index].into())) + .into_bound_py_any(py), + } + } + + let obs = self.base.read().map_err(|_| InnerReadError)?; + match self.slot { + ArraySlot::Coeffs => get_from_slice::<_, Complex64>(py, obs.coeffs(), index), + ArraySlot::BitTerms => get_from_slice::<_, u8>(py, obs.bit_terms(), index), + ArraySlot::Indices => get_from_slice::<_, u32>(py, obs.indices(), index), + ArraySlot::Boundaries => get_from_slice::<_, usize>(py, obs.boundaries(), index), + } + } + + fn __setitem__(&self, index: PySequenceIndex, values: &Bound) -> PyResult<()> { + /// Set values of a slice according to the indexer, using `extract` to retrieve the + /// Rust-space object from the collection of Python-space values. + /// + /// This indirects the Python extraction through an intermediate type to marginally improve + /// the error messages for things like `BitTerm`, where Python-space extraction might fail + /// because the user supplied an invalid alphabet letter. + /// + /// This allows broadcasting a single item into many locations in a slice (like Numpy), but + /// otherwise requires that the index and values are the same length (unlike Python's + /// `list`) because that would change the length. + fn set_in_slice<'a, 'py, T, S>( + slice: &mut [T], + index: PySequenceIndex<'py>, + values: Borrowed<'a, 'py, PyAny>, + ) -> PyResult<()> + where + T: Copy + TryFrom, + S: for<'b> FromPyObject<'b, 'py, Error: Into>, + PyErr: From<>::Error>, + { + match index.with_len(slice.len())? { + SequenceIndex::Int(index) => { + slice[index] = values + .extract::() + .map_err(Into::::into)? + .try_into()?; + Ok(()) + } + indices => { + if let Ok(value) = values.extract::() { + let value = value.try_into()?; + for index in indices { + slice[index] = value; + } + } else { + let values = values + .try_iter()? + .map(|value| { + value? + .extract::() + .map_err(Into::::into)? + .try_into() + .map_err(PyErr::from) + }) + .collect::>>()?; + if indices.len() != values.len() { + return Err(PyValueError::new_err(format!( + "tried to set a slice of length {} with a sequence of length {}", + indices.len(), + values.len(), + ))); + } + for (index, value) in indices.into_iter().zip(values) { + slice[index] = value; + } + } + Ok(()) + } + } + } + + let mut obs = self.base.write().map_err(|_| InnerWriteError)?; + let values = values.as_borrowed(); + match self.slot { + ArraySlot::Coeffs => set_in_slice::<_, Complex64>(obs.coeffs_mut(), index, values), + ArraySlot::BitTerms => set_in_slice::(obs.bit_terms_mut(), index, values), + ArraySlot::Indices => unsafe { + set_in_slice::<_, u32>(obs.indices_mut(), index, values) + }, + ArraySlot::Boundaries => unsafe { + set_in_slice::<_, usize>(obs.boundaries_mut(), index, values) + }, + } + } + + fn __len__(&self, _py: Python) -> PyResult { + let obs = self.base.read().map_err(|_| InnerReadError)?; + let len = match self.slot { + ArraySlot::Coeffs => obs.coeffs().len(), + ArraySlot::BitTerms => obs.bit_terms().len(), + ArraySlot::Indices => obs.indices().len(), + ArraySlot::Boundaries => obs.boundaries().len(), + }; + Ok(len) + } + + #[pyo3(signature = (/, dtype=None, copy=None))] + fn __array__<'py>( + &self, + py: Python<'py>, + dtype: Option<&Bound<'py, PyAny>>, + copy: Option, + ) -> PyResult> { + // This method always copies, so we don't leave dangling pointers lying around in Numpy + // arrays; it's not enough just to set the `base` of the Numpy array to the + // `SparseObservable`, since the `Vec` we're referring to might re-allocate and invalidate + // the pointer the Numpy array is wrapping. + if !copy.unwrap_or(true) { + return Err(PyValueError::new_err( + "cannot produce a safe view onto movable memory", + )); + } + let obs = self.base.read().map_err(|_| InnerReadError)?; + match self.slot { + ArraySlot::Coeffs => cast_array_type(py, PyArray1::from_slice(py, obs.coeffs()), dtype), + ArraySlot::Indices => { + cast_array_type(py, PyArray1::from_slice(py, obs.indices()), dtype) + } + ArraySlot::Boundaries => { + cast_array_type(py, PyArray1::from_slice(py, obs.boundaries()), dtype) + } + ArraySlot::BitTerms => { + let bit_terms: &[u8] = ::bytemuck::cast_slice(obs.bit_terms()); + cast_array_type(py, PyArray1::from_slice(py, bit_terms), dtype) + } + } + } +} + +/// Use the Numpy Python API to convert a `PyArray` into a dynamically chosen `dtype`, copying only +/// if required. +fn cast_array_type<'py, T: numpy::Element>( + py: Python<'py>, + array: Bound<'py, PyArray1>, + dtype: Option<&Bound<'py, PyAny>>, +) -> PyResult> { + let base_dtype = array.dtype(); + let dtype = dtype + .map(|dtype| PyArrayDescr::new(py, dtype)) + .unwrap_or_else(|| Ok(base_dtype.clone()))?; + if dtype.is_equiv_to(&base_dtype) { + return Ok(array.into_any()); + } + PyModule::import(py, intern!(py, "numpy"))? + .getattr(intern!(py, "array"))? + .call( + (array,), + Some(&[(intern!(py, "dtype"), dtype.as_any())].into_py_dict(py)?), + ) +} + +/// Attempt to coerce an arbitrary Python object to a [PySparseObservable]. +/// +/// This returns: +/// +/// * `Ok(Some(obs))` if the coercion was completely successful. +/// * `Ok(None)` if the input value was just completely the wrong type and no coercion could be +/// attempted. +/// * `Err` if the input was a valid type for coercion, but the coercion failed with a Python +/// exception. +/// +/// The purpose of this is for conversion the arithmetic operations, which should return +/// [PyNotImplemented] if the type is not valid for coercion. +fn coerce_to_observable<'py>( + value: &Bound<'py, PyAny>, +) -> PyResult>> { + let py = value.py(); + if let Ok(obs) = value.cast_exact::() { + return Ok(Some(obs.clone())); + } + match PySparseObservable::py_new(value, None) { + Ok(obs) => Ok(Some(Bound::new(py, obs)?)), + Err(e) => { + if e.is_instance_of::(py) { + Ok(None) + } else { + Err(e) + } + } + } +} + +pub fn sparse_observable(m: &Bound) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} diff --git a/crates/quantum_info/src/sparse_pauli_op.rs b/crates/quantum_info/src/python/sparse_pauli_op.rs similarity index 99% rename from crates/quantum_info/src/sparse_pauli_op.rs rename to crates/quantum_info/src/python/sparse_pauli_op.rs index 8f6d91fe94cf..9375befc2f1e 100644 --- a/crates/quantum_info/src/sparse_pauli_op.rs +++ b/crates/quantum_info/src/python/sparse_pauli_op.rs @@ -30,7 +30,7 @@ use thiserror::Error; use qiskit_util::complex::{C_ZERO, c64}; -use crate::rayon_ext::*; +use super::rayon_ext::*; #[derive(Error, Debug)] pub enum PauliCompressionError { diff --git a/crates/quantum_info/src/sparse_observable/mod.rs b/crates/quantum_info/src/sparse_observable/mod.rs index 42a8b9d6aa20..454976ddc001 100644 --- a/crates/quantum_info/src/sparse_observable/mod.rs +++ b/crates/quantum_info/src/sparse_observable/mod.rs @@ -15,48 +15,10 @@ mod lookup; use hashbrown::HashSet; use itertools::Itertools; use lookup::conjugate_bitterm; -#[cfg(feature = "python")] -use ndarray::Array2; use num_complex::Complex64; -#[cfg(feature = "python")] -use num_traits::Zero; -#[cfg(feature = "python")] -use numpy::{ - PyArray1, PyArray2, PyArrayDescr, PyArrayDescrMethods, PyArrayLike1, PyArrayMethods, - PyReadonlyArray1, PyReadonlyArray2, PyUntypedArrayMethods, -}; -#[cfg(feature = "python")] -use pyo3::{ - IntoPyObjectExt, PyErr, - exceptions::{PyRuntimeError, PyTypeError, PyValueError, PyZeroDivisionError}, - intern, - prelude::*, - sync::PyOnceLock, - types::{IntoPyDict, PyList, PyString, PyTuple, PyType}, -}; -#[cfg(feature = "python")] -use qiskit_util::IndexSet; -#[cfg(feature = "python")] -use qiskit_util::py::{ImportOnceCell, PySequenceIndex, SequenceIndex}; -#[cfg(feature = "python")] -use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign}; -#[cfg(feature = "python")] -use std::sync::{Arc, RwLock, RwLockReadGuard}; use std::{cmp::Ordering, collections::btree_map}; use thiserror::Error; -#[cfg(feature = "python")] -static PAULI_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "Pauli"); -#[cfg(feature = "python")] -static PAULI_LIST_TYPE: ImportOnceCell = ImportOnceCell::new("qiskit.quantum_info", "PauliList"); -#[cfg(feature = "python")] -static SPARSE_PAULI_OP_TYPE: ImportOnceCell = - ImportOnceCell::new("qiskit.quantum_info", "SparsePauliOp"); -#[cfg(feature = "python")] -static BIT_TERM_PY_ENUM: PyOnceLock> = PyOnceLock::new(); -#[cfg(feature = "python")] -static BIT_TERM_INTO_PY: PyOnceLock<[Option>; 16]> = PyOnceLock::new(); - /// Named handle to the alphabet of single-qubit terms. /// /// This is just the Rust-space representation. We make a separate Python-space `enum.IntEnum` to @@ -169,7 +131,7 @@ impl BitTerm { /// returning `Ok(None)` for it. All other letters outside the alphabet return the complete /// error condition. #[inline] - fn try_from_u8(value: u8) -> Result, BitTermFromU8Error> { + pub(crate) fn try_from_u8(value: u8) -> Result, BitTermFromU8Error> { match value { b'+' => Ok(Some(BitTerm::Plus)), b'-' => Ok(Some(BitTerm::Minus)), @@ -1817,2713 +1779,12 @@ impl SparseTerm { #[derive(Error, Debug)] pub struct InnerReadError; -#[cfg(feature = "python")] // Only currently used by python, remove if needed from rust -#[derive(Error, Debug)] -struct InnerWriteError; - impl ::std::fmt::Display for InnerReadError { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { write!(f, "Failed acquiring lock for reading.") } } -#[cfg(feature = "python")] // Only currently used by python, remove if needed from rust -impl ::std::fmt::Display for InnerWriteError { - fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - write!(f, "Failed acquiring lock for writing.") - } -} - -#[cfg(feature = "python")] -impl From for PyErr { - fn from(value: InnerReadError) -> PyErr { - PyRuntimeError::new_err(value.to_string()) - } -} -#[cfg(feature = "python")] -impl From for PyErr { - fn from(value: InnerWriteError) -> PyErr { - PyRuntimeError::new_err(value.to_string()) - } -} - -#[cfg(feature = "python")] -impl From for PyErr { - fn from(value: BitTermFromU8Error) -> PyErr { - PyValueError::new_err(value.to_string()) - } -} - -#[cfg(feature = "python")] -impl From for PyErr { - fn from(value: CoherenceError) -> PyErr { - PyValueError::new_err(value.to_string()) - } -} - -#[cfg(feature = "python")] -impl From for PyErr { - fn from(value: LabelError) -> PyErr { - PyValueError::new_err(value.to_string()) - } -} - -#[cfg(feature = "python")] -impl From for PyErr { - fn from(value: ArithmeticError) -> PyErr { - PyValueError::new_err(value.to_string()) - } -} - -/// The single-character string label used to represent this term in the :class:`SparseObservable` -/// alphabet. - -#[cfg(feature = "python")] -#[pyfunction] -#[pyo3(name = "label")] -fn bit_term_label(py: Python<'_>, slf: BitTerm) -> &Bound<'_, PyString> { - // This doesn't use `py_label` so we can use `intern!`. - match slf { - BitTerm::X => intern!(py, "X"), - BitTerm::Plus => intern!(py, "+"), - BitTerm::Minus => intern!(py, "-"), - BitTerm::Y => intern!(py, "Y"), - BitTerm::Right => intern!(py, "r"), - BitTerm::Left => intern!(py, "l"), - BitTerm::Z => intern!(py, "Z"), - BitTerm::Zero => intern!(py, "0"), - BitTerm::One => intern!(py, "1"), - } -} -/// Construct the Python-space `IntEnum` that represents the same values as the Rust-spce `BitTerm`. -/// -/// We don't make `BitTerm` a direct `pyclass` because we want the behaviour of `IntEnum`, which -/// specifically also makes its variants subclasses of the Python `int` type; we use a type-safe -/// enum in Rust, but from Python space we expect people to (carefully) deal with the raw ints in -/// Numpy arrays for efficiency. -/// -/// The resulting class is attached to `SparseObservable` as a class attribute, and its -/// `__qualname__` is set to reflect this. -#[cfg(feature = "python")] -fn make_py_bit_term(py: Python) -> PyResult> { - let terms = [ - BitTerm::X, - BitTerm::Plus, - BitTerm::Minus, - BitTerm::Y, - BitTerm::Right, - BitTerm::Left, - BitTerm::Z, - BitTerm::Zero, - BitTerm::One, - ] - .into_iter() - .flat_map(|term| { - let mut out = vec![(term.py_name(), term as u8)]; - if term.py_name() != term.py_label() { - // Also ensure that the labels are created as aliases. These can't be (easily) accessed - // by attribute-getter (dot) syntax, but will work with the item-getter (square-bracket) - // syntax, or programmatically with `getattr`. - out.push((term.py_label(), term as u8)); - } - out - }) - .collect::>(); - let obj = py.import("enum")?.getattr("IntEnum")?.call( - ("BitTerm", terms), - Some( - &[ - ("module", "qiskit.quantum_info"), - ("qualname", "SparseObservable.BitTerm"), - ] - .into_py_dict(py)?, - ), - )?; - let label_property = py - .import("builtins")? - .getattr("property")? - .call1((wrap_pyfunction!(bit_term_label, py)?,))?; - obj.setattr("label", label_property)?; - Ok(obj.cast_into::()?.unbind()) -} - -// Return the relevant value from the Python-space sister enumeration. These are Python-space -// singletons and subclasses of Python `int`. We only use this for interaction with "high level" -// Python space; the efficient Numpy-like array paths use `u8` directly so Numpy can act on it -// efficiently. -#[cfg(feature = "python")] -impl<'py> IntoPyObject<'py> for BitTerm { - type Target = PyAny; - type Output = Bound<'py, PyAny>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> PyResult { - let terms = BIT_TERM_INTO_PY.get_or_init(py, || { - let py_enum = BIT_TERM_PY_ENUM - .get_or_try_init(py, || make_py_bit_term(py)) - .expect("creating a simple Python enum class should be infallible") - .bind(py); - ::std::array::from_fn(|val| { - ::bytemuck::checked::try_cast(val as u8) - .ok() - .map(|term: BitTerm| { - py_enum - .getattr(term.py_name()) - .expect("the created `BitTerm` enum should have matching attribute names to the terms") - .unbind() - }) - }) - }); - Ok(terms[self as usize] - .as_ref() - .expect("the lookup table initializer populated a 'Some' in all valid locations") - .bind(py) - .clone()) - } -} - -#[cfg(feature = "python")] -impl<'a, 'py> FromPyObject<'a, 'py> for BitTerm { - type Error = PyErr; - - fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { - let value = ob - .extract::() - .map_err(|_| match ob.get_type().repr() { - Ok(repr) => PyTypeError::new_err(format!("bad type for 'BitTerm': {repr}")), - Err(err) => err, - })?; - let value_error = || { - PyValueError::new_err(format!( - "value {value} is not a valid letter of the single-qubit alphabet for 'BitTerm'" - )) - }; - let value: u8 = value.try_into().map_err(|_| value_error())?; - value.try_into().map_err(|_| value_error()) - } -} - -/// A single term from a complete :class:`SparseObservable`. -/// -/// These are typically created by indexing into or iterating through a :class:`SparseObservable`. -#[cfg(feature = "python")] -#[pyclass( - name = "Term", - frozen, - module = "qiskit.quantum_info", - skip_from_py_object -)] -#[derive(Clone, Debug)] -struct PySparseTerm { - inner: SparseTerm, -} - -#[cfg(feature = "python")] -#[pymethods] -impl PySparseTerm { - // Mark the Python class as being defined "within" the `SparseObservable` class namespace. - #[classattr] - #[pyo3(name = "__qualname__")] - fn type_qualname() -> &'static str { - "SparseObservable.Term" - } - - #[new] - #[pyo3(signature = (/, num_qubits, coeff, bit_terms, indices))] - fn py_new( - num_qubits: u32, - coeff: Complex64, - bit_terms: Vec, - indices: Vec, - ) -> PyResult { - if bit_terms.len() != indices.len() { - return Err(CoherenceError::MismatchedItemCount { - bit_terms: bit_terms.len(), - indices: indices.len(), - } - .into()); - } - let mut order = (0..bit_terms.len()).collect::>(); - order.sort_unstable_by_key(|a| indices[*a]); - let bit_terms = order.iter().map(|i| bit_terms[*i]).collect(); - let mut sorted_indices = Vec::::with_capacity(order.len()); - for i in order { - let index = indices[i]; - if sorted_indices - .last() - .map(|prev| *prev >= index) - .unwrap_or(false) - { - return Err(CoherenceError::UnsortedIndices.into()); - } - sorted_indices.push(index) - } - let inner = SparseTerm::new( - num_qubits, - coeff, - bit_terms, - sorted_indices.into_boxed_slice(), - )?; - Ok(PySparseTerm { inner }) - } - - /// Convert this term to a complete :class:`SparseObservable`. - fn to_observable(&self) -> PyResult { - let obs = SparseObservable::new( - self.inner.num_qubits(), - vec![self.inner.coeff()], - self.inner.bit_terms().to_vec(), - self.inner.indices().to_vec(), - vec![0, self.inner.bit_terms().len()], - )?; - Ok(obs.into()) - } - - fn to_label(&self) -> PyResult { - Ok(self.inner.view().to_sparse_str()) - } - - fn __eq__(slf: Bound, other: Bound) -> PyResult { - if slf.is(&other) { - return Ok(true); - } - let Ok(other) = other.cast_into::() else { - return Ok(false); - }; - let slf = slf.borrow(); - let other = other.borrow(); - Ok(slf.inner.eq(&other.inner)) - } - - fn __repr__(&self) -> PyResult { - Ok(format!( - "<{} on {} qubit{}: {}>", - Self::type_qualname(), - self.inner.num_qubits(), - if self.inner.num_qubits() == 1 { - "" - } else { - "s" - }, - self.inner.view().to_sparse_str(), - )) - } - - fn __getnewargs__(slf_: Bound) -> PyResult> { - let py = slf_.py(); - let borrowed = slf_.borrow(); - ( - borrowed.inner.num_qubits(), - borrowed.inner.coeff(), - Self::get_bit_terms(slf_.clone()), - Self::get_indices(slf_), - ) - .into_pyobject(py) - } - - /// Get a copy of this term. - fn copy(&self) -> Self { - self.clone() - } - - /// Read-only view onto the individual single-qubit terms. - /// - /// The only valid values in the array are those with a corresponding - /// :class:`~SparseObservable.BitTerm`. - #[getter] - fn get_bit_terms(slf_: Bound) -> Bound> { - let borrowed = slf_.borrow(); - let bit_terms = borrowed.inner.bit_terms(); - let arr = ::ndarray::aview1(::bytemuck::cast_slice::<_, u8>(bit_terms)); - // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. - // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the - // `Box<[BitTerm]>` allocation (including dropping or reallocating it) other than the entire - // object getting dropped, which Python will keep safe. - let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; - out.readwrite().make_nonwriteable(); - out - } - - /// The number of qubits the term is defined on. - #[getter] - fn get_num_qubits(&self) -> u32 { - self.inner.num_qubits() - } - - /// The term's coefficient. - #[getter] - fn get_coeff(&self) -> Complex64 { - self.inner.coeff() - } - - /// Read-only view onto the indices of each non-identity single-qubit term. - /// - /// The indices will always be in sorted order. - #[getter] - fn get_indices(slf_: Bound) -> Bound> { - let borrowed = slf_.borrow(); - let indices = borrowed.inner.indices(); - let arr = ::ndarray::aview1(indices); - // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python. - // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the - // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire - // object getting dropped, which Python will keep safe. - let out = unsafe { PyArray1::borrow_from_array(&arr, slf_.into_any()) }; - out.readwrite().make_nonwriteable(); - out - } - - /// Get a :class:`~.quantum_info.Pauli` object that represents the measurement basis needed for - /// this term. - /// - /// For example, the projector ``0l+`` will return a Pauli ``ZYX``. The resulting - /// :class:`~.quantum_info.Pauli` is dense, in the sense that explicit identities are stored. - /// An identity in the Pauli output does not require a concrete measurement. - /// - /// Returns: :class:`~.quantum_info.Pauli`: the Pauli operator representing the necessary - /// measurement basis. - /// - /// See also: - /// :meth:`SparseObservable.pauli_bases` - /// A similar method for an entire observable at once. - fn pauli_base<'py>(&self, py: Python<'py>) -> PyResult> { - let mut x = vec![false; self.inner.num_qubits() as usize]; - let mut z = vec![false; self.inner.num_qubits() as usize]; - for (bit_term, index) in self - .inner - .bit_terms() - .iter() - .zip(self.inner.indices().iter()) - { - x[*index as usize] = bit_term.has_x_component(); - z[*index as usize] = bit_term.has_z_component(); - } - PAULI_TYPE - .get_bound(py) - .call1(((PyArray1::from_vec(py, z), PyArray1::from_vec(py, x)),)) - } - - /// Return the bit labels of the term as string. - /// - /// The bit labels will match the order of :attr:`.SparseTerm.indices`, such that the - /// i-th character in the string is applied to the qubit index at ``term.indices[i]``. - /// - /// Returns: - /// The non-identity bit terms as concatenated string. - fn bit_labels<'py>(&self, py: Python<'py>) -> Bound<'py, PyString> { - let string: String = self - .inner - .bit_terms() - .iter() - .map(|bit| bit.py_label()) - .collect(); - PyString::new(py, string.as_str()) - } -} - -/// An observable over Pauli bases that stores its data in a qubit-sparse format. -/// -/// Mathematics -/// =========== -/// -/// This observable represents a sum over strings of the Pauli operators and Pauli-eigenstate -/// projectors, with each term weighted by some complex number. That is, the full observable is -/// -/// .. math:: -/// -/// \text{\texttt{SparseObservable}} = \sum_i c_i \bigotimes_n A^{(n)}_i -/// -/// for complex numbers :math:`c_i` and single-qubit operators acting on qubit :math:`n` from a -/// restricted alphabet :math:`A^{(n)}_i`. The sum over :math:`i` is the sum of the individual -/// terms, and the tensor product produces the operator strings. -/// -/// The alphabet of allowed single-qubit operators that the :math:`A^{(n)}_i` are drawn from is the -/// Pauli operators and the Pauli-eigenstate projection operators. Explicitly, these are: -/// -/// .. _sparse-observable-alphabet: -/// .. table:: Alphabet of single-qubit terms used in :class:`SparseObservable` -/// -/// ======= ======================================= =============== =========================== -/// Label Operator Numeric value :class:`.BitTerm` attribute -/// ======= ======================================= =============== =========================== -/// ``"I"`` :math:`I` (identity) Not stored. Not stored. -/// -/// ``"X"`` :math:`X` (Pauli X) ``0b0010`` (2) :attr:`~.BitTerm.X` -/// -/// ``"Y"`` :math:`Y` (Pauli Y) ``0b0011`` (3) :attr:`~.BitTerm.Y` -/// -/// ``"Z"`` :math:`Z` (Pauli Z) ``0b0001`` (1) :attr:`~.BitTerm.Z` -/// -/// ``"+"`` :math:`\lvert+\rangle\langle+\rvert` ``0b1010`` (10) :attr:`~.BitTerm.PLUS` -/// (projector to positive eigenstate of X) -/// -/// ``"-"`` :math:`\lvert-\rangle\langle-\rvert` ``0b0110`` (6) :attr:`~.BitTerm.MINUS` -/// (projector to negative eigenstate of X) -/// -/// ``"r"`` :math:`\lvert r\rangle\langle r\rvert` ``0b1011`` (11) :attr:`~.BitTerm.RIGHT` -/// (projector to positive eigenstate of Y) -/// -/// ``"l"`` :math:`\lvert l\rangle\langle l\rvert` ``0b0111`` (7) :attr:`~.BitTerm.LEFT` -/// (projector to negative eigenstate of Y) -/// -/// ``"0"`` :math:`\lvert0\rangle\langle0\rvert` ``0b1001`` (9) :attr:`~.BitTerm.ZERO` -/// (projector to positive eigenstate of Z) -/// -/// ``"1"`` :math:`\lvert1\rangle\langle1\rvert` ``0b0101`` (5) :attr:`~.BitTerm.ONE` -/// (projector to negative eigenstate of Z) -/// ======= ======================================= =============== =========================== -/// -/// The allowed alphabet forms an overcomplete basis of the operator space. This means that there -/// is not a unique summation to represent a given observable. By comparison, -/// :class:`.SparsePauliOp` uses a precise basis of the operator space, so (after combining terms of -/// the same Pauli string, removing zeros, and sorting the terms to :ref:`some canonical order -/// `) there is only one representation of any operator. -/// -/// :class:`SparseObservable` uses its particular overcomplete basis with the aim of making -/// "efficiency of measurement" equivalent to "efficiency of representation". For example, the -/// observable :math:`{\lvert0\rangle\langle0\rvert}^{\otimes n}` can be efficiently measured on -/// hardware with simple :math:`Z` measurements, but can only be represented by -/// :class:`.SparsePauliOp` as :math:`{(I + Z)}^{\otimes n}/2^n`, which requires :math:`2^n` stored -/// terms. :class:`SparseObservable` requires only a single term to store this. -/// -/// The downside to this is that it is impractical to take an arbitrary matrix or -/// :class:`.SparsePauliOp` and find the *best* :class:`SparseObservable` representation. You -/// typically will want to construct a :class:`SparseObservable` directly, rather than trying to -/// decompose into one. -/// -/// -/// Representation -/// ============== -/// -/// The internal representation of a :class:`SparseObservable` stores only the non-identity qubit -/// operators. This makes it significantly more efficient to represent observables such as -/// :math:`\sum_{n\in \text{qubits}} Z^{(n)}`; :class:`SparseObservable` requires an amount of -/// memory linear in the total number of qubits, while :class:`.SparsePauliOp` scales quadratically. -/// -/// The terms are stored compressed, similar in spirit to the compressed sparse row format of sparse -/// matrices. In this analogy, the terms of the sum are the "rows", and the qubit terms are the -/// "columns", where an absent entry represents the identity rather than a zero. More explicitly, -/// the representation is made up of four contiguous arrays: -/// -/// .. _sparse-observable-arrays: -/// .. table:: Data arrays used to represent :class:`.SparseObservable` -/// -/// ================== =========== ============================================================= -/// Attribute Length Description -/// ================== =========== ============================================================= -/// :attr:`coeffs` :math:`t` The complex scalar multiplier for each term. -/// -/// :attr:`bit_terms` :math:`s` Each of the non-identity single-qubit terms for all of the -/// operators, in order. These correspond to the non-identity -/// :math:`A^{(n)}_i` in the sum description, where the entries -/// are stored in order of increasing :math:`i` first, and in -/// order of increasing :math:`n` within each term. -/// -/// :attr:`indices` :math:`s` The corresponding qubit (:math:`n`) for each of the operators -/// in :attr:`bit_terms`. :class:`SparseObservable` requires -/// that this list is term-wise sorted, and algorithms can rely -/// on this invariant being upheld. -/// -/// :attr:`boundaries` :math:`t+1` The indices that partition :attr:`bit_terms` and -/// :attr:`indices` into complete terms. For term number -/// :math:`i`, its complex coefficient is ``coeffs[i]``, and its -/// non-identity single-qubit operators and their corresponding -/// qubits are the slice ``boundaries[i] : boundaries[i+1]`` into -/// :attr:`bit_terms` and :attr:`indices` respectively. -/// :attr:`boundaries` always has an explicit 0 as its first -/// element. -/// ================== =========== ============================================================= -/// -/// The length parameter :math:`t` is the number of terms in the sum, and the parameter :math:`s` is -/// the total number of non-identity single-qubit terms. -/// -/// As illustrative examples: -/// -/// * in the case of a zero operator, :attr:`boundaries` is length 1 (a single 0) and all other -/// vectors are empty. -/// * in the case of a fully simplified identity operator, :attr:`boundaries` is ``[0, 0]``, -/// :attr:`coeffs` has a single entry, and :attr:`bit_terms` and :attr:`indices` are empty. -/// * for the operator :math:`Z_2 Z_0 - X_3 Y_1`, :attr:`boundaries` is ``[0, 2, 4]``, -/// :attr:`coeffs` is ``[1.0, -1.0]``, :attr:`bit_terms` is ``[BitTerm.Z, BitTerm.Z, BitTerm.Y, -/// BitTerm.X]`` and :attr:`indices` is ``[0, 2, 1, 3]``. The operator might act on more than -/// four qubits, depending on the :attr:`num_qubits` parameter. The :attr:`bit_terms` are integer -/// values, whose magic numbers can be accessed via the :class:`BitTerm` attribute class. Note -/// that the single-bit terms and indices are sorted into termwise sorted order. This is a -/// requirement of the class. -/// -/// These cases are not special, they're fully consistent with the rules and should not need special -/// handling. -/// -/// The scalar item of the :attr:`bit_terms` array is stored as a numeric byte. The numeric values -/// are related to the symplectic Pauli representation that :class:`.SparsePauliOp` uses, and are -/// accessible with named access by an enumeration: -/// -/// .. -/// This is documented manually here because the Python-space `Enum` is generated -/// programmatically from Rust - it'd be _more_ confusing to try and write a docstring somewhere -/// else in this source file. The use of `autoattribute` is because it pulls in the numeric -/// value. -/// -/// .. py:class:: SparseObservable.BitTerm -/// -/// An :class:`~enum.IntEnum` that provides named access to the numerical values used to -/// represent each of the single-qubit alphabet terms enumerated in -/// :ref:`sparse-observable-alphabet`. -/// -/// This class is attached to :class:`.SparseObservable`. Access it as -/// :class:`.SparseObservable.BitTerm`. If this is too much typing, and you are solely dealing -/// with :class:¬SparseObservable` objects and the :class:`BitTerm` name is not ambiguous, you -/// might want to shorten it as:: -/// -/// >>> ops = SparseObservable.BitTerm -/// >>> assert ops.X is SparseObservable.BitTerm.X -/// -/// You can access all the values of the enumeration by either their full all-capitals name, or -/// by their single-letter label. The single-letter labels are not generally valid Python -/// identifiers, so you must use indexing notation to access them:: -/// -/// >>> assert SparseObservable.BitTerm.ZERO is SparseObservable.BitTerm["0"] -/// -/// The numeric structure of these is that they are all four-bit values of which the low two -/// bits are the (phase-less) symplectic representation of the Pauli operator related to the -/// object, where the low bit denotes a contribution by :math:`Z` and the second lowest a -/// contribution by :math:`X`, while the upper two bits are ``00`` for a Pauli operator, ``01`` -/// for the negative-eigenstate projector, and ``10`` for the positive-eigenstate projector. -/// -/// Values -/// ------ -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.X -/// -/// The Pauli :math:`X` operator. Uses the single-letter label ``"X"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.PLUS -/// -/// The projector to the positive eigenstate of the :math:`X` operator: -/// :math:`\lvert+\rangle\langle+\rvert`. Uses the single-letter label ``"+"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.MINUS -/// -/// The projector to the negative eigenstate of the :math:`X` operator: -/// :math:`\lvert-\rangle\langle-\rvert`. Uses the single-letter label ``"-"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.Y -/// -/// The Pauli :math:`Y` operator. Uses the single-letter label ``"Y"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.RIGHT -/// -/// The projector to the positive eigenstate of the :math:`Y` operator: -/// :math:`\lvert r\rangle\langle r\rvert`. Uses the single-letter label ``"r"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.LEFT -/// -/// The projector to the negative eigenstate of the :math:`Y` operator: -/// :math:`\lvert l\rangle\langle l\rvert`. Uses the single-letter label ``"l"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.Z -/// -/// The Pauli :math:`Z` operator. Uses the single-letter label ``"Z"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.ZERO -/// -/// The projector to the positive eigenstate of the :math:`Z` operator: -/// :math:`\lvert0\rangle\langle0\rvert`. Uses the single-letter label ``"0"``. -/// -/// .. autoattribute:: qiskit.quantum_info::SparseObservable.BitTerm.ONE -/// -/// The projector to the negative eigenstate of the :math:`Z` operator: -/// :math:`\lvert1\rangle\langle1\rvert`. Uses the single-letter label ``"1"``. -/// -/// Attributes -/// ---------- -/// -/// .. autoproperty:: qiskit.quantum_info::SparseObservable.BitTerm.label -/// -/// Each of the array-like attributes behaves like a Python sequence. You can index and slice these -/// with standard :class:`list`-like semantics. Slicing an attribute returns a Numpy -/// :class:`~numpy.ndarray` containing a copy of the relevant data with the natural ``dtype`` of the -/// field; this lets you easily do mathematics on the results, like bitwise operations on -/// :attr:`bit_terms`. You can assign to indices or slices of each of the attributes, but beware -/// that you must uphold :ref:`the data coherence rules ` while doing -/// this. For example:: -/// -/// >>> obs = SparseObservable.from_list([("XZY", 1.5j), ("+1r", -0.5)]) -/// >>> assert isinstance(obs.coeffs[:], np.ndarray) -/// >>> # Reduce all single-qubit terms to the relevant Pauli operator, if they are a projector. -/// >>> obs.bit_terms[:] = obs.bit_terms[:] & 0b00_11 -/// >>> assert obs == SparseObservable.from_list([("XZY", 1.5j), ("XZY", -0.5)]) -/// -/// .. note:: -/// -/// The above reduction to the Pauli bases can also be achieved with :meth:`pauli_bases`. -/// -/// .. _sparse-observable-canonical-order: -/// -/// Canonical ordering -/// ------------------ -/// -/// For any given mathematical observable, there are several ways of representing it with -/// :class:`SparseObservable`. For example, the same set of single-bit terms and their -/// corresponding indices might appear multiple times in the observable. Mathematically, this is -/// equivalent to having only a single term with all the coefficients summed. Similarly, the terms -/// of the sum in a :class:`SparseObservable` can be in any order while representing the same -/// observable, since addition is commutative (although while floating-point addition is not -/// associative, :class:`SparseObservable` makes no guarantees about the summation order). -/// -/// These two categories of representation degeneracy can cause the ``==`` operator to claim that -/// two observables are not equal, despite representing the same object. In these cases, it can -/// be convenient to define some *canonical form*, which allows observables to be compared -/// structurally. -/// -/// You can put a :class:`SparseObservable` in canonical form by using the :meth:`simplify` method. -/// The precise ordering of terms in canonical ordering is not specified, and may change between -/// versions of Qiskit. Within the same version of Qiskit, however, you can compare two observables -/// structurally by comparing their simplified forms. -/// -/// .. note:: -/// -/// If you wish to account for floating-point tolerance in the comparison, it is safest to use -/// a recipe such as:: -/// -/// def equivalent(left, right, tol): -/// return (left - right).simplify(tol) == SparseObservable.zero(left.num_qubits) -/// -/// .. note:: -/// -/// The canonical form produced by :meth:`simplify` alone will not universally detect all -/// observables that are equivalent due to the over-complete basis alphabet. To obtain a -/// unique expression, you can first represent the observable using Pauli terms only by -/// calling :meth:`as_paulis`, followed by :meth:`simplify`. Note that the projector -/// expansion (e.g. ``+`` into ``I`` and ``X``) is not computationally feasible at scale. -/// -/// Indexing -/// -------- -/// -/// :class:`SparseObservable` behaves as `a Python sequence -/// `__ (the standard form, not the expanded -/// :class:`collections.abc.Sequence`). The observable can be indexed by integers, and iterated -/// through to yield individual terms. -/// -/// Each term appears as an instance a self-contained class. The individual terms are copied out of -/// the base observable; mutations to them will not affect the observable. -/// -/// .. autoclass:: qiskit.quantum_info::SparseObservable.Term -/// :members: -/// -/// Construction -/// ============ -/// -/// :class:`SparseObservable` defines several constructors. The default constructor will attempt to -/// delegate to one of the more specific constructors, based on the type of the input. You can -/// always use the specific constructors to have more control over the construction. -/// -/// .. _sparse-observable-convert-constructors: -/// .. table:: Construction from other objects -/// -/// ============================ ================================================================ -/// Method Summary -/// ============================ ================================================================ -/// :meth:`from_label` Convert a dense string label into a single-term -/// :class:`.SparseObservable`. -/// -/// :meth:`from_list` Sum a list of tuples of dense string labels and the associated -/// coefficients into an observable. -/// -/// :meth:`from_sparse_list` Sum a list of tuples of sparse string labels, the qubits they -/// apply to, and their coefficients into an observable. -/// -/// :meth:`from_pauli` Raise a single :class:`~.quantum_info.Pauli` into a single-term -/// :class:`.SparseObservable`. -/// -/// :meth:`from_sparse_pauli_op` Raise a :class:`.SparsePauliOp` into a -/// :class:`SparseObservable`. -/// -/// :meth:`from_terms` Sum explicit single :class:`Term` instances. -/// -/// :meth:`from_raw_parts` Build the observable from :ref:`the raw data arrays -/// `. -/// ============================ ================================================================ -/// -/// .. py:function:: SparseObservable.__new__(data, /, num_qubits=None) -/// -/// The default constructor of :class:`SparseObservable`. -/// -/// This delegates to one of :ref:`the explicit conversion-constructor methods -/// `, based on the type of the ``data`` argument. If -/// ``num_qubits`` is supplied and constructor implied by the type of ``data`` does not accept a -/// number, the given integer must match the input. -/// -/// :param data: The data type of the input. This can be another :class:`SparseObservable`, in -/// which case the input is copied, a :class:`~.quantum_info.Pauli` or :class:`.SparsePauliOp`, in which -/// case :meth:`from_pauli` or :meth:`from_sparse_pauli_op` are called as appropriate, or it -/// can be a list in a valid format for either :meth:`from_list` or -/// :meth:`from_sparse_list`. -/// :param int|None num_qubits: Optional number of qubits for the operator. For most data -/// inputs, this can be inferred and need not be passed. It is only necessary for empty -/// lists or the sparse-list format. If given unnecessarily, it must match the data input. -/// -/// In addition to the conversion-based constructors, there are also helper methods that construct -/// special forms of observables. -/// -/// .. table:: Construction of special observables -/// -/// ============================ ================================================================ -/// Method Summary -/// ============================ ================================================================ -/// :meth:`zero` The zero operator on a given number of qubits. -/// -/// :meth:`identity` The identity operator on a given number of qubits. -/// ============================ ================================================================ -/// -/// Conversions -/// =========== -/// -/// An existing :class:`SparseObservable` can be converted into other :mod:`~qiskit.quantum_info` -/// operators or generic formats. Beware that other objects may not be able to represent the same -/// observable as efficiently as :class:`SparseObservable`, including potentially needed -/// exponentially more memory. -/// -/// .. table:: Conversion methods to other observable forms. -/// -/// =========================== ================================================================= -/// Method Summary -/// =========================== ================================================================= -/// :meth:`as_paulis` Create a new :class:`SparseObservable`, expanding in terms -/// of Pauli operators only. -/// -/// :meth:`to_sparse_list` Express the observable in a sparse list format with elements -/// ``(bit_terms, indices, coeff)``. -/// =========================== ================================================================= -/// -/// In addition, :meth:`.SparsePauliOp.from_sparse_observable` is available for conversion from this -/// class to :class:`.SparsePauliOp`. Beware that this method suffers from the same -/// exponential-memory usage concerns as :meth:`as_paulis`. -/// -/// Mathematical manipulation -/// ========================= -/// -/// :class:`SparseObservable` supports the standard set of Python mathematical operators like other -/// :mod:`~qiskit.quantum_info` operators. -/// -/// In basic arithmetic, you can: -/// -/// * add two observables using ``+`` -/// * subtract two observables using ``-`` -/// * multiply or divide by an :class:`int`, :class:`float` or :class:`complex` using ``*`` and ``/`` -/// * negate all the coefficients in an observable with unary ``-`` -/// -/// Each of the basic binary arithmetic operators has a corresponding specialized in-place method, -/// which mutates the left-hand side in-place. Using these is typically more efficient than the -/// infix operators, especially for building an observable in a loop. -/// -/// The tensor product is calculated with :meth:`tensor` (for standard, juxtaposition ordering of -/// Pauli labels) or :meth:`expand` (for the reverse order). The ``^`` operator is overloaded to be -/// equivalent to :meth:`tensor`. -/// -/// .. note:: -/// -/// When using the binary operators ``^`` (:meth:`tensor`) and ``&`` (:meth:`compose`), beware -/// that `Python's operator-precedence rules -/// `__ may cause the -/// evaluation order to be different to your expectation. In particular, the operator ``+`` -/// binds more tightly than ``^`` or ``&``, just like ``*`` binds more tightly than ``+``. -/// -/// When using the operators in mixed expressions, it is safest to use parentheses to group the -/// operands of tensor products. -/// -/// A :class:`SparseObservable` has a well-defined :meth:`adjoint`. The notions of scalar complex -/// conjugation (:meth:`conjugate`) and real-value transposition (:meth:`transpose`) are defined -/// analogously to the matrix representation of other Pauli operators in Qiskit. -/// -/// -/// Efficiency notes -/// ---------------- -/// -/// Internally, :class:`SparseObservable` is in-place mutable, including using over-allocating -/// growable vectors for extending the number of terms. This means that the cost of appending to an -/// observable using ``+=`` is amortised linear in the total number of terms added, rather than -/// the quadratic complexity that the binary ``+`` would require. -/// -/// Additions and subtractions are implemented by a term-stacking operation; there is no automatic -/// "simplification" (summing of like terms), because the majority of additions to build up an -/// observable generate only a small number of duplications, and like-term detection has additional -/// costs. If this does not fit your use cases, you can either periodically call :meth:`simplify`, -/// or discuss further APIs with us for better building of observables. -#[cfg(feature = "python")] -#[pyclass(name = "SparseObservable", module = "qiskit.quantum_info", sequence)] -#[derive(Debug)] -pub struct PySparseObservable { - // This class keeps a pointer to a pure Rust-SparseTerm and serves as interface from Python. - pub inner: Arc>, -} - -#[cfg(feature = "python")] -#[pymethods] -impl PySparseObservable { - #[pyo3(signature = (data, /, num_qubits=None))] - #[new] - fn py_new(data: &Bound, num_qubits: Option) -> PyResult { - let py = data.py(); - let check_num_qubits = |data: &Bound| -> PyResult<()> { - let Some(num_qubits) = num_qubits else { - return Ok(()); - }; - let other_qubits = data.getattr(intern!(py, "num_qubits"))?.extract::()?; - if num_qubits == other_qubits { - return Ok(()); - } - Err(PyValueError::new_err(format!( - "explicitly given 'num_qubits' ({num_qubits}) does not match operator ({other_qubits})" - ))) - }; - - if data.is_instance(PAULI_TYPE.get_bound(py))? { - check_num_qubits(data)?; - return Self::from_pauli(data); - } - if data.is_instance(SPARSE_PAULI_OP_TYPE.get_bound(py))? { - check_num_qubits(data)?; - return Self::from_sparse_pauli_op(data); - } - if let Ok(label) = data.extract::() { - let num_qubits = num_qubits.unwrap_or(label.len() as u32); - if num_qubits as usize != label.len() { - return Err(PyValueError::new_err(format!( - "explicitly given 'num_qubits' ({}) does not match label ({})", - num_qubits, - label.len(), - ))); - } - return Self::from_label(&label).map_err(PyErr::from); - } - if let Ok(observable) = data.cast_exact::() { - check_num_qubits(data)?; - let borrowed = observable.borrow(); - let inner = borrowed.inner.read().map_err(|_| InnerReadError)?; - return Ok(inner.clone().into()); - } - // The type of `vec` is inferred from the subsequent calls to `Self::from_list` or - // `Self::from_sparse_list` to be either the two-tuple or the three-tuple form during the - // `extract`. The empty list will pass either, but it means the same to both functions. - if let Ok(vec) = data.extract() { - return Self::from_list(vec, num_qubits); - } - if let Ok(vec) = data.extract() { - let Some(num_qubits) = num_qubits else { - return Err(PyValueError::new_err( - "if using the sparse-list form, 'num_qubits' must be provided", - )); - }; - return Self::from_sparse_list(vec, num_qubits); - } - if let Ok(term) = data.cast_exact::() { - return term.borrow().to_observable(); - }; - if let Ok(observable) = Self::from_terms(data, num_qubits) { - return Ok(observable); - } - Err(PyTypeError::new_err(format!( - "unknown input format for 'SparseObservable': {}", - data.get_type().repr()?, - ))) - } - - /// Get a copy of this observable. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> obs = SparseObservable.from_list([("IXZ+lr01", 2.5), ("ZXI-rl10", 0.5j)]) - /// >>> assert obs == obs.copy() - /// >>> assert obs is not obs.copy() - fn copy(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.clone().into()) - } - - /// The number of qubits the operator acts on. - /// - /// This is not inferable from any other shape or values, since identities are not stored - /// explicitly. - #[getter] - #[inline] - pub fn num_qubits(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.num_qubits()) - } - - /// The number of terms in the sum this operator is tracking. - #[getter] - #[inline] - pub fn num_terms(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.num_terms()) - } - - /// The coefficients of each abstract term in in the sum. This has as many elements as terms in - /// the sum. - #[getter] - fn get_coeffs(slf_: &Bound) -> ArrayView { - let borrowed = slf_.borrow(); - ArrayView { - base: borrowed.inner.clone(), - slot: ArraySlot::Coeffs, - } - } - - /// A flat list of single-qubit terms. This is more naturally a list of lists, but is stored - /// flat for memory usage and locality reasons, with the sublists denoted by `boundaries.` - #[getter] - fn get_bit_terms(slf_: &Bound) -> ArrayView { - let borrowed = slf_.borrow(); - ArrayView { - base: borrowed.inner.clone(), - slot: ArraySlot::BitTerms, - } - } - - /// A flat list of the qubit indices that the corresponding entries in :attr:`bit_terms` act on. - /// This list must always be term-wise sorted, where a term is a sublist as denoted by - /// :attr:`boundaries`. - /// - /// .. warning:: - /// - /// If writing to this attribute from Python space, you *must* ensure that you only write in - /// indices that are term-wise sorted. - #[getter] - fn get_indices(slf_: &Bound) -> ArrayView { - let borrowed = slf_.borrow(); - ArrayView { - base: borrowed.inner.clone(), - slot: ArraySlot::Indices, - } - } - - /// Indices that partition :attr:`bit_terms` and :attr:`indices` into sublists for each - /// individual term in the sum. ``boundaries[0] : boundaries[1]`` is the range of indices into - /// :attr:`bit_terms` and :attr:`indices` that correspond to the first term of the sum. All - /// unspecified qubit indices are implicitly the identity. This is one item longer than - /// :attr:`coeffs`, since ``boundaries[0]`` is always an explicit zero (for algorithmic ease). - #[getter] - fn get_boundaries(slf_: &Bound) -> ArrayView { - let borrowed = slf_.borrow(); - ArrayView { - base: borrowed.inner.clone(), - slot: ArraySlot::Boundaries, - } - } - - /// Get the zero operator over the given number of qubits. - /// - /// The zero operator is the operator whose expectation value is zero for all quantum states. - /// It has no terms. It is the identity element for addition of two :class:`SparseObservable` - /// instances; anything added to the zero operator is equal to itself. - /// - /// If you want the projector onto the all zeros state, use:: - /// - /// >>> num_qubits = 10 - /// >>> all_zeros = SparseObservable.from_label("0" * num_qubits) - /// - /// Examples: - /// - /// Get the zero operator for 100 qubits:: - /// - /// >>> SparseObservable.zero(100) - /// - #[pyo3(signature = (/, num_qubits))] - #[staticmethod] - pub fn zero(num_qubits: u32) -> Self { - SparseObservable::zero(num_qubits).into() - } - - /// Get the identity operator over the given number of qubits. - /// - /// Examples: - /// - /// Get the identity operator for 100 qubits:: - /// - /// >>> SparseObservable.identity(100) - /// - #[pyo3(signature = (/, num_qubits))] - #[staticmethod] - pub fn identity(num_qubits: u32) -> Self { - SparseObservable::identity(num_qubits).into() - } - - /// Construct a :class:`.SparseObservable` from a single :class:`~.quantum_info.Pauli` instance. - /// - /// The output observable will have a single term, with a unitary coefficient dependent on the - /// phase. - /// - /// Args: - /// pauli (:class:`~.quantum_info.Pauli`): the single Pauli to convert. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> label = "IYXZI" - /// >>> pauli = Pauli(label) - /// >>> SparseObservable.from_pauli(pauli) - /// - /// >>> assert SparseObservable.from_label(label) == SparseObservable.from_pauli(pauli) - #[staticmethod] - #[pyo3(signature = (pauli, /))] - fn from_pauli(pauli: &Bound) -> PyResult { - let py = pauli.py(); - let num_qubits = pauli.getattr(intern!(py, "num_qubits"))?.extract::()?; - let z = pauli - .getattr(intern!(py, "z"))? - .extract::>()?; - let x = pauli - .getattr(intern!(py, "x"))? - .extract::>()?; - let mut bit_terms = Vec::new(); - let mut indices = Vec::new(); - let mut num_ys = 0; - for (i, (x, z)) in x.as_array().iter().zip(z.as_array().iter()).enumerate() { - // The only failure case possible here is the identity, because of how we're - // constructing the value to convert. - let Ok(term) = ::bytemuck::checked::try_cast(((*x as u8) << 1) | (*z as u8)) else { - continue; - }; - num_ys += (term == BitTerm::Y) as isize; - indices.push(i as u32); - bit_terms.push(term); - } - let boundaries = vec![0, indices.len()]; - // The "empty" state of a `Pauli` represents the identity, which isn't our empty state - // (that's zero), so we're always going to have a coefficient. - let group_phase = pauli - // `Pauli`'s `_phase` is a Numpy array ... - .getattr(intern!(py, "_phase"))? - // ... that should have exactly 1 element ... - .call_method0(intern!(py, "item"))? - // ... which is some integral type. - .extract::()?; - let phase = match (group_phase - num_ys).rem_euclid(4) { - 0 => Complex64::new(1.0, 0.0), - 1 => Complex64::new(0.0, -1.0), - 2 => Complex64::new(-1.0, 0.0), - 3 => Complex64::new(0.0, 1.0), - _ => unreachable!("`x % 4` has only four values"), - }; - let coeffs = vec![phase]; - let inner = SparseObservable::new(num_qubits, coeffs, bit_terms, indices, boundaries)?; - Ok(inner.into()) - } - - /// Construct a single-term observable from a dense string label. - /// - /// The resulting operator will have a coefficient of 1. The label must be a sequence of the - /// alphabet ``'IXYZ+-rl01'``. The label is interpreted analogously to a bitstring. In other - /// words, the right-most letter is associated with qubit 0, and so on. This is the same as the - /// labels for :class:`~.quantum_info.Pauli` and :class:`.SparsePauliOp`. - /// - /// Args: - /// label (str): the dense label. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> SparseObservable.from_label("IIII+ZI") - /// - /// >>> label = "IYXZI" - /// >>> pauli = Pauli(label) - /// >>> assert SparseObservable.from_label(label) == SparseObservable.from_pauli(pauli) - /// - /// See also: - /// :meth:`from_list` - /// A generalization of this method that constructs a sum operator from multiple labels - /// and their corresponding coefficients. - #[staticmethod] - #[pyo3(signature = (label, /))] - pub fn from_label(label: &str) -> Result { - let mut inner = SparseObservable::zero(label.len() as u32); - inner.add_dense_label(label, Complex64::new(1.0, 0.0))?; - Ok(inner.into()) - } - - /// Construct an observable from a list of dense labels and coefficients. - /// - /// This is analogous to :meth:`.SparsePauliOp.from_list`, except it uses - /// :ref:`the extended alphabet ` of :class:`.SparseObservable`. In - /// this dense form, you must supply all identities explicitly in each label. - /// - /// The label must be a sequence of the alphabet ``'IXYZ+-rl01'``. The label is interpreted - /// analogously to a bitstring. In other words, the right-most letter is associated with qubit - /// 0, and so on. This is the same as the labels for :class:`~.quantum_info.Pauli` and - /// :class:`.SparsePauliOp`. - /// - /// Args: - /// iter (list[tuple[str, complex]]): Pairs of labels and their associated coefficients to - /// sum. The labels are interpreted the same way as in :meth:`from_label`. - /// num_qubits (int | None): It is not necessary to specify this if you are sure that - /// ``iter`` is not an empty sequence, since it can be inferred from the label lengths. - /// If ``iter`` may be empty, you must specify this argument to disambiguate how many - /// qubits the observable is for. If this is given and ``iter`` is not empty, the value - /// must match the label lengths. - /// - /// Examples: - /// - /// Construct an observable from a list of labels of the same length:: - /// - /// >>> SparseObservable.from_list([ - /// ... ("III++", 1.0), - /// ... ("II--I", 1.0j), - /// ... ("I++II", -0.5), - /// ... ("--III", -0.25j), - /// ... ]) - /// - /// - /// Use ``num_qubits`` to disambiguate potentially empty inputs:: - /// - /// >>> SparseObservable.from_list([], num_qubits=10) - /// - /// - /// This method is equivalent to calls to :meth:`from_sparse_list` with the explicit - /// qubit-arguments field set to decreasing integers:: - /// - /// >>> labels = ["XY+Z", "rl01", "-lXZ"] - /// >>> coeffs = [1.5j, 2.0, -0.5] - /// >>> from_list = SparseObservable.from_list(list(zip(labels, coeffs))) - /// >>> from_sparse_list = SparseObservable.from_sparse_list([ - /// ... (label, (3, 2, 1, 0), coeff) - /// ... for label, coeff in zip(labels, coeffs) - /// ... ]) - /// >>> assert from_list == from_sparse_list - /// - /// See also: - /// :meth:`from_label` - /// A similar constructor, but takes only a single label and always has its coefficient - /// set to ``1.0``. - /// - /// :meth:`from_sparse_list` - /// Construct the observable from a list of labels without explicit identities, but with - /// the qubits each single-qubit term applies to listed explicitly. - #[staticmethod] - #[pyo3(signature = (iter, /, *, num_qubits=None))] - fn from_list(iter: Vec<(String, Complex64)>, num_qubits: Option) -> PyResult { - if iter.is_empty() && num_qubits.is_none() { - return Err(PyValueError::new_err( - "cannot construct an observable from an empty list without knowing `num_qubits`", - )); - } - let num_qubits = match num_qubits { - Some(num_qubits) => num_qubits, - None => iter[0].0.len() as u32, - }; - let mut inner = SparseObservable::with_capacity(num_qubits, iter.len(), 0); - for (label, coeff) in iter { - inner.add_dense_label(&label, coeff)?; - } - Ok(inner.into()) - } - - /// Construct an observable from a list of labels, the qubits each item applies to, and the - /// coefficient of the whole term. - /// - /// This is analogous to :meth:`.SparsePauliOp.from_sparse_list`, except it uses - /// :ref:`the extended alphabet ` of :class:`.SparseObservable`. - /// - /// The "labels" and "indices" fields of the triples are associated by zipping them together. - /// For example, this means that a call to :meth:`from_list` can be converted to the form used - /// by this method by setting the "indices" field of each triple to ``(num_qubits-1, ..., 1, - /// 0)``. - /// - /// Args: - /// iter (list[tuple[str, Sequence[int], complex]]): triples of labels, the qubits - /// each single-qubit term applies to, and the coefficient of the entire term. - /// - /// num_qubits (int): the number of qubits in the operator. - /// - /// Examples: - /// - /// Construct a simple operator:: - /// - /// >>> SparseObservable.from_sparse_list( - /// ... [("ZX", (1, 4), 1.0), ("YY", (0, 3), 2j)], - /// ... num_qubits=5, - /// ... ) - /// - /// - /// Construct the identity observable (though really, just use :meth:`identity`):: - /// - /// >>> SparseObservable.from_sparse_list([("", (), 1.0)], num_qubits=100) - /// - /// - /// This method can replicate the behavior of :meth:`from_list`, if the qubit-arguments - /// field of the triple is set to decreasing integers:: - /// - /// >>> labels = ["XY+Z", "rl01", "-lXZ"] - /// >>> coeffs = [1.5j, 2.0, -0.5] - /// >>> from_list = SparseObservable.from_list(list(zip(labels, coeffs))) - /// >>> from_sparse_list = SparseObservable.from_sparse_list([ - /// ... (label, (3, 2, 1, 0), coeff) - /// ... for label, coeff in zip(labels, coeffs) - /// ... ]) - /// >>> assert from_list == from_sparse_list - /// - /// See also: - /// :meth:`to_sparse_list` - /// The reverse of this method. - #[staticmethod] - #[pyo3(signature = (iter, /, num_qubits))] - fn from_sparse_list( - iter: Vec<(String, Vec, Complex64)>, - num_qubits: u32, - ) -> PyResult { - let coeffs = iter.iter().map(|(_, _, coeff)| *coeff).collect(); - let mut boundaries = Vec::with_capacity(iter.len() + 1); - boundaries.push(0); - let mut indices = Vec::new(); - let mut bit_terms = Vec::new(); - // Insertions to the `BTreeMap` keep it sorted by keys, so we use this to do the termwise - // sorting on-the-fly. - let mut sorted = btree_map::BTreeMap::new(); - for (label, qubits, _) in iter { - sorted.clear(); - let label: &[u8] = label.as_ref(); - if label.len() != qubits.len() { - return Err(LabelError::WrongLengthIndices { - label: label.len(), - indices: indices.len(), - } - .into()); - } - for (letter, index) in label.iter().zip(qubits) { - if index >= num_qubits { - return Err(LabelError::BadIndex { index, num_qubits }.into()); - } - let btree_map::Entry::Vacant(entry) = sorted.entry(index) else { - return Err(LabelError::DuplicateIndex { index }.into()); - }; - entry.insert( - BitTerm::try_from_u8(*letter).map_err(|_| LabelError::OutsideAlphabet)?, - ); - } - for (index, term) in sorted.iter() { - let Some(term) = term else { - continue; - }; - indices.push(*index); - bit_terms.push(*term); - } - boundaries.push(bit_terms.len()); - } - let inner = SparseObservable::new(num_qubits, coeffs, bit_terms, indices, boundaries)?; - Ok(inner.into()) - } - - /// Express the observable in Pauli terms only, by writing each projector as sum of Pauli terms. - /// - /// Note that there is no guarantee of the order the resulting Pauli terms. Use - /// :meth:`SparseObservable.simplify` in addition to obtain a canonical representation. - /// - /// .. warning:: - /// - /// 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 exponential number of terms. - /// - /// Returns: - /// The same observable, but expressed in Pauli terms only. - /// - /// Examples: - /// - /// Rewrite an observable in terms of projectors into Pauli operators:: - /// - /// >>> obs = SparseObservable("+") - /// >>> obs.as_paulis() - /// - /// >>> direct = SparseObservable.from_list([("I", 0.5), ("Z", 0.5)]) - /// >>> assert direct.simplify() == obs.as_paulis().simplify() - /// - /// For small operators, this can be used with :meth:`simplify` as a unique canonical form:: - /// - /// >>> left = SparseObservable.from_list([("+", 0.5), ("-", 0.5)]) - /// >>> right = SparseObservable.from_list([("r", 0.5), ("l", 0.5)]) - /// >>> assert left.as_paulis().simplify() == right.as_paulis().simplify() - /// - /// See also: - /// :meth:`.SparsePauliOp.from_sparse_observable` - /// A constructor of :class:`.SparsePauliOp` that can convert a - /// :class:`SparseObservable` in the :class:`.SparsePauliOp` dense Pauli representation. - fn as_paulis(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.as_paulis().into()) - } - - /// Express the observable in terms of a sparse list format. - /// - /// 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. - /// - /// Examples: - /// - /// >>> obs = SparseObservable.from_list([("IIXIZ", 2j), ("IIZIX", 2j)]) - /// >>> reconstructed = SparseObservable.from_sparse_list(obs.to_sparse_list(), obs.num_qubits) - /// - /// See also: - /// :meth:`from_sparse_list` - /// The constructor that can interpret these lists. - #[pyo3(signature = ())] - fn to_sparse_list(&self, py: Python) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - - // turn a SparseView into a Python tuple of (bit terms, indices, coeff) - let to_py_tuple = |view: SparseTermView| { - let mut pauli_string = String::with_capacity(view.bit_terms.len()); - - for bit in view.bit_terms.iter() { - pauli_string.push_str(bit.py_label()); - } - let py_string = PyString::new(py, &pauli_string).unbind(); - let py_indices = PyList::new(py, view.indices.iter())?.unbind(); - let py_coeff = view.coeff.into_py_any(py)?; - - PyTuple::new(py, vec![py_string.as_any(), py_indices.as_any(), &py_coeff]) - }; - - let out = PyList::empty(py); - for view in inner.iter() { - out.append(to_py_tuple(view)?)?; - } - Ok(out.unbind()) - } - - /// Construct a :class:`.SparseObservable` from a :class:`.SparsePauliOp` instance. - /// - /// This will be a largely direct translation of the :class:`.SparsePauliOp`; in particular, - /// there is no on-the-fly summing of like terms, nor any attempt to refactorize sums of Pauli - /// terms into equivalent projection operators. - /// - /// Args: - /// op (:class:`.SparsePauliOp`): the operator to convert. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> spo = SparsePauliOp.from_list([("III", 1.0), ("IIZ", 0.5), ("IZI", 0.5)]) - /// >>> SparseObservable.from_sparse_pauli_op(spo) - /// - #[staticmethod] - #[pyo3(signature = (op, /))] - fn from_sparse_pauli_op(op: &Bound) -> PyResult { - let py = op.py(); - let pauli_list_ob = op.getattr(intern!(py, "paulis"))?; - let coeffs = op - .getattr(intern!(py, "coeffs"))? - .extract::>() - .map_err(|_| PyTypeError::new_err("only 'SparsePauliOp' with complex-typed coefficients can be converted to 'SparseObservable'"))? - .as_array() - .to_vec(); - let op_z = pauli_list_ob - .getattr(intern!(py, "z"))? - .extract::>()?; - let op_x = pauli_list_ob - .getattr(intern!(py, "x"))? - .extract::>()?; - // We don't extract the `phase`, because that's supposed to be 0 for all `SparsePauliOp` - // instances - they use the symplectic convention in the representation with any phase term - // absorbed into the coefficients (like us). - let [num_terms, num_qubits] = *op_z.shape() else { - unreachable!("shape is statically known to be 2D") - }; - if op_x.shape() != [num_terms, num_qubits] { - return Err(PyValueError::new_err(format!( - "'x' and 'z' have different shapes ({:?} and {:?})", - op_x.shape(), - op_z.shape() - ))); - } - if num_terms != coeffs.len() { - return Err(PyValueError::new_err(format!( - "'x' and 'z' have a different number of operators to 'coeffs' ({} and {})", - num_terms, - coeffs.len(), - ))); - } - - let mut bit_terms = Vec::new(); - let mut indices = Vec::new(); - let mut boundaries = Vec::with_capacity(num_terms + 1); - boundaries.push(0); - for (term_x, term_z) in op_x - .as_array() - .rows() - .into_iter() - .zip(op_z.as_array().rows()) - { - for (i, (x, z)) in term_x.iter().zip(term_z.iter()).enumerate() { - // The only failure case possible here is the identity, because of how we're - // constructing the value to convert. - let Ok(term) = ::bytemuck::checked::try_cast(((*x as u8) << 1) | (*z as u8)) else { - continue; - }; - indices.push(i as u32); - bit_terms.push(term); - } - boundaries.push(indices.len()); - } - - let inner = - SparseObservable::new(num_qubits as u32, coeffs, bit_terms, indices, boundaries)?; - Ok(inner.into()) - } - - /// Construct a :class:`SparseObservable` out of individual terms. - /// - /// All the terms must have the same number of qubits. If supplied, the ``num_qubits`` argument - /// must match the terms. - /// - /// No simplification is done as part of the observable creation. - /// - /// Args: - /// obj (Iterable[Term]): Iterable of individual terms to build the observable from. - /// num_qubits (int | None): The number of qubits the observable should act on. This is - /// usually inferred from the input, but can be explicitly given to handle the case - /// of an empty iterable. - /// - /// Returns: - /// The corresponding observable. - #[staticmethod] - #[pyo3(signature = (obj, /, num_qubits=None))] - fn from_terms(obj: &Bound, num_qubits: Option) -> PyResult { - let mut iter = obj.try_iter()?; - let mut inner = match num_qubits { - Some(num_qubits) => SparseObservable::zero(num_qubits), - None => { - let Some(first) = iter.next() else { - return Err(PyValueError::new_err( - "cannot construct an observable from an empty list without knowing `num_qubits`", - )); - }; - let py_term = first?.cast::()?.borrow(); - py_term.inner.to_observable() - } - }; - for bound_py_term in iter { - let py_term = bound_py_term?.cast::()?.borrow(); - inner.add_term(py_term.inner.view())?; - } - Ok(inner.into()) - } - - // SAFETY: this cannot invoke undefined behaviour if `check = true`, but if `check = false` then - // the `bit_terms` must all be valid `BitTerm` representations. - /// Construct a :class:`.SparseObservable` from raw Numpy arrays that match :ref:`the required - /// data representation described in the class-level documentation `. - /// - /// The data from each array is copied into fresh, growable Rust-space allocations. - /// - /// Args: - /// num_qubits: number of qubits in the observable. - /// coeffs: complex coefficients of each term of the observable. This should be a Numpy - /// array with dtype :attr:`~numpy.complex128`. - /// bit_terms: flattened list of the single-qubit terms comprising all complete terms. This - /// should be a Numpy array with dtype :attr:`~numpy.uint8` (which is compatible with - /// :class:`.BitTerm`). - /// indices: flattened term-wise sorted list of the qubits each single-qubit term corresponds - /// to. This should be a Numpy array with dtype :attr:`~numpy.uint32`. - /// boundaries: the indices that partition ``bit_terms`` and ``indices`` into terms. This - /// should be a Numpy array with dtype :attr:`~numpy.uintp`. - /// check: if ``True`` (the default), validate that the data satisfies all coherence - /// guarantees. If ``False``, no checks are done. - /// - /// .. warning:: - /// - /// If ``check=False``, the ``bit_terms`` absolutely *must* be all be valid values - /// of :class:`.SparseObservable.BitTerm`. If they are not, Rust-space undefined - /// behavior may occur, entirely invalidating the program execution. - /// - /// Examples: - /// - /// Construct a sum of :math:`Z` on each individual qubit:: - /// - /// >>> num_qubits = 100 - /// >>> terms = np.full((num_qubits,), SparseObservable.BitTerm.Z, dtype=np.uint8) - /// >>> indices = np.arange(num_qubits, dtype=np.uint32) - /// >>> coeffs = np.ones((num_qubits,), dtype=complex) - /// >>> boundaries = np.arange(num_qubits + 1, dtype=np.uintp) - /// >>> SparseObservable.from_raw_parts(num_qubits, coeffs, terms, indices, boundaries) - /// - #[staticmethod] - #[pyo3( - signature = (/, num_qubits, coeffs, bit_terms, indices, boundaries, check=true), - )] - unsafe fn from_raw_parts<'py>( - num_qubits: u32, - coeffs: PyArrayLike1<'py, Complex64, numpy::AllowTypeChange>, - bit_terms: PyArrayLike1<'py, u8, numpy::AllowTypeChange>, - indices: PyArrayLike1<'py, u32, numpy::AllowTypeChange>, - boundaries: PyArrayLike1<'py, usize, numpy::AllowTypeChange>, - check: bool, - ) -> PyResult { - let coeffs = coeffs.as_array().to_vec(); - let bit_terms = if check { - bit_terms - .as_array() - .into_iter() - .copied() - .map(BitTerm::try_from) - .collect::>()? - } else { - let bit_terms_as_u8 = bit_terms.as_array().to_vec(); - // SAFETY: the caller enforced that each `u8` is a valid `BitTerm`, and `BitTerm` is be - // represented by a `u8`. We can't use `bytemuck` because we're casting a `Vec`. - unsafe { ::std::mem::transmute::, Vec>(bit_terms_as_u8) } - }; - let indices = indices.as_array().to_vec(); - let boundaries = boundaries.as_array().to_vec(); - - let inner = if check { - SparseObservable::new(num_qubits, coeffs, bit_terms, indices, boundaries) - .map_err(PyErr::from) - } else { - // SAFETY: the caller promised they have upheld the coherence guarantees. - Ok(unsafe { - SparseObservable::new_unchecked(num_qubits, coeffs, bit_terms, indices, boundaries) - }) - }?; - Ok(inner.into()) - } - - /// Clear all the terms from this operator, making it equal to the zero operator again. - /// - /// This does not change the capacity of the internal allocations, so subsequent addition or - /// subtraction operations may not need to reallocate. - /// - /// Examples: - /// - /// .. code-block:: python - /// - /// >>> obs = SparseObservable.from_list([("IX+-rl", 2.0), ("01YZII", -1j)]) - /// >>> obs.clear() - /// >>> assert obs == SparseObservable.zero(obs.py_num_qubits()) - pub fn clear(&mut self) -> PyResult<()> { - let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; - inner.clear(); - Ok(()) - } - - /// Sum any like terms in this operator, removing them if the resulting complex coefficient has - /// an absolute value within tolerance of zero. - /// - /// As a side effect, this sorts the operator into :ref:`canonical order - /// `. - /// - /// .. note:: - /// - /// When using this for equality comparisons, note that floating-point rounding and the - /// non-associativity of floating-point addition may cause non-zero coefficients of summed - /// terms to compare unequal. To compare two observables up to a tolerance, it is safest to - /// compare the canonicalized difference of the two observables to zero. - /// - /// Args: - /// tol (float): after summing like terms, any coefficients whose absolute value is less - /// than the given absolute tolerance will be suppressed from the output. - /// - /// Examples: - /// - /// Using :meth:`simplify` to compare two operators that represent the same observable, but - /// would compare unequal due to the structural tests by default:: - /// - /// >>> base = SparseObservable.from_sparse_list([ - /// ... ("XZ", (2, 1), 1e-10), # value too small - /// ... ("+-", (3, 1), 2j), - /// ... ("+-", (3, 1), 2j), # can be combined with the above - /// ... ("01", (3, 1), 0.5), # out of order compared to `expected` - /// ... ], num_qubits=5) - /// >>> expected = SparseObservable.from_list([("I0I1I", 0.5), ("I+I-I", 4j)]) - /// >>> assert base != expected # non-canonical comparison - /// >>> assert base.simplify() == expected.simplify() - /// - /// Note that in the above example, the coefficients are chosen such that all floating-point - /// calculations are exact, and there are no intermediate rounding or associativity - /// concerns. If this cannot be guaranteed to be the case, the safer form is:: - /// - /// >>> left = SparseObservable.from_list([("XYZ", 1.0/3.0)] * 3) # sums to 1.0 - /// >>> right = SparseObservable.from_list([("XYZ", 1.0/7.0)] * 7) # doesn't sum to 1.0 - /// >>> assert left.simplify() != right.simplify() - /// >>> assert (left - right).simplify() == SparseObservable.zero(left.num_qubits) - #[pyo3( - signature = (/, tol=1e-8), - )] - fn simplify(&self, tol: f64) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let simplified = inner.canonicalize(tol); - Ok(simplified.into()) - } - - /// Calculate the adjoint of this observable. - /// - /// - /// This is well defined in the abstract mathematical sense. All the terms of the single-qubit - /// alphabet are self-adjoint, so the result of this operation is the same observable, except - /// its coefficients are all their complex conjugates. - /// - /// Examples: - /// - /// .. code-block:: - /// - /// >>> left = SparseObservable.from_list([("XY+-", 1j)]) - /// >>> right = SparseObservable.from_list([("XY+-", -1j)]) - /// >>> assert left.adjoint() == right - fn adjoint(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.adjoint().into()) - } - - /// Calculate the matrix transposition of this observable. - /// - /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the - /// matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related - /// alphabet terms are unaffected by the transposition, but $Y$-related terms modify their - /// alphabet terms. Precisely: - /// - /// * :math:`Y` transposes to :math:`-Y` - /// * :math:`\lvert r\rangle\langle r\rvert` transposes to :math:`\lvert l\rangle\langle l\rvert` - /// * :math:`\lvert l\rangle\langle l\rvert` transposes to :math:`\lvert r\rangle\langle r\rvert` - /// - /// Examples: - /// - /// .. code-block:: - /// - /// >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)]) - /// >>> assert obs.transpose() == SparseObservable([("III", 1j), ("Ylr", -0.5)]) - fn transpose(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.transpose().into()) - } - - /// Calculate the complex conjugation of this observable. - /// - /// This operation is defined in terms of the standard matrix conventions of Qiskit, in that the - /// matrix form is taken to be in the $Z$ computational basis. The $X$- and $Z$-related - /// alphabet terms are unaffected by the complex conjugation, but $Y$-related terms modify their - /// alphabet terms. Precisely: - /// - /// * :math:`Y` conjguates to :math:`-Y` - /// * :math:`\lvert r\rangle\langle r\rvert` conjugates to :math:`\lvert l\rangle\langle l\rvert` - /// * :math:`\lvert l\rangle\langle l\rvert` conjugates to :math:`\lvert r\rangle\langle r\rvert` - /// - /// Additionally, all coefficients are conjugated. - /// - /// Examples: - /// - /// .. code-block:: - /// - /// >>> obs = SparseObservable([("III", 1j), ("Yrl", 0.5)]) - /// >>> assert obs.conjugate() == SparseObservable([("III", -1j), ("Ylr", -0.5)]) - fn conjugate(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.conjugate().into()) - } - - /// Tensor product of two observables. - /// - /// The bit ordering is defined such that the qubit indices of the argument will remain the - /// same, and the indices of ``self`` will be offset by the number of qubits in ``other``. This - /// is the same convention as used by the rest of Qiskit's :mod:`~qiskit.quantum_info` - /// operators. - /// - /// This function is used for the infix ``^`` operator. If using this operator, beware that - /// `Python's operator-precedence rules - /// `__ may cause the - /// evaluation order to be different to your expectation. In particular, the operator ``+`` - /// binds more tightly than ``^``, just like ``*`` binds more tightly than ``+``. Use - /// parentheses to fix the evaluation order, if needed. - /// - /// The argument will be cast to :class:`SparseObservable` using its default constructor, if it - /// is not already in the correct form. - /// - /// Args: - /// - /// other: the observable to put on the right-hand side of the tensor product. - /// - /// Examples: - /// - /// The bit ordering of this is such that the tensor product of two observables made from a - /// single label "looks like" an observable made by concatenating the two strings:: - /// - /// >>> left = SparseObservable.from_label("XYZ") - /// >>> right = SparseObservable.from_label("+-IIrl") - /// >>> assert left.tensor(right) == SparseObservable.from_label("XYZ+-IIrl") - /// - /// You can also use the infix ``^`` operator for tensor products, which will similarly cast - /// the right-hand side of the operation if it is not already a :class:`SparseObservable`:: - /// - /// >>> assert SparseObservable("rl") ^ Pauli("XYZ") == SparseObservable("rlXYZ") - /// - /// See also: - /// :meth:`expand` - /// - /// The same function, but with the order of arguments flipped. This can be useful if - /// you like using the casting behavior for the argument, but you want your existing - /// :class:`SparseObservable` to be on the right-hand side of the tensor ordering. - #[pyo3(signature = (other, /))] - fn tensor(&self, other: &Bound) -> PyResult> { - let py = other.py(); - let Some(other) = coerce_to_observable(other)? else { - return Err(PyTypeError::new_err(format!( - "unknown type for tensor: {}", - other.get_type().repr()? - ))); - }; - - let other = other.borrow(); - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let other_inner = other.inner.read().map_err(|_| InnerReadError)?; - inner.tensor(&other_inner).into_py_any(py) - } - - /// Reverse-order tensor product. - /// - /// This is equivalent to ``other.tensor(self)``, except that ``other`` will first be type-cast - /// to :class:`SparseObservable` if it isn't already one (by calling the default constructor). - /// - /// Args: - /// - /// other: the observable to put on the left-hand side of the tensor product. - /// - /// Examples: - /// - /// This is equivalent to :meth:`tensor` with the order of the arguments flipped:: - /// - /// >>> left = SparseObservable.from_label("XYZ") - /// >>> right = SparseObservable.from_label("+-IIrl") - /// >>> assert left.tensor(right) == right.expand(left) - /// - /// See also: - /// :meth:`tensor` - /// - /// The same function with the order of arguments flipped. :meth:`tensor` is the more - /// standard argument ordering, and matches Qiskit's other conventions. - #[pyo3(signature = (other, /))] - fn expand<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { - let py = other.py(); - let Some(other) = coerce_to_observable(other)? else { - return Err(PyTypeError::new_err(format!( - "unknown type for expand: {}", - other.get_type().repr()? - ))); - }; - - let other = other.borrow(); - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let other_inner = other.inner.read().map_err(|_| InnerReadError)?; - other_inner.tensor(&inner).into_pyobject(py) - } - - /// Compose another :class:`SparseObservable` onto this one. - /// - /// In terms of operator algebras, composition corresponds to left-multiplication: - /// ``c = a.compose(b)`` corresponds to $C = B A$. In other words, ``a.compose(b)`` returns an - /// operator that "performs ``a``, and then performs ``b`` on the result". The argument - /// ``front=True`` instead makes this a right multiplication. - /// - /// ``self`` and ``other`` must be the same size, unless ``qargs`` is given, in which case - /// ``other`` can be smaller than ``self``, provided the number of qubits in ``other`` and the - /// length of ``qargs`` match. ``qargs`` can never contain duplicates or indices of qubits that - /// do not exist in ``self``. - /// - /// Beware that this function can cause exponential explosion of the memory usage of the - /// observable, as the alphabet of :class:`SparseObservable` is not closed under composition; - /// the composition of two single-bit terms can be a sum, which multiplies the total number of - /// terms. This memory usage is not _necessarily_ inherent to the resultant observable, but - /// finding an efficient re-factorization of the sum is generally equally computationally hard. - /// It's better to use domain knowledge of your observables to minimize the number of terms that - /// ever exist, rather than trying to simplify them after the fact. - /// - /// Args: - /// other: the observable used to left-multiply ``self``. - /// qargs: if given, the qubits in ``self`` to be associated with the qubits in ``other``. - /// Put another way: if this is given, it is similar to a more efficient implementation - /// of:: - /// - /// self.compose(other.apply_layout(qargs, self.num_qubits)) - /// - /// as no temporary observable is created to store the applied-layout form of ``other``. - /// front: if ``True``, then right-multiply by ``other`` instead of left-multiplying - /// (default ``False``). The ``qargs`` are still applied to ``other``. This is most - /// useful when ``qargs`` is set, or ``other`` might be an object that must be coerced - /// to :class:`SparseObservable`. - #[pyo3(signature = (other, /, qargs=None, *, front=false))] - fn compose<'py>( - &self, - other: &Bound<'py, PyAny>, - qargs: Option<&Bound<'py, PyAny>>, - front: bool, - ) -> PyResult> { - let py = other.py(); - let Some(other) = coerce_to_observable(other)? else { - return Err(PyTypeError::new_err(format!( - "unknown type for compose: {}", - other.get_type().repr()? - ))); - }; - let other = other.borrow(); - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let other_inner = other.inner.read().map_err(|_| InnerReadError)?; - if let Some(order) = qargs { - if other_inner.num_qubits() > inner.num_qubits() { - return Err(PyValueError::new_err(format!( - "argument has more qubits ({}) than the base ({})", - other_inner.num_qubits(), - inner.num_qubits() - ))); - } - let in_length = order.len()?; - if other_inner.num_qubits() as usize != in_length { - return Err(PyValueError::new_err(format!( - "input qargs has length {}, but the observable is on {} qubit(s)", - in_length, - other_inner.num_qubits() - ))); - } - let order = order - .try_iter()? - .map(|obj| obj.and_then(|obj| obj.extract::())) - .collect::>>()?; - if order.len() != in_length { - return Err(PyValueError::new_err("duplicate indices in qargs")); - } - if order - .iter() - .max() - .is_some_and(|max| *max >= inner.num_qubits()) - { - return Err(PyValueError::new_err("qargs contains out-of-range qubits")); - } - if front { - // This implementation can be improved if it turns out to be needed a lot and the - // extra copy is a bottleneck. - let other_to_self = order.iter().copied().collect::>(); - other_inner - .apply_layout(Some(other_to_self.as_slice()), inner.num_qubits)? - .compose(&inner) - .into_pyobject(py) - } else { - inner - .compose_map(&other_inner, |bit| { - *order - .get_index(bit as usize) - .expect("order has the same length and no duplicates") - }) - .into_pyobject(py) - } - } else { - if other_inner.num_qubits() != inner.num_qubits() { - return Err(PyValueError::new_err(format!( - "mismatched numbers of qubits: {} (base) and {} (argument)", - inner.num_qubits(), - other_inner.num_qubits() - ))); - } - if front { - other_inner.compose(&inner).into_pyobject(py) - } else { - inner.compose(&other_inner).into_pyobject(py) - } - } - } - - /// Evolve this observable by a Pauli term. - /// - /// An evolution of the observable :math:`O` by a Pauli :math:`P` corresponds to - /// :math:`P^\dagger O P`. - /// - /// Unlike a literal implementation via two full compositions, this method - /// performs the conjugation directly at the single-qubit level using a fixed - /// lookup table. This avoids materializing any intermediate - /// :class:`SparseObservable` and computes the evolved observable in a single - /// pass over the terms. - /// ``self`` and ``other`` must have the same number of qubits, unless ``qargs`` is given, - /// in which case ``other`` can be smaller than ``self``, provided the number of qubits - /// in ``other`` and the length of ``qargs`` match. ``qargs`` specifies which qubits of - /// ``self`` are evolved by ``other``. - /// - /// Currently, this method supports evolution only by a *single-term* operator, meaning - /// that ``other`` must be a Pauli represented by :class:`~.quantum_info.Pauli`. - /// - /// Args: - /// other: the Pauli Operator used to conjugate ``self``. - /// qargs: if given, the qubits in ``self`` to be evolved by ``other``. - /// The length must match the number of qubits in ``other``. - /// - /// Returns: - /// A new evolved :class:`SparseObservable` with applied conjugations. - /// - /// Raises: - /// TypeError : if ``other`` is not of Type :class:`~.quantum_info.Pauli`. - /// ValueError: if ``self`` and ``other`` have different numbers of qubits (and ``qargs`` is not given). - /// ValueError: if ``qargs`` length doesn't match ``other`` number of qubits. - /// ValueError: if ``qargs`` contains duplicates or out-of-range indices. - /// ValueError: if ``other`` contains more than one term. - #[pyo3(signature = (other, /, qargs=None))] - fn evolve<'py>( - &self, - other: &Bound<'py, PyAny>, - qargs: Option<&Bound<'py, PyAny>>, - ) -> PyResult> { - let py = other.py(); - - if !other.is_instance(PAULI_TYPE.get_bound(py))? { - return Err(PyTypeError::new_err(format!( - "evolve only accepts Pauli instances, got: {}", - other.get_type().repr()? - ))); - } - - let base = self.as_inner()?; - let u_obs = Self::from_pauli(other)?; - let u_inner = u_obs.as_inner()?; - - let qargs_vec = if let Some(qargs) = qargs { - let vec = qargs - .try_iter()? - .map(|obj| obj.and_then(|obj| obj.extract::())) - .collect::>>()?; - Some(vec) - } else { - None - }; - - let out = base - .evolve(&u_inner, qargs_vec.as_deref()) - .map_err(|e| PyValueError::new_err(e.to_string()))?; - - out.into_pyobject(py) - } - - /// Apply a transpiler layout to this :class:`SparseObservable`. - /// - /// Typically you will have defined your observable in terms of the virtual qubits of the - /// circuits you will use to prepare states. After transpilation, the virtual qubits are mapped - /// to particular physical qubits on a device, which may be wider than your circuit. That - /// mapping can also change over the course of the circuit. This method transforms the input - /// observable on virtual qubits to an observable that is suitable to apply immediately after - /// the fully transpiled *physical* circuit. - /// - /// Args: - /// layout (TranspileLayout | list[int] | None): The layout to apply. Most uses of this - /// function should pass the :attr:`.QuantumCircuit.layout` field from a circuit that - /// was transpiled for hardware. In addition, you can pass a list of new qubit indices. - /// If given as explicitly ``None``, no remapping is applied (but you can still use - /// ``num_qubits`` to expand the observable). - /// num_qubits (int | None): The number of qubits to expand the observable to. If not - /// supplied, the output will be as wide as the given :class:`.TranspileLayout`, or the - /// same width as the input if the ``layout`` is given in another form. - /// - /// Returns: - /// A new :class:`SparseObservable` with the provided layout applied. - #[pyo3(signature = (/, layout, num_qubits=None))] - fn apply_layout(&self, layout: Bound, num_qubits: Option) -> PyResult { - let py = layout.py(); - let inner = self.inner.read().map_err(|_| InnerReadError)?; - - // A utility to check the number of qubits is compatible with the observable. - let check_inferred_qubits = |inferred: u32| -> PyResult { - if inferred < inner.num_qubits() { - return Err(CoherenceError::NotEnoughQubits { - current: inner.num_qubits() as usize, - target: inferred as usize, - } - .into()); - } - Ok(inferred) - }; - - // Normalize the number of qubits in the layout and the layout itself, depending on the - // input types, before calling SparseObservable.apply_layout to do the actual work. - let (num_qubits, layout): (u32, Option>) = if layout.is_none() { - (num_qubits.unwrap_or(inner.num_qubits()), None) - } else if layout.is_instance( - &py.import(intern!(py, "qiskit.transpiler"))? - .getattr(intern!(py, "TranspileLayout"))?, - )? { - ( - check_inferred_qubits( - layout.getattr(intern!(py, "_output_qubit_list"))?.len()? as u32 - )?, - Some( - layout - .call_method0(intern!(py, "final_index_layout"))? - .extract::>()?, - ), - ) - } else { - ( - check_inferred_qubits(num_qubits.unwrap_or(inner.num_qubits()))?, - Some(layout.extract()?), - ) - }; - - let out = inner.apply_layout(layout.as_deref(), num_qubits)?; - Ok(out.into()) - } - - /// Get a :class:`.PauliList` object that represents the measurement basis needed for each term - /// (in order) in this observable. - /// - /// For example, the projector ``0l+`` will return a Pauli ``ZXY``. The resulting - /// :class:`~.quantum_info.Pauli` is dense, in the sense that explicit identities are stored. - /// An identity in the Pauli output does not require a concrete measurement. - /// - /// This will return an entry in the Pauli list for every term in the sum. - /// - /// Returns: - /// :class:`.PauliList`: the Pauli operator list representing the necessary measurement - /// bases. - fn pauli_bases<'py>(&self, py: Python<'py>) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let mut x = Array2::from_elem([inner.num_terms(), inner.num_qubits() as usize], false); - let mut z = Array2::from_elem([inner.num_terms(), inner.num_qubits() as usize], false); - for (loc, term) in inner.iter().enumerate() { - let mut x_row = x.row_mut(loc); - let mut z_row = z.row_mut(loc); - for (bit_term, index) in term.bit_terms.iter().zip(term.indices) { - x_row[*index as usize] = bit_term.has_x_component(); - z_row[*index as usize] = bit_term.has_z_component(); - } - } - PAULI_LIST_TYPE - .get_bound(py) - .getattr(intern!(py, "from_symplectic"))? - .call1(( - PyArray2::from_owned_array(py, z), - PyArray2::from_owned_array(py, x), - )) - } - - /// Check whether the observable commutes with another one. - /// - /// Args: - /// other (SparseObservable): The other observable to check commutation with. - /// tol (float): If coefficients in the product of `self` and `other` are below the - /// tolerance (in magnitude), the terms are ignored. - /// - /// Returns: - /// ``True`` if the terms commute, up to tolerance, ``False`` otherwise. - /// - /// Raises: - /// TypeError: If ``other`` could not be coerced to ``SparseObservable``. - #[pyo3(signature = (other, tol=1e-12))] - pub fn commutes(&self, other: &Bound, tol: f64) -> PyResult { - let Some(other) = coerce_to_observable(other)? else { - return Err(PyTypeError::new_err("Invalid type of other.")); - }; - - let self_inner = self.inner.read().map_err(|_| InnerReadError)?; - let other = other.borrow(); - let other_inner = other.inner.read().map_err(|_| InnerReadError)?; - - Ok(self_inner.commutes(&other_inner, tol)) - } - - fn __len__(&self) -> PyResult { - self.num_terms() - } - - fn __getitem__<'py>( - &self, - py: Python<'py>, - index: PySequenceIndex<'py>, - ) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let indices = match index.with_len(inner.num_terms())? { - SequenceIndex::Int(index) => { - return PySparseTerm { - inner: inner.term(index).to_term(), - } - .into_bound_py_any(py); - } - indices => indices, - }; - let mut out = SparseObservable::zero(inner.num_qubits()); - for index in indices.iter() { - out.add_term(inner.term(index))?; - } - out.into_bound_py_any(py) - } - - fn __eq__(slf: Bound, other: Bound) -> PyResult { - // this is also important to check before trying to read both slf and other - if slf.is(&other) { - return Ok(true); - } - let Ok(other) = other.cast_into::() else { - return Ok(false); - }; - let slf_borrowed = slf.borrow(); - let other_borrowed = other.borrow(); - let slf_inner = slf_borrowed.inner.read().map_err(|_| InnerReadError)?; - let other_inner = other_borrowed.inner.read().map_err(|_| InnerReadError)?; - Ok(slf_inner.eq(&other_inner)) - } - - fn __repr__(&self) -> PyResult { - let num_terms = self.num_terms()?; - let num_qubits = self.num_qubits()?; - - let str_num_terms = format!( - "{} term{}", - num_terms, - if num_terms == 1 { "" } else { "s" } - ); - let str_num_qubits = format!( - "{} qubit{}", - num_qubits, - if num_qubits == 1 { "" } else { "s" } - ); - - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let str_terms = if num_terms == 0 { - "0.0".to_owned() - } else { - inner - .iter() - .map(SparseTermView::to_sparse_str) - .collect::>() - .join(" + ") - }; - Ok(format!( - "" - )) - } - - fn __reduce__<'py>(&self, py: Python<'py>) -> PyResult> { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let bit_terms: &[u8] = ::bytemuck::cast_slice(inner.bit_terms()); - ( - py.get_type::().getattr("from_raw_parts")?, - ( - inner.num_qubits(), - PyArray1::from_slice(py, inner.coeffs()), - PyArray1::from_slice(py, bit_terms), - PyArray1::from_slice(py, inner.indices()), - PyArray1::from_slice(py, inner.boundaries()), - false, - ), - ) - .into_pyobject(py) - } - - fn __add__<'py>( - slf_: &Bound<'py, Self>, - other: &Bound<'py, PyAny>, - ) -> PyResult> { - let py = slf_.py(); - let Some(other) = coerce_to_observable(other)? else { - return Ok(py.NotImplemented().into_bound(py)); - }; - - let other = other.borrow(); - let slf_ = slf_.borrow(); - if Arc::ptr_eq(&slf_.inner, &other.inner) { - // This fast path is for consistency with the in-place `__iadd__`, which would otherwise - // struggle to do the addition to itself. - let inner = slf_.inner.read().map_err(|_| InnerReadError)?; - return <&SparseObservable as ::std::ops::Mul<_>>::mul( - &inner, - Complex64::new(2.0, 0.0), - ) - .into_bound_py_any(py); - } - let slf_inner = slf_.inner.read().map_err(|_| InnerReadError)?; - let other_inner = other.inner.read().map_err(|_| InnerReadError)?; - slf_inner.check_equal_qubits(&other_inner)?; - <&SparseObservable as ::std::ops::Add>::add(&slf_inner, &other_inner).into_bound_py_any(py) - } - - fn __radd__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { - // No need to handle the `self is other` case here, because `__add__` will get it. - let py = other.py(); - let Some(other) = coerce_to_observable(other)? else { - return Ok(py.NotImplemented().into_bound(py)); - }; - - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let other = other.borrow(); - let other_inner = other.inner.read().map_err(|_| InnerReadError)?; - inner.check_equal_qubits(&other_inner)?; - <&SparseObservable as ::std::ops::Add>::add(&other_inner, &inner).into_bound_py_any(py) - } - - fn __iadd__(slf_: Bound, other: &Bound) -> PyResult<()> { - let Some(other) = coerce_to_observable(other)? else { - // This is not well behaved - we _should_ return `NotImplemented` to Python space - // without an exception, but limitations in PyO3 prevent this at the moment. See - // https://github.com/PyO3/pyo3/issues/4605. - return Err(PyTypeError::new_err(format!( - "invalid object for in-place addition of 'SparseObservable': {}", - other.repr()? - ))); - }; - - let other = other.borrow(); - let slf_ = slf_.borrow(); - let mut slf_inner = slf_.inner.write().map_err(|_| InnerWriteError)?; - - // Check if slf_ and other point to the same SparseObservable object, in which case - // we just multiply it by 2 - if Arc::ptr_eq(&slf_.inner, &other.inner) { - *slf_inner *= Complex64::new(2.0, 0.0); - return Ok(()); - } - - let other_inner = other.inner.read().map_err(|_| InnerReadError)?; - slf_inner.check_equal_qubits(&other_inner)?; - slf_inner.add_assign(&other_inner); - Ok(()) - } - - fn __sub__<'py>( - slf_: &Bound<'py, Self>, - other: &Bound<'py, PyAny>, - ) -> PyResult> { - let py = slf_.py(); - let Some(other) = coerce_to_observable(other)? else { - return Ok(py.NotImplemented().into_bound(py)); - }; - - let other = other.borrow(); - let slf_ = slf_.borrow(); - if Arc::ptr_eq(&slf_.inner, &other.inner) { - return PySparseObservable::zero(slf_.num_qubits()?).into_bound_py_any(py); - } - - let slf_inner = slf_.inner.read().map_err(|_| InnerReadError)?; - let other_inner = other.inner.read().map_err(|_| InnerReadError)?; - slf_inner.check_equal_qubits(&other_inner)?; - <&SparseObservable as ::std::ops::Sub>::sub(&slf_inner, &other_inner).into_bound_py_any(py) - } - - fn __rsub__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { - let py = other.py(); - let Some(other) = coerce_to_observable(other)? else { - return Ok(py.NotImplemented().into_bound(py)); - }; - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let other = other.borrow(); - let other_inner = other.inner.read().map_err(|_| InnerReadError)?; - inner.check_equal_qubits(&other_inner)?; - <&SparseObservable as ::std::ops::Sub>::sub(&other_inner, &inner).into_bound_py_any(py) - } - - fn __isub__(slf_: Bound, other: &Bound) -> PyResult<()> { - let Some(other) = coerce_to_observable(other)? else { - // This is not well behaved - we _should_ return `NotImplemented` to Python space - // without an exception, but limitations in PyO3 prevent this at the moment. See - // https://github.com/PyO3/pyo3/issues/4605. - return Err(PyTypeError::new_err(format!( - "invalid object for in-place subtraction of 'SparseObservable': {}", - other.repr()? - ))); - }; - let other = other.borrow(); - let slf_ = slf_.borrow(); - let mut slf_inner = slf_.inner.write().map_err(|_| InnerWriteError)?; - - if Arc::ptr_eq(&slf_.inner, &other.inner) { - // This is not strictly the same thing as `a - a` if `a` contains non-finite - // floating-point values (`inf - inf` is `NaN`, for example); we don't really have a - // clear view on what floating-point guarantees we're going to make right now. - slf_inner.clear(); - return Ok(()); - } - - let other_inner = other.inner.read().map_err(|_| InnerReadError)?; - slf_inner.check_equal_qubits(&other_inner)?; - slf_inner.sub_assign(&other_inner); - Ok(()) - } - - fn __pos__(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - Ok(inner.clone().into()) - } - - fn __neg__(&self) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let neg = <&SparseObservable as ::std::ops::Neg>::neg(&inner); - Ok(neg.into()) - } - - fn __mul__(&self, other: Complex64) -> PyResult { - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let mult = <&SparseObservable as ::std::ops::Mul<_>>::mul(&inner, other); - Ok(mult.into()) - } - fn __rmul__(&self, other: Complex64) -> PyResult { - self.__mul__(other) - } - - fn __imul__(&mut self, other: Complex64) -> PyResult<()> { - let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; - inner.mul_assign(other); - Ok(()) - } - - fn __truediv__(&self, other: Complex64) -> PyResult { - if other.is_zero() { - return Err(PyZeroDivisionError::new_err("complex division by zero")); - } - let inner = self.inner.read().map_err(|_| InnerReadError)?; - let div = <&SparseObservable as ::std::ops::Div<_>>::div(&inner, other); - Ok(div.into()) - } - fn __itruediv__(&mut self, other: Complex64) -> PyResult<()> { - if other.is_zero() { - return Err(PyZeroDivisionError::new_err("complex division by zero")); - } - let mut inner = self.inner.write().map_err(|_| InnerWriteError)?; - inner.div_assign(other); - Ok(()) - } - - fn __xor__(&self, other: &Bound) -> PyResult> { - // we cannot just delegate this to ``tensor`` since ``other`` might allow - // right-hand-side arithmetic and we have to try deferring to that object, - // which is done by returning ``NotImplemented`` - let py = other.py(); - let Some(other) = coerce_to_observable(other)? else { - return Ok(py.NotImplemented()); - }; - - self.tensor(&other) - } - - fn __rxor__<'py>(&self, other: &Bound<'py, PyAny>) -> PyResult> { - let py = other.py(); - let Some(other) = coerce_to_observable(other)? else { - return Ok(py.NotImplemented().into_bound(py)); - }; - self.expand(&other).map(|obj| obj.into_any()) - } - - // The documentation for this is inlined into the class-level documentation of - // `SparseObservable`. - #[allow(non_snake_case)] - #[classattr] - fn BitTerm(py: Python) -> PyResult> { - BIT_TERM_PY_ENUM - .get_or_try_init(py, || make_py_bit_term(py)) - .map(|obj| obj.clone_ref(py)) - } - - // The documentation for this is inlined into the class-level documentation of - // `SparseObservable`. - #[allow(non_snake_case)] - #[classattr] - fn Term(py: Python) -> Bound { - py.get_type::() - } -} - -#[cfg(feature = "python")] -impl PySparseObservable { - /// This is an immutable reference as opposed to a `copy`. - pub fn as_inner(&self) -> Result, InnerReadError> { - let data = self.inner.read().map_err(|_| InnerReadError)?; - Ok(data) - } -} - -#[cfg(feature = "python")] -impl From for PySparseObservable { - fn from(val: SparseObservable) -> PySparseObservable { - PySparseObservable { - inner: Arc::new(RwLock::new(val)), - } - } -} - -#[cfg(feature = "python")] -impl<'py> IntoPyObject<'py> for SparseObservable { - type Target = PySparseObservable; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> PyResult { - PySparseObservable::from(self).into_pyobject(py) - } -} - -/// Helper class of `ArrayView` that denotes the slot of the `SparseObservable` we're looking at. -#[cfg(feature = "python")] -#[derive(Clone, Copy, PartialEq, Eq)] -enum ArraySlot { - Coeffs, - BitTerms, - Indices, - Boundaries, -} - -/// Custom wrapper sequence class to get safe views onto the Rust-space data. We can't directly -/// expose Python-managed wrapped pointers without introducing some form of runtime exclusion on the -/// ability of `SparseObservable` to re-allocate in place; we can't leave dangling pointers for -/// Python space. -#[cfg(feature = "python")] -#[pyclass(frozen, sequence)] -struct ArrayView { - base: Arc>, - slot: ArraySlot, -} - -#[cfg(feature = "python")] -#[pymethods] -impl ArrayView { - fn __repr__(&self, py: Python) -> PyResult { - let obs = self.base.read().map_err(|_| InnerReadError)?; - let data = match self.slot { - // Simple integers look the same in Rust-space debug as Python. - ArraySlot::Indices => format!("{:?}", obs.indices()), - ArraySlot::Boundaries => format!("{:?}", obs.boundaries()), - // Complexes don't have a nice repr in Rust, so just delegate the whole load to Python - // and convert back. - ArraySlot::Coeffs => PyList::new(py, obs.coeffs())?.repr()?.to_string(), - ArraySlot::BitTerms => format!( - "[{}]", - obs.bit_terms() - .iter() - .map(BitTerm::py_label) - .collect::>() - .join(", ") - ), - }; - Ok(format!( - "", - match self.slot { - ArraySlot::Coeffs => "coeffs", - ArraySlot::BitTerms => "bit_terms", - ArraySlot::Indices => "indices", - ArraySlot::Boundaries => "boundaries", - }, - data, - )) - } - - fn __getitem__<'py>( - &self, - py: Python<'py>, - index: PySequenceIndex, - ) -> PyResult> { - // The slightly verbose generic setup here is to allow the type of a scalar return to be - // different to the type that gets put into the Numpy array, since the `BitTerm` enum can be - // a direct scalar, but for Numpy, we need it to be a raw `u8`. - fn get_from_slice<'py, T, S>( - py: Python<'py>, - slice: &[T], - index: PySequenceIndex, - ) -> PyResult> - where - T: IntoPyObject<'py> + Copy + Into, - S: ::numpy::Element, - { - match index.with_len(slice.len())? { - SequenceIndex::Int(index) => slice[index].into_bound_py_any(py), - indices => PyArray1::from_iter(py, indices.iter().map(|index| slice[index].into())) - .into_bound_py_any(py), - } - } - - let obs = self.base.read().map_err(|_| InnerReadError)?; - match self.slot { - ArraySlot::Coeffs => get_from_slice::<_, Complex64>(py, obs.coeffs(), index), - ArraySlot::BitTerms => get_from_slice::<_, u8>(py, obs.bit_terms(), index), - ArraySlot::Indices => get_from_slice::<_, u32>(py, obs.indices(), index), - ArraySlot::Boundaries => get_from_slice::<_, usize>(py, obs.boundaries(), index), - } - } - - fn __setitem__(&self, index: PySequenceIndex, values: &Bound) -> PyResult<()> { - /// Set values of a slice according to the indexer, using `extract` to retrieve the - /// Rust-space object from the collection of Python-space values. - /// - /// This indirects the Python extraction through an intermediate type to marginally improve - /// the error messages for things like `BitTerm`, where Python-space extraction might fail - /// because the user supplied an invalid alphabet letter. - /// - /// This allows broadcasting a single item into many locations in a slice (like Numpy), but - /// otherwise requires that the index and values are the same length (unlike Python's - /// `list`) because that would change the length. - fn set_in_slice<'a, 'py, T, S>( - slice: &mut [T], - index: PySequenceIndex<'py>, - values: Borrowed<'a, 'py, PyAny>, - ) -> PyResult<()> - where - T: Copy + TryFrom, - S: for<'b> FromPyObject<'b, 'py, Error: Into>, - PyErr: From<>::Error>, - { - match index.with_len(slice.len())? { - SequenceIndex::Int(index) => { - slice[index] = values - .extract::() - .map_err(Into::::into)? - .try_into()?; - Ok(()) - } - indices => { - if let Ok(value) = values.extract::() { - let value = value.try_into()?; - for index in indices { - slice[index] = value; - } - } else { - let values = values - .try_iter()? - .map(|value| { - value? - .extract::() - .map_err(Into::::into)? - .try_into() - .map_err(PyErr::from) - }) - .collect::>>()?; - if indices.len() != values.len() { - return Err(PyValueError::new_err(format!( - "tried to set a slice of length {} with a sequence of length {}", - indices.len(), - values.len(), - ))); - } - for (index, value) in indices.into_iter().zip(values) { - slice[index] = value; - } - } - Ok(()) - } - } - } - - let mut obs = self.base.write().map_err(|_| InnerWriteError)?; - let values = values.as_borrowed(); - match self.slot { - ArraySlot::Coeffs => set_in_slice::<_, Complex64>(obs.coeffs_mut(), index, values), - ArraySlot::BitTerms => set_in_slice::(obs.bit_terms_mut(), index, values), - ArraySlot::Indices => unsafe { - set_in_slice::<_, u32>(obs.indices_mut(), index, values) - }, - ArraySlot::Boundaries => unsafe { - set_in_slice::<_, usize>(obs.boundaries_mut(), index, values) - }, - } - } - - fn __len__(&self, _py: Python) -> PyResult { - let obs = self.base.read().map_err(|_| InnerReadError)?; - let len = match self.slot { - ArraySlot::Coeffs => obs.coeffs().len(), - ArraySlot::BitTerms => obs.bit_terms().len(), - ArraySlot::Indices => obs.indices().len(), - ArraySlot::Boundaries => obs.boundaries().len(), - }; - Ok(len) - } - - #[pyo3(signature = (/, dtype=None, copy=None))] - fn __array__<'py>( - &self, - py: Python<'py>, - dtype: Option<&Bound<'py, PyAny>>, - copy: Option, - ) -> PyResult> { - // This method always copies, so we don't leave dangling pointers lying around in Numpy - // arrays; it's not enough just to set the `base` of the Numpy array to the - // `SparseObservable`, since the `Vec` we're referring to might re-allocate and invalidate - // the pointer the Numpy array is wrapping. - if !copy.unwrap_or(true) { - return Err(PyValueError::new_err( - "cannot produce a safe view onto movable memory", - )); - } - let obs = self.base.read().map_err(|_| InnerReadError)?; - match self.slot { - ArraySlot::Coeffs => cast_array_type(py, PyArray1::from_slice(py, obs.coeffs()), dtype), - ArraySlot::Indices => { - cast_array_type(py, PyArray1::from_slice(py, obs.indices()), dtype) - } - ArraySlot::Boundaries => { - cast_array_type(py, PyArray1::from_slice(py, obs.boundaries()), dtype) - } - ArraySlot::BitTerms => { - let bit_terms: &[u8] = ::bytemuck::cast_slice(obs.bit_terms()); - cast_array_type(py, PyArray1::from_slice(py, bit_terms), dtype) - } - } - } -} - -/// Use the Numpy Python API to convert a `PyArray` into a dynamically chosen `dtype`, copying only -/// if required. -#[cfg(feature = "python")] -fn cast_array_type<'py, T: numpy::Element>( - py: Python<'py>, - array: Bound<'py, PyArray1>, - dtype: Option<&Bound<'py, PyAny>>, -) -> PyResult> { - let base_dtype = array.dtype(); - let dtype = dtype - .map(|dtype| PyArrayDescr::new(py, dtype)) - .unwrap_or_else(|| Ok(base_dtype.clone()))?; - if dtype.is_equiv_to(&base_dtype) { - return Ok(array.into_any()); - } - PyModule::import(py, intern!(py, "numpy"))? - .getattr(intern!(py, "array"))? - .call( - (array,), - Some(&[(intern!(py, "dtype"), dtype.as_any())].into_py_dict(py)?), - ) -} - -/// Attempt to coerce an arbitrary Python object to a [PySparseObservable]. -/// -/// This returns: -/// -/// * `Ok(Some(obs))` if the coercion was completely successful. -/// * `Ok(None)` if the input value was just completely the wrong type and no coercion could be -/// attempted. -/// * `Err` if the input was a valid type for coercion, but the coercion failed with a Python -/// exception. -/// -/// The purpose of this is for conversion the arithmetic operations, which should return -/// [PyNotImplemented] if the type is not valid for coercion. -#[cfg(feature = "python")] -fn coerce_to_observable<'py>( - value: &Bound<'py, PyAny>, -) -> PyResult>> { - let py = value.py(); - if let Ok(obs) = value.cast_exact::() { - return Ok(Some(obs.clone())); - } - match PySparseObservable::py_new(value, None) { - Ok(obs) => Ok(Some(Bound::new(py, obs)?)), - Err(e) => { - if e.is_instance_of::(py) { - Ok(None) - } else { - Err(e) - } - } - } -} - -#[cfg(feature = "python")] -pub fn sparse_observable(m: &Bound) -> PyResult<()> { - m.add_class::()?; - Ok(()) -} - #[cfg(test)] mod test { use super::*; diff --git a/crates/transpiler/Cargo.toml b/crates/transpiler/Cargo.toml index 31aaa8e6466d..5f35cbc41b93 100644 --- a/crates/transpiler/Cargo.toml +++ b/crates/transpiler/Cargo.toml @@ -25,7 +25,7 @@ num-complex.workspace = true rustworkx-core.workspace = true qiskit-circuit.workspace = true qiskit-synthesis.workspace = true -qiskit-quantum-info.workspace = true +qiskit-quantum-info = { workspace = true, features = ["python"] } qiskit-util = { workspace = true, features = ["py"] } thiserror.workspace = true bytemuck.workspace = true diff --git a/crates/transpiler/src/commutation_checker.rs b/crates/transpiler/src/commutation_checker.rs index e0210630ee79..24a9a6f61b26 100644 --- a/crates/transpiler/src/commutation_checker.rs +++ b/crates/transpiler/src/commutation_checker.rs @@ -17,7 +17,8 @@ use num_complex::Complex64; use num_complex::ComplexFloat; use qiskit_circuit::object_registry::PyObjectAsKey; use qiskit_circuit::standard_gate::standard_generators::standard_gate_exponent; -use qiskit_quantum_info::sparse_observable::{PySparseObservable, SparseObservable}; +use qiskit_quantum_info::python::sparse_observable::PySparseObservable; +use qiskit_quantum_info::sparse_observable::SparseObservable; use smallvec::SmallVec; use std::fmt::Debug;