use std::error::Error;
use std::fmt::{Display, Formatter};
use nalgebra::{DMatrix, DVector};
use crate::{RietveldCalculationOptions, RietveldError, RietveldInput, calculate_rietveld_pattern};
#[derive(Clone, Debug, PartialEq)]
pub struct PhaseScaleEstimationResult {
pub input: RietveldInput,
pub scales: Vec<f64>,
pub included_points: usize,
pub active_phases: usize,
pub iterations: usize,
pub weighted_residual_sum_squares: f64,
}
#[derive(Debug)]
pub enum PhaseScaleEstimationError {
MissingObservations,
EmptyDomain,
AllocationOverflow,
Rietveld(RietveldError),
LinearSolve,
DidNotConverge,
}
impl Display for PhaseScaleEstimationError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingObservations => {
formatter.write_str("phase-scale estimation requires observed intensities")
}
Self::EmptyDomain => {
formatter.write_str("phase-scale estimation has no mask-included observations")
}
Self::AllocationOverflow => {
formatter.write_str("phase-scale estimation matrix allocation overflow")
}
Self::Rietveld(error) => Display::fmt(error, formatter),
Self::LinearSolve => {
formatter.write_str("phase-scale active least-squares solve failed")
}
Self::DidNotConverge => {
formatter.write_str("phase-scale active-set solve did not converge")
}
}
}
}
impl Error for PhaseScaleEstimationError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Rietveld(error) => Some(error),
_ => None,
}
}
}
impl From<RietveldError> for PhaseScaleEstimationError {
fn from(value: RietveldError) -> Self {
Self::Rietveld(value)
}
}
pub fn estimate_initial_phase_scales(
input: &RietveldInput,
options: &RietveldCalculationOptions,
) -> Result<PhaseScaleEstimationResult, PhaseScaleEstimationError> {
input.validate()?;
let observed = input
.pattern
.observed_y
.as_ref()
.ok_or(PhaseScaleEstimationError::MissingObservations)?;
let mut unit_input = input.clone();
unit_input.phases = input
.phases
.iter()
.map(|phase| {
let mut definition = phase.definition().clone();
definition.scale = 1.0;
phase.with_definition(definition)
})
.collect::<Result<Vec<_>, _>>()?;
let calculation = calculate_rietveld_pattern(&unit_input, options)?;
let included_indices = (0..input.pattern.sample_count())
.filter(|index| input.pattern.mask.as_ref().is_none_or(|mask| mask[*index]))
.collect::<Vec<_>>();
if included_indices.is_empty() {
return Err(PhaseScaleEstimationError::EmptyDomain);
}
let rows = included_indices.len();
let columns = calculation.phases.len();
let capacity = rows
.checked_mul(columns)
.ok_or(PhaseScaleEstimationError::AllocationOverflow)?;
let mut design = Vec::with_capacity(capacity);
let mut target = Vec::with_capacity(rows);
for index in included_indices {
let sigma = if options.use_uncertainty {
input
.pattern
.uncertainty
.as_ref()
.map_or(1.0, |values| values[index])
} else {
1.0
};
for phase in &calculation.phases {
design.push(phase.result.accumulation.y[index] / sigma);
}
target.push((observed[index] - calculation.background_y[index]) / sigma);
}
let matrix = DMatrix::from_row_slice(rows, columns, &design);
let target = DVector::from_vec(target);
let (scales, iterations) = solve_non_negative_least_squares(&matrix, &target)?;
let residual = &matrix * &scales - target;
let weighted_residual_sum_squares = residual.dot(&residual);
let scales = scales.as_slice().to_vec();
if !weighted_residual_sum_squares.is_finite()
|| scales
.iter()
.any(|scale| !scale.is_finite() || *scale < 0.0)
{
return Err(PhaseScaleEstimationError::LinearSolve);
}
let mut estimated_input = input.clone();
estimated_input.phases = input
.phases
.iter()
.zip(&scales)
.map(|(phase, scale)| {
let mut definition = phase.definition().clone();
definition.scale = *scale;
phase.with_definition(definition)
})
.collect::<Result<Vec<_>, _>>()?;
estimated_input.validate()?;
Ok(PhaseScaleEstimationResult {
input: estimated_input,
active_phases: scales.iter().filter(|scale| **scale > 0.0).count(),
included_points: rows,
scales,
iterations,
weighted_residual_sum_squares,
})
}
fn solve_non_negative_least_squares(
matrix: &DMatrix<f64>,
target: &DVector<f64>,
) -> Result<(DVector<f64>, usize), PhaseScaleEstimationError> {
let columns = matrix.ncols();
let gram = matrix.transpose() * matrix;
let correlation = matrix.transpose() * target;
let tolerance = 1.0e-12
* correlation
.iter()
.fold(1.0_f64, |maximum, value| maximum.max(value.abs()));
let maximum_iterations = columns
.checked_mul(columns)
.and_then(|value| value.checked_mul(10))
.and_then(|value| value.checked_add(10))
.ok_or(PhaseScaleEstimationError::AllocationOverflow)?;
let mut solution = DVector::zeros(columns);
let mut passive = vec![false; columns];
let mut iterations = 0usize;
loop {
let gradient = &correlation - &gram * &solution;
let candidate = (0..columns)
.filter(|index| !passive[*index] && gradient[*index] > tolerance)
.max_by(|left, right| {
gradient[*left]
.total_cmp(&gradient[*right])
.then_with(|| right.cmp(left))
});
let Some(candidate) = candidate else {
return Ok((solution, iterations));
};
passive[candidate] = true;
iterations += 1;
if iterations > maximum_iterations {
return Err(PhaseScaleEstimationError::DidNotConverge);
}
loop {
let active = passive
.iter()
.enumerate()
.filter_map(|(index, selected)| selected.then_some(index))
.collect::<Vec<_>>();
if active.is_empty() {
break;
}
let active_matrix = DMatrix::from_fn(matrix.nrows(), active.len(), |row, column| {
matrix[(row, active[column])]
});
let active_solution = active_matrix
.svd(true, true)
.solve(target, f64::EPSILON)
.map_err(|_| PhaseScaleEstimationError::LinearSolve)?;
if active_solution.iter().any(|value| !value.is_finite()) {
return Err(PhaseScaleEstimationError::LinearSolve);
}
let mut candidate_solution = DVector::zeros(columns);
for (active_index, value) in active.into_iter().zip(active_solution.iter()) {
candidate_solution[active_index] = *value;
}
if (0..columns).all(|index| !passive[index] || candidate_solution[index] > 0.0) {
solution = candidate_solution;
break;
}
let alpha = (0..columns)
.filter(|index| passive[*index] && candidate_solution[*index] <= 0.0)
.map(|index| solution[index] / (solution[index] - candidate_solution[index]))
.fold(1.0_f64, f64::min);
solution += alpha * (candidate_solution - &solution);
for index in 0..columns {
if passive[index] && solution[index] <= 0.0 {
solution[index] = 0.0;
passive[index] = false;
}
}
iterations += 1;
if iterations > maximum_iterations {
return Err(PhaseScaleEstimationError::DidNotConverge);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn high_count_design_accepts_a_small_positive_scale() {
let matrix = DMatrix::from_row_slice(1, 1, &[1.0e8]);
let target = DVector::from_vec(vec![1.0]);
let (solution, iterations) =
solve_non_negative_least_squares(&matrix, &target).expect("bounded scale solve");
assert_eq!(iterations, 1);
assert!((solution[0] - 1.0e-8).abs() < 1.0e-20);
}
}