use super::Engine;
use super::coeffs::Coefficients;
use super::edge_material::EdgeMaterials;
use super::grid::Grid;
use crate::axis::Axis;
use crate::constants::{C0, EPS0, MU0, Z0};
use crate::fdtd::Face;
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Medium {
pub eps_r: f64,
pub mu_r: f64,
pub kappa: f64,
pub sigma_m: f64,
}
impl Default for Medium {
fn default() -> Self {
Self::VACUUM
}
}
impl Medium {
pub const VACUUM: Self = Self {
eps_r: 1.0,
mu_r: 1.0,
kappa: 0.0,
sigma_m: 0.0,
};
pub const fn lossless(eps_r: f64, mu_r: f64) -> Self {
Self {
eps_r,
mu_r,
kappa: 0.0,
sigma_m: 0.0,
}
}
pub fn is_lossless(&self) -> bool {
self.kappa == 0.0 && self.sigma_m == 0.0
}
pub fn velocity(&self) -> f64 {
C0 / (self.eps_r * self.mu_r).sqrt()
}
pub fn impedance(&self) -> f64 {
Z0 * (self.mu_r / self.eps_r).sqrt()
}
pub fn validate(&self) -> Result<()> {
let pos = |x: f64| x.is_finite() && x > 0.0;
let nonneg = |x: f64| x.is_finite() && x >= 0.0;
if pos(self.eps_r) && pos(self.mu_r) && nonneg(self.kappa) && nonneg(self.sigma_m) {
Ok(())
} else {
Err(Error::Invalid(format!("invalid medium {self:?}")))
}
}
pub fn of_box_surface(engine: &Engine, lo: [usize; 3], hi: [usize; 3]) -> Result<Option<Self>> {
let grid = engine.grid();
if grid.is_cylindrical() {
return Err(Error::Unsupported(
"background detection needs a Cartesian grid".into(),
));
}
let n = grid.nodes();
if (0..3).any(|a| lo[a] > hi[a] || hi[a] >= n[a]) {
return Err(Error::Invalid(format!(
"box {lo:?}..={hi:?} does not fit the {n:?} nodes"
)));
}
let faces = interior_faces(grid, lo, hi);
let co = engine.coefficients();
let media = CoefficientMedia {
grid,
co: &co,
dt: engine.dt(),
};
surface_medium(grid, &media, lo, hi, faces).map_err(Error::Invalid)
}
}
pub(crate) fn interior_faces(grid: &Grid, lo: [usize; 3], hi: [usize; 3]) -> [bool; 6] {
let n = grid.nodes();
Face::ALL.map(|f| {
let a = f.axis().index();
if f.is_max() {
hi[a] + 1 < n[a]
} else {
lo[a] > 0
}
})
}
pub(crate) trait EdgeMedia {
fn e_edge(&self, axis: Axis, node: [usize; 3]) -> Option<(f64, f64)>;
fn h_edge(&self, axis: Axis, node: [usize; 3]) -> Option<(f64, f64)>;
}
impl EdgeMedia for EdgeMaterials {
fn e_edge(&self, axis: Axis, node: [usize; 3]) -> Option<(f64, f64)> {
let i = index(self.nodes(), node);
(!self.pec_mask(axis)[i]).then(|| (self.eps_r(axis)[i], self.kappa(axis)[i]))
}
fn h_edge(&self, axis: Axis, node: [usize; 3]) -> Option<(f64, f64)> {
let i = index(self.nodes(), node);
Some((self.mu_r(axis)[i], self.sigma_m(axis)[i]))
}
}
fn index(n: [usize; 3], [i, j, k]: [usize; 3]) -> usize {
(i * n[1] + j) * n[2] + k
}
pub(crate) struct CoefficientMedia<'a> {
pub grid: &'a Grid,
pub co: &'a Coefficients,
pub dt: f64,
}
impl EdgeMedia for CoefficientMedia<'_> {
fn e_edge(&self, axis: Axis, p: [usize; 3]) -> Option<(f64, f64)> {
let i = self.grid.index(p);
let (ca, cb) = (
f64::from(self.co.ca(axis)[i]),
f64::from(self.co.cb(axis)[i]),
);
if cb == 0.0 {
return None;
}
let (b, c) = axis.others();
let g = self.grid;
let geo = g.dual_delta(b, p[b.index()]) * g.dual_delta(c, p[c.index()])
/ g.delta(axis, p[axis.index()]);
let cap = (1.0 + ca) * self.dt / (2.0 * cb);
let cond = (1.0 - ca) / cb;
Some((cap / (EPS0 * geo), (cond / geo).max(0.0)))
}
fn h_edge(&self, axis: Axis, q: [usize; 3]) -> Option<(f64, f64)> {
let i = self.grid.index(q);
let (da, db) = (
f64::from(self.co.da(axis)[i]),
f64::from(self.co.db(axis)[i]),
);
if db == 0.0 {
return None;
}
let (b, c) = axis.others();
let g = self.grid;
let geo = g.delta(b, q[b.index()]) * g.delta(c, q[c.index()])
/ g.dual_delta(axis, q[axis.index()]);
let ind = (1.0 + da) * self.dt / (2.0 * db);
let res = (1.0 - da) / db;
Some((ind / (MU0 * geo), (res / geo).max(0.0)))
}
}
struct Sample {
what: &'static str,
axis: Axis,
node: [usize; 3],
value: (f64, f64),
}
pub(crate) fn surface_medium(
grid: &Grid,
media: &impl EdgeMedia,
lo: [usize; 3],
hi: [usize; 3],
faces: [bool; 6],
) -> core::result::Result<Option<Medium>, String> {
let n = grid.nodes();
let (mut e, mut h): (Vec<Sample>, Vec<Sample>) = (Vec::new(), Vec::new());
for face in Face::ALL {
if !faces[face.index()] {
continue;
}
let a = face.axis();
let ai = a.index();
let p = if face.is_max() { hi[ai] } else { lo[ai] };
let (b, c) = a.others();
for (t, u) in [(b, c), (c, b)] {
let (ti, ui) = (t.index(), u.index());
for pt in lo[ti]..hi[ti] {
for pu in lo[ui]..=hi[ui] {
let mut q = [0; 3];
(q[ai], q[ti], q[ui]) = (p, pt, pu);
if let Some(v) = media.e_edge(t, q) {
e.push(Sample {
what: "E",
axis: t,
node: q,
value: v,
});
}
}
}
for pa in [p.wrapping_sub(1), p] {
if pa + 1 >= n[ai] {
continue;
}
for pt in lo[ti]..=hi[ti] {
for pu in lo[ui]..hi[ui] {
let mut q = [0; 3];
(q[ai], q[ti], q[ui]) = (pa, pt, pu);
if let Some(v) = media.h_edge(t, q) {
h.push(Sample {
what: "H",
axis: t,
node: q,
value: v,
});
}
}
}
}
}
for pb in lo[b.index()]..hi[b.index()] {
for pc in lo[c.index()]..hi[c.index()] {
let mut q = [0; 3];
(q[ai], q[b.index()], q[c.index()]) = (p, pb, pc);
if let Some(v) = media.h_edge(a, q) {
h.push(Sample {
what: "H",
axis: a,
node: q,
value: v,
});
}
}
}
}
let first = |s: &[Sample]| s.first().map(|x| x.value);
let (ev, hv) = match (first(&e), first(&h)) {
(None, None) => return Ok(None),
(Some(ev), None) => (ev, (1.0, 0.0)),
(None, Some(hv)) => ((1.0, 0.0), hv),
(Some(ev), Some(hv)) => (ev, hv),
};
let medium = Medium {
eps_r: ev.0,
kappa: ev.1,
mu_r: hv.0,
sigma_m: hv.1,
};
let d_min = Axis::ALL
.map(|ax| grid.min_delta(ax))
.into_iter()
.fold(f64::INFINITY, f64::min);
let tol = 1e-4;
let k_scale = EPS0 * ev.0 * C0 / d_min;
let s_scale = MU0 * hv.0 * C0 / d_min;
let close = |x: f64, y: f64, scale: f64| (x - y).abs() <= tol * scale.max(x.abs());
let check = |s: &[Sample], (v0, l0): (f64, f64), scale: f64, names: [&str; 2]| {
for x in s {
let (v, l) = x.value;
if !(close(v, v0, v0) && close(l, l0, scale)) {
let r = &s[0];
return Err(format!(
"the medium on the box surface is not homogeneous: {} edge along {} at {:?} \
has {} = {v}, {} = {l}, but {} edge along {} at {:?} has {} = {v0}, {} = {l0}",
x.what,
x.axis,
x.node,
names[0],
names[1],
r.what,
r.axis,
r.node,
names[0],
names[1]
));
}
}
Ok(())
};
check(&e, ev, k_scale, ["eps_r", "kappa"])?;
check(&h, hv, s_scale, ["mu_r", "sigma_m"])?;
Ok(Some(medium))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::{CellMaterials, Material};
#[test]
fn detects_homogeneous_and_rejects_mixed() {
let g = Grid::uniform([12; 3], [1e-3; 3]).unwrap();
let bg = Material::dielectric(4.0).with_kappa(0.05).with_mu_r(1.5);
let mut m = CellMaterials::new(&g, bg);
let em = EdgeMaterials::from_cells(&g, &m).unwrap();
let all = [true; 6];
let got = surface_medium(&g, &em, [3; 3], [8; 3], all)
.unwrap()
.unwrap();
assert!((got.eps_r - 4.0).abs() < 1e-12 && (got.mu_r - 1.5).abs() < 1e-12);
assert!((got.kappa - 0.05).abs() < 1e-12 && got.sigma_m == 0.0);
let e = Engine::builder(g.clone())
.materials(m.clone())
.threads(1)
.build()
.unwrap();
let r = Medium::of_box_surface(&e, [3; 3], [8; 3]).unwrap().unwrap();
assert!((r.eps_r / 4.0 - 1.0).abs() < 1e-6, "{r:?}");
assert!((r.mu_r / 1.5 - 1.0).abs() < 1e-6, "{r:?}");
assert!((r.kappa / 0.05 - 1.0).abs() < 1e-3, "{r:?}");
m.fill_box([5..6, 5..6, 5..6], Material::dielectric(9.0));
let em = EdgeMaterials::from_cells(&g, &m).unwrap();
assert!(surface_medium(&g, &em, [3; 3], [8; 3], all).is_ok());
m.fill_box([8..9, 5..6, 5..6], Material::dielectric(9.0));
let em = EdgeMaterials::from_cells(&g, &m).unwrap();
let err = surface_medium(&g, &em, [3; 3], [8; 3], all).unwrap_err();
assert!(err.contains("not homogeneous"), "{err}");
let mut f = all;
f[Face::XMax.index()] = false;
assert!(surface_medium(&g, &em, [3; 3], [8; 3], f).is_ok());
let mut aniso = Material::VACUUM;
aniso.eps_r = [1.0, 2.0, 1.0];
let em = EdgeMaterials::from_cells(&g, &CellMaterials::new(&g, aniso)).unwrap();
assert!(surface_medium(&g, &em, [3; 3], [8; 3], all).is_err());
}
}