use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Clone, Debug, PartialEq)]
pub struct QuantitativePhase {
pub phase_id: String,
pub scale: f64,
pub formula_units_per_cell: f64,
pub formula_mass_g_mol: f64,
pub cell_volume_angstrom3: f64,
}
impl QuantitativePhase {
pub fn new(
phase_id: impl Into<String>,
scale: f64,
formula_units_per_cell: f64,
formula_mass_g_mol: f64,
cell_volume_angstrom3: f64,
) -> Result<Self, QuantitativeError> {
let result = Self {
phase_id: phase_id.into(),
scale,
formula_units_per_cell,
formula_mass_g_mol,
cell_volume_angstrom3,
};
result.validate()?;
Ok(result)
}
fn validate(&self) -> Result<(), QuantitativeError> {
if self.phase_id.is_empty() {
return Err(QuantitativeError::EmptyPhaseId);
}
if !self.scale.is_finite() || self.scale < 0.0 {
return Err(QuantitativeError::InvalidScale);
}
if [
self.formula_units_per_cell,
self.formula_mass_g_mol,
self.cell_volume_angstrom3,
]
.into_iter()
.any(|value| !value.is_finite() || value <= 0.0)
{
return Err(QuantitativeError::InvalidMetadata);
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PhaseWeightFraction {
pub phase_id: String,
pub weight_fraction: f64,
}
pub fn quantitative_phase_analysis(
phases: &[QuantitativePhase],
) -> Result<Vec<PhaseWeightFraction>, QuantitativeError> {
if phases.is_empty() {
return Err(QuantitativeError::EmptyPhases);
}
let mut identities = std::collections::BTreeSet::new();
let mut contributions = Vec::with_capacity(phases.len());
for phase in phases {
phase.validate()?;
if !identities.insert(&phase.phase_id) {
return Err(QuantitativeError::DuplicatePhaseId);
}
let contribution = phase.scale
* phase.formula_units_per_cell
* phase.formula_mass_g_mol
* phase.cell_volume_angstrom3;
if !contribution.is_finite() {
return Err(QuantitativeError::ContributionOverflow);
}
contributions.push(contribution);
}
let total = contributions.iter().sum::<f64>();
if !total.is_finite() || total <= 0.0 {
return Err(QuantitativeError::ZeroTotal);
}
Ok(phases
.iter()
.zip(contributions)
.map(|(phase, contribution)| PhaseWeightFraction {
phase_id: phase.phase_id.clone(),
weight_fraction: contribution / total,
})
.collect())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QuantitativeError {
EmptyPhases,
EmptyPhaseId,
DuplicatePhaseId,
InvalidScale,
InvalidMetadata,
ContributionOverflow,
ZeroTotal,
}
impl Display for QuantitativeError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::EmptyPhases => "quantitative phase analysis requires at least one phase",
Self::EmptyPhaseId => "quantitative phase IDs must not be empty",
Self::DuplicatePhaseId => "quantitative phase IDs must be unique",
Self::InvalidScale => "quantitative phase scales must be finite and non-negative",
Self::InvalidMetadata => "quantitative Z, mass, and volume must be positive and finite",
Self::ContributionOverflow => "quantitative phase contribution overflowed",
Self::ZeroTotal => "at least one quantitative phase scale must be positive",
})
}
}
impl Error for QuantitativeError {}