use crate::error::{Result, SimulatorError};
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::parallel_ops::{par_chunks, par_join};
use scirs2_core::Complex64;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PauliOp {
I,
X,
Y,
Z,
}
impl PauliOp {
pub fn from_char(c: char) -> Result<Self> {
match c {
'I' => Ok(PauliOp::I),
'X' => Ok(PauliOp::X),
'Y' => Ok(PauliOp::Y),
'Z' => Ok(PauliOp::Z),
_ => Err(SimulatorError::InvalidInput(format!(
"Invalid Pauli operator: {}",
c
))),
}
}
pub fn matrix(&self) -> Array2<Complex64> {
match self {
PauliOp::I => {
let mut m = Array2::zeros((2, 2));
m[[0, 0]] = Complex64::new(1.0, 0.0);
m[[1, 1]] = Complex64::new(1.0, 0.0);
m
}
PauliOp::X => {
let mut m = Array2::zeros((2, 2));
m[[0, 1]] = Complex64::new(1.0, 0.0);
m[[1, 0]] = Complex64::new(1.0, 0.0);
m
}
PauliOp::Y => {
let mut m = Array2::zeros((2, 2));
m[[0, 1]] = Complex64::new(0.0, -1.0);
m[[1, 0]] = Complex64::new(0.0, 1.0);
m
}
PauliOp::Z => {
let mut m = Array2::zeros((2, 2));
m[[0, 0]] = Complex64::new(1.0, 0.0);
m[[1, 1]] = Complex64::new(-1.0, 0.0);
m
}
}
}
pub fn eigenvalues(&self) -> [f64; 2] {
match self {
PauliOp::I => [1.0, 1.0],
PauliOp::X | PauliOp::Y | PauliOp::Z => [1.0, -1.0],
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PauliObservable {
pub operators: Vec<PauliOp>,
pub coefficient: Complex64,
}
impl PauliObservable {
pub fn new(operators: Vec<PauliOp>) -> Self {
Self {
operators,
coefficient: Complex64::new(1.0, 0.0),
}
}
pub fn from_string(s: &str) -> Result<Self> {
let operators: Result<Vec<PauliOp>> = s.chars().map(PauliOp::from_char).collect();
Ok(Self::new(operators?))
}
pub fn with_coefficient(mut self, coeff: Complex64) -> Self {
self.coefficient = coeff;
self
}
pub fn with_real_coefficient(mut self, coeff: f64) -> Self {
self.coefficient = Complex64::new(coeff, 0.0);
self
}
pub fn n_qubits(&self) -> usize {
self.operators.len()
}
pub fn is_diagonal(&self) -> bool {
self.operators
.iter()
.all(|op| matches!(op, PauliOp::I | PauliOp::Z))
}
pub fn non_identity_qubits(&self) -> Vec<(usize, PauliOp)> {
self.operators
.iter()
.enumerate()
.filter(|(_, op)| **op != PauliOp::I)
.map(|(i, op)| (i, *op))
.collect()
}
}
#[derive(Debug, Clone)]
pub struct PauliHamiltonian {
pub terms: Vec<PauliObservable>,
pub n_qubits: usize,
}
impl PauliHamiltonian {
pub fn new(n_qubits: usize) -> Self {
Self {
terms: Vec::new(),
n_qubits,
}
}
pub fn add_term(&mut self, term: PauliObservable) -> Result<()> {
if term.n_qubits() != self.n_qubits {
return Err(SimulatorError::InvalidInput(format!(
"Term has {} qubits but Hamiltonian has {} qubits",
term.n_qubits(),
self.n_qubits
)));
}
self.terms.push(term);
Ok(())
}
pub fn from_terms(terms: Vec<PauliObservable>) -> Result<Self> {
if terms.is_empty() {
return Err(SimulatorError::InvalidInput(
"Hamiltonian must have at least one term".to_string(),
));
}
let n_qubits = terms[0].n_qubits();
for term in &terms {
if term.n_qubits() != n_qubits {
return Err(SimulatorError::InvalidInput(
"All terms must act on the same number of qubits".to_string(),
));
}
}
Ok(Self { terms, n_qubits })
}
pub fn n_terms(&self) -> usize {
self.terms.len()
}
}
#[derive(Debug, Clone)]
pub struct ObservableConfig {
pub use_gpu: bool,
pub batch_size: usize,
pub use_diagonal_optimization: bool,
}
impl Default for ObservableConfig {
fn default() -> Self {
Self {
use_gpu: true,
batch_size: 1024,
use_diagonal_optimization: true,
}
}
}
pub struct ObservableCalculator {
config: ObservableConfig,
pauli_cache: HashMap<PauliOp, Array2<Complex64>>,
}
impl ObservableCalculator {
pub fn new() -> Self {
Self::with_config(ObservableConfig::default())
}
pub fn with_config(config: ObservableConfig) -> Self {
let mut pauli_cache = HashMap::new();
pauli_cache.insert(PauliOp::I, PauliOp::I.matrix());
pauli_cache.insert(PauliOp::X, PauliOp::X.matrix());
pauli_cache.insert(PauliOp::Y, PauliOp::Y.matrix());
pauli_cache.insert(PauliOp::Z, PauliOp::Z.matrix());
Self {
config,
pauli_cache,
}
}
pub fn expectation_value(
&self,
state: &Array1<Complex64>,
observable: &PauliObservable,
) -> Result<Complex64> {
let n_qubits = observable.n_qubits();
let state_size = 1 << n_qubits;
if state.len() != state_size {
return Err(SimulatorError::InvalidInput(format!(
"State size {} does not match observable size 2^{} = {}",
state.len(),
n_qubits,
state_size
)));
}
if self.config.use_diagonal_optimization && observable.is_diagonal() {
return self.expectation_value_diagonal(state, observable);
}
let o_psi = self.apply_pauli_observable(state, observable)?;
let expectation = state
.iter()
.zip(o_psi.iter())
.map(|(a, b)| a.conj() * b)
.sum::<Complex64>();
Ok(expectation * observable.coefficient)
}
fn expectation_value_diagonal(
&self,
state: &Array1<Complex64>,
observable: &PauliObservable,
) -> Result<Complex64> {
let n_qubits = observable.n_qubits();
let state_size = state.len();
let mut expectation = Complex64::new(0.0, 0.0);
for i in 0..state_size {
let mut sign = 1.0;
for (qubit_idx, op) in observable.operators.iter().enumerate() {
if *op == PauliOp::Z {
let bit = (i >> (n_qubits - 1 - qubit_idx)) & 1;
if bit == 1 {
sign *= -1.0;
}
}
}
expectation += state[i].norm_sqr() * sign;
}
Ok(expectation * observable.coefficient)
}
fn apply_pauli_observable(
&self,
state: &Array1<Complex64>,
observable: &PauliObservable,
) -> Result<Array1<Complex64>> {
let n_qubits = observable.n_qubits();
let state_size = state.len();
let mut result = state.clone();
let non_identity = observable.non_identity_qubits();
for (qubit_idx, op) in non_identity {
result = self.apply_single_qubit_pauli(&result, qubit_idx, op, n_qubits)?;
}
Ok(result)
}
fn apply_single_qubit_pauli(
&self,
state: &Array1<Complex64>,
qubit: usize,
op: PauliOp,
n_qubits: usize,
) -> Result<Array1<Complex64>> {
let state_size = state.len();
let mut new_state = Array1::zeros(state_size);
match op {
PauliOp::I => {
return Ok(state.clone());
}
PauliOp::X => {
for i in 0..state_size {
let j = i ^ (1 << (n_qubits - 1 - qubit));
new_state[i] = state[j];
}
}
PauliOp::Y => {
for i in 0..state_size {
let j = i ^ (1 << (n_qubits - 1 - qubit));
let bit = (i >> (n_qubits - 1 - qubit)) & 1;
let sign = if bit == 0 {
Complex64::new(0.0, -1.0)
} else {
Complex64::new(0.0, 1.0)
};
new_state[i] = sign * state[j];
}
}
PauliOp::Z => {
for i in 0..state_size {
let bit = (i >> (n_qubits - 1 - qubit)) & 1;
let sign = if bit == 0 { 1.0 } else { -1.0 };
new_state[i] = state[i] * sign;
}
}
}
Ok(new_state)
}
pub fn hamiltonian_expectation_value(
&self,
state: &Array1<Complex64>,
hamiltonian: &PauliHamiltonian,
) -> Result<Complex64> {
if state.len() != (1 << hamiltonian.n_qubits) {
return Err(SimulatorError::InvalidInput(
"State size does not match Hamiltonian".to_string(),
));
}
let mut total = Complex64::new(0.0, 0.0);
for term in &hamiltonian.terms {
let exp_val = self.expectation_value(state, term)?;
total += exp_val;
}
Ok(total)
}
pub fn variance(&self, state: &Array1<Complex64>, observable: &PauliObservable) -> Result<f64> {
let exp_o = self.expectation_value(state, observable)?;
let var = 1.0 - exp_o.norm_sqr();
Ok(var.max(0.0)) }
pub fn batch_expectation_values(
&self,
state: &Array1<Complex64>,
observables: &[PauliObservable],
) -> Result<Vec<Complex64>> {
if observables.is_empty() {
return Ok(Vec::new());
}
let n_qubits = observables[0].n_qubits();
for obs in observables {
if obs.n_qubits() != n_qubits {
return Err(SimulatorError::InvalidInput(
"All observables must act on the same number of qubits".to_string(),
));
}
}
if observables.len() > 4 {
let results: Vec<Complex64> = observables
.iter()
.map(|obs| self.expectation_value(state, obs))
.collect::<Result<Vec<_>>>()?;
Ok(results)
} else {
observables
.iter()
.map(|obs| self.expectation_value(state, obs))
.collect()
}
}
}
impl Default for ObservableCalculator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn normalize(mut state: Array1<Complex64>) -> Array1<Complex64> {
let norm = state.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
for c in state.iter_mut() {
*c /= norm;
}
state
}
#[test]
fn test_pauli_op_from_char() {
assert_eq!(PauliOp::from_char('I').unwrap(), PauliOp::I);
assert_eq!(PauliOp::from_char('X').unwrap(), PauliOp::X);
assert_eq!(PauliOp::from_char('Y').unwrap(), PauliOp::Y);
assert_eq!(PauliOp::from_char('Z').unwrap(), PauliOp::Z);
assert!(PauliOp::from_char('A').is_err());
}
#[test]
fn test_pauli_observable_from_string() {
let obs = PauliObservable::from_string("XYZ").unwrap();
assert_eq!(obs.n_qubits(), 3);
assert_eq!(obs.operators[0], PauliOp::X);
assert_eq!(obs.operators[1], PauliOp::Y);
assert_eq!(obs.operators[2], PauliOp::Z);
}
#[test]
fn test_pauli_observable_is_diagonal() {
let diag = PauliObservable::from_string("IZI").unwrap();
assert!(diag.is_diagonal());
let non_diag = PauliObservable::from_string("XZI").unwrap();
assert!(!non_diag.is_diagonal());
}
#[test]
fn test_expectation_value_z_basis() {
let calc = ObservableCalculator::new();
let state = normalize(Array1::from_vec(vec![
Complex64::new(1.0, 0.0),
Complex64::new(0.0, 0.0),
Complex64::new(0.0, 0.0),
Complex64::new(0.0, 0.0),
]));
let z0 = PauliObservable::from_string("ZI").unwrap();
let exp_val = calc.expectation_value(&state, &z0).unwrap();
assert!((exp_val.re - 1.0).abs() < 1e-10); assert!(exp_val.im.abs() < 1e-10);
let z1 = PauliObservable::from_string("IZ").unwrap();
let exp_val = calc.expectation_value(&state, &z1).unwrap();
assert!((exp_val.re - 1.0).abs() < 1e-10);
}
#[test]
fn test_expectation_value_x_basis() {
let calc = ObservableCalculator::new();
let state = normalize(Array1::from_vec(vec![
Complex64::new(1.0, 0.0),
Complex64::new(1.0, 0.0),
]));
let x = PauliObservable::from_string("X").unwrap();
let exp_val = calc.expectation_value(&state, &x).unwrap();
assert!((exp_val.re - 1.0).abs() < 1e-10); assert!(exp_val.im.abs() < 1e-10);
}
#[test]
fn test_hamiltonian_expectation() {
let calc = ObservableCalculator::new();
let z0 = PauliObservable::from_string("ZI").unwrap();
let z1 = PauliObservable::from_string("IZ").unwrap();
let h = PauliHamiltonian::from_terms(vec![z0, z1]).unwrap();
let state = normalize(Array1::from_vec(vec![
Complex64::new(1.0, 0.0),
Complex64::new(0.0, 0.0),
Complex64::new(0.0, 0.0),
Complex64::new(0.0, 0.0),
]));
let exp_val = calc.hamiltonian_expectation_value(&state, &h).unwrap();
assert!((exp_val.re - 2.0).abs() < 1e-10); }
#[test]
fn test_variance() {
let calc = ObservableCalculator::new();
let state = normalize(Array1::from_vec(vec![
Complex64::new(1.0, 0.0),
Complex64::new(0.0, 0.0),
]));
let z = PauliObservable::from_string("Z").unwrap();
let var = calc.variance(&state, &z).unwrap();
assert!(var.abs() < 1e-10);
let state_plus = normalize(Array1::from_vec(vec![
Complex64::new(1.0, 0.0),
Complex64::new(1.0, 0.0),
]));
let var = calc.variance(&state_plus, &z).unwrap();
assert!((var - 1.0).abs() < 1e-10); }
#[test]
fn test_batch_expectation_values() {
let calc = ObservableCalculator::new();
let state = normalize(Array1::from_vec(vec![
Complex64::new(1.0, 0.0),
Complex64::new(0.0, 0.0),
Complex64::new(0.0, 0.0),
Complex64::new(0.0, 0.0),
]));
let observables = vec![
PauliObservable::from_string("ZI").unwrap(),
PauliObservable::from_string("IZ").unwrap(),
PauliObservable::from_string("ZZ").unwrap(),
];
let results = calc.batch_expectation_values(&state, &observables).unwrap();
assert_eq!(results.len(), 3);
assert!((results[0].re - 1.0).abs() < 1e-10);
assert!((results[1].re - 1.0).abs() < 1e-10);
assert!((results[2].re - 1.0).abs() < 1e-10); }
}