phasesmith_workflows/
quantitative.rs1use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6#[derive(Clone, Debug, PartialEq)]
8pub struct QuantitativePhase {
9 pub phase_id: String,
11 pub scale: f64,
13 pub formula_units_per_cell: f64,
15 pub formula_mass_g_mol: f64,
17 pub cell_volume_angstrom3: f64,
19}
20
21impl QuantitativePhase {
22 pub fn new(
29 phase_id: impl Into<String>,
30 scale: f64,
31 formula_units_per_cell: f64,
32 formula_mass_g_mol: f64,
33 cell_volume_angstrom3: f64,
34 ) -> Result<Self, QuantitativeError> {
35 let result = Self {
36 phase_id: phase_id.into(),
37 scale,
38 formula_units_per_cell,
39 formula_mass_g_mol,
40 cell_volume_angstrom3,
41 };
42 result.validate()?;
43 Ok(result)
44 }
45
46 fn validate(&self) -> Result<(), QuantitativeError> {
47 if self.phase_id.is_empty() {
48 return Err(QuantitativeError::EmptyPhaseId);
49 }
50 if !self.scale.is_finite() || self.scale < 0.0 {
51 return Err(QuantitativeError::InvalidScale);
52 }
53 if [
54 self.formula_units_per_cell,
55 self.formula_mass_g_mol,
56 self.cell_volume_angstrom3,
57 ]
58 .into_iter()
59 .any(|value| !value.is_finite() || value <= 0.0)
60 {
61 return Err(QuantitativeError::InvalidMetadata);
62 }
63 Ok(())
64 }
65}
66
67#[derive(Clone, Debug, PartialEq)]
69pub struct PhaseWeightFraction {
70 pub phase_id: String,
72 pub weight_fraction: f64,
74}
75
76pub fn quantitative_phase_analysis(
85 phases: &[QuantitativePhase],
86) -> Result<Vec<PhaseWeightFraction>, QuantitativeError> {
87 if phases.is_empty() {
88 return Err(QuantitativeError::EmptyPhases);
89 }
90 let mut identities = std::collections::BTreeSet::new();
91 let mut contributions = Vec::with_capacity(phases.len());
92 for phase in phases {
93 phase.validate()?;
94 if !identities.insert(&phase.phase_id) {
95 return Err(QuantitativeError::DuplicatePhaseId);
96 }
97 let contribution = phase.scale
98 * phase.formula_units_per_cell
99 * phase.formula_mass_g_mol
100 * phase.cell_volume_angstrom3;
101 if !contribution.is_finite() {
102 return Err(QuantitativeError::ContributionOverflow);
103 }
104 contributions.push(contribution);
105 }
106 let total = contributions.iter().sum::<f64>();
107 if !total.is_finite() || total <= 0.0 {
108 return Err(QuantitativeError::ZeroTotal);
109 }
110 Ok(phases
111 .iter()
112 .zip(contributions)
113 .map(|(phase, contribution)| PhaseWeightFraction {
114 phase_id: phase.phase_id.clone(),
115 weight_fraction: contribution / total,
116 })
117 .collect())
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub enum QuantitativeError {
123 EmptyPhases,
125 EmptyPhaseId,
127 DuplicatePhaseId,
129 InvalidScale,
131 InvalidMetadata,
133 ContributionOverflow,
135 ZeroTotal,
137}
138
139impl Display for QuantitativeError {
140 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
141 formatter.write_str(match self {
142 Self::EmptyPhases => "quantitative phase analysis requires at least one phase",
143 Self::EmptyPhaseId => "quantitative phase IDs must not be empty",
144 Self::DuplicatePhaseId => "quantitative phase IDs must be unique",
145 Self::InvalidScale => "quantitative phase scales must be finite and non-negative",
146 Self::InvalidMetadata => "quantitative Z, mass, and volume must be positive and finite",
147 Self::ContributionOverflow => "quantitative phase contribution overflowed",
148 Self::ZeroTotal => "at least one quantitative phase scale must be positive",
149 })
150 }
151}
152
153impl Error for QuantitativeError {}