use crate::types::{Component, R_GAS};
use thiserror::Error;
#[derive(Debug, Error, PartialEq)]
pub enum VirialError {
#[error("invalid input to virial layer: {0}")]
InvalidInput(String),
}
pub fn pitzer_b0(tr: f64) -> f64 {
0.083 - 0.422 / tr.powf(1.6)
}
pub fn pitzer_b1(tr: f64) -> f64 {
0.139 - 0.172 / tr.powf(4.2)
}
pub fn pitzer_b(comp: &Component, t: f64) -> f64 {
let tr = t / comp.tc;
let b_reduced = pitzer_b0(tr) + comp.omega * pitzer_b1(tr);
1000.0 * R_GAS * comp.tc / comp.pc * b_reduced
}
pub fn pitzer_d_b_d_t(comp: &Component, t: f64) -> f64 {
let tr = t / comp.tc;
let db0_dtr = 0.422 * 1.6 * tr.powf(-2.6);
let db1_dtr = 0.172 * 4.2 * tr.powf(-5.2);
1000.0 * R_GAS / comp.pc * (db0_dtr + comp.omega * db1_dtr)
}
pub fn z_factor_virial(comp: &Component, t: f64, p: f64) -> f64 {
let b = pitzer_b(comp, t); 1.0 + b * p / (1000.0 * R_GAS * t)
}
pub fn ln_phi_pure_virial(comp: &Component, t: f64, p: f64) -> f64 {
let b = pitzer_b(comp, t);
b * p / (1000.0 * R_GAS * t)
}
pub fn h_departure_rt_virial(comp: &Component, t: f64, p: f64) -> f64 {
let b = pitzer_b(comp, t);
let db_dt = pitzer_d_b_d_t(comp, t);
(b - t * db_dt) * p / (1000.0 * R_GAS * t)
}
pub fn s_departure_r_virial(comp: &Component, t: f64, p: f64) -> f64 {
let db_dt = pitzer_d_b_d_t(comp, t);
-p * db_dt / (1000.0 * R_GAS)
}
pub fn b_mix_matrix(components: &[Component], t: f64) -> Vec<Vec<f64>> {
let n = components.len();
let flat = b_mix_matrix_flat(components, t);
(0..n).map(|i| flat[i * n..(i + 1) * n].to_vec()).collect()
}
fn b_cross(components: &[Component], i: usize, j: usize, t: f64) -> f64 {
if i == j {
return pitzer_b(&components[i], t);
}
let tc_ij = (components[i].tc * components[j].tc).sqrt();
let pc_ij = 0.5 * (components[i].pc + components[j].pc);
let omega_ij = 0.5 * (components[i].omega + components[j].omega);
let tr = t / tc_ij;
let b_reduced = pitzer_b0(tr) + omega_ij * pitzer_b1(tr);
1000.0 * R_GAS * tc_ij / pc_ij * b_reduced
}
pub fn b_mix_matrix_flat(components: &[Component], t: f64) -> Vec<f64> {
let n = components.len();
let mut mat = vec![0.0_f64; n * n];
for i in 0..n {
for j in i..n {
let bij = b_cross(components, i, j, t);
mat[i * n + j] = bij;
mat[j * n + i] = bij;
}
}
mat
}
pub fn ln_phi_mix_virial_flat_into(
mat: &[f64],
x: &[f64],
t: f64,
p: f64,
row_dot: &mut [f64],
out: &mut [f64],
) {
let n = x.len();
let mut bmix = 0.0_f64;
for i in 0..n {
let row = &mat[i * n..(i + 1) * n];
let dot: f64 = row.iter().zip(x).map(|(&b, &xj)| b * xj).sum();
row_dot[i] = dot;
bmix += x[i] * dot;
}
let factor = p / (1000.0 * R_GAS * t);
for i in 0..n {
out[i] = factor * (2.0 * row_dot[i] - bmix);
}
}
pub fn b_mix(components: &[Component], mole_fractions: &[f64], t: f64) -> Result<f64, VirialError> {
if components.len() != mole_fractions.len() {
return Err(VirialError::InvalidInput(format!(
"components.len()={} but mole_fractions.len()={}",
components.len(),
mole_fractions.len()
)));
}
let mat = b_mix_matrix(components, t);
let n = components.len();
let mut acc = 0.0_f64;
for i in 0..n {
for j in 0..n {
acc += mole_fractions[i] * mole_fractions[j] * mat[i][j];
}
}
Ok(acc)
}
pub fn ln_phi_mix_virial(
components: &[Component],
mole_fractions: &[f64],
t: f64,
p: f64,
) -> Result<Vec<f64>, VirialError> {
let n = components.len();
if n != mole_fractions.len() {
return Err(VirialError::InvalidInput(format!(
"components.len()={} but mole_fractions.len()={}",
n,
mole_fractions.len()
)));
}
let mat = b_mix_matrix_flat(components, t);
let mut row_dot = vec![0.0; n];
let mut out = vec![0.0; n];
ln_phi_mix_virial_flat_into(&mat, mole_fractions, t, p, &mut row_dot, &mut out);
Ok(out)
}
pub fn ln_phi_mix_virial_with_matrix(
mat: &[Vec<f64>],
mole_fractions: &[f64],
t: f64,
p: f64,
) -> Vec<f64> {
let mut bmix = 0.0_f64;
for (row, x_i) in mat.iter().zip(mole_fractions.iter()) {
for (b_ij, x_j) in row.iter().zip(mole_fractions.iter()) {
bmix += x_i * x_j * b_ij;
}
}
let factor = p / (1000.0 * R_GAS * t);
mat.iter()
.map(|row| {
let sum_j: f64 = row
.iter()
.zip(mole_fractions.iter())
.map(|(b_ij, x_j)| b_ij * x_j)
.sum();
factor * (2.0 * sum_j - bmix)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_methane() -> Component {
Component {
name: "methane".into(),
tc: 190.564,
pc: 4599.0, omega: 0.0115,
..Component::default()
}
}
#[test]
fn flat_virial_matches_nested() {
let ethane = Component {
name: "ethane".into(),
tc: 305.32,
pc: 4872.0,
omega: 0.099,
..Component::default()
};
let propane = Component {
name: "propane".into(),
tc: 369.83,
pc: 4248.0,
omega: 0.152,
..Component::default()
};
let comps = [dummy_methane(), ethane, propane];
let n = comps.len();
let (t, p) = (300.0, 200.0);
let nested = b_mix_matrix(&comps, t);
let flat = b_mix_matrix_flat(&comps, t);
for i in 0..n {
for j in 0..n {
assert_eq!(flat[i * n + j], nested[i][j], "B[{i}][{j}]");
}
}
let x = [0.5, 0.3, 0.2];
let want = ln_phi_mix_virial_with_matrix(&nested, &x, t, p);
let mut row_dot = vec![0.0; n];
let mut got = vec![0.0; n];
ln_phi_mix_virial_flat_into(&flat, &x, t, p, &mut row_dot, &mut got);
for i in 0..n {
assert!(
(got[i] - want[i]).abs() <= 1e-13 * want[i].abs().max(1.0),
"comp {i}: flat={} nested={}",
got[i],
want[i]
);
}
}
#[test]
fn pitzer_b0_b1_at_critical() {
assert!((pitzer_b0(1.0) + 0.339).abs() < 1e-6);
assert!((pitzer_b1(1.0) + 0.033).abs() < 1e-6);
}
#[test]
fn virial_z_ideal_limit() {
let c = dummy_methane();
let z = z_factor_virial(&c, 300.0, 1.0); assert!((z - 1.0).abs() < 1e-3);
}
#[test]
fn ln_phi_mix_with_matrix_matches_one_shot() {
let ethane = Component {
name: "ethane".into(),
tc: 305.3,
pc: 4872.0,
omega: 0.0995,
..Component::default()
};
let comps = [dummy_methane(), ethane];
let x = [0.6, 0.4];
let (t, p) = (280.0, 800.0);
let one_shot = ln_phi_mix_virial(&comps, &x, t, p).unwrap();
let mat = b_mix_matrix(&comps, t);
let cached = ln_phi_mix_virial_with_matrix(&mat, &x, t, p);
for (a, b) in one_shot.iter().zip(cached.iter()) {
assert!((a - b).abs() < 1e-15, "one-shot {a} vs cached {b}");
}
}
}