use std::error::Error;
use std::fmt::{Display, Formatter};
use phasesmith_core::SupportPolicy;
use phasesmith_engine::{
PreparedStructuralModelInputView, PreparedStructuralMultiphase, StructuralMultiphaseError,
};
use crate::rietveld::{
assemble_rietveld_calculation, prepare_phase_model, resolve_phase_contributions,
};
use crate::{
DifferentiableBackground, RietveldCalculation, RietveldCalculationOptions, RietveldError,
RietveldInput, RietveldParameterError, RietveldStructuralLayout,
};
#[derive(Clone, Debug, PartialEq)]
pub struct PreparedRietveldLinearization {
pub calculation: RietveldCalculation,
pub jacobian: Vec<f64>,
pub parameter_count: usize,
}
pub struct PreparedRietveldObjective {
input: RietveldInput,
options: RietveldCalculationOptions,
layout: RietveldStructuralLayout,
prepared: PreparedStructuralMultiphase,
}
impl PreparedRietveldObjective {
pub fn new(
input: RietveldInput,
options: RietveldCalculationOptions,
layout: RietveldStructuralLayout,
) -> Result<Self, RietveldObjectiveError> {
input.validate()?;
options.validate()?;
let models = input
.phases
.iter()
.map(|phase| {
prepare_phase_model(phase, input.fixed_spectrum.as_ref(), &options.execution)
})
.collect::<Result<Vec<_>, _>>()?;
let prepared = PreparedStructuralMultiphase::new(models, options.execution.clone())?;
layout.validate_phases(&input.phases)?;
Ok(Self {
input,
options,
layout,
prepared,
})
}
#[must_use]
pub const fn layout(&self) -> &RietveldStructuralLayout {
&self.layout
}
pub fn dense_element_count(&self) -> Result<usize, RietveldObjectiveError> {
self.prepared
.structural_parameter_counts()
.into_iter()
.try_fold(0_usize, usize::checked_add)
.and_then(|rows| rows.checked_mul(self.input.pattern.sample_count()))
.ok_or(RietveldObjectiveError::AllocationOverflow)
}
pub fn linearize(&self) -> Result<PreparedRietveldLinearization, RietveldObjectiveError> {
let products = self.with_inputs(|inputs| self.prepared.linearize(inputs))?;
let sample_count = self.input.pattern.sample_count();
let jacobians = products
.iter()
.map(|product| (product.d_y.as_slice(), product.parameter_count))
.collect::<Vec<_>>();
let jacobian = self
.layout
.project_native_jacobians(&jacobians, sample_count)?;
let calculation = assemble_rietveld_calculation(
&self.input,
&self.options,
products.into_iter().map(|product| product.result).collect(),
)?;
Ok(PreparedRietveldLinearization {
calculation,
jacobian,
parameter_count: self.layout.parameters().specs().len(),
})
}
pub fn jvp(&self, direction: &[f64]) -> Result<(Vec<f64>, Vec<f64>), RietveldObjectiveError> {
let tangents = self.layout.native_tangents(direction)?;
let tangent_views = tangents.iter().map(Vec::as_slice).collect::<Vec<_>>();
let products = self.with_inputs(|inputs| self.prepared.jvp(inputs, &tangent_views))?;
let sample_count = self.input.pattern.sample_count();
let mut profile = vec![0.0; sample_count];
let mut derivative = vec![0.0; sample_count];
for product in products {
for sample in 0..sample_count {
profile[sample] += product.result.accumulation.y[sample];
derivative[sample] += product.d_y[sample];
}
}
Ok((profile, derivative))
}
pub fn vjp(&self, sample_weights: &[f64]) -> Result<Vec<f64>, RietveldObjectiveError> {
let products = self.with_inputs(|inputs| self.prepared.vjp(inputs, sample_weights))?;
let gradients = products
.iter()
.map(|item| item.gradient.as_slice())
.collect::<Vec<_>>();
Ok(self.layout.project_native_gradients(&gradients)?)
}
pub fn normal_product(
&self,
direction: &[f64],
damping: f64,
) -> Result<Vec<f64>, RietveldObjectiveError> {
if !damping.is_finite() || damping < 0.0 {
return Err(RietveldObjectiveError::InvalidDamping);
}
let (_, derivative) = self.jvp(direction)?;
let weights = self.weight_samples(&derivative);
let mut product = self.vjp(&weights)?;
for (value, direction) in product.iter_mut().zip(direction) {
*value += damping * direction;
}
Ok(product)
}
pub fn gradient(&self) -> Result<(Vec<f64>, Vec<f64>), RietveldObjectiveError> {
let zero = vec![0.0; self.layout.parameters().specs().len()];
let (profile, _) = self.jvp(&zero)?;
let mut background = self.input.pattern.background_y.clone();
if let Some(model) = &self.input.background {
for (target, value) in background.iter_mut().zip(
model
.calculate(&self.input.pattern.x_deg)
.map_err(RietveldError::Background)?,
) {
*target += value;
}
}
let calculated = profile
.iter()
.zip(background)
.map(|(profile, background)| profile + background)
.collect::<Vec<_>>();
if calculated.iter().any(|value| !value.is_finite()) {
return Err(RietveldError::NonFiniteCalculation.into());
}
let observed = self
.input
.pattern
.observed_y
.as_deref()
.ok_or(RietveldError::MissingObservations)?;
let residual = calculated
.iter()
.zip(observed)
.map(|(calculated, observed)| calculated - observed)
.collect::<Vec<_>>();
Ok((calculated, self.vjp(&self.weight_samples(&residual))?))
}
fn weight_samples(&self, values: &[f64]) -> Vec<f64> {
let mask = self.input.pattern.mask.as_deref();
let uncertainty = self
.options
.use_uncertainty
.then_some(self.input.pattern.uncertainty.as_deref())
.flatten();
values
.iter()
.enumerate()
.map(|(index, value)| {
if mask.is_some_and(|mask| !mask[index]) {
0.0
} else if let Some(sigma) = uncertainty {
value / (sigma[index] * sigma[index])
} else {
*value
}
})
.collect()
}
fn with_inputs<R>(
&self,
operation: impl FnOnce(
&[PreparedStructuralModelInputView<'_>],
) -> Result<R, StructuralMultiphaseError>,
) -> Result<R, RietveldObjectiveError> {
let owned = self
.input
.phases
.iter()
.map(|phase| resolve_phase_contributions(phase, &self.input))
.collect::<Result<Vec<_>, _>>()?;
let views = owned
.iter()
.map(|phase| phase.iter().map(|item| item.as_view()).collect::<Vec<_>>())
.collect::<Vec<_>>();
let inputs = views
.iter()
.map(|contributions| PreparedStructuralModelInputView {
x_deg: &self.input.pattern.x_deg,
instrument: self.input.instrument,
axial_geometry: self.input.axial_geometry,
position_correction: self.input.position_correction,
contributions,
support: SupportPolicy::FwhmMultiple(self.options.support_fwhm),
})
.collect::<Vec<_>>();
operation(&inputs).map_err(Into::into)
}
}
#[derive(Debug)]
pub enum RietveldObjectiveError {
Rietveld(RietveldError),
Parameter(RietveldParameterError),
Structural(StructuralMultiphaseError),
InvalidDamping,
AllocationOverflow,
}
impl Display for RietveldObjectiveError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Rietveld(error) => Display::fmt(error, formatter),
Self::Parameter(error) => Display::fmt(error, formatter),
Self::Structural(error) => Display::fmt(error, formatter),
Self::InvalidDamping => {
formatter.write_str("Rietveld damping must be finite and non-negative")
}
Self::AllocationOverflow => {
formatter.write_str("Rietveld dense linearization allocation overflow")
}
}
}
}
impl Error for RietveldObjectiveError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Rietveld(error) => Some(error),
Self::Parameter(error) => Some(error),
Self::Structural(error) => Some(error),
Self::InvalidDamping | Self::AllocationOverflow => None,
}
}
}
impl From<RietveldError> for RietveldObjectiveError {
fn from(value: RietveldError) -> Self {
Self::Rietveld(value)
}
}
impl From<RietveldParameterError> for RietveldObjectiveError {
fn from(value: RietveldParameterError) -> Self {
Self::Parameter(value)
}
}
impl From<StructuralMultiphaseError> for RietveldObjectiveError {
fn from(value: StructuralMultiphaseError) -> Self {
Self::Structural(value)
}
}