use num_complex::Complex64;
use crate::error::OxiPhotonError;
const EPS0: f64 = 8.854_187_812_8e-12;
const MU0: f64 = 1.256_637_062_12e-6;
const C0: f64 = 299_792_458.0;
pub struct AnisotropicFdtd3d {
pub nx: usize,
pub ny: usize,
pub nz: usize,
pub dx: f64,
pub dy: f64,
pub dz: f64,
pub dt: f64,
pub ex: Vec<f64>,
pub ey: Vec<f64>,
pub ez: Vec<f64>,
pub hx: Vec<f64>,
pub hy: Vec<f64>,
pub hz: Vec<f64>,
pub eps_xx: Vec<f64>,
pub eps_yy: Vec<f64>,
pub eps_zz: Vec<f64>,
pub mu_xx: Vec<f64>,
pub mu_yy: Vec<f64>,
pub mu_zz: Vec<f64>,
pub sigma_e: Vec<f64>,
pub sigma_m: Vec<f64>,
pub time_step: usize,
ca_ex: Vec<f64>,
cb_ex: Vec<f64>,
ca_ey: Vec<f64>,
cb_ey: Vec<f64>,
ca_ez: Vec<f64>,
cb_ez: Vec<f64>,
da_hx: Vec<f64>,
db_hx: Vec<f64>,
da_hy: Vec<f64>,
db_hy: Vec<f64>,
da_hz: Vec<f64>,
db_hz: Vec<f64>,
coeffs_ready: bool,
}
impl AnisotropicFdtd3d {
pub fn new(nx: usize, ny: usize, nz: usize, dx: f64, dy: f64, dz: f64, dt: f64) -> Self {
let n = nx * ny * nz;
Self {
nx,
ny,
nz,
dx,
dy,
dz,
dt,
ex: vec![0.0; n],
ey: vec![0.0; n],
ez: vec![0.0; n],
hx: vec![0.0; n],
hy: vec![0.0; n],
hz: vec![0.0; n],
eps_xx: vec![1.0; n],
eps_yy: vec![1.0; n],
eps_zz: vec![1.0; n],
mu_xx: vec![1.0; n],
mu_yy: vec![1.0; n],
mu_zz: vec![1.0; n],
sigma_e: vec![0.0; n],
sigma_m: vec![0.0; n],
time_step: 0,
ca_ex: vec![0.0; n],
cb_ex: vec![0.0; n],
ca_ey: vec![0.0; n],
cb_ey: vec![0.0; n],
ca_ez: vec![0.0; n],
cb_ez: vec![0.0; n],
da_hx: vec![0.0; n],
db_hx: vec![0.0; n],
da_hy: vec![0.0; n],
db_hy: vec![0.0; n],
da_hz: vec![0.0; n],
db_hz: vec![0.0; n],
coeffs_ready: false,
}
}
#[inline(always)]
pub fn idx(&self, i: usize, j: usize, k: usize) -> usize {
k * (self.nx * self.ny) + j * self.nx + i
}
#[allow(clippy::too_many_arguments)]
pub fn fill_eps_box(
&mut self,
i0: usize,
i1: usize,
j0: usize,
j1: usize,
k0: usize,
k1: usize,
exx: f64,
eyy: f64,
ezz: f64,
) {
for k in k0..k1.min(self.nz) {
for j in j0..j1.min(self.ny) {
for i in i0..i1.min(self.nx) {
let idx = self.idx(i, j, k);
self.eps_xx[idx] = exx;
self.eps_yy[idx] = eyy;
self.eps_zz[idx] = ezz;
}
}
}
self.coeffs_ready = false;
}
#[allow(clippy::too_many_arguments)]
pub fn fill_mu_box(
&mut self,
i0: usize,
i1: usize,
j0: usize,
j1: usize,
k0: usize,
k1: usize,
mxx: f64,
myy: f64,
mzz: f64,
) {
for k in k0..k1.min(self.nz) {
for j in j0..j1.min(self.ny) {
for i in i0..i1.min(self.nx) {
let idx = self.idx(i, j, k);
self.mu_xx[idx] = mxx;
self.mu_yy[idx] = myy;
self.mu_zz[idx] = mzz;
}
}
}
self.coeffs_ready = false;
}
pub fn compute_coefficients(&mut self) {
let dt = self.dt;
let n = self.nx * self.ny * self.nz;
for p in 0..n {
let se = self.sigma_e[p];
let denom_ex = 1.0 + se * dt / (2.0 * EPS0 * self.eps_xx[p]);
self.ca_ex[p] = (1.0 - se * dt / (2.0 * EPS0 * self.eps_xx[p])) / denom_ex;
self.cb_ex[p] = (dt / (EPS0 * self.eps_xx[p])) / denom_ex;
let denom_ey = 1.0 + se * dt / (2.0 * EPS0 * self.eps_yy[p]);
self.ca_ey[p] = (1.0 - se * dt / (2.0 * EPS0 * self.eps_yy[p])) / denom_ey;
self.cb_ey[p] = (dt / (EPS0 * self.eps_yy[p])) / denom_ey;
let denom_ez = 1.0 + se * dt / (2.0 * EPS0 * self.eps_zz[p]);
self.ca_ez[p] = (1.0 - se * dt / (2.0 * EPS0 * self.eps_zz[p])) / denom_ez;
self.cb_ez[p] = (dt / (EPS0 * self.eps_zz[p])) / denom_ez;
let sm = self.sigma_m[p];
let denom_hx = 1.0 + sm * dt / (2.0 * MU0 * self.mu_xx[p]);
self.da_hx[p] = (1.0 - sm * dt / (2.0 * MU0 * self.mu_xx[p])) / denom_hx;
self.db_hx[p] = (dt / (MU0 * self.mu_xx[p])) / denom_hx;
let denom_hy = 1.0 + sm * dt / (2.0 * MU0 * self.mu_yy[p]);
self.da_hy[p] = (1.0 - sm * dt / (2.0 * MU0 * self.mu_yy[p])) / denom_hy;
self.db_hy[p] = (dt / (MU0 * self.mu_yy[p])) / denom_hy;
let denom_hz = 1.0 + sm * dt / (2.0 * MU0 * self.mu_zz[p]);
self.da_hz[p] = (1.0 - sm * dt / (2.0 * MU0 * self.mu_zz[p])) / denom_hz;
self.db_hz[p] = (dt / (MU0 * self.mu_zz[p])) / denom_hz;
}
self.coeffs_ready = true;
}
pub fn step_h(&mut self) {
if !self.coeffs_ready {
self.compute_coefficients();
}
let nx = self.nx;
let ny = self.ny;
let nz = self.nz;
let dx = self.dx;
let dy = self.dy;
let dz = self.dz;
for k in 0..nz.saturating_sub(1) {
for j in 0..ny.saturating_sub(1) {
for i in 0..nx.saturating_sub(1) {
let p = self.idx(i, j, k);
let dez_dy = (self.ez[self.idx(i, j + 1, k)] - self.ez[p]) / dy;
let dey_dz = (self.ey[self.idx(i, j, k + 1)] - self.ey[p]) / dz;
let dex_dz = (self.ex[self.idx(i, j, k + 1)] - self.ex[p]) / dz;
let dez_dx = (self.ez[self.idx(i + 1, j, k)] - self.ez[p]) / dx;
let dey_dx = (self.ey[self.idx(i + 1, j, k)] - self.ey[p]) / dx;
let dex_dy = (self.ex[self.idx(i, j + 1, k)] - self.ex[p]) / dy;
self.hx[p] = self.da_hx[p] * self.hx[p] - self.db_hx[p] * (dez_dy - dey_dz);
self.hy[p] = self.da_hy[p] * self.hy[p] - self.db_hy[p] * (dex_dz - dez_dx);
self.hz[p] = self.da_hz[p] * self.hz[p] - self.db_hz[p] * (dey_dx - dex_dy);
}
}
}
}
pub fn step_e(&mut self) {
if !self.coeffs_ready {
self.compute_coefficients();
}
let nx = self.nx;
let ny = self.ny;
let nz = self.nz;
let dx = self.dx;
let dy = self.dy;
let dz = self.dz;
for k in 1..nz {
for j in 1..ny {
for i in 1..nx {
let p = self.idx(i, j, k);
let dhz_dy = (self.hz[p] - self.hz[self.idx(i, j - 1, k)]) / dy;
let dhy_dz = (self.hy[p] - self.hy[self.idx(i, j, k - 1)]) / dz;
let dhx_dz = (self.hx[p] - self.hx[self.idx(i, j, k - 1)]) / dz;
let dhz_dx = (self.hz[p] - self.hz[self.idx(i - 1, j, k)]) / dx;
let dhy_dx = (self.hy[p] - self.hy[self.idx(i - 1, j, k)]) / dx;
let dhx_dy = (self.hx[p] - self.hx[self.idx(i, j - 1, k)]) / dy;
self.ex[p] = self.ca_ex[p] * self.ex[p] + self.cb_ex[p] * (dhz_dy - dhy_dz);
self.ey[p] = self.ca_ey[p] * self.ey[p] + self.cb_ey[p] * (dhx_dz - dhz_dx);
self.ez[p] = self.ca_ez[p] * self.ez[p] + self.cb_ez[p] * (dhy_dx - dhx_dy);
}
}
}
}
pub fn step(&mut self) {
self.step_h();
self.step_e();
self.time_step += 1;
}
pub fn set_point_source_ez(&mut self, i: usize, j: usize, k: usize, amplitude: f64) {
if i < self.nx && j < self.ny && k < self.nz {
let idx = self.idx(i, j, k);
self.ez[idx] += amplitude;
}
}
pub fn current_time(&self) -> f64 {
self.time_step as f64 * self.dt
}
}
#[derive(Debug, Clone)]
pub struct UniaxialCrystal {
pub no: f64,
pub ne: f64,
pub optic_axis: [f64; 3],
}
impl UniaxialCrystal {
pub fn new(no: f64, ne: f64, optic_axis: [f64; 3]) -> Result<Self, OxiPhotonError> {
if no <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"ordinary index no={no} must be positive"
)));
}
if ne <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"extraordinary index ne={ne} must be positive"
)));
}
let mag = (optic_axis[0].powi(2) + optic_axis[1].powi(2) + optic_axis[2].powi(2)).sqrt();
if mag < 1e-30 {
return Err(OxiPhotonError::NumericalError(
"optic_axis must be a non-zero vector".to_string(),
));
}
let axis = [
optic_axis[0] / mag,
optic_axis[1] / mag,
optic_axis[2] / mag,
];
Ok(Self {
no,
ne,
optic_axis: axis,
})
}
pub fn permittivity_tensor(&self) -> [[f64; 3]; 3] {
let [cx, cy, cz] = self.optic_axis;
let no2 = self.no * self.no;
let delta = self.ne * self.ne - no2;
[
[no2 + delta * cx * cx, delta * cx * cy, delta * cx * cz],
[delta * cy * cx, no2 + delta * cy * cy, delta * cy * cz],
[delta * cz * cx, delta * cz * cy, no2 + delta * cz * cz],
]
}
pub fn diagonal_eps(&self) -> [f64; 3] {
let t = self.permittivity_tensor();
[t[0][0], t[1][1], t[2][2]]
}
pub fn phase_velocity_ordinary(&self) -> f64 {
C0 / self.no
}
pub fn phase_velocity_extraordinary(&self) -> f64 {
C0 / self.ne
}
pub fn birefringence(&self) -> f64 {
(self.ne - self.no).abs()
}
}
#[derive(Debug, Clone)]
pub struct GyroelectricMedium {
pub eps_d: f64,
pub eps_g: f64,
pub g_vec: [f64; 3],
}
impl GyroelectricMedium {
pub fn new(eps_d: f64, eps_g: f64, g_vec: [f64; 3]) -> Result<Self, OxiPhotonError> {
let mag = (g_vec[0].powi(2) + g_vec[1].powi(2) + g_vec[2].powi(2)).sqrt();
if mag < 1e-30 {
return Err(OxiPhotonError::NumericalError(
"g_vec must be a non-zero vector".to_string(),
));
}
let gn = [g_vec[0] / mag, g_vec[1] / mag, g_vec[2] / mag];
Ok(Self {
eps_d,
eps_g,
g_vec: gn,
})
}
pub fn permittivity_tensor(&self) -> [[Complex64; 3]; 3] {
let eps_local: [[Complex64; 3]; 3] = [
[
Complex64::new(self.eps_d, 0.0),
Complex64::new(0.0, -self.eps_g),
Complex64::new(0.0, 0.0),
],
[
Complex64::new(0.0, self.eps_g),
Complex64::new(self.eps_d, 0.0),
Complex64::new(0.0, 0.0),
],
[
Complex64::new(0.0, 0.0),
Complex64::new(0.0, 0.0),
Complex64::new(self.eps_d, 0.0),
],
];
let r = rotation_z_to(&self.g_vec);
rotate_tensor_complex(&eps_local, &r)
}
pub fn circular_indices(&self) -> (f64, f64) {
let np = (self.eps_d + self.eps_g).abs().sqrt();
let nm = (self.eps_d - self.eps_g).abs().sqrt();
(np, nm)
}
pub fn faraday_rotation_rate(&self, omega: f64) -> f64 {
let (np, nm) = self.circular_indices();
omega / (2.0 * C0) * (np - nm)
}
pub fn verdet_constant(&self, omega: f64, b_field: f64) -> Result<f64, OxiPhotonError> {
if b_field.abs() < 1e-30 {
return Err(OxiPhotonError::NumericalError(
"b_field must be non-zero to compute Verdet constant".to_string(),
));
}
Ok(self.faraday_rotation_rate(omega) / b_field)
}
}
#[derive(Debug, Clone)]
pub struct DoublNegativeMedium {
pub eps_r: f64,
pub mu_r: f64,
}
impl DoublNegativeMedium {
pub fn new(eps_r: f64, mu_r: f64) -> Result<Self, OxiPhotonError> {
if eps_r >= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"eps_r={eps_r} must be negative for a double-negative medium"
)));
}
if mu_r >= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"mu_r={mu_r} must be negative for a double-negative medium"
)));
}
Ok(Self { eps_r, mu_r })
}
pub fn refractive_index(&self) -> f64 {
-(self.eps_r.abs() * self.mu_r.abs()).sqrt()
}
pub fn phase_velocity(&self) -> f64 {
C0 / self.refractive_index()
}
pub fn group_velocity(&self, omega: f64, domega: f64) -> f64 {
let n = self.refractive_index();
let k1 = omega * n / C0;
let k2 = (omega + domega) * n / C0;
let dk = k2 - k1;
if dk.abs() < 1e-30 {
(C0 / n).abs()
} else {
(domega / dk).abs()
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn fill_uniaxial_crystal(
fdtd: &mut AnisotropicFdtd3d,
crystal: &UniaxialCrystal,
i0: usize,
i1: usize,
j0: usize,
j1: usize,
k0: usize,
k1: usize,
) {
let [exx, eyy, ezz] = crystal.diagonal_eps();
fdtd.fill_eps_box(i0, i1, j0, j1, k0, k1, exx, eyy, ezz);
}
fn rotation_z_to(target: &[f64; 3]) -> [[f64; 3]; 3] {
let z = [0.0_f64, 0.0, 1.0];
let t = *target;
let kx = z[1] * t[2] - z[2] * t[1];
let ky = z[2] * t[0] - z[0] * t[2];
let kz = z[0] * t[1] - z[1] * t[0];
let sin_theta = (kx * kx + ky * ky + kz * kz).sqrt();
let cos_theta = z[0] * t[0] + z[1] * t[1] + z[2] * t[2];
if sin_theta < 1e-12 {
if cos_theta > 0.0 {
return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
} else {
return [[1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0]];
}
}
let kxn = kx / sin_theta;
let kyn = ky / sin_theta;
let kzn = kz / sin_theta;
let c = cos_theta;
let s = sin_theta;
let t1 = 1.0 - c;
[
[
c + kxn * kxn * t1,
kxn * kyn * t1 - kzn * s,
kxn * kzn * t1 + kyn * s,
],
[
kyn * kxn * t1 + kzn * s,
c + kyn * kyn * t1,
kyn * kzn * t1 - kxn * s,
],
[
kzn * kxn * t1 - kyn * s,
kzn * kyn * t1 + kxn * s,
c + kzn * kzn * t1,
],
]
}
fn rotate_tensor_complex(t: &[[Complex64; 3]; 3], r: &[[f64; 3]; 3]) -> [[Complex64; 3]; 3] {
let mut tmp = [[Complex64::new(0.0, 0.0); 3]; 3];
for row in 0..3 {
for col in 0..3 {
let mut sum = Complex64::new(0.0, 0.0);
for m in 0..3 {
sum += Complex64::new(r[row][m], 0.0) * t[m][col];
}
tmp[row][col] = sum;
}
}
let mut result = [[Complex64::new(0.0, 0.0); 3]; 3];
for row in 0..3 {
for col in 0..3 {
let mut sum = Complex64::new(0.0, 0.0);
for m in 0..3 {
sum += tmp[row][m] * Complex64::new(r[col][m], 0.0);
}
result[row][col] = sum;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
const APPROX_TOL: f64 = 1e-10;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_anisotropic_fdtd_creation() {
let fdtd = AnisotropicFdtd3d::new(10, 10, 10, 1e-9, 1e-9, 1e-9, 1e-18);
assert_eq!(fdtd.nx, 10);
assert_eq!(fdtd.ny, 10);
assert_eq!(fdtd.nz, 10);
let n = 10 * 10 * 10;
assert_eq!(fdtd.ex.len(), n);
assert_eq!(fdtd.hz.len(), n);
for &v in &fdtd.ex {
assert_eq!(v, 0.0);
}
for &v in &fdtd.ey {
assert_eq!(v, 0.0);
}
for &v in &fdtd.ez {
assert_eq!(v, 0.0);
}
for &v in &fdtd.hx {
assert_eq!(v, 0.0);
}
for &v in &fdtd.hy {
assert_eq!(v, 0.0);
}
for &v in &fdtd.hz {
assert_eq!(v, 0.0);
}
for &v in &fdtd.eps_xx {
assert_eq!(v, 1.0);
}
for &v in &fdtd.eps_yy {
assert_eq!(v, 1.0);
}
for &v in &fdtd.eps_zz {
assert_eq!(v, 1.0);
}
for &v in &fdtd.mu_xx {
assert_eq!(v, 1.0);
}
for &v in &fdtd.mu_yy {
assert_eq!(v, 1.0);
}
for &v in &fdtd.mu_zz {
assert_eq!(v, 1.0);
}
}
#[test]
fn test_uniaxial_crystal_tensor() {
let crystal = UniaxialCrystal::new(1.5, 1.7, [0.0, 0.0, 1.0]).expect("valid crystal");
let t = crystal.permittivity_tensor();
let no2 = 1.5_f64 * 1.5;
let ne2 = 1.7_f64 * 1.7;
assert!(
approx_eq(t[0][0], no2, APPROX_TOL),
"ε_xx = no² failed: {}",
t[0][0]
);
assert!(
approx_eq(t[1][1], no2, APPROX_TOL),
"ε_yy = no² failed: {}",
t[1][1]
);
assert!(
approx_eq(t[2][2], ne2, APPROX_TOL),
"ε_zz = ne² failed: {}",
t[2][2]
);
assert!(approx_eq(t[0][1], 0.0, APPROX_TOL));
assert!(approx_eq(t[0][2], 0.0, APPROX_TOL));
assert!(approx_eq(t[1][2], 0.0, APPROX_TOL));
let d = crystal.diagonal_eps();
assert!(approx_eq(d[0], no2, APPROX_TOL));
assert!(approx_eq(d[1], no2, APPROX_TOL));
assert!(approx_eq(d[2], ne2, APPROX_TOL));
}
#[test]
fn test_birefringence() {
let crystal = UniaxialCrystal::new(1.5, 1.7, [0.0, 0.0, 1.0]).expect("valid crystal");
assert!(approx_eq(crystal.birefringence(), 0.2, APPROX_TOL));
let crystal2 = UniaxialCrystal::new(1.7, 1.5, [0.0, 0.0, 1.0]).expect("valid crystal");
assert!(approx_eq(crystal2.birefringence(), 0.2, APPROX_TOL));
}
#[test]
fn test_gyroelectric_tensor() {
let medium = GyroelectricMedium::new(2.5, 0.3, [0.0, 0.0, 1.0]).expect("valid medium");
let t = medium.permittivity_tensor();
for (i, row) in t.iter().enumerate() {
for (j, _) in row.iter().enumerate() {
let diff = (t[i][j] - t[j][i].conj()).norm();
assert!(
diff < APPROX_TOL,
"Hermitian property violated at ({i},{j}): diff={diff}"
);
}
}
for (k, _) in t.iter().enumerate() {
assert!(approx_eq(t[k][k].re, 2.5, APPROX_TOL));
assert!(approx_eq(t[k][k].im, 0.0, APPROX_TOL));
}
assert!(approx_eq(t[0][1].re, 0.0, APPROX_TOL));
assert!(approx_eq(t[0][1].im, -0.3, APPROX_TOL));
assert!(approx_eq(t[1][0].re, 0.0, APPROX_TOL));
assert!(approx_eq(t[1][0].im, 0.3, APPROX_TOL));
}
#[test]
fn test_faraday_rotation_rate() {
let medium = GyroelectricMedium::new(2.5, 0.5, [0.0, 0.0, 1.0]).expect("valid medium");
let omega = 2.0 * std::f64::consts::PI * 3e14; let rate = medium.faraday_rotation_rate(omega);
assert!(
rate > 0.0,
"Faraday rotation rate must be positive for eps_g > 0: {rate}"
);
}
#[test]
fn test_double_negative_medium() {
let dng = DoublNegativeMedium::new(-2.0, -1.5).expect("valid DNG");
let n = dng.refractive_index();
assert!(n < 0.0, "Refractive index must be negative for DNG: {n}");
let expected_abs = (2.0_f64 * 1.5_f64).sqrt();
assert!(
approx_eq(n.abs(), expected_abs, APPROX_TOL),
"|n| = {}, expected {}",
n.abs(),
expected_abs
);
let vp = dng.phase_velocity();
assert!(vp < 0.0, "Phase velocity must be negative for DNG: {vp}");
let omega = 2.0 * std::f64::consts::PI * 3e14;
let vg = dng.group_velocity(omega, omega * 1e-6);
assert!(vg > 0.0, "Group velocity must be positive for DNG: {vg}");
assert!(DoublNegativeMedium::new(2.0, -1.5).is_err());
assert!(DoublNegativeMedium::new(-2.0, 1.5).is_err());
}
#[test]
fn test_fill_uniaxial_box() {
let mut fdtd = AnisotropicFdtd3d::new(20, 20, 20, 10e-9, 10e-9, 10e-9, 1e-17);
let p_before = fdtd.idx(10, 10, 10);
assert_eq!(fdtd.eps_xx[p_before], 1.0);
let crystal = UniaxialCrystal::new(1.5, 1.7, [0.0, 0.0, 1.0]).expect("valid crystal");
fill_uniaxial_crystal(&mut fdtd, &crystal, 5, 15, 5, 15, 5, 15);
let p_after = fdtd.idx(10, 10, 10);
let no2 = 1.5_f64 * 1.5_f64;
let ne2 = 1.7_f64 * 1.7_f64;
assert!(
approx_eq(fdtd.eps_xx[p_after], no2, APPROX_TOL),
"eps_xx should be no²={no2}, got {}",
fdtd.eps_xx[p_after]
);
assert!(
approx_eq(fdtd.eps_yy[p_after], no2, APPROX_TOL),
"eps_yy should be no²={no2}, got {}",
fdtd.eps_yy[p_after]
);
assert!(
approx_eq(fdtd.eps_zz[p_after], ne2, APPROX_TOL),
"eps_zz should be ne²={ne2}, got {}",
fdtd.eps_zz[p_after]
);
let p_out = fdtd.idx(0, 0, 0);
assert_eq!(fdtd.eps_xx[p_out], 1.0);
}
#[test]
fn test_anisotropic_step() {
let dx = 20e-9_f64; let dt = dx / (3.0_f64.sqrt() * C0) * 0.95;
let mut fdtd = AnisotropicFdtd3d::new(16, 16, 16, dx, dx, dx, dt);
let crystal = UniaxialCrystal::new(1.5, 1.7, [0.0, 0.0, 1.0]).expect("valid crystal");
fill_uniaxial_crystal(&mut fdtd, &crystal, 4, 12, 4, 12, 4, 12);
fdtd.set_point_source_ez(8, 8, 8, 1.0);
for _ in 0..10 {
fdtd.step();
}
assert_eq!(fdtd.time_step, 10);
for &v in fdtd.ez.iter().chain(&fdtd.ey).chain(&fdtd.ex) {
assert!(v.is_finite(), "Ez field contains non-finite value: {v}");
}
for &v in fdtd.hz.iter().chain(&fdtd.hy).chain(&fdtd.hx) {
assert!(v.is_finite(), "Hz field contains non-finite value: {v}");
}
let max_e: f64 = fdtd.ez.iter().cloned().fold(0.0_f64, f64::max);
assert!(
max_e > 0.0,
"Ez field should contain non-zero values after stepping"
);
}
}