Skip to main content

ems/engine/
grid.rs

1//! Rectilinear (tensor-product) FDTD mesh.
2//!
3//! The primary mesh is given by its node coordinates along each axis. Primary
4//! cells span two consecutive lines on every axis; the dual mesh has its nodes
5//! at the primary-cell midpoints (Yee 1966; Taflove & Hagness,
6//! *Computational Electrodynamics*, 3rd ed., ch. 3 and the non-uniform-grid
7//! discussion there). At the outer boundary the dual cells are half cells.
8//!
9//! A grid is either Cartesian `(x, y, z)` or cylindrical `(ρ, α, z)`
10//! ([`Grid::cylindrical`], design §5.15). The cylindrical geometry
11//! (edge lengths, face areas) is in [`Grid::edge_length`],
12//! [`Grid::dual_edge_length`], [`Grid::dual_face_area`] and
13//! [`Grid::face_area`]; the [`Axis`] names `X`, `Y`, `Z` then stand for
14//! `ρ`, `α`, `z`.
15
16use std::sync::OnceLock;
17
18use crate::axis::Axis;
19use crate::{Error, Result};
20
21/// The coordinate system of a [`Grid`].
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
23pub enum Coordinates {
24    /// `(x, y, z)` in metres.
25    #[default]
26    Cartesian,
27    /// `(ρ, α, z)`: radius and height in metres, azimuth in radians
28    /// ([`Axis::X`] = ρ, [`Axis::Y`] = α, [`Axis::Z`] = z).
29    Cylindrical,
30}
31
32/// Relative tolerance on `α_max − α_min = 2π` for a closed azimuth.
33const CLOSED_TOL: f64 = 1e-6;
34
35/// A validated rectilinear mesh, Cartesian (metres) or cylindrical
36/// (`ρ`, `z` in metres, `α` in radians).
37///
38/// Per-axis arrays such as [`nodes`](Self::nodes) are indexed by
39/// [`Axis::index`] (`[x, y, z]`, or `[ρ, α, z]`). The grid has `n[a]` node
40/// lines along axis `a` (at least 2) and therefore `n[a] − 1` cells.
41#[derive(Debug, Clone)]
42pub struct Grid {
43    lines: [Vec<f64>; 3],
44    coords: Coordinates,
45    /// Azimuthal multigrid zone boundaries of a cylindrical grid: ρ line
46    /// indices, strictly decreasing (outermost first); empty when off.
47    multigrid: Vec<usize>,
48    /// Cached stability limit of a cylindrical grid (power iteration).
49    courant: OnceLock<f64>,
50}
51
52impl PartialEq for Grid {
53    fn eq(&self, other: &Self) -> bool {
54        self.lines == other.lines
55            && self.coords == other.coords
56            && self.multigrid == other.multigrid
57    }
58}
59
60impl Grid {
61    /// Builds a grid from the node coordinates (metres) of each axis.
62    ///
63    /// Every axis needs at least two lines; the coordinates must be finite
64    /// and strictly increasing. The total node count must fit in `usize`.
65    pub fn new(lines: [Vec<f64>; 3]) -> Result<Self> {
66        for (a, l) in Axis::ALL.into_iter().zip(&lines) {
67            if l.len() < 2 {
68                return Err(Error::Invalid(format!(
69                    "grid axis {a} needs at least 2 lines, got {}",
70                    l.len()
71                )));
72            }
73            if let Some(v) = l.iter().find(|v| !v.is_finite()) {
74                return Err(Error::Invalid(format!(
75                    "grid axis {a} has a non-finite line {v}"
76                )));
77            }
78            if let Some(w) = l.windows(2).position(|w| w[1] <= w[0]) {
79                return Err(Error::Invalid(format!(
80                    "grid axis {a} lines are not strictly increasing at index {w} ({} >= {})",
81                    l[w],
82                    l[w + 1]
83                )));
84            }
85        }
86        let n = [lines[0].len(), lines[1].len(), lines[2].len()];
87        n[0].checked_mul(n[1])
88            .and_then(|v| v.checked_mul(n[2]))
89            .ok_or_else(|| Error::Invalid("grid node count overflows usize".into()))?;
90        Ok(Self {
91            lines,
92            coords: Coordinates::Cartesian,
93            multigrid: Vec::new(),
94            courant: OnceLock::new(),
95        })
96    }
97
98    /// Builds a cylindrical grid from the lines `[ρ, α, z]`: `ρ` and `z` in
99    /// metres, `α` in radians.
100    ///
101    /// Besides the checks of [`new`](Self::new), `ρ` must be non-negative
102    /// and the azimuth span `α_max − α_min` at most 2π. A first `ρ` line at
103    /// 0 is the axis ([`has_axis`](Self::has_axis)); an azimuth span of 2π
104    /// (to a relative 1e-6) closes the circle
105    /// ([`is_closed`](Self::is_closed)): the first and last `α` lines are
106    /// then the same half-plane, and the engine treats the α faces as
107    /// periodic.
108    ///
109    /// ```
110    /// use ems::engine::Grid;
111    /// use std::f64::consts::TAU;
112    ///
113    /// let g = Grid::cylindrical([
114    ///     vec![0.0, 1e-3, 2e-3],
115    ///     (0..=8).map(|j| j as f64 * TAU / 8.0).collect(),
116    ///     vec![0.0, 1e-3],
117    /// ])?;
118    /// assert!(g.has_axis() && g.is_closed());
119    /// # Ok::<(), ems::Error>(())
120    /// ```
121    pub fn cylindrical(lines: [Vec<f64>; 3]) -> Result<Self> {
122        let mut g = Self::new(lines)?;
123        if g.lines[0][0] < 0.0 {
124            return Err(Error::Invalid(format!(
125                "cylindrical grid: rho lines must be >= 0, got {}",
126                g.lines[0][0]
127            )));
128        }
129        let span = g.alpha_span();
130        if span > core::f64::consts::TAU * (1.0 + CLOSED_TOL) {
131            return Err(Error::Invalid(format!(
132                "cylindrical grid: alpha span {span} rad exceeds 2π"
133            )));
134        }
135        g.coords = Coordinates::Cylindrical;
136        Ok(g)
137    }
138
139    /// The coordinate system.
140    pub fn coordinates(&self) -> Coordinates {
141        self.coords
142    }
143
144    /// Whether the grid is cylindrical.
145    pub fn is_cylindrical(&self) -> bool {
146        self.coords == Coordinates::Cylindrical
147    }
148
149    /// Whether the grid is cylindrical with its first `ρ` line at 0 (the
150    /// z axis lies on the `ρ_min` face, see design §5.15).
151    pub fn has_axis(&self) -> bool {
152        self.is_cylindrical() && self.lines[0][0] == 0.0
153    }
154
155    /// Whether the grid is cylindrical and spans the full circle in `α`.
156    pub fn is_closed(&self) -> bool {
157        self.is_cylindrical()
158            && (self.alpha_span() - core::f64::consts::TAU).abs()
159                <= CLOSED_TOL * core::f64::consts::TAU
160    }
161
162    fn alpha_span(&self) -> f64 {
163        let a = &self.lines[1];
164        a[a.len() - 1] - a[0]
165    }
166
167    /// The cached cylindrical stability limit, computed by `f` once.
168    pub(crate) fn cached_courant(&self, f: impl FnOnce(&Self) -> f64) -> f64 {
169        *self.courant.get_or_init(|| f(self))
170    }
171
172    /// Enables azimuthal multigrid on a cylindrical grid (design §5.15.1):
173    /// each entry of `rings` is the index of a `ρ` line; inside each of
174    /// them (cells `i < ring`) the α resolution is halved once more, so
175    /// inside the innermost of `L` rings only every `2^L`-th α line is
176    /// kept. The rings may be given in any order; an empty list switches
177    /// multigrid off.
178    ///
179    /// # Errors
180    /// [`Error::Invalid`] if the grid is not cylindrical, a ring index is
181    /// repeated or not in `1..=nρ−2` (each zone boundary needs a cell on
182    /// both sides), the number of α cells is not a multiple of `2^L`, or a
183    /// closed grid would keep fewer than 3 α cells in its coarsest zone.
184    ///
185    /// ```
186    /// use ems::engine::Grid;
187    /// use std::f64::consts::TAU;
188    ///
189    /// let g = Grid::cylindrical([
190    ///     (0..=8).map(|i| i as f64 * 1e-3).collect(),
191    ///     (0..=32).map(|j| j as f64 * TAU / 32.0).collect(),
192    ///     vec![0.0, 1e-3],
193    /// ])?
194    /// .with_multigrid(&[4, 2])?;
195    /// assert_eq!(g.alpha_factor(0), 4); // cells 0, 1: every 4th α line
196    /// assert_eq!(g.alpha_factor(3), 2); // cells 2, 3: every 2nd
197    /// assert_eq!(g.alpha_factor(4), 1);
198    /// # Ok::<(), ems::Error>(())
199    /// ```
200    pub fn with_multigrid(mut self, rings: &[usize]) -> Result<Self> {
201        let mut r = rings.to_vec();
202        r.sort_unstable_by(|a, b| b.cmp(a));
203        if !r.is_empty() && !self.is_cylindrical() {
204            return Err(Error::Invalid("multigrid needs a cylindrical grid".into()));
205        }
206        if r.windows(2).any(|w| w[0] == w[1]) {
207            return Err(Error::Invalid(format!(
208                "multigrid: repeated zone boundary in {rings:?}"
209            )));
210        }
211        let [nr, na, _] = self.nodes();
212        if let Some(&bad) = r.iter().find(|&&i| i == 0 || i + 2 > nr) {
213            return Err(Error::Invalid(format!(
214                "multigrid: zone boundary rho line {bad} must be in 1..={}",
215                nr.saturating_sub(2)
216            )));
217        }
218        let f = 1usize << r.len().min(usize::BITS as usize - 1);
219        let cells = na - 1;
220        if cells % f != 0 {
221            return Err(Error::Invalid(format!(
222                "multigrid: {} levels need a multiple of {f} alpha cells, got {cells}",
223                r.len()
224            )));
225        }
226        if self.is_closed() && !r.is_empty() && cells / f < 3 {
227            return Err(Error::Invalid(format!(
228                "multigrid: the coarsest zone would keep {} alpha cells (at least 3 needed)",
229                cells / f
230            )));
231        }
232        self.multigrid = r;
233        self.courant = OnceLock::new();
234        Ok(self)
235    }
236
237    /// The multigrid zone boundaries (`ρ` line indices, outermost first);
238    /// empty without multigrid (see [`with_multigrid`](Self::with_multigrid)).
239    pub fn multigrid(&self) -> &[usize] {
240        &self.multigrid
241    }
242
243    /// Whether azimuthal multigrid is enabled.
244    pub fn has_multigrid(&self) -> bool {
245        !self.multigrid.is_empty()
246    }
247
248    /// The azimuthal coarsening factor of radial cell `i` (between `ρ_i`
249    /// and `ρ_{i+1}`): 1 without multigrid or outside every zone, `2^ℓ`
250    /// inside `ℓ` zone boundaries. Indices past the last cell use the last
251    /// cell.
252    pub fn alpha_factor(&self, i: usize) -> usize {
253        let i = i.min(self.lines[0].len() - 2);
254        1 << self.multigrid.iter().filter(|&&r| i < r).count()
255    }
256
257    /// Coarsening factors `(inner, outer)` of the cells on both sides of
258    /// the `ρ` line `i` (the first line uses its outer cell twice).
259    pub(crate) fn ring_factors(&self, i: usize) -> (usize, usize) {
260        let out = self.alpha_factor(i);
261        let inn = if i > 0 { self.alpha_factor(i - 1) } else { out };
262        (inn, out)
263    }
264
265    /// Dual α step `Δα*` at line `j` of a grid that keeps every `f`-th α
266    /// line (`j` a multiple of `f`): half the distance between the kept
267    /// neighbours, a half cell at the first and last line.
268    pub(crate) fn alpha_dual(&self, j: usize, f: usize) -> f64 {
269        let a = &self.lines[1];
270        let lo = if j >= f { a[j] - a[j - f] } else { 0.0 };
271        let hi = if j + f < a.len() {
272            a[j + f] - a[j]
273        } else {
274            0.0
275        };
276        0.5 * (lo + hi)
277    }
278
279    /// Whether the E edge along `axis` at `node` is removed by multigrid:
280    /// ρ edges and z edges off the kept α lines of their cell / line (the
281    /// axis z edge is never removed).
282    pub(crate) fn mg_removed_e(&self, axis: Axis, [i, j, _]: [usize; 3]) -> bool {
283        if self.multigrid.is_empty() {
284            return false;
285        }
286        match axis {
287            Axis::X => j % self.alpha_factor(i) != 0,
288            Axis::Y => false,
289            Axis::Z => !(i == 0 && self.has_axis()) && j % self.ring_factors(i).1 != 0,
290        }
291    }
292
293    /// Whether the H dual edge along `axis` at `node` is removed by
294    /// multigrid: the α dual edges off the kept α lines of their cell.
295    pub(crate) fn mg_removed_h(&self, axis: Axis, [i, j, _]: [usize; 3]) -> bool {
296        !self.multigrid.is_empty() && axis == Axis::Y && j % self.alpha_factor(i) != 0
297    }
298
299    /// Radius of the dual line through cell `i` (`ρ_{i+½}`), or of node
300    /// `i` itself for the last node (cylindrical helper).
301    fn rho_dual(&self, i: usize) -> f64 {
302        let r = &self.lines[0];
303        if i + 1 < r.len() {
304            0.5 * (r[i] + r[i + 1])
305        } else {
306            r[i]
307        }
308    }
309
310    /// Physical length \[m\] of the primary edge along `axis` starting at
311    /// `node`: [`delta`](Self::delta), or `ρ_i·Δα_j` for an α edge of a
312    /// cylindrical grid (0 on the axis).
313    pub fn edge_length(&self, axis: Axis, node: [usize; 3]) -> f64 {
314        let d = self.delta(axis, node[axis.index()]);
315        if self.is_cylindrical() && axis == Axis::Y {
316            self.lines[0][node[0]] * d
317        } else {
318            d
319        }
320    }
321
322    /// Physical length \[m\] of the (in-domain part of the) dual edge along
323    /// `axis` through `node`: [`dual_delta`](Self::dual_delta), or
324    /// `ρ_{i+½}·Δα*_j` for an α dual edge of a cylindrical grid.
325    pub fn dual_edge_length(&self, axis: Axis, node: [usize; 3]) -> f64 {
326        let mut d = self.dual_delta(axis, node[axis.index()]);
327        if self.is_cylindrical() && axis == Axis::Y {
328            // Multigrid: a kept α dual edge spans the coarse dual cell.
329            let f = self.alpha_factor(node[0]);
330            if f > 1 && node[1].is_multiple_of(f) {
331                d = self.alpha_dual(node[1], f);
332            }
333            self.rho_dual(node[0]) * d
334        } else {
335            d
336        }
337    }
338
339    /// Area \[m²\] of the (in-domain part of the) dual face pierced by the
340    /// primary edge along `axis` starting at `node`: the product of the two
341    /// transverse [`dual_delta`](Self::dual_delta)s in Cartesian
342    /// coordinates; in cylindrical coordinates `ρ_{i+½}·Δα*·Δz*` (ρ edge),
343    /// `Δρ*·Δz*` (α edge) and the annular sector `Δα*·(ρ_hi² − ρ_lo²)/2`
344    /// (z edge, `ρ_lo`, `ρ_hi` the dual lines around node `i`, clamped to
345    /// the domain).
346    pub fn dual_face_area(&self, axis: Axis, node: [usize; 3]) -> f64 {
347        let (ab, ac) = axis.others();
348        let (db, dc) = (
349            self.dual_delta(ab, node[ab.index()]),
350            self.dual_delta(ac, node[ac.index()]),
351        );
352        if !self.is_cylindrical() {
353            return db * dc;
354        }
355        let i = node[0];
356        let j = node[1];
357        match axis {
358            // others() = (Y, Z): Δα*, Δz*.
359            Axis::X => {
360                let f = self.alpha_factor(i);
361                let db = if f > 1 && j.is_multiple_of(f) {
362                    self.alpha_dual(j, f)
363                } else {
364                    db
365                };
366                self.rho_dual(i) * db * dc
367            }
368            // others() = (Z, X): Δz*, Δρ*.
369            Axis::Y => db * dc,
370            // others() = (X, Y).
371            Axis::Z => {
372                let r = &self.lines[0];
373                let lo = if i > 0 { 0.5 * (r[i - 1] + r[i]) } else { r[i] };
374                let hi = self.rho_dual(i);
375                let (inn, out) = self.ring_factors(i);
376                if inn == 1 || (i == 0 && self.has_axis()) {
377                    return dc * 0.5 * (hi * hi - lo * lo);
378                }
379                // Multigrid (§5.15.1): the inner and outer halves of the
380                // dual face span the dual cells of their zones; a z edge
381                // kept only by the outer zone (a slave on a zone
382                // boundary) has the outer half only.
383                let (ri, outer) = (
384                    r[i],
385                    self.alpha_dual(j, out) * 0.5 * (hi * hi - r[i] * r[i]),
386                );
387                if j.is_multiple_of(inn) {
388                    self.alpha_dual(j, inn) * 0.5 * (ri * ri - lo * lo) + outer
389                } else {
390                    outer
391                }
392            }
393        }
394    }
395
396    /// Area \[m²\] of the primary face normal to `axis` whose lowest corner
397    /// is `node`: the product of the two transverse
398    /// [`delta`](Self::delta)s in Cartesian coordinates; in cylindrical
399    /// coordinates `ρ_i·Δα·Δz` (ρ face, 0 on the axis), `Δρ·Δz` (α face)
400    /// and the annular sector `Δα·(ρ_{i+1}² − ρ_i²)/2` (z face).
401    pub fn face_area(&self, axis: Axis, node: [usize; 3]) -> f64 {
402        let (ab, ac) = axis.others();
403        let (db, dc) = (
404            self.delta(ab, node[ab.index()]),
405            self.delta(ac, node[ac.index()]),
406        );
407        if !self.is_cylindrical() {
408            return db * dc;
409        }
410        let r = &self.lines[0];
411        let i = node[0];
412        match axis {
413            Axis::X => r[i] * db * dc,
414            Axis::Y => db * dc,
415            Axis::Z => dc * 0.5 * (r[i + 1] * r[i + 1] - r[i] * r[i]),
416        }
417    }
418
419    /// Uniform grid with `cells[a]` cells of size `cell_size[a]` (metres)
420    /// along each axis, starting at the origin.
421    pub fn uniform(cells: [usize; 3], cell_size: [f64; 3]) -> Result<Self> {
422        let axis =
423            |a: usize| -> Vec<f64> { (0..=cells[a]).map(|i| i as f64 * cell_size[a]).collect() };
424        Self::new([axis(0), axis(1), axis(2)])
425    }
426
427    /// Node coordinates along `axis`.
428    pub fn lines(&self, axis: Axis) -> &[f64] {
429        &self.lines[axis.index()]
430    }
431
432    /// Number of nodes per axis, `[nx, ny, nz]`.
433    pub fn nodes(&self) -> [usize; 3] {
434        [
435            self.lines[0].len(),
436            self.lines[1].len(),
437            self.lines[2].len(),
438        ]
439    }
440
441    /// Total number of nodes `nx·ny·nz` (the length of every field array).
442    pub fn num_nodes(&self) -> usize {
443        let n = self.nodes();
444        n[0] * n[1] * n[2]
445    }
446
447    /// Number of primary cells per axis, `[nx − 1, ny − 1, nz − 1]`.
448    pub fn cells(&self) -> [usize; 3] {
449        let n = self.nodes();
450        [n[0] - 1, n[1] - 1, n[2] - 1]
451    }
452
453    /// Total number of primary cells.
454    pub fn num_cells(&self) -> usize {
455        let c = self.cells();
456        c[0] * c[1] * c[2]
457    }
458
459    /// Linear node index `(i·ny + j)·nz + k` (z contiguous).
460    #[inline]
461    pub fn index(&self, [i, j, k]: [usize; 3]) -> usize {
462        let n = self.nodes();
463        (i * n[1] + j) * n[2] + k
464    }
465
466    /// Primary edge length `Δ_a(i) = line[i+1] − line[i]` along `axis`,
467    /// for `i < n[axis] − 1` (also the size of cell `i` along that axis).
468    #[inline]
469    pub fn delta(&self, axis: Axis, i: usize) -> f64 {
470        let l = self.lines(axis);
471        l[i + 1] - l[i]
472    }
473
474    /// All primary edge lengths of `axis` (`n[axis] − 1` values).
475    pub fn deltas(&self, axis: Axis) -> Vec<f64> {
476        self.lines(axis).windows(2).map(|w| w[1] - w[0]).collect()
477    }
478
479    /// Dual node position along `axis`: midpoint of cell `i`
480    /// (`i < n[axis] − 1`).
481    pub fn dual_line(&self, axis: Axis, i: usize) -> f64 {
482        let l = self.lines(axis);
483        0.5 * (l[i] + l[i + 1])
484    }
485
486    /// All dual node positions of `axis` (`n[axis] − 1` values).
487    pub fn dual_lines(&self, axis: Axis) -> Vec<f64> {
488        self.lines(axis)
489            .windows(2)
490            .map(|w| 0.5 * (w[0] + w[1]))
491            .collect()
492    }
493
494    /// Dual edge length through primary node `i` along `axis`: from the
495    /// midpoint of cell `i − 1` to the midpoint of cell `i`, i.e.
496    /// `(Δ(i−1) + Δ(i)) / 2`. At the outer nodes (`i = 0`, `i = n − 1`) the
497    /// dual cell is a half cell and the length is `Δ/2` of the single
498    /// adjacent cell.
499    pub fn dual_delta(&self, axis: Axis, i: usize) -> f64 {
500        let n = self.lines(axis).len();
501        let lo = if i > 0 { self.delta(axis, i - 1) } else { 0.0 };
502        let hi = if i + 1 < n { self.delta(axis, i) } else { 0.0 };
503        0.5 * (lo + hi)
504    }
505
506    /// Smallest cell size along `axis`.
507    pub fn min_delta(&self, axis: Axis) -> f64 {
508        self.lines(axis)
509            .windows(2)
510            .map(|w| w[1] - w[0])
511            .fold(f64::INFINITY, f64::min)
512    }
513
514    /// Smallest cell size per axis.
515    pub fn min_deltas(&self) -> [f64; 3] {
516        Axis::ALL.map(|a| self.min_delta(a))
517    }
518}
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523
524    #[test]
525    fn validation() {
526        let ok = vec![0.0, 1.0];
527        assert!(Grid::new([ok.clone(), ok.clone(), ok.clone()]).is_ok());
528        assert!(Grid::new([vec![0.0], ok.clone(), ok.clone()]).is_err());
529        assert!(Grid::new([ok.clone(), vec![0.0, 0.0], ok.clone()]).is_err());
530        assert!(Grid::new([ok.clone(), ok.clone(), vec![1.0, 0.0]]).is_err());
531        assert!(Grid::new([ok.clone(), vec![0.0, f64::NAN], ok.clone()]).is_err());
532        assert!(Grid::new([ok.clone(), ok.clone(), vec![0.0, f64::INFINITY]]).is_err());
533    }
534
535    #[test]
536    fn helpers() {
537        let g = Grid::new([
538            vec![0.0, 1.0, 3.0, 6.0],
539            vec![0.0, 2.0],
540            vec![-1.0, 0.0, 0.5],
541        ])
542        .unwrap();
543        assert_eq!(g.nodes(), [4, 2, 3]);
544        assert_eq!(g.num_nodes(), 24);
545        assert_eq!(g.cells(), [3, 1, 2]);
546        assert_eq!(g.num_cells(), 6);
547        assert_eq!(g.deltas(Axis::X), vec![1.0, 2.0, 3.0]);
548        assert_eq!(g.dual_lines(Axis::X), vec![0.5, 2.0, 4.5]);
549        assert_eq!(g.dual_delta(Axis::X, 0), 0.5);
550        assert_eq!(g.dual_delta(Axis::X, 1), 1.5);
551        assert_eq!(g.dual_delta(Axis::X, 3), 1.5);
552        assert_eq!(g.min_deltas(), [1.0, 2.0, 0.5]);
553        assert_eq!(g.index([1, 1, 2]), (2 + 1) * 3 + 2);
554        // Dual edges tile the whole axis.
555        let s: f64 = (0..4).map(|i| g.dual_delta(Axis::X, i)).sum();
556        assert_eq!(s, 6.0);
557    }
558}