Skip to content

Commit 747f955

Browse files
yaelbhmergify[bot]
authored andcommitted
doc fix (#13914)
(cherry picked from commit 67c7406) # Conflicts: # crates/accelerate/src/sparse_observable.rs
1 parent a92f14e commit 747f955

File tree

1 file changed

+290
-0
lines changed

1 file changed

+290
-0
lines changed

crates/accelerate/src/sparse_observable.rs

+290
Original file line numberDiff line numberDiff line change
@@ -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+
>>>>>>> 67c740601 (doc fix (#13914))
368658
/// An observable over Pauli bases that stores its data in a qubit-sparse format.
369659
///
370660
/// Mathematics

0 commit comments

Comments
 (0)