use crate::Thermodynamics::ChemEquilibrium::equilibrium_activity::PhaseActivityModel;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_ids::ReactionId;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_log_moles::GibbsFn;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_nonlinear::ReactionExtentError;
use crate::Thermodynamics::ChemEquilibrium::equilibrium_problem::{
EquilibriumConditions, PreparedEquilibriumProblem,
};
use crate::Thermodynamics::ChemEquilibrium::equilibrium_reaction_basis::{
ReactionBasisTolerances, ValidatedReactionBasis,
};
pub const MOLAR_GAS_CONSTANT: f64 = 8.314_462_618_153_24;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EquilibriumConstantActivityModel {
IdealGas,
}
pub struct EquilibriumConstantProblem {
basis: ValidatedReactionBasis,
initial_moles: Vec<f64>,
standard_gibbs: Vec<GibbsFn>,
conditions: EquilibriumConditions,
activity_model: EquilibriumConstantActivityModel,
}
impl EquilibriumConstantProblem {
pub fn from_prepared_ideal_gas(
prepared: &PreparedEquilibriumProblem,
basis_tolerances: ReactionBasisTolerances,
) -> Result<Self, ReactionExtentError> {
let source = prepared.problem();
if source.phases().len() != 1
|| !matches!(source.phases()[0].kind, PhaseActivityModel::IdealGas)
{
return Err(invalid_problem(
"independent validation currently supports exactly one ideal-gas phase",
));
}
let basis = ValidatedReactionBasis::new(
source.species().to_vec(),
source.element_composition(),
prepared.reaction_basis().reactions.clone(),
prepared.reaction_basis().rank,
basis_tolerances,
)?;
Self::new(
basis,
source.initial_moles().to_vec(),
source.gibbs().to_vec(),
source.conditions(),
EquilibriumConstantActivityModel::IdealGas,
)
}
pub fn new(
basis: ValidatedReactionBasis,
initial_moles: Vec<f64>,
standard_gibbs: Vec<GibbsFn>,
conditions: EquilibriumConditions,
activity_model: EquilibriumConstantActivityModel,
) -> Result<Self, ReactionExtentError> {
let species_count = basis.species().len();
if initial_moles.len() != species_count || standard_gibbs.len() != species_count {
return Err(invalid_problem(format!(
"basis has {species_count} species, initial moles has {}, and Gibbs functions has {}",
initial_moles.len(),
standard_gibbs.len()
)));
}
if initial_moles
.iter()
.any(|value| !value.is_finite() || *value < 0.0)
{
return Err(invalid_problem(
"initial mole numbers must be finite and non-negative",
));
}
if initial_moles.iter().sum::<f64>() <= 0.0 {
return Err(invalid_problem(
"at least one initial mole number must be positive",
));
}
Ok(Self {
basis,
initial_moles,
standard_gibbs,
conditions,
activity_model,
})
}
pub fn basis(&self) -> &ValidatedReactionBasis {
&self.basis
}
pub fn initial_moles(&self) -> &[f64] {
&self.initial_moles
}
pub fn conditions(&self) -> EquilibriumConditions {
self.conditions
}
pub fn activity_model(&self) -> EquilibriumConstantActivityModel {
self.activity_model
}
pub fn standard_gibbs(&self) -> &[GibbsFn] {
&self.standard_gibbs
}
pub fn standard_reaction_gibbs(
&self,
reaction: ReactionId,
) -> Result<f64, ReactionExtentError> {
let coefficients = self.basis.reaction(reaction)?;
let temperature = self.conditions.temperature();
coefficients
.iter()
.zip(&self.standard_gibbs)
.enumerate()
.try_fold(0.0, |sum, (species, (nu, gibbs))| {
let value = gibbs(temperature);
if !value.is_finite() {
return Err(ReactionExtentError::InvalidDG0 {
species_index: species,
dg0: value,
temperature,
});
}
Ok(sum + nu * value)
})
}
pub fn ln_equilibrium_constant(
&self,
reaction: ReactionId,
) -> Result<f64, ReactionExtentError> {
Ok(-self.standard_reaction_gibbs(reaction)?
/ (MOLAR_GAS_CONSTANT * self.conditions.temperature()))
}
pub fn ln_equilibrium_constants(&self) -> Result<Vec<f64>, ReactionExtentError> {
(0..self.basis.reaction_count())
.map(|index| {
let id = self.basis.reaction_id(index)?;
self.ln_equilibrium_constant(id)
})
.collect()
}
pub fn ln_reaction_quotient(
&self,
reaction: ReactionId,
moles: &[f64],
) -> Result<f64, ReactionExtentError> {
let log_activities = self.log_activities(moles)?;
let coefficients = self.basis.reaction(reaction)?;
Ok(coefficients
.iter()
.zip(log_activities)
.map(|(nu, ln_activity)| nu * ln_activity)
.sum())
}
pub fn equilibrium_residuals(&self, moles: &[f64]) -> Result<Vec<f64>, ReactionExtentError> {
(0..self.basis.reaction_count())
.map(|index| {
let reaction = self.basis.reaction_id(index)?;
Ok(self.ln_reaction_quotient(reaction, moles)?
- self.ln_equilibrium_constant(reaction)?)
})
.collect()
}
fn log_activities(&self, moles: &[f64]) -> Result<Vec<f64>, ReactionExtentError> {
if moles.len() != self.basis.species().len() {
return Err(invalid_problem(format!(
"candidate has {} mole numbers for {} species",
moles.len(),
self.basis.species().len()
)));
}
if moles
.iter()
.any(|value| !value.is_finite() || *value <= 0.0)
{
return Err(invalid_problem(
"reaction quotients require finite, strictly positive candidate moles",
));
}
let total: f64 = moles.iter().sum();
if !total.is_finite() || total <= 0.0 {
return Err(invalid_problem(
"candidate total moles must be finite and positive",
));
}
match self.activity_model {
EquilibriumConstantActivityModel::IdealGas => {
let pressure_ratio =
self.conditions.pressure() / self.conditions.reference_pressure();
Ok(moles
.iter()
.map(|moles_i| (moles_i / total * pressure_ratio).ln())
.collect())
}
}
}
}
fn invalid_problem(message: impl Into<String>) -> ReactionExtentError {
ReactionExtentError::InvalidProblem {
field: "equilibrium_constant_problem",
message: message.into(),
}
}