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 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 let w = weighted_densities.shape()[0]; let s = density.shape()[0]; let n = density.shape()[1]; 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 if let Some(df_drho) = first_partial_derivatives {
59 df_drho.assign(&df.t().dot(&w0));
60 }
61
62 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 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 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 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 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 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 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 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 let gamma_int = ((influence_diagonal * delta_omega.clone() * 2.0).mapv(Quantity::sqrt)
205 * drho)
206 .sum_axis(Axis(0));
207
208 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 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 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 *z -= ze;
225 }
226
227 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 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}