use std::collections::BTreeSet;
use std::error::Error;
use std::fmt::{Display, Formatter};
use phasesmith_core::OwnedCwContributions;
use phasesmith_model::{DomainError, ProjectRecord, RadiationDefinition, RecordId};
use crate::{
Constraint, ConstraintError, ConstraintTransform, LatticeBounds, RietveldCovarianceOptions,
RietveldGeneralCheckpoint, RietveldGeneralParameterError, RietveldGeneralRefinementError,
RietveldInput, RietveldParameterLayout, RietveldParameterSelection, RietveldRefinementError,
RietveldRefinementOptions,
};
#[derive(Clone, Debug, PartialEq)]
pub struct RietveldAnalysis {
pub histogram_id: RecordId,
pub input: RietveldInput,
pub selection: RietveldParameterSelection,
pub lattice_bounds: Vec<Option<LatticeBounds>>,
pub constraints: Vec<Constraint>,
pub options: RietveldRefinementOptions,
pub covariance: RietveldCovarianceOptions,
pub checkpoint: Option<RietveldGeneralCheckpoint>,
}
impl RietveldAnalysis {
pub fn validate(&self) -> Result<(), RietveldProjectError> {
self.input.validate()?;
self.selection.validate()?;
self.options.validate()?;
RietveldCovarianceOptions::new(
self.covariance.enabled,
self.covariance.max_parameters,
self.covariance.unresolved_correlation,
)?;
if self.lattice_bounds.len() != self.input.phases.len() {
return Err(RietveldProjectError::LatticeBoundCountMismatch);
}
let layout =
RietveldParameterLayout::new(&self.input, &self.selection, &self.lattice_bounds)?;
let transform =
ConstraintTransform::new(layout.parameters().clone(), self.constraints.clone())?;
let constrained = transform.unpack(&transform.pack()?, false)?;
if layout.parameters().specs().iter().any(|spec| {
constrained
.get(spec.key())
.is_none_or(|value| (value - spec.value()).abs() > 2.0e-12)
}) {
return Err(RietveldProjectError::UnsatisfiedConstraint);
}
if let Some(checkpoint) = &self.checkpoint {
if checkpoint.completed_iterations > self.options.limits.max_iterations() {
return Err(RietveldProjectError::CheckpointExceedsIterationLimit);
}
checkpoint.validate_for(
&self.input,
&self.selection,
&self.lattice_bounds,
&self.constraints,
)?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct RietveldProjectState {
pub project: ProjectRecord,
pub analyses: Vec<RietveldAnalysis>,
}
impl RietveldProjectState {
pub fn validate(&self) -> Result<(), RietveldProjectError> {
self.project.validate()?;
let mut histogram_ids = BTreeSet::new();
for analysis in &self.analyses {
analysis.validate()?;
if !histogram_ids.insert(analysis.histogram_id.clone()) {
return Err(RietveldProjectError::DuplicateAnalysis {
histogram_id: analysis.histogram_id.clone(),
});
}
let histogram = self
.project
.histograms
.iter()
.find(|item| item.histogram_id == analysis.histogram_id)
.ok_or_else(|| RietveldProjectError::UnknownHistogram {
histogram_id: analysis.histogram_id.clone(),
})?;
let expected_spectrum = match &histogram.experiment.radiation {
RadiationDefinition::Monochromatic { .. } => None,
RadiationDefinition::FixedSpectrum { spectrum, .. } => Some(spectrum),
};
if analysis.input.pattern != histogram.pattern
|| analysis.input.instrument != histogram.experiment.instrument
|| analysis.input.fixed_spectrum.as_ref() != expected_spectrum
|| analysis.input.axial_geometry != histogram.experiment.axial_geometry
|| analysis.input.position_correction != histogram.experiment.position_correction
{
return Err(RietveldProjectError::HistogramStateMismatch {
histogram_id: analysis.histogram_id.clone(),
});
}
let analysis_phase_ids = analysis
.input
.phases
.iter()
.map(crate::RietveldPhase::phase_id)
.collect::<Vec<_>>();
if analysis_phase_ids != histogram.phase_ids.iter().collect::<Vec<_>>() {
return Err(RietveldProjectError::PhaseOrderMismatch {
histogram_id: analysis.histogram_id.clone(),
});
}
for phase in &analysis.input.phases {
let stored = self
.project
.phases
.iter()
.find(|item| &item.phase_id == phase.phase_id())
.ok_or_else(|| RietveldProjectError::PhaseOrderMismatch {
histogram_id: analysis.histogram_id.clone(),
})?;
if !stored.required_providers.is_empty() {
return Err(RietveldProjectError::ExternalProviderRequired {
phase_id: stored.phase_id.clone(),
});
}
if phase.sample_physics().is_none()
&& phase.contributions()
!= &OwnedCwContributions::neutral(phase.reflection_ids().len())
{
return Err(RietveldProjectError::OpaqueStaticContributions {
phase_id: stored.phase_id.clone(),
});
}
if stored.name != phase.name() || stored.definition != *phase.definition() {
return Err(RietveldProjectError::PhaseStateMismatch {
phase_id: stored.phase_id.clone(),
});
}
}
}
Ok(())
}
}
#[derive(Debug)]
pub enum RietveldProjectError {
Domain(DomainError),
Rietveld(crate::RietveldError),
Parameter(RietveldGeneralParameterError),
Constraint(ConstraintError),
Options(RietveldRefinementError),
General(RietveldGeneralRefinementError),
LatticeBoundCountMismatch,
UnsatisfiedConstraint,
CheckpointExceedsIterationLimit,
DuplicateAnalysis {
histogram_id: RecordId,
},
UnknownHistogram {
histogram_id: RecordId,
},
HistogramStateMismatch {
histogram_id: RecordId,
},
PhaseOrderMismatch {
histogram_id: RecordId,
},
ExternalProviderRequired {
phase_id: RecordId,
},
OpaqueStaticContributions {
phase_id: RecordId,
},
PhaseStateMismatch {
phase_id: RecordId,
},
}
impl Display for RietveldProjectError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Domain(error) => Display::fmt(error, formatter),
Self::Rietveld(error) => Display::fmt(error, formatter),
Self::Parameter(error) => Display::fmt(error, formatter),
Self::Constraint(error) => Display::fmt(error, formatter),
Self::Options(error) => Display::fmt(error, formatter),
Self::General(error) => Display::fmt(error, formatter),
Self::LatticeBoundCountMismatch => {
formatter.write_str("Rietveld lattice bounds must align with phase order")
}
Self::UnsatisfiedConstraint => {
formatter.write_str("initial Rietveld values do not satisfy their constraints")
}
Self::CheckpointExceedsIterationLimit => {
formatter.write_str("Rietveld checkpoint exceeds the saved maximum iteration limit")
}
Self::DuplicateAnalysis { histogram_id } => {
write!(
formatter,
"histogram {histogram_id} has multiple Rietveld analyses"
)
}
Self::UnknownHistogram { histogram_id } => {
write!(
formatter,
"Rietveld analysis references unknown histogram {histogram_id}"
)
}
Self::HistogramStateMismatch { histogram_id } => write!(
formatter,
"Rietveld analysis state differs from histogram {histogram_id}"
),
Self::PhaseOrderMismatch { histogram_id } => write!(
formatter,
"Rietveld phase order differs from histogram {histogram_id} references"
),
Self::ExternalProviderRequired { phase_id } => write!(
formatter,
"phase {phase_id} requires an unavailable external provider"
),
Self::OpaqueStaticContributions { phase_id } => write!(
formatter,
"phase {phase_id} has opaque static sample-physics contributions"
),
Self::PhaseStateMismatch { phase_id } => {
write!(
formatter,
"Rietveld phase {phase_id} differs from project state"
)
}
}
}
}
impl Error for RietveldProjectError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Domain(error) => Some(error),
Self::Rietveld(error) => Some(error),
Self::Parameter(error) => Some(error),
Self::Constraint(error) => Some(error),
Self::Options(error) => Some(error),
Self::General(error) => Some(error),
_ => None,
}
}
}
macro_rules! from_error {
($source:ty, $variant:ident) => {
impl From<$source> for RietveldProjectError {
fn from(error: $source) -> Self {
Self::$variant(error)
}
}
};
}
from_error!(DomainError, Domain);
from_error!(crate::RietveldError, Rietveld);
from_error!(RietveldGeneralParameterError, Parameter);
from_error!(ConstraintError, Constraint);
from_error!(RietveldRefinementError, Options);
from_error!(RietveldGeneralRefinementError, General);