use crate::Thermodynamics::ChemEquilibrium::equilibrium_log_moles::compute_species_moles;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_nonlinear::ReactionExtentError;
use nalgebra::DMatrix;
use std::cmp::Ordering;
#[derive(Debug, Clone, Copy)]
pub struct EquilibriumCandidateResiduals<'a> {
pub log_moles: &'a [f64],
pub raw_residual: &'a [f64],
pub acceptance_residual: &'a [f64],
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EquilibriumAcceptanceCriteria {
pub residual_tolerance: f64,
pub element_balance_tolerance: f64,
pub reaction_affinity_tolerance: f64,
}
impl EquilibriumAcceptanceCriteria {
pub fn new(
residual_tolerance: f64,
element_balance_tolerance: f64,
reaction_affinity_tolerance: f64,
) -> Result<Self, ReactionExtentError> {
if !residual_tolerance.is_finite()
|| residual_tolerance < 0.0
|| !element_balance_tolerance.is_finite()
|| element_balance_tolerance < 0.0
|| !reaction_affinity_tolerance.is_finite()
|| reaction_affinity_tolerance < 0.0
{
return Err(ReactionExtentError::InvalidProblem {
field: "candidate_tolerances",
message: "candidate tolerances must be finite and non-negative".to_string(),
});
}
Ok(Self {
residual_tolerance,
element_balance_tolerance,
reaction_affinity_tolerance,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct EquilibriumCandidateReport {
pub residual_l2_norm: f64,
pub residual_rms: f64,
pub max_abs_residual: f64,
pub raw_residual_l2_norm: f64,
pub raw_residual_rms: f64,
pub raw_max_abs_residual: f64,
pub max_abs_element_balance_error: f64,
pub reaction_affinity_l2_norm: f64,
pub max_abs_reaction_affinity: f64,
pub min_moles: f64,
}
pub fn compare_candidate_reports(
lhs: &EquilibriumCandidateReport,
rhs: &EquilibriumCandidateReport,
) -> Ordering {
lhs.residual_l2_norm
.total_cmp(&rhs.residual_l2_norm)
.then_with(|| lhs.max_abs_residual.total_cmp(&rhs.max_abs_residual))
.then_with(|| {
lhs.raw_residual_l2_norm
.total_cmp(&rhs.raw_residual_l2_norm)
})
.then_with(|| {
lhs.raw_max_abs_residual
.total_cmp(&rhs.raw_max_abs_residual)
})
.then_with(|| {
lhs.max_abs_element_balance_error
.total_cmp(&rhs.max_abs_element_balance_error)
})
.then_with(|| rhs.min_moles.total_cmp(&lhs.min_moles))
}
pub fn select_preferred_candidate_index(
candidates: &[EquilibriumCandidateReport],
) -> Option<usize> {
let mut best_index = None;
for (index, candidate) in candidates.iter().enumerate() {
match best_index {
None => best_index = Some(index),
Some(best) => {
if compare_candidate_reports(candidate, &candidates[best]) == Ordering::Less {
best_index = Some(index);
}
}
}
}
best_index
}
pub fn validate_equilibrium_candidate(
residuals: EquilibriumCandidateResiduals<'_>,
criteria: EquilibriumAcceptanceCriteria,
element_composition: &DMatrix<f64>,
element_totals: &[f64],
) -> Result<EquilibriumCandidateReport, ReactionExtentError> {
if element_composition.nrows() != residuals.log_moles.len()
|| element_composition.ncols() != element_totals.len()
{
return Err(ReactionExtentError::DimensionMismatch(format!(
"candidate has {} species, element matrix is {}x{}, and totals have {} entries",
residuals.log_moles.len(),
element_composition.nrows(),
element_composition.ncols(),
element_totals.len(),
)));
}
if residuals.raw_residual.len() != residuals.log_moles.len()
|| residuals.acceptance_residual.len() != residuals.log_moles.len()
{
return Err(ReactionExtentError::DimensionMismatch(format!(
"candidate has {} log-moles, raw residual has {}, and acceptance residual has {} entries",
residuals.log_moles.len(),
residuals.raw_residual.len(),
residuals.acceptance_residual.len(),
)));
}
if residuals.log_moles.iter().any(|value| !value.is_finite()) {
return Err(ReactionExtentError::InvalidCandidate {
field: "candidate_log_moles",
message: "candidate contains a non-finite log-mole value".to_string(),
});
}
if residuals
.raw_residual
.iter()
.any(|value| !value.is_finite())
{
return Err(ReactionExtentError::InvalidCandidate {
field: "candidate_raw_residual",
message: "candidate raw residual contains a non-finite value".to_string(),
});
}
if residuals
.acceptance_residual
.iter()
.any(|value| !value.is_finite())
{
return Err(ReactionExtentError::InvalidCandidate {
field: "candidate_acceptance_residual",
message: "candidate acceptance residual contains a non-finite value".to_string(),
});
}
if element_totals.iter().any(|value| !value.is_finite()) {
return Err(ReactionExtentError::InvalidProblem {
field: "element_totals",
message: "element totals must be finite".to_string(),
});
}
let moles = compute_species_moles(residuals.log_moles)?;
let reaction_count = residuals
.log_moles
.len()
.checked_sub(element_composition.ncols())
.ok_or_else(|| {
ReactionExtentError::DimensionMismatch(format!(
"candidate has {} species but {} element columns; the canonical system is not square",
residuals.log_moles.len(),
element_composition.ncols(),
))
})?;
if reaction_count > residuals.acceptance_residual.len() {
return Err(ReactionExtentError::DimensionMismatch(format!(
"candidate reaction block has {} rows but acceptance residual has {} entries",
reaction_count,
residuals.acceptance_residual.len(),
)));
}
let reaction_affinity_block = &residuals.acceptance_residual[..reaction_count];
let residual_l2_norm = residuals
.acceptance_residual
.iter()
.map(|value| value * value)
.sum::<f64>()
.sqrt();
let residual_rms = residual_l2_norm / (residuals.acceptance_residual.len() as f64).sqrt();
let max_abs_residual = residuals
.acceptance_residual
.iter()
.map(|value| value.abs())
.fold(0.0_f64, f64::max);
if residual_l2_norm > criteria.residual_tolerance {
return Err(ReactionExtentError::InvalidCandidate {
field: "candidate_acceptance_residual",
message: format!(
"residual L2 norm {residual_l2_norm:e} exceeds tolerance {:e}",
criteria.residual_tolerance
),
});
}
let raw_residual_l2_norm = residuals
.raw_residual
.iter()
.map(|value| value * value)
.sum::<f64>()
.sqrt();
let raw_residual_rms = raw_residual_l2_norm / (residuals.raw_residual.len() as f64).sqrt();
let raw_max_abs_residual = residuals
.raw_residual
.iter()
.map(|value| value.abs())
.fold(0.0_f64, f64::max);
let mut max_abs_element_balance_error = 0.0_f64;
for element in 0..element_composition.ncols() {
let reconstructed_total = (0..moles.len())
.map(|species| element_composition[(species, element)] * moles[species])
.sum::<f64>();
let error = (reconstructed_total - element_totals[element]).abs();
if !error.is_finite() || error > criteria.element_balance_tolerance {
return Err(ReactionExtentError::InvalidCandidate {
field: "candidate_element_balance",
message: format!(
"element {element} balance error {error:e} exceeds tolerance {:e}",
criteria.element_balance_tolerance
),
});
}
max_abs_element_balance_error = max_abs_element_balance_error.max(error);
}
let reaction_affinity_l2_norm = reaction_affinity_block
.iter()
.map(|value| value * value)
.sum::<f64>()
.sqrt();
let max_abs_reaction_affinity = reaction_affinity_block
.iter()
.map(|value| value.abs())
.fold(0.0_f64, f64::max);
if reaction_affinity_l2_norm > criteria.reaction_affinity_tolerance {
return Err(ReactionExtentError::InvalidCandidate {
field: "candidate_reaction_affinity",
message: format!(
"reaction-affinity L2 norm {reaction_affinity_l2_norm:e} exceeds tolerance {:e}",
criteria.reaction_affinity_tolerance
),
});
}
Ok(EquilibriumCandidateReport {
residual_l2_norm,
residual_rms,
max_abs_residual,
raw_residual_l2_norm,
raw_residual_rms,
raw_max_abs_residual,
max_abs_element_balance_error,
reaction_affinity_l2_norm,
max_abs_reaction_affinity,
min_moles: moles.into_iter().fold(f64::INFINITY, f64::min),
})
}