use super::FlashError;
use super::incipient::{Point, solve_pressure, solve_temperature};
use super::system::SystemSpec;
#[derive(Debug, Clone, PartialEq)]
pub struct SaturationResult {
pub value: f64,
pub incipient: Vec<f64>,
pub k: Vec<f64>,
}
pub fn bubble_pressure(
spec: &SystemSpec,
t: f64,
x: &[f64],
tol: f64,
max_iter: usize,
) -> Result<SaturationResult, FlashError> {
check_len(spec, x)?;
let sp = solve_pressure(spec, t, x, Point::Bubble, tol, max_iter)?;
Ok(SaturationResult {
value: sp.var,
incipient: sp.incipient,
k: sp.k,
})
}
pub fn bubble_temperature(
spec: &SystemSpec,
p: f64,
x: &[f64],
tol: f64,
max_iter: usize,
) -> Result<SaturationResult, FlashError> {
check_len(spec, x)?;
let sp = solve_temperature(spec, p, x, Point::Bubble, tol, max_iter)?;
Ok(SaturationResult {
value: sp.var,
incipient: sp.incipient,
k: sp.k,
})
}
fn check_len(spec: &SystemSpec, phase: &[f64]) -> Result<(), FlashError> {
if phase.len() != spec.n() {
return Err(FlashError::Dimension(format!(
"components={}, composition={}",
spec.n(),
phase.len()
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::activity::ActivityModel;
use crate::eos::{CubicEos, LiquidModel, VaporModel};
use crate::flash::system::k_values;
use crate::mixing::MixingRule;
use crate::types::Component;
fn n_butane() -> Component {
Component {
name: "n-butane".into(),
tc: 425.12,
pc: 3796.0,
omega: 0.200,
tb: 272.65,
psat_coeffs: vec![4.35, 2277.0, -30.0],
..Component::default()
}
}
fn n_heptane() -> Component {
Component {
name: "n-heptane".into(),
tc: 540.2,
pc: 2740.0,
omega: 0.350,
tb: 371.6,
psat_coeffs: vec![4.02, 2911.0, -56.0],
..Component::default()
}
}
fn rks(components: &[Component]) -> SystemSpec<'_> {
SystemSpec {
components,
vapor: VaporModel::Cubic(CubicEos::RKS1972),
liquid: LiquidModel::Cubic(CubicEos::RKS1972),
mixing_rule: MixingRule::Classical,
kij: &[],
aij: &[],
alpha: &[],
vl: &[],
delta: &[],
sat_models: &[],
ge_model: None,
}
}
#[test]
fn bubble_pressure_satisfies_saturation_condition() {
let comps = [n_butane(), n_heptane()];
let spec = rks(&comps);
let x = [0.4, 0.6];
let res = bubble_pressure(&spec, 400.0, &x, 1e-10, 200).unwrap();
let k = k_values(&spec, 400.0, res.value, &x, &res.incipient).unwrap();
let s: f64 = (0..2).map(|i| k[i] * x[i]).sum();
assert!((s - 1.0).abs() < 1e-7, "Σ Kx = {s}");
assert!((res.incipient.iter().sum::<f64>() - 1.0).abs() < 1e-9);
assert!(res.value > 0.0);
}
#[test]
fn bubble_temperature_satisfies_saturation_condition() {
let comps = [n_butane(), n_heptane()];
let spec = rks(&comps);
let x = [0.4, 0.6];
let res = bubble_temperature(&spec, 1000.0, &x, 1e-9, 200).unwrap();
let k = k_values(&spec, res.value, 1000.0, &x, &res.incipient).unwrap();
let s: f64 = (0..2).map(|i| k[i] * x[i]).sum();
assert!((s - 1.0).abs() < 1e-5, "Σ Kx = {s} at T={}", res.value);
assert!(res.value > 250.0 && res.value < 600.0, "T={}", res.value);
}
#[test]
fn bubble_temperature_close_boiling_phi_phi() {
let benzene = Component {
name: "benzene".into(),
tc: 562.02,
pc: 4907.277,
omega: 0.211,
tb: 353.219,
..Component::default()
};
let cyclohexane = Component {
name: "cyclohexane".into(),
tc: 553.6,
pc: 4080.5,
omega: 0.2096,
tb: 353.865,
..Component::default()
};
let comps = [benzene, cyclohexane];
let spec = rks(&comps);
for i in 0..=10 {
let x1 = 0.001 + 0.998 * i as f64 / 10.0;
let x = [x1, 1.0 - x1];
let res = bubble_temperature(&spec, 101.325, &x, 1e-9, 200)
.unwrap_or_else(|e| panic!("x1={x1}: {e}"));
let k = k_values(&spec, res.value, 101.325, &x, &res.incipient).unwrap();
let s: f64 = (0..2).map(|j| k[j] * x[j]).sum();
assert!((s - 1.0).abs() < 1e-4, "x1={x1}: Σ Kx = {s}");
assert!(
res.value > 345.0 && res.value < 362.0,
"x1={x1}: bubble T {} K off the ~353 K band",
res.value
);
}
}
#[test]
fn bubble_pressure_between_pure_component_vapor_pressures() {
let comps = [n_butane(), n_heptane()];
let spec = rks(&comps);
let t = 380.0;
let res = bubble_pressure(&spec, t, &[0.5, 0.5], 1e-10, 200).unwrap();
let p1 = crate::saturation::psat(comps[0].sat_model, &comps[0], t).unwrap();
let p2 = crate::saturation::psat(comps[1].sat_model, &comps[1], t).unwrap();
let (lo, hi) = (p1.min(p2), p1.max(p2));
assert!(
res.value > lo * 0.5 && res.value < hi * 2.0,
"bubble P {} outside plausible band [{lo}, {hi}]",
res.value
);
}
#[test]
fn bubble_pressure_gamma_phi_van_laar() {
let a = Component {
name: "methanol".into(),
tc: 512.6,
pc: 8097.0,
omega: 0.564,
liquid_volume: 40.7,
psat_coeffs: vec![5.20, 3200.0, -35.0],
..Component::default()
};
let b = Component {
name: "water".into(),
tc: 647.1,
pc: 22064.0,
omega: 0.344,
liquid_volume: 18.07,
psat_coeffs: vec![5.11, 3800.0, -46.0],
..Component::default()
};
let comps = [a, b];
let aij = vec![vec![0.0, 0.5], vec![0.9, 0.0]];
let spec = SystemSpec {
components: &comps,
vapor: VaporModel::IdealGas,
liquid: LiquidModel::Activity(ActivityModel::VanLaar),
mixing_rule: MixingRule::Classical,
kij: &[],
aij: &aij,
alpha: &[],
vl: &[],
delta: &[],
sat_models: &[],
ge_model: None,
};
let x = [0.5, 0.5];
let res = bubble_pressure(&spec, 298.15, &x, 1e-10, 200).unwrap();
let k = k_values(&spec, 298.15, res.value, &x, &res.incipient).unwrap();
let s: f64 = (0..2).map(|i| k[i] * x[i]).sum();
assert!((s - 1.0).abs() < 1e-7, "Σ Kx = {s}");
}
}