From 1addcfd99a985bc27b0fe12ed8449ed5eb0ecfbb Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Tue, 5 Mar 2024 16:06:45 +0100 Subject: [PATCH 01/23] added ePC-SAFT --- Cargo.toml | 3 +- src/eos.rs | 5 + src/epcsaft/association/mod.rs | 744 ++++++++++++++++++++ src/epcsaft/association/python.rs | 49 ++ src/epcsaft/eos/born.rs | 46 ++ src/epcsaft/eos/dispersion.rs | 218 ++++++ src/epcsaft/eos/hard_chain.rs | 83 +++ src/epcsaft/eos/ionic.rs | 91 +++ src/epcsaft/eos/mod.rs | 585 ++++++++++++++++ src/epcsaft/eos/permittivity.rs | 275 ++++++++ src/epcsaft/hard_sphere/mod.rs | 138 ++++ src/epcsaft/mod.rs | 16 + src/epcsaft/parameters.rs | 1064 +++++++++++++++++++++++++++++ src/epcsaft/python.rs | 343 ++++++++++ src/lib.rs | 2 + src/python/eos.rs | 51 ++ src/python/mod.rs | 6 + 17 files changed, 3718 insertions(+), 1 deletion(-) create mode 100644 src/epcsaft/association/mod.rs create mode 100644 src/epcsaft/association/python.rs create mode 100644 src/epcsaft/eos/born.rs create mode 100644 src/epcsaft/eos/dispersion.rs create mode 100644 src/epcsaft/eos/hard_chain.rs create mode 100644 src/epcsaft/eos/ionic.rs create mode 100644 src/epcsaft/eos/mod.rs create mode 100644 src/epcsaft/eos/permittivity.rs create mode 100644 src/epcsaft/hard_sphere/mod.rs create mode 100644 src/epcsaft/mod.rs create mode 100644 src/epcsaft/parameters.rs create mode 100644 src/epcsaft/python.rs diff --git a/Cargo.toml b/Cargo.toml index 87e4d6e3a..511b8ee3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ dft = ["feos-dft", "petgraph"] estimator = [] association = [] pcsaft = ["association"] +epcsaft = ["assocation"] gc_pcsaft = ["association"] uvtheory = ["lazy_static"] pets = [] @@ -69,7 +70,7 @@ saftvrqmie = [] saftvrmie = [] rayon = ["dep:rayon", "ndarray/rayon", "feos-core/rayon", "feos-dft?/rayon"] python = ["pyo3", "numpy", "quantity/python", "feos-core/python", "feos-dft?/python", "rayon"] -all_models = ["dft", "estimator", "pcsaft", "gc_pcsaft", "uvtheory", "pets", "saftvrqmie", "saftvrmie"] +all_models = ["dft", "estimator", "pcsaft", "epcsaft", "gc_pcsaft", "uvtheory", "pets", "saftvrqmie", "saftvrmie"] [[bench]] name = "state_properties" diff --git a/src/eos.rs b/src/eos.rs index a94adb4e0..99be30e96 100644 --- a/src/eos.rs +++ b/src/eos.rs @@ -2,6 +2,8 @@ use crate::gc_pcsaft::GcPcSaft; #[cfg(feature = "pcsaft")] use crate::pcsaft::PcSaft; +#[cfg(feature = "epcsaft")] +use crate::epcsaft::ElectrolytePcSaft; #[cfg(feature = "pets")] use crate::pets::Pets; #[cfg(feature = "saftvrmie")] @@ -29,6 +31,9 @@ pub enum ResidualModel { #[cfg(feature = "pcsaft")] #[implement(entropy_scaling)] PcSaft(PcSaft), + #[cfg(feature = "epcsaft")] + #[implement(entropy_scaling)] + ElectrolytePcSaft(ElectrolytePcSaft), #[cfg(feature = "gc_pcsaft")] GcPcSaft(GcPcSaft), PengRobinson(PengRobinson), diff --git a/src/epcsaft/association/mod.rs b/src/epcsaft/association/mod.rs new file mode 100644 index 000000000..76321c5a1 --- /dev/null +++ b/src/epcsaft/association/mod.rs @@ -0,0 +1,744 @@ +//! Generic implementation of the SAFT association contribution +//! that can be used across models. +use crate::epcsaft::hard_sphere::HardSphereProperties; +use feos_core::{EosError, EosResult, StateHD}; +use ndarray::*; +use num_dual::linalg::{norm, LU}; +use num_dual::*; +use num_traits::Zero; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fmt; +use std::sync::Arc; + +#[cfg(feature = "python")] +mod python; +#[cfg(feature = "python")] +pub use python::PyAssociationRecord; + +#[derive(Clone, Copy, Debug)] +struct AssociationSite { + assoc_comp: usize, + site_index: usize, + n: f64, + kappa_ab: f64, + epsilon_k_ab: f64, +} + +impl AssociationSite { + fn new(assoc_comp: usize, site_index: usize, n: f64, kappa_ab: f64, epsilon_k_ab: f64) -> Self { + Self { + assoc_comp, + site_index, + n, + kappa_ab, + epsilon_k_ab, + } + } +} + +/// Pure component association parameters. +#[derive(Serialize, Deserialize, Clone, Copy)] +pub struct AssociationRecord { + /// Association volume parameter + #[serde(skip_serializing_if = "f64::is_zero")] + #[serde(default)] + pub kappa_ab: f64, + /// Association energy parameter in units of Kelvin + #[serde(skip_serializing_if = "f64::is_zero")] + #[serde(default)] + pub epsilon_k_ab: f64, + /// \# of association sites of type A + #[serde(skip_serializing_if = "f64::is_zero")] + #[serde(default)] + pub na: f64, + /// \# of association sites of type B + #[serde(skip_serializing_if = "f64::is_zero")] + #[serde(default)] + pub nb: f64, + /// \# of association sites of type C + #[serde(skip_serializing_if = "f64::is_zero")] + #[serde(default)] + pub nc: f64, +} + +impl AssociationRecord { + pub fn new(kappa_ab: f64, epsilon_k_ab: f64, na: f64, nb: f64, nc: f64) -> Self { + Self { + kappa_ab, + epsilon_k_ab, + na, + nb, + nc, + } + } +} + +impl fmt::Display for AssociationRecord { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "AssociationRecord(kappa_ab={}", self.kappa_ab)?; + write!(f, ", epsilon_k_ab={}", self.epsilon_k_ab)?; + if self.na > 0.0 { + write!(f, ", na={}", self.na)?; + } + if self.nb > 0.0 { + write!(f, ", nb={}", self.nb)?; + } + if self.nc > 0.0 { + write!(f, ", nc={}", self.nc)?; + } + write!(f, ")") + } +} + +/// Binary association parameters. +#[derive(Serialize, Deserialize, Clone, Copy)] +pub struct BinaryAssociationRecord { + /// Cross-association association volume parameter. + #[serde(skip_serializing_if = "Option::is_none")] + pub kappa_ab: Option, + /// Cross-association energy parameter. + #[serde(skip_serializing_if = "Option::is_none")] + pub epsilon_k_ab: Option, + /// Indices of sites that the record refers to. + #[serde(skip_serializing_if = "is_default_site_indices")] + #[serde(default)] + pub site_indices: [usize; 2], +} + +fn is_default_site_indices([i, j]: &[usize; 2]) -> bool { + *i == 0 && *j == 0 +} + +impl BinaryAssociationRecord { + pub fn new( + kappa_ab: Option, + epsilon_k_ab: Option, + site_indices: Option<[usize; 2]>, + ) -> Self { + Self { + kappa_ab, + epsilon_k_ab, + site_indices: site_indices.unwrap_or_default(), + } + } +} + +/// Parameter set required for the SAFT association Helmoltz energy +/// contribution and functional. +#[derive(Clone)] +pub struct AssociationParameters { + component_index: Array1, + sites_a: Array1, + sites_b: Array1, + sites_c: Array1, + pub sigma3_kappa_ab: Array2, + pub sigma3_kappa_cc: Array2, + pub epsilon_k_ab: Array2, + pub epsilon_k_cc: Array2, +} + +impl AssociationParameters { + pub fn new( + records: &[Vec], + sigma: &Array1, + binary_records: &[((usize, usize), BinaryAssociationRecord)], + component_index: Option<&Array1>, + ) -> Self { + let mut sites_a = Vec::new(); + let mut sites_b = Vec::new(); + let mut sites_c = Vec::new(); + + for (i, record) in records.iter().enumerate() { + for (s, site) in record.iter().enumerate() { + if site.na > 0.0 { + sites_a.push(AssociationSite::new( + i, + s, + site.na, + site.kappa_ab, + site.epsilon_k_ab, + )); + } + if site.nb > 0.0 { + sites_b.push(AssociationSite::new( + i, + s, + site.nb, + site.kappa_ab, + site.epsilon_k_ab, + )); + } + if site.nc > 0.0 { + sites_c.push(AssociationSite::new( + i, + s, + site.nc, + site.kappa_ab, + site.epsilon_k_ab, + )); + } + } + } + + let indices_a: HashMap<_, _> = sites_a + .iter() + .enumerate() + .map(|(i, site)| ((site.assoc_comp, site.site_index), i)) + .collect(); + + let indices_b: HashMap<_, _> = sites_b + .iter() + .enumerate() + .map(|(i, site)| ((site.assoc_comp, site.site_index), i)) + .collect(); + + let indices_c: HashMap<_, _> = sites_c + .iter() + .enumerate() + .map(|(i, site)| ((site.assoc_comp, site.site_index), i)) + .collect(); + + let mut sigma3_kappa_ab = + Array2::from_shape_fn([sites_a.len(), sites_b.len()], |(i, j)| { + (sigma[sites_a[i].assoc_comp] * sigma[sites_b[j].assoc_comp]).powf(1.5) + * (sites_a[i].kappa_ab * sites_b[j].kappa_ab).sqrt() + }); + let mut sigma3_kappa_cc = Array2::from_shape_fn([sites_c.len(); 2], |(i, j)| { + (sigma[sites_c[i].assoc_comp] * sigma[sites_c[j].assoc_comp]).powf(1.5) + * (sites_c[i].kappa_ab * sites_c[j].kappa_ab).sqrt() + }); + let mut epsilon_k_ab = Array2::from_shape_fn([sites_a.len(), sites_b.len()], |(i, j)| { + 0.5 * (sites_a[i].epsilon_k_ab + sites_b[j].epsilon_k_ab) + }); + let mut epsilon_k_cc = Array2::from_shape_fn([sites_c.len(); 2], |(i, j)| { + 0.5 * (sites_c[i].epsilon_k_ab + sites_c[j].epsilon_k_ab) + }); + + for &((i, j), record) in binary_records.iter() { + let [a, b] = record.site_indices; + if let (Some(x), Some(y)) = (indices_a.get(&(i, a)), indices_b.get(&(j, b))) { + if let Some(epsilon_k_aibj) = record.epsilon_k_ab { + epsilon_k_ab[[*x, *y]] = epsilon_k_aibj; + } + if let Some(kappa_aibj) = record.kappa_ab { + sigma3_kappa_ab[[*x, *y]] = (sigma[i] * sigma[j]).powf(1.5) * kappa_aibj; + } + } + if let (Some(y), Some(x)) = (indices_b.get(&(i, a)), indices_a.get(&(j, b))) { + if let Some(epsilon_k_aibj) = record.epsilon_k_ab { + epsilon_k_ab[[*x, *y]] = epsilon_k_aibj; + } + if let Some(kappa_aibj) = record.kappa_ab { + sigma3_kappa_ab[[*x, *y]] = (sigma[i] * sigma[j]).powf(1.5) * kappa_aibj; + } + } + if let (Some(x), Some(y)) = (indices_c.get(&(i, a)), indices_c.get(&(j, b))) { + if let Some(epsilon_k_aibj) = record.epsilon_k_ab { + epsilon_k_cc[[*x, *y]] = epsilon_k_aibj; + epsilon_k_cc[[*y, *x]] = epsilon_k_aibj; + } + if let Some(kappa_aibj) = record.kappa_ab { + sigma3_kappa_cc[[*x, *y]] = (sigma[i] * sigma[j]).powf(1.5) * kappa_aibj; + sigma3_kappa_cc[[*y, *x]] = (sigma[i] * sigma[j]).powf(1.5) * kappa_aibj; + } + } + } + + Self { + component_index: component_index + .cloned() + .unwrap_or_else(|| Array1::from_shape_fn(records.len(), |i| i)), + sites_a: Array1::from_vec(sites_a), + sites_b: Array1::from_vec(sites_b), + sites_c: Array1::from_vec(sites_c), + sigma3_kappa_ab, + sigma3_kappa_cc, + epsilon_k_ab, + epsilon_k_cc, + } + } + + pub fn is_empty(&self) -> bool { + (self.sites_a.is_empty() | self.sites_b.is_empty()) & self.sites_c.is_empty() + } +} + +/// Implementation of the SAFT association Helmholtz energy +/// contribution and functional. +pub struct Association

{ + parameters: Arc

, + association_parameters: Arc, + max_iter: usize, + tol: f64, + force_cross_association: bool, +} + +impl Association

{ + pub fn new( + parameters: &Arc

, + association_parameters: &AssociationParameters, + max_iter: usize, + tol: f64, + ) -> Self { + Self { + parameters: parameters.clone(), + association_parameters: Arc::new(association_parameters.clone()), + max_iter, + tol, + force_cross_association: false, + } + } + + pub fn new_cross_association( + parameters: &Arc

, + association_parameters: &AssociationParameters, + max_iter: usize, + tol: f64, + ) -> Self { + let mut res = Self::new(parameters, association_parameters, max_iter, tol); + res.force_cross_association = true; + res + } + + fn association_strength + Copy>( + &self, + temperature: D, + diameter: &Array1, + n2: D, + n3i: D, + xi: D, + ) -> [Array2; 2] { + let p = &self.association_parameters; + let delta_ab = Array2::from_shape_fn([p.sites_a.len(), p.sites_b.len()], |(i, j)| { + let di = diameter[p.sites_a[i].assoc_comp]; + let dj = diameter[p.sites_b[j].assoc_comp]; + let k = di * dj / (di + dj) * (n2 * n3i); + n3i * (k * xi * (k / 18.0 + 0.5) + 1.0) + * p.sigma3_kappa_ab[(i, j)] + * (temperature.recip() * p.epsilon_k_ab[(i, j)]).exp_m1() + }); + let delta_cc = Array2::from_shape_fn([p.sites_c.len(); 2], |(i, j)| { + let di = diameter[p.sites_c[i].assoc_comp]; + let dj = diameter[p.sites_c[j].assoc_comp]; + let k = di * dj / (di + dj) * (n2 * n3i); + n3i * (k * xi * (k / 18.0 + 0.5) + 1.0) + * p.sigma3_kappa_cc[(i, j)] + * (temperature.recip() * p.epsilon_k_cc[(i, j)]).exp_m1() + }); + [delta_ab, delta_cc] + } +} + +impl Association

