use feos_core::FeosResult;
use feos_core::parameter::{CombiningRule, FromSegments, FromSegmentsBinary, Parameters};
use nalgebra::{DMatrix, DVector};
use num_traits::Zero;
use quantity::{JOULE, KB, KELVIN};
use serde::{Deserialize, Serialize};
use std::ops::AddAssign;
#[derive(Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct PcSaftRecord {
pub m: f64,
pub sigma: f64,
pub epsilon_k: f64,
#[serde(skip_serializing_if = "f64::is_zero")]
#[serde(default)]
pub mu: f64,
#[serde(skip_serializing_if = "f64::is_zero")]
#[serde(default)]
pub q: f64,
#[serde(skip_serializing_if = "Option::is_none")]
viscosity: Option<[f64; 4]>,
#[serde(skip_serializing_if = "Option::is_none")]
diffusion: Option<[f64; 5]>,
#[serde(skip_serializing_if = "Option::is_none")]
thermal_conductivity: Option<[f64; 4]>,
}
impl FromSegments for PcSaftRecord {
fn from_segments(segments: &[(Self, f64)]) -> FeosResult<Self> {
let mut m = 0.0;
let mut sigma3 = 0.0;
let mut epsilon_k = 0.0;
let mut mu = 0.0;
let mut q = 0.0;
segments.iter().for_each(|(s, n)| {
m += s.m * n;
sigma3 += s.m * s.sigma.powi(3) * n;
epsilon_k += s.m * s.epsilon_k * n;
mu += s.mu * n;
q += s.q * n;
});
let mut viscosity = if segments
.iter()
.all(|(record, _)| record.viscosity.is_some())
{
Some([0.0; 4])
} else {
None
};
let mut thermal_conductivity = if segments
.iter()
.all(|(record, _)| record.thermal_conductivity.is_some())
{
Some([0.0; 4])
} else {
None
};
let diffusion = if segments
.iter()
.all(|(record, _)| record.diffusion.is_some())
{
Some([0.0; 5])
} else {
None
};
let n_t = segments.iter().fold(0.0, |acc, (_, n)| acc + n);
segments.iter().for_each(|(s, n)| {
let s3 = s.m * s.sigma.powi(3) * n;
if let Some(p) = viscosity.as_mut() {
let [a, b, c, d] = s.viscosity.unwrap();
p[0] += s3 * a;
p[1] += s3 * b / sigma3.powf(0.45);
p[2] += n * c;
p[3] += n * d;
}
if let Some(p) = thermal_conductivity.as_mut() {
let [a, b, c, d] = s.thermal_conductivity.unwrap();
p[0] += n * a;
p[1] += n * b;
p[2] += n * c;
p[3] += n_t * d;
}
});
viscosity = viscosity.map(|v| [v[0] - 0.5 * m.ln(), v[1], v[2], v[3]]);
Ok(Self {
m,
sigma: (sigma3 / m).cbrt(),
epsilon_k: epsilon_k / m,
mu,
q,
viscosity,
diffusion,
thermal_conductivity,
})
}
}
impl PcSaftRecord {
#[expect(clippy::too_many_arguments)]
pub fn new(
m: f64,
sigma: f64,
epsilon_k: f64,
mu: f64,
q: f64,
viscosity: Option<[f64; 4]>,
diffusion: Option<[f64; 5]>,
thermal_conductivity: Option<[f64; 4]>,
) -> PcSaftRecord {
PcSaftRecord {
m,
sigma,
epsilon_k,
mu,
q,
viscosity,
diffusion,
thermal_conductivity,
}
}
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
pub struct PcSaftAssociationRecord {
pub kappa_ab: f64,
pub epsilon_k_ab: f64,
}
impl PcSaftAssociationRecord {
pub fn new(kappa_ab: f64, epsilon_k_ab: f64) -> Self {
Self {
kappa_ab,
epsilon_k_ab,
}
}
}
impl CombiningRule<PcSaftRecord> for PcSaftAssociationRecord {
fn combining_rule(
_: &PcSaftRecord,
_: &PcSaftRecord,
parameters_i: &Self,
parameters_j: &Self,
) -> Self {
let kappa_ab = (parameters_i.kappa_ab * parameters_j.kappa_ab).sqrt();
let epsilon_k_ab = 0.5 * (parameters_i.epsilon_k_ab + parameters_j.epsilon_k_ab);
Self {
kappa_ab,
epsilon_k_ab,
}
}
}
#[derive(Serialize, Deserialize, Clone, Copy, Default)]
pub struct PcSaftBinaryRecord {
pub k_ij: f64,
}
impl PcSaftBinaryRecord {
pub fn new(k_ij: f64) -> Self {
Self { k_ij }
}
}
impl FromSegmentsBinary for PcSaftBinaryRecord {
fn from_segments_binary(segments: &[(Self, f64, f64)]) -> FeosResult<Self> {
let (k_ij, n) = segments.iter().fold((0.0, 0.0), |(k_ij, n), (br, n1, n2)| {
let nab = n1 * n2;
(k_ij + br.k_ij * nab, n + nab)
});
Ok(Self { k_ij: k_ij / n })
}
}
pub type PcSaftParameters = Parameters<PcSaftRecord, PcSaftBinaryRecord, PcSaftAssociationRecord>;
pub struct PcSaftPars {
pub m: DVector<f64>,
pub sigma: DVector<f64>,
pub epsilon_k: DVector<f64>,
pub mu2: DVector<f64>,
pub q2: DVector<f64>,
pub sigma_ij: DMatrix<f64>,
pub epsilon_k_ij: DMatrix<f64>,
pub e_k_ij: DMatrix<f64>,
pub ndipole: usize,
pub nquadpole: usize,
pub dipole_comp: Vec<usize>,
pub quadpole_comp: Vec<usize>,
pub viscosity: Option<DMatrix<f64>>,
pub diffusion: Option<DMatrix<f64>>,
pub thermal_conductivity: Option<DMatrix<f64>>,
}
impl PcSaftPars {
pub fn new(parameters: &PcSaftParameters) -> Self {
let n = parameters.pure.len();
let [m, sigma, epsilon_k] = parameters.collate(|pr| [pr.m, pr.sigma, pr.epsilon_k]);
let [viscosity, thermal_conductivity] =
parameters.collate(|pr| [pr.viscosity, pr.thermal_conductivity]);
let [diffusion] = parameters.collate(|pr| [pr.diffusion]);
let [k_ij] = parameters.collate_binary(|br| [br.k_ij]);
let [mu2, q2] = parameters.collate(|pr| {
[
pr.mu * pr.mu / (pr.m * pr.sigma.powi(3) * pr.epsilon_k)
* 1e-19
* (JOULE / KELVIN / KB).into_value(),
pr.q * pr.q / (pr.m * pr.sigma.powi(5) * pr.epsilon_k)
* 1e-19
* (JOULE / KELVIN / KB).into_value(),
]
});
let dipole_comp: Vec<usize> = mu2
.iter()
.enumerate()
.filter_map(|(i, &mu2)| (mu2.abs() > 0.0).then_some(i))
.collect();
let ndipole = dipole_comp.len();
let quadpole_comp: Vec<usize> = q2
.iter()
.enumerate()
.filter_map(|(i, &q2)| (q2.abs() > 0.0).then_some(i))
.collect();
let nquadpole = quadpole_comp.len();
let mut sigma_ij = DMatrix::zeros(n, n);
let mut e_k_ij = DMatrix::zeros(n, n);
for i in 0..n {
for j in 0..n {
e_k_ij[(i, j)] = (epsilon_k[i] * epsilon_k[j]).sqrt();
sigma_ij[(i, j)] = 0.5 * (sigma[i] + sigma[j]);
}
}
let epsilon_k_ij = (-k_ij).add_scalar(1.0).component_mul(&e_k_ij);
let viscosity = if viscosity.iter().any(|v| v.is_none()) {
None
} else {
let mut v = DMatrix::zeros(4, viscosity.len());
for (i, vi) in viscosity.iter().enumerate() {
v.column_mut(i)
.add_assign(&DVector::from(vi.unwrap().to_vec()));
}
Some(v)
};
let diffusion = if diffusion.iter().any(|v| v.is_none()) {
None
} else {
let mut v = DMatrix::zeros(5, diffusion.len());
for (i, vi) in diffusion.iter().enumerate() {
v.column_mut(i)
.add_assign(&DVector::from(vi.unwrap().to_vec()));
}
Some(v)
};
let thermal_conductivity = if thermal_conductivity.iter().any(|v| v.is_none()) {
None
} else {
let mut v = DMatrix::zeros(4, thermal_conductivity.len());
for (i, vi) in thermal_conductivity.iter().enumerate() {
v.column_mut(i)
.add_assign(&DVector::from(vi.unwrap().to_vec()));
}
Some(v)
};
Self {
m,
sigma,
epsilon_k,
mu2,
q2,
sigma_ij,
epsilon_k_ij,
e_k_ij,
ndipole,
nquadpole,
dipole_comp,
quadpole_comp,
viscosity,
diffusion,
thermal_conductivity,
}
}
}
#[cfg(test)]
pub mod utils {
use super::*;
use crate::pcsaft::PcSaft;
use feos_core::parameter::{BinarySegmentRecord, ChemicalRecord, PureRecord, SegmentRecord};
pub fn propane_parameters() -> PcSaft {
let propane_json = r#"
{
"identifier": {
"cas": "74-98-6",
"name": "propane",
"iupac_name": "propane",
"smiles": "CCC",
"inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3",
"formula": "C3H8"
},
"m": 2.001829,
"sigma": 3.618353,
"epsilon_k": 208.1101,
"viscosity": [-0.8013, -1.9972,-0.2907, -0.0467],
"thermal_conductivity": [-0.15348, -0.6388, 1.21342, -0.01664],
"diffusion": [-0.675163251512047, 0.3212017677695878, 0.100175249144429, 0.0, 0.0],
"molarweight": 44.0962
}"#;
let propane_record: PureRecord<PcSaftRecord, PcSaftAssociationRecord> =
serde_json::from_str(propane_json).expect("Unable to parse json.");
PcSaft::new(PcSaftParameters::new_pure(propane_record).unwrap())
}
pub fn carbon_dioxide_parameters() -> PcSaftPars {
let co2_json = r#"
{
"identifier": {
"cas": "124-38-9",
"name": "carbon-dioxide",
"iupac_name": "carbon dioxide",
"smiles": "O=C=O",
"inchi": "InChI=1/CO2/c2-1-3",
"formula": "CO2"
},
"molarweight": 44.0098,
"m": 1.5131,
"sigma": 3.1869,
"epsilon_k": 163.333,
"q": 4.4
}"#;
let co2_record: PureRecord<PcSaftRecord, PcSaftAssociationRecord> =
serde_json::from_str(co2_json).expect("Unable to parse json.");
PcSaftPars::new(&PcSaftParameters::new_pure(co2_record).unwrap())
}
pub fn butane_parameters() -> PcSaft {
let butane_json = r#"
{
"identifier": {
"cas": "106-97-8",
"name": "butane",
"iupac_name": "butane",
"smiles": "CCCC",
"inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3",
"formula": "C4H10"
},
"m": 2.331586,
"sigma": 3.7086010000000003,
"epsilon_k": 222.8774,
"molarweight": 58.123
}"#;
let butane_record: PureRecord<PcSaftRecord, PcSaftAssociationRecord> =
serde_json::from_str(butane_json).expect("Unable to parse json.");
PcSaft::new(PcSaftParameters::new_pure(butane_record).unwrap())
}
pub fn nonane_parameters() -> PcSaft {
let nonane_json = r#"
{
"identifier": {
"cas": "111-84-2",
"name": "nonane",
"iupac_name": "nonane",
"smiles": "CCCCCCCCC",
"inchi": "InChI=1S/C9H20/c1-3-5-7-9-8-6-4-2/h3-9H2,1-2H3",
"formula": "C9H20"
},
"molarweight": 128.258,
"m": 4.2079,
"sigma": 3.8448,
"epsilon_k": 244.51,
"viscosity": [
-1.4629,
-3.0058,
-0.5842,
-0.1222
]
}"#;
let nonane_record: PureRecord<PcSaftRecord, PcSaftAssociationRecord> =
serde_json::from_str(nonane_json).expect("Unable to parse json.");
PcSaft::new(PcSaftParameters::new_pure(nonane_record).unwrap())
}
pub fn dme_parameters() -> PcSaftPars {
let dme_json = r#"
{
"identifier": {
"cas": "115-10-6",
"name": "dimethyl-ether",
"iupac_name": "methoxymethane",
"smiles": "COC",
"inchi": "InChI=1/C2H6O/c1-3-2/h1-2H3",
"formula": "C2H6O"
},
"m": 2.2634,
"sigma": 3.2723,
"epsilon_k": 210.29,
"mu": 1.3,
"molarweight": 46.0688
}"#;
let dme_record: PureRecord<PcSaftRecord, PcSaftAssociationRecord> =
serde_json::from_str(dme_json).expect("Unable to parse json.");
PcSaftPars::new(&PcSaftParameters::new_pure(dme_record).unwrap())
}
pub fn water_parameters(na: f64) -> PcSaftParameters {
let water_json = r#"
{
"identifier": {
"cas": "7732-18-5",
"name": "water_np",
"iupac_name": "oxidane",
"smiles": "O",
"inchi": "InChI=1/H2O/h1H2",
"formula": "H2O"
},
"m": 1.065587,
"sigma": 3.000683,
"epsilon_k": 366.5121,
"molarweight": 18.0152,
"association_sites": [
{
"kappa_ab": 0.034867983,
"epsilon_k_ab": 2500.6706,
"na": 1.0,
"nb": 1.0
}
]
}"#;
let mut water_record: PureRecord<PcSaftRecord, PcSaftAssociationRecord> =
serde_json::from_str(water_json).expect("Unable to parse json.");
water_record.association_sites[0].na = na;
PcSaftParameters::new_pure(water_record).unwrap()
}
pub fn dme_co2_parameters() -> PcSaftPars {
let binary_json = r#"[
{
"identifier": {
"cas": "115-10-6",
"name": "dimethyl-ether",
"iupac_name": "methoxymethane",
"smiles": "COC",
"inchi": "InChI=1/C2H6O/c1-3-2/h1-2H3",
"formula": "C2H6O"
},
"molarweight": 46.0688,
"m": 2.2634,
"sigma": 3.2723,
"epsilon_k": 210.29,
"mu": 1.3
},
{
"identifier": {
"cas": "124-38-9",
"name": "carbon-dioxide",
"iupac_name": "carbon dioxide",
"smiles": "O=C=O",
"inchi": "InChI=1/CO2/c2-1-3",
"formula": "CO2"
},
"molarweight": 44.0098,
"m": 1.5131,
"sigma": 3.1869,
"epsilon_k": 163.333,
"q": 4.4
}
]"#;
let binary_record: [PureRecord<PcSaftRecord, PcSaftAssociationRecord>; 2] =
serde_json::from_str(binary_json).expect("Unable to parse json.");
PcSaftPars::new(&PcSaftParameters::new_binary(binary_record, None, vec![]).unwrap())
}
pub fn propane_butane_parameters() -> PcSaft {
let binary_json = r#"[
{
"identifier": {
"cas": "74-98-6",
"name": "propane",
"iupac_name": "propane",
"smiles": "CCC",
"inchi": "InChI=1/C3H8/c1-3-2/h3H2,1-2H3",
"formula": "C3H8"
},
"m": 2.0018290000000003,
"sigma": 3.618353,
"epsilon_k": 208.1101,
"viscosity": [-0.8013, -1.9972, -0.2907, -0.0467],
"thermal_conductivity": [-0.15348, -0.6388, 1.21342, -0.01664],
"diffusion": [-0.675163251512047, 0.3212017677695878, 0.100175249144429, 0.0, 0.0],
"molarweight": 44.0962
},
{
"identifier": {
"cas": "106-97-8",
"name": "butane",
"iupac_name": "butane",
"smiles": "CCCC",
"inchi": "InChI=1/C4H10/c1-3-4-2/h3-4H2,1-2H3",
"formula": "C4H10"
},
"m": 2.331586,
"sigma": 3.7086010000000003,
"epsilon_k": 222.8774,
"viscosity": [-0.9763, -2.2413, -0.3690, -0.0605],
"diffusion": [-0.8985872992958458, 0.3428584416613513, 0.10236616087103916, 0.0, 0.0],
"molarweight": 58.123
}
]"#;
let binary_record: [PureRecord<PcSaftRecord, PcSaftAssociationRecord>; 2] =
serde_json::from_str(binary_json).expect("Unable to parse json.");
PcSaft::new(PcSaftParameters::new_binary(binary_record, None, vec![]).unwrap())
}
pub fn nonane_heptane_parameters() -> PcSaft {
let binary_json = r#"[
{
"identifier": {
"cas": "111-84-2",
"name": "nonane",
"iupac_name": "nonane",
"smiles": "CCCCCCCCC",
"inchi": "InChI=1S/C9H20/c1-3-5-7-9-8-6-4-2/h3-9H2,1-2H3",
"formula": "C9H20"
},
"molarweight": 128.258,
"m": 4.2079,
"sigma": 3.8448,
"epsilon_k": 244.51,
"viscosity": [
-1.4629,
-3.0058,
-0.5842,
-0.1222
]
},
{
"identifier": {
"cas": "142-82-5",
"name": "heptane",
"iupac_name": "heptane",
"smiles": "CCCCCCC",
"inchi": "InChI=1S/C7H16/c1-3-5-7-6-4-2/h3-7H2,1-2H3",
"formula": "C7H16"
},
"molarweight": 100.204,
"m": 3.4831,
"sigma": 3.8049,
"epsilon_k": 238.4,
"viscosity": [
-1.2979,
-2.6936,
-0.4951,
-0.0988
]
}
]"#;
let binary_record: [PureRecord<PcSaftRecord, PcSaftAssociationRecord>; 2] =
serde_json::from_str(binary_json).expect("Unable to parse json.");
PcSaft::new(PcSaftParameters::new_binary(binary_record, None, vec![]).unwrap())
}
#[test]
pub fn test_kij() -> FeosResult<()> {
let ch3: String = "CH3".into();
let ch2: String = "CH2".into();
let oh = "OH".into();
let propane = ChemicalRecord::new(
Default::default(),
vec![ch3.clone(), ch2.clone(), ch3.clone()],
None,
);
let ethanol = ChemicalRecord::new(Default::default(), vec![ch3, ch2, oh], None);
let segment_records =
SegmentRecord::from_json("../../parameters/pcsaft/sauer2014_homo.json")?;
let kij = [("CH3", "OH", -0.2), ("CH2", "OH", -0.1)];
let binary_segment_records: Vec<_> = kij
.iter()
.map(|&(id1, id2, k_ij)| {
BinarySegmentRecord::new(id1.into(), id2.into(), Some(PcSaftBinaryRecord { k_ij }))
})
.collect();
let params = PcSaftParameters::from_segments(
vec![propane, ethanol],
&segment_records,
Some(&binary_segment_records),
)?;
assert_eq!(params.binary[0].id1, 0);
assert_eq!(params.binary[0].id2, 1);
assert_eq!(params.binary[0].model_record.k_ij, -0.5 / 9.);
Ok(())
}
#[test]
fn test_association_json() -> FeosResult<()> {
let json1 = r#"
{
"identifier": {
"name": "comp1"
},
"m": 1.065587,
"sigma": 3.000683,
"epsilon_k": 366.5121,
"association_sites": [
{
"kappa_ab": 0.034867983,
"epsilon_k_ab": 2500.6706,
"na": 1.0,
"nb": 1.0
}
]
}"#;
let record1: PureRecord<PcSaftRecord, PcSaftAssociationRecord> =
serde_json::from_str(json1)?;
let json2 = r#"
{
"identifier": {
"name": "comp2"
},
"m": 1.065587,
"sigma": 3.000683,
"epsilon_k": 366.5121,
"association_sites": [
{
"id": "site1",
"kappa_ab": 0.034867983,
"epsilon_k_ab": 2500.6706,
"na": 1.0,
"nb": 1.0
},
{
"id": "site2",
"kappa_ab": 0.01,
"epsilon_k_ab": 2200.0,
"nb": 1.0
}
]
}"#;
let record2: PureRecord<PcSaftRecord, PcSaftAssociationRecord> =
serde_json::from_str(json2)?;
assert_eq!(
record1.association_sites[0].parameters.unwrap().kappa_ab,
record2.association_sites[0].parameters.unwrap().kappa_ab
);
Ok(())
}
}