use pantometry::em::{Boundary, Cavity, Medium, Wall};
use pantometry::optics::fresnel_reflectance;
use pantometry::prelude::*;
const PER_WAVELENGTH: usize = 40;
fn testbed(cells_along_z: usize, dx: f64) -> Cavity {
let mut c = Cavity::new(
"slab",
(2, 1, cells_along_z),
Length::from_si(dx),
Medium::vacuum(),
);
c.set_boundary(Wall::XLow, Boundary::Magnetic);
c.set_boundary(Wall::XHigh, Boundary::Magnetic);
c.set_boundary(Wall::ZLow, Boundary::Open);
c.set_boundary(Wall::ZHigh, Boundary::Open);
c
}
fn pulse(centre: f64, width: f64, wavelength: f64) -> impl Fn(f64, f64, f64) -> f64 {
let k = 2.0 * std::f64::consts::PI / wavelength;
move |_x: f64, _y: f64, z: f64| {
let u = (z - centre) / width;
(-u * u).exp() * (k * (z - centre)).cos()
}
}
fn watch(c: &mut Cavity, monitor: usize, steps: usize, dt: Time) -> Vec<f64> {
let mut bus = Exchange::new();
let mut out = Vec::with_capacity(steps);
for n in 0..steps {
c.step(Time::from_si(n as f64 * dt.to_si()), dt, &mut bus)
.expect("stable");
out.push(c.electric_at(1, 0, monitor).y);
}
out
}
fn energy(series: &[f64], from: usize, to: usize) -> f64 {
series[from.min(series.len())..to.min(series.len())]
.iter()
.map(|v| v * v)
.sum()
}
fn centroid(series: &[f64], from: usize, to: usize, dt: f64) -> f64 {
let slice = &series[from.min(series.len())..to.min(series.len())];
let total: f64 = slice.iter().map(|v| v * v).sum();
if total <= 0.0 {
return 0.0;
}
let weighted: f64 = slice
.iter()
.enumerate()
.map(|(i, v)| (from + i) as f64 * v * v)
.sum();
weighted / total * dt
}
struct Layout {
source: f64,
width: f64,
monitor: f64,
interface: f64,
length: f64,
}
impl Layout {
fn cells(&self, what: f64, dx: f64, wavelength: f64) -> usize {
(what * wavelength / dx).round() as usize
}
}
fn reflectance(
layout: &Layout,
wavelength: f64,
per_wavelength: usize,
build: impl Fn(&mut Cavity, usize),
) -> f64 {
let dx = wavelength / per_wavelength as f64;
let nz = layout.cells(layout.length, dx, wavelength);
let interface = layout.cells(layout.interface, dx, wavelength);
let monitor = layout.cells(layout.monitor, dx, wavelength);
let mut c = testbed(nz, dx);
build(&mut c, interface);
let dt = Time::from_si(c.courant_limit().to_si() * 0.5);
c.launch_along_z(
dt,
pulse(
layout.source * wavelength,
layout.width * wavelength,
wavelength,
),
);
let step_distance = 299_792_458.0 * dt.to_si();
let there = ((layout.monitor - layout.source) * wavelength / step_distance) as usize;
let back = ((2.0 * layout.interface - layout.monitor - layout.source) * wavelength
/ step_distance) as usize;
let series = watch(&mut c, monitor, back + (back - there), dt);
let split = (there + back) / 2;
energy(&series, split, series.len()) / energy(&series, 0, split)
}
#[test]
fn the_grid_and_the_algebra_agree_on_a_surfaces_reflectance() {
let wavelength = 500e-9;
let layout = Layout {
source: 4.0,
width: 1.0,
monitor: 8.0,
interface: 14.0,
length: 20.0,
};
let closed = fresnel_reflectance(1.0, 2.0, 1.0);
let mut errors = Vec::new();
for per in [20usize, 40, 80] {
let measured = reflectance(&layout, wavelength, per, |c, interface| {
c.fill(Medium::dielectric(4.0), |_, _, k| k >= interface);
});
let error = (measured / closed - 1.0).abs();
println!(
" {per:>2} cells per wavelength: {:.4}% against Fresnel's {:.4}% — off {:.3}%",
measured * 100.0,
closed * 100.0,
error * 100.0
);
errors.push(error);
}
for pair in errors.windows(2) {
let rate = pair[0] / pair[1];
println!(" refinement ratio {rate:.2} (second order is 4)");
assert!(
(2.6..6.0).contains(&rate),
"the agreement converges at second order: {rate:.3}"
);
}
for n2 in [1.5, 2.0, 3.5] {
let measured = reflectance(&layout, wavelength, 80, |c, interface| {
c.fill(Medium::dielectric(n2 * n2), |_, _, k| k >= interface);
});
let closed = fresnel_reflectance(1.0, n2, 1.0);
println!(
" n = {n2}: FDTD {:.4}% against Fresnel {:.4}% — off {:.3}%",
measured * 100.0,
closed * 100.0,
(measured / closed - 1.0).abs() * 100.0
);
assert!(
(measured / closed - 1.0).abs() < 0.02,
"and lands on it at eighty cells: {measured:.5} against {closed:.5}"
);
}
}
#[test]
fn a_quarter_wave_coating_cancels_its_own_reflection() {
let wavelength = 500e-9;
let layout = Layout {
source: 10.0,
width: 3.0,
monitor: 22.0,
interface: 34.0,
length: 50.0,
};
let n_glass = 2.25f64;
let n_coat = n_glass.sqrt();
let dx = wavelength / PER_WAVELENGTH as f64;
let thickness_cells = (wavelength / (4.0 * n_coat) / dx).round() as usize;
let mut measured = Vec::new();
for coated in [false, true] {
let r = reflectance(&layout, wavelength, PER_WAVELENGTH, |c, interface| {
c.fill(Medium::dielectric(n_glass * n_glass), |_, _, k| {
k >= interface
});
if coated {
c.fill(Medium::dielectric(n_coat * n_coat), |_, _, k| {
k >= interface - thickness_cells && k < interface
});
}
});
measured.push(r);
println!(
" {:<8} reflectance {:.4}%",
if coated { "coated:" } else { "bare:" },
r * 100.0
);
}
let bare_closed = fresnel_reflectance(1.0, n_glass, 1.0);
println!(
" bare against Fresnel {:.4}%, and the coating removed {:.1}x of it",
bare_closed * 100.0,
measured[0] / measured[1].max(1e-12)
);
assert!(
(measured[0] / bare_closed - 1.0).abs() < 0.05,
"the bare surface is the Fresnel one: {:.5} against {bare_closed:.5}",
measured[0]
);
assert!(
measured[1] < measured[0] / 10.0,
"a quarter wave of the geometric mean must very nearly cancel it: {:.5} against {:.5}",
measured[1],
measured[0]
);
}
#[test]
fn a_slab_delays_a_pulse_by_exactly_its_index() {
let wavelength = 500e-9;
let dx = wavelength / PER_WAVELENGTH as f64;
let nz = 40 * PER_WAVELENGTH;
let n = 2.0f64;
let slab = (10 * PER_WAVELENGTH, 20 * PER_WAVELENGTH);
let monitor = 32 * PER_WAVELENGTH;
let mut arrivals = Vec::new();
for with_slab in [false, true] {
let mut c = testbed(nz, dx);
if with_slab {
c.fill(Medium::dielectric(n * n), |_, _, k| {
k >= slab.0 && k < slab.1
});
}
let dt = Time::from_si(c.courant_limit().to_si() * 0.5);
c.launch_along_z(dt, pulse(4.0 * wavelength, 1.2 * wavelength, wavelength));
let step_distance = 299_792_458.0 * dt.to_si();
let steps = (1.4 * (monitor as f64 * dx) / step_distance) as usize;
let series = watch(&mut c, monitor, steps, dt);
arrivals.push(centroid(&series, 0, series.len(), dt.to_si()));
}
let extra = arrivals[1] - arrivals[0];
let thickness = (slab.1 - slab.0) as f64 * dx;
let closed = thickness * (n - 1.0) / 299_792_458.0;
println!(
" a {:.2} um slab of n = {n} delayed the pulse {:.4} fs against (n-1)d/c = {:.4} fs",
thickness * 1e6,
extra * 1e15,
closed * 1e15
);
assert!(
(extra / closed - 1.0).abs() < 0.06,
"the delay is (n-1)d/c: {:.4e} against {closed:.4e}",
extra
);
}