Skip to main content

phasesmith_workflows/
tof_project.rs

1//! Validated project-level ownership for runnable fixed-instrument 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::{TofLeBailCheckpoint, TofLeBailError, TofLeBailInput, TofLeBailOptions};
10
11/// One complete runnable fixed-instrument TOF Le Bail analysis.
12#[derive(Clone, Debug, PartialEq)]
13pub struct TofLeBailAnalysis {
14    /// TOF histogram owned by the surrounding project.
15    pub histogram_id: RecordId,
16    /// Editable fixed-instrument extraction state.
17    pub input: TofLeBailInput,
18    /// Numerical and execution controls, including total cycle budget.
19    pub options: TofLeBailOptions,
20    /// Optional last accepted continuation state.
21    pub checkpoint: Option<TofLeBailCheckpoint>,
22}
23
24impl TofLeBailAnalysis {
25    /// Validate the standalone workflow and optional checkpoint contract.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`TofProjectError`] for invalid input, options, or continuation.
30    pub fn validate(&self) -> Result<(), TofProjectError> {
31        self.input.validate()?;
32        self.options.validate()?;
33        if let Some(checkpoint) = &self.checkpoint {
34            checkpoint.validate_for(&self.input, &self.options)?;
35        }
36        Ok(())
37    }
38}
39
40/// Revisioned native project plus zero or one TOF analysis per TOF histogram.
41#[derive(Clone, Debug, PartialEq)]
42pub struct TofLeBailProjectState {
43    /// Application-neutral mixed CW/TOF project snapshot.
44    pub project: ProjectRecord,
45    /// Runnable TOF analyses in stable caller-owned order.
46    pub analyses: Vec<TofLeBailAnalysis>,
47}
48
49impl TofLeBailProjectState {
50    /// Validate project records and every TOF analysis reference.
51    ///
52    /// # Errors
53    ///
54    /// Returns [`TofProjectError`] for duplicate/missing ownership, mismatched
55    /// pattern/instrument/phase state, or an invalid workflow checkpoint.
56    pub fn validate(&self) -> Result<(), TofProjectError> {
57        self.project.validate()?;
58        let mut histogram_ids = BTreeSet::new();
59        for analysis in &self.analyses {
60            analysis.validate()?;
61            if !histogram_ids.insert(analysis.histogram_id.clone()) {
62                return Err(TofProjectError::DuplicateAnalysis {
63                    histogram_id: analysis.histogram_id.clone(),
64                });
65            }
66            let histogram = self
67                .project
68                .tof_histograms
69                .iter()
70                .find(|item| item.histogram_id == analysis.histogram_id)
71                .ok_or_else(|| TofProjectError::UnknownHistogram {
72                    histogram_id: analysis.histogram_id.clone(),
73                })?;
74            if analysis.input.pattern != histogram.pattern
75                || analysis.input.instrument != histogram.experiment.instrument
76            {
77                return Err(TofProjectError::HistogramStateMismatch {
78                    histogram_id: analysis.histogram_id.clone(),
79                });
80            }
81            let phase_ids = analysis
82                .input
83                .phases
84                .iter()
85                .map(crate::TofLeBailPhase::phase_id)
86                .collect::<Vec<_>>();
87            if phase_ids != histogram.phase_ids.iter().collect::<Vec<_>>() {
88                return Err(TofProjectError::PhaseOrderMismatch {
89                    histogram_id: analysis.histogram_id.clone(),
90                });
91            }
92            for phase in &analysis.input.phases {
93                let stored = self
94                    .project
95                    .phases
96                    .iter()
97                    .find(|item| &item.phase_id == phase.phase_id())
98                    .ok_or_else(|| TofProjectError::PhaseOrderMismatch {
99                        histogram_id: analysis.histogram_id.clone(),
100                    })?;
101                if stored.name != phase.name() {
102                    return Err(TofProjectError::PhaseStateMismatch {
103                        phase_id: stored.phase_id.clone(),
104                    });
105                }
106            }
107        }
108        Ok(())
109    }
110}
111
112/// Invalid project-level fixed-instrument TOF state.
113#[derive(Debug)]
114pub enum TofProjectError {
115    /// Application-neutral project validation failed.
116    Domain(DomainError),
117    /// TOF input, options, or checkpoint validation failed.
118    Workflow(TofLeBailError),
119    /// More than one analysis owns one TOF histogram.
120    DuplicateAnalysis {
121        /// Duplicated histogram identity.
122        histogram_id: RecordId,
123    },
124    /// Analysis references a missing TOF histogram.
125    UnknownHistogram {
126        /// Missing histogram identity.
127        histogram_id: RecordId,
128    },
129    /// Pattern or instrument differs from the project TOF histogram.
130    HistogramStateMismatch {
131        /// Inconsistent histogram identity.
132        histogram_id: RecordId,
133    },
134    /// Active phase order differs from the histogram references.
135    PhaseOrderMismatch {
136        /// Inconsistent histogram identity.
137        histogram_id: RecordId,
138    },
139    /// Analysis phase label differs from the project phase record.
140    PhaseStateMismatch {
141        /// Inconsistent phase identity.
142        phase_id: RecordId,
143    },
144}
145
146impl Display for TofProjectError {
147    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
148        match self {
149            Self::Domain(error) => Display::fmt(error, formatter),
150            Self::Workflow(error) => Display::fmt(error, formatter),
151            Self::DuplicateAnalysis { histogram_id } => {
152                write!(
153                    formatter,
154                    "duplicate TOF analysis for histogram {histogram_id}"
155                )
156            }
157            Self::UnknownHistogram { histogram_id } => {
158                write!(formatter, "unknown TOF histogram {histogram_id}")
159            }
160            Self::HistogramStateMismatch { histogram_id } => write!(
161                formatter,
162                "TOF analysis state differs from histogram {histogram_id}"
163            ),
164            Self::PhaseOrderMismatch { histogram_id } => write!(
165                formatter,
166                "TOF analysis phase order differs from histogram {histogram_id}"
167            ),
168            Self::PhaseStateMismatch { phase_id } => {
169                write!(formatter, "TOF analysis phase state differs for {phase_id}")
170            }
171        }
172    }
173}
174
175impl Error for TofProjectError {
176    fn source(&self) -> Option<&(dyn Error + 'static)> {
177        match self {
178            Self::Domain(error) => Some(error),
179            Self::Workflow(error) => Some(error),
180            _ => None,
181        }
182    }
183}
184
185impl From<DomainError> for TofProjectError {
186    fn from(value: DomainError) -> Self {
187        Self::Domain(value)
188    }
189}
190
191impl From<TofLeBailError> for TofProjectError {
192    fn from(value: TofLeBailError) -> Self {
193        Self::Workflow(value)
194    }
195}