use super::FlashError;
use super::isothermal::{FlashResult, flash_isothermal_warm};
use super::system::SystemSpec;
use crate::energy::phase_enthalpy_entropy;
use crate::eos::{CubicEos, LiquidModel, PhaseId, VaporModel};
#[derive(Debug, Clone, PartialEq)]
pub struct AdiabaticResult {
pub t: f64,
pub flash: FlashResult,
pub enthalpy: f64,
}
fn cubic_pair(spec: &SystemSpec) -> Result<(CubicEos, CubicEos), FlashError> {
match (spec.liquid, spec.vapor) {
(LiquidModel::Cubic(le), VaporModel::Cubic(ve)) => Ok((le, ve)),
_ => Err(FlashError::Unsupported(
"adiabatic flash currently requires a cubic EOS for both phases (φ-φ)".into(),
)),
}
}
fn stream_enthalpy(
spec: &SystemSpec,
t: f64,
p: f64,
res: &FlashResult,
t_ref: f64,
p_ref: f64,
) -> Result<f64, FlashError> {
let (le, ve) = cubic_pair(spec)?;
let to_thermo = |e: crate::mixture::MixError| FlashError::Thermo(e.to_string());
let (h_liq, _) = phase_enthalpy_entropy(
&spec.mixture_spec(le),
t,
p,
&res.x,
PhaseId::Liquid,
t_ref,
p_ref,
&[],
&[],
)
.map_err(to_thermo)?;
let (h_vap, _) = phase_enthalpy_entropy(
&spec.mixture_spec(ve),
t,
p,
&res.y,
PhaseId::Vapor,
t_ref,
p_ref,
&[],
&[],
)
.map_err(to_thermo)?;
Ok(res.beta * h_vap + (1.0 - res.beta) * h_liq)
}
#[allow(clippy::too_many_arguments)]
pub fn flash_adiabatic(
spec: &SystemSpec,
p: f64,
z: &[f64],
h_feed: f64,
t_ref: f64,
p_ref: f64,
t_lo: f64,
t_hi: f64,
tol: f64,
max_iter: usize,
) -> Result<AdiabaticResult, FlashError> {
let n = spec.n();
if z.len() != n {
return Err(FlashError::Dimension(format!(
"components={n}, z={}",
z.len()
)));
}
cubic_pair(spec)?;
let mut warm: Option<Vec<f64>> = None;
let eval = |t: f64, warm: &mut Option<Vec<f64>>| -> Result<(f64, FlashResult), FlashError> {
let res = flash_isothermal_warm(spec, t, p, z, warm.as_deref(), 1e-10, 200)?;
*warm = Some(res.k.clone());
let h = stream_enthalpy(spec, t, p, &res, t_ref, p_ref)?;
Ok((h - h_feed, res))
};
let (mut g_lo, _) = eval(t_lo, &mut warm)?;
let (mut g_hi, _) = eval(t_hi, &mut warm)?;
if g_lo * g_hi > 0.0 {
return Err(FlashError::NoConvergence {
what: "adiabatic flash bracket (H_feed not within [H(t_lo), H(t_hi)])",
iters: 0,
residual: g_lo.abs().min(g_hi.abs()),
});
}
let _ = &mut g_hi;
let (mut lo, mut hi) = (t_lo, t_hi);
for iter in 0..max_iter {
let mid = 0.5 * (lo + hi);
let (g_mid, res) = eval(mid, &mut warm)?;
if g_mid.abs() < tol || (hi - lo) < 1e-8 {
let h = g_mid + h_feed;
return Ok(AdiabaticResult {
t: mid,
flash: res,
enthalpy: h,
});
}
if g_mid * g_lo > 0.0 {
lo = mid;
g_lo = g_mid;
} else {
hi = mid;
}
if iter + 1 == max_iter {
return Err(FlashError::NoConvergence {
what: "adiabatic flash",
iters: max_iter,
residual: g_mid.abs(),
});
}
}
unreachable!("loop returns via convergence or NoConvergence")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::flash::isothermal::flash_isothermal;
use crate::mixing::MixingRule;
use crate::types::Component;
fn n_pentane() -> Component {
Component {
name: "n-pentane".into(),
tc: 469.7,
pc: 3370.0,
omega: 0.252,
cp_coeffs: [1.5, 4.0e-2, -1.2e-5, 0.0, 0.0],
..Component::default()
}
}
fn n_decane() -> Component {
Component {
name: "n-decane".into(),
tc: 617.7,
pc: 2110.0,
omega: 0.4884,
cp_coeffs: [2.0, 8.0e-2, -2.4e-5, 0.0, 0.0],
..Component::default()
}
}
fn pr(components: &[Component]) -> SystemSpec<'_> {
SystemSpec {
components,
vapor: VaporModel::Cubic(CubicEos::PR1976),
liquid: LiquidModel::Cubic(CubicEos::PR1976),
mixing_rule: MixingRule::Classical,
kij: &[],
aij: &[],
alpha: &[],
vl: &[],
delta: &[],
sat_models: &[],
ge_model: None,
}
}
#[test]
fn adiabatic_recovers_the_temperature_of_a_known_enthalpy() {
let comps = [n_pentane(), n_decane()];
let spec = pr(&comps);
let z = [0.5, 0.5];
let (p, t_star) = (500.0, 450.0);
let res_star = flash_isothermal(&spec, t_star, p, &z, 1e-10, 200).unwrap();
assert!(res_star.two_phase, "reference point should be two-phase");
let h_star = stream_enthalpy(&spec, t_star, p, &res_star, 298.15, 101.325).unwrap();
let out = flash_adiabatic(
&spec, p, &z, h_star, 298.15, 101.325, 400.0, 500.0, 1e-4, 200,
)
.unwrap();
assert!(
(out.t - t_star).abs() < 0.05,
"recovered T={} vs true {t_star}",
out.t
);
assert!((out.enthalpy - h_star).abs() < 1e-3);
}
#[test]
fn adiabatic_enthalpy_matches_target() {
let comps = [n_pentane(), n_decane()];
let spec = pr(&comps);
let z = [0.4, 0.6];
let lo = flash_isothermal(&spec, 420.0, 500.0, &z, 1e-10, 200).unwrap();
let hi = flash_isothermal(&spec, 480.0, 500.0, &z, 1e-10, 200).unwrap();
let h_lo = stream_enthalpy(&spec, 420.0, 500.0, &lo, 298.15, 101.325).unwrap();
let h_hi = stream_enthalpy(&spec, 480.0, 500.0, &hi, 298.15, 101.325).unwrap();
let target = 0.5 * (h_lo + h_hi);
let out = flash_adiabatic(
&spec, 500.0, &z, target, 298.15, 101.325, 420.0, 480.0, 1e-4, 200,
)
.unwrap();
assert!((out.enthalpy - target).abs() < 1e-3);
assert!(out.t > 420.0 && out.t < 480.0);
}
#[test]
fn adiabatic_rejects_non_cubic_system() {
let comps = [n_pentane(), n_decane()];
let mut spec = pr(&comps);
spec.liquid = LiquidModel::IdealSolution;
assert!(matches!(
flash_adiabatic(
&spec,
300.0,
&[0.5, 0.5],
0.0,
298.15,
101.325,
300.0,
400.0,
1e-4,
100
),
Err(FlashError::Unsupported(_))
));
}
}