#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int))]
pub enum CubicEos {
PR1976 = 0,
RK1949 = 1,
RKS1972 = 2,
VdW1870 = 3,
PRL1997 = 4,
RKSL1997 = 5,
RKSGD1978 = 6,
RP1978 = 7,
Berth1899 = 8,
VdWAda1984 = 9,
VdWVald1989 = 10,
RKSmn1980 = 11,
RKSATmn1995 = 12,
PRATmng1997 = 13,
PRMmn1989 = 14,
PRSV1986 = 15,
VdWOL1998 = 16,
RKOL1998 = 17,
PROL1998 = 18,
SchmidtWenzel = 19,
PatelTeja = 20,
PatelTejaUSB = 21,
}
impl CubicEos {
pub fn is_three_parameter(&self) -> bool {
matches!(
self,
CubicEos::SchmidtWenzel | CubicEos::PatelTeja | CubicEos::PatelTejaUSB
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VaporModel {
IdealGas,
Virial,
Cubic(CubicEos),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LiquidModel {
IdealSolution,
Cubic(CubicEos),
Activity(super::ActivityModel),
ChaoSeader,
GraysonStreed,
BraunK10,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int))]
#[repr(i32)]
pub enum PhaseId {
Vapor = 0,
Liquid = 1,
}
use crate::numerics::cubic::{CubicError, solve_real};
use crate::types::Component;
use num_dual::DualNum;
use thiserror::Error;
#[derive(Debug, Error, PartialEq)]
pub enum EosError {
#[error("cubic solver failed: {0}")]
Cubic(#[from] CubicError),
#[error("no real root above B={big_b:.6e} found for phase {phase:?}")]
NoRootForPhase { phase: PhaseId, big_b: f64 },
#[error("EOS variant {0:?} not yet ported — see M7 sub-milestones")]
NotImplemented(CubicEos),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FamilyConstants {
pub k1: f64,
pub k2: f64,
pub om_a: f64,
pub om_b: f64,
}
pub fn family_constants(eos: CubicEos) -> FamilyConstants {
use CubicEos::*;
match eos {
PR1976 | RP1978 | PRL1997 | PRATmng1997 | PRSV1986 | PROL1998 | PRMmn1989 => {
FamilyConstants {
k1: 2.0,
k2: -1.0,
om_a: 0.457235528921382,
om_b: 0.0777960739038885,
}
}
RKS1972 | RKSL1997 | RKSGD1978 | RK1949 | VdWVald1989 | RKSATmn1995 | RKOL1998
| RKSmn1980 => FamilyConstants {
k1: 1.0,
k2: 0.0,
om_a: 0.427480233540341,
om_b: 0.0866403499649577,
},
VdW1870 | Berth1899 | VdWAda1984 | VdWOL1998 => FamilyConstants {
k1: 0.0,
k2: 0.0,
om_a: 27.0 / 64.0,
om_b: 1.0 / 8.0,
},
SchmidtWenzel | PatelTeja | PatelTejaUSB => FamilyConstants {
k1: 0.0,
k2: 0.0,
om_a: 0.0,
om_b: 0.0,
},
}
}
fn pt_f(w: f64) -> f64 {
0.452413 + 1.30982 * w - 0.295937 * w * w
}
fn pt_xi_c(w: f64) -> f64 {
0.329032 - 0.076799 * w + 0.0211947 * w * w
}
fn pt_om_b(w: f64) -> f64 {
0.08517138 - 0.02640592 * w + 0.00788769 * w * w
}
fn pt_om_a(w: f64) -> f64 {
let xc = pt_xi_c(w);
let ob = pt_om_b(w);
3.0 * xc * xc + 3.0 * (1.0 - 2.0 * xc) * ob + ob * ob + 1.0 - 3.0 * xc
}
fn sw_beta(w: f64) -> f64 {
0.25988221 - 0.02142913 * w + 0.00337143 * w * w
}
fn sw_m0(w: f64) -> f64 {
0.465 + 1.347 * w - 0.528 * w * w
}
fn sw_m(w: f64, tr: f64) -> f64 {
let m0 = sw_m0(w);
if tr <= 1.0 {
let g = 5.0 * tr - 3.0 * m0 - 1.0;
m0 + g * g / 70.0
} else {
let g = 4.0 - 3.0 * m0;
m0 + g * g / 70.0
}
}
fn sw_dm_dtr(w: f64, tr: f64) -> f64 {
if tr <= 1.0 {
let m0 = sw_m0(w);
let g = 5.0 * tr - 3.0 * m0 - 1.0;
2.0 * g * 5.0 / 70.0
} else {
0.0
}
}
fn sw_om_b(w: f64) -> f64 {
let b = sw_beta(w);
b / (3.0 * (1.0 + b * w))
}
fn sw_om_a(w: f64) -> f64 {
let b = sw_beta(w);
let inner = 1.0 - (1.0 - b) / (3.0 * (1.0 + b * w));
inner * inner * inner
}
fn ol_coeffs(eos: CubicEos) -> ([f64; 10], f64) {
use CubicEos::*;
match eos {
VdWOL1998 => (
[
0.33333333,
0.35112597,
0.011287433,
-0.0038485685,
0.00064261934,
-0.000067252383,
0.0000045962725,
-0.00000019990875,
5.0318465e-09,
-5.5827084e-11,
],
-1.02,
),
RKOL1998 => (
[
0.32748,
0.34376954,
0.010596403,
-0.0037538497,
0.00063257197,
-0.000066481,
0.0000045517956,
-0.00000019796921,
4.9748592e-09,
-5.5024228e-11,
],
-1.014,
),
PROL1998 => (
[
0.29803582,
0.015003698,
-0.0047527103,
0.0008036716,
-0.000089548695,
0.0000068691611,
-0.00000036067317,
0.000000012409205,
-2.5222671e-10,
2.2955503e-12,
],
-0.00041,
),
_ => unreachable!("ol_coeffs called for non-OL EOS {eos:?}"),
}
}
fn ol_sumhk(eos: CubicEos, tr: f64, comp: &Component) -> Option<f64> {
let (h, e_exp) = ol_coeffs(eos);
let t = tr * comp.tc;
let pr = crate::saturation::reduced_psat(comp.sat_model, comp, t).ok()?;
let arg = -(pr / tr).ln(); let mut sum = 0.0;
let mut argk = 1.0; for hk in h {
argk *= arg; sum += hk * argk;
}
Some((1.0 + arg).powf(e_exp) * sum)
}
fn ol_sumhk_and_deriv(eos: CubicEos, tr: f64, comp: &Component) -> Option<(f64, f64)> {
let (h, e_exp) = ol_coeffs(eos);
let t = tr * comp.tc;
let pr = crate::saturation::reduced_psat(comp.sat_model, comp, t).ok()?;
let dpsat_dt = crate::saturation::d_psat_dt(comp.sat_model, comp, t).ok()?;
let dpr_dtr = (dpsat_dt / comp.pc) * comp.tc;
let arg = -(pr / tr).ln();
let darg_dtr = 1.0 / tr - dpr_dtr / pr;
let mut psum = 0.0;
let mut dpsum = 0.0;
let mut argk_minus1 = 1.0; for (i, hk) in h.iter().enumerate() {
let k = (i + 1) as f64;
psum += hk * argk_minus1 * arg;
dpsum += hk * k * argk_minus1;
argk_minus1 *= arg;
}
let base = (1.0 + arg).powf(e_exp);
let dbase = e_exp * (1.0 + arg).powf(e_exp - 1.0);
let s = base * psum;
let ds_dtr = (dbase * psum + base * dpsum) * darg_dtr;
Some((s, ds_dtr))
}
pub fn alpha(eos: CubicEos, tr: f64, comp: &Component) -> f64 {
use CubicEos::*;
let w = comp.omega;
match eos {
VdW1870 => 1.0,
RK1949 => 1.0 / tr.sqrt(),
RKS1972 => {
let m = 0.48 + 1.574 * w - 0.176 * w * w;
let s = 1.0 - tr.sqrt();
(1.0 + m * s).powi(2)
}
PR1976 => {
let kappa = 0.37464 + 1.54226 * w - 0.26992 * w * w;
let s = 1.0 - tr.sqrt();
(1.0 + kappa * s).powi(2)
}
Berth1899 => 1.0 / tr,
VdWAda1984 => {
let m = 0.228165 + 1.446486 * w - 0.648552 * w * w;
10f64.powf(m * (1.0 - tr))
}
RKSGD1978 => {
let m = 0.48508 + 1.55171 * w - 0.15613 * w * w;
let s = 1.0 - tr.sqrt();
(1.0 + m * s).powi(2)
}
RKSL1997 => {
let m = 0.478972559 + 1.576809191 * w - 0.187219516 * w * w + 0.020424946 * w * w * w;
let s = 1.0 - tr.sqrt();
(1.0 + m * s).powi(2)
}
RP1978 => {
let m = 0.379642 + 1.48503 * w - 0.164423 * w * w + 0.016666 * w * w * w;
let s = 1.0 - tr.sqrt();
(1.0 + m * s).powi(2)
}
PRL1997 => {
let m = 0.378710697 + 1.487972964 * w - 0.166754831 * w * w + 0.017169486 * w * w * w;
let s = 1.0 - tr.sqrt();
(1.0 + m * s).powi(2)
}
VdWVald1989 => {
let omegac = w * comp.zc;
let m = 0.4745 + (2.7349 + 6.0984 * omegac) * omegac;
let n = 0.0674 + (2.1031 + 3.9512 * omegac) * omegac;
1.0 + (1.0 - tr) * (m + n / tr)
}
RKSmn1980 => {
let (m, n) = (comp.m_polar, comp.n_polar);
1.0 + (1.0 - tr) * (m + n / tr)
}
RKSATmn1995 | PRATmng1997 => {
let (m, n, g) = (comp.m_polar, comp.n_polar, comp.g_polar);
let u = 1.0 - tr;
(u * m * u.abs().powf(g - 1.0) + n * (1.0 / tr - 1.0)).exp()
}
PRMmn1989 => {
let (m, n) = (comp.m_polar, comp.n_polar);
let s = 1.0 - tr.sqrt();
((1.0 - tr) * m + n * s * s).exp()
}
PRSV1986 => {
let r = tr.sqrt();
let kappa0 = 0.378893 + 1.4897153 * w - 0.17131848 * w * w + 0.0196554 * w * w * w;
let kappa = kappa0 + comp.prsv_k1 * (1.0 + r) * (0.7 - tr);
let inner = 1.0 + kappa * (1.0 - r);
inner * inner
}
VdWOL1998 | RKOL1998 | PROL1998 => {
ol_sumhk(eos, tr, comp)
.map(|s| tr * (1.0 + s))
.unwrap_or(f64::NAN)
}
SchmidtWenzel => {
let s = 1.0 - tr.sqrt();
let m = sw_m(w, tr);
(1.0 + m * s).powi(2)
}
PatelTeja | PatelTejaUSB => {
let f = pt_f(w);
let s = 1.0 - tr.sqrt();
(1.0 + f * s).powi(2)
}
}
}
fn sw_m_generic<D: DualNum<f64> + Copy>(w: f64, tr: D) -> D {
let m0 = sw_m0(w);
if tr.re() <= 1.0 {
let g = tr * 5.0 - (3.0 * m0 + 1.0);
g * g * (1.0 / 70.0) + m0
} else {
let g = 4.0 - 3.0 * m0;
D::from(m0 + g * g / 70.0)
}
}
pub fn alpha_generic<D: DualNum<f64> + Copy>(eos: CubicEos, tr: D, comp: &Component) -> D {
use CubicEos::*;
let w = comp.omega;
let soave = |m: f64| -> D {
let s = -tr.sqrt() + 1.0; (s * m + 1.0).powi(2)
};
match eos {
VdW1870 => D::from(1.0),
RK1949 => tr.sqrt().recip(),
RKS1972 => soave(0.48 + 1.574 * w - 0.176 * w * w),
PR1976 => soave(0.37464 + 1.54226 * w - 0.26992 * w * w),
Berth1899 => tr.recip(),
VdWAda1984 => {
let m = 0.228165 + 1.446486 * w - 0.648552 * w * w;
((-tr + 1.0) * (m * std::f64::consts::LN_10)).exp()
}
RKSGD1978 => soave(0.48508 + 1.55171 * w - 0.15613 * w * w),
RKSL1997 => {
soave(0.478972559 + 1.576809191 * w - 0.187219516 * w * w + 0.020424946 * w * w * w)
}
RP1978 => soave(0.379642 + 1.48503 * w - 0.164423 * w * w + 0.016666 * w * w * w),
PRL1997 => {
soave(0.378710697 + 1.487972964 * w - 0.166754831 * w * w + 0.017169486 * w * w * w)
}
VdWVald1989 => {
let omegac = w * comp.zc;
let m = 0.4745 + (2.7349 + 6.0984 * omegac) * omegac;
let n = 0.0674 + (2.1031 + 3.9512 * omegac) * omegac;
(-tr + 1.0) * (tr.recip() * n + m) + 1.0
}
RKSmn1980 => {
let (m, n) = (comp.m_polar, comp.n_polar);
(-tr + 1.0) * (tr.recip() * n + m) + 1.0
}
RKSATmn1995 | PRATmng1997 => {
let (m, n, g) = (comp.m_polar, comp.n_polar, comp.g_polar);
let u = -tr + 1.0;
(u * m * u.abs().powf(g - 1.0) + (tr.recip() - 1.0) * n).exp()
}
PRMmn1989 => {
let (m, n) = (comp.m_polar, comp.n_polar);
let s = -tr.sqrt() + 1.0;
((-tr + 1.0) * m + s * s * n).exp()
}
PRSV1986 => {
let r = tr.sqrt();
let kappa0 = 0.378893 + 1.4897153 * w - 0.17131848 * w * w + 0.0196554 * w * w * w;
let kappa = (r + 1.0) * (-tr + 0.7) * comp.prsv_k1 + kappa0;
let inner = kappa * (-r + 1.0) + 1.0;
inner * inner
}
VdWOL1998 | RKOL1998 | PROL1998 => {
D::from(alpha(eos, tr.re(), comp))
}
SchmidtWenzel => {
let s = -tr.sqrt() + 1.0;
let m = sw_m_generic(w, tr);
(m * s + 1.0).powi(2)
}
PatelTeja | PatelTejaUSB => soave(pt_f(w)),
}
}
pub fn eos_dimensionless_generic<D: DualNum<f64> + Copy>(
eos: CubicEos,
t: D,
p: D,
comp: &Component,
) -> (D, D, D, D) {
let tr = t / comp.tc;
let pr = p / comp.pc;
let a_val = alpha_generic(eos, tr, comp);
if eos.is_three_parameter() {
use CubicEos::*;
let w = comp.omega;
match eos {
PatelTeja | PatelTejaUSB => {
let big_a = a_val * (pt_om_a(w)) * pr / (tr * tr);
let big_b = pr * pt_om_b(w) / tr;
let big_c = pr * (1.0 - 3.0 * pt_xi_c(w)) / tr;
(big_a, big_b, big_b + big_c, -(big_b * big_c))
}
SchmidtWenzel => {
let big_a = a_val * sw_om_a(w) * pr / (tr * tr);
let big_b = pr * sw_om_b(w) / tr;
(
big_a,
big_b,
big_b * (1.0 + 3.0 * w),
big_b * big_b * (-3.0 * w),
)
}
_ => unreachable!("is_three_parameter but not PT/SW"),
}
} else {
let fc = family_constants(eos);
let big_a = a_val * fc.om_a * pr / (tr * tr);
let big_b = pr * fc.om_b / tr;
(big_a, big_b, big_b * fc.k1, big_b * big_b * fc.k2)
}
}
pub fn d_alpha_d_tr(eos: CubicEos, tr: f64, comp: &Component) -> f64 {
use CubicEos::*;
let w = comp.omega;
match eos {
VdW1870 => 0.0,
RK1949 => -0.5 / (tr * tr.sqrt()),
RKS1972 => {
let m = 0.48 + 1.574 * w - 0.176 * w * w;
let s = 1.0 - tr.sqrt();
-m * (1.0 + m * s) / tr.sqrt()
}
PR1976 => {
let kappa = 0.37464 + 1.54226 * w - 0.26992 * w * w;
let s = 1.0 - tr.sqrt();
-kappa * (1.0 + kappa * s) / tr.sqrt()
}
Berth1899 => -1.0 / (tr * tr),
VdWAda1984 => {
let m = 0.228165 + 1.446486 * w - 0.648552 * w * w;
let a = 10f64.powf(m * (1.0 - tr));
-m * std::f64::consts::LN_10 * a
}
RKSGD1978 => {
let m = 0.48508 + 1.55171 * w - 0.15613 * w * w;
let s = 1.0 - tr.sqrt();
-m * (1.0 + m * s) / tr.sqrt()
}
RKSL1997 => {
let m = 0.478972559 + 1.576809191 * w - 0.187219516 * w * w + 0.020424946 * w * w * w;
let s = 1.0 - tr.sqrt();
-m * (1.0 + m * s) / tr.sqrt()
}
RP1978 => {
let m = 0.379642 + 1.48503 * w - 0.164423 * w * w + 0.016666 * w * w * w;
let s = 1.0 - tr.sqrt();
-m * (1.0 + m * s) / tr.sqrt()
}
PRL1997 => {
let m = 0.378710697 + 1.487972964 * w - 0.166754831 * w * w + 0.017169486 * w * w * w;
let s = 1.0 - tr.sqrt();
-m * (1.0 + m * s) / tr.sqrt()
}
VdWVald1989 => {
let omegac = w * comp.zc;
let m = 0.4745 + (2.7349 + 6.0984 * omegac) * omegac;
let n = 0.0674 + (2.1031 + 3.9512 * omegac) * omegac;
-m - n / (tr * tr)
}
RKSmn1980 => {
let (m, n) = (comp.m_polar, comp.n_polar);
-m - n / (tr * tr)
}
RKSATmn1995 | PRATmng1997 => {
let (m, n, g) = (comp.m_polar, comp.n_polar, comp.g_polar);
let u = 1.0 - tr;
let a = (u * m * u.abs().powf(g - 1.0) + n * (1.0 / tr - 1.0)).exp();
a * (-m * g * u.abs().powf(g - 1.0) - n / (tr * tr))
}
PRMmn1989 => {
let (m, n) = (comp.m_polar, comp.n_polar);
let r = tr.sqrt();
let s = 1.0 - r;
let a = ((1.0 - tr) * m + n * s * s).exp();
a * (-m - n * s / r)
}
PRSV1986 => {
let r = tr.sqrt();
let kappa0 = 0.378893 + 1.4897153 * w - 0.17131848 * w * w + 0.0196554 * w * w * w;
let kappa = kappa0 + comp.prsv_k1 * (1.0 + r) * (0.7 - tr);
let inner = 1.0 + kappa * (1.0 - r);
let dkappa = comp.prsv_k1 * ((0.7 - tr) / (2.0 * r) - (1.0 + r));
let dinner = dkappa * (1.0 - r) - kappa / (2.0 * r);
2.0 * inner * dinner
}
VdWOL1998 | RKOL1998 | PROL1998 => {
ol_sumhk_and_deriv(eos, tr, comp)
.map(|(s, ds)| (1.0 + s) + tr * ds)
.unwrap_or(f64::NAN)
}
SchmidtWenzel => {
let r = tr.sqrt();
let s = 1.0 - r;
let m = sw_m(w, tr);
let dm = sw_dm_dtr(w, tr);
let ds = -0.5 / r;
2.0 * (1.0 + m * s) * (dm * s + m * ds)
}
PatelTeja | PatelTejaUSB => {
let f = pt_f(w);
let s = 1.0 - tr.sqrt();
-f * (1.0 + f * s) / tr.sqrt()
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct EosState {
pub eos: CubicEos,
pub t: f64,
pub p: f64,
pub tr: f64,
pub alpha: f64,
pub d_alpha_d_tr: f64,
pub big_a: f64,
pub big_b: f64,
pub u: f64,
pub w: f64,
}
impl EosState {
pub fn new(eos: CubicEos, t: f64, p: f64, comp: &Component) -> Self {
let tr = t / comp.tc;
let a_val = alpha(eos, tr, comp);
let da_val = d_alpha_d_tr(eos, tr, comp);
let (big_a, big_b, u, w) = if eos.is_three_parameter() {
three_param_aubw(eos, t, p, comp, a_val)
} else {
let fc = family_constants(eos);
let pr = p / comp.pc;
let big_a = fc.om_a * a_val * pr / (tr * tr);
let big_b = fc.om_b * pr / tr;
(big_a, big_b, fc.k1 * big_b, fc.k2 * big_b * big_b)
};
Self {
eos,
t,
p,
tr,
alpha: a_val,
d_alpha_d_tr: da_val,
big_a,
big_b,
u,
w,
}
}
pub fn z(&self, phase: PhaseId) -> Result<f64, EosError> {
let (big_a, big_b, u, w) = (self.big_a, self.big_b, self.u, self.w);
let a2 = u - big_b - 1.0;
let a1 = big_a + w - u - big_b * u;
let a0 = -(big_a * big_b + w + big_b * w);
let (roots, count) = solve_real(1.0, a2, a1, a0)?;
select_physical_root(&roots[..count], big_b, phase)
}
pub fn attractive_term(&self, z: f64) -> f64 {
attractive_term_uw(z, self.big_a, self.u, self.w)
}
pub fn ln_phi_at(&self, z: f64) -> f64 {
z - 1.0 - (z - self.big_b).ln() - self.attractive_term(z)
}
pub fn ln_phi(&self, phase: PhaseId) -> Result<f64, EosError> {
Ok(self.ln_phi_at(self.z(phase)?))
}
pub fn h_departure_rt_at(&self, z: f64) -> f64 {
let g = self.attractive_term(z);
(z - 1.0) + g * (self.tr * self.d_alpha_d_tr / self.alpha - 1.0)
}
pub fn h_departure_rt(&self, phase: PhaseId) -> Result<f64, EosError> {
Ok(self.h_departure_rt_at(self.z(phase)?))
}
pub fn s_departure_r_at(&self, z: f64) -> f64 {
self.h_departure_rt_at(z) - self.ln_phi_at(z)
}
pub fn s_departure_r(&self, phase: PhaseId) -> Result<f64, EosError> {
Ok(self.s_departure_r_at(self.z(phase)?))
}
}
pub fn z_factor(
eos: CubicEos,
t: f64,
p: f64,
comp: &Component,
phase: PhaseId,
) -> Result<f64, EosError> {
EosState::new(eos, t, p, comp).z(phase)
}
fn select_physical_root(roots: &[f64], big_b: f64, phase: PhaseId) -> Result<f64, EosError> {
let mut selected: Option<f64> = None;
for &z in roots {
if z <= big_b {
continue;
}
selected = Some(match (selected, phase) {
(None, _) => z,
(Some(cur), PhaseId::Liquid) => cur.min(z),
(Some(cur), PhaseId::Vapor) => cur.max(z),
});
}
selected.ok_or(EosError::NoRootForPhase { phase, big_b })
}
pub fn ln_phi_pure(
eos: CubicEos,
t: f64,
p: f64,
comp: &Component,
phase: PhaseId,
) -> Result<f64, EosError> {
EosState::new(eos, t, p, comp).ln_phi(phase)
}
pub fn h_departure_rt(
eos: CubicEos,
t: f64,
p: f64,
comp: &Component,
phase: PhaseId,
) -> Result<f64, EosError> {
EosState::new(eos, t, p, comp).h_departure_rt(phase)
}
pub fn s_departure_r(
eos: CubicEos,
t: f64,
p: f64,
comp: &Component,
phase: PhaseId,
) -> Result<f64, EosError> {
EosState::new(eos, t, p, comp).s_departure_r(phase)
}
fn three_param_aubw(
eos: CubicEos,
t: f64,
p: f64,
comp: &Component,
a_val: f64,
) -> (f64, f64, f64, f64) {
use CubicEos::*;
let tr = t / comp.tc;
let pr = p / comp.pc;
let w = comp.omega;
match eos {
PatelTeja | PatelTejaUSB => {
let big_a = pt_om_a(w) * a_val * pr / (tr * tr);
let big_b = pt_om_b(w) * pr / tr;
let big_c = (1.0 - 3.0 * pt_xi_c(w)) * pr / tr;
(big_a, big_b, big_b + big_c, -big_b * big_c)
}
SchmidtWenzel => {
let big_a = sw_om_a(w) * a_val * pr / (tr * tr);
let big_b = sw_om_b(w) * pr / tr;
(
big_a,
big_b,
(1.0 + 3.0 * w) * big_b,
-3.0 * w * big_b * big_b,
)
}
_ => unreachable!("three_param_aubw called for 2-parameter EOS {eos:?}"),
}
}
pub fn attractive_term_uw(z: f64, big_a: f64, u: f64, w: f64) -> f64 {
let disc = u * u - 4.0 * w;
let scale = (u * u).max(4.0 * w.abs()).max(1e-300);
if disc.abs() <= 1e-12 * scale {
2.0 * big_a / (2.0 * z + u)
} else if disc > 0.0 {
let delta = disc.sqrt();
(big_a / delta) * ((2.0 * z + u + delta) / (2.0 * z + u - delta)).ln()
} else {
let delta = (-disc).sqrt();
(2.0 * big_a / delta) * (std::f64::consts::FRAC_PI_2 - ((2.0 * z + u) / delta).atan())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int))]
#[repr(i32)]
pub enum ChaoSeaderSpecies {
Normal = 0,
Hydrogen = 1,
Methane = 2,
}
impl ChaoSeaderSpecies {
pub fn for_component(comp: &Component) -> Self {
let name = comp.name.trim().to_ascii_lowercase();
match name.as_str() {
"hydrogen" | "h2" => ChaoSeaderSpecies::Hydrogen,
"methane" | "ch4" => ChaoSeaderSpecies::Methane,
_ => ChaoSeaderSpecies::Normal,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int))]
#[repr(i32)]
pub enum RegularSolutionSet {
ChaoSeader1961 = 0,
GraysonStreed1963 = 1,
}
fn nu0_coefficients(set: RegularSolutionSet, species: ChaoSeaderSpecies) -> [f64; 10] {
match (set, species) {
(RegularSolutionSet::GraysonStreed1963, ChaoSeaderSpecies::Normal) => [
2.05135, -2.10899, 0.0, -0.19396, 0.02282, 0.08852, 0.0, -0.00872, -0.00353, 0.00203,
],
(RegularSolutionSet::GraysonStreed1963, ChaoSeaderSpecies::Hydrogen) => [
1.50709, 2.74283, -0.02110, 0.00011, 0.0, 0.008585, 0.0, 0.0, 0.0, 0.0,
],
(RegularSolutionSet::GraysonStreed1963, ChaoSeaderSpecies::Methane) => [
1.36822, -1.54831, 0.0, 0.02889, -0.01076, 0.10486, -0.02529, 0.0, 0.0, 0.0,
],
(RegularSolutionSet::ChaoSeader1961, ChaoSeaderSpecies::Normal) => [
5.75748, -3.01761, -4.98500, 2.02299, 0.0, 0.08427, 0.26667, -0.31138, -0.02655,
0.02883,
],
(RegularSolutionSet::ChaoSeader1961, ChaoSeaderSpecies::Hydrogen) => [
1.96718, 1.02972, -0.054009, 0.0005288, 0.0, 0.008585, 0.0, 0.0, 0.0, 0.0,
],
(RegularSolutionSet::ChaoSeader1961, ChaoSeaderSpecies::Methane) => [
2.43840, -2.24550, -0.34084, 0.00212, -0.00223, 0.10486, -0.03691, 0.0, 0.0, 0.0,
],
}
}
pub fn regular_solution_ln_nu(
set: RegularSolutionSet,
t: f64,
p: f64,
comp: &Component,
species: ChaoSeaderSpecies,
) -> f64 {
let tr = t / comp.tc;
let pr = p / comp.pc;
let a = nu0_coefficients(set, species);
let q = [-4.23893, 8.65808, -1.22060, -3.15224, -0.025];
let tr2 = tr * tr;
let nu0 = a[0]
+ a[1] / tr
+ a[2] * tr
+ a[3] * tr2
+ a[4] * tr2 * tr
+ (a[5] + a[6] * tr + a[7] * tr2) * pr
+ (a[8] + a[9] * tr) * pr * pr
- pr.log10();
let nu1 = q[0] + q[1] * tr + q[2] / tr + q[3] * tr2 * tr + q[4] * (pr - 0.6);
(nu0 + comp.omega * nu1) * std::f64::consts::LN_10
}
pub fn chao_seader_ln_phi(t: f64, p: f64, comp: &Component, species: ChaoSeaderSpecies) -> f64 {
regular_solution_ln_nu(RegularSolutionSet::GraysonStreed1963, t, p, comp, species)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discriminant_values_match_legacy() {
assert_eq!(CubicEos::PR1976 as i32, 0);
assert_eq!(CubicEos::RKS1972 as i32, 2);
assert_eq!(CubicEos::PRSV1986 as i32, 15);
assert_eq!(CubicEos::PROL1998 as i32, 18);
assert_eq!(CubicEos::SchmidtWenzel as i32, 19);
assert_eq!(CubicEos::PatelTeja as i32, 20);
assert_eq!(CubicEos::PatelTejaUSB as i32, 21);
}
#[test]
fn three_parameter_detection() {
assert!(!CubicEos::PR1976.is_three_parameter());
assert!(!CubicEos::RKS1972.is_three_parameter());
assert!(CubicEos::SchmidtWenzel.is_three_parameter());
assert!(CubicEos::PatelTeja.is_three_parameter());
assert!(CubicEos::PatelTejaUSB.is_three_parameter());
}
fn methane() -> Component {
Component {
name: "methane".into(),
tc: 190.564,
pc: 4599.0, omega: 0.0115,
..Component::default()
}
}
fn n_pentane() -> Component {
Component {
name: "n-pentane".into(),
tc: 469.7,
pc: 3370.0,
omega: 0.252,
..Component::default()
}
}
#[test]
fn family_constants_match_legacy_table() {
let fc = family_constants(CubicEos::PR1976);
assert_eq!(fc.k1, 2.0);
assert_eq!(fc.k2, -1.0);
assert!((fc.om_a - 0.457235528921382).abs() < 1e-15);
assert!((fc.om_b - 0.0777960739038885).abs() < 1e-15);
let fc = family_constants(CubicEos::RKS1972);
assert_eq!(fc.k1, 1.0);
assert_eq!(fc.k2, 0.0);
assert!((fc.om_a - 0.427480233540341).abs() < 1e-15);
assert!((fc.om_b - 0.0866403499649577).abs() < 1e-15);
let fc = family_constants(CubicEos::VdW1870);
assert_eq!(fc.k1, 0.0);
assert_eq!(fc.k2, 0.0);
assert_eq!(fc.om_a, 27.0 / 64.0);
assert_eq!(fc.om_b, 1.0 / 8.0);
}
#[test]
fn alpha_at_tr_one_is_one_for_pr_rks() {
let c = n_pentane();
for eos in [
CubicEos::PR1976,
CubicEos::RKS1972,
CubicEos::RK1949,
CubicEos::VdW1870,
] {
let a = alpha(eos, 1.0, &c);
assert!(
(a - 1.0).abs() < 1e-12,
"{:?}: α(Tr=1) = {} (expected 1.0)",
eos,
a
);
}
}
fn d_alpha_numerical(eos: CubicEos, tr: f64, comp: &Component, h: f64) -> f64 {
(alpha(eos, tr + h, comp) - alpha(eos, tr - h, comp)) / (2.0 * h)
}
#[test]
fn analytical_d_alpha_matches_numerical() {
let c = n_pentane();
for eos in [
CubicEos::PR1976,
CubicEos::RKS1972,
CubicEos::RK1949,
CubicEos::VdW1870,
] {
for tr in [0.5_f64, 0.8, 1.0, 1.2, 2.0] {
let analytical = d_alpha_d_tr(eos, tr, &c);
let numerical = d_alpha_numerical(eos, tr, &c, 1e-6);
let rel = if analytical.abs() < 1e-10 {
(analytical - numerical).abs()
} else {
((analytical - numerical) / analytical).abs()
};
assert!(
rel < 1e-5,
"{:?} Tr={} analytical={} numerical={} rel={}",
eos,
tr,
analytical,
numerical,
rel
);
}
}
}
#[test]
fn z_factor_methane_supercritical() {
let c = methane();
let z_v = z_factor(CubicEos::PR1976, 300.0, 5000.0, &c, PhaseId::Vapor).unwrap();
assert!(
z_v > 0.8 && z_v < 1.05,
"Z(vapor) = {} not in plausible range",
z_v
);
}
#[test]
fn z_factor_n_pentane_two_phase() {
let c = n_pentane();
let z_l = z_factor(CubicEos::PR1976, 400.0, 1500.0, &c, PhaseId::Liquid).unwrap();
let z_v = z_factor(CubicEos::PR1976, 400.0, 1500.0, &c, PhaseId::Vapor).unwrap();
assert!(z_l < z_v, "expected Z_liquid={} < Z_vapor={}", z_l, z_v);
assert!(z_l < 0.1, "liquid Z should be small, got {}", z_l);
assert!(z_v > 0.5, "vapor Z should be > 0.5, got {}", z_v);
}
#[test]
fn ln_phi_ideal_gas_limit() {
let c = methane();
for eos in [
CubicEos::PR1976,
CubicEos::RKS1972,
CubicEos::RK1949,
CubicEos::VdW1870,
] {
let ln_phi = ln_phi_pure(eos, 300.0, 0.1, &c, PhaseId::Vapor).unwrap();
assert!(
ln_phi.abs() < 1e-3,
"{:?}: ln(φ) at P→0 = {} (expected near 0)",
eos,
ln_phi
);
}
}
const THREE_PARAM: [CubicEos; 3] = [
CubicEos::SchmidtWenzel,
CubicEos::PatelTeja,
CubicEos::PatelTejaUSB,
];
#[test]
fn three_parameter_alpha_unity_at_critical() {
let c = n_pentane();
for eos in THREE_PARAM {
assert!((alpha(eos, 1.0, &c) - 1.0).abs() < 1e-12, "{eos:?}");
}
}
#[test]
fn three_parameter_d_alpha_matches_numerical() {
let c = n_pentane();
for eos in THREE_PARAM {
for tr in [0.5_f64, 0.7, 0.9, 1.2, 1.5, 2.0] {
let analytical = d_alpha_d_tr(eos, tr, &c);
let numerical = d_alpha_numerical(eos, tr, &c, 1e-6);
let rel = if analytical.abs() < 1e-10 {
(analytical - numerical).abs()
} else {
((analytical - numerical) / analytical).abs()
};
assert!(
rel < 1e-5,
"{eos:?} Tr={tr} a={analytical} n={numerical} rel={rel}"
);
}
}
}
#[test]
fn three_parameter_ideal_gas_limit() {
let c = n_pentane();
for eos in THREE_PARAM {
let z = z_factor(eos, 400.0, 1e-3, &c, PhaseId::Vapor).unwrap();
assert!((z - 1.0).abs() < 1e-4, "{eos:?} Z={z}");
let lnphi = ln_phi_pure(eos, 400.0, 1e-3, &c, PhaseId::Vapor).unwrap();
assert!(lnphi.abs() < 1e-4, "{eos:?} lnphi={lnphi}");
}
}
#[test]
fn three_parameter_roots_and_fugacity_sane() {
let c = n_pentane();
for eos in THREE_PARAM {
let zv = z_factor(eos, 400.0, 50.0, &c, PhaseId::Vapor).unwrap();
assert!(zv > 0.0 && zv < 1.1, "{eos:?} Zv={zv}");
let zl = z_factor(eos, 300.0, 2000.0, &c, PhaseId::Liquid).unwrap();
assert!(
zl.is_finite() && zl > 0.0 && zl < zv,
"{eos:?} Zl={zl} Zv={zv}"
);
let lnphi = ln_phi_pure(eos, 400.0, 50.0, &c, PhaseId::Vapor).unwrap();
assert!(lnphi.is_finite(), "{eos:?} lnphi={lnphi}");
}
}
#[test]
fn three_parameter_entropy_consistency() {
let c = n_pentane();
for eos in THREE_PARAM {
let s = s_departure_r(eos, 400.0, 500.0, &c, PhaseId::Vapor).unwrap();
let h = h_departure_rt(eos, 400.0, 500.0, &c, PhaseId::Vapor).unwrap();
let g = ln_phi_pure(eos, 400.0, 500.0, &c, PhaseId::Vapor).unwrap();
assert!(s.is_finite() && (s - (h - g)).abs() < 1e-9, "{eos:?} s={s}");
}
}
#[test]
fn schmidt_wenzel_tr1_entropy_finite() {
let c = n_pentane();
let s = s_departure_r(CubicEos::SchmidtWenzel, c.tc, 500.0, &c, PhaseId::Vapor).unwrap();
assert!(s.is_finite(), "SW entropy at Tr=1 not finite: {s}");
}
#[test]
fn chao_seader_pure_fugacity_sane() {
let hydrogen = Component {
tc: 33.2,
pc: 1300.0,
omega: -0.216,
..Component::default()
};
let cases = [
(ChaoSeaderSpecies::Normal, n_pentane()),
(ChaoSeaderSpecies::Methane, methane()),
(ChaoSeaderSpecies::Hydrogen, hydrogen),
];
for (species, c) in cases {
let lnphi = chao_seader_ln_phi(0.7 * c.tc, 500.0, &c, species);
assert!(lnphi.is_finite(), "{species:?} lnphi not finite: {lnphi}");
assert!(lnphi.abs() < 50.0, "{species:?} lnphi out of band: {lnphi}");
}
}
const OL_FAMILY: [CubicEos; 3] = [CubicEos::VdWOL1998, CubicEos::RKOL1998, CubicEos::PROL1998];
fn pentane_full() -> Component {
Component {
name: "n-pentane".into(),
tc: 469.7,
pc: 3370.0,
omega: 0.252,
tb: 309.2,
psat_coeffs: vec![6.738, 3165.0, 0.0],
liquid_volume: 116.0,
..Component::default()
}
}
#[test]
fn ol_alpha_finite_and_positive() {
let c = pentane_full();
for eos in OL_FAMILY {
for tr in [0.6_f64, 0.8, 0.95] {
let a = alpha(eos, tr, &c);
assert!(a.is_finite() && a > 0.0, "{eos:?} Tr={tr} α={a}");
}
}
}
#[test]
fn ol_d_alpha_matches_numerical() {
let c = pentane_full();
for eos in OL_FAMILY {
for tr in [0.6_f64, 0.75, 0.9] {
let analytical = d_alpha_d_tr(eos, tr, &c);
let numerical = d_alpha_numerical(eos, tr, &c, 1e-6);
let rel = ((analytical - numerical) / analytical).abs();
assert!(
rel < 1e-4,
"{eos:?} Tr={tr} a={analytical} n={numerical} rel={rel}"
);
}
}
}
#[test]
fn ol_z_factor_and_entropy_finite() {
let c = pentane_full();
for eos in OL_FAMILY {
let zv = z_factor(eos, 400.0, 100.0, &c, PhaseId::Vapor).unwrap();
assert!(zv.is_finite() && zv > 0.0 && zv < 1.1, "{eos:?} Zv={zv}");
let s = s_departure_r(eos, 400.0, 100.0, &c, PhaseId::Vapor).unwrap();
assert!(s.is_finite(), "{eos:?} S^R/R={s}");
}
}
#[test]
fn ol_alpha_nan_without_sat_data() {
let c = n_pentane();
assert!(alpha(CubicEos::RKOL1998, 0.8, &c).is_nan());
}
fn polar_component() -> Component {
Component {
name: "synthetic-polar".into(),
tc: 647.1,
pc: 22064.0,
omega: 0.344,
zc: 0.229,
m_polar: 0.45,
n_polar: 0.12,
g_polar: 1.5,
prsv_k1: 0.07,
..Component::default()
}
}
const M72_VARIANTS: [CubicEos; 12] = [
CubicEos::Berth1899,
CubicEos::VdWAda1984,
CubicEos::RKSGD1978,
CubicEos::RKSL1997,
CubicEos::RP1978,
CubicEos::PRL1997,
CubicEos::VdWVald1989,
CubicEos::RKSmn1980,
CubicEos::RKSATmn1995,
CubicEos::PRATmng1997,
CubicEos::PRMmn1989,
CubicEos::PRSV1986,
];
#[test]
fn m72_alpha_is_finite_and_positive() {
let c = polar_component();
for eos in M72_VARIANTS {
for tr in [0.5_f64, 0.7, 0.9, 1.0, 1.3, 2.0] {
let a = alpha(eos, tr, &c);
assert!(
a.is_finite() && a > 0.0,
"{:?} Tr={} gave α={} (expected finite, positive)",
eos,
tr,
a
);
}
}
}
#[test]
fn m72_alpha_at_tr_one_is_one() {
let c = polar_component();
for eos in M72_VARIANTS {
let a = alpha(eos, 1.0, &c);
assert!(
(a - 1.0).abs() < 1e-12,
"{:?}: α(Tr=1) = {} (expected 1.0)",
eos,
a
);
}
}
#[test]
fn m72_analytical_d_alpha_matches_numerical() {
let c = polar_component();
for eos in M72_VARIANTS {
for tr in [0.55_f64, 0.7, 0.85, 1.1, 1.4] {
let analytical = d_alpha_d_tr(eos, tr, &c);
let numerical = d_alpha_numerical(eos, tr, &c, 1e-6);
let rel = if analytical.abs() < 1e-8 {
(analytical - numerical).abs()
} else {
((analytical - numerical) / analytical).abs()
};
assert!(
rel < 1e-5,
"{:?} Tr={} analytical={} numerical={} rel={}",
eos,
tr,
analytical,
numerical,
rel
);
}
}
}
#[test]
fn prsv_k1_recovers_kappa0_when_zero() {
let mut c = polar_component();
c.prsv_k1 = 0.0;
let w = c.omega;
let kappa0 = 0.378893 + 1.4897153 * w - 0.17131848 * w * w + 0.0196554 * w * w * w;
for tr in [0.6_f64, 0.8, 1.2] {
let s = 1.0 - tr.sqrt();
let expected = (1.0 + kappa0 * s).powi(2);
let got = alpha(CubicEos::PRSV1986, tr, &c);
assert!(
(got - expected).abs() < 1e-12,
"PRSV K₁=0 Tr={}: got {} expected {}",
tr,
got,
expected
);
}
}
#[test]
fn eos_state_reuse_matches_one_shot_functions() {
let c = n_pentane();
for eos in [CubicEos::PR1976, CubicEos::RKS1972, CubicEos::PatelTeja] {
let st = EosState::new(eos, 400.0, 1500.0, &c);
for phase in [PhaseId::Vapor, PhaseId::Liquid] {
assert_eq!(
st.z(phase).unwrap(),
z_factor(eos, 400.0, 1500.0, &c, phase).unwrap(),
"{eos:?} {phase:?} Z"
);
assert_eq!(
st.ln_phi(phase).unwrap(),
ln_phi_pure(eos, 400.0, 1500.0, &c, phase).unwrap(),
"{eos:?} {phase:?} ln_phi"
);
assert_eq!(
st.h_departure_rt(phase).unwrap(),
h_departure_rt(eos, 400.0, 1500.0, &c, phase).unwrap(),
"{eos:?} {phase:?} H^R"
);
assert_eq!(
st.s_departure_r(phase).unwrap(),
s_departure_r(eos, 400.0, 1500.0, &c, phase).unwrap(),
"{eos:?} {phase:?} S^R"
);
}
}
}
#[test]
fn generalized_uw_reproduces_two_param_families() {
let (big_a, big_b, z) = (0.5_f64, 0.05_f64, 0.8_f64);
let sd = 8.0_f64.sqrt();
let f_pr = (1.0 / sd)
* ((2.0 * z + big_b * (2.0 + sd)) / (2.0 * z + big_b * (2.0 - sd))).ln()
* (big_a / big_b);
let g_pr = attractive_term_uw(z, big_a, 2.0 * big_b, -big_b * big_b);
assert!((f_pr - g_pr).abs() < 1e-14, "PR: {f_pr} vs {g_pr}");
let g_vdw = attractive_term_uw(z, big_a, 0.0, 0.0);
assert!((g_vdw - big_a / z).abs() < 1e-14, "VdW: {g_vdw}");
}
#[test]
fn attractive_term_arctan_branch_hydrogen_like_sw() {
let hydrogen = Component {
name: "hydrogen".into(),
tc: 33.2,
pc: 1300.0,
omega: -0.216,
..Component::default()
};
let st = EosState::new(CubicEos::SchmidtWenzel, 40.0, 500.0, &hydrogen);
assert!(
st.u * st.u - 4.0 * st.w < 0.0,
"test premise: SW hydrogen must hit the negative-discriminant branch"
);
let g = st.attractive_term(0.9);
assert!(g.is_finite() && g > 0.0, "g = {g}");
assert!(st.attractive_term(1e9).abs() < 1e-6);
let lnphi = st.ln_phi(PhaseId::Vapor).unwrap();
assert!(lnphi.is_finite(), "ln φ = {lnphi}");
}
#[test]
fn m72_z_factor_and_ln_phi_work() {
let c = polar_component();
for eos in M72_VARIANTS {
let z = z_factor(eos, 500.0, 2000.0, &c, PhaseId::Vapor).unwrap();
assert!(z > 0.0 && z < 1.2, "{:?}: Z={} out of range", eos, z);
let ln_phi = ln_phi_pure(eos, 500.0, 2000.0, &c, PhaseId::Vapor).unwrap();
assert!(ln_phi.is_finite(), "{:?}: ln(φ)={} not finite", eos, ln_phi);
}
}
fn non_ol_variants_with_comp() -> Vec<(CubicEos, Component)> {
let mut v: Vec<(CubicEos, Component)> = vec![
(CubicEos::VdW1870, n_pentane()),
(CubicEos::RK1949, n_pentane()),
(CubicEos::RKS1972, n_pentane()),
(CubicEos::PR1976, n_pentane()),
(CubicEos::SchmidtWenzel, n_pentane()),
(CubicEos::PatelTeja, n_pentane()),
(CubicEos::PatelTejaUSB, n_pentane()),
];
for eos in M72_VARIANTS {
v.push((eos, polar_component()));
}
v
}
#[test]
fn alpha_generic_f64_matches_scalar_alpha() {
for (eos, c) in non_ol_variants_with_comp() {
for &tr in &[0.5, 0.7, 0.95, 1.0, 1.05, 1.5, 2.0] {
let scalar = alpha(eos, tr, &c);
let generic = alpha_generic::<f64>(eos, tr, &c);
let tol = 1e-12 * scalar.abs().max(1.0);
assert!(
(scalar - generic).abs() <= tol,
"{eos:?} Tr={tr}: scalar={scalar} generic={generic}"
);
}
}
}
#[test]
fn alpha_generic_dual_matches_analytic_d_alpha() {
use num_dual::Dual64;
for (eos, c) in non_ol_variants_with_comp() {
for &tr in &[0.6, 0.85, 1.2, 1.8] {
let d = alpha_generic(eos, Dual64::new(tr, 1.0), &c);
let analytic = d_alpha_d_tr(eos, tr, &c);
let tol = 1e-9 * analytic.abs().max(1.0);
assert!(
(d.eps - analytic).abs() <= tol,
"{eos:?} Tr={tr}: dual dα/dTr={} analytic={analytic}",
d.eps
);
}
}
}
#[test]
fn eos_dimensionless_generic_f64_matches_eos_state() {
let (t, p) = (360.0, 2500.0);
for (eos, c) in non_ol_variants_with_comp() {
let st = EosState::new(eos, t, p, &c);
let (a, b, u, w) = eos_dimensionless_generic::<f64>(eos, t, p, &c);
let tol = 1e-10;
assert!(
(a - st.big_a).abs() <= tol * st.big_a.abs().max(1.0),
"{eos:?} A"
);
assert!(
(b - st.big_b).abs() <= tol * st.big_b.abs().max(1.0),
"{eos:?} B"
);
assert!((u - st.u).abs() <= tol * st.u.abs().max(1.0), "{eos:?} U");
assert!((w - st.w).abs() <= tol * st.w.abs().max(1.0), "{eos:?} W");
}
}
#[test]
fn grayson_streed_set_is_the_legacy_chao_seader_table_and_1961_differs() {
let c = n_pentane();
for species in [
ChaoSeaderSpecies::Normal,
ChaoSeaderSpecies::Hydrogen,
ChaoSeaderSpecies::Methane,
] {
let legacy = chao_seader_ln_phi(400.0, 1500.0, &c, species);
let gs = regular_solution_ln_nu(
RegularSolutionSet::GraysonStreed1963,
400.0,
1500.0,
&c,
species,
);
let cs = regular_solution_ln_nu(
RegularSolutionSet::ChaoSeader1961,
400.0,
1500.0,
&c,
species,
);
assert_eq!(legacy, gs);
assert!((cs - gs).abs() > 1e-3, "{species:?}: {cs} vs {gs}");
if species == ChaoSeaderSpecies::Normal {
assert!(
(cs - gs).abs() < 0.5 * std::f64::consts::LN_10,
"{species:?}: {cs} vs {gs}"
);
}
}
}
#[test]
fn species_selection_by_name() {
let mk = |n: &str| Component {
name: n.into(),
..Component::default()
};
assert_eq!(
ChaoSeaderSpecies::for_component(&mk("Hydrogen")),
ChaoSeaderSpecies::Hydrogen
);
assert_eq!(
ChaoSeaderSpecies::for_component(&mk("H2")),
ChaoSeaderSpecies::Hydrogen
);
assert_eq!(
ChaoSeaderSpecies::for_component(&mk("methane")),
ChaoSeaderSpecies::Methane
);
assert_eq!(
ChaoSeaderSpecies::for_component(&mk("CH4")),
ChaoSeaderSpecies::Methane
);
assert_eq!(
ChaoSeaderSpecies::for_component(&mk("n-decane")),
ChaoSeaderSpecies::Normal
);
assert_eq!(
ChaoSeaderSpecies::for_component(&mk("PC-12")),
ChaoSeaderSpecies::Normal
);
}
}