use super::parameters::PetsParameters;
use crate::hard_sphere::HardSphere;
use feos_core::joback::Joback;
use feos_core::parameter::Parameter;
use feos_core::{
Contributions, EntropyScaling, EosError, EosResult, EosUnit, EquationOfState, HelmholtzEnergy,
IdealGasContribution, MolarWeight, State,
};
use ndarray::Array1;
use quantity::si::*;
use std::f64::consts::{FRAC_PI_6, PI};
use std::sync::Arc;
pub(crate) mod dispersion;
mod qspr;
use dispersion::Dispersion;
use qspr::QSPR;
#[allow(clippy::upper_case_acronyms)]
enum IdealGasContributions {
QSPR(QSPR),
Joback(Joback),
}
#[derive(Copy, Clone)]
pub struct PetsOptions {
pub max_eta: f64,
}
impl Default for PetsOptions {
fn default() -> Self {
Self { max_eta: 0.5 }
}
}
pub struct Pets {
parameters: Arc<PetsParameters>,
options: PetsOptions,
contributions: Vec<Box<dyn HelmholtzEnergy>>,
ideal_gas: IdealGasContributions,
}
impl Pets {
pub fn new(parameters: Arc<PetsParameters>) -> Self {
Self::with_options(parameters, PetsOptions::default())
}
pub fn with_options(parameters: Arc<PetsParameters>, options: PetsOptions) -> Self {
let contributions: Vec<Box<dyn HelmholtzEnergy>> = vec![
Box::new(HardSphere::new(¶meters)),
Box::new(Dispersion {
parameters: parameters.clone(),
}),
];
let joback_records = parameters.joback_records.clone();
Self {
parameters: parameters.clone(),
options,
contributions,
ideal_gas: joback_records.map_or(
IdealGasContributions::QSPR(QSPR { parameters }),
|joback_records| IdealGasContributions::Joback(Joback::new(joback_records)),
),
}
}
}
impl EquationOfState for Pets {
fn components(&self) -> usize {
self.parameters.pure_records.len()
}
fn subset(&self, component_list: &[usize]) -> Self {
Self::with_options(
Arc::new(self.parameters.subset(component_list)),
self.options,
)
}
fn compute_max_density(&self, moles: &Array1<f64>) -> f64 {
self.options.max_eta * moles.sum()
/ (FRAC_PI_6 * self.parameters.sigma.mapv(|v| v.powi(3)) * moles).sum()
}
fn residual(&self) -> &[Box<dyn HelmholtzEnergy>] {
&self.contributions
}
fn ideal_gas(&self) -> &dyn IdealGasContribution {
match &self.ideal_gas {
IdealGasContributions::QSPR(qspr) => qspr,
IdealGasContributions::Joback(joback) => joback,
}
}
}
impl MolarWeight for Pets {
fn molar_weight(&self) -> SIArray1 {
self.parameters.molarweight.clone() * GRAM / MOL
}
}
fn omega11(t: f64) -> f64 {
1.06036 * t.powf(-0.15610)
+ 0.19300 * (-0.47635 * t).exp()
+ 1.03587 * (-1.52996 * t).exp()
+ 1.76474 * (-3.89411 * t).exp()
}
fn omega22(t: f64) -> f64 {
1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp()
- 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin()
}
impl EntropyScaling for Pets {
fn viscosity_reference(
&self,
temperature: SINumber,
_: SINumber,
moles: &SIArray1,
) -> EosResult<SINumber> {
let x = moles.to_reduced(moles.sum())?;
let p = &self.parameters;
let mw = &p.molarweight;
let ce: Array1<SINumber> = (0..self.components())
.map(|i| {
let tr = (temperature / p.epsilon_k[i] / KELVIN)
.into_value()
.unwrap();
5.0 / 16.0
* (mw[i] * GRAM / MOL * KB / NAV * temperature / PI)
.sqrt()
.unwrap()
/ omega22(tr)
/ (p.sigma[i] * ANGSTROM).powi(2)
})
.collect();
let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND;
for i in 0..self.components() {
let denom: f64 = (0..self.components())
.map(|j| {
x[j] * (1.0
+ (ce[i] / ce[j]).into_value().unwrap().sqrt()
* (mw[j] / mw[i]).powf(1.0 / 4.0))
.powi(2)
/ (8.0 * (1.0 + mw[i] / mw[j])).sqrt()
})
.sum();
ce_mix += ce[i] * x[i] / denom
}
Ok(ce_mix)
}
fn viscosity_correlation(&self, s_res: f64, x: &Array1<f64>) -> EosResult<f64> {
let coefficients = self
.parameters
.viscosity
.as_ref()
.expect("Missing viscosity coefficients.");
let a: f64 = (&coefficients.row(0) * x).sum();
let b: f64 = (&coefficients.row(1) * x).sum();
let c: f64 = (&coefficients.row(2) * x).sum();
let d: f64 = (&coefficients.row(3) * x).sum();
Ok(a + b * s_res + c * s_res.powi(2) + d * s_res.powi(3))
}
fn diffusion_reference(
&self,
temperature: SINumber,
volume: SINumber,
moles: &SIArray1,
) -> EosResult<SINumber> {
if self.components() != 1 {
return Err(EosError::IncompatibleComponents(self.components(), 1));
}
let p = &self.parameters;
let density = moles.sum() / volume;
let res: Array1<SINumber> = (0..self.components())
.map(|i| {
let tr = (temperature / p.epsilon_k[i] / KELVIN)
.into_value()
.unwrap();
3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi(2) / omega11(tr) / (density * NAV)
* (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL))
.sqrt()
.unwrap()
})
.collect();
Ok(res[0])
}
fn diffusion_correlation(&self, s_res: f64, x: &Array1<f64>) -> EosResult<f64> {
if self.components() != 1 {
return Err(EosError::IncompatibleComponents(self.components(), 1));
}
let coefficients = self
.parameters
.diffusion
.as_ref()
.expect("Missing diffusion coefficients.");
let a: f64 = (&coefficients.row(0) * x).sum();
let b: f64 = (&coefficients.row(1) * x).sum();
let c: f64 = (&coefficients.row(2) * x).sum();
let d: f64 = (&coefficients.row(3) * x).sum();
let e: f64 = (&coefficients.row(4) * x).sum();
Ok(a + b * s_res
- c * (1.0 - s_res.exp()) * s_res.powi(2)
- d * s_res.powi(4)
- e * s_res.powi(8))
}
fn thermal_conductivity_reference(
&self,
temperature: SINumber,
volume: SINumber,
moles: &SIArray1,
) -> EosResult<SINumber> {
if self.components() != 1 {
return Err(EosError::IncompatibleComponents(self.components(), 1));
}
let p = &self.parameters;
let state = State::new_nvt(
&Arc::new(Self::new(self.parameters.clone())),
temperature,
volume,
moles,
)?;
let res: Array1<SINumber> = (0..self.components())
.map(|i| {
let tr = (temperature / p.epsilon_k[i] / KELVIN)
.into_value()
.unwrap();
let ce = 83.235
* f64::powf(10.0, -1.5)
* ((temperature / KELVIN).into_value().unwrap() / p.molarweight[0]).sqrt()
/ (p.sigma[0] * p.sigma[0])
/ omega22(tr);
ce * WATT / METER / KELVIN
+ state.density
* self
.diffusion_reference(temperature, volume, moles)
.unwrap()
* self
.diffusion_correlation(
state
.molar_entropy(Contributions::ResidualNvt)
.to_reduced(SIUnit::reference_molar_entropy())
.unwrap(),
&state.molefracs,
)
.unwrap()
* (state.c_v(Contributions::Total) - 1.5 * RGAS)
})
.collect();
Ok(res[0])
}
fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1<f64>) -> EosResult<f64> {
if self.components() != 1 {
return Err(EosError::IncompatibleComponents(self.components(), 1));
}
let coefficients = self
.parameters
.thermal_conductivity
.as_ref()
.expect("Missing thermal conductivity coefficients");
let a: f64 = (&coefficients.row(0) * x).sum();
let b: f64 = (&coefficients.row(1) * x).sum();
let c: f64 = (&coefficients.row(2) * x).sum();
let d: f64 = (&coefficients.row(3) * x).sum();
Ok(a + b * s_res + c * (1.0 - s_res.exp()) + d * s_res.powi(2))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pets::parameters::utils::{
argon_krypton_parameters, argon_parameters, krypton_parameters,
};
use approx::assert_relative_eq;
use feos_core::{
Contributions, DensityInitialization, HelmholtzEnergyDual, PhaseEquilibrium, State, StateHD,
};
use ndarray::arr1;
use quantity::si::{BAR, KELVIN, METER, PASCAL, RGAS};
#[test]
fn ideal_gas_pressure() {
let e = Arc::new(Pets::new(argon_parameters()));
let t = 200.0 * KELVIN;
let v = 1e-3 * METER.powi(3);
let n = arr1(&[1.0]) * MOL;
let s = State::new_nvt(&e, t, v, &n).unwrap();
let p_ig = s.total_moles * RGAS * t / v;
assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10);
assert_relative_eq!(
s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt),
s.pressure(Contributions::Total),
epsilon = 1e-10
);
}
#[test]
fn ideal_gas_heat_capacity_joback() {
let e = Arc::new(Pets::new(argon_parameters()));
let t = 200.0 * KELVIN;
let v = 1e-3 * METER.powi(3);
let n = arr1(&[1.0]) * MOL;
let s = State::new_nvt(&e, t, v, &n).unwrap();
let p_ig = s.total_moles * RGAS * t / v;
assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10);
assert_relative_eq!(
s.pressure(Contributions::IdealGas) + s.pressure(Contributions::ResidualNvt),
s.pressure(Contributions::Total),
epsilon = 1e-10
);
}
#[test]
fn hard_sphere_mix() {
let c1 = HardSphere::new(&argon_parameters());
let c2 = HardSphere::new(&krypton_parameters());
let c12 = HardSphere::new(&argon_krypton_parameters());
let t = 250.0;
let v = 2.5e28;
let n = 1.0;
let s = StateHD::new(t, v, arr1(&[n]));
let a1 = c1.helmholtz_energy(&s);
let a2 = c2.helmholtz_energy(&s);
let s1m = StateHD::new(t, v, arr1(&[n, 0.0]));
let a1m = c12.helmholtz_energy(&s1m);
let s2m = StateHD::new(t, v, arr1(&[0.0, n]));
let a2m = c12.helmholtz_energy(&s2m);
assert_relative_eq!(a1, a1m, epsilon = 1e-14);
assert_relative_eq!(a2, a2m, epsilon = 1e-14);
}
#[test]
fn new_tpn() {
let e = Arc::new(Pets::new(argon_parameters()));
let t = 300.0 * KELVIN;
let p = BAR;
let m = arr1(&[1.0]) * MOL;
let s = State::new_npt(&e, t, p, &m, DensityInitialization::None);
let p_calc = if let Ok(state) = s {
state.pressure(Contributions::Total)
} else {
0.0 * PASCAL
};
assert_relative_eq!(p, p_calc, epsilon = 1e-6);
}
#[test]
fn vle_pure_t() {
let e = Arc::new(Pets::new(argon_parameters()));
let t = 300.0 * KELVIN;
let vle = PhaseEquilibrium::pure(&e, t, None, Default::default());
if let Ok(v) = vle {
assert_relative_eq!(
v.vapor().pressure(Contributions::Total),
v.liquid().pressure(Contributions::Total),
epsilon = 1e-6
)
}
}
#[test]
fn mix_single() {
let e1 = Arc::new(Pets::new(argon_parameters()));
let e2 = Arc::new(Pets::new(krypton_parameters()));
let e12 = Arc::new(Pets::new(argon_krypton_parameters()));
let t = 300.0 * KELVIN;
let v = 0.02456883872966545 * METER.powi(3);
let m1 = arr1(&[2.0]) * MOL;
let m1m = arr1(&[2.0, 0.0]) * MOL;
let m2m = arr1(&[0.0, 2.0]) * MOL;
let s1 = State::new_nvt(&e1, t, v, &m1).unwrap();
let s2 = State::new_nvt(&e2, t, v, &m1).unwrap();
let s1m = State::new_nvt(&e12, t, v, &m1m).unwrap();
let s2m = State::new_nvt(&e12, t, v, &m2m).unwrap();
assert_relative_eq!(
s1.pressure(Contributions::Total),
s1m.pressure(Contributions::Total),
epsilon = 1e-12
);
assert_relative_eq!(
s2.pressure(Contributions::Total),
s2m.pressure(Contributions::Total),
epsilon = 1e-12
)
}
}