@@ -365,6 +365,296 @@ impl From<ArithmeticError> for PyErr {
365365 }
366366}
367367
368+ <<<<<<< HEAD
369+ =======
370+ /// Construct the Python-space `IntEnum` that represents the same values as the Rust-spce `BitTerm`.
371+ ///
372+ /// We don't make `BitTerm` a direct `pyclass` because we want the behaviour of `IntEnum`, which
373+ /// specifically also makes its variants subclasses of the Python `int` type ; we use a type -safe
374+ /// enum in Rust, but from Python space we expect people to (carefully) deal with the raw ints in
375+ /// Numpy arrays for efficiency.
376+ ///
377+ /// The resulting class is attached to `SparseObservable` as a class attribute, and its
378+ /// `__qualname__` is set to reflect this.
379+ fn make_py_bit_term ( py : Python ) -> PyResult < Py < PyType > > {
380+ let terms = [
381+ BitTerm :: X ,
382+ BitTerm :: Plus ,
383+ BitTerm :: Minus ,
384+ BitTerm :: Y ,
385+ BitTerm :: Right ,
386+ BitTerm :: Left ,
387+ BitTerm :: Z ,
388+ BitTerm :: Zero ,
389+ BitTerm :: One ,
390+ ]
391+ . into_iter( )
392+ . flat_map( |term| {
393+ let mut out = vec ! [ ( term. py_name( ) , term as u8 ) ] ;
394+ if term. py_name ( ) != term. py_label ( ) {
395+ // Also ensure that the labels are created as aliases. These can't be (easily) accessed
396+ // by attribute-getter (dot) syntax, but will work with the item-getter (square-bracket)
397+ // syntax, or programmatically with `getattr`.
398+ out. push ( ( term. py_label ( ) , term as u8 ) ) ;
399+ }
400+ out
401+ } )
402+ . collect :: < Vec < _ > > ( ) ;
403+ let obj = py. import( "enum" ) ?. getattr( "IntEnum" ) ?. call(
404+ ( "BitTerm" , terms) ,
405+ Some (
406+ & [
407+ ( "module" , "qiskit.quantum_info" ) ,
408+ ( "qualname" , "SparseObservable.BitTerm" ) ,
409+ ]
410+ . into_py_dict( py) ?,
411+ ) ,
412+ ) ?;
413+ Ok ( obj. downcast_into :: < PyType > ( ) ?. unbind( ) )
414+ }
415+
416+ // Return the relevant value from the Python-space sister enumeration. These are Python-space
417+ // singletons and subclasses of Python `int`. We only use this for interaction with "high level"
418+ // Python space; the efficient Numpy-like array paths use `u8` directly so Numpy can act on it
419+ // efficiently.
420+ impl <' py > IntoPyObject < ' py > for BitTerm {
421+ type Target = PyAny ;
422+ type Output = Bound < ' py , PyAny > ;
423+ type Error = PyErr ;
424+
425+ fn into_pyobject ( self , py : Python < ' py > ) -> PyResult < Self :: Output > {
426+ let terms = BIT_TERM_INTO_PY . get_or_init( py , || {
427+ let py_enum = BIT_TERM_PY_ENUM
428+ . get_or_try_init( py, || make_py_bit_term( py) )
429+ . expect( "creating a simple Python enum class should be infallible" )
430+ . bind( py) ;
431+ :: std:: array:: from_fn ( |val| {
432+ :: bytemuck:: checked:: try_cast( val as u8 )
433+ . ok( )
434+ . map( |term : BitTerm | {
435+ py_enum
436+ . getattr ( term. py_name ( ) )
437+ . expect ( "the created `BitTerm` enum should have matching attribute names to the terms" )
438+ . unbind ( )
439+ } )
440+ } )
441+ } ) ;
442+ Ok ( terms[ self as usize ]
443+ . as_ref( )
444+ . expect( "the lookup table initializer populated a 'Some' in all valid locations" )
445+ . bind( py)
446+ . clone ( ) )
447+ }
448+ }
449+
450+ impl <' py> FromPyObject <' py> for BitTerm {
451+ fn extract_bound ( ob : & Bound < ' py , PyAny > ) -> PyResult < Self > {
452+ let value = ob
453+ . extract :: < isize > ( )
454+ . map_err( |_| match ob. get_type ( ) . repr ( ) {
455+ Ok ( repr) => PyTypeError :: new_err ( format ! ( "bad type for 'BitTerm': {}" , repr) ) ,
456+ Err ( err) => err,
457+ } ) ?;
458+ let value_error = || {
459+ PyValueError :: new_err( format ! (
460+ "value {} is not a valid letter of the single-qubit alphabet for 'BitTerm'" ,
461+ value
462+ ) )
463+ } ;
464+ let value: u8 = value. try_into( ) . map_err( |_| value_error( ) ) ?;
465+ value. try_into( ) . map_err( |_| value_error( ) )
466+ }
467+ }
468+
469+ /// A single term from a complete :class:`SparseObservable`.
470+ ///
471+ /// These are typically created by indexing into or iterating through a :class:`SparseObservable`.
472+ #[ pyclass( name = "Term" , frozen, module = "qiskit.quantum_info" ) ]
473+ #[ derive( Clone , Debug ) ]
474+ struct PySparseTerm {
475+ inner : SparseTerm ,
476+ }
477+ #[ pymethods]
478+ impl PySparseTerm {
479+ // Mark the Python class as being defined "within" the `SparseObservable` class namespace.
480+ #[ classattr]
481+ #[ pyo3( name = "__qualname__" ) ]
482+ fn type_qualname( ) -> & ' static str {
483+ "SparseObservable.Term"
484+ }
485+
486+ #[ new]
487+ #[ pyo3( signature = ( /, num_qubits, coeff, bit_terms, indices) ) ]
488+ fn py_new(
489+ num_qubits: u32 ,
490+ coeff: Complex64 ,
491+ bit_terms: Vec < BitTerm > ,
492+ indices: Vec < u32 > ,
493+ ) -> PyResult < Self > {
494+ if bit_terms. len( ) != indices. len( ) {
495+ return Err ( CoherenceError :: MismatchedItemCount {
496+ bit_terms : bit_terms. len( ) ,
497+ indices : indices. len( ) ,
498+ }
499+ . into( ) ) ;
500+ }
501+ let mut order = ( 0 ..bit_terms. len( ) ) . collect :: < Vec < _ > > ( ) ;
502+ order. sort_unstable_by_key( |a| indices[ * a] ) ;
503+ let bit_terms = order. iter( ) . map( |i| bit_terms[ * i] ) . collect( ) ;
504+ let mut sorted_indices = Vec :: < u32 > :: with_capacity( order. len( ) ) ;
505+ for i in order {
506+ let index = indices[ i] ;
507+ if sorted_indices
508+ . last( )
509+ . map( |prev| * prev >= index)
510+ . unwrap_or( false )
511+ {
512+ return Err ( CoherenceError :: UnsortedIndices . into( ) ) ;
513+ }
514+ sorted_indices. push( index)
515+ }
516+ let inner = SparseTerm :: new(
517+ num_qubits,
518+ coeff,
519+ bit_terms,
520+ sorted_indices. into_boxed_slice( ) ,
521+ ) ?;
522+ Ok ( PySparseTerm { inner } )
523+ }
524+
525+ /// Convert this term to a complete :class:`SparseObservable`.
526+ fn to_observable( & self ) -> PyResult < PySparseObservable > {
527+ let obs = SparseObservable :: new(
528+ self . inner. num_qubits( ) ,
529+ vec ! [ self . inner. coeff( ) ] ,
530+ self . inner. bit_terms( ) . to_vec( ) ,
531+ self . inner. indices( ) . to_vec( ) ,
532+ vec ! [ 0 , self . inner. bit_terms( ) . len( ) ] ,
533+ ) ?;
534+ Ok ( obs. into( ) )
535+ }
536+
537+ fn __eq__( slf: Bound < Self > , other: Bound < PyAny > ) -> PyResult < bool > {
538+ if slf. is( & other) {
539+ return Ok ( true ) ;
540+ }
541+ let Ok ( other) = other. downcast_into :: < Self > ( ) else {
542+ return Ok ( false ) ;
543+ } ;
544+ let slf = slf. borrow( ) ;
545+ let other = other. borrow( ) ;
546+ Ok ( slf. inner. eq( & other. inner) )
547+ }
548+
549+ fn __repr__( & self ) -> PyResult < String > {
550+ Ok ( format ! (
551+ "<{} on {} qubit{}: {}>" ,
552+ Self :: type_qualname( ) ,
553+ self . inner. num_qubits( ) ,
554+ if self . inner. num_qubits( ) == 1 {
555+ ""
556+ } else {
557+ "s"
558+ } ,
559+ self . inner. view( ) . to_sparse_str( ) ,
560+ ) )
561+ }
562+
563+ fn __getnewargs__( slf_: Bound <Self >) -> PyResult < Bound < PyTuple > > {
564+ let py = slf_. py( ) ;
565+ let borrowed = slf_. borrow( ) ;
566+ (
567+ borrowed. inner. num_qubits( ) ,
568+ borrowed. inner. coeff( ) ,
569+ Self :: get_bit_terms( slf_. clone( ) ) ,
570+ Self :: get_indices( slf_) ,
571+ )
572+ . into_pyobject( py)
573+ }
574+
575+ /// Get a copy of this term.
576+ fn copy( & self ) -> Self {
577+ self. clone( )
578+ }
579+
580+ /// Read-only view onto the individual single-qubit terms.
581+ ///
582+ /// The only valid values in the array are those with a corresponding
583+ /// :class:`~SparseObservable.BitTerm`.
584+ #[ getter]
585+ fn get_bit_terms( slf_: Bound < Self > ) -> Bound < PyArray1 < u8 > > {
586+ let borrowed = slf_. borrow( ) ;
587+ let bit_terms = borrowed. inner. bit_terms( ) ;
588+ let arr = :: ndarray:: aview1( :: bytemuck:: cast_slice:: < _ , u8 > ( bit_terms) ) ;
589+ // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python.
590+ // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the
591+ // `Box<[BitTerm]>` allocation (including dropping or reallocating it) other than the entire
592+ // object getting dropped, which Python will keep safe.
593+ let out = unsafe { PyArray1 :: borrow_from_array( & arr, slf_. into_any( ) ) } ;
594+ out. readwrite( ) . make_nonwriteable( ) ;
595+ out
596+ }
597+
598+ /// The number of qubits the term is defined on.
599+ #[ getter]
600+ fn get_num_qubits( & self ) -> u32 {
601+ self . inner. num_qubits( )
602+ }
603+
604+ /// The term's coefficient.
605+ #[ getter]
606+ fn get_coeff( & self ) -> Complex64 {
607+ self. inner. coeff( )
608+ }
609+
610+ /// Read-only view onto the indices of each non-identity single-qubit term.
611+ ///
612+ /// The indices will always be in sorted order.
613+ #[ getter]
614+ fn get_indices( slf_: Bound < Self > ) -> Bound < PyArray1 < u32 > > {
615+ let borrowed = slf_. borrow( ) ;
616+ let indices = borrowed. inner. indices( ) ;
617+ let arr = :: ndarray:: aview1( indices) ;
618+ // SAFETY: in order to call this function, the lifetime of `self` must be managed by Python.
619+ // We tie the lifetime of the array to `slf_`, and there are no public ways to modify the
620+ // `Box<[u32]>` allocation (including dropping or reallocating it) other than the entire
621+ // object getting dropped, which Python will keep safe.
622+ let out = unsafe { PyArray1 :: borrow_from_array( & arr, slf_. into_any( ) ) } ;
623+ out. readwrite( ) . make_nonwriteable( ) ;
624+ out
625+ }
626+
627+ /// Get a :class:`.Pauli` object that represents the measurement basis needed for this term.
628+ ///
629+ /// For example, the projector ``0l+`` will return a Pauli ``ZYX``. The resulting
630+ /// :class:`.Pauli` is dense, in the sense that explicit identities are stored. An identity in
631+ /// the Pauli output does not require a concrete measurement.
632+ ///
633+ /// Returns:
634+ /// :class:`.Pauli`: the Pauli operator representing the necessary measurement basis.
635+ ///
636+ /// See also:
637+ /// :meth:`SparseObservable.pauli_bases`
638+ /// A similar method for an entire observable at once.
639+ fn pauli_base < ' py > ( & self , py: Python < ' py > ) -> PyResult < Bound < ' py , PyAny > > {
640+ let mut x = vec ! [ false ; self . inner. num_qubits( ) as usize ] ;
641+ let mut z = vec ! [ false ; self . inner. num_qubits( ) as usize ] ;
642+ for ( bit_term, index) in self
643+ . inner
644+ . bit_terms( )
645+ . iter( )
646+ . zip( self . inner. indices( ) . iter( ) )
647+ {
648+ x[ * index as usize] = bit_term. has_x_component( ) ;
649+ z[ * index as usize] = bit_term. has_z_component( ) ;
650+ }
651+ PAULI_TYPE
652+ . get_bound( py)
653+ . call1( ( ( PyArray1 :: from_vec( py, z) , PyArray1 :: from_vec( py, x) ) , ) )
654+ }
655+ }
656+
657+ >>>>>>> 67 c740601 ( doc fix ( #13914 ) )
368658/// An observable over Pauli bases that stores its data in a qubit-sparse format.
369659///
370660/// Mathematics
0 commit comments