1111// that they have been altered from the originals.
1212
1313use hashbrown:: HashSet ;
14+ use itertools:: Itertools ;
1415use ndarray:: Array2 ;
1516use num_complex:: Complex64 ;
1617use num_traits:: Zero ;
@@ -23,7 +24,7 @@ use pyo3::{
2324 intern,
2425 prelude:: * ,
2526 sync:: GILOnceCell ,
26- types:: { IntoPyDict , PyList , PyTuple , PyType } ,
27+ types:: { IntoPyDict , PyList , PyString , PyTuple , PyType } ,
2728 IntoPyObjectExt , PyErr ,
2829} ;
2930use std:: {
@@ -184,6 +185,24 @@ impl BitTerm {
184185 pub fn has_z_component ( & self ) -> bool {
185186 ( ( * self as u8 ) & ( Self :: Z as u8 ) ) != 0
186187 }
188+
189+ pub fn is_projector ( & self ) -> bool {
190+ !matches ! ( self , BitTerm :: X | BitTerm :: Y | BitTerm :: Z )
191+ }
192+ }
193+
194+ fn bit_term_as_pauli ( bit : & BitTerm ) -> & ' static [ ( bool , Option < BitTerm > ) ] {
195+ match bit {
196+ BitTerm :: X => & [ ( true , Some ( BitTerm :: X ) ) ] ,
197+ BitTerm :: Y => & [ ( true , Some ( BitTerm :: Y ) ) ] ,
198+ BitTerm :: Z => & [ ( true , Some ( BitTerm :: Z ) ) ] ,
199+ BitTerm :: Plus => & [ ( true , None ) , ( true , Some ( BitTerm :: X ) ) ] ,
200+ BitTerm :: Minus => & [ ( true , None ) , ( false , Some ( BitTerm :: X ) ) ] ,
201+ BitTerm :: Left => & [ ( true , None ) , ( true , Some ( BitTerm :: Y ) ) ] ,
202+ BitTerm :: Right => & [ ( true , None ) , ( false , Some ( BitTerm :: Y ) ) ] ,
203+ BitTerm :: Zero => & [ ( true , None ) , ( true , Some ( BitTerm :: Z ) ) ] ,
204+ BitTerm :: One => & [ ( true , None ) , ( false , Some ( BitTerm :: Z ) ) ] ,
205+ }
187206}
188207
189208/// The error type for a failed conversion into `BitTerm`.
@@ -641,6 +660,58 @@ impl SparseObservable {
641660 }
642661 }
643662
663+ /// Expand all projectors into Pauli representation.
664+ ///
665+ /// # Warning
666+ ///
667+ /// This representation is highly inefficient for projectors. For example, a term with
668+ /// :math:`n` projectors :math:`|+\rangle\langle +|` will use :math:`2^n` Pauli terms.
669+ pub fn as_paulis ( & self ) -> Self {
670+ let mut paulis: Vec < BitTerm > = Vec :: new ( ) ; // maybe get capacity here
671+ let mut indices: Vec < u32 > = Vec :: new ( ) ;
672+ let mut coeffs: Vec < Complex64 > = Vec :: new ( ) ;
673+ let mut boundaries: Vec < usize > = vec ! [ 0 ] ;
674+
675+ for view in self . iter ( ) {
676+ let num_projectors = view
677+ . bit_terms
678+ . iter ( )
679+ . filter ( |& bit| bit. is_projector ( ) )
680+ . count ( ) ;
681+ let div = 2_f64 . powi ( num_projectors as i32 ) ;
682+
683+ let combinations = view
684+ . bit_terms
685+ . iter ( )
686+ . map ( bit_term_as_pauli)
687+ . multi_cartesian_product ( ) ;
688+
689+ for combination in combinations {
690+ let mut positive = true ;
691+
692+ for ( index, ( sign, bit) ) in combination. iter ( ) . enumerate ( ) {
693+ positive &= sign;
694+ if let Some ( bit) = bit {
695+ paulis. push ( * bit) ;
696+ indices. push ( view. indices [ index] ) ;
697+ }
698+ }
699+ boundaries. push ( paulis. len ( ) ) ;
700+
701+ let coeff = if positive { view. coeff } else { -view. coeff } ;
702+ coeffs. push ( coeff / div)
703+ }
704+ }
705+
706+ Self {
707+ num_qubits : self . num_qubits ,
708+ coeffs,
709+ bit_terms : paulis,
710+ indices,
711+ boundaries,
712+ }
713+ }
714+
644715 /// Add the term implied by a dense string label onto this observable.
645716 pub fn add_dense_label < L : AsRef < [ u8 ] > > (
646717 & mut self ,
@@ -1741,10 +1812,11 @@ impl PySparseTerm {
17411812///
17421813/// .. note::
17431814///
1744- /// The canonical form produced by :meth:`simplify` will still not universally detect all
1745- /// observables that are equivalent due to the over-complete basis alphabet; it is not
1746- /// computationally feasible to do this at scale. For example, on observable built from ``+``
1747- /// and ``-`` components will not canonicalize to a single ``X`` term.
1815+ /// The canonical form produced by :meth:`simplify` alone will not universally detect all
1816+ /// observables that are equivalent due to the over-complete basis alphabet. To obtain a
1817+ /// unique expression, you can first represent the observable using Pauli terms only by
1818+ /// calling :meth:`as_paulis`, followed by :meth:`simplify`. Note that the projector
1819+ /// expansion (e.g. ``+`` into ``I`` and ``X``) is not computationally feasible at scale.
17481820///
17491821/// Indexing
17501822/// --------
@@ -1824,6 +1896,29 @@ impl PySparseTerm {
18241896/// :meth:`identity` The identity operator on a given number of qubits.
18251897/// ============================ ================================================================
18261898///
1899+ /// Conversions
1900+ /// ===========
1901+ ///
1902+ /// An existing :class:`SparseObservable` can be converted into other :mod:`~qiskit.quantum_info`
1903+ /// operators or generic formats. Beware that other objects may not be able to represent the same
1904+ /// observable as efficiently as :class:`SparseObservable`, including potentially needed
1905+ /// exponentially more memory.
1906+ ///
1907+ /// .. table:: Conversion methods to other observable forms.
1908+ ///
1909+ /// =========================== =================================================================
1910+ /// Method Summary
1911+ /// =========================== =================================================================
1912+ /// :meth:`as_paulis` Create a new :class:`SparseObservable`, expanding in terms
1913+ /// of Pauli operators only.
1914+ ///
1915+ /// :meth:`to_sparse_list` Express the observable in a sparse list format with elements
1916+ /// ``(bit_terms, indices, coeff)``.
1917+ /// =========================== =================================================================
1918+ ///
1919+ /// In addition, :meth:`.SparsePauliOp.from_sparse_observable` is available for conversion from this
1920+ /// class to :class:`.SparsePauliOp`. Beware that this method suffers from the same
1921+ /// exponential-memory usage concerns as :meth:`as_paulis`.
18271922///
18281923/// Mathematical manipulation
18291924/// =========================
@@ -1925,8 +2020,8 @@ impl PySparseObservable {
19252020 let inner = borrowed. inner . read ( ) . map_err ( |_| InnerReadError ) ?;
19262021 return Ok ( inner. clone ( ) . into ( ) ) ;
19272022 }
1928- // The type of `vec` is inferred from the subsequent calls to `Self::py_from_list ` or
1929- // `Self::py_from_sparse_list ` to be either the two-tuple or the three-tuple form during the
2023+ // The type of `vec` is inferred from the subsequent calls to `Self::from_list ` or
2024+ // `Self::from_sparse_list ` to be either the two-tuple or the three-tuple form during the
19302025 // `extract`. The empty list will pass either, but it means the same to both functions.
19312026 if let Ok ( vec) = data. extract ( ) {
19322027 return Self :: from_list ( vec, num_qubits) ;
@@ -2289,6 +2384,10 @@ impl PySparseObservable {
22892384 /// ... for label, coeff in zip(labels, coeffs)
22902385 /// ... ])
22912386 /// >>> assert from_list == from_sparse_list
2387+ ///
2388+ /// See also:
2389+ /// :meth:`to_sparse_list`
2390+ /// The reverse of this method.
22922391 #[ staticmethod]
22932392 #[ pyo3( signature = ( iter, /, num_qubits) ) ]
22942393 fn from_sparse_list (
@@ -2337,6 +2436,86 @@ impl PySparseObservable {
23372436 Ok ( inner. into ( ) )
23382437 }
23392438
2439+ /// Express the observable in Pauli terms only, by writing each projector as sum of Pauli terms.
2440+ ///
2441+ /// Note that there is no guarantee of the order the resulting Pauli terms. Use
2442+ /// :meth:`SparseObservable.simplify` in addition to obtain a canonical representation.
2443+ ///
2444+ /// .. warning::
2445+ ///
2446+ /// Beware that this will use at least :math:`2^n` terms if there are :math:`n`
2447+ /// single-qubit projectors present, which can lead to an exponential number of terms.
2448+ ///
2449+ /// Returns:
2450+ /// The same observable, but expressed in Pauli terms only.
2451+ ///
2452+ /// Examples:
2453+ ///
2454+ /// Rewrite an observable in terms of projectors into Pauli operators::
2455+ ///
2456+ /// >>> obs = SparseObservable("+")
2457+ /// >>> obs.as_paulis()
2458+ /// <SparseObservable with 2 terms on 1 qubit: (0.5+0j)() + (0.5+0j)(X_0)>
2459+ /// >>> direct = SparseObservable.from_list([("I", 0.5), ("Z", 0.5)])
2460+ /// >>> assert direct.simplify() == obs.as_paulis().simplify()
2461+ ///
2462+ /// For small operators, this can be used with :meth:`simplify` as a unique canonical form::
2463+ ///
2464+ /// >>> left = SparseObservable.from_list([("+", 0.5), ("-", 0.5)])
2465+ /// >>> right = SparseObservable.from_list([("r", 0.5), ("l", 0.5)])
2466+ /// >>> assert left.as_paulis().simplify() == right.as_paulis().simplify()
2467+ ///
2468+ /// See also:
2469+ /// :meth:`.SparsePauliOp.from_sparse_observable`
2470+ /// A constructor of :class:`.SparsePauliOp` that can convert a
2471+ /// :class:`SparseObservable` in the :class:`.SparsePauliOp` dense Pauli representation.
2472+ fn as_paulis ( & self ) -> PyResult < Self > {
2473+ let inner = self . inner . read ( ) . map_err ( |_| InnerReadError ) ?;
2474+ Ok ( inner. as_paulis ( ) . into ( ) )
2475+ }
2476+
2477+ /// Express the observable in terms of a sparse list format.
2478+ ///
2479+ /// This can be seen as counter-operation of :meth:`.SparseObservable.from_sparse_list`, however
2480+ /// the order of terms is not guaranteed to be the same at after a roundtrip to a sparse
2481+ /// list and back.
2482+ ///
2483+ /// Examples:
2484+ ///
2485+ /// >>> obs = SparseObservable.from_list([("IIXIZ", 2j), ("IIZIX", 2j)])
2486+ /// >>> reconstructed = SparseObservable.from_sparse_list(obs.to_sparse_list(), obs.num_qubits)
2487+ ///
2488+ /// See also:
2489+ /// :meth:`from_sparse_list`
2490+ /// The constructor that can interpret these lists.
2491+ #[ pyo3( signature = ( ) ) ]
2492+ fn to_sparse_list ( & self , py : Python ) -> PyResult < Py < PyList > > {
2493+ let inner = self . inner . read ( ) . map_err ( |_| InnerReadError ) ?;
2494+
2495+ // turn a SparseView into a Python tuple of (bit terms, indices, coeff)
2496+ let to_py_tuple = |view : SparseTermView | {
2497+ let mut pauli_string = String :: with_capacity ( view. bit_terms . len ( ) ) ;
2498+
2499+ // we reverse the order of bits and indices so the Pauli string comes out in
2500+ // "reading order", consistent with how one would write the label in
2501+ // SparseObservable.from_list or .from_label
2502+ for bit in view. bit_terms . iter ( ) . rev ( ) {
2503+ pauli_string. push_str ( bit. py_label ( ) ) ;
2504+ }
2505+ let py_string = PyString :: new ( py, & pauli_string) . unbind ( ) ;
2506+ let py_indices = PyList :: new ( py, view. indices . iter ( ) . rev ( ) ) ?. unbind ( ) ;
2507+ let py_coeff = view. coeff . into_py_any ( py) ?;
2508+
2509+ PyTuple :: new ( py, vec ! [ py_string. as_any( ) , py_indices. as_any( ) , & py_coeff] )
2510+ } ;
2511+
2512+ let out = PyList :: empty ( py) ;
2513+ for view in inner. iter ( ) {
2514+ out. append ( to_py_tuple ( view) ?) ?;
2515+ }
2516+ Ok ( out. unbind ( ) )
2517+ }
2518+
23402519 /// Construct a :class:`.SparseObservable` from a :class:`.SparsePauliOp` instance.
23412520 ///
23422521 /// This will be a largely direct translation of the :class:`.SparsePauliOp`; in particular,
0 commit comments