Skip to main content

ems/engine/
bloch_wave.rs

1//! Plane-wave injection into a periodic unit cell at fixed transverse
2//! wavevector (design §5.6.2): the oblique-incidence source of the Bloch
3//! (sine–cosine) method.
4//!
5//! # Geometry
6//!
7//! The unit cell is periodic (or Bloch-periodic) along the two transverse
8//! axes and open along the **normal** axis `n`. The total-field region is
9//! the slab of node planes `start ..= stop` along `n`, spanning the whole
10//! cell transversally; the wave enters through the plane `start` (forward,
11//! `+n`) or `stop` (backward, `−n`). The other plane removes the incident
12//! wave again (scattered-field region beyond it), unless it lies on the
13//! domain boundary (a plane on the boundary gets no corrections, so the
14//! total-field region then reaches through the absorbing layer).
15//!
16//! The corrections are those of the total-field / scattered-field method
17//! ([`crate::engine::planewave`], consistency conditions): the tangential
18//! E edges on the two planes get `+cb·K·i_inc` for their scattered-field H
19//! neighbours, and those H edges get `+db·K·u_inc`.
20//!
21//! # Incident field at fixed transverse wavevector
22//!
23//! The incident field is `F(r, t) = F̂(ζ, t)·e^{−j k_t·(r − r_0)}` with
24//! `ζ` the coordinate along `n`, `r_0` the first mesh line of every axis
25//! and a fixed transverse wavevector `k_t` (`k_t·n̂ = 0`). Inserting it
26//! into Maxwell's equations replaces every transverse derivative by
27//! `−j k̃_t`, which leaves a **complex 1-D problem** for the six
28//! components `F̂(ζ, t)`. On the Yee grid the transverse difference of two
29//! neighbouring samples of `e^{−jk x}` is exactly `−j k̃·(sample between
30//! them)` with the discrete wavenumber
31//!
32//! ```text
33//! k̃ = 2·sin(k·Δ/2)/Δ
34//! ```
35//!
36//! for a uniform transverse spacing `Δ`, so the 1-D problem *is* the 3-D
37//! Yee scheme restricted to Bloch-phased fields. It is integrated on an
38//! auxiliary grid that reuses the 3-D mesh lines along `n` (so its update
39//! is the 3-D update of the incident field, edge for edge) with the
40//! engine's time step. The incident field therefore leaks into the
41//! scattered-field region only at `f32` rounding on a uniform transverse
42//! mesh. On a graded transverse mesh `k̃` is taken with the mean spacing
43//! and the incident field is approximate.
44//!
45//! The auxiliary grid is driven by a **hard source** on its tangential E
46//! one or two cells upstream of the entry plane, `Ê_t = p·a(t)` with the
47//! tangential polarisation `p` (V/m at amplitude 1 of the time function),
48//! and ends in a graded CPML along `ζ` (reflectionless for any `k_t`, in
49//! the continuum). Its field at the entry plane
50//! is recorded each step ([`incident`](PeriodicPlaneWave::incident)): it
51//! is `p·a(t)` filtered by the short upstream path, the reference against
52//! which reflection and transmission are measured.
53//!
54//! # Constant transverse wavevector
55//!
56//! With `k_t` fixed, each frequency travels at its own angle,
57//! `sin θ(f) = |k_t|·v/(2πf)` (`v` the background phase velocity), and
58//! frequencies below `f_c = |k_t|·v/(2π)` are evanescent (grazing at
59//! `f_c`). This is inherent to the sine–cosine method. The tangential E of
60//! the incident wave is `p` at every frequency; its normal component
61//! follows from the field equations.
62//!
63//! # Real and imaginary part
64//!
65//! The Bloch method carries the complex field in two real engines. One
66//! operator per engine is needed, each running the same auxiliary grid
67//! (cheap: 1-D) and injecting its own part
68//! ([`WavePart::Real`]: `Re(F̂·e^{−jφ})`, [`WavePart::Imag`]:
69//! `Im(F̂·e^{−jφ})`). With `k_t = 0` the imaginary part is zero.
70
71use super::Engine;
72use super::excite::Amplitude;
73use super::fields::Fields;
74use super::operator::{Hooks, Operator, StepContext};
75use crate::axis::Axis;
76use crate::constants::{C0, EPS0, MU0};
77use crate::post::Complex64;
78use crate::{Error, Result};
79
80/// Which part of the complex incident field an operator injects.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum WavePart {
83    /// `Re(F̂·e^{−jφ})` (the real engine of a Bloch pair, or a plain
84    /// periodic engine at `k_t = 0`).
85    Real,
86    /// `Im(F̂·e^{−jφ})` (the imaginary engine of a Bloch pair).
87    Imag,
88}
89
90/// Geometry and physics of a [`PeriodicPlaneWave`] (see the
91/// [module docs](self)).
92#[derive(Debug, Clone, PartialEq)]
93pub struct PeriodicPlaneWaveSpec {
94    /// Normal axis of the injection planes.
95    pub axis: Axis,
96    /// First node plane of the total-field region along `axis`.
97    pub start: usize,
98    /// Last node plane of the total-field region (`start < stop`).
99    pub stop: usize,
100    /// Propagation towards `+axis` (enters at `start`) or `−axis` (enters
101    /// at `stop`).
102    pub forward: bool,
103    /// Transverse wavevector \[rad/m\] (component along `axis` must be 0).
104    pub k_t: [f64; 3],
105    /// Tangential incident E \[V/m\] at amplitude 1 of the time function
106    /// (component along `axis` must be 0).
107    pub polarization: [f64; 3],
108    /// Relative permittivity of the background at the injection planes.
109    pub eps_r: f64,
110    /// Relative permeability of the background at the injection planes.
111    pub mu_r: f64,
112}
113
114impl PeriodicPlaneWaveSpec {
115    /// Total-field slab `start..=stop` along `axis`, direction, transverse
116    /// wavevector and tangential polarisation; vacuum background.
117    pub fn new(
118        axis: Axis,
119        start: usize,
120        stop: usize,
121        forward: bool,
122        k_t: [f64; 3],
123        polarization: [f64; 3],
124    ) -> Self {
125        Self {
126            axis,
127            start,
128            stop,
129            forward,
130            k_t,
131            polarization,
132            eps_r: 1.0,
133            mu_r: 1.0,
134        }
135    }
136
137    /// Sets the background material at the injection planes.
138    #[must_use]
139    pub fn with_background(mut self, eps_r: f64, mu_r: f64) -> Self {
140        self.eps_r = eps_r;
141        self.mu_r = mu_r;
142        self
143    }
144
145    /// The incidence angle \[rad\] at frequency `f` \[Hz\] (`None` below the
146    /// cut-off `|k_t|·v/(2π)`, where the wave is evanescent).
147    pub fn angle(&self, f: f64) -> Option<f64> {
148        let kt = norm(self.k_t);
149        let k = 2.0 * core::f64::consts::PI * f * (self.eps_r * self.mu_r).sqrt() / C0;
150        (kt <= k).then(|| (kt / k).asin())
151    }
152}
153
154fn norm(v: [f64; 3]) -> f64 {
155    (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
156}
157
158/// One correction: `field[axis][node] += coef·(wr·re + wi·im)` of the
159/// auxiliary component `comp` at index `m`.
160#[derive(Debug, Clone, Copy)]
161struct Correction {
162    axis: Axis,
163    node: [usize; 3],
164    comp: usize,
165    m: usize,
166    wr: f64,
167    wi: f64,
168}
169
170/// Cells of the CPML at the far end of the auxiliary grid.
171const AUX_PML: usize = 64;
172/// Polynomial grading order of that layer.
173const AUX_PML_M: f64 = 3.0;
174
175/// The complex 1-D grid (see the [module docs](self)).
176#[derive(Debug, Clone)]
177struct Aux {
178    n: usize,
179    /// Discrete transverse wavenumbers `k̃` (0 along `n`).
180    kt: [f64; 3],
181    /// Primary spacings `Δ_j` (between nodes `j` and `j + 1`).
182    d: Vec<f64>,
183    /// Dual spacings at nodes.
184    dd: Vec<f64>,
185    /// `dt/ε` and `dt/μ` of the background.
186    cb: f64,
187    db: f64,
188    /// CPML decay factors `b = e^{−σ·dt/ε0}` of the `ζ` derivatives at
189    /// nodes (for tangential E) and duals (for tangential H); 1 outside the
190    /// layer.
191    b_node: Vec<f64>,
192    b_dual: Vec<f64>,
193    /// Field components (complex): tangential E at nodes, normal E at
194    /// duals; tangential H at duals, normal H at nodes.
195    e: [Vec<Complex64>; 3],
196    h: [Vec<Complex64>; 3],
197    /// CPML convolution terms of the `ζ` derivatives of the tangential
198    /// components (E at nodes, H at duals).
199    psi_e: [Vec<Complex64>; 3],
200    psi_h: [Vec<Complex64>; 3],
201    /// Hard source node and the node of the PEC end.
202    src: usize,
203    end: usize,
204}
205
206/// `−j·k·x`.
207fn mj(k: f64, x: Complex64) -> Complex64 {
208    Complex64::new(k * x.im, -k * x.re)
209}
210
211/// The stretched `ζ` derivative `∂ζ + ψ` with the CPML recursion
212/// `ψ ← b·ψ + (b − 1)·∂ζ` (κ = 1, α = 0).
213fn stretched(diff: Complex64, b: f64, psi: &mut Complex64) -> Complex64 {
214    if b == 1.0 {
215        return diff;
216    }
217    *psi = *psi * b + diff * (b - 1.0);
218    diff + *psi
219}
220
221impl Aux {
222    fn update_h(&mut self) {
223        let n = self.n;
224        let nodes = self.d.len() + 1;
225        for a in 0..3 {
226            let (b, c) = ((a + 1) % 3, (a + 2) % 3);
227            if a == n {
228                // H_n at node j: T_b E_c − T_c E_b (tangential E at node j).
229                for j in 0..nodes {
230                    let curl = mj(self.kt[b], self.e[c][j]) - mj(self.kt[c], self.e[b][j]);
231                    self.h[a][j] -= curl * self.db;
232                }
233            } else {
234                // The ζ-differentiated tangential E component and its sign.
235                let (t, s) = if b == n { (c, 1.0) } else { (b, -1.0) };
236                let other = if b == n { c } else { b };
237                for j in 0..nodes - 1 {
238                    let diff = (self.e[t][j + 1] - self.e[t][j]) * (1.0 / self.d[j]);
239                    let dz = stretched(diff, self.b_dual[j], &mut self.psi_h[a][j]);
240                    let tr = mj(self.kt[other], self.e[n][j]);
241                    // b == n: ∂ζE_c − T_c E_n; c == n: T_b E_n − ∂ζE_b.
242                    let curl = dz * s - tr * s;
243                    self.h[a][j] -= curl * self.db;
244                }
245            }
246        }
247    }
248
249    fn update_e(&mut self) {
250        let n = self.n;
251        let nodes = self.d.len() + 1;
252        for a in 0..3 {
253            let (b, c) = ((a + 1) % 3, (a + 2) % 3);
254            if a == n {
255                // E_n at dual j: T_b H_c − T_c H_b (tangential H at dual j).
256                for j in 0..nodes - 1 {
257                    let curl = mj(self.kt[b], self.h[c][j]) - mj(self.kt[c], self.h[b][j]);
258                    self.e[a][j] += curl * self.cb;
259                }
260            } else {
261                let (t, s) = if b == n { (c, 1.0) } else { (b, -1.0) };
262                let other = if b == n { c } else { b };
263                for j in 1..nodes - 1 {
264                    if j == self.src || j == self.end {
265                        continue;
266                    }
267                    let diff = (self.h[t][j] - self.h[t][j - 1]) * (1.0 / self.dd[j]);
268                    let dz = stretched(diff, self.b_node[j], &mut self.psi_e[a][j]);
269                    let tr = mj(self.kt[other], self.h[n][j]);
270                    // b == n: ∂ζH_c − T_c H_n; c == n: T_b H_n − ∂ζH_b.
271                    let curl = dz * s - tr * s;
272                    self.e[a][j] += curl * self.cb;
273                }
274            }
275        }
276    }
277}
278
279/// Plane-wave source for a periodic unit cell at fixed transverse
280/// wavevector; see the [module docs](self).
281///
282/// ```
283/// use ems::engine::{Engine, FaceCondition, FaceConditions, Grid};
284/// use ems::engine::bloch_wave::{PeriodicPlaneWave, PeriodicPlaneWaveSpec, WavePart};
285/// use ems::{Axis, Face};
286///
287/// let g = Grid::uniform([4, 4, 40], [1e-3; 3])?;
288/// let faces = FaceConditions::default()
289///     .with(Face::XMin, FaceCondition::Periodic)
290///     .with(Face::XMax, FaceCondition::Periodic)
291///     .with(Face::YMin, FaceCondition::Periodic)
292///     .with(Face::YMax, FaceCondition::Periodic);
293/// let mut e = Engine::builder(g).faces(faces).threads(1).build()?;
294/// // Normal incidence along +z, E along x, total field from plane 10 on.
295/// let spec = PeriodicPlaneWaveSpec::new(Axis::Z, 10, 39, true, [0.0; 3], [1.0, 0.0, 0.0]);
296/// let pw = PeriodicPlaneWave::new(&e, &spec, WavePart::Real, |t| (-((t - 5e-11) / 1.5e-11).powi(2)).exp())?;
297/// let h = e.add_operator(pw);
298/// e.run(20);
299/// assert_eq!(e.operator(&h).incident().len(), 20);
300/// # Ok::<(), ems::Error>(())
301/// ```
302pub struct PeriodicPlaneWave {
303    spec: PeriodicPlaneWaveSpec,
304    part: WavePart,
305    amplitude: Amplitude,
306    aux: Aux,
307    /// Auxiliary node of the entry plane.
308    entry: usize,
309    e_corr: Vec<Correction>,
310    h_corr: Vec<Correction>,
311    incident: Vec<[Complex64; 3]>,
312}
313
314impl core::fmt::Debug for PeriodicPlaneWave {
315    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
316        f.debug_struct("PeriodicPlaneWave")
317            .field("spec", &self.spec)
318            .field("part", &self.part)
319            .field("e_corrections", &self.e_corr.len())
320            .field("h_corrections", &self.h_corr.len())
321            .finish_non_exhaustive()
322    }
323}
324
325/// The discrete transverse wavenumber of `k` on the lines of `axis`.
326fn discrete_k(lines: &[f64], k: f64) -> f64 {
327    if k == 0.0 || lines.len() < 2 {
328        return k;
329    }
330    let d = (lines[lines.len() - 1] - lines[0]) / (lines.len() - 1) as f64;
331    2.0 * (0.5 * k * d).sin() / d
332}
333
334impl PeriodicPlaneWave {
335    /// Sets up the source for `engine`'s grid, time step and coefficients
336    /// (take them as final: build after any operator that changes
337    /// coefficients on the injection planes), injecting `part` of the
338    /// complex incident field. `amplitude(t)` drives the auxiliary grid's
339    /// hard source.
340    ///
341    /// # Errors
342    /// [`Error::Invalid`] for planes outside the mesh or `start ≥ stop`,
343    /// an entry plane on the domain boundary (no room for the source), a
344    /// non-finite or normal `k_t`, a polarisation that is zero, non-finite
345    /// or has a normal component, or an invalid background.
346    pub fn new(
347        engine: &Engine,
348        spec: &PeriodicPlaneWaveSpec,
349        part: WavePart,
350        amplitude: impl Fn(f64) -> f64 + Send + 'static,
351    ) -> Result<Self> {
352        let grid = engine.grid();
353        let nn = grid.nodes();
354        let ax = spec.axis;
355        let n = ax.index();
356        let (lo, hi) = (spec.start, spec.stop);
357        if lo >= hi || hi >= nn[n] {
358            return Err(Error::Invalid(format!(
359                "periodic plane wave: planes {lo}..={hi} must have start < stop within the \
360                 {} nodes along {ax}",
361                nn[n]
362            )));
363        }
364        let entry_3d = if spec.forward { lo } else { hi };
365        if entry_3d == 0 || entry_3d == nn[n] - 1 {
366            return Err(Error::Invalid(format!(
367                "periodic plane wave: the entry plane {entry_3d} lies on the domain boundary"
368            )));
369        }
370        if spec.k_t.iter().any(|k| !k.is_finite()) || spec.k_t[n] != 0.0 {
371            return Err(Error::Invalid(format!(
372                "periodic plane wave: transverse wavevector {:?} must be finite with no \
373                 component along {ax}",
374                spec.k_t
375            )));
376        }
377        let p = spec.polarization;
378        let pl = norm(p);
379        if !(pl.is_finite() && pl > 0.0) || p[n] != 0.0 {
380            return Err(Error::Invalid(format!(
381                "periodic plane wave: polarisation {p:?} must be non-zero, finite and \
382                 tangential to the planes"
383            )));
384        }
385        let (er, mr) = (spec.eps_r, spec.mu_r);
386        if !(er.is_finite() && er > 0.0 && mr.is_finite() && mr > 0.0) {
387            return Err(Error::Invalid(format!(
388                "periodic plane wave: invalid background eps_r = {er}, mu_r = {mr}"
389            )));
390        }
391        let dt = engine.dt();
392        let lines = grid.lines(ax);
393
394        // Auxiliary nodes: 3-D lines [first ..= last] plus the loss layer
395        // downstream; `off` = 3-D node of auxiliary node 0.
396        // Source cells upstream of the entry plane (at least 1, see the
397        // boundary check above).
398        let m0 = if spec.forward {
399            lo.min(2)
400        } else {
401            (nn[n] - 1 - hi).min(2)
402        };
403        let (first, last) = if spec.forward {
404            (lo - m0, (hi + 2).min(nn[n] - 1))
405        } else {
406            (lo.saturating_sub(2), hi + m0)
407        };
408        let mut z: Vec<f64> = lines[first..=last].to_vec();
409        let off = if spec.forward {
410            let d = z[z.len() - 1] - z[z.len() - 2];
411            for _ in 0..AUX_PML + 1 {
412                let x = z[z.len() - 1] + d;
413                z.push(x);
414            }
415            first as isize
416        } else {
417            let d = z[1] - z[0];
418            for _ in 0..AUX_PML + 1 {
419                z.insert(0, z[0] - d);
420            }
421            first as isize - (AUX_PML + 1) as isize
422        };
423        let nodes = z.len();
424        let d: Vec<f64> = z.windows(2).map(|w| w[1] - w[0]).collect();
425        let dd: Vec<f64> = (0..nodes)
426            .map(|j| {
427                let a = if j > 0 { d[j - 1] } else { 0.0 };
428                let b = if j + 1 < nodes { d[j] } else { 0.0 };
429                0.5 * (a + b)
430            })
431            .collect();
432        let (src, end) = if spec.forward {
433            (0, nodes - 1)
434        } else {
435            (nodes - 1, 0)
436        };
437        // CPML profile: depth into the layer (cells) of a position given as
438        // a fractional node index; σ_max = 0.8·(m + 1)/(η·Δ).
439        let (eps, mu) = (EPS0 * er, MU0 * mr);
440        let eta = (mu / eps).sqrt();
441        let dl = if spec.forward { d[d.len() - 1] } else { d[0] };
442        let smax = 0.8 * (AUX_PML_M + 1.0) / (eta * dl);
443        let depth = |pos: f64| -> f64 {
444            let x = if spec.forward {
445                pos - (nodes - 1 - AUX_PML - 1) as f64
446            } else {
447                (AUX_PML + 1) as f64 - pos
448            };
449            x.clamp(0.0, AUX_PML as f64)
450        };
451        let bfac = |pos: f64| {
452            let s = smax * (depth(pos) / AUX_PML as f64).powf(AUX_PML_M);
453            (-s * dt / eps).exp()
454        };
455        let b_node = (0..nodes).map(|j| bfac(j as f64)).collect();
456        let b_dual = (0..nodes - 1).map(|j| bfac(j as f64 + 0.5)).collect();
457        let kt = Axis::ALL.map(|a| {
458            if a == ax {
459                0.0
460            } else {
461                discrete_k(grid.lines(a), spec.k_t[a.index()])
462            }
463        });
464        let alloc = |at_node: bool| vec![Complex64::ZERO; if at_node { nodes } else { nodes - 1 }];
465        let aux = Aux {
466            n,
467            kt,
468            d,
469            dd,
470            cb: dt / eps,
471            db: dt / mu,
472            b_node,
473            b_dual,
474            e: [0, 1, 2].map(|a| alloc(a != n)),
475            h: [0, 1, 2].map(|a| alloc(a == n)),
476            psi_e: [0, 1, 2].map(|a| alloc(a != n)),
477            psi_h: [0, 1, 2].map(|a| alloc(a == n)),
478            src,
479            end,
480        };
481        let aux_of = |node3: usize| -> usize { (node3 as isize - off) as usize };
482        let entry = aux_of(entry_3d);
483
484        // Phase reference: the first mesh line of every axis.
485        let r0 = Axis::ALL.map(|a| grid.lines(a)[0]);
486        let phase = |r: [f64; 3]| (0..3).map(|a| spec.k_t[a] * (r[a] - r0[a])).sum::<f64>();
487        let weights = |phi: f64| {
488            let (s, c) = phi.sin_cos();
489            match part {
490                WavePart::Real => (c, s),
491                WavePart::Imag => (-s, c),
492            }
493        };
494        let e_pos = |a: Axis, q: [usize; 3]| {
495            let (b, c) = a.others();
496            let mut r = [0.0; 3];
497            r[a.index()] = grid.dual_line(a, q[a.index()]);
498            r[b.index()] = grid.lines(b)[q[b.index()]];
499            r[c.index()] = grid.lines(c)[q[c.index()]];
500            r
501        };
502        let h_pos = |dax: Axis, q: [usize; 3]| {
503            let (e, f) = dax.others();
504            let mut r = [0.0; 3];
505            r[dax.index()] = grid.lines(dax)[q[dax.index()]];
506            r[e.index()] = grid.dual_line(e, q[e.index()]);
507            r[f.index()] = grid.dual_line(f, q[f.index()]);
508            r
509        };
510        // Total-field H: tangential H at duals lo ≤ q < hi, normal H at
511        // nodes lo ≤ q ≤ hi.
512        let tf_h = |dax: Axis, q: [usize; 3]| {
513            if dax == ax {
514                (lo..=hi).contains(&q[n])
515            } else {
516                (lo..hi).contains(&q[n])
517            }
518        };
519        let h_valid = |dax: Axis, q: [usize; 3]| {
520            let (e, f) = dax.others();
521            (0..3).all(|a| q[a] < nn[a])
522                && q[e.index()] + 1 < nn[e.index()]
523                && q[f.index()] + 1 < nn[f.index()]
524        };
525
526        let co = engine.coefficients();
527        let (mut e_corr, mut h_corr) = (Vec::new(), Vec::new());
528        for plane in [lo, hi] {
529            for a in Axis::ALL.into_iter().filter(|&a| a != ax) {
530                let (b, c) = a.others();
531                let t = if b == ax { c } else { b };
532                let (ai, ti) = (a.index(), t.index());
533                for pa in 0..nn[ai] - 1 {
534                    for pt in 0..nn[ti] {
535                        let mut pnode = [0; 3];
536                        (pnode[ai], pnode[ti], pnode[n]) = (pa, pt, plane);
537                        let cbv = co.cb(a)[grid.index(pnode)] as f64;
538                        let minus = |s: usize| {
539                            let mut q = pnode;
540                            q[s] = q[s].wrapping_sub(1);
541                            q
542                        };
543                        let terms = [
544                            (c, pnode, 1.0),
545                            (c, minus(b.index()), -1.0),
546                            (b, pnode, -1.0),
547                            (b, minus(c.index()), 1.0),
548                        ];
549                        for (dax, q, s) in terms {
550                            if !h_valid(dax, q) || tf_h(dax, q) {
551                                continue;
552                            }
553                            let di = dax.index();
554                            // Tangential H at dual q[n] (scattered side).
555                            debug_assert_ne!(dax, ax);
556                            if cbv != 0.0 {
557                                let (wr, wi) = weights(phase(h_pos(dax, q)));
558                                let coef = cbv * s * grid.dual_delta(dax, q[di]);
559                                e_corr.push(Correction {
560                                    axis: a,
561                                    node: pnode,
562                                    comp: di,
563                                    m: aux_of(q[n]),
564                                    wr: coef * wr,
565                                    wi: coef * wi,
566                                });
567                            }
568                            let dbv = co.db(dax)[grid.index(q)] as f64;
569                            if dbv != 0.0 {
570                                let (wr, wi) = weights(phase(e_pos(a, pnode)));
571                                let coef = dbv * s * grid.delta(a, pa);
572                                h_corr.push(Correction {
573                                    axis: dax,
574                                    node: q,
575                                    comp: ai,
576                                    m: aux_of(plane),
577                                    wr: coef * wr,
578                                    wi: coef * wi,
579                                });
580                            }
581                        }
582                    }
583                }
584            }
585        }
586        drop(co);
587        Ok(Self {
588            spec: spec.clone(),
589            part,
590            amplitude: Box::new(amplitude),
591            aux,
592            entry,
593            e_corr,
594            h_corr,
595            incident: Vec::new(),
596        })
597    }
598
599    /// The settings.
600    pub fn spec(&self) -> &PeriodicPlaneWaveSpec {
601        &self.spec
602    }
603
604    /// The part of the complex field this operator injects.
605    pub fn part(&self) -> WavePart {
606        self.part
607    }
608
609    /// Number of E and H edge corrections applied per step.
610    pub fn corrections(&self) -> (usize, usize) {
611        (self.e_corr.len(), self.h_corr.len())
612    }
613
614    /// The discrete transverse wavenumbers `k̃` used by the auxiliary grid
615    /// \[rad/m\].
616    pub fn discrete_wavevector(&self) -> [f64; 3] {
617        self.aux.kt
618    }
619
620    /// The complex incident E \[V/m\] on the entry plane at the phase
621    /// reference (the first mesh line of every transverse axis), one value
622    /// per step at the E times `(n + 1)·dt`: the tangential components as
623    /// computed, the normal component averaged from the two half cells
624    /// around the plane.
625    pub fn incident(&self) -> &[[Complex64; 3]] {
626        &self.incident
627    }
628}
629
630impl Operator for PeriodicPlaneWave {
631    fn hooks(&self) -> Hooks {
632        Hooks::POST_H | Hooks::POST_E
633    }
634
635    fn post_h(&mut self, fields: &mut Fields<'_>, _ctx: &StepContext<'_>) {
636        // The H kernel read u^n; the auxiliary E is at n·dt.
637        for c in &self.h_corr {
638            let v = self.aux.e[c.comp][c.m];
639            *fields.i.get_mut(c.axis, c.node) += (c.wr * v.re + c.wi * v.im) as f32;
640        }
641        self.aux.update_h();
642    }
643
644    fn post_e(&mut self, fields: &mut Fields<'_>, ctx: &StepContext<'_>) {
645        // The E kernel read i^{n+½}; so does the auxiliary H now.
646        for c in &self.e_corr {
647            let v = self.aux.h[c.comp][c.m];
648            *fields.u.get_mut(c.axis, c.node) += (c.wr * v.re + c.wi * v.im) as f32;
649        }
650        self.aux.update_e();
651        let a = (self.amplitude)(ctx.e_time());
652        let n = self.aux.n;
653        for t in (0..3).filter(|&t| t != n) {
654            self.aux.e[t][self.aux.src] = Complex64::new(self.spec.polarization[t] * a, 0.0);
655            self.aux.e[t][self.aux.end] = Complex64::ZERO;
656        }
657        let j = self.entry;
658        let mut inc = [Complex64::ZERO; 3];
659        for (t, v) in inc.iter_mut().enumerate() {
660            *v = if t == n {
661                (self.aux.e[n][j - 1] + self.aux.e[n][j]) * 0.5
662            } else {
663                self.aux.e[t][j]
664            };
665        }
666        self.incident.push(inc);
667    }
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673
674    #[test]
675    fn discrete_wavenumber() {
676        let l: Vec<f64> = (0..11).map(|i| i as f64 * 1e-3).collect();
677        let k = 300.0;
678        let kt = discrete_k(&l, k);
679        assert!((kt - 2.0 * (0.5 * k * 1e-3).sin() / 1e-3).abs() < 1e-12);
680        assert!(kt < k);
681        assert_eq!(discrete_k(&l, 0.0), 0.0);
682    }
683
684    #[test]
685    fn angle_and_cutoff() {
686        let s = PeriodicPlaneWaveSpec::new(Axis::Z, 2, 9, true, [10.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
687        let fc = 10.0 * C0 / (2.0 * core::f64::consts::PI);
688        assert!(s.angle(0.99 * fc).is_none());
689        let th = s.angle(2.0 * fc).unwrap();
690        assert!((th - 30f64.to_radians()).abs() < 1e-12);
691    }
692}