Skip to main content

phasesmith_workflows/
tof_structural_project.rs

1//! Validated project ownership for structural multi-bank neutron TOF analyses.
2
3use std::collections::BTreeSet;
4use std::error::Error;
5use std::fmt::{Display, Formatter};
6
7use phasesmith_model::{DomainError, ProjectRecord, RecordId};
8
9use crate::{
10    StructuralTofMultiBankCheckpoint, StructuralTofMultiBankError, StructuralTofMultiBankInput,
11    StructuralTofMultiBankRefinementError, StructuralTofMultiBankRefinementOptions,
12};
13
14/// One complete runnable structural multi-bank TOF analysis.
15#[derive(Clone, Debug, PartialEq)]
16pub struct StructuralTofMultiBankAnalysis {
17    /// Stable analysis identity independent of its member histogram IDs.
18    pub analysis_id: RecordId,
19    /// Editable shared structure and bank-local observation/model state.
20    pub input: StructuralTofMultiBankInput,
21    /// Numerical and bounded-runtime controls.
22    pub options: StructuralTofMultiBankRefinementOptions,
23    /// Optional complete last-accepted continuation state.
24    pub checkpoint: Option<StructuralTofMultiBankCheckpoint>,
25}
26
27impl StructuralTofMultiBankAnalysis {
28    /// Validate the workflow and optional checkpoint contract.
29    ///
30    /// # Errors
31    ///
32    /// Returns [`StructuralTofProjectError`] for invalid numerical state.
33    pub fn validate(&self) -> Result<(), StructuralTofProjectError> {
34        self.input.validate()?;
35        self.options.validate()?;
36        if let Some(checkpoint) = &self.checkpoint {
37            checkpoint.validate_for(&self.input)?;
38        }
39        Ok(())
40    }
41}
42
43/// Revisioned project plus disjoint structural multi-bank TOF analyses.
44#[derive(Clone, Debug, PartialEq)]
45pub struct StructuralTofMultiBankProjectState {
46    /// Application-neutral mixed CW/TOF project snapshot.
47    pub project: ProjectRecord,
48    /// Runnable structural TOF analyses in stable caller-owned order.
49    pub analyses: Vec<StructuralTofMultiBankAnalysis>,
50}
51
52impl StructuralTofMultiBankProjectState {
53    /// Validate project ownership, exact histogram/phase state, and checkpoints.
54    ///
55    /// Each TOF histogram may belong to at most one structural analysis. Bank
56    /// IDs are the corresponding project histogram IDs, and every member
57    /// histogram references exactly the one shared structural phase.
58    ///
59    /// # Errors
60    ///
61    /// Returns [`StructuralTofProjectError`] for invalid or inconsistent state.
62    pub fn validate(&self) -> Result<(), StructuralTofProjectError> {
63        self.project.validate()?;
64        let mut analysis_ids = BTreeSet::new();
65        let mut histogram_ids = BTreeSet::new();
66        for analysis in &self.analyses {
67            analysis.validate()?;
68            if !analysis_ids.insert(analysis.analysis_id.clone()) {
69                return Err(StructuralTofProjectError::DuplicateAnalysis {
70                    analysis_id: analysis.analysis_id.clone(),
71                });
72            }
73            let phase_id = analysis.input.phase.phase_id();
74            let stored_phase = self
75                .project
76                .phases
77                .iter()
78                .find(|phase| &phase.phase_id == phase_id)
79                .ok_or_else(|| StructuralTofProjectError::PhaseStateMismatch {
80                    phase_id: phase_id.clone(),
81                })?;
82            if stored_phase.name != analysis.input.phase.name()
83                || stored_phase.definition != *analysis.input.phase.definition()
84                || !stored_phase.required_providers.is_empty()
85            {
86                return Err(StructuralTofProjectError::PhaseStateMismatch {
87                    phase_id: phase_id.clone(),
88                });
89            }
90            for bank in &analysis.input.banks {
91                if !histogram_ids.insert(bank.bank_id.clone()) {
92                    return Err(StructuralTofProjectError::DuplicateHistogramOwnership {
93                        histogram_id: bank.bank_id.clone(),
94                    });
95                }
96                let histogram = self
97                    .project
98                    .tof_histograms
99                    .iter()
100                    .find(|item| item.histogram_id == bank.bank_id)
101                    .ok_or_else(|| StructuralTofProjectError::UnknownHistogram {
102                        histogram_id: bank.bank_id.clone(),
103                    })?;
104                if histogram.pattern != bank.pattern
105                    || histogram.experiment.instrument != bank.instrument
106                {
107                    return Err(StructuralTofProjectError::HistogramStateMismatch {
108                        histogram_id: bank.bank_id.clone(),
109                    });
110                }
111                if histogram.phase_ids.as_slice() != std::slice::from_ref(phase_id) {
112                    return Err(StructuralTofProjectError::PhaseOrderMismatch {
113                        histogram_id: bank.bank_id.clone(),
114                    });
115                }
116            }
117        }
118        Ok(())
119    }
120}
121
122/// Invalid project-level structural TOF state.
123#[derive(Debug)]
124pub enum StructuralTofProjectError {
125    /// Application-neutral project validation failed.
126    Domain(DomainError),
127    /// Structural TOF input failed validation.
128    Workflow(StructuralTofMultiBankError),
129    /// Structural TOF solver/checkpoint state failed validation.
130    Refinement(StructuralTofMultiBankRefinementError),
131    /// More than one analysis has the same stable identity.
132    DuplicateAnalysis {
133        /// Duplicated analysis identity.
134        analysis_id: RecordId,
135    },
136    /// A TOF histogram is assigned to multiple structural analyses.
137    DuplicateHistogramOwnership {
138        /// Multiply owned histogram identity.
139        histogram_id: RecordId,
140    },
141    /// Analysis references a missing TOF histogram.
142    UnknownHistogram {
143        /// Missing histogram identity.
144        histogram_id: RecordId,
145    },
146    /// Bank pattern or initial instrument differs from its histogram record.
147    HistogramStateMismatch {
148        /// Inconsistent histogram identity.
149        histogram_id: RecordId,
150    },
151    /// Histogram phase references are not exactly the shared analysis phase.
152    PhaseOrderMismatch {
153        /// Inconsistent histogram identity.
154        histogram_id: RecordId,
155    },
156    /// Analysis phase identity, label, definition, or provider contract differs.
157    PhaseStateMismatch {
158        /// Inconsistent phase identity.
159        phase_id: RecordId,
160    },
161}
162
163impl Display for StructuralTofProjectError {
164    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
165        match self {
166            Self::Domain(error) => Display::fmt(error, formatter),
167            Self::Workflow(error) => Display::fmt(error, formatter),
168            Self::Refinement(error) => Display::fmt(error, formatter),
169            Self::DuplicateAnalysis { analysis_id } => {
170                write!(formatter, "duplicate structural TOF analysis {analysis_id}")
171            }
172            Self::DuplicateHistogramOwnership { histogram_id } => write!(
173                formatter,
174                "TOF histogram {histogram_id} belongs to multiple structural analyses"
175            ),
176            Self::UnknownHistogram { histogram_id } => {
177                write!(formatter, "unknown TOF histogram {histogram_id}")
178            }
179            Self::HistogramStateMismatch { histogram_id } => write!(
180                formatter,
181                "structural TOF bank state differs from histogram {histogram_id}"
182            ),
183            Self::PhaseOrderMismatch { histogram_id } => write!(
184                formatter,
185                "structural TOF phase order differs from histogram {histogram_id}"
186            ),
187            Self::PhaseStateMismatch { phase_id } => {
188                write!(
189                    formatter,
190                    "structural TOF phase state differs for {phase_id}"
191                )
192            }
193        }
194    }
195}
196
197impl Error for StructuralTofProjectError {
198    fn source(&self) -> Option<&(dyn Error + 'static)> {
199        match self {
200            Self::Domain(error) => Some(error),
201            Self::Workflow(error) => Some(error),
202            Self::Refinement(error) => Some(error),
203            _ => None,
204        }
205    }
206}
207
208impl From<DomainError> for StructuralTofProjectError {
209    fn from(value: DomainError) -> Self {
210        Self::Domain(value)
211    }
212}
213
214impl From<StructuralTofMultiBankError> for StructuralTofProjectError {
215    fn from(value: StructuralTofMultiBankError) -> Self {
216        Self::Workflow(value)
217    }
218}
219
220impl From<StructuralTofMultiBankRefinementError> for StructuralTofProjectError {
221    fn from(value: StructuralTofMultiBankRefinementError) -> Self {
222        Self::Refinement(value)
223    }
224}