Skip to main content

phasesmith_workflows/
tof_multibank_project.rs

1//! Validated project ownership for joint multi-bank TOF geometry 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    LatticeParameterization, TofMultiBankGeometryCheckpoint, TofMultiBankGeometryError,
11    TofMultiBankGeometryInput, TofMultiBankGeometryOptions,
12};
13
14/// One complete runnable joint multi-bank TOF geometry analysis.
15#[derive(Clone, Debug, PartialEq)]
16pub struct TofMultiBankGeometryAnalysis {
17    /// Stable analysis identity independent of its member histogram IDs.
18    pub analysis_id: RecordId,
19    /// Editable bank, shared-cell, and bank-local instrument state.
20    pub input: TofMultiBankGeometryInput,
21    /// Numerical and bounded-runtime controls.
22    pub options: TofMultiBankGeometryOptions,
23    /// Optional complete last-accepted continuation state.
24    pub checkpoint: Option<TofMultiBankGeometryCheckpoint>,
25}
26
27impl TofMultiBankGeometryAnalysis {
28    /// Validate the workflow and optional checkpoint contract.
29    ///
30    /// # Errors
31    ///
32    /// Returns [`TofMultiBankProjectError`] for invalid numerical state.
33    pub fn validate(&self) -> Result<(), TofMultiBankProjectError> {
34        self.input.validate()?;
35        self.options.validate()?;
36        if let Some(checkpoint) = &self.checkpoint {
37            checkpoint.validate_for(&self.input, &self.options)?;
38        }
39        Ok(())
40    }
41}
42
43/// Revisioned project plus disjoint joint multi-bank TOF geometry analyses.
44#[derive(Clone, Debug, PartialEq)]
45pub struct TofMultiBankGeometryProjectState {
46    /// Application-neutral mixed CW/TOF project snapshot.
47    pub project: ProjectRecord,
48    /// Runnable joint analyses in stable caller-owned order.
49    pub analyses: Vec<TofMultiBankGeometryAnalysis>,
50}
51
52impl TofMultiBankGeometryProjectState {
53    /// Validate project ownership, exact histogram state, cells, and checkpoints.
54    ///
55    /// Each TOF histogram may belong to at most one joint analysis. Bank IDs are
56    /// the corresponding project histogram IDs; this prevents a detached alias
57    /// from silently resolving to the wrong observed data.
58    ///
59    /// # Errors
60    ///
61    /// Returns [`TofMultiBankProjectError`] for invalid or inconsistent state.
62    pub fn validate(&self) -> Result<(), TofMultiBankProjectError> {
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(TofMultiBankProjectError::DuplicateAnalysis {
70                    analysis_id: analysis.analysis_id.clone(),
71                });
72            }
73            for bank in &analysis.input.lattice.multibank.banks {
74                if !histogram_ids.insert(bank.bank_id.clone()) {
75                    return Err(TofMultiBankProjectError::DuplicateHistogramOwnership {
76                        histogram_id: bank.bank_id.clone(),
77                    });
78                }
79                let histogram = self
80                    .project
81                    .tof_histograms
82                    .iter()
83                    .find(|item| item.histogram_id == bank.bank_id)
84                    .ok_or_else(|| TofMultiBankProjectError::UnknownHistogram {
85                        histogram_id: bank.bank_id.clone(),
86                    })?;
87                if bank.input.pattern != histogram.pattern
88                    || bank.input.instrument != histogram.experiment.instrument
89                {
90                    return Err(TofMultiBankProjectError::HistogramStateMismatch {
91                        histogram_id: bank.bank_id.clone(),
92                    });
93                }
94                let phase_ids = bank
95                    .input
96                    .phases
97                    .iter()
98                    .map(crate::TofLeBailPhase::phase_id)
99                    .collect::<Vec<_>>();
100                if phase_ids != histogram.phase_ids.iter().collect::<Vec<_>>() {
101                    return Err(TofMultiBankProjectError::PhaseOrderMismatch {
102                        histogram_id: bank.bank_id.clone(),
103                    });
104                }
105                for phase in &bank.input.phases {
106                    let stored = self
107                        .project
108                        .phases
109                        .iter()
110                        .find(|item| &item.phase_id == phase.phase_id())
111                        .ok_or_else(|| TofMultiBankProjectError::PhaseOrderMismatch {
112                            histogram_id: bank.bank_id.clone(),
113                        })?;
114                    if stored.name != phase.name() {
115                        return Err(TofMultiBankProjectError::PhaseStateMismatch {
116                            phase_id: stored.phase_id.clone(),
117                        });
118                    }
119                }
120            }
121            for lattice in &analysis.input.lattice.lattice_phases {
122                let stored = self
123                    .project
124                    .phases
125                    .iter()
126                    .find(|phase| &phase.phase_id == lattice.phase_id())
127                    .ok_or_else(|| TofMultiBankProjectError::PhaseStateMismatch {
128                        phase_id: lattice.phase_id().clone(),
129                    })?;
130                let expected = LatticeParameterization::new(
131                    stored.definition.space_group.clone(),
132                    stored.definition.cell,
133                )?;
134                if lattice.initial_cell() != stored.definition.cell
135                    || lattice.parameterization() != &expected
136                {
137                    return Err(TofMultiBankProjectError::LatticeStateMismatch {
138                        phase_id: lattice.phase_id().clone(),
139                    });
140                }
141            }
142        }
143        Ok(())
144    }
145}
146
147/// Invalid project-level joint multi-bank TOF state.
148#[derive(Debug)]
149pub enum TofMultiBankProjectError {
150    /// Application-neutral project validation failed.
151    Domain(DomainError),
152    /// Joint geometry input, options, or checkpoint validation failed.
153    Workflow(TofMultiBankGeometryError),
154    /// More than one analysis has the same stable identity.
155    DuplicateAnalysis {
156        /// Duplicated analysis identity.
157        analysis_id: RecordId,
158    },
159    /// A TOF histogram is assigned to multiple joint analyses.
160    DuplicateHistogramOwnership {
161        /// Multiply owned histogram identity.
162        histogram_id: RecordId,
163    },
164    /// Analysis references a missing TOF histogram.
165    UnknownHistogram {
166        /// Missing histogram identity.
167        histogram_id: RecordId,
168    },
169    /// Bank pattern or initial instrument differs from its histogram record.
170    HistogramStateMismatch {
171        /// Inconsistent histogram identity.
172        histogram_id: RecordId,
173    },
174    /// Active phase order differs from the histogram references.
175    PhaseOrderMismatch {
176        /// Inconsistent histogram identity.
177        histogram_id: RecordId,
178    },
179    /// Analysis phase label or identity differs from project state.
180    PhaseStateMismatch {
181        /// Inconsistent phase identity.
182        phase_id: RecordId,
183    },
184    /// Shared initial cell or exact symmetry setting differs from project state.
185    LatticeStateMismatch {
186        /// Inconsistent phase identity.
187        phase_id: RecordId,
188    },
189}
190
191impl Display for TofMultiBankProjectError {
192    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
193        match self {
194            Self::Domain(error) => Display::fmt(error, formatter),
195            Self::Workflow(error) => Display::fmt(error, formatter),
196            Self::DuplicateAnalysis { analysis_id } => {
197                write!(formatter, "duplicate joint TOF analysis {analysis_id}")
198            }
199            Self::DuplicateHistogramOwnership { histogram_id } => write!(
200                formatter,
201                "TOF histogram {histogram_id} belongs to multiple joint analyses"
202            ),
203            Self::UnknownHistogram { histogram_id } => {
204                write!(formatter, "unknown TOF histogram {histogram_id}")
205            }
206            Self::HistogramStateMismatch { histogram_id } => write!(
207                formatter,
208                "joint TOF bank state differs from histogram {histogram_id}"
209            ),
210            Self::PhaseOrderMismatch { histogram_id } => write!(
211                formatter,
212                "joint TOF phase order differs from histogram {histogram_id}"
213            ),
214            Self::PhaseStateMismatch { phase_id } => {
215                write!(formatter, "joint TOF phase state differs for {phase_id}")
216            }
217            Self::LatticeStateMismatch { phase_id } => write!(
218                formatter,
219                "joint TOF lattice state differs from project phase {phase_id}"
220            ),
221        }
222    }
223}
224
225impl Error for TofMultiBankProjectError {
226    fn source(&self) -> Option<&(dyn Error + 'static)> {
227        match self {
228            Self::Domain(error) => Some(error),
229            Self::Workflow(error) => Some(error),
230            _ => None,
231        }
232    }
233}
234
235impl From<DomainError> for TofMultiBankProjectError {
236    fn from(value: DomainError) -> Self {
237        Self::Domain(value)
238    }
239}
240
241impl From<TofMultiBankGeometryError> for TofMultiBankProjectError {
242    fn from(value: TofMultiBankGeometryError) -> Self {
243        Self::Workflow(value)
244    }
245}
246
247impl From<crate::LatticeError> for TofMultiBankProjectError {
248    fn from(value: crate::LatticeError) -> Self {
249        Self::Workflow(value.into())
250    }
251}