1use crate::functional::HelmholtzEnergyFunctional;
3use crate::geometry::{Axis, Grid};
4use crate::pdgt::PdgtFunctionalProperties;
5use crate::profile::{DFTProfile, DFTSpecifications};
6use crate::solver::DFTSolver;
7use feos_core::{Contributions, FeosError, FeosResult, PhaseEquilibrium, ReferenceSystem};
8use ndarray::{Array1, Array2, Axis as Axis_nd, Ix1, s};
9use quantity::{Area, Density, Length, Moles, SurfaceTension, Temperature};
10use std::sync::Arc;
11
12mod surface_tension_diagram;
13pub use surface_tension_diagram::SurfaceTensionDiagram;
14
15const RELATIVE_WIDTH: f64 = 6.0;
16const MIN_WIDTH: f64 = 100.0;
17
18#[derive(Clone)]
20pub struct PlanarInterface<F: HelmholtzEnergyFunctional> {
21 pub profile: DFTProfile<Ix1, F>,
22 pub vle: PhaseEquilibrium<F, 2>,
23 pub surface_tension: Option<SurfaceTension>,
24 pub equimolar_radius: Option<Length>,
25}
26
27impl<F: HelmholtzEnergyFunctional> PlanarInterface<F> {
28 pub fn solve_inplace(&mut self, solver: Option<&DFTSolver>, debug: bool) -> FeosResult<()> {
29 self.profile.solve(solver, debug)?;
31
32 self.surface_tension = Some(
34 (self.profile.integrate(
35 &(self.profile.grand_potential_density()?
36 + self.vle.vapor().pressure(Contributions::Total)),
37 )) / Area::from_reduced(1.0),
38 );
39 let delta_rho = self.vle.liquid().density - self.vle.vapor().density;
40 self.equimolar_radius = Some(
41 self.profile
42 .integrate(&(self.profile.density.sum_axis(Axis_nd(0)) - self.vle.vapor().density))
43 / delta_rho
44 / Area::from_reduced(1.0),
45 );
46
47 Ok(())
48 }
49
50 pub fn solve(mut self, solver: Option<&DFTSolver>) -> FeosResult<Self> {
51 self.solve_inplace(solver, false)?;
52 Ok(self)
53 }
54}
55
56impl<F: HelmholtzEnergyFunctional> PlanarInterface<F> {
57 pub fn new(vle: &PhaseEquilibrium<F, 2>, n_grid: usize, l_grid: Length) -> Self {
58 let grid = Grid::Cartesian1(Axis::new_cartesian(n_grid, l_grid, None));
60
61 Self {
62 profile: DFTProfile::new(grid, vle.vapor(), None, None, None),
63 vle: vle.clone(),
64 surface_tension: None,
65 equimolar_radius: None,
66 }
67 }
68
69 pub fn from_tanh(
70 vle: &PhaseEquilibrium<F, 2>,
71 n_grid: usize,
72 l_grid: Length,
73 critical_temperature: Temperature,
74 fix_equimolar_surface: bool,
75 ) -> Self {
76 let mut profile = Self::new(vle, n_grid, l_grid);
77
78 let indices = &profile.profile.bulk.eos.component_index();
80
81 let z0 = 0.5 * l_grid.to_reduced();
83 let (z0, sign) = (z0.abs(), -z0.signum());
84 let reduced_temperature = (vle.vapor().temperature / critical_temperature).into_value();
85 profile.profile.density =
86 Density::from_shape_fn(profile.profile.density.raw_dim(), |(i, z)| {
87 let rho_v = profile.vle.vapor().partial_density.get(indices[i]);
88 let rho_l = profile.vle.liquid().partial_density.get(indices[i]);
89 0.5 * (rho_l - rho_v)
90 * (sign * (profile.profile.grid.grids()[0][z] - z0) / 3.0
91 * (2.4728 - 2.3625 * reduced_temperature))
92 .tanh()
93 + 0.5 * (rho_l + rho_v)
94 });
95
96 if fix_equimolar_surface {
98 profile.profile.specification = Arc::new(DFTSpecifications::total_moles_from_profile(
99 &profile.profile,
100 ));
101 }
102
103 profile
104 }
105
106 pub fn from_pdgt(
107 vle: &PhaseEquilibrium<F, 2>,
108 n_grid: usize,
109 fix_equimolar_surface: bool,
110 ) -> FeosResult<Self> {
111 let dft = &vle.vapor().eos;
112
113 if dft.component_index().len() != 1 {
114 panic!("Initialization from pDGT not possible for segment DFT or mixtures");
115 }
116
117 let n_grid_pdgt = 20;
119 let mut z_pdgt = Length::zeros(n_grid_pdgt);
120 let mut w_pdgt = Length::from_reduced(0.0);
121 let (rho_pdgt, gamma_pdgt) =
122 dft.solve_pdgt(vle, 20, 0, Some((&mut z_pdgt, &mut w_pdgt)))?;
123 if !gamma_pdgt.to_reduced().is_normal() {
124 return Err(FeosError::InvalidState(
125 String::from("DFTProfile::from_pdgt"),
126 String::from("gamma_pdgt"),
127 gamma_pdgt.to_reduced(),
128 ));
129 }
130
131 let l_grid = Length::from_reduced(MIN_WIDTH).max(w_pdgt * RELATIVE_WIDTH);
133 let mut profile = Self::new(vle, n_grid, l_grid);
134
135 let r = l_grid * 0.5;
137 profile.profile.density = interp_symmetric(
138 vle,
139 z_pdgt,
140 rho_pdgt,
141 &profile.vle,
142 profile.profile.grid.grids()[0],
143 r,
144 )?;
145
146 if fix_equimolar_surface {
148 profile.profile.specification = Arc::new(DFTSpecifications::total_moles_from_profile(
149 &profile.profile,
150 ));
151 }
152
153 Ok(profile)
154 }
155}
156
157impl<F: HelmholtzEnergyFunctional> PlanarInterface<F> {
158 pub fn shift_equimolar_inplace(&mut self) {
159 let s = self.profile.density.shape();
160 let m = &self.profile.bulk.eos.m();
161 let mut rho_l = Density::from_reduced(0.0);
162 let mut rho_v = Density::from_reduced(0.0);
163 let mut rho = Density::zeros(s[1]);
164 for i in 0..s[0] {
165 rho_l += self.profile.density.get((i, 0)) * m[i];
166 rho_v += self.profile.density.get((i, s[1] - 1)) * m[i];
167 rho += &(&self.profile.density.index_axis(Axis_nd(0), i) * m[i]);
168 }
169
170 let x = (rho - rho_v) / (rho_l - rho_v);
171 let ze = self.profile.grid.axes()[0].edges[0] + self.profile.integrate(&x).to_reduced();
172 self.profile.grid.axes_mut()[0].grid -= ze;
173 }
174
175 pub fn shift_equimolar(mut self) -> Self {
176 self.shift_equimolar_inplace();
177 self
178 }
179
180 pub fn relative_adsorption(&self) -> Moles<Array2<f64>> {
182 let s = self.profile.density.shape();
183 let mut rho_l = Density::zeros(s[0]);
184 let mut rho_v = Density::zeros(s[0]);
185
186 for i in 0..s[0] {
188 rho_l.set(i, self.profile.density.get((i, 0)));
189 rho_v.set(i, self.profile.density.get((i, s[1] - 1)));
190 }
191
192 Moles::from_shape_fn((s[0], s[0]), |(i, j)| {
194 if i == j {
195 Moles::from_reduced(0.0)
196 } else {
197 self.profile.integrate(
198 &(-(rho_l.get(i) - rho_v.get(i))
199 * ((&self.profile.density.index_axis(Axis_nd(0), j) - rho_l.get(j))
200 / (rho_l.get(j) - rho_v.get(j))
201 - (&self.profile.density.index_axis(Axis_nd(0), i) - rho_l.get(i))
202 / (rho_l.get(i) - rho_v.get(i)))),
203 )
204 }
205 })
206 }
207
208 pub fn interfacial_enrichment(&self) -> Array1<f64> {
210 let s = self.profile.density.shape();
211 let density = self.profile.density.to_reduced();
212 let rho_l = density.index_axis(Axis_nd(1), 0);
213 let rho_v = density.index_axis(Axis_nd(1), s[1] - 1);
214
215 Array1::from_shape_fn(s[0], |i| {
216 *(density
217 .index_axis(Axis_nd(0), i)
218 .iter()
219 .max_by(|&a, &b| a.total_cmp(b))
220 .unwrap()) / rho_l[i].max(rho_v[i])
222 })
223 }
224
225 pub fn interfacial_thickness(&self) -> FeosResult<Length> {
227 let s = self.profile.density.shape();
228 let rho = self.profile.density.sum_axis(Axis_nd(0)).to_reduced();
229 let z = self.profile.grid.grids()[0];
230 let dz = z[1] - z[0];
231
232 let limits = (0.9_f64, 0.1_f64);
233 let (limit_upper, limit_lower) = if limits.0 > limits.1 {
234 (limits.0, limits.1)
235 } else {
236 (limits.1, limits.0)
237 };
238
239 if limit_upper >= 1.0 || limit_upper.is_sign_negative() {
240 return Err(FeosError::IterationFailed(String::from(
241 "Upper limit 'l' of interface thickness needs to satisfy 0 < l < 1.",
242 )));
243 }
244 if limit_lower >= 1.0 || limit_lower.is_sign_negative() {
245 return Err(FeosError::IterationFailed(String::from(
246 "Lower limit 'l' of interface thickness needs to satisfy 0 < l < 1.",
247 )));
248 }
249
250 let rho_v = rho[0].min(rho[s[1] - 1]);
252 let rho_l = rho[0].max(rho[s[1] - 1]);
253
254 if (rho_l - rho_v).abs() < 1.0e-10 {
255 return Ok(Length::from_reduced(0.0));
256 }
257
258 let rho_upper = rho_v + limit_upper * (rho_l - rho_v);
260 let rho_lower = rho_v + limit_lower * (rho_l - rho_v);
261
262 let index_upper_plus = if rho[0] >= rho[s[1] - 1] {
265 rho.iter()
266 .enumerate()
267 .find(|&(_, &x)| (x - rho_upper).is_sign_negative())
268 .expect("Could not find rho_upper value!")
269 .0
270 } else {
271 rho.iter()
272 .enumerate()
273 .find(|&(_, &x)| (rho_upper - x).is_sign_negative())
274 .expect("Could not find rho_upper value!")
275 .0
276 };
277 let index_lower_plus = if rho[0] >= rho[s[1] - 1] {
278 rho.iter()
279 .enumerate()
280 .find(|&(_, &x)| (x - rho_lower).is_sign_negative())
281 .expect("Could not find rho_lower value!")
282 .0
283 } else {
284 rho.iter()
285 .enumerate()
286 .find(|&(_, &x)| (rho_lower - x).is_sign_negative())
287 .expect("Could not find rho_lower value!")
288 .0
289 };
290
291 let z_upper = z[index_upper_plus - 1]
295 + (rho_upper - rho[index_upper_plus - 1])
296 / (rho[index_upper_plus] - rho[index_upper_plus - 1])
297 * dz;
298 let z_lower = z[index_lower_plus - 1]
299 + (rho_lower - rho[index_lower_plus - 1])
300 / (rho[index_lower_plus] - rho[index_lower_plus - 1])
301 * dz;
302
303 Ok(Length::from_reduced(z_lower - z_upper))
305 }
306
307 fn set_density_scale(&mut self, init: &Density<Array2<f64>>) {
308 assert_eq!(self.profile.density.shape(), init.shape());
309 let n_grid = self.profile.density.shape()[1];
310 let drho_init = &init.index_axis(Axis_nd(1), 0) - &init.index_axis(Axis_nd(1), n_grid - 1);
311 let rho_init_0 = init.index_axis(Axis_nd(1), n_grid - 1);
312 let drho = &self.profile.density.index_axis(Axis_nd(1), 0)
313 - &self.profile.density.index_axis(Axis_nd(1), n_grid - 1);
314 let rho_0 = self.profile.density.index_axis(Axis_nd(1), n_grid - 1);
315
316 self.profile.density = Density::from_shape_fn(self.profile.density.raw_dim(), |(i, j)| {
317 ((init.get((i, j)) - rho_init_0.get(i)) / drho_init.get(i)).into_value() * drho.get(i)
318 + rho_0.get(i)
319 });
320 }
321
322 pub fn set_density_inplace(&mut self, init: &Density<Array2<f64>>, scale: bool) {
323 if scale {
324 self.set_density_scale(init)
325 } else {
326 assert_eq!(self.profile.density.shape(), init.shape());
327 self.profile.density = init.clone();
328 }
329 }
330
331 pub fn set_density(mut self, init: &Density<Array2<f64>>, scale: bool) -> Self {
332 self.set_density_inplace(init, scale);
333 self
334 }
335}
336
337fn interp_symmetric<F: HelmholtzEnergyFunctional>(
338 vle_pdgt: &PhaseEquilibrium<F, 2>,
339 z_pdgt: Length<Array1<f64>>,
340 rho_pdgt: Density<Array2<f64>>,
341 vle: &PhaseEquilibrium<F, 2>,
342 z: &Array1<f64>,
343 radius: Length,
344) -> FeosResult<Density<Array2<f64>>> {
345 let reduced_density = Array2::from_shape_fn(rho_pdgt.raw_dim(), |(i, j)| {
346 ((rho_pdgt.get((i, j)) - vle_pdgt.vapor().partial_density.get(i))
347 / (vle_pdgt.liquid().partial_density.get(i) - vle_pdgt.vapor().partial_density.get(i)))
348 .into_value()
349 - 0.5
350 });
351 let segments = vle_pdgt.vapor().eos.component_index().len();
352 let mut reduced_density = interp(
353 &z_pdgt.to_reduced(),
354 &reduced_density,
355 &(z - radius.to_reduced()),
356 &Array1::from_elem(segments, 0.5),
357 &Array1::from_elem(segments, -0.5),
358 false,
359 ) + interp(
360 &z_pdgt.to_reduced(),
361 &reduced_density,
362 &(z + radius.to_reduced()),
363 &Array1::from_elem(segments, -0.5),
364 &Array1::from_elem(segments, 0.5),
365 true,
366 );
367 if radius.is_sign_negative() {
368 reduced_density += 1.0;
369 }
370 Ok(Density::from_shape_fn(
371 reduced_density.raw_dim(),
372 |(i, j)| {
373 reduced_density[(i, j)]
374 * (vle.liquid().partial_density.get(i) - vle.vapor().partial_density.get(i))
375 + vle.vapor().partial_density.get(i)
376 },
377 ))
378}
379
380fn interp(
381 x_old: &Array1<f64>,
382 y_old: &Array2<f64>,
383 x_new: &Array1<f64>,
384 y_left: &Array1<f64>,
385 y_right: &Array1<f64>,
386 reverse: bool,
387) -> Array2<f64> {
388 let n = x_old.len();
389
390 let (x_rev, y_rev) = if reverse {
391 (-&x_old.slice(s![..;-1]), y_old.slice(s![.., ..;-1]))
392 } else {
393 (x_old.to_owned(), y_old.view())
394 };
395
396 let mut y_new = Array2::zeros((y_rev.shape()[0], x_new.len()));
397 let mut k = 0;
398 for i in 0..x_new.len() {
399 while k < n && x_new[i] > x_rev[k] {
400 k += 1;
401 }
402 y_new.slice_mut(s![.., i]).assign(&if k == 0 {
403 y_left
404 + &((&y_rev.slice(s![.., 0]) - y_left)
405 * ((&y_rev.slice(s![.., 1]) - y_left) / (&y_rev.slice(s![.., 0]) - y_left))
406 .mapv(|x| x.powf((x_new[i] - x_rev[0]) / (x_rev[1] - x_rev[0]))))
407 } else if k == n {
408 y_right
409 + &((&y_rev.slice(s![.., n - 2]) - y_right)
410 * ((&y_rev.slice(s![.., n - 1]) - y_right)
411 / (&y_rev.slice(s![.., n - 2]) - y_right))
412 .mapv(|x| {
413 x.powf((x_new[i] - x_rev[n - 2]) / (x_rev[n - 1] - x_rev[n - 2]))
414 }))
415 } else {
416 &y_rev.slice(s![.., k - 1])
417 + &((x_new[i] - x_rev[k - 1]) / (x_rev[k] - x_rev[k - 1])
418 * (&y_rev.slice(s![.., k]) - &y_rev.slice(s![.., k - 1])))
419 });
420 }
421 y_new
422}