use super::RefineryError;
use crate::eos::{CubicEos, PhaseId, family_constants};
use crate::mixture::{MixtureSpec, z_mix};
use crate::types::{Component, R_GAS};
fn rackett_zra(comp: &Component) -> f64 {
if comp.zra > 0.0 {
comp.zra
} else {
0.29056 - 0.08775 * comp.omega
}
}
pub fn peneloux_shift(eos: CubicEos, comp: &Component) -> Result<f64, RefineryError> {
if !(comp.tc > 0.0 && comp.pc > 0.0) {
return Err(RefineryError::InvalidInput(format!(
"component '{}' needs Tc > 0 and Pc > 0 (got {}, {})",
comp.name, comp.tc, comp.pc
)));
}
let fc = family_constants(eos);
let (k, z_ref) = if eos.is_three_parameter() {
return Err(RefineryError::Unsupported(format!(
"Peneloux shift is not defined for the three-parameter EOS {eos:?}"
)));
} else if fc.k1 == 1.0 {
(0.40768, 0.29441) } else if fc.k1 == 2.0 {
(0.50033, 0.25969) } else {
return Err(RefineryError::Unsupported(format!(
"Peneloux shift is not defined for the van der Waals-family EOS {eos:?}"
)));
};
let rtc_pc = 1000.0 * R_GAS * comp.tc / comp.pc;
Ok(k * rtc_pc * (z_ref - rackett_zra(comp)))
}
pub fn peneloux_shift_mix(
eos: CubicEos,
components: &[Component],
x: &[f64],
) -> Result<f64, RefineryError> {
if components.len() != x.len() {
return Err(RefineryError::InvalidInput(format!(
"components={}, x={}",
components.len(),
x.len()
)));
}
let mut c = 0.0;
for (comp, &xi) in components.iter().zip(x) {
if xi != 0.0 {
c += xi * peneloux_shift(eos, comp)?;
}
}
Ok(c)
}
pub fn translated_molar_volume(
spec: &MixtureSpec,
t: f64,
p: f64,
x: &[f64],
phase: PhaseId,
) -> Result<f64, RefineryError> {
let c = peneloux_shift_mix(spec.eos, spec.components, x)?;
let z = z_mix(spec, t, p, x, phase).map_err(|e| RefineryError::InvalidInput(e.to_string()))?;
Ok(1000.0 * z * R_GAS * t / p - c)
}
pub fn translated_liquid_density(
spec: &MixtureSpec,
t: f64,
p: f64,
x: &[f64],
phase: PhaseId,
) -> Result<f64, RefineryError> {
let v = translated_molar_volume(spec, t, p, x, phase)?;
let mut mw = 0.0;
for (comp, &xi) in spec.components.iter().zip(x) {
if xi > 0.0 && comp.mw <= 0.0 {
return Err(RefineryError::InvalidInput(format!(
"component '{}' has no molecular weight",
comp.name
)));
}
mw += xi * comp.mw;
}
Ok(1000.0 * mw / v)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mixing::MixingRule;
fn n_decane() -> Component {
Component {
name: "n-decane".into(),
tc: 617.7,
pc: 2110.0,
omega: 0.492,
mw: 142.28,
..Component::default()
}
}
fn n_heptane() -> Component {
Component {
name: "n-heptane".into(),
tc: 540.2,
pc: 2740.0,
omega: 0.350,
mw: 100.20,
..Component::default()
}
}
#[test]
fn shift_is_positive_and_of_the_expected_size_for_srk() {
let c = peneloux_shift(CubicEos::RKS1972, &n_heptane()).unwrap();
assert!(c > 5.0 && c < 25.0, "c = {c} cm³/mol");
let c10 = peneloux_shift(CubicEos::RKS1972, &n_decane()).unwrap();
assert!(c10 > c, "shift should grow with size: {c10} vs {c}");
}
#[test]
fn a_measured_zra_overrides_the_correlation() {
let mut c = n_heptane();
let base = peneloux_shift(CubicEos::PR1976, &c).unwrap();
c.zra = 0.26;
let with = peneloux_shift(CubicEos::PR1976, &c).unwrap();
assert!(with != base);
let want = 0.50033 * 1000.0 * R_GAS * c.tc / c.pc * (0.25969 - 0.26);
assert!((with - want).abs() < 1e-9);
}
#[test]
fn translated_srk_liquid_density_of_n_heptane_is_close_to_measured() {
let comps = [n_heptane()];
let spec = MixtureSpec {
eos: CubicEos::RKS1972,
rule: MixingRule::Classical,
components: &comps,
kij: &[],
ge: None,
};
let rho =
translated_liquid_density(&spec, 298.15, 101.325, &[1.0], PhaseId::Liquid).unwrap();
assert!((rho - 680.0).abs() / 680.0 < 0.03, "ρ = {rho} kg/m³");
let z = z_mix(&spec, 298.15, 101.325, &[1.0], PhaseId::Liquid).unwrap();
let rho_raw = 1000.0 * 100.20 / (1000.0 * z * R_GAS * 298.15 / 101.325);
assert!(rho > rho_raw, "{rho} vs raw {rho_raw}");
}
#[test]
fn translated_pr_liquid_density_of_n_decane_is_close_to_measured() {
let comps = [n_decane()];
let spec = MixtureSpec {
eos: CubicEos::PR1976,
rule: MixingRule::Classical,
components: &comps,
kij: &[],
ge: None,
};
let rho =
translated_liquid_density(&spec, 298.15, 101.325, &[1.0], PhaseId::Liquid).unwrap();
assert!((rho - 727.0).abs() / 727.0 < 0.03, "ρ = {rho} kg/m³");
}
#[test]
fn mixture_shift_is_the_mole_fraction_average() {
let comps = [n_heptane(), n_decane()];
let a = peneloux_shift(CubicEos::PR1976, &comps[0]).unwrap();
let b = peneloux_shift(CubicEos::PR1976, &comps[1]).unwrap();
let m = peneloux_shift_mix(CubicEos::PR1976, &comps, &[0.25, 0.75]).unwrap();
assert!((m - (0.25 * a + 0.75 * b)).abs() < 1e-12);
}
#[test]
fn unsupported_families_are_reported() {
assert!(peneloux_shift(CubicEos::VdW1870, &n_heptane()).is_err());
assert!(peneloux_shift(CubicEos::PatelTeja, &n_heptane()).is_err());
assert!(peneloux_shift(CubicEos::PR1976, &Component::default()).is_err());
assert!(peneloux_shift_mix(CubicEos::PR1976, &[n_heptane()], &[0.5, 0.5]).is_err());
}
}