1use super::parameters::{ElectrolytePcSaftParameters, ElectrolytePcSaftPars};
2use crate::association::Association;
3use crate::hard_sphere::{HardSphere, HardSphereProperties};
4use feos_core::{FeosResult, ResidualDyn, Subset};
5use feos_core::{Molarweight, StateHD};
6use nalgebra::DVector;
7use num_dual::DualNum;
8use quantity::*;
9use std::f64::consts::FRAC_PI_6;
10
11pub(crate) mod born;
12pub(crate) mod dispersion;
13pub(crate) mod hard_chain;
14pub(crate) mod ionic;
15pub(crate) mod permittivity;
16use born::Born;
17use dispersion::Dispersion;
18use hard_chain::HardChain;
19use ionic::Ionic;
20
21#[derive(Copy, Clone, PartialEq)]
23pub enum ElectrolytePcSaftVariants {
24 Advanced,
25 Revised,
26}
27
28#[derive(Copy, Clone)]
30pub struct ElectrolytePcSaftOptions {
31 pub max_eta: f64,
32 pub max_iter_cross_assoc: usize,
33 pub tol_cross_assoc: f64,
34 pub epcsaft_variant: ElectrolytePcSaftVariants,
35}
36
37impl Default for ElectrolytePcSaftOptions {
38 fn default() -> Self {
39 Self {
40 max_eta: 0.5,
41 max_iter_cross_assoc: 50,
42 tol_cross_assoc: 1e-10,
43 epcsaft_variant: ElectrolytePcSaftVariants::Advanced,
44 }
45 }
46}
47
48pub struct ElectrolytePcSaft {
50 pub parameters: ElectrolytePcSaftParameters,
51 pub params: ElectrolytePcSaftPars,
52 pub options: ElectrolytePcSaftOptions,
53 hard_chain: bool,
54 association: Option<Association>,
55 ionic: bool,
56 born: bool,
57}
58
59impl ElectrolytePcSaft {
60 pub fn new(parameters: ElectrolytePcSaftParameters) -> FeosResult<Self> {
61 Self::with_options(parameters, ElectrolytePcSaftOptions::default())
62 }
63
64 pub fn with_options(
65 parameters: ElectrolytePcSaftParameters,
66 options: ElectrolytePcSaftOptions,
67 ) -> FeosResult<Self> {
68 let params = ElectrolytePcSaftPars::new(¶meters)?;
69 let hard_chain = params.m.iter().any(|m| (m - 1.0).abs() > 1e-15);
70
71 let association = (!parameters.association.is_empty())
72 .then(|| Association::new(options.max_iter_cross_assoc, options.tol_cross_assoc));
73
74 let ionic = params.nionic > 0;
75 let born = ionic && matches!(options.epcsaft_variant, ElectrolytePcSaftVariants::Advanced);
76
77 Ok(Self {
78 parameters,
79 params,
80 options,
81 hard_chain,
82 association,
83 ionic,
84 born,
85 })
86 }
87}
88
89impl Subset for ElectrolytePcSaft {
90 fn subset(&self, component_list: &[usize]) -> Self {
91 Self::with_options(self.parameters.subset(component_list), self.options).unwrap()
92 }
93}
94
95impl ResidualDyn for ElectrolytePcSaft {
96 fn components(&self) -> usize {
97 self.parameters.pure.len()
98 }
99
100 fn compute_max_density<D: DualNum<f64> + Copy>(&self, molefracs: &DVector<D>) -> D {
101 let msigma3 = self
102 .params
103 .m
104 .component_mul(&self.params.sigma.map(|v| v.powi(3)));
105 (msigma3.map(D::from).dot(molefracs) * FRAC_PI_6).recip() * self.options.max_eta
106 }
107
108 fn reduced_helmholtz_energy_density_contributions<D: DualNum<f64> + Copy>(
109 &self,
110 state: &StateHD<D>,
111 ) -> Vec<(&'static str, D)> {
112 let mut v = Vec::with_capacity(7);
113 let d = self.params.hs_diameter(state.temperature);
114
115 v.push((
116 "Hard Sphere",
117 HardSphere.helmholtz_energy_density(&self.params, state),
118 ));
119 if self.hard_chain {
120 v.push((
121 "Hard Chain",
122 HardChain.helmholtz_energy_density(&self.params, state),
123 ))
124 }
125 v.push((
126 "Dispersion",
127 Dispersion.helmholtz_energy_density(&self.params, state, &d),
128 ));
129 if let Some(association) = self.association.as_ref() {
130 v.push((
131 "Association",
132 association.helmholtz_energy_density(
133 &self.params,
134 &self.parameters.association,
135 state,
136 &d,
137 ),
138 ))
139 }
140 if self.ionic {
141 v.push((
142 "Ionic",
143 Ionic.helmholtz_energy_density(
144 &self.params,
145 state,
146 &d,
147 self.options.epcsaft_variant,
148 ),
149 ))
150 };
151 if self.born {
152 v.push((
153 "Born",
154 Born.helmholtz_energy_density(&self.params, state, &d),
155 ))
156 };
157 v
158 }
159}
160
161impl Molarweight for ElectrolytePcSaft {
162 fn molar_weight(&self) -> MolarWeight<DVector<f64>> {
163 self.parameters.molar_weight.clone()
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::epcsaft::parameters::utils::{
171 butane_parameters, propane_butane_parameters, propane_parameters,
172 };
173 use approx::assert_relative_eq;
174 use feos_core::*;
175 use nalgebra::dvector;
176 use typenum::P3;
177
178 #[test]
179 fn ideal_gas_pressure() {
180 let e = ElectrolytePcSaft::new(propane_parameters()).unwrap();
181 let t = 200.0 * KELVIN;
182 let v = 1e-3 * METER.powi::<P3>();
183 let n = dvector![1.0] * MOL;
184 let s = State::new_nvt(&&e, t, v, &n).unwrap();
185 let p_ig = s.total_moles * RGAS * t / v;
186 assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10);
187 assert_relative_eq!(
188 s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual),
189 s.pressure(Contributions::Total),
190 epsilon = 1e-10
191 );
192 }
193
194 #[test]
195 fn ideal_gas_heat_capacity_joback() {
196 let e = ElectrolytePcSaft::new(propane_parameters()).unwrap();
197 let t = 200.0 * KELVIN;
198 let v = 1e-3 * METER.powi::<P3>();
199 let n = dvector![1.0] * MOL;
200 let s = State::new_nvt(&&e, t, v, &n).unwrap();
201 let p_ig = s.total_moles * RGAS * t / v;
202 assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10);
203 assert_relative_eq!(
204 s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual),
205 s.pressure(Contributions::Total),
206 epsilon = 1e-10
207 );
208 }
209
210 #[test]
211 fn hard_sphere() {
212 let p = ElectrolytePcSaftPars::new(&propane_parameters()).unwrap();
213 let t = 250.0;
214 let v = 1000.0;
215 let n = 1.0;
216 let s = StateHD::new(t, v, &dvector![n]);
217 let a_rust = HardSphere.helmholtz_energy_density(&p, &s) * v;
218 assert_relative_eq!(a_rust, 0.410610492598808, epsilon = 1e-10);
219 }
220
221 #[test]
222 fn hard_sphere_mix() {
223 let p1 = ElectrolytePcSaftPars::new(&propane_parameters()).unwrap();
224 let p2 = ElectrolytePcSaftPars::new(&butane_parameters()).unwrap();
225 let p12 = ElectrolytePcSaftPars::new(&propane_butane_parameters()).unwrap();
226 let t = 250.0;
227 let v = 2.5e28;
228 let n = 1.0;
229 let s = StateHD::new(t, v, &dvector![n]);
230 let a1 = HardSphere.helmholtz_energy_density(&p1, &s);
231 let a2 = HardSphere.helmholtz_energy_density(&p2, &s);
232 let s1m = StateHD::new(t, v, &dvector![n, 0.0]);
233 let a1m = HardSphere.helmholtz_energy_density(&p12, &s1m);
234 let s2m = StateHD::new(t, v, &dvector![0.0, n]);
235 let a2m = HardSphere.helmholtz_energy_density(&p12, &s2m);
236 assert_relative_eq!(a1, a1m, epsilon = 1e-14);
237 assert_relative_eq!(a2, a2m, epsilon = 1e-14);
238 }
239
240 #[test]
241 fn new_tpn() {
242 let e = ElectrolytePcSaft::new(propane_parameters()).unwrap();
243 let t = 300.0 * KELVIN;
244 let p = BAR;
245 let m = dvector![1.0] * MOL;
246 let s = State::new_npt(&&e, t, p, &m, None);
247 let p_calc = if let Ok(state) = s {
248 state.pressure(Contributions::Total)
249 } else {
250 0.0 * PASCAL
251 };
252 assert_relative_eq!(p, p_calc, epsilon = 1e-6);
253 }
254
255 #[test]
256 fn vle_pure() {
257 let e = ElectrolytePcSaft::new(propane_parameters()).unwrap();
258 let t = 300.0 * KELVIN;
259 let vle = PhaseEquilibrium::pure(&&e, t, None, Default::default());
260 if let Ok(v) = vle {
261 assert_relative_eq!(
262 v.vapor().pressure(Contributions::Total),
263 v.liquid().pressure(Contributions::Total),
264 epsilon = 1e-6
265 )
266 }
267 }
268
269 #[test]
270 fn critical_point() {
271 let e = ElectrolytePcSaft::new(propane_parameters()).unwrap();
272 let t = 300.0 * KELVIN;
273 let cp = State::critical_point(&&e, None, Some(t), None, Default::default());
274 if let Ok(v) = cp {
275 assert_relative_eq!(v.temperature, 375.1244078318015 * KELVIN, epsilon = 1e-8)
276 }
277 }
278
279 #[test]
280 fn mix_single() {
281 let e1 = ElectrolytePcSaft::new(propane_parameters()).unwrap();
282 let e2 = ElectrolytePcSaft::new(butane_parameters()).unwrap();
283 let e12 = ElectrolytePcSaft::new(propane_butane_parameters()).unwrap();
284 let t = 300.0 * KELVIN;
285 let v = 0.02456883872966545 * METER.powi::<P3>();
286 let m1 = dvector![2.0] * MOL;
287 let m1m = dvector![2.0, 0.0] * MOL;
288 let m2m = dvector![0.0, 2.0] * MOL;
289 let s1 = State::new_nvt(&&e1, t, v, &m1).unwrap();
290 let s2 = State::new_nvt(&&e2, t, v, &m1).unwrap();
291 let s1m = State::new_nvt(&&e12, t, v, &m1m).unwrap();
292 let s2m = State::new_nvt(&&e12, t, v, &m2m).unwrap();
293 assert_relative_eq!(
294 s1.pressure(Contributions::Total),
295 s1m.pressure(Contributions::Total),
296 epsilon = 1e-12
297 );
298 assert_relative_eq!(
299 s2.pressure(Contributions::Total),
300 s2m.pressure(Contributions::Total),
301 epsilon = 1e-12
302 );
303 assert_relative_eq!(
304 s2.pressure(Contributions::Total),
305 s2m.pressure(Contributions::Total),
306 epsilon = 1e-12
307 )
308 }
309}