phasesmith_workflows/
residuals.rs1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use phasesmith_model::{DomainError, PatternRecord};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct ResidualOptions {
11 pub use_uncertainty: bool,
13 pub parameter_count: usize,
15}
16
17impl Default for ResidualOptions {
18 fn default() -> Self {
19 Self {
20 use_uncertainty: true,
21 parameter_count: 0,
22 }
23 }
24}
25
26#[derive(Clone, Debug, PartialEq)]
28pub struct ResidualEvaluation {
29 pub included: Vec<bool>,
31 pub residual: Vec<f64>,
33 pub weighted_residual: Vec<f64>,
35 pub rp: f64,
37 pub rwp: f64,
39 pub chi_square: f64,
41 pub reduced_chi_square: f64,
43}
44
45pub fn evaluate_residuals(
57 pattern: &PatternRecord,
58 calculated_y: &[f64],
59 options: ResidualOptions,
60) -> Result<ResidualEvaluation, ResidualError> {
61 pattern.validate().map_err(ResidualError::Pattern)?;
62 let observed_y = pattern
63 .observed_y
64 .as_deref()
65 .ok_or(ResidualError::MissingObservations)?;
66 if calculated_y.len() != pattern.sample_count() {
67 return Err(ResidualError::CalculatedLengthMismatch {
68 expected: pattern.sample_count(),
69 actual: calculated_y.len(),
70 });
71 }
72 if let Some(index) = calculated_y.iter().position(|value| !value.is_finite()) {
73 return Err(ResidualError::NonFiniteCalculated { index });
74 }
75 let included = pattern
76 .mask
77 .clone()
78 .unwrap_or_else(|| vec![true; pattern.sample_count()]);
79 let residual = calculated_y
80 .iter()
81 .zip(observed_y)
82 .map(|(calculated, observed)| calculated - observed)
83 .collect::<Vec<_>>();
84 let uncertainty = options
85 .use_uncertainty
86 .then_some(pattern.uncertainty.as_deref())
87 .flatten();
88 let weighted_residual = match uncertainty {
89 Some(uncertainty) => residual
90 .iter()
91 .zip(uncertainty)
92 .map(|(value, sigma)| value / sigma)
93 .collect(),
94 None => residual.clone(),
95 };
96
97 let mut included_count = 0_usize;
98 let mut absolute_residual_sum = 0.0;
99 let mut absolute_observed_sum = 0.0;
100 let mut weighted_observed_square_sum = 0.0;
101 let mut chi_square = 0.0;
102 for index in 0..pattern.sample_count() {
103 if !included[index] {
104 continue;
105 }
106 included_count += 1;
107 absolute_residual_sum += residual[index].abs();
108 absolute_observed_sum += observed_y[index].abs();
109 chi_square += weighted_residual[index] * weighted_residual[index];
110 weighted_observed_square_sum += match uncertainty {
111 Some(uncertainty) => {
112 let sigma = uncertainty[index];
113 (1.0 / (sigma * sigma)) * (observed_y[index] * observed_y[index])
114 }
115 None => observed_y[index] * observed_y[index],
116 };
117 }
118 let rp = if absolute_observed_sum == 0.0 {
119 f64::INFINITY
120 } else {
121 absolute_residual_sum / absolute_observed_sum
122 };
123 let rwp = if weighted_observed_square_sum == 0.0 {
124 f64::INFINITY
125 } else {
126 (chi_square / weighted_observed_square_sum).sqrt()
127 };
128 let degrees_of_freedom = included_count.checked_sub(options.parameter_count);
129 let reduced_chi_square = match degrees_of_freedom {
130 Some(degrees) if degrees > 0 => chi_square / count_as_f64(degrees),
131 _ => f64::INFINITY,
132 };
133 Ok(ResidualEvaluation {
134 included,
135 residual,
136 weighted_residual,
137 rp,
138 rwp,
139 chi_square,
140 reduced_chi_square,
141 })
142}
143
144#[allow(clippy::cast_precision_loss)]
147fn count_as_f64(value: usize) -> f64 {
148 value as f64
149}
150
151#[derive(Debug)]
153pub enum ResidualError {
154 Pattern(DomainError),
156 MissingObservations,
158 CalculatedLengthMismatch {
160 expected: usize,
162 actual: usize,
164 },
165 NonFiniteCalculated {
167 index: usize,
169 },
170}
171
172impl Display for ResidualError {
173 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
174 match self {
175 Self::Pattern(error) => Display::fmt(error, formatter),
176 Self::MissingObservations => {
177 formatter.write_str("observed_y is required for residual evaluation")
178 }
179 Self::CalculatedLengthMismatch { expected, actual } => write!(
180 formatter,
181 "calculated_y length {actual} does not match pattern length {expected}"
182 ),
183 Self::NonFiniteCalculated { index } => {
184 write!(
185 formatter,
186 "calculated_y contains a non-finite value at index {index}"
187 )
188 }
189 }
190 }
191}
192
193impl Error for ResidualError {
194 fn source(&self) -> Option<&(dyn Error + 'static)> {
195 match self {
196 Self::Pattern(error) => Some(error),
197 _ => None,
198 }
199 }
200}