Skip to content
This repository was archived by the owner on Jul 28, 2022. It is now read-only.

Commit a7665ae

Browse files
authored
Add ideal gas contribution to DFT calculations (#10)
* add ideal_gas getter to HelmholtzEnergyFunctional * Add ideal gas contribution to DFT calculations
1 parent 0a2b955 commit a7665ae

File tree

5 files changed

+156
-57
lines changed

5 files changed

+156
-57
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- `HelmholtzEnergyFunctional`s can now overwrite the `ideal_gas` method to provide a non-default ideal gas contribution, that is accounted for in the calculation of the entropy, the internal energy and other properties. [#10](https://github.com/feos-org/feos-core/pull/10)
10+
811
### Changed
9-
- Removed the `functional` field in `Pore1D` and `Pore3D`. [#9](https://github.com/feos-org/feos-core/pull/9)
12+
- Removed the `functional` field in `Pore1D` and `Pore3D`. [#9](https://github.com/feos-org/feos-core/pull/9)
1013

1114
### Fixed
1215
- Fixed the units of default values for adsorption isotherms. [#8](https://github.com/feos-org/feos-core/pull/8)

src/functional.rs

Lines changed: 111 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,28 @@ use crate::ideal_chain_contribution::IdealChainContribution;
44
use crate::weight_functions::{WeightFunction, WeightFunctionInfo, WeightFunctionShape};
55
use feos_core::{
66
Contributions, EosResult, EosUnit, EquationOfState, HelmholtzEnergy, HelmholtzEnergyDual,
7-
MolarWeight, StateHD,
7+
IdealGasContribution, IdealGasContributionDual, MolarWeight, StateHD,
88
};
99
use ndarray::*;
1010
use num_dual::*;
1111
use petgraph::graph::{Graph, UnGraph};
1212
use petgraph::visit::EdgeRef;
1313
use petgraph::Directed;
1414
use quantity::{QuantityArray, QuantityArray1, QuantityScalar};
15+
use std::fmt;
1516
use std::ops::{AddAssign, MulAssign};
1617
use std::rc::Rc;
1718

1819
/// Wrapper struct for the [HelmholtzEnergyFunctional] trait.
1920
#[derive(Clone)]
2021
pub struct DFT<T> {
22+
/// Helmholtz energy functional
2123
pub functional: T,
24+
/// map segment -> component
2225
pub component_index: Array1<usize>,
26+
/// chain lengths of individual components
2327
pub m: Array1<f64>,
28+
/// ideal chain contribution
2429
pub ideal_chain_contribution: IdealChainContribution,
2530
}
2631

@@ -54,6 +59,19 @@ impl<T: MolarWeight<U>, U: EosUnit> MolarWeight<U> for DFT<T> {
5459
}
5560
}
5661

62+
struct DefaultIdealGasContribution();
63+
impl<D: DualNum<f64>> IdealGasContributionDual<D> for DefaultIdealGasContribution {
64+
fn de_broglie_wavelength(&self, _: D, components: usize) -> Array1<D> {
65+
Array1::zeros(components)
66+
}
67+
}
68+
69+
impl fmt::Display for DefaultIdealGasContribution {
70+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71+
write!(f, "Ideal gas (default)")
72+
}
73+
}
74+
5775
impl<T: HelmholtzEnergyFunctional> EquationOfState for DFT<T> {
5876
fn components(&self) -> usize {
5977
self.component_index[self.component_index.len() - 1] + 1
@@ -107,6 +125,10 @@ impl<T: HelmholtzEnergyFunctional> EquationOfState for DFT<T> {
107125
));
108126
res
109127
}
128+
129+
fn ideal_gas(&self) -> &dyn IdealGasContribution {
130+
self.functional.ideal_gas()
131+
}
110132
}
111133

112134
/// A general Helmholtz energy functional.
@@ -125,6 +147,18 @@ pub trait HelmholtzEnergyFunctional: Sized {
125147
/// equation of state anyways).
126148
fn compute_max_density(&self, moles: &Array1<f64>) -> f64;
127149

150+
/// Return the ideal gas contribution.
151+
///
152+
/// Per default this function returns an ideal gas contribution
153+
/// in which the de Broglie wavelength is 1 for every component.
154+
/// Therefore, the correct ideal gas pressure is obtained even
155+
/// with no explicit ideal gas term. If a more detailed model is
156+
/// required (e.g. for the calculation of internal energies) this
157+
/// function has to be overwritten.
158+
fn ideal_gas(&self) -> &dyn IdealGasContribution {
159+
&DefaultIdealGasContribution()
160+
}
161+
128162
/// Overwrite this, if the functional consists of heterosegmented chains.
129163
fn bond_lengths(&self, _temperature: f64) -> UnGraph<(), f64> {
130164
Graph::with_capacity(0, 0)
@@ -139,6 +173,7 @@ pub trait HelmholtzEnergyFunctional: Sized {
139173
}
140174

141175
impl<T: HelmholtzEnergyFunctional> DFT<T> {
176+
/// Calculate the grand potential density $\omega$.
142177
pub fn grand_potential_density<U, D>(
143178
&self,
144179
temperature: QuantityScalar<U>,
@@ -169,12 +204,49 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
169204
Ok(f * t * U::reference_pressure())
170205
}
171206

207+
pub(crate) fn ideal_gas_contribution<D>(
208+
&self,
209+
temperature: f64,
210+
density: &Array<f64, D::Larger>,
211+
) -> Array<f64, D>
212+
where
213+
D: Dimension,
214+
D::Larger: Dimension<Smaller = D>,
215+
{
216+
let n = self.components();
217+
let ig = self.functional.ideal_gas();
218+
let lambda = ig.de_broglie_wavelength(temperature, n);
219+
let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0)));
220+
for (i, rhoi) in density.outer_iter().enumerate() {
221+
phi += &rhoi.mapv(|rhoi| (rhoi.ln() + lambda[i] - 1.0) * rhoi);
222+
}
223+
phi * temperature
224+
}
225+
226+
fn ideal_gas_contribution_dual<D>(
227+
&self,
228+
temperature: Dual64,
229+
density: &Array<f64, D::Larger>,
230+
) -> Array<Dual64, D>
231+
where
232+
D: Dimension,
233+
D::Larger: Dimension<Smaller = D>,
234+
{
235+
let n = self.components();
236+
let ig = self.functional.ideal_gas();
237+
let lambda = ig.de_broglie_wavelength(temperature, n);
238+
let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0)));
239+
for (i, rhoi) in density.outer_iter().enumerate() {
240+
phi += &rhoi.mapv(|rhoi| (lambda[i] + rhoi.ln() - 1.0) * rhoi);
241+
}
242+
phi * temperature
243+
}
244+
172245
fn intrinsic_helmholtz_energy_density<D, N>(
173246
&self,
174247
temperature: N,
175248
density: &Array<f64, D::Larger>,
176249
convolver: &Rc<dyn Convolver<N, D>>,
177-
contributions: Contributions,
178250
) -> EosResult<Array<N, D>>
179251
where
180252
N: DualNum<f64> + ScalarOperand,
@@ -187,7 +259,7 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
187259
let functional_contributions = self.functional.contributions();
188260
let mut helmholtz_energy_density: Array<N, D> = self
189261
.ideal_chain_contribution
190-
.calculate_helmholtz_energy_density(&density.mapv(N::from), contributions)?;
262+
.calculate_helmholtz_energy_density(&density.mapv(N::from))?;
191263
for (c, wd) in functional_contributions.iter().zip(weighted_densities) {
192264
let nwd = wd.shape()[0];
193265
let ngrid = wd.len() / nwd;
@@ -203,6 +275,9 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
203275
Ok(helmholtz_energy_density * temperature)
204276
}
205277

