1use std::collections::BTreeSet;
4use std::error::Error;
5use std::fmt::{Display, Formatter};
6
7use phasesmith_core::OwnedCwContributions;
8use phasesmith_model::{DomainError, ProjectRecord, RadiationDefinition, RecordId};
9
10use crate::{
11 Constraint, ConstraintError, ConstraintTransform, LatticeBounds, RietveldCovarianceOptions,
12 RietveldGeneralCheckpoint, RietveldGeneralParameterError, RietveldGeneralRefinementError,
13 RietveldInput, RietveldParameterLayout, RietveldParameterSelection, RietveldRefinementError,
14 RietveldRefinementOptions,
15};
16
17#[derive(Clone, Debug, PartialEq)]
19pub struct RietveldAnalysis {
20 pub histogram_id: RecordId,
22 pub input: RietveldInput,
24 pub selection: RietveldParameterSelection,
26 pub lattice_bounds: Vec<Option<LatticeBounds>>,
28 pub constraints: Vec<Constraint>,
30 pub options: RietveldRefinementOptions,
32 pub covariance: RietveldCovarianceOptions,
34 pub checkpoint: Option<RietveldGeneralCheckpoint>,
36}
37
38impl RietveldAnalysis {
39 pub fn validate(&self) -> Result<(), RietveldProjectError> {
46 self.input.validate()?;
47 self.selection.validate()?;
48 self.options.validate()?;
49 RietveldCovarianceOptions::new(
50 self.covariance.enabled,
51 self.covariance.max_parameters,
52 self.covariance.unresolved_correlation,
53 )?;
54 if self.lattice_bounds.len() != self.input.phases.len() {
55 return Err(RietveldProjectError::LatticeBoundCountMismatch);
56 }
57 let layout =
58 RietveldParameterLayout::new(&self.input, &self.selection, &self.lattice_bounds)?;
59 let transform =
60 ConstraintTransform::new(layout.parameters().clone(), self.constraints.clone())?;
61 let constrained = transform.unpack(&transform.pack()?, false)?;
62 if layout.parameters().specs().iter().any(|spec| {
63 constrained
64 .get(spec.key())
65 .is_none_or(|value| (value - spec.value()).abs() > 2.0e-12)
66 }) {
67 return Err(RietveldProjectError::UnsatisfiedConstraint);
68 }
69 if let Some(checkpoint) = &self.checkpoint {
70 if checkpoint.completed_iterations > self.options.limits.max_iterations() {
71 return Err(RietveldProjectError::CheckpointExceedsIterationLimit);
72 }
73 checkpoint.validate_for(
74 &self.input,
75 &self.selection,
76 &self.lattice_bounds,
77 &self.constraints,
78 )?;
79 }
80 Ok(())
81 }
82}
83
84#[derive(Clone, Debug, PartialEq)]
86pub struct RietveldProjectState {
87 pub project: ProjectRecord,
89 pub analyses: Vec<RietveldAnalysis>,
91}
92
93impl RietveldProjectState {
94 pub fn validate(&self) -> Result<(), RietveldProjectError> {
102 self.project.validate()?;
103 let mut histogram_ids = BTreeSet::new();
104 for analysis in &self.analyses {
105 analysis.validate()?;
106 if !histogram_ids.insert(analysis.histogram_id.clone()) {
107 return Err(RietveldProjectError::DuplicateAnalysis {
108 histogram_id: analysis.histogram_id.clone(),
109 });
110 }
111 let histogram = self
112 .project
113 .histograms
114 .iter()
115 .find(|item| item.histogram_id == analysis.histogram_id)
116 .ok_or_else(|| RietveldProjectError::UnknownHistogram {
117 histogram_id: analysis.histogram_id.clone(),
118 })?;
119 let expected_spectrum = match &histogram.experiment.radiation {
120 RadiationDefinition::Monochromatic { .. } => None,
121 RadiationDefinition::FixedSpectrum { spectrum, .. } => Some(spectrum),
122 };
123 if analysis.input.pattern != histogram.pattern
124 || analysis.input.instrument != histogram.experiment.instrument
125 || analysis.input.fixed_spectrum.as_ref() != expected_spectrum
126 || analysis.input.axial_geometry != histogram.experiment.axial_geometry
127 || analysis.input.position_correction != histogram.experiment.position_correction
128 {
129 return Err(RietveldProjectError::HistogramStateMismatch {
130 histogram_id: analysis.histogram_id.clone(),
131 });
132 }
133 let analysis_phase_ids = analysis
134 .input
135 .phases
136 .iter()
137 .map(crate::RietveldPhase::phase_id)
138 .collect::<Vec<_>>();
139 if analysis_phase_ids != histogram.phase_ids.iter().collect::<Vec<_>>() {
140 return Err(RietveldProjectError::PhaseOrderMismatch {
141 histogram_id: analysis.histogram_id.clone(),
142 });
143 }
144 for phase in &analysis.input.phases {
145 let stored = self
146 .project
147 .phases
148 .iter()
149 .find(|item| &item.phase_id == phase.phase_id())
150 .ok_or_else(|| RietveldProjectError::PhaseOrderMismatch {
151 histogram_id: analysis.histogram_id.clone(),
152 })?;
153 if !stored.required_providers.is_empty() {
154 return Err(RietveldProjectError::ExternalProviderRequired {
155 phase_id: stored.phase_id.clone(),
156 });
157 }
158 if phase.sample_physics().is_none()
159 && phase.contributions()
160 != &OwnedCwContributions::neutral(phase.reflection_ids().len())
161 {
162 return Err(RietveldProjectError::OpaqueStaticContributions {
163 phase_id: stored.phase_id.clone(),
164 });
165 }
166 if stored.name != phase.name() || stored.definition != *phase.definition() {
167 return Err(RietveldProjectError::PhaseStateMismatch {
168 phase_id: stored.phase_id.clone(),
169 });
170 }
171 }
172 }
173 Ok(())
174 }
175}
176
177#[derive(Debug)]
179pub enum RietveldProjectError {
180 Domain(DomainError),
182 Rietveld(crate::RietveldError),
184 Parameter(RietveldGeneralParameterError),
186 Constraint(ConstraintError),
188 Options(RietveldRefinementError),
190 General(RietveldGeneralRefinementError),
192 LatticeBoundCountMismatch,
194 UnsatisfiedConstraint,
196 CheckpointExceedsIterationLimit,
198 DuplicateAnalysis {
200 histogram_id: RecordId,
202 },
203 UnknownHistogram {
205 histogram_id: RecordId,
207 },
208 HistogramStateMismatch {
210 histogram_id: RecordId,
212 },
213 PhaseOrderMismatch {
215 histogram_id: RecordId,
217 },
218 ExternalProviderRequired {
220 phase_id: RecordId,
222 },
223 OpaqueStaticContributions {
225 phase_id: RecordId,
227 },
228 PhaseStateMismatch {
230 phase_id: RecordId,
232 },
233}
234
235impl Display for RietveldProjectError {
236 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
237 match self {
238 Self::Domain(error) => Display::fmt(error, formatter),
239 Self::Rietveld(error) => Display::fmt(error, formatter),
240 Self::Parameter(error) => Display::fmt(error, formatter),
241 Self::Constraint(error) => Display::fmt(error, formatter),
242 Self::Options(error) => Display::fmt(error, formatter),
243 Self::General(error) => Display::fmt(error, formatter),
244 Self::LatticeBoundCountMismatch => {
245 formatter.write_str("Rietveld lattice bounds must align with phase order")
246 }
247 Self::UnsatisfiedConstraint => {
248 formatter.write_str("initial Rietveld values do not satisfy their constraints")
249 }
250 Self::CheckpointExceedsIterationLimit => {
251 formatter.write_str("Rietveld checkpoint exceeds the saved maximum iteration limit")
252 }
253 Self::DuplicateAnalysis { histogram_id } => {
254 write!(
255 formatter,
256 "histogram {histogram_id} has multiple Rietveld analyses"
257 )
258 }
259 Self::UnknownHistogram { histogram_id } => {
260 write!(
261 formatter,
262 "Rietveld analysis references unknown histogram {histogram_id}"
263 )
264 }
265 Self::HistogramStateMismatch { histogram_id } => write!(
266 formatter,
267 "Rietveld analysis state differs from histogram {histogram_id}"
268 ),
269 Self::PhaseOrderMismatch { histogram_id } => write!(
270 formatter,
271 "Rietveld phase order differs from histogram {histogram_id} references"
272 ),
273 Self::ExternalProviderRequired { phase_id } => write!(
274 formatter,
275 "phase {phase_id} requires an unavailable external provider"
276 ),
277 Self::OpaqueStaticContributions { phase_id } => write!(
278 formatter,
279 "phase {phase_id} has opaque static sample-physics contributions"
280 ),
281 Self::PhaseStateMismatch { phase_id } => {
282 write!(
283 formatter,
284 "Rietveld phase {phase_id} differs from project state"
285 )
286 }
287 }
288 }
289}
290
291impl Error for RietveldProjectError {
292 fn source(&self) -> Option<&(dyn Error + 'static)> {
293 match self {
294 Self::Domain(error) => Some(error),
295 Self::Rietveld(error) => Some(error),
296 Self::Parameter(error) => Some(error),
297 Self::Constraint(error) => Some(error),
298 Self::Options(error) => Some(error),
299 Self::General(error) => Some(error),
300 _ => None,
301 }
302 }
303}
304
305macro_rules! from_error {
306 ($source:ty, $variant:ident) => {
307 impl From<$source> for RietveldProjectError {
308 fn from(error: $source) -> Self {
309 Self::$variant(error)
310 }
311 }
312 };
313}
314
315from_error!(DomainError, Domain);
316from_error!(crate::RietveldError, Rietveld);
317from_error!(RietveldGeneralParameterError, Parameter);
318from_error!(ConstraintError, Constraint);
319from_error!(RietveldRefinementError, Options);
320from_error!(RietveldGeneralRefinementError, General);