Skip to main content

feos_dft/
pdgt.rs

1use super::functional::HelmholtzEnergyFunctional;
2use super::functional_contribution::FunctionalContribution;
3use super::weight_functions::WeightFunctionInfo;
4use feos_core::{Contributions, FeosResult, PhaseEquilibrium, ReferenceSystem};
5use nalgebra::DVector;
6use ndarray::*;
7use num_dual::Dual2_64;
8use quantity::{
9    _Area, _Density, _MolarEnergy, Density, Length, Pressure, Quantity, RGAS, SurfaceTension,
10    Temperature,
11};
12use std::ops::{Add, AddAssign, Sub};
13use typenum::{Diff, P2, Sum};
14
15type _InfluenceParameter = Diff<Sum<_MolarEnergy, _Area>, _Density>;
16type InfluenceParameter<T> = Quantity<T, _InfluenceParameter>;
17
18impl WeightFunctionInfo<Dual2_64> {
19    fn pdgt_weight_constants(&self) -> (Array2<f64>, Array2<f64>, Array2<f64>) {
20        let k = Dual2_64::from(0.0).derivative();
21        let w = self.weight_constants(k, 1);
22        (w.mapv(|w| w.re), w.mapv(|w| -w.v1), w.mapv(|w| -0.5 * w.v2))
23    }
24}
25
26trait PdgtProperties: FunctionalContribution {
27    #[expect(clippy::too_many_arguments)]
28    fn pdgt_properties(
29        &self,
30        temperature: f64,
31        density: &Array2<f64>,
32        helmholtz_energy_density: &mut Array1<f64>,
33        first_partial_derivatives: Option<&mut Array2<f64>>,
34        second_partial_derivatives: Option<&mut Array3<f64>>,
35        influence_diagonal: Option<&mut Array2<f64>>,
36        influence_matrix: Option<&mut Array3<f64>>,
37    ) -> FeosResult<()> {
38        // calculate weighted densities
39        let weight_functions = self.weight_functions_pdgt(Dual2_64::from(temperature));
40        let (w0, w1, w2) = weight_functions.pdgt_weight_constants();
41        let weighted_densities = w0.dot(density);
42
43        // calculate Helmholtz energy and derivatives
44        let w = weighted_densities.shape()[0]; // # of weighted densities
45        let s = density.shape()[0]; // # of segments
46        let n = density.shape()[1]; // # of grid points
47        let mut df = Array::zeros((w, n));
48        let mut d2f = Array::zeros((w, w, n));
49        self.second_partial_derivatives(
50            temperature,
51            weighted_densities.view(),
52            helmholtz_energy_density.view_mut(),
53            df.view_mut(),
54            d2f.view_mut(),
55        )?;
56
57        // calculate partial derivatives w.r.t. density
58        if let Some(df_drho) = first_partial_derivatives {
59            df_drho.assign(&df.t().dot(&w0));
60        }
61
62        // calculate second partial derivatives w.r.t. density
63        if let Some(d2f_drho2) = second_partial_derivatives {
64            for i in 0..s {
65                for j in 0..s {
66                    for alpha in 0..w {
67                        for beta in 0..w {
68                            d2f_drho2
69                                .index_axis_mut(Axis(0), i)
70                                .index_axis_mut(Axis(0), j)
71                                .add_assign(
72                                    &(&d2f.index_axis(Axis(0), alpha).index_axis(Axis(0), beta)
73                                        * w0[(alpha, i)]
74                                        * w0[(beta, j)]),
75                                );
76                        }
77                    }
78                }
79            }
80        }
81
82        // calculate influence diagonal
83        if let Some(c) = influence_diagonal {
84            for i in 0..s {
85                for alpha in 0..w {
86                    for beta in 0..w {
87                        c.index_axis_mut(Axis(0), i).add_assign(
88                            &(&d2f.index_axis(Axis(0), alpha).index_axis(Axis(0), beta)
89                                * (w1[(alpha, i)] * w1[(beta, i)]
90                                    - w0[(alpha, i)] * w2[(beta, i)]
91                                    - w2[(alpha, i)] * w0[(beta, i)])),
92                        );
93                    }
94                }
95            }
96        }
97
98        // calculate influence matrix
99        if let Some(c) = influence_matrix {
100            for i in 0..s {
101                for j in 0..s {
102                    for alpha in 0..w {
103                        for beta in 0..w {
104                            c.index_axis_mut(Axis(0), i)
105                                .index_axis_mut(Axis(0), j)
106                                .add_assign(
107                                    &(&d2f.index_axis(Axis(0), alpha).index_axis(Axis(0), beta)
108                                        * (w1[(alpha, i)] * w1[(beta, j)]
109                                            - w0[(alpha, i)] * w2[(beta, j)]
110                                            - w2[(alpha, i)] * w0[(beta, j)])),
111                                );
112                        }
113                    }
114                }
115            }
116        }
117        Ok(())
118    }
119
120    #[expect(clippy::type_complexity)]
121    fn influence_diagonal(
122        &self,
123        temperature: Temperature,
124        density: &Density<Array2<f64>>,
125    ) -> FeosResult<(Pressure<Array1<f64>>, InfluenceParameter<Array2<f64>>)> {
126        let t = temperature.to_reduced();
127        let n = density.shape()[1];
128        let mut f = Array::zeros(n);
129        let mut c = Array::zeros(density.raw_dim());
130        self.pdgt_properties(
131            t,
132            &density.to_reduced(),
133            &mut f,
134            None,
135            None,
136            Some(&mut c),
137            None,
138        )?;
139        Ok((
140            Pressure::from_reduced(f * t),
141            InfluenceParameter::from_reduced(c * t),
142        ))
143    }
144}
145
146impl<T: FunctionalContribution> PdgtProperties for T {}
147
148pub trait PdgtFunctionalProperties: HelmholtzEnergyFunctional {
149    // impl<T: HelmholtzEnergyFunctional> T {
150    fn solve_pdgt(
151        &self,
152        vle: &PhaseEquilibrium<Self, 2>,
153        n_grid: usize,
154        reference_component: usize,
155        z: Option<(&mut Length<Array1<f64>>, &mut Length)>,
156    ) -> FeosResult<(Density<Array2<f64>>, SurfaceTension)> {
157        // calculate density profile
158        let density = if self.components() == 1 {
159            let delta_rho = (vle.vapor().density - vle.liquid().density) / (n_grid + 1) as f64;
160            Density::linspace(
161                vle.liquid().density + delta_rho,
162                vle.vapor().density - delta_rho,
163                n_grid,
164            )
165            .insert_axis(Axis(0))
166        } else {
167            self.pdgt_density_profile_mix(vle, n_grid, reference_component)?
168        };
169
170        // calculate Helmholtz energy density and influence parameter
171        let mut delta_omega = Pressure::zeros(n_grid);
172        let mut influence_diagonal = InfluenceParameter::zeros(density.raw_dim());
173        for contribution in self.contributions() {
174            let (f, c) = contribution.influence_diagonal(vle.vapor().temperature, &density)?;
175            delta_omega += &f;
176            influence_diagonal += &c;
177        }
178        delta_omega += &self
179            .ideal_chain_contribution()
180            .helmholtz_energy_density_units::<Ix1>(vle.vapor().temperature, &density)?;
181
182        // calculate excess grand potential density
183        let mu_res = vle.vapor().residual_chemical_potential();
184        for i in 0..self.components() {
185            let rhoi = density.index_axis(Axis(0), i).to_owned();
186            let rhoi_b = vle.vapor().partial_density.get(i);
187            let mui_res = mu_res.get(i);
188            let kt = RGAS * vle.vapor().temperature;
189            delta_omega +=
190                &(&rhoi * (&((&rhoi / rhoi_b).into_value().mapv(f64::ln) - 1.0) * kt - mui_res));
191        }
192        delta_omega += vle.vapor().pressure(Contributions::Total);
193
194        // calculate density gradients w.r.t. reference density
195        let dx = density.get((reference_component, 0)) - density.get((reference_component, 1));
196        let drho = gradient(
197            &density,
198            -dx,
199            &vle.liquid().partial_density,
200            &vle.vapor().partial_density,
201        );
202
203        // calculate integrand
204        let gamma_int = ((influence_diagonal * delta_omega.clone() * 2.0).mapv(Quantity::sqrt)
205            * drho)
206            .sum_axis(Axis(0));
207
208        // calculate z-axis
209        if let Some((z, w)) = z {
210            let z_int = gamma_int.clone() / (delta_omega * 2.0);
211            *z = integrate_trapezoidal_cumulative(&z_int, dx);
212
213            // calculate equimolar surface
214            let rho_v = density.index_axis(Axis(1), 0).sum();
215            let rho_l = density.index_axis(Axis(1), n_grid - 1).sum();
216            let rho_r = (density.sum_axis(Axis(0)) - rho_v) / (rho_l - rho_v);
217            let ze = integrate_trapezoidal(&rho_r * &z_int, dx);
218
219            // calculate interfacial width
220            let w_temp = integrate_trapezoidal(&rho_r * &*z * z_int, dx);
221            *w = (24.0 * (w_temp - 0.5 * ze.powi::<P2>())).sqrt();
222
223            // shift density profile
224            *z -= ze;
225        }
226
227        // integration weights (First and last points are 0)
228        let mut weights = Array::ones(n_grid);
229        weights[0] = 7.0 / 6.0;
230        weights[1] = 23.0 / 24.0;
231        weights[n_grid - 2] = 23.0 / 24.0;
232        weights[n_grid - 1] = 7.0 / 6.0;
233        let weights = &weights * dx;
234
235        // calculate surface tension
236        Ok((density, (gamma_int * weights).sum()))
237    }
238
239    fn pdgt_density_profile_mix(
240        &self,
241        _vle: &PhaseEquilibrium<Self, 2>,
242        _n_grid: usize,
243        _reference_component: usize,
244    ) -> FeosResult<Density<Array2<f64>>> {
245        unimplemented!()
246    }
247}
248
249impl<T: HelmholtzEnergyFunctional> PdgtFunctionalProperties for T {}
250
251fn gradient<UF: Sub<UX>, UX: Copy>(
252    df: &Quantity<Array2<f64>, UF>,
253    dx: Quantity<f64, UX>,
254    left: &Quantity<DVector<f64>, UF>,
255    right: &Quantity<DVector<f64>, UF>,
256) -> Quantity<Array2<f64>, Diff<UF, UX>> {
257    Quantity::from_shape_fn(df.raw_dim(), |(c, i)| {
258        let d = if i == 0 {
259            df.get((c, 1)) - left.get(c)
260        } else if i == df.len() - 1 {
261            right.get(c) - df.get((c, df.len() - 2))
262        } else {
263            df.get((c, i + 1)) - df.get((c, i - 1))
264        };
265        d / (2.0 * dx)
266    })
267}
268
269pub fn integrate_trapezoidal<UF: Add<UX>, UX>(
270    f: Quantity<Array1<f64>, UF>,
271    dx: Quantity<f64, UX>,
272) -> Quantity<f64, Sum<UF, UX>> {
273    let mut weights = Array::ones(f.len());
274    weights[0] = 0.5;
275    weights[f.len() - 1] = 0.5;
276    (&f * &(&weights * dx)).sum()
277}
278
279pub fn integrate_trapezoidal_cumulative<UF: Add<UX>, UX>(
280    f: &Quantity<Array1<f64>, UF>,
281    dx: Quantity<f64, UX>,
282) -> Quantity<Array1<f64>, Sum<UF, UX>> {
283    let mut value = Quantity::zeros(f.len());
284    for i in 1..value.len() {
285        value.set(i, value.get(i - 1) + (f.get(i - 1) + f.get(i)) * 0.5);
286    }
287    value * dx
288}