Skip to content

Commit 329770b

Browse files
authored
Rewrite and document core DFT functionality (feos-org#60)
* Rewrite and document core DFT functionality * remove print and unnecessary ifs * make specification optional for planar interfaces * include DFT guide in the documentation * include DFT theory in documentation * add section on functional derivatives * use Arc in BulkConvolver * udpate theory guide toc * remove stub * Add changelog entry * incorporate comments from review
1 parent e343948 commit 329770b

File tree

19 files changed

+390
-145
lines changed

19 files changed

+390
-145
lines changed

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
myst_enable_extensions = [
3131
"colon_fence",
3232
"deflist",
33+
"dollarmath"
3334
]
3435
myst_heading_anchors = 3
3536

docs/index.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ help_and_feedback
120120
:caption: Python
121121
:hidden:
122122
123-
api/index
124123
tutorials/index
125124
recipes/index
125+
api/index
126126
```
127127

128128
```{toctree}
@@ -131,4 +131,13 @@ recipes/index
131131
132132
rustguide/index
133133
rust_api
134+
```
135+
136+
```{toctree}
137+
:caption: Theory
138+
:hidden:
139+
140+
theory/eos/index
141+
theory/dft/index
142+
theory/models/index
134143
```
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
## Euler-Lagrange equation
2+
The fundamental expression in classical density functional theory is the relation between the grand potential $\Omega$ and the intrinsic Helmholtz energy $F$.
3+
4+
$$
5+
\Omega(T,\mu,[\rho(r)])=F(T,[\rho(r)])-\sum_i\int\rho_i(r)\left(\mu_i-V_i^\mathrm{ext}(r)\right)\mathrm{d}r
6+
$$
7+
8+
What makes this expression so appealing is that the intrinsic Helmholtz energy does only depend on the temperature $T$ and the density profiles $\rho_i(r)$ of the system and not on the external potential $V_i^\mathrm{ext}$.
9+
10+
For a given temperature $T$, chemical potentials $\mu$ and external potentials $V^\mathrm{ext}(r)$ the grand potential reaches a minimum at equilibrium. Mathematically this condition can be written as
11+
12+
$$\left.\frac{\delta\Omega}{\delta\rho_i(r)}\right|_{T,\mu}=F_{\rho_i}(r)-\mu_i+V_i^{\mathrm{ext}}(r)=0\tag{1}$$
13+
14+
where $F_{\rho_i}(r)=\left.\frac{\delta F}{\delta\rho_i(r)}\right|_T$ is short for the functional derivative of the intrinsic Helmholtz energy. In this context, eq. (1) is commonly referred to as the Euler-Lagrange equation, an implicit nonlinear integral equation which needs to be solved for the density profiles of the system.
15+
16+
For a homogeneous (bulk) system, $V^\mathrm{ext}=0$ and we get
17+
18+
$$F_{\rho_i}^\mathrm{b}-\mu_i=0$$
19+
20+
which can be inserted into (1) to give
21+
22+
$$F_{\rho_i}(r)=F_{\rho_i}^\mathrm{b}-V_i^\mathrm{ext}(r)\tag{2}$$
23+
24+
### Spherical molecules
25+
In the simplest case, the molecules under consideration can be described as spherical. Then the Helmholtz energy can be split into an ideal and a residual part:
26+
27+
$$\beta F=\sum_i\int\rho_i(r)\left(\ln\left(\rho_i(r)\Lambda_i^3\right)-1\right)\mathrm{d}r+\beta F^\mathrm{res}$$
28+
29+
with the de Broglie wavelength $\Lambda_i$. The functional derivatives for an inhomogeneous and a bulk system follow as
30+
31+
$$\beta F_{\rho_i}=\ln\left(\rho_i(r)\Lambda_i^3\right)+\beta F_{\rho_i}^\mathrm{res}$$
32+
33+
$$\beta F_{\rho_i}^\mathrm{b}=\ln\left(\rho_i^\mathrm{b}\Lambda_i^3\right)+\beta F_{\rho_i}^\mathrm{b,res}$$
34+
35+
Using these expressions in eq. (2) and solving for the density results in
36+
37+
$$\rho_i(r)=\rho_i^\mathrm{b}e^{\beta\left(F_{\rho_i}^\mathrm{b,res}-F_{\rho_i}^\mathrm{res}(r)-V_i^\mathrm{ext}(r)\right)}$$
38+
39+
which is the common form of the Euler-Lagrange equation for spherical molecules.
40+
41+
### Homosegmented chains
42+
For chain molecules that do not resolve individual segments (essentially the PC-SAFT Helmholtz energy functional) a chain contribution is introduced as
43+
44+
$$\beta F^\mathrm{chain}=-\sum_i\int\rho_i(r)\left(m_i-1\right)\ln\left(\frac{y_{ii}\lambda_i(r)}{\rho_i(r)}\right)\mathrm{d}r$$
45+
46+
Here, $m_i$ is the number of segments (i.e., the PC-SAFT chain length parameter), $y_{ii}$ the cavity correlation function at contact in the reference fluid, and $\lambda_i$ a weighted density.
47+
The presence of $\rho(r)$ in the logarithm poses numerical problems. Therefore, it is convenient to rearrange the expression as
48+
49+
$$\begin{align}
50+
\beta F^\mathrm{chain}=&\sum_i\int\rho_i(r)\left(m_i-1\right)\left(\ln\left(\rho_i(r)\Lambda_i^3\right)-1\right)\mathrm{d}r\\
51+
&\underbrace{-\sum_i\int\rho_i(r)\left(m_i-1\right)\left(\ln\left(y_{ii}\lambda_i(r)\Lambda_i^3\right)-1\right)\mathrm{d}r}_{\beta\hat{F}^\mathrm{chain}}
52+
\end{align}$$
53+
54+
Then the total Helmholtz energy
55+
56+
$$\beta F=\sum_i\int\rho_i(r)\left(\ln\left(\rho_i(r)\Lambda_i^3\right)-1\right)\mathrm{d}r+\beta F^\mathrm{chain}+\beta F^\mathrm{res}$$
57+
58+
can be rearranged to
59+
60+
$$\beta F=\sum_i\int\rho_i(r)m_i\left(\ln\left(\rho_i(r)\Lambda_i^3\right)-1\right)\mathrm{d}r+\underbrace{\beta\hat{F}^\mathrm{chain}+\beta F^\mathrm{res}}_{\beta\hat{F}^\mathrm{res}}$$
61+
62+
The functional derivatives are then similar to the spherical case
63+
64+
$$\beta F_{\rho_i}=m_i\ln\left(\rho_i(r)\Lambda_i^3\right)+\beta\hat{F}_{\rho_i}^\mathrm{res}$$
65+
66+
$$\beta F_{\rho_i}^\mathrm{b}=m_i\ln\left(\rho_i^\mathrm{b}\Lambda_i^3\right)+\beta\hat{F}_{\rho_i}^\mathrm{b,res}$$
67+
68+
and lead to a slightly modified Euler-Lagrange equation
69+
70+
$$\rho_i(r)=\rho_i^\mathrm{b}e^{\frac{\beta}{m_i}\left(\hat F_{\rho_i}^\mathrm{b,res}-\hat F_{\rho_i}^\mathrm{res}(r)-V_i^\mathrm{ext}(r)\right)}$$
71+
72+
### Heterosegmented chains
73+
The expressions are more complex for models in which density profiles of individual segments are considered. A derivation is given in the appendix of [Rehner et al. (2022)](https://journals.aps.org/pre/abstract/10.1103/PhysRevE.105.034110). The resulting Euler-Lagrange equation is given as
74+
75+
$$\rho_\alpha(r)=\Lambda_i^{-3}e^{\beta\left(\mu_i-\hat F_{\rho_\alpha}(r)-V_\alpha^\mathrm{ext}(r)\right)}\prod_{\alpha'}I_{\alpha\alpha'}(r)$$
76+
77+
with
78+
79+
$$I_{\alpha\alpha'}(r)=\int e^{-\beta\left(F_{\rho_{\alpha'}}(r')+V_{\alpha'}^\mathrm{ext}(r')\right)}\left(\prod_{\alpha''\neq\alpha}I_{\alpha'\alpha''}(r)\right)\omega_\mathrm{chain}^{\alpha\alpha'}(r-r')\mathrm{d}r$$
80+
81+
The index $\alpha$ is used for every segment on component $i$, $\alpha'$ refers to all segments bonded to segment $\alpha$ and $\alpha''$ to all segments bonded to $\alpha'$.
82+
For bulk systems the expressions simplify to
83+
84+
$$\rho_\alpha^\mathrm{b}=\Lambda_i^{-3}e^{\beta\left(\mu_i-\sum_\gamma\hat F_{\rho_\gamma}^\mathrm{b,res}\right)}$$
85+
86+
which shows that, by construction, the density of every segment in a molecule is identical in a bulk system. The index $\gamma$ refers to all segments on moecule $i$. The expressions can be combined in a similar way as for the molecular DFT:
87+
88+
$$\rho_\alpha(r)=\rho_\alpha^\mathrm{b}e^{\beta\left(\sum_\gamma\hat F_{\rho_\gamma}^\mathrm{b,res}-\hat F_{\rho_\alpha}^\mathrm{res}(r)-V_\alpha^\mathrm{ext}(r)\right)}\prod_{\alpha'}I_{\alpha\alpha'}(r)$$
89+
90+
At this point it can be numerically useful to redistribute the bulk contributions back into the bond integrals
91+
92+
$$\rho_\alpha(r)=\rho_\alpha^\mathrm{b}e^{\beta\left(\hat F_{\rho_\alpha}^\mathrm{b,res}-\hat F_{\rho_\alpha}^\mathrm{res}(r)-V_\alpha^\mathrm{ext}(r)\right)}\prod_{\alpha'}I_{\alpha\alpha'}(r)$$
93+
94+
$$I_{\alpha\alpha'}(r)=\int e^{\beta\left(\hat F_{\rho_{\alpha'}}^\mathrm{b,res}-\hat F_{\rho_{\alpha'}}^\mathrm{res}(r')-V_{\alpha'}^\mathrm{ext}(r')\right)}\left(\prod_{\alpha''\neq\alpha}I_{\alpha'\alpha''}(r)\right)\omega_\mathrm{chain}^{\alpha\alpha'}(r-r')\mathrm{d}r$$
95+
96+
### Combined expression
97+
To avoid having multiple implementations of the central part of the DFT code, the different descriptions of molecules can be combined in a single version of the Euler-Lagrange equation:
98+
99+
$$\rho_\alpha(r)=\rho_\alpha^\mathrm{b}e^{\frac{\beta}{m_\alpha}\left(\hat F_{\rho_\alpha}^\mathrm{b,res}-\hat F_{\rho_\alpha}^\mathrm{res}(r)-V_\alpha^\mathrm{ext}(r)\right)}\prod_{\alpha'}I_{\alpha\alpha'}(r)$$
100+
101+
If molecules consist of single (possibly non-spherical) segments, the Euler-Lagrange equation simplifies to that of the homosegmented chains shown above. For heterosegmented chains, the correct expression is obtained by setting $m_\alpha=1$.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
## Functional derivatives
2+
3+
In the last section the functional derivative
4+
5+
$$\hat F_{\rho_\alpha}^\mathrm{res}(r)=\left(\frac{\delta\hat F^\mathrm{res}}{\delta\rho_\alpha(r)}\right)_{T,\rho_{\alpha'\neq\alpha}}$$
6+
7+
was introduced as part of the Euler-Lagrange equation. The implementation of these functional derivatives can be a major difficulty during the development of a new Helmholtz energy model. In $\text{FeO}_\text{s}$ it is fully automated. The core assumption is that the residual Helmholtz energy functional $\hat F^\mathrm{res}$ can be written as a sum of contributions that each can be written in the following way:
8+
9+
$$F=\int f[\rho(r)]dr=\int f(\lbrace n_\gamma\rbrace)dr$$
10+
11+
The Helmholtz energy density $f$ which would in general be a functional of the density itself can be expressed as a *function* of weighted densities $n_\gamma$ which are obtained by convolving the density profiles with weight functions $\omega_\gamma^\alpha$
12+
13+
$$n_\gamma(r)=\sum_\alpha\int\rho_\alpha(r')\omega_\gamma^\alpha(r-r')dr'\tag{1}$$
14+
15+
In practice the weight functions tend to have simple shapes like step functions (i.e. the weighted density is an average over a sphere) or Dirac distributions (i.e. the weighted density is an average over the surface of a sphere).
16+
17+
For Helmholtz energy functionals that can be written in this form, the calculation of the functional derivative can be automated. In general the functional derivative can be written as
18+
19+
$$F_{\rho_\alpha}(r)=\int\frac{\delta f(r')}{\delta\rho_\alpha(r)}dr'=\int\sum_\gamma f_{n_\gamma}(r')\frac{\delta n_\gamma(r')}{\delta\rho_\alpha(r)}dr'$$
20+
21+
with $f_{n_\gamma}$ as abbreviation for the *partial* derivative $\frac{\partial f}{\partial n_\gamma}$. Using the definition of the weighted densities (1), the expression can be rewritten as
22+
23+
$$\begin{align}
24+
F_{\rho_\alpha}(r)&=\int\sum_\gamma f_{n_\gamma}(r')\frac{\delta n_\gamma(r')}{\delta\rho_\alpha(r)}dr'=\int\sum_\gamma f_{n_\gamma}(r')\sum_{\alpha'}\int\underbrace{\frac{\delta\rho_{\alpha'}(r'')}{\delta\rho_\alpha(r)}}_{\delta(r-r'')\delta_{\alpha\alpha'}}\omega_\gamma^{\alpha'}(r'-r'')dr''dr'\\
25+
&=\sum_\gamma\int f_{n_\gamma}(r')\omega_\gamma^\alpha(r'-r)dr
26+
\end{align}$$
27+
28+
At this point the parity of the weight functions has to be taken into account. By construction scalar and spherically symmetric weight functions (the standard case) are even functions, i.e., $\omega(-r)=\omega(r)$. In contrast, vector valued weight functions, as they appear in fundamental measure theory, have odd parity, i.e., $\omega(-r)=-\omega(r)$. Therefore, the sum over the weight functions needs to be split into two contributions, as
29+
30+
$$F_{\rho_\alpha}(r)=\sum_\gamma^\mathrm{scal}\int f_{n_\gamma}(r')\omega_\gamma^\alpha(r-r')dr-\sum_\gamma^\mathrm{vec}\int f_{n_\gamma}(r')\omega_\gamma^\alpha(r-r')dr\tag{2}$$
31+
32+
With this distinction, the calculation of the functional derivative is split into three steps
33+
34+
1. Calculate the weighted densities $n_\gamma$ from eq. (1)
35+
2. Evaluate the partial derivatives $f_{n_\gamma}$
36+
3. Use eq. (2) to obtain the functional derivative $F_{\rho_\alpha}$
37+
38+
A fast method to calculate the convolution integrals required in steps 1 and 3 is shown in the next section.
39+
40+
The implementation of partial derivatives by hand can be cumbersome and error prone. $\text{FeO}_\text{s}$ uses automatic differentiation with dual numbers to facilitate this step. For details about dual numbers and their generalization, we refer to [our publication](https://www.frontiersin.org/articles/10.3389/fceng.2021.758090/full). The essential relation is that the Helmholtz energy density evaluated with a dual number as input for one of the weighted densities evaluates to a dual number with the function value as real part and the partial derivative as dual part.
41+
42+
$$f(n_\gamma+\varepsilon,\lbrace n_{\gamma'\neq\gamma}\rbrace)=f+f_{n_\gamma}\varepsilon$$

docs/theory/dft/index.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Classical density functional theory
2+
This section explains the implementation of the core expressions from classical density functional theory in $\text{FeO}_\text{s}$.
3+
4+
```{eval-rst}
5+
.. toctree::
6+
:maxdepth: 1
7+
8+
euler_lagrange_equation
9+
functional_derivatives
10+
```
11+
12+
It is currently still under construction. You can help by [contributing](https://github.com/feos-org/feos/issues/70).

docs/theory/eos/index.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Equations of state
2+
This section explains the thermodynamic principles and algorithms used for equation of state modeling in $\text{FeO}_\text{s}$.
3+
4+
It is currently still under construction. You can help by [contributing](https://github.com/feos-org/feos/issues/70).
5+
6+
```{eval-rst}
7+
.. toctree::
8+
:maxdepth: 1
9+
```

docs/theory/models/index.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Models
2+
This section describes the thermodynamic models implemented in $\text{FeO}_\text{s}$.
3+
4+
It is currently still under construction. You can help by [contributing](https://github.com/feos-org/feos/issues/70).
5+
6+
```{eval-rst}
7+
.. toctree::
8+
:maxdepth: 1
9+
```

feos-dft/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
### Changed
99
- Added `Send` and `Sync` as supertraits to `HelmholtzEnergyFunctional` and all related traits. [#57](https://github.com/feos-org/feos/pull/57)
1010
- Renamed the `3d_dft` feature to `rayon` in accordance to the other feos crates. [#62](https://github.com/feos-org/feos/pull/62)
11+
- internally rewrote the implementation of the Euler-Lagrange equation to use a bulk density instead of the chemical potential as variable. [#60](https://github.com/feos-org/feos/pull/60)
1112

1213
## [0.3.2] - 2022-10-13
1314
### Changed

feos-dft/src/convolver/mod.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use crate::geometry::{Axis, Geometry, Grid};
22
use crate::weight_functions::*;
3+
use ndarray::linalg::Dot;
34
use ndarray::prelude::*;
45
use ndarray::{Axis as Axis_nd, RemoveAxis, ScalarOperand, Slice};
56
use num_dual::*;
7+
use num_traits::Zero;
68
use rustdct::DctNum;
79
use std::ops::{AddAssign, MulAssign, SubAssign};
810
use std::sync::Arc;
@@ -34,6 +36,45 @@ pub trait Convolver<T, D: Dimension>: Send + Sync {
3436
) -> Array<T, D::Larger>;
3537
}
3638

39+
pub(crate) struct BulkConvolver<T> {
40+
weight_constants: Vec<Array2<T>>,
41+
}
42+
43+
impl<T: DualNum<f64>> BulkConvolver<T> {
44+
pub(crate) fn new(weight_functions: Vec<WeightFunctionInfo<T>>) -> Arc<dyn Convolver<T, Ix0>> {
45+
let weight_constants = weight_functions
46+
.into_iter()
47+
.map(|w| w.weight_constants(Zero::zero(), 0))
48+
.collect();
49+
Arc::new(Self { weight_constants })
50+
}
51+
}
52+
53+
impl<T: DualNum<f64>> Convolver<T, Ix0> for BulkConvolver<T>
54+
where
55+
Array2<T>: Dot<Array1<T>, Output = Array1<T>>,
56+
{
57+
fn convolve(&self, _: Array0<T>, _: &WeightFunction<T>) -> Array0<T> {
58+
unreachable!()
59+
}
60+
61+
fn weighted_densities(&self, density: &Array1<T>) -> Vec<Array1<T>> {
62+
self.weight_constants
63+
.iter()
64+
.map(|w| w.dot(density))
65+
.collect()
66+
}
67+
68+
fn functional_derivative(&self, partial_derivatives: &[Array1<T>]) -> Array1<T> {
69+
self.weight_constants
70+
.iter()
71+
.zip(partial_derivatives.iter())
72+
.map(|(w, pd)| pd.dot(w))
73+
.reduce(|a, b| a + b)
74+
.unwrap()
75+
}
76+
}
77+
3778
/// Base structure to hold either information about the weight function through
3879
/// `WeightFunctionInfo` or the weight functions themselves via
3980
/// `FFTWeightFunctions`.

feos-dft/src/functional.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
445445
pub fn bond_integrals<D>(
446446
&self,
447447
temperature: f64,
448-
functional_derivative: &Array<f64, D::Larger>,
448+
exponential: &Array<f64, D::Larger>,
449449
convolver: &Arc<dyn Convolver<f64, D>>,
450450
) -> Array<f64, D::Larger>
451451
where
@@ -468,7 +468,6 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
468468
}
469469
}
470470

471-
let expdfdrho = functional_derivative.mapv(|x| (-x).exp());
472471
let mut i_graph: Graph<_, Option<Array<f64, D>>, Directed> =
473472
bond_weight_functions.map(|_, _| (), |_, _| None);
474473

@@ -495,7 +494,7 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
495494
if edges.clone().all(|e| e.weight().is_some()) {
496495
edge_id = Some(edge.id());
497496
let i0 = edges.fold(
498-
expdfdrho
497+
exponential
499498
.index_axis(Axis(0), edge.target().index())
500499
.to_owned(),
501500
|acc: Array<f64, D>, e| acc * e.weight().as_ref().unwrap(),
@@ -514,7 +513,7 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
514513
}
515514
}
516515

517-
let mut i = Array::ones(functional_derivative.raw_dim());
516+
let mut i = Array::ones(exponential.raw_dim());
518517
for node in i_graph.node_indices() {
519518
for edge in i_graph.edges(node) {
520519
i.index_axis_mut(Axis(0), node.index())

0 commit comments

Comments
 (0)