use std::fmt;
use std::str::FromStr;
use super::{check_qubits, matrix_from_rows, StabilizerError};
use crate::ffield::GfMatrix;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PauliOperator {
n: usize,
x: Vec<u8>,
z: Vec<u8>,
phase: u8,
}
impl PauliOperator {
pub fn identity(n: usize) -> Result<Self, StabilizerError> {
check_qubits(n)?;
Ok(Self {
n,
x: vec![0; n],
z: vec![0; n],
phase: 0,
})
}
pub fn from_xz(x: &[u8], z: &[u8], phase: u8) -> Result<Self, StabilizerError> {
if x.len() != z.len() {
return Err(StabilizerError::ShapeMismatch {
op: "build a Pauli operator",
expected: "x and z of the same length".to_string(),
got: format!("lengths {} and {}", x.len(), z.len()),
});
}
check_qubits(x.len())?;
for (&a, &b) in x.iter().zip(z) {
if a > 1 || b > 1 {
return Err(StabilizerError::MalformedPauli {
reason: format!("bits must be 0 or 1; got {}", a.max(b)),
});
}
}
Ok(Self {
n: x.len(),
x: x.to_vec(),
z: z.to_vec(),
phase: phase % 4,
})
}
pub fn from_symplectic(bits: &[u8], phase: u8) -> Result<Self, StabilizerError> {
if bits.len() % 2 != 0 {
return Err(StabilizerError::ShapeMismatch {
op: "build a Pauli operator",
expected: "a symplectic vector of even length 2n".to_string(),
got: format!("length {}", bits.len()),
});
}
let n = bits.len() / 2;
Self::from_xz(&bits[..n], &bits[n..], phase)
}
pub fn hermitian(x: &[u8], z: &[u8], negative: bool) -> Result<Self, StabilizerError> {
let p = Self::from_xz(x, z, 0)?;
let y = p.y_count();
Self::from_xz(x, z, ((y % 4) as u8 + if negative { 2 } else { 0 }) % 4)
}
pub fn qubits(&self) -> usize {
self.n
}
pub fn x_bits(&self) -> &[u8] {
&self.x
}
pub fn z_bits(&self) -> &[u8] {
&self.z
}
pub fn phase(&self) -> u8 {
self.phase
}
pub fn symplectic(&self) -> Vec<u8> {
let mut v = Vec::with_capacity(2 * self.n);
v.extend_from_slice(&self.x);
v.extend_from_slice(&self.z);
v
}
fn y_count(&self) -> usize {
self.x
.iter()
.zip(&self.z)
.filter(|(&a, &b)| a & b == 1)
.count()
}
pub fn weight(&self) -> usize {
self.x
.iter()
.zip(&self.z)
.filter(|(&a, &b)| (a | b) == 1)
.count()
}
pub fn letter(&self, qubit: usize) -> Option<char> {
match (self.x.get(qubit)?, self.z.get(qubit)?) {
(0, 0) => Some('I'),
(1, 0) => Some('X'),
(0, 1) => Some('Z'),
_ => Some('Y'),
}
}
pub fn is_hermitian(&self) -> bool {
(self.phase % 2) as usize == self.y_count() % 2
}
pub fn is_identity(&self) -> bool {
self.phase == 0 && self.x.iter().all(|&b| b == 0) && self.z.iter().all(|&b| b == 0)
}
pub fn is_scalar(&self) -> bool {
self.x.iter().all(|&b| b == 0) && self.z.iter().all(|&b| b == 0)
}
fn same_size(&self, other: &Self) -> Result<(), StabilizerError> {
if self.n != other.n {
return Err(StabilizerError::QubitCountMismatch {
left: self.n,
right: other.n,
});
}
Ok(())
}
pub fn mul(&self, other: &Self) -> Result<Self, StabilizerError> {
self.same_size(other)?;
let mut cross = 0usize;
for i in 0..self.n {
cross += usize::from(self.z[i] & other.x[i]);
}
let phase = (self.phase + other.phase + 2 * ((cross % 2) as u8)) % 4;
let x: Vec<u8> = self.x.iter().zip(&other.x).map(|(a, b)| a ^ b).collect();
let z: Vec<u8> = self.z.iter().zip(&other.z).map(|(a, b)| a ^ b).collect();
Ok(Self {
n: self.n,
x,
z,
phase,
})
}
pub fn inverse(&self) -> Self {
let cross = self.y_count() % 2;
let phase = ((4 - self.phase % 4) + 2 * (cross as u8)) % 4;
Self {
n: self.n,
x: self.x.clone(),
z: self.z.clone(),
phase,
}
}
pub fn negate(&self) -> Self {
Self {
n: self.n,
x: self.x.clone(),
z: self.z.clone(),
phase: (self.phase + 2) % 4,
}
}
pub fn symplectic_product(&self, other: &Self) -> Result<u8, StabilizerError> {
self.same_size(other)?;
let mut acc = 0u8;
for i in 0..self.n {
acc ^= (self.x[i] & other.z[i]) ^ (self.z[i] & other.x[i]);
}
Ok(acc & 1)
}
pub fn commutes_with(&self, other: &Self) -> Result<bool, StabilizerError> {
Ok(self.symplectic_product(other)? == 0)
}
}
fn phase_prefix(e: u8) -> &'static str {
match e % 4 {
0 => "+",
1 => "+i",
2 => "-",
_ => "-i",
}
}
impl fmt::Display for PauliOperator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let y = (self.y_count() % 4) as u8;
let shown = (self.phase + 4 - y) % 4;
f.write_str(phase_prefix(shown))?;
for i in 0..self.n {
f.write_str(match self.letter(i).unwrap_or('I') {
'X' => "X",
'Y' => "Y",
'Z' => "Z",
_ => "I",
})?;
}
Ok(())
}
}
impl FromStr for PauliOperator {
type Err = StabilizerError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let t = s.trim();
let (sign, rest) = if let Some(r) = t.strip_prefix("-i") {
(3u8, r)
} else if let Some(r) = t.strip_prefix("+i") {
(1u8, r)
} else if let Some(r) = t.strip_prefix('-') {
(2u8, r)
} else if let Some(r) = t.strip_prefix('+') {
(0u8, r)
} else if let Some(r) = t.strip_prefix('i') {
(1u8, r)
} else {
(0u8, t)
};
let rest = rest.trim();
let mut x = Vec::with_capacity(rest.len());
let mut z = Vec::with_capacity(rest.len());
for c in rest.chars() {
let (a, b) = match c.to_ascii_uppercase() {
'I' => (0u8, 0u8),
'X' => (1, 0),
'Y' => (1, 1),
'Z' => (0, 1),
other => {
return Err(StabilizerError::MalformedPauli {
reason: format!(
"{other:?} is not a Pauli letter; expected one of I, X, Y, Z"
),
})
}
};
x.push(a);
z.push(b);
}
check_qubits(x.len())?;
let y = x.iter().zip(&z).filter(|(&a, &b)| a & b == 1).count();
Self::from_xz(&x, &z, (sign + (y % 4) as u8) % 4)
}
}
#[derive(Clone, Debug)]
pub struct StabilizerGroup {
n: usize,
generators: Vec<PauliOperator>,
rank: usize,
}
impl StabilizerGroup {
pub fn new(generators: Vec<PauliOperator>) -> Result<Self, StabilizerError> {
let n = match generators.first() {
Some(g) => g.qubits(),
None => {
return Err(StabilizerError::ShapeMismatch {
op: "build a stabilizer group",
expected: "at least one generator, so that the qubit count is determined; \
use `StabilizerGroup::with_qubits(n, gens)` for the trivial group"
.to_string(),
got: "an empty generator list".to_string(),
})
}
};
Self::with_qubits(n, generators)
}
pub fn with_qubits(n: usize, generators: Vec<PauliOperator>) -> Result<Self, StabilizerError> {
check_qubits(n)?;
for g in &generators {
if g.qubits() != n {
return Err(StabilizerError::QubitCountMismatch {
left: n,
right: g.qubits(),
});
}
if !g.is_hermitian() {
return Err(StabilizerError::MalformedPauli {
reason: format!(
"{g} is not Hermitian (phase exponent {}); a stabilizer generator is an \
observable, so its phase must be ±1 on the Hermitian letters",
g.phase()
),
});
}
}
for i in 0..generators.len() {
for j in (i + 1)..generators.len() {
if generators[i].symplectic_product(&generators[j])? == 1 {
return Err(StabilizerError::NotCommuting { i, j });
}
}
}
let rows: Vec<Vec<u8>> = generators.iter().map(|g| g.symplectic()).collect();
let rank = if rows.is_empty() || n == 0 {
0
} else {
matrix_from_rows(&rows, 2 * n)?.rank()
};
if rank < generators.len() && n > 0 {
let m = matrix_from_rows(&rows, 2 * n)?.transpose();
let deps = m.nullspace()?;
let combos = super::matrix_cols(&deps)?;
for combo in &combos {
let idx: Vec<usize> = combo
.iter()
.enumerate()
.filter(|(_, &b)| b == 1)
.map(|(i, _)| i)
.collect();
if idx.is_empty() {
continue;
}
let mut prod = PauliOperator::identity(n)?;
for &i in &idx {
prod = prod.mul(&generators[i])?;
}
match prod.phase() {
0 => {}
2 => return Err(StabilizerError::ContainsMinusIdentity { combination: idx }),
other => {
return Err(StabilizerError::InconsistentResult {
reason: format!(
"the product of generators {idx:?} is scalar with phase \
exponent {other}, which commuting Hermitian Paulis cannot be"
),
})
}
}
}
}
Ok(Self {
n,
generators,
rank,
})
}
pub fn qubits(&self) -> usize {
self.n
}
pub fn generators(&self) -> &[PauliOperator] {
&self.generators
}
pub fn rank(&self) -> usize {
self.rank
}
pub fn is_independent(&self) -> bool {
self.rank == self.generators.len()
}
pub fn check_matrix(&self) -> Result<GfMatrix, StabilizerError> {
let rows: Vec<Vec<u8>> = self.generators.iter().map(|g| g.symplectic()).collect();
matrix_from_rows(&rows, 2 * self.n)
}
pub fn syndrome(&self, error: &PauliOperator) -> Result<Vec<u8>, StabilizerError> {
if error.qubits() != self.n {
return Err(StabilizerError::QubitCountMismatch {
left: self.n,
right: error.qubits(),
});
}
self.generators
.iter()
.map(|g| g.symplectic_product(error))
.collect()
}
pub fn centralizes(&self, p: &PauliOperator) -> Result<bool, StabilizerError> {
Ok(self.syndrome(p)?.iter().all(|&b| b == 0))
}
pub fn contains(&self, p: &PauliOperator) -> Result<bool, StabilizerError> {
if p.qubits() != self.n {
return Err(StabilizerError::QubitCountMismatch {
left: self.n,
right: p.qubits(),
});
}
if self.generators.is_empty() || self.n == 0 {
return Ok(p.is_identity());
}
let gt = self.check_matrix()?.transpose();
let target = matrix_from_rows(&[p.symplectic()], 2 * self.n)?.transpose();
let coeffs = match gt.solve(&target) {
Ok(c) => c,
Err(crate::ffield::FiniteFieldError::Inconsistent) => return Ok(false),
Err(e) => return Err(e.into()),
};
let bits = super::matrix_cols(&coeffs)?;
let sel = &bits[0];
let mut prod = PauliOperator::identity(self.n)?;
for (i, &b) in sel.iter().enumerate() {
if b == 1 {
prod = prod.mul(&self.generators[i])?;
}
}
Ok(prod == *p)
}
}