feos_dft/profile/
properties.rs

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    /// Calculate the grand potential density $\omega$.
29    pub fn grand_potential_density(&self) -> FeosResult<Pressure<Array<f64, D>>> {
30        // Calculate residual Helmholtz energy density and functional derivative
31        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        // Calculate the grand potential density
39        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    /// Calculate the grand potential $\Omega$.
57    pub fn grand_potential(&self) -> FeosResult<Energy> {
58        Ok(self.integrate(&self.grand_potential_density()?))
59    }
60
61    /// Calculate the (residual) intrinsic functional derivative $\frac{\delta\mathcal{F}}{\delta\rho_i(\mathbf{r})}$.
62    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    /// Calculate the residual entropy density $s^\mathrm{res}(\mathbf{r})$.
111    ///
112    /// Untested with heterosegmented functionals.
113    pub fn residual_entropy_density(&self) -> FeosResult<EntropyDensity<Array<f64, D>>> {
114        // initialize convolver
115        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    /// Calculate the individual contributions to the entropy density.
137    ///
138    /// Untested with heterosegmented functionals.
139    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    /// Calculate the entropy density $s(\mathbf{r})$.
196    ///
197    /// Untested with heterosegmented functionals.
198    pub fn entropy_density(
199        &self,
200        contributions: Contributions,
201    ) -> FeosResult<EntropyDensity<Array<f64, D>>> {
202        // initialize convolver
203        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    /// Calculate the entropy $S$.
235    ///
236    /// Untested with heterosegmented functionals.
237    pub fn entropy(&self, contributions: Contributions) -> FeosResult<Entropy> {
238        Ok(self.integrate(&self.entropy_density(contributions)?))
239    }
240
241    /// Calculate the internal energy density $u(\mathbf{r})$.
242    ///
243    /// Untested with heterosegmented functionals.
244    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        // initialize convolver
253        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    /// Calculate the internal energy $U$.
286    ///
287    /// Untested with heterosegmented functionals.
288    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    /// Return the partial derivatives of the density profiles w.r.t. the chemical potentials $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial\mu_k}\right)_T$
328    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    /// Return the partial derivatives of the number of moles w.r.t. the chemical potentials $\left(\frac{\partial N_i}{\partial\mu_k}\right)_T$
350    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    /// Return the partial derivatives of the density profiles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial p}\right)_{T,\mathbf{x}}$
362    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    /// Return the partial derivatives of the number of moles w.r.t. the bulk pressure at constant temperature and bulk composition $\left(\frac{\partial N_i}{\partial p}\right)_{T,\mathbf{x}}$
377    pub fn dn_dp(&self) -> FeosResult<DnDp> {
378        Ok(self.integrate_segments(&self.drho_dp()?))
379    }
380
381    /// Return the partial derivatives of the density profiles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial\rho_i(\mathbf{r})}{\partial T}\right)_{p,\mathbf{x}}$
382    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        // calculate intrinsic functional derivative
389        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        // calculate total functional derivative
402        dfdrho += &(&self.external_potential * t).mapv(|v| Dual64::from(v) / t_dual);
403
404        // calculate bulk functional derivative
405        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        // calculate bond integrals
428        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        // solve for drho_dt
435        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    /// Return the partial derivatives of the number of moles w.r.t. the temperature at constant bulk pressure and composition $\left(\frac{\partial N_i}{\partial T}\right)_{p,\mathbf{x}}$
466    pub fn dn_dt(&self) -> FeosResult<DnDT> {
467        Ok(self.integrate_segments(&self.drho_dt()?))
468    }
469}