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}