278+
/// Calculate the entropy density $s$.
279+
///
280+
/// Untested with heterosegmented functionals.
206281
pub fn entropy_density<D>(
207282
&self,
208283
temperature: f64,
@@ -215,21 +290,26 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
215290
D::Larger: Dimension<Smaller = D>,
216291
{
217292
let temperature_dual = Dual64::from(temperature).derive();
218-
let helmholtz_energy_density = self.intrinsic_helmholtz_energy_density(
219-
temperature_dual,
220-
density,
221-
convolver,
222-
contributions,
223-
)?;
293+
let mut helmholtz_energy_density =
294+
self.intrinsic_helmholtz_energy_density(temperature_dual, density, convolver)?;
295+
match contributions {
296+
Contributions::Total => {
297+
helmholtz_energy_density += &self.ideal_gas_contribution_dual::<D>(temperature_dual, density);
298+
},
299+
Contributions::ResidualP|Contributions::IdealGas => panic!("Entropy density can only be calculated for Contributions::Residual or Contributions::Total"),
300+
Contributions::Residual => (),
301+
}
224302
Ok(helmholtz_energy_density.mapv(|f| -f.eps[0]))
225303
}
226304

305+
/// Calculate the individual contributions to the entropy density.
306+
///
307+
/// Untested with heterosegmented functionals.
227308
pub fn entropy_density_contributions<D>(
228309
&self,
229310
temperature: f64,
230311
density: &Array<f64, D::Larger>,
231312
convolver: &Rc<dyn Convolver<Dual64, D>>,
232-
contributions: Contributions,
233313
) -> EosResult<Vec<Array<f64, D>>>
234314
where
235315
D: Dimension,
@@ -244,7 +324,7 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
244324
Vec::with_capacity(functional_contributions.len() + 1);
245325
helmholtz_energy_density.push(
246326
self.ideal_chain_contribution
247-
.calculate_helmholtz_energy_density(&density.mapv(Dual64::from), contributions)?,
327+
.calculate_helmholtz_energy_density(&density.mapv(Dual64::from))?,
248328
);
249329

250330
for (c, wd) in functional_contributions.iter().zip(weighted_densities) {
@@ -265,6 +345,9 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
265345
.collect())
266346
}
267347

