use crate::math::Fix128;
use core::fmt;
#[cfg(not(feature = "std"))]
use alloc::boxed::Box;
#[cfg(not(feature = "std"))]
use alloc::vec;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
pub const COURANT_3D: Fix128 = Fix128::from_raw(0, 10_376_293_541_461_622_784);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Component {
Ex,
Ey,
Ez,
Hx,
Hy,
Hz,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Absorber {
None,
GradedPml {
depth: [usize; 3],
sigma_max: Fix128,
},
Uniform {
sigma: Fix128,
},
}
#[must_use]
pub fn loss_coefficients(sigma: Fix128, courant: Fix128) -> (Fix128, Fix128) {
if sigma.is_zero() {
return (Fix128::ONE, courant);
}
let a = (sigma * courant).half();
let denom = Fix128::ONE + a;
((Fix128::ONE - a) / denom, courant / denom)
}
fn cubic_graded_sigma(into2: i64, depth2: i64, sigma_max: Fix128) -> Fix128 {
if depth2 <= 0 || into2 <= 0 {
return Fix128::ZERO;
}
let t = Fix128::from_ratio(into2.min(depth2), depth2);
sigma_max * t * t * t
}
#[must_use]
pub fn theoretical_pml_reflection(depth: usize, sigma_max: Fix128) -> Fix128 {
if depth == 0 {
return Fix128::ONE;
}
let d2 = 2 * depth as i64;
let mut integral = Fix128::ZERO;
for cell in 0..depth as i64 {
integral = integral + cubic_graded_sigma(2 * cell + 1, d2, sigma_max);
}
(Fix128::from_int(-2) * integral).exp()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Material {
pub eps_r: Fix128,
pub mu_r: Fix128,
pub sigma: Fix128,
}
impl Material {
pub const VACUUM: Self = Self {
eps_r: Fix128::ONE,
mu_r: Fix128::ONE,
sigma: Fix128::ZERO,
};
#[must_use]
pub const fn new(eps_r: Fix128, mu_r: Fix128, sigma: Fix128) -> Self {
Self { eps_r, mu_r, sigma }
}
#[must_use]
pub const fn dielectric(eps_r: Fix128) -> Self {
Self {
eps_r,
mu_r: Fix128::ONE,
sigma: Fix128::ZERO,
}
}
#[must_use]
pub fn refractive_index(&self) -> Fix128 {
(self.eps_r * self.mu_r).sqrt()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MaterialMap {
nx: usize,
ny: usize,
nz: usize,
cells: Vec<Material>,
}
impl MaterialMap {
#[must_use]
pub fn vacuum(nx: usize, ny: usize, nz: usize) -> Self {
Self {
nx,
ny,
nz,
cells: vec![Material::VACUUM; nx * ny * nz],
}
}
#[must_use]
pub const fn dims(&self) -> (usize, usize, usize) {
(self.nx, self.ny, self.nz)
}
#[inline]
fn offset(&self, i: usize, j: usize, k: usize) -> usize {
assert!(
i < self.nx && j < self.ny && k < self.nz,
"cell index out of range"
);
(i * self.ny + j) * self.nz + k
}
#[must_use]
pub fn get(&self, i: usize, j: usize, k: usize) -> Material {
self.cells[self.offset(i, j, k)]
}
pub fn set(&mut self, i: usize, j: usize, k: usize, material: Material) {
let at = self.offset(i, j, k);
self.cells[at] = material;
}
pub fn fill(&mut self, lo: [usize; 3], hi: [usize; 3], material: Material) {
let dims = [self.nx, self.ny, self.nz];
assert!(
(0..3).all(|a| lo[a] <= hi[a] && hi[a] <= dims[a]),
"fill box {lo:?}..{hi:?} is not inside the map {dims:?}"
);
for i in lo[0]..hi[0] {
for j in lo[1]..hi[1] {
for k in lo[2]..hi[2] {
self.set(i, j, k, material);
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MaterialError {
DimensionMismatch {
lattice: (usize, usize, usize),
map: (usize, usize, usize),
},
NonPositivePermittivity {
cell: (usize, usize, usize),
},
NonPositivePermeability {
cell: (usize, usize, usize),
},
NegativeConductivity {
cell: (usize, usize, usize),
},
CourantViolated {
courant: Fix128,
eps_min: Fix128,
mu_min: Fix128,
},
AbsorberOverlap {
component: Component,
index: (usize, usize, usize),
},
}
impl fmt::Display for MaterialError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DimensionMismatch { lattice, map } => write!(
f,
"material map is {}x{}x{} but the lattice is {}x{}x{}",
map.0, map.1, map.2, lattice.0, lattice.1, lattice.2
),
Self::NonPositivePermittivity { cell } => {
write!(f, "cell {cell:?} has a non-positive permittivity")
}
Self::NonPositivePermeability { cell } => {
write!(f, "cell {cell:?} has a non-positive permeability")
}
Self::NegativeConductivity { cell } => {
write!(f, "cell {cell:?} has a negative conductivity")
}
Self::CourantViolated {
courant,
eps_min,
mu_min,
} => write!(
f,
"Courant number {} exceeds sqrt(eps_min*mu_min/3) with eps_min {} and mu_min {}",
courant.to_f64(),
eps_min.to_f64(),
mu_min.to_f64()
),
Self::AbsorberOverlap { component, index } => write!(
f,
"{component:?}{index:?} is inside the absorber and is not vacuum"
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for MaterialError {}
#[derive(Debug, Clone, PartialEq, Eq)]
struct MaterialCoeffs {
e_active: [Vec<bool>; 3],
e_ca: [Vec<Fix128>; 3],
e_cb: [Vec<Fix128>; 3],
e_eps: [Vec<Fix128>; 3],
e_sigma: [Vec<Fix128>; 3],
h_active: [Vec<bool>; 3],
h_cb: [Vec<Fix128>; 3],
h_mu: [Vec<Fix128>; 3],
}
fn arithmetic_mean(values: &[Fix128]) -> Fix128 {
if values.iter().all(|&v| v == values[0]) {
return values[0];
}
let sum = values.iter().fold(Fix128::ZERO, |acc, &v| acc + v);
sum / Fix128::from_int(values.len() as i64)
}
fn harmonic_mean(values: &[Fix128]) -> Fix128 {
if values.iter().all(|&v| v == values[0]) {
return values[0];
}
let inv = values
.iter()
.fold(Fix128::ZERO, |acc, &v| acc + Fix128::ONE / v);
Fix128::from_int(values.len() as i64) / inv
}
fn material_e_coefficients(eps: Fix128, sigma: Fix128, courant: Fix128) -> (Fix128, Fix128) {
let s_over_eps = courant / eps;
if sigma.is_zero() {
return (Fix128::ONE, s_over_eps);
}
let a = (sigma * courant).half() / eps;
let denom = Fix128::ONE + a;
((Fix128::ONE - a) / denom, s_over_eps / denom)
}
const fn component_axis(component: Component) -> usize {
match component {
Component::Ex | Component::Hx => 0,
Component::Ey | Component::Hy => 1,
Component::Ez | Component::Hz => 2,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Splits {
exy: Vec<Fix128>,
exz: Vec<Fix128>,
eyz: Vec<Fix128>,
eyx: Vec<Fix128>,
ezx: Vec<Fix128>,
ezy: Vec<Fix128>,
hxy: Vec<Fix128>,
hxz: Vec<Fix128>,
hyz: Vec<Fix128>,
hyx: Vec<Fix128>,
hzx: Vec<Fix128>,
hzy: Vec<Fix128>,
ca_e: [Vec<Fix128>; 3],
cb_e: [Vec<Fix128>; 3],
ca_h: [Vec<Fix128>; 3],
cb_h: [Vec<Fix128>; 3],
lossy_e: [Vec<bool>; 3],
lossy_h: [Vec<bool>; 3],
}
impl Splits {
fn halves_mut(&mut self, component: Component) -> (&mut Vec<Fix128>, &mut Vec<Fix128>) {
match component {
Component::Ex => (&mut self.exy, &mut self.exz),
Component::Ey => (&mut self.eyz, &mut self.eyx),
Component::Ez => (&mut self.ezx, &mut self.ezy),
Component::Hx => (&mut self.hxy, &mut self.hxz),
Component::Hy => (&mut self.hyz, &mut self.hyx),
Component::Hz => (&mut self.hzx, &mut self.hzy),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Sources {
jx: Vec<Fix128>,
jy: Vec<Fix128>,
jz: Vec<Fix128>,
rho: Vec<Fix128>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct YeeGrid {
nx: usize,
ny: usize,
nz: usize,
courant: Fix128,
ex: Vec<Fix128>,
ey: Vec<Fix128>,
ez: Vec<Fix128>,
hx: Vec<Fix128>,
hy: Vec<Fix128>,
hz: Vec<Fix128>,
sources: Option<Box<Sources>>,
absorber: Option<Box<Splits>>,
materials: Option<Box<MaterialCoeffs>>,
}
impl YeeGrid {
#[must_use]
pub fn new(nx: usize, ny: usize, nz: usize, courant: Fix128) -> Self {
assert!(
nx > 0 && ny > 0 && nz > 0,
"lattice must have a cell in each axis"
);
Self {
nx,
ny,
nz,
courant,
ex: vec![Fix128::ZERO; nx * (ny + 1) * (nz + 1)],
ey: vec![Fix128::ZERO; (nx + 1) * ny * (nz + 1)],
ez: vec![Fix128::ZERO; (nx + 1) * (ny + 1) * nz],
hx: vec![Fix128::ZERO; (nx + 1) * ny * nz],
hy: vec![Fix128::ZERO; nx * (ny + 1) * nz],
hz: vec![Fix128::ZERO; nx * ny * (nz + 1)],
sources: None,
absorber: None,
materials: None,
}
}
#[must_use]
pub fn new_with_absorber(
nx: usize,
ny: usize,
nz: usize,
courant: Fix128,
absorber: Absorber,
) -> Self {
let mut grid = Self::new(nx, ny, nz, courant);
let dims = [nx, ny, nz];
let sigma_at = |axis: usize, coord2: i64| -> Fix128 {
let n2 = 2 * dims[axis] as i64;
match absorber {
Absorber::None => Fix128::ZERO,
Absorber::Uniform { sigma } => sigma,
Absorber::GradedPml { depth, sigma_max } => {
let d2 = 2 * depth[axis] as i64;
let into2 = (d2 - coord2).max(coord2 - (n2 - d2));
cubic_graded_sigma(into2, d2, sigma_max)
}
}
};
if let Absorber::GradedPml { depth, .. } = absorber {
assert!(
(0..3).all(|a| 2 * depth[a] <= dims[a]),
"a PML layer must fit twice into its own axis"
);
}
let inert = match absorber {
Absorber::None => true,
Absorber::GradedPml { depth, .. } => depth == [0, 0, 0],
Absorber::Uniform { .. } => false,
};
if inert {
return grid;
}
let mut ca_e = [Vec::new(), Vec::new(), Vec::new()];
let mut cb_e = [Vec::new(), Vec::new(), Vec::new()];
let mut ca_h = [Vec::new(), Vec::new(), Vec::new()];
let mut cb_h = [Vec::new(), Vec::new(), Vec::new()];
let mut lossy_e = [Vec::new(), Vec::new(), Vec::new()];
let mut lossy_h = [Vec::new(), Vec::new(), Vec::new()];
for axis in 0..3 {
let n = dims[axis];
for coord in 0..=n {
let sigma = sigma_at(axis, 2 * coord as i64);
let (ca, cb) = loss_coefficients(sigma, courant);
ca_e[axis].push(ca);
cb_e[axis].push(cb);
lossy_e[axis].push(!sigma.is_zero());
}
for coord in 0..n {
let sigma = sigma_at(axis, 2 * coord as i64 + 1);
let (ca, cb) = loss_coefficients(sigma, courant);
ca_h[axis].push(ca);
cb_h[axis].push(cb);
lossy_h[axis].push(!sigma.is_zero());
}
}
grid.absorber = Some(Box::new(Splits {
exy: vec![Fix128::ZERO; grid.ex.len()],
exz: vec![Fix128::ZERO; grid.ex.len()],
eyz: vec![Fix128::ZERO; grid.ey.len()],
eyx: vec![Fix128::ZERO; grid.ey.len()],
ezx: vec![Fix128::ZERO; grid.ez.len()],
ezy: vec![Fix128::ZERO; grid.ez.len()],
hxy: vec![Fix128::ZERO; grid.hx.len()],
hxz: vec![Fix128::ZERO; grid.hx.len()],
hyz: vec![Fix128::ZERO; grid.hy.len()],
hyx: vec![Fix128::ZERO; grid.hy.len()],
hzx: vec![Fix128::ZERO; grid.hz.len()],
hzy: vec![Fix128::ZERO; grid.hz.len()],
ca_e,
cb_e,
ca_h,
cb_h,
lossy_e,
lossy_h,
}));
grid
}
pub fn with_materials(mut self, map: &MaterialMap) -> Result<Self, MaterialError> {
let dims = [self.nx, self.ny, self.nz];
if map.dims() != (self.nx, self.ny, self.nz) {
return Err(MaterialError::DimensionMismatch {
lattice: (self.nx, self.ny, self.nz),
map: map.dims(),
});
}
let mut eps_min = Fix128::ONE;
let mut mu_min = Fix128::ONE;
let mut first = true;
for i in 0..self.nx {
for j in 0..self.ny {
for k in 0..self.nz {
let m = map.get(i, j, k);
let cell = (i, j, k);
if m.eps_r <= Fix128::ZERO {
return Err(MaterialError::NonPositivePermittivity { cell });
}
if m.mu_r <= Fix128::ZERO {
return Err(MaterialError::NonPositivePermeability { cell });
}
if m.sigma.is_negative() {
return Err(MaterialError::NegativeConductivity { cell });
}
if first || m.eps_r < eps_min {
eps_min = m.eps_r;
}
if first || m.mu_r < mu_min {
mu_min = m.mu_r;
}
first = false;
}
}
}
let s = self.courant;
if Fix128::from_int(3) * s * s > eps_min * mu_min {
return Err(MaterialError::CourantViolated {
courant: s,
eps_min,
mu_min,
});
}
let mut mc = MaterialCoeffs {
e_active: Default::default(),
e_ca: Default::default(),
e_cb: Default::default(),
e_eps: Default::default(),
e_sigma: Default::default(),
h_active: Default::default(),
h_cb: Default::default(),
h_mu: Default::default(),
};
let around = |p: usize, n: usize| -> ([usize; 2], usize) {
match (p >= 1, p < n) {
(true, true) => ([p - 1, p], 2),
(true, false) => ([p - 1, 0], 1),
(false, true) => ([p, 0], 1),
(false, false) => ([0, 0], 0),
}
};
let mut cells = Vec::with_capacity(4);
for component in [
Component::Ex,
Component::Ey,
Component::Ez,
Component::Hx,
Component::Hy,
Component::Hz,
] {
let axis = component_axis(component);
let electric = matches!(component, Component::Ex | Component::Ey | Component::Ez);
let (ni, nj, nk) = self.component_dims(component);
for i in 0..ni {
for j in 0..nj {
for k in 0..nk {
let pos = [i, j, k];
let mut lists = [([0usize; 2], 1usize); 3];
for d in 0..3 {
lists[d] = if (d == axis) == electric {
([pos[d], 0], 1)
} else {
around(pos[d], dims[d])
};
}
cells.clear();
for &ci in &lists[0].0[..lists[0].1] {
for &cj in &lists[1].0[..lists[1].1] {
for &ck in &lists[2].0[..lists[2].1] {
cells.push(map.get(ci, cj, ck));
}
}
}
if electric {
let eps: Vec<Fix128> = cells.iter().map(|m| m.eps_r).collect();
let sig: Vec<Fix128> = cells.iter().map(|m| m.sigma).collect();
let eps = arithmetic_mean(&eps);
let sigma = arithmetic_mean(&sig);
let (ca, cb) = material_e_coefficients(eps, sigma, s);
let active = !(eps == Fix128::ONE && sigma.is_zero());
if active && self.is_absorbing(component, i, j, k) {
return Err(MaterialError::AbsorberOverlap {
component,
index: (i, j, k),
});
}
mc.e_active[axis].push(active);
mc.e_ca[axis].push(ca);
mc.e_cb[axis].push(cb);
mc.e_eps[axis].push(eps);
mc.e_sigma[axis].push(sigma);
} else {
let mus: Vec<Fix128> = cells.iter().map(|m| m.mu_r).collect();
let mu = harmonic_mean(&mus);
let active = mu != Fix128::ONE;
if active && self.is_absorbing(component, i, j, k) {
return Err(MaterialError::AbsorberOverlap {
component,
index: (i, j, k),
});
}
mc.h_active[axis].push(active);
mc.h_cb[axis].push(s / mu);
mc.h_mu[axis].push(mu);
}
}
}
}
}
self.materials = Some(Box::new(mc));
Ok(self)
}
#[must_use]
pub fn effective_material(
&self,
component: Component,
i: usize,
j: usize,
k: usize,
) -> Material {
let at = self.offset(component, i, j, k);
let Some(m) = self.materials.as_deref() else {
return Material::VACUUM;
};
let axis = component_axis(component);
match component {
Component::Ex | Component::Ey | Component::Ez => Material {
eps_r: m.e_eps[axis][at],
mu_r: Fix128::ONE,
sigma: m.e_sigma[axis][at],
},
Component::Hx | Component::Hy | Component::Hz => Material {
eps_r: Fix128::ONE,
mu_r: m.h_mu[axis][at],
sigma: Fix128::ZERO,
},
}
}
#[inline]
fn source_coefficient(&self, axis: usize, at: usize, s: Fix128) -> Fix128 {
match self.materials.as_deref() {
Some(m) if m.e_active[axis][at] => m.e_cb[axis][at],
_ => s,
}
}
#[inline]
fn d_at(&self, component: Component, i: usize, j: usize, k: usize) -> Fix128 {
let e = self.get(component, i, j, k);
match self.materials.as_deref() {
Some(m) => {
let axis = component_axis(component);
let at = self.offset(component, i, j, k);
if m.e_active[axis][at] {
m.e_eps[axis][at] * e
} else {
e
}
}
None => e,
}
}
#[must_use]
pub fn is_absorbing(&self, component: Component, i: usize, j: usize, k: usize) -> bool {
let _ = self.offset(component, i, j, k);
let Some(sp) = self.absorber.as_ref() else {
return false;
};
let (axis_a, ca, axis_b, cb) = match component {
Component::Ex => (1, j, 2, k),
Component::Ey => (2, k, 0, i),
Component::Ez => (0, i, 1, j),
Component::Hx => (1, j, 2, k),
Component::Hy => (2, k, 0, i),
Component::Hz => (0, i, 1, j),
};
match component {
Component::Ex | Component::Ey | Component::Ez => {
sp.lossy_e[axis_a][ca] || sp.lossy_e[axis_b][cb]
}
Component::Hx | Component::Hy | Component::Hz => {
sp.lossy_h[axis_a][ca] || sp.lossy_h[axis_b][cb]
}
}
}
#[must_use]
pub const fn dims(&self) -> (usize, usize, usize) {
(self.nx, self.ny, self.nz)
}
#[must_use]
pub const fn courant(&self) -> Fix128 {
self.courant
}
#[must_use]
pub const fn component_dims(&self, component: Component) -> (usize, usize, usize) {
match component {
Component::Ex => (self.nx, self.ny + 1, self.nz + 1),
Component::Ey => (self.nx + 1, self.ny, self.nz + 1),
Component::Ez => (self.nx + 1, self.ny + 1, self.nz),
Component::Hx => (self.nx + 1, self.ny, self.nz),
Component::Hy => (self.nx, self.ny + 1, self.nz),
Component::Hz => (self.nx, self.ny, self.nz + 1),
}
}
#[inline]
fn offset(&self, component: Component, i: usize, j: usize, k: usize) -> usize {
let (ni, nj, nk) = self.component_dims(component);
assert!(i < ni && j < nj && k < nk, "field index out of range");
(i * nj + j) * nk + k
}
#[inline]
const fn slot(&self, component: Component) -> &Vec<Fix128> {
match component {
Component::Ex => &self.ex,
Component::Ey => &self.ey,
Component::Ez => &self.ez,
Component::Hx => &self.hx,
Component::Hy => &self.hy,
Component::Hz => &self.hz,
}
}
#[inline]
fn slot_mut(&mut self, component: Component) -> &mut Vec<Fix128> {
match component {
Component::Ex => &mut self.ex,
Component::Ey => &mut self.ey,
Component::Ez => &mut self.ez,
Component::Hx => &mut self.hx,
Component::Hy => &mut self.hy,
Component::Hz => &mut self.hz,
}
}
#[must_use]
pub fn get(&self, component: Component, i: usize, j: usize, k: usize) -> Fix128 {
self.slot(component)[self.offset(component, i, j, k)]
}
pub fn set(&mut self, component: Component, i: usize, j: usize, k: usize, value: Fix128) {
let at = self.offset(component, i, j, k);
self.slot_mut(component)[at] = value;
if let Some(sp) = self.absorber.as_deref_mut() {
let (first, second) = sp.halves_mut(component);
first[at] = value;
second[at] = Fix128::ZERO;
}
}
#[must_use]
pub const fn interior_node_dims(&self) -> (usize, usize, usize) {
(
self.nx.saturating_sub(1),
self.ny.saturating_sub(1),
self.nz.saturating_sub(1),
)
}
#[inline]
fn node_offset(&self, i: usize, j: usize, k: usize) -> usize {
let (ni, nj, nk) = self.interior_node_dims();
assert!(
i >= 1 && j >= 1 && k >= 1 && i <= ni && j <= nj && k <= nk,
"node index is not an interior node"
);
((i - 1) * nj + (j - 1)) * nk + (k - 1)
}
#[inline]
const fn is_updated_edge(&self, component: Component, i: usize, j: usize, k: usize) -> bool {
match component {
Component::Ex => i < self.nx && j >= 1 && j < self.ny && k >= 1 && k < self.nz,
Component::Ey => i >= 1 && i < self.nx && j < self.ny && k >= 1 && k < self.nz,
Component::Ez => i >= 1 && i < self.nx && j >= 1 && j < self.ny && k < self.nz,
Component::Hx | Component::Hy | Component::Hz => false,
}
}
fn sources_mut(&mut self) -> &mut Sources {
let (ni, nj, nk) = self.interior_node_dims();
let (nx, ny, nz) = (self.nx, self.ny, self.nz);
self.sources.get_or_insert_with(|| {
Box::new(Sources {
jx: vec![Fix128::ZERO; nx * (ny + 1) * (nz + 1)],
jy: vec![Fix128::ZERO; (nx + 1) * ny * (nz + 1)],
jz: vec![Fix128::ZERO; (nx + 1) * (ny + 1) * nz],
rho: vec![Fix128::ZERO; ni * nj * nk],
})
})
}
#[must_use]
pub fn current(&self, component: Component, i: usize, j: usize, k: usize) -> Fix128 {
assert!(
matches!(component, Component::Ex | Component::Ey | Component::Ez),
"current density lives on electric edges, not {component:?}"
);
let at = self.offset(component, i, j, k);
self.sources
.as_ref()
.map_or(Fix128::ZERO, |src| match component {
Component::Ex => src.jx[at],
Component::Ey => src.jy[at],
_ => src.jz[at],
})
}
pub fn set_current(
&mut self,
component: Component,
i: usize,
j: usize,
k: usize,
value: Fix128,
) {
assert!(
matches!(component, Component::Ex | Component::Ey | Component::Ez),
"current density lives on electric edges, not {component:?}"
);
let at = self.offset(component, i, j, k);
assert!(
self.is_updated_edge(component, i, j, k),
"{component:?}[{i}][{j}][{k}] is tangential to a PEC wall, so a current there would be discarded"
);
assert!(
!self.is_absorbing(component, i, j, k),
"{component:?}[{i}][{j}][{k}] is a split sample: its value is rebuilt from the two half fields every step, so a current there would be discarded"
);
let src = self.sources_mut();
match component {
Component::Ex => src.jx[at] = value,
Component::Ey => src.jy[at] = value,
Component::Ez => src.jz[at] = value,
Component::Hx | Component::Hy | Component::Hz => unreachable!(),
}
}
#[must_use]
pub fn charge(&self, i: usize, j: usize, k: usize) -> Fix128 {
let at = self.node_offset(i, j, k);
self.sources
.as_ref()
.map_or(Fix128::ZERO, |src| src.rho[at])
}
pub fn set_charge(&mut self, i: usize, j: usize, k: usize, value: Fix128) {
let at = self.node_offset(i, j, k);
self.sources_mut().rho[at] = value;
}
#[must_use]
pub fn div_e(&self, i: usize, j: usize, k: usize) -> Fix128 {
let _ = self.node_offset(i, j, k);
(self.get(Component::Ex, i, j, k) - self.get(Component::Ex, i - 1, j, k))
+ (self.get(Component::Ey, i, j, k) - self.get(Component::Ey, i, j - 1, k))
+ (self.get(Component::Ez, i, j, k) - self.get(Component::Ez, i, j, k - 1))
}
#[must_use]
pub fn div_j(&self, i: usize, j: usize, k: usize) -> Fix128 {
let _ = self.node_offset(i, j, k);
let Some(src) = self.sources.as_ref() else {
return Fix128::ZERO;
};
let jx = |i: usize, j: usize, k: usize| src.jx[self.offset(Component::Ex, i, j, k)];
let jy = |i: usize, j: usize, k: usize| src.jy[self.offset(Component::Ey, i, j, k)];
let jz = |i: usize, j: usize, k: usize| src.jz[self.offset(Component::Ez, i, j, k)];
(jx(i, j, k) - jx(i - 1, j, k))
+ (jy(i, j, k) - jy(i, j - 1, k))
+ (jz(i, j, k) - jz(i, j, k - 1))
}
#[must_use]
pub fn div_d(&self, i: usize, j: usize, k: usize) -> Fix128 {
let _ = self.node_offset(i, j, k);
(self.d_at(Component::Ex, i, j, k) - self.d_at(Component::Ex, i - 1, j, k))
+ (self.d_at(Component::Ey, i, j, k) - self.d_at(Component::Ey, i, j - 1, k))
+ (self.d_at(Component::Ez, i, j, k) - self.d_at(Component::Ez, i, j, k - 1))
}
#[must_use]
pub fn gauss_residual(&self, i: usize, j: usize, k: usize) -> Fix128 {
self.div_d(i, j, k) - self.charge(i, j, k)
}
#[must_use]
pub fn max_abs_gauss_residual(&self) -> Fix128 {
let (ni, nj, nk) = self.interior_node_dims();
let mut worst = Fix128::ZERO;
for i in 1..=ni {
for j in 1..=nj {
for k in 1..=nk {
let r = self.gauss_residual(i, j, k).abs();
if r > worst {
worst = r;
}
}
}
}
worst
}
#[must_use]
pub fn total_charge(&self) -> Fix128 {
self.sources.as_ref().map_or(Fix128::ZERO, |src| {
src.rho.iter().fold(Fix128::ZERO, |acc, &r| acc + r)
})
}
pub fn step(&mut self) {
self.step_fields();
if self.sources.is_some() {
self.apply_sources();
}
}
fn apply_sources(&mut self) {
let s = self.courant;
let (nx, ny, nz) = (self.nx, self.ny, self.nz);
let (ni, nj, nk) = self.interior_node_dims();
for i in 1..=ni {
for j in 1..=nj {
for k in 1..=nk {
let d = self.div_j(i, j, k);
let at = self.node_offset(i, j, k);
let src = self.sources_mut();
src.rho[at] = src.rho[at] - s * d;
}
}
}
for i in 0..nx {
for j in 1..ny {
for k in 1..nz {
let at = self.offset(Component::Ex, i, j, k);
let j_here = self.sources.as_ref().map_or(Fix128::ZERO, |v| v.jx[at]);
let coef = self.source_coefficient(0, at, s);
self.ex[at] = self.ex[at] - coef * j_here;
}
}
}
for i in 1..nx {
for j in 0..ny {
for k in 1..nz {
let at = self.offset(Component::Ey, i, j, k);
let j_here = self.sources.as_ref().map_or(Fix128::ZERO, |v| v.jy[at]);
let coef = self.source_coefficient(1, at, s);
self.ey[at] = self.ey[at] - coef * j_here;
}
}
}
for i in 1..nx {
for j in 1..ny {
for k in 0..nz {
let at = self.offset(Component::Ez, i, j, k);
let j_here = self.sources.as_ref().map_or(Fix128::ZERO, |v| v.jz[at]);
let coef = self.source_coefficient(2, at, s);
self.ez[at] = self.ez[at] - coef * j_here;
}
}
}
}
fn step_fields(&mut self) {
let Self {
nx,
ny,
nz,
courant,
ex,
ey,
ez,
hx,
hy,
hz,
sources: _,
absorber,
materials,
} = self;
let (nx, ny, nz) = (*nx, *ny, *nz);
let s = *courant;
let mut ab = absorber.as_deref_mut();
let mat = materials.as_deref();
let (ex_i, ex_j) = ((ny + 1) * (nz + 1), nz + 1);
let (ey_i, ey_j) = (ny * (nz + 1), nz + 1);
let (ez_i, ez_j) = ((ny + 1) * nz, nz);
let (hx_i, hx_j) = (ny * nz, nz);
let (hy_i, hy_j) = ((ny + 1) * nz, nz);
let (hz_i, hz_j) = (ny * (nz + 1), nz + 1);
for i in 0..=nx {
for j in 0..ny {
for k in 0..nz {
let a = ez[i * ez_i + (j + 1) * ez_j + k] - ez[i * ez_i + j * ez_j + k];
let b = ey[i * ey_i + j * ey_j + (k + 1)] - ey[i * ey_i + j * ey_j + k];
let curl = a - b;
let at = i * hx_i + j * hx_j + k;
match ab.as_deref_mut() {
Some(sp) if sp.lossy_h[1][j] || sp.lossy_h[2][k] => {
sp.hxy[at] = sp.ca_h[1][j] * sp.hxy[at] - sp.cb_h[1][j] * a;
sp.hxz[at] = sp.ca_h[2][k] * sp.hxz[at] + sp.cb_h[2][k] * b;
hx[at] = sp.hxy[at] + sp.hxz[at];
}
_ => match mat {
Some(m) if m.h_active[0][at] => {
hx[at] = hx[at] - m.h_cb[0][at] * curl;
}
_ => hx[at] = hx[at] - s * curl,
},
}
}
}
}
for i in 0..nx {
for j in 0..=ny {
for k in 0..nz {
let a = ex[i * ex_i + j * ex_j + (k + 1)] - ex[i * ex_i + j * ex_j + k];
let b = ez[(i + 1) * ez_i + j * ez_j + k] - ez[i * ez_i + j * ez_j + k];
let curl = a - b;
let at = i * hy_i + j * hy_j + k;
match ab.as_deref_mut() {
Some(sp) if sp.lossy_h[2][k] || sp.lossy_h[0][i] => {
sp.hyz[at] = sp.ca_h[2][k] * sp.hyz[at] - sp.cb_h[2][k] * a;
sp.hyx[at] = sp.ca_h[0][i] * sp.hyx[at] + sp.cb_h[0][i] * b;
hy[at] = sp.hyz[at] + sp.hyx[at];
}
_ => match mat {
Some(m) if m.h_active[1][at] => {
hy[at] = hy[at] - m.h_cb[1][at] * curl;
}
_ => hy[at] = hy[at] - s * curl,
},
}
}
}
}
for i in 0..nx {
for j in 0..ny {
for k in 0..=nz {
let a = ey[(i + 1) * ey_i + j * ey_j + k] - ey[i * ey_i + j * ey_j + k];
let b = ex[i * ex_i + (j + 1) * ex_j + k] - ex[i * ex_i + j * ex_j + k];
let curl = a - b;
let at = i * hz_i + j * hz_j + k;
match ab.as_deref_mut() {
Some(sp) if sp.lossy_h[0][i] || sp.lossy_h[1][j] => {
sp.hzx[at] = sp.ca_h[0][i] * sp.hzx[at] - sp.cb_h[0][i] * a;
sp.hzy[at] = sp.ca_h[1][j] * sp.hzy[at] + sp.cb_h[1][j] * b;
hz[at] = sp.hzx[at] + sp.hzy[at];
}
_ => match mat {
Some(m) if m.h_active[2][at] => {
hz[at] = hz[at] - m.h_cb[2][at] * curl;
}
_ => hz[at] = hz[at] - s * curl,
},
}
}
}
}
for i in 0..nx {
for j in 1..ny {
for k in 1..nz {
let a = hz[i * hz_i + j * hz_j + k] - hz[i * hz_i + (j - 1) * hz_j + k];
let b = hy[i * hy_i + j * hy_j + k] - hy[i * hy_i + j * hy_j + (k - 1)];
let curl = a - b;
let at = i * ex_i + j * ex_j + k;
match ab.as_deref_mut() {
Some(sp) if sp.lossy_e[1][j] || sp.lossy_e[2][k] => {
sp.exy[at] = sp.ca_e[1][j] * sp.exy[at] + sp.cb_e[1][j] * a;
sp.exz[at] = sp.ca_e[2][k] * sp.exz[at] - sp.cb_e[2][k] * b;
ex[at] = sp.exy[at] + sp.exz[at];
}
_ => match mat {
Some(m) if m.e_active[0][at] => {
ex[at] = m.e_ca[0][at] * ex[at] + m.e_cb[0][at] * curl;
}
_ => ex[at] = ex[at] + s * curl,
},
}
}
}
}
for i in 1..nx {
for j in 0..ny {
for k in 1..nz {
let a = hx[i * hx_i + j * hx_j + k] - hx[i * hx_i + j * hx_j + (k - 1)];
let b = hz[i * hz_i + j * hz_j + k] - hz[(i - 1) * hz_i + j * hz_j + k];
let curl = a - b;
let at = i * ey_i + j * ey_j + k;
match ab.as_deref_mut() {
Some(sp) if sp.lossy_e[2][k] || sp.lossy_e[0][i] => {
sp.eyz[at] = sp.ca_e[2][k] * sp.eyz[at] + sp.cb_e[2][k] * a;
sp.eyx[at] = sp.ca_e[0][i] * sp.eyx[at] - sp.cb_e[0][i] * b;
ey[at] = sp.eyz[at] + sp.eyx[at];
}
_ => match mat {
Some(m) if m.e_active[1][at] => {
ey[at] = m.e_ca[1][at] * ey[at] + m.e_cb[1][at] * curl;
}
_ => ey[at] = ey[at] + s * curl,
},
}
}
}
}
for i in 1..nx {
for j in 1..ny {
for k in 0..nz {
let a = hy[i * hy_i + j * hy_j + k] - hy[(i - 1) * hy_i + j * hy_j + k];
let b = hx[i * hx_i + j * hx_j + k] - hx[i * hx_i + (j - 1) * hx_j + k];
let curl = a - b;
let at = i * ez_i + j * ez_j + k;
match ab.as_deref_mut() {
Some(sp) if sp.lossy_e[0][i] || sp.lossy_e[1][j] => {
sp.ezx[at] = sp.ca_e[0][i] * sp.ezx[at] + sp.cb_e[0][i] * a;
sp.ezy[at] = sp.ca_e[1][j] * sp.ezy[at] - sp.cb_e[1][j] * b;
ez[at] = sp.ezx[at] + sp.ezy[at];
}
_ => match mat {
Some(m) if m.e_active[2][at] => {
ez[at] = m.e_ca[2][at] * ez[at] + m.e_cb[2][at] * curl;
}
_ => ez[at] = ez[at] + s * curl,
},
}
}
}
}
}
#[must_use]
pub fn div_b(&self, i: usize, j: usize, k: usize) -> Fix128 {
assert!(
i < self.nx && j < self.ny && k < self.nz,
"cell index out of range"
);
(self.get(Component::Hx, i + 1, j, k) - self.get(Component::Hx, i, j, k))
+ (self.get(Component::Hy, i, j + 1, k) - self.get(Component::Hy, i, j, k))
+ (self.get(Component::Hz, i, j, k + 1) - self.get(Component::Hz, i, j, k))
}
#[must_use]
pub fn max_abs_div_b(&self) -> Fix128 {
let mut worst = Fix128::ZERO;
for i in 0..self.nx {
for j in 0..self.ny {
for k in 0..self.nz {
let d = self.div_b(i, j, k).abs();
if d > worst {
worst = d;
}
}
}
}
worst
}
#[must_use]
pub fn max_abs_field(&self) -> Fix128 {
[&self.ex, &self.ey, &self.ez, &self.hx, &self.hy, &self.hz]
.iter()
.flat_map(|v| v.iter())
.fold(Fix128::ZERO, |worst, &v| {
let a = v.abs();
if a > worst {
a
} else {
worst
}
})
}
}
#[must_use]
pub fn cfl_limit_3d() -> Fix128 {
Fix128::ONE / Fix128::from_int(3).sqrt()
}