use crate::qis::error::{PauliStringParseError, QisError};
use bitvec::prelude::*;
use ndarray::{Array2, arr2, linalg::kron};
use num_complex::Complex64;
use std::fmt;
use std::ops::{Add, AddAssign, Mul, MulAssign};
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Phase {
Plus = 0,
I = 1,
Minus = 2,
MinusI = 3,
}
impl fmt::Display for Phase {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Phase::Plus => "1",
Phase::I => "i",
Phase::Minus => "-1",
Phase::MinusI => "-i",
};
f.write_str(s)
}
}
impl From<u8> for Phase {
fn from(val: u8) -> Self {
match val % 4 {
0 => Phase::Plus,
1 => Phase::I,
2 => Phase::Minus,
3 => Phase::MinusI,
_ => unreachable!(),
}
}
}
impl Add for Phase {
type Output = Phase;
fn add(self, other: Phase) -> Phase {
Phase::from(self as u8 + other as u8)
}
}
impl Add<u8> for Phase {
type Output = Phase;
fn add(self, rhs: u8) -> Phase {
Phase::from(self as u8 + rhs)
}
}
impl AddAssign for Phase {
fn add_assign(&mut self, other: Self) {
*self = *self + other;
}
}
impl AddAssign<u8> for Phase {
fn add_assign(&mut self, rhs: u8) {
*self = *self + rhs;
}
}
#[allow(clippy::suspicious_arithmetic_impl)]
impl Mul for Phase {
type Output = Phase;
fn mul(self, rhs: Self) -> Self::Output {
self + rhs
}
}
impl Phase {
pub fn to_complex(&self) -> Complex64 {
match self {
Phase::Plus => Complex64::new(1.0, 0.0),
Phase::I => Complex64::new(0.0, 1.0),
Phase::Minus => Complex64::new(-1.0, 0.0),
Phase::MinusI => Complex64::new(0.0, -1.0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Pauli {
X,
Y,
Z,
I,
}
impl TryFrom<char> for Pauli {
type Error = QisError;
fn try_from(s: char) -> Result<Self, Self::Error> {
match s {
'I' => Ok(Pauli::I),
'X' => Ok(Pauli::X),
'Y' => Ok(Pauli::Y),
'Z' => Ok(Pauli::Z),
other => Err(QisError::InvalidParameterValue(format!(
"invalid Pauli character '{}'",
other
))),
}
}
}
impl FromStr for Pauli {
type Err = QisError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(QisError::InvalidParameterValue(
"empty string cannot be converted to Pauli".to_string(),
));
}
if s.len() > 1 {
return Err(QisError::InvalidParameterValue(format!(
"expected a single Pauli character, got \"{}\"",
s
)));
}
s.chars().next().unwrap().try_into()
}
}
impl fmt::Display for Pauli {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Pauli::I => "I",
Pauli::X => "X",
Pauli::Y => "Y",
Pauli::Z => "Z",
};
write!(f, "{}", s)
}
}
impl Pauli {
pub fn to_symplectic(&self) -> (u8, u8) {
match self {
Pauli::I => (0, 0),
Pauli::X => (1, 0),
Pauli::Y => (1, 1),
Pauli::Z => (0, 1),
}
}
pub fn to_matrix(&self) -> Array2<Complex64> {
let zero = Complex64::new(0.0, 0.0);
let one = Complex64::new(1.0, 0.0);
let i = Complex64::new(0.0, 1.0);
let neg_one = Complex64::new(-1.0, 0.0);
let neg_i = Complex64::new(0.0, -1.0);
match self {
Pauli::I => arr2(&[[one, zero], [zero, one]]),
Pauli::X => arr2(&[[zero, one], [one, zero]]),
Pauli::Y => arr2(&[[zero, neg_i], [i, zero]]),
Pauli::Z => arr2(&[[one, zero], [zero, neg_one]]),
}
}
pub fn mul_with_phase(&self, other: Pauli) -> (Pauli, Phase) {
match (self, other) {
(Pauli::I, p) => (p, Phase::Plus),
(p, Pauli::I) => (*p, Phase::Plus),
(Pauli::X, Pauli::X) => (Pauli::I, Phase::Plus),
(Pauli::Y, Pauli::Y) => (Pauli::I, Phase::Plus),
(Pauli::Z, Pauli::Z) => (Pauli::I, Phase::Plus),
(Pauli::X, Pauli::Y) => (Pauli::Z, Phase::I), (Pauli::X, Pauli::Z) => (Pauli::Y, Phase::MinusI), (Pauli::Y, Pauli::X) => (Pauli::Z, Phase::MinusI), (Pauli::Y, Pauli::Z) => (Pauli::X, Phase::I), (Pauli::Z, Pauli::X) => (Pauli::Y, Phase::I), (Pauli::Z, Pauli::Y) => (Pauli::X, Phase::MinusI), }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PauliString {
pub num_qubits: usize,
pub phase: Phase,
pub z: BitVec,
pub x: BitVec,
}
pub struct PauliIter<'a> {
pauli: &'a PauliString,
indices: std::ops::Range<usize>,
}
impl Iterator for PauliIter<'_> {
type Item = Pauli;
fn next(&mut self) -> Option<Self::Item> {
self.indices.next().map(|index| self.pauli.get_pauli(index))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.indices.size_hint()
}
}
impl ExactSizeIterator for PauliIter<'_> {}
impl std::iter::FusedIterator for PauliIter<'_> {}
impl<'a> IntoIterator for &'a PauliString {
type Item = Pauli;
type IntoIter = PauliIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl fmt::Display for PauliString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let phase_str = match self.phase {
Phase::Plus => "+",
Phase::I => "+i",
Phase::Minus => "-",
Phase::MinusI => "-i",
};
write!(f, "{}", phase_str)?;
for i in (0..self.num_qubits).rev() {
let char_code = match (self.x[i], self.z[i]) {
(false, false) => 'I',
(true, false) => 'X',
(false, true) => 'Z',
(true, true) => 'Y',
};
write!(f, "{}", char_code)?;
}
Ok(())
}
}
impl PauliString {
pub fn iter(&self) -> PauliIter<'_> {
PauliIter {
pauli: self,
indices: 0..self.num_qubits,
}
}
pub fn new(num_qubits: usize) -> Self {
Self {
num_qubits,
phase: Phase::Plus,
z: bitvec![0; num_qubits],
x: bitvec![0; num_qubits],
}
}
pub fn set_pauli(&mut self, idx: usize, p: Pauli) {
assert!(
idx < self.num_qubits,
"Index {} out of bounds for {} qubits",
idx,
self.num_qubits
);
let (x_val, z_val) = p.to_symplectic();
self.x.set(idx, x_val == 1);
self.z.set(idx, z_val == 1);
}
pub fn get_pauli(&self, idx: usize) -> Pauli {
assert!(
idx < self.num_qubits,
"Index {} out of bounds for {} qubits",
idx,
self.num_qubits
);
let x_bit = self.x[idx];
let z_bit = self.z[idx];
match (x_bit, z_bit) {
(false, false) => Pauli::I,
(true, false) => Pauli::X,
(false, true) => Pauli::Z,
(true, true) => Pauli::Y,
}
}
pub fn to_matrix(&self) -> Array2<Complex64> {
let mut matrix = Array2::from_elem((1, 1), Complex64::new(1.0, 0.0));
for qubit in (0..self.num_qubits).rev() {
matrix = kron(&matrix, &self.get_pauli(qubit).to_matrix());
}
let phase = self.phase.to_complex();
matrix.mapv_inplace(|value| value * phase);
matrix
}
pub fn try_get_pauli(&self, idx: usize) -> Result<Pauli, QisError> {
if idx >= self.num_qubits {
return Err(QisError::IndexOutOfBounds {
index: idx,
max: self.num_qubits.saturating_sub(1),
});
}
Ok(self.get_pauli(idx))
}
pub fn try_set_pauli(&mut self, idx: usize, pauli: Pauli) -> Result<(), QisError> {
if idx >= self.num_qubits {
return Err(QisError::IndexOutOfBounds {
index: idx,
max: self.num_qubits.saturating_sub(1),
});
}
self.set_pauli(idx, pauli);
Ok(())
}
pub fn commutes_with(&self, other: &Self) -> bool {
assert_eq!(self.num_qubits, other.num_qubits);
let term1 = self.x.clone() & &other.z;
let term2 = self.z.clone() & &other.x;
let anti_commutations = (term1 ^ term2).count_ones();
anti_commutations & 1 == 0
}
pub fn x_mask(&self) -> usize {
self.x.iter().enumerate().fold(
0usize,
|acc, (i, bit)| {
if *bit { acc | (1 << i) } else { acc }
},
)
}
pub fn z_mask(&self) -> usize {
self.z.iter().enumerate().fold(
0usize,
|acc, (i, bit)| {
if *bit { acc | (1 << i) } else { acc }
},
)
}
pub fn support(&self) -> Vec<usize> {
let mut support = Vec::new();
for i in 0..self.num_qubits {
if self.x[i] || self.z[i] {
support.push(i);
}
}
support
}
pub fn y_phase(&self) -> Complex64 {
let y_count: u32 = self
.x
.iter()
.zip(self.z.iter())
.filter(|(x_bit, z_bit)| **x_bit && **z_bit)
.count() as u32;
match y_count % 4 {
0 => Complex64::new(1.0, 0.0),
1 => Complex64::new(0.0, 1.0),
2 => Complex64::new(-1.0, 0.0),
3 => Complex64::new(0.0, -1.0),
_ => unreachable!(),
}
}
pub fn expectation(
&self,
probs: &std::collections::HashMap<String, f64>,
) -> Result<f64, crate::qis::error::QisError> {
if self.x.iter().any(|bit| *bit) {
return Ok(0.0);
}
let z_mask = self.z_mask();
let global_phase = self.phase.to_complex();
let mut exp_value = 0.0;
for (state_str, prob) in probs {
if state_str.len() != self.num_qubits {
return Err(crate::qis::error::QisError::DimensionMismatch {
expected: self.num_qubits,
actual: state_str.len(),
});
}
let mut state_idx = 0usize;
for (i, c) in state_str.chars().rev().enumerate() {
match c {
'1' => state_idx |= 1 << i,
'0' => {}
_ => {
return Err(crate::qis::error::QisError::PauliStringParseError(
crate::qis::error::PauliStringParseError::InvalidCharacter(c),
));
}
}
}
let overlap = state_idx & z_mask;
let parity = overlap.count_ones();
let eigenvalue = if parity & 1 == 0 { 1.0 } else { -1.0 };
exp_value += prob * eigenvalue;
}
Ok((exp_value * global_phase).re)
}
#[inline]
fn calculate_phase_step(x1: bool, z1: bool, x2: bool, z2: bool) -> u8 {
match (x1, z1, x2, z2) {
(x1, z1, x2, z2) if x1 == x2 && z1 == z2 => 0,
(false, false, _, _) => 0, (_, _, false, false) => 0,
(true, false, false, true) => 3,
(false, true, true, false) => 1,
(true, false, true, true) => 1,
(true, true, true, false) => 3,
(false, true, true, true) => 3,
(true, true, false, true) => 1,
_ => 0,
}
}
}
impl Mul for &PauliString {
type Output = PauliString;
fn mul(self, rhs: Self) -> PauliString {
let mut result = self.clone();
result *= rhs;
result
}
}
impl MulAssign<&PauliString> for PauliString {
fn mul_assign(&mut self, rhs: &PauliString) {
assert_eq!(self.num_qubits, rhs.num_qubits, "Qubit count mismatch");
for i in 0..self.num_qubits {
let step = PauliString::calculate_phase_step(self.x[i], self.z[i], rhs.x[i], rhs.z[i]);
self.phase += step;
}
self.phase += rhs.phase;
self.x ^= &rhs.x;
self.z ^= &rhs.z;
}
}
impl FromStr for PauliString {
type Err = PauliStringParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
return Err(PauliStringParseError::EmptyString);
}
let mut chars = s.chars().peekable();
let mut phase = Phase::Plus;
if let Some(&c) = chars.peek() {
if c == '+' || c == '-' {
let sign = if c == '+' { 0 } else { 2 }; chars.next();
if let Some(&c) = chars.peek() {
if c == 'i' || c == 'j' {
chars.next();
phase = if sign == 0 { Phase::I } else { Phase::MinusI };
} else {
phase = if sign == 0 { Phase::Plus } else { Phase::Minus };
}
} else {
return Err(PauliStringParseError::NoOperators);
}
} else if c == 'i' || c == 'j' {
chars.next();
phase = Phase::I;
}
}
let mut operators: Vec<char> = Vec::new();
for c in chars {
match c {
'I' | 'X' | 'Y' | 'Z' => operators.push(c),
c => return Err(PauliStringParseError::InvalidCharacter(c)),
}
}
if operators.is_empty() {
return Err(PauliStringParseError::NoOperators);
}
let num_qubits = operators.len();
let mut result = PauliString::new(num_qubits);
result.phase = phase;
for (i, &op) in operators.iter().rev().enumerate() {
let pauli = match op {
'I' => Pauli::I,
'X' => Pauli::X,
'Y' => Pauli::Y,
'Z' => Pauli::Z,
_ => unreachable!(),
};
result.set_pauli(i, pauli);
}
Ok(result)
}
}
#[cfg(test)]
#[path = "./pauli_test.rs"]
mod tests;