- this will allow it to be used from within the spirv code. - had to change some coremem code which was previously peering into privates or now-unrestricted constraints. - may need to put the serde stuff behind a feature flag (or force nostd?)
2074 lines
75 KiB
Rust
2074 lines
75 KiB
Rust
use crate::geom::{Coord, Cube, Index, InvertedRegion, Meters, Region, Vec3, Vec3u};
|
|
use crate::mat::{self, GenericMaterial, Material, MaterialExt as _};
|
|
use crate::real::{R32, Real};
|
|
use crate::stim::{AbstractStimulus, NoopStimulus};
|
|
use log::trace;
|
|
use ndarray::{Array3, Zip};
|
|
use rayon::prelude::*;
|
|
use serde::{Serialize, Deserialize};
|
|
use std::convert::From;
|
|
use std::iter::Sum;
|
|
|
|
pub mod spirv;
|
|
pub mod units;
|
|
|
|
pub type StaticSim = SimState<R32, mat::Static<R32>>;
|
|
|
|
#[derive(Default, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct PmlState<R> {
|
|
/// Auxiliary fields used when stepping E
|
|
aux_e: PmlAux<R>,
|
|
/// Auxiliary fields used when stepping H
|
|
aux_h: PmlAux<R>,
|
|
}
|
|
|
|
impl<R: Default> PmlState<R> {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
|
struct PmlAux<R> {
|
|
/// ```tex
|
|
/// $Sy^-1 \ast dFz/dy$, where F is either E or H.
|
|
/// This is used to advance the X coordinate of the opposite field
|
|
/// ```
|
|
sy_conv_dfz_dy: ConvFlt<R>,
|
|
sz_conv_dfy_dz: ConvFlt<R>,
|
|
/// Used to advance the Y coordinate of the opposite field
|
|
sz_conv_dfx_dz: ConvFlt<R>,
|
|
sx_conv_dfz_dx: ConvFlt<R>,
|
|
/// Used to advance the Z coordinate of the opposite field
|
|
sx_conv_dfy_dx: ConvFlt<R>,
|
|
sy_conv_dfx_dy: ConvFlt<R>,
|
|
}
|
|
|
|
#[derive(Default, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
|
struct ConvFlt<R>(R);
|
|
|
|
impl<R: Real> ConvFlt<R> {
|
|
///```tex
|
|
/// Consider $v(t) = (a \delta(t) + b exp[-c t] u(t)) \ast y(t)$
|
|
/// This method will advance from self = $v(t)$ to self = $v(t + \Delta t)$
|
|
/// ```
|
|
#[allow(unused)]
|
|
fn step(&mut self, a: R, b: R, c: R, y: R, delta_t: R) -> R {
|
|
assert!(c > R::zero(), "ConvFlt::step called without decay: {}", c.to_f64());
|
|
//```tex
|
|
// let $v(t) = a \delta(t) \ast y(t) + m(t)$
|
|
// where $m(t) = b exp[-c t] u(t) \ast y(t)$
|
|
// let $k(t) = b exp[-c t] u(t)$, where "k" is for "Kernel".
|
|
// $m(t) = \int_0^{t} k(T) y(t - T)dT$
|
|
// $m(t) = \int_0^{\Delta t} k(T) y(t - T)dT + \int_{\Delta t}^t k(T) y(t - T)dT$
|
|
// $m(t) = \int_0^{\Delta t} k(T) y(t - T)dT + \int_{\Delta t}^t b exp[-c T] y(t - T)dT$
|
|
// $m(t) = \int_0^{\Delta t} k(T) y(t - T)dT + \int_0^{t-\Delta t} b exp[-c T] exp[-c \Delta t] y(t - T)dT$
|
|
// $m(t) = \int_0^{\Delta t} k(T) y(t - T)dT + exp[-c \Delta t] v_prev$
|
|
//
|
|
// Assume that $y(t)$ is constant for this time-step.
|
|
// Looking just at the integral term:
|
|
// $b y \int_0^{\Delta t} exp[-c T] dT$
|
|
// $= b y [-1/c](exp[-c \Delta t] - 1)$
|
|
// So
|
|
// $m(t) = exp[-c \Delta t] v_prev + -b y/c (exp[-c \Delta t] - 1)$
|
|
//```
|
|
// trace!("pre: {}", self.0);
|
|
let e = (-c * delta_t).exp();
|
|
// assert!(self.0 < 1e50 && self.0 > -1e50, "self={}, a={} b={} c={} y={} dt={} e={}", self.0, a, b, c, y, delta_t, e);
|
|
self.0 = self.0*e - b*y/c * (e - R::one());
|
|
// trace!("post: {}", self.0);
|
|
self.0 + a*y
|
|
}
|
|
|
|
///```tex
|
|
/// Consider $v(t) = (\delta(t) - \sigma exp[-\sigma t] u(t)) \ast y(t)$
|
|
/// This method will advance from self = $v(t)$ to self = $v(t + \Delta t)$
|
|
///
|
|
/// The user should call this with $e = exp[-\sigma \Delta t]$.
|
|
/// This is a more computationally-efficient version of `step` for when $a=1$ and
|
|
/// $b = -c$ (i.e. `dc_shift` = 0).
|
|
/// ```
|
|
fn step_only_sigma(&mut self, e: R, y: R) -> R {
|
|
//```tex
|
|
// From the `step` method above,
|
|
// $v(t) = a \delta(t) \ast y(t) + m(t)$
|
|
// $m(t) = exp[-c \Delta t] v_prev + -b y/c (exp[-c \Delta t] - 1)$
|
|
// where $a = 1$, $c=\sigma$, $b=-\sigma$.
|
|
// So:
|
|
// $m(t) = e v_prev + \sigma/\sigma (e - 1) y$
|
|
// and $v(t) = m(t) + y(t)$
|
|
// Simplify this:
|
|
// $v(t) = e (v_prev + y)$
|
|
// $m(t) = v(t) - y$
|
|
//```
|
|
let r = e*(self.0 + y);
|
|
self.0 = r - y;
|
|
r
|
|
}
|
|
}
|
|
|
|
|
|
/// Used to instruct the Sim how to advance the state at a given location.
|
|
#[derive(Default, PartialEq)]
|
|
pub struct StepParametersMut<'a, R> {
|
|
conductivity: Vec3<R>,
|
|
pml: Option<(&'a mut PmlState<R>, PmlParameters<R>)>,
|
|
}
|
|
|
|
impl<'a, R: Real> StepParametersMut<'a, R> {
|
|
pub fn new<R2: Real>(
|
|
conductivity: Vec3<R2>, pml: Option<(&'a mut PmlState<R>, PmlParameters<R>)>
|
|
) -> Self {
|
|
Self {
|
|
conductivity: conductivity.cast(),
|
|
pml,
|
|
}
|
|
}
|
|
pub fn with_conductivity<R2: Real>(mut self, conductivity: Vec3<R2>) -> Self {
|
|
self.conductivity = conductivity.cast();
|
|
self
|
|
}
|
|
pub fn with_pml(mut self, state: &'a mut PmlState<R>, params: PmlParameters<R>) -> Self {
|
|
self.pml = Some((state, params));
|
|
self
|
|
}
|
|
}
|
|
|
|
// /// Like `PmlParameters`, but with some additional things parameterized.
|
|
// #[derive(Copy, Clone, Default, PartialEq, Serialize, Deserialize)]
|
|
// pub struct CpmlParameters {
|
|
// /// K; this is similar to \epsilon_r. Try setting this to 1.0 at the interface and
|
|
// /// increasing it towards a larger constant at the simulation boundary.
|
|
// propagation_constant: Vec3<Real>,
|
|
// /// sigma; this is the main parameter that should be varied by location within
|
|
// /// the sim space
|
|
// pseudo_conductivity: Vec3<Real>,
|
|
// /// a; this combats artifacting from DC waves. Try setting this to a small constant
|
|
// /// at the interface and decreasing it to zero at the sim boundary.
|
|
// dc_shift: Vec3<Real>,
|
|
// }
|
|
//
|
|
// impl CpmlParameters {
|
|
// pub fn new(pseudo_conductivity: Vec3<Real>, propagation_constant: Vec3<Real>, dc_shift: Vec3<Real>) -> Self {
|
|
// Self {
|
|
// pseudo_conductivity, propagation_constant, dc_shift
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
/// PML applies a modified formulation of Maxwell's equations which are reflectionless
|
|
/// for normally incident waves. This allows the material to behave as a "Perfectly Matched
|
|
/// Layer" (PML), dissipating energy AS IF it was a conductor with the provided
|
|
/// `pseudo_conductivity`.
|
|
#[derive(Copy, Clone, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct PmlParameters<R> {
|
|
/// sigma; this is the main parameter that should be varied by location within
|
|
/// the sim space. Initialize it to zero at the interface and increase it
|
|
/// to a large number at the simulation boundary.
|
|
pseudo_conductivity: Vec3<R>,
|
|
}
|
|
|
|
impl<R: Real> PmlParameters<R> {
|
|
pub fn new<R2: Real>(pseudo_conductivity: Vec3<R2>) -> Self {
|
|
Self {
|
|
pseudo_conductivity: pseudo_conductivity.cast(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// See `StepParametersMut`
|
|
#[derive(PartialEq)]
|
|
pub struct StepParameters<'a, R> {
|
|
conductivity: Vec3<R>,
|
|
pml: Option<(&'a PmlState<R>, PmlParameters<R>)>,
|
|
}
|
|
|
|
impl<'a, R: Real> StepParameters<'a, R> {
|
|
pub fn conductivity(&self) -> Vec3<R> {
|
|
self.conductivity
|
|
}
|
|
pub fn pml(&self) -> Option<(&'a PmlState<R>, PmlParameters<R>)> {
|
|
self.pml
|
|
}
|
|
}
|
|
|
|
impl<'a, R> From<StepParametersMut<'a, R>> for StepParameters<'a, R> {
|
|
fn from(p: StepParametersMut<'a, R>) -> Self {
|
|
Self {
|
|
conductivity: p.conductivity,
|
|
pml: p.pml.map(|(s, p)| (&*s, p)),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait MaterialSim: GenericSim {
|
|
type Material: PartialEq;
|
|
|
|
fn put_material<C: Coord, M: Into<Self::Material>>(&mut self, pos: C, mat: M);
|
|
// XXX: would ideally return by-ref, but some backends need to return a handle instead
|
|
fn get_material<C: Coord>(&self, pos: C) -> Self::Material;
|
|
|
|
fn fill_region_using<C, Reg, F, M>(&mut self, region: &Reg, f: F)
|
|
where
|
|
Reg: Region,
|
|
F: Fn(C) -> M,
|
|
C: Coord,
|
|
M: Into<Self::Material>
|
|
{
|
|
for z in 0..self.depth() {
|
|
for y in 0..self.height() {
|
|
for x in 0..self.width() {
|
|
let loc = Index((x, y, z).into());
|
|
let meters = loc.to_meters(self.feature_size());
|
|
if region.contains(meters) {
|
|
self.put_material(loc, f(C::from_either(loc, meters)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn fill_region<Reg: Region, M: Into<Self::Material> + Clone>(&mut self, region: &Reg, mat: M) {
|
|
self.fill_region_using(region, |_idx: Index| mat.clone());
|
|
}
|
|
|
|
fn examine_region<C, Reg, F>(&self, region: &Reg, mut f: F)
|
|
where
|
|
Reg: Region,
|
|
F: FnMut(C, &Self::Material),
|
|
C: Coord
|
|
{
|
|
for z in 0..self.depth() {
|
|
for y in 0..self.height() {
|
|
for x in 0..self.width() {
|
|
let loc = Index((x, y, z).into());
|
|
let meters = loc.to_meters(self.feature_size());
|
|
if region.contains(meters) {
|
|
f(C::from_either(loc, meters), &self.get_material(loc));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Return true if the given region is filled exclusively with the provided material.
|
|
fn test_region_filled<Reg: Region, M: Into<Self::Material> + Clone>(&self, region: &Reg, mat: M) -> bool {
|
|
let mut all = true;
|
|
self.examine_region(region, |_idx: Index, m: &Self::Material| {
|
|
all = all && m == &mat.clone().into();
|
|
});
|
|
all
|
|
}
|
|
|
|
/// Fill the boundary, where `thickness` describes how far the boundary extends in each
|
|
/// direction, and `f` takes a vec where each coordinate represents how far into the boundary
|
|
/// the location being queried is, in each direction.
|
|
/// e.g. `f((1.0, 0.0, 0.2))` means the location being queried is at either extreme end on the
|
|
/// x axis, is not inside the y axis boundary, and is 20% of the way from the onset of the z
|
|
/// boundary to the edge of the z world.
|
|
fn fill_boundary_using<C, F, M>(&mut self, thickness: C, f: F)
|
|
where
|
|
C: Coord,
|
|
F: Fn(Vec3<f32>) -> M,
|
|
M: Into<Self::Material>,
|
|
{
|
|
// TODO: maybe this function belongs on the Driver?
|
|
let feat = self.feature_size();
|
|
let upper_left = thickness.to_index(feat);
|
|
let size = self.size();
|
|
let lower_right = size - upper_left - Index::new(1, 1, 1);
|
|
let region = InvertedRegion::new(Cube::new(upper_left.to_meters(feat), lower_right.to_meters(feat)));
|
|
self.fill_region_using(®ion, |loc: Index| {
|
|
let depth_x = if loc.x() < upper_left.x() {
|
|
(upper_left.x() - loc.x()) as f32 / upper_left.x() as f32
|
|
} else if loc.x() > lower_right.x() {
|
|
(loc.x() - lower_right.x()) as f32 / upper_left.x() as f32
|
|
} else {
|
|
0.0
|
|
};
|
|
let depth_y = if loc.y() < upper_left.y() {
|
|
(upper_left.y() - loc.y()) as f32 / upper_left.y() as f32
|
|
} else if loc.y() > lower_right.y() {
|
|
(loc.y() - lower_right.y()) as f32 / upper_left.y() as f32
|
|
} else {
|
|
0.0
|
|
};
|
|
let depth_z = if loc.z() < upper_left.z() {
|
|
(upper_left.z() - loc.z()) as f32 / upper_left.z() as f32
|
|
} else if loc.z() > lower_right.z() {
|
|
(loc.z() - lower_right.z()) as f32 / upper_left.z() as f32
|
|
} else {
|
|
0.0
|
|
};
|
|
// println!("{} {}", loc, Vec3::new(depth_x, depth_y, depth_z));
|
|
f(Vec3::new(depth_x, depth_y, depth_z))
|
|
});
|
|
}
|
|
}
|
|
|
|
// XXX the Send/Sync bounds here could be removed with some refactoring
|
|
pub trait GenericSim: SampleableSim {
|
|
fn step(&mut self) {
|
|
// XXX: try not to exercise this path! NoopStimulus is probably a lot of waste.
|
|
self.step_multiple(1, &NoopStimulus);
|
|
}
|
|
fn step_multiple<S: AbstractStimulus>(&mut self, num_steps: u32, s: &S);
|
|
|
|
/// DEPRECATED. Use stimulus instead
|
|
fn impulse_e_meters(&mut self, pos: Meters, amount: Vec3<f32>);
|
|
/// DEPRECATED. Use stimulus instead
|
|
fn impulse_h_meters(&mut self, pos: Meters, amount: Vec3<f32>) {
|
|
self.impulse_b_meters(pos, amount * f32::mu0());
|
|
}
|
|
/// DEPRECATED. Use stimulus instead
|
|
fn impulse_b_meters(&mut self, pos: Meters, amount: Vec3<f32>) {
|
|
self.impulse_h_meters(pos, amount * f32::mu0_inv());
|
|
}
|
|
|
|
fn impulse_e<C: Coord>(&mut self, pos: C, amt: Vec3<f32>) {
|
|
self.impulse_e_meters(pos.to_meters(self.feature_size()), amt)
|
|
}
|
|
|
|
fn impulse_h<C: Coord>(&mut self, pos: C, amt: Vec3<f32>) {
|
|
self.impulse_h_meters(pos.to_meters(self.feature_size()), amt)
|
|
}
|
|
|
|
fn impulse_b<C: Coord>(&mut self, pos: C, amt: Vec3<f32>) {
|
|
self.impulse_b_meters(pos.to_meters(self.feature_size()), amt)
|
|
}
|
|
|
|
fn impulse_ex<C: Coord>(&mut self, c: C, ex: f32) {
|
|
self.impulse_e(c, Vec3::new_x(ex));
|
|
}
|
|
fn impulse_ey<C: Coord>(&mut self, c: C, ey: f32) {
|
|
self.impulse_e(c, Vec3::new_y(ey));
|
|
}
|
|
fn impulse_ez<C: Coord>(&mut self, c: C, ez: f32) {
|
|
self.impulse_e(c, Vec3::new_z(ez));
|
|
}
|
|
|
|
fn impulse_hx<C: Coord>(&mut self, c: C, hx: f32) {
|
|
self.impulse_h(c, Vec3::new_x(hx));
|
|
}
|
|
fn impulse_hy<C: Coord>(&mut self, c: C, hy: f32) {
|
|
self.impulse_h(c, Vec3::new_y(hy));
|
|
}
|
|
fn impulse_hz<C: Coord>(&mut self, c: C, hz: f32) {
|
|
self.impulse_h(c, Vec3::new_z(hz));
|
|
}
|
|
|
|
fn impulse_bx<C: Coord>(&mut self, c: C, bx: f32) {
|
|
self.impulse_b(c, Vec3::new_x(bx));
|
|
}
|
|
fn impulse_by<C: Coord>(&mut self, c: C, by: f32) {
|
|
self.impulse_b(c, Vec3::new_y(by));
|
|
}
|
|
fn impulse_bz<C: Coord>(&mut self, c: C, bz: f32) {
|
|
self.impulse_b(c, Vec3::new_z(bz));
|
|
}
|
|
}
|
|
|
|
impl<'a> dyn SampleableSim + 'a {
|
|
pub fn get<C: Coord>(&self, at: C) -> Sample {
|
|
self.sample(at.to_meters(self.feature_size()))
|
|
}
|
|
|
|
/// Apply `F` to each Cell, and sum the results.
|
|
pub fn map_sum<F, Ret>(&self, f: F) -> Ret
|
|
where
|
|
F: Fn(&Sample) -> Ret + Sync,
|
|
Ret: Sum<Ret> + Send,
|
|
{
|
|
self.map_sum_enumerated(|_at: Index, cell| f(cell))
|
|
}
|
|
|
|
pub fn map_sum_enumerated<C, F, Ret>(&self, f: F) -> Ret
|
|
where C: Coord,
|
|
F: Fn(C, &Sample) -> Ret + Sync,
|
|
Ret: Sum<Ret> + Send,
|
|
{
|
|
let (w, h, d) = (self.width(), self.height(), self.depth());
|
|
(0..d).into_par_iter().map(
|
|
|z| (0..h).into_par_iter().map_with(z,
|
|
|&mut z, y| (0..w).into_par_iter().map_with((z, y),
|
|
|&mut (z, y), x|
|
|
{
|
|
let at = Index(Vec3u::new(x, y, z));
|
|
f(C::from_index(at, self.feature_size()), &self.get(at))
|
|
}))).flatten().flatten().sum()
|
|
}
|
|
|
|
pub fn volume_of_region<R: Region + ?Sized>(&self, region: &R) -> u32 {
|
|
self.map_sum_over(region, |_| 1)
|
|
}
|
|
|
|
/// Apply `F` to each Cell, and sum the results.
|
|
pub fn map_sum_over<F, Ret, Reg>(&self, region: &Reg, f: F) -> Ret
|
|
where
|
|
F: Fn(&Sample) -> Ret + Sync,
|
|
Ret: Sum<Ret> + Default + Send,
|
|
Reg: Region + ?Sized
|
|
{
|
|
self.map_sum_over_enumerated(region, |_at: Index, cell| f(cell))
|
|
}
|
|
|
|
pub fn map_sum_over_enumerated<C, F, Ret, Reg>(&self, region: &Reg, f: F) -> Ret
|
|
where C: Coord,
|
|
F: Fn(C, &Sample) -> Ret + Sync,
|
|
Ret: Sum<Ret> + Default + Send,
|
|
Reg: Region + ?Sized,
|
|
{
|
|
let (w, h, d) = (self.width(), self.height(), self.depth());
|
|
(0..d).into_par_iter().map(
|
|
|z| (0..h).into_par_iter().map_with(z,
|
|
|&mut z, y| (0..w).into_par_iter().map_with((z, y),
|
|
|&mut (z, y), x|
|
|
{
|
|
let at = Index(Vec3u::new(x, y, z));
|
|
let meters = at.to_meters(self.feature_size());
|
|
if region.contains(meters) {
|
|
f(C::from_index(at, self.feature_size()), &self.get(at))
|
|
} else {
|
|
Default::default()
|
|
}
|
|
}))).flatten().flatten().sum()
|
|
}
|
|
|
|
pub fn current<C: Coord>(&self, c: C) -> Vec3<f32> {
|
|
self.get(c).current_density() * self.feature_size() * self.feature_size()
|
|
}
|
|
}
|
|
|
|
// XXX the Send/Sync bounds here could be removed with some refactoring
|
|
pub trait SampleableSim: Send + Sync {
|
|
fn sample(&self, pos: Meters) -> Sample;
|
|
|
|
fn size(&self) -> Index;
|
|
fn feature_size(&self) -> f32;
|
|
fn feature_volume(&self) -> f32 {
|
|
let f = self.feature_size();
|
|
f*f*f
|
|
}
|
|
fn volume(&self) -> f32 {
|
|
let s = self.size().to_meters(self.feature_size());
|
|
s.x() * s.y() * s.z()
|
|
}
|
|
fn timestep(&self) -> f32;
|
|
|
|
fn step_no(&self) -> u64;
|
|
|
|
fn width(&self) -> u32 {
|
|
self.size().x()
|
|
}
|
|
fn height(&self) -> u32 {
|
|
self.size().y()
|
|
}
|
|
fn depth(&self) -> u32 {
|
|
self.size().z()
|
|
}
|
|
fn time(&self) -> f32 {
|
|
self.timestep() * self.step_no() as f32
|
|
}
|
|
|
|
/// Take a "snapshot" of the simulation, dropping all material-specific information.
|
|
fn to_static(&self) -> StaticSim {
|
|
let mut state = SimState::new(self.size(), self.feature_size());
|
|
state.step_no = self.step_no();
|
|
Zip::from(ndarray::indices_of(&state.cells)).and(&mut state.e).and(&mut state.h).par_map_assign_into(
|
|
&mut state.cells,
|
|
|(z, y, x), e, h| {
|
|
let idx = Index((x as u32, y as u32, z as u32).into());
|
|
let cell = self.sample(idx.to_meters(self.feature_size()));
|
|
*e = cell.e().cast();
|
|
*h = cell.h().cast();
|
|
mat::Static {
|
|
conductivity: cell.conductivity().cast(),
|
|
m: cell.m().cast(),
|
|
}
|
|
});
|
|
state
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Clone, Serialize, Deserialize)]
|
|
pub struct SimState<R=R32, M=GenericMaterial<R>> {
|
|
cells: Array3<M>,
|
|
e: Array3<Vec3<R>>,
|
|
h: Array3<Vec3<R>>,
|
|
feature_size: R,
|
|
step_no: u64,
|
|
timestep: R,
|
|
}
|
|
|
|
impl<R, M> SimState<R, M> {
|
|
pub fn size(&self) -> Index {
|
|
Index(Vec3u::new(
|
|
self.cells.shape()[2] as _,
|
|
self.cells.shape()[1] as _,
|
|
self.cells.shape()[0] as _,
|
|
))
|
|
}
|
|
pub fn width(&self) -> u32 {
|
|
self.size().x()
|
|
}
|
|
pub fn height(&self) -> u32 {
|
|
self.size().y()
|
|
}
|
|
pub fn depth(&self) -> u32 {
|
|
self.size().z()
|
|
}
|
|
}
|
|
|
|
impl<R: Real, M> SimState<R, M> {
|
|
pub fn feature_size(&self) -> f32 {
|
|
self.feature_size.to_f32()
|
|
}
|
|
pub fn inv_feature_size(&self) -> f32 {
|
|
(R::one() / self.feature_size).to_f32()
|
|
}
|
|
pub fn timestep(&self) -> f32 {
|
|
self.timestep.to_f32()
|
|
}
|
|
}
|
|
|
|
impl<R: Real, M: Default> SimState<R, M> {
|
|
pub fn new(size: Index, feature_size: f32) -> Self {
|
|
// XXX this has to be multiplied by a Courant Number in order to ensure stability.
|
|
// For 3d, we need 3 full timesteps in order to communicate information across the corner
|
|
// of a grid. The distance traveled during that time is \sqrt{3}. Hence each timestep needs
|
|
// to be \sqrt{3}/3, or 0.577.
|
|
// In 2d, this would be \sqrt{2}/2.
|
|
// It's an upper limit though; it should be safe to go lower.
|
|
let courant = 0.577;
|
|
let feature_size = R::from_primitive(feature_size);
|
|
let timestep = feature_size / R::c() * R::from_primitive(courant);
|
|
Self {
|
|
cells: Array3::default((size.z() as _, size.y() as _, size.x() as _)),
|
|
e: Array3::default((size.z() as _, size.y() as _, size.x() as _)),
|
|
h: Array3::default((size.z() as _, size.y() as _, size.x() as _)),
|
|
feature_size,
|
|
timestep,
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<R: Real, M: Material<R> + Send + Sync> SimState<R, M> {
|
|
pub fn step(&mut self) {
|
|
let time_step = self.timestep;
|
|
let half_time_step = R::half() * time_step;
|
|
let feature_size = self.feature_size;
|
|
let inv_feature_size = R::one() / feature_size;
|
|
|
|
// advance all the electic fields
|
|
trace!("step_e begin");
|
|
let h_field = &self.h;
|
|
Zip::from(ndarray::indices_of(&self.cells)).and(&mut self.cells).and(&mut self.e).par_for_each(
|
|
|(z, y, x), mat, e| {
|
|
let neighbors = Neighbors::new(h_field, [z, y, x]);
|
|
let h = &h_field[[z, y, x]];
|
|
ECell { mat, e, h }.step(neighbors, half_time_step, inv_feature_size);
|
|
});
|
|
trace!("step_e end");
|
|
|
|
// advance all the magnetic fields
|
|
trace!("step_h begin");
|
|
let e_field = &self.e;
|
|
Zip::from(ndarray::indices_of(&self.cells)).and(&mut self.cells).and(&mut self.h).par_for_each(
|
|
|(z, y, x), mat, h| {
|
|
let neighbors = Neighbors::new(e_field, [z, y, x]);
|
|
let e = &e_field[[z, y, x]];
|
|
HCell { mat, h, e }.step(neighbors, half_time_step, inv_feature_size);
|
|
});
|
|
trace!("step_h end");
|
|
|
|
self.step_no += 1;
|
|
}
|
|
|
|
pub fn apply_stimulus<S: AbstractStimulus + ?Sized>(&mut self, stim: &S) {
|
|
let feature_size = self.feature_size();
|
|
|
|
let t_sec = self.time();
|
|
let timestep = self.timestep;
|
|
trace!("apply_stimulus begin");
|
|
Zip::from(ndarray::indices_of(&self.e)).and(&mut self.e).and(&mut self.h).par_for_each(
|
|
|(z, y, x), e, h| {
|
|
let pos_meters = Index((x as u32, y as u32, z as u32).into()).to_meters(feature_size);
|
|
let (density_e, density_h) = stim.at(t_sec, pos_meters);
|
|
let value_e = density_e.cast::<R>() * timestep.cast::<R>();
|
|
let value_h = density_h.cast::<R>() * timestep.cast::<R>();
|
|
*e += value_e;
|
|
*h += value_h;
|
|
});
|
|
trace!("apply_stimulus end");
|
|
}
|
|
}
|
|
|
|
impl<R: Real, M: Material<R> + Clone + Send + Sync + PartialEq> MaterialSim for SimState<R, M> {
|
|
type Material = M;
|
|
|
|
fn put_material<C: Coord, M2: Into<Self::Material>>(&mut self, pos: C, mat: M2) {
|
|
*self.get_mat_mut(pos) = mat.into();
|
|
}
|
|
|
|
fn get_material<C: Coord>(&self, pos: C) -> M {
|
|
self.get_material(pos).clone()
|
|
}
|
|
}
|
|
|
|
impl<R: Real, M: Material<R> + Send + Sync> SampleableSim for SimState<R, M> {
|
|
fn sample(&self, pos: Meters) -> Sample {
|
|
// TODO: smarter sampling than nearest neighbor?
|
|
let pos_sim = pos.to_index(self.feature_size());
|
|
let idx = [pos_sim.z() as usize, pos_sim.y() as _, pos_sim.x() as _];
|
|
match self.cells.get(idx) {
|
|
Some(mat) => Sample {
|
|
state: CellStateWithM {
|
|
e: self.e[idx].cast(),
|
|
h: self.h[idx].cast(),
|
|
m: mat.m().cast(),
|
|
},
|
|
conductivity: mat.conductivity().cast(),
|
|
},
|
|
None => Default::default(),
|
|
}
|
|
}
|
|
|
|
fn size(&self) -> Index {
|
|
Index(Vec3u::new(
|
|
self.cells.shape()[2] as _,
|
|
self.cells.shape()[1] as _,
|
|
self.cells.shape()[0] as _,
|
|
))
|
|
}
|
|
fn feature_size(&self) -> f32 {
|
|
self.feature_size.to_f32()
|
|
}
|
|
fn timestep(&self) -> f32 {
|
|
self.timestep.to_f32()
|
|
}
|
|
fn step_no(&self) -> u64 {
|
|
self.step_no
|
|
}
|
|
}
|
|
|
|
|
|
impl<R: Real, M: Material<R> + Send + Sync> GenericSim for SimState<R, M> {
|
|
fn step_multiple<S: AbstractStimulus>(&mut self, num_steps: u32, s: &S) {
|
|
for _ in 0..num_steps {
|
|
self.apply_stimulus(s);
|
|
SimState::step(self);
|
|
}
|
|
}
|
|
|
|
fn impulse_e_meters(&mut self, pos: Meters, amount: Vec3<f32>) {
|
|
*self.get_e_mut(pos) += amount.cast();
|
|
}
|
|
|
|
fn impulse_h_meters(&mut self, pos: Meters, amount: Vec3<f32>) {
|
|
self.impulse_b_meters(pos, amount * f32::mu0());
|
|
}
|
|
|
|
fn impulse_b_meters(&mut self, pos: Meters, amount: Vec3<f32>) {
|
|
self.get_hcell(pos).impulse_b(amount.cast());
|
|
}
|
|
}
|
|
|
|
#[allow(unused)]
|
|
impl<R: Real, M> SimState<R, M> {
|
|
fn get_material<C: Coord>(&self, c: C) -> &M {
|
|
let at = c.to_index(self.feature_size.cast());
|
|
&self.cells[at.row_major_idx()]
|
|
}
|
|
fn get_mat_mut<C: Coord>(&mut self, c: C) -> &mut M {
|
|
let at = c.to_index(self.feature_size.cast());
|
|
&mut self.cells[at.row_major_idx()]
|
|
}
|
|
|
|
fn get_e<C: Coord>(&self, c: C) -> &Vec3<R> {
|
|
let at = c.to_index(self.feature_size.cast());
|
|
&self.e[at.row_major_idx()]
|
|
}
|
|
fn get_e_mut<C: Coord>(&mut self, c: C) -> &mut Vec3<R> {
|
|
let at = c.to_index(self.feature_size.cast());
|
|
&mut self.e[at.row_major_idx()]
|
|
}
|
|
|
|
fn get_h<C: Coord>(&self, c: C) -> &Vec3<R> {
|
|
let at = c.to_index(self.feature_size.cast());
|
|
&self.h[at.row_major_idx()]
|
|
}
|
|
fn get_h_mut<C: Coord>(&mut self, c: C) -> &mut Vec3<R> {
|
|
let at = c.to_index(self.feature_size.cast());
|
|
&mut self.h[at.row_major_idx()]
|
|
}
|
|
|
|
fn get_hcell<'a, C: Coord>(&'a mut self, c: C) -> HCell<'a, R, M> {
|
|
let at = c.to_index(self.feature_size.cast());
|
|
let idx = at.row_major_idx();
|
|
HCell {
|
|
e: &self.e[idx],
|
|
h: &mut self.h[idx],
|
|
mat: &mut self.cells[idx],
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Conceptually, one cell looks like this (in 2d):
|
|
///
|
|
/// +-------.-------+
|
|
/// | Ex |
|
|
/// | |
|
|
/// | |
|
|
/// .Ey .Bz .
|
|
/// | |
|
|
/// | |
|
|
/// | |
|
|
/// +-------.-------+
|
|
///
|
|
/// Where the right hand rule indicates that positive Bz is pointing into the page, away from the
|
|
/// reader.
|
|
///
|
|
/// The dot on bottom is Ex of the cell at (x, y+1) and the dot on the right is the Ey of the cell at
|
|
/// (x+1, y). The `+` only indicates the corner of the cell -- nothing of interest is measured at
|
|
/// the pluses.
|
|
///
|
|
/// To adapt this to 3d, we keep the above cross-section, and then add another cross-section at a
|
|
/// half cell depth into the page:
|
|
/// 3d version (there may be multiple solutions):
|
|
///
|
|
/// Ez------.-------+
|
|
/// | By |
|
|
/// | |
|
|
/// | |
|
|
/// .Bx .
|
|
/// | |
|
|
/// | |
|
|
/// | |
|
|
/// +-------.-------+
|
|
///
|
|
/// Altogether, each cell is a cube, with the field vectors lying on the surface of the 1/8th of
|
|
/// the cube closest to the asterisk, at (0, 0, 0)
|
|
///
|
|
/// +------------+------------+
|
|
/// |\ \ \
|
|
/// | \ \ \
|
|
/// | \ \ \
|
|
/// | \ Ez \ By \
|
|
/// | +------------+------------+
|
|
/// | |\ \ \ z
|
|
/// + | \ \ \ \
|
|
/// |\ | \ \ \ \
|
|
/// | \ | \ \ Ex \ \
|
|
/// | \ | *------------+------------+ *--- x
|
|
/// | \|Bx | | | |
|
|
/// | + | | | |
|
|
/// | |\ | | | |
|
|
/// + | \ | | | y
|
|
/// \ | \ | | |
|
|
/// \ | \|Ey |Bz |
|
|
/// \ | +------------+------------+
|
|
/// \| | | |
|
|
/// + | | |
|
|
/// \ | | |
|
|
/// \ | | |
|
|
/// \ | | |
|
|
/// \| | |
|
|
/// +------------+------------+
|
|
///
|
|
#[derive(Clone, Default, Serialize, Deserialize)]
|
|
pub struct Sample {
|
|
state: CellStateWithM<f32>,
|
|
conductivity: Vec3<f32>,
|
|
}
|
|
|
|
struct Neighbors<'a, R> {
|
|
left: Option<&'a Vec3<R>>,
|
|
right: Option<&'a Vec3<R>>,
|
|
up: Option<&'a Vec3<R>>,
|
|
down: Option<&'a Vec3<R>>,
|
|
out: Option<&'a Vec3<R>>,
|
|
in_: Option<&'a Vec3<R>>,
|
|
}
|
|
|
|
impl<'a, R> Neighbors<'a, R> {
|
|
fn new(array: &'a Array3<Vec3<R>>, location: [usize; 3]) -> Self {
|
|
let [z, y, x] = location;
|
|
let left = array.get([z, y, x.wrapping_sub(1)]);
|
|
let right = array.get([z, y, x + 1]);
|
|
let up = array.get([z, y.wrapping_sub(1), x]);
|
|
let down = array.get([z, y + 1, x]);
|
|
let out = array.get([z.wrapping_sub(1), y, x]);
|
|
let in_ = array.get([z + 1, y, x]);
|
|
Self {
|
|
left, right, up, down, out, in_
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Sample {
|
|
pub fn e(&self) -> Vec3<f32> {
|
|
self.state.e()
|
|
}
|
|
pub fn ex(&self) -> f32 {
|
|
self.e().x()
|
|
}
|
|
pub fn ey(&self) -> f32 {
|
|
self.e().y()
|
|
}
|
|
pub fn ez(&self) -> f32 {
|
|
self.e().z()
|
|
}
|
|
|
|
pub fn h(&self) -> Vec3<f32> {
|
|
self.state.h()
|
|
}
|
|
pub fn hx(&self) -> f32 {
|
|
self.h().x()
|
|
}
|
|
pub fn hy(&self) -> f32 {
|
|
self.h().y()
|
|
}
|
|
pub fn hz(&self) -> f32 {
|
|
self.h().z()
|
|
}
|
|
|
|
pub fn b(&self) -> Vec3<f32> {
|
|
self.state.b()
|
|
}
|
|
pub fn bx(&self) -> f32 {
|
|
self.b().x()
|
|
}
|
|
pub fn by(&self) -> f32 {
|
|
self.b().y()
|
|
}
|
|
pub fn bz(&self) -> f32 {
|
|
self.b().z()
|
|
}
|
|
|
|
pub fn m(&self) -> Vec3<f32> {
|
|
self.state.m()
|
|
}
|
|
pub fn conductivity(&self) -> Vec3<f32> {
|
|
self.conductivity
|
|
}
|
|
|
|
pub fn current_density(&self) -> Vec3<f32> {
|
|
// TODO: does this make sense for Pml?
|
|
let conductivity = self.conductivity();
|
|
self.e().elem_mul(conductivity)
|
|
}
|
|
|
|
}
|
|
|
|
/// Solve df/dt in:
|
|
/// nabla_g = df_dt_coeff * df/dt + f_coeff * f + f_int_coeff * \int f + f_int_int_coeff * \int \int f
|
|
#[allow(unused)]
|
|
fn solve_step_diff_eq<R: Real>(
|
|
nabla_g: Vec3<R>,
|
|
df_dt_coeff: Vec3<R>,
|
|
f_coeff: Vec3<R>,
|
|
f_int_coeff: Vec3<R>,
|
|
f_int_int_coeff: Vec3<R>,
|
|
delta_t: R,
|
|
f_prev: Vec3<R>,
|
|
f_int_prev: Vec3<R>,
|
|
f_int_int_prev: Vec3<R>,
|
|
) -> Vec3<R> {
|
|
// ```tex
|
|
// $f(t)$ from $(T-\Delta t)$ to $(T+\Delta t)$ is a line. As such:
|
|
// $f(t') = f_prev + t'*db/dt$ where $t'$ is the time since the last sample of $f$, i.e. $f_prev$
|
|
// and
|
|
// $\int f|t' = f_int_prev + t'*f_prev + 1/2 t'^2 df/dt$
|
|
// $\int \int f|t' = f_int_int_prev + t'*f_int_prev + 1/2 t'^2*f_prev + 1/6 t'^3*df/dt$
|
|
// ```
|
|
// Solve coeffs in:
|
|
// nabla_g = df_dt_coeff * df/dt + f_prev_coeff * f_prev
|
|
// + f_int_prev_coeff * f_int_prev + f_int_int_prev_coeff * f_int_int_prev
|
|
let f_int_int_prev_coeff = f_int_int_coeff;
|
|
let f_int_prev_coeff = f_int_coeff + f_int_int_coeff*delta_t;
|
|
let f_prev_coeff = f_coeff + (f_int_coeff +
|
|
f_int_int_coeff*R::half()
|
|
)*delta_t;
|
|
let df_dt_coeff = df_dt_coeff + (f_coeff +
|
|
(f_int_coeff + f_int_int_coeff*R::third()*delta_t)*R::half()*delta_t
|
|
)*delta_t;
|
|
|
|
// Solve coeffs in nabla_g = df_dt_coeff + offset;
|
|
let offset = f_prev_coeff.elem_mul(f_prev) +
|
|
f_int_prev_coeff.elem_mul(f_int_prev) +
|
|
f_int_int_prev_coeff.elem_mul(f_int_int_prev);
|
|
(nabla_g - offset).elem_div(df_dt_coeff)
|
|
}
|
|
|
|
/// Solve df/dt in:
|
|
/// nabla_g = df_dt_coeff * df/dt + f_coeff * f
|
|
/// Lower-order (more efficient) version of `solve_step_diff_eq`.
|
|
fn solve_step_diff_eq_linear<R: Real>(
|
|
nabla_g: Vec3<R>,
|
|
df_dt_coeff: Vec3<R>,
|
|
f_coeff: Vec3<R>,
|
|
delta_t: R,
|
|
f_prev: Vec3<R>,
|
|
) -> Vec3<R> {
|
|
// Solve coeffs in:
|
|
// nabla_g = df_dt_coeff * df/dt + f_prev_coeff * f_prev
|
|
let df_dt_coeff = df_dt_coeff + f_coeff*delta_t;
|
|
|
|
// Solve coeffs in nabla_g = df_dt_coeff + offset;
|
|
let offset = f_coeff.elem_mul(f_prev);
|
|
(nabla_g - offset).elem_div(df_dt_coeff)
|
|
}
|
|
|
|
struct HCell<'a, R, M> {
|
|
mat: &'a mut M,
|
|
h: &'a mut Vec3<R>,
|
|
e: &'a Vec3<R>,
|
|
}
|
|
|
|
struct ECell<'a, R, M> {
|
|
mat: &'a mut M,
|
|
e: &'a mut Vec3<R>,
|
|
h: &'a Vec3<R>,
|
|
}
|
|
|
|
impl<'a, R: Real, M: Material<R>> HCell<'a, R, M> {
|
|
pub fn b(&self) -> Vec3<R> {
|
|
(*self.h + self.mat.m()) * R::mu0()
|
|
}
|
|
|
|
fn impulse_b(&mut self, delta_b: Vec3<R>) {
|
|
let b_prev = self.b();
|
|
let state = CellState {
|
|
e: *self.e,
|
|
h: *self.h,
|
|
};
|
|
self.mat.step_b(&state, delta_b);
|
|
// mu0 (H + M) = B
|
|
// therefore, H = B mu0^-1 - M
|
|
*self.h = (b_prev + delta_b) * R::mu0_inv() - self.mat.m();
|
|
}
|
|
|
|
fn step(&mut self, neighbors: Neighbors<'_, R>, delta_t: R, inv_feature_size: R) {
|
|
// ```tex
|
|
// Maxwell-Faraday equation: $\nabla x E = -dB/dt$ (1)
|
|
//
|
|
// Note: $\nabla x E$ evaluates to:
|
|
// | i j k | [ dEz/dy - dEy/dz ]
|
|
// | | | |
|
|
// | d/dx d/dy d/dz | = | dEx/dz - dEz/dx |
|
|
// | | | |
|
|
// | Ex Ey Ez | [ dEy/dx - dEx/dy ]
|
|
//
|
|
// To accomodate PML materials, we allow coordinate stretching, specifically
|
|
// in the complex domain.
|
|
//
|
|
// For the rest of this analysis, we'll use $\omega$. This comes from a wave of the form
|
|
// $E(t) = A exp(-j\omega t)$ (2)
|
|
// Maxwell's equations are all linear, so we'll do the derivations considering just a
|
|
// single such wave, knowing that what we derive fully generalizes to any $E(t)$.
|
|
// If we do it right, then $\omega$ will drop out from the end result.
|
|
//
|
|
// Consider: $x' = x + jf(x)$.
|
|
// Then $dx' = (1 + jdf/dx)dx$
|
|
// Let $df/dx = \gamma_x/\omega$. This is chosen to eliminate $\omega$ in the end result.
|
|
// Then, $d/dx = 1/(1 + j\gamma_x/\omega) d/dx'$
|
|
// Likewise, $d/dy = 1/(1 + j\gamma_y/\omega) d/dy'$
|
|
// $d/dz = 1/(1 + j\gamma_z/\omega) d/dz'$
|
|
//
|
|
// Let $S = 1 + j\gamma/\omega$, such that $d/dx = Sx^-1 d/dx'$, etc.
|
|
//
|
|
// Then (1) can be expanded to the following matrix multiplication
|
|
// $ [ 0 -Sz^-1 d/dz' Sy^-1 d/dy' ] $
|
|
// $ | Sz^-1 d/dz' 0 -Sx^-1 d/dx' | E = -dB/dt$
|
|
// $ [ -Sy^-1 d/dy' Sx^-1 d/dx' 0 ]$
|
|
//
|
|
// Normalize by multiplying each side by a diagonal matrix:
|
|
//
|
|
// $ [ 0 -Sy d/dz' Sz d/dy' ] [ SySz ] $
|
|
// $ -| Sx d/dz' 0 -Sz d/dx' | E = diag| SxSz | dB/dt (3)$
|
|
// $ [ -Sx d/dy' Sy d/dx' 0 ] [ SxSy ] $
|
|
//
|
|
// The LHS simplifies to:
|
|
// $ -\nabla x (diag(S) E)$
|
|
// $ = -\nabla x (E + diag(\gamma) j/\omega E)$
|
|
//
|
|
// From (2), we know $B$ is of the form $B = A exp(-j\omega t)$
|
|
// Therefore $\int_0^t B(\tau) d\tau = j/\omega [B(t) - B(0)] = j/\omega B(t)$ (we assume $B(0) = 0$)
|
|
// Thus the LHS becomes:
|
|
// $ = -\nabla x (E + diag(\gamma) \int E)$
|
|
//
|
|
// Expand (3), letting $I = j/\omega$:
|
|
// $ |(1 + \gamma_yI)(1 + \gamma_zI)|$
|
|
// $ -\nabla x (E + diag(\gamma) \int E) = diag[(1 + \gamma_xI)(1 + \gamma_zI)] dB/dt$
|
|
// $ |(1 + \gamma_xI)(1 + \gamma_yI)|$
|
|
//
|
|
// $ |\gamma_y + \gamma_z| |\gamma_y\gamma_z|$
|
|
// $ -\nabla x (E + diag(\gamma) \int E) = dB/dt + diag[\gamma_x + \gamma_z] I dB/dt + diag[\gamma_x\gamma_z] I^2 dB/dt$
|
|
// $ |\gamma_x + \gamma_y| |\gamma_x\gamma_y|$
|
|
//
|
|
// As seen above, $I = j/w$ is equivalent to integration:
|
|
// $ |\gamma_y + \gamma_z| |\gamma_y\gamma_z|$
|
|
// $ -\nabla x (E + diag(\gamma) \int E) = dB/dt + diag[\gamma_x + \gamma_z] B + diag[\gamma_x\gamma_z] \int B$
|
|
// $ |\gamma_x + \gamma_y| |\gamma_x\gamma_y|$
|
|
//
|
|
// Finally, discretization. We're expanding about $t = T$, where we know $B(T-\Delta t)$ and $E(T)$.
|
|
// $B(t)$ from $(T-\Delta t)$ to $(T+\Delta t)$ is a line. As such:
|
|
// $B(t') = B_prev + t'*db/dt$ where $t'$ is the time since the last sample of $B$, i.e. $B_prev$
|
|
// and
|
|
// $\int B|t' = B_int_prev + t'*B_prev + 1/2 t'^2 dB/dt
|
|
//
|
|
// ```
|
|
// let inv_feature_size = Real::from_inner(1.0) / feature_size;
|
|
let twice_delta_t = R::two() * delta_t;
|
|
|
|
let b_prev = self.b();
|
|
|
|
let (delta_ey_x, delta_ez_x) = match (neighbors.right, neighbors.left) {
|
|
(Some(right), _) => (
|
|
inv_feature_size * (right.y() - self.e.y()),
|
|
inv_feature_size * (right.z() - self.e.z()),
|
|
),
|
|
_ => (R::zero(), R::zero()),
|
|
};
|
|
let (delta_ex_y, delta_ez_y) = match (neighbors.down, neighbors.up) {
|
|
(Some(down), _) => (
|
|
inv_feature_size * (down.x() - self.e.x()),
|
|
inv_feature_size * (down.z() - self.e.z()),
|
|
),
|
|
_ => (R::zero(), R::zero()),
|
|
};
|
|
let (delta_ex_z, delta_ey_z) = match (neighbors.in_, neighbors.out) {
|
|
(Some(in_), _) => (
|
|
inv_feature_size * (in_.x() - self.e.x()),
|
|
inv_feature_size * (in_.y() - self.e.y()),
|
|
),
|
|
_ => (R::zero(), R::zero()),
|
|
};
|
|
|
|
let StepParametersMut { conductivity: _, pml } = self.mat.step_parameters_mut();
|
|
// XXX conductivity is unused when stepping h.
|
|
let nabla_e = match pml {
|
|
None => {
|
|
// \nabla x E
|
|
Vec3::from((delta_ez_y - delta_ey_z, delta_ex_z - delta_ez_x, delta_ey_x - delta_ex_y))
|
|
}
|
|
Some((pml, PmlParameters { pseudo_conductivity })) => {
|
|
//```tex
|
|
// S = propagation_const + pseudo_conductivity / (dc_shift + j\omega \mu_0)
|
|
// $S = 1 + sigma/(j\omega \mu_0)$
|
|
// After some derivation, we get:
|
|
// $F{S^-1} = \delta(t) - \sigma/\mu_0exp[-t(\sigma/\mu_0)]u(t)$
|
|
//```
|
|
let sigma = pseudo_conductivity;
|
|
|
|
let e = (-sigma * twice_delta_t).exp();
|
|
let aux = &mut pml.aux_h;
|
|
// (S^-1 \nabla) x E
|
|
Vec3::from((
|
|
aux.sy_conv_dfz_dy.step_only_sigma(e.y(), delta_ez_y)
|
|
- aux.sz_conv_dfy_dz.step_only_sigma(e.z(), delta_ey_z),
|
|
aux.sz_conv_dfx_dz.step_only_sigma(e.z(), delta_ex_z)
|
|
- aux.sx_conv_dfz_dx.step_only_sigma(e.x(), delta_ez_x),
|
|
aux.sx_conv_dfy_dx.step_only_sigma(e.x(), delta_ey_x)
|
|
- aux.sy_conv_dfx_dy.step_only_sigma(e.y(), delta_ex_y),
|
|
))
|
|
}
|
|
};
|
|
|
|
// From: $\nabla x E = -dB/dt$
|
|
let db_dt = -nabla_e;
|
|
// let db_dt = solve_step_diff_eq(
|
|
// nabla_e,
|
|
// -Vec3::unit(), // dB/dt coeff
|
|
// Vec3::zero(), // B coeff
|
|
// Vec3::zero(), // \int B coeff
|
|
// Vec3::zero(), // \int \int B coeff
|
|
// delta_t,
|
|
// b_prev,
|
|
// Vec3::zero(), // b_int_prev,
|
|
// Vec3::zero(), // b_int_int_prev
|
|
// );
|
|
|
|
// Maxwell's equation gave us delta_b. Material parameters let us turn that into h.
|
|
let delta_b = db_dt * twice_delta_t;
|
|
let state = CellState {
|
|
e: *self.e,
|
|
h: *self.h,
|
|
};
|
|
self.mat.step_b(&state, delta_b);
|
|
// mu0 (H + M) = B
|
|
// therefore, H = B mu0^-1 - M
|
|
// println!("cpu-step_h: nabla_e: {}", nabla_e);
|
|
// println!("cpu-step_h: delta_h: {}", delta_b * R::mu0_inv());
|
|
*self.h = (b_prev + delta_b) * R::mu0_inv() - self.mat.m();
|
|
}
|
|
}
|
|
|
|
impl<'a, R: Real, M: Material<R>> ECell<'a, R, M> {
|
|
/// delta_t = timestep covered by this function. i.e. it should be half the timestep of the simulation
|
|
/// feature_size = how many units apart is the center of each adjacent cell on the grid.
|
|
fn step(&mut self, neighbors: Neighbors<'_, R>, delta_t: R, inv_feature_size: R) {
|
|
// ```tex
|
|
// Ampere's circuital law with Maxwell's addition, in SI units ("macroscopic version"):
|
|
// $\nabla x H = J_f + dD/dt$ where $J_f$ = current density = $\sigma E$, $\sigma$ being a material parameter ("conductivity")
|
|
// Note that $D = \epsilon_0 E + P$, but we don't simulate any material where $P \ne 0$.
|
|
// Expand $D$: $\nabla x H = J_f + \epsilon_0 dE/dt$
|
|
// Expand $J$: $\nabla x H = \sigma E + \epsilon_0 dE/dt$
|
|
// Rearrange: $dE/dt = 1/\epsilon_0 (\nabla x H - \sigma E)$
|
|
//
|
|
// let $E_p$ be $E$ at $T-\Delta t$ and $E_n$ be $E$ at $T+\Delta t$.
|
|
// $(E_n - E_p)/(2\Delta{t}) = 1/\epsilon_0 (\nabla x H - \sigma (E_n + E_p)/2)$
|
|
// Normalize: $E_n - E_p = 2\Delta{t}/\epsilon_0 (\nabla x H - \sigma (E_n + E_p)/2)$
|
|
// Expand: $E_n - E_p = 2\Delta{t}/\epsilon_0 \nabla x H - \sigma \Delta{t}/\epsilon_0 (E_n + E_p)$
|
|
// Rearrange: $E_n(1 + \sigma \Delta{t}/\epsilon_0) = E_p(1 - \sigma \Delta{t}/\epsilon_0) + 2\Delta{t}/\epsilon_0 \nabla x H$
|
|
// Then $E_n$ (i.e. the E vector after this step) is trivially solved
|
|
//
|
|
// Note: $\nabla x H$ evaluates to:
|
|
// | i j k | [ dHz/dy - dHy/dz ]
|
|
// | | | |
|
|
// | d/dx d/dy d/dz | = | dHx/dz - dHz/dx |
|
|
// | | | |
|
|
// | Hx Hy Hz | [ dHy/dx - dHx/dy ]
|
|
//
|
|
// Boundary conditions (TODO):
|
|
// For something on the yz plane, E(x=0, t+1) should be E(x=1, t). i.e. the wave travels
|
|
// $\Delta x$ = 1 per timestep. Roughly, we want to take the line from E(x=0, t), E(x=1, t)
|
|
// and continue that to E(x=-1, t) ?
|
|
//
|
|
// ```
|
|
|
|
let twice_delta_t = R::two() * delta_t;
|
|
|
|
let e_prev = *self.e;
|
|
|
|
let (delta_hy_x, delta_hz_x) = match (neighbors.left, neighbors.right) {
|
|
(Some(left), _) => (
|
|
inv_feature_size * (self.h.y() - left.y()),
|
|
inv_feature_size * (self.h.z() - left.z()),
|
|
),
|
|
_ => (R::zero(), R::zero()),
|
|
};
|
|
let (delta_hx_y, delta_hz_y) = match (neighbors.up, neighbors.down) {
|
|
(Some(up), _) => (
|
|
inv_feature_size * (self.h.x() - up.x()),
|
|
inv_feature_size * (self.h.z() - up.z()),
|
|
),
|
|
_ => (R::zero(), R::zero()),
|
|
};
|
|
let (delta_hx_z, delta_hy_z) = match (neighbors.out, neighbors.in_) {
|
|
(Some(out), _) => (
|
|
inv_feature_size * (self.h.x() - out.x()),
|
|
inv_feature_size * (self.h.y() - out.y()),
|
|
),
|
|
_ => (R::zero(), R::zero()),
|
|
};
|
|
|
|
let StepParametersMut { conductivity, pml } = self.mat.step_parameters_mut();
|
|
let nabla_h = match pml {
|
|
None => {
|
|
// \nabla x H
|
|
Vec3::from((delta_hz_y - delta_hy_z, delta_hx_z - delta_hz_x, delta_hy_x - delta_hx_y))
|
|
}
|
|
Some((pml, PmlParameters { pseudo_conductivity })) => {
|
|
//```tex
|
|
// S = propagation_const + pseudo_conductivity / (dc_shift + j\omega \epsilon_0)
|
|
// $S = 1 + sigma/(j\omega \epsilon_0)$
|
|
// After some derivation, we get:
|
|
// $F{S^-1} = \delta(t) - \sigma/\epsilon_0exp[-t(\sigma/\epsilon_0)]u(t)$
|
|
//```
|
|
let sigma = pseudo_conductivity;
|
|
|
|
let e = (-sigma * twice_delta_t).exp();
|
|
let aux = &mut pml.aux_e;
|
|
// (S^-1 \nabla) x H
|
|
Vec3::from((
|
|
aux.sy_conv_dfz_dy.step_only_sigma(e.y(), delta_hz_y)
|
|
- aux.sz_conv_dfy_dz.step_only_sigma(e.z(), delta_hy_z),
|
|
aux.sz_conv_dfx_dz.step_only_sigma(e.z(), delta_hx_z)
|
|
- aux.sx_conv_dfz_dx.step_only_sigma(e.x(), delta_hz_x),
|
|
aux.sx_conv_dfy_dx.step_only_sigma(e.x(), delta_hy_x)
|
|
- aux.sy_conv_dfx_dy.step_only_sigma(e.y(), delta_hx_y),
|
|
))
|
|
}
|
|
};
|
|
|
|
// From: $\nabla x H = \epsilon_0 dE/dt + \sigma E$
|
|
let de_dt = solve_step_diff_eq_linear(
|
|
nabla_h,
|
|
Vec3::uniform(R::eps0()), // dE/dt coeff
|
|
conductivity, // E coeff
|
|
delta_t,
|
|
e_prev,
|
|
);
|
|
|
|
// println!("cpu-step_e: nabla_h: {}", nabla_h);
|
|
// println!("cpu-step_e: delta_e: {}", de_dt*twice_delta_t);
|
|
*self.e = e_prev + de_dt*twice_delta_t;
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct CellState<R> {
|
|
e: Vec3<R>,
|
|
h: Vec3<R>,
|
|
}
|
|
|
|
impl<R: Real> CellState<R> {
|
|
pub fn e(&self) -> Vec3<R> {
|
|
self.e
|
|
}
|
|
pub fn h(&self) -> Vec3<R> {
|
|
self.h
|
|
}
|
|
|
|
pub fn with_m(&self, m: Vec3<R>) -> CellStateWithM<R> {
|
|
CellStateWithM {
|
|
e: self.e,
|
|
h: self.h,
|
|
m,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
pub struct CellStateWithM<R> {
|
|
e: Vec3<R>,
|
|
h: Vec3<R>,
|
|
m: Vec3<R>,
|
|
}
|
|
|
|
impl<R: Real> CellStateWithM<R> {
|
|
pub fn e(&self) -> Vec3<R> {
|
|
self.e
|
|
}
|
|
pub fn h(&self) -> Vec3<R> {
|
|
self.h
|
|
}
|
|
pub fn m(&self) -> Vec3<R> {
|
|
self.m
|
|
}
|
|
pub fn b(&self) -> Vec3<R> {
|
|
(self.h() + self.m()) * R::mu0()
|
|
}
|
|
}
|
|
|
|
/// Most of these serve as "smoke tests", to catch and pinpoint the most egregious of errors, or to
|
|
/// understand the impact of future changes (like changing default boundary conditions).
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use crate::geom::{Cube, Region, WorldRegion};
|
|
use crate::mat::{Conductor, Pml, Static};
|
|
use crate::real::{R64, ToFloat as _};
|
|
use float_eq::assert_float_eq;
|
|
use more_asserts::*;
|
|
use rand::rngs::StdRng;
|
|
use rand::{Rng as _, SeedableRng as _};
|
|
use std::fmt::Debug;
|
|
use std::ops::RangeBounds;
|
|
|
|
fn do_conv_flt_test(a: f64, b: f64, c: f64, delta_t: f64, signal: &[f64], expected: &[f64]) {
|
|
let mut cf = ConvFlt(0.0);
|
|
for (s, e) in signal.iter().zip(expected.iter()) {
|
|
let v = cf.step(
|
|
a,
|
|
b,
|
|
c,
|
|
*s,
|
|
delta_t,
|
|
);
|
|
assert_float_eq!(v, e, r2nd <= 1e-6);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn conv_flt_trivial() {
|
|
let signal = [2.0, 7.0, -3.0];
|
|
// kernel: 1.0 \delta(t)
|
|
let expected = [2.0, 7.0, -3.0];
|
|
let (a, b, c, delta_t) = (1.0, 0.0, 1.0, 0.1);
|
|
do_conv_flt_test(a, b, c, delta_t, &signal, &expected);
|
|
}
|
|
|
|
#[test]
|
|
fn conv_flt_trivial_scaled() {
|
|
let signal = [2.0, 7.0, -3.0];
|
|
// kernel: 10.0 \delta(t)
|
|
let expected = [20.0, 70.0, -30.0];
|
|
let (a, b, c, delta_t) = (10.0, 0.0, 1.0, 0.1);
|
|
do_conv_flt_test(a, b, c, delta_t, &signal, &expected);
|
|
}
|
|
|
|
#[test]
|
|
fn conv_flt_exp_trivial() {
|
|
let signal = [2.0, 0.0, 0.0];
|
|
// kernel: e(-t)
|
|
// \int_0^1 e(-t) dt = [1 - exp(-1)]
|
|
let exp_neg_0 = (-0.0f64).exp();
|
|
let exp_neg_1 = (-1.0f64).exp();
|
|
let exp_neg_2 = (-2.0f64).exp();
|
|
let exp_neg_3 = (-3.0f64).exp();
|
|
let expected = [
|
|
2.0*(exp_neg_0 - exp_neg_1),
|
|
2.0*(exp_neg_1 - exp_neg_2),
|
|
2.0*(exp_neg_2 - exp_neg_3),
|
|
];
|
|
let (a, b, c, delta_t) = (0.0, 1.0, 1.0, 1.0);
|
|
do_conv_flt_test(a, b, c, delta_t, &signal, &expected);
|
|
}
|
|
|
|
#[test]
|
|
fn conv_flt_exp_time_scaled() {
|
|
let signal = [2.0, 0.0, 0.0];
|
|
// kernel: e(-3*t)
|
|
// \int_0^0.2 e(-3*t) dt = [1 - exp(-0.6)]/3
|
|
let exp_neg_00 = (-0.0f64).exp();
|
|
let exp_neg_06 = (-0.6f64).exp();
|
|
let exp_neg_12 = (-1.2f64).exp();
|
|
let exp_neg_18 = (-1.8f64).exp();
|
|
let expected = [
|
|
2.0/3.0*(exp_neg_00 - exp_neg_06),
|
|
2.0/3.0*(exp_neg_06 - exp_neg_12),
|
|
2.0/3.0*(exp_neg_12 - exp_neg_18),
|
|
];
|
|
let (a, b, c, delta_t) = (0.0, 1.0, 3.0, 0.2);
|
|
do_conv_flt_test(a, b, c, delta_t, &signal, &expected);
|
|
}
|
|
|
|
#[test]
|
|
fn conv_flt_exp_multi_input() {
|
|
let signal = [2.0, 7.0, -3.0];
|
|
// kernel: e(-3*t)
|
|
// \int_0^0.2 e(-3*t) dt = [1 - exp(-0.6)]/3
|
|
let exp_neg_00 = (-0.0f64).exp();
|
|
let exp_neg_06 = (-0.6f64).exp();
|
|
let exp_neg_12 = (-1.2f64).exp();
|
|
let exp_neg_18 = (-1.8f64).exp();
|
|
let expected = [
|
|
2.0/3.0*(exp_neg_00 - exp_neg_06),
|
|
7.0/3.0*(exp_neg_00 - exp_neg_06) + 2.0/3.0*(exp_neg_06 - exp_neg_12),
|
|
-3.0/3.0*(exp_neg_00 - exp_neg_06)
|
|
+ 7.0/3.0*(exp_neg_06 - exp_neg_12)
|
|
+ 2.0/3.0*(exp_neg_12 - exp_neg_18),
|
|
];
|
|
let (a, b, c, delta_t) = (0.0, 1.0, 3.0, 0.2);
|
|
do_conv_flt_test(a, b, c, delta_t, &signal, &expected);
|
|
}
|
|
|
|
#[test]
|
|
fn conv_flt_all_params() {
|
|
let signal = [2.0, 7.0, -3.0];
|
|
// kernel: 0.3 \delta(t) + 0.4 * e(-3*t)
|
|
// \int_0^0.2 e(-3*t) dt = [1 - exp(-0.6)]/3
|
|
let exp_neg_00 = (-0.0f64).exp();
|
|
let exp_neg_06 = (-0.6f64).exp();
|
|
let exp_neg_12 = (-1.2f64).exp();
|
|
let exp_neg_18 = (-1.8f64).exp();
|
|
let expected_exp = [
|
|
2.0/3.0*(exp_neg_00 - exp_neg_06),
|
|
7.0/3.0*(exp_neg_00 - exp_neg_06) + 2.0/3.0*(exp_neg_06 - exp_neg_12),
|
|
-3.0/3.0*(exp_neg_00 - exp_neg_06)
|
|
+ 7.0/3.0*(exp_neg_06 - exp_neg_12)
|
|
+ 2.0/3.0*(exp_neg_12 - exp_neg_18),
|
|
];
|
|
let expected = [
|
|
0.3 * signal[0] + 0.4 * expected_exp[0],
|
|
0.3 * signal[1] + 0.4 * expected_exp[1],
|
|
0.3 * signal[2] + 0.4 * expected_exp[2],
|
|
];
|
|
let (a, b, c, delta_t) = (0.3, 0.4, 3.0, 0.2);
|
|
do_conv_flt_test(a, b, c, delta_t, &signal, &expected);
|
|
}
|
|
|
|
fn assert_vec3_eq<R: Real>(actual: Vec3<R>, expected: Vec3<R>) {
|
|
let actual = actual.cast::<f64>();
|
|
let expected = expected.cast::<f64>();
|
|
assert_float_eq!(actual.x(), expected.x(), r2nd <= 1e-6);
|
|
assert_float_eq!(actual.y(), expected.y(), r2nd <= 1e-6);
|
|
assert_float_eq!(actual.z(), expected.z(), r2nd <= 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn step_diff_eq_trivial() {
|
|
// f(t) = 0.0
|
|
// nabla_g = 0.1 df/dt
|
|
let df_dt = solve_step_diff_eq(
|
|
Vec3::new(2.0, 3.0, 4.0), // nabla_g
|
|
Vec3::uniform(0.1), // df_dt_coeff
|
|
Vec3::zero(),
|
|
Vec3::zero(),
|
|
Vec3::zero(),
|
|
0.5, // delta_t
|
|
Vec3::zero(),
|
|
Vec3::zero(),
|
|
Vec3::zero(),
|
|
);
|
|
assert_vec3_eq(df_dt, Vec3::new(20.0, 30.0, 40.0));
|
|
}
|
|
|
|
#[test]
|
|
fn step_diff_eq_trivial_3rd_order() {
|
|
// f(t) = 0.0
|
|
// nabla_g = 0.1 df/dt - f - 3 f_int + 0.4 f_int_int
|
|
let df_dt = solve_step_diff_eq(
|
|
Vec3::new(2.0, 3.0, 4.0), // nabla_g
|
|
Vec3::uniform(0.1), // df_dt_coeff
|
|
Vec3::uniform(-1.0), // f_coeff
|
|
Vec3::uniform(-3.0), // f_int_coeff
|
|
Vec3::uniform(0.4), // f_int_int_coeff
|
|
0.0, // delta_t
|
|
Vec3::zero(),
|
|
Vec3::zero(),
|
|
Vec3::zero(),
|
|
);
|
|
assert_vec3_eq(df_dt, Vec3::new(20.0, 30.0, 40.0));
|
|
}
|
|
|
|
#[test]
|
|
fn step_diff_eq_1st_order_no_dt() {
|
|
// nabla_g = 0.1 df/dt + 2 f
|
|
// Let f(t) = sin(2 t)
|
|
// then f'(t) = 2 cos(2 t)
|
|
let f = Vec3::uniform((5.0f64).sin());
|
|
let df_dt = Vec3::uniform(2.0 * (5.0f64).cos());
|
|
let nabla_g = df_dt*0.1 + f*2.0;
|
|
let actual_df_dt = solve_step_diff_eq(
|
|
nabla_g,
|
|
Vec3::uniform(0.1),
|
|
Vec3::uniform(2.0),
|
|
Vec3::zero(),
|
|
Vec3::zero(),
|
|
0.0, // delta_t
|
|
f, // f_prev
|
|
Vec3::zero(),
|
|
Vec3::zero(),
|
|
);
|
|
assert_vec3_eq(actual_df_dt, df_dt);
|
|
}
|
|
|
|
#[test]
|
|
fn step_diff_eq_2nd_order_no_dt() {
|
|
// nabla_g = 0.1 df/dt + 2 f + 0.4 f_int
|
|
// Let f(t) = sin(2 t)
|
|
// then f'(t) = 2 cos(2 t)
|
|
// then \int f(t) = -1/2 cos(2 t)
|
|
let f = Vec3::uniform((5.0f64).sin());
|
|
let df_dt = Vec3::uniform(2.0 * (5.0f64).cos());
|
|
let f_int = Vec3::uniform(-0.5 * (5.0f64).cos());
|
|
let nabla_g = df_dt*0.1 + f*2.0 + f_int*0.4;
|
|
let actual_df_dt = solve_step_diff_eq(
|
|
nabla_g,
|
|
Vec3::uniform(0.1),
|
|
Vec3::uniform(2.0),
|
|
Vec3::uniform(0.4),
|
|
Vec3::zero(),
|
|
0.0, // delta_t
|
|
f, // f_prev
|
|
f_int, // f_int_prev
|
|
Vec3::zero(),
|
|
);
|
|
assert_vec3_eq(actual_df_dt, df_dt);
|
|
}
|
|
|
|
#[test]
|
|
fn step_diff_eq_3rd_order_no_dt() {
|
|
// nabla_g = 0.1 df/dt + 2 f + 0.4 f_int + 0.3 f_int_int
|
|
// Let f(t) = sin(2 t)
|
|
// then f'(t) = 2 cos(2 t)
|
|
// then \int f(t) = -1/2 cos(2 t)
|
|
// then \int \int f(t) = -1/4 sin(2 t)
|
|
let f = Vec3::unit()*(5.0f64).sin();
|
|
let df_dt = Vec3::unit()*2.0 * (5.0f64).cos();
|
|
let f_int = Vec3::unit()*-0.5 * (5.0f64).cos();
|
|
let f_int_int = Vec3::unit()*-0.25 * (5.0f64).sin();
|
|
let nabla_g = df_dt*0.1 + f*2.0 + f_int*0.4 + f_int_int*0.3;
|
|
let actual_df_dt = solve_step_diff_eq(
|
|
nabla_g,
|
|
Vec3::uniform(0.1),
|
|
Vec3::uniform(2.0),
|
|
Vec3::uniform(0.4),
|
|
Vec3::uniform(0.3),
|
|
0.0, // delta_t
|
|
f, // f_prev
|
|
f_int, // f_int_prev
|
|
f_int_int, // f_int_int_prev
|
|
);
|
|
assert_vec3_eq(actual_df_dt, df_dt);
|
|
}
|
|
|
|
#[test]
|
|
fn step_diff_eq_1st_order_homogenous() {
|
|
// 0 = f'(t) + f(t)
|
|
// solution: f(t) = k*e^-t
|
|
let f = |t: f32| -> Vec3<f32> {
|
|
Vec3::unit()*(-t).exp()
|
|
};
|
|
let df_dt = |t: f32| -> Vec3<f32> {
|
|
Vec3::unit()*-(-t).exp()
|
|
};
|
|
let f_int = |t: f32| -> Vec3<f32> {
|
|
Vec3::unit()*-(-t).exp()
|
|
};
|
|
let f_int_int = |t: f32| -> Vec3<f32> {
|
|
Vec3::unit()*(-t).exp()
|
|
};
|
|
let tl = 3.0000;
|
|
let tm = 3.0001;
|
|
let actual_df_dt = solve_step_diff_eq(
|
|
Vec3::zero(),
|
|
Vec3::unit(), // df_dt_coeff
|
|
Vec3::unit(), // f_coeff
|
|
Vec3::zero(),
|
|
Vec3::zero(),
|
|
tm - tl, // delta_t
|
|
f(tl), // f_prev
|
|
f_int(tl),
|
|
f_int_int(tl),
|
|
);
|
|
assert_vec3_eq(actual_df_dt, df_dt(tm));
|
|
}
|
|
|
|
#[test]
|
|
fn step_diff_eq_1st_order_nonhomogenous() {
|
|
// 1 = f'(t) + f(t)
|
|
// let f(t) = 1 + e^-t
|
|
let f = |t: f32| -> Vec3<f32> {
|
|
Vec3::unit() + Vec3::unit()*(-t).exp()
|
|
};
|
|
let df_dt = |t: f32| -> Vec3<f32> {
|
|
Vec3::unit()*-(-t).exp()
|
|
};
|
|
let f_int = |t: f32| -> Vec3<f32> {
|
|
Vec3::unit()*-(-t).exp()
|
|
};
|
|
let f_int_int = |t: f32| -> Vec3<f32> {
|
|
Vec3::unit()*(-t).exp()
|
|
};
|
|
let tl = 3.0000;
|
|
let tm = 3.0001;
|
|
let actual_df_dt = solve_step_diff_eq(
|
|
Vec3::unit(), // nabla_g
|
|
Vec3::unit(), // df_dt_coeff
|
|
Vec3::unit(), // f_coeff
|
|
Vec3::zero(),
|
|
Vec3::zero(),
|
|
tm - tl, // delta_t
|
|
f(tl), // f_prev
|
|
f_int(tl),
|
|
f_int_int(tl),
|
|
);
|
|
assert_vec3_eq(actual_df_dt, df_dt(tm));
|
|
}
|
|
|
|
#[test]
|
|
fn step_diff_eq_3rd_order_nonhomogenous() {
|
|
// c0 = af'(t) + bf(t) + c\int f(t) + d\int \int f(t)
|
|
// let f(t) = v0*e^wt, and \int \int f(t) absorbs C
|
|
// These constants are chosen arbitrarily: this test
|
|
// should pass for any "valid" values
|
|
let v0 = Vec3::new(3.0, 5.0, -0.5);
|
|
let w = Vec3::new(0.1, -2.0, 0.4);
|
|
let c0 = Vec3::new(-1.0, -2.0, -3.0);
|
|
let a = Vec3::new(0.1, 1.0, 2.0); // must be non-zero
|
|
let b = Vec3::new(0.5, -0.3, 0.0);
|
|
let c = Vec3::new(-5.0, 0.0, -0.1);
|
|
let d = -w.elem_mul(c + w.elem_mul(b + w.elem_mul(a)));
|
|
let f = |t: f64| -> Vec3<f64> {
|
|
v0.elem_mul((w*t).exp())
|
|
};
|
|
let df_dt = |t: f64| -> Vec3<f64> {
|
|
v0.elem_mul((w*t).exp()).elem_mul(w)
|
|
};
|
|
let f_int = |t: f64| -> Vec3<f64> {
|
|
v0.elem_mul((w*t).exp()).elem_div(w)
|
|
};
|
|
let f_int_int = |t: f64| -> Vec3<f64> {
|
|
v0.elem_mul((w*t).exp()).elem_div(w).elem_div(w) + c0.elem_div(d)
|
|
};
|
|
let tl = 3.00000000;
|
|
let tm = 3.00000001;
|
|
let actual_df_dt = solve_step_diff_eq(
|
|
c0, // nabla_g
|
|
a, // df_dt_coeff
|
|
b, // f_coeff
|
|
c, // int_f_coeff
|
|
d, // int_int_f_coeff
|
|
tm - tl, // delta_t
|
|
f(tl), // f_prev
|
|
f_int(tl), // f_int_prev
|
|
f_int_int(tl), // f_int_int_prev
|
|
);
|
|
assert_vec3_eq(actual_df_dt, df_dt(tm));
|
|
}
|
|
|
|
fn energy(s: &dyn SampleableSim) -> f32 {
|
|
0.5 * s.feature_volume() * s.map_sum_enumerated(|_pos: Index, cell| {
|
|
// println!("{}: {}", _pos, cell.e());
|
|
cell.e().mag_sq().to_f64()
|
|
}).to_f32()
|
|
}
|
|
|
|
fn energy_now_and_then<S: GenericSim>(state: &mut S, frames: u32) -> (f32, f32) {
|
|
let energy_0 = energy(state);
|
|
for _f in 0..frames {
|
|
// println!("e({}) = {}", _f, energy(state));
|
|
state.step();
|
|
}
|
|
let energy_1 = energy(state);
|
|
(energy_0, energy_1)
|
|
}
|
|
|
|
#[test]
|
|
fn accessors() {
|
|
let mut state = SimState::<R64, Static<R64>>::new(Index((15, 17, 19).into()), 1e-6);
|
|
assert_eq!(state.width(), 15);
|
|
assert_eq!(state.height(), 17);
|
|
assert_eq!(state.depth(), 19);
|
|
assert_eq!(state.time(), 0.0);
|
|
// feat_size / C * courant: 1e-6 / 3e8 * 0.577
|
|
assert_eq!(state.timestep(), 0.000000000000001924664829293337);
|
|
state.step();
|
|
assert_eq!(state.time(), state.timestep());
|
|
}
|
|
|
|
#[test]
|
|
fn energy_conservation_over_time() {
|
|
let mut state = SimState::<R64, Static<R64>>::new(Index((2001, 1, 1).into()), 1e-6);
|
|
for t in 0..100 {
|
|
let angle = (t as f32)/100.0*f32::two_pi();
|
|
let sig = angle.sin();
|
|
let gate = 0.5*(1.0 - angle.cos());
|
|
//println!("{}", g.exp());
|
|
state.impulse_ez(Index((1000, 0, 0).into()), gate*sig);
|
|
//dyn_state.impulse_by(index((x, 0, 0).into()), 1.5e-9*g.exp());
|
|
state.step();
|
|
}
|
|
let (energy_0, energy_1) = energy_now_and_then(&mut state, 800);
|
|
// This has some sensitivity to courant number. But surprisingly, not much to f32 v.s. f64
|
|
assert_float_eq!(energy_1, energy_0, r2nd <= 1e-9);
|
|
}
|
|
|
|
#[test]
|
|
fn sane_boundary_conditions() {
|
|
let mut state = SimState::<R64, Static<R64>>::new(Index((21, 21, 21).into()), 1e-6);
|
|
for t in 0..40 {
|
|
let angle = (t as f32)/10.0*f32::two_pi();
|
|
let sig = angle.sin();
|
|
let gate = 1e9 * 0.5*(1.0 - angle.cos());
|
|
state.impulse_ez(Index((10, 10, 10).into()), gate*sig);
|
|
state.step();
|
|
}
|
|
let (energy_0, energy_1) = energy_now_and_then(&mut state, 500);
|
|
// Default boundary conditions reflect waves.
|
|
assert_float_eq!(energy_1, energy_0, r2nd <= 1e-2);
|
|
}
|
|
|
|
/// Fill the world with the provided material and a stimulus.
|
|
/// Measure energy at the start, and then again after advancing many steps.
|
|
/// Return these two measurements (energy(t=0), energy(t=~=1000))
|
|
fn conductor_test<M: Into<mat::Static<R64>>>(mat: M) -> (f32, f32) {
|
|
let mut state = SimState::<R64, Static<R64>>::new(Index((201, 1, 1).into()), 1e-6);
|
|
state.fill_region(&WorldRegion, mat.into());
|
|
for t in 0..100 {
|
|
let progress = (t as f32)/100.0;
|
|
let angle = 2.0*progress*f32::two_pi();
|
|
let sig = angle.sin();
|
|
let gate = (progress*f32::pi()).sin();
|
|
state.impulse_ez(Index((100, 0, 0).into()), gate*sig);
|
|
state.step();
|
|
}
|
|
energy_now_and_then(&mut state, 1000)
|
|
}
|
|
|
|
#[test]
|
|
fn conductor_dissipates_energy() {
|
|
let (energy_0, energy_1) = conductor_test(Conductor::new(R64::from_f32(1e3)));
|
|
assert_float_eq!(energy_1/energy_0, 0.0, abs <= 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn anisotropic_conductor_inactive_x() {
|
|
let (energy_0, energy_1) = conductor_test(Conductor::new_anisotropic(
|
|
Vec3::new_x(1e3)
|
|
));
|
|
assert_gt!(energy_1, 0.9*energy_0);
|
|
// XXX: I think this gains energy because we only set the E field and not the H field
|
|
assert_lt!(energy_1, 1.5*energy_0);
|
|
}
|
|
|
|
#[test]
|
|
fn anisotropic_conductor_inactive_y() {
|
|
let (energy_0, energy_1) = conductor_test(Conductor::new_anisotropic(
|
|
Vec3::new_y(1e3)
|
|
));
|
|
assert_gt!(energy_1, 0.9*energy_0);
|
|
assert_lt!(energy_1, 1.5*energy_0);
|
|
}
|
|
|
|
#[test]
|
|
fn anisotropic_conductor_active_z() {
|
|
let (energy_0, energy_1) = conductor_test(Conductor::new_anisotropic(
|
|
Vec3::new_z(1e3)
|
|
));
|
|
assert_float_eq!(energy_1/energy_0, 0.0, abs <= 1e-6);
|
|
}
|
|
|
|
fn state_for_pml(size: Index) -> SimState<R64, Pml<R64>> {
|
|
let mut state = SimState::new(size, 1e-6);
|
|
let timestep = state.timestep();
|
|
state.fill_boundary_using(size/4, |boundary_ness| {
|
|
let b = boundary_ness.elem_pow(3.0);
|
|
let conductivity = b * (0.5 / timestep);
|
|
// K scales up to 11; dc_shift scales from 0.05:
|
|
// https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.461.4606&rep=rep1&type=pdf
|
|
// let propagation = Vec3::unit() + b * 10.0;
|
|
// let dc_shift = (Vec3::unit() - b) * 0.05;
|
|
//
|
|
// let propagation = Vec3::unit();
|
|
// let dc_shift = Vec3::unit() * 1e-6;
|
|
Pml::new(conductivity)
|
|
});
|
|
state
|
|
}
|
|
|
|
/// Like state_for_pml, but the boundary is an ordinary electric conductor -- not PML
|
|
fn state_for_graded_conductor(size: Index) -> SimState<R64, Conductor<R64>> {
|
|
let mut state = SimState::new(size, 1e-6);
|
|
let timestep = state.timestep();
|
|
state.fill_boundary_using(size/4, |boundary_ness| {
|
|
let b = boundary_ness.elem_pow(3.0);
|
|
let conductivity = f32::eps0() * b.mag() * 0.5 / timestep;
|
|
Conductor::new(conductivity.cast())
|
|
});
|
|
state
|
|
}
|
|
|
|
/// Stimulus which applies a (possibly different) e*sin(wt)*gate(t) at every point
|
|
struct PmlStim<F> {
|
|
/// Maps location -> (field direction, frequency)
|
|
f: F,
|
|
t_end: f32,
|
|
feat_size: f32,
|
|
sqrt_vol: f32,
|
|
}
|
|
impl<F: Fn(Index) -> (Vec3<f32>, f32) + Sync> AbstractStimulus for PmlStim<F> {
|
|
fn at(&self, t_sec: f32, pos: Meters) -> (Vec3<f32>, Vec3<f32>) {
|
|
let angle = t_sec/self.t_end * f32::two_pi();
|
|
let gate = 0.5*(1.0 - angle.cos());
|
|
let (e, hz) = (self.f)(pos.to_index(self.feat_size));
|
|
let sig_angle = angle*hz;
|
|
let sig = sig_angle.sin();
|
|
// TODO: test H component?
|
|
(e*(gate*sig / (self.t_end * self.sqrt_vol)), Vec3::zero())
|
|
}
|
|
}
|
|
|
|
/// Returns the energy ratio (Eend / Estart)
|
|
fn pml_test_full_interior<F, S: GenericSim>(state: &mut S, f: F) -> f32
|
|
where F: Fn(Index) -> (Vec3<f32>, f32) + Sync // returns (E vector, cycles (like Hz))
|
|
{
|
|
let stim = PmlStim {
|
|
f,
|
|
t_end: 50.0 * state.timestep(),
|
|
feat_size: state.feature_size(),
|
|
sqrt_vol: state.volume().sqrt(),
|
|
};
|
|
for _t in 0..50 {
|
|
// println!("estim({}) = {}", _t, energy(state));
|
|
state.step_multiple(1, &stim);
|
|
}
|
|
let (energy_0, energy_1) = energy_now_and_then(state, 1000);
|
|
// sanity check the starting energy
|
|
assert_gt!(energy_0, 1e-11);
|
|
assert_lt!(energy_0, 1.0);
|
|
energy_1 / energy_0
|
|
}
|
|
|
|
fn pml_test_over_region<R, F, S>(state: &mut S, reg: R, f: F) -> f32
|
|
where
|
|
R: Region,
|
|
F: Fn(Index) -> (Vec3<f32>, f32) + Sync,
|
|
S: GenericSim
|
|
{
|
|
let feat = state.feature_size();
|
|
pml_test_full_interior(state, |idx| {
|
|
if reg.contains(idx.to_meters(feat)) {
|
|
f(idx)
|
|
} else {
|
|
(Vec3::zero(), 0.0)
|
|
}
|
|
})
|
|
}
|
|
fn pml_test_uniform_over_region<R: Region, S: GenericSim>(state: &mut S, reg: R, e: Vec3<f32>, hz: f32) -> f32 {
|
|
pml_test_over_region(state, reg, |_idx| {
|
|
(e, hz)
|
|
})
|
|
}
|
|
|
|
fn pml_test_at<S: GenericSim>(state: &mut S, e: Vec3<f32>, center: Index) -> f32 {
|
|
pml_test_full_interior(state, |idx| {
|
|
if idx == center {
|
|
(e, 1.0)
|
|
} else {
|
|
(Vec3::zero(), 1.0)
|
|
}
|
|
})
|
|
}
|
|
|
|
fn pml_test<S: GenericSim, R: RangeBounds<f32> + Debug>(state: &mut S, e: Vec3<f32>, en_range: R) {
|
|
let center = Index(state.size().0 / 2);
|
|
let pml = pml_test_at(state, e, center);
|
|
// PML should absorb all energy
|
|
assert!(en_range.contains(&pml), "{} not in {:?}", pml, en_range);
|
|
}
|
|
|
|
fn pml_test_against_baseline<P: GenericSim, B: GenericSim>(pml_state: &mut P, baseline_state: &mut B, e: Vec3<f32>) {
|
|
assert_eq!(pml_state.size(), baseline_state.size());
|
|
let center = Index(pml_state.size().0 / 2);
|
|
let en_pml = pml_test_at(pml_state, e, center);
|
|
let en_baseline = pml_test_at(baseline_state, e, center);
|
|
// PML should absorb all energy
|
|
println!("en_pml: {}", en_pml);
|
|
println!("en_baseline: {}", en_baseline);
|
|
assert_float_eq!(en_pml, 0.0, abs <= 1.2e-6);
|
|
// XXX: why is PML energy sometimes MORE here?
|
|
assert_lt!(en_pml / en_baseline, 3.0, "{}/{}", en_pml, en_baseline);
|
|
// panic!("debugging");
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_conditions_x() {
|
|
let mut pml_state = state_for_pml(Index::new(201, 1, 1));
|
|
let mut baseline_state = state_for_graded_conductor(Index::new(201, 1, 1));
|
|
pml_test_against_baseline(&mut pml_state, &mut baseline_state, Vec3::unit_z());
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_conditions_y() {
|
|
let mut pml_state = state_for_pml(Index::new(1, 201, 1));
|
|
let mut baseline_state = state_for_graded_conductor(Index::new(1, 201, 1));
|
|
pml_test_against_baseline(&mut pml_state, &mut baseline_state, Vec3::unit_x());
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_conditions_z() {
|
|
let mut pml_state = state_for_pml(Index::new(1, 1, 201));
|
|
let mut baseline_state = state_for_graded_conductor(Index::new(1, 1, 201));
|
|
pml_test_against_baseline(&mut pml_state, &mut baseline_state, Vec3::unit_y());
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_conditions_plane_xy() {
|
|
let mut pml_state = state_for_pml(Index::new(201, 201, 1));
|
|
let mut baseline_state = state_for_graded_conductor(Index::new(201, 201, 1));
|
|
pml_test_against_baseline(&mut pml_state, &mut baseline_state, Vec3::unit_z());
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_conditions_plane_xz() {
|
|
let mut pml_state = state_for_pml(Index::new(201, 1, 201));
|
|
let mut baseline_state = state_for_graded_conductor(Index::new(201, 1, 201));
|
|
pml_test_against_baseline(&mut pml_state, &mut baseline_state, Vec3::unit_y());
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_conditions_plane_yz() {
|
|
let mut pml_state = state_for_pml(Index::new(1, 201, 201));
|
|
let mut baseline_state = state_for_graded_conductor(Index::new(1, 201, 201));
|
|
pml_test_against_baseline(&mut pml_state, &mut baseline_state, Vec3::unit_x());
|
|
}
|
|
|
|
fn state_for_monodirectional_pml(size: Index) -> SimState<R64, Pml<R64>> {
|
|
let mut state = SimState::new(size, 1e-6);
|
|
let timestep = state.timestep();
|
|
state.fill_boundary_using(size/4, |boundary_ness| {
|
|
let b = boundary_ness.elem_pow(3.0);
|
|
let coord_stretch = b * (0.5 / timestep);
|
|
// let coord_stretch = b * 0.5;
|
|
// Force the stretch to be only along one axis
|
|
let coord_stretch: Vec3<_> = match coord_stretch.to_tuple() {
|
|
(x, y, z) if x >= y && x >= z => (x, 0.0, 0.0),
|
|
(x, y, z) if y >= x && y >= z => (0.0, y, 0.0),
|
|
(x, y, z) if z >= x && z >= y => (0.0, 0.0, z),
|
|
_ => unreachable!(),
|
|
}.into();
|
|
Pml::new(coord_stretch)
|
|
});
|
|
state
|
|
}
|
|
|
|
/// Despite PML existing, verify that it doesn't interfere with a specific wave.
|
|
fn pml_ineffective_mono_linear_test<F: Fn(Vec3<f32>) -> Vec3<f32>>(
|
|
size: Index, e: Vec3<f32>, shuffle: F
|
|
) {
|
|
let mut state = SimState::<R64, Pml<R64>>::new(size, 1e-6);
|
|
let timestep = state.timestep();
|
|
state.fill_boundary_using(size/4, |boundary_ness| {
|
|
let b = boundary_ness.elem_pow(3.0);
|
|
let cs = b * (0.5 / timestep);
|
|
// let cs = b * 0.5;
|
|
// permute the stretching so that it shouldn't interfere with the wave
|
|
let coord_stretch = shuffle(cs);
|
|
Pml::<R64>::new(coord_stretch)
|
|
});
|
|
let center = Index(state.size().0 / 2);
|
|
let en = pml_test_at(&mut state, e, center);
|
|
// XXX: would be better to compare it to an empty sim?
|
|
assert_float_eq!(en, 1.0, abs <= 0.1);
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_3d_inneffective_monodirectional_linear() {
|
|
for (size, e) in vec![
|
|
(Index::new(201, 1, 1), Vec3::unit_y()),
|
|
(Index::new(201, 1, 1), Vec3::unit_z()),
|
|
(Index::new(1, 201, 1), Vec3::unit_x()),
|
|
(Index::new(1, 201, 1), Vec3::unit_z()),
|
|
(Index::new(1, 1, 201), Vec3::unit_x()),
|
|
(Index::new(1, 1, 201), Vec3::unit_y()),
|
|
] {
|
|
pml_ineffective_mono_linear_test(
|
|
size, e, |v| Vec3::new(v.y(), v.z(), v.x())
|
|
);
|
|
pml_ineffective_mono_linear_test(
|
|
size, e, |v| Vec3::new(v.z(), v.x(), v.y())
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_3d_monodirectional_plane_xy() {
|
|
//let mut state = state_for_monodirectional_pml(Index::unit()*41);
|
|
let mut state = state_for_monodirectional_pml(Index::new(201, 201, 1));
|
|
pml_test(&mut state, Vec3::unit_z(), ..7e-5);
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_3d_monodirectional_plane_xz() {
|
|
//let mut state = state_for_monodirectional_pml(Index::unit()*41);
|
|
let mut state = state_for_monodirectional_pml(Index::new(201, 1, 201));
|
|
pml_test(&mut state, Vec3::unit_y(), ..7e-5);
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_3d_monodirectional_plane_yz() {
|
|
//let mut state = state_for_monodirectional_pml(Index::unit()*41);
|
|
let mut state = state_for_monodirectional_pml(Index::new(1, 201, 201));
|
|
pml_test(&mut state, Vec3::unit_x(), ..7e-5);
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_3d_monodirectional_x() {
|
|
let mut state = state_for_monodirectional_pml(Index::unit()*41);
|
|
pml_test(&mut state, Vec3::unit_x(), 1e30..);
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_3d_monodirectional_y() {
|
|
let mut state = state_for_monodirectional_pml(Index::unit()*41);
|
|
pml_test(&mut state, Vec3::unit_y(), 1e30..);
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_3d_monodirectional_z() {
|
|
let mut state = state_for_monodirectional_pml(Index::unit()*41);
|
|
pml_test(&mut state, Vec3::unit_z(), 1e30..);
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_3d_multidirectional() {
|
|
for dir in vec![Vec3::unit_x(), Vec3::unit_y(), Vec3::unit_z()] {
|
|
let mut pml_state = state_for_pml(Index::unit()*41);
|
|
let mut baseline_state = state_for_graded_conductor(Index::unit()*41);
|
|
pml_test_against_baseline(&mut pml_state, &mut baseline_state, dir);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_3d_multidirectional_non_axis_aligned() {
|
|
for dir in vec![
|
|
Vec3::new(0.0, 1.0, 1.0),
|
|
Vec3::new(-1.0, 0.0, 1.0),
|
|
Vec3::new(1.0, -1.0, 0.0),
|
|
Vec3::new(1.0, -1.0, 1.0),
|
|
Vec3::new(-1.0, -1.0, -1.0),
|
|
Vec3::new(0.2, 0.5, 0.6),
|
|
Vec3::new(-2.0, 0.5, -0.5),
|
|
] {
|
|
let mut pml_state = state_for_pml(Index::unit()*41);
|
|
let mut baseline_state = state_for_graded_conductor(Index::unit()*41);
|
|
pml_test_against_baseline(&mut pml_state, &mut baseline_state, dir);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pml_boundary_3d_multidirectional_off_center() {
|
|
for dir in vec![Vec3::unit_x(), Vec3::unit_y(), Vec3::unit_z()] {
|
|
for center in vec![
|
|
Index::new(15, 15, 15),
|
|
Index::new(15, 15, 25),
|
|
Index::new(15, 25, 15),
|
|
Index::new(15, 25, 25),
|
|
Index::new(25, 15, 15),
|
|
Index::new(25, 15, 25),
|
|
Index::new(25, 25, 15),
|
|
Index::new(25, 25, 25),
|
|
] {
|
|
let mut pml_state = state_for_pml(Index::unit()*41);
|
|
let mut baseline_state = state_for_graded_conductor(Index::unit()*41);
|
|
let pml_en = pml_test_at(&mut pml_state, dir, center);
|
|
let baseline_en = pml_test_at(&mut baseline_state, dir, center);
|
|
assert_float_eq!(pml_en, 0.0, abs <= 2e-5);
|
|
// XXX: why is PML energy MORE here?
|
|
assert_lt!(pml_en / baseline_en, 2.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// XXX the energies here are even worse than above.
|
|
// just don't use our PML implementation: it's not great.
|
|
// #[test]
|
|
// fn pml_boundary_3d_multidirectional_off_center_multisource() {
|
|
// for dir in vec![Vec3::unit_x(), Vec3::unit_y(), Vec3::unit_z()] {
|
|
// for cycles in vec![1.0, 2.0, 4.0, 8.0] {
|
|
// let mut pml_state = state_for_pml(Index::unit()*41);
|
|
// let mut baseline_state = state_for_graded_conductor(Index::unit()*41);
|
|
// let feat = pml_state.feature_size();
|
|
// let interior = Cube::new(Index::new(15, 15, 15).to_meters(feat), Index::new(25, 25, 25).to_meters(feat));
|
|
// let pml_en = pml_test_uniform_over_region(&mut pml_state, interior, dir, cycles);
|
|
// let baseline_en = pml_test_uniform_over_region(&mut baseline_state, interior, dir, cycles);
|
|
// assert_float_eq!(pml_en, 0.0, abs <= 4e-5);
|
|
// // XXX: why is PML energy MORE here?
|
|
// assert_lt!(pml_en / baseline_en, 5.0, "{} {} {}/{}", dir, cycles, pml_en, baseline_en);
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
#[test]
|
|
fn pml_boundary_3d_chaotic_field() {
|
|
let excitation = |idx: Index| {
|
|
let seed = (idx.x() as u64) ^ ((idx.y() as u64) << 16) ^ ((idx.z() as u64) << 32);
|
|
let mut rng = StdRng::seed_from_u64(seed);
|
|
let dir = Vec3::new(
|
|
rng.gen_range(-1.0..1.0),
|
|
rng.gen_range(-1.0..1.0),
|
|
rng.gen_range(-1.0..1.0),
|
|
);
|
|
// XXX only works if it's a whole number. I suppose this makes sense though, as
|
|
// other numbers would create higher harmonics when gated.
|
|
let hz = rng.gen_range(0..=5);
|
|
(dir, hz as _)
|
|
};
|
|
let mut pml_state = state_for_pml(Index::unit()*41);
|
|
let mut baseline_state = state_for_graded_conductor(Index::unit()*41);
|
|
let feat = pml_state.feature_size();
|
|
let interior = Cube::new(Index::new(15, 15, 15).to_meters(feat), Index::new(25, 25, 25).to_meters(feat));
|
|
let en_pml = pml_test_over_region(&mut pml_state, interior, excitation);
|
|
let en_baseline = pml_test_over_region(&mut baseline_state, interior, excitation);
|
|
assert_float_eq!(en_pml, 0.0, abs <= 2e-5);
|
|
// XXX: why is PML energy MORE here?
|
|
assert_lt!(en_pml/en_baseline, 3.0);
|
|
}
|
|
}
|