phasesmith_workflows/
residuals.rs1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use phasesmith_model::{DomainError, PatternRecord, TofPatternRecord};
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 evaluate_residual_arrays(
63 pattern.sample_count(),
64 pattern.observed_y.as_deref(),
65 pattern.uncertainty.as_deref(),
66 pattern.mask.as_deref(),
67 calculated_y,
68 options,
69 )
70}
71
72pub fn evaluate_tof_residuals(
83 pattern: &TofPatternRecord,
84 calculated_y: &[f64],
85 options: ResidualOptions,
86) -> Result<ResidualEvaluation, ResidualError> {
87 pattern.validate().map_err(ResidualError::Pattern)?;
88 evaluate_residual_arrays(
89 pattern.sample_count(),
90 pattern.observed_y.as_deref(),
91 pattern.uncertainty.as_deref(),
92 pattern.mask.as_deref(),
93 calculated_y,
94 options,
95 )
96}
97
98fn evaluate_residual_arrays(
99 sample_count: usize,
100 observed_y: Option<&[f64]>,
101 uncertainty: Option<&[f64]>,
102 mask: Option<&[bool]>,
103 calculated_y: &[f64],
104 options: ResidualOptions,
105) -> Result<ResidualEvaluation, ResidualError> {
106 let observed_y = observed_y.ok_or(ResidualError::MissingObservations)?;
107 if calculated_y.len() != sample_count {
108 return Err(ResidualError::CalculatedLengthMismatch {
109 expected: sample_count,
110 actual: calculated_y.len(),
111 });
112 }
113 if let Some(index) = calculated_y.iter().position(|value| !value.is_finite()) {
114 return Err(ResidualError::NonFiniteCalculated { index });
115 }
116 let included = mask.map_or_else(|| vec![true; sample_count], <[bool]>::to_vec);
117 let residual = calculated_y
118 .iter()
119 .zip(observed_y)
120 .map(|(calculated, observed)| calculated - observed)
121 .collect::<Vec<_>>();
122 let uncertainty = options.use_uncertainty.then_some(uncertainty).flatten();
123 let weighted_residual = match uncertainty {
124 Some(uncertainty) => residual
125 .iter()
126 .zip(uncertainty)
127 .map(|(value, sigma)| value / sigma)
128 .collect(),
129 None => residual.clone(),
130 };
131
132 let mut included_count = 0_usize;
133 let mut absolute_residual_sum = 0.0;
134 let mut absolute_observed_sum = 0.0;
135 let mut weighted_observed_square_sum = 0.0;
136 let mut chi_square = 0.0;
137 for index in 0..sample_count {
138 if !included[index] {
139 continue;
140 }
141 included_count += 1;
142 absolute_residual_sum += residual[index].abs();
143 absolute_observed_sum += observed_y[index].abs();
144 chi_square += weighted_residual[index] * weighted_residual[index];
145 weighted_observed_square_sum += match uncertainty {
146 Some(uncertainty) => {
147 let sigma = uncertainty[index];
148 (1.0 / (sigma * sigma)) * (observed_y[index] * observed_y[index])
149 }
150 None => observed_y[index] * observed_y[index],
151 };
152 }
153 let rp = if absolute_observed_sum == 0.0 {
154 f64::INFINITY
155 } else {
156 absolute_residual_sum / absolute_observed_sum
157 };
158 let rwp = if weighted_observed_square_sum == 0.0 {
159 f64::INFINITY
160 } else {
161 (chi_square / weighted_observed_square_sum).sqrt()
162 };
163 let degrees_of_freedom = included_count.checked_sub(options.parameter_count);
164 let reduced_chi_square = match degrees_of_freedom {
165 Some(degrees) if degrees > 0 => chi_square / count_as_f64(degrees),
166 _ => f64::INFINITY,
167 };
168 Ok(ResidualEvaluation {
169 included,
170 residual,
171 weighted_residual,
172 rp,
173 rwp,
174 chi_square,
175 reduced_chi_square,
176 })
177}
178
179#[allow(clippy::cast_precision_loss)]
182fn count_as_f64(value: usize) -> f64 {
183 value as f64
184}
185
186#[derive(Debug)]
188pub enum ResidualError {
189 Pattern(DomainError),
191 MissingObservations,
193 CalculatedLengthMismatch {
195 expected: usize,
197 actual: usize,
199 },
200 NonFiniteCalculated {
202 index: usize,
204 },
205}
206
207impl Display for ResidualError {
208 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
209 match self {
210 Self::Pattern(error) => Display::fmt(error, formatter),
211 Self::MissingObservations => {
212 formatter.write_str("observed_y is required for residual evaluation")
213 }
214 Self::CalculatedLengthMismatch { expected, actual } => write!(
215 formatter,
216 "calculated_y length {actual} does not match pattern length {expected}"
217 ),
218 Self::NonFiniteCalculated { index } => {
219 write!(
220 formatter,
221 "calculated_y contains a non-finite value at index {index}"
222 )
223 }
224 }
225 }
226}
227
228impl Error for ResidualError {
229 fn source(&self) -> Option<&(dyn Error + 'static)> {
230 match self {
231 Self::Pattern(error) => Some(error),
232 _ => None,
233 }
234 }
235}