use crate::types::{Result, StagesError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int))]
pub enum CondenserKind {
#[default]
Total,
Partial,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct Feed {
pub rate: f64,
pub z: f64,
pub q: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "python", pyo3::pyclass(get_all))]
pub struct BinaryColumn {
pub pressure: f64,
pub condenser: CondenserKind,
pub feed: Feed,
pub x_distillate: f64,
pub x_bottoms: f64,
}
impl BinaryColumn {
pub fn validate(&self) -> Result<()> {
let Feed { rate, z, q: _ } = self.feed;
if rate.is_nan() || rate <= 0.0 {
return Err(StagesError::Dimension(format!(
"feed rate must be positive, got {rate} kmol/h"
)));
}
for (name, v) in [
("z_F", z),
("x_D", self.x_distillate),
("x_B", self.x_bottoms),
] {
if !(0.0..=1.0).contains(&v) {
return Err(StagesError::Dimension(format!(
"{name} must be a mole fraction in [0, 1], got {v}"
)));
}
}
if !(self.x_bottoms < z && z < self.x_distillate) {
return Err(StagesError::Dimension(format!(
"specs must satisfy x_B < z_F < x_D, got x_B = {}, z_F = {z}, x_D = {}",
self.x_bottoms, self.x_distillate
)));
}
Ok(())
}
pub fn distillate_rate(&self) -> Result<f64> {
self.validate()?;
Ok(self.feed.rate * (self.feed.z - self.x_bottoms) / (self.x_distillate - self.x_bottoms))
}
pub fn bottoms_rate(&self) -> Result<f64> {
Ok(self.feed.rate - self.distillate_rate()?)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn column() -> BinaryColumn {
BinaryColumn {
pressure: 101.325,
condenser: CondenserKind::Total,
feed: Feed {
rate: 100.0,
z: 0.5,
q: 1.0,
},
x_distillate: 0.95,
x_bottoms: 0.05,
}
}
#[test]
fn material_balance_closes() {
let c = column();
let d = c.distillate_rate().unwrap();
let b = c.bottoms_rate().unwrap();
assert!((d - 50.0).abs() < 1e-12, "D = {d}");
assert!((d + b - c.feed.rate).abs() < 1e-12);
let light = d * c.x_distillate + b * c.x_bottoms;
assert!((light - c.feed.rate * c.feed.z).abs() < 1e-12);
}
#[test]
fn misordered_specs_rejected() {
let mut c = column();
c.x_bottoms = 0.6; assert!(matches!(
c.distillate_rate(),
Err(StagesError::Dimension(_))
));
}
}