#[must_use]
pub fn gibbs_formation(formula: &str) -> Option<f64> {
kimiya::lookup_thermochem(formula).map(|d| d.delta_gf_kj)
}
#[must_use]
pub fn enthalpy_formation(formula: &str) -> Option<f64> {
kimiya::lookup_thermochem(formula).map(|d| d.delta_hf_kj)
}
#[must_use]
pub fn standard_entropy(formula: &str) -> Option<f64> {
kimiya::lookup_thermochem(formula).map(|d| d.s_standard_j)
}
#[must_use]
pub fn gibbs_at_temperature(delta_h_kj: f64, delta_s_j_per_k: f64, temperature_k: f64) -> f64 {
delta_h_kj - temperature_k * delta_s_j_per_k / 1000.0
}
#[must_use]
pub fn stable_polymorph<'a>(
phase_a: &'a str,
phase_b: &'a str,
temperature_k: f64,
) -> Option<&'a str> {
let a = kimiya::lookup_thermochem(phase_a)?;
let b = kimiya::lookup_thermochem(phase_b)?;
let g_a = gibbs_at_temperature(a.delta_hf_kj, a.s_standard_j, temperature_k);
let g_b = gibbs_at_temperature(b.delta_hf_kj, b.s_standard_j, temperature_k);
if g_a <= g_b {
Some(phase_a)
} else {
Some(phase_b)
}
}
#[must_use]
pub fn reaction_gibbs(products: &[(&str, f64)], reactants: &[(&str, f64)]) -> Option<f64> {
let sum_products: f64 = products
.iter()
.map(|(f, n)| gibbs_formation(f).map(|g| n * g))
.collect::<Option<Vec<_>>>()?
.into_iter()
.sum();
let sum_reactants: f64 = reactants
.iter()
.map(|(f, n)| gibbs_formation(f).map(|g| n * g))
.collect::<Option<Vec<_>>>()?
.into_iter()
.sum();
Some(sum_products - sum_reactants)
}
#[must_use]
pub fn is_reaction_spontaneous(
products: &[(&str, f64)],
reactants: &[(&str, f64)],
) -> Option<bool> {
reaction_gibbs(products, reactants).map(|dg| dg < 0.0)
}
#[must_use]
pub fn equilibrium_temperature(products: &[(&str, f64)], reactants: &[(&str, f64)]) -> Option<f64> {
let dh = reaction_enthalpy(products, reactants)?;
let ds = reaction_entropy(products, reactants)?;
if ds.abs() < 1e-10 {
return None; }
let t_eq = (dh * 1000.0) / ds; if t_eq > 0.0 { Some(t_eq) } else { None }
}
#[must_use]
pub fn reaction_enthalpy(products: &[(&str, f64)], reactants: &[(&str, f64)]) -> Option<f64> {
let sum_p: f64 = products
.iter()
.map(|(f, n)| enthalpy_formation(f).map(|h| n * h))
.collect::<Option<Vec<_>>>()?
.into_iter()
.sum();
let sum_r: f64 = reactants
.iter()
.map(|(f, n)| enthalpy_formation(f).map(|h| n * h))
.collect::<Option<Vec<_>>>()?
.into_iter()
.sum();
Some(sum_p - sum_r)
}
#[must_use]
pub fn reaction_entropy(products: &[(&str, f64)], reactants: &[(&str, f64)]) -> Option<f64> {
let sum_p: f64 = products
.iter()
.map(|(f, n)| standard_entropy(f).map(|s| n * s))
.collect::<Option<Vec<_>>>()?
.into_iter()
.sum();
let sum_r: f64 = reactants
.iter()
.map(|(f, n)| standard_entropy(f).map(|s| n * s))
.collect::<Option<Vec<_>>>()?
.into_iter()
.sum();
Some(sum_p - sum_r)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quartz_gibbs_negative() {
let g = gibbs_formation("SiO2(s)").unwrap();
assert!(g < 0.0, "SiO₂ should have negative ΔG°_f, got {g}");
}
#[test]
fn calcite_gibbs_negative() {
let g = gibbs_formation("CaCO3(s)").unwrap();
assert!(g < 0.0);
}
#[test]
fn unknown_formula_returns_none() {
assert!(gibbs_formation("unobtanium").is_none());
}
#[test]
fn gibbs_at_temperature_standard() {
let g = gibbs_at_temperature(-100.0, 50.0, 298.15);
assert!((g - (-114.9075)).abs() < 0.01);
}
#[test]
fn calcite_decomposition_not_spontaneous_at_room_temp() {
let spontaneous =
is_reaction_spontaneous(&[("CaO(s)", 1.0), ("CO2(g)", 1.0)], &[("CaCO3(s)", 1.0)]);
assert_eq!(spontaneous, Some(false));
}
#[test]
fn calcite_decomposition_equilibrium_temperature() {
let t_eq =
equilibrium_temperature(&[("CaO(s)", 1.0), ("CO2(g)", 1.0)], &[("CaCO3(s)", 1.0)]);
assert!(t_eq.is_some());
let t = t_eq.unwrap();
assert!(
t > 800.0 && t < 1400.0,
"CaCO₃ decomposition T_eq should be ~1100K, got {t}"
);
}
#[test]
fn iron_oxide_formation_spontaneous() {
let spontaneous =
is_reaction_spontaneous(&[("Fe2O3(s)", 1.0)], &[("Fe(s)", 2.0), ("O2(g)", 1.5)]);
assert_eq!(spontaneous, Some(true));
}
#[test]
fn reaction_gibbs_with_missing_formula() {
let result = reaction_gibbs(&[("unobtanium", 1.0)], &[("SiO2(s)", 1.0)]);
assert!(result.is_none());
}
}