1#![allow(type_alias_bounds)]
2use super::DFTProfile;
3use crate::convolver::{BulkConvolver, Convolver};
4use crate::functional_contribution::FunctionalContribution;
5use crate::{ConvolverFFT, DFTSolverLog, HelmholtzEnergyFunctional, WeightFunctionInfo};
6use feos_core::{Contributions, FeosResult, ReferenceSystem, Total, Verbosity};
7use nalgebra::{DMatrix, DVector};
8use ndarray::{Array, Array1, Axis, Dimension, RemoveAxis};
9use num_dual::{Dual64, DualNum};
10use quantity::{
11 Density, Energy, Entropy, EntropyDensity, MolarEnergy, Moles, Pressure, Quantity, Temperature,
12};
13use std::ops::{AddAssign, Div};
14use std::sync::Arc;
15
16type DrhoDmu<D: Dimension> =
17 <Density<Array<f64, <D::Larger as Dimension>::Larger>> as Div<MolarEnergy>>::Output;
18type DnDmu = <Moles<DMatrix<f64>> as Div<MolarEnergy>>::Output;
19type DrhoDp<D: Dimension> = <Density<Array<f64, D::Larger>> as Div<Pressure>>::Output;
20type DnDp = <Moles<DVector<f64>> as Div<Pressure>>::Output;
21type DrhoDT<D: Dimension> = <Density<Array<f64, D::Larger>> as Div<Temperature>>::Output;
22type DnDT = <Moles<DVector<f64>> as Div<Temperature>>::Output;
23
24impl<D: Dimension, F: HelmholtzEnergyFunctional> DFTProfile<D, F>
25where
26 D::Larger: Dimension<Smaller = D>,
27{
28 pub fn grand_potential_density(&self) -> FeosResult<Pressure<Array<f64, D>>> {
30 let t = self.temperature.to_reduced();
32 let rho = self.density.to_reduced();
33 let (mut f, dfdrho) =
34 self.bulk
35 .eos
36 .functional_derivative(t, &rho, self.convolver.as_ref())?;
37
38 for ((rho, dfdrho), &m) in rho
40 .outer_iter()
41 .zip(dfdrho.outer_iter())
42 .zip(self.bulk.eos.m().iter())
43 {
44 f -= &((&dfdrho + m) * &rho);
45 }
46
47 let bond_lengths = self.bulk.eos.bond_lengths(t);
48 for segment in bond_lengths.node_indices() {
49 let n = bond_lengths.neighbors(segment).count();
50 f += &(&rho.index_axis(Axis(0), segment.index()) * (0.5 * n as f64));
51 }
52
53 Ok(Pressure::from_reduced(f * t))
54 }
55
56 pub fn grand_potential(&self) -> FeosResult<Energy> {
58 Ok(self.integrate(&self.grand_potential_density()?))
59 }
60
61 pub fn functional_derivative(&self) -> FeosResult<Array<f64, D::Larger>> {
63 let (_, dfdrho) = self.bulk.eos.functional_derivative(
64 self.temperature.to_reduced(),
65 &self.density.to_reduced(),
66 self.convolver.as_ref(),
67 )?;
68 Ok(dfdrho)
69 }
70}
71
72impl<D: Dimension + RemoveAxis + 'static, F: HelmholtzEnergyFunctional> DFTProfile<D, F>
73where
74 D::Larger: Dimension<Smaller = D>,
75 D::Smaller: Dimension<Larger = D>,
76 <D::Larger as Dimension>::Larger: Dimension<Smaller = D::Larger>,
77{
78 fn intrinsic_helmholtz_energy_density<N>(
79 &self,
80 temperature: N,
81 density: &Array<f64, D::Larger>,
82 convolver: &dyn Convolver<N, D>,
83 ) -> FeosResult<Array<N, D>>
84 where
85 N: DualNum<f64> + Copy,
86 {
87 let density_dual = density.mapv(N::from);
88 let weighted_densities = convolver.weighted_densities(&density_dual);
89 let functional_contributions = self.bulk.eos.contributions();
90 let mut helmholtz_energy_density: Array<N, D> = self
91 .bulk
92 .eos
93 .ideal_chain_contribution()
94 .helmholtz_energy_density(&density.mapv(N::from))?;
95 for (c, wd) in functional_contributions.into_iter().zip(weighted_densities) {
96 let nwd = wd.shape()[0];
97 let ngrid = wd.len() / nwd;
98 helmholtz_energy_density
99 .view_mut()
100 .into_shape_with_order(ngrid)
101 .unwrap()
102 .add_assign(&c.helmholtz_energy_density(
103 temperature,
104 wd.into_shape_with_order((nwd, ngrid)).unwrap().view(),
105 )?);
106 }
107 Ok(helmholtz_energy_density.mapv(|a| a * temperature))
108 }
109
110 pub fn residual_entropy_density(&self) -> FeosResult<EntropyDensity<Array<f64, D>>> {
114 let temperature = self.temperature.to_reduced();
116 let temperature_dual = Dual64::from(temperature).derivative();
117 let functional_contributions = self.bulk.eos.contributions();
118 let weight_functions: Vec<WeightFunctionInfo<Dual64>> = functional_contributions
119 .into_iter()
120 .map(|c| c.weight_functions(temperature_dual))
121 .collect();
122 let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, self.lanczos);
123
124 let density = self.density.to_reduced();
125
126 let helmholtz_energy_density = self.intrinsic_helmholtz_energy_density(
127 temperature_dual,
128 &density,
129 convolver.as_ref(),
130 )?;
131 Ok(EntropyDensity::from_reduced(
132 helmholtz_energy_density.mapv(|f| -f.eps),
133 ))
134 }
135
136 pub fn entropy_density_contributions(
140 &self,
141 temperature: f64,
142 density: &Array<f64, D::Larger>,
143 convolver: &dyn Convolver<Dual64, D>,
144 ) -> FeosResult<Vec<Array<f64, D>>> {
145 let density_dual = density.mapv(Dual64::from);
146 let temperature_dual = Dual64::from(temperature).derivative();
147 let weighted_densities = convolver.weighted_densities(&density_dual);
148 let functional_contributions = self.bulk.eos.contributions();
149 let mut helmholtz_energy_density: Vec<Array<Dual64, _>> = Vec::new();
150 helmholtz_energy_density.push(
151 self.bulk
152 .eos
153 .ideal_chain_contribution()
154 .helmholtz_energy_density(&density.mapv(Dual64::from))?,
155 );
156
157 for (c, wd) in functional_contributions.into_iter().zip(weighted_densities) {
158 let nwd = wd.shape()[0];
159 let ngrid = wd.len() / nwd;
160 helmholtz_energy_density.push(
161 c.helmholtz_energy_density(
162 temperature_dual,
163 wd.into_shape_with_order((nwd, ngrid)).unwrap().view(),
164 )?
165 .into_shape_with_order(density.raw_dim().remove_axis(Axis(0)))
166 .unwrap(),
167 );
168 }
169 Ok(helmholtz_energy_density
170 .iter()
171 .map(|v| v.mapv(|f| -(f * temperature_dual).eps))
172 .collect())
173 }
174}
175
176impl<D: Dimension + RemoveAxis + 'static, F: HelmholtzEnergyFunctional + Total> DFTProfile<D, F>
177where
178 D::Larger: Dimension<Smaller = D>,
179 D::Smaller: Dimension<Larger = D>,
180 <D::Larger as Dimension>::Larger: Dimension<Smaller = D::Larger>,
181{
182 fn ideal_gas_contribution_dual(
183 &self,
184 temperature: Dual64,
185 density: &Array<f64, D::Larger>,
186 ) -> Array<Dual64, D> {
187 let lambda = self.bulk.eos.ln_lambda3(temperature);
188 let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0)));
189 for (i, rhoi) in density.outer_iter().enumerate() {
190 phi += &rhoi.mapv(|rhoi| (lambda[i] + rhoi.ln() - 1.0) * rhoi);
191 }
192 phi.map(|p| p * temperature)
193 }
194
195 pub fn entropy_density(
199 &self,
200 contributions: Contributions,
201 ) -> FeosResult<EntropyDensity<Array<f64, D>>> {
202 let temperature = self.temperature.to_reduced();
204 let temperature_dual = Dual64::from(temperature).derivative();
205 let functional_contributions = self.bulk.eos.contributions();
206 let weight_functions: Vec<WeightFunctionInfo<Dual64>> = functional_contributions
207 .into_iter()
208 .map(|c| c.weight_functions(temperature_dual))
209 .collect();
210 let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, self.lanczos);
211
212 let density = self.density.to_reduced();
213
214 let mut helmholtz_energy_density = self.intrinsic_helmholtz_energy_density(
215 temperature_dual,
216 &density,
217 convolver.as_ref(),
218 )?;
219 match contributions {
220 Contributions::Total => {
221 helmholtz_energy_density +=
222 &self.ideal_gas_contribution_dual(temperature_dual, &density);
223 }
224 Contributions::IdealGas => panic!(
225 "Entropy density can only be calculated for Contributions::Residual or Contributions::Total"
226 ),
227 Contributions::Residual => (),
228 }
229 Ok(EntropyDensity::from_reduced(
230 helmholtz_energy_density.mapv(|f| -f.eps),
231 ))
232 }
233
234 pub fn entropy(&self, contributions: Contributions) -> FeosResult<Entropy> {
238 Ok(self.integrate(&self.entropy_density(contributions)?))
239 }
240
241 pub fn internal_energy_density(
245 &self,
246 contributions: Contributions,
247 ) -> FeosResult<Pressure<Array<f64, D>>>
248 where
249 D: Dimension,
250 D::Larger: Dimension<Smaller = D>,
251 {
252 let temperature = self.temperature.to_reduced();
254 let temperature_dual = Dual64::from(temperature).derivative();
255 let functional_contributions = self.bulk.eos.contributions();
256 let weight_functions: Vec<WeightFunctionInfo<Dual64>> = functional_contributions
257 .into_iter()
258 .map(|c| c.weight_functions(temperature_dual))
259 .collect();
260 let convolver = ConvolverFFT::plan(&self.grid, &weight_functions, self.lanczos);
261
262 let density = self.density.to_reduced();
263
264 let mut helmholtz_energy_density_dual = self.intrinsic_helmholtz_energy_density(
265 temperature_dual,
266 &density,
267 convolver.as_ref(),
268 )?;
269 match contributions {
270 Contributions::Total => {
271 helmholtz_energy_density_dual +=
272 &self.ideal_gas_contribution_dual(temperature_dual, &density);
273 }
274 Contributions::IdealGas => panic!(
275 "Internal energy density can only be calculated for Contributions::Residual or Contributions::Total"
276 ),
277 Contributions::Residual => (),
278 }
279 let helmholtz_energy_density = helmholtz_energy_density_dual
280 .mapv(|f| f.re - f.eps * temperature)
281 + (&self.external_potential * density).sum_axis(Axis(0)) * temperature;
282 Ok(Pressure::from_reduced(helmholtz_energy_density))
283 }
284
285 pub fn internal_energy(&self, contributions: Contributions) -> FeosResult<Energy> {
289 Ok(self.integrate(&self.internal_energy_density(contributions)?))
290 }
291}
292
293impl<D: Dimension + RemoveAxis + 'static, F: HelmholtzEnergyFunctional> DFTProfile<D, F>
294where
295 D::Larger: Dimension<Smaller = D>,
296 D::Smaller: Dimension<Larger = D>,
297 <D::Larger as Dimension>::Larger: Dimension<Smaller = D::Larger>,
298{
299 fn density_derivative(&self, lhs: &Array<f64, D::Larger>) -> FeosResult<Array<f64, D::Larger>> {
300 let rho = self.density.to_reduced();
301 let partial_density = self.bulk.partial_density.to_reduced();
302 let rho_bulk = self
303 .bulk
304 .eos
305 .component_index()
306 .iter()
307 .map(|&i| partial_density[i])
308 .collect();
309
310 let second_partial_derivatives = self.second_partial_derivatives(&rho)?;
311 let (_, _, _, exp_dfdrho, _) = self.euler_lagrange_equation(&rho, &rho_bulk, false)?;
312
313 let rhs = |x: &_| {
314 let delta_functional_derivative =
315 self.delta_functional_derivative(x, &second_partial_derivatives);
316 let mut xm = x.clone();
317 xm.outer_iter_mut()
318 .zip(self.bulk.eos.m().iter())
319 .for_each(|(mut x, &m)| x *= m);
320 let delta_i = self.delta_bond_integrals(&exp_dfdrho, &delta_functional_derivative);
321 xm + (delta_functional_derivative - delta_i) * &rho
322 };
323 let mut log = DFTSolverLog::new(Verbosity::None);
324 Self::gmres(rhs, lhs, 200, 1e-13, &mut log)
325 }
326
327 pub fn drho_dmu(&self) -> FeosResult<DrhoDmu<D>> {
329 let shape: Vec<_> = std::iter::once(&self.bulk.eos.components())
330 .chain(self.density.shape())
331 .copied()
332 .collect();
333 let mut drho_dmu = Array::zeros(shape).into_dimensionality().unwrap();
334 let component_index = self.bulk.eos.component_index();
335 for (k, mut d) in drho_dmu.outer_iter_mut().enumerate() {
336 let mut lhs = self.density.to_reduced();
337 for (i, mut l) in lhs.outer_iter_mut().enumerate() {
338 if component_index[i] != k {
339 l.fill(0.0);
340 }
341 }
342 d.assign(&self.density_derivative(&lhs)?);
343 }
344 Ok(Quantity::from_reduced(
345 drho_dmu / self.temperature.to_reduced(),
346 ))
347 }
348
349 pub fn dn_dmu(&self) -> FeosResult<DnDmu> {
351 let drho_dmu = self.drho_dmu()?.into_reduced();
352 let n = drho_dmu.shape()[0];
353 let mut dn_dmu = DMatrix::zeros(n, n);
354 dn_dmu
355 .column_iter_mut()
356 .zip(drho_dmu.outer_iter())
357 .for_each(|(mut dn, drho)| dn.add_assign(&self.integrate_reduced_segments(&drho)));
358 Ok(DnDmu::from_reduced(dn_dmu))
359 }
360
361 pub fn drho_dp(&self) -> FeosResult<DrhoDp<D>> {
363 let mut lhs = self.density.to_reduced();
364 let v = self.bulk.partial_molar_volume().to_reduced();
365 for (mut l, &c) in lhs
366 .outer_iter_mut()
367 .zip(self.bulk.eos.component_index().iter())
368 {
369 l *= v[c];
370 }
371 Ok(Quantity::from_reduced(
372 self.density_derivative(&lhs)? / self.temperature.to_reduced(),
373 ))
374 }
375
376 pub fn dn_dp(&self) -> FeosResult<DnDp> {
378 Ok(self.integrate_segments(&self.drho_dp()?))
379 }
380
381 pub fn drho_dt(&self) -> FeosResult<DrhoDT<D>> {
383 let rho = self.density.to_reduced();
384 let t = self.temperature.to_reduced();
385 let rho_dual = rho.mapv(Dual64::from);
386 let t_dual = Dual64::from(t).derivative();
387
388 let functional_contributions = self.bulk.eos.contributions();
390 let weight_functions: Vec<WeightFunctionInfo<Dual64>> = functional_contributions
391 .into_iter()
392 .map(|c| c.weight_functions(t_dual))
393 .collect();
394 let convolver: Arc<dyn Convolver<_, D>> =
395 ConvolverFFT::plan(&self.grid, &weight_functions, self.lanczos);
396 let (_, mut dfdrho) =
397 self.bulk
398 .eos
399 .functional_derivative(t_dual, &rho_dual, convolver.as_ref())?;
400
401 dfdrho += &(&self.external_potential * t).mapv(|v| Dual64::from(v) / t_dual);
403
404 let partial_density = self.bulk.partial_density.to_reduced();
406 let rho_bulk: Array1<_> = self
407 .bulk
408 .eos
409 .component_index()
410 .iter()
411 .map(|&i| partial_density[i])
412 .collect();
413 let rho_bulk_dual = rho_bulk.mapv(Dual64::from);
414 let bulk_convolver = BulkConvolver::new(weight_functions);
415 let (_, dfdrho_bulk) =
416 self.bulk
417 .eos
418 .functional_derivative(t_dual, &rho_bulk_dual, bulk_convolver.as_ref())?;
419 dfdrho
420 .outer_iter_mut()
421 .zip(dfdrho_bulk)
422 .zip(self.bulk.eos.m().iter())
423 .for_each(|((mut df, df_b), &m)| {
424 df.map_inplace(|df| *df = (*df - df_b) / Dual64::from(m));
425 });
426
427 let exp_dfdrho = dfdrho.mapv(|x| (-x).exp());
429 let bonds = self
430 .bulk
431 .eos
432 .bond_integrals(t_dual, &exp_dfdrho, convolver.as_ref());
433
434 let mut lhs = (exp_dfdrho * bonds).mapv(|x| (-x.ln() * t_dual).eps);
436 let x =
437 (self.bulk.partial_molar_volume() * self.bulk.dp_dt(Contributions::Total)).to_reduced();
438 let x: Array1<_> = self
439 .bulk
440 .eos
441 .component_index()
442 .iter()
443 .map(|&i| x[i])
444 .collect();
445 lhs.outer_iter_mut()
446 .zip(rho.outer_iter())
447 .zip(rho_bulk)
448 .zip(self.bulk.eos.m().iter())
449 .zip(x)
450 .for_each(|((((mut lhs, rho), rho_b), &m), x)| {
451 lhs += &(&rho / rho_b).mapv(f64::ln);
452 lhs *= m;
453 lhs += x;
454 });
455
456 lhs *= &(-&rho / t);
457 lhs.iter_mut().for_each(|l| {
458 if !l.is_finite() {
459 *l = 0.0
460 }
461 });
462 Ok(Quantity::from_reduced(self.density_derivative(&lhs)?))
463 }
464
465 pub fn dn_dt(&self) -> FeosResult<DnDT> {
467 Ok(self.integrate_segments(&self.drho_dt()?))
468 }
469}