use super::Engine;
use super::excite::Amplitude;
use super::fields::Fields;
use super::medium::{CoefficientMedia, EdgeMedia, Medium};
use super::operator::{Hooks, Operator, StepContext};
use crate::axis::Axis;
use crate::constants::{EPS0, MU0};
use crate::post::Complex64;
use crate::{Error, Result};
#[derive(Debug, Clone, PartialEq)]
pub struct PlaneWaveSpec {
pub start: [usize; 3],
pub stop: [usize; 3],
pub direction: [f64; 3],
pub polarization: [f64; 3],
pub eps_r: f64,
pub mu_r: f64,
pub kappa: f64,
pub sigma_m: f64,
pub match_frequency: f64,
}
impl PlaneWaveSpec {
pub fn new(
start: [usize; 3],
stop: [usize; 3],
direction: [f64; 3],
polarization: [f64; 3],
) -> Self {
Self {
start,
stop,
direction,
polarization,
eps_r: 1.0,
mu_r: 1.0,
kappa: 0.0,
sigma_m: 0.0,
match_frequency: 0.0,
}
}
#[must_use]
pub fn with_background(mut self, eps_r: f64, mu_r: f64) -> Self {
self.eps_r = eps_r;
self.mu_r = mu_r;
self
}
#[must_use]
pub fn with_losses(mut self, kappa: f64, sigma_m: f64) -> Self {
self.kappa = kappa;
self.sigma_m = sigma_m;
self
}
#[must_use]
pub fn with_medium(self, m: Medium) -> Self {
self.with_background(m.eps_r, m.mu_r)
.with_losses(m.kappa, m.sigma_m)
}
pub fn medium(&self) -> Medium {
Medium {
eps_r: self.eps_r,
mu_r: self.mu_r,
kappa: self.kappa,
sigma_m: self.sigma_m,
}
}
#[must_use]
pub fn with_match_frequency(mut self, f: f64) -> Self {
self.match_frequency = f;
self
}
}
#[derive(Debug, Clone, Copy)]
struct Correction {
axis: Axis,
node: [usize; 3],
coef: f64,
m: usize,
w: [f64; 4],
}
const AUX_PML: usize = 64;
const AUX_PML_R: f64 = 1e-12;
const AUX_TAPER: usize = 256;
pub struct PlaneWave {
spec: PlaneWaveSpec,
amplitude: Amplitude,
k_hat: [f64; 3],
e0: [f64; 3],
h0: [f64; 3],
r_ref: [f64; 3],
dxi: f64,
m0: usize,
lead: f64,
gain: f64,
transit: f64,
main: usize,
e: Vec<f64>,
h: Vec<f64>,
ca: Vec<f64>,
cb: Vec<f64>,
da: Vec<f64>,
db: Vec<f64>,
e_corr: Vec<Correction>,
h_corr: Vec<Correction>,
}
impl core::fmt::Debug for PlaneWave {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PlaneWave")
.field("spec", &self.spec)
.field("spacing", &self.dxi)
.field("reference_point", &self.r_ref)
.field("e_corrections", &self.e_corr.len())
.field("h_corrections", &self.h_corr.len())
.finish_non_exhaustive()
}
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn bisect(mut lo: f64, mut hi: f64, f: impl Fn(f64) -> f64) -> f64 {
let s_lo = f(lo) < 0.0;
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if mid <= lo || mid >= hi {
break;
}
if (f(mid) < 0.0) == s_lo {
lo = mid;
} else {
hi = mid;
}
}
0.5 * (lo + hi)
}
fn match_spacing(dt: f64, v: f64, f: f64, dir: [f64; 3], cell: [f64; 3]) -> Result<(f64, f64)> {
let s = (core::f64::consts::PI * f * dt).sin() / (v * dt);
let k_hi = (0..3)
.filter(|&a| dir[a].abs() > 0.0)
.map(|a| core::f64::consts::PI / (dir[a].abs() * cell[a]))
.fold(f64::INFINITY, f64::min);
let rhs = |k: f64| {
(0..3)
.map(|a| {
let x = (0.5 * k * dir[a] * cell[a]).sin() / cell[a];
x * x
})
.sum::<f64>()
- s * s
};
if !(s.is_finite() && s > 0.0 && rhs(k_hi) > 0.0) {
return Err(Error::Invalid(format!(
"plane wave: matching frequency {f} Hz is beyond the resolution of the mesh"
)));
}
let k = bisect(0.0, k_hi, rhs);
let axes: Vec<usize> = (0..3).filter(|&a| dir[a] != 0.0).collect();
if let &[a] = axes.as_slice() {
return Ok((cell[a], k));
}
let g = |d: f64| (0.5 * k * d).sin() / d - s;
let lo = v * dt;
if g(lo) <= 0.0 {
return Ok((lo, k));
}
let hi = core::f64::consts::PI / k;
if g(hi) > 0.0 {
return Err(Error::Invalid(format!(
"plane wave: cannot match the auxiliary grid dispersion at {f} Hz"
)));
}
Ok((bisect(lo, hi, g), k))
}
fn aux_gamma(wdt: f64, se: f64, sm: f64, r: f64) -> Complex64 {
let (sn, cs) = (0.5 * wdt).sin_cos();
let a = |s: f64| Complex64::new(2.0 * s * cs, 2.0 * sn);
let z = (a(se) * a(sm)).sqrt() / (2.0 * r);
let g = (z + (z * z + 1.0).sqrt()).ln() * 2.0;
if g.re < 0.0 { -g } else { g }
}
fn describe(
engine: &Engine,
co: &super::Coefficients,
what: &str,
axis: Axis,
node: [usize; 3],
e: bool,
) -> String {
let m = CoefficientMedia {
grid: engine.grid(),
co,
dt: engine.dt(),
};
let v = if e {
m.e_edge(axis, node)
} else {
m.h_edge(axis, node)
};
let (name, loss) = if e {
("eps_r", "kappa")
} else {
("mu_r", "sigma_m")
};
match v {
Some((x, l)) => format!(
"the {what} edge along {axis} at {node:?} on the box surface has {name} = {x:.6}, \
{loss} = {l:.6e}"
),
None => format!("the {what} edge along {axis} at {node:?} on the box surface is PEC"),
}
}
fn stencil(s: f64) -> (usize, [f64; 4]) {
let m = s.floor();
let t = s - m;
let w = [
-t * (t - 1.0) * (t - 2.0) / 6.0,
(t + 1.0) * (t - 1.0) * (t - 2.0) / 2.0,
-(t + 1.0) * t * (t - 2.0) / 2.0,
(t + 1.0) * t * (t - 1.0) / 6.0,
];
(m as usize - 1, w)
}
fn interp(v: &[f64], m: usize, w: &[f64; 4]) -> f64 {
w[0] * v[m] + w[1] * v[m + 1] + w[2] * v[m + 2] + w[3] * v[m + 3]
}
struct Raw {
axis: Axis,
node: [usize; 3],
coef: f64,
xi: f64,
}
impl PlaneWave {
pub fn new(
engine: &Engine,
spec: &PlaneWaveSpec,
amplitude: impl Fn(f64) -> f64 + Send + 'static,
) -> Result<Self> {
let grid = engine.grid();
if grid.is_cylindrical() {
return Err(Error::Unsupported(
"plane waves need a Cartesian grid".into(),
));
}
let n = grid.nodes();
let (lo, hi) = (spec.start, spec.stop);
if (0..3).any(|a| lo[a] >= hi[a] || hi[a] >= n[a]) {
return Err(Error::Invalid(format!(
"plane wave: box {lo:?}..={hi:?} must have start < stop within the {n:?} nodes"
)));
}
let len = dot(spec.direction, spec.direction).sqrt();
if !(len.is_finite() && len > 0.0) {
return Err(Error::Invalid(format!(
"plane wave: invalid propagation direction {:?}",
spec.direction
)));
}
let k_hat = spec.direction.map(|d| d / len);
let e0 = spec.polarization;
let e_len = dot(e0, e0).sqrt();
if !(e_len.is_finite() && e_len > 0.0) || dot(e0, k_hat).abs() > 1e-9 * e_len {
return Err(Error::Invalid(format!(
"plane wave: polarisation {e0:?} must be non-zero and perpendicular to the \
direction {k_hat:?}"
)));
}
let medium = spec.medium();
if medium.validate().is_err() {
return Err(Error::Invalid(format!(
"plane wave: invalid background eps_r = {}, mu_r = {}, kappa = {}, sigma_m = {}",
spec.eps_r, spec.mu_r, spec.kappa, spec.sigma_m
)));
}
let (er, mr) = (spec.eps_r, spec.mu_r);
let v = medium.velocity();
let eta = medium.impedance();
let h0 = cross(k_hat, e0).map(|x| x / eta);
let dt = engine.dt();
let se = spec.kappa * dt / (2.0 * EPS0 * er);
let sm = spec.sigma_m * dt / (2.0 * MU0 * mr);
let expect = |geo: f64, c: f64, g: f64| {
let (cap, cond) = (c * geo, g * geo);
let den = cap + 0.5 * cond * dt;
(((cap - 0.5 * cond * dt) / den) as f32, (dt / den) as f32)
};
let matches = |(a, b): (f32, f32), (ea, eb): (f32, f32)| {
(a - ea).abs() <= 1e-5 && (b - eb).abs() <= 1e-5 * eb.abs()
};
let mut mismatch: Option<String> = None;
let cell = Axis::ALL.map(|ax| {
let a = ax.index();
let l = grid.lines(ax);
(l[hi[a]] - l[lo[a]]) / (hi[a] - lo[a]) as f64
});
let mean = (cell[0] + cell[1] + cell[2]) / 3.0;
let f = if spec.match_frequency > 0.0 {
spec.match_frequency
} else {
v / (20.0 * mean)
};
if !f.is_finite() {
return Err(Error::Invalid(format!(
"plane wave: invalid matching frequency {f}"
)));
}
let (dxi, k) = match_spacing(dt, v, f, k_hat, cell)?;
let r_ref = Axis::ALL.map(|ax| {
let a = ax.index();
let l = grid.lines(ax);
if k_hat[a] >= 0.0 { l[lo[a]] } else { l[hi[a]] }
});
let xi = |r: [f64; 3]| dot(k_hat, [r[0] - r_ref[0], r[1] - r_ref[1], r[2] - r_ref[2]]);
let tf_h = |d: Axis, q: [usize; 3]| {
let (e, f) = d.others();
let (di, ei, fi) = (d.index(), e.index(), f.index());
(lo[di]..=hi[di]).contains(&q[di])
&& (lo[ei]..hi[ei]).contains(&q[ei])
&& (lo[fi]..hi[fi]).contains(&q[fi])
};
let h_valid = |d: Axis, q: [usize; 3]| {
let (e, f) = d.others();
(0..3).all(|a| q[a] < n[a])
&& q[e.index()] + 1 < n[e.index()]
&& q[f.index()] + 1 < n[f.index()]
};
let e_pos = |a: Axis, p: [usize; 3]| {
let (b, c) = a.others();
let mut r = [0.0; 3];
r[a.index()] = grid.dual_line(a, p[a.index()]);
r[b.index()] = grid.lines(b)[p[b.index()]];
r[c.index()] = grid.lines(c)[p[c.index()]];
r
};
let h_pos = |d: Axis, q: [usize; 3]| {
let (e, f) = d.others();
let mut r = [0.0; 3];
r[d.index()] = grid.lines(d)[q[d.index()]];
r[e.index()] = grid.dual_line(e, q[e.index()]);
r[f.index()] = grid.dual_line(f, q[f.index()]);
r
};
let co = engine.coefficients();
let (mut e_raw, mut h_raw) = (Vec::new(), Vec::new());
for a in Axis::ALL {
let (b, c) = a.others();
let (ai, bi, ci) = (a.index(), b.index(), c.index());
let (cb_a, ea) = (co.cb(a), e0[ai]);
for pa in lo[ai]..hi[ai] {
for pb in lo[bi]..=hi[bi] {
for pc in lo[ci]..=hi[ci] {
let on_surface =
pb == lo[bi] || pb == hi[bi] || pc == lo[ci] || pc == hi[ci];
if !on_surface {
continue;
}
let mut p = [0; 3];
(p[ai], p[bi], p[ci]) = (pa, pb, pc);
let cbv = cb_a[grid.index(p)] as f64;
let mut e_checked = false;
let minus = |t: usize| {
let mut q = p;
q[t] = q[t].wrapping_sub(1);
q
};
let terms = [
(c, p, 1.0),
(c, minus(bi), -1.0),
(b, p, -1.0),
(b, minus(ci), 1.0),
];
for (d, q, s) in terms {
if !h_valid(d, q) || tf_h(d, q) {
continue;
}
let di = d.index();
if mismatch.is_none() && cbv != 0.0 && !e_checked {
e_checked = true;
let geo = grid.dual_delta(b, pb) * grid.dual_delta(c, pc)
/ grid.delta(a, pa);
let got = (co.ca(a)[grid.index(p)], co.cb(a)[grid.index(p)]);
if !matches(got, expect(geo, EPS0 * er, spec.kappa)) {
mismatch = Some(describe(engine, &co, "E", a, p, true));
}
}
let dbv = co.db(d)[grid.index(q)] as f64;
if mismatch.is_none() && dbv != 0.0 {
let (de, df) = d.others();
let geo = grid.delta(de, q[de.index()])
* grid.delta(df, q[df.index()])
/ grid.dual_delta(d, q[di]);
let got = (co.da(d)[grid.index(q)], co.db(d)[grid.index(q)]);
if !matches(got, expect(geo, MU0 * mr, spec.sigma_m)) {
mismatch = Some(describe(engine, &co, "H", d, q, false));
}
}
if cbv != 0.0 && h0[di] != 0.0 {
e_raw.push(Raw {
axis: a,
node: p,
coef: cbv * s * h0[di] * grid.dual_delta(d, q[di]),
xi: xi(h_pos(d, q)),
});
}
if dbv != 0.0 && ea != 0.0 {
h_raw.push(Raw {
axis: d,
node: q,
coef: dbv * s * ea * grid.delta(a, pa),
xi: xi(e_pos(a, p)),
});
}
}
}
}
}
}
drop(co);
if let Some(m) = mismatch {
return Err(Error::Invalid(format!(
"plane wave: {m}, not the background {medium:?} of the source; the medium \
around the total-field box must be homogeneous and match \
PlaneWaveSpec::with_background / with_losses"
)));
}
let (xi_min, xi_max) = e_raw
.iter()
.chain(&h_raw)
.fold((0.0f64, 0.0f64), |(a, b), r| (a.min(r.xi), b.max(r.xi)));
let xi_dom = (0..8).fold(xi_max, |m, c: usize| {
let r = Axis::ALL.map(|ax| {
let l = grid.lines(ax);
if c >> ax.index() & 1 == 1 {
l[l.len() - 1]
} else {
l[0]
}
});
m.max(xi(r))
});
let m0 = (-xi_min / dxi).ceil().max(0.0) as usize + 3;
let main = (xi_dom / dxi).ceil() as usize + m0 + 4;
let taper = if se == sm { 0 } else { AUX_TAPER };
let s_top = se.max(sm);
let pml0 = main + taper;
let nodes = pml0 + AUX_PML + 1;
let r = v * dt / dxi;
let smax = -AUX_PML_R.ln() * r / AUX_PML as f64;
let loss = |x: f64, bg: f64| {
let t = ((x - main as f64) / taper.max(1) as f64).clamp(0.0, 1.0);
let g = t * t * t * (10.0 - 15.0 * t + 6.0 * t * t);
let pml = x - pml0 as f64;
let p = if pml > 0.0 {
smax * (pml / AUX_PML as f64).powi(3)
} else {
0.0
};
bg + (s_top - bg) * if taper == 0 { 0.0 } else { g } + p
};
let (mut ca, mut cb) = (vec![1.0; nodes], vec![0.0; nodes]);
for m in 1..nodes - 1 {
let s = loss(m as f64, se);
ca[m] = (1.0 - s) / (1.0 + s);
cb[m] = r / (1.0 + s);
}
let (mut da, mut db) = (vec![1.0; nodes - 1], vec![0.0; nodes - 1]);
for m in 0..nodes - 1 {
let s = loss(m as f64 + 0.5, sm);
da[m] = (1.0 - s) / (1.0 + s);
db[m] = r / (1.0 + s);
}
let finish = |raw: Vec<Raw>, half: f64| -> Vec<Correction> {
raw.into_iter()
.map(|c| {
let (m, w) = stencil(c.xi / dxi + m0 as f64 - half);
Correction {
axis: c.axis,
node: c.node,
coef: c.coef,
m,
w,
}
})
.collect()
};
let omega = 2.0 * core::f64::consts::PI * f;
let (lead, gain) = if se == 0.0 && sm == 0.0 {
(m0 as f64 * dxi * k / omega, 1.0)
} else {
let g = aux_gamma(omega * dt, se, sm, r) / dxi;
let x = m0 as f64 * dxi;
(g.im * x / omega, (g.re * x).exp())
};
let far = (0..8).fold(0.0f64, |m, c: usize| {
let r = Axis::ALL.map(|ax| {
let l = grid.lines(ax);
let a = ax.index();
l[if c >> a & 1 == 1 { hi[a] } else { lo[a] }]
});
m.max(xi(r))
});
let transit = 1.1 * far / v;
Ok(Self {
spec: spec.clone(),
amplitude: Box::new(amplitude),
k_hat,
e0,
h0,
r_ref,
dxi,
m0,
lead,
gain,
transit,
main,
e: vec![0.0; nodes],
h: vec![0.0; nodes - 1],
ca,
cb,
da,
db,
e_corr: finish(e_raw, 0.5),
h_corr: finish(h_raw, 0.0),
})
}
pub fn spec(&self) -> &PlaneWaveSpec {
&self.spec
}
pub fn direction(&self) -> [f64; 3] {
self.k_hat
}
pub fn spacing(&self) -> f64 {
self.dxi
}
pub fn reference_point(&self) -> [f64; 3] {
self.r_ref
}
pub fn transit_time(&self) -> f64 {
self.transit
}
pub fn corrections(&self) -> (usize, usize) {
(self.e_corr.len(), self.h_corr.len())
}
fn aux_at(&self, r: [f64; 3], half: f64) -> Option<(usize, [f64; 4])> {
let d = [
r[0] - self.r_ref[0],
r[1] - self.r_ref[1],
r[2] - self.r_ref[2],
];
let s = dot(self.k_hat, d) / self.dxi + self.m0 as f64 - half;
(s >= 1.0 && s + 3.0 < self.main as f64).then(|| stencil(s))
}
pub fn incident_e(&self, r: [f64; 3]) -> Option<[f64; 3]> {
let (m, w) = self.aux_at(r, 0.0)?;
let f = interp(&self.e, m, &w);
Some(self.e0.map(|x| x * f))
}
pub fn incident_h(&self, r: [f64; 3]) -> Option<[f64; 3]> {
let (m, w) = self.aux_at(r, 0.5)?;
let f = interp(&self.h, m, &w);
Some(self.h0.map(|x| x * f))
}
}
impl Operator for PlaneWave {
fn hooks(&self) -> Hooks {
Hooks::POST_H | Hooks::POST_E
}
fn post_h(&mut self, fields: &mut Fields<'_>, _ctx: &StepContext<'_>) {
for c in &self.h_corr {
*fields.i.get_mut(c.axis, c.node) += (c.coef * interp(&self.e, c.m, &c.w)) as f32;
}
let e = &self.e;
for (m, h) in self.h.iter_mut().enumerate() {
*h = self.da[m] * *h - self.db[m] * (e[m + 1] - e[m]);
}
}
fn post_e(&mut self, fields: &mut Fields<'_>, ctx: &StepContext<'_>) {
for c in &self.e_corr {
*fields.u.get_mut(c.axis, c.node) += (c.coef * interp(&self.h, c.m, &c.w)) as f32;
}
let last = self.e.len() - 1;
for m in 1..last {
self.e[m] = self.ca[m] * self.e[m] - self.cb[m] * (self.h[m] - self.h[m - 1]);
}
self.e[0] = self.gain * (self.amplitude)(ctx.e_time() + self.lead);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::C0;
#[test]
fn aux_gamma_solves_the_dispersion_relation() {
let dt = 0.99e-3 / (C0 * 3f64.sqrt());
let (er, kappa, f) = (2.0f64, 0.13, 8e9 / 2f64.sqrt());
let se = kappa * dt / (2.0 * EPS0 * er);
let r = C0 / er.sqrt() * dt / 1e-3;
let g = aux_gamma(2.0 * core::f64::consts::PI * f * dt, se, 0.0, r);
let om = 2.0 / dt * (core::f64::consts::PI * f * dt).sin();
let c = (core::f64::consts::PI * f * dt).cos();
let x = Complex64::new(
om * om * MU0 * EPS0 * er * 1e-6,
-om * MU0 * kappa * c * 1e-6,
);
let sh = ((g * 0.5).exp() - (g * -0.5).exp()) * 0.5;
assert!((sh * sh + x * 0.25).norm() < 1e-12 * x.norm(), "{g:?}");
assert!(g.re > 0.0 && g.im > 0.0, "{g:?}");
}
#[test]
fn stencil_reproduces_cubics() {
let f = |x: f64| 0.3 - x + 0.2 * x * x - 0.05 * x * x * x;
let v: Vec<f64> = (0..8).map(|m| f(m as f64)).collect();
for s in [1.0, 1.25, 2.5, 3.999, 4.0] {
let (m, w) = stencil(s);
assert!((interp(&v, m, &w) - f(s)).abs() < 1e-12, "{s}");
}
}
#[test]
fn axis_aligned_match_is_the_mesh_spacing() {
let dt = 0.99 * 1e-3 / (C0 * 3f64.sqrt());
let (d, _) = match_spacing(dt, C0, 10e9, [1.0, 0.0, 0.0], [1e-3; 3]).unwrap();
assert_eq!(d, 1e-3);
let s = 0.5f64.sqrt();
let (d, _) = match_spacing(dt, C0, 10e9, [s, s, 0.0], [1e-3; 3]).unwrap();
assert!(d > C0 * dt && d < 1e-3, "{d}");
}
}