use crate::saturation::SatPressureModel;
use crate::types::Component;
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::OnceLock;
const COMPONENTS_JSON: &str = include_str!("../data/components.json");
#[derive(Deserialize)]
#[serde(default)]
struct RawComponent {
formula: String,
cas: String,
mw: f64,
tc: f64,
pc: f64,
omega: f64,
zc: f64,
vc: f64,
tb: f64,
psat_coeffs: Vec<f64>,
cp_coeffs: Vec<f64>,
liquid_volume: f64,
}
impl Default for RawComponent {
fn default() -> Self {
Self {
formula: String::new(),
cas: String::new(),
mw: 0.0,
tc: 0.0,
pc: 0.0,
omega: 0.0,
zc: 0.0,
vc: 0.0,
tb: 0.0,
psat_coeffs: Vec::new(),
cp_coeffs: Vec::new(),
liquid_volume: 0.0,
}
}
}
#[derive(Deserialize)]
struct RawDb {
compounds: HashMap<String, RawComponent>,
}
fn to_component(name: &str, raw: &RawComponent) -> Component {
let mut cp_coeffs = [0.0f64; 5];
for (slot, &v) in cp_coeffs.iter_mut().zip(raw.cp_coeffs.iter()) {
*slot = v;
}
Component {
name: name.to_string(),
tc: raw.tc,
pc: raw.pc,
vc: raw.vc,
zc: raw.zc,
omega: raw.omega,
tb: raw.tb,
mw: raw.mw,
cp_coeffs,
psat_coeffs: raw.psat_coeffs.clone(),
liquid_volume: raw.liquid_volume,
sat_model: SatPressureModel::Antoine,
..Component::default()
}
}
fn db() -> &'static HashMap<String, Component> {
static DB: OnceLock<HashMap<String, Component>> = OnceLock::new();
DB.get_or_init(|| {
let raw: RawDb =
serde_json::from_str(COMPONENTS_JSON).expect("bundled components.json must parse");
raw.compounds
.iter()
.map(|(name, rc)| (name.to_lowercase(), to_component(name, rc)))
.collect()
})
}
pub fn component(name: &str) -> Option<Component> {
db().get(&name.trim().to_lowercase()).cloned()
}
pub fn available() -> Vec<String> {
let mut names: Vec<String> = db().values().map(|c| c.name.clone()).collect();
names.sort();
names
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_25_compounds_parse() {
assert_eq!(available().len(), 25);
}
#[test]
fn lookup_hit_case_and_whitespace_insensitive() {
let a = component("water").unwrap();
let b = component("Water").unwrap();
let c = component(" WATER ").unwrap();
assert_eq!(a.name, "water");
assert_eq!(a.tc, b.tc);
assert_eq!(a.tc, c.tc);
}
#[test]
fn lookup_miss_returns_none() {
assert!(component("unobtainium").is_none());
assert!(component("H2O").is_none());
}
#[test]
fn spot_check_legacy_compound_vs_json_literals() {
let benzene = component("benzene").unwrap();
assert_eq!(benzene.tc, 562.02);
assert_eq!(benzene.pc, 4907.277);
assert_eq!(benzene.omega, 0.211);
}
#[test]
fn spot_check_new_compound_vs_json_literals() {
let toluene = component("toluene").unwrap();
assert_eq!(toluene.tc, 591.75);
assert_eq!(toluene.pc, 4126.3);
assert_eq!(toluene.omega, 0.2657);
}
#[test]
fn spot_check_ammonia_vs_json_literals() {
let nh3 = component("ammonia").unwrap();
assert_eq!(nh3.tc, 405.56);
assert_eq!(nh3.pc, 11363.4);
assert_eq!(nh3.omega, 0.256);
assert_eq!(nh3.mw, 17.03052);
assert_eq!(nh3.psat_coeffs, vec![5.595032, 2132.497737, -32.98]);
assert_eq!(nh3.liquid_volume, 28.24);
assert_eq!(nh3.sat_model, SatPressureModel::Antoine);
assert!(nh3.cp_coeffs[0] != 0.0);
}
#[test]
fn mapped_fields_are_populated_and_defaults_left_zero() {
let toluene = component("toluene").unwrap();
assert_eq!(toluene.psat_coeffs, vec![5.606494, 3056.958021, -55.525]);
assert_eq!(toluene.cp_coeffs.len(), 5);
assert!(toluene.cp_coeffs[0] != 0.0);
assert_eq!(toluene.sat_model, SatPressureModel::Antoine);
assert_eq!(toluene.dipole_moment, 0.0);
assert_eq!(toluene.zra, 0.0);
assert_eq!(toluene.prsv_k1, 0.0);
}
#[test]
fn available_is_sorted() {
let names = available();
let mut sorted = names.clone();
sorted.sort();
assert_eq!(names, sorted);
}
}