diff --git a/paulimer/bindings/python/paulimer.pyi b/paulimer/bindings/python/paulimer.pyi index 15c424d..5642c48 100644 --- a/paulimer/bindings/python/paulimer.pyi +++ b/paulimer/bindings/python/paulimer.pyi @@ -169,6 +169,30 @@ class DensePauli: """ ... + def indexed_anti_commutators_of(self, others: Iterable["DensePauli" | "SparsePauli"]) -> list[int]: + """Return the indices of operators in ``others`` that anticommute with this operator. + + Args: + others: An iterable of Pauli operators to test against. + + Returns: + A list of integer indices into ``others`` for operators that + anticommute with this operator. + """ + ... + + def indexed_commutators_of(self, others: Iterable["DensePauli" | "SparsePauli"]) -> list[int]: + """Return the indices of operators in ``others`` that commute with this operator. + + Args: + others: An iterable of Pauli operators to test against. + + Returns: + A list of integer indices into ``others`` for operators that + commute with this operator. + """ + ... + def copy(self) -> "DensePauli": ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... @@ -277,6 +301,30 @@ class SparsePauli: """Check if this operator commutes with another or collection of operators.""" ... + def indexed_anti_commutators_of(self, others: Iterable["DensePauli" | "SparsePauli"]) -> list[int]: + """Return the indices of operators in ``others`` that anticommute with this operator. + + Args: + others: An iterable of Pauli operators to test against. + + Returns: + A list of integer indices into ``others`` for operators that + anticommute with this operator. + """ + ... + + def indexed_commutators_of(self, others: Iterable["DensePauli" | "SparsePauli"]) -> list[int]: + """Return the indices of operators in ``others`` that commute with this operator. + + Args: + others: An iterable of Pauli operators to test against. + + Returns: + A list of integer indices into ``others`` for operators that + commute with this operator. + """ + ... + def copy(self) -> "SparsePauli": ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... diff --git a/paulimer/bindings/python/src/py_clifford.rs b/paulimer/bindings/python/src/py_clifford.rs index 8443467..a664c22 100644 --- a/paulimer/bindings/python/src/py_clifford.rs +++ b/paulimer/bindings/python/src/py_clifford.rs @@ -3,10 +3,12 @@ use paulimer::clifford::{ group_encoding_clifford_of, split_phased_css, split_qubit_cliffords_and_css, Clifford, CliffordMutable, CliffordUnitary, XOrZ, }; -use paulimer::pauli::{as_sparse, DensePauli, SparsePauli}; +use paulimer::pauli::{anti_commutes_with, as_sparse, dense_from, DensePauli, Pauli, SparsePauli}; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::PySliceIndices; +use pyo3::types::{PyIterator, PySliceIndices}; + +use binar::{vec::AlignedBitVec, BitwisePair, IndexSet}; use crate::enums::PyUnitaryOp; use crate::format_spec::parse_format_spec; @@ -25,6 +27,48 @@ impl PyPauliInput { PyPauliInput::Sparse(sparse) => sparse.clone(), } } + + pub fn to_dense(&self, qubit_count: usize) -> DensePauli { + match self { + PyPauliInput::Dense(dense) => dense_from(dense, qubit_count), + PyPauliInput::Sparse(sparse) => dense_from(sparse, qubit_count), + } + } + + pub fn anti_commutes_with(&self, other: &P) -> bool + where + P::Bits: BitwisePair + BitwisePair, + { + match self { + PyPauliInput::Dense(ref d) => anti_commutes_with(other, d), + PyPauliInput::Sparse(ref s) => anti_commutes_with(other, s), + } + } + + pub fn commutes_with(&self, other: &P) -> bool + where + P::Bits: BitwisePair + BitwisePair, + { + !self.anti_commutes_with(other) + } +} + +pub(crate) fn indexes_of_paulis_where( + observable: &P, + paulis: &Bound<'_, PyAny>, + predicate: fn(&PyPauliInput, &P) -> bool, +) -> PyResult> +where + P::Bits: BitwisePair + BitwisePair, +{ + let iter = PyIterator::from_object(paulis)?; + let mut result = Vec::new(); + for (i, item) in iter.enumerate() { + if predicate(&item?.extract::()?, observable) { + result.push(i); + } + } + Ok(result) } impl<'a, 'py> FromPyObject<'a, 'py> for PyPauliInput { diff --git a/paulimer/bindings/python/src/py_dense_pauli.rs b/paulimer/bindings/python/src/py_dense_pauli.rs index 581dfaf..1a4b79f 100644 --- a/paulimer/bindings/python/src/py_dense_pauli.rs +++ b/paulimer/bindings/python/src/py_dense_pauli.rs @@ -16,6 +16,7 @@ use pyo3::{ }; use crate::format_spec::parse_format_spec; +use crate::py_clifford::{indexes_of_paulis_where, PyPauliInput}; use crate::py_sparse_pauli::PySparsePauli; #[derive(Clone, Deref, DerefMut, From, Into)] @@ -162,6 +163,18 @@ impl PyDensePauli { Ok(true) } + /// # Errors + /// Will return an error if the extraction of Pauli(s) fails. + pub fn indexed_anti_commutators_of(&self, others: &Bound<'_, PyAny>) -> PyResult> { + indexes_of_paulis_where(&self.inner, others, PyPauliInput::anti_commutes_with) + } + + /// # Errors + /// Will return an error if the extraction of Pauli(s) fails. + pub fn indexed_commutators_of(&self, others: &Bound<'_, PyAny>) -> PyResult> { + indexes_of_paulis_where(&self.inner, others, PyPauliInput::commutes_with) + } + /// # Errors /// /// Will return error for >,>=,<,<= diff --git a/paulimer/bindings/python/src/py_sparse_pauli.rs b/paulimer/bindings/python/src/py_sparse_pauli.rs index 2c93519..6c8f53f 100644 --- a/paulimer/bindings/python/src/py_sparse_pauli.rs +++ b/paulimer/bindings/python/src/py_sparse_pauli.rs @@ -13,6 +13,7 @@ use std::collections::HashMap; use std::hash::{DefaultHasher, Hash, Hasher}; use crate::format_spec::parse_format_spec; +use crate::py_clifford::{indexes_of_paulis_where, PyPauliInput}; use crate::py_dense_pauli::PyDensePauli; #[must_use] @@ -184,6 +185,18 @@ impl PySparsePauli { Ok(true) } + /// # Errors + /// Will return an error if the extraction of Pauli(s) fails. + pub fn indexed_anti_commutators_of(&self, others: &Bound<'_, PyAny>) -> PyResult> { + indexes_of_paulis_where(&self.inner, others, PyPauliInput::anti_commutes_with) + } + + /// # Errors + /// Will return an error if the extraction of Pauli(s) fails. + pub fn indexed_commutators_of(&self, others: &Bound<'_, PyAny>) -> PyResult> { + indexes_of_paulis_where(&self.inner, others, PyPauliInput::commutes_with) + } + /// # Errors /// /// Will return for >, <, >=, <= comparisons diff --git a/paulimer/bindings/python/tests/commutation_test.py b/paulimer/bindings/python/tests/commutation_test.py new file mode 100644 index 0000000..c732a47 --- /dev/null +++ b/paulimer/bindings/python/tests/commutation_test.py @@ -0,0 +1,93 @@ +from paulimer import DensePauli, SparsePauli + + +def test_dense_indexed_anti_commutators_of(): + x = DensePauli("X") + z = DensePauli("Z") + y = DensePauli("Y") + i = DensePauli("I") + result = x.indexed_anti_commutators_of([x, z, y, i]) + assert sorted(result) == [1, 2] + + +def test_dense_indexed_anti_commutators_of_all_commute(): + x = DensePauli("X") + result = x.indexed_anti_commutators_of([x, DensePauli("I")]) + assert result == [] + + +def test_dense_indexed_anti_commutators_of_empty(): + x = DensePauli("X") + result = x.indexed_anti_commutators_of([]) + assert result == [] + + +def test_dense_indexed_commutators_of(): + x = DensePauli("X") + z = DensePauli("Z") + y = DensePauli("Y") + i = DensePauli("I") + result = x.indexed_commutators_of([x, z, y, i]) + assert sorted(result) == [0, 3] + + +def test_dense_indexed_commutators_of_all_anticommute(): + x = DensePauli("X") + z = DensePauli("Z") + y = DensePauli("Y") + result = x.indexed_commutators_of([z, y]) + assert result == [] + + +def test_dense_indexed_commutators_of_empty(): + x = DensePauli("X") + result = x.indexed_commutators_of([]) + assert result == [] + + +def test_sparse_indexed_anti_commutators_of(): + x = SparsePauli("X_0") + z = SparsePauli("Z_0") + i = SparsePauli("I") + result = x.indexed_anti_commutators_of([z, i, x]) + assert result == [0] + + +def test_sparse_indexed_commutators_of(): + x = SparsePauli("X_0") + z = SparsePauli("Z_0") + i = SparsePauli("I") + result = x.indexed_commutators_of([z, i, x]) + assert sorted(result) == [1, 2] + + +def test_dense_with_sparse_args_indexed_commutators_of(): + x = DensePauli("X") + z = SparsePauli("Z_0") + i = SparsePauli("I") + result = x.indexed_commutators_of([z, i]) + assert result == [1] + + +def test_dense_with_sparse_args_indexed_anti_commutators_of(): + x = DensePauli("X") + z = SparsePauli("Z_0") + i = SparsePauli("I") + result = x.indexed_anti_commutators_of([z, i]) + assert result == [0] + + +def test_sparse_with_dense_args_indexed_commutators_of(): + x = SparsePauli("X_0") + z = DensePauli("Z") + i = DensePauli("I") + result = x.indexed_commutators_of([z, i]) + assert result == [1] + + +def test_sparse_with_dense_args_indexed_anti_commutators_of(): + x = SparsePauli("X_0") + z = DensePauli("Z") + i = DensePauli("I") + result = x.indexed_anti_commutators_of([z, i]) + assert result == [0] diff --git a/paulimer/src/pauli.rs b/paulimer/src/pauli.rs index db65251..d97adf0 100644 --- a/paulimer/src/pauli.rs +++ b/paulimer/src/pauli.rs @@ -17,7 +17,8 @@ pub use sparse::{SparsePauli, SparsePauliProjective, as_sparse, as_sparse_projec mod algorithms; pub use algorithms::{ apply_pauli_exponent, apply_root_x, apply_root_y, apply_root_z, are_mutually_commuting, - are_the_same_group_up_to_phases, complete_to_full_pauli_basis, paulis_qubit_count, + are_the_same_group_up_to_phases, complete_to_full_pauli_basis, indexed_anti_commutators_of, indexed_commutators_of, + paulis_qubit_count, }; use binar::{Bitwise, BitwiseMut, BitwisePair, BitwisePairMut}; diff --git a/paulimer/src/pauli/algorithms.rs b/paulimer/src/pauli/algorithms.rs index 6af14f9..7b61a60 100644 --- a/paulimer/src/pauli/algorithms.rs +++ b/paulimer/src/pauli/algorithms.rs @@ -1,7 +1,7 @@ -use super::{DensePauli, Pauli, PauliBinaryOps, anti_commutes_with}; +use super::{DensePauli, Pauli, PauliBinaryOps, anti_commutes_with, commutes_with}; use crate::setwise::complement; use crate::traits::NeutralElement; -use binar::{BitMatrix, Bitwise}; +use binar::{BitMatrix, Bitwise, BitwisePair, IndexSet}; /// # Panics /// Will panic if the input `paulis` are not independent @@ -70,6 +70,44 @@ pub fn are_the_same_group_up_to_phases( } } +fn indexes_of_paulis_where( + observable: &Observable, + paulis: &[PauliLike], + predicate: fn(&Observable, &PauliLike) -> bool, +) -> IndexSet +where + Observable::Bits: BitwisePair, +{ + paulis + .iter() + .enumerate() + .filter(|(_, p)| predicate(observable, *p)) + .map(|(i, _)| i) + .collect() +} + +/// Returns the indices of Pauli operators in `paulis` that anticommute with `observable`. +pub fn indexed_anti_commutators_of( + observable: &Observable, + paulis: &[PauliLike], +) -> IndexSet +where + Observable::Bits: BitwisePair, +{ + indexes_of_paulis_where(observable, paulis, anti_commutes_with) +} + +/// Returns the indices of Pauli operators in `paulis` that commute with `observable`. +pub fn indexed_commutators_of( + observable: &Observable, + paulis: &[PauliLike], +) -> IndexSet +where + Observable::Bits: BitwisePair, +{ + indexes_of_paulis_where(observable, paulis, commutes_with) +} + pub fn are_mutually_commuting(paulis: &[PauliLike]) -> bool { for i in 0..paulis.len() { for j in 0..i { diff --git a/paulimer/tests/pauli_test.rs b/paulimer/tests/pauli_test.rs index f5c952f..9e40105 100644 --- a/paulimer/tests/pauli_test.rs +++ b/paulimer/tests/pauli_test.rs @@ -2,13 +2,14 @@ use core::fmt; use std::collections::HashSet; use std::str::FromStr; +use binar::Bitwise; use binar::vec::AlignedBitViewMut as MutableBitView; use paulimer::StringLayout::{Dense, Sparse}; use paulimer::StringNotation::{Ascii, Tex, Unicode}; use paulimer::core::{x, y, z}; use paulimer::pauli::{ DensePauli, DensePauliProjective, Pauli, PauliBinaryOps, PauliMutable, PauliUnitary, Phase, SparsePauli, - SparsePauliProjective, commutes_with, generic::PhaseExponent, + SparsePauliProjective, commutes_with, generic::PhaseExponent, indexed_anti_commutators_of, indexed_commutators_of, }; use proptest::prelude::*; @@ -437,3 +438,49 @@ fn pauli_tex_notation() { let identity: DensePauli = "I".parse().unwrap(); assert_eq!(identity.to_string_with(Dense, Tex), "I"); } + +#[test] +fn indexed_anti_commutators_of_known() { + let x: DensePauli = "X".parse().unwrap(); + let z: DensePauli = "Z".parse().unwrap(); + let y: DensePauli = "Y".parse().unwrap(); + let i: DensePauli = "I".parse().unwrap(); + + let paulis = vec![x.clone(), z.clone(), y.clone(), i.clone()]; + let result = indexed_anti_commutators_of(&x, &paulis); + assert!(result.index(1)); // Z + assert!(result.index(2)); // Y + assert!(!result.index(0)); // X commutes with X + assert!(!result.index(3)); // I commutes with everything +} + +#[test] +fn indexed_anti_commutators_of_empty() { + let x: DensePauli = "X".parse().unwrap(); + let empty: Vec = vec![]; + let result = indexed_anti_commutators_of(&x, &empty); + assert_eq!(result.weight(), 0); +} + +#[test] +fn indexed_commutators_of_known() { + let x: DensePauli = "X".parse().unwrap(); + let z: DensePauli = "Z".parse().unwrap(); + let y: DensePauli = "Y".parse().unwrap(); + let i: DensePauli = "I".parse().unwrap(); + + let paulis = vec![x.clone(), z.clone(), y.clone(), i.clone()]; + let result = indexed_commutators_of(&x, &paulis); + assert!(result.index(0)); // X commutes with X + assert!(result.index(3)); // I commutes with everything + assert!(!result.index(1)); // Z anticommutes with X + assert!(!result.index(2)); // Y anticommutes with X +} + +#[test] +fn indexed_commutators_of_empty() { + let x: DensePauli = "X".parse().unwrap(); + let empty: Vec = vec![]; + let result = indexed_commutators_of(&x, &empty); + assert_eq!(result.weight(), 0); +}