@@ -365,6 +365,296 @@ impl From<ArithmeticError> for PyErr {
365
365
}
366
366
}
367
367
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 ) )
368
658
/// An observable over Pauli bases that stores its data in a qubit-sparse format.
369
659
///
370
660
/// Mathematics
0 commit comments