{ + #[inline] + pub fn helmholtz_energy + Copy>( + &self, + state: &StateHD, + diameter: &Array1, + ) -> D { + let p: &P = &self.parameters; + let a = &self.association_parameters; + + // auxiliary variables + let [zeta2, n3] = p.zeta(state.temperature, &state.partial_density, [2, 3]); + let n2 = zeta2 * 6.0; + let n3i = (-n3 + 1.0).recip(); + + // association strength + let [delta_ab, delta_cc] = + self.association_strength(state.temperature, diameter, n2, n3i, D::one()); + + match ( + a.sites_a.len() * a.sites_b.len(), + a.sites_c.len(), + self.force_cross_association, + ) { + (0, 0, _) => D::zero(), + (1, 0, false) => self.helmholtz_energy_ab_analytic(state, delta_ab[(0, 0)]), + (0, 1, false) => self.helmholtz_energy_cc_analytic(state, delta_cc[(0, 0)]), + (1, 1, false) => { + self.helmholtz_energy_ab_analytic(state, delta_ab[(0, 0)]) + + self.helmholtz_energy_cc_analytic(state, delta_cc[(0, 0)]) + } + _ => { + // extract site densities of associating segments + let rho: Array1<_> = a + .sites_a + .iter() + .chain(a.sites_b.iter()) + .chain(a.sites_c.iter()) + .map(|s| state.partial_density[a.component_index[s.assoc_comp]] * s.n) + .collect(); + + // Helmholtz energy + Self::helmholtz_energy_density_cross_association( + &rho, + &delta_ab, + &delta_cc, + self.max_iter, + self.tol, + None, + ) + .unwrap_or_else(|_| D::from(std::f64::NAN)) + * state.volume + } + } + } +} + +impl

fmt::Display for Association

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Association") + } +} + +impl Association

{ + fn helmholtz_energy_ab_analytic + Copy>( + &self, + state: &StateHD, + delta: D, + ) -> D { + let a = &self.association_parameters; + + // site densities + let rhoa = + state.partial_density[a.component_index[a.sites_a[0].assoc_comp]] * a.sites_a[0].n; + let rhob = + state.partial_density[a.component_index[a.sites_b[0].assoc_comp]] * a.sites_b[0].n; + + // fraction of non-bonded association sites + let sqrt = ((delta * (rhoa - rhob) + 1.0).powi(2) + delta * rhob * 4.0).sqrt(); + let xa = (sqrt + (delta * (rhob - rhoa) + 1.0)).recip() * 2.0; + let xb = (sqrt + (delta * (rhoa - rhob) + 1.0)).recip() * 2.0; + + (rhoa * (xa.ln() - xa * 0.5 + 0.5) + rhob * (xb.ln() - xb * 0.5 + 0.5)) * state.volume + } + + fn helmholtz_energy_cc_analytic + Copy>( + &self, + state: &StateHD, + delta: D, + ) -> D { + let a = &self.association_parameters; + + // site density + let rhoc = + state.partial_density[a.component_index[a.sites_c[0].assoc_comp]] * a.sites_c[0].n; + + // fraction of non-bonded association sites + let xc = ((delta * 4.0 * rhoc + 1.0).sqrt() + 1.0).recip() * 2.0; + rhoc * (xc.ln() - xc * 0.5 + 0.5) * state.volume + } + + #[allow(clippy::too_many_arguments)] + fn helmholtz_energy_density_cross_association + Copy, S: Data>( + rho: &ArrayBase, + delta_ab: &Array2, + delta_cc: &Array2, + max_iter: usize, + tol: f64, + x0: Option<&mut Array1>, + ) -> EosResult { + // check if density is close to 0 + if rho.sum().re() < f64::EPSILON { + if let Some(x0) = x0 { + x0.fill(1.0); + } + return Ok(D::zero()); + } + + // cross-association according to Michelsen2006 + // initialize monomer fraction + let mut x = match &x0 { + Some(x0) => (*x0).clone(), + None => Array::from_elem(rho.len(), 0.2), + }; + let delta_ab_re = delta_ab.map(D::re); + let delta_cc_re = delta_cc.map(D::re); + let rho_re = rho.map(D::re); + for k in 0..max_iter { + if Self::newton_step_cross_association( + &mut x, + &delta_ab_re, + &delta_cc_re, + &rho_re, + tol, + )? { + break; + } + if k == max_iter - 1 { + return Err(EosError::NotConverged("Cross association".into())); + } + } + + // calculate derivatives + let mut x_dual = x.mapv(D::from); + for _ in 0..D::NDERIV { + Self::newton_step_cross_association(&mut x_dual, delta_ab, delta_cc, rho, tol)?; + } + + // save monomer fraction + if let Some(x0) = x0 { + *x0 = x; + } + + // Helmholtz energy density + let f = |x: D| x.ln() - x * 0.5 + 0.5; + + Ok((rho * x_dual.mapv(f)).sum()) + } + + fn newton_step_cross_association + Copy, S: Data>( + x: &mut Array1, + delta_ab: &Array2, + delta_cc: &Array2, + rho: &ArrayBase, + tol: f64, + ) -> EosResult { + let nassoc = x.len(); + // gradient + let mut g = x.map(D::recip); + // Hessian + let mut h: Array2 = Array::zeros([nassoc; 2]); + + // split arrays + let &[a, b] = delta_ab.shape() else { + panic!("wrong shape!") + }; + let c = delta_cc.shape()[0]; + let (xa, xc) = x.view().split_at(Axis(0), a + b); + let (xa, xb) = xa.split_at(Axis(0), a); + let (rhoa, rhoc) = rho.view().split_at(Axis(0), a + b); + let (rhoa, rhob) = rhoa.split_at(Axis(0), a); + + for i in 0..nassoc { + // calculate gradients + let (d, dnx) = if i < a { + let d = delta_ab.index_axis(Axis(0), i); + (d, (&xb * &rhob * d).sum() + 1.0) + } else if i < a + b { + let d = delta_ab.index_axis(Axis(1), i - a); + (d, (&xa * &rhoa * d).sum() + 1.0) + } else { + let d = delta_cc.index_axis(Axis(0), i - a - b); + (d, (&xc * &rhoc * d).sum() + 1.0) + }; + g[i] -= dnx; + + // approximate hessian + h[(i, i)] = -dnx / x[i]; + if i < a { + for j in 0..b { + h[(i, a + j)] = -d[j] * rhob[j]; + } + } else if i < a + b { + for j in 0..a { + h[(i, j)] = -d[j] * rhoa[j]; + } + } else { + for j in 0..c { + h[(i, a + b + j)] -= d[j] * rhoc[j]; + } + } + } + + // Newton step + // avoid stepping to negative values for x (see Michelsen 2006) + let delta_x = LU::new(h)?.solve(&g); + Zip::from(x).and(&delta_x).for_each(|x, &delta_x| { + if delta_x.re() < x.re() * 0.8 { + *x -= delta_x + } else { + *x *= 0.2 + } + }); + + // check convergence + Ok(norm(&g.map(D::re)) < tol) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_binary_parameters() { + let comp1 = vec![AssociationRecord::new(0.1, 2500., 1.0, 1.0, 0.0)]; + let comp2 = vec![AssociationRecord::new(0.2, 1500., 1.0, 1.0, 0.0)]; + let comp3 = vec![AssociationRecord::new(0.3, 500., 0.0, 1.0, 0.0)]; + let comp4 = vec![ + AssociationRecord::new(0.3, 1000., 1.0, 0.0, 0.0), + AssociationRecord::new(0.3, 2000., 0.0, 1.0, 0.0), + ]; + let records = [comp1, comp2, comp3, comp4]; + let sigma = arr1(&[3.0, 3.0, 3.0, 3.0]); + let binary = [ + ( + (0, 1), + BinaryAssociationRecord::new(Some(3.5), Some(1234.), Some([0, 0])), + ), + ( + (0, 2), + BinaryAssociationRecord::new(Some(3.5), Some(3140.), Some([0, 0])), + ), + ( + (1, 3), + BinaryAssociationRecord::new(Some(3.5), Some(3333.), Some([0, 1])), + ), + ]; + let assoc = AssociationParameters::new(&records, &sigma, &binary, None); + println!("{}", assoc.epsilon_k_ab); + let epsilon_k_ab = arr2(&[ + [2500., 1234., 3140., 2250.], + [1234., 1500., 1000., 3333.], + [1750., 1250., 750., 1500.], + ]); + assert_eq!(assoc.epsilon_k_ab, epsilon_k_ab); + } + + #[test] + fn test_induced_association() { + let comp1 = vec![AssociationRecord::new(0.1, 2500., 1.0, 1.0, 0.0)]; + let comp2 = vec![AssociationRecord::new(0.1, -500., 0.0, 1.0, 0.0)]; + let comp3 = vec![AssociationRecord::new(0.0, 0.0, 0.0, 1.0, 0.0)]; + let sigma = arr1(&[3.0, 3.5]); + let binary = [( + (0, 1), + BinaryAssociationRecord::new(Some(0.1), Some(1000.), None), + )]; + let assoc1 = AssociationParameters::new(&[comp1.clone(), comp2], &sigma, &[], None); + let assoc2 = AssociationParameters::new(&[comp1, comp3], &sigma, &binary, None); + println!("{}", assoc1.epsilon_k_ab); + println!("{}", assoc2.epsilon_k_ab); + assert_eq!(assoc1.epsilon_k_ab, assoc2.epsilon_k_ab); + println!("{}", assoc1.sigma3_kappa_ab); + println!("{}", assoc2.sigma3_kappa_ab); + assert_eq!(assoc1.sigma3_kappa_ab, assoc2.sigma3_kappa_ab); + } +} + +#[cfg(test)] +#[cfg(feature = "pcsaft")] +mod tests_pcsaft { + use super::*; + use crate::pcsaft::parameters::utils::water_parameters; + use crate::pcsaft::PcSaftParameters; + use approx::assert_relative_eq; + use feos_core::parameter::{Parameter, ParameterError}; + + #[test] + fn helmholtz_energy() { + let params = Arc::new(water_parameters()); + let assoc = Association::new(¶ms, ¶ms.association, 50, 1e-10); + let t = 350.0; + let v = 41.248289328513216; + let n = 1.23; + let s = StateHD::new(t, v, arr1(&[n])); + let d = params.hs_diameter(t); + let a_rust = assoc.helmholtz_energy(&s, &d) / n; + assert_relative_eq!(a_rust, -4.229878997054543, epsilon = 1e-10); + } + + #[test] + fn helmholtz_energy_cross() { + let params = Arc::new(water_parameters()); + let assoc = Association::new_cross_association(¶ms, ¶ms.association, 50, 1e-10); + let t = 350.0; + let v = 41.248289328513216; + let n = 1.23; + let s = StateHD::new(t, v, arr1(&[n])); + let d = params.hs_diameter(t); + let a_rust = assoc.helmholtz_energy(&s, &d) / n; + assert_relative_eq!(a_rust, -4.229878997054543, epsilon = 1e-10); + } + + #[test] + fn helmholtz_energy_cross_3b() -> Result<(), ParameterError> { + let mut params = water_parameters(); + let mut record = params.pure_records.pop().unwrap(); + let mut association_record = record.model_record.association_record.unwrap(); + association_record.na = 2.0; + record.model_record.association_record = Some(association_record); + let params = Arc::new(PcSaftParameters::new_pure(record)?); + let assoc = Association::new(¶ms, ¶ms.association, 50, 1e-10); + let cross_assoc = + Association::new_cross_association(¶ms, ¶ms.association, 50, 1e-10); + let t = 350.0; + let v = 41.248289328513216; + let n = 1.23; + let s = StateHD::new(t, v, arr1(&[n])); + let d = params.hs_diameter(t); + let a_assoc = assoc.helmholtz_energy(&s, &d) / n; + let a_cross_assoc = cross_assoc.helmholtz_energy(&s, &d) / n; + assert_relative_eq!(a_assoc, a_cross_assoc, epsilon = 1e-10); + Ok(()) + } +} + +#[cfg(test)] +#[cfg(feature = "gc_pcsaft")] +mod tests_gc_pcsaft { + use super::*; + use crate::gc_pcsaft::eos::parameter::test::*; + use approx::assert_relative_eq; + use feos_core::si::{Pressure, METER, MOL, PASCAL}; + use ndarray::arr1; + use num_dual::Dual64; + use typenum::P3; + + #[test] + fn test_assoc_propanol() { + let params = Arc::new(propanol()); + let contrib = Association::new(¶ms, ¶ms.association, 50, 1e-10); + let temperature = 300.0; + let volume = METER.powi::().to_reduced(); + let moles = (1.5 * MOL).to_reduced(); + let state = StateHD::new( + Dual64::from_re(temperature), + Dual64::from_re(volume).derivative(), + arr1(&[Dual64::from_re(moles)]), + ); + let diameter = params.hs_diameter(state.temperature); + let pressure = + Pressure::from_reduced(-contrib.helmholtz_energy(&state, &diameter).eps * temperature); + assert_relative_eq!(pressure, -3.6819598891967344 * PASCAL, max_relative = 1e-10); + } + + #[test] + fn test_cross_assoc_propanol() { + let params = Arc::new(propanol()); + let contrib = Association::new_cross_association(¶ms, ¶ms.association, 50, 1e-10); + let temperature = 300.0; + let volume = METER.powi::().to_reduced(); + let moles = (1.5 * MOL).to_reduced(); + let state = StateHD::new( + Dual64::from_re(temperature), + Dual64::from_re(volume).derivative(), + arr1(&[Dual64::from_re(moles)]), + ); + let diameter = params.hs_diameter(state.temperature); + let pressure = + Pressure::from_reduced(-contrib.helmholtz_energy(&state, &diameter).eps * temperature); + assert_relative_eq!(pressure, -3.6819598891967344 * PASCAL, max_relative = 1e-10); + } + + #[test] + fn test_cross_assoc_ethanol_propanol() { + let params = Arc::new(ethanol_propanol(false)); + let contrib = Association::new(¶ms, ¶ms.association, 50, 1e-10); + let temperature = 300.0; + let volume = METER.powi::().to_reduced(); + let moles = (arr1(&[1.5, 2.5]) * MOL).to_reduced(); + let state = StateHD::new( + Dual64::from_re(temperature), + Dual64::from_re(volume).derivative(), + moles.mapv(Dual64::from_re), + ); + let diameter = params.hs_diameter(state.temperature); + let pressure = + Pressure::from_reduced(-contrib.helmholtz_energy(&state, &diameter).eps * temperature); + assert_relative_eq!(pressure, -26.105606376765632 * PASCAL, max_relative = 1e-10); + } +} diff --git a/src/epcsaft/association/python.rs b/src/epcsaft/association/python.rs new file mode 100644 index 000000000..9744f5552 --- /dev/null +++ b/src/epcsaft/association/python.rs @@ -0,0 +1,49 @@ +use super::AssociationRecord; +use feos_core::impl_json_handling; +use feos_core::parameter::ParameterError; +use pyo3::prelude::*; + +/// Pure component association parameters +#[pyclass(name = "AssociationRecord")] +#[derive(Clone)] +pub struct PyAssociationRecord(pub AssociationRecord); + +#[pymethods] +impl PyAssociationRecord { + #[new] + #[pyo3(signature = (kappa_ab, epsilon_k_ab, na=0.0, nb=0.0, nc=0.0))] + fn new(kappa_ab: f64, epsilon_k_ab: f64, na: f64, nb: f64, nc: f64) -> Self { + Self(AssociationRecord::new(kappa_ab, epsilon_k_ab, na, nb, nc)) + } + + #[getter] + fn get_kappa_ab(&self) -> f64 { + self.0.kappa_ab + } + + #[getter] + fn get_epsilon_k_ab(&self) -> f64 { + self.0.epsilon_k_ab + } + + #[getter] + fn get_na(&self) -> f64 { + self.0.na + } + + #[getter] + fn get_nb(&self) -> f64 { + self.0.nb + } + + #[getter] + fn get_nc(&self) -> f64 { + self.0.nc + } + + fn __repr__(&self) -> PyResult { + Ok(self.0.to_string()) + } +} + +impl_json_handling!(PyAssociationRecord); diff --git a/src/epcsaft/eos/born.rs b/src/epcsaft/eos/born.rs new file mode 100644 index 000000000..1555f1e0b --- /dev/null +++ b/src/epcsaft/eos/born.rs @@ -0,0 +1,46 @@ +use crate::epcsaft::eos::permittivity::Permittivity; +use ndarray::Array1; +use crate::epcsaft::parameters::ElectrolytePcSaftParameters; +use feos_core::StateHD; +use num_dual::DualNum; +use std::fmt; +use std::sync::Arc; + +use super::ElectrolytePcSaftVariants; + +pub struct Born { + pub parameters: Arc, +} + +impl Born { + pub fn helmholtz_energy + Copy>(&self, state: &StateHD, diameter: &Array1) -> D { + // Parameters + let p = &self.parameters; + + // Calculate Bjerrum length + let lambda_b = p.bjerrum_length(state, ElectrolytePcSaftVariants::Advanced); + + // Calculate relative permittivity + let epsilon_r = Permittivity::new( + state, + &self.parameters, + &ElectrolytePcSaftVariants::Advanced, + ) + .permittivity; + + // Calculate sum xi zi^2 / di + let mut sum_xi_zi_ai = D::zero(); + for i in 0..state.molefracs.len() { + sum_xi_zi_ai += state.molefracs[i] * p.z[i].powi(2) / diameter[i]; + } + + // Calculate born contribution + -lambda_b * (epsilon_r - 1.) * sum_xi_zi_ai * state.moles.sum() + } +} + +impl fmt::Display for Born { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Born") + } +} diff --git a/src/epcsaft/eos/dispersion.rs b/src/epcsaft/eos/dispersion.rs new file mode 100644 index 000000000..c7101fba2 --- /dev/null +++ b/src/epcsaft/eos/dispersion.rs @@ -0,0 +1,218 @@ +use crate::epcsaft::parameters::ElectrolytePcSaftParameters; +use crate::epcsaft::hard_sphere::HardSphereProperties; +use feos_core::StateHD; +use ndarray::{Array, Array1, Array2}; +use num_dual::DualNum; +use std::f64::consts::{FRAC_PI_3, PI}; +use std::fmt; +use std::sync::Arc; + +pub const A0: [f64; 7] = [ + 0.91056314451539, + 0.63612814494991, + 2.68613478913903, + -26.5473624914884, + 97.7592087835073, + -159.591540865600, + 91.2977740839123, +]; +pub const A1: [f64; 7] = [ + -0.30840169182720, + 0.18605311591713, + -2.50300472586548, + 21.4197936296668, + -65.2558853303492, + 83.3186804808856, + -33.7469229297323, +]; +pub const A2: [f64; 7] = [ + -0.09061483509767, + 0.45278428063920, + 0.59627007280101, + -1.72418291311787, + -4.13021125311661, + 13.7766318697211, + -8.67284703679646, +]; +pub const B0: [f64; 7] = [ + 0.72409469413165, + 2.23827918609380, + -4.00258494846342, + -21.00357681484648, + 26.8556413626615, + 206.5513384066188, + -355.60235612207947, +]; +pub const B1: [f64; 7] = [ + -0.57554980753450, + 0.69950955214436, + 3.89256733895307, + -17.21547164777212, + 192.6722644652495, + -161.8264616487648, + -165.2076934555607, +]; +pub const B2: [f64; 7] = [ + 0.09768831158356, + -0.25575749816100, + -9.15585615297321, + 20.64207597439724, + -38.80443005206285, + 93.6267740770146, + -29.66690558514725, +]; + +pub const T_REF: f64 = 298.15; + +impl ElectrolytePcSaftParameters { + pub fn k_ij_t>(&self, temperature: D) -> Array2 { + let k_ij = &self.k_ij; + let n = self.m.len(); + + let mut k_ij_t = Array::zeros((n, n)); + + for i in 0..n { + for j in 0..n { + // Calculate k_ij(T) + k_ij_t[[i, j]] = (temperature.re() - T_REF) * k_ij[[i, j]][1] + + (temperature.re() - T_REF).powi(2) * k_ij[[i, j]][2] + + (temperature.re() - T_REF).powi(3) * k_ij[[i, j]][3] + + k_ij[[i, j]][0]; + } + } + //println!("k_ij_t: {}", k_ij_t); + k_ij_t + } + + pub fn epsilon_k_ij_t>(&self, temperature: D) -> Array2 { + let k_ij_t = self.k_ij_t(temperature); + let n = self.m.len(); + + let mut epsilon_k_ij_t = Array::zeros((n, n)); + + for i in 0..n { + for j in 0..n { + epsilon_k_ij_t[[i, j]] = (1.0 - k_ij_t[[i, j]]) * self.e_k_ij[[i, j]]; + } + } + epsilon_k_ij_t + } +} + +pub struct Dispersion { + pub parameters: Arc, +} + +impl Dispersion { + pub fn helmholtz_energy + Copy>( + &self, + state: &StateHD, + diameter: &Array1, + ) -> D { + // auxiliary variables + let n = self.parameters.m.len(); + let p = &self.parameters; + let rho = &state.partial_density; + + // temperature dependent segment radius + let r = diameter * 0.5; + + // convert sigma_ij + let sigma_ij_t = self.parameters.sigma_ij_t(state.temperature); + + // convert epsilon_k_ij + let epsilon_k_ij_t = self.parameters.epsilon_k_ij_t(state.temperature); + + // packing fraction + let eta = (rho * &p.m * &r * &r * &r).sum() * 4.0 * FRAC_PI_3; + + // mean segment number + let m = (&state.molefracs * &p.m).sum(); + + // mixture densities, crosswise interactions of all segments on all chains + let mut rho1mix = D::zero(); + let mut rho2mix = D::zero(); + for i in 0..n { + for j in 0..n { + let eps_ij = state.temperature.recip() * epsilon_k_ij_t[(i, j)]; + let sigma_ij = sigma_ij_t[[i, j]].powi(3); + rho1mix += rho[i] * rho[j] * p.m[i] * p.m[j] * eps_ij * sigma_ij; + rho2mix += rho[i] * rho[j] * p.m[i] * p.m[j] * eps_ij * eps_ij * sigma_ij; + } + } + + // I1, I2 and C1 + let mut i1 = D::zero(); + let mut i2 = D::zero(); + let mut eta_i = D::one(); + for i in 0..=6 { + i1 += ((m - 1.0) / m * ((m - 2.0) / m * A2[i] + A1[i]) + A0[i]) * eta_i; + i2 += ((m - 1.0) / m * ((m - 2.0) / m * B2[i] + B1[i]) + B0[i]) * eta_i; + eta_i *= eta; + } + let c1 = (m * (eta * 8.0 - eta.powi(2) * 2.0) / (eta - 1.0).powi(4) + + (D::one() - m) + * (eta * 20.0 - eta.powi(2) * 27.0 + eta.powi(3) * 12.0 - eta.powi(4) * 2.0) + / ((eta - 1.0) * (eta - 2.0)).powi(2) + + 1.0) + .recip(); + + // Helmholtz energy + (-rho1mix * i1 * 2.0 - rho2mix * m * c1 * i2) * PI * state.volume + } +} + +impl fmt::Display for Dispersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Dispersion") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::epcsaft::parameters::utils::{ + butane_parameters, propane_butane_parameters, propane_parameters, + }; + use approx::assert_relative_eq; + use ndarray::arr1; + + #[test] + fn helmholtz_energy() { + let disp = Dispersion { + parameters: propane_parameters(), + }; + let t = 250.0; + let v = 1000.0; + let n = 1.0; + let s = StateHD::new(t, v, arr1(&[n])); + let d = disp.parameters.hs_diameter(t); + let a_rust = disp.helmholtz_energy(&s, &d); + assert_relative_eq!(a_rust, -1.0622531100351962, epsilon = 1e-10); + } + + #[test] + fn mix() { + let c1 = Dispersion { + parameters: propane_parameters(), + }; + let c2 = Dispersion { + parameters: butane_parameters(), + }; + let c12 = Dispersion { + parameters: propane_butane_parameters(), + }; + let t = 250.0; + let v = 2.5e28; + let n = 1.0; + let s = StateHD::new(t, v, arr1(&[n])); + let a1 = c1.helmholtz_energy(&s, &c1.parameters.hs_diameter(t)); + let a2 = c2.helmholtz_energy(&s, &c2.parameters.hs_diameter(t)); + let s1m = StateHD::new(t, v, arr1(&[n, 0.0])); + let a1m = c12.helmholtz_energy(&s1m, &c12.parameters.hs_diameter(t)); + let s2m = StateHD::new(t, v, arr1(&[0.0, n])); + let a2m = c12.helmholtz_energy(&s2m, &c12.parameters.hs_diameter(t)); + assert_relative_eq!(a1, a1m, epsilon = 1e-14); + assert_relative_eq!(a2, a2m, epsilon = 1e-14); + } +} diff --git a/src/epcsaft/eos/hard_chain.rs b/src/epcsaft/eos/hard_chain.rs new file mode 100644 index 000000000..68a1ffdac --- /dev/null +++ b/src/epcsaft/eos/hard_chain.rs @@ -0,0 +1,83 @@ +use crate::epcsaft::hard_sphere::HardSphereProperties; +use crate::epcsaft::parameters::ElectrolytePcSaftParameters; +use feos_core::StateHD; +use ndarray::Array; +use num_dual::*; +use std::fmt; +use std::sync::Arc; + +pub struct HardChain { + pub parameters: Arc, +} + +impl HardChain { + #[inline] + pub fn helmholtz_energy + Copy>(&self, state: &StateHD) -> D { + let p = &self.parameters; + let d = self.parameters.hs_diameter(state.temperature); + let [zeta2, zeta3] = p.zeta(state.temperature, &state.partial_density, [2, 3]); + let frac_1mz3 = -(zeta3 - 1.0).recip(); + let c = zeta2 * frac_1mz3 * frac_1mz3; + let g_hs = + d.mapv(|d| frac_1mz3 + d * c * 1.5 - d.powi(2) * c.powi(2) * (zeta3 - 1.0) * 0.5); + Array::from_shape_fn(self.parameters.m.len(), |i| { + state.partial_density[i] * (1.0 - self.parameters.m[i]) * g_hs[i].ln() + }) + .sum() + * state.volume + } +} + +impl fmt::Display for HardChain { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Hard Chain") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::epcsaft::parameters::utils::{ + butane_parameters, propane_butane_parameters, propane_parameters, + }; + use approx::assert_relative_eq; + use ndarray::arr1; + + #[test] + fn helmholtz_energy() { + let hc = HardChain { + parameters: propane_parameters(), + }; + let t = 250.0; + let v = 1000.0; + let n = 1.0; + let s = StateHD::new(t, v, arr1(&[n])); + let a_rust = hc.helmholtz_energy(&s); + assert_relative_eq!(a_rust, -0.12402626171926148, epsilon = 1e-10); + } + + #[test] + fn mix() { + let c1 = HardChain { + parameters: propane_parameters(), + }; + let c2 = HardChain { + parameters: butane_parameters(), + }; + let c12 = HardChain { + parameters: propane_butane_parameters(), + }; + let t = 250.0; + let v = 2.5e28; + let n = 1.0; + let s = StateHD::new(t, v, arr1(&[n])); + let a1 = c1.helmholtz_energy(&s); + let a2 = c2.helmholtz_energy(&s); + let s1m = StateHD::new(t, v, arr1(&[n, 0.0])); + let a1m = c12.helmholtz_energy(&s1m); + let s2m = StateHD::new(t, v, arr1(&[0.0, n])); + let a2m = c12.helmholtz_energy(&s2m); + assert_relative_eq!(a1, a1m, epsilon = 1e-14); + assert_relative_eq!(a2, a2m, epsilon = 1e-14); + } +} diff --git a/src/epcsaft/eos/ionic.rs b/src/epcsaft/eos/ionic.rs new file mode 100644 index 000000000..8f8482f8e --- /dev/null +++ b/src/epcsaft/eos/ionic.rs @@ -0,0 +1,91 @@ +use crate::epcsaft::eos::permittivity::Permittivity; +use crate::epcsaft::parameters::ElectrolytePcSaftParameters; +use feos_core::StateHD; +use ndarray::*; +use num_dual::DualNum; +use std::f64::consts::PI; +use std::fmt; +use std::sync::Arc; + +use super::ElectrolytePcSaftVariants; + +impl ElectrolytePcSaftParameters { + pub fn bjerrum_length + Copy>( + &self, + state: &StateHD, + epcsaft_variant: ElectrolytePcSaftVariants, + ) -> D { + // permittivity in vacuum + let epsilon_0 = 8.85416e-12; + + // relative permittivity of water (usually function of T,p,x) + let epsilon_r = Permittivity::new(state, self, &epcsaft_variant).permittivity; + + let epsreps0 = epsilon_r * epsilon_0; + + // unit charge + let qe2 = 1.602176634e-19f64.powi(2); + + // Boltzmann constant + let boltzmann = 1.380649e-23; + + // Bjerrum length + (state.temperature * 4.0 * std::f64::consts::PI * epsreps0 * boltzmann).recip() + * qe2 + * 1.0e10 + } +} + +pub struct Ionic { + pub parameters: Arc, + pub variant: ElectrolytePcSaftVariants, +} + +impl Ionic { + pub fn helmholtz_energy + Copy>(&self, state: &StateHD, diameter: &Array1) -> D { + // Extract parameters + let p = &self.parameters; + + // Calculate Bjerrum length + let lambda_b = p.bjerrum_length(state, self.variant); + + // Calculate inverse Debye length + let mut sum_dens_z = D::zero(); + for i in 0..state.molefracs.len() { + //let ai = p.ionic_comp[i]; + sum_dens_z += state.partial_density[i] * p.z[i].powi(2); + } + + let kappa = (lambda_b * sum_dens_z * 4.0 * PI).sqrt(); + + let chi: Array1 = diameter + .iter() + .map(|&d| { + (kappa * d).powi(3).recip() + * ((kappa * d + 1.0).ln() - (kappa * d + 1.0) * 2.0 + + (kappa * d + 1.0).powi(2) * 0.5 + + 1.5) + }) + .collect(); + + let mut sum_x_z_chi = D::zero(); + for i in 0..state.molefracs.len() { + sum_x_z_chi += chi[i] * state.molefracs[i] * p.z[i].powi(2); + } + + // Set to zero if one of the ions is 0 + let sum_mole_fraction: f64 = p.ionic_comp.iter().map(|&i| state.molefracs[i].re()).sum(); + let mut a_ion = -kappa * lambda_b * sum_x_z_chi * state.moles.sum(); + if sum_mole_fraction == 0. { + a_ion = D::zero(); + } + + a_ion + } +} + +impl fmt::Display for Ionic { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Ionic") + } +} diff --git a/src/epcsaft/eos/mod.rs b/src/epcsaft/eos/mod.rs new file mode 100644 index 000000000..37ca25fb1 --- /dev/null +++ b/src/epcsaft/eos/mod.rs @@ -0,0 +1,585 @@ +use crate::epcsaft::association::Association; +use crate::epcsaft::hard_sphere::HardSphere; +use crate::epcsaft::parameters::ElectrolytePcSaftParameters; +use crate::epcsaft::hard_sphere::HardSphereProperties; +use feos_core::parameter::Parameter; +use feos_core::{si::*, StateHD}; +use feos_core::{ + Components, EntropyScaling, EosError, EosResult, Residual, State}; +use ndarray::Array1; +use num_dual::DualNum; +use std::f64::consts::{FRAC_PI_6, PI}; +use std::fmt; +use std::sync::Arc; +use typenum::P2; + +pub(crate) mod born; +pub(crate) mod dispersion; +pub(crate) mod hard_chain; +pub(crate) mod ionic; +pub(crate) mod permittivity; +use born::Born; +use dispersion::Dispersion; +use hard_chain::HardChain; +use ionic::Ionic; + +/// Customization options for the ePC-SAFT equation of state. +#[derive(Copy, Clone)] +#[cfg_attr(feature = "python", pyo3::pyclass)] +pub enum ElectrolytePcSaftVariants { + Advanced, + Revised, +} + +#[derive(Copy, Clone)] +pub struct ElectrolytePcSaftOptions { + pub max_eta: f64, + pub max_iter_cross_assoc: usize, + pub tol_cross_assoc: f64, + pub epcsaft_variant: ElectrolytePcSaftVariants, +} + +impl Default for ElectrolytePcSaftOptions { + fn default() -> Self { + Self { + max_eta: 0.5, + max_iter_cross_assoc: 50, + tol_cross_assoc: 1e-10, + epcsaft_variant: ElectrolytePcSaftVariants::Advanced, + } + } +} + +pub struct ElectrolytePcSaft { + pub parameters: Arc, + pub options: ElectrolytePcSaftOptions, + hard_sphere: HardSphere, + hard_chain: Option, + dispersion: Dispersion, + association: Option>, + ionic: Option, + born: Option +} + +impl ElectrolytePcSaft { + pub fn new(parameters: Arc) -> Self { + Self::with_options(parameters, ElectrolytePcSaftOptions::default()) + } + + pub fn with_options( + parameters: Arc, + options: ElectrolytePcSaftOptions, + ) -> Self { + let hard_sphere = HardSphere::new(¶meters); + let dispersion = Dispersion { + parameters: parameters.clone(), + }; + let hard_chain = if parameters.m.iter().any(|m| (m - 1.0).abs() > 1e-15) { + Some(HardChain { + parameters: parameters.clone(), + }) + } else { + None + }; + + let association = if !parameters.association.is_empty() { + Some(Association::new( + ¶meters, + ¶meters.association, + options.max_iter_cross_assoc, + options.tol_cross_assoc, + )) + } else { + None + }; + + let ionic = if parameters.nionic > 0 { + Some(Ionic { + parameters: parameters.clone(), + variant: options.epcsaft_variant, + }) + } else { + None + }; + + let born = if parameters.nionic > 0 { + match options.epcsaft_variant { + ElectrolytePcSaftVariants::Revised => None, + ElectrolytePcSaftVariants::Advanced => Some(Born { + parameters: parameters.clone(), + }), + } + } else { + None + }; + + Self { + parameters, + options, + hard_sphere, + hard_chain, + dispersion, + association, + ionic, + born + } + + } +} + +impl Components for ElectrolytePcSaft { + fn components(&self) -> usize { + self.parameters.pure_records.len() + } + + fn subset(&self, component_list: &[usize]) -> Self { + Self::with_options( + Arc::new(self.parameters.subset(component_list)), + self.options, + ) + } +} + +impl Residual for ElectrolytePcSaft { + fn compute_max_density(&self, moles: &Array1) -> f64 { + self.options.max_eta * moles.sum() + / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles) + .sum() + } + + fn residual_helmholtz_energy_contributions + Copy>( + &self, + state: &StateHD, + ) -> Vec<(String, D)> { + let mut v = Vec::with_capacity(7); + let d = self.parameters.hs_diameter(state.temperature); + + v.push(( + self.hard_sphere.to_string(), + self.hard_sphere.helmholtz_energy(state), + )); + if let Some(hc) = self.hard_chain.as_ref() { + v.push((hc.to_string(), hc.helmholtz_energy(state))) + } + v.push(( + self.dispersion.to_string(), + self.dispersion.helmholtz_energy(state, &d), + )); + if let Some(association) = self.association.as_ref() { + v.push(( + association.to_string(), + association.helmholtz_energy(state, &d), + )) + } + if let Some(ionic) = self.ionic.as_ref() { + v.push((ionic.to_string(), ionic.helmholtz_energy(state, &d))) + }; + if let Some(born) = self.born.as_ref() { + v.push((born.to_string(), born.helmholtz_energy(state, &d))) + }; + v + } + + fn molar_weight(&self) -> MolarWeight> { + self.parameters.molarweight.clone() * GRAM / MOL + } +} + +impl fmt::Display for ElectrolytePcSaft { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ePC-SAFT") + } +} + +fn omega11(t: f64) -> f64 { + 1.06036 * t.powf(-0.15610) + + 0.19300 * (-0.47635 * t).exp() + + 1.03587 * (-1.52996 * t).exp() + + 1.76474 * (-3.89411 * t).exp() +} + +fn omega22(t: f64) -> f64 { + 1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp() + - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin() +} + +#[inline] +fn chapman_enskog_thermal_conductivity( + temperature: Temperature, + molarweight: MolarWeight, + m: f64, + sigma: f64, + epsilon_k: f64, +) -> ThermalConductivity { + let t = temperature.to_reduced(); + 0.083235 * (t * m / molarweight.convert_into(GRAM / MOL)).sqrt() + / sigma.powi(2) + / omega22(t / epsilon_k) + * WATT + / METER + / KELVIN +} + +impl EntropyScaling for ElectrolytePcSaft { + fn viscosity_reference( + &self, + temperature: Temperature, + _: Volume, + moles: &Moles>, +) -> EosResult { + let p = &self.parameters; + let mw = &p.molarweight; + let x = (moles / moles.sum()).into_value(); + let ce: Array1<_> = (0..self.components()) + .map(|i| { + let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); + 5.0 / 16.0 * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI).sqrt() + / omega22(tr) + / (p.sigma[i] * ANGSTROM).powi::() + }) + .collect(); + let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; + for i in 0..self.components() { + let denom: f64 = (0..self.components()) + .map(|j| { + x[j] * (1.0 + + (ce[i] / ce[j]).into_value().sqrt() * (mw[j] / mw[i]).powf(1.0 / 4.0)) + .powi(2) + / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() + }) + .sum(); + ce_mix += ce[i] * x[i] / denom + } + Ok(ce_mix) +} + +fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + let coefficients = self + .parameters + .viscosity + .as_ref() + .expect("Missing viscosity coefficients."); + let m = (x * &self.parameters.m).sum(); + let s = s_res / m; + let pref = (x * &self.parameters.m) / m; + let a: f64 = (&coefficients.row(0) * x).sum(); + let b: f64 = (&coefficients.row(1) * &pref).sum(); + let c: f64 = (&coefficients.row(2) * &pref).sum(); + let d: f64 = (&coefficients.row(3) * &pref).sum(); + Ok(a + b * s + c * s.powi(2) + d * s.powi(3)) +} + +fn diffusion_reference( + &self, + temperature: Temperature, + volume: Volume, + moles: &Moles>, +) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let p = &self.parameters; + let density = moles.sum() / volume; + let res: Array1<_> = (0..self.components()) + .map(|i| { + let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); + 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi::() / omega11(tr) / (density * NAV) + * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL) / p.m[i]).sqrt() + }) + .collect(); + Ok(res[0]) +} + +fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let coefficients = self + .parameters + .diffusion + .as_ref() + .expect("Missing diffusion coefficients."); + let m = (x * &self.parameters.m).sum(); + let s = s_res / m; + let pref = (x * &self.parameters.m).mapv(|v| v / m); + let a: f64 = (&coefficients.row(0) * x).sum(); + let b: f64 = (&coefficients.row(1) * &pref).sum(); + let c: f64 = (&coefficients.row(2) * &pref).sum(); + let d: f64 = (&coefficients.row(3) * &pref).sum(); + let e: f64 = (&coefficients.row(4) * &pref).sum(); + Ok(a + b * s - c * (1.0 - s.exp()) * s.powi(2) - d * s.powi(4) - e * s.powi(8)) +} + +// Equation 4 of DOI: 10.1021/acs.iecr.9b04289 +fn thermal_conductivity_reference( + &self, + temperature: Temperature, + volume: Volume, + moles: &Moles>, +) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let p = &self.parameters; + let mws = self.molar_weight(); + let state = State::new_nvt(&Arc::new(Self::new(p.clone())), temperature, volume, moles)?; + let res: Array1<_> = (0..self.components()) + .map(|i| { + let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); + let s_res_reduced = state.residual_molar_entropy().to_reduced() / p.m[i]; + let ref_ce = chapman_enskog_thermal_conductivity( + temperature, + mws.get(i), + p.m[i], + p.sigma[i], + p.epsilon_k[i], + ); + let alpha_visc = (-s_res_reduced / -0.5).exp(); + let ref_ts = (-0.0167141 * tr / p.m[i] + 0.0470581 * (tr / p.m[i]).powi(2)) + * (p.m[i] * p.m[i] * p.sigma[i].powi(3) * p.epsilon_k[i]) + * 1e-5 + * WATT + / METER + / KELVIN; + ref_ce + ref_ts * alpha_visc + }) + .collect(); + Ok(res[0]) +} + +fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let coefficients = self + .parameters + .thermal_conductivity + .as_ref() + .expect("Missing thermal conductivity coefficients"); + let m = (x * &self.parameters.m).sum(); + let s = s_res / m; + let pref = (x * &self.parameters.m).mapv(|v| v / m); + let a: f64 = (&coefficients.row(0) * x).sum(); + let b: f64 = (&coefficients.row(1) * &pref).sum(); + let c: f64 = (&coefficients.row(2) * &pref).sum(); + let d: f64 = (&coefficients.row(3) * &pref).sum(); + Ok(a + b * s + c * (1.0 - s.exp()) + d * s.powi(2)) +} +} + + +#[cfg(test)] +mod tests { + use super::*; + use crate::epcsaft::parameters::utils::{ + butane_parameters, propane_butane_parameters, propane_parameters, water_parameters + }; + use approx::assert_relative_eq; + use feos_core::si::{BAR, KELVIN, METER, MILLI, PASCAL, RGAS, SECOND}; + use feos_core::*; + use ndarray::arr1; + use typenum::P3; + + #[test] + fn ideal_gas_pressure() { + let e = Arc::new(ElectrolytePcSaft::new(propane_parameters())); + let t = 200.0 * KELVIN; + let v = 1e-3 * METER.powi::(); + let n = arr1(&[1.0]) * MOL; + let s = State::new_nvt(&e, t, v, &n).unwrap(); + let p_ig = s.total_moles * RGAS * t / v; + assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); + assert_relative_eq!( + s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual), + s.pressure(Contributions::Total), + epsilon = 1e-10 + ); + } + + #[test] + fn ideal_gas_heat_capacity_joback() { + let e = Arc::new(ElectrolytePcSaft::new(propane_parameters())); + let t = 200.0 * KELVIN; + let v = 1e-3 * METER.powi::(); + let n = arr1(&[1.0]) * MOL; + let s = State::new_nvt(&e, t, v, &n).unwrap(); + let p_ig = s.total_moles * RGAS * t / v; + assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10); + assert_relative_eq!( + s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual), + s.pressure(Contributions::Total), + epsilon = 1e-10 + ); + } + + #[test] + fn hard_sphere() { + let hs = HardSphere::new(&propane_parameters()); + let t = 250.0; + let v = 1000.0; + let n = 1.0; + let s = StateHD::new(t, v, arr1(&[n])); + let a_rust = hs.helmholtz_energy(&s); + assert_relative_eq!(a_rust, 0.410610492598808, epsilon = 1e-10); + } + + #[test] + fn hard_sphere_mix() { + let c1 = HardSphere::new(&propane_parameters()); + let c2 = HardSphere::new(&butane_parameters()); + let c12 = HardSphere::new(&propane_butane_parameters()); + let t = 250.0; + let v = 2.5e28; + let n = 1.0; + let s = StateHD::new(t, v, arr1(&[n])); + let a1 = c1.helmholtz_energy(&s); + let a2 = c2.helmholtz_energy(&s); + let s1m = StateHD::new(t, v, arr1(&[n, 0.0])); + let a1m = c12.helmholtz_energy(&s1m); + let s2m = StateHD::new(t, v, arr1(&[0.0, n])); + let a2m = c12.helmholtz_energy(&s2m); + assert_relative_eq!(a1, a1m, epsilon = 1e-14); + assert_relative_eq!(a2, a2m, epsilon = 1e-14); + } + + #[test] + fn association() { + let parameters = Arc::new(water_parameters()); + let assoc = Association::new(¶meters, ¶meters.association, 50, 1e-10); + let t = 350.0; + let v = 41.248289328513216; + let n = 1.23; + let s = StateHD::new(t, v, arr1(&[n])); + let d = parameters.hs_diameter(t); + let a_rust = assoc.helmholtz_energy(&s, &d) / n; + assert_relative_eq!(a_rust, -4.229878997054543, epsilon = 1e-10); + } + + #[test] + fn cross_association() { + let parameters = Arc::new(water_parameters()); + let assoc = + Association::new_cross_association(¶meters, ¶meters.association, 50, 1e-10); + let t = 350.0; + let v = 41.248289328513216; + let n = 1.23; + let s = StateHD::new(t, v, arr1(&[n])); + let d = parameters.hs_diameter(t); + let a_rust = assoc.helmholtz_energy(&s, &d) / n; + assert_relative_eq!(a_rust, -4.229878997054543, epsilon = 1e-10); + } + + #[test] + fn new_tpn() { + let e = Arc::new(ElectrolytePcSaft::new(propane_parameters())); + let t = 300.0 * KELVIN; + let p = BAR; + let m = arr1(&[1.0]) * MOL; + let s = State::new_npt(&e, t, p, &m, DensityInitialization::None); + let p_calc = if let Ok(state) = s { + state.pressure(Contributions::Total) + } else { + 0.0 * PASCAL + }; + assert_relative_eq!(p, p_calc, epsilon = 1e-6); + } + + #[test] + fn vle_pure() { + let e = Arc::new(ElectrolytePcSaft::new(propane_parameters())); + let t = 300.0 * KELVIN; + let vle = PhaseEquilibrium::pure(&e, t, None, Default::default()); + if let Ok(v) = vle { + assert_relative_eq!( + v.vapor().pressure(Contributions::Total), + v.liquid().pressure(Contributions::Total), + epsilon = 1e-6 + ) + } + } + + #[test] + fn critical_point() { + let e = Arc::new(ElectrolytePcSaft::new(propane_parameters())); + let t = 300.0 * KELVIN; + let cp = State::critical_point(&e, None, Some(t), Default::default()); + if let Ok(v) = cp { + assert_relative_eq!(v.temperature, 375.1244078318015 * KELVIN, epsilon = 1e-8) + } + } + + #[test] + fn mix_single() { + let e1 = Arc::new(ElectrolytePcSaft::new(propane_parameters())); + let e2 = Arc::new(ElectrolytePcSaft::new(butane_parameters())); + let e12 = Arc::new(ElectrolytePcSaft::new(propane_butane_parameters())); + let t = 300.0 * KELVIN; + let v = 0.02456883872966545 * METER.powi::(); + let m1 = arr1(&[2.0]) * MOL; + let m1m = arr1(&[2.0, 0.0]) * MOL; + let m2m = arr1(&[0.0, 2.0]) * MOL; + let s1 = State::new_nvt(&e1, t, v, &m1).unwrap(); + let s2 = State::new_nvt(&e2, t, v, &m1).unwrap(); + let s1m = State::new_nvt(&e12, t, v, &m1m).unwrap(); + let s2m = State::new_nvt(&e12, t, v, &m2m).unwrap(); + assert_relative_eq!( + s1.pressure(Contributions::Total), + s1m.pressure(Contributions::Total), + epsilon = 1e-12 + ); + assert_relative_eq!( + s2.pressure(Contributions::Total), + s2m.pressure(Contributions::Total), + epsilon = 1e-12 + ); + assert_relative_eq!( + s2.pressure(Contributions::Total), + s2m.pressure(Contributions::Total), + epsilon = 1e-12 + ) + } + + #[test] + fn viscosity() -> EosResult<()> { + let e = Arc::new(ElectrolytePcSaft::new(propane_parameters())); + let t = 300.0 * KELVIN; + let p = BAR; + let n = arr1(&[1.0]) * MOL; + let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap(); + assert_relative_eq!( + s.viscosity()?, + 0.00797 * MILLI * PASCAL * SECOND, + epsilon = 1e-5 + ); + assert_relative_eq!( + s.ln_viscosity_reduced()?, + (s.viscosity()? / e.viscosity_reference(s.temperature, s.volume, &s.moles)?) + .into_value() + .ln(), + epsilon = 1e-15 + ); + Ok(()) + } + + #[test] + fn diffusion() -> EosResult<()> { + let e = Arc::new(ElectrolytePcSaft::new(propane_parameters())); + let t = 300.0 * KELVIN; + let p = BAR; + let n = arr1(&[1.0]) * MOL; + let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap(); + assert_relative_eq!( + s.diffusion()?, + 0.01505 * (CENTI * METER).powi::() / SECOND, + epsilon = 1e-5 + ); + assert_relative_eq!( + s.ln_diffusion_reduced()?, + (s.diffusion()? / e.diffusion_reference(s.temperature, s.volume, &s.moles)?) + .into_value() + .ln(), + epsilon = 1e-15 + ); + Ok(()) + } +} diff --git a/src/epcsaft/eos/permittivity.rs b/src/epcsaft/eos/permittivity.rs new file mode 100644 index 000000000..3da488c76 --- /dev/null +++ b/src/epcsaft/eos/permittivity.rs @@ -0,0 +1,275 @@ +use feos_core::StateHD; +use ndarray::Array1; +use num_dual::DualNum; +use serde::{Deserialize, Serialize}; + +use std::f64::consts::PI; + +use crate::epcsaft::eos::ElectrolytePcSaftVariants; +use crate::epcsaft::parameters::ElectrolytePcSaftParameters; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum PermittivityRecord { + ExperimentalData { + data: Vec>, + }, + PerturbationTheory { + dipole_scaling: Vec, + polarizability_scaling: Vec, + correlation_integral_parameter: Vec, + }, +} + +impl std::fmt::Display for PermittivityRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PermittivityRecord::ExperimentalData { data } => { + write!(f, "PermittivityRecord(data={:?}", data)?; + write!(f, ")") + } + PermittivityRecord::PerturbationTheory { + dipole_scaling, + polarizability_scaling, + correlation_integral_parameter, + } => { + write!(f, "PermittivityRecord(dipole_scaling={}", dipole_scaling[0])?; + write!(f, ", polarizability_scaling={}", polarizability_scaling[0])?; + write!( + f, + ", correlation_integral_parameter={}", + correlation_integral_parameter[0] + )?; + write!(f, ")") + } + } + } +} + +#[derive(Clone)] +pub struct Permittivity> { + pub permittivity: D, +} + +impl + Copy> Permittivity { + pub fn new( + state: &StateHD, + parameters: &ElectrolytePcSaftParameters, + epcsaft_variant: &ElectrolytePcSaftVariants, + ) -> Self { + let n = parameters.pure_records.len(); + + // Set permittivity to an arbitrary value of 1 if system contains no ions + // Ionic and Born contributions will be zero anyways + if parameters.nionic == 0 { + return Self { + permittivity: D::one() * 1., + }; + } + let all_comp: Array1 = parameters + .pure_records + .iter() + .enumerate() + .map(|(i, _pr)| i) + .collect(); + + if let ElectrolytePcSaftVariants::Advanced = epcsaft_variant { + let permittivity = match parameters.permittivity.as_ref().unwrap() { + PermittivityRecord::ExperimentalData { data } => { + // Check length of permittivity_record + if data.len() != n { + panic!("Provide permittivities for all components for ePC-SAFT advanced.") + } + Self::from_experimental_data(data, state.temperature, &state.molefracs) + .permittivity + } + PermittivityRecord::PerturbationTheory { + dipole_scaling, + polarizability_scaling, + correlation_integral_parameter, + } => { + // Check length of permittivity_record + if dipole_scaling.len() != n { + panic!("Provide permittivities for all components for ePC-SAFT advanced.") + } + Self::from_perturbation_theory( + state, + dipole_scaling, + polarizability_scaling, + correlation_integral_parameter, + &all_comp, + ) + .permittivity + } + }; + + return Self { permittivity }; + } + if let ElectrolytePcSaftVariants::Revised = epcsaft_variant { + if parameters.nsolvent > 1 { + panic!( + "The use of ePC-SAFT revised requires the definition of exactly 1 solvent. Currently specified: {} solvents", parameters.nsolvent + ) + }; + let permittivity = match parameters.permittivity.as_ref().unwrap() { + PermittivityRecord::ExperimentalData { data } => { + Self::pure_from_experimental_data(&data[0], state.temperature).permittivity + } + PermittivityRecord::PerturbationTheory { + dipole_scaling, + polarizability_scaling, + correlation_integral_parameter, + } => { + // Check length of permittivity_record + if dipole_scaling.len() != n { + panic!("Provide permittivities for all components for ePC-SAFT advanced.") + } + Self::pure_from_perturbation_theory( + state, + &dipole_scaling[0], + &polarizability_scaling[0], + &correlation_integral_parameter[0], + ) + .permittivity + } + }; + + return Self { permittivity }; + }; + Self { + permittivity: D::zero(), + } + } + + pub fn pure_from_experimental_data(data: &[(f64, f64)], temperature: D) -> Self { + let permittivity_pure = Self::interpolate(data.to_vec(), temperature).permittivity; + Self { + permittivity: permittivity_pure, + } + } + + pub fn pure_from_perturbation_theory( + state: &StateHD, + dipole_scaling: &f64, + polarizability_scaling: &f64, + correlation_integral_parameter: &f64, + ) -> Self { + // reciprocal thermodynamic temperature + let boltzmann = 1.380649e-23; + let beta = (state.temperature * boltzmann).recip(); + + // Density + // let total_moles = state.moles.sum(); + let density = state.moles.mapv(|n| n / state.volume).sum(); + + // dipole density y -> scaled dipole density y_star + let y_star = density * (beta * *dipole_scaling * 1e-19 + *polarizability_scaling * 3.) * 4. + / 9. + * PI; + + // correlation integral + let correlation_integral = ((-y_star).exp() - 1.0) * *correlation_integral_parameter + 1.0; + + // dielectric constan + let permittivity_pure = y_star + * 3.0 + * (y_star.powi(2) * (correlation_integral * (17. / 16.) - 1.0) + y_star + 1.0) + + 1.0; + + Self { + permittivity: permittivity_pure, + } + } + + pub fn from_experimental_data( + data: &[Vec<(f64, f64)>], + temperature: D, + molefracs: &Array1, + ) -> Self { + let permittivity = data + .iter() + .enumerate() + .map(|(i, d)| Self::interpolate(d.to_vec(), temperature).permittivity * molefracs[i]) + .sum(); + Self { permittivity } + } + + pub fn from_perturbation_theory( + state: &StateHD, + dipole_scaling: &[f64], + polarizability_scaling: &[f64], + correlation_integral_parameter: &[f64], + comp: &Array1, + ) -> Self { + //let nsolvent = comp.len(); + // reciprocal thermodynamic temperature + let boltzmann = 1.380649e-23; + let beta = (state.temperature * boltzmann).recip(); + // Determine scaled dipole density and correlation integral parameter of the mixture + let mut y_star = D::zero(); + let mut correlation_integral_parameter_mixture = D::zero(); + for i in comp.iter() { + let rho_i = state.partial_density[*i]; + let x_i = state.molefracs[*i]; + + y_star += + rho_i * (beta * dipole_scaling[*i] * 1e-19 + polarizability_scaling[*i] * 3.) * 4. + / 9. + * PI; + correlation_integral_parameter_mixture += x_i * correlation_integral_parameter[*i]; + } + + // correlation integral + let correlation_integral = + ((-y_star).exp() - 1.0) * correlation_integral_parameter_mixture + 1.0; + + // permittivity + let permittivity = y_star + * 3.0 + * (y_star.powi(2) * (correlation_integral * (17. / 16.) - 1.0) + y_star + 1.0) + + 1.0; + + Self { permittivity } + } + + pub fn interpolate(interpolation_points: Vec<(f64, f64)>, temperature: D) -> Self { + let t_interpol = Array1::from_iter(interpolation_points.iter().map(|t| (t.0))); + let eps_interpol = Array1::from_iter(interpolation_points.iter().map(|e| e.1)); + + // Initialize permittivity + let mut permittivity_pure = D::zero(); + + // Check if only 1 data point is given + if interpolation_points.len() == 1 { + permittivity_pure = D::one() * eps_interpol[0]; + } else { + // Check which interval temperature is in + let temperature = temperature.re(); + for i in 0..(t_interpol.len() - 1) { + // Temperature is within intervals + if temperature >= t_interpol[i] && temperature < t_interpol[i + 1] { + // Interpolate + permittivity_pure = D::one() * eps_interpol[i] + + (temperature - t_interpol[i]) * (eps_interpol[i + 1] - eps_interpol[i]) + / (t_interpol[i + 1] - t_interpol[i]); + } + } + // Temperature is lower than lowest temperature + if temperature < t_interpol[0] { + // Extrapolate from eps_0 and eps_1 + permittivity_pure = D::one() * eps_interpol[0] + + (temperature - t_interpol[0]) * (eps_interpol[1] - eps_interpol[0]) + / (t_interpol[1] - t_interpol[0]); + // Temperature is higher than highest temperature + } else if temperature >= t_interpol[t_interpol.len() - 1] { + // extrapolate from last two epsilons + permittivity_pure = D::one() * eps_interpol[t_interpol.len() - 2] + + (temperature - t_interpol[t_interpol.len() - 2]) + * (eps_interpol[t_interpol.len() - 1] - eps_interpol[t_interpol.len() - 2]) + / (t_interpol[t_interpol.len() - 1] - t_interpol[t_interpol.len() - 2]); + } + } + Self { + permittivity: permittivity_pure, + } + } +} diff --git a/src/epcsaft/hard_sphere/mod.rs b/src/epcsaft/hard_sphere/mod.rs new file mode 100644 index 000000000..7d5a80885 --- /dev/null +++ b/src/epcsaft/hard_sphere/mod.rs @@ -0,0 +1,138 @@ +//! Generic implementation of the hard-sphere contribution +//! that can be used across models. +use feos_core::StateHD; +use ndarray::*; +use num_dual::DualNum; +use std::f64::consts::FRAC_PI_6; +use std::fmt; +use std::{borrow::Cow, sync::Arc}; + +/// Different monomer shapes for FMT and BMCSL. +pub enum MonomerShape<'a, D> { + /// For spherical monomers, the number of components. + Spherical(usize), + /// For non-spherical molecules in a homosegmented approach, the + /// chain length parameter $m$. + NonSpherical(Array1), + /// For non-spherical molecules in a heterosegmented approach, + /// the geometry factors for every segment and the component + /// index for every segment. + Heterosegmented([Array1; 4], &'a Array1), +} + +/// Properties of (generalized) hard sphere systems. +pub trait HardSphereProperties { + /// The [MonomerShape] used in the model. + fn monomer_shape>(&self, temperature: D) -> MonomerShape; + + /// The temperature dependent hard-sphere diameters of every segment. + fn hs_diameter>(&self, temperature: D) -> Array1; + + /// The temperature dependent sigma. + fn sigma_t>(&self, temperature: D) -> Array1; + + /// The temperature dependent sigma_ij. + fn sigma_ij_t>(&self, temperature: D) -> Array2; + + /// For every segment, the index of the component that it is on. + fn component_index(&self) -> Cow> { + match self.monomer_shape(1.0) { + MonomerShape::Spherical(n) => Cow::Owned(Array1::from_shape_fn(n, |i| i)), + MonomerShape::NonSpherical(m) => Cow::Owned(Array1::from_shape_fn(m.len(), |i| i)), + MonomerShape::Heterosegmented(_, component_index) => Cow::Borrowed(component_index), + } + } + + /// The geometry coefficients $C_{k,\alpha}$ for every segment. + fn geometry_coefficients>(&self, temperature: D) -> [Array1; 4] { + match self.monomer_shape(temperature) { + MonomerShape::Spherical(n) => { + let m = Array1::ones(n); + [m.clone(), m.clone(), m.clone(), m] + } + MonomerShape::NonSpherical(m) => [m.clone(), m.clone(), m.clone(), m], + MonomerShape::Heterosegmented(g, _) => g, + } + } + + /// The packing fractions $\zeta_k$. + fn zeta + Copy, const N: usize>( + &self, + temperature: D, + partial_density: &Array1, + k: [i32; N], + ) -> [D; N] { + let component_index = self.component_index(); + let geometry_coefficients = self.geometry_coefficients(temperature); + let diameter = self.hs_diameter(temperature); + let mut zeta = [D::zero(); N]; + for i in 0..diameter.len() { + for (z, &k) in zeta.iter_mut().zip(k.iter()) { + *z += partial_density[component_index[i]] + * diameter[i].powi(k) + * (geometry_coefficients[k as usize][i] * FRAC_PI_6); + } + } + + zeta + } + + /// The fraction $\frac{\zeta_2}{\zeta_3}$ evaluated in a way to avoid a division by 0 when the density is 0. + fn zeta_23 + Copy>(&self, temperature: D, molefracs: &Array1) -> D { + let component_index = self.component_index(); + let geometry_coefficients = self.geometry_coefficients(temperature); + let diameter = self.hs_diameter(temperature); + let mut zeta: [D; 2] = [D::zero(); 2]; + for i in 0..diameter.len() { + for (k, z) in zeta.iter_mut().enumerate() { + *z += molefracs[component_index[i]] + * diameter[i].powi((k + 2) as i32) + * (geometry_coefficients[k + 2][i] * FRAC_PI_6); + } + } + + zeta[0] / zeta[1] + } +} + +/// Implementation of the BMCSL equation of state for hard-sphere mixtures. +/// +/// This structure provides an implementation of the Boublík-Mansoori-Carnahan-Starling-Leland (BMCSL) equation of state ([Boublík, 1970](https://doi.org/10.1063/1.1673824), [Mansoori et al., 1971](https://doi.org/10.1063/1.1675048)) that is often used as reference contribution in SAFT equations of state. The implementation is generalized to allow the description of non-sperical or fused-sphere reference fluids. +/// +/// The reduced Helmholtz energy is calculated according to +/// $$\frac{\beta A}{V}=\frac{6}{\pi}\left(\frac{3\zeta_1\zeta_2}{1-\zeta_3}+\frac{\zeta_2^3}{\zeta_3\left(1-\zeta_3\right)^2}+\left(\frac{\zeta_2^3}{\zeta_3^2}-\zeta_0\right)\ln\left(1-\zeta_3\right)\right)$$ +/// with the packing fractions +/// $$\zeta_k=\frac{\pi}{6}\sum_\alpha C_{k,\alpha}\rho_\alpha d_\alpha^k,~~~~~~~~k=0\ldots 3.$$ +/// +/// The geometry coefficients $C_{k,\alpha}$ and the segment diameters $d_\alpha$ are specified via the [HardSphereProperties] trait. +pub struct HardSphere

{ + parameters: Arc

, +} + +impl

HardSphere

{ + pub fn new(parameters: &Arc

) -> Self { + Self { + parameters: parameters.clone(), + } + } +} + +impl HardSphere

{ + #[inline] + pub fn helmholtz_energy + Copy>(&self, state: &StateHD) -> D { + let p = &self.parameters; + let zeta = p.zeta(state.temperature, &state.partial_density, [0, 1, 2, 3]); + let frac_1mz3 = -(zeta[3] - 1.0).recip(); + let zeta_23 = p.zeta_23(state.temperature, &state.molefracs); + state.volume * 6.0 / std::f64::consts::PI + * (zeta[1] * zeta[2] * frac_1mz3 * 3.0 + + zeta[2].powi(2) * frac_1mz3.powi(2) * zeta_23 + + (zeta[2] * zeta_23.powi(2) - zeta[0]) * (zeta[3] * (-1.0)).ln_1p()) + } +} + +impl

fmt::Display for HardSphere

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Hard Sphere") + } +} diff --git a/src/epcsaft/mod.rs b/src/epcsaft/mod.rs new file mode 100644 index 000000000..fa3167d68 --- /dev/null +++ b/src/epcsaft/mod.rs @@ -0,0 +1,16 @@ +//! Electrolyte Perturbed-Chain Statistical Associating Fluid Theory (e12PC-SAFT) +//! + +#![warn(clippy::all)] +#![allow(clippy::too_many_arguments)] + +mod eos; +mod association; +mod hard_sphere; +pub(crate) mod parameters; + +pub use eos::{ElectrolytePcSaft, ElectrolytePcSaftOptions, ElectrolytePcSaftVariants}; +pub use parameters::{ElectrolytePcSaftBinaryRecord, ElectrolytePcSaftParameters, ElectrolytePcSaftRecord}; + +#[cfg(feature = "python")] +pub mod python; \ No newline at end of file diff --git a/src/epcsaft/parameters.rs b/src/epcsaft/parameters.rs new file mode 100644 index 000000000..70edcad0a --- /dev/null +++ b/src/epcsaft/parameters.rs @@ -0,0 +1,1064 @@ +use crate::epcsaft::association::{AssociationParameters, AssociationRecord, BinaryAssociationRecord}; +use crate::epcsaft::hard_sphere::{HardSphereProperties, MonomerShape}; +use feos_core::parameter::{FromSegments, Parameter, ParameterError, PureRecord}; +use ndarray::{Array, Array1, Array2}; +use num_dual::DualNum; +use num_traits::Zero; +use feos_core::si::{JOULE, KB, KELVIN}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fmt::Write; + +use crate::epcsaft::eos::permittivity::PermittivityRecord; + +/// PC-SAFT pure-component parameters. +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct ElectrolytePcSaftRecord { + /// Segment number + pub m: f64, + /// Segment diameter in units of Angstrom + pub sigma: f64, + /// Energetic parameter in units of Kelvin + pub epsilon_k: f64, + /// Dipole moment in units of Debye + #[serde(skip_serializing_if = "Option::is_none")] + pub mu: Option, + /// Quadrupole moment in units of Debye + #[serde(skip_serializing_if = "Option::is_none")] + pub q: Option, + /// Association parameters + #[serde(flatten)] + #[serde(skip_serializing_if = "Option::is_none")] + pub association_record: Option, + /// Entropy scaling coefficients for the viscosity + #[serde(skip_serializing_if = "Option::is_none")] + pub viscosity: Option<[f64; 4]>, + /// Entropy scaling coefficients for the diffusion coefficient + #[serde(skip_serializing_if = "Option::is_none")] + pub diffusion: Option<[f64; 5]>, + /// Entropy scaling coefficients for the thermal conductivity + #[serde(skip_serializing_if = "Option::is_none")] + pub thermal_conductivity: Option<[f64; 4]>, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub z: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub permittivity_record: Option, +} + +impl FromSegments for ElectrolytePcSaftRecord { + fn from_segments(segments: &[(Self, f64)]) -> Result { + let mut m = 0.0; + let mut sigma3 = 0.0; + let mut epsilon_k = 0.0; + let mut z = 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; + z += s.z.unwrap_or(0.0); + }); + + let q = segments + .iter() + .filter_map(|(s, n)| s.q.map(|q| q * n)) + .reduce(|a, b| a + b); + let mu = segments + .iter() + .filter_map(|(s, n)| s.mu.map(|mu| mu * n)) + .reduce(|a, b| a + b); + let association_record = segments + .iter() + .filter_map(|(s, n)| { + s.association_record.as_ref().map(|record| { + [ + record.kappa_ab * n, + record.epsilon_k_ab * n, + record.na * n, + record.nb * n, + record.nc * n, + ] + }) + }) + .reduce(|a, b| { + [ + a[0] + b[0], + a[1] + b[1], + a[2] + b[2], + a[3] + b[3], + a[4] + b[4], + ] + }) + .map(|[kappa_ab, epsilon_k_ab, na, nb, nc]| { + AssociationRecord::new(kappa_ab, epsilon_k_ab, na, nb, nc) + }); + + // entropy scaling + 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; + } + // if let Some(p) = diffusion.as_mut() { + // let [a, b, c, d, e] = s.diffusion.unwrap(); + // p[0] += s3 * a; + // p[1] += s3 * b / sigma3.powf(0.45); + // p[2] += *n * c; + // p[3] += *n * d; + // } + }); + // correction due to difference in Chapman-Enskog reference between GC and regular formulation. + 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, + association_record, + viscosity, + diffusion, + thermal_conductivity, + z: Some(z), + permittivity_record: None, + }) + } +} + +impl FromSegments for ElectrolytePcSaftRecord { + fn from_segments(segments: &[(Self, usize)]) -> Result { + // We do not allow more than a single segment for q, mu, kappa_ab, epsilon_k_ab + let segments: Vec<_> = segments + .iter() + .cloned() + .map(|(s, c)| (s, c as f64)) + .collect(); + Self::from_segments(&segments) + } +} + + +impl std::fmt::Display for ElectrolytePcSaftRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ElectrolytePcSaftRecord(m={}", self.m)?; + write!(f, ", sigma={}", self.sigma)?; + write!(f, ", epsilon_k={}", self.epsilon_k)?; + if let Some(n) = &self.mu { + write!(f, ", mu={}", n)?; + } + if let Some(n) = &self.q { + write!(f, ", q={}", n)?; + } + if let Some(n) = &self.association_record { + write!(f, ", association_record={}", n)?; + } + if let Some(n) = &self.viscosity { + write!(f, ", viscosity={:?}", n)?; + } + if let Some(n) = &self.diffusion { + write!(f, ", diffusion={:?}", n)?; + } + if let Some(n) = &self.thermal_conductivity { + write!(f, ", thermal_conductivity={:?}", n)?; + } + if let Some(n) = &self.z { + write!(f, ", z={}", n)?; + } + if let Some(n) = &self.permittivity_record { + write!(f, ", permittivity_record={:?}", n)?; + } + write!(f, ")") + } +} + +impl ElectrolytePcSaftRecord { + pub fn new( + m: f64, + sigma: f64, + epsilon_k: f64, + mu: Option, + q: Option, + kappa_ab: Option, + epsilon_k_ab: Option, + na: Option, + nb: Option, + nc: Option, + viscosity: Option<[f64; 4]>, + diffusion: Option<[f64; 5]>, + thermal_conductivity: Option<[f64; 4]>, + z: Option, + permittivity_record: Option, + ) -> ElectrolytePcSaftRecord { + let association_record = if kappa_ab.is_none() + && epsilon_k_ab.is_none() + && na.is_none() + && nb.is_none() + && nc.is_none() + { + None + } else { + Some(AssociationRecord::new( + kappa_ab.unwrap_or_default(), + epsilon_k_ab.unwrap_or_default(), + na.unwrap_or_default(), + nb.unwrap_or_default(), + nc.unwrap_or_default(), + )) + }; + ElectrolytePcSaftRecord { + m, + sigma, + epsilon_k, + mu, + q, + association_record, + viscosity, + diffusion, + thermal_conductivity, + z, + permittivity_record, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Default)] +pub struct ElectrolytePcSaftBinaryRecord { + /// Binary dispersion interaction parameter + #[serde(default)] + pub k_ij: Vec, + /// Binary association parameters + #[serde(flatten)] + association: Option, +} + +impl ElectrolytePcSaftBinaryRecord { + pub fn new(k_ij: Option>, kappa_ab: Option, epsilon_k_ab: Option) -> Self { + let k_ij = k_ij.unwrap_or_default(); + let association = if kappa_ab.is_none() && epsilon_k_ab.is_none() { + None + } else { + Some(BinaryAssociationRecord::new(kappa_ab, epsilon_k_ab, None)) + }; + Self { k_ij, association } + } +} + +impl TryFrom for ElectrolytePcSaftBinaryRecord { + type Error = ParameterError; + + fn try_from(k_ij: f64) -> Result { + Ok(Self { + k_ij: vec![k_ij, 0., 0., 0.], + association: None, + }) + } +} + +impl TryFrom for f64 { + type Error = ParameterError; + + fn try_from(_f: ElectrolytePcSaftBinaryRecord) -> Result { + Err(ParameterError::IncompatibleParameters( + "Cannot infer k_ij from single float.".to_string(), + )) + } +} + +impl std::fmt::Display for ElectrolytePcSaftBinaryRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut tokens = vec![]; + if !self.k_ij[0].is_zero() { + tokens.push(format!("ElectrolytePcSaftBinaryRecord(k_ij_0={})", self.k_ij[0])); + tokens.push(format!("ElectrolytePcSaftBinaryRecord(k_ij_1={})", self.k_ij[1])); + tokens.push(format!("ElectrolytePcSaftBinaryRecord(k_ij_2={})", self.k_ij[2])); + tokens.push(format!("ElectrolytePcSaftBinaryRecord(k_ij_3={})", self.k_ij[3])); + tokens.push(")".to_string());} + if let Some(association) = self.association { + if let Some(kappa_ab) = association.kappa_ab { + tokens.push(format!("kappa_ab={}", kappa_ab)); + } + if let Some(epsilon_k_ab) = association.epsilon_k_ab { + tokens.push(format!("epsilon_k_ab={}", epsilon_k_ab)); + } + } + write!(f, "PcSaftBinaryRecord({})", tokens.join(", ")) + } +} + +pub struct ElectrolytePcSaftParameters { + pub molarweight: Array1, + pub m: Array1, + pub sigma: Array1, + pub epsilon_k: Array1, + pub mu: Array1, + pub q: Array1, + pub mu2: Array1, + pub q2: Array1, + pub association: AssociationParameters, + pub z: Array1, + pub k_ij: Array2>, + pub sigma_ij: Array2, + pub e_k_ij: Array2, + pub ndipole: usize, + pub nquadpole: usize, + pub nionic: usize, + pub nsolvent: usize, + pub sigma_t_comp: Array1, + pub dipole_comp: Array1, + pub quadpole_comp: Array1, + pub ionic_comp: Array1, + pub solvent_comp: Array1, + pub viscosity: Option>, + pub diffusion: Option>, + pub permittivity: Option, + pub thermal_conductivity: Option>, + pub pure_records: Vec>, + pub binary_records: Option>, +} + +impl Parameter for ElectrolytePcSaftParameters { + type Pure = ElectrolytePcSaftRecord; + type Binary = ElectrolytePcSaftBinaryRecord; + + fn from_records( + pure_records: Vec>, + binary_records: Option>, + ) -> Result { + let n = pure_records.len(); + + let mut molarweight = Array::zeros(n); + let mut m = Array::zeros(n); + let mut sigma = Array::zeros(n); + let mut epsilon_k = Array::zeros(n); + let mut mu = Array::zeros(n); + let mut q = Array::zeros(n); + let mut z = Array::zeros(n); + let mut association_records = Vec::with_capacity(n); + let mut viscosity = Vec::with_capacity(n); + let mut diffusion = Vec::with_capacity(n); + let mut thermal_conductivity = Vec::with_capacity(n); + + let mut component_index = HashMap::with_capacity(n); + + for (i, record) in pure_records.iter().enumerate() { + component_index.insert(record.identifier.clone(), i); + let r = &record.model_record; + m[i] = r.m; + sigma[i] = r.sigma; + epsilon_k[i] = r.epsilon_k; + mu[i] = r.mu.unwrap_or(0.0); + q[i] = r.q.unwrap_or(0.0); + z[i] = r.z.unwrap_or(0.0); + association_records.push(r.association_record.into_iter().collect()); + viscosity.push(r.viscosity); + diffusion.push(r.diffusion); + thermal_conductivity.push(r.thermal_conductivity); + molarweight[i] = record.molarweight; + } + + let mu2 = &mu * &mu / (&m * &sigma * &sigma * &sigma * &epsilon_k) + * 1e-19 + * (JOULE / KELVIN / KB).into_value(); + let q2 = &q * &q / (&m * &sigma.mapv(|s| s.powi(5)) * &epsilon_k) + * 1e-19 + * (JOULE / KELVIN / KB).into_value(); + let dipole_comp: Array1 = mu2 + .iter() + .enumerate() + .filter_map(|(i, &mu2)| (mu2.abs() > 0.0).then_some(i)) + .collect(); + let ndipole = dipole_comp.len(); + let quadpole_comp: Array1 = q2 + .iter() + .enumerate() + .filter_map(|(i, &q2)| (q2.abs() > 0.0).then_some(i)) + .collect(); + let nquadpole = quadpole_comp.len(); + + let binary_association: Vec<_> = binary_records + .iter() + .flat_map(|r| { + r.indexed_iter() + .filter_map(|(i, record)| record.association.map(|r| (i, r))) + }) + .collect(); + let association = + AssociationParameters::new(&association_records, &sigma, &binary_association, None); + + let ionic_comp: Array1 = z + .iter() + .enumerate() + .filter_map(|(i, &zi)| (zi.abs() > 0.0).then_some(i)) + .collect(); + + let nionic = ionic_comp.len(); + + let solvent_comp: Array1 = z + .iter() + .enumerate() + .filter_map(|(i, &zi)| (zi.abs() == 0.0).then_some(i)) + .collect(); + let nsolvent = solvent_comp.len(); + + let mut bool_sigma_t = Array1::zeros(n); + for i in 0..n { + let name = pure_records[i] + .identifier + .name + .clone() + .unwrap_or(String::from("unknown")); + if name.contains("sigma_t") { + bool_sigma_t[i] = 1usize + } + } + let sigma_t_comp: Array1 = Array::from_iter( + bool_sigma_t + .iter() + .enumerate() + .filter(|x| x.1 == &1usize) + .map(|x| x.0), + ); + + let mut k_ij: Array2> = Array2::from_elem((n, n), vec![0., 0., 0., 0.]); + + if let Some(binary_records) = binary_records.as_ref() { + for i in 0..n { + for j in 0..n { + let temp_kij = binary_records[[i, j]].k_ij.clone(); + if temp_kij.len() > 4 { + panic!("Binary interaction for component {} with {} is parametrized with more than 4 k_ij coefficients.", i, j); + } else { + (0..temp_kij.len()).for_each(|k| { + k_ij[[i, j]][k] = temp_kij[k]; + }); + } + } + } + + // No binary interaction between charged species of same kind (+/+ and -/-) + ionic_comp.iter().for_each(|ai| { + k_ij[[*ai, *ai]][0] = 1.0; + for k in 1..4usize { + k_ij[[*ai, *ai]][k] = 0.0; + } + }); + } + + let mut sigma_ij = Array::zeros((n, n)); + let mut e_k_ij = Array::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 viscosity_coefficients = if viscosity.iter().any(|v| v.is_none()) { + None + } else { + let mut v = Array2::zeros((4, viscosity.len())); + for (i, vi) in viscosity.iter().enumerate() { + v.column_mut(i).assign(&Array1::from(vi.unwrap().to_vec())); + } + Some(v) + }; + + let diffusion_coefficients = if diffusion.iter().any(|v| v.is_none()) { + None + } else { + let mut v = Array2::zeros((5, diffusion.len())); + for (i, vi) in diffusion.iter().enumerate() { + v.column_mut(i).assign(&Array1::from(vi.unwrap().to_vec())); + } + Some(v) + }; + + let thermal_conductivity_coefficients = if thermal_conductivity.iter().any(|v| v.is_none()) + { + None + } else { + let mut v = Array2::zeros((4, thermal_conductivity.len())); + for (i, vi) in thermal_conductivity.iter().enumerate() { + v.column_mut(i).assign(&Array1::from(vi.unwrap().to_vec())); + } + Some(v) + }; + + // Permittivity + let permittivity_records: Array1 = pure_records + .iter() + .filter(|&record| (record.model_record.permittivity_record.is_some())).map(|record| record.clone().model_record.permittivity_record.unwrap()) + .collect(); + + if nionic != 0 && permittivity_records.len() < nsolvent { + panic!("Provide permittivity records for each solvent.") + } + + let mut modeltype = -1; + let mut mu_scaling: Vec = vec![]; + let mut alpha_scaling: Vec = vec![]; + let mut ci_param: Vec = vec![]; + let mut points: Vec> = vec![]; + + permittivity_records + .iter() + .enumerate() + .for_each(|(i, record)| { + match record { + PermittivityRecord::PerturbationTheory { + dipole_scaling, + polarizability_scaling, + correlation_integral_parameter, + } => { + if modeltype == 2 { + panic!("Inconsistent models for permittivity.") + }; + modeltype = 1; + mu_scaling.push(dipole_scaling[0]); + alpha_scaling.push(polarizability_scaling[0]); + ci_param.push(correlation_integral_parameter[0]); + } + PermittivityRecord::ExperimentalData { data } => { + if modeltype == 1 { + panic!("Inconsistent models for permittivity.") + }; + modeltype = 2; + points.push(data[0].clone()); + // Check if experimental data points are sorted + let mut t_check = 0.0; + for point in &data[0] { + if point.0 < t_check { + panic!("Permittivity points for component {} are unsorted.", i); + } + t_check = point.0; + } + } + } + }); + + let permittivity = match modeltype { + 1 => Some(PermittivityRecord::PerturbationTheory { + dipole_scaling: mu_scaling, + polarizability_scaling: alpha_scaling, + correlation_integral_parameter: ci_param, + }), + 2 => Some(PermittivityRecord::ExperimentalData { data: points }), + _ => None, + }; + + if nionic > 0 && permittivity.is_none() { + panic!("Permittivity of one or more solvents must be specified.") + }; + + Ok(Self { + molarweight, + m, + sigma, + epsilon_k, + mu, + q, + mu2, + q2, + association, + z, + k_ij, + sigma_ij, + e_k_ij, + ndipole, + nquadpole, + nionic, + nsolvent, + dipole_comp, + quadpole_comp, + ionic_comp, + solvent_comp, + sigma_t_comp, + viscosity: viscosity_coefficients, + diffusion: diffusion_coefficients, + thermal_conductivity: thermal_conductivity_coefficients, + permittivity, + pure_records, + binary_records + }) + } + + fn records( + &self, + ) -> ( + &[PureRecord], + Option<&Array2>, + ) { + (&self.pure_records, self.binary_records.as_ref()) + } + +} + +impl HardSphereProperties for ElectrolytePcSaftParameters { + fn monomer_shape>(&self, _: N) -> MonomerShape { + MonomerShape::NonSpherical(self.m.mapv(N::from)) + } + + fn hs_diameter>(&self, temperature: D) -> Array1 { + let sigma_t = self.sigma_t(temperature.clone()); + + let ti = temperature.recip() * -3.0; + let mut d = Array::from_shape_fn(sigma_t.len(), |i| { + -((ti.clone() * self.epsilon_k[i]).exp() * 0.12 - 1.0) * sigma_t[i] + }); + for i in 0..self.nionic { + let ai = self.ionic_comp[i]; + d[ai] = D::one() * sigma_t[ai] * (1.0 - 0.12); + } + d + } + + fn sigma_t>(&self, temperature: D) -> Array1 { + let mut sigma_t: Array1 = Array::from_shape_fn(self.sigma.len(), |i| self.sigma[i]); + for i in 0..self.sigma_t_comp.len() { + sigma_t[i] = (sigma_t[i] + (temperature.re() * -0.01775).exp() * 10.11 + - (temperature.re() * -0.01146).exp() * 1.417) + .re() + } + sigma_t + } + + fn sigma_ij_t>(&self, temperature: D) -> Array2 { + + let diameter = self.sigma_t(temperature); + let n = diameter.len(); + + let mut sigma_ij_t = Array::zeros((n, n)); + for i in 0..n { + for j in 0..n { + sigma_ij_t[[i, j]] = (diameter[i] + diameter[j]) * 0.5; + } + } + sigma_ij_t + } +} + +impl ElectrolytePcSaftParameters { + pub fn to_markdown(&self) -> String { + let mut output = String::new(); + let o = &mut output; + write!( + o, + "|component|molarweight|$m$|$\\sigma$|$\\varepsilon$|$\\mu$|$Q$|$z$|$\\kappa_{{AB}}$|$\\varepsilon_{{AB}}$|$N_A$|$N_B$|\n|-|-|-|-|-|-|-|-|-|-|-|-|" + ) + .unwrap(); + for (i, record) in self.pure_records.iter().enumerate() { + let component = record.identifier.name.clone(); + let component = component.unwrap_or(format!("Component {}", i + 1)); + let association = record + .model_record + .association_record + .unwrap_or_else(|| AssociationRecord::new(0.0, 0.0, 0.0, 0.0, 0.0)); + write!( + o, + "\n|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|{}|", + component, + record.molarweight, + record.model_record.m, + record.model_record.sigma, + record.model_record.epsilon_k, + record.model_record.mu.unwrap_or(0.0), + record.model_record.q.unwrap_or(0.0), + record.model_record.z.unwrap_or(0.0), + association.kappa_ab, + association.epsilon_k_ab, + association.na, + association.nb, + association.nc + ) + .unwrap(); + } + + output + } +} + + +#[allow(dead_code)] +#[cfg(test)] +pub mod utils { + use super::*; + use std::sync::Arc; + + pub fn propane_parameters() -> Arc { + 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" + }, + "model_record": { + "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 = + serde_json::from_str(propane_json).expect("Unable to parse json."); + Arc::new(ElectrolytePcSaftParameters::new_pure(propane_record).unwrap()) + } + + pub fn carbon_dioxide_parameters() -> ElectrolytePcSaftParameters { + 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, + "model_record": { + "m": 1.5131, + "sigma": 3.1869, + "epsilon_k": 163.333, + "q": 4.4 + } + }"#; + let co2_record: PureRecord = + serde_json::from_str(co2_json).expect("Unable to parse json."); + ElectrolytePcSaftParameters::new_pure(co2_record).unwrap() + } + + pub fn butane_parameters() -> Arc { + 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" + }, + "model_record": { + "m": 2.331586, + "sigma": 3.7086010000000003, + "epsilon_k": 222.8774 + }, + "molarweight": 58.123 + }"#; + let butane_record: PureRecord = + serde_json::from_str(butane_json).expect("Unable to parse json."); + Arc::new(ElectrolytePcSaftParameters::new_pure(butane_record).unwrap()) + } + + pub fn dme_parameters() -> ElectrolytePcSaftParameters { + 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" + }, + "model_record": { + "m": 2.2634, + "sigma": 3.2723, + "epsilon_k": 210.29, + "mu": 1.3 + }, + "molarweight": 46.0688 + }"#; + let dme_record: PureRecord = + serde_json::from_str(dme_json).expect("Unable to parse json."); + ElectrolytePcSaftParameters::new_pure(dme_record).unwrap() + } + + pub fn water_parameters_sigma_t() -> ElectrolytePcSaftParameters { + let water_json = r#" + { + "identifier": { + "cas": "7732-18-5", + "name": "water_np_sigma_t", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "model_record": { + "m": 1.2047, + "sigma": 2.7927, + "epsilon_k": 353.95, + "kappa_ab": 0.04509, + "epsilon_k_ab": 2425.7 + }, + "molarweight": 18.0152 + }"#; + let water_record: PureRecord = + serde_json::from_str(water_json).expect("Unable to parse json."); + ElectrolytePcSaftParameters::new_pure(water_record).unwrap() + } + + pub fn water_nacl_parameters() -> ElectrolytePcSaftParameters { + // Water parameters from Held et al. (2014), originally from Fuchs et al. (2006) + let pure_json = r#"[ + { + "identifier": { + "cas": "7732-18-5", + "name": "water_np_sigma_t", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "saft_record": { + "m": 1.2047, + "sigma": 2.7927, + "epsilon_k": 353.95, + "kappa_ab": 0.04509, + "epsilon_k_ab": 2425.7 + }, + "molarweight": 18.0152 + }, + { + "identifier": { + "cas": "110-54-3", + "name": "na+", + "formula": "na+" + }, + "saft_record": { + "m": 1, + "sigma": 2.8232, + "epsilon_k": 230.0, + "z": 1 + }, + "molarweight": 22.98976 + }, + { + "identifier": { + "cas": "7782-50-5", + "name": "cl-", + "formula": "cl-" + }, + "saft_record": { + "m": 1, + "sigma": 2.7560, + "epsilon_k": 170, + "z": -1 + }, + "molarweight": 35.45 + } + ]"#; + let binary_json = r#"[ + { + "id1": { + "cas": "7732-18-5", + "name": "water_np", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "110-54-3", + "name": "na+", + "formula": "na+" + }, + "k_ij": [0.0045] + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water_np", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "7782-50-5", + "name": "cl-", + "formula": "cl-" + }, + "k_ij": [-0.25] + }, + { + "id1": { + "cas": "110-54-3", + "name": "na+", + "formula": "na+" + }, + "id2": { + "cas": "7782-50-5", + "name": "cl-", + "formula": "cl-" + }, + "k_ij": [0.317] + } + ]"#; + let pure_records: Vec> = + serde_json::from_str(pure_json).expect("Unable to parse json."); + let binary_records: ElectrolytePcSaftBinaryRecord = + serde_json::from_str(binary_json).expect("Unable to parse json."); + ElectrolytePcSaftParameters::new_binary(pure_records, Some(binary_records)).unwrap() + } + + pub fn water_parameters() -> ElectrolytePcSaftParameters { + let water_json = r#" + { + "identifier": { + "cas": "7732-18-5", + "name": "water_np", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "model_record": { + "m": 1.065587, + "sigma": 3.000683, + "epsilon_k": 366.5121, + "kappa_ab": 0.034867983, + "epsilon_k_ab": 2500.6706 + }, + "molarweight": 18.0152 + }"#; + let water_record: PureRecord = + serde_json::from_str(water_json).expect("Unable to parse json."); + ElectrolytePcSaftParameters::new_pure(water_record).unwrap() + } + + pub fn dme_co2_parameters() -> ElectrolytePcSaftParameters { + 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, + "model_record": { + "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, + "model_record": { + "m": 1.5131, + "sigma": 3.1869, + "epsilon_k": 163.333, + "q": 4.4 + } + } + ]"#; + let binary_record: Vec> = + serde_json::from_str(binary_json).expect("Unable to parse json."); + ElectrolytePcSaftParameters::new_binary(binary_record, None).unwrap() + } + + pub fn propane_butane_parameters() -> Arc { + 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" + }, + "model_record": { + "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" + }, + "model_record": { + "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: Vec> = + serde_json::from_str(binary_json).expect("Unable to parse json."); + Arc::new(ElectrolytePcSaftParameters::new_binary(binary_record, None).unwrap()) + } +} diff --git a/src/epcsaft/python.rs b/src/epcsaft/python.rs new file mode 100644 index 000000000..c19840996 --- /dev/null +++ b/src/epcsaft/python.rs @@ -0,0 +1,343 @@ +use super::eos::permittivity::PermittivityRecord; +use super::parameters::{ + ElectrolytePcSaftBinaryRecord, ElectrolytePcSaftParameters, ElectrolytePcSaftRecord, +}; +use super::ElectrolytePcSaftVariants; +use feos_core::parameter::{ + BinaryRecord, Identifier, IdentifierOption, Parameter, ParameterError, PureRecord, + SegmentRecord, +}; +use feos_core::python::parameter::*; +use feos_core::*; +use numpy::{PyArray2, PyReadonlyArray2, ToPyArray}; +use pyo3::exceptions::PyTypeError; +use pyo3::prelude::*; +use std::convert::{TryFrom, TryInto}; +use std::sync::Arc; + + +// Pure-substance parameters for the ePC-SAFT equation of state. +/// +/// Parameters +/// ---------- +/// m : float +/// Segment number +/// sigma : float +/// Segment diameter in units of Angstrom. +/// epsilon_k : float +/// Energetic parameter in units of Kelvin. +/// mu : float, optional +/// Dipole moment in units of Debye. +/// q : float, optional +/// Quadrupole moment in units of Debye * Angstrom. +/// kappa_ab : float, optional +/// Association volume parameter. +/// epsilon_k_ab : float, optional +/// Association energy parameter in units of Kelvin. +/// na : float, optional +/// Number of association sites of type A. +/// nb : float, optional +/// Number of association sites of type B. +/// nc : float, optional +/// Number of association sites of type C. +/// z : float, optional +/// Charge of the electrolyte. +/// viscosity : List[float], optional +/// Entropy-scaling parameters for viscosity. Defaults to `None`. +/// diffusion : List[float], optional +/// Entropy-scaling parameters for diffusion. Defaults to `None`. +/// thermal_conductivity : List[float], optional +/// Entropy-scaling parameters for thermal_conductivity. Defaults to `None`. +/// permittivity_record : PyPermittivityRecord, optional +/// Permittivity record. Defaults to `None`. + +#[pyclass(name = "ElectrolytePcSaftRecord")] +#[derive(Clone)] +pub struct PyElectrolytePcSaftRecord(ElectrolytePcSaftRecord); + +#[pymethods] +impl PyElectrolytePcSaftRecord { + #[new] + #[pyo3( + text_signature = "(m, sigma, epsilon_k, mu=None, q=None, kappa_ab=None, epsilon_k_ab=None, na=None, nb=None, nc=None, viscosity=None, diffusion=None, thermal_conductivity=None, permittivity_record=None)" + )] + fn new( + m: f64, + sigma: f64, + epsilon_k: f64, + mu: Option, + q: Option, + kappa_ab: Option, + epsilon_k_ab: Option, + na: Option, + nb: Option, + nc: Option, + z: Option, + viscosity: Option<[f64; 4]>, + diffusion: Option<[f64; 5]>, + thermal_conductivity: Option<[f64; 4]>, + permittivity_record: Option, + ) -> Self { + let perm = match permittivity_record { + Some(p) => Some(p.0), + None => None, + }; + Self(ElectrolytePcSaftRecord::new( + m, + sigma, + epsilon_k, + mu, + q, + kappa_ab, + epsilon_k_ab, + na, + nb, + nc, + viscosity, + diffusion, + thermal_conductivity, + z, + perm, + )) + } + + #[getter] + fn get_m(&self) -> f64 { + self.0.m + } + + #[getter] + fn get_sigma(&self) -> f64 { + self.0.sigma + } + + #[getter] + fn get_epsilon_k(&self) -> f64 { + self.0.epsilon_k + } + + #[getter] + fn get_kappa_ab(&self) -> Option { + self.0.association_record.map(|a| a.kappa_ab) + } + + #[getter] + fn get_epsilon_k_ab(&self) -> Option { + self.0.association_record.map(|a| a.epsilon_k_ab) + } + + #[getter] + fn get_z(&self) -> Option { + self.0.z + } + + #[getter] + fn get_na(&self) -> Option { + self.0.association_record.map(|a| a.na) + } + + #[getter] + fn get_nb(&self) -> Option { + self.0.association_record.map(|a| a.nb) + } + + #[getter] + fn get_nc(&self) -> Option { + self.0.association_record.map(|a| a.nc) + } + + #[getter] + fn get_viscosity(&self) -> Option<[f64; 4]> { + self.0.viscosity + } + + #[getter] + fn get_diffusion(&self) -> Option<[f64; 5]> { + self.0.diffusion + } + + #[getter] + fn get_thermal_conductivity(&self) -> Option<[f64; 4]> { + self.0.thermal_conductivity + } + + fn __repr__(&self) -> PyResult { + Ok(self.0.to_string()) + } +} + +impl_json_handling!(PyElectrolytePcSaftRecord); + +impl_pure_record!( + ElectrolytePcSaftRecord, + PyElectrolytePcSaftRecord +); +impl_segment_record!( + ElectrolytePcSaftRecord, + PyElectrolytePcSaftRecord +); + +#[pyclass(name = "ElectrolytePcSaftBinaryRecord")] +#[derive(Clone)] +pub struct PyElectrolytePcSaftBinaryRecord(ElectrolytePcSaftBinaryRecord); + +#[pymethods] +impl PyElectrolytePcSaftBinaryRecord { + #[new] + fn new(k_ij: [f64; 4]) -> Self { + Self(ElectrolytePcSaftBinaryRecord::new(Some(k_ij.to_vec()), None, None)) + } + + #[getter] + fn get_k_ij(&self) -> Vec { + self.0.k_ij.clone() + } + + #[setter] + fn set_k_ij(&mut self, k_ij: [f64; 4]) { + self.0.k_ij = k_ij.to_vec() + } +} + +impl_json_handling!(PyElectrolytePcSaftBinaryRecord); + +impl_binary_record!( + ElectrolytePcSaftBinaryRecord, + PyElectrolytePcSaftBinaryRecord +); + + +#[pyclass(name = "ElectrolytePcSaftParameters")] +#[derive(Clone)] +pub struct PyElectrolytePcSaftParameters(pub Arc); + +impl_parameter!(ElectrolytePcSaftParameters, PyElectrolytePcSaftParameters, PyElectrolytePcSaftRecord, PyElectrolytePcSaftBinaryRecord); + +#[pymethods] +impl PyElectrolytePcSaftParameters { + fn _repr_markdown_(&self) -> String { + self.0.to_markdown() + } +} + +/// Class permittivity record +#[pyclass(name = "PermittivityRecord", unsendable)] +#[derive(Clone)] +pub struct PyPermittivityRecord(pub PermittivityRecord); + +#[pymethods] +impl PyPermittivityRecord { + /// pure_from_experimental_data + /// + /// Parameters + /// ---------- + /// interpolation_points : Vec<(f64, f64)> + /// + /// Returns + /// ------- + /// PermittivityRecord + /// + #[staticmethod] + #[pyo3(text_signature = "(interpolation_points)")] + pub fn pure_from_experimental_data(interpolation_points: Vec<(f64, f64)>) -> Self { + Self(PermittivityRecord::ExperimentalData { + data: Vec::from([interpolation_points]), + }) + } + + /// from_experimental_data + /// + /// Parameters + /// ---------- + /// interpolation_points : Vec> + /// + /// Returns + /// ------- + /// PermittivityRecord + /// + #[staticmethod] + #[allow(non_snake_case)] + #[pyo3(text_signature = "(interpolation_points)")] + pub fn from_experimental_data(interpolation_points: Vec>) -> Self { + Self(PermittivityRecord::ExperimentalData { + data: interpolation_points, + }) + } + + /// pure_from_perturbation_theory + /// + /// Parameters + /// ---------- + /// dipole_scaling : f64, + /// polarizability_scaling: f64, + /// correlation_integral_parameter : f64, + /// + /// Returns + /// ------- + /// PermittivityRecord + /// + #[staticmethod] + #[allow(non_snake_case)] + #[pyo3( + text_signature = "(dipole_scaling, polarizability_scaling, correlation_integral_parameter)" + )] + pub fn pure_from_perturbation_theory( + dipole_scaling: f64, + polarizability_scaling: f64, + correlation_integral_parameter: f64, + ) -> Self { + Self(PermittivityRecord::PerturbationTheory { + dipole_scaling: Vec::from([dipole_scaling]), + polarizability_scaling: Vec::from([polarizability_scaling]), + correlation_integral_parameter: Vec::from([correlation_integral_parameter]), + }) + } + + /// from_perturbation_theory + /// + /// Parameters + /// ---------- + /// dipole_scaling : Vec, + /// polarizability_scaling : Vec, + /// correlation_integral_parameter : Vec, + /// + /// Returns + /// ------- + /// PermittivityRecord + /// + #[staticmethod] + #[allow(non_snake_case)] + #[pyo3( + text_signature = "(dipole_scaling, polarizability_scaling, correlation_integral_parameter)" + )] + pub fn from_perturbation_theory( + dipole_scaling: Vec, + polarizability_scaling: Vec, + correlation_integral_parameter: Vec, + ) -> Self { + Self(PermittivityRecord::PerturbationTheory { + dipole_scaling, + polarizability_scaling, + correlation_integral_parameter, + }) + } +} + +#[pymodule] +pub fn epcsaft(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 295a7c7e2..b7aeecb57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,8 @@ pub mod hard_sphere; pub mod gc_pcsaft; #[cfg(feature = "pcsaft")] pub mod pcsaft; +#[cfg(feature = "epcsaft")] +pub mod epcsaft; #[cfg(feature = "pets")] pub mod pets; #[cfg(feature = "saftvrmie")] diff --git a/src/python/eos.rs b/src/python/eos.rs index d96e975f6..e7f3acae5 100644 --- a/src/python/eos.rs +++ b/src/python/eos.rs @@ -14,6 +14,10 @@ use crate::impl_estimator_entropy_scaling; use crate::pcsaft::python::PyPcSaftParameters; #[cfg(feature = "pcsaft")] use crate::pcsaft::{DQVariants, PcSaft, PcSaftOptions}; +#[cfg(feature = "epcsaft")] +use crate::epcsaft::python::PyElectrolytePcSaftParameters; +#[cfg(feature = "epcsaft")] +use crate::epcsaft::{ElectrolytePcSaft, ElectrolytePcSaftOptions, ElectrolytePcSaftVariants}; #[cfg(feature = "pets")] use crate::pets::python::PyPetsParameters; #[cfg(feature = "pets")] @@ -190,6 +194,53 @@ impl PyEquationOfState { Self(Arc::new(EquationOfState::new(ideal_gas, residual))) } + /// ePC-SAFT equation of state. + /// + /// Parameters + /// ---------- + /// parameters : ElectrolytePcSaftParameters + /// The parameters of the PC-SAFT equation of state to use. + /// max_eta : float, optional + /// Maximum packing fraction. Defaults to 0.5. + /// max_iter_cross_assoc : unsigned integer, optional + /// Maximum number of iterations for cross association. Defaults to 50. + /// tol_cross_assoc : float + /// Tolerance for convergence of cross association. Defaults to 1e-10. + /// epcsaft_variant : ElectrolytePcSaftVariants, optional + /// Variant of the ePC-SAFT equation of state. Defaults to 'advanced' + /// + /// Returns + /// ------- + /// EquationOfState + /// The PC-SAFT equation of state that can be used to compute thermodynamic + /// states. + #[cfg(feature = "epcsaft")] + #[staticmethod] + #[pyo3( + signature = (parameters, max_eta=0.5, max_iter_cross_assoc=50, tol_cross_assoc=1e-10, epcsaft_variant=ElectrolytePcSaftVariants::Advanced), + text_signature = "(parameters, max_eta=0.5, max_iter_cross_assoc=50, tol_cross_assoc=1e-10, epcsaft_variant=advanced)", + )] + pub fn epcsaft( + parameters: PyElectrolytePcSaftParameters, + max_eta: f64, + max_iter_cross_assoc: usize, + tol_cross_assoc: f64, + epcsaft_variant: ElectrolytePcSaftVariants + ) -> Self { + let options = ElectrolytePcSaftOptions { + max_eta, + max_iter_cross_assoc, + tol_cross_assoc, + epcsaft_variant, + }; + let residual = Arc::new(ResidualModel::ElectrolytePcSaft(ElectrolytePcSaft::with_options( + parameters.0, + options, + ))); + let ideal_gas = Arc::new(IdealGasModel::NoModel(residual.components())); + Self(Arc::new(EquationOfState::new(ideal_gas, residual))) + } + /// Peng-Robinson equation of state. /// /// Parameters diff --git a/src/python/mod.rs b/src/python/mod.rs index 0ad4e3b9b..6449d87e3 100644 --- a/src/python/mod.rs +++ b/src/python/mod.rs @@ -2,6 +2,8 @@ use crate::gc_pcsaft::python::gc_pcsaft as gc_pcsaft_module; #[cfg(feature = "pcsaft")] use crate::pcsaft::python::pcsaft as pcsaft_module; +#[cfg(feature = "epcsaft")] +use crate::epcsaft::python::epcsaft as epcsaft_module; #[cfg(feature = "pets")] use crate::pets::python::pets as pets_module; #[cfg(feature = "saftvrmie")] @@ -42,6 +44,8 @@ pub fn feos(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(cubic_module))?; #[cfg(feature = "pcsaft")] m.add_wrapped(wrap_pymodule!(pcsaft_module))?; + #[cfg(feature = "epcsaft")] + m.add_wrapped(wrap_pymodule!(epcsaft_module))?; #[cfg(feature = "gc_pcsaft")] m.add_wrapped(wrap_pymodule!(gc_pcsaft_module))?; #[cfg(feature = "pets")] @@ -66,6 +70,8 @@ pub fn feos(py: Python<'_>, m: &PyModule) -> PyResult<()> { set_path(py, m, "feos.cubic", "cubic")?; #[cfg(feature = "pcsaft")] set_path(py, m, "feos.pcsaft", "pcsaft")?; + #[cfg(feature = "epcsaft")] + set_path(py, m, "feos.epcsaft", "epcsaft")?; #[cfg(feature = "gc_pcsaft")] set_path(py, m, "feos.gc_pcsaft", "gc_pcsaft")?; #[cfg(feature = "pets")] From 39c1f4595ed8971b15e14aed1bde95e6d6968b89 Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Wed, 6 Mar 2024 08:59:05 +0100 Subject: [PATCH 02/23] added epcsaft to test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a8536372..0a7e8bde8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: strategy: fail-fast: false matrix: - model: [pcsaft, gc_pcsaft, pets, uvtheory, saftvrqmie, saftvrmie] + model: [pcsaft, epcsaft, gc_pcsaft, pets, uvtheory, saftvrqmie, saftvrmie] steps: - uses: actions/checkout@v4 From d9b9396f1aaa1b880ab040554af29f77b0a5c237 Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Wed, 6 Mar 2024 09:01:40 +0100 Subject: [PATCH 03/23] removed association for epcsaft --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 511b8ee3c..4c798a4f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ dft = ["feos-dft", "petgraph"] estimator = [] association = [] pcsaft = ["association"] -epcsaft = ["assocation"] +epcsaft = [] gc_pcsaft = ["association"] uvtheory = ["lazy_static"] pets = [] From 08bbea659a1a766e94747bdfccfb9a7958dc483b Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Wed, 6 Mar 2024 09:10:33 +0100 Subject: [PATCH 04/23] added new standard na, nb to parameter file --- src/epcsaft/parameters.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/epcsaft/parameters.rs b/src/epcsaft/parameters.rs index 70edcad0a..b4568aca8 100644 --- a/src/epcsaft/parameters.rs +++ b/src/epcsaft/parameters.rs @@ -966,7 +966,9 @@ pub mod utils { "sigma": 3.000683, "epsilon_k": 366.5121, "kappa_ab": 0.034867983, - "epsilon_k_ab": 2500.6706 + "epsilon_k_ab": 2500.6706, + "na": 1.0, + "nb": 1.0 }, "molarweight": 18.0152 }"#; From 5eead2c068ad6f727d9003862c87adf902572409 Mon Sep 17 00:00:00 2001 From: Gernot Bauer Date: Fri, 22 Mar 2024 11:06:23 +0100 Subject: [PATCH 05/23] Moved temperature dependent sigma to parameters so that copies of HardSphere and Association are no longer needed. --- Cargo.toml | 16 +- src/epcsaft/eos/dispersion.rs | 2 +- src/epcsaft/eos/hard_chain.rs | 2 +- src/epcsaft/eos/mod.rs | 279 +++++++++++++++++----------------- src/epcsaft/mod.rs | 10 +- src/epcsaft/parameters.rs | 125 +++++++++------ 6 files changed, 239 insertions(+), 195 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4c798a4f5..848b17a2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,27 @@ [package] name = "feos" version = "0.6.1" -authors = ["Gernot Bauer ", "Philipp Rehner "] +authors = [ + "Gernot Bauer ", + "Philipp Rehner ", +] edition = "2021" readme = "README.md" license = "MIT OR Apache-2.0" description = "FeOs - A framework for equations of state and classical density functional theory." homepage = "https://github.com/feos-org" repository = "https://github.com/feos-org/feos" -keywords = ["physics", "thermodynamics", "equations_of_state", "phase_equilibria"] +keywords = [ + "physics", + "thermodynamics", + "equations_of_state", + "phase_equilibria", +] categories = ["science"] [package.metadata.docs.rs] features = ["all_models", "rayon"] -rustdoc-args = [ "--html-in-header", "./docs-header.html" ] +rustdoc-args = ["--html-in-header", "./docs-header.html"] [workspace] members = ["feos-core", "feos-dft", "feos-derive"] @@ -62,7 +70,7 @@ dft = ["feos-dft", "petgraph"] estimator = [] association = [] pcsaft = ["association"] -epcsaft = [] +epcsaft = ["association"] gc_pcsaft = ["association"] uvtheory = ["lazy_static"] pets = [] diff --git a/src/epcsaft/eos/dispersion.rs b/src/epcsaft/eos/dispersion.rs index c7101fba2..1859519f7 100644 --- a/src/epcsaft/eos/dispersion.rs +++ b/src/epcsaft/eos/dispersion.rs @@ -1,5 +1,5 @@ use crate::epcsaft::parameters::ElectrolytePcSaftParameters; -use crate::epcsaft::hard_sphere::HardSphereProperties; +use crate::hard_sphere::HardSphereProperties; use feos_core::StateHD; use ndarray::{Array, Array1, Array2}; use num_dual::DualNum; diff --git a/src/epcsaft/eos/hard_chain.rs b/src/epcsaft/eos/hard_chain.rs index 68a1ffdac..7a08b9559 100644 --- a/src/epcsaft/eos/hard_chain.rs +++ b/src/epcsaft/eos/hard_chain.rs @@ -1,5 +1,5 @@ -use crate::epcsaft::hard_sphere::HardSphereProperties; use crate::epcsaft::parameters::ElectrolytePcSaftParameters; +use crate::hard_sphere::HardSphereProperties; use feos_core::StateHD; use ndarray::Array; use num_dual::*; diff --git a/src/epcsaft/eos/mod.rs b/src/epcsaft/eos/mod.rs index 37ca25fb1..845254c5c 100644 --- a/src/epcsaft/eos/mod.rs +++ b/src/epcsaft/eos/mod.rs @@ -1,11 +1,12 @@ -use crate::epcsaft::association::Association; -use crate::epcsaft::hard_sphere::HardSphere; +use crate::association::Association; +// use crate::epcsaft::association::Association; +// use crate::epcsaft::hard_sphere::HardSphere; +// use crate::epcsaft::hard_sphere::HardSphereProperties; use crate::epcsaft::parameters::ElectrolytePcSaftParameters; -use crate::epcsaft::hard_sphere::HardSphereProperties; +use crate::hard_sphere::{HardSphere, HardSphereProperties}; use feos_core::parameter::Parameter; use feos_core::{si::*, StateHD}; -use feos_core::{ - Components, EntropyScaling, EosError, EosResult, Residual, State}; +use feos_core::{Components, EntropyScaling, EosError, EosResult, Residual, State}; use ndarray::Array1; use num_dual::DualNum; use std::f64::consts::{FRAC_PI_6, PI}; @@ -58,7 +59,7 @@ pub struct ElectrolytePcSaft { dispersion: Dispersion, association: Option>, ionic: Option, - born: Option + born: Option, } impl ElectrolytePcSaft { @@ -101,7 +102,7 @@ impl ElectrolytePcSaft { } else { None }; - + let born = if parameters.nionic > 0 { match options.epcsaft_variant { ElectrolytePcSaftVariants::Revised => None, @@ -112,7 +113,7 @@ impl ElectrolytePcSaft { } else { None }; - + Self { parameters, options, @@ -121,9 +122,8 @@ impl ElectrolytePcSaft { dispersion, association, ionic, - born + born, } - } } @@ -226,153 +226,152 @@ impl EntropyScaling for ElectrolytePcSaft { temperature: Temperature, _: Volume, moles: &Moles>, -) -> EosResult { - let p = &self.parameters; - let mw = &p.molarweight; - let x = (moles / moles.sum()).into_value(); - let ce: Array1<_> = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); - 5.0 / 16.0 * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI).sqrt() - / omega22(tr) - / (p.sigma[i] * ANGSTROM).powi::() - }) - .collect(); - let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; - for i in 0..self.components() { - let denom: f64 = (0..self.components()) - .map(|j| { - x[j] * (1.0 - + (ce[i] / ce[j]).into_value().sqrt() * (mw[j] / mw[i]).powf(1.0 / 4.0)) - .powi(2) - / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() + ) -> EosResult { + let p = &self.parameters; + let mw = &p.molarweight; + let x = (moles / moles.sum()).into_value(); + let ce: Array1<_> = (0..self.components()) + .map(|i| { + let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); + 5.0 / 16.0 * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI).sqrt() + / omega22(tr) + / (p.sigma[i] * ANGSTROM).powi::() }) - .sum(); - ce_mix += ce[i] * x[i] / denom + .collect(); + let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; + for i in 0..self.components() { + let denom: f64 = (0..self.components()) + .map(|j| { + x[j] * (1.0 + + (ce[i] / ce[j]).into_value().sqrt() * (mw[j] / mw[i]).powf(1.0 / 4.0)) + .powi(2) + / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() + }) + .sum(); + ce_mix += ce[i] * x[i] / denom + } + Ok(ce_mix) } - Ok(ce_mix) -} -fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - let coefficients = self - .parameters - .viscosity - .as_ref() - .expect("Missing viscosity coefficients."); - let m = (x * &self.parameters.m).sum(); - let s = s_res / m; - let pref = (x * &self.parameters.m) / m; - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * &pref).sum(); - let c: f64 = (&coefficients.row(2) * &pref).sum(); - let d: f64 = (&coefficients.row(3) * &pref).sum(); - Ok(a + b * s + c * s.powi(2) + d * s.powi(3)) -} + fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + let coefficients = self + .parameters + .viscosity + .as_ref() + .expect("Missing viscosity coefficients."); + let m = (x * &self.parameters.m).sum(); + let s = s_res / m; + let pref = (x * &self.parameters.m) / m; + let a: f64 = (&coefficients.row(0) * x).sum(); + let b: f64 = (&coefficients.row(1) * &pref).sum(); + let c: f64 = (&coefficients.row(2) * &pref).sum(); + let d: f64 = (&coefficients.row(3) * &pref).sum(); + Ok(a + b * s + c * s.powi(2) + d * s.powi(3)) + } -fn diffusion_reference( - &self, - temperature: Temperature, - volume: Volume, - moles: &Moles>, -) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); + fn diffusion_reference( + &self, + temperature: Temperature, + volume: Volume, + moles: &Moles>, + ) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let p = &self.parameters; + let density = moles.sum() / volume; + let res: Array1<_> = (0..self.components()) + .map(|i| { + let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); + 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi::() / omega11(tr) / (density * NAV) + * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL) / p.m[i]).sqrt() + }) + .collect(); + Ok(res[0]) } - let p = &self.parameters; - let density = moles.sum() / volume; - let res: Array1<_> = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); - 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi::() / omega11(tr) / (density * NAV) - * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL) / p.m[i]).sqrt() - }) - .collect(); - Ok(res[0]) -} -fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); + fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let coefficients = self + .parameters + .diffusion + .as_ref() + .expect("Missing diffusion coefficients."); + let m = (x * &self.parameters.m).sum(); + let s = s_res / m; + let pref = (x * &self.parameters.m).mapv(|v| v / m); + let a: f64 = (&coefficients.row(0) * x).sum(); + let b: f64 = (&coefficients.row(1) * &pref).sum(); + let c: f64 = (&coefficients.row(2) * &pref).sum(); + let d: f64 = (&coefficients.row(3) * &pref).sum(); + let e: f64 = (&coefficients.row(4) * &pref).sum(); + Ok(a + b * s - c * (1.0 - s.exp()) * s.powi(2) - d * s.powi(4) - e * s.powi(8)) } - let coefficients = self - .parameters - .diffusion - .as_ref() - .expect("Missing diffusion coefficients."); - let m = (x * &self.parameters.m).sum(); - let s = s_res / m; - let pref = (x * &self.parameters.m).mapv(|v| v / m); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * &pref).sum(); - let c: f64 = (&coefficients.row(2) * &pref).sum(); - let d: f64 = (&coefficients.row(3) * &pref).sum(); - let e: f64 = (&coefficients.row(4) * &pref).sum(); - Ok(a + b * s - c * (1.0 - s.exp()) * s.powi(2) - d * s.powi(4) - e * s.powi(8)) -} -// Equation 4 of DOI: 10.1021/acs.iecr.9b04289 -fn thermal_conductivity_reference( - &self, - temperature: Temperature, - volume: Volume, - moles: &Moles>, -) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); + // Equation 4 of DOI: 10.1021/acs.iecr.9b04289 + fn thermal_conductivity_reference( + &self, + temperature: Temperature, + volume: Volume, + moles: &Moles>, + ) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let p = &self.parameters; + let mws = self.molar_weight(); + let state = State::new_nvt(&Arc::new(Self::new(p.clone())), temperature, volume, moles)?; + let res: Array1<_> = (0..self.components()) + .map(|i| { + let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); + let s_res_reduced = state.residual_molar_entropy().to_reduced() / p.m[i]; + let ref_ce = chapman_enskog_thermal_conductivity( + temperature, + mws.get(i), + p.m[i], + p.sigma[i], + p.epsilon_k[i], + ); + let alpha_visc = (-s_res_reduced / -0.5).exp(); + let ref_ts = (-0.0167141 * tr / p.m[i] + 0.0470581 * (tr / p.m[i]).powi(2)) + * (p.m[i] * p.m[i] * p.sigma[i].powi(3) * p.epsilon_k[i]) + * 1e-5 + * WATT + / METER + / KELVIN; + ref_ce + ref_ts * alpha_visc + }) + .collect(); + Ok(res[0]) } - let p = &self.parameters; - let mws = self.molar_weight(); - let state = State::new_nvt(&Arc::new(Self::new(p.clone())), temperature, volume, moles)?; - let res: Array1<_> = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); - let s_res_reduced = state.residual_molar_entropy().to_reduced() / p.m[i]; - let ref_ce = chapman_enskog_thermal_conductivity( - temperature, - mws.get(i), - p.m[i], - p.sigma[i], - p.epsilon_k[i], - ); - let alpha_visc = (-s_res_reduced / -0.5).exp(); - let ref_ts = (-0.0167141 * tr / p.m[i] + 0.0470581 * (tr / p.m[i]).powi(2)) - * (p.m[i] * p.m[i] * p.sigma[i].powi(3) * p.epsilon_k[i]) - * 1e-5 - * WATT - / METER - / KELVIN; - ref_ce + ref_ts * alpha_visc - }) - .collect(); - Ok(res[0]) -} -fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); + fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { + if self.components() != 1 { + return Err(EosError::IncompatibleComponents(self.components(), 1)); + } + let coefficients = self + .parameters + .thermal_conductivity + .as_ref() + .expect("Missing thermal conductivity coefficients"); + let m = (x * &self.parameters.m).sum(); + let s = s_res / m; + let pref = (x * &self.parameters.m).mapv(|v| v / m); + let a: f64 = (&coefficients.row(0) * x).sum(); + let b: f64 = (&coefficients.row(1) * &pref).sum(); + let c: f64 = (&coefficients.row(2) * &pref).sum(); + let d: f64 = (&coefficients.row(3) * &pref).sum(); + Ok(a + b * s + c * (1.0 - s.exp()) + d * s.powi(2)) } - let coefficients = self - .parameters - .thermal_conductivity - .as_ref() - .expect("Missing thermal conductivity coefficients"); - let m = (x * &self.parameters.m).sum(); - let s = s_res / m; - let pref = (x * &self.parameters.m).mapv(|v| v / m); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * &pref).sum(); - let c: f64 = (&coefficients.row(2) * &pref).sum(); - let d: f64 = (&coefficients.row(3) * &pref).sum(); - Ok(a + b * s + c * (1.0 - s.exp()) + d * s.powi(2)) -} } - #[cfg(test)] mod tests { use super::*; use crate::epcsaft::parameters::utils::{ - butane_parameters, propane_butane_parameters, propane_parameters, water_parameters + butane_parameters, propane_butane_parameters, propane_parameters, water_parameters, }; use approx::assert_relative_eq; use feos_core::si::{BAR, KELVIN, METER, MILLI, PASCAL, RGAS, SECOND}; diff --git a/src/epcsaft/mod.rs b/src/epcsaft/mod.rs index fa3167d68..288e9b022 100644 --- a/src/epcsaft/mod.rs +++ b/src/epcsaft/mod.rs @@ -4,13 +4,15 @@ #![warn(clippy::all)] #![allow(clippy::too_many_arguments)] +// mod association; mod eos; -mod association; -mod hard_sphere; +// mod hard_sphere; pub(crate) mod parameters; pub use eos::{ElectrolytePcSaft, ElectrolytePcSaftOptions, ElectrolytePcSaftVariants}; -pub use parameters::{ElectrolytePcSaftBinaryRecord, ElectrolytePcSaftParameters, ElectrolytePcSaftRecord}; +pub use parameters::{ + ElectrolytePcSaftBinaryRecord, ElectrolytePcSaftParameters, ElectrolytePcSaftRecord, +}; #[cfg(feature = "python")] -pub mod python; \ No newline at end of file +pub mod python; diff --git a/src/epcsaft/parameters.rs b/src/epcsaft/parameters.rs index b4568aca8..edb6478e5 100644 --- a/src/epcsaft/parameters.rs +++ b/src/epcsaft/parameters.rs @@ -1,10 +1,10 @@ -use crate::epcsaft::association::{AssociationParameters, AssociationRecord, BinaryAssociationRecord}; -use crate::epcsaft::hard_sphere::{HardSphereProperties, MonomerShape}; +use crate::association::{AssociationParameters, AssociationRecord, BinaryAssociationRecord}; +use crate::hard_sphere::{HardSphereProperties, MonomerShape}; use feos_core::parameter::{FromSegments, Parameter, ParameterError, PureRecord}; +use feos_core::si::{JOULE, KB, KELVIN}; use ndarray::{Array, Array1, Array2}; use num_dual::DualNum; use num_traits::Zero; -use feos_core::si::{JOULE, KB, KELVIN}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::convert::TryFrom; @@ -167,7 +167,7 @@ impl FromSegments for ElectrolytePcSaftRecord { impl FromSegments for ElectrolytePcSaftRecord { fn from_segments(segments: &[(Self, usize)]) -> Result { - // We do not allow more than a single segment for q, mu, kappa_ab, epsilon_k_ab + // We do not allow more than a single segment for q, mu, kappa_ab, epsilon_k_ab let segments: Vec<_> = segments .iter() .cloned() @@ -176,7 +176,6 @@ impl FromSegments for ElectrolytePcSaftRecord { Self::from_segments(&segments) } } - impl std::fmt::Display for ElectrolytePcSaftRecord { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -308,20 +307,33 @@ impl std::fmt::Display for ElectrolytePcSaftBinaryRecord { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut tokens = vec![]; if !self.k_ij[0].is_zero() { - tokens.push(format!("ElectrolytePcSaftBinaryRecord(k_ij_0={})", self.k_ij[0])); - tokens.push(format!("ElectrolytePcSaftBinaryRecord(k_ij_1={})", self.k_ij[1])); - tokens.push(format!("ElectrolytePcSaftBinaryRecord(k_ij_2={})", self.k_ij[2])); - tokens.push(format!("ElectrolytePcSaftBinaryRecord(k_ij_3={})", self.k_ij[3])); - tokens.push(")".to_string());} - if let Some(association) = self.association { - if let Some(kappa_ab) = association.kappa_ab { - tokens.push(format!("kappa_ab={}", kappa_ab)); - } - if let Some(epsilon_k_ab) = association.epsilon_k_ab { - tokens.push(format!("epsilon_k_ab={}", epsilon_k_ab)); - } + tokens.push(format!( + "ElectrolytePcSaftBinaryRecord(k_ij_0={})", + self.k_ij[0] + )); + tokens.push(format!( + "ElectrolytePcSaftBinaryRecord(k_ij_1={})", + self.k_ij[1] + )); + tokens.push(format!( + "ElectrolytePcSaftBinaryRecord(k_ij_2={})", + self.k_ij[2] + )); + tokens.push(format!( + "ElectrolytePcSaftBinaryRecord(k_ij_3={})", + self.k_ij[3] + )); + tokens.push(")".to_string()); + } + if let Some(association) = self.association { + if let Some(kappa_ab) = association.kappa_ab { + tokens.push(format!("kappa_ab={}", kappa_ab)); + } + if let Some(epsilon_k_ab) = association.epsilon_k_ab { + tokens.push(format!("epsilon_k_ab={}", epsilon_k_ab)); } - write!(f, "PcSaftBinaryRecord({})", tokens.join(", ")) + } + write!(f, "PcSaftBinaryRecord({})", tokens.join(", ")) } } @@ -356,6 +368,31 @@ pub struct ElectrolytePcSaftParameters { pub binary_records: Option>, } +impl ElectrolytePcSaftParameters { + pub fn sigma_t>(&self, temperature: D) -> Array1 { + let mut sigma_t: Array1 = Array::from_shape_fn(self.sigma.len(), |i| self.sigma[i]); + for i in 0..self.sigma_t_comp.len() { + sigma_t[i] = (sigma_t[i] + (temperature.re() * -0.01775).exp() * 10.11 + - (temperature.re() * -0.01146).exp() * 1.417) + .re() + } + sigma_t + } + + pub fn sigma_ij_t>(&self, temperature: D) -> Array2 { + let diameter = self.sigma_t(temperature); + let n = diameter.len(); + + let mut sigma_ij_t = Array::zeros((n, n)); + for i in 0..n { + for j in 0..n { + sigma_ij_t[[i, j]] = (diameter[i] + diameter[j]) * 0.5; + } + } + sigma_ij_t + } +} + impl Parameter for ElectrolytePcSaftParameters { type Pure = ElectrolytePcSaftRecord; type Binary = ElectrolytePcSaftBinaryRecord; @@ -527,7 +564,8 @@ impl Parameter for ElectrolytePcSaftParameters { // Permittivity let permittivity_records: Array1 = pure_records .iter() - .filter(|&record| (record.model_record.permittivity_record.is_some())).map(|record| record.clone().model_record.permittivity_record.unwrap()) + .filter(|&record| (record.model_record.permittivity_record.is_some())) + .map(|record| record.clone().model_record.permittivity_record.unwrap()) .collect(); if nionic != 0 && permittivity_records.len() < nsolvent { @@ -618,7 +656,7 @@ impl Parameter for ElectrolytePcSaftParameters { thermal_conductivity: thermal_conductivity_coefficients, permittivity, pure_records, - binary_records + binary_records, }) } @@ -630,7 +668,6 @@ impl Parameter for ElectrolytePcSaftParameters { ) { (&self.pure_records, self.binary_records.as_ref()) } - } impl HardSphereProperties for ElectrolytePcSaftParameters { @@ -652,29 +689,28 @@ impl HardSphereProperties for ElectrolytePcSaftParameters { d } - fn sigma_t>(&self, temperature: D) -> Array1 { - let mut sigma_t: Array1 = Array::from_shape_fn(self.sigma.len(), |i| self.sigma[i]); - for i in 0..self.sigma_t_comp.len() { - sigma_t[i] = (sigma_t[i] + (temperature.re() * -0.01775).exp() * 10.11 - - (temperature.re() * -0.01146).exp() * 1.417) - .re() - } - sigma_t - } - - fn sigma_ij_t>(&self, temperature: D) -> Array2 { - - let diameter = self.sigma_t(temperature); - let n = diameter.len(); - - let mut sigma_ij_t = Array::zeros((n, n)); - for i in 0..n { - for j in 0..n { - sigma_ij_t[[i, j]] = (diameter[i] + diameter[j]) * 0.5; - } - } - sigma_ij_t - } + // fn sigma_t>(&self, temperature: D) -> Array1 { + // let mut sigma_t: Array1 = Array::from_shape_fn(self.sigma.len(), |i| self.sigma[i]); + // for i in 0..self.sigma_t_comp.len() { + // sigma_t[i] = (sigma_t[i] + (temperature.re() * -0.01775).exp() * 10.11 + // - (temperature.re() * -0.01146).exp() * 1.417) + // .re() + // } + // sigma_t + // } + + // fn sigma_ij_t>(&self, temperature: D) -> Array2 { + // let diameter = self.sigma_t(temperature); + // let n = diameter.len(); + + // let mut sigma_ij_t = Array::zeros((n, n)); + // for i in 0..n { + // for j in 0..n { + // sigma_ij_t[[i, j]] = (diameter[i] + diameter[j]) * 0.5; + // } + // } + // sigma_ij_t + // } } impl ElectrolytePcSaftParameters { @@ -717,7 +753,6 @@ impl ElectrolytePcSaftParameters { } } - #[allow(dead_code)] #[cfg(test)] pub mod utils { From 4d3f639ac7404f6454e225ca4b277ddc0d946bd4 Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Fri, 22 Mar 2024 12:19:16 +0100 Subject: [PATCH 06/23] tidied up after removal of assoc and HS from epcsaft, some formatting --- src/epcsaft/association/mod.rs | 744 ------------------------------ src/epcsaft/association/python.rs | 49 -- src/epcsaft/eos/born.rs | 8 +- src/epcsaft/eos/ionic.rs | 12 +- src/epcsaft/eos/mod.rs | 3 - src/epcsaft/hard_sphere/mod.rs | 138 ------ src/epcsaft/mod.rs | 2 - src/epcsaft/parameters.rs | 23 - 8 files changed, 14 insertions(+), 965 deletions(-) delete mode 100644 src/epcsaft/association/mod.rs delete mode 100644 src/epcsaft/association/python.rs delete mode 100644 src/epcsaft/hard_sphere/mod.rs diff --git a/src/epcsaft/association/mod.rs b/src/epcsaft/association/mod.rs deleted file mode 100644 index 76321c5a1..000000000 --- a/src/epcsaft/association/mod.rs +++ /dev/null @@ -1,744 +0,0 @@ -//! Generic implementation of the SAFT association contribution -//! that can be used across models. -use crate::epcsaft::hard_sphere::HardSphereProperties; -use feos_core::{EosError, EosResult, StateHD}; -use ndarray::*; -use num_dual::linalg::{norm, LU}; -use num_dual::*; -use num_traits::Zero; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fmt; -use std::sync::Arc; - -#[cfg(feature = "python")] -mod python; -#[cfg(feature = "python")] -pub use python::PyAssociationRecord; - -#[derive(Clone, Copy, Debug)] -struct AssociationSite { - assoc_comp: usize, - site_index: usize, - n: f64, - kappa_ab: f64, - epsilon_k_ab: f64, -} - -impl AssociationSite { - fn new(assoc_comp: usize, site_index: usize, n: f64, kappa_ab: f64, epsilon_k_ab: f64) -> Self { - Self { - assoc_comp, - site_index, - n, - kappa_ab, - epsilon_k_ab, - } - } -} - -/// Pure component association parameters. -#[derive(Serialize, Deserialize, Clone, Copy)] -pub struct AssociationRecord { - /// Association volume parameter - #[serde(skip_serializing_if = "f64::is_zero")] - #[serde(default)] - pub kappa_ab: f64, - /// Association energy parameter in units of Kelvin - #[serde(skip_serializing_if = "f64::is_zero")] - #[serde(default)] - pub epsilon_k_ab: f64, - /// \# of association sites of type A - #[serde(skip_serializing_if = "f64::is_zero")] - #[serde(default)] - pub na: f64, - /// \# of association sites of type B - #[serde(skip_serializing_if = "f64::is_zero")] - #[serde(default)] - pub nb: f64, - /// \# of association sites of type C - #[serde(skip_serializing_if = "f64::is_zero")] - #[serde(default)] - pub nc: f64, -} - -impl AssociationRecord { - pub fn new(kappa_ab: f64, epsilon_k_ab: f64, na: f64, nb: f64, nc: f64) -> Self { - Self { - kappa_ab, - epsilon_k_ab, - na, - nb, - nc, - } - } -} - -impl fmt::Display for AssociationRecord { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "AssociationRecord(kappa_ab={}", self.kappa_ab)?; - write!(f, ", epsilon_k_ab={}", self.epsilon_k_ab)?; - if self.na > 0.0 { - write!(f, ", na={}", self.na)?; - } - if self.nb > 0.0 { - write!(f, ", nb={}", self.nb)?; - } - if self.nc > 0.0 { - write!(f, ", nc={}", self.nc)?; - } - write!(f, ")") - } -} - -/// Binary association parameters. -#[derive(Serialize, Deserialize, Clone, Copy)] -pub struct BinaryAssociationRecord { - /// Cross-association association volume parameter. - #[serde(skip_serializing_if = "Option::is_none")] - pub kappa_ab: Option, - /// Cross-association energy parameter. - #[serde(skip_serializing_if = "Option::is_none")] - pub epsilon_k_ab: Option, - /// Indices of sites that the record refers to. - #[serde(skip_serializing_if = "is_default_site_indices")] - #[serde(default)] - pub site_indices: [usize; 2], -} - -fn is_default_site_indices([i, j]: &[usize; 2]) -> bool { - *i == 0 && *j == 0 -} - -impl BinaryAssociationRecord { - pub fn new( - kappa_ab: Option, - epsilon_k_ab: Option, - site_indices: Option<[usize; 2]>, - ) -> Self { - Self { - kappa_ab, - epsilon_k_ab, - site_indices: site_indices.unwrap_or_default(), - } - } -} - -/// Parameter set required for the SAFT association Helmoltz energy -/// contribution and functional. -#[derive(Clone)] -pub struct AssociationParameters { - component_index: Array1, - sites_a: Array1, - sites_b: Array1, - sites_c: Array1, - pub sigma3_kappa_ab: Array2, - pub sigma3_kappa_cc: Array2, - pub epsilon_k_ab: Array2, - pub epsilon_k_cc: Array2, -} - -impl AssociationParameters { - pub fn new( - records: &[Vec], - sigma: &Array1, - binary_records: &[((usize, usize), BinaryAssociationRecord)], - component_index: Option<&Array1>, - ) -> Self { - let mut sites_a = Vec::new(); - let mut sites_b = Vec::new(); - let mut sites_c = Vec::new(); - - for (i, record) in records.iter().enumerate() { - for (s, site) in record.iter().enumerate() { - if site.na > 0.0 { - sites_a.push(AssociationSite::new( - i, - s, - site.na, - site.kappa_ab, - site.epsilon_k_ab, - )); - } - if site.nb > 0.0 { - sites_b.push(AssociationSite::new( - i, - s, - site.nb, - site.kappa_ab, - site.epsilon_k_ab, - )); - } - if site.nc > 0.0 { - sites_c.push(AssociationSite::new( - i, - s, - site.nc, - site.kappa_ab, - site.epsilon_k_ab, - )); - } - } - } - - let indices_a: HashMap<_, _> = sites_a - .iter() - .enumerate() - .map(|(i, site)| ((site.assoc_comp, site.site_index), i)) - .collect(); - - let indices_b: HashMap<_, _> = sites_b - .iter() - .enumerate() - .map(|(i, site)| ((site.assoc_comp, site.site_index), i)) - .collect(); - - let indices_c: HashMap<_, _> = sites_c - .iter() - .enumerate() - .map(|(i, site)| ((site.assoc_comp, site.site_index), i)) - .collect(); - - let mut sigma3_kappa_ab = - Array2::from_shape_fn([sites_a.len(), sites_b.len()], |(i, j)| { - (sigma[sites_a[i].assoc_comp] * sigma[sites_b[j].assoc_comp]).powf(1.5) - * (sites_a[i].kappa_ab * sites_b[j].kappa_ab).sqrt() - }); - let mut sigma3_kappa_cc = Array2::from_shape_fn([sites_c.len(); 2], |(i, j)| { - (sigma[sites_c[i].assoc_comp] * sigma[sites_c[j].assoc_comp]).powf(1.5) - * (sites_c[i].kappa_ab * sites_c[j].kappa_ab).sqrt() - }); - let mut epsilon_k_ab = Array2::from_shape_fn([sites_a.len(), sites_b.len()], |(i, j)| { - 0.5 * (sites_a[i].epsilon_k_ab + sites_b[j].epsilon_k_ab) - }); - let mut epsilon_k_cc = Array2::from_shape_fn([sites_c.len(); 2], |(i, j)| { - 0.5 * (sites_c[i].epsilon_k_ab + sites_c[j].epsilon_k_ab) - }); - - for &((i, j), record) in binary_records.iter() { - let [a, b] = record.site_indices; - if let (Some(x), Some(y)) = (indices_a.get(&(i, a)), indices_b.get(&(j, b))) { - if let Some(epsilon_k_aibj) = record.epsilon_k_ab { - epsilon_k_ab[[*x, *y]] = epsilon_k_aibj; - } - if let Some(kappa_aibj) = record.kappa_ab { - sigma3_kappa_ab[[*x, *y]] = (sigma[i] * sigma[j]).powf(1.5) * kappa_aibj; - } - } - if let (Some(y), Some(x)) = (indices_b.get(&(i, a)), indices_a.get(&(j, b))) { - if let Some(epsilon_k_aibj) = record.epsilon_k_ab { - epsilon_k_ab[[*x, *y]] = epsilon_k_aibj; - } - if let Some(kappa_aibj) = record.kappa_ab { - sigma3_kappa_ab[[*x, *y]] = (sigma[i] * sigma[j]).powf(1.5) * kappa_aibj; - } - } - if let (Some(x), Some(y)) = (indices_c.get(&(i, a)), indices_c.get(&(j, b))) { - if let Some(epsilon_k_aibj) = record.epsilon_k_ab { - epsilon_k_cc[[*x, *y]] = epsilon_k_aibj; - epsilon_k_cc[[*y, *x]] = epsilon_k_aibj; - } - if let Some(kappa_aibj) = record.kappa_ab { - sigma3_kappa_cc[[*x, *y]] = (sigma[i] * sigma[j]).powf(1.5) * kappa_aibj; - sigma3_kappa_cc[[*y, *x]] = (sigma[i] * sigma[j]).powf(1.5) * kappa_aibj; - } - } - } - - Self { - component_index: component_index - .cloned() - .unwrap_or_else(|| Array1::from_shape_fn(records.len(), |i| i)), - sites_a: Array1::from_vec(sites_a), - sites_b: Array1::from_vec(sites_b), - sites_c: Array1::from_vec(sites_c), - sigma3_kappa_ab, - sigma3_kappa_cc, - epsilon_k_ab, - epsilon_k_cc, - } - } - - pub fn is_empty(&self) -> bool { - (self.sites_a.is_empty() | self.sites_b.is_empty()) & self.sites_c.is_empty() - } -} - -/// Implementation of the SAFT association Helmholtz energy -/// contribution and functional. -pub struct Association

{ - parameters: Arc

, - association_parameters: Arc, - max_iter: usize, - tol: f64, - force_cross_association: bool, -} - -impl Association

{ - pub fn new( - parameters: &Arc

, - association_parameters: &AssociationParameters, - max_iter: usize, - tol: f64, - ) -> Self { - Self { - parameters: parameters.clone(), - association_parameters: Arc::new(association_parameters.clone()), - max_iter, - tol, - force_cross_association: false, - } - } - - pub fn new_cross_association( - parameters: &Arc

, - association_parameters: &AssociationParameters, - max_iter: usize, - tol: f64, - ) -> Self { - let mut res = Self::new(parameters, association_parameters, max_iter, tol); - res.force_cross_association = true; - res - } - - fn association_strength + Copy>( - &self, - temperature: D, - diameter: &Array1, - n2: D, - n3i: D, - xi: D, - ) -> [Array2; 2] { - let p = &self.association_parameters; - let delta_ab = Array2::from_shape_fn([p.sites_a.len(), p.sites_b.len()], |(i, j)| { - let di = diameter[p.sites_a[i].assoc_comp]; - let dj = diameter[p.sites_b[j].assoc_comp]; - let k = di * dj / (di + dj) * (n2 * n3i); - n3i * (k * xi * (k / 18.0 + 0.5) + 1.0) - * p.sigma3_kappa_ab[(i, j)] - * (temperature.recip() * p.epsilon_k_ab[(i, j)]).exp_m1() - }); - let delta_cc = Array2::from_shape_fn([p.sites_c.len(); 2], |(i, j)| { - let di = diameter[p.sites_c[i].assoc_comp]; - let dj = diameter[p.sites_c[j].assoc_comp]; - let k = di * dj / (di + dj) * (n2 * n3i); - n3i * (k * xi * (k / 18.0 + 0.5) + 1.0) - * p.sigma3_kappa_cc[(i, j)] - * (temperature.recip() * p.epsilon_k_cc[(i, j)]).exp_m1() - }); - [delta_ab, delta_cc] - } -} - -impl Association

{ - #[inline] - pub fn helmholtz_energy + Copy>( - &self, - state: &StateHD, - diameter: &Array1, - ) -> D { - let p: &P = &self.parameters; - let a = &self.association_parameters; - - // auxiliary variables - let [zeta2, n3] = p.zeta(state.temperature, &state.partial_density, [2, 3]); - let n2 = zeta2 * 6.0; - let n3i = (-n3 + 1.0).recip(); - - // association strength - let [delta_ab, delta_cc] = - self.association_strength(state.temperature, diameter, n2, n3i, D::one()); - - match ( - a.sites_a.len() * a.sites_b.len(), - a.sites_c.len(), - self.force_cross_association, - ) { - (0, 0, _) => D::zero(), - (1, 0, false) => self.helmholtz_energy_ab_analytic(state, delta_ab[(0, 0)]), - (0, 1, false) => self.helmholtz_energy_cc_analytic(state, delta_cc[(0, 0)]), - (1, 1, false) => { - self.helmholtz_energy_ab_analytic(state, delta_ab[(0, 0)]) - + self.helmholtz_energy_cc_analytic(state, delta_cc[(0, 0)]) - } - _ => { - // extract site densities of associating segments - let rho: Array1<_> = a - .sites_a - .iter() - .chain(a.sites_b.iter()) - .chain(a.sites_c.iter()) - .map(|s| state.partial_density[a.component_index[s.assoc_comp]] * s.n) - .collect(); - - // Helmholtz energy - Self::helmholtz_energy_density_cross_association( - &rho, - &delta_ab, - &delta_cc, - self.max_iter, - self.tol, - None, - ) - .unwrap_or_else(|_| D::from(std::f64::NAN)) - * state.volume - } - } - } -} - -impl

fmt::Display for Association

{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Association") - } -} - -impl Association

{ - fn helmholtz_energy_ab_analytic + Copy>( - &self, - state: &StateHD, - delta: D, - ) -> D { - let a = &self.association_parameters; - - // site densities - let rhoa = - state.partial_density[a.component_index[a.sites_a[0].assoc_comp]] * a.sites_a[0].n; - let rhob = - state.partial_density[a.component_index[a.sites_b[0].assoc_comp]] * a.sites_b[0].n; - - // fraction of non-bonded association sites - let sqrt = ((delta * (rhoa - rhob) + 1.0).powi(2) + delta * rhob * 4.0).sqrt(); - let xa = (sqrt + (delta * (rhob - rhoa) + 1.0)).recip() * 2.0; - let xb = (sqrt + (delta * (rhoa - rhob) + 1.0)).recip() * 2.0; - - (rhoa * (xa.ln() - xa * 0.5 + 0.5) + rhob * (xb.ln() - xb * 0.5 + 0.5)) * state.volume - } - - fn helmholtz_energy_cc_analytic + Copy>( - &self, - state: &StateHD, - delta: D, - ) -> D { - let a = &self.association_parameters; - - // site density - let rhoc = - state.partial_density[a.component_index[a.sites_c[0].assoc_comp]] * a.sites_c[0].n; - - // fraction of non-bonded association sites - let xc = ((delta * 4.0 * rhoc + 1.0).sqrt() + 1.0).recip() * 2.0; - rhoc * (xc.ln() - xc * 0.5 + 0.5) * state.volume - } - - #[allow(clippy::too_many_arguments)] - fn helmholtz_energy_density_cross_association + Copy, S: Data>( - rho: &ArrayBase, - delta_ab: &Array2, - delta_cc: &Array2, - max_iter: usize, - tol: f64, - x0: Option<&mut Array1>, - ) -> EosResult { - // check if density is close to 0 - if rho.sum().re() < f64::EPSILON { - if let Some(x0) = x0 { - x0.fill(1.0); - } - return Ok(D::zero()); - } - - // cross-association according to Michelsen2006 - // initialize monomer fraction - let mut x = match &x0 { - Some(x0) => (*x0).clone(), - None => Array::from_elem(rho.len(), 0.2), - }; - let delta_ab_re = delta_ab.map(D::re); - let delta_cc_re = delta_cc.map(D::re); - let rho_re = rho.map(D::re); - for k in 0..max_iter { - if Self::newton_step_cross_association( - &mut x, - &delta_ab_re, - &delta_cc_re, - &rho_re, - tol, - )? { - break; - } - if k == max_iter - 1 { - return Err(EosError::NotConverged("Cross association".into())); - } - } - - // calculate derivatives - let mut x_dual = x.mapv(D::from); - for _ in 0..D::NDERIV { - Self::newton_step_cross_association(&mut x_dual, delta_ab, delta_cc, rho, tol)?; - } - - // save monomer fraction - if let Some(x0) = x0 { - *x0 = x; - } - - // Helmholtz energy density - let f = |x: D| x.ln() - x * 0.5 + 0.5; - - Ok((rho * x_dual.mapv(f)).sum()) - } - - fn newton_step_cross_association + Copy, S: Data>( - x: &mut Array1, - delta_ab: &Array2, - delta_cc: &Array2, - rho: &ArrayBase, - tol: f64, - ) -> EosResult { - let nassoc = x.len(); - // gradient - let mut g = x.map(D::recip); - // Hessian - let mut h: Array2 = Array::zeros([nassoc; 2]); - - // split arrays - let &[a, b] = delta_ab.shape() else { - panic!("wrong shape!") - }; - let c = delta_cc.shape()[0]; - let (xa, xc) = x.view().split_at(Axis(0), a + b); - let (xa, xb) = xa.split_at(Axis(0), a); - let (rhoa, rhoc) = rho.view().split_at(Axis(0), a + b); - let (rhoa, rhob) = rhoa.split_at(Axis(0), a); - - for i in 0..nassoc { - // calculate gradients - let (d, dnx) = if i < a { - let d = delta_ab.index_axis(Axis(0), i); - (d, (&xb * &rhob * d).sum() + 1.0) - } else if i < a + b { - let d = delta_ab.index_axis(Axis(1), i - a); - (d, (&xa * &rhoa * d).sum() + 1.0) - } else { - let d = delta_cc.index_axis(Axis(0), i - a - b); - (d, (&xc * &rhoc * d).sum() + 1.0) - }; - g[i] -= dnx; - - // approximate hessian - h[(i, i)] = -dnx / x[i]; - if i < a { - for j in 0..b { - h[(i, a + j)] = -d[j] * rhob[j]; - } - } else if i < a + b { - for j in 0..a { - h[(i, j)] = -d[j] * rhoa[j]; - } - } else { - for j in 0..c { - h[(i, a + b + j)] -= d[j] * rhoc[j]; - } - } - } - - // Newton step - // avoid stepping to negative values for x (see Michelsen 2006) - let delta_x = LU::new(h)?.solve(&g); - Zip::from(x).and(&delta_x).for_each(|x, &delta_x| { - if delta_x.re() < x.re() * 0.8 { - *x -= delta_x - } else { - *x *= 0.2 - } - }); - - // check convergence - Ok(norm(&g.map(D::re)) < tol) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_binary_parameters() { - let comp1 = vec![AssociationRecord::new(0.1, 2500., 1.0, 1.0, 0.0)]; - let comp2 = vec![AssociationRecord::new(0.2, 1500., 1.0, 1.0, 0.0)]; - let comp3 = vec![AssociationRecord::new(0.3, 500., 0.0, 1.0, 0.0)]; - let comp4 = vec![ - AssociationRecord::new(0.3, 1000., 1.0, 0.0, 0.0), - AssociationRecord::new(0.3, 2000., 0.0, 1.0, 0.0), - ]; - let records = [comp1, comp2, comp3, comp4]; - let sigma = arr1(&[3.0, 3.0, 3.0, 3.0]); - let binary = [ - ( - (0, 1), - BinaryAssociationRecord::new(Some(3.5), Some(1234.), Some([0, 0])), - ), - ( - (0, 2), - BinaryAssociationRecord::new(Some(3.5), Some(3140.), Some([0, 0])), - ), - ( - (1, 3), - BinaryAssociationRecord::new(Some(3.5), Some(3333.), Some([0, 1])), - ), - ]; - let assoc = AssociationParameters::new(&records, &sigma, &binary, None); - println!("{}", assoc.epsilon_k_ab); - let epsilon_k_ab = arr2(&[ - [2500., 1234., 3140., 2250.], - [1234., 1500., 1000., 3333.], - [1750., 1250., 750., 1500.], - ]); - assert_eq!(assoc.epsilon_k_ab, epsilon_k_ab); - } - - #[test] - fn test_induced_association() { - let comp1 = vec![AssociationRecord::new(0.1, 2500., 1.0, 1.0, 0.0)]; - let comp2 = vec![AssociationRecord::new(0.1, -500., 0.0, 1.0, 0.0)]; - let comp3 = vec![AssociationRecord::new(0.0, 0.0, 0.0, 1.0, 0.0)]; - let sigma = arr1(&[3.0, 3.5]); - let binary = [( - (0, 1), - BinaryAssociationRecord::new(Some(0.1), Some(1000.), None), - )]; - let assoc1 = AssociationParameters::new(&[comp1.clone(), comp2], &sigma, &[], None); - let assoc2 = AssociationParameters::new(&[comp1, comp3], &sigma, &binary, None); - println!("{}", assoc1.epsilon_k_ab); - println!("{}", assoc2.epsilon_k_ab); - assert_eq!(assoc1.epsilon_k_ab, assoc2.epsilon_k_ab); - println!("{}", assoc1.sigma3_kappa_ab); - println!("{}", assoc2.sigma3_kappa_ab); - assert_eq!(assoc1.sigma3_kappa_ab, assoc2.sigma3_kappa_ab); - } -} - -#[cfg(test)] -#[cfg(feature = "pcsaft")] -mod tests_pcsaft { - use super::*; - use crate::pcsaft::parameters::utils::water_parameters; - use crate::pcsaft::PcSaftParameters; - use approx::assert_relative_eq; - use feos_core::parameter::{Parameter, ParameterError}; - - #[test] - fn helmholtz_energy() { - let params = Arc::new(water_parameters()); - let assoc = Association::new(¶ms, ¶ms.association, 50, 1e-10); - let t = 350.0; - let v = 41.248289328513216; - let n = 1.23; - let s = StateHD::new(t, v, arr1(&[n])); - let d = params.hs_diameter(t); - let a_rust = assoc.helmholtz_energy(&s, &d) / n; - assert_relative_eq!(a_rust, -4.229878997054543, epsilon = 1e-10); - } - - #[test] - fn helmholtz_energy_cross() { - let params = Arc::new(water_parameters()); - let assoc = Association::new_cross_association(¶ms, ¶ms.association, 50, 1e-10); - let t = 350.0; - let v = 41.248289328513216; - let n = 1.23; - let s = StateHD::new(t, v, arr1(&[n])); - let d = params.hs_diameter(t); - let a_rust = assoc.helmholtz_energy(&s, &d) / n; - assert_relative_eq!(a_rust, -4.229878997054543, epsilon = 1e-10); - } - - #[test] - fn helmholtz_energy_cross_3b() -> Result<(), ParameterError> { - let mut params = water_parameters(); - let mut record = params.pure_records.pop().unwrap(); - let mut association_record = record.model_record.association_record.unwrap(); - association_record.na = 2.0; - record.model_record.association_record = Some(association_record); - let params = Arc::new(PcSaftParameters::new_pure(record)?); - let assoc = Association::new(¶ms, ¶ms.association, 50, 1e-10); - let cross_assoc = - Association::new_cross_association(¶ms, ¶ms.association, 50, 1e-10); - let t = 350.0; - let v = 41.248289328513216; - let n = 1.23; - let s = StateHD::new(t, v, arr1(&[n])); - let d = params.hs_diameter(t); - let a_assoc = assoc.helmholtz_energy(&s, &d) / n; - let a_cross_assoc = cross_assoc.helmholtz_energy(&s, &d) / n; - assert_relative_eq!(a_assoc, a_cross_assoc, epsilon = 1e-10); - Ok(()) - } -} - -#[cfg(test)] -#[cfg(feature = "gc_pcsaft")] -mod tests_gc_pcsaft { - use super::*; - use crate::gc_pcsaft::eos::parameter::test::*; - use approx::assert_relative_eq; - use feos_core::si::{Pressure, METER, MOL, PASCAL}; - use ndarray::arr1; - use num_dual::Dual64; - use typenum::P3; - - #[test] - fn test_assoc_propanol() { - let params = Arc::new(propanol()); - let contrib = Association::new(¶ms, ¶ms.association, 50, 1e-10); - let temperature = 300.0; - let volume = METER.powi::().to_reduced(); - let moles = (1.5 * MOL).to_reduced(); - let state = StateHD::new( - Dual64::from_re(temperature), - Dual64::from_re(volume).derivative(), - arr1(&[Dual64::from_re(moles)]), - ); - let diameter = params.hs_diameter(state.temperature); - let pressure = - Pressure::from_reduced(-contrib.helmholtz_energy(&state, &diameter).eps * temperature); - assert_relative_eq!(pressure, -3.6819598891967344 * PASCAL, max_relative = 1e-10); - } - - #[test] - fn test_cross_assoc_propanol() { - let params = Arc::new(propanol()); - let contrib = Association::new_cross_association(¶ms, ¶ms.association, 50, 1e-10); - let temperature = 300.0; - let volume = METER.powi::().to_reduced(); - let moles = (1.5 * MOL).to_reduced(); - let state = StateHD::new( - Dual64::from_re(temperature), - Dual64::from_re(volume).derivative(), - arr1(&[Dual64::from_re(moles)]), - ); - let diameter = params.hs_diameter(state.temperature); - let pressure = - Pressure::from_reduced(-contrib.helmholtz_energy(&state, &diameter).eps * temperature); - assert_relative_eq!(pressure, -3.6819598891967344 * PASCAL, max_relative = 1e-10); - } - - #[test] - fn test_cross_assoc_ethanol_propanol() { - let params = Arc::new(ethanol_propanol(false)); - let contrib = Association::new(¶ms, ¶ms.association, 50, 1e-10); - let temperature = 300.0; - let volume = METER.powi::().to_reduced(); - let moles = (arr1(&[1.5, 2.5]) * MOL).to_reduced(); - let state = StateHD::new( - Dual64::from_re(temperature), - Dual64::from_re(volume).derivative(), - moles.mapv(Dual64::from_re), - ); - let diameter = params.hs_diameter(state.temperature); - let pressure = - Pressure::from_reduced(-contrib.helmholtz_energy(&state, &diameter).eps * temperature); - assert_relative_eq!(pressure, -26.105606376765632 * PASCAL, max_relative = 1e-10); - } -} diff --git a/src/epcsaft/association/python.rs b/src/epcsaft/association/python.rs deleted file mode 100644 index 9744f5552..000000000 --- a/src/epcsaft/association/python.rs +++ /dev/null @@ -1,49 +0,0 @@ -use super::AssociationRecord; -use feos_core::impl_json_handling; -use feos_core::parameter::ParameterError; -use pyo3::prelude::*; - -/// Pure component association parameters -#[pyclass(name = "AssociationRecord")] -#[derive(Clone)] -pub struct PyAssociationRecord(pub AssociationRecord); - -#[pymethods] -impl PyAssociationRecord { - #[new] - #[pyo3(signature = (kappa_ab, epsilon_k_ab, na=0.0, nb=0.0, nc=0.0))] - fn new(kappa_ab: f64, epsilon_k_ab: f64, na: f64, nb: f64, nc: f64) -> Self { - Self(AssociationRecord::new(kappa_ab, epsilon_k_ab, na, nb, nc)) - } - - #[getter] - fn get_kappa_ab(&self) -> f64 { - self.0.kappa_ab - } - - #[getter] - fn get_epsilon_k_ab(&self) -> f64 { - self.0.epsilon_k_ab - } - - #[getter] - fn get_na(&self) -> f64 { - self.0.na - } - - #[getter] - fn get_nb(&self) -> f64 { - self.0.nb - } - - #[getter] - fn get_nc(&self) -> f64 { - self.0.nc - } - - fn __repr__(&self) -> PyResult { - Ok(self.0.to_string()) - } -} - -impl_json_handling!(PyAssociationRecord); diff --git a/src/epcsaft/eos/born.rs b/src/epcsaft/eos/born.rs index 1555f1e0b..ec690a973 100644 --- a/src/epcsaft/eos/born.rs +++ b/src/epcsaft/eos/born.rs @@ -1,7 +1,7 @@ use crate::epcsaft::eos::permittivity::Permittivity; -use ndarray::Array1; use crate::epcsaft::parameters::ElectrolytePcSaftParameters; use feos_core::StateHD; +use ndarray::Array1; use num_dual::DualNum; use std::fmt; use std::sync::Arc; @@ -13,7 +13,11 @@ pub struct Born { } impl Born { - pub fn helmholtz_energy + Copy>(&self, state: &StateHD, diameter: &Array1) -> D { + pub fn helmholtz_energy + Copy>( + &self, + state: &StateHD, + diameter: &Array1, + ) -> D { // Parameters let p = &self.parameters; diff --git a/src/epcsaft/eos/ionic.rs b/src/epcsaft/eos/ionic.rs index 8f8482f8e..dd54b72da 100644 --- a/src/epcsaft/eos/ionic.rs +++ b/src/epcsaft/eos/ionic.rs @@ -20,7 +20,7 @@ impl ElectrolytePcSaftParameters { // relative permittivity of water (usually function of T,p,x) let epsilon_r = Permittivity::new(state, self, &epcsaft_variant).permittivity; - + let epsreps0 = epsilon_r * epsilon_0; // unit charge @@ -42,13 +42,17 @@ pub struct Ionic { } impl Ionic { - pub fn helmholtz_energy + Copy>(&self, state: &StateHD, diameter: &Array1) -> D { + pub fn helmholtz_energy + Copy>( + &self, + state: &StateHD, + diameter: &Array1, + ) -> D { // Extract parameters let p = &self.parameters; // Calculate Bjerrum length let lambda_b = p.bjerrum_length(state, self.variant); - + // Calculate inverse Debye length let mut sum_dens_z = D::zero(); for i in 0..state.molefracs.len() { @@ -67,7 +71,7 @@ impl Ionic { + 1.5) }) .collect(); - + let mut sum_x_z_chi = D::zero(); for i in 0..state.molefracs.len() { sum_x_z_chi += chi[i] * state.molefracs[i] * p.z[i].powi(2); diff --git a/src/epcsaft/eos/mod.rs b/src/epcsaft/eos/mod.rs index 845254c5c..e613a592e 100644 --- a/src/epcsaft/eos/mod.rs +++ b/src/epcsaft/eos/mod.rs @@ -1,7 +1,4 @@ use crate::association::Association; -// use crate::epcsaft::association::Association; -// use crate::epcsaft::hard_sphere::HardSphere; -// use crate::epcsaft::hard_sphere::HardSphereProperties; use crate::epcsaft::parameters::ElectrolytePcSaftParameters; use crate::hard_sphere::{HardSphere, HardSphereProperties}; use feos_core::parameter::Parameter; diff --git a/src/epcsaft/hard_sphere/mod.rs b/src/epcsaft/hard_sphere/mod.rs deleted file mode 100644 index 7d5a80885..000000000 --- a/src/epcsaft/hard_sphere/mod.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! Generic implementation of the hard-sphere contribution -//! that can be used across models. -use feos_core::StateHD; -use ndarray::*; -use num_dual::DualNum; -use std::f64::consts::FRAC_PI_6; -use std::fmt; -use std::{borrow::Cow, sync::Arc}; - -/// Different monomer shapes for FMT and BMCSL. -pub enum MonomerShape<'a, D> { - /// For spherical monomers, the number of components. - Spherical(usize), - /// For non-spherical molecules in a homosegmented approach, the - /// chain length parameter $m$. - NonSpherical(Array1), - /// For non-spherical molecules in a heterosegmented approach, - /// the geometry factors for every segment and the component - /// index for every segment. - Heterosegmented([Array1; 4], &'a Array1), -} - -/// Properties of (generalized) hard sphere systems. -pub trait HardSphereProperties { - /// The [MonomerShape] used in the model. - fn monomer_shape>(&self, temperature: D) -> MonomerShape; - - /// The temperature dependent hard-sphere diameters of every segment. - fn hs_diameter>(&self, temperature: D) -> Array1; - - /// The temperature dependent sigma. - fn sigma_t>(&self, temperature: D) -> Array1; - - /// The temperature dependent sigma_ij. - fn sigma_ij_t>(&self, temperature: D) -> Array2; - - /// For every segment, the index of the component that it is on. - fn component_index(&self) -> Cow> { - match self.monomer_shape(1.0) { - MonomerShape::Spherical(n) => Cow::Owned(Array1::from_shape_fn(n, |i| i)), - MonomerShape::NonSpherical(m) => Cow::Owned(Array1::from_shape_fn(m.len(), |i| i)), - MonomerShape::Heterosegmented(_, component_index) => Cow::Borrowed(component_index), - } - } - - /// The geometry coefficients $C_{k,\alpha}$ for every segment. - fn geometry_coefficients>(&self, temperature: D) -> [Array1; 4] { - match self.monomer_shape(temperature) { - MonomerShape::Spherical(n) => { - let m = Array1::ones(n); - [m.clone(), m.clone(), m.clone(), m] - } - MonomerShape::NonSpherical(m) => [m.clone(), m.clone(), m.clone(), m], - MonomerShape::Heterosegmented(g, _) => g, - } - } - - /// The packing fractions $\zeta_k$. - fn zeta + Copy, const N: usize>( - &self, - temperature: D, - partial_density: &Array1, - k: [i32; N], - ) -> [D; N] { - let component_index = self.component_index(); - let geometry_coefficients = self.geometry_coefficients(temperature); - let diameter = self.hs_diameter(temperature); - let mut zeta = [D::zero(); N]; - for i in 0..diameter.len() { - for (z, &k) in zeta.iter_mut().zip(k.iter()) { - *z += partial_density[component_index[i]] - * diameter[i].powi(k) - * (geometry_coefficients[k as usize][i] * FRAC_PI_6); - } - } - - zeta - } - - /// The fraction $\frac{\zeta_2}{\zeta_3}$ evaluated in a way to avoid a division by 0 when the density is 0. - fn zeta_23 + Copy>(&self, temperature: D, molefracs: &Array1) -> D { - let component_index = self.component_index(); - let geometry_coefficients = self.geometry_coefficients(temperature); - let diameter = self.hs_diameter(temperature); - let mut zeta: [D; 2] = [D::zero(); 2]; - for i in 0..diameter.len() { - for (k, z) in zeta.iter_mut().enumerate() { - *z += molefracs[component_index[i]] - * diameter[i].powi((k + 2) as i32) - * (geometry_coefficients[k + 2][i] * FRAC_PI_6); - } - } - - zeta[0] / zeta[1] - } -} - -/// Implementation of the BMCSL equation of state for hard-sphere mixtures. -/// -/// This structure provides an implementation of the Boublík-Mansoori-Carnahan-Starling-Leland (BMCSL) equation of state ([Boublík, 1970](https://doi.org/10.1063/1.1673824), [Mansoori et al., 1971](https://doi.org/10.1063/1.1675048)) that is often used as reference contribution in SAFT equations of state. The implementation is generalized to allow the description of non-sperical or fused-sphere reference fluids. -/// -/// The reduced Helmholtz energy is calculated according to -/// $$\frac{\beta A}{V}=\frac{6}{\pi}\left(\frac{3\zeta_1\zeta_2}{1-\zeta_3}+\frac{\zeta_2^3}{\zeta_3\left(1-\zeta_3\right)^2}+\left(\frac{\zeta_2^3}{\zeta_3^2}-\zeta_0\right)\ln\left(1-\zeta_3\right)\right)$$ -/// with the packing fractions -/// $$\zeta_k=\frac{\pi}{6}\sum_\alpha C_{k,\alpha}\rho_\alpha d_\alpha^k,~~~~~~~~k=0\ldots 3.$$ -/// -/// The geometry coefficients $C_{k,\alpha}$ and the segment diameters $d_\alpha$ are specified via the [HardSphereProperties] trait. -pub struct HardSphere

{ - parameters: Arc

, -} - -impl

HardSphere

{ - pub fn new(parameters: &Arc

) -> Self { - Self { - parameters: parameters.clone(), - } - } -} - -impl HardSphere

{ - #[inline] - pub fn helmholtz_energy + Copy>(&self, state: &StateHD) -> D { - let p = &self.parameters; - let zeta = p.zeta(state.temperature, &state.partial_density, [0, 1, 2, 3]); - let frac_1mz3 = -(zeta[3] - 1.0).recip(); - let zeta_23 = p.zeta_23(state.temperature, &state.molefracs); - state.volume * 6.0 / std::f64::consts::PI - * (zeta[1] * zeta[2] * frac_1mz3 * 3.0 - + zeta[2].powi(2) * frac_1mz3.powi(2) * zeta_23 - + (zeta[2] * zeta_23.powi(2) - zeta[0]) * (zeta[3] * (-1.0)).ln_1p()) - } -} - -impl

fmt::Display for HardSphere

{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Hard Sphere") - } -} diff --git a/src/epcsaft/mod.rs b/src/epcsaft/mod.rs index 288e9b022..834f53c22 100644 --- a/src/epcsaft/mod.rs +++ b/src/epcsaft/mod.rs @@ -4,9 +4,7 @@ #![warn(clippy::all)] #![allow(clippy::too_many_arguments)] -// mod association; mod eos; -// mod hard_sphere; pub(crate) mod parameters; pub use eos::{ElectrolytePcSaft, ElectrolytePcSaftOptions, ElectrolytePcSaftVariants}; diff --git a/src/epcsaft/parameters.rs b/src/epcsaft/parameters.rs index edb6478e5..77f982940 100644 --- a/src/epcsaft/parameters.rs +++ b/src/epcsaft/parameters.rs @@ -688,29 +688,6 @@ impl HardSphereProperties for ElectrolytePcSaftParameters { } d } - - // fn sigma_t>(&self, temperature: D) -> Array1 { - // let mut sigma_t: Array1 = Array::from_shape_fn(self.sigma.len(), |i| self.sigma[i]); - // for i in 0..self.sigma_t_comp.len() { - // sigma_t[i] = (sigma_t[i] + (temperature.re() * -0.01775).exp() * 10.11 - // - (temperature.re() * -0.01146).exp() * 1.417) - // .re() - // } - // sigma_t - // } - - // fn sigma_ij_t>(&self, temperature: D) -> Array2 { - // let diameter = self.sigma_t(temperature); - // let n = diameter.len(); - - // let mut sigma_ij_t = Array::zeros((n, n)); - // for i in 0..n { - // for j in 0..n { - // sigma_ij_t[[i, j]] = (diameter[i] + diameter[j]) * 0.5; - // } - // } - // sigma_ij_t - // } } impl ElectrolytePcSaftParameters { From f55433126d1dbecd7db9eeb66ea9d6cf6652c24f Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Fri, 22 Mar 2024 16:15:15 +0100 Subject: [PATCH 07/23] changed content of enum PermittivityRecord to not contain an additional vector --- src/epcsaft/eos/born.rs | 1 + src/epcsaft/eos/ionic.rs | 4 +- src/epcsaft/eos/mod.rs | 9 ++ src/epcsaft/eos/permittivity.rs | 164 +++++++++++++++++++------------- src/epcsaft/parameters.rs | 108 ++++++++++----------- src/epcsaft/python.rs | 82 ++++------------ 6 files changed, 179 insertions(+), 189 deletions(-) diff --git a/src/epcsaft/eos/born.rs b/src/epcsaft/eos/born.rs index ec690a973..a283fc7e9 100644 --- a/src/epcsaft/eos/born.rs +++ b/src/epcsaft/eos/born.rs @@ -30,6 +30,7 @@ impl Born { &self.parameters, &ElectrolytePcSaftVariants::Advanced, ) + .unwrap() .permittivity; // Calculate sum xi zi^2 / di diff --git a/src/epcsaft/eos/ionic.rs b/src/epcsaft/eos/ionic.rs index dd54b72da..d2401afaa 100644 --- a/src/epcsaft/eos/ionic.rs +++ b/src/epcsaft/eos/ionic.rs @@ -19,7 +19,9 @@ impl ElectrolytePcSaftParameters { let epsilon_0 = 8.85416e-12; // relative permittivity of water (usually function of T,p,x) - let epsilon_r = Permittivity::new(state, self, &epcsaft_variant).permittivity; + let epsilon_r = Permittivity::new(state, self, &epcsaft_variant) + .unwrap() + .permittivity; let epsreps0 = epsilon_r * epsilon_0; diff --git a/src/epcsaft/eos/mod.rs b/src/epcsaft/eos/mod.rs index e613a592e..6494941a0 100644 --- a/src/epcsaft/eos/mod.rs +++ b/src/epcsaft/eos/mod.rs @@ -111,6 +111,15 @@ impl ElectrolytePcSaft { None }; + match options.epcsaft_variant { + ElectrolytePcSaftVariants::Revised => { + if ionic.is_some() { + panic!("Ionic contribution is not available in the revised ePC-SAFT variant.") + } + } + ElectrolytePcSaftVariants::Advanced => (), + } + Self { parameters, options, diff --git a/src/epcsaft/eos/permittivity.rs b/src/epcsaft/eos/permittivity.rs index 3da488c76..54097d3f7 100644 --- a/src/epcsaft/eos/permittivity.rs +++ b/src/epcsaft/eos/permittivity.rs @@ -1,4 +1,5 @@ -use feos_core::StateHD; +use feos_core::parameter::ParameterError; +use feos_core::{EosError, StateHD}; use ndarray::Array1; use num_dual::DualNum; use serde::{Deserialize, Serialize}; @@ -11,12 +12,12 @@ use crate::epcsaft::parameters::ElectrolytePcSaftParameters; #[derive(Serialize, Deserialize, Clone, Debug)] pub enum PermittivityRecord { ExperimentalData { - data: Vec>, + data: Vec<(f64, f64)>, }, PerturbationTheory { - dipole_scaling: Vec, - polarizability_scaling: Vec, - correlation_integral_parameter: Vec, + dipole_scaling: f64, + polarizability_scaling: f64, + correlation_integral_parameter: f64, }, } @@ -32,12 +33,12 @@ impl std::fmt::Display for PermittivityRecord { polarizability_scaling, correlation_integral_parameter, } => { - write!(f, "PermittivityRecord(dipole_scaling={}", dipole_scaling[0])?; - write!(f, ", polarizability_scaling={}", polarizability_scaling[0])?; + write!(f, "PermittivityRecord(dipole_scaling={}", dipole_scaling)?; + write!(f, ", polarizability_scaling={}", polarizability_scaling)?; write!( f, ", correlation_integral_parameter={}", - correlation_integral_parameter[0] + correlation_integral_parameter )?; write!(f, ")") } @@ -55,15 +56,13 @@ impl + Copy> Permittivity { state: &StateHD, parameters: &ElectrolytePcSaftParameters, epcsaft_variant: &ElectrolytePcSaftVariants, - ) -> Self { - let n = parameters.pure_records.len(); - + ) -> Result { // Set permittivity to an arbitrary value of 1 if system contains no ions // Ionic and Born contributions will be zero anyways if parameters.nionic == 0 { - return Self { + return Ok(Self { permittivity: D::one() * 1., - }; + }); } let all_comp: Array1 = parameters .pure_records @@ -73,71 +72,103 @@ impl + Copy> Permittivity { .collect(); if let ElectrolytePcSaftVariants::Advanced = epcsaft_variant { - let permittivity = match parameters.permittivity.as_ref().unwrap() { - PermittivityRecord::ExperimentalData { data } => { - // Check length of permittivity_record - if data.len() != n { - panic!("Provide permittivities for all components for ePC-SAFT advanced.") - } - Self::from_experimental_data(data, state.temperature, &state.molefracs) - .permittivity - } - PermittivityRecord::PerturbationTheory { - dipole_scaling, - polarizability_scaling, - correlation_integral_parameter, - } => { - // Check length of permittivity_record - if dipole_scaling.len() != n { - panic!("Provide permittivities for all components for ePC-SAFT advanced.") - } - Self::from_perturbation_theory( - state, + // check if permittivity is Some for all components + if parameters + .permittivity + .iter() + .any(|record| record.is_none()) + { + return Err(EosError::ParameterError( + ParameterError::IncompatibleParameters( + "Provide permittivities for all components for ePC-SAFT advanced." + .to_string(), + ), + )); + } + + // Extract parameters from PermittivityRecords + let mut mu_scaling: Vec<&f64> = vec![]; + let mut alpha_scaling: Vec<&f64> = vec![]; + let mut ci_param: Vec<&f64> = vec![]; + let mut datas: Vec> = vec![]; + + parameters + .permittivity + .iter() + .for_each(|record| match record.as_ref().unwrap() { + PermittivityRecord::PerturbationTheory { dipole_scaling, polarizability_scaling, correlation_integral_parameter, - &all_comp, - ) - .permittivity - } - }; + } => { + mu_scaling.push(dipole_scaling); + alpha_scaling.push(polarizability_scaling); + ci_param.push(correlation_integral_parameter); + } + PermittivityRecord::ExperimentalData { data } => { + datas.push(data.clone()); + } + }); - return Self { permittivity }; + if let PermittivityRecord::ExperimentalData { .. } = + parameters.permittivity[0].as_ref().unwrap() + { + let permittivity = + Self::from_experimental_data(&datas, state.temperature, &state.molefracs) + .permittivity; + return Ok(Self { permittivity }); + } + + if let PermittivityRecord::PerturbationTheory { .. } = + parameters.permittivity[0].as_ref().unwrap() + { + let permittivity = Self::from_perturbation_theory( + state, + &mu_scaling, + &alpha_scaling, + &ci_param, + &all_comp, + ) + .permittivity; + return Ok(Self { permittivity }); + } } + if let ElectrolytePcSaftVariants::Revised = epcsaft_variant { if parameters.nsolvent > 1 { - panic!( - "The use of ePC-SAFT revised requires the definition of exactly 1 solvent. Currently specified: {} solvents", parameters.nsolvent - ) + return Err(EosError::ParameterError( + ParameterError::IncompatibleParameters( + "ePC-SAFT revised cannot be used for more than 1 solvent.".to_string(), + ), + )); }; - let permittivity = match parameters.permittivity.as_ref().unwrap() { + let permittivity = match parameters.permittivity[parameters.solvent_comp[0]] + .as_ref() + .unwrap() + { PermittivityRecord::ExperimentalData { data } => { - Self::pure_from_experimental_data(&data[0], state.temperature).permittivity + Self::pure_from_experimental_data(data, state.temperature).permittivity } PermittivityRecord::PerturbationTheory { dipole_scaling, polarizability_scaling, correlation_integral_parameter, } => { - // Check length of permittivity_record - if dipole_scaling.len() != n { - panic!("Provide permittivities for all components for ePC-SAFT advanced.") - } Self::pure_from_perturbation_theory( state, - &dipole_scaling[0], - &polarizability_scaling[0], - &correlation_integral_parameter[0], + *dipole_scaling, + *polarizability_scaling, + *correlation_integral_parameter, ) .permittivity } }; - return Self { permittivity }; + return Ok(Self { permittivity }); }; - Self { - permittivity: D::zero(), - } + Err(EosError::ParameterError( + ParameterError::IncompatibleParameters("Permittivity computation failed".to_string()), + )) } pub fn pure_from_experimental_data(data: &[(f64, f64)], temperature: D) -> Self { @@ -149,9 +180,9 @@ impl + Copy> Permittivity { pub fn pure_from_perturbation_theory( state: &StateHD, - dipole_scaling: &f64, - polarizability_scaling: &f64, - correlation_integral_parameter: &f64, + dipole_scaling: f64, + polarizability_scaling: f64, + correlation_integral_parameter: f64, ) -> Self { // reciprocal thermodynamic temperature let boltzmann = 1.380649e-23; @@ -162,12 +193,11 @@ impl + Copy> Permittivity { let density = state.moles.mapv(|n| n / state.volume).sum(); // dipole density y -> scaled dipole density y_star - let y_star = density * (beta * *dipole_scaling * 1e-19 + *polarizability_scaling * 3.) * 4. - / 9. - * PI; + let y_star = + density * (beta * dipole_scaling * 1e-19 + polarizability_scaling * 3.) * 4. / 9. * PI; // correlation integral - let correlation_integral = ((-y_star).exp() - 1.0) * *correlation_integral_parameter + 1.0; + let correlation_integral = ((-y_star).exp() - 1.0) * correlation_integral_parameter + 1.0; // dielectric constan let permittivity_pure = y_star @@ -195,9 +225,9 @@ impl + Copy> Permittivity { pub fn from_perturbation_theory( state: &StateHD, - dipole_scaling: &[f64], - polarizability_scaling: &[f64], - correlation_integral_parameter: &[f64], + dipole_scaling: &[&f64], + polarizability_scaling: &[&f64], + correlation_integral_parameter: &[&f64], comp: &Array1, ) -> Self { //let nsolvent = comp.len(); @@ -212,10 +242,10 @@ impl + Copy> Permittivity { let x_i = state.molefracs[*i]; y_star += - rho_i * (beta * dipole_scaling[*i] * 1e-19 + polarizability_scaling[*i] * 3.) * 4. + rho_i * (beta * *dipole_scaling[*i] * 1e-19 + polarizability_scaling[*i] * 3.) * 4. / 9. * PI; - correlation_integral_parameter_mixture += x_i * correlation_integral_parameter[*i]; + correlation_integral_parameter_mixture += x_i * *correlation_integral_parameter[*i]; } // correlation integral diff --git a/src/epcsaft/parameters.rs b/src/epcsaft/parameters.rs index 77f982940..e55cc8a9c 100644 --- a/src/epcsaft/parameters.rs +++ b/src/epcsaft/parameters.rs @@ -362,7 +362,7 @@ pub struct ElectrolytePcSaftParameters { pub solvent_comp: Array1, pub viscosity: Option>, pub diffusion: Option>, - pub permittivity: Option, + pub permittivity: Array1>, pub thermal_conductivity: Option>, pub pure_records: Vec>, pub binary_records: Option>, @@ -503,7 +503,9 @@ impl Parameter for ElectrolytePcSaftParameters { for j in 0..n { let temp_kij = binary_records[[i, j]].k_ij.clone(); if temp_kij.len() > 4 { - panic!("Binary interaction for component {} with {} is parametrized with more than 4 k_ij coefficients.", i, j); + return Err(ParameterError::IncompatibleParameters( + format!("Binary interaction for component {} with {} is parametrized with more than 4 k_ij coefficients.", i, j), + )); } else { (0..temp_kij.len()).for_each(|k| { k_ij[[i, j]][k] = temp_kij[k]; @@ -561,72 +563,64 @@ impl Parameter for ElectrolytePcSaftParameters { Some(v) }; - // Permittivity - let permittivity_records: Array1 = pure_records + // Permittivity records + let mut permittivity_records: Array1> = pure_records .iter() - .filter(|&record| (record.model_record.permittivity_record.is_some())) - .map(|record| record.clone().model_record.permittivity_record.unwrap()) + .map(|record| record.clone().model_record.permittivity_record) .collect(); - if nionic != 0 && permittivity_records.len() < nsolvent { - panic!("Provide permittivity records for each solvent.") + // Check if permittivity_records contains maximum one record for each solvent + // Permittivity + if nionic != 0 + && permittivity_records + .iter() + .enumerate() + .any(|(i, record)| record.is_none() && z[i] == 0.0) + { + return Err(ParameterError::IncompatibleParameters( + "Provide permittivity record for all solvent components.".to_string(), + )); } - let mut modeltype = -1; - let mut mu_scaling: Vec = vec![]; - let mut alpha_scaling: Vec = vec![]; - let mut ci_param: Vec = vec![]; - let mut points: Vec> = vec![]; + let mut modeltypes: Vec = vec![]; permittivity_records .iter() - .enumerate() - .for_each(|(i, record)| { - match record { - PermittivityRecord::PerturbationTheory { - dipole_scaling, - polarizability_scaling, - correlation_integral_parameter, - } => { - if modeltype == 2 { - panic!("Inconsistent models for permittivity.") - }; - modeltype = 1; - mu_scaling.push(dipole_scaling[0]); - alpha_scaling.push(polarizability_scaling[0]); - ci_param.push(correlation_integral_parameter[0]); - } - PermittivityRecord::ExperimentalData { data } => { - if modeltype == 1 { - panic!("Inconsistent models for permittivity.") - }; - modeltype = 2; - points.push(data[0].clone()); - // Check if experimental data points are sorted - let mut t_check = 0.0; - for point in &data[0] { - if point.0 < t_check { - panic!("Permittivity points for component {} are unsorted.", i); - } - t_check = point.0; - } - } + .filter(|&record| (record.is_some())) + .for_each(|record| match record.as_ref().unwrap() { + PermittivityRecord::PerturbationTheory { .. } => { + modeltypes.push(1); + } + PermittivityRecord::ExperimentalData { .. } => { + modeltypes.push(2); } }); - let permittivity = match modeltype { - 1 => Some(PermittivityRecord::PerturbationTheory { - dipole_scaling: mu_scaling, - polarizability_scaling: alpha_scaling, - correlation_integral_parameter: ci_param, - }), - 2 => Some(PermittivityRecord::ExperimentalData { data: points }), - _ => None, - }; + // check if modeltypes contains a mix of 1 and 2 + if modeltypes.iter().any(|&x| x == 1) && modeltypes.iter().any(|&x| x == 2) { + return Err(ParameterError::IncompatibleParameters( + "Inconsistent models for permittivity.".to_string(), + )); + } - if nionic > 0 && permittivity.is_none() { - panic!("Permittivity of one or more solvents must be specified.") - }; + if modeltypes[0] == 2 { + // order points in data by increasing temperature + let mut permittivity_records_clone = permittivity_records.clone(); + permittivity_records_clone + .iter_mut() + .filter(|record| (record.is_some())) + .enumerate() + .for_each(|(i, record)| { + if let PermittivityRecord::ExperimentalData { data } = record.as_mut().unwrap() + { + let mut data = data.clone(); + data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + // save data again in record + permittivity_records[i] = + Some(PermittivityRecord::ExperimentalData { data }); + } + }); + } Ok(Self { molarweight, @@ -654,7 +648,7 @@ impl Parameter for ElectrolytePcSaftParameters { viscosity: viscosity_coefficients, diffusion: diffusion_coefficients, thermal_conductivity: thermal_conductivity_coefficients, - permittivity, + permittivity: permittivity_records, pure_records, binary_records, }) diff --git a/src/epcsaft/python.rs b/src/epcsaft/python.rs index c19840996..03841a76c 100644 --- a/src/epcsaft/python.rs +++ b/src/epcsaft/python.rs @@ -15,7 +15,6 @@ use pyo3::prelude::*; use std::convert::{TryFrom, TryInto}; use std::sync::Arc; - // Pure-substance parameters for the ePC-SAFT equation of state. /// /// Parameters @@ -41,7 +40,7 @@ use std::sync::Arc; /// nc : float, optional /// Number of association sites of type C. /// z : float, optional -/// Charge of the electrolyte. +/// Charge of the electrolyte. /// viscosity : List[float], optional /// Entropy-scaling parameters for viscosity. Defaults to `None`. /// diffusion : List[float], optional @@ -168,14 +167,8 @@ impl PyElectrolytePcSaftRecord { impl_json_handling!(PyElectrolytePcSaftRecord); -impl_pure_record!( - ElectrolytePcSaftRecord, - PyElectrolytePcSaftRecord -); -impl_segment_record!( - ElectrolytePcSaftRecord, - PyElectrolytePcSaftRecord -); +impl_pure_record!(ElectrolytePcSaftRecord, PyElectrolytePcSaftRecord); +impl_segment_record!(ElectrolytePcSaftRecord, PyElectrolytePcSaftRecord); #[pyclass(name = "ElectrolytePcSaftBinaryRecord")] #[derive(Clone)] @@ -185,7 +178,11 @@ pub struct PyElectrolytePcSaftBinaryRecord(ElectrolytePcSaftBinaryRecord); impl PyElectrolytePcSaftBinaryRecord { #[new] fn new(k_ij: [f64; 4]) -> Self { - Self(ElectrolytePcSaftBinaryRecord::new(Some(k_ij.to_vec()), None, None)) + Self(ElectrolytePcSaftBinaryRecord::new( + Some(k_ij.to_vec()), + None, + None, + )) } #[getter] @@ -206,12 +203,16 @@ impl_binary_record!( PyElectrolytePcSaftBinaryRecord ); - #[pyclass(name = "ElectrolytePcSaftParameters")] #[derive(Clone)] pub struct PyElectrolytePcSaftParameters(pub Arc); -impl_parameter!(ElectrolytePcSaftParameters, PyElectrolytePcSaftParameters, PyElectrolytePcSaftRecord, PyElectrolytePcSaftBinaryRecord); +impl_parameter!( + ElectrolytePcSaftParameters, + PyElectrolytePcSaftParameters, + PyElectrolytePcSaftRecord, + PyElectrolytePcSaftBinaryRecord +); #[pymethods] impl PyElectrolytePcSaftParameters { @@ -227,24 +228,6 @@ pub struct PyPermittivityRecord(pub PermittivityRecord); #[pymethods] impl PyPermittivityRecord { - /// pure_from_experimental_data - /// - /// Parameters - /// ---------- - /// interpolation_points : Vec<(f64, f64)> - /// - /// Returns - /// ------- - /// PermittivityRecord - /// - #[staticmethod] - #[pyo3(text_signature = "(interpolation_points)")] - pub fn pure_from_experimental_data(interpolation_points: Vec<(f64, f64)>) -> Self { - Self(PermittivityRecord::ExperimentalData { - data: Vec::from([interpolation_points]), - }) - } - /// from_experimental_data /// /// Parameters @@ -258,41 +241,12 @@ impl PyPermittivityRecord { #[staticmethod] #[allow(non_snake_case)] #[pyo3(text_signature = "(interpolation_points)")] - pub fn from_experimental_data(interpolation_points: Vec>) -> Self { + pub fn from_experimental_data(interpolation_points: Vec<(f64, f64)>) -> Self { Self(PermittivityRecord::ExperimentalData { data: interpolation_points, }) } - /// pure_from_perturbation_theory - /// - /// Parameters - /// ---------- - /// dipole_scaling : f64, - /// polarizability_scaling: f64, - /// correlation_integral_parameter : f64, - /// - /// Returns - /// ------- - /// PermittivityRecord - /// - #[staticmethod] - #[allow(non_snake_case)] - #[pyo3( - text_signature = "(dipole_scaling, polarizability_scaling, correlation_integral_parameter)" - )] - pub fn pure_from_perturbation_theory( - dipole_scaling: f64, - polarizability_scaling: f64, - correlation_integral_parameter: f64, - ) -> Self { - Self(PermittivityRecord::PerturbationTheory { - dipole_scaling: Vec::from([dipole_scaling]), - polarizability_scaling: Vec::from([polarizability_scaling]), - correlation_integral_parameter: Vec::from([correlation_integral_parameter]), - }) - } - /// from_perturbation_theory /// /// Parameters @@ -311,9 +265,9 @@ impl PyPermittivityRecord { text_signature = "(dipole_scaling, polarizability_scaling, correlation_integral_parameter)" )] pub fn from_perturbation_theory( - dipole_scaling: Vec, - polarizability_scaling: Vec, - correlation_integral_parameter: Vec, + dipole_scaling: f64, + polarizability_scaling: f64, + correlation_integral_parameter: f64, ) -> Self { Self(PermittivityRecord::PerturbationTheory { dipole_scaling, From 243525b8d1a4af52b4a99e8af6d0f73ab3032819 Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Fri, 22 Mar 2024 16:36:05 +0100 Subject: [PATCH 08/23] changed interpolation function to binary search, made sure parameters.rs creates points in correct order and all temperatures are finite --- src/epcsaft/eos/ionic.rs | 15 ++++--- src/epcsaft/eos/permittivity.rs | 69 +++++++++++++++------------------ src/epcsaft/parameters.rs | 6 +++ 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/epcsaft/eos/ionic.rs b/src/epcsaft/eos/ionic.rs index d2401afaa..26521298f 100644 --- a/src/epcsaft/eos/ionic.rs +++ b/src/epcsaft/eos/ionic.rs @@ -52,6 +52,12 @@ impl Ionic { // Extract parameters let p = &self.parameters; + // Set to zero if one of the ions is 0 + let sum_mole_fraction: f64 = p.ionic_comp.iter().map(|&i| state.molefracs[i].re()).sum(); + if sum_mole_fraction == 0. { + return D::zero(); + } + // Calculate Bjerrum length let lambda_b = p.bjerrum_length(state, self.variant); @@ -79,14 +85,7 @@ impl Ionic { sum_x_z_chi += chi[i] * state.molefracs[i] * p.z[i].powi(2); } - // Set to zero if one of the ions is 0 - let sum_mole_fraction: f64 = p.ionic_comp.iter().map(|&i| state.molefracs[i].re()).sum(); - let mut a_ion = -kappa * lambda_b * sum_x_z_chi * state.moles.sum(); - if sum_mole_fraction == 0. { - a_ion = D::zero(); - } - - a_ion + -kappa * lambda_b * sum_x_z_chi * state.moles.sum() } } diff --git a/src/epcsaft/eos/permittivity.rs b/src/epcsaft/eos/permittivity.rs index 54097d3f7..da2c07305 100644 --- a/src/epcsaft/eos/permittivity.rs +++ b/src/epcsaft/eos/permittivity.rs @@ -172,7 +172,7 @@ impl + Copy> Permittivity { } pub fn pure_from_experimental_data(data: &[(f64, f64)], temperature: D) -> Self { - let permittivity_pure = Self::interpolate(data.to_vec(), temperature).permittivity; + let permittivity_pure = Self::interpolate(data, temperature).permittivity; Self { permittivity: permittivity_pure, } @@ -218,7 +218,7 @@ impl + Copy> Permittivity { let permittivity = data .iter() .enumerate() - .map(|(i, d)| Self::interpolate(d.to_vec(), temperature).permittivity * molefracs[i]) + .map(|(i, d)| Self::interpolate(d, temperature).permittivity * molefracs[i]) .sum(); Self { permittivity } } @@ -261,43 +261,38 @@ impl + Copy> Permittivity { Self { permittivity } } - pub fn interpolate(interpolation_points: Vec<(f64, f64)>, temperature: D) -> Self { - let t_interpol = Array1::from_iter(interpolation_points.iter().map(|t| (t.0))); - let eps_interpol = Array1::from_iter(interpolation_points.iter().map(|e| e.1)); + /// Structure: &[(temperature, epsilon)] + /// Assume ordered by temperature + /// and temperatures are all finite. + pub fn interpolate(interpolation_points: &[(f64, f64)], temperature: D) -> Self { + // find index where temperature could be inserted + let i = interpolation_points.binary_search_by(|&(ti, _)| { + ti.partial_cmp(&temperature.re()) + .expect("Unexpected value for temperature in interpolation points.") + }); + // if the result is OK we don't need to interpolate ... + if let Ok(i) = i { + return Self { + permittivity: D::one() * interpolation_points[i].1, + }; + } - // Initialize permittivity - let mut permittivity_pure = D::zero(); + // if not, we can unwrap safely: + let i = i.unwrap_err(); + let n = interpolation_points.len(); + + // check cases: + // 0. : below lowest temperature + // >= n : above highest temperature + // else : regular interpolation + + let (l, u) = match i { + 0 => (interpolation_points[0], interpolation_points[1]), + i if i >= n => (interpolation_points[n - 2], interpolation_points[n - 1]), + _ => (interpolation_points[i - 1], interpolation_points[i]), + }; + let permittivity_pure = (temperature - l.0) / (u.0 - l.0) * (u.1 - l.1) + l.1; - // Check if only 1 data point is given - if interpolation_points.len() == 1 { - permittivity_pure = D::one() * eps_interpol[0]; - } else { - // Check which interval temperature is in - let temperature = temperature.re(); - for i in 0..(t_interpol.len() - 1) { - // Temperature is within intervals - if temperature >= t_interpol[i] && temperature < t_interpol[i + 1] { - // Interpolate - permittivity_pure = D::one() * eps_interpol[i] - + (temperature - t_interpol[i]) * (eps_interpol[i + 1] - eps_interpol[i]) - / (t_interpol[i + 1] - t_interpol[i]); - } - } - // Temperature is lower than lowest temperature - if temperature < t_interpol[0] { - // Extrapolate from eps_0 and eps_1 - permittivity_pure = D::one() * eps_interpol[0] - + (temperature - t_interpol[0]) * (eps_interpol[1] - eps_interpol[0]) - / (t_interpol[1] - t_interpol[0]); - // Temperature is higher than highest temperature - } else if temperature >= t_interpol[t_interpol.len() - 1] { - // extrapolate from last two epsilons - permittivity_pure = D::one() * eps_interpol[t_interpol.len() - 2] - + (temperature - t_interpol[t_interpol.len() - 2]) - * (eps_interpol[t_interpol.len() - 1] - eps_interpol[t_interpol.len() - 2]) - / (t_interpol[t_interpol.len() - 1] - t_interpol[t_interpol.len() - 2]); - } - } Self { permittivity: permittivity_pure, } diff --git a/src/epcsaft/parameters.rs b/src/epcsaft/parameters.rs index e55cc8a9c..13c6b7c86 100644 --- a/src/epcsaft/parameters.rs +++ b/src/epcsaft/parameters.rs @@ -615,6 +615,12 @@ impl Parameter for ElectrolytePcSaftParameters { { let mut data = data.clone(); data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + // check if all temperatures a.0 in data are finite, if not, make them finite by rounding to four digits + data.iter_mut().for_each(|a| { + if !a.0.is_finite() { + a.0 = (a.0 * 1e4).round() / 1e4; + } + }); // save data again in record permittivity_records[i] = Some(PermittivityRecord::ExperimentalData { data }); From b66199221818ce888ba0c7aad8dbc0e8e6d09049 Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Fri, 22 Mar 2024 16:51:58 +0100 Subject: [PATCH 09/23] removed bug: check if permittivity model types agree only if permittivity records exist --- src/epcsaft/parameters.rs | 49 +++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/epcsaft/parameters.rs b/src/epcsaft/parameters.rs index 13c6b7c86..1ac484000 100644 --- a/src/epcsaft/parameters.rs +++ b/src/epcsaft/parameters.rs @@ -603,29 +603,32 @@ impl Parameter for ElectrolytePcSaftParameters { )); } - if modeltypes[0] == 2 { - // order points in data by increasing temperature - let mut permittivity_records_clone = permittivity_records.clone(); - permittivity_records_clone - .iter_mut() - .filter(|record| (record.is_some())) - .enumerate() - .for_each(|(i, record)| { - if let PermittivityRecord::ExperimentalData { data } = record.as_mut().unwrap() - { - let mut data = data.clone(); - data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - // check if all temperatures a.0 in data are finite, if not, make them finite by rounding to four digits - data.iter_mut().for_each(|a| { - if !a.0.is_finite() { - a.0 = (a.0 * 1e4).round() / 1e4; - } - }); - // save data again in record - permittivity_records[i] = - Some(PermittivityRecord::ExperimentalData { data }); - } - }); + if modeltypes.len() >= 1 { + if modeltypes[0] == 2 { + // order points in data by increasing temperature + let mut permittivity_records_clone = permittivity_records.clone(); + permittivity_records_clone + .iter_mut() + .filter(|record| (record.is_some())) + .enumerate() + .for_each(|(i, record)| { + if let PermittivityRecord::ExperimentalData { data } = + record.as_mut().unwrap() + { + let mut data = data.clone(); + data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + // check if all temperatures a.0 in data are finite, if not, make them finite by rounding to four digits + data.iter_mut().for_each(|a| { + if !a.0.is_finite() { + a.0 = (a.0 * 1e4).round() / 1e4; + } + }); + // save data again in record + permittivity_records[i] = + Some(PermittivityRecord::ExperimentalData { data }); + } + }); + } } Ok(Self { From dfe64c9fce436b2bb7242d7fd8dc7b8784809363 Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Tue, 26 Mar 2024 13:19:28 +0100 Subject: [PATCH 10/23] added possibility of constant permittivity (only 1 data point provided) --- src/epcsaft/eos/permittivity.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/epcsaft/eos/permittivity.rs b/src/epcsaft/eos/permittivity.rs index da2c07305..f656e9697 100644 --- a/src/epcsaft/eos/permittivity.rs +++ b/src/epcsaft/eos/permittivity.rs @@ -265,6 +265,13 @@ impl + Copy> Permittivity { /// Assume ordered by temperature /// and temperatures are all finite. pub fn interpolate(interpolation_points: &[(f64, f64)], temperature: D) -> Self { + // if there is only one point, return it (means constant permittivity) + if interpolation_points.len() == 1 { + return Self { + permittivity: D::one() * interpolation_points[0].1, + }; + } + // find index where temperature could be inserted let i = interpolation_points.binary_search_by(|&(ti, _)| { ti.partial_cmp(&temperature.re()) From 73e55ee078fabd2258b526f1e6087305f941c480 Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Tue, 26 Mar 2024 13:22:03 +0100 Subject: [PATCH 11/23] added tests for Helmholtz energy of Born and Ionic term, added parameters of water NaCl to utils, some binary record troubleshooting --- src/epcsaft/eos/born.rs | 39 ++++ src/epcsaft/eos/ionic.rs | 41 ++++ src/epcsaft/eos/mod.rs | 4 +- src/epcsaft/parameters.rs | 465 ++++++++++++++++++++++++++++---------- 4 files changed, 430 insertions(+), 119 deletions(-) diff --git a/src/epcsaft/eos/born.rs b/src/epcsaft/eos/born.rs index a283fc7e9..623fadd13 100644 --- a/src/epcsaft/eos/born.rs +++ b/src/epcsaft/eos/born.rs @@ -1,5 +1,6 @@ use crate::epcsaft::eos::permittivity::Permittivity; use crate::epcsaft::parameters::ElectrolytePcSaftParameters; +use crate::hard_sphere::HardSphereProperties; use feos_core::StateHD; use ndarray::Array1; use num_dual::DualNum; @@ -49,3 +50,41 @@ impl fmt::Display for Born { write!(f, "Born") } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::epcsaft::parameters::utils::{water_nacl_parameters, water_nacl_parameters_perturb}; + use approx::assert_relative_eq; + use ndarray::arr1; + + #[test] + fn helmholtz_energy_perturb() { + let born = Born { + parameters: water_nacl_parameters_perturb(), + }; + + let t = 298.0; + let v = 31.875; + let s = StateHD::new(t, v, arr1(&[0.9, 0.05, 0.05])); + let d = born.parameters.hs_diameter(t); + let a_rust = born.helmholtz_energy(&s, &d); + + assert_relative_eq!(a_rust, -22.51064553710294, epsilon = 1e-10); + } + + #[test] + fn helmholtz_energy() { + let born = Born { + parameters: water_nacl_parameters(), + }; + + let t = 298.0; + let v = 31.875; + let s = StateHD::new(t, v, arr1(&[0.9, 0.05, 0.05])); + let d = born.parameters.hs_diameter(t); + let a_rust = born.helmholtz_energy(&s, &d); + + assert_relative_eq!(a_rust, -22.525624511559244, epsilon = 1e-10); + } +} diff --git a/src/epcsaft/eos/ionic.rs b/src/epcsaft/eos/ionic.rs index 26521298f..cfe2ec693 100644 --- a/src/epcsaft/eos/ionic.rs +++ b/src/epcsaft/eos/ionic.rs @@ -1,5 +1,6 @@ use crate::epcsaft::eos::permittivity::Permittivity; use crate::epcsaft::parameters::ElectrolytePcSaftParameters; +use crate::hard_sphere::HardSphereProperties; use feos_core::StateHD; use ndarray::*; use num_dual::DualNum; @@ -94,3 +95,43 @@ impl fmt::Display for Ionic { write!(f, "Ionic") } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::epcsaft::parameters::utils::{water_nacl_parameters, water_nacl_parameters_perturb}; + use approx::assert_relative_eq; + use ndarray::arr1; + + #[test] + fn helmholtz_energy_perturb() { + let ionic = Ionic { + parameters: water_nacl_parameters_perturb(), + variant: ElectrolytePcSaftVariants::Advanced, + }; + let t = 298.0; + let v = 31.875; + + let s = StateHD::new(t, v, arr1(&[0.9, 0.05, 0.05])); + + let d = ionic.parameters.hs_diameter(t); + let a_rust = ionic.helmholtz_energy(&s, &d); + + assert_relative_eq!(a_rust, -0.07775796084032328, epsilon = 1e-10); + } + + #[test] + fn helmholtz_energy() { + let ionic = Ionic { + parameters: water_nacl_parameters(), + variant: ElectrolytePcSaftVariants::Advanced, + }; + let t = 298.0; + let v = 31.875; + let s = StateHD::new(t, v, arr1(&[0.9, 0.05, 0.05])); + let d = ionic.parameters.hs_diameter(t); + let a_rust = ionic.helmholtz_energy(&s, &d); + + assert_relative_eq!(a_rust, -0.07341337106244776, epsilon = 1e-10); + } +} diff --git a/src/epcsaft/eos/mod.rs b/src/epcsaft/eos/mod.rs index 6494941a0..cc4acbae0 100644 --- a/src/epcsaft/eos/mod.rs +++ b/src/epcsaft/eos/mod.rs @@ -449,7 +449,7 @@ mod tests { #[test] fn association() { - let parameters = Arc::new(water_parameters()); + let parameters = water_parameters(); let assoc = Association::new(¶meters, ¶meters.association, 50, 1e-10); let t = 350.0; let v = 41.248289328513216; @@ -462,7 +462,7 @@ mod tests { #[test] fn cross_association() { - let parameters = Arc::new(water_parameters()); + let parameters = water_parameters(); let assoc = Association::new_cross_association(¶meters, ¶meters.association, 50, 1e-10); let t = 350.0; diff --git a/src/epcsaft/parameters.rs b/src/epcsaft/parameters.rs index 1ac484000..79b19b1f0 100644 --- a/src/epcsaft/parameters.rs +++ b/src/epcsaft/parameters.rs @@ -7,7 +7,6 @@ use num_dual::DualNum; use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::convert::TryFrom; use std::fmt::Write; use crate::epcsaft::eos::permittivity::PermittivityRecord; @@ -282,24 +281,36 @@ impl ElectrolytePcSaftBinaryRecord { } } -impl TryFrom for ElectrolytePcSaftBinaryRecord { - type Error = ParameterError; - - fn try_from(k_ij: f64) -> Result { - Ok(Self { +impl From for ElectrolytePcSaftBinaryRecord { + fn from(k_ij: f64) -> Self { + Self { k_ij: vec![k_ij, 0., 0., 0.], association: None, - }) + } } } -impl TryFrom for f64 { - type Error = ParameterError; +impl From> for ElectrolytePcSaftBinaryRecord { + fn from(k_ij: Vec) -> Self { + Self { + k_ij, + association: None, + } + } +} - fn try_from(_f: ElectrolytePcSaftBinaryRecord) -> Result { - Err(ParameterError::IncompatibleParameters( - "Cannot infer k_ij from single float.".to_string(), - )) +impl From for f64 { + fn from(binary_record: ElectrolytePcSaftBinaryRecord) -> Self { + match binary_record.k_ij.first() { + Some(&k_ij) => k_ij, + None => 0.0, + } + } +} + +impl From for Vec { + fn from(binary_record: ElectrolytePcSaftBinaryRecord) -> Self { + binary_record.k_ij } } @@ -307,23 +318,10 @@ impl std::fmt::Display for ElectrolytePcSaftBinaryRecord { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut tokens = vec![]; if !self.k_ij[0].is_zero() { - tokens.push(format!( - "ElectrolytePcSaftBinaryRecord(k_ij_0={})", - self.k_ij[0] - )); - tokens.push(format!( - "ElectrolytePcSaftBinaryRecord(k_ij_1={})", - self.k_ij[1] - )); - tokens.push(format!( - "ElectrolytePcSaftBinaryRecord(k_ij_2={})", - self.k_ij[2] - )); - tokens.push(format!( - "ElectrolytePcSaftBinaryRecord(k_ij_3={})", - self.k_ij[3] - )); - tokens.push(")".to_string()); + tokens.push(format!("k_ij_0={}", self.k_ij[0])); + tokens.push(format!(", k_ij_1={}", self.k_ij[1])); + tokens.push(format!(", k_ij_2={}", self.k_ij[2])); + tokens.push(format!(", k_ij_3={})", self.k_ij[3])); } if let Some(association) = self.association { if let Some(kappa_ab) = association.kappa_ab { @@ -333,7 +331,7 @@ impl std::fmt::Display for ElectrolytePcSaftBinaryRecord { tokens.push(format!("epsilon_k_ab={}", epsilon_k_ab)); } } - write!(f, "PcSaftBinaryRecord({})", tokens.join(", ")) + write!(f, "ElectrolytePcSaftBinaryRecord({})", tokens.join("")) } } @@ -355,7 +353,7 @@ pub struct ElectrolytePcSaftParameters { pub nquadpole: usize, pub nionic: usize, pub nsolvent: usize, - pub sigma_t_comp: Array1, + pub water_sigma_t_comp: Option, pub dipole_comp: Array1, pub quadpole_comp: Array1, pub ionic_comp: Array1, @@ -371,11 +369,13 @@ pub struct ElectrolytePcSaftParameters { impl ElectrolytePcSaftParameters { pub fn sigma_t>(&self, temperature: D) -> Array1 { let mut sigma_t: Array1 = Array::from_shape_fn(self.sigma.len(), |i| self.sigma[i]); - for i in 0..self.sigma_t_comp.len() { + + if let Some(i) = self.water_sigma_t_comp { sigma_t[i] = (sigma_t[i] + (temperature.re() * -0.01775).exp() * 10.11 - (temperature.re() * -0.01146).exp() * 1.417) - .re() + .re(); } + sigma_t } @@ -414,6 +414,7 @@ impl Parameter for ElectrolytePcSaftParameters { let mut viscosity = Vec::with_capacity(n); let mut diffusion = Vec::with_capacity(n); let mut thermal_conductivity = Vec::with_capacity(n); + let mut water_sigma_t_comp = None; let mut component_index = HashMap::with_capacity(n); @@ -431,6 +432,16 @@ impl Parameter for ElectrolytePcSaftParameters { diffusion.push(r.diffusion); thermal_conductivity.push(r.thermal_conductivity); molarweight[i] = record.molarweight; + // check if component i is water with temperature-dependent sigma + if (m[i] * 1000.0).round() / 1000.0 == 1.205 && epsilon_k[i].round() == 354.0 { + if let Some(record) = r.association_record { + if (record.kappa_ab * 1000.0).round() / 1000.0 == 0.045 + && record.epsilon_k_ab.round() == 2426.0 + { + water_sigma_t_comp = Some(i); + } + } + } } let mu2 = &mu * &mu / (&m * &sigma * &sigma * &sigma * &epsilon_k) @@ -477,25 +488,6 @@ impl Parameter for ElectrolytePcSaftParameters { .collect(); let nsolvent = solvent_comp.len(); - let mut bool_sigma_t = Array1::zeros(n); - for i in 0..n { - let name = pure_records[i] - .identifier - .name - .clone() - .unwrap_or(String::from("unknown")); - if name.contains("sigma_t") { - bool_sigma_t[i] = 1usize - } - } - let sigma_t_comp: Array1 = Array::from_iter( - bool_sigma_t - .iter() - .enumerate() - .filter(|x| x.1 == &1usize) - .map(|x| x.0), - ); - let mut k_ij: Array2> = Array2::from_elem((n, n), vec![0., 0., 0., 0.]); if let Some(binary_records) = binary_records.as_ref() { @@ -653,7 +645,7 @@ impl Parameter for ElectrolytePcSaftParameters { quadpole_comp, ionic_comp, solvent_comp, - sigma_t_comp, + water_sigma_t_comp, viscosity: viscosity_coefficients, diffusion: diffusion_coefficients, thermal_conductivity: thermal_conductivity_coefficients, @@ -736,6 +728,9 @@ impl ElectrolytePcSaftParameters { #[allow(dead_code)] #[cfg(test)] pub mod utils { + use feos_core::parameter::{BinaryRecord, Identifier}; + use ndarray::ArrayBase; + use super::*; use std::sync::Arc; @@ -765,7 +760,7 @@ pub mod utils { Arc::new(ElectrolytePcSaftParameters::new_pure(propane_record).unwrap()) } - pub fn carbon_dioxide_parameters() -> ElectrolytePcSaftParameters { + pub fn carbon_dioxide_parameters() -> Arc { let co2_json = r#" { "identifier": { @@ -786,7 +781,7 @@ pub mod utils { }"#; let co2_record: PureRecord = serde_json::from_str(co2_json).expect("Unable to parse json."); - ElectrolytePcSaftParameters::new_pure(co2_record).unwrap() + Arc::new(ElectrolytePcSaftParameters::new_pure(co2_record).unwrap()) } pub fn butane_parameters() -> Arc { @@ -812,7 +807,7 @@ pub mod utils { Arc::new(ElectrolytePcSaftParameters::new_pure(butane_record).unwrap()) } - pub fn dme_parameters() -> ElectrolytePcSaftParameters { + pub fn dme_parameters() -> Arc { let dme_json = r#" { "identifier": { @@ -833,10 +828,10 @@ pub mod utils { }"#; let dme_record: PureRecord = serde_json::from_str(dme_json).expect("Unable to parse json."); - ElectrolytePcSaftParameters::new_pure(dme_record).unwrap() + Arc::new(ElectrolytePcSaftParameters::new_pure(dme_record).unwrap()) } - pub fn water_parameters_sigma_t() -> ElectrolytePcSaftParameters { + pub fn water_parameters_sigma_t() -> Arc { let water_json = r#" { "identifier": { @@ -858,10 +853,10 @@ pub mod utils { }"#; let water_record: PureRecord = serde_json::from_str(water_json).expect("Unable to parse json."); - ElectrolytePcSaftParameters::new_pure(water_record).unwrap() + Arc::new(ElectrolytePcSaftParameters::new_pure(water_record).unwrap()) } - pub fn water_nacl_parameters() -> ElectrolytePcSaftParameters { + pub fn water_nacl_parameters_perturb() -> Arc { // Water parameters from Held et al. (2014), originally from Fuchs et al. (2006) let pure_json = r#"[ { @@ -873,12 +868,197 @@ pub mod utils { "inchi": "InChI=1/H2O/h1H2", "formula": "H2O" }, - "saft_record": { + "model_record": { "m": 1.2047, "sigma": 2.7927, "epsilon_k": 353.95, "kappa_ab": 0.04509, - "epsilon_k_ab": 2425.7 + "epsilon_k_ab": 2425.7, + "permittivity_record": { + "PerturbationTheory": { + "dipole_scaling": + 5.199 + , + "polarizability_scaling": + 0.0 + , + "correlation_integral_parameter": + 0.1276 + + } + } + }, + "molarweight": 18.0152 + }, + { + "identifier": { + "cas": "110-54-3", + "name": "na+", + "formula": "na+" + }, + "model_record": { + "m": 1, + "sigma": 2.8232, + "epsilon_k": 230.0, + "z": 1, + "permittivity_record": { + "PerturbationTheory": { + "dipole_scaling": + 0.0, + "polarizability_scaling": + 0.0, + "correlation_integral_parameter": + 0.0658 + } + } + }, + "molarweight": 22.98977 + }, + { + "identifier": { + "cas": "7782-50-5", + "name": "cl-", + "formula": "cl-" + }, + "model_record": { + "m": 1, + "sigma": 2.7560, + "epsilon_k": 170, + "z": -1, + "permittivity_record": { + "PerturbationTheory": { + "dipole_scaling": + 7.3238, + "polarizability_scaling": + 0.0, + "correlation_integral_parameter": + 0.2620 + } + } + }, + "molarweight": 35.45 + } + ]"#; + let binary_json = r#"[ + { + "id1": { + "cas": "7732-18-5", + "name": "water_np_sigma_t", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "110-54-3", + "name": "sodium ion", + "formula": "na+" + }, + "model_record": { + "k_ij": [ + 0.0045, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water_np_sigma_t", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "7782-50-5", + "name": "chloride ion", + "formula": "cl-" + }, + "model_record": { + "k_ij": [ + -0.25, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "110-54-3", + "name": "sodium ion", + "formula": "na+" + }, + "id2": { + "cas": "7782-50-5", + "name": "chloride ion", + "formula": "cl-" + }, + "model_record": { + "k_ij": [ + 0.317, + 0.0, + 0.0, + 0.0 + ] + } + } + ]"#; + let pure_records: Vec> = + serde_json::from_str(pure_json).expect("Unable to parse json."); + let binary_records: Vec> = + serde_json::from_str(binary_json).expect("Unable to parse json."); + let binary_matrix = ElectrolytePcSaftParameters::binary_matrix_from_records( + &pure_records, + &binary_records, + feos_core::parameter::IdentifierOption::Name, + ) + .unwrap(); + Arc::new( + ElectrolytePcSaftParameters::from_records(pure_records, Some(binary_matrix)).unwrap(), + ) + } + + pub fn water_nacl_parameters() -> Arc { + // Water parameters from Held et al. (2014), originally from Fuchs et al. (2006) + let pure_json = r#"[ + { + "identifier": { + "cas": "7732-18-5", + "name": "water_np_sigma_t", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "model_record": { + "m": 1.2047, + "sigma": 2.7927, + "epsilon_k": 353.95, + "kappa_ab": 0.04509, + "epsilon_k_ab": 2425.7, + "permittivity_record": { + "ExperimentalData": { + "data": + [ + [ + 280.15, + 84.89 + ], + [ + 298.15, + 78.39 + ], + [ + 360.15, + 58.73 + ] + ] + } + } }, "molarweight": 18.0152 }, @@ -888,13 +1068,24 @@ pub mod utils { "name": "na+", "formula": "na+" }, - "saft_record": { + "model_record": { "m": 1, "sigma": 2.8232, "epsilon_k": 230.0, - "z": 1 + "z": 1, + "permittivity_record": { + "ExperimentalData": { + "data": + [ + [ + 298.15, + 8.0 + ] + ] + } + } }, - "molarweight": 22.98976 + "molarweight": 22.98977 }, { "identifier": { @@ -902,70 +1093,110 @@ pub mod utils { "name": "cl-", "formula": "cl-" }, - "saft_record": { + "model_record": { "m": 1, "sigma": 2.7560, "epsilon_k": 170, - "z": -1 + "z": -1, + "permittivity_record": { + "ExperimentalData": { + "data": + [ + [ + 298.15, + 8.0 + ] + ] + } + } }, "molarweight": 35.45 } ]"#; let binary_json = r#"[ - { - "id1": { - "cas": "7732-18-5", - "name": "water_np", - "iupac_name": "oxidane", - "smiles": "O", - "inchi": "InChI=1/H2O/h1H2", - "formula": "H2O" - }, - "id2": { - "cas": "110-54-3", - "name": "na+", - "formula": "na+" - }, - "k_ij": [0.0045] + { + "id1": { + "cas": "7732-18-5", + "name": "water_np_sigma_t", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "110-54-3", + "name": "sodium ion", + "formula": "na+" + }, + "model_record": { + "k_ij": [ + 0.0045, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water_np_sigma_t", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" }, - { - "id1": { - "cas": "7732-18-5", - "name": "water_np", - "iupac_name": "oxidane", - "smiles": "O", - "inchi": "InChI=1/H2O/h1H2", - "formula": "H2O" - }, - "id2": { - "cas": "7782-50-5", - "name": "cl-", - "formula": "cl-" - }, - "k_ij": [-0.25] + "id2": { + "cas": "7782-50-5", + "name": "chloride ion", + "formula": "cl-" }, - { - "id1": { - "cas": "110-54-3", - "name": "na+", - "formula": "na+" - }, - "id2": { - "cas": "7782-50-5", - "name": "cl-", - "formula": "cl-" - }, - "k_ij": [0.317] + "model_record": { + "k_ij": [ + -0.25, + 0.0, + 0.0, + 0.0 + ] } - ]"#; + }, + { + "id1": { + "cas": "110-54-3", + "name": "sodium ion", + "formula": "na+" + }, + "id2": { + "cas": "7782-50-5", + "name": "chloride ion", + "formula": "cl-" + }, + "model_record": { + "k_ij": [ + 0.317, + 0.0, + 0.0, + 0.0 + ] + } + } + ]"#; let pure_records: Vec> = serde_json::from_str(pure_json).expect("Unable to parse json."); - let binary_records: ElectrolytePcSaftBinaryRecord = + let binary_records: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); - ElectrolytePcSaftParameters::new_binary(pure_records, Some(binary_records)).unwrap() + let binary_matrix = ElectrolytePcSaftParameters::binary_matrix_from_records( + &pure_records, + &binary_records, + feos_core::parameter::IdentifierOption::Name, + ) + .unwrap(); + Arc::new( + ElectrolytePcSaftParameters::from_records(pure_records, Some(binary_matrix)).unwrap(), + ) } - pub fn water_parameters() -> ElectrolytePcSaftParameters { + pub fn water_parameters() -> Arc { let water_json = r#" { "identifier": { @@ -989,10 +1220,10 @@ pub mod utils { }"#; let water_record: PureRecord = serde_json::from_str(water_json).expect("Unable to parse json."); - ElectrolytePcSaftParameters::new_pure(water_record).unwrap() + Arc::new(ElectrolytePcSaftParameters::new_pure(water_record).unwrap()) } - pub fn dme_co2_parameters() -> ElectrolytePcSaftParameters { + pub fn dme_co2_parameters() -> Arc { let binary_json = r#"[ { "identifier": { @@ -1031,7 +1262,7 @@ pub mod utils { ]"#; let binary_record: Vec> = serde_json::from_str(binary_json).expect("Unable to parse json."); - ElectrolytePcSaftParameters::new_binary(binary_record, None).unwrap() + Arc::new(ElectrolytePcSaftParameters::new_binary(binary_record, None).unwrap()) } pub fn propane_butane_parameters() -> Arc { From 24825c9db924325d366a766ee4f82077fd9b1e46 Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Thu, 18 Apr 2024 11:58:45 +0200 Subject: [PATCH 12/23] moved constants to top of file and named as constants --- src/epcsaft/eos/ionic.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/epcsaft/eos/ionic.rs b/src/epcsaft/eos/ionic.rs index cfe2ec693..89fd8bdff 100644 --- a/src/epcsaft/eos/ionic.rs +++ b/src/epcsaft/eos/ionic.rs @@ -10,30 +10,28 @@ use std::sync::Arc; use super::ElectrolytePcSaftVariants; +const EPSILON_0: f64 = 8.85416e-12; +const QE: f64 = 1.602176634e-19f64; +const BOLTZMANN: f64 = 1.380649e-23; + impl ElectrolytePcSaftParameters { pub fn bjerrum_length + Copy>( &self, state: &StateHD, + diameter: &Array1, epcsaft_variant: ElectrolytePcSaftVariants, ) -> D { - // permittivity in vacuum - let epsilon_0 = 8.85416e-12; - // relative permittivity of water (usually function of T,p,x) - let epsilon_r = Permittivity::new(state, self, &epcsaft_variant) + let epsilon_r = Permittivity::new(state, diameter, self, &epcsaft_variant) .unwrap() .permittivity; - let epsreps0 = epsilon_r * epsilon_0; - - // unit charge - let qe2 = 1.602176634e-19f64.powi(2); + let epsreps0 = epsilon_r * EPSILON_0; - // Boltzmann constant - let boltzmann = 1.380649e-23; + let qe2 = QE.powi(2); // Bjerrum length - (state.temperature * 4.0 * std::f64::consts::PI * epsreps0 * boltzmann).recip() + (state.temperature * 4.0 * std::f64::consts::PI * epsreps0 * BOLTZMANN).recip() * qe2 * 1.0e10 } @@ -60,7 +58,7 @@ impl Ionic { } // Calculate Bjerrum length - let lambda_b = p.bjerrum_length(state, self.variant); + let lambda_b = p.bjerrum_length(state, diameter, self.variant); // Calculate inverse Debye length let mut sum_dens_z = D::zero(); From 9534c6027a09a05ee3d026be45cf8a1f55926809 Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Thu, 18 Apr 2024 12:04:29 +0200 Subject: [PATCH 13/23] reverted some changes in ionic.rs due to Pottel that weren't supposed to be there in last commit --- src/epcsaft/eos/ionic.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/epcsaft/eos/ionic.rs b/src/epcsaft/eos/ionic.rs index 89fd8bdff..7a8c806f4 100644 --- a/src/epcsaft/eos/ionic.rs +++ b/src/epcsaft/eos/ionic.rs @@ -18,11 +18,10 @@ impl ElectrolytePcSaftParameters { pub fn bjerrum_length + Copy>( &self, state: &StateHD, - diameter: &Array1, epcsaft_variant: ElectrolytePcSaftVariants, ) -> D { // relative permittivity of water (usually function of T,p,x) - let epsilon_r = Permittivity::new(state, diameter, self, &epcsaft_variant) + let epsilon_r = Permittivity::new(state, self, &epcsaft_variant) .unwrap() .permittivity; @@ -58,7 +57,7 @@ impl Ionic { } // Calculate Bjerrum length - let lambda_b = p.bjerrum_length(state, diameter, self.variant); + let lambda_b = p.bjerrum_length(state, self.variant); // Calculate inverse Debye length let mut sum_dens_z = D::zero(); From 8f13c7f28073b3a6dccf569a8d344b287d7f5db7 Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Thu, 18 Apr 2024 12:15:24 +0200 Subject: [PATCH 14/23] removed transport properties from ePC-SAFT --- src/eos.rs | 5 +- src/epcsaft/eos/mod.rs | 227 +------------------------------------- src/epcsaft/parameters.rs | 132 ---------------------- src/epcsaft/python.rs | 24 ---- 4 files changed, 5 insertions(+), 383 deletions(-) diff --git a/src/eos.rs b/src/eos.rs index 99be30e96..7ad8c063b 100644 --- a/src/eos.rs +++ b/src/eos.rs @@ -1,9 +1,9 @@ +#[cfg(feature = "epcsaft")] +use crate::epcsaft::ElectrolytePcSaft; #[cfg(feature = "gc_pcsaft")] use crate::gc_pcsaft::GcPcSaft; #[cfg(feature = "pcsaft")] use crate::pcsaft::PcSaft; -#[cfg(feature = "epcsaft")] -use crate::epcsaft::ElectrolytePcSaft; #[cfg(feature = "pets")] use crate::pets::Pets; #[cfg(feature = "saftvrmie")] @@ -32,7 +32,6 @@ pub enum ResidualModel { #[implement(entropy_scaling)] PcSaft(PcSaft), #[cfg(feature = "epcsaft")] - #[implement(entropy_scaling)] ElectrolytePcSaft(ElectrolytePcSaft), #[cfg(feature = "gc_pcsaft")] GcPcSaft(GcPcSaft), diff --git a/src/epcsaft/eos/mod.rs b/src/epcsaft/eos/mod.rs index cc4acbae0..ce547b479 100644 --- a/src/epcsaft/eos/mod.rs +++ b/src/epcsaft/eos/mod.rs @@ -3,13 +3,12 @@ use crate::epcsaft::parameters::ElectrolytePcSaftParameters; use crate::hard_sphere::{HardSphere, HardSphereProperties}; use feos_core::parameter::Parameter; use feos_core::{si::*, StateHD}; -use feos_core::{Components, EntropyScaling, EosError, EosResult, Residual, State}; +use feos_core::{Components, Residual}; use ndarray::Array1; use num_dual::DualNum; -use std::f64::consts::{FRAC_PI_6, PI}; +use std::f64::consts::FRAC_PI_6; use std::fmt; use std::sync::Arc; -use typenum::P2; pub(crate) mod born; pub(crate) mod dispersion; @@ -197,182 +196,6 @@ impl fmt::Display for ElectrolytePcSaft { } } -fn omega11(t: f64) -> f64 { - 1.06036 * t.powf(-0.15610) - + 0.19300 * (-0.47635 * t).exp() - + 1.03587 * (-1.52996 * t).exp() - + 1.76474 * (-3.89411 * t).exp() -} - -fn omega22(t: f64) -> f64 { - 1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp() - - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin() -} - -#[inline] -fn chapman_enskog_thermal_conductivity( - temperature: Temperature, - molarweight: MolarWeight, - m: f64, - sigma: f64, - epsilon_k: f64, -) -> ThermalConductivity { - let t = temperature.to_reduced(); - 0.083235 * (t * m / molarweight.convert_into(GRAM / MOL)).sqrt() - / sigma.powi(2) - / omega22(t / epsilon_k) - * WATT - / METER - / KELVIN -} - -impl EntropyScaling for ElectrolytePcSaft { - fn viscosity_reference( - &self, - temperature: Temperature, - _: Volume, - moles: &Moles>, - ) -> EosResult { - let p = &self.parameters; - let mw = &p.molarweight; - let x = (moles / moles.sum()).into_value(); - let ce: Array1<_> = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); - 5.0 / 16.0 * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI).sqrt() - / omega22(tr) - / (p.sigma[i] * ANGSTROM).powi::() - }) - .collect(); - let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND; - for i in 0..self.components() { - let denom: f64 = (0..self.components()) - .map(|j| { - x[j] * (1.0 - + (ce[i] / ce[j]).into_value().sqrt() * (mw[j] / mw[i]).powf(1.0 / 4.0)) - .powi(2) - / (8.0 * (1.0 + mw[i] / mw[j])).sqrt() - }) - .sum(); - ce_mix += ce[i] * x[i] / denom - } - Ok(ce_mix) - } - - fn viscosity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - let coefficients = self - .parameters - .viscosity - .as_ref() - .expect("Missing viscosity coefficients."); - let m = (x * &self.parameters.m).sum(); - let s = s_res / m; - let pref = (x * &self.parameters.m) / m; - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * &pref).sum(); - let c: f64 = (&coefficients.row(2) * &pref).sum(); - let d: f64 = (&coefficients.row(3) * &pref).sum(); - Ok(a + b * s + c * s.powi(2) + d * s.powi(3)) - } - - fn diffusion_reference( - &self, - temperature: Temperature, - volume: Volume, - moles: &Moles>, - ) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let p = &self.parameters; - let density = moles.sum() / volume; - let res: Array1<_> = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); - 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi::() / omega11(tr) / (density * NAV) - * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL) / p.m[i]).sqrt() - }) - .collect(); - Ok(res[0]) - } - - fn diffusion_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let coefficients = self - .parameters - .diffusion - .as_ref() - .expect("Missing diffusion coefficients."); - let m = (x * &self.parameters.m).sum(); - let s = s_res / m; - let pref = (x * &self.parameters.m).mapv(|v| v / m); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * &pref).sum(); - let c: f64 = (&coefficients.row(2) * &pref).sum(); - let d: f64 = (&coefficients.row(3) * &pref).sum(); - let e: f64 = (&coefficients.row(4) * &pref).sum(); - Ok(a + b * s - c * (1.0 - s.exp()) * s.powi(2) - d * s.powi(4) - e * s.powi(8)) - } - - // Equation 4 of DOI: 10.1021/acs.iecr.9b04289 - fn thermal_conductivity_reference( - &self, - temperature: Temperature, - volume: Volume, - moles: &Moles>, - ) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let p = &self.parameters; - let mws = self.molar_weight(); - let state = State::new_nvt(&Arc::new(Self::new(p.clone())), temperature, volume, moles)?; - let res: Array1<_> = (0..self.components()) - .map(|i| { - let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value(); - let s_res_reduced = state.residual_molar_entropy().to_reduced() / p.m[i]; - let ref_ce = chapman_enskog_thermal_conductivity( - temperature, - mws.get(i), - p.m[i], - p.sigma[i], - p.epsilon_k[i], - ); - let alpha_visc = (-s_res_reduced / -0.5).exp(); - let ref_ts = (-0.0167141 * tr / p.m[i] + 0.0470581 * (tr / p.m[i]).powi(2)) - * (p.m[i] * p.m[i] * p.sigma[i].powi(3) * p.epsilon_k[i]) - * 1e-5 - * WATT - / METER - / KELVIN; - ref_ce + ref_ts * alpha_visc - }) - .collect(); - Ok(res[0]) - } - - fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1) -> EosResult { - if self.components() != 1 { - return Err(EosError::IncompatibleComponents(self.components(), 1)); - } - let coefficients = self - .parameters - .thermal_conductivity - .as_ref() - .expect("Missing thermal conductivity coefficients"); - let m = (x * &self.parameters.m).sum(); - let s = s_res / m; - let pref = (x * &self.parameters.m).mapv(|v| v / m); - let a: f64 = (&coefficients.row(0) * x).sum(); - let b: f64 = (&coefficients.row(1) * &pref).sum(); - let c: f64 = (&coefficients.row(2) * &pref).sum(); - let d: f64 = (&coefficients.row(3) * &pref).sum(); - Ok(a + b * s + c * (1.0 - s.exp()) + d * s.powi(2)) - } -} - #[cfg(test)] mod tests { use super::*; @@ -380,7 +203,7 @@ mod tests { butane_parameters, propane_butane_parameters, propane_parameters, water_parameters, }; use approx::assert_relative_eq; - use feos_core::si::{BAR, KELVIN, METER, MILLI, PASCAL, RGAS, SECOND}; + use feos_core::si::{BAR, KELVIN, METER, PASCAL, RGAS}; use feos_core::*; use ndarray::arr1; use typenum::P3; @@ -543,48 +366,4 @@ mod tests { epsilon = 1e-12 ) } - - #[test] - fn viscosity() -> EosResult<()> { - let e = Arc::new(ElectrolytePcSaft::new(propane_parameters())); - let t = 300.0 * KELVIN; - let p = BAR; - let n = arr1(&[1.0]) * MOL; - let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap(); - assert_relative_eq!( - s.viscosity()?, - 0.00797 * MILLI * PASCAL * SECOND, - epsilon = 1e-5 - ); - assert_relative_eq!( - s.ln_viscosity_reduced()?, - (s.viscosity()? / e.viscosity_reference(s.temperature, s.volume, &s.moles)?) - .into_value() - .ln(), - epsilon = 1e-15 - ); - Ok(()) - } - - #[test] - fn diffusion() -> EosResult<()> { - let e = Arc::new(ElectrolytePcSaft::new(propane_parameters())); - let t = 300.0 * KELVIN; - let p = BAR; - let n = arr1(&[1.0]) * MOL; - let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap(); - assert_relative_eq!( - s.diffusion()?, - 0.01505 * (CENTI * METER).powi::() / SECOND, - epsilon = 1e-5 - ); - assert_relative_eq!( - s.ln_diffusion_reduced()?, - (s.diffusion()? / e.diffusion_reference(s.temperature, s.volume, &s.moles)?) - .into_value() - .ln(), - epsilon = 1e-15 - ); - Ok(()) - } } diff --git a/src/epcsaft/parameters.rs b/src/epcsaft/parameters.rs index 79b19b1f0..67061b6f2 100644 --- a/src/epcsaft/parameters.rs +++ b/src/epcsaft/parameters.rs @@ -30,15 +30,6 @@ pub struct ElectrolytePcSaftRecord { #[serde(flatten)] #[serde(skip_serializing_if = "Option::is_none")] pub association_record: Option, - /// Entropy scaling coefficients for the viscosity - #[serde(skip_serializing_if = "Option::is_none")] - pub viscosity: Option<[f64; 4]>, - /// Entropy scaling coefficients for the diffusion coefficient - #[serde(skip_serializing_if = "Option::is_none")] - pub diffusion: Option<[f64; 5]>, - /// Entropy scaling coefficients for the thermal conductivity - #[serde(skip_serializing_if = "Option::is_none")] - pub thermal_conductivity: Option<[f64; 4]>, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub z: Option, @@ -94,60 +85,6 @@ impl FromSegments for ElectrolytePcSaftRecord { AssociationRecord::new(kappa_ab, epsilon_k_ab, na, nb, nc) }); - // entropy scaling - 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; - } - // if let Some(p) = diffusion.as_mut() { - // let [a, b, c, d, e] = s.diffusion.unwrap(); - // p[0] += s3 * a; - // p[1] += s3 * b / sigma3.powf(0.45); - // p[2] += *n * c; - // p[3] += *n * d; - // } - }); - // correction due to difference in Chapman-Enskog reference between GC and regular formulation. - viscosity = viscosity.map(|v| [v[0] - 0.5 * m.ln(), v[1], v[2], v[3]]); - Ok(Self { m, sigma: (sigma3 / m).cbrt(), @@ -155,9 +92,6 @@ impl FromSegments for ElectrolytePcSaftRecord { mu, q, association_record, - viscosity, - diffusion, - thermal_conductivity, z: Some(z), permittivity_record: None, }) @@ -190,15 +124,6 @@ impl std::fmt::Display for ElectrolytePcSaftRecord { if let Some(n) = &self.association_record { write!(f, ", association_record={}", n)?; } - if let Some(n) = &self.viscosity { - write!(f, ", viscosity={:?}", n)?; - } - if let Some(n) = &self.diffusion { - write!(f, ", diffusion={:?}", n)?; - } - if let Some(n) = &self.thermal_conductivity { - write!(f, ", thermal_conductivity={:?}", n)?; - } if let Some(n) = &self.z { write!(f, ", z={}", n)?; } @@ -221,9 +146,6 @@ impl ElectrolytePcSaftRecord { na: Option, nb: Option, nc: Option, - viscosity: Option<[f64; 4]>, - diffusion: Option<[f64; 5]>, - thermal_conductivity: Option<[f64; 4]>, z: Option, permittivity_record: Option, ) -> ElectrolytePcSaftRecord { @@ -250,9 +172,6 @@ impl ElectrolytePcSaftRecord { mu, q, association_record, - viscosity, - diffusion, - thermal_conductivity, z, permittivity_record, } @@ -358,10 +277,7 @@ pub struct ElectrolytePcSaftParameters { pub quadpole_comp: Array1, pub ionic_comp: Array1, pub solvent_comp: Array1, - pub viscosity: Option>, - pub diffusion: Option>, pub permittivity: Array1>, - pub thermal_conductivity: Option>, pub pure_records: Vec>, pub binary_records: Option>, } @@ -411,9 +327,6 @@ impl Parameter for ElectrolytePcSaftParameters { let mut q = Array::zeros(n); let mut z = Array::zeros(n); let mut association_records = Vec::with_capacity(n); - let mut viscosity = Vec::with_capacity(n); - let mut diffusion = Vec::with_capacity(n); - let mut thermal_conductivity = Vec::with_capacity(n); let mut water_sigma_t_comp = None; let mut component_index = HashMap::with_capacity(n); @@ -428,9 +341,6 @@ impl Parameter for ElectrolytePcSaftParameters { q[i] = r.q.unwrap_or(0.0); z[i] = r.z.unwrap_or(0.0); association_records.push(r.association_record.into_iter().collect()); - viscosity.push(r.viscosity); - diffusion.push(r.diffusion); - thermal_conductivity.push(r.thermal_conductivity); molarweight[i] = record.molarweight; // check if component i is water with temperature-dependent sigma if (m[i] * 1000.0).round() / 1000.0 == 1.205 && epsilon_k[i].round() == 354.0 { @@ -524,37 +434,6 @@ impl Parameter for ElectrolytePcSaftParameters { } } - let viscosity_coefficients = if viscosity.iter().any(|v| v.is_none()) { - None - } else { - let mut v = Array2::zeros((4, viscosity.len())); - for (i, vi) in viscosity.iter().enumerate() { - v.column_mut(i).assign(&Array1::from(vi.unwrap().to_vec())); - } - Some(v) - }; - - let diffusion_coefficients = if diffusion.iter().any(|v| v.is_none()) { - None - } else { - let mut v = Array2::zeros((5, diffusion.len())); - for (i, vi) in diffusion.iter().enumerate() { - v.column_mut(i).assign(&Array1::from(vi.unwrap().to_vec())); - } - Some(v) - }; - - let thermal_conductivity_coefficients = if thermal_conductivity.iter().any(|v| v.is_none()) - { - None - } else { - let mut v = Array2::zeros((4, thermal_conductivity.len())); - for (i, vi) in thermal_conductivity.iter().enumerate() { - v.column_mut(i).assign(&Array1::from(vi.unwrap().to_vec())); - } - Some(v) - }; - // Permittivity records let mut permittivity_records: Array1> = pure_records .iter() @@ -646,9 +525,6 @@ impl Parameter for ElectrolytePcSaftParameters { ionic_comp, solvent_comp, water_sigma_t_comp, - viscosity: viscosity_coefficients, - diffusion: diffusion_coefficients, - thermal_conductivity: thermal_conductivity_coefficients, permittivity: permittivity_records, pure_records, binary_records, @@ -749,9 +625,6 @@ pub mod utils { "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 }"#; @@ -1280,9 +1153,6 @@ pub mod utils { "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 }, @@ -1299,8 +1169,6 @@ pub mod utils { "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 } diff --git a/src/epcsaft/python.rs b/src/epcsaft/python.rs index 03841a76c..80e1ac68e 100644 --- a/src/epcsaft/python.rs +++ b/src/epcsaft/python.rs @@ -41,12 +41,6 @@ use std::sync::Arc; /// Number of association sites of type C. /// z : float, optional /// Charge of the electrolyte. -/// viscosity : List[float], optional -/// Entropy-scaling parameters for viscosity. Defaults to `None`. -/// diffusion : List[float], optional -/// Entropy-scaling parameters for diffusion. Defaults to `None`. -/// thermal_conductivity : List[float], optional -/// Entropy-scaling parameters for thermal_conductivity. Defaults to `None`. /// permittivity_record : PyPermittivityRecord, optional /// Permittivity record. Defaults to `None`. @@ -92,9 +86,6 @@ impl PyElectrolytePcSaftRecord { na, nb, nc, - viscosity, - diffusion, - thermal_conductivity, z, perm, )) @@ -145,21 +136,6 @@ impl PyElectrolytePcSaftRecord { self.0.association_record.map(|a| a.nc) } - #[getter] - fn get_viscosity(&self) -> Option<[f64; 4]> { - self.0.viscosity - } - - #[getter] - fn get_diffusion(&self) -> Option<[f64; 5]> { - self.0.diffusion - } - - #[getter] - fn get_thermal_conductivity(&self) -> Option<[f64; 4]> { - self.0.thermal_conductivity - } - fn __repr__(&self) -> PyResult { Ok(self.0.to_string()) } From 99b8664dfc5489b892ee1473b063bad6aa53eee9 Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Thu, 18 Apr 2024 13:01:15 +0200 Subject: [PATCH 15/23] added Python documentation --- docs/api/eos.md | 1 + docs/api/epcsaft.md | 35 +++++++++++++++++++++++++++++++++++ docs/api/index.md | 1 + 3 files changed, 37 insertions(+) create mode 100644 docs/api/epcsaft.md diff --git a/docs/api/eos.md b/docs/api/eos.md index 5ffef5349..d4ae8a825 100644 --- a/docs/api/eos.md +++ b/docs/api/eos.md @@ -15,6 +15,7 @@ If you want to adjust parameters of a model to experimental data you can use cla EquationOfState EquationOfState.pcsaft + EquationOfState.epcsaft EquationOfState.gc_pcsaft EquationOfState.peng_robinson EquationOfState.pets diff --git a/docs/api/epcsaft.md b/docs/api/epcsaft.md new file mode 100644 index 000000000..348a566f6 --- /dev/null +++ b/docs/api/epcsaft.md @@ -0,0 +1,35 @@ +# `feos.epcsaft` + +Utilities to build `ElectrolytePcSaftParameters`. + +## Example + +```python +from feos.epcsaft import ElectrolytePcSaftParameters + +path = 'parameters/epcsaft/held2014_w_permittivity_added.json' +parameters = ElectrolytePcSaftParameters.from_json(['water', 'sodium ion', 'chloride ion'], path) +``` + +## Data types + +```{eval-rst} +.. currentmodule:: feos.epcsaft + +.. autosummary:: + :toctree: generated/ + + Identifier + IdentifierOption + ChemicalRecord + SmartsRecord + ElectrolytePcSaftVariants + ElectrolytePcSaftRecord + ElectrolytePcSaftBinaryRecord + PureRecord + SegmentRecord + BinaryRecord + BinarySegmentRecord + ElectrolytePcSaftParameters + PermittivityRecord +``` \ No newline at end of file diff --git a/docs/api/index.md b/docs/api/index.md index f540da68e..d02c12415 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -20,6 +20,7 @@ These modules contain the objects to e.g. read parameters from files or build pa :maxdepth: 1 pcsaft + epcsaft gc_pcsaft peng_robinson pets From def0b2d15406133217528df8e6f0ee04a465ba92 Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Thu, 18 Apr 2024 13:01:53 +0200 Subject: [PATCH 16/23] removed some last remainder of transport properties --- src/epcsaft/python.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/epcsaft/python.rs b/src/epcsaft/python.rs index 80e1ac68e..9a9a0a415 100644 --- a/src/epcsaft/python.rs +++ b/src/epcsaft/python.rs @@ -52,7 +52,7 @@ pub struct PyElectrolytePcSaftRecord(ElectrolytePcSaftRecord); impl PyElectrolytePcSaftRecord { #[new] #[pyo3( - text_signature = "(m, sigma, epsilon_k, mu=None, q=None, kappa_ab=None, epsilon_k_ab=None, na=None, nb=None, nc=None, viscosity=None, diffusion=None, thermal_conductivity=None, permittivity_record=None)" + text_signature = "(m, sigma, epsilon_k, mu=None, q=None, kappa_ab=None, epsilon_k_ab=None, na=None, nb=None, nc=None, permittivity_record=None)" )] fn new( m: f64, @@ -66,9 +66,6 @@ impl PyElectrolytePcSaftRecord { nb: Option, nc: Option, z: Option, - viscosity: Option<[f64; 4]>, - diffusion: Option<[f64; 5]>, - thermal_conductivity: Option<[f64; 4]>, permittivity_record: Option, ) -> Self { let perm = match permittivity_record { From babf87de3f105d0b8656423e6a7691011e0b3e29 Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Thu, 18 Apr 2024 13:57:37 +0200 Subject: [PATCH 17/23] changed permittivity interpolation to preserve derivative --- src/epcsaft/eos/permittivity.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/epcsaft/eos/permittivity.rs b/src/epcsaft/eos/permittivity.rs index f656e9697..48041a437 100644 --- a/src/epcsaft/eos/permittivity.rs +++ b/src/epcsaft/eos/permittivity.rs @@ -277,15 +277,9 @@ impl + Copy> Permittivity { ti.partial_cmp(&temperature.re()) .expect("Unexpected value for temperature in interpolation points.") }); - // if the result is OK we don't need to interpolate ... - if let Ok(i) = i { - return Self { - permittivity: D::one() * interpolation_points[i].1, - }; - } - // if not, we can unwrap safely: - let i = i.unwrap_err(); + // unwrap + let i = i.unwrap_or_else(|i| i); let n = interpolation_points.len(); // check cases: From 6518a22e8f115c0f0b4d5b84864ea5d2949d2cce Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Thu, 18 Apr 2024 15:10:51 +0200 Subject: [PATCH 18/23] added epcsaft parameters --- docs/api/epcsaft.md | 5 +- parameters/epcsaft/held2014_binary.json | 1264 +++++++++++++++++ .../held2014_w_permittivity_added.json | 440 ++++++ 3 files changed, 1707 insertions(+), 2 deletions(-) create mode 100644 parameters/epcsaft/held2014_binary.json create mode 100644 parameters/epcsaft/held2014_w_permittivity_added.json diff --git a/docs/api/epcsaft.md b/docs/api/epcsaft.md index 348a566f6..51900fafb 100644 --- a/docs/api/epcsaft.md +++ b/docs/api/epcsaft.md @@ -7,8 +7,9 @@ Utilities to build `ElectrolytePcSaftParameters`. ```python from feos.epcsaft import ElectrolytePcSaftParameters -path = 'parameters/epcsaft/held2014_w_permittivity_added.json' -parameters = ElectrolytePcSaftParameters.from_json(['water', 'sodium ion', 'chloride ion'], path) +pure_path = 'parameters/epcsaft/held2014_w_permittivity_added.json' +binary_path = 'parameters/epcsaft/held2014_binary.json' +parameters = ElectrolytePcSaftParameters.from_json(['water', 'sodium ion', 'chloride ion'], pure_path) ``` ## Data types diff --git a/parameters/epcsaft/held2014_binary.json b/parameters/epcsaft/held2014_binary.json new file mode 100644 index 000000000..796b4e152 --- /dev/null +++ b/parameters/epcsaft/held2014_binary.json @@ -0,0 +1,1264 @@ +[ + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "7440-23-5", + "name": "sodium ion", + "formula": "Na", + "smiles": "[Na+]" + }, + "model_record": { + "k_ij": [ + 0.0045, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "16887-00-6", + "name": "chloride ion", + "formula": "Cl", + "smiles": "[Cl-]" + }, + "model_record": { + "k_ij": [ + -0.25, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "3812-32-6", + "name": "carbonate", + "formula": "CO3", + "smiles": "O=C([O-])[O-]" + }, + "model_record": { + "k_ij": [ + -0.25, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "13968-08-6", + "name": "hydronium", + "formula": "H", + "smiles": "[H+]" + }, + "model_record": { + "k_ij": [ + 0.25, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "17341-24-1", + "name": "lithium ion", + "formula": "Li", + "smiles": "[Li+]" + }, + "model_record": { + "k_ij": [ + -0.25, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "24203-36-9", + "name": "potassium ion", + "formula": "K", + "smiles": "[K+]" + }, + "model_record": { + "k_ij": [ + 0.1997, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "18459-37-5", + "name": "cesium ion", + "formula": "Cs", + "smiles": "[Cs+]" + }, + "model_record": { + "k_ij": [ + 0.081, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "14798-03-9", + "name": "ammonium ion", + "formula": "NH4", + "smiles": "[NH4+]" + }, + "model_record": { + "k_ij": [ + 0.064, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "16887-00-6", + "name": "chloride ion", + "formula": "Cl", + "smiles": "[Cl-]" + }, + "model_record": { + "k_ij": [ + -0.25, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "24959-67-9", + "name": "bromide ion", + "formula": "Br", + "smiles": "[Br-]" + }, + "model_record": { + "k_ij": [ + -0.25, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "20461-54-5", + "name": "iodide ion", + "formula": "I", + "smiles": "[I-]" + }, + "model_record": { + "k_ij": [ + -0.25, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "14797-55-8", + "name": "nitrate ion", + "formula": "NO3", + "smiles": "[N+](=O)([O-])[O-]" + }, + "model_record": { + "k_ij": [ + 0.098, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "14280-30-9", + "name": "hydroxide", + "formula": "OH", + "smiles": "[OH-]" + }, + "model_record": { + "k_ij": [ + -0.25, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "14797-73-0", + "name": "perchlorate ion", + "formula": "ClO4", + "smiles": "[O-]Cl(=O)(=O)=O" + }, + "model_record": { + "k_ij": [ + -0.25, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "id2": { + "cas": "14066-20-7", + "name": "dihydrogenphosphate ion", + "formula": "H2PO4", + "smiles": "OP(=O)(O)[O-]" + }, + "model_record": { + "k_ij": [ + 0.25, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "16887-00-6", + "name": "chloride ion", + "formula": "Cl", + "smiles": "[Cl-]" + }, + "id2": { + "cas": "13968-08-6", + "name": "hydronium", + "formula": "H", + "smiles": "[H+]" + }, + "model_record": { + "k_ij": [ + 0.654, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "24959-67-9", + "name": "bromide ion", + "formula": "Br", + "smiles": "[Br-]" + }, + "id2": { + "cas": "13968-08-6", + "name": "hydronium", + "formula": "H", + "smiles": "[H+]" + }, + "model_record": { + "k_ij": [ + 0.645, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "20461-54-5", + "name": "iodide ion", + "formula": "I", + "smiles": "[I-]" + }, + "id2": { + "cas": "13968-08-6", + "name": "hydronium", + "formula": "H", + "smiles": "[H+]" + }, + "model_record": { + "k_ij": [ + 0.497, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14797-73-0", + "name": "perchlorate ion", + "formula": "ClO4", + "smiles": "[O-]Cl(=O)(=O)=O" + }, + "id2": { + "cas": "13968-08-6", + "name": "hydronium", + "formula": "H", + "smiles": "[H+]" + }, + "model_record": { + "k_ij": [ + 0.861, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "16887-00-6", + "name": "chloride ion", + "formula": "Cl", + "smiles": "[Cl-]" + }, + "id2": { + "cas": "17341-24-1", + "name": "lithium ion", + "formula": "Li", + "smiles": "[Li+]" + }, + "model_record": { + "k_ij": [ + 0.669, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "24959-67-9", + "name": "bromide ion", + "formula": "Br", + "smiles": "[Br-]" + }, + "id2": { + "cas": "17341-24-1", + "name": "lithium ion", + "formula": "Li", + "smiles": "[Li+]" + }, + "model_record": { + "k_ij": [ + 0.591, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "20461-54-5", + "name": "iodide ion", + "formula": "I", + "smiles": "[I-]" + }, + "id2": { + "cas": "17341-24-1", + "name": "lithium ion", + "formula": "Li", + "smiles": "[Li+]" + }, + "model_record": { + "k_ij": [ + 0.002, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14797-73-0", + "name": "perchlorate ion", + "formula": "ClO4", + "smiles": "[O-]Cl(=O)(=O)=O" + }, + "id2": { + "cas": "17341-24-1", + "name": "lithium ion", + "formula": "Li", + "smiles": "[Li+]" + }, + "model_record": { + "k_ij": [ + 0.406, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14797-55-8", + "name": "nitrate ion", + "formula": "NO3", + "smiles": "[N+](=O)([O-])[O-]" + }, + "id2": { + "cas": "17341-24-1", + "name": "lithium ion", + "formula": "Li", + "smiles": "[Li+]" + }, + "model_record": { + "k_ij": [ + 0.364, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14280-30-9", + "name": "hydroxide", + "formula": "OH", + "smiles": "[OH-]" + }, + "id2": { + "cas": "17341-24-1", + "name": "lithium ion", + "formula": "Li", + "smiles": "[Li+]" + }, + "model_record": { + "k_ij": [ + -0.566, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "16984-48-8", + "name": "fluoride ion", + "formula": "F", + "smiles": "[F-]" + }, + "id2": { + "cas": "7440-23-5", + "name": "sodium ion", + "formula": "Na", + "smiles": "[Na+]" + }, + "model_record": { + "k_ij": [ + 0.665, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "16887-00-6", + "name": "chloride ion", + "formula": "Cl", + "smiles": "[Cl-]" + }, + "id2": { + "cas": "7440-23-5", + "name": "sodium ion", + "formula": "Na", + "smiles": "[Na+]" + }, + "model_record": { + "k_ij": [ + 0.317, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "24959-67-9", + "name": "bromide ion", + "formula": "Br", + "smiles": "[Br-]" + }, + "id2": { + "cas": "7440-23-5", + "name": "sodium ion", + "formula": "Na", + "smiles": "[Na+]" + }, + "model_record": { + "k_ij": [ + 0.290, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "20461-54-5", + "name": "iodide ion", + "formula": "I", + "smiles": "[I-]" + }, + "id2": { + "cas": "7440-23-5", + "name": "sodium ion", + "formula": "Na", + "smiles": "[Na+]" + }, + "model_record": { + "k_ij": [ + 0.018, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14797-73-0", + "name": "perchlorate ion", + "formula": "ClO4", + "smiles": "[O-]Cl(=O)(=O)=O" + }, + "id2": { + "cas": "7440-23-5", + "name": "sodium ion", + "formula": "Na", + "smiles": "[Na+]" + }, + "model_record": { + "k_ij": [ + -0.118, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14797-55-8", + "name": "nitrate ion", + "formula": "NO3", + "smiles": "[N+](=O)([O-])[O-]" + }, + "id2": { + "cas": "7440-23-5", + "name": "sodium ion", + "formula": "Na", + "smiles": "[Na+]" + }, + "model_record": { + "k_ij": [ + -0.300, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7440-23-5", + "name": "sodium ion", + "formula": "Na", + "smiles": "[Na+]" + }, + "id2": { + "cas": "14066-20-7", + "name": "dihydrogenphosphate ion", + "formula": "H2PO4", + "smiles": "OP(=O)(O)[O-]" + }, + "model_record": { + "k_ij": [ + -0.071, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14280-30-9", + "name": "hydroxide", + "formula": "OH", + "smiles": "[OH-]" + }, + "id2": { + "cas": "7440-23-5", + "name": "sodium ion", + "formula": "Na", + "smiles": "[Na+]" + }, + "model_record": { + "k_ij": [ + 0.649, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7440-23-5", + "name": "sodium ion", + "formula": "Na", + "smiles": "[Na+]" + }, + "id2": { + "cas": "3812-32-6", + "name": "carbonate", + "formula": "CO3", + "smiles": "O=C([O-])[O-]" + }, + "model_record": { + "k_ij": [ + -1.0, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "7440-23-5", + "name": "sodium ion", + "formula": "Na", + "smiles": "[Na+]" + }, + "id2": { + "cas": "72-52-3", + "name": "hydrogen-carbonate", + "formula": "HCO3", + "smiles": "C(=O)(O)[O-]" + }, + "model_record": { + "k_ij": [ + -0.514, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "16984-48-8", + "name": "fluoride ion", + "formula": "F", + "smiles": "[F-]" + }, + "id2": { + "cas": "24203-36-9", + "name": "potassium ion", + "formula": "K", + "smiles": "[K+]" + }, + "model_record": { + "k_ij": [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "16887-00-6", + "name": "chloride ion", + "formula": "Cl", + "smiles": "[Cl-]" + }, + "id2": { + "cas": "24203-36-9", + "name": "potassium ion", + "formula": "K", + "smiles": "[K+]" + }, + "model_record": { + "k_ij": [ + 0.064, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "24959-67-9", + "name": "bromide ion", + "formula": "Br", + "smiles": "[Br-]" + }, + "id2": { + "cas": "24203-36-9", + "name": "potassium ion", + "formula": "K", + "smiles": "[K+]" + }, + "model_record": { + "k_ij": [ + -0.102, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "20461-54-5", + "name": "iodide ion", + "formula": "I", + "smiles": "[I-]" + }, + "id2": { + "cas": "24203-36-9", + "name": "potassium ion", + "formula": "K", + "smiles": "[K+]" + }, + "model_record": { + "k_ij": [ + -0.312, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14797-55-8", + "name": "nitrate ion", + "formula": "NO3", + "smiles": "[N+](=O)([O-])[O-]" + }, + "id2": { + "cas": "24203-36-9", + "name": "potassium ion", + "formula": "K", + "smiles": "[K+]" + }, + "model_record": { + "k_ij": [ + -0.585, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "24203-36-9", + "name": "potassium ion", + "formula": "K", + "smiles": "[K+]" + }, + "id2": { + "cas": "14066-20-7", + "name": "dihydrogenphosphate ion", + "formula": "H2PO4", + "smiles": "OP(=O)(O)[O-]" + }, + "model_record": { + "k_ij": [ + -0.018, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14280-30-9", + "name": "hydroxide", + "formula": "OH", + "smiles": "[OH-]" + }, + "id2": { + "cas": "24203-36-9", + "name": "potassium ion", + "formula": "K", + "smiles": "[K+]" + }, + "model_record": { + "k_ij": [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "24203-36-9", + "name": "potassium ion", + "formula": "K", + "smiles": "[K+]" + }, + "id2": { + "cas": "3812-32-6", + "name": "carbonate", + "formula": "CO3", + "smiles": "O=C([O-])[O-]" + }, + "model_record": { + "k_ij": [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "24203-36-9", + "name": "potassium ion", + "formula": "K", + "smiles": "[K+]" + }, + "id2": { + "cas": "72-52-3", + "name": "hydrogen-carbonate", + "formula": "HCO3", + "smiles": "C(=O)(O)[O-]" + }, + "model_record": { + "k_ij": [ + -0.476, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "16984-48-8", + "name": "fluoride ion", + "formula": "F", + "smiles": "[F-]" + }, + "id2": { + "cas": "18459-37-5", + "name": "cesium ion", + "formula": "Cs", + "smiles": "[Cs+]" + }, + "model_record": { + "k_ij": [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "16887-00-6", + "name": "chloride ion", + "formula": "Cl", + "smiles": "[Cl-]" + }, + "id2": { + "cas": "18459-37-5", + "name": "cesium ion", + "formula": "Cs", + "smiles": "[Cs+]" + }, + "model_record": { + "k_ij": [ + -0.417, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "24959-67-9", + "name": "bromide ion", + "formula": "Br", + "smiles": "[Br-]" + }, + "id2": { + "cas": "18459-37-5", + "name": "cesium ion", + "formula": "Cs", + "smiles": "[Cs+]" + }, + "model_record": { + "k_ij": [ + -0.67, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "20461-54-5", + "name": "iodide ion", + "formula": "I", + "smiles": "[I-]" + }, + "id2": { + "cas": "18459-37-5", + "name": "cesium ion", + "formula": "Cs", + "smiles": "[Cs+]" + }, + "model_record": { + "k_ij": [ + -1.0, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14797-55-8", + "name": "nitrate ion", + "formula": "NO3", + "smiles": "[N+](=O)([O-])[O-]" + }, + "id2": { + "cas": "18459-37-5", + "name": "cesium ion", + "formula": "Cs", + "smiles": "[Cs+]" + }, + "model_record": { + "k_ij": [ + -0.855, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14280-30-9", + "name": "hydroxide", + "formula": "OH", + "smiles": "[OH-]" + }, + "id2": { + "cas": "18459-37-5", + "name": "cesium ion", + "formula": "Cs", + "smiles": "[Cs+]" + }, + "model_record": { + "k_ij": [ + 0.564, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "16887-00-6", + "name": "chloride ion", + "formula": "Cl", + "smiles": "[Cl-]" + }, + "id2": { + "cas": "14798-03-9", + "name": "ammonium ion", + "formula": "NH4", + "smiles": "[NH4+]" + }, + "model_record": { + "k_ij": [ + -0.566, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "24959-67-9", + "name": "bromide ion", + "formula": "Br", + "smiles": "[Br-]" + }, + "id2": { + "cas": "14798-03-9", + "name": "ammonium ion", + "formula": "NH4", + "smiles": "[NH4+]" + }, + "model_record": { + "k_ij": [ + -0.639, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "20461-54-5", + "name": "iodide ion", + "formula": "I", + "smiles": "[I-]" + }, + "id2": { + "cas": "14798-03-9", + "name": "ammonium ion", + "formula": "NH4", + "smiles": "[NH4+]" + }, + "model_record": { + "k_ij": [ + -0.787, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14797-73-0", + "name": "perchlorate ion", + "formula": "ClO4", + "smiles": "[O-]Cl(=O)(=O)=O" + }, + "id2": { + "cas": "14798-03-9", + "name": "ammonium ion", + "formula": "NH4", + "smiles": "[NH4+]" + }, + "model_record": { + "k_ij": [ + -1.0, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14797-55-8", + "name": "nitrate ion", + "formula": "NO3", + "smiles": "[N+](=O)([O-])[O-]" + }, + "id2": { + "cas": "14798-03-9", + "name": "ammonium ion", + "formula": "NH4", + "smiles": "[NH4+]" + }, + "model_record": { + "k_ij": [ + -0.419, + 0.0, + 0.0, + 0.0 + ] + } + }, + { + "id1": { + "cas": "14798-03-9", + "name": "ammonium ion", + "formula": "NH4", + "smiles": "[NH4+]" + }, + "id2": { + "cas": "14066-20-7", + "name": "dihydrogenphosphate ion", + "formula": "H2PO4", + "smiles": "OP(=O)(O)[O-]" + }, + "model_record": { + "k_ij": [ + -1.0, + 0.0, + 0.0, + 0.0 + ] + } + } +] \ No newline at end of file diff --git a/parameters/epcsaft/held2014_w_permittivity_added.json b/parameters/epcsaft/held2014_w_permittivity_added.json new file mode 100644 index 000000000..6c8ae355b --- /dev/null +++ b/parameters/epcsaft/held2014_w_permittivity_added.json @@ -0,0 +1,440 @@ +[ + { + "identifier": { + "cas": "7732-18-5", + "name": "water", + "iupac_name": "oxidane", + "smiles": "O", + "inchi": "InChI=1/H2O/h1H2", + "formula": "H2O" + }, + "model_record": { + "m": 1.2047, + "sigma": 2.7927, + "epsilon_k": 353.95, + "kappa_ab": 0.04509, + "epsilon_k_ab": 2425.7, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 280.15, + 84.89 + ], + [ + 298.15, + 78.39 + ], + [ + 360.15, + 58.73 + ] + ] + } + }, + "na": 1.0, + "nb": 1.0 + }, + "molarweight": 18.0152 + }, + { + "identifier": { + "cas": "24959-67-9", + "name": "bromide ion", + "formula": "Br", + "smiles": "[Br-]" + }, + "molarweight": 79.9, + "model_record": { + "m": 1, + "sigma": 3.0707, + "epsilon_k": 190, + "z": -1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "3812-32-6", + "name": "carbonate", + "formula": "CO3", + "smiles": "O=C([O-])[O-]" + }, + "molarweight": 60.009, + "model_record": { + "m": 1, + "sigma": 2.4422, + "epsilon_k": 249.26, + "z": -2, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "16887-00-6", + "name": "chloride ion", + "formula": "Cl", + "smiles": "[Cl-]" + }, + "molarweight": 35.45, + "model_record": { + "m": 1, + "sigma": 2.756, + "epsilon_k": 170, + "z": -1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "16984-48-8", + "name": "fluoride ion", + "formula": "F", + "smiles": "[F-]" + }, + "molarweight": 18.9984, + "model_record": { + "m": 1, + "sigma": 1.7712, + "epsilon_k": 275, + "z": -1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "72-52-3", + "name": "hydrogen-carbonate", + "formula": "HCO3", + "smiles": "C(=O)(O)[O-]" + }, + "molarweight": 61.0168, + "model_record": { + "m": 1, + "sigma": 2.9296, + "epsilon_k": 70, + "z": -1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "13968-08-6", + "name": "hydronium", + "formula": "H", + "smiles": "[H+]" + }, + "molarweight": 1.0074, + "model_record": { + "m": 1, + "sigma": 3.4654, + "epsilon_k": 500, + "z": 1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "14280-30-9", + "name": "hydroxide", + "formula": "OH", + "smiles": "[OH-]" + }, + "molarweight": 17.0073, + "model_record": { + "m": 1, + "sigma": 2.0177, + "epsilon_k": 650, + "z": -1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "20461-54-5", + "name": "iodide ion", + "formula": "I", + "smiles": "[I-]" + }, + "molarweight": 126.9045, + "model_record": { + "m": 1, + "sigma": 3.6672, + "epsilon_k": 200, + "z": -1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "17341-24-1", + "name": "lithium ion", + "formula": "Li", + "smiles": "[Li+]" + }, + "molarweight": 7, + "model_record": { + "m": 1, + "sigma": 2.8449, + "epsilon_k": 360, + "z": 1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "24203-36-9", + "name": "potassium ion", + "formula": "K", + "smiles": "[K+]" + }, + "molarweight": 39.098, + "model_record": { + "m": 1, + "sigma": 3.3417, + "epsilon_k": 200, + "z": 1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "7440-23-5", + "name": "sodium ion", + "formula": "Na", + "smiles": "[Na+]" + }, + "molarweight": 22.98977, + "model_record": { + "m": 1, + "sigma": 2.8232, + "epsilon_k": 230, + "z": 1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "18459-37-5", + "name": "cesium ion", + "formula": "Cs", + "smiles": "[Cs+]" + }, + "molarweight": 132.905, + "model_record": { + "m": 1, + "sigma": 3.9246, + "epsilon_k": 180, + "z": 1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "14798-03-9", + "name": "ammonium ion", + "formula": "NH4", + "smiles": "[NH4+]" + }, + "molarweight": 18.04, + "model_record": { + "m": 1, + "sigma": 3.5740, + "epsilon_k": 230, + "z": 1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "14797-55-8", + "name": "nitrate ion", + "formula": "NO3", + "smiles": "[N+](=O)([O-])[O-]" + }, + "molarweight": 62.0049, + "model_record": { + "m": 1, + "sigma": 3.2988, + "epsilon_k": 130, + "z": -1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "14797-73-0", + "name": "perchlorate ion", + "formula": "ClO4", + "smiles": "[O-]Cl(=O)(=O)=O" + }, + "molarweight": 99.45, + "model_record": { + "m": 1, + "sigma": 4.0186, + "epsilon_k": 104.16, + "z": -1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + }, + { + "identifier": { + "cas": "14066-20-7", + "name": "dihydrogenphosphate ion", + "formula": "H2PO4", + "smiles": "OP(=O)(O)[O-]" + }, + "molarweight": 96.986, + "model_record": { + "m": 1, + "sigma": 3.6505, + "epsilon_k": 95.0, + "z": -1, + "permittivity_record": { + "ExperimentalData": { + "data": [ + [ + 298.15, + 8.0 + ] + ] + } + } + } + } +] \ No newline at end of file From eb129babaa0e1b9a77a20ea430404c0575d1ae7b Mon Sep 17 00:00:00 2001 From: LisaNeumaier Date: Thu, 18 Apr 2024 15:24:26 +0200 Subject: [PATCH 19/23] added documentation for parameters --- parameters/epcsaft/README.md | 22 ++++++++++++++++++++++ parameters/epcsaft/literature.bib | 15 +++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 parameters/epcsaft/README.md create mode 100644 parameters/epcsaft/literature.bib diff --git a/parameters/epcsaft/README.md b/parameters/epcsaft/README.md new file mode 100644 index 000000000..7f9542e0c --- /dev/null +++ b/parameters/epcsaft/README.md @@ -0,0 +1,22 @@ +# ePC-SAFT Parameters + +This directory contains files with parameters for ePC-SAFT equation of state. +The files named according to the pattern `NameYear.json` correspond to published parameters. The corresponding publication is provided in the [`literature.bib`](literature.bib) file. + +## Notes + +- Experimental data for the permittivity has not been part of the original publication and has been added. +- Ion permittivity has been set to 8.0 as in [Bülow et al. (2021)](https://www.sciencedirect.com/science/article/pii/S0378381221000297). +- Contains only univalent cations and anions from [Held et al. (2014)](https://www.sciencedirect.com/science/article/pii/S0263876214002469) and carbonate. Multivalent ions need to be added. +- Correlation for `k_ij` of water/Na+ and water/K+ has not been adapted from [Held et al. (2014)](https://www.sciencedirect.com/science/article/pii/S0263876214002469). Instead, a constant value at 298.15 K is assumed. + +## Pure Substance Parameters +| file | model | publication | +| -------------------------------------------------------------------------- | ---------------------------------------- | :------------------------------------------------------: | +| [`held2014_w_permittivity_added.json`](held2014_w_permittivity_added.json) | parameters for univalent ions and water. | [🔗](https://doi.org/10.1016/j.cherd.2014.05.017) | + +## Binary Parameters + +| file | model | publication | +| ---------------------------------------------- | ----------------------------------------------- | :------------------------------------------------------: | +| [`held2014_binary.json`](held2014_binary.json) | binary parameters for univalent ions and water. | [🔗](https://doi.org/10.1016/j.cherd.2014.05.017) | diff --git a/parameters/epcsaft/literature.bib b/parameters/epcsaft/literature.bib new file mode 100644 index 000000000..b0aafff0c --- /dev/null +++ b/parameters/epcsaft/literature.bib @@ -0,0 +1,15 @@ +@article{Held.2014, + author = {Held, Christoph and Reschke, Thomas and Mohammad, Sultan and Luza, Armando and Sadowski, Gabriele}, + journal = {{Chemical Engineering Research and Design}}, + title = {{ePC-SAFT revised}}, + year = {2014}, + issn = {02638762}, + number = {12}, + pages = {2884--2897}, + volume = {92}, + abstract = {Chemical Engineering Research and Design, 92 (2014) 2884-2897. doi:10.1016/j.cherd.2014.05.017}, + doi = {10.1016/j.cherd.2014.05.017}, + file = {Held2014 ePC-SAFT revised (2):C\:\\Users\\lneumaier\\Documents\\Citavi 6\\Projects\\PhD_Lisa\\Citavi Attachments\\Held2014 ePC-SAFT revised (2).pdf:pdf}, + groups = {Permittivity, Electrolyte thermodynamics}, + keywords = {Activity coefficients;Electrolytes;LLE;Osmotic coefficients;Solubility;VLE}, +} \ No newline at end of file From e45db99e25ba949923c73f3d3603dbf2fbfefed6 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 18 Apr 2024 16:11:58 +0200 Subject: [PATCH 20/23] update CHANGELOG, Cargo.toml, README --- CHANGELOG.md | 3 +++ Cargo.toml | 14 +++----------- README.md | 1 + 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1196ddc47..63e330224 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added SAFT-VR Mie equation of state.[#237](https://github.com/feos-org/feos/pull/237) +### Added +- Added ePC-SAFT equation of state. [#229](https://github.com/feos-org/feos/pull/229) + ### Changed - Updated model implementations to account for the removal of trait objects for Helmholtz energy contributions and the de Broglie in `feos-core`. [#226](https://github.com/feos-org/feos/pull/226) - Changed Helmholtz energy functions in `PcSaft` contributions so that the temperature-dependent diameter is re-used across different contributions. [#226](https://github.com/feos-org/feos/pull/226) diff --git a/Cargo.toml b/Cargo.toml index 848b17a2c..ae41c6a70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,27 +1,19 @@ [package] name = "feos" version = "0.6.1" -authors = [ - "Gernot Bauer ", - "Philipp Rehner ", -] +authors = ["Gernot Bauer ", "Philipp Rehner "] edition = "2021" readme = "README.md" license = "MIT OR Apache-2.0" description = "FeOs - A framework for equations of state and classical density functional theory." homepage = "https://github.com/feos-org" repository = "https://github.com/feos-org/feos" -keywords = [ - "physics", - "thermodynamics", - "equations_of_state", - "phase_equilibria", -] +keywords = ["physics", "thermodynamics", "equations_of_state", "phase_equilibria"] categories = ["science"] [package.metadata.docs.rs] features = ["all_models", "rayon"] -rustdoc-args = ["--html-in-header", "./docs-header.html"] +rustdoc-args = [ "--html-in-header", "./docs-header.html" ] [workspace] members = ["feos-core", "feos-dft", "feos-derive"] diff --git a/README.md b/README.md index a9248ac66..80676187e 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ The following models are currently published as part of the `FeOs` framework |name|description|eos|dft| |-|-|:-:|:-:| |`pcsaft`|perturbed-chain (polar) statistical associating fluid theory|✓|✓| +|`epcsaft`|electrolyte PC-SAFT|✓|| |`gc-pcsaft`|(heterosegmented) group contribution PC-SAFT|✓|✓| |`pets`|perturbed truncated and shifted Lennard-Jones mixtures|✓|✓| |`uvtheory`|equation of state for Mie fluids and mixtures|✓|| From 0400804c31a7dd3c54ed05c544d3ace90c8cd0fe Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 18 Apr 2024 16:17:05 +0200 Subject: [PATCH 21/23] only one Added category in CHANGELOG --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63e330224..3c22d58c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Added SAFT-VR Mie equation of state.[#237](https://github.com/feos-org/feos/pull/237) - -### Added - Added ePC-SAFT equation of state. [#229](https://github.com/feos-org/feos/pull/229) ### Changed From b1725f46e030a400d642225bdc5bebe3fc23650f Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 18 Apr 2024 16:37:35 +0200 Subject: [PATCH 22/23] add binary_path in doc example --- docs/api/epcsaft.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/epcsaft.md b/docs/api/epcsaft.md index 51900fafb..d125e1e3e 100644 --- a/docs/api/epcsaft.md +++ b/docs/api/epcsaft.md @@ -9,7 +9,7 @@ from feos.epcsaft import ElectrolytePcSaftParameters pure_path = 'parameters/epcsaft/held2014_w_permittivity_added.json' binary_path = 'parameters/epcsaft/held2014_binary.json' -parameters = ElectrolytePcSaftParameters.from_json(['water', 'sodium ion', 'chloride ion'], pure_path) +parameters = ElectrolytePcSaftParameters.from_json(['water', 'sodium ion', 'chloride ion'], pure_path, binary_path) ``` ## Data types From 787f06ea6e84424844aa29ea720865209905bee5 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Thu, 18 Apr 2024 16:44:30 +0200 Subject: [PATCH 23/23] removed some warnings and fixed tests --- src/epcsaft/eos/born.rs | 2 +- src/epcsaft/eos/dispersion.rs | 2 +- src/epcsaft/eos/ionic.rs | 2 +- src/epcsaft/parameters.rs | 56 ++++++++++++++++------------------- 4 files changed, 29 insertions(+), 33 deletions(-) diff --git a/src/epcsaft/eos/born.rs b/src/epcsaft/eos/born.rs index 623fadd13..ad67cebac 100644 --- a/src/epcsaft/eos/born.rs +++ b/src/epcsaft/eos/born.rs @@ -1,6 +1,5 @@ use crate::epcsaft::eos::permittivity::Permittivity; use crate::epcsaft::parameters::ElectrolytePcSaftParameters; -use crate::hard_sphere::HardSphereProperties; use feos_core::StateHD; use ndarray::Array1; use num_dual::DualNum; @@ -55,6 +54,7 @@ impl fmt::Display for Born { mod tests { use super::*; use crate::epcsaft::parameters::utils::{water_nacl_parameters, water_nacl_parameters_perturb}; + use crate::hard_sphere::HardSphereProperties; use approx::assert_relative_eq; use ndarray::arr1; diff --git a/src/epcsaft/eos/dispersion.rs b/src/epcsaft/eos/dispersion.rs index 1859519f7..f01ea8b63 100644 --- a/src/epcsaft/eos/dispersion.rs +++ b/src/epcsaft/eos/dispersion.rs @@ -1,5 +1,4 @@ use crate::epcsaft::parameters::ElectrolytePcSaftParameters; -use crate::hard_sphere::HardSphereProperties; use feos_core::StateHD; use ndarray::{Array, Array1, Array2}; use num_dual::DualNum; @@ -174,6 +173,7 @@ mod tests { use crate::epcsaft::parameters::utils::{ butane_parameters, propane_butane_parameters, propane_parameters, }; + use crate::hard_sphere::HardSphereProperties; use approx::assert_relative_eq; use ndarray::arr1; diff --git a/src/epcsaft/eos/ionic.rs b/src/epcsaft/eos/ionic.rs index 7a8c806f4..623174455 100644 --- a/src/epcsaft/eos/ionic.rs +++ b/src/epcsaft/eos/ionic.rs @@ -1,6 +1,5 @@ use crate::epcsaft::eos::permittivity::Permittivity; use crate::epcsaft::parameters::ElectrolytePcSaftParameters; -use crate::hard_sphere::HardSphereProperties; use feos_core::StateHD; use ndarray::*; use num_dual::DualNum; @@ -97,6 +96,7 @@ impl fmt::Display for Ionic { mod tests { use super::*; use crate::epcsaft::parameters::utils::{water_nacl_parameters, water_nacl_parameters_perturb}; + use crate::hard_sphere::HardSphereProperties; use approx::assert_relative_eq; use ndarray::arr1; diff --git a/src/epcsaft/parameters.rs b/src/epcsaft/parameters.rs index 67061b6f2..596e6e923 100644 --- a/src/epcsaft/parameters.rs +++ b/src/epcsaft/parameters.rs @@ -474,32 +474,29 @@ impl Parameter for ElectrolytePcSaftParameters { )); } - if modeltypes.len() >= 1 { - if modeltypes[0] == 2 { - // order points in data by increasing temperature - let mut permittivity_records_clone = permittivity_records.clone(); - permittivity_records_clone - .iter_mut() - .filter(|record| (record.is_some())) - .enumerate() - .for_each(|(i, record)| { - if let PermittivityRecord::ExperimentalData { data } = - record.as_mut().unwrap() - { - let mut data = data.clone(); - data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - // check if all temperatures a.0 in data are finite, if not, make them finite by rounding to four digits - data.iter_mut().for_each(|a| { - if !a.0.is_finite() { - a.0 = (a.0 * 1e4).round() / 1e4; - } - }); - // save data again in record - permittivity_records[i] = - Some(PermittivityRecord::ExperimentalData { data }); - } - }); - } + if !modeltypes.is_empty() && modeltypes[0] == 2 { + // order points in data by increasing temperature + let mut permittivity_records_clone = permittivity_records.clone(); + permittivity_records_clone + .iter_mut() + .filter(|record| (record.is_some())) + .enumerate() + .for_each(|(i, record)| { + if let PermittivityRecord::ExperimentalData { data } = record.as_mut().unwrap() + { + let mut data = data.clone(); + data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + // check if all temperatures a.0 in data are finite, if not, make them finite by rounding to four digits + data.iter_mut().for_each(|a| { + if !a.0.is_finite() { + a.0 = (a.0 * 1e4).round() / 1e4; + } + }); + // save data again in record + permittivity_records[i] = + Some(PermittivityRecord::ExperimentalData { data }); + } + }); } Ok(Self { @@ -605,7 +602,6 @@ impl ElectrolytePcSaftParameters { #[cfg(test)] pub mod utils { use feos_core::parameter::{BinaryRecord, Identifier}; - use ndarray::ArrayBase; use super::*; use std::sync::Arc; @@ -624,7 +620,7 @@ pub mod utils { "model_record": { "m": 2.001829, "sigma": 3.618353, - "epsilon_k": 208.1101, + "epsilon_k": 208.1101 }, "molarweight": 44.0962 }"#; @@ -1152,7 +1148,7 @@ pub mod utils { "model_record": { "m": 2.0018290000000003, "sigma": 3.618353, - "epsilon_k": 208.1101, + "epsilon_k": 208.1101 }, "molarweight": 44.0962 }, @@ -1168,7 +1164,7 @@ pub mod utils { "model_record": { "m": 2.331586, "sigma": 3.7086010000000003, - "epsilon_k": 222.8774, + "epsilon_k": 222.8774 }, "molarweight": 58.123 }