1use super::parameters::PcSaftParameters;
2use crate::association::Association;
3use crate::hard_sphere::{HardSphere, HardSphereProperties};
4use feos_core::parameter::Parameter;
5use feos_core::{
6 Components, EntropyScaling, EosError, EosResult, Molarweight, ReferenceSystem, Residual, State,
7 StateHD,
8};
9use ndarray::Array1;
10use num_dual::DualNum;
11use quantity::*;
12use std::f64::consts::{FRAC_PI_6, PI};
13use std::fmt;
14use std::sync::Arc;
15use typenum::P2;
16
17pub(crate) mod dispersion;
18pub(crate) mod hard_chain;
19pub(crate) mod polar;
20use dispersion::Dispersion;
21use hard_chain::HardChain;
22pub use polar::DQVariants;
23use polar::{Dipole, DipoleQuadrupole, Quadrupole};
24
25#[derive(Copy, Clone)]
27pub struct PcSaftOptions {
28 pub max_eta: f64,
29 pub max_iter_cross_assoc: usize,
30 pub tol_cross_assoc: f64,
31 pub dq_variant: DQVariants,
32}
33
34impl Default for PcSaftOptions {
35 fn default() -> Self {
36 Self {
37 max_eta: 0.5,
38 max_iter_cross_assoc: 50,
39 tol_cross_assoc: 1e-10,
40 dq_variant: DQVariants::DQ35,
41 }
42 }
43}
44
45pub struct PcSaft {
47 parameters: Arc<PcSaftParameters>,
48 options: PcSaftOptions,
49 hard_sphere: HardSphere<PcSaftParameters>,
50 hard_chain: Option<HardChain>,
51 dispersion: Dispersion,
52 dipole: Option<Dipole>,
53 quadrupole: Option<Quadrupole>,
54 dipole_quadrupole: Option<DipoleQuadrupole>,
55 association: Option<Association<PcSaftParameters>>,
56}
57
58impl PcSaft {
59 pub fn new(parameters: Arc<PcSaftParameters>) -> Self {
60 Self::with_options(parameters, PcSaftOptions::default())
61 }
62
63 pub fn with_options(parameters: Arc<PcSaftParameters>, options: PcSaftOptions) -> Self {
64 let hard_sphere = HardSphere::new(¶meters);
65 let dispersion = Dispersion {
66 parameters: parameters.clone(),
67 };
68 let hard_chain = if parameters.m.iter().any(|m| (m - 1.0).abs() > 1e-15) {
69 Some(HardChain {
70 parameters: parameters.clone(),
71 })
72 } else {
73 None
74 };
75
76 let dipole = if parameters.ndipole > 0 {
77 Some(Dipole {
78 parameters: parameters.clone(),
79 })
80 } else {
81 None
82 };
83 let quadrupole = if parameters.nquadpole > 0 {
84 Some(Quadrupole {
85 parameters: parameters.clone(),
86 })
87 } else {
88 None
89 };
90 let dipole_quadrupole = if parameters.ndipole > 0 && parameters.nquadpole > 0 {
91 Some(DipoleQuadrupole {
92 parameters: parameters.clone(),
93 variant: options.dq_variant,
94 })
95 } else {
96 None
97 };
98 let association = if !parameters.association.is_empty() {
99 Some(Association::new(
100 ¶meters,
101 ¶meters.association,
102 options.max_iter_cross_assoc,
103 options.tol_cross_assoc,
104 ))
105 } else {
106 None
107 };
108
109 Self {
110 parameters,
111 options,
112 hard_sphere,
113 hard_chain,
114 dispersion,
115 dipole,
116 quadrupole,
117 dipole_quadrupole,
118 association,
119 }
120 }
121}
122
123impl Components for PcSaft {
124 fn components(&self) -> usize {
125 self.parameters.pure_records.len()
126 }
127
128 fn subset(&self, component_list: &[usize]) -> Self {
129 Self::with_options(
130 Arc::new(self.parameters.subset(component_list)),
131 self.options,
132 )
133 }
134}
135
136impl Residual for PcSaft {
137 fn compute_max_density(&self, moles: &Array1<f64>) -> f64 {
138 self.options.max_eta * moles.sum()
139 / (FRAC_PI_6 * &self.parameters.m * self.parameters.sigma.mapv(|v| v.powi(3)) * moles)
140 .sum()
141 }
142
143 fn residual_helmholtz_energy_contributions<D: DualNum<f64> + Copy>(
144 &self,
145 state: &StateHD<D>,
146 ) -> Vec<(String, D)> {
147 let mut v = Vec::with_capacity(7);
148 let d = self.parameters.hs_diameter(state.temperature);
149
150 v.push((
151 self.hard_sphere.to_string(),
152 self.hard_sphere.helmholtz_energy(state),
153 ));
154 if let Some(hc) = self.hard_chain.as_ref() {
155 v.push((hc.to_string(), hc.helmholtz_energy(state)))
156 }
157 v.push((
158 self.dispersion.to_string(),
159 self.dispersion.helmholtz_energy(state, &d),
160 ));
161 if let Some(dipole) = self.dipole.as_ref() {
162 v.push((dipole.to_string(), dipole.helmholtz_energy(state, &d)))
163 }
164 if let Some(quadrupole) = self.quadrupole.as_ref() {
165 v.push((
166 quadrupole.to_string(),
167 quadrupole.helmholtz_energy(state, &d),
168 ))
169 }
170 if let Some(dipole_quadrupole) = self.dipole_quadrupole.as_ref() {
171 v.push((
172 dipole_quadrupole.to_string(),
173 dipole_quadrupole.helmholtz_energy(state, &d),
174 ))
175 }
176 if let Some(association) = self.association.as_ref() {
177 v.push((
178 association.to_string(),
179 association.helmholtz_energy(state, &d),
180 ))
181 }
182 v
183 }
184}
185
186impl Molarweight for PcSaft {
187 fn molar_weight(&self) -> MolarWeight<Array1<f64>> {
188 self.parameters.molarweight.clone() * GRAM / MOL
189 }
190}
191
192impl fmt::Display for PcSaft {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 write!(f, "PC-SAFT")
195 }
196}
197
198fn omega11(t: f64) -> f64 {
199 1.06036 * t.powf(-0.15610)
200 + 0.19300 * (-0.47635 * t).exp()
201 + 1.03587 * (-1.52996 * t).exp()
202 + 1.76474 * (-3.89411 * t).exp()
203}
204
205fn omega22(t: f64) -> f64 {
206 1.16145 * t.powf(-0.14874) + 0.52487 * (-0.77320 * t).exp() + 2.16178 * (-2.43787 * t).exp()
207 - 6.435e-4 * t.powf(0.14874) * (18.0323 * t.powf(-0.76830) - 7.27371).sin()
208}
209
210#[inline]
211fn chapman_enskog_thermal_conductivity(
212 temperature: Temperature,
213 molarweight: MolarWeight,
214 m: f64,
215 sigma: f64,
216 epsilon_k: f64,
217) -> ThermalConductivity {
218 let t = temperature.to_reduced();
219 0.083235 * (t * m / molarweight.convert_to(GRAM / MOL)).sqrt()
220 / sigma.powi(2)
221 / omega22(t / epsilon_k)
222 * WATT
223 / METER
224 / KELVIN
225}
226
227impl EntropyScaling for PcSaft {
228 fn viscosity_reference(
229 &self,
230 temperature: Temperature,
231 _: Volume,
232 moles: &Moles<Array1<f64>>,
233 ) -> EosResult<Viscosity> {
234 let p = &self.parameters;
235 let mw = &p.molarweight;
236 let x = (moles / moles.sum()).into_value();
237 let ce: Array1<_> = (0..self.components())
238 .map(|i| {
239 let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value();
240 5.0 / 16.0 * (mw[i] * GRAM / MOL * KB / NAV * temperature / PI).sqrt()
241 / omega22(tr)
242 / (p.sigma[i] * ANGSTROM).powi::<P2>()
243 })
244 .collect();
245 let mut ce_mix = 0.0 * MILLI * PASCAL * SECOND;
246 for i in 0..self.components() {
247 let denom: f64 = (0..self.components())
248 .map(|j| {
249 x[j] * (1.0
250 + (ce[i] / ce[j]).into_value().sqrt() * (mw[j] / mw[i]).powf(1.0 / 4.0))
251 .powi(2)
252 / (8.0 * (1.0 + mw[i] / mw[j])).sqrt()
253 })
254 .sum();
255 ce_mix += ce[i] * x[i] / denom
256 }
257 Ok(ce_mix)
258 }
259
260 fn viscosity_correlation(&self, s_res: f64, x: &Array1<f64>) -> EosResult<f64> {
261 let coefficients = self
262 .parameters
263 .viscosity
264 .as_ref()
265 .expect("Missing viscosity coefficients.");
266 let m = (x * &self.parameters.m).sum();
267 let s = s_res / m;
268 let pref = (x * &self.parameters.m) / m;
269 let a: f64 = (&coefficients.row(0) * x).sum();
270 let b: f64 = (&coefficients.row(1) * &pref).sum();
271 let c: f64 = (&coefficients.row(2) * &pref).sum();
272 let d: f64 = (&coefficients.row(3) * &pref).sum();
273 Ok(a + b * s + c * s.powi(2) + d * s.powi(3))
274 }
275
276 fn diffusion_reference(
277 &self,
278 temperature: Temperature,
279 volume: Volume,
280 moles: &Moles<Array1<f64>>,
281 ) -> EosResult<Diffusivity> {
282 if self.components() != 1 {
283 return Err(EosError::IncompatibleComponents(self.components(), 1));
284 }
285 let p = &self.parameters;
286 let density = moles.sum() / volume;
287 let res: Array1<_> = (0..self.components())
288 .map(|i| {
289 let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value();
290 3.0 / 8.0 / (p.sigma[i] * ANGSTROM).powi::<P2>() / omega11(tr) / (density * NAV)
291 * (temperature * RGAS / PI / (p.molarweight[i] * GRAM / MOL) / p.m[i]).sqrt()
292 })
293 .collect();
294 Ok(res[0])
295 }
296
297 fn diffusion_correlation(&self, s_res: f64, x: &Array1<f64>) -> EosResult<f64> {
298 if self.components() != 1 {
299 return Err(EosError::IncompatibleComponents(self.components(), 1));
300 }
301 let coefficients = self
302 .parameters
303 .diffusion
304 .as_ref()
305 .expect("Missing diffusion coefficients.");
306 let m = (x * &self.parameters.m).sum();
307 let s = s_res / m;
308 let pref = (x * &self.parameters.m).mapv(|v| v / m);
309 let a: f64 = (&coefficients.row(0) * x).sum();
310 let b: f64 = (&coefficients.row(1) * &pref).sum();
311 let c: f64 = (&coefficients.row(2) * &pref).sum();
312 let d: f64 = (&coefficients.row(3) * &pref).sum();
313 let e: f64 = (&coefficients.row(4) * &pref).sum();
314 Ok(a + b * s - c * (1.0 - s.exp()) * s.powi(2) - d * s.powi(4) - e * s.powi(8))
315 }
316
317 fn thermal_conductivity_reference(
319 &self,
320 temperature: Temperature,
321 volume: Volume,
322 moles: &Moles<Array1<f64>>,
323 ) -> EosResult<ThermalConductivity> {
324 if self.components() != 1 {
325 return Err(EosError::IncompatibleComponents(self.components(), 1));
326 }
327 let p = &self.parameters;
328 let mws = self.molar_weight();
329 let state = State::new_nvt(&Arc::new(Self::new(p.clone())), temperature, volume, moles)?;
330 let res: Array1<_> = (0..self.components())
331 .map(|i| {
332 let tr = (temperature / p.epsilon_k[i] / KELVIN).into_value();
333 let s_res_reduced = state.residual_molar_entropy().to_reduced() / p.m[i];
334 let ref_ce = chapman_enskog_thermal_conductivity(
335 temperature,
336 mws.get(i),
337 p.m[i],
338 p.sigma[i],
339 p.epsilon_k[i],
340 );
341 let alpha_visc = (-s_res_reduced / -0.5).exp();
342 let ref_ts = (-0.0167141 * tr / p.m[i] + 0.0470581 * (tr / p.m[i]).powi(2))
343 * (p.m[i] * p.m[i] * p.sigma[i].powi(3) * p.epsilon_k[i])
344 * 1e-5
345 * WATT
346 / METER
347 / KELVIN;
348 ref_ce + ref_ts * alpha_visc
349 })
350 .collect();
351 Ok(res[0])
352 }
353
354 fn thermal_conductivity_correlation(&self, s_res: f64, x: &Array1<f64>) -> EosResult<f64> {
355 if self.components() != 1 {
356 return Err(EosError::IncompatibleComponents(self.components(), 1));
357 }
358 let coefficients = self
359 .parameters
360 .thermal_conductivity
361 .as_ref()
362 .expect("Missing thermal conductivity coefficients");
363 let m = (x * &self.parameters.m).sum();
364 let s = s_res / m;
365 let pref = (x * &self.parameters.m).mapv(|v| v / m);
366 let a: f64 = (&coefficients.row(0) * x).sum();
367 let b: f64 = (&coefficients.row(1) * &pref).sum();
368 let c: f64 = (&coefficients.row(2) * &pref).sum();
369 let d: f64 = (&coefficients.row(3) * &pref).sum();
370 Ok(a + b * s + c * (1.0 - s.exp()) + d * s.powi(2))
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use crate::pcsaft::parameters::utils::{
378 butane_parameters, propane_butane_parameters, propane_parameters, water_parameters,
379 };
380 use approx::assert_relative_eq;
381 use feos_core::*;
382 use ndarray::arr1;
383 use quantity::{BAR, KELVIN, METER, MILLI, PASCAL, RGAS, SECOND};
384 use typenum::P3;
385
386 #[test]
387 fn ideal_gas_pressure() {
388 let e = Arc::new(PcSaft::new(propane_parameters()));
389 let t = 200.0 * KELVIN;
390 let v = 1e-3 * METER.powi::<P3>();
391 let n = arr1(&[1.0]) * MOL;
392 let s = State::new_nvt(&e, t, v, &n).unwrap();
393 let p_ig = s.total_moles * RGAS * t / v;
394 assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10);
395 assert_relative_eq!(
396 s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual),
397 s.pressure(Contributions::Total),
398 epsilon = 1e-10
399 );
400 }
401
402 #[test]
403 fn ideal_gas_heat_capacity_joback() {
404 let e = Arc::new(PcSaft::new(propane_parameters()));
405 let t = 200.0 * KELVIN;
406 let v = 1e-3 * METER.powi::<P3>();
407 let n = arr1(&[1.0]) * MOL;
408 let s = State::new_nvt(&e, t, v, &n).unwrap();
409 let p_ig = s.total_moles * RGAS * t / v;
410 assert_relative_eq!(s.pressure(Contributions::IdealGas), p_ig, epsilon = 1e-10);
411 assert_relative_eq!(
412 s.pressure(Contributions::IdealGas) + s.pressure(Contributions::Residual),
413 s.pressure(Contributions::Total),
414 epsilon = 1e-10
415 );
416 }
417
418 #[test]
419 fn hard_sphere() {
420 let hs = HardSphere::new(&propane_parameters());
421 let t = 250.0;
422 let v = 1000.0;
423 let n = 1.0;
424 let s = StateHD::new(t, v, arr1(&[n]));
425 let a_rust = hs.helmholtz_energy(&s);
426 assert_relative_eq!(a_rust, 0.410610492598808, epsilon = 1e-10);
427 }
428
429 #[test]
430 fn hard_sphere_mix() {
431 let c1 = HardSphere::new(&propane_parameters());
432 let c2 = HardSphere::new(&butane_parameters());
433 let c12 = HardSphere::new(&propane_butane_parameters());
434 let t = 250.0;
435 let v = 2.5e28;
436 let n = 1.0;
437 let s = StateHD::new(t, v, arr1(&[n]));
438 let a1 = c1.helmholtz_energy(&s);
439 let a2 = c2.helmholtz_energy(&s);
440 let s1m = StateHD::new(t, v, arr1(&[n, 0.0]));
441 let a1m = c12.helmholtz_energy(&s1m);
442 let s2m = StateHD::new(t, v, arr1(&[0.0, n]));
443 let a2m = c12.helmholtz_energy(&s2m);
444 assert_relative_eq!(a1, a1m, epsilon = 1e-14);
445 assert_relative_eq!(a2, a2m, epsilon = 1e-14);
446 }
447
448 #[test]
449 fn association() {
450 let parameters = Arc::new(water_parameters());
451 let assoc = Association::new(¶meters, ¶meters.association, 50, 1e-10);
452 let t = 350.0;
453 let v = 41.248289328513216;
454 let n = 1.23;
455 let s = StateHD::new(t, v, arr1(&[n]));
456 let d = parameters.hs_diameter(t);
457 let a_rust = assoc.helmholtz_energy(&s, &d) / n;
458 assert_relative_eq!(a_rust, -4.229878997054543, epsilon = 1e-10);
459 }
460
461 #[test]
462 fn cross_association() {
463 let parameters = Arc::new(water_parameters());
464 let assoc =
465 Association::new_cross_association(¶meters, ¶meters.association, 50, 1e-10);
466 let t = 350.0;
467 let v = 41.248289328513216;
468 let n = 1.23;
469 let s = StateHD::new(t, v, arr1(&[n]));
470 let d = parameters.hs_diameter(t);
471 let a_rust = assoc.helmholtz_energy(&s, &d) / n;
472 assert_relative_eq!(a_rust, -4.229878997054543, epsilon = 1e-10);
473 }
474
475 #[test]
476 fn new_tpn() {
477 let e = Arc::new(PcSaft::new(propane_parameters()));
478 let t = 300.0 * KELVIN;
479 let p = BAR;
480 let m = arr1(&[1.0]) * MOL;
481 let s = State::new_npt(&e, t, p, &m, DensityInitialization::None);
482 let p_calc = if let Ok(state) = s {
483 state.pressure(Contributions::Total)
484 } else {
485 0.0 * PASCAL
486 };
487 assert_relative_eq!(p, p_calc, epsilon = 1e-6);
488 }
489
490 #[test]
491 fn vle_pure() {
492 let e = Arc::new(PcSaft::new(propane_parameters()));
493 let t = 300.0 * KELVIN;
494 let vle = PhaseEquilibrium::pure(&e, t, None, Default::default());
495 if let Ok(v) = vle {
496 assert_relative_eq!(
497 v.vapor().pressure(Contributions::Total),
498 v.liquid().pressure(Contributions::Total),
499 epsilon = 1e-6
500 )
501 }
502 }
503
504 #[test]
505 fn critical_point() {
506 let e = Arc::new(PcSaft::new(propane_parameters()));
507 let t = 300.0 * KELVIN;
508 let cp = State::critical_point(&e, None, Some(t), Default::default());
509 if let Ok(v) = cp {
510 assert_relative_eq!(v.temperature, 375.1244078318015 * KELVIN, epsilon = 1e-8)
511 }
512 }
513
514 #[test]
515 fn mix_single() {
516 let e1 = Arc::new(PcSaft::new(propane_parameters()));
517 let e2 = Arc::new(PcSaft::new(butane_parameters()));
518 let e12 = Arc::new(PcSaft::new(propane_butane_parameters()));
519 let t = 300.0 * KELVIN;
520 let v = 0.02456883872966545 * METER.powi::<P3>();
521 let m1 = arr1(&[2.0]) * MOL;
522 let m1m = arr1(&[2.0, 0.0]) * MOL;
523 let m2m = arr1(&[0.0, 2.0]) * MOL;
524 let s1 = State::new_nvt(&e1, t, v, &m1).unwrap();
525 let s2 = State::new_nvt(&e2, t, v, &m1).unwrap();
526 let s1m = State::new_nvt(&e12, t, v, &m1m).unwrap();
527 let s2m = State::new_nvt(&e12, t, v, &m2m).unwrap();
528 assert_relative_eq!(
529 s1.pressure(Contributions::Total),
530 s1m.pressure(Contributions::Total),
531 epsilon = 1e-12
532 );
533 assert_relative_eq!(
534 s2.pressure(Contributions::Total),
535 s2m.pressure(Contributions::Total),
536 epsilon = 1e-12
537 )
538 }
539
540 #[test]
541 fn viscosity() -> EosResult<()> {
542 let e = Arc::new(PcSaft::new(propane_parameters()));
543 let t = 300.0 * KELVIN;
544 let p = BAR;
545 let n = arr1(&[1.0]) * MOL;
546 let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap();
547 assert_relative_eq!(
548 s.viscosity()?,
549 0.00797 * MILLI * PASCAL * SECOND,
550 epsilon = 1e-5
551 );
552 assert_relative_eq!(
553 s.ln_viscosity_reduced()?,
554 (s.viscosity()? / e.viscosity_reference(s.temperature, s.volume, &s.moles)?)
555 .into_value()
556 .ln(),
557 epsilon = 1e-15
558 );
559 Ok(())
560 }
561
562 #[test]
563 fn diffusion() -> EosResult<()> {
564 let e = Arc::new(PcSaft::new(propane_parameters()));
565 let t = 300.0 * KELVIN;
566 let p = BAR;
567 let n = arr1(&[1.0]) * MOL;
568 let s = State::new_npt(&e, t, p, &n, DensityInitialization::None).unwrap();
569 assert_relative_eq!(
570 s.diffusion()?,
571 0.01505 * (CENTI * METER).powi::<P2>() / SECOND,
572 epsilon = 1e-5
573 );
574 assert_relative_eq!(
575 s.ln_diffusion_reduced()?,
576 (s.diffusion()? / e.diffusion_reference(s.temperature, s.volume, &s.moles)?)
577 .into_value()
578 .ln(),
579 epsilon = 1e-15
580 );
581 Ok(())
582 }
583}