use crate::qis::error::QisError;
use crate::qis::hamiltonian::Hamiltonian;
use crate::qis::pauli::{PauliString, Phase};
use crate::qis::state::{DensityMatrix, Statevector};
use num_complex::Complex64;
use rayon::prelude::*;
use std::collections::HashMap;
const VARIANCE_TOLERANCE: f64 = 1e-10;
pub trait Observable {
fn expectation_statevector(&self, sv: &Statevector) -> Result<f64, QisError>;
fn expectation_density_matrix(&self, dm: &DensityMatrix) -> Result<f64, QisError>;
fn expectation_probs(
&self,
measurements: &[(PauliString, HashMap<String, f64>)],
) -> Result<f64, QisError>;
fn num_qubits(&self) -> usize;
fn variance_statevector(&self, _sv: &Statevector) -> Result<f64, QisError> {
Err(QisError::UnsupportedOperation(
"variance_statevector not implemented for this observable type".into(),
))
}
}
fn apply_pauli_string_to_statevector(
pauli_str: &PauliString,
sv: &Statevector,
coeff: Complex64,
) -> Vec<Complex64> {
let x_mask = pauli_str.x_mask();
let z_mask = pauli_str.z_mask();
let y_phase = pauli_str.y_phase();
let global_phase = pauli_str.phase.to_complex();
let base_factor = coeff * global_phase * y_phase;
let sv_data = sv.data();
(0..sv_data.len())
.into_par_iter()
.map(|j| {
let source_j = j ^ x_mask;
let z_parity = (source_j & z_mask).count_ones();
let sign = if z_parity % 2 == 1 { -1.0 } else { 1.0 };
sv_data[source_j] * base_factor * sign
})
.collect()
}
fn finalize_variance(variance: f64) -> Result<f64, QisError> {
if variance < -VARIANCE_TOLERANCE {
return Err(QisError::InvalidParameterValue(format!(
"variance computation produced a negative result beyond tolerance: {}",
variance
)));
}
if variance < 0.0 {
Ok(0.0)
} else {
Ok(variance)
}
}
fn normalized_observable(h: &Hamiltonian) -> Result<Hamiltonian, QisError> {
h.normalized_hermitian().map_err(|error| match error {
nonfinite @ QisError::NonFiniteCoefficient { .. } => {
QisError::InvalidParameterValue(nonfinite.to_string())
}
other => other,
})
}
impl Observable for Hamiltonian {
fn expectation_statevector(&self, sv: &Statevector) -> Result<f64, QisError> {
if sv.num_qubits != self.num_qubits {
return Err(QisError::QubitMismatch {
expected: self.num_qubits,
actual: sv.num_qubits,
});
}
let normalized = normalized_observable(self)?;
let mut expected_value = 0.0;
for (pauli_str, coeff) in &normalized.terms {
let x_mask = pauli_str.x_mask();
let z_mask = pauli_str.z_mask();
let y_phase = pauli_str.y_phase();
let global_phase = pauli_str.phase.to_complex();
let base_factor = global_phase * y_phase;
let sv_data = sv.data();
let term_expectation: Complex64 = sv_data
.par_iter()
.enumerate()
.map(|(j, amp)| {
let source_j = j ^ x_mask;
let source_amp = sv_data[source_j];
let z_parity = (source_j & z_mask).count_ones();
let sign = if z_parity % 2 == 1 { -1.0 } else { 1.0 };
let phase_factor = base_factor * sign;
amp.conj() * source_amp * phase_factor
})
.sum();
expected_value += (term_expectation * coeff).re;
}
Ok(expected_value)
}
fn expectation_density_matrix(&self, dm: &DensityMatrix) -> Result<f64, QisError> {
if dm.num_qubits != self.num_qubits {
return Err(QisError::QubitMismatch {
expected: self.num_qubits,
actual: dm.num_qubits,
});
}
let normalized = normalized_observable(self)?;
let mut expected_value = 0.0;
let n = self.num_qubits;
let dim = 1 << n;
for (pauli_str, coeff) in &normalized.terms {
let x_mask = pauli_str.x_mask();
let z_mask = pauli_str.z_mask();
let y_phase = pauli_str.y_phase();
let global_phase = pauli_str.phase.to_complex();
let base_factor = global_phase * y_phase;
let term_expectation: Complex64 = (0..dim)
.into_par_iter()
.map(|j| {
let col = j ^ x_mask;
let flat_index = (j << n) | col;
let rho_elem = dm.data()[flat_index];
let z_parity = (j & z_mask).count_ones();
let sign = if z_parity % 2 == 1 { -1.0 } else { 1.0 };
let phase_factor = base_factor * sign;
rho_elem * phase_factor
})
.sum();
expected_value += (term_expectation * coeff).re;
}
Ok(expected_value)
}
fn expectation_probs(
&self,
measurements: &[(PauliString, HashMap<String, f64>)],
) -> Result<f64, QisError> {
let normalized = normalized_observable(self)?;
let mut expected_value = 0.0;
for (term_pauli, coeff) in &normalized.terms {
let term_x_mask = term_pauli.x_mask();
let term_z_mask = term_pauli.z_mask();
let p_active = term_x_mask | term_z_mask;
if p_active == 0 {
let term_contrib = (term_pauli.phase.to_complex() * coeff).re;
expected_value += term_contrib;
continue;
}
let mut compatible_measurement = None;
for (m_pauli, probs) in measurements {
let m_x_mask = m_pauli.x_mask();
let m_z_mask = m_pauli.z_mask();
if (m_x_mask & p_active) == term_x_mask && (m_z_mask & p_active) == term_z_mask {
compatible_measurement = Some(probs);
break;
}
}
if let Some(probs) = compatible_measurement {
let mut exp_val = 0.0;
for (state_str, prob) in probs {
if state_str.len() != self.num_qubits {
return Err(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(QisError::PauliStringParseError(
crate::qis::error::PauliStringParseError::InvalidCharacter(c),
));
}
}
}
let parity = (state_idx & p_active).count_ones();
let eigenvalue = if parity % 2 == 1 { -1.0 } else { 1.0 };
exp_val += prob * eigenvalue;
}
let term_contrib = (exp_val * term_pauli.phase.to_complex() * coeff).re;
expected_value += term_contrib;
} else {
return Err(QisError::UnsupportedOperation(format!(
"No compatible measurement basis found for term {}",
term_pauli
)));
}
}
Ok(expected_value)
}
fn num_qubits(&self) -> usize {
self.num_qubits
}
fn variance_statevector(&self, sv: &Statevector) -> Result<f64, QisError> {
if sv.num_qubits != self.num_qubits {
return Err(QisError::QubitMismatch {
expected: self.num_qubits,
actual: sv.num_qubits,
});
}
let mut simplified = normalized_observable(self)?;
for (_, coeff) in &mut simplified.terms {
if coeff.im.abs() > VARIANCE_TOLERANCE {
return Err(QisError::NotHermitian);
}
coeff.im = 0.0;
}
let expectation = simplified.expectation_statevector(sv)?;
let mut applied = vec![Complex64::new(0.0, 0.0); sv.data().len()];
for (pauli_str, coeff) in &simplified.terms {
let term_state = apply_pauli_string_to_statevector(pauli_str, sv, *coeff);
for (acc, term_amp) in applied.iter_mut().zip(term_state) {
*acc += term_amp;
}
}
let second_moment: f64 = applied.iter().map(|amp| amp.norm_sqr()).sum();
finalize_variance(second_moment - expectation * expectation)
}
}
impl Observable for PauliString {
fn expectation_statevector(&self, sv: &Statevector) -> Result<f64, QisError> {
if sv.num_qubits != self.num_qubits {
return Err(QisError::QubitMismatch {
expected: self.num_qubits,
actual: sv.num_qubits,
});
}
self.ensure_hermitian()?;
let x_mask = self.x_mask();
let z_mask = self.z_mask();
let y_phase = self.y_phase();
let global_phase = self.phase.to_complex();
let base_factor = global_phase * y_phase;
let sv_data = sv.data();
let term_expectation: Complex64 = sv_data
.par_iter()
.enumerate()
.map(|(j, amp)| {
let source_j = j ^ x_mask;
let source_amp = sv_data[source_j];
let z_parity = (source_j & z_mask).count_ones();
let sign = if z_parity % 2 == 1 { -1.0 } else { 1.0 };
let phase_factor = base_factor * sign;
amp.conj() * source_amp * phase_factor
})
.sum();
Ok(term_expectation.re)
}
fn expectation_density_matrix(&self, dm: &DensityMatrix) -> Result<f64, QisError> {
if dm.num_qubits != self.num_qubits {
return Err(QisError::QubitMismatch {
expected: self.num_qubits,
actual: dm.num_qubits,
});
}
self.ensure_hermitian()?;
let n = self.num_qubits;
let dim = 1 << n;
let x_mask = self.x_mask();
let z_mask = self.z_mask();
let y_phase = self.y_phase();
let global_phase = self.phase.to_complex();
let base_factor = global_phase * y_phase;
let term_expectation: Complex64 = (0..dim)
.into_par_iter()
.map(|j| {
let col = j ^ x_mask;
let flat_index = (j << n) | col;
let rho_elem = dm.data()[flat_index];
let z_parity = (j & z_mask).count_ones();
let sign = if z_parity % 2 == 1 { -1.0 } else { 1.0 };
let phase_factor = base_factor * sign;
rho_elem * phase_factor
})
.sum();
Ok(term_expectation.re)
}
fn expectation_probs(
&self,
measurements: &[(PauliString, HashMap<String, f64>)],
) -> Result<f64, QisError> {
self.ensure_hermitian()?;
let term_x_mask = self.x_mask();
let term_z_mask = self.z_mask();
let p_active = term_x_mask | term_z_mask;
if p_active == 0 {
return Ok(self.phase.to_complex().re);
}
let mut compatible_measurement = None;
for (m_pauli, probs) in measurements {
let m_x_mask = m_pauli.x_mask();
let m_z_mask = m_pauli.z_mask();
if (m_x_mask & p_active) == term_x_mask && (m_z_mask & p_active) == term_z_mask {
compatible_measurement = Some(probs);
break;
}
}
if let Some(probs) = compatible_measurement {
let mut exp_val = 0.0;
for (state_str, prob) in probs {
if state_str.len() != self.num_qubits {
return Err(QisError::QubitMismatch {
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(QisError::UnsupportedOperation(format!(
"Invalid character '{}' in state string",
c
)));
}
}
}
let parity = (state_idx & p_active).count_ones();
let eigenvalue = if parity % 2 == 1 { -1.0 } else { 1.0 };
exp_val += prob * eigenvalue;
}
Ok((exp_val * self.phase.to_complex()).re)
} else {
Err(QisError::UnsupportedOperation(format!(
"No compatible measurement basis found for term {}",
self
)))
}
}
fn num_qubits(&self) -> usize {
self.num_qubits
}
fn variance_statevector(&self, sv: &Statevector) -> Result<f64, QisError> {
if sv.num_qubits != self.num_qubits {
return Err(QisError::QubitMismatch {
expected: self.num_qubits,
actual: sv.num_qubits,
});
}
match self.phase {
Phase::Plus | Phase::Minus => {}
Phase::I | Phase::MinusI => return Err(QisError::NotHermitian),
}
let expectation = self.expectation_statevector(sv)?;
let second_moment: f64 =
apply_pauli_string_to_statevector(self, sv, Complex64::new(1.0, 0.0))
.iter()
.map(|amp| amp.norm_sqr())
.sum();
finalize_variance(second_moment - expectation * expectation)
}
}
#[cfg(test)]
#[path = "./observable_test.rs"]
mod observable_test;