348+
/// Calculate the internal energy density $u$.
349+
///
350+
/// Untested with heterosegmented functionals.
268351
pub fn internal_energy_density<D>(
269352
&self,
270353
temperature: f64,
@@ -278,18 +361,22 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
278361
D::Larger: Dimension<Smaller = D>,
279362
{
280363
let temperature_dual = Dual64::from(temperature).derive();
281-
let helmholtz_energy_density_dual = self.intrinsic_helmholtz_energy_density(
282-
temperature_dual,
283-
density,
284-
convolver,
285-
contributions,
286-
)?;
364+
let mut helmholtz_energy_density_dual =
365+
self.intrinsic_helmholtz_energy_density(temperature_dual, density, convolver)?;
366+
match contributions {
367+
Contributions::Total => {
368+
helmholtz_energy_density_dual += &self.ideal_gas_contribution_dual::<D>(temperature_dual, density);
369+
},
370+
Contributions::ResidualP|Contributions::IdealGas => panic!("Internal energy density can only be calculated for Contributions::Residual or Contributions::Total"),
371+
Contributions::Residual => (),
372+
}
287373
let helmholtz_energy_density = helmholtz_energy_density_dual
288374
.mapv(|f| f.re - f.eps[0] * temperature)
289375
+ (external_potential * density).sum_axis(Axis(0)) * temperature;
290376
Ok(helmholtz_energy_density)
291377
}
292378

379+
/// Calculate the (residual) functional derivative $\frac{\delta\mathcal{F}}{\delta\rho_i(\mathbf{r})}$.
293380
#[allow(clippy::type_complexity)]
294381
pub fn functional_derivative<D>(
295382
&self,
@@ -325,8 +412,8 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
325412
))
326413
}
327414

328-
// iSAFT correction to the functional derivative
329-
pub fn isaft_integrals<D>(
415+
/// Calculate the bond integrals $I_{\alpha\alpha'}(\mathbf{r})$
416+
pub fn bond_integrals<D>(
330417
&self,
331418
temperature: f64,
332419
functional_derivative: &Array<f64, D::Larger>,
@@ -338,13 +425,13 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
338425
{
339426
// calculate weight functions
340427
let bond_lengths = self.functional.bond_lengths(temperature).into_edge_type();
341-
let mut isaft_weight_functions = bond_lengths.map(
428+
let mut bond_weight_functions = bond_lengths.map(
342429
|_, _| (),
343430
|_, &l| WeightFunction::new_scaled(arr1(&[l]), WeightFunctionShape::Delta),
344431
);
345432
for n in bond_lengths.node_indices() {
346433
for e in bond_lengths.edges(n) {
347-
isaft_weight_functions.add_edge(
434+
bond_weight_functions.add_edge(
348435
e.target(),
349436
e.source(),
350437
WeightFunction::new_scaled(arr1(&[*e.weight()]), WeightFunctionShape::Delta),
@@ -354,7 +441,7 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
354441

355442
let expdfdrho = functional_derivative.mapv(|x| (-x).exp());
356443
let mut i_graph: Graph<_, Option<Array<f64, D>>, Directed> =
357-
isaft_weight_functions.map(|_, _| (), |_, _| None);
444+
bond_weight_functions.map(|_, _| (), |_, _| None);
358445

359446
let bonds = i_graph.edge_count();
360447
let mut calc = 0;
@@ -384,9 +471,8 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
384471
.to_owned(),
385472
|acc: Array<f64, D>, e| acc * e.weight().as_ref().unwrap(),
386473
);
387-
i1 = Some(
388-
convolver.convolve(i0.clone(), &isaft_weight_functions[edge.id()]),
389-
);
474+
i1 =
475+
Some(convolver.convolve(i0.clone(), &bond_weight_functions[edge.id()]));
390476
break 'nodes;
391477
}
392478
}

src/ideal_chain_contribution.rs

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use feos_core::{Contributions, EosResult, EosUnit, HelmholtzEnergyDual, StateHD};
1+
use feos_core::{EosResult, EosUnit, HelmholtzEnergyDual, StateHD};
22
use ndarray::*;
33
use num_dual::DualNum;
44
use quantity::{QuantityArray, QuantityScalar};
@@ -48,22 +48,15 @@ impl IdealChainContribution {
4848
pub fn calculate_helmholtz_energy_density<D, N>(
4949
&self,
5050
density: &Array<N, D::Larger>,
51-
contributions: Contributions,
5251
) -> EosResult<Array<N, D>>
5352
where
5453
D: Dimension,
5554
D::Larger: Dimension<Smaller = D>,
5655
N: DualNum<f64>,
5756
{
5857
let mut phi = Array::zeros(density.raw_dim().remove_axis(Axis(0)));
59-
let m = match contributions {
60-
Contributions::Total => self.m.clone(),
61-
Contributions::Residual => self.m.clone() - 1.0,
62-
Contributions::IdealGas => Array::ones(density.shape()[0]),
63-
Contributions::ResidualP => unreachable!(),
64-
};
6558
for (i, rhoi) in density.outer_iter().enumerate() {
66-
phi = phi + rhoi.mapv(|rhoi| (rhoi.ln() - 1.0) * m[i] * rhoi);
59+
phi += &rhoi.mapv(|rhoi| (rhoi.ln() - 1.0) * (self.m[i] - 1.0) * rhoi);
6760
}
6861
Ok(phi)
6962
}
@@ -72,18 +65,13 @@ impl IdealChainContribution {
7265
&self,
7366
temperature: QuantityScalar<U>,
7467
density: &QuantityArray<U, D::Larger>,
75-
contributions: Contributions,
7668
) -> EosResult<QuantityArray<U, D>>
7769
where
7870
D: Dimension,
7971
D::Larger: Dimension<Smaller = D>,
8072
{
8173
let rho = density.to_reduced(U::reference_density())?;
8274
let t = temperature.to_reduced(U::reference_temperature())?;
83-
Ok(
84-
self.calculate_helmholtz_energy_density(&rho, contributions)?
85-
* t
86-
* U::reference_pressure(),
87-
)
75+
Ok(self.calculate_helmholtz_energy_density(&rho)? * t * U::reference_pressure())
8876
}
8977
}

src/pdgt.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,14 @@ impl<T: HelmholtzEnergyFunctional> DFT<T> {
169169
}
170170
delta_omega += &self
171171
.ideal_chain_contribution
172-
.helmholtz_energy_density::<_, Ix1>(
173-
vle.vapor().temperature,
174-
&density,
175-
Contributions::Total,
176-
)?;
172+
.helmholtz_energy_density::<_, Ix1>(vle.vapor().temperature, &density)?;
173+
174+
let t = vle
175+
.vapor()
176+
.temperature
177+
.to_reduced(U::reference_temperature())?;
178+
let rho = density.to_reduced(U::reference_density())?;
179+
delta_omega += &(self.ideal_gas_contribution::<Ix1>(t, &rho) * U::reference_pressure());
177180

178181
// calculate excess grand potential density
179182
let mu = vle.vapor().chemical_potential(Contributions::Total);

0 commit comments

Comments
 (0)