use core::ops::{Index, IndexMut};
use super::edge_material::EdgeMaterials;
use super::grid::Grid;
use super::material::CellMaterials;
use crate::axis::Axis;
use crate::constants::{C0, EPS0, MU0};
use crate::fdtd::Face;
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FaceCondition {
#[default]
Pec,
Pmc,
Periodic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct FaceConditions([FaceCondition; 6]);
impl FaceConditions {
pub const fn all(c: FaceCondition) -> Self {
Self([c; 6])
}
pub const fn from_array(faces: [FaceCondition; 6]) -> Self {
Self(faces)
}
pub const fn to_array(self) -> [FaceCondition; 6] {
self.0
}
pub const fn with(mut self, face: Face, c: FaceCondition) -> Self {
self.0[face.index()] = c;
self
}
pub fn set(&mut self, face: Face, c: FaceCondition) {
self.0[face.index()] = c;
}
pub const fn get(&self, face: Face) -> FaceCondition {
self.0[face.index()]
}
pub fn iter(&self) -> impl Iterator<Item = (Face, FaceCondition)> + '_ {
Face::ALL.into_iter().zip(self.0.iter().copied())
}
}
impl From<[FaceCondition; 6]> for FaceConditions {
fn from(faces: [FaceCondition; 6]) -> Self {
Self(faces)
}
}
impl Index<Face> for FaceConditions {
type Output = FaceCondition;
fn index(&self, face: Face) -> &FaceCondition {
&self.0[face.index()]
}
}
impl IndexMut<Face> for FaceConditions {
fn index_mut(&mut self, face: Face) -> &mut FaceCondition {
&mut self.0[face.index()]
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TimeStep {
Courant(f64),
Fixed(f64),
}
impl Default for TimeStep {
fn default() -> Self {
TimeStep::Courant(0.99)
}
}
impl TimeStep {
pub fn resolve(self, grid: &Grid) -> Result<f64> {
let max = courant_limit(grid);
match self {
TimeStep::Courant(s) if s.is_finite() && s > 0.0 && s <= 1.0 => Ok(s * max),
TimeStep::Courant(s) => Err(Error::Invalid(format!(
"Courant factor {s} must be in (0, 1]"
))),
TimeStep::Fixed(dt) if dt.is_finite() && dt > 0.0 && dt <= max => Ok(dt),
TimeStep::Fixed(dt) => Err(Error::Invalid(format!(
"time step {dt:e} s must be positive and at most the stability limit {max:e} s"
))),
}
}
}
pub fn courant_limit(grid: &Grid) -> f64 {
if grid.is_cylindrical() {
return grid.cached_courant(super::cylindrical::courant_limit);
}
let s: f64 = grid.min_deltas().iter().map(|d| 1.0 / (d * d)).sum();
1.0 / (C0 * s.sqrt())
}
pub fn primary_edge_cg(
grid: &Grid,
mats: &CellMaterials,
axis: Axis,
node: [usize; 3],
) -> (f64, f64) {
let n = grid.nodes();
let (ab, ac) = axis.others();
let (a, b, c) = (axis.index(), ab.index(), ac.index());
assert!(node[a] + 1 < n[a], "not a primary edge");
let l = grid.delta(axis, node[a]);
let (mut cap, mut cond) = (0.0, 0.0);
for cb in [node[b].wrapping_sub(1), node[b]] {
if cb >= n[b] - 1 {
continue;
}
for cc in [node[c].wrapping_sub(1), node[c]] {
if cc >= n[c] - 1 {
continue;
}
let mut cell = [0; 3];
cell[a] = node[a];
cell[b] = cb;
cell[c] = cc;
let area = 0.25 * grid.delta(ab, cb) * grid.delta(ac, cc);
let m = mats.get(cell);
cap += EPS0 * m.eps_r[a] * area;
cond += m.kappa[a] * area;
}
}
(cap / l, cond / l)
}
pub fn dual_edge_lr(grid: &Grid, mats: &CellMaterials, axis: Axis, node: [usize; 3]) -> (f64, f64) {
let n = grid.nodes();
let (ab, ac) = axis.others();
let (a, b, c) = (axis.index(), ab.index(), ac.index());
assert!(node[b] + 1 < n[b] && node[c] + 1 < n[c], "not a dual edge");
let area = grid.delta(ab, node[b]) * grid.delta(ac, node[c]);
let (mut inv_l, mut r_over_l2) = (0.0, 0.0);
for ca in [node[a].wrapping_sub(1), node[a]] {
if ca >= n[a] - 1 {
continue;
}
let mut cell = node;
cell[a] = ca;
let half = 0.5 * grid.delta(axis, ca);
let m = mats.get(cell);
let lc = MU0 * m.mu_r[a] * area / half;
let rc = m.sigma_m[a] * area / half;
inv_l += 1.0 / lc;
r_over_l2 += rc / (lc * lc);
}
let l = 1.0 / inv_l;
(l, l * l * r_over_l2)
}
#[derive(Debug, Clone, PartialEq)]
pub struct Coefficients {
pub(crate) data: [Vec<f32>; 12],
nodes: [usize; 3],
}
impl Coefficients {
pub fn compute(
grid: &Grid,
mats: &CellMaterials,
faces: &FaceConditions,
dt: f64,
) -> Result<Self> {
if mats.cells() != grid.cells() {
return Err(Error::Invalid(format!(
"material map has {:?} cells, grid has {:?}",
mats.cells(),
grid.cells()
)));
}
mats.validate()?;
Self::check_dt(dt)?;
Self::from_edges(grid, &EdgeMaterials::from_cells(grid, mats)?, faces, dt)
}
fn check_dt(dt: f64) -> Result<()> {
if dt.is_finite() && dt > 0.0 {
Ok(())
} else {
Err(Error::Invalid(format!("invalid time step {dt}")))
}
}
pub fn from_edges(
grid: &Grid,
mats: &EdgeMaterials,
faces: &FaceConditions,
dt: f64,
) -> Result<Self> {
if mats.nodes() != grid.nodes() {
return Err(Error::Invalid(format!(
"edge material map has {:?} nodes, grid has {:?}",
mats.nodes(),
grid.nodes()
)));
}
mats.validate()?;
Self::check_dt(dt)?;
let n = grid.nodes();
let len = grid.num_nodes();
let mut data: [Vec<f32>; 12] = core::array::from_fn(|_| vec![0.0; len]);
let co = &mut data;
let cyl = grid.is_cylindrical();
let faces = super::cylindrical::effective_faces(grid, faces);
let pec_face = Axis::ALL
.map(|t| [false, true].map(|max| faces[Face::new(t, max)] == FaceCondition::Pec));
for axis in Axis::ALL {
let (eps, kap) = (mats.eps_r(axis), mats.kappa(axis));
let (mu, sig) = (mats.mu_r(axis), mats.sigma_m(axis));
let pec = mats.pec_mask(axis);
let (ab, ac) = axis.others();
let (a, b, c) = (axis.index(), ab.index(), ac.index());
for i in 0..n[0] {
for j in 0..n[1] {
for k in 0..n[2] {
let p = [i, j, k];
let idx = grid.index(p);
if p[a] + 1 < n[a] {
let on_pec_face = [b, c].iter().any(|&t| {
(p[t] == 0 && pec_face[t][0])
|| (p[t] == n[t] - 1 && pec_face[t][1])
});
let degenerate = cyl
&& (grid.edge_length(axis, p) == 0.0 || grid.mg_removed_e(axis, p));
if !(on_pec_face || pec[idx] || degenerate) {
let geo = if cyl {
grid.dual_face_area(axis, p) / grid.edge_length(axis, p)
} else {
grid.dual_delta(ab, p[b]) * grid.dual_delta(ac, p[c])
/ grid.delta(axis, p[a])
};
let cap = EPS0 * eps[idx] * geo;
let g = kap[idx] * geo;
let den = cap + 0.5 * g * dt;
co[a][idx] = ((cap - 0.5 * g * dt) / den) as f32;
co[3 + a][idx] = (dt / den) as f32;
}
}
if p[b] + 1 < n[b]
&& p[c] + 1 < n[c]
&& !(cyl
&& (grid.face_area(axis, p) == 0.0 || grid.mg_removed_h(axis, p)))
{
let geo = if cyl {
grid.face_area(axis, p) / grid.dual_edge_length(axis, p)
} else {
grid.delta(ab, p[b]) * grid.delta(ac, p[c])
/ grid.dual_delta(axis, p[a])
};
let l = MU0 * mu[idx] * geo;
let r = sig[idx] * geo;
let den = l + 0.5 * r * dt;
co[6 + a][idx] = ((l - 0.5 * r * dt) / den) as f32;
co[9 + a][idx] = (dt / den) as f32;
}
}
}
}
}
Ok(Self { data, nodes: n })
}
pub fn nodes(&self) -> [usize; 3] {
self.nodes
}
pub fn ca(&self, axis: Axis) -> &[f32] {
&self.data[axis.index()]
}
pub fn cb(&self, axis: Axis) -> &[f32] {
&self.data[3 + axis.index()]
}
pub fn da(&self, axis: Axis) -> &[f32] {
&self.data[6 + axis.index()]
}
pub fn db(&self, axis: Axis) -> &[f32] {
&self.data[9 + axis.index()]
}
pub fn e_mut(&mut self, axis: Axis) -> (&mut [f32], &mut [f32]) {
self.pair_mut(axis.index())
}
pub fn h_mut(&mut self, axis: Axis) -> (&mut [f32], &mut [f32]) {
self.pair_mut(6 + axis.index())
}
fn pair_mut(&mut self, first: usize) -> (&mut [f32], &mut [f32]) {
let (lo, hi) = self.data.split_at_mut(first + 3);
(&mut lo[first], &mut hi[0])
}
#[inline]
pub(crate) fn capacitance(ca: f32, cb: f32, dt: f64) -> f64 {
if cb == 0.0 {
0.0
} else {
dt * (1.0 + ca as f64) / (2.0 * cb as f64)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::material::Material;
fn rel(a: f64, b: f64) -> f64 {
((a - b) / b).abs()
}
#[test]
fn courant() {
let g = Grid::uniform([4, 4, 4], [1e-3, 2e-3, 1e-3]).unwrap();
let lim = courant_limit(&g);
let want = 1.0 / (C0 * (1e6f64 + 0.25e6 + 1e6).sqrt());
assert!(rel(lim, want) < 1e-14);
assert!(rel(TimeStep::default().resolve(&g).unwrap(), 0.99 * want) < 1e-14);
assert_eq!(TimeStep::Fixed(0.5 * lim).resolve(&g).unwrap(), 0.5 * lim);
assert!(TimeStep::Fixed(lim).resolve(&g).is_ok());
assert!(TimeStep::Fixed(1.0001 * lim).resolve(&g).is_err());
assert!(TimeStep::Fixed(-1.0).resolve(&g).is_err());
assert!(TimeStep::Fixed(f64::NAN).resolve(&g).is_err());
assert!(TimeStep::Courant(1.01).resolve(&g).is_err());
assert!(TimeStep::Courant(0.0).resolve(&g).is_err());
let g2 = Grid::new([vec![0.0, 1.0, 1.5], vec![0.0, 1.0], vec![0.0, 1.0]]).unwrap();
let want = 1.0 / (C0 * (4.0f64 + 1.0 + 1.0).sqrt());
assert!(rel(courant_limit(&g2), want) < 1e-14);
}
#[test]
fn edge_between_dielectric_and_vacuum() {
let g = Grid::new([
vec![0.0, 1.0, 2.0],
vec![0.0, 1.0, 3.0],
vec![0.0, 2.0, 3.0],
])
.unwrap();
let mut m = CellMaterials::vacuum(&g);
m.fill_box(
[0..2, 0..1, 0..2],
Material::dielectric(4.0).with_kappa(2.0),
);
let (c, gg) = primary_edge_cg(&g, &m, Axis::X, [0, 1, 1]);
let a_diel = 0.5 * (1.0 + 0.5);
let a_vac = 1.0 * (1.0 + 0.5);
let want_c = EPS0 * (4.0 * a_diel + a_vac) / 1.0;
assert!(rel(c, want_c) < 1e-14);
assert!(rel(gg, 2.0 * a_diel) < 1e-14);
let eps_eff = c * 1.0 / (EPS0 * (a_diel + a_vac));
assert!(rel(eps_eff, (4.0 * a_diel + a_vac) / (a_diel + a_vac)) < 1e-14);
let (c, _) = primary_edge_cg(&g, &m, Axis::X, [1, 0, 0]);
assert!(rel(c, EPS0 * 4.0 * 0.5 * 1.0) < 1e-14);
}
#[test]
fn dual_edge_series() {
let g = Grid::new([vec![0.0, 1.0, 3.0], vec![0.0, 1.0], vec![0.0, 2.0]]).unwrap();
let mut m = CellMaterials::vacuum(&g);
m.set([1, 0, 0], Material::VACUUM.with_mu_r(3.0).with_sigma_m(5.0));
let (l, r) = dual_edge_lr(&g, &m, Axis::X, [1, 0, 0]);
let area = 2.0;
let l1 = MU0 * area / 0.5;
let l2 = MU0 * 3.0 * area / 1.0;
let want = 1.0 / (1.0 / l1 + 1.0 / l2);
assert!(rel(l, want) < 1e-14);
assert!(rel(r, want * want * (5.0 * area / 1.0) / (l2 * l2)) < 1e-14);
let mut m2 = CellMaterials::new(&g, Material::VACUUM.with_mu_r(2.0).with_sigma_m(7.0));
m2.set([0, 0, 0], *m2.get([1, 0, 0]));
let (l, r) = dual_edge_lr(&g, &m2, Axis::X, [1, 0, 0]);
assert!(rel(l, MU0 * 2.0 * area / 1.5) < 1e-14);
assert!(rel(r, 7.0 * area / 1.5) < 1e-14);
let (l, _) = dual_edge_lr(&g, &m2, Axis::X, [2, 0, 0]);
assert!(rel(l, MU0 * 2.0 * area / 1.0) < 1e-14);
}
#[test]
fn faces_pec_and_loss() {
let g = Grid::uniform([3, 3, 3], [1e-3; 3]).unwrap();
let mut m = CellMaterials::new(&g, Material::VACUUM.with_kappa(1.0));
m.set_pec_edge(Axis::Z, [1, 1, 1]);
let dt = TimeStep::default().resolve(&g).unwrap();
let mut faces = FaceConditions::default();
let co = Coefficients::compute(&g, &m, &faces, dt).unwrap();
assert_eq!(co.cb(Axis::Y)[g.index([0, 1, 1])], 0.0);
assert!(co.cb(Axis::Y)[g.index([1, 1, 1])] > 0.0);
assert_eq!(co.cb(Axis::Z)[g.index([1, 1, 1])], 0.0);
assert_eq!(co.ca(Axis::Z)[g.index([1, 1, 1])], 0.0);
assert!(co.cb(Axis::X)[g.index([0, 1, 1])] > 0.0);
assert_eq!(co.cb(Axis::X)[g.index([3, 1, 1])], 0.0);
assert_eq!(co.db(Axis::X)[g.index([1, 3, 1])], 0.0);
let (c, gg) = primary_edge_cg(&g, &m, Axis::X, [1, 1, 1]);
let idx = g.index([1, 1, 1]);
let ca = (c - gg * dt / 2.0) / (c + gg * dt / 2.0);
assert!(rel(co.ca(Axis::X)[idx] as f64, ca) < 1e-6);
assert!(ca < 1.0);
assert!(
rel(
Coefficients::capacitance(co.ca(Axis::X)[idx], co.cb(Axis::X)[idx], dt),
c
) < 1e-6
);
faces[Face::XMin] = FaceCondition::Pmc;
let co = Coefficients::compute(&g, &m, &faces, dt).unwrap();
let (c_face, _) = primary_edge_cg(&g, &m, Axis::Y, [0, 1, 1]);
assert!(rel(c_face, 0.5 * c) < 1e-12);
assert!(co.cb(Axis::Y)[g.index([0, 1, 1])] > 0.0);
assert_eq!(co.cb(Axis::Y)[g.index([0, 1, 0])], 0.0);
}
}