1use super::association::N0_CUTOFF;
2use super::polar::{pair_integral_ij, triplet_integral_ijk};
3use crate::eos::association::{assoc_site_frac_a, assoc_site_frac_ab};
4use crate::eos::dispersion::{A0, A1, A2, B0, B1, B2};
5use crate::eos::polar::{AD, AQ, BD, BQ, CD, CQ, PI_SQ_43};
6use crate::parameters::PcSaftParameters;
7use feos_core::{EosError, EosResult};
8use feos_dft::fundamental_measure_theory::FMTVersion;
9use feos_dft::{
10 FunctionalContributionDual, WeightFunction, WeightFunctionInfo, WeightFunctionShape,
11};
12use ndarray::*;
13use num_dual::*;
14use std::f64::consts::{FRAC_PI_6, PI};
15use std::fmt;
16use std::rc::Rc;
17
18const PI36M1: f64 = 1.0 / (36.0 * PI);
19const N3_CUTOFF: f64 = 1e-5;
20
21#[derive(Clone)]
22pub struct PureFMTAssocFunctional {
23 parameters: Rc<PcSaftParameters>,
24 version: FMTVersion,
25}
26
27impl PureFMTAssocFunctional {
28 pub fn new(parameters: Rc<PcSaftParameters>, version: FMTVersion) -> Self {
29 Self {
30 parameters,
31 version,
32 }
33 }
34}
35
36impl<N: DualNum<f64> + ScalarOperand> FunctionalContributionDual<N> for PureFMTAssocFunctional {
37 fn weight_functions(&self, temperature: N) -> WeightFunctionInfo<N> {
38 let r = self.parameters.hs_diameter(temperature) * 0.5;
39 WeightFunctionInfo::new(arr1(&[0]), false).extend(
40 vec![
41 WeightFunctionShape::Delta,
42 WeightFunctionShape::Theta,
43 WeightFunctionShape::DeltaVec,
44 ]
45 .into_iter()
46 .map(|s| WeightFunction {
47 prefactor: self.parameters.m.mapv(|m| m.into()),
48 kernel_radius: r.clone(),
49 shape: s,
50 })
51 .collect(),
52 false,
53 )
54 }
55
56 fn calculate_helmholtz_energy_density(
57 &self,
58 temperature: N,
59 weighted_densities: ArrayView2<N>,
60 ) -> EosResult<Array1<N>> {
61 let p = &self.parameters;
62
63 let n2 = weighted_densities.index_axis(Axis(0), 0);
65 let n3 = weighted_densities.index_axis(Axis(0), 1);
66 let n2v = weighted_densities.slice_axis(Axis(0), Slice::new(2, None, 1));
67
68 let r = self.parameters.hs_diameter(temperature)[0] * 0.5;
70
71 if n3.iter().any(|n3| n3.re() > 1.0) {
73 return Err(EosError::IterationFailed(String::from(
74 "PureFMTAssocFunctional",
75 )));
76 }
77 let ln31 = n3.mapv(|n3| (-n3).ln_1p());
78 let n3rec = n3.mapv(|n3| n3.recip());
79 let n3m1 = n3.mapv(|n3| -n3 + 1.0);
80 let n3m1rec = n3m1.mapv(|n3m1| n3m1.recip());
81 let n1 = n2.mapv(|n2| n2 / (r * 4.0 * PI));
82 let n0 = n2.mapv(|n2| n2 / (r * r * 4.0 * PI));
83 let n1v = n2v.mapv(|n2v| n2v / (r * 4.0 * PI));
84
85 let (n1n2, n2n2) = match self.version {
86 FMTVersion::WhiteBear => (
87 &n1 * &n2 - (&n1v * &n2v).sum_axis(Axis(0)),
88 &n2 * &n2 - (&n2v * &n2v).sum_axis(Axis(0)) * 3.0,
89 ),
90 FMTVersion::AntiSymWhiteBear => {
91 let mut xi2 = (&n2v * &n2v).sum_axis(Axis(0)) / n2.map(|n| n.powi(2));
92 xi2.iter_mut().for_each(|x| {
93 if x.re() > 1.0 {
94 *x = N::one()
95 }
96 });
97 (
98 &n1 * &n2 - (&n1v * &n2v).sum_axis(Axis(0)),
99 &n2 * &n2 * xi2.mapv(|x| (-x + 1.0).powi(3)),
100 )
101 }
102 _ => unreachable!(),
103 };
104
105 let mut f3 = (&n3m1 * &n3m1 * &ln31 + n3) * &n3rec * n3rec * &n3m1rec * &n3m1rec;
107 f3.iter_mut().zip(n3).for_each(|(f3, &n3)| {
108 if n3.re() < N3_CUTOFF {
109 *f3 = (((n3 * 35.0 / 6.0 + 4.8) * n3 + 3.75) * n3 + 8.0 / 3.0) * n3 + 1.5;
110 }
111 });
112 let mut phi = -(&n0 * &ln31) + n1n2 * &n3m1rec + n2n2 * n2 * PI36M1 * f3;
113
114 if p.nassoc == 1 {
116 let mut xi = -(&n2v * &n2v).sum_axis(Axis(0)) / (&n2 * &n2) + 1.0;
117 xi.iter_mut().zip(&n2).for_each(|(xi, &n2)| {
118 if n2.re() < N0_CUTOFF * 4.0 * PI * p.m[0] * r.re().powi(2) {
119 *xi = N::one();
120 }
121 });
122
123 let k = &n2 * &n3m1rec * r;
124 let deltarho = (((&k / 18.0 + 0.5) * &k * &xi + 1.0) * n3m1rec)
125 * ((temperature.recip() * p.epsilon_k_aibj[(0, 0)]).exp_m1()
126 * (p.sigma[0].powi(3) * p.kappa_aibj[(0, 0)]))
127 * (&n0 / p.m[0] * &xi);
128
129 let f = |x: N| x.ln() - x * 0.5 + 0.5;
130 phi = phi
131 + if p.nb[0] > 0.0 {
132 let xa = deltarho.mapv(|d| assoc_site_frac_ab(d, p.na[0], p.nb[0]));
133 let xb = (xa.clone() - 1.0) * p.na[0] / p.nb[0] + 1.0;
134 (n0 / p.m[0] * xi) * (xa.mapv(f) * p.na[0] + xb.mapv(f) * p.nb[0])
135 } else {
136 let xa = deltarho.mapv(|d| assoc_site_frac_a(d, p.na[0]));
137 n0 / p.m[0] * xi * (xa.mapv(f) * p.na[0])
138 };
139 }
140
141 Ok(phi)
142 }
143}
144
145impl fmt::Display for PureFMTAssocFunctional {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 write!(f, "Pure FMT+association")
148 }
149}
150
151#[derive(Clone)]
152pub struct PureChainFunctional {
153 parameters: Rc<PcSaftParameters>,
154}
155
156impl PureChainFunctional {
157 pub fn new(parameters: Rc<PcSaftParameters>) -> Self {
158 Self { parameters }
159 }
160}
161
162impl<N: DualNum<f64> + ScalarOperand> FunctionalContributionDual<N> for PureChainFunctional {
163 fn weight_functions(&self, temperature: N) -> WeightFunctionInfo<N> {
164 let d = self.parameters.hs_diameter(temperature);
165 WeightFunctionInfo::new(arr1(&[0]), true)
166 .add(
167 WeightFunction::new_scaled(d.clone(), WeightFunctionShape::Delta),
168 false,
169 )
170 .add(
171 WeightFunction {
172 prefactor: (&self.parameters.m / 8.0).mapv(|x| x.into()),
173 kernel_radius: d,
174 shape: WeightFunctionShape::Theta,
175 },
176 false,
177 )
178 }
179
180 fn calculate_helmholtz_energy_density(
181 &self,
182 _: N,
183 weighted_densities: ArrayView2<N>,
184 ) -> EosResult<Array1<N>> {
185 let rho = weighted_densities.index_axis(Axis(0), 0);
186 let lambda = weighted_densities
188 .index_axis(Axis(0), 1)
189 .map(|&l| if l.re() < 0.0 { -l } else { l } + N::from(f64::EPSILON));
190 let eta = weighted_densities.index_axis(Axis(0), 2);
191
192 let y = eta.mapv(|eta| (eta * 0.5 - 1.0) / (eta - 1.0).powi(3));
193 Ok(-(y * lambda).mapv(|x| (x.ln() - 1.0) * (self.parameters.m[0] - 1.0)) * rho)
194 }
195}
196
197impl fmt::Display for PureChainFunctional {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 write!(f, "Pure chain")
200 }
201}
202
203#[derive(Clone)]
204pub struct PureAttFunctional {
205 parameters: Rc<PcSaftParameters>,
206}
207
208impl PureAttFunctional {
209 pub fn new(parameters: Rc<PcSaftParameters>) -> Self {
210 Self { parameters }
211 }
212}
213
214impl<N: DualNum<f64> + ScalarOperand> FunctionalContributionDual<N> for PureAttFunctional {
215 fn weight_functions(&self, temperature: N) -> WeightFunctionInfo<N> {
216 let d = self.parameters.hs_diameter(temperature);
217 const PSI: f64 = 1.3862; WeightFunctionInfo::new(arr1(&[0]), false).add(
219 WeightFunction::new_scaled(d * PSI, WeightFunctionShape::Theta),
220 false,
221 )
222 }
223
224 fn weight_functions_pdgt(&self, temperature: N) -> WeightFunctionInfo<N> {
225 let d = self.parameters.hs_diameter(temperature);
226 const PSI: f64 = 1.3286; WeightFunctionInfo::new(arr1(&[0]), false).add(
228 WeightFunction::new_scaled(d * PSI, WeightFunctionShape::Theta),
229 false,
230 )
231 }
232
233 fn calculate_helmholtz_energy_density(
234 &self,
235 temperature: N,
236 weighted_densities: ArrayView2<N>,
237 ) -> EosResult<Array1<N>> {
238 let p = &self.parameters;
239 let rho = weighted_densities.index_axis(Axis(0), 0);
240
241 let d = p.hs_diameter(temperature)[0];
243
244 let eta = rho.mapv(|rho| rho * FRAC_PI_6 * p.m[0] * d.powi(3));
245 let m1 = (p.m[0] - 1.0) / p.m[0];
246 let m2 = m1 * (p.m[0] - 2.0) / p.m[0];
247 let e = temperature.recip() * p.epsilon_k[0];
248 let s3 = p.sigma[0].powi(3);
249
250 let mut i1: Array1<N> = Array::zeros(eta.raw_dim());
252 let mut i2: Array1<N> = Array::zeros(eta.raw_dim());
253 for i in 0..=6 {
254 i1 = i1 + eta.mapv(|eta| eta.powi(i as i32) * (A0[i] + m1 * A1[i] + m2 * A2[i]));
255 i2 = i2 + eta.mapv(|eta| eta.powi(i as i32) * (B0[i] + m1 * B1[i] + m2 * B2[i]));
256 }
257 let c1 = eta.mapv(|eta| {
258 ((eta * 8.0 - eta.powi(2) * 2.0) / (eta - 1.0).powi(4) * p.m[0]
259 + (eta * 20.0 - eta.powi(2) * 27.0 + eta.powi(3) * 12.0 - eta.powi(4) * 2.0)
260 / ((eta - 1.0) * (eta - 2.0)).powi(2)
261 * (1.0 - p.m[0])
262 + 1.0)
263 .recip()
264 });
265 let mut phi = rho.mapv(|rho| -(rho * p.m[0]).powi(2) * e * s3 * PI)
266 * (i1 * 2.0 + c1 * i2.mapv(|i2| i2 * p.m[0] * e));
267
268 if p.ndipole > 0 {
270 let mu2_term = e * s3 * p.mu2[0];
271 let m = p.m[0].min(2.0);
272 let m1 = (m - 1.0) / m;
273 let m2 = m1 * (m - 2.0) / m;
274
275 let phi2 = -(&rho * &rho)
276 * pair_integral_ij(m1, m2, &eta, &AD, &BD, e)
277 * (mu2_term * mu2_term / s3 * PI);
278 let phi3 = -(&rho * &rho * rho)
279 * triplet_integral_ijk(m1, m2, &eta, &CD)
280 * (mu2_term * mu2_term * mu2_term / s3 * PI_SQ_43);
281
282 let mut phi_d = &phi2 * &phi2 / (&phi2 - &phi3);
283 phi_d.iter_mut().zip(phi2.iter()).for_each(|(p, &p2)| {
284 if p.re().is_nan() {
285 *p = p2;
286 }
287 });
288 phi += &phi_d;
289 }
290
291 if p.nquadpole > 0 {
293 let q2_term = e * p.sigma[0].powi(5) * p.q2[0];
294 let m = p.m[0].min(2.0);
295 let m1 = (m - 1.0) / m;
296 let m2 = m1 * (m - 2.0) / m;
297
298 let phi2 = -(&rho * &rho)
299 * pair_integral_ij(m1, m2, &eta, &AQ, &BQ, e)
300 * (q2_term * q2_term / p.sigma[0].powi(7) * PI * 0.5625);
301 let phi3 = (&rho * &rho * rho)
302 * triplet_integral_ijk(m1, m2, &eta, &CQ)
303 * (q2_term * q2_term * q2_term / s3.powi(3) * PI * PI * 0.5625);
304
305 let mut phi_q = &phi2 * &phi2 / (&phi2 - &phi3);
306 phi_q.iter_mut().zip(phi2.iter()).for_each(|(p, &p2)| {
307 if p.re().is_nan() {
308 *p = p2;
309 }
310 });
311 phi += &phi_q;
312 }
313
314 Ok(phi)
315 }
316}
317
318impl fmt::Display for PureAttFunctional {
319 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320 write!(f, "Pure attractive")
321 }
322}