phasesmith_workflows/
phase_scale_estimation.rs1use std::error::Error;
14use std::fmt::{Display, Formatter};
15
16use nalgebra::{DMatrix, DVector};
17
18use crate::{RietveldCalculationOptions, RietveldError, RietveldInput, calculate_rietveld_pattern};
19
20#[derive(Clone, Debug, PartialEq)]
22pub struct PhaseScaleEstimationResult {
23 pub input: RietveldInput,
25 pub scales: Vec<f64>,
27 pub included_points: usize,
29 pub active_phases: usize,
31 pub iterations: usize,
33 pub weighted_residual_sum_squares: f64,
35}
36
37#[derive(Debug)]
39pub enum PhaseScaleEstimationError {
40 MissingObservations,
42 EmptyDomain,
44 AllocationOverflow,
46 Rietveld(RietveldError),
48 LinearSolve,
50 DidNotConverge,
52}
53
54impl Display for PhaseScaleEstimationError {
55 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
56 match self {
57 Self::MissingObservations => {
58 formatter.write_str("phase-scale estimation requires observed intensities")
59 }
60 Self::EmptyDomain => {
61 formatter.write_str("phase-scale estimation has no mask-included observations")
62 }
63 Self::AllocationOverflow => {
64 formatter.write_str("phase-scale estimation matrix allocation overflow")
65 }
66 Self::Rietveld(error) => Display::fmt(error, formatter),
67 Self::LinearSolve => {
68 formatter.write_str("phase-scale active least-squares solve failed")
69 }
70 Self::DidNotConverge => {
71 formatter.write_str("phase-scale active-set solve did not converge")
72 }
73 }
74 }
75}
76
77impl Error for PhaseScaleEstimationError {
78 fn source(&self) -> Option<&(dyn Error + 'static)> {
79 match self {
80 Self::Rietveld(error) => Some(error),
81 _ => None,
82 }
83 }
84}
85
86impl From<RietveldError> for PhaseScaleEstimationError {
87 fn from(value: RietveldError) -> Self {
88 Self::Rietveld(value)
89 }
90}
91
92pub fn estimate_initial_phase_scales(
100 input: &RietveldInput,
101 options: &RietveldCalculationOptions,
102) -> Result<PhaseScaleEstimationResult, PhaseScaleEstimationError> {
103 input.validate()?;
104 let observed = input
105 .pattern
106 .observed_y
107 .as_ref()
108 .ok_or(PhaseScaleEstimationError::MissingObservations)?;
109 let mut unit_input = input.clone();
110 unit_input.phases = input
111 .phases
112 .iter()
113 .map(|phase| {
114 let mut definition = phase.definition().clone();
115 definition.scale = 1.0;
116 phase.with_definition(definition)
117 })
118 .collect::<Result<Vec<_>, _>>()?;
119 let calculation = calculate_rietveld_pattern(&unit_input, options)?;
120 let included_indices = (0..input.pattern.sample_count())
121 .filter(|index| input.pattern.mask.as_ref().is_none_or(|mask| mask[*index]))
122 .collect::<Vec<_>>();
123 if included_indices.is_empty() {
124 return Err(PhaseScaleEstimationError::EmptyDomain);
125 }
126 let rows = included_indices.len();
127 let columns = calculation.phases.len();
128 let capacity = rows
129 .checked_mul(columns)
130 .ok_or(PhaseScaleEstimationError::AllocationOverflow)?;
131 let mut design = Vec::with_capacity(capacity);
132 let mut target = Vec::with_capacity(rows);
133 for index in included_indices {
134 let sigma = if options.use_uncertainty {
135 input
136 .pattern
137 .uncertainty
138 .as_ref()
139 .map_or(1.0, |values| values[index])
140 } else {
141 1.0
142 };
143 for phase in &calculation.phases {
144 design.push(phase.result.accumulation.y[index] / sigma);
145 }
146 target.push((observed[index] - calculation.background_y[index]) / sigma);
147 }
148 let matrix = DMatrix::from_row_slice(rows, columns, &design);
149 let target = DVector::from_vec(target);
150 let (scales, iterations) = solve_non_negative_least_squares(&matrix, &target)?;
151 let residual = &matrix * &scales - target;
152 let weighted_residual_sum_squares = residual.dot(&residual);
153 let scales = scales.as_slice().to_vec();
154 if !weighted_residual_sum_squares.is_finite()
155 || scales
156 .iter()
157 .any(|scale| !scale.is_finite() || *scale < 0.0)
158 {
159 return Err(PhaseScaleEstimationError::LinearSolve);
160 }
161 let mut estimated_input = input.clone();
162 estimated_input.phases = input
163 .phases
164 .iter()
165 .zip(&scales)
166 .map(|(phase, scale)| {
167 let mut definition = phase.definition().clone();
168 definition.scale = *scale;
169 phase.with_definition(definition)
170 })
171 .collect::<Result<Vec<_>, _>>()?;
172 estimated_input.validate()?;
173 Ok(PhaseScaleEstimationResult {
174 input: estimated_input,
175 active_phases: scales.iter().filter(|scale| **scale > 0.0).count(),
176 included_points: rows,
177 scales,
178 iterations,
179 weighted_residual_sum_squares,
180 })
181}
182
183fn solve_non_negative_least_squares(
184 matrix: &DMatrix<f64>,
185 target: &DVector<f64>,
186) -> Result<(DVector<f64>, usize), PhaseScaleEstimationError> {
187 let columns = matrix.ncols();
188 let gram = matrix.transpose() * matrix;
189 let correlation = matrix.transpose() * target;
190 let tolerance = 1.0e-12
191 * correlation
192 .iter()
193 .fold(1.0_f64, |maximum, value| maximum.max(value.abs()));
194 let maximum_iterations = columns
195 .checked_mul(columns)
196 .and_then(|value| value.checked_mul(10))
197 .and_then(|value| value.checked_add(10))
198 .ok_or(PhaseScaleEstimationError::AllocationOverflow)?;
199 let mut solution = DVector::zeros(columns);
200 let mut passive = vec![false; columns];
201 let mut iterations = 0usize;
202
203 loop {
204 let gradient = &correlation - &gram * &solution;
205 let candidate = (0..columns)
206 .filter(|index| !passive[*index] && gradient[*index] > tolerance)
207 .max_by(|left, right| {
208 gradient[*left]
209 .total_cmp(&gradient[*right])
210 .then_with(|| right.cmp(left))
211 });
212 let Some(candidate) = candidate else {
213 return Ok((solution, iterations));
214 };
215 passive[candidate] = true;
216 iterations += 1;
217 if iterations > maximum_iterations {
218 return Err(PhaseScaleEstimationError::DidNotConverge);
219 }
220
221 loop {
222 let active = passive
223 .iter()
224 .enumerate()
225 .filter_map(|(index, selected)| selected.then_some(index))
226 .collect::<Vec<_>>();
227 if active.is_empty() {
231 break;
232 }
233 let active_matrix = DMatrix::from_fn(matrix.nrows(), active.len(), |row, column| {
234 matrix[(row, active[column])]
235 });
236 let active_solution = active_matrix
237 .svd(true, true)
238 .solve(target, f64::EPSILON)
239 .map_err(|_| PhaseScaleEstimationError::LinearSolve)?;
240 if active_solution.iter().any(|value| !value.is_finite()) {
241 return Err(PhaseScaleEstimationError::LinearSolve);
242 }
243 let mut candidate_solution = DVector::zeros(columns);
244 for (active_index, value) in active.into_iter().zip(active_solution.iter()) {
245 candidate_solution[active_index] = *value;
246 }
247 if (0..columns).all(|index| !passive[index] || candidate_solution[index] > 0.0) {
252 solution = candidate_solution;
253 break;
254 }
255 let alpha = (0..columns)
256 .filter(|index| passive[*index] && candidate_solution[*index] <= 0.0)
257 .map(|index| solution[index] / (solution[index] - candidate_solution[index]))
258 .fold(1.0_f64, f64::min);
259 solution += alpha * (candidate_solution - &solution);
260 for index in 0..columns {
261 if passive[index] && solution[index] <= 0.0 {
262 solution[index] = 0.0;
263 passive[index] = false;
264 }
265 }
266 iterations += 1;
267 if iterations > maximum_iterations {
268 return Err(PhaseScaleEstimationError::DidNotConverge);
269 }
270 }
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn high_count_design_accepts_a_small_positive_scale() {
280 let matrix = DMatrix::from_row_slice(1, 1, &[1.0e8]);
281 let target = DVector::from_vec(vec![1.0]);
282
283 let (solution, iterations) =
284 solve_non_negative_least_squares(&matrix, &target).expect("bounded scale solve");
285
286 assert_eq!(iterations, 1);
287 assert!((solution[0] - 1.0e-8).abs() < 1.0e-20);
288 }
289}