Skip to main content

ems/
fdtd.rs

1//! Solver settings: the equivalent of the openEMS `<FDTD>` section.
2//!
3//! [`Fdtd`] collects everything that is not part of the model itself: the
4//! excitation [`Waveform`], the [`Boundaries`] of the six domain faces, the
5//! [`EndCriteria`], the timestep count and the time-step choice. This module
6//! only holds data and validates it; the engine interprets it.
7//!
8//! The file-level openEMS `<FDTD>` element is typed by csx
9//! ([`csx::fdtd::FdtdSettings`]); [`Fdtd::from_csx`] and [`Fdtd::to_csx`]
10//! convert between the two (see [`Fdtd::from_csx`] for the mapping).
11
12use crate::axis::Axis;
13use crate::error::{Error, Result};
14use crate::waveform::Waveform;
15use core::fmt;
16use core::ops::{Index, IndexMut};
17
18mod convert;
19
20/// One of the six faces of the computational domain.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub enum Face {
23    /// Face at the lowest x mesh line.
24    XMin,
25    /// Face at the highest x mesh line.
26    XMax,
27    /// Face at the lowest y mesh line.
28    YMin,
29    /// Face at the highest y mesh line.
30    YMax,
31    /// Face at the lowest z mesh line.
32    ZMin,
33    /// Face at the highest z mesh line.
34    ZMax,
35}
36
37impl Face {
38    /// All faces in index order (the order of the openEMS `<BoundaryCond>`
39    /// attributes: xmin, xmax, ymin, ymax, zmin, zmax).
40    pub const ALL: [Face; 6] = [
41        Face::XMin,
42        Face::XMax,
43        Face::YMin,
44        Face::YMax,
45        Face::ZMin,
46        Face::ZMax,
47    ];
48
49    /// Index in `0..6`: `2·axis + (1 if max side)`.
50    pub const fn index(self) -> usize {
51        self as usize
52    }
53
54    /// The face with index `i` (see [`Face::index`]).
55    pub const fn from_index(i: usize) -> Option<Face> {
56        if i < 6 { Some(Face::ALL[i]) } else { None }
57    }
58
59    /// The face of `axis` on the max (`true`) or min (`false`) side.
60    pub const fn new(axis: Axis, max: bool) -> Face {
61        Face::ALL[2 * axis.index() + max as usize]
62    }
63
64    /// The axis normal to the face.
65    pub const fn axis(self) -> Axis {
66        match self {
67            Face::XMin | Face::XMax => Axis::X,
68            Face::YMin | Face::YMax => Axis::Y,
69            Face::ZMin | Face::ZMax => Axis::Z,
70        }
71    }
72
73    /// `true` for the faces at the highest mesh line of their axis.
74    pub const fn is_max(self) -> bool {
75        self.index() % 2 == 1
76    }
77
78    /// The face on the other side of the domain.
79    pub const fn opposite(self) -> Face {
80        Face::new(self.axis(), !self.is_max())
81    }
82
83    /// Lower-case name, as in openEMS XML attributes: `"xmin"`, `"xmax"`, ….
84    pub const fn name(self) -> &'static str {
85        match self {
86            Face::XMin => "xmin",
87            Face::XMax => "xmax",
88            Face::YMin => "ymin",
89            Face::YMax => "ymax",
90            Face::ZMin => "zmin",
91            Face::ZMax => "zmax",
92        }
93    }
94}
95
96impl fmt::Display for Face {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        f.write_str(self.name())
99    }
100}
101
102/// First-order Mur absorbing boundary (G. Mur, "Absorbing boundary
103/// conditions for the finite-difference approximation of the time-domain
104/// electromagnetic-field equations", IEEE Trans. EMC 23(4), 1981).
105///
106/// The one-way wave equation is applied to the tangential E field of the
107/// face, tuned for waves leaving at normal incidence with the given phase
108/// velocity.
109#[derive(Debug, Clone, Copy, PartialEq, Default)]
110pub struct Mur {
111    /// Phase velocity the boundary is tuned for \[m/s\]; `None` means the
112    /// vacuum speed of light [`C0`](crate::constants::C0).
113    pub phase_velocity: Option<f64>,
114}
115
116impl Mur {
117    /// Mur boundary tuned for `c0`.
118    pub const fn new() -> Self {
119        Mur {
120            phase_velocity: None,
121        }
122    }
123
124    /// Mur boundary tuned for phase velocity `v` \[m/s\].
125    pub const fn with_phase_velocity(v: f64) -> Self {
126        Mur {
127            phase_velocity: Some(v),
128        }
129    }
130
131    /// The effective phase velocity \[m/s\].
132    pub fn velocity(&self) -> f64 {
133        self.phase_velocity.unwrap_or(crate::constants::C0)
134    }
135
136    /// Checks that the phase velocity (if set) is finite and positive.
137    pub fn validate(&self) -> Result<()> {
138        match self.phase_velocity {
139            Some(v) if !(v.is_finite() && v > 0.0) => Err(Error::Invalid(format!(
140                "Mur boundary: phase velocity {v} must be finite and > 0"
141            ))),
142            _ => Ok(()),
143        }
144    }
145}
146
147/// Convolutional perfectly matched layer (CPML) parameters.
148///
149/// Formulation of J. A. Roden and S. D. Gedney, "Convolutional PML (CPML):
150/// An efficient FDTD implementation of the CFS-PML for arbitrary media",
151/// Microwave and Optical Technology Letters 27(5), 2000; see also Taflove &
152/// Hagness, *Computational Electrodynamics*, 3rd ed., ch. 7.
153///
154/// The layer occupies the outermost `cells` cells of the mesh
155/// at the face (openEMS `PML_n` semantics). With `ρ ∈ [0, 1]` the normalised
156/// depth into the layer, the stretching parameters are graded as
157///
158/// ```text
159/// σ(ρ) = σ_max·ρ^m,   κ(ρ) = 1 + (κ_max − 1)·ρ^m,   α(ρ) = α_max·(1 − ρ)
160/// ```
161///
162/// with `m` = `grading_order` and `σ_max = sigma_factor · 0.8·(m + 1)/(η·Δ)`,
163/// the near-optimal value for polynomial grading given in Taflove & Hagness,
164/// ch. 7, with `Δ` the local cell size normal to the layer and
165/// `η = n·Z0`, `n` the mean refractive index of the layer's medium (the
166/// engine measures it per face, see
167/// [`CpmlBoundary`](crate::engine::boundary::cpml::CpmlBoundary)), so the
168/// default is near-optimal in dielectrics too; `sigma_factor` is relative
169/// to that optimum. The defaults (`m = 3`, `κ_max = 1`, `α_max = 0`) give a
170/// classic graded PML; `κ_max > 1` and `α_max > 0` enable the CFS terms.
171#[derive(Debug, Clone, Copy, PartialEq)]
172pub struct Cpml {
173    /// Number of mesh cells the layer occupies (default 8).
174    pub cells: usize,
175    /// Polynomial grading order `m` (default 3).
176    pub grading_order: f64,
177    /// Multiplier on the optimal `σ_max` (default 1).
178    pub sigma_factor: f64,
179    /// Maximum coordinate stretching `κ_max ≥ 1` (default 1).
180    pub kappa_max: f64,
181    /// Maximum complex-frequency shift `α_max ≥ 0` \[S/m\] (default 0).
182    pub alpha_max: f64,
183}
184
185impl Default for Cpml {
186    fn default() -> Self {
187        Cpml {
188            cells: 8,
189            grading_order: 3.0,
190            sigma_factor: 1.0,
191            kappa_max: 1.0,
192            alpha_max: 0.0,
193        }
194    }
195}
196
197impl Cpml {
198    /// Default CPML with `n` cells.
199    pub fn cells(n: usize) -> Self {
200        Cpml {
201            cells: n,
202            ..Cpml::default()
203        }
204    }
205
206    /// Sets the polynomial grading order `m`.
207    pub fn grading_order(mut self, m: f64) -> Self {
208        self.grading_order = m;
209        self
210    }
211
212    /// Sets the multiplier on the optimal `σ_max`.
213    pub fn sigma_factor(mut self, k: f64) -> Self {
214        self.sigma_factor = k;
215        self
216    }
217
218    /// Sets `κ_max`.
219    pub fn kappa_max(mut self, k: f64) -> Self {
220        self.kappa_max = k;
221        self
222    }
223
224    /// Sets `α_max` \[S/m\].
225    pub fn alpha_max(mut self, a: f64) -> Self {
226        self.alpha_max = a;
227        self
228    }
229
230    /// `σ_max = sigma_factor · 0.8·(m + 1)/(η·Δ)` \[S/m\] for `eta` \[Ω\]
231    /// (`n·Z0` for a layer of refractive index `n`) and cell size `delta`
232    /// \[m\] normal to the layer.
233    pub fn sigma_max(&self, eta: f64, delta: f64) -> f64 {
234        self.sigma_factor * 0.8 * (self.grading_order + 1.0) / (eta * delta)
235    }
236
237    /// Checks the parameters: `cells ≥ 1`, finite `m ≥ 0`, `sigma_factor ≥ 0`,
238    /// `κ_max ≥ 1`, `α_max ≥ 0`.
239    pub fn validate(&self) -> Result<()> {
240        let bad = |what: &str, v: f64, req: &str| {
241            Err(Error::Invalid(format!("CPML: {what} = {v} must be {req}")))
242        };
243        if self.cells == 0 {
244            return Err(Error::Invalid("CPML: needs at least one cell".into()));
245        }
246        if !(self.grading_order.is_finite() && self.grading_order >= 0.0) {
247            return bad("grading order", self.grading_order, "finite and >= 0");
248        }
249        if !(self.sigma_factor.is_finite() && self.sigma_factor >= 0.0) {
250            return bad("sigma factor", self.sigma_factor, "finite and >= 0");
251        }
252        if !(self.kappa_max.is_finite() && self.kappa_max >= 1.0) {
253            return bad("kappa_max", self.kappa_max, "finite and >= 1");
254        }
255        if !(self.alpha_max.is_finite() && self.alpha_max >= 0.0) {
256            return bad("alpha_max", self.alpha_max, "finite and >= 0");
257        }
258        Ok(())
259    }
260}
261
262/// Boundary condition of one domain face.
263#[derive(Debug, Clone, Copy, PartialEq, Default)]
264pub enum Boundary {
265    /// Perfect electric conductor: tangential E is zero on the face.
266    #[default]
267    Pec,
268    /// Perfect magnetic conductor: tangential H is zero on the face.
269    Pmc,
270    /// First-order Mur absorbing boundary.
271    Mur(Mur),
272    /// Convolutional PML.
273    Pml(Cpml),
274    /// Periodic (zero phase shift): the face is identified with the
275    /// opposite face, which must be periodic too. The period is the domain
276    /// length along the axis (first to last mesh line). ems-only: the
277    /// openEMS file format has no periodic boundary (see
278    /// [`Fdtd::to_csx`]).
279    Periodic,
280}
281
282impl Boundary {
283    /// Mur boundary tuned for `c0`.
284    pub const fn mur() -> Self {
285        Boundary::Mur(Mur::new())
286    }
287
288    /// Default CPML with `n` cells.
289    pub fn pml(n: usize) -> Self {
290        Boundary::Pml(Cpml::cells(n))
291    }
292
293    /// Number of mesh cells the boundary occupies inside the domain
294    /// (the PML thickness; 0 for the other kinds).
295    pub fn cells(&self) -> usize {
296        match self {
297            Boundary::Pml(p) => p.cells,
298            _ => 0,
299        }
300    }
301
302    /// Checks the boundary's parameters.
303    pub fn validate(&self) -> Result<()> {
304        match self {
305            Boundary::Pec | Boundary::Pmc | Boundary::Periodic => Ok(()),
306            Boundary::Mur(m) => m.validate(),
307            Boundary::Pml(p) => p.validate(),
308        }
309    }
310}
311
312/// Boundary conditions of all six faces (default: all PEC).
313#[derive(Debug, Clone, Copy, PartialEq, Default)]
314pub struct Boundaries([Boundary; 6]);
315
316impl Boundaries {
317    /// The same boundary on every face.
318    pub fn all(b: Boundary) -> Self {
319        Boundaries([b; 6])
320    }
321
322    /// Builder: sets the boundary of `face`.
323    pub fn with(mut self, face: Face, b: Boundary) -> Self {
324        self.0[face.index()] = b;
325        self
326    }
327
328    /// Sets the boundary of `face`.
329    pub fn set(&mut self, face: Face, b: Boundary) {
330        self.0[face.index()] = b;
331    }
332
333    /// The boundary of `face`.
334    pub fn get(&self, face: Face) -> &Boundary {
335        &self.0[face.index()]
336    }
337
338    /// The boundaries in [`Face::ALL`] order.
339    pub fn as_array(&self) -> &[Boundary; 6] {
340        &self.0
341    }
342
343    /// Iterates over `(face, boundary)` pairs in [`Face::ALL`] order.
344    pub fn iter(&self) -> impl Iterator<Item = (Face, &Boundary)> {
345        Face::ALL.into_iter().zip(self.0.iter())
346    }
347
348    /// Checks every face's parameters, and that periodic faces come in
349    /// opposite pairs.
350    pub fn validate(&self) -> Result<()> {
351        for (face, b) in self.iter() {
352            b.validate()
353                .map_err(|e| Error::Invalid(format!("boundary {face}: {e}")))?;
354            if *b == Boundary::Periodic && self[face.opposite()] != Boundary::Periodic {
355                return Err(Error::Invalid(format!(
356                    "boundary {face}: periodic needs the opposite face {} periodic too",
357                    face.opposite()
358                )));
359            }
360        }
361        Ok(())
362    }
363
364    /// Whether the faces of `axis` are periodic.
365    pub fn is_periodic(&self, axis: Axis) -> bool {
366        self[Face::new(axis, false)] == Boundary::Periodic
367            && self[Face::new(axis, true)] == Boundary::Periodic
368    }
369}
370
371impl From<[Boundary; 6]> for Boundaries {
372    fn from(b: [Boundary; 6]) -> Self {
373        Boundaries(b)
374    }
375}
376
377impl Index<Face> for Boundaries {
378    type Output = Boundary;
379    fn index(&self, face: Face) -> &Boundary {
380        &self.0[face.index()]
381    }
382}
383
384impl IndexMut<Face> for Boundaries {
385    fn index_mut(&mut self, face: Face) -> &mut Boundary {
386        &mut self.0[face.index()]
387    }
388}
389
390/// When to stop a simulation before the maximum timestep count: once the
391/// excitation is over and the total field energy has decayed below
392/// `energy_ratio` times its maximum.
393#[derive(Debug, Clone, Copy, PartialEq)]
394pub struct EndCriteria {
395    /// Threshold as a linear energy ratio `W/W_max` in `(0, 1]`.
396    pub energy_ratio: f64,
397    /// Number of timesteps between energy evaluations (default 50).
398    pub check_interval: u64,
399}
400
401impl EndCriteria {
402    /// Default `check_interval`.
403    pub const DEFAULT_CHECK_INTERVAL: u64 = 50;
404
405    /// Stop when the energy is `db` decibels below its maximum, e.g.
406    /// `-40.0` for a ratio of 1e-4 (energy is a power quantity:
407    /// `ratio = 10^(db/10)`).
408    pub fn energy_decay_db(db: f64) -> Self {
409        EndCriteria::energy_ratio(10f64.powf(db / 10.0))
410    }
411
412    /// Stop when `W/W_max < ratio`.
413    pub fn energy_ratio(ratio: f64) -> Self {
414        EndCriteria {
415            energy_ratio: ratio,
416            check_interval: Self::DEFAULT_CHECK_INTERVAL,
417        }
418    }
419
420    /// Builder: sets the number of timesteps between energy evaluations.
421    pub fn check_interval(mut self, steps: u64) -> Self {
422        self.check_interval = steps;
423        self
424    }
425
426    /// The threshold in decibels, `10·log10(energy_ratio)`.
427    pub fn decay_db(&self) -> f64 {
428        10.0 * self.energy_ratio.log10()
429    }
430
431    /// Checks that the ratio is in `(0, 1]` and the interval is at least 1.
432    pub fn validate(&self) -> Result<()> {
433        let r = self.energy_ratio;
434        if !(r > 0.0 && r <= 1.0) {
435            return Err(Error::Invalid(format!(
436                "end criteria: energy ratio {r} must be in (0, 1]"
437            )));
438        }
439        if self.check_interval == 0 {
440            return Err(Error::Invalid(
441                "end criteria: check interval must be at least 1 step".into(),
442            ));
443        }
444        Ok(())
445    }
446}
447
448/// FDTD solver settings (the openEMS `<FDTD>` section).
449///
450/// Built with chained setters; read back with the `get_*` getters (the plain
451/// names are taken by the setters, as in [`std::process::Command`]).
452///
453/// ```
454/// use ems::prelude::*;
455/// let fdtd = Fdtd::new()
456///     .waveform(Waveform::gaussian(2e9, 1e9))
457///     .boundaries(Boundaries::all(Boundary::Pml(Cpml::cells(8))))
458///     .end_criteria(EndCriteria::energy_decay_db(-40.0))
459///     .max_timesteps(200_000);
460/// fdtd.validate()?;
461/// assert_eq!(fdtd.get_max_timesteps(), 200_000);
462/// # Ok::<(), ems::Error>(())
463/// ```
464#[derive(Debug, Clone)]
465pub struct Fdtd {
466    waveform: Option<Waveform>,
467    boundaries: Boundaries,
468    end_criteria: Option<EndCriteria>,
469    max_timesteps: u64,
470    timestep_factor: f64,
471    timestep: Option<f64>,
472    threads: Option<usize>,
473    max_frequency: Option<f64>,
474    max_time: Option<f64>,
475    oversampling: Option<u32>,
476    bloch_wavevector: Option<[f64; 3]>,
477    bloch_frequency: Option<f64>,
478    multigrid: Vec<f64>,
479}
480
481impl Default for Fdtd {
482    fn default() -> Self {
483        Fdtd {
484            waveform: None,
485            boundaries: Boundaries::default(),
486            end_criteria: None,
487            max_timesteps: Self::DEFAULT_MAX_TIMESTEPS,
488            timestep_factor: Self::DEFAULT_TIMESTEP_FACTOR,
489            timestep: None,
490            threads: None,
491            max_frequency: None,
492            max_time: None,
493            oversampling: None,
494            bloch_wavevector: None,
495            bloch_frequency: None,
496            multigrid: Vec::new(),
497        }
498    }
499}
500
501impl Fdtd {
502    /// Default [`max_timesteps`](Fdtd::max_timesteps).
503    pub const DEFAULT_MAX_TIMESTEPS: u64 = 1_000_000;
504    /// Default [`timestep_factor`](Fdtd::timestep_factor) (Courant number
505    /// safety factor).
506    pub const DEFAULT_TIMESTEP_FACTOR: f64 = 0.99;
507
508    /// Default settings: no waveform, all-PEC boundaries, no end criteria,
509    /// [`DEFAULT_MAX_TIMESTEPS`](Fdtd::DEFAULT_MAX_TIMESTEPS) steps, time
510    /// step factor [`DEFAULT_TIMESTEP_FACTOR`](Fdtd::DEFAULT_TIMESTEP_FACTOR),
511    /// automatic time step and thread count.
512    pub fn new() -> Self {
513        Fdtd::default()
514    }
515
516    /// Sets the excitation waveform shared by all excitations of the model.
517    pub fn waveform(mut self, w: Waveform) -> Self {
518        self.waveform = Some(w);
519        self
520    }
521
522    /// Sets the boundary conditions of all faces.
523    pub fn boundaries(mut self, b: Boundaries) -> Self {
524        self.boundaries = b;
525        self
526    }
527
528    /// Sets the boundary condition of one face.
529    pub fn boundary(mut self, face: Face, b: Boundary) -> Self {
530        self.boundaries.set(face, b);
531        self
532    }
533
534    /// Enables early termination by energy decay. Without end criteria the
535    /// simulation runs for [`max_timesteps`](Fdtd::max_timesteps).
536    pub fn end_criteria(mut self, e: EndCriteria) -> Self {
537        self.end_criteria = Some(e);
538        self
539    }
540
541    /// Removes the end criteria (run to the maximum timestep count).
542    pub fn no_end_criteria(mut self) -> Self {
543        self.end_criteria = None;
544        self
545    }
546
547    /// Sets the maximum number of timesteps (≥ 1).
548    pub fn max_timesteps(mut self, n: u64) -> Self {
549        self.max_timesteps = n;
550        self
551    }
552
553    /// Sets the Courant safety factor `S ∈ (0, 1]`: the automatic time step
554    /// is `S` times the Courant–Friedrichs–Lewy limit of the mesh.
555    pub fn timestep_factor(mut self, s: f64) -> Self {
556        self.timestep_factor = s;
557        self
558    }
559
560    /// Overrides the automatic time step with `dt` \[s\]. It must still
561    /// satisfy the stability limit of the mesh (checked when compiling).
562    pub fn timestep(mut self, dt: f64) -> Self {
563        self.timestep = Some(dt);
564        self
565    }
566
567    /// Sets the number of worker threads (≥ 1).
568    pub fn threads(mut self, n: usize) -> Self {
569        self.threads = Some(n);
570        self
571    }
572
573    /// Sets the highest frequency of interest `f` \[Hz\] (openEMS `f_max`).
574    /// Needed for waveforms without a bounded spectrum (Dirac, step) when
575    /// the mesh resolution is checked; see [`Fdtd::get_max_frequency`].
576    pub fn max_frequency(mut self, f: f64) -> Self {
577        self.max_frequency = Some(f);
578        self
579    }
580
581    /// Limits the simulated (physical) time to `t` \[s\] (openEMS
582    /// `MaxTime`); the run stops after `ceil(t/dt)` steps if that is fewer
583    /// than [`max_timesteps`](Fdtd::max_timesteps).
584    pub fn max_time(mut self, t: f64) -> Self {
585        self.max_time = Some(t);
586        self
587    }
588
589    /// Sets the probe / time-domain dump oversampling factor relative to
590    /// the Nyquist rate of the maximum frequency (openEMS `OverSampling`,
591    /// default 4). Stored for interoperability; not yet used by the engine.
592    pub fn oversampling(mut self, n: u32) -> Self {
593        self.oversampling = Some(n);
594        self
595    }
596
597    /// Bloch (Floquet) boundaries with the fixed transverse wavevector `k`
598    /// \[rad/m\] (design §5.6.2): every axis with a non-zero component
599    /// must have [`Boundary::Periodic`] on both faces, and the phase shift
600    /// per period becomes `ψ = k·L`. The simulation then runs the
601    /// sine–cosine method (two engines, see
602    /// [`BlochPair`](crate::engine::BlochPair)) and reports complex results
603    /// (see the Bloch mode of [`Simulation`](crate::Simulation)). With a
604    /// fixed `k` each frequency `f` sees its own incidence angle,
605    /// `sin θ = |k|·c/(2πf)`.
606    ///
607    /// Without this setting, an oblique plane-wave excitation spanning a
608    /// periodic cell selects the Bloch mode, with `k` derived from its
609    /// direction at the [`bloch_frequency`](Fdtd::bloch_frequency).
610    /// `k = [0; 3]` forces the Bloch mode at normal incidence.
611    pub fn bloch_wavevector(mut self, k: [f64; 3]) -> Self {
612        self.bloch_wavevector = Some(k);
613        self
614    }
615
616    /// The design frequency \[Hz\] at which an oblique plane wave in a
617    /// periodic cell travels along its propagation direction (default: the
618    /// plane wave's dispersion-matching frequency, i.e. the centre of the
619    /// waveform); see [`bloch_wavevector`](Fdtd::bloch_wavevector).
620    pub fn bloch_frequency(mut self, f: f64) -> Self {
621        self.bloch_frequency = Some(f);
622        self
623    }
624
625    /// Azimuthal multigrid near the axis of a cylindrical grid (openEMS
626    /// `MultiGrid`, design §5.15.1): radii in **drawing units** (like the
627    /// mesh lines). Inside each radius the number of α lines is halved
628    /// once more, so inside the smallest of `L` radii only every `2^L`-th
629    /// α line is kept and the short arcs next to the axis no longer set
630    /// the time step. Each radius snaps to the nearest `ρ` mesh line, which
631    /// must have a cell on both sides; the number of α cells must be a
632    /// multiple of `2^L`. Ignored (with a warning) on Cartesian grids. An
633    /// empty list (the default) switches multigrid off.
634    ///
635    /// The zone boundaries couple the zones to second order for the z
636    /// field and first order (in the arc `ρ·Δα`) for the azimuthal one, so
637    /// keep the radii small: the time step is set by the innermost cells
638    /// anyway (see [`Grid::with_multigrid`](crate::engine::Grid::with_multigrid)).
639    pub fn multigrid(mut self, radii: impl Into<Vec<f64>>) -> Self {
640        self.multigrid = radii.into();
641        self
642    }
643
644    /// The multigrid radii (drawing units); empty when off.
645    pub fn get_multigrid(&self) -> &[f64] {
646        &self.multigrid
647    }
648
649    /// The explicit Bloch wavevector \[rad/m\], if set.
650    pub fn get_bloch_wavevector(&self) -> Option<[f64; 3]> {
651        self.bloch_wavevector
652    }
653
654    /// The Bloch design frequency \[Hz\], if set.
655    pub fn get_bloch_frequency(&self) -> Option<f64> {
656        self.bloch_frequency
657    }
658
659    /// The highest frequency of interest \[Hz\]: the explicit
660    /// [`max_frequency`](Fdtd::max_frequency), else the waveform's
661    /// [`Waveform::max_frequency`].
662    pub fn get_max_frequency(&self) -> Option<f64> {
663        self.max_frequency
664            .or_else(|| self.waveform.as_ref().and_then(Waveform::max_frequency))
665    }
666
667    /// The explicitly set highest frequency of interest \[Hz\], if any.
668    pub fn get_explicit_max_frequency(&self) -> Option<f64> {
669        self.max_frequency
670    }
671
672    /// The simulated time limit \[s\], if any.
673    pub fn get_max_time(&self) -> Option<f64> {
674        self.max_time
675    }
676
677    /// The oversampling factor, if set.
678    pub fn get_oversampling(&self) -> Option<u32> {
679        self.oversampling
680    }
681
682    /// The step limit for time step `dt` \[s\]: [`max_timesteps`](Fdtd::max_timesteps),
683    /// reduced to `ceil(max_time/dt)` (at least 1) when a
684    /// [`max_time`](Fdtd::max_time) is set.
685    pub fn timestep_limit(&self, dt: f64) -> u64 {
686        match self.max_time {
687            Some(t) if dt > 0.0 => {
688                // Round first so that e.g. 1e-9 / 1e-11 = 100.000…01 gives 100.
689                let r = t / dt;
690                let n = if (r - r.round()).abs() <= 1e-9 * r {
691                    r.round()
692                } else {
693                    r.ceil()
694                };
695                if n < self.max_timesteps as f64 {
696                    (n as u64).max(1)
697                } else {
698                    self.max_timesteps
699                }
700            }
701            _ => self.max_timesteps,
702        }
703    }
704
705    /// The excitation waveform, if set.
706    pub fn get_waveform(&self) -> Option<&Waveform> {
707        self.waveform.as_ref()
708    }
709
710    /// The boundary conditions.
711    pub fn get_boundaries(&self) -> &Boundaries {
712        &self.boundaries
713    }
714
715    /// Mutable access to the boundary conditions.
716    pub fn boundaries_mut(&mut self) -> &mut Boundaries {
717        &mut self.boundaries
718    }
719
720    /// The end criteria, if any.
721    pub fn get_end_criteria(&self) -> Option<&EndCriteria> {
722        self.end_criteria.as_ref()
723    }
724
725    /// The maximum number of timesteps.
726    pub fn get_max_timesteps(&self) -> u64 {
727        self.max_timesteps
728    }
729
730    /// The Courant safety factor `S`.
731    pub fn get_timestep_factor(&self) -> f64 {
732        self.timestep_factor
733    }
734
735    /// The time step override \[s\], if any.
736    pub fn get_timestep(&self) -> Option<f64> {
737        self.timestep
738    }
739
740    /// The configured thread count; `None` lets the engine choose
741    /// ([`engine::default_threads`](crate::engine::default_threads)).
742    pub fn get_threads(&self) -> Option<usize> {
743        self.threads
744    }
745
746    /// The thread count for the compile step (discretisation): the
747    /// configured one, else [`std::thread::available_parallelism`] (1 if
748    /// unknown). The engine's default is
749    /// [`engine::default_threads`](crate::engine::default_threads).
750    pub fn resolved_threads(&self) -> usize {
751        self.threads.unwrap_or_else(|| {
752            std::thread::available_parallelism()
753                .map(|n| n.get())
754                .unwrap_or(1)
755        })
756    }
757
758    /// Checks all settings that can be checked without the mesh.
759    ///
760    /// A missing waveform is not an error here: whether one is needed
761    /// depends on the model's excitations, which the compile step checks.
762    pub fn validate(&self) -> Result<()> {
763        if let Some(w) = &self.waveform {
764            w.validate()?;
765        }
766        self.boundaries.validate()?;
767        if let Some(e) = &self.end_criteria {
768            e.validate()?;
769        }
770        if self.max_timesteps == 0 {
771            return Err(Error::Invalid("max_timesteps must be at least 1".into()));
772        }
773        let s = self.timestep_factor;
774        if !(s > 0.0 && s <= 1.0) {
775            return Err(Error::Invalid(format!(
776                "timestep factor {s} must be in (0, 1]"
777            )));
778        }
779        if let Some(r) = self
780            .multigrid
781            .iter()
782            .find(|r| !(r.is_finite() && **r > 0.0))
783        {
784            return Err(Error::Invalid(format!(
785                "multigrid radius {r} must be finite and > 0"
786            )));
787        }
788        if let Some(dt) = self.timestep
789            && !(dt.is_finite() && dt > 0.0)
790        {
791            return Err(Error::Invalid(format!(
792                "timestep {dt} must be finite and > 0"
793            )));
794        }
795        if self.threads == Some(0) {
796            return Err(Error::Invalid("thread count must be at least 1".into()));
797        }
798        if let Some(f) = self.max_frequency
799            && !(f.is_finite() && f > 0.0)
800        {
801            return Err(Error::Invalid(format!(
802                "maximum frequency {f} must be finite and > 0"
803            )));
804        }
805        if let Some(t) = self.max_time
806            && !(t.is_finite() && t > 0.0)
807        {
808            return Err(Error::Invalid(format!(
809                "maximum time {t} must be finite and > 0"
810            )));
811        }
812        if self.oversampling == Some(0) {
813            return Err(Error::Invalid("oversampling must be at least 1".into()));
814        }
815        if let Some(k) = self.bloch_wavevector {
816            for a in Axis::ALL {
817                let ka = k[a.index()];
818                if !ka.is_finite() {
819                    return Err(Error::Invalid(format!(
820                        "Bloch wavevector component {ka} along {a} must be finite"
821                    )));
822                }
823                if ka != 0.0 && !self.boundaries.is_periodic(a) {
824                    return Err(Error::Invalid(format!(
825                        "Bloch wavevector has a component along {a}, whose faces are not \
826                         periodic"
827                    )));
828                }
829            }
830        }
831        if let Some(f) = self.bloch_frequency
832            && !(f.is_finite() && f > 0.0)
833        {
834            return Err(Error::Invalid(format!(
835                "Bloch design frequency {f} must be finite and > 0"
836            )));
837        }
838        Ok(())
839    }
840}
841
842#[cfg(test)]
843mod tests {
844    use super::*;
845    use crate::constants::{C0, Z0};
846
847    #[test]
848    fn face_helpers() {
849        for (i, f) in Face::ALL.into_iter().enumerate() {
850            assert_eq!(f.index(), i);
851            assert_eq!(Face::from_index(i), Some(f));
852            assert_eq!(Face::new(f.axis(), f.is_max()), f);
853            assert_eq!(f.opposite().opposite(), f);
854            assert_ne!(f.opposite(), f);
855            assert_eq!(f.opposite().axis(), f.axis());
856        }
857        assert_eq!(Face::from_index(6), None);
858        assert_eq!(Face::new(Axis::Y, true), Face::YMax);
859        assert_eq!(Face::ZMin.axis(), Axis::Z);
860        assert!(!Face::XMin.is_max());
861        assert_eq!(Face::XMax.to_string(), "xmax");
862    }
863
864    #[test]
865    fn boundaries() {
866        let b = Boundaries::default();
867        assert!(b.iter().all(|(_, b)| *b == Boundary::Pec));
868        let b = Boundaries::all(Boundary::pml(8))
869            .with(Face::ZMin, Boundary::Pec)
870            .with(Face::XMax, Boundary::mur());
871        assert_eq!(*b.get(Face::ZMin), Boundary::Pec);
872        assert_eq!(b[Face::XMax], Boundary::Mur(Mur::new()));
873        assert_eq!(b[Face::YMin], Boundary::Pml(Cpml::default()));
874        assert_eq!(b[Face::YMin].cells(), 8);
875        assert_eq!(b[Face::ZMin].cells(), 0);
876        let mut b2 = b;
877        b2[Face::ZMin] = Boundary::Pmc;
878        b2.set(Face::ZMax, Boundary::Pmc);
879        assert_eq!(b2.as_array()[4], Boundary::Pmc);
880        assert_eq!(b2.as_array()[5], Boundary::Pmc);
881        assert!(b.validate().is_ok());
882        let bad = b.with(Face::YMax, Boundary::pml(0));
883        let e = bad.validate().unwrap_err().to_string();
884        assert!(e.contains("ymax"), "{e}");
885        assert_eq!(
886            Boundaries::from([Boundary::Pmc; 6]),
887            Boundaries::all(Boundary::Pmc)
888        );
889    }
890
891    #[test]
892    fn periodic_pairs() {
893        let b = Boundaries::all(Boundary::pml(8))
894            .with(Face::XMin, Boundary::Periodic)
895            .with(Face::XMax, Boundary::Periodic);
896        assert!(b.validate().is_ok());
897        assert!(b.is_periodic(Axis::X));
898        assert!(!b.is_periodic(Axis::Y));
899        let e = b
900            .with(Face::YMax, Boundary::Periodic)
901            .validate()
902            .unwrap_err()
903            .to_string();
904        assert!(e.contains("ymax") && e.contains("ymin"), "{e}");
905        assert_eq!(Boundary::Periodic.cells(), 0);
906    }
907
908    #[test]
909    fn cpml() {
910        let p = Cpml::cells(10)
911            .grading_order(4.0)
912            .sigma_factor(0.5)
913            .kappa_max(5.0)
914            .alpha_max(0.05);
915        assert_eq!(p.cells, 10);
916        assert_eq!(p.grading_order, 4.0);
917        assert!(p.validate().is_ok());
918        let d = Cpml::default();
919        assert_eq!((d.cells, d.grading_order, d.sigma_factor), (8, 3.0, 1.0));
920        assert_eq!((d.kappa_max, d.alpha_max), (1.0, 0.0));
921        let s = d.sigma_max(Z0, 1e-3);
922        assert!((s - 0.8 * 4.0 / (Z0 * 1e-3)).abs() < 1e-9 * s);
923        assert!((p.sigma_max(Z0, 1e-3) - 0.5 * 0.8 * 5.0 / (Z0 * 1e-3)).abs() < 1e-9);
924        assert!(Cpml::cells(0).validate().is_err());
925        assert!(d.kappa_max(0.5).validate().is_err());
926        assert!(d.alpha_max(-1.0).validate().is_err());
927        assert!(d.sigma_factor(f64::NAN).validate().is_err());
928        assert!(d.grading_order(-1.0).validate().is_err());
929    }
930
931    #[test]
932    fn mur() {
933        assert_eq!(Mur::new().velocity(), C0);
934        assert_eq!(Mur::with_phase_velocity(1e8).velocity(), 1e8);
935        assert!(Mur::new().validate().is_ok());
936        assert!(Mur::with_phase_velocity(0.0).validate().is_err());
937        assert!(
938            Boundary::Mur(Mur::with_phase_velocity(f64::INFINITY))
939                .validate()
940                .is_err()
941        );
942    }
943
944    #[test]
945    fn end_criteria() {
946        let e = EndCriteria::energy_decay_db(-40.0);
947        assert!((e.energy_ratio - 1e-4).abs() < 1e-18);
948        assert!((e.decay_db() + 40.0).abs() < 1e-12);
949        assert_eq!(e.check_interval, EndCriteria::DEFAULT_CHECK_INTERVAL);
950        let e = EndCriteria::energy_ratio(1e-5).check_interval(10);
951        assert_eq!(e.check_interval, 10);
952        assert!(e.validate().is_ok());
953        assert!(e.check_interval(0).validate().is_err());
954        assert!(EndCriteria::energy_ratio(0.0).validate().is_err());
955        assert!(EndCriteria::energy_ratio(1.5).validate().is_err());
956        assert!(EndCriteria::energy_ratio(f64::NAN).validate().is_err());
957        assert!(EndCriteria::energy_decay_db(0.0).validate().is_ok());
958    }
959
960    #[test]
961    fn fdtd_builder_and_getters() {
962        let f = Fdtd::new();
963        assert!(f.get_waveform().is_none());
964        assert_eq!(*f.get_boundaries(), Boundaries::default());
965        assert!(f.get_end_criteria().is_none());
966        assert_eq!(f.get_max_timesteps(), Fdtd::DEFAULT_MAX_TIMESTEPS);
967        assert_eq!(f.get_timestep_factor(), 0.99);
968        assert_eq!(f.get_timestep(), None);
969        assert_eq!(f.get_threads(), None);
970        assert!(f.resolved_threads() >= 1);
971        assert!(f.validate().is_ok());
972
973        let mut f = Fdtd::new()
974            .waveform(Waveform::gaussian(1e9, 0.5e9))
975            .boundaries(Boundaries::all(Boundary::pml(8)))
976            .boundary(Face::ZMin, Boundary::Pec)
977            .end_criteria(EndCriteria::energy_decay_db(-50.0))
978            .max_timesteps(1234)
979            .timestep_factor(0.5)
980            .timestep(1e-12)
981            .threads(3);
982        assert!(f.validate().is_ok());
983        assert!(matches!(
984            f.get_waveform(),
985            Some(Waveform::Gaussian { f0: 1e9, .. })
986        ));
987        assert_eq!(f.get_boundaries()[Face::ZMin], Boundary::Pec);
988        assert_eq!(f.get_boundaries()[Face::ZMax], Boundary::pml(8));
989        assert!(f.get_end_criteria().is_some());
990        assert_eq!(f.get_max_timesteps(), 1234);
991        assert_eq!(f.get_timestep_factor(), 0.5);
992        assert_eq!(f.get_timestep(), Some(1e-12));
993        assert_eq!((f.get_threads(), f.resolved_threads()), (Some(3), 3));
994        f.boundaries_mut()[Face::XMin] = Boundary::Pmc;
995        assert_eq!(f.get_boundaries()[Face::XMin], Boundary::Pmc);
996        assert!(f.clone().no_end_criteria().get_end_criteria().is_none());
997    }
998
999    #[test]
1000    fn fdtd_validation() {
1001        let ok = Fdtd::new();
1002        assert!(
1003            ok.clone()
1004                .waveform(Waveform::gaussian(1e9, 0.0))
1005                .validate()
1006                .is_err()
1007        );
1008        assert!(
1009            ok.clone()
1010                .boundary(Face::XMin, Boundary::pml(0))
1011                .validate()
1012                .is_err()
1013        );
1014        assert!(
1015            ok.clone()
1016                .end_criteria(EndCriteria::energy_ratio(-1.0))
1017                .validate()
1018                .is_err()
1019        );
1020        assert!(ok.clone().max_timesteps(0).validate().is_err());
1021        assert!(ok.clone().timestep_factor(0.0).validate().is_err());
1022        assert!(ok.clone().timestep_factor(1.01).validate().is_err());
1023        assert!(ok.clone().timestep_factor(f64::NAN).validate().is_err());
1024        assert!(ok.clone().timestep_factor(1.0).validate().is_ok());
1025        assert!(ok.clone().timestep(0.0).validate().is_err());
1026        assert!(ok.clone().timestep(f64::INFINITY).validate().is_err());
1027        assert!(ok.clone().threads(0).validate().is_err());
1028        assert!(ok.clone().max_frequency(0.0).validate().is_err());
1029        assert!(ok.clone().max_time(f64::NAN).validate().is_err());
1030        assert!(ok.clone().oversampling(0).validate().is_err());
1031    }
1032
1033    #[test]
1034    fn max_frequency_time_and_oversampling() {
1035        let f = Fdtd::new();
1036        assert_eq!(f.get_max_frequency(), None);
1037        assert_eq!(f.get_max_time(), None);
1038        assert_eq!(f.get_oversampling(), None);
1039        assert_eq!(f.timestep_limit(1e-12), Fdtd::DEFAULT_MAX_TIMESTEPS);
1040        let f = f.waveform(Waveform::gaussian(1e9, 5e8));
1041        assert_eq!(f.get_max_frequency(), Some(1.5e9));
1042        assert_eq!(f.get_explicit_max_frequency(), None);
1043        let f = f
1044            .max_frequency(3e9)
1045            .max_time(1e-9)
1046            .oversampling(8)
1047            .max_timesteps(500);
1048        assert!(f.validate().is_ok());
1049        assert_eq!(f.get_max_frequency(), Some(3e9));
1050        assert_eq!(
1051            (f.get_max_time(), f.get_oversampling()),
1052            (Some(1e-9), Some(8))
1053        );
1054        assert_eq!(f.timestep_limit(1e-11), 100);
1055        assert_eq!(f.timestep_limit(3e-12), 334);
1056        assert_eq!(f.timestep_limit(1e-13), 500);
1057        assert_eq!(f.timestep_limit(1.0), 1);
1058    }
1059}