Skip to main content

phasesmith_workflows/
rietveld_project.rs

1//! Validated project-level ownership for runnable native Rietveld analyses.
2
3use 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/// One complete runnable single-histogram native Rietveld analysis.
18#[derive(Clone, Debug, PartialEq)]
19pub struct RietveldAnalysis {
20    /// Histogram owned by the surrounding project.
21    pub histogram_id: RecordId,
22    /// Editable calculation state synchronized with the project snapshot.
23    pub input: RietveldInput,
24    /// Complete selected parameter families.
25    pub selection: RietveldParameterSelection,
26    /// Optional lattice bounds aligned with `input.phases`.
27    pub lattice_bounds: Vec<Option<LatticeBounds>>,
28    /// Ordered physical constraint graph.
29    pub constraints: Vec<Constraint>,
30    /// Numerical and bounded-runtime controls.
31    pub options: RietveldRefinementOptions,
32    /// Optional final covariance controls.
33    pub covariance: RietveldCovarianceOptions,
34    /// Optional last accepted continuation state.
35    pub checkpoint: Option<RietveldGeneralCheckpoint>,
36}
37
38impl RietveldAnalysis {
39    /// Validate the complete standalone solver contract.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`RietveldProjectError`] for invalid input, selection, bounds,
44    /// constraints, options, or checkpoint state.
45    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/// Revisioned native project plus zero or one runnable analysis per histogram.
85#[derive(Clone, Debug, PartialEq)]
86pub struct RietveldProjectState {
87    /// Application-neutral multi-histogram project snapshot.
88    pub project: ProjectRecord,
89    /// Runnable native analyses in stable caller-owned order.
90    pub analyses: Vec<RietveldAnalysis>,
91}
92
93impl RietveldProjectState {
94    /// Validate the project and every cross-record Rietveld reference.
95    ///
96    /// # Errors
97    ///
98    /// Returns [`RietveldProjectError`] for invalid project state, duplicate or
99    /// missing histogram ownership, unsupported radiation/providers, or a
100    /// solver request that differs from its project records.
101    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/// Invalid project-level native Rietveld state.
178#[derive(Debug)]
179pub enum RietveldProjectError {
180    /// Application-neutral project validation failed.
181    Domain(DomainError),
182    /// Rietveld input validation failed.
183    Rietveld(crate::RietveldError),
184    /// Selection or parameter-layout validation failed.
185    Parameter(RietveldGeneralParameterError),
186    /// Constraint graph validation failed.
187    Constraint(ConstraintError),
188    /// Solver-control validation failed.
189    Options(RietveldRefinementError),
190    /// Checkpoint or covariance validation failed.
191    General(RietveldGeneralRefinementError),
192    /// Lattice bounds do not align with phase order.
193    LatticeBoundCountMismatch,
194    /// Initial physical values do not satisfy the constraint graph.
195    UnsatisfiedConstraint,
196    /// Saved iteration limits cannot resume the accepted checkpoint.
197    CheckpointExceedsIterationLimit,
198    /// More than one analysis owns one histogram.
199    DuplicateAnalysis {
200        /// Duplicated histogram identity.
201        histogram_id: RecordId,
202    },
203    /// Analysis references a missing histogram.
204    UnknownHistogram {
205        /// Missing histogram identity.
206        histogram_id: RecordId,
207    },
208    /// Pattern or experiment state differs from the project histogram.
209    HistogramStateMismatch {
210        /// Inconsistent histogram identity.
211        histogram_id: RecordId,
212    },
213    /// Active phase order differs from the histogram references.
214    PhaseOrderMismatch {
215        /// Inconsistent histogram identity.
216        histogram_id: RecordId,
217    },
218    /// Native analysis cannot execute a project phase requiring an extension.
219    ExternalProviderRequired {
220        /// Phase requiring the extension.
221        phase_id: RecordId,
222    },
223    /// Phase carries opaque static contribution arrays with no native model.
224    OpaqueStaticContributions {
225        /// Phase with non-reconstructible contributions.
226        phase_id: RecordId,
227    },
228    /// Analysis phase label or structural definition differs from the project.
229    PhaseStateMismatch {
230        /// Inconsistent phase identity.
231        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);