From a5d7cccb169d1b845c84132e6619e84610bff328 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 13 Jul 2026 10:23:05 -0400 Subject: [PATCH 1/2] Reorganize quantum_info crate to clearly separate Python interface code In #16567 we added support for conditional compilation of the python interface code in quantum info. This made it possible to build the crate in isolation without needing PyO3 or python present. However, the mechanics of how it went about this was fairly brute force, it added a cfg attribute on everything using python in the crate. While this was fine for a first pass it wasn't the best for maintainability because the Python interface code is mixed in with everything else. In an attempt to create a cleaner separation between the Python code and the rest of the crate this commit adds a new top level module `quantum_info::python` to the crate which contains all the Python interface code. This both keeps a clear separation of concerns for the module and also moves to only needing a single cfg attribute. This also catches a few places in the dependencies of other crates where the quantum-info crate's python feature was not explicitly listed even though it depended on the feature. While cargo was resolving the feature as part of the circuit crate in most cases, making this explicit makes it easier to work with in the future, especially as we work up the dependency tree on removing default pyo3/python usage. --- crates/accelerate/Cargo.toml | 2 +- crates/accelerate/src/sampled_exp_val.rs | 2 +- crates/cext/Cargo.toml | 2 +- crates/cext/src/sparse_observable.rs | 3 +- crates/circuit/src/operations.rs | 2 +- crates/pyext/src/lib.rs | 6 +- crates/qpy/src/py_methods.rs | 2 +- crates/quantum_info/src/lib.rs | 21 +- .../src/pauli_lindblad_map/mod.rs | 19 - .../pauli_lindblad_map_class.rs | 1423 +-------- .../phased_qubit_sparse_pauli.rs | 1285 +------- .../pauli_lindblad_map/qubit_sparse_pauli.rs | 1466 --------- crates/quantum_info/src/python/mod.rs | 28 + .../src/python/pauli_lindblad_map/mod.rs | 29 + .../pauli_lindblad_map_class.rs | 1414 +++++++++ .../phased_qubit_sparse_pauli.rs | 1261 ++++++++ .../pauli_lindblad_map/qubit_sparse_pauli.rs | 1460 +++++++++ .../src/{ => python}/rayon_ext.rs | 0 .../src/python/sparse_observable.rs | 2721 ++++++++++++++++ .../src/{ => python}/sparse_pauli_op.rs | 2 +- .../quantum_info/src/sparse_observable/mod.rs | 2741 +---------------- crates/transpiler/Cargo.toml | 2 +- crates/transpiler/src/commutation_checker.rs | 3 +- 23 files changed, 6941 insertions(+), 6953 deletions(-) create mode 100644 crates/quantum_info/src/python/mod.rs create mode 100644 crates/quantum_info/src/python/pauli_lindblad_map/mod.rs create mode 100644 crates/quantum_info/src/python/pauli_lindblad_map/pauli_lindblad_map_class.rs create mode 100644 crates/quantum_info/src/python/pauli_lindblad_map/phased_qubit_sparse_pauli.rs create mode 100644 crates/quantum_info/src/python/pauli_lindblad_map/qubit_sparse_pauli.rs rename crates/quantum_info/src/{ => python}/rayon_ext.rs (100%) create mode 100644 crates/quantum_info/src/python/sparse_observable.rs rename crates/quantum_info/src/{ => python}/sparse_pauli_op.rs (99%) 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..de4e7fbf0758 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, } @@ -176,22 +148,6 @@ impl PhasedQubitSparsePauliList { Ok(()) } - // Check equality of operators - #[cfg(feature = "python")] // Only currently used by python remove if needed from rust - fn eq(&self, other: &PhasedQubitSparsePauliList) -> bool { - if self.qubit_sparse_pauli_list != other.qubit_sparse_pauli_list { - return false; - } - - // assume here number of terms is equal - for (self_phase, other_phase) in self.phases.iter().zip(other.phases.iter()) { - if (self_phase - other_phase).rem_euclid(4) != 0 { - return false; - } - } - - true - } /// Apply a transpiler layout. pub fn apply_layout( &self, @@ -245,10 +201,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 { @@ -397,1239 +353,4 @@ impl PhasedQubitSparsePauli { self.qubit_sparse_pauli.commutes(&other.qubit_sparse_pauli) } - - // Check equality of operators - #[cfg(feature = "python")] // Only currently used by python remove if needed from rust - 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; From 953ffc69ecdff3f58dbc41aa5651b5a709708a67 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 13 Jul 2026 12:42:35 -0400 Subject: [PATCH 2/2] Restore eq methods on phase qubit sparse pauli types In the previous commit the `PhasedQubitSparsePauliList::eq` and `PhasedQubitSparsePauli::eq` methods were accidently removed. This was caused by a confusion in the implementation where both types derive the PartialEq trait which defines an eq method based on the fields in the types. This will be used for the `==` operator and also other places working with the generic `PartialEq` trait. However, for the python interface's `__eq__` method the custom eq methods were used. The overloaded names tripped me up and I accidently removed the inner eq method overzealously. In a follow up PR we should either move the custom eq to a custom `PartialEq` implementation or rename the overloaded `eq` method used for python equality. But for right now this just restores the previous state to keep this PR just a straight code reorganization. --- .../phased_qubit_sparse_pauli.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 de4e7fbf0758..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 @@ -148,6 +148,21 @@ impl PhasedQubitSparsePauliList { Ok(()) } + // Check equality of operators + pub fn eq(&self, other: &PhasedQubitSparsePauliList) -> bool { + if self.qubit_sparse_pauli_list != other.qubit_sparse_pauli_list { + return false; + } + + // assume here number of terms is equal + for (self_phase, other_phase) in self.phases.iter().zip(other.phases.iter()) { + if (self_phase - other_phase).rem_euclid(4) != 0 { + return false; + } + } + true + } + /// Apply a transpiler layout. pub fn apply_layout( &self, @@ -353,4 +368,10 @@ impl PhasedQubitSparsePauli { self.qubit_sparse_pauli.commutes(&other.qubit_sparse_pauli) } + + // Check equality of operators + pub fn eq(&self, other: &PhasedQubitSparsePauli) -> bool { + ((self.phase - other.phase).rem_euclid(4) == 0) + && self.qubit_sparse_pauli == other.qubit_sparse_pauli + } }