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}