feos_dft/
geometry.rs

1use feos_core::ReferenceSystem;
2use ndarray::{Array1, Array2};
3use quantity::{Angle, Length, Quantity};
4use std::f64::consts::{FRAC_PI_3, PI};
5
6/// Grids with up to three dimensions.
7#[derive(Clone)]
8pub enum Grid {
9    Cartesian1(Axis),
10    Cartesian2(Axis, Axis),
11    Periodical2(Axis, Axis, Angle),
12    Cartesian3(Axis, Axis, Axis),
13    Periodical3(Axis, Axis, Axis, [Angle; 3]),
14    Spherical(Axis),
15    Polar(Axis),
16    Cylindrical { r: Axis, z: Axis },
17}
18
19impl Grid {
20    pub fn new_1d(axis: Axis) -> Self {
21        match axis.geometry {
22            Geometry::Cartesian => Self::Cartesian1(axis),
23            Geometry::Cylindrical => Self::Polar(axis),
24            Geometry::Spherical => Self::Spherical(axis),
25        }
26    }
27
28    pub fn axes(&self) -> Vec<&Axis> {
29        match self {
30            Self::Cartesian1(x) => vec![x],
31            Self::Cartesian2(x, y) | Self::Periodical2(x, y, _) => vec![x, y],
32            Self::Cartesian3(x, y, z) | Self::Periodical3(x, y, z, _) => vec![x, y, z],
33            Self::Spherical(r) | Self::Polar(r) => vec![r],
34            Self::Cylindrical { r, z } => vec![r, z],
35        }
36    }
37
38    pub fn axes_mut(&mut self) -> Vec<&mut Axis> {
39        match self {
40            Self::Cartesian1(x) => vec![x],
41            Self::Cartesian2(x, y) | Self::Periodical2(x, y, _) => vec![x, y],
42            Self::Cartesian3(x, y, z) | Self::Periodical3(x, y, z, _) => vec![x, y, z],
43            Self::Spherical(r) | Self::Polar(r) => vec![r],
44            Self::Cylindrical { r, z } => vec![r, z],
45        }
46    }
47
48    pub fn grids(&self) -> Vec<&Array1<f64>> {
49        self.axes().iter().map(|ax| &ax.grid).collect()
50    }
51
52    pub(crate) fn integration_weights(&self) -> (Vec<&Array1<f64>>, f64) {
53        (
54            self.axes()
55                .iter()
56                .map(|ax| &ax.integration_weights)
57                .collect(),
58            self.functional_determinant(),
59        )
60    }
61
62    pub(crate) fn functional_determinant(&self) -> f64 {
63        match &self {
64            Self::Periodical2(_, _, alpha) => alpha.sin(),
65            Self::Periodical3(_, _, _, [alpha, beta, gamma]) => {
66                let xi = (alpha.cos() - gamma.cos() * beta.cos()) / gamma.sin();
67                gamma.sin() * (1.0 - beta.cos().powi(2) - xi * xi).sqrt()
68            }
69            _ => 1.0,
70        }
71    }
72}
73
74/// Geometries of individual axes.
75#[derive(Copy, Clone, PartialEq)]
76#[cfg_attr(feature = "python", pyo3::pyclass(eq))]
77pub enum Geometry {
78    Cartesian,
79    Cylindrical,
80    Spherical,
81}
82
83impl Geometry {
84    /// Return the number of spatial dimensions for this geometry.
85    pub fn dimension(&self) -> i32 {
86        match self {
87            Self::Cartesian => 1,
88            Self::Cylindrical => 2,
89            Self::Spherical => 3,
90        }
91    }
92}
93
94/// An individual discretized axis.
95#[derive(Clone)]
96pub struct Axis {
97    pub geometry: Geometry,
98    pub grid: Array1<f64>,
99    pub edges: Array1<f64>,
100    integration_weights: Array1<f64>,
101    potential_offset: f64,
102}
103
104impl Axis {
105    /// Create a new (equidistant) cartesian axis.
106    ///
107    /// The potential_offset is required to make sure that particles
108    /// can not interact through walls.
109    pub fn new_cartesian(points: usize, length: Length, potential_offset: Option<f64>) -> Self {
110        let potential_offset = potential_offset.unwrap_or(0.0);
111        let l = length.to_reduced() + potential_offset;
112        let cell_size = l / points as f64;
113        let grid = Array1::linspace(0.5 * cell_size, l - 0.5 * cell_size, points);
114        let edges = Array1::linspace(0.0, l, points + 1);
115        let integration_weights = Array1::from_elem(points, cell_size);
116        Self {
117            geometry: Geometry::Cartesian,
118            grid,
119            edges,
120            integration_weights,
121            potential_offset,
122        }
123    }
124
125    /// Create a new (equidistant) spherical axis.
126    pub fn new_spherical(points: usize, length: Length) -> Self {
127        let l = length.to_reduced();
128        let cell_size = l / points as f64;
129        let grid = Array1::linspace(0.5 * cell_size, l - 0.5 * cell_size, points);
130        let edges = Array1::linspace(0.0, l, points + 1);
131        let integration_weights = Array1::from_shape_fn(points, |k| {
132            4.0 * FRAC_PI_3 * cell_size.powi(3) * (3 * k * k + 3 * k + 1) as f64
133        });
134        Self {
135            geometry: Geometry::Spherical,
136            grid,
137            edges,
138            integration_weights,
139            potential_offset: 0.0,
140        }
141    }
142
143    /// Create a new logarithmically scaled cylindrical axis.
144    pub fn new_polar(points: usize, length: Length) -> Self {
145        let l = length.to_reduced();
146
147        let mut alpha = 0.002_f64;
148        for _ in 0..20 {
149            alpha = -(1.0 - (-alpha).exp()).ln() / (points - 1) as f64;
150        }
151        let x0 = 0.5 * ((-alpha * points as f64).exp() + (-alpha * (points - 1) as f64).exp());
152        let grid = (0..points)
153            .map(|i| l * x0 * (alpha * i as f64).exp())
154            .collect();
155        let edges = (0..=points)
156            .map(|i| {
157                if i == 0 {
158                    0.0
159                } else {
160                    l * (-alpha * (points - i) as f64).exp()
161                }
162            })
163            .collect();
164
165        let k0 = (2.0 * alpha).exp() * (2.0 * alpha.exp() + (2.0 * alpha).exp() - 1.0)
166            / ((1.0 + alpha.exp()).powi(2) * ((2.0 * alpha).exp() - 1.0));
167        let integration_weights = (0..points)
168            .map(|i| {
169                (match i {
170                    0 => k0 * (2.0 * alpha).exp(),
171                    1 => ((2.0 * alpha).exp() - k0) * (2.0 * alpha).exp(),
172                    _ => (2.0 * alpha * i as f64).exp() * ((2.0 * alpha).exp() - 1.0),
173                }) * ((-2.0 * alpha * points as f64).exp() * PI * l * l)
174            })
175            .collect();
176
177        Self {
178            geometry: Geometry::Cylindrical,
179            grid,
180            edges,
181            integration_weights,
182            potential_offset: 0.0,
183        }
184    }
185
186    /// Returns the total length of the axis.
187    ///
188    /// This includes the `potential_offset` and used e.g.
189    /// to determine the correct frequency vector in FFT.
190    pub fn length(&self) -> f64 {
191        self.edges[self.grid.len()] - self.edges[0]
192    }
193
194    /// Returns the volume of the axis.
195    ///
196    /// Depending on the geometry, the result is in m, m² or m³.
197    /// The `potential_offset` is not included in the volume, as
198    /// it is mainly used to calculate excess properties.
199    pub fn volume(&self) -> f64 {
200        let length = self.edges[self.grid.len()] - self.potential_offset - self.edges[0];
201        (match self.geometry {
202            Geometry::Cartesian => 1.0,
203            Geometry::Cylindrical => 4.0 * PI,
204            Geometry::Spherical => 4.0 * FRAC_PI_3,
205        }) * length.powi(self.geometry.dimension())
206    }
207
208    /// Interpolate a function on the given axis.
209    pub fn interpolate<U>(
210        &self,
211        x: f64,
212        y: &Quantity<Array2<f64>, U>,
213        i: usize,
214    ) -> Quantity<f64, U> {
215        let n = self.grid.len();
216        y.get((
217            i,
218            if x >= self.edges[n] {
219                n - 1
220            } else {
221                match self.geometry {
222                    Geometry::Cartesian | Geometry::Spherical => (x / self.edges[1]) as usize,
223                    Geometry::Cylindrical => {
224                        if x < self.edges[1] {
225                            0
226                        } else {
227                            (n as f64
228                                - (n - 1) as f64 * (x / self.edges[n]).ln()
229                                    / (self.edges[1] / self.edges[n]).ln())
230                                as usize
231                        }
232                    }
233                }
234            },
235        ))
236    }
237}