1use crate::convolver::{BulkConvolver, Convolver, ConvolverFFT};
2use crate::functional::HelmholtzEnergyFunctional;
3use crate::geometry::Grid;
4use crate::solver::{DFTSolver, DFTSolverLog};
5use feos_core::{FeosError, FeosResult, ReferenceSystem, State};
6use nalgebra::{DVector, Dyn, U1};
7use ndarray::{
8 Array, Array1, Array2, Array3, ArrayBase, Axis as Axis_nd, Data, Dimension, Ix1, Ix2, Ix3,
9 RemoveAxis,
10};
11use num_dual::DualNum;
12use quantity::{_Volume, DEGREES, Density, Length, Moles, Quantity, Temperature, Volume};
13use std::ops::{Add, MulAssign};
14use std::sync::Arc;
15use typenum::Sum;
16
17mod properties;
18
19pub(crate) const MAX_POTENTIAL: f64 = 50.0;
20#[cfg(feature = "rayon")]
21pub(crate) const CUTOFF_RADIUS: f64 = 14.0;
22
23pub trait DFTSpecification<D: Dimension, F>: Send + Sync {
29 fn calculate_bulk_density(
30 &self,
31 profile: &DFTProfile<D, F>,
32 bulk_density: &Array1<f64>,
33 z: &Array1<f64>,
34 ) -> FeosResult<Array1<f64>>;
35}
36
37pub enum DFTSpecifications {
39 ChemicalPotential,
41 Moles { moles: Array1<f64> },
47 TotalMoles { total_moles: f64 },
49}
50
51impl DFTSpecifications {
52 pub fn moles_from_profile<D: Dimension, F: HelmholtzEnergyFunctional>(
57 profile: &DFTProfile<D, F>,
58 ) -> Self
59 where
60 D::Larger: Dimension<Smaller = D>,
61 {
62 let rho = profile.density.to_reduced();
63 Self::Moles {
64 moles: profile.integrate_reduced_comp(&rho),
65 }
66 }
67
68 pub fn total_moles_from_profile<D: Dimension, F: HelmholtzEnergyFunctional>(
73 profile: &DFTProfile<D, F>,
74 ) -> Self
75 where
76 D::Larger: Dimension<Smaller = D>,
77 {
78 let rho = profile.density.to_reduced();
79 let moles = profile.integrate_reduced_comp(&rho).sum();
80 Self::TotalMoles { total_moles: moles }
81 }
82}
83
84impl<D: Dimension, F: HelmholtzEnergyFunctional> DFTSpecification<D, F> for DFTSpecifications {
85 fn calculate_bulk_density(
86 &self,
87 _profile: &DFTProfile<D, F>,
88 bulk_density: &Array1<f64>,
89 z: &Array1<f64>,
90 ) -> FeosResult<Array1<f64>> {
91 Ok(match self {
92 Self::ChemicalPotential => bulk_density.clone(),
93 Self::Moles { moles } => moles / z,
94 Self::TotalMoles { total_moles } => {
95 bulk_density * *total_moles / (bulk_density * z).sum()
96 }
97 })
98 }
99}
100
101#[derive(Clone)]
102pub struct DFTProfile<D: Dimension, F> {
104 pub grid: Grid,
105 pub convolver: Arc<dyn Convolver<f64, D>>,
106 pub temperature: Temperature,
107 pub density: Density<Array<f64, D::Larger>>,
108 pub specification: Arc<dyn DFTSpecification<D, F>>,
109 pub external_potential: Array<f64, D::Larger>,
110 pub bulk: State<F>,
111 pub solver_log: Option<DFTSolverLog>,
112 pub lanczos: Option<i32>,
113}
114
115impl<F> DFTProfile<Ix1, F> {
116 pub fn r(&self) -> Length<Array1<f64>> {
117 Length::from_reduced(self.grid.grids()[0].to_owned())
118 }
119
120 pub fn z(&self) -> Length<Array1<f64>> {
121 Length::from_reduced(self.grid.grids()[0].to_owned())
122 }
123}
124
125impl<F> DFTProfile<Ix2, F> {
126 pub fn edges(&self) -> [Length<Array1<f64>>; 2] {
127 [
128 Length::from_reduced(self.grid.axes()[0].edges.to_owned()),
129 Length::from_reduced(self.grid.axes()[1].edges.to_owned()),
130 ]
131 }
132
133 pub fn meshgrid(&self) -> [Length<Array2<f64>>; 2] {
134 let (u, v, alpha) = match &self.grid {
135 Grid::Cartesian2(u, v) => (u, v, 90.0 * DEGREES),
136 Grid::Periodical2(u, v, alpha) => (u, v, *alpha),
137 _ => unreachable!(),
138 };
139 let u_grid = Array::from_shape_fn([u.grid.len(), v.grid.len()], |(i, _)| u.grid[i]);
140 let v_grid = Array::from_shape_fn([u.grid.len(), v.grid.len()], |(_, j)| v.grid[j]);
141 let x = Length::from_reduced(u_grid + &v_grid * alpha.cos());
142 let y = Length::from_reduced(v_grid * alpha.sin());
143 [x, y]
144 }
145
146 pub fn r(&self) -> Length<Array1<f64>> {
147 Length::from_reduced(self.grid.grids()[0].to_owned())
148 }
149
150 pub fn z(&self) -> Length<Array1<f64>> {
151 Length::from_reduced(self.grid.grids()[1].to_owned())
152 }
153}
154
155impl<F> DFTProfile<Ix3, F> {
156 pub fn edges(&self) -> [Length<Array1<f64>>; 3] {
157 [
158 Length::from_reduced(self.grid.axes()[0].edges.to_owned()),
159 Length::from_reduced(self.grid.axes()[1].edges.to_owned()),
160 Length::from_reduced(self.grid.axes()[2].edges.to_owned()),
161 ]
162 }
163
164 pub fn meshgrid(&self) -> [Length<Array3<f64>>; 3] {
165 let (u, v, w, [alpha, beta, gamma]) = match &self.grid {
166 Grid::Cartesian3(u, v, w) => (u, v, w, [90.0 * DEGREES; 3]),
167 Grid::Periodical3(u, v, w, angles) => (u, v, w, *angles),
168 _ => unreachable!(),
169 };
170 let shape = [u.grid.len(), v.grid.len(), w.grid.len()];
171 let u_grid = Array::from_shape_fn(shape, |(i, _, _)| u.grid[i]);
172 let v_grid = Array::from_shape_fn(shape, |(_, j, _)| v.grid[j]);
173 let w_grid = Array::from_shape_fn(shape, |(_, _, k)| w.grid[k]);
174 let xi = (alpha.cos() - gamma.cos() * beta.cos()) / gamma.sin();
175 let zeta = (1.0_f64 - beta.cos().powi(2) - xi * xi).sqrt();
176 let x = Length::from_reduced(u_grid + &v_grid * gamma.cos() + &w_grid * beta.cos());
177 let y = Length::from_reduced(v_grid * gamma.sin() + &w_grid * xi);
178 let z = Length::from_reduced(w_grid * zeta);
179 [x, y, z]
180 }
181
182 pub fn x(&self) -> Length<Array1<f64>> {
183 Length::from_reduced(self.grid.grids()[0].to_owned())
184 }
185
186 pub fn y(&self) -> Length<Array1<f64>> {
187 Length::from_reduced(self.grid.grids()[1].to_owned())
188 }
189
190 pub fn z(&self) -> Length<Array1<f64>> {
191 Length::from_reduced(self.grid.grids()[2].to_owned())
192 }
193}
194
195impl<D: Dimension + RemoveAxis + 'static, F: HelmholtzEnergyFunctional> DFTProfile<D, F>
196where
197 D::Larger: Dimension<Smaller = D>,
198 D::Smaller: Dimension<Larger = D>,
199 <D::Larger as Dimension>::Larger: Dimension<Smaller = D::Larger>,
200{
201 pub fn new(
208 grid: Grid,
209 bulk: &State<F>,
210 external_potential: Option<Array<f64, D::Larger>>,
211 density: Option<&Density<Array<f64, D::Larger>>>,
212 lanczos: Option<i32>,
213 ) -> Self {
214 let t = bulk.temperature.to_reduced();
216 let weight_functions = bulk.eos.weight_functions(t);
217 let convolver = ConvolverFFT::plan(&grid, &weight_functions, lanczos);
218
219 let external_potential = external_potential.unwrap_or_else(|| {
221 let mut n_grid = vec![bulk.eos.component_index().len()];
222 grid.axes()
223 .iter()
224 .for_each(|&ax| n_grid.push(ax.grid.len()));
225 Array::zeros(n_grid).into_dimensionality().unwrap()
226 });
227
228 let density = if let Some(density) = density {
230 density.to_owned()
231 } else {
232 let exp_dfdrho = (-&external_potential).mapv(f64::exp);
233 let mut bonds = bulk.eos.bond_integrals(t, &exp_dfdrho, convolver.as_ref());
234 bonds *= &exp_dfdrho;
235 let mut density = Array::zeros(external_potential.raw_dim());
236 let bulk_density = bulk.partial_density.to_reduced();
237 for (s, &c) in bulk.eos.component_index().iter().enumerate() {
238 density.index_axis_mut(Axis_nd(0), s).assign(
239 &(bonds.index_axis(Axis_nd(0), s).map(|is| is.min(1.0)) * bulk_density[c]),
240 );
241 }
242 Density::from_reduced(density)
243 };
244
245 Self {
246 grid,
247 convolver,
248 temperature: bulk.temperature,
249 density,
250 specification: Arc::new(DFTSpecifications::ChemicalPotential),
251 external_potential,
252 bulk: bulk.clone(),
253 solver_log: None,
254 lanczos,
255 }
256 }
257}
258
259impl<D: Dimension, F: HelmholtzEnergyFunctional> DFTProfile<D, F>
260where
261 D::Larger: Dimension<Smaller = D>,
262{
263 fn integrate_reduced<N: DualNum<f64> + Copy>(&self, mut profile: Array<N, D>) -> N {
264 let (integration_weights, functional_determinant) = self.grid.integration_weights();
265
266 for (i, w) in integration_weights.into_iter().enumerate() {
267 for mut l in profile.lanes_mut(Axis_nd(i)) {
268 l.mul_assign(&w.mapv(N::from));
269 }
270 }
271 profile.sum() * functional_determinant
272 }
273
274 fn integrate_reduced_comp<S: Data<Elem = N>, N: DualNum<f64> + Copy>(
275 &self,
276 profile: &ArrayBase<S, D::Larger>,
277 ) -> Array1<N> {
278 Array1::from_shape_fn(profile.shape()[0], |i| {
279 self.integrate_reduced(profile.index_axis(Axis_nd(0), i).to_owned())
280 })
281 }
282
283 pub(crate) fn integrate_reduced_segments<S: Data<Elem = N>, N: DualNum<f64> + Copy>(
284 &self,
285 profile: &ArrayBase<S, D::Larger>,
286 ) -> DVector<N> {
287 let integral = self.integrate_reduced_comp(profile);
288 let mut integral_comp = DVector::zeros(self.bulk.eos.components());
289 for (i, &j) in self.bulk.eos.component_index().iter().enumerate() {
290 integral_comp[j] = integral[i];
291 }
292 integral_comp
293 }
294
295 pub fn volume(&self) -> Volume {
299 let volume: f64 = self.grid.axes().iter().map(|ax| ax.volume()).product();
300 Volume::from_reduced(volume * self.grid.functional_determinant())
301 }
302
303 pub fn integrate<S: Data<Elem = f64>, U>(
305 &self,
306 profile: &Quantity<ArrayBase<S, D>, U>,
307 ) -> Quantity<f64, Sum<_Volume, U>>
308 where
309 _Volume: Add<U>,
310 {
311 let (integration_weights, functional_determinant) = self.grid.integration_weights();
312 let mut value = profile.to_owned();
313 for (i, &w) in integration_weights.iter().enumerate() {
314 for mut l in value.lanes_mut(Axis_nd(i)) {
315 l.mul_assign(w);
316 }
317 }
318 Volume::from_reduced(functional_determinant) * value.sum()
319 }
320
321 pub fn integrate_comp<S: Data<Elem = f64>, U>(
323 &self,
324 profile: &Quantity<ArrayBase<S, D::Larger>, U>,
325 ) -> Quantity<DVector<f64>, Sum<_Volume, U>>
326 where
327 _Volume: Add<U>,
328 {
329 Quantity::from_fn_generic(Dyn(profile.shape()[0]), U1, |i, _| {
330 self.integrate(&profile.index_axis(Axis_nd(0), i))
331 })
332 }
333
334 pub fn integrate_segments<S: Data<Elem = f64>, U>(
336 &self,
337 profile: &Quantity<ArrayBase<S, D::Larger>, U>,
338 ) -> Quantity<DVector<f64>, Sum<_Volume, U>>
339 where
340 _Volume: Add<U>,
341 {
342 let integral = self.integrate_comp(profile);
343 let mut integral_comp = Quantity::new(DVector::zeros(self.bulk.eos.components()));
344 for (i, &j) in self.bulk.eos.component_index().iter().enumerate() {
345 integral_comp.set(j, integral.get(i));
346 }
347 integral_comp
348 }
349
350 pub fn moles(&self) -> Moles<DVector<f64>> {
352 self.integrate_segments(&self.density)
353 }
354
355 pub fn total_moles(&self) -> Moles {
357 self.moles().sum()
358 }
359}
360
361impl<D: Dimension, F> DFTProfile<D, F>
362where
363 D::Larger: Dimension<Smaller = D>,
364 <D::Larger as Dimension>::Larger: Dimension<Smaller = D::Larger>,
365 F: HelmholtzEnergyFunctional,
366{
367 pub fn weighted_densities(&self) -> FeosResult<Vec<Array<f64, D::Larger>>> {
368 Ok(self
369 .convolver
370 .weighted_densities(&self.density.to_reduced()))
371 }
372
373 #[expect(clippy::type_complexity)]
374 pub fn residual(&self, log: bool) -> FeosResult<(Array<f64, D::Larger>, Array1<f64>, f64)> {
375 let density = self.density.to_reduced();
377 let partial_density = self.bulk.partial_density.to_reduced();
378 let bulk_density = self
379 .bulk
380 .eos
381 .component_index()
382 .iter()
383 .map(|&i| partial_density[i])
384 .collect();
385
386 let (res, res_bulk, res_norm, _, _) =
387 self.euler_lagrange_equation(&density, &bulk_density, log)?;
388 Ok((res, res_bulk, res_norm))
389 }
390
391 #[expect(clippy::type_complexity)]
392 pub(crate) fn euler_lagrange_equation(
393 &self,
394 density: &Array<f64, D::Larger>,
395 bulk_density: &Array1<f64>,
396 log: bool,
397 ) -> FeosResult<(
398 Array<f64, D::Larger>,
399 Array1<f64>,
400 f64,
401 Array<f64, D::Larger>,
402 Array<f64, D::Larger>,
403 )> {
404 let temperature = self.temperature.to_reduced();
406
407 let (_, mut dfdrho) =
409 self.bulk
410 .eos
411 .functional_derivative(temperature, density, self.convolver.as_ref())?;
412
413 dfdrho += &self.external_potential;
415
416 let bulk_convolver = BulkConvolver::new(self.bulk.eos.weight_functions(temperature));
418 let (_, dfdrho_bulk) = self.bulk.eos.functional_derivative(
419 temperature,
420 bulk_density,
421 bulk_convolver.as_ref(),
422 )?;
423 dfdrho
424 .outer_iter_mut()
425 .zip(dfdrho_bulk)
426 .zip(self.bulk.eos.m().iter())
427 .for_each(|((mut df, df_b), &m)| {
428 df -= df_b;
429 df /= m
430 });
431
432 let exp_dfdrho = dfdrho.mapv(|x| (-x).exp());
434 let bonds = self
435 .bulk
436 .eos
437 .bond_integrals(temperature, &exp_dfdrho, self.convolver.as_ref());
438 let mut rho_projected = &exp_dfdrho * bonds;
439
440 rho_projected
442 .outer_iter_mut()
443 .zip(bulk_density.iter())
444 .for_each(|(mut x, &rho_b)| {
445 x *= rho_b;
446 });
447
448 let mut res = if log {
450 rho_projected.mapv(f64::ln) - density.mapv(f64::ln)
451 } else {
452 &rho_projected - density
453 };
454
455 res.iter_mut()
457 .zip(self.external_potential.iter())
458 .filter(|&(_, &p)| p + f64::EPSILON >= MAX_POTENTIAL)
459 .for_each(|(r, _)| *r = 0.0);
460
461 let z = self.integrate_reduced_comp(&rho_projected);
463 let res_bulk = bulk_density
464 - self
465 .specification
466 .calculate_bulk_density(self, bulk_density, &z)?;
467
468 let res_norm = ((density - &rho_projected).mapv(|x| x * x).sum()
470 + res_bulk.mapv(|x| x * x).sum())
471 .sqrt()
472 / ((res.len() + res_bulk.len()) as f64).sqrt();
473
474 if res_norm.is_finite() {
475 Ok((res, res_bulk, res_norm, exp_dfdrho, rho_projected))
476 } else {
477 Err(FeosError::IterationFailed("Euler-Lagrange equation".into()))
478 }
479 }
480
481 pub fn solve(&mut self, solver: Option<&DFTSolver>, debug: bool) -> FeosResult<()> {
482 let solver = solver.cloned().unwrap_or_default();
484
485 let component_index = self.bulk.eos.component_index().into_owned();
487 let mut density = self.density.to_reduced();
488 let partial_density = self.bulk.partial_density.to_reduced();
489 let mut bulk_density = component_index
490 .iter()
491 .map(|&i| partial_density[i])
492 .collect();
493
494 self.call_solver(&mut density, &mut bulk_density, &solver, debug)?;
496
497 self.density = Density::from_reduced(density);
499 let volume = Volume::from_reduced(1.0);
500 let mut moles = self.bulk.moles.clone();
501 bulk_density
502 .into_iter()
503 .enumerate()
504 .for_each(|(i, r)| moles.set(component_index[i], Density::from_reduced(r) * volume));
505 self.bulk = State::new_nvt(&self.bulk.eos, self.bulk.temperature, volume, &moles)?;
506
507 Ok(())
508 }
509}