use crate::error::SolveError;
pub fn fdtd_courant_check(dx: f64, dt: f64, c: f64) -> bool {
dx.is_finite()
&& dt.is_finite()
&& c.is_finite()
&& dx > 0.0
&& dt > 0.0
&& c > 0.0
&& c * dt <= dx * (1.0 + 1e-12)
}
pub fn fdtd_courant_check_2d(dx: f64, dy: f64, dt: f64, c: f64) -> bool {
if !(dx.is_finite() && dy.is_finite() && dt.is_finite() && c.is_finite()) {
return false;
}
if dx <= 0.0 || dy <= 0.0 || dt <= 0.0 || c <= 0.0 {
return false;
}
let limit = 1.0 / (1.0 / (dx * dx) + 1.0 / (dy * dy)).sqrt();
c * dt <= limit * (1.0 + 1e-12)
}
#[derive(Debug, Clone, PartialEq)]
pub struct Fdtd1d {
pub e: Vec<Vec<f64>>,
pub h: Vec<Vec<f64>>,
}
impl Fdtd1d {
pub fn energy(&self, eps_r: &[f64], n: usize) -> Option<f64> {
if n + 1 >= self.h.len() || n >= self.e.len() || eps_r.len() != self.e[n].len() {
return None;
}
let electric: f64 =
self.e[n].iter().zip(eps_r.iter()).map(|(v, e)| e * v * v).sum::<f64>() * 0.5;
let magnetic: f64 =
self.h[n].iter().zip(self.h[n + 1].iter()).map(|(a, b)| a * b).sum::<f64>() * 0.5;
Some(electric + magnetic)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Boundary1d {
Conductor,
Mur,
}
pub fn fdtd_1d(
eps_r: &[f64],
source: &dyn Fn(usize) -> f64,
source_cell: usize,
courant: f64,
steps: usize,
boundary: Boundary1d,
) -> Result<Fdtd1d, SolveError> {
let n = eps_r.len();
if n < 3 {
return Err(SolveError::InvalidArgument("need at least three cells"));
}
if eps_r.iter().any(|&e| !e.is_finite() || e <= 0.0) {
return Err(SolveError::InvalidArgument("permittivity must be positive and finite"));
}
if source_cell >= n {
return Err(SolveError::InvalidArgument("the source is outside the grid"));
}
if !courant.is_finite() || courant <= 0.0 || courant > 1.0 + 1e-12 {
return Err(SolveError::InvalidArgument("the Courant number must lie in (0, 1]"));
}
let slowest = eps_r.iter().copied().fold(f64::INFINITY, f64::min);
if courant > slowest.sqrt() * (1.0 + 1e-12) {
return Err(SolveError::InvalidArgument(
"the Courant number exceeds the limit set by the fastest medium in the grid",
));
}
let mut e = vec![0.0; n];
let mut h = vec![0.0; n - 1];
let mut e_hist = Vec::with_capacity(steps + 1);
let mut h_hist = Vec::with_capacity(steps + 1);
e_hist.push(e.clone());
h_hist.push(h.clone());
for step in 0..steps {
for i in 0..n - 1 {
h[i] += courant * (e[i + 1] - e[i]);
}
let (old_edge_l, old_next_l) = (e[0], e[1]);
let (old_edge_r, old_next_r) = (e[n - 1], e[n - 2]);
for i in 1..n - 1 {
e[i] += courant / eps_r[i] * (h[i] - h[i - 1]);
}
match boundary {
Boundary1d::Conductor => {
e[0] = 0.0;
e[n - 1] = 0.0;
}
Boundary1d::Mur => {
let coeff = |cell: usize| {
let s = courant / eps_r[cell].sqrt();
(s - 1.0) / (s + 1.0)
};
e[0] = old_next_l + coeff(0) * (e[1] - old_edge_l);
e[n - 1] = old_next_r + coeff(n - 1) * (e[n - 2] - old_edge_r);
}
}
e[source_cell] += source(step);
if !e.iter().all(|v| v.is_finite()) {
return Err(SolveError::NoConvergence { iters: step, residual: f64::INFINITY });
}
e_hist.push(e.clone());
h_hist.push(h.clone());
}
Ok(Fdtd1d { e: e_hist, h: h_hist })
}
pub fn photonic_crystal_bandgap_1d(
eps_a: f64,
eps_b: f64,
d_a: f64,
d_b: f64,
omega_max: f64,
samples: usize,
) -> Result<Vec<(f64, f64)>, SolveError> {
for v in [eps_a, eps_b, d_a, d_b, omega_max] {
if !v.is_finite() || v <= 0.0 {
return Err(SolveError::InvalidArgument("stack parameters must be positive"));
}
}
if samples < 2 {
return Err(SolveError::InvalidArgument("need at least two samples"));
}
let (na, nb) = (eps_a.sqrt(), eps_b.sqrt());
let mix = 0.5 * (na / nb + nb / na);
let trace = |w: f64| {
let (pa, pb) = (w * na * d_a, w * nb * d_b);
pa.cos() * pb.cos() - mix * pa.sin() * pb.sin()
};
let gap = |w: f64| trace(w).abs() - 1.0;
let mut edges = Vec::new();
let step = omega_max / samples as f64;
let mut previous = step * 1e-6;
let mut previous_gap = gap(previous);
for k in 1..=samples {
let w = k as f64 * step;
let g = gap(w);
if previous_gap.signum() != g.signum() && previous_gap != 0.0 {
let (mut lo, mut hi) = (previous, w);
let lo_sign = previous_gap.signum();
for _ in 0..200 {
let mid = 0.5 * (lo + hi);
if hi - lo <= 1e-15 * (1.0 + hi.abs()) {
break;
}
if gap(mid).signum() == lo_sign {
lo = mid;
} else {
hi = mid;
}
}
edges.push((0.5 * (lo + hi), g > 0.0));
}
previous = w;
previous_gap = g;
}
let mut gaps = Vec::new();
let mut open: Option<f64> = None;
for (w, rising) in edges {
if rising {
open = Some(w);
} else if let Some(start) = open.take() {
gaps.push((start, w));
}
}
if let Some(start) = open {
gaps.push((start, omega_max));
}
Ok(gaps)
}
#[derive(Debug, Clone, PartialEq)]
pub struct Fdtd2d {
pub nx: usize,
pub ny: usize,
pub ez: Vec<f64>,
pub envelope: Vec<f64>,
}
fn pml_loss(pos: f64, n: usize, pml: usize, courant: f64, reflection: f64) -> f64 {
if pml == 0 {
return 0.0;
}
const ORDER: f64 = 3.0;
let d = pml as f64;
let depth = if pos < d {
d - pos
} else if pos > (n - 1) as f64 - d {
pos - ((n - 1) as f64 - d)
} else {
return 0.0;
};
let s_max = -(ORDER + 1.0) * courant * reflection.ln() / (2.0 * d);
s_max * (depth / d).powf(ORDER)
}
#[allow(clippy::too_many_arguments)]
pub fn fdtd_2d_tm(
eps_r: &[f64],
source_pos: (usize, usize),
source: &dyn Fn(usize) -> f64,
nx: usize,
ny: usize,
steps: usize,
pml: (usize, usize),
courant: f64,
reflection: f64,
) -> Result<Fdtd2d, SolveError> {
if source_pos.0 >= nx || source_pos.1 >= ny {
return Err(SolveError::InvalidArgument("the source is outside the grid"));
}
let profile = [(source_pos.1 * nx + source_pos.0, 1.0)];
march_2d(eps_r, &profile, source, nx, ny, steps, pml, courant, reflection)
}
#[allow(clippy::too_many_arguments)]
fn march_2d(
eps_r: &[f64],
sources: &[(usize, f64)],
source: &dyn Fn(usize) -> f64,
nx: usize,
ny: usize,
steps: usize,
pml: (usize, usize),
courant: f64,
reflection: f64,
) -> Result<Fdtd2d, SolveError> {
if nx < 5 || ny < 5 {
return Err(SolveError::InvalidArgument("the grid must be at least five cells across"));
}
if eps_r.len() != nx * ny {
return Err(SolveError::DimensionMismatch { expected: nx * ny, got: eps_r.len() });
}
if eps_r.iter().any(|&e| !e.is_finite() || e <= 0.0) {
return Err(SolveError::InvalidArgument("permittivity must be positive and finite"));
}
if 2 * pml.0 + 1 >= nx || 2 * pml.1 + 1 >= ny {
return Err(SolveError::InvalidArgument("the absorbing layers leave no interior"));
}
if sources.iter().any(|&(k, w)| k >= nx * ny || !w.is_finite()) {
return Err(SolveError::InvalidArgument("a source is outside the grid or not finite"));
}
if !reflection.is_finite() || reflection <= 0.0 || reflection >= 1.0 {
return Err(SolveError::InvalidArgument("the reflection target must lie in (0, 1)"));
}
let slowest = eps_r.iter().copied().fold(f64::INFINITY, f64::min);
if !courant.is_finite()
|| courant <= 0.0
|| courant > (slowest / 2.0).sqrt() * (1.0 + 1e-12)
{
return Err(SolveError::InvalidArgument(
"the Courant number exceeds the two-dimensional limit for the fastest medium",
));
}
let coeffs = |n: usize, depth: usize, offset: f64| -> (Vec<f64>, Vec<f64>) {
(0..n)
.map(|k| {
let a = 0.5 * pml_loss(k as f64 + offset, n, depth, courant, reflection);
((1.0 - a) / (1.0 + a), courant / (1.0 + a))
})
.unzip()
};
let (cax, cbx) = coeffs(nx, pml.0, 0.0);
let (cay, cby) = coeffs(ny, pml.1, 0.0);
let (dax, dbx) = coeffs(nx, pml.0, 0.5);
let (day, dby) = coeffs(ny, pml.1, 0.5);
let mut ezx = vec![0.0; nx * ny];
let mut ezy = vec![0.0; nx * ny];
let mut ez = vec![0.0; nx * ny];
let mut hy = vec![0.0; ny * (nx - 1)];
let mut hx = vec![0.0; (ny - 1) * nx];
let mut envelope = vec![0.0; nx * ny];
let record_from = steps - steps / 4;
for step in 0..steps {
for j in 0..ny {
for i in 0..nx - 1 {
let k = j * (nx - 1) + i;
hy[k] = dax[i] * hy[k] + dbx[i] * (ez[j * nx + i + 1] - ez[j * nx + i]);
}
}
for j in 0..ny - 1 {
for i in 0..nx {
let k = j * nx + i;
hx[k] = day[j] * hx[k] - dby[j] * (ez[(j + 1) * nx + i] - ez[j * nx + i]);
}
}
for j in 1..ny - 1 {
for i in 1..nx - 1 {
let k = j * nx + i;
let e = eps_r[k];
ezx[k] = cax[i] * ezx[k]
+ cbx[i] / e * (hy[j * (nx - 1) + i] - hy[j * (nx - 1) + i - 1]);
ezy[k] = cay[j] * ezy[k]
- cby[j] / e * (hx[j * nx + i] - hx[(j - 1) * nx + i]);
ez[k] = ezx[k] + ezy[k];
}
}
let drive = source(step);
if !drive.is_finite() {
return Err(SolveError::InvalidArgument("the source must be finite"));
}
if drive != 0.0 {
for &(k, weight) in sources {
ezx[k] += 0.5 * drive * weight;
ezy[k] += 0.5 * drive * weight;
ez[k] = ezx[k] + ezy[k];
}
}
if !ez.iter().all(|v| v.is_finite()) {
return Err(SolveError::NoConvergence { iters: step, residual: f64::INFINITY });
}
if step >= record_from {
for (slot, v) in envelope.iter_mut().zip(ez.iter()) {
*slot = f64::max(*slot, v.abs());
}
}
}
Ok(Fdtd2d { nx, ny, ez, envelope })
}
pub fn waveguide_cutoff_check_fdtd(
width: usize,
length: usize,
mode: usize,
omega: f64,
courant: f64,
steps: usize,
) -> Result<f64, SolveError> {
if width < 4 || mode == 0 || mode >= width {
return Err(SolveError::InvalidArgument("the mode must lie in 1..width"));
}
if length < 80 {
return Err(SolveError::InvalidArgument("the guide is too short to measure a decay"));
}
if !omega.is_finite() || omega <= 0.0 {
return Err(SolveError::InvalidArgument("the frequency must be positive and finite"));
}
if !courant.is_finite() || courant <= 0.0 || courant > 0.5f64.sqrt() * (1.0 + 1e-12) {
return Err(SolveError::InvalidArgument("the Courant number exceeds the plane limit"));
}
let ky = std::f64::consts::PI * mode as f64 / width as f64;
let numerical_cutoff = 2.0 / courant * (courant * (0.5 * ky).sin()).asin();
if omega >= numerical_cutoff {
return Err(SolveError::InvalidArgument(
"the drive is at or above cutoff, so there is no evanescent decay to measure",
));
}
let (nx, ny) = (length, width + 1);
let eps = vec![1.0; nx * ny];
let pad = 12usize;
let column = pad + 3;
let profile: Vec<(usize, f64)> = (1..ny - 1)
.map(|j| {
(j * nx + column, (ky * j as f64).sin())
})
.collect();
let period_steps = std::f64::consts::TAU / (omega * courant);
let ramp = 20.0 * period_steps;
let drive = |step: usize| {
let t = step as f64;
let window = if t < ramp {
0.5 * (1.0 - (std::f64::consts::PI * t / ramp).cos())
} else {
1.0
};
window * (omega * courant * t).sin()
};
let run = march_2d(&eps, &profile, &drive, nx, ny, steps, (pad, 0), courant, 1e-6)?;
let profile: Vec<f64> = (0..nx)
.map(|i| (1..ny - 1).map(|j| run.envelope[j * nx + i]).sum::<f64>())
.collect();
let start = column + 3;
let end = nx - pad - width - 2;
if end <= start + 8 {
return Err(SolveError::InvalidArgument("the guide is too short to measure a decay"));
}
let floor = (start..end).map(|i| profile[i]).fold(f64::INFINITY, f64::min);
if !(profile[start] > 0.0) {
return Err(SolveError::NoConvergence { iters: steps, residual: profile[start] });
}
let threshold = (50.0 * floor).max(profile[start] * 1e-13);
let mut stop = start;
while stop < end && profile[stop] > threshold {
stop += 1;
}
let points: Vec<(f64, f64)> = (start..stop)
.filter(|&i| profile[i] > 0.0)
.map(|i| (i as f64, profile[i].ln()))
.collect();
if points.len() < 10 {
return Err(SolveError::NoConvergence { iters: steps, residual: points.len() as f64 });
}
let n = points.len() as f64;
let mx = points.iter().map(|p| p.0).sum::<f64>() / n;
let my = points.iter().map(|p| p.1).sum::<f64>() / n;
let sxx: f64 = points.iter().map(|p| (p.0 - mx) * (p.0 - mx)).sum();
let syy: f64 = points.iter().map(|p| (p.1 - my) * (p.1 - my)).sum();
let sxy: f64 = points.iter().map(|p| (p.0 - mx) * (p.1 - my)).sum();
if sxx <= 0.0 || syy <= 0.0 {
return Err(SolveError::NoConvergence { iters: steps, residual: f64::INFINITY });
}
let correlation = sxy / (sxx * syy).sqrt();
if correlation > -0.999 {
return Err(SolveError::NoConvergence { iters: steps, residual: correlation });
}
let alpha = -sxy / sxx;
if alpha <= 0.0 {
return Err(SolveError::NoConvergence { iters: steps, residual: alpha });
}
let rhs = (0.5 * alpha).sinh().powi(2)
+ (0.5 * omega * courant).sin().powi(2) / (courant * courant);
let arg = courant * rhs.sqrt();
if !(0.0..=1.0).contains(&arg) {
return Err(SolveError::NoConvergence { iters: steps, residual: arg });
}
Ok(2.0 / courant * arg.asin())
}
pub fn waveguide_cutoff_numerical(
width: usize,
mode: usize,
courant: f64,
) -> Result<f64, SolveError> {
if width == 0 || mode == 0 || mode >= width {
return Err(SolveError::InvalidArgument("the mode must lie in 1..width"));
}
if !courant.is_finite() || courant <= 0.0 || courant > 0.5f64.sqrt() * (1.0 + 1e-12) {
return Err(SolveError::InvalidArgument("the Courant number exceeds the plane limit"));
}
let ky = std::f64::consts::PI * mode as f64 / width as f64;
Ok(2.0 / courant * (courant * (0.5 * ky).sin()).asin())
}
#[cfg(test)]
mod tests {
use super::*;
const PI: f64 = std::f64::consts::PI;
fn pulse(step: usize) -> f64 {
let t = step as f64 - 30.0;
(-t * t / 120.0).exp()
}
fn burst(step: usize) -> f64 {
if step >= 60 {
return 0.0;
}
let x = step as f64 / 60.0;
0.5 * (1.0 - (std::f64::consts::TAU * x).cos())
}
#[test]
fn the_magic_time_step_is_an_exact_shift() {
let n = 200;
let eps = vec![1.0; n];
let r = fdtd_1d(&eps, &pulse, 100, 1.0, 90, Boundary1d::Conductor).unwrap();
let mut worst: f64 = 0.0;
for step in 60..80 {
for i in 120..180 {
worst = worst.max((r.e[step + 1][i] - r.e[step][i - 1]).abs());
}
}
assert_eq!(worst, 0.0, "the pulse did not translate exactly");
let peak = r.e[70].iter().cloned().fold(0.0f64, f64::max);
assert!(peak > 0.4, "the pulse faded to {peak}");
}
#[test]
fn the_leapfrog_conserves_its_own_energy_and_not_the_obvious_one() {
let n = 200;
let eps: Vec<f64> = (0..n).map(|i| 1.0 + 2.0 * (i as f64 / n as f64)).collect();
let r = fdtd_1d(&eps, &burst, 100, 0.9, 120, Boundary1d::Conductor).unwrap();
let reference = r.energy(&eps, 70).unwrap();
assert!(reference > 0.1, "there was no energy to conserve");
for step in 70..=119 {
let u = r.energy(&eps, step).unwrap();
assert!(
(u - reference).abs() < 1e-12 * reference,
"step {step} drifted to {u} from {reference}"
);
}
let naive = |k: usize| -> f64 {
0.5 * r.e[k].iter().zip(eps.iter()).map(|(v, e)| e * v * v).sum::<f64>()
+ 0.5 * r.h[k].iter().map(|v| v * v).sum::<f64>()
};
let spread = (70..=119).map(naive).fold(f64::NEG_INFINITY, f64::max)
- (70..=119).map(naive).fold(f64::INFINITY, f64::min);
assert!(spread > 1e-9 * reference, "the naive energy was conserved after all");
assert_eq!(r.energy(&eps, 0), Some(0.0));
assert!(r.energy(&eps, 120).is_none());
assert!(r.energy(&eps[..3], 5).is_none());
}
#[test]
fn the_courant_conditions_bound_what_they_should() {
assert!(fdtd_courant_check(1.0, 1.0, 1.0));
assert!(fdtd_courant_check(1.0, 0.5, 1.0));
assert!(!fdtd_courant_check(1.0, 1.001, 1.0));
assert!(!fdtd_courant_check(0.0, 1.0, 1.0));
assert!(!fdtd_courant_check(1.0, -1.0, 1.0));
assert!(!fdtd_courant_check(1.0, 1.0, f64::NAN));
let root2 = 2.0f64.sqrt();
assert!(fdtd_courant_check_2d(1.0, 1.0, 1.0 / root2, 1.0));
assert!(!fdtd_courant_check_2d(1.0, 1.0, 1.0 / root2 * 1.001, 1.0));
assert!(fdtd_courant_check(1.0, 1.0 / root2 * 1.001, 1.0));
assert!(fdtd_courant_check_2d(1.0, 0.1, 0.09, 1.0));
assert!(!fdtd_courant_check_2d(1.0, 0.1, 0.11, 1.0));
assert!(!fdtd_courant_check_2d(1.0, 0.0, 0.1, 1.0));
}
#[test]
fn a_medium_faster_than_vacuum_tightens_the_limit() {
let mut eps = vec![1.0; 60];
for e in eps.iter_mut().skip(30).take(10) {
*e = 0.25;
}
assert!(fdtd_1d(&eps, &pulse, 5, 0.9, 10, Boundary1d::Mur).is_err());
assert!(fdtd_1d(&eps, &pulse, 5, 0.5, 10, Boundary1d::Mur).is_ok());
assert!(fdtd_1d(&eps, &pulse, 5, 0.51, 10, Boundary1d::Mur).is_err());
}
#[test]
fn the_absorbing_boundary_beats_a_wall_by_orders_of_magnitude() {
let n = 400;
let eps = vec![1.0; n];
let residual = |b| {
let r = fdtd_1d(&eps, &pulse, n / 2, 1.0, 400, b).unwrap();
r.e[400].iter().cloned().fold(0.0f64, |a, v| a.max(v.abs()))
};
let mur = residual(Boundary1d::Mur);
let wall = residual(Boundary1d::Conductor);
assert!(wall > 0.4, "the wall did not reflect the pulse: {wall}");
assert!(mur < 1e-3, "the absorbing boundary left {mur} behind");
assert!(wall / mur > 1e3, "the absorber was only {}x better", wall / mur);
}
#[test]
fn a_dielectric_interface_reproduces_the_fresnel_coefficients() {
let n = 400;
let mut eps = vec![1.0; n];
for e in eps.iter_mut().skip(n / 2) {
*e = 4.0;
}
let r = fdtd_1d(&eps, &pulse, 60, 1.0, 260, Boundary1d::Mur).unwrap();
let extreme = |v: &[f64]| v.iter().copied().fold(0.0f64, |a, x| if x.abs() > a.abs() { x } else { a });
let incident = extreme(&r.e[120][..190]);
let reflected = extreme(&r.e[250][..190]);
let transmitted = extreme(&r.e[250][210..]);
assert!(incident > 0.4, "no incident pulse: {incident}");
assert!(
(reflected / incident + 1.0 / 3.0).abs() < 0.02,
"reflection was {}",
reflected / incident
);
assert!(
(transmitted / incident - 2.0 / 3.0).abs() < 0.02,
"transmission was {}",
transmitted / incident
);
}
#[test]
fn the_quarter_wave_stack_has_the_gaps_the_theory_gives_it() {
let (ea, eb): (f64, f64) = (1.0, 4.0);
let (na, nb) = (ea.sqrt(), eb.sqrt());
let (da, db) = (1.0, na / nb);
let w0 = std::f64::consts::PI / (2.0 * na * da);
let gaps = photonic_crystal_bandgap_1d(ea, eb, da, db, 4.5 * w0, 4000).unwrap();
assert!(gaps.len() >= 2, "found only {} gaps", gaps.len());
for (m, (lo, hi)) in [(1.0, gaps[0]), (3.0, gaps[1])] {
let centre = 0.5 * (lo + hi);
assert!((centre / w0 - m).abs() < 1e-9, "gap centred at {} w0", centre / w0);
let want = 4.0 / (m * std::f64::consts::PI)
* ((nb - na) / (nb + na)).asin();
let got = (hi - lo) / centre;
assert!((got - want).abs() < 1e-6, "gap {m}: width {got}, theory {want}");
}
assert!(
!gaps.iter().any(|&(lo, hi)| lo < 2.0 * w0 && hi > 2.0 * w0),
"the second-order gap did not close"
);
}
#[test]
fn a_homogeneous_stack_has_no_gaps_and_scaling_moves_them_all() {
assert!(photonic_crystal_bandgap_1d(2.25, 2.25, 1.0, 0.7, 40.0, 3000).unwrap().is_empty());
let base = photonic_crystal_bandgap_1d(1.0, 4.0, 1.0, 0.5, 12.0, 6000).unwrap();
let stretched = photonic_crystal_bandgap_1d(1.0, 4.0, 2.0, 1.0, 6.0, 6000).unwrap();
assert!(!base.is_empty());
assert_eq!(base.len(), stretched.len());
for (a, b) in base.iter().zip(stretched.iter()) {
assert!((a.0 - 2.0 * b.0).abs() < 1e-8 * a.0);
assert!((a.1 - 2.0 * b.1).abs() < 1e-8 * a.1);
}
}
#[test]
fn the_matched_layer_absorbs_what_a_wall_reflects() {
let (nx, ny) = (60usize, 60usize);
let eps = vec![1.0; nx * ny];
let s = 0.5f64.sqrt() * 0.99;
let src = |step: usize| -> f64 {
if step >= 100 {
return 0.0;
}
let x = step as f64 / 100.0;
0.5 * (1.0 - (std::f64::consts::TAU * x).cos())
* (std::f64::consts::TAU * 0.07 * step as f64).sin()
};
let residual = |pml: usize| {
let r =
fdtd_2d_tm(&eps, (nx / 2, ny / 2), &src, nx, ny, 500, (pml, pml), s, 1e-6)
.unwrap();
let mut peak: f64 = 0.0;
for j in pml + 3..ny - pml - 3 {
for i in pml + 3..nx - pml - 3 {
peak = peak.max(r.envelope[j * nx + i]);
}
}
peak
};
let wall = residual(0);
let thin = residual(4);
let thick = residual(10);
assert!(wall > 1e-2, "the conductor did not reflect: {wall}");
assert!(wall / thin > 1e3, "four cells of layer were only {}x better", wall / thin);
assert!(thick < thin, "a deeper layer absorbed less");
}
#[test]
fn the_plane_scheme_respects_the_symmetry_of_its_own_grid() {
let n = 41usize;
let eps = vec![1.0; n * n];
let s = 0.5f64.sqrt() * 0.9;
let src = |step: usize| -> f64 {
let t = step as f64 - 20.0;
(-t * t / 60.0).exp()
};
let r = fdtd_2d_tm(&eps, (n / 2, n / 2), &src, n, n, 120, (0, 0), s, 1e-6).unwrap();
for j in 0..n {
for i in 0..n {
let v = r.ez[j * n + i];
assert_eq!(v, r.ez[j * n + (n - 1 - i)], "not mirrored in x at ({i}, {j})");
assert_eq!(v, r.ez[(n - 1 - j) * n + i], "not mirrored in y at ({i}, {j})");
assert_eq!(v, r.ez[i * n + j], "not symmetric under transposition");
}
}
}
#[test]
fn the_numerical_cutoff_sits_below_the_continuum_one_and_approaches_it() {
let s = 0.5f64.sqrt() * 0.99;
let mut previous = f64::INFINITY;
for width in [8usize, 16, 32, 64] {
let got = waveguide_cutoff_numerical(width, 1, s).unwrap();
let continuum = PI / width as f64;
assert!(got < continuum, "the grid cutoff was not below the continuum one");
let relative = (continuum - got) / continuum;
assert!(relative < previous, "refining did not close the gap");
previous = relative;
}
assert!(previous < 1e-3, "sixty-four cells still left {previous}");
let a = waveguide_cutoff_numerical(20, 1, s).unwrap();
let b = waveguide_cutoff_numerical(20, 2, s).unwrap();
let c = waveguide_cutoff_numerical(40, 1, s).unwrap();
assert!(b > a && c < a);
assert!(waveguide_cutoff_numerical(10, 0, s).is_err());
assert!(waveguide_cutoff_numerical(10, 10, s).is_err());
assert!(waveguide_cutoff_numerical(10, 1, 1.0).is_err());
}
#[test]
fn the_measured_evanescent_decay_gives_the_cutoff_back() {
let s = 0.5f64.sqrt() * 0.99;
let width = 16;
let want = waveguide_cutoff_numerical(width, 1, s).unwrap();
for frac in [0.5, 0.85] {
let got = waveguide_cutoff_check_fdtd(width, 200, 1, frac * want, s, 6000).unwrap();
assert!(
(got - want).abs() < 5e-3 * want,
"at {frac} of cutoff the measurement gave {got}, wanted {want}"
);
}
assert!(waveguide_cutoff_check_fdtd(width, 200, 1, want, s, 500).is_err());
assert!(waveguide_cutoff_check_fdtd(width, 200, 1, 2.0 * want, s, 500).is_err());
}
#[test]
fn the_plane_solver_refuses_impossible_arguments() {
let eps = vec![1.0; 40 * 40];
let quiet = |_: usize| 0.0;
let s = 0.5f64.sqrt() * 0.9;
assert!(fdtd_2d_tm(&eps[..16], (0, 0), &quiet, 4, 4, 5, (0, 0), s, 1e-6).is_err());
assert!(fdtd_2d_tm(&eps[..100], (0, 0), &quiet, 40, 40, 5, (0, 0), s, 1e-6).is_err());
assert!(fdtd_2d_tm(&eps, (99, 0), &quiet, 40, 40, 5, (0, 0), s, 1e-6).is_err());
assert!(fdtd_2d_tm(&eps, (0, 0), &quiet, 40, 40, 5, (25, 0), s, 1e-6).is_err());
assert!(fdtd_2d_tm(&eps, (0, 0), &quiet, 40, 40, 5, (0, 0), 0.9, 1e-6).is_err());
assert!(fdtd_2d_tm(&eps, (0, 0), &quiet, 40, 40, 5, (0, 0), s, 0.0).is_err());
assert!(fdtd_2d_tm(&eps, (0, 0), &quiet, 40, 40, 5, (0, 0), s, 1.5).is_err());
let mut bad = eps.clone();
bad[7] = -1.0;
assert!(fdtd_2d_tm(&bad, (0, 0), &quiet, 40, 40, 5, (0, 0), s, 1e-6).is_err());
assert!(waveguide_cutoff_check_fdtd(16, 20, 1, 0.05, s, 500).is_err());
assert!(waveguide_cutoff_check_fdtd(2, 200, 1, 0.05, s, 500).is_err());
assert!(waveguide_cutoff_check_fdtd(16, 200, 1, -1.0, s, 500).is_err());
assert!(waveguide_cutoff_check_fdtd(16, 200, 1, 0.05, 1.0, 500).is_err());
}
#[test]
fn the_solvers_refuse_impossible_arguments() {
let eps = vec![1.0; 10];
assert!(fdtd_1d(&eps[..2], &pulse, 0, 0.5, 3, Boundary1d::Mur).is_err());
assert!(fdtd_1d(&[1.0, 0.0, 1.0], &pulse, 0, 0.5, 3, Boundary1d::Mur).is_err());
assert!(fdtd_1d(&eps, &pulse, 99, 0.5, 3, Boundary1d::Mur).is_err());
assert!(fdtd_1d(&eps, &pulse, 0, 0.0, 3, Boundary1d::Mur).is_err());
assert!(fdtd_1d(&eps, &pulse, 0, 1.5, 3, Boundary1d::Mur).is_err());
assert!(photonic_crystal_bandgap_1d(0.0, 1.0, 1.0, 1.0, 1.0, 10).is_err());
assert!(photonic_crystal_bandgap_1d(1.0, 1.0, 1.0, 1.0, -1.0, 10).is_err());
assert!(photonic_crystal_bandgap_1d(1.0, 2.0, 1.0, 1.0, 1.0, 1).is_err());
}
}