use crate::element::GAS_CONSTANT;
use crate::error::{KimiyaError, Result};
use serde::Serialize;
#[inline]
pub fn raoult_pressure(mole_fraction: f64, pure_vapor_pressure: f64) -> Result<f64> {
if !(0.0..=1.0).contains(&mole_fraction) {
return Err(KimiyaError::InvalidInput(
"mole fraction must be between 0 and 1".into(),
));
}
if pure_vapor_pressure < 0.0 {
return Err(KimiyaError::InvalidInput(
"vapor pressure must be non-negative".into(),
));
}
Ok(mole_fraction * pure_vapor_pressure)
}
#[inline]
pub fn raoult_total_binary(x_a: f64, p_pure_a: f64, p_pure_b: f64) -> Result<f64> {
if !(0.0..=1.0).contains(&x_a) {
return Err(KimiyaError::InvalidInput(
"mole fraction must be between 0 and 1".into(),
));
}
Ok(x_a * p_pure_a + (1.0 - x_a) * p_pure_b)
}
#[inline]
pub fn henry_pressure(henry_constant: f64, mole_fraction: f64) -> Result<f64> {
if henry_constant <= 0.0 {
return Err(KimiyaError::InvalidInput(
"Henry's constant must be positive".into(),
));
}
if mole_fraction < 0.0 {
return Err(KimiyaError::InvalidInput(
"mole fraction must be non-negative".into(),
));
}
Ok(henry_constant * mole_fraction)
}
#[inline]
pub fn henry_solubility(partial_pressure: f64, henry_constant: f64) -> Result<f64> {
if henry_constant <= 0.0 {
return Err(KimiyaError::InvalidInput(
"Henry's constant must be positive".into(),
));
}
Ok(partial_pressure / henry_constant)
}
#[must_use]
#[inline]
pub fn boiling_point_elevation(kb: f64, molality: f64, van_t_hoff_factor: f64) -> f64 {
kb * molality * van_t_hoff_factor
}
#[must_use]
#[inline]
pub fn freezing_point_depression(kf: f64, molality: f64, van_t_hoff_factor: f64) -> f64 {
kf * molality * van_t_hoff_factor
}
#[inline]
pub fn osmotic_pressure(van_t_hoff_factor: f64, molarity: f64, temperature_k: f64) -> Result<f64> {
if temperature_k <= 0.0 {
return Err(KimiyaError::InvalidTemperature(
"temperature must be positive".into(),
));
}
Ok(van_t_hoff_factor * molarity * GAS_CONSTANT * 1000.0 * temperature_k)
}
pub const KB_WATER: f64 = 0.512;
pub const KB_BENZENE: f64 = 2.53;
pub const KB_ETHANOL: f64 = 1.22;
pub const KB_CHLOROFORM: f64 = 3.63;
pub const KF_WATER: f64 = 1.86;
pub const KF_BENZENE: f64 = 5.12;
pub const KF_CYCLOHEXANE: f64 = 20.0;
pub const KF_CAMPHOR: f64 = 40.0;
pub fn clausius_clapeyron(p1: f64, t1_k: f64, t2_k: f64, delta_h_vap_j: f64) -> Result<f64> {
if p1 <= 0.0 {
return Err(KimiyaError::InvalidInput(
"reference pressure must be positive".into(),
));
}
if t1_k <= 0.0 || t2_k <= 0.0 {
return Err(KimiyaError::InvalidTemperature(
"temperatures must be positive".into(),
));
}
let exponent = -(delta_h_vap_j / GAS_CONSTANT) * (1.0 / t2_k - 1.0 / t1_k);
Ok(p1 * exponent.exp())
}
#[derive(Debug, Clone, Serialize)]
pub struct AntoineCoeffs {
pub a: f64,
pub b: f64,
pub c: f64,
pub t_min_c: f64,
pub t_max_c: f64,
}
#[must_use]
#[inline]
pub fn antoine_pressure_mmhg(coeffs: &AntoineCoeffs, temperature_c: f64) -> f64 {
10.0_f64.powf(coeffs.a - coeffs.b / (coeffs.c + temperature_c))
}
#[must_use]
#[inline]
pub fn antoine_pressure_pa(coeffs: &AntoineCoeffs, temperature_c: f64) -> f64 {
antoine_pressure_mmhg(coeffs, temperature_c) * 133.322
}
pub static ANTOINE_DATA: &[(&str, AntoineCoeffs)] = &[
(
"water",
AntoineCoeffs {
a: 8.07131,
b: 1730.63,
c: 233.426,
t_min_c: 1.0,
t_max_c: 100.0,
},
),
(
"ethanol",
AntoineCoeffs {
a: 8.20417,
b: 1642.89,
c: 230.300,
t_min_c: -57.0,
t_max_c: 80.0,
},
),
(
"methanol",
AntoineCoeffs {
a: 8.08097,
b: 1582.27,
c: 239.700,
t_min_c: -14.0,
t_max_c: 65.0,
},
),
(
"benzene",
AntoineCoeffs {
a: 6.90565,
b: 1211.03,
c: 220.790,
t_min_c: 8.0,
t_max_c: 80.0,
},
),
(
"acetone",
AntoineCoeffs {
a: 7.02447,
b: 1161.00,
c: 224.000,
t_min_c: -20.0,
t_max_c: 77.0,
},
),
(
"toluene",
AntoineCoeffs {
a: 6.95464,
b: 1344.80,
c: 219.480,
t_min_c: 6.0,
t_max_c: 137.0,
},
),
(
"diethyl_ether",
AntoineCoeffs {
a: 6.92032,
b: 1064.07,
c: 228.800,
t_min_c: -60.0,
t_max_c: 35.0,
},
),
(
"chloroform",
AntoineCoeffs {
a: 6.95465,
b: 1170.97,
c: 226.232,
t_min_c: -10.0,
t_max_c: 60.0,
},
),
];
#[must_use]
#[inline]
pub fn lookup_antoine(name: &str) -> Option<&'static AntoineCoeffs> {
ANTOINE_DATA
.iter()
.find(|(n, _)| *n == name)
.map(|(_, c)| c)
}
#[inline]
pub fn phase_rule(components: u32, phases: u32) -> Result<u32> {
let f = components as i32 - phases as i32 + 2;
if f < 0 {
return Err(KimiyaError::InvalidInput(
"too many phases for the number of components".into(),
));
}
Ok(f as u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn raoult_basic() {
let p = raoult_pressure(0.8, 101325.0).unwrap();
assert!((p - 81060.0).abs() < 1.0);
}
#[test]
fn raoult_pure_component() {
let p = raoult_pressure(1.0, 101325.0).unwrap();
assert!((p - 101325.0).abs() < f64::EPSILON);
}
#[test]
fn raoult_invalid_fraction() {
assert!(raoult_pressure(1.5, 101325.0).is_err());
assert!(raoult_pressure(-0.1, 101325.0).is_err());
}
#[test]
fn raoult_binary_total() {
let p = raoult_total_binary(0.6, 100.0, 50.0).unwrap();
assert!((p - 80.0).abs() < f64::EPSILON);
}
#[test]
fn henry_basic() {
let x = henry_solubility(21278.0, 4.3e9).unwrap();
assert!(x > 4e-6 && x < 6e-6);
}
#[test]
fn henry_roundtrip() {
let kh = 1e8;
let x = 0.001;
let p = henry_pressure(kh, x).unwrap();
let back = henry_solubility(p, kh).unwrap();
assert!((back - x).abs() < 1e-15);
}
#[test]
fn bp_elevation_water() {
let dt = boiling_point_elevation(KB_WATER, 1.0, 2.0);
assert!((dt - 1.024).abs() < 0.001);
}
#[test]
fn fp_depression_water() {
let dt = freezing_point_depression(KF_WATER, 1.0, 1.0);
assert!((dt - 1.86).abs() < f64::EPSILON);
}
#[test]
fn osmotic_pressure_basic() {
let pi = osmotic_pressure(2.0, 0.1, 298.15).unwrap();
assert!(pi > 400_000.0 && pi < 600_000.0);
}
#[test]
fn osmotic_pressure_zero_temp_is_error() {
assert!(osmotic_pressure(1.0, 0.1, 0.0).is_err());
}
#[test]
fn clausius_clapeyron_water() {
let p2 = clausius_clapeyron(101325.0, 373.15, 363.15, 40670.0).unwrap();
assert!(
p2 > 60_000.0 && p2 < 80_000.0,
"water at 90°C should be ~70 kPa, got {p2}"
);
}
#[test]
fn clausius_clapeyron_same_temp() {
let p2 = clausius_clapeyron(101325.0, 373.15, 373.15, 40670.0).unwrap();
assert!((p2 - 101325.0).abs() < 1.0);
}
#[test]
fn clausius_clapeyron_zero_temp_is_error() {
assert!(clausius_clapeyron(101325.0, 0.0, 373.15, 40670.0).is_err());
}
#[test]
fn antoine_water_100c() {
let c = lookup_antoine("water").unwrap();
let p = antoine_pressure_mmhg(c, 100.0);
assert!(
(p - 760.0).abs() < 10.0,
"water at 100°C should be ~760 mmHg, got {p}"
);
}
#[test]
fn antoine_water_pa() {
let c = lookup_antoine("water").unwrap();
let p = antoine_pressure_pa(c, 100.0);
assert!(
(p - 101325.0).abs() < 2000.0,
"water at 100°C should be ~101 kPa, got {p}"
);
}
#[test]
fn antoine_ethanol_78c() {
let c = lookup_antoine("ethanol").unwrap();
let p = antoine_pressure_mmhg(c, 78.4);
assert!(
(p - 760.0).abs() < 30.0,
"ethanol at bp should be ~760 mmHg, got {p}"
);
}
#[test]
fn antoine_lookup_nonexistent() {
assert!(lookup_antoine("unobtainium").is_none());
}
#[test]
fn phase_rule_water_triple_point() {
let f = phase_rule(1, 3).unwrap();
assert_eq!(f, 0);
}
#[test]
fn phase_rule_water_liquid_vapor() {
let f = phase_rule(1, 2).unwrap();
assert_eq!(f, 1);
}
#[test]
fn phase_rule_single_phase() {
let f = phase_rule(1, 1).unwrap();
assert_eq!(f, 2);
}
#[test]
fn phase_rule_too_many_phases() {
assert!(phase_rule(1, 4).is_err());
}
}