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,
}
#[derive(Clone, Debug, PartialEq)]
pub struct QuantitativePhaseAnalysis {
pub phases: Vec<PhaseWeightFraction>,
pub covariance: Vec<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())
}
pub fn quantitative_phase_analysis_with_covariance(
phases: &[QuantitativePhase],
scale_covariance: &[f64],
) -> Result<QuantitativePhaseAnalysis, QuantitativeError> {
let fractions = quantitative_phase_analysis(phases)?;
let count = phases.len();
if scale_covariance.len()
!= count
.checked_mul(count)
.ok_or(QuantitativeError::CovarianceShape)?
{
return Err(QuantitativeError::CovarianceShape);
}
if scale_covariance.iter().any(|value| !value.is_finite()) {
return Err(QuantitativeError::NonFiniteCovariance);
}
let factors = phases
.iter()
.map(|phase| {
phase.formula_units_per_cell * phase.formula_mass_g_mol * phase.cell_volume_angstrom3
})
.collect::<Vec<_>>();
let total = phases
.iter()
.zip(&factors)
.map(|(phase, factor)| phase.scale * factor)
.sum::<f64>();
let mut jacobian = vec![0.0; count * count];
for row in 0..count {
for column in 0..count {
jacobian[row * count + column] = (if row == column { factors[row] } else { 0.0 }
- fractions[row].weight_fraction * factors[column])
/ total;
}
}
let mut covariance = vec![0.0; count * count];
for row in 0..count {
for column in 0..count {
let mut value = 0.0;
for left in 0..count {
for right in 0..count {
value += jacobian[row * count + left]
* scale_covariance[left * count + right]
* jacobian[column * count + right];
}
}
if !value.is_finite() {
return Err(QuantitativeError::NonFiniteCovariance);
}
covariance[row * count + column] = value;
}
}
Ok(QuantitativePhaseAnalysis {
phases: fractions,
covariance,
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum QuantitativeError {
EmptyPhases,
EmptyPhaseId,
DuplicatePhaseId,
InvalidScale,
InvalidMetadata,
ContributionOverflow,
ZeroTotal,
CovarianceShape,
NonFiniteCovariance,
}
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",
Self::CovarianceShape => "scale covariance must be square with one row per phase",
Self::NonFiniteCovariance => {
"scale covariance and propagated covariance must be finite"
}
})
}
}
impl Error for QuantitativeError {}