use std::error::Error;
use std::fmt::{Display, Formatter};
use phasesmith_model::{DomainError, PatternRecord};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ResidualOptions {
pub use_uncertainty: bool,
pub parameter_count: usize,
}
impl Default for ResidualOptions {
fn default() -> Self {
Self {
use_uncertainty: true,
parameter_count: 0,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ResidualEvaluation {
pub included: Vec<bool>,
pub residual: Vec<f64>,
pub weighted_residual: Vec<f64>,
pub rp: f64,
pub rwp: f64,
pub chi_square: f64,
pub reduced_chi_square: f64,
}
pub fn evaluate_residuals(
pattern: &PatternRecord,
calculated_y: &[f64],
options: ResidualOptions,
) -> Result<ResidualEvaluation, ResidualError> {
pattern.validate().map_err(ResidualError::Pattern)?;
let observed_y = pattern
.observed_y
.as_deref()
.ok_or(ResidualError::MissingObservations)?;
if calculated_y.len() != pattern.sample_count() {
return Err(ResidualError::CalculatedLengthMismatch {
expected: pattern.sample_count(),
actual: calculated_y.len(),
});
}
if let Some(index) = calculated_y.iter().position(|value| !value.is_finite()) {
return Err(ResidualError::NonFiniteCalculated { index });
}
let included = pattern
.mask
.clone()
.unwrap_or_else(|| vec![true; pattern.sample_count()]);
let residual = calculated_y
.iter()
.zip(observed_y)
.map(|(calculated, observed)| calculated - observed)
.collect::<Vec<_>>();
let uncertainty = options
.use_uncertainty
.then_some(pattern.uncertainty.as_deref())
.flatten();
let weighted_residual = match uncertainty {
Some(uncertainty) => residual
.iter()
.zip(uncertainty)
.map(|(value, sigma)| value / sigma)
.collect(),
None => residual.clone(),
};
let mut included_count = 0_usize;
let mut absolute_residual_sum = 0.0;
let mut absolute_observed_sum = 0.0;
let mut weighted_observed_square_sum = 0.0;
let mut chi_square = 0.0;
for index in 0..pattern.sample_count() {
if !included[index] {
continue;
}
included_count += 1;
absolute_residual_sum += residual[index].abs();
absolute_observed_sum += observed_y[index].abs();
chi_square += weighted_residual[index] * weighted_residual[index];
weighted_observed_square_sum += match uncertainty {
Some(uncertainty) => {
let sigma = uncertainty[index];
(1.0 / (sigma * sigma)) * (observed_y[index] * observed_y[index])
}
None => observed_y[index] * observed_y[index],
};
}
let rp = if absolute_observed_sum == 0.0 {
f64::INFINITY
} else {
absolute_residual_sum / absolute_observed_sum
};
let rwp = if weighted_observed_square_sum == 0.0 {
f64::INFINITY
} else {
(chi_square / weighted_observed_square_sum).sqrt()
};
let degrees_of_freedom = included_count.checked_sub(options.parameter_count);
let reduced_chi_square = match degrees_of_freedom {
Some(degrees) if degrees > 0 => chi_square / count_as_f64(degrees),
_ => f64::INFINITY,
};
Ok(ResidualEvaluation {
included,
residual,
weighted_residual,
rp,
rwp,
chi_square,
reduced_chi_square,
})
}
#[allow(clippy::cast_precision_loss)]
fn count_as_f64(value: usize) -> f64 {
value as f64
}
#[derive(Debug)]
pub enum ResidualError {
Pattern(DomainError),
MissingObservations,
CalculatedLengthMismatch {
expected: usize,
actual: usize,
},
NonFiniteCalculated {
index: usize,
},
}
impl Display for ResidualError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pattern(error) => Display::fmt(error, formatter),
Self::MissingObservations => {
formatter.write_str("observed_y is required for residual evaluation")
}
Self::CalculatedLengthMismatch { expected, actual } => write!(
formatter,
"calculated_y length {actual} does not match pattern length {expected}"
),
Self::NonFiniteCalculated { index } => {
write!(
formatter,
"calculated_y contains a non-finite value at index {index}"
)
}
}
}
}
impl Error for ResidualError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Pattern(error) => Some(error),
_ => None,
}
}
}