use num_complex::Complex64;
use std::f64::consts::PI;
const C: f64 = 2.997_924_58e8;
pub struct NearToFarField2d {
pub nx: usize,
pub ny: usize,
pub dx: f64,
pub dy: f64,
pub i_min: usize,
pub i_max: usize,
pub j_min: usize,
pub j_max: usize,
pub omega: f64,
hz_bot: Vec<Complex64>,
ex_bot: Vec<Complex64>,
hz_top: Vec<Complex64>,
ex_top: Vec<Complex64>,
hz_left: Vec<Complex64>,
ey_left: Vec<Complex64>,
hz_right: Vec<Complex64>,
ey_right: Vec<Complex64>,
}
impl NearToFarField2d {
#[allow(clippy::too_many_arguments)]
pub fn new(
nx: usize,
ny: usize,
dx: f64,
dy: f64,
i_min: usize,
i_max: usize,
j_min: usize,
j_max: usize,
omega: f64,
) -> Self {
let ni = i_max - i_min + 1;
let nj = j_max - j_min + 1;
Self {
nx,
ny,
dx,
dy,
i_min,
i_max,
j_min,
j_max,
omega,
hz_bot: vec![Complex64::new(0.0, 0.0); ni],
ex_bot: vec![Complex64::new(0.0, 0.0); ni],
hz_top: vec![Complex64::new(0.0, 0.0); ni],
ex_top: vec![Complex64::new(0.0, 0.0); ni],
hz_left: vec![Complex64::new(0.0, 0.0); nj],
ey_left: vec![Complex64::new(0.0, 0.0); nj],
hz_right: vec![Complex64::new(0.0, 0.0); nj],
ey_right: vec![Complex64::new(0.0, 0.0); nj],
}
}
pub fn accumulate(&mut self, hz: &[f64], ex: &[f64], ey: &[f64], t: f64, dt: f64) {
let phase = Complex64::new(0.0, self.omega * t).exp() * dt;
let j = self.j_min;
for (k, i) in (self.i_min..=self.i_max).enumerate() {
if j < self.ny && i < self.nx {
self.hz_bot[k] += hz[j * self.nx + i] * phase;
}
if j < self.ny && i <= self.nx {
self.ex_bot[k] += ex[j * (self.nx + 1) + i] * phase;
}
}
let j = self.j_max;
for (k, i) in (self.i_min..=self.i_max).enumerate() {
if j < self.ny && i < self.nx {
self.hz_top[k] += hz[j * self.nx + i] * phase;
}
if j < self.ny && i <= self.nx {
self.ex_top[k] += ex[j * (self.nx + 1) + i] * phase;
}
}
let i = self.i_min;
for (k, j) in (self.j_min..=self.j_max).enumerate() {
if j < self.ny && i < self.nx {
self.hz_left[k] += hz[j * self.nx + i] * phase;
}
if j <= self.ny && i < self.nx {
self.ey_left[k] += ey[j * self.nx + i] * phase;
}
}
let i = self.i_max;
for (k, j) in (self.j_min..=self.j_max).enumerate() {
if j < self.ny && i < self.nx {
self.hz_right[k] += hz[j * self.nx + i] * phase;
}
if j <= self.ny && i < self.nx {
self.ey_right[k] += ey[j * self.nx + i] * phase;
}
}
}
pub fn radiation_pattern(&self, theta_deg_values: &[f64]) -> Vec<f64> {
let k0 = self.omega / C;
let ni = self.i_max - self.i_min + 1;
let nj = self.j_max - self.j_min + 1;
let cx = (self.i_min + self.i_max) as f64 / 2.0 * self.dx;
let cy = (self.j_min + self.j_max) as f64 / 2.0 * self.dy;
theta_deg_values
.iter()
.map(|&theta_deg| {
let theta = theta_deg * PI / 180.0;
let cos_t = theta.cos();
let sin_t = theta.sin();
let mut f = Complex64::new(0.0, 0.0);
let y_bot = self.j_min as f64 * self.dy - cy;
for k in 0..ni {
let x = (self.i_min + k) as f64 * self.dx - cx;
let r_dot = x * cos_t + y_bot * sin_t;
let ph = Complex64::new(0.0, k0 * r_dot).exp();
f += (self.ex_bot[k] - self.hz_bot[k]) * ph * self.dx;
}
let y_top = self.j_max as f64 * self.dy - cy;
for k in 0..ni {
let x = (self.i_min + k) as f64 * self.dx - cx;
let r_dot = x * cos_t + y_top * sin_t;
let ph = Complex64::new(0.0, k0 * r_dot).exp();
f += (-self.ex_top[k] + self.hz_top[k]) * ph * self.dx;
}
let x_left = self.i_min as f64 * self.dx - cx;
for k in 0..nj {
let y = (self.j_min + k) as f64 * self.dy - cy;
let r_dot = x_left * cos_t + y * sin_t;
let ph = Complex64::new(0.0, k0 * r_dot).exp();
f += (-self.ey_left[k] - self.hz_left[k]) * ph * self.dy;
}
let x_right = self.i_max as f64 * self.dx - cx;
for k in 0..nj {
let y = (self.j_min + k) as f64 * self.dy - cy;
let r_dot = x_right * cos_t + y * sin_t;
let ph = Complex64::new(0.0, k0 * r_dot).exp();
f += (self.ey_right[k] + self.hz_right[k]) * ph * self.dy;
}
f.norm_sqr()
})
.collect()
}
pub fn directivity(&self, n_angles: usize) -> f64 {
let thetas: Vec<f64> = (0..n_angles)
.map(|i| i as f64 / n_angles as f64 * 360.0)
.collect();
let pattern = self.radiation_pattern(&thetas);
let max_p = pattern.iter().cloned().fold(0.0_f64, f64::max);
let mean_p = pattern.iter().sum::<f64>() / n_angles as f64;
if mean_p > 0.0 {
max_p / mean_p
} else {
0.0
}
}
}
pub struct NearToFarField3d {
pub wavelength: f64,
pub i0: usize,
pub i1: usize,
pub j0: usize,
pub j1: usize,
pub k0: usize,
pub k1: usize,
pub omega: f64,
jx_bot: Vec<Complex64>,
jy_bot: Vec<Complex64>,
mx_bot: Vec<Complex64>,
my_bot: Vec<Complex64>,
jx_top: Vec<Complex64>,
jy_top: Vec<Complex64>,
mx_top: Vec<Complex64>,
my_top: Vec<Complex64>,
jx_fnt: Vec<Complex64>,
jz_fnt: Vec<Complex64>,
mx_fnt: Vec<Complex64>,
mz_fnt: Vec<Complex64>,
jx_bck: Vec<Complex64>,
jz_bck: Vec<Complex64>,
mx_bck: Vec<Complex64>,
mz_bck: Vec<Complex64>,
jy_lft: Vec<Complex64>,
jz_lft: Vec<Complex64>,
my_lft: Vec<Complex64>,
mz_lft: Vec<Complex64>,
jy_rgt: Vec<Complex64>,
jz_rgt: Vec<Complex64>,
my_rgt: Vec<Complex64>,
mz_rgt: Vec<Complex64>,
pub n_samples: usize,
pub dt: f64,
pub dx: f64,
pub dy: f64,
pub dz: f64,
pub nx: usize,
pub ny: usize,
pub nz: usize,
ni: usize,
nj: usize,
nk: usize,
}
impl NearToFarField3d {
#[allow(clippy::too_many_arguments)]
pub fn new(
i0: usize,
i1: usize,
j0: usize,
j1: usize,
k0: usize,
k1: usize,
omega: f64,
dt: f64,
dx: f64,
dy: f64,
dz: f64,
nx: usize,
ny: usize,
nz: usize,
) -> Self {
let wavelength = 2.0 * PI * C / omega;
let ni = i1.saturating_sub(i0) + 1;
let nj = j1.saturating_sub(j0) + 1;
let nk = k1.saturating_sub(k0) + 1;
let n_bot_top = ni * nj;
let n_fnt_bck = ni * nk;
let n_lft_rgt = nj * nk;
let zero_vec = |n: usize| vec![Complex64::new(0.0, 0.0); n];
Self {
wavelength,
i0,
i1,
j0,
j1,
k0,
k1,
omega,
jx_bot: zero_vec(n_bot_top),
jy_bot: zero_vec(n_bot_top),
mx_bot: zero_vec(n_bot_top),
my_bot: zero_vec(n_bot_top),
jx_top: zero_vec(n_bot_top),
jy_top: zero_vec(n_bot_top),
mx_top: zero_vec(n_bot_top),
my_top: zero_vec(n_bot_top),
jx_fnt: zero_vec(n_fnt_bck),
jz_fnt: zero_vec(n_fnt_bck),
mx_fnt: zero_vec(n_fnt_bck),
mz_fnt: zero_vec(n_fnt_bck),
jx_bck: zero_vec(n_fnt_bck),
jz_bck: zero_vec(n_fnt_bck),
mx_bck: zero_vec(n_fnt_bck),
mz_bck: zero_vec(n_fnt_bck),
jy_lft: zero_vec(n_lft_rgt),
jz_lft: zero_vec(n_lft_rgt),
my_lft: zero_vec(n_lft_rgt),
mz_lft: zero_vec(n_lft_rgt),
jy_rgt: zero_vec(n_lft_rgt),
jz_rgt: zero_vec(n_lft_rgt),
my_rgt: zero_vec(n_lft_rgt),
mz_rgt: zero_vec(n_lft_rgt),
n_samples: 0,
dt,
dx,
dy,
dz,
nx,
ny,
nz,
ni,
nj,
nk,
}
}
#[inline]
fn idx3(&self, i: usize, j: usize, k: usize) -> usize {
i * self.ny * self.nz + j * self.nz + k
}
#[allow(clippy::too_many_arguments)]
pub fn accumulate(
&mut self,
time_step: usize,
ex: &[f64],
ey: &[f64],
ez: &[f64],
hx: &[f64],
hy: &[f64],
hz: &[f64],
) {
let t = time_step as f64 * self.dt;
let phase = Complex64::new(0.0, self.omega * t).exp() * self.dt;
let get = |arr: &[f64], idx: usize| arr.get(idx).copied().unwrap_or(0.0);
let k = self.k0;
if k < self.nz {
for (ai, i) in (self.i0..=self.i1.min(self.nx.saturating_sub(1))).enumerate() {
for (aj, j) in (self.j0..=self.j1.min(self.ny.saturating_sub(1))).enumerate() {
let idx = self.idx3(i, j, k);
let cell = ai * self.nj + aj;
if cell < self.jx_bot.len() {
self.jx_bot[cell] += get(hy, idx) * phase;
self.jy_bot[cell] += -get(hx, idx) * phase;
self.mx_bot[cell] += -get(ey, idx) * phase;
self.my_bot[cell] += get(ex, idx) * phase;
}
}
}
}
let k = self.k1.min(self.nz.saturating_sub(1));
for (ai, i) in (self.i0..=self.i1.min(self.nx.saturating_sub(1))).enumerate() {
for (aj, j) in (self.j0..=self.j1.min(self.ny.saturating_sub(1))).enumerate() {
let idx = self.idx3(i, j, k);
let cell = ai * self.nj + aj;
if cell < self.jx_top.len() {
self.jx_top[cell] += -get(hy, idx) * phase;
self.jy_top[cell] += get(hx, idx) * phase;
self.mx_top[cell] += get(ey, idx) * phase;
self.my_top[cell] += -get(ex, idx) * phase;
}
}
}
let j = self.j0;
if j < self.ny {
for (ai, i) in (self.i0..=self.i1.min(self.nx.saturating_sub(1))).enumerate() {
for (ak, k) in (self.k0..=self.k1.min(self.nz.saturating_sub(1))).enumerate() {
let idx = self.idx3(i, j, k);
let cell = ai * self.nk + ak;
if cell < self.jx_fnt.len() {
self.jx_fnt[cell] += -get(hz, idx) * phase;
self.jz_fnt[cell] += get(hx, idx) * phase;
self.mx_fnt[cell] += get(ez, idx) * phase;
self.mz_fnt[cell] += -get(ex, idx) * phase;
}
}
}
}
let j = self.j1.min(self.ny.saturating_sub(1));
for (ai, i) in (self.i0..=self.i1.min(self.nx.saturating_sub(1))).enumerate() {
for (ak, k) in (self.k0..=self.k1.min(self.nz.saturating_sub(1))).enumerate() {
let idx = self.idx3(i, j, k);
let cell = ai * self.nk + ak;
if cell < self.jx_bck.len() {
self.jx_bck[cell] += get(hz, idx) * phase;
self.jz_bck[cell] += -get(hx, idx) * phase;
self.mx_bck[cell] += -get(ez, idx) * phase;
self.mz_bck[cell] += get(ex, idx) * phase;
}
}
}
let i = self.i0;
if i < self.nx {
for (aj, j) in (self.j0..=self.j1.min(self.ny.saturating_sub(1))).enumerate() {
for (ak, k) in (self.k0..=self.k1.min(self.nz.saturating_sub(1))).enumerate() {
let idx = self.idx3(i, j, k);
let cell = aj * self.nk + ak;
if cell < self.jy_lft.len() {
self.jy_lft[cell] += get(hz, idx) * phase;
self.jz_lft[cell] += -get(hy, idx) * phase;
self.my_lft[cell] += -get(ez, idx) * phase;
self.mz_lft[cell] += get(ey, idx) * phase;
}
}
}
}
let i = self.i1.min(self.nx.saturating_sub(1));
for (aj, j) in (self.j0..=self.j1.min(self.ny.saturating_sub(1))).enumerate() {
for (ak, k) in (self.k0..=self.k1.min(self.nz.saturating_sub(1))).enumerate() {
let idx = self.idx3(i, j, k);
let cell = aj * self.nk + ak;
if cell < self.jy_rgt.len() {
self.jy_rgt[cell] += -get(hz, idx) * phase;
self.jz_rgt[cell] += get(hy, idx) * phase;
self.my_rgt[cell] += get(ez, idx) * phase;
self.mz_rgt[cell] += -get(ey, idx) * phase;
}
}
}
self.n_samples += 1;
}
fn radiation_integrals(&self, theta: f64, phi: f64) -> ([Complex64; 2], [Complex64; 2]) {
let k0 = self.omega / C;
let sin_t = theta.sin();
let cos_t = theta.cos();
let sin_p = phi.sin();
let cos_p = phi.cos();
let rx = sin_t * cos_p;
let ry = sin_t * sin_p;
let rz = cos_t;
let cx = 0.5 * (self.i0 + self.i1) as f64 * self.dx;
let cy = 0.5 * (self.j0 + self.j1) as f64 * self.dy;
let cz = 0.5 * (self.k0 + self.k1) as f64 * self.dz;
let mut nx_c = Complex64::new(0.0, 0.0);
let mut ny_c = Complex64::new(0.0, 0.0);
let mut nz_c = Complex64::new(0.0, 0.0);
let mut lx_c = Complex64::new(0.0, 0.0);
let mut ly_c = Complex64::new(0.0, 0.0);
let mut lz_c = Complex64::new(0.0, 0.0);
let z = self.k0 as f64 * self.dz - cz;
let ds = self.dx * self.dy;
for (ai, i) in (self.i0..=self.i1.min(self.nx.saturating_sub(1))).enumerate() {
for (aj, j) in (self.j0..=self.j1.min(self.ny.saturating_sub(1))).enumerate() {
let x = i as f64 * self.dx - cx;
let y = j as f64 * self.dy - cy;
let r_dot = rx * x + ry * y + rz * z;
let ph = Complex64::new(0.0, k0 * r_dot).exp() * ds;
let cell = ai * self.nj + aj;
if cell < self.jx_bot.len() {
nx_c += self.jx_bot[cell] * ph;
ny_c += self.jy_bot[cell] * ph;
lx_c += self.mx_bot[cell] * ph;
ly_c += self.my_bot[cell] * ph;
}
}
}
let z = self.k1 as f64 * self.dz - cz;
for (ai, i) in (self.i0..=self.i1.min(self.nx.saturating_sub(1))).enumerate() {
for (aj, j) in (self.j0..=self.j1.min(self.ny.saturating_sub(1))).enumerate() {
let x = i as f64 * self.dx - cx;
let y = j as f64 * self.dy - cy;
let r_dot = rx * x + ry * y + rz * z;
let ph = Complex64::new(0.0, k0 * r_dot).exp() * ds;
let cell = ai * self.nj + aj;
if cell < self.jx_top.len() {
nx_c += self.jx_top[cell] * ph;
ny_c += self.jy_top[cell] * ph;
lx_c += self.mx_top[cell] * ph;
ly_c += self.my_top[cell] * ph;
}
}
}
let y = self.j0 as f64 * self.dy - cy;
let ds_fnt = self.dx * self.dz;
for (ai, i) in (self.i0..=self.i1.min(self.nx.saturating_sub(1))).enumerate() {
for (ak, k) in (self.k0..=self.k1.min(self.nz.saturating_sub(1))).enumerate() {
let x = i as f64 * self.dx - cx;
let z = k as f64 * self.dz - cz;
let r_dot = rx * x + ry * y + rz * z;
let ph = Complex64::new(0.0, k0 * r_dot).exp() * ds_fnt;
let cell = ai * self.nk + ak;
if cell < self.jx_fnt.len() {
nx_c += self.jx_fnt[cell] * ph;
nz_c += self.jz_fnt[cell] * ph;
lx_c += self.mx_fnt[cell] * ph;
lz_c += self.mz_fnt[cell] * ph;
}
}
}
let y = self.j1 as f64 * self.dy - cy;
for (ai, i) in (self.i0..=self.i1.min(self.nx.saturating_sub(1))).enumerate() {
for (ak, k) in (self.k0..=self.k1.min(self.nz.saturating_sub(1))).enumerate() {
let x = i as f64 * self.dx - cx;
let z = k as f64 * self.dz - cz;
let r_dot = rx * x + ry * y + rz * z;
let ph = Complex64::new(0.0, k0 * r_dot).exp() * ds_fnt;
let cell = ai * self.nk + ak;
if cell < self.jx_bck.len() {
nx_c += self.jx_bck[cell] * ph;
nz_c += self.jz_bck[cell] * ph;
lx_c += self.mx_bck[cell] * ph;
lz_c += self.mz_bck[cell] * ph;
}
}
}
let x = self.i0 as f64 * self.dx - cx;
let ds_lft = self.dy * self.dz;
for (aj, j) in (self.j0..=self.j1.min(self.ny.saturating_sub(1))).enumerate() {
for (ak, k) in (self.k0..=self.k1.min(self.nz.saturating_sub(1))).enumerate() {
let y = j as f64 * self.dy - cy;
let z = k as f64 * self.dz - cz;
let r_dot = rx * x + ry * y + rz * z;
let ph = Complex64::new(0.0, k0 * r_dot).exp() * ds_lft;
let cell = aj * self.nk + ak;
if cell < self.jy_lft.len() {
ny_c += self.jy_lft[cell] * ph;
nz_c += self.jz_lft[cell] * ph;
ly_c += self.my_lft[cell] * ph;
lz_c += self.mz_lft[cell] * ph;
}
}
}
let x = self.i1 as f64 * self.dx - cx;
for (aj, j) in (self.j0..=self.j1.min(self.ny.saturating_sub(1))).enumerate() {
for (ak, k) in (self.k0..=self.k1.min(self.nz.saturating_sub(1))).enumerate() {
let y = j as f64 * self.dy - cy;
let z = k as f64 * self.dz - cz;
let r_dot = rx * x + ry * y + rz * z;
let ph = Complex64::new(0.0, k0 * r_dot).exp() * ds_lft;
let cell = aj * self.nk + ak;
if cell < self.jy_rgt.len() {
ny_c += self.jy_rgt[cell] * ph;
nz_c += self.jz_rgt[cell] * ph;
ly_c += self.my_rgt[cell] * ph;
lz_c += self.mz_rgt[cell] * ph;
}
}
}
let n_theta = nx_c * (cos_t * cos_p) + ny_c * (cos_t * sin_p) + nz_c * (-sin_t);
let n_phi = nx_c * (-sin_p) + ny_c * cos_p;
let l_theta = lx_c * (cos_t * cos_p) + ly_c * (cos_t * sin_p) + lz_c * (-sin_t);
let l_phi = lx_c * (-sin_p) + ly_c * cos_p;
([n_theta, n_phi], [l_theta, l_phi])
}
pub fn far_field(&self, theta: f64, phi: f64, r: f64) -> ([f64; 2], [f64; 2]) {
let k0 = self.omega / C;
let eta0 = 376.730_313_461_77_f64;
let ([n_theta, n_phi], [l_theta, l_phi]) = self.radiation_integrals(theta, phi);
let j = Complex64::new(0.0, 1.0);
let factor = -j * k0 / (4.0 * PI * r);
let e_theta = factor * (l_phi + eta0 * n_theta);
let e_phi = factor * (-l_theta + eta0 * n_phi);
([e_theta.re, e_theta.im], [e_phi.re, e_phi.im])
}
pub fn directivity_pattern(&self, n_theta: usize, n_phi: usize) -> Vec<Vec<f64>> {
let r = 1.0_f64;
let mut power_grid = vec![vec![0.0_f64; n_phi]; n_theta];
let mut total_power = 0.0_f64;
for (it, theta_row) in power_grid.iter_mut().enumerate().take(n_theta) {
let theta = it as f64 / (n_theta - 1).max(1) as f64 * PI;
let sin_t = theta.sin();
for (ip, cell) in theta_row.iter_mut().enumerate().take(n_phi) {
let phi = ip as f64 / (n_phi - 1).max(1) as f64 * 2.0 * PI;
let ([et_re, et_im], [ep_re, ep_im]) = self.far_field(theta, phi, r);
let power = et_re * et_re + et_im * et_im + ep_re * ep_re + ep_im * ep_im;
*cell = power;
total_power += power * sin_t;
}
}
let d_theta = if n_theta > 1 {
PI / (n_theta - 1) as f64
} else {
1.0
};
let d_phi = if n_phi > 1 {
2.0 * PI / (n_phi - 1) as f64
} else {
1.0
};
let p_total = total_power * d_theta * d_phi;
let p_isotropic = p_total / (4.0 * PI);
power_grid
.iter()
.map(|row| {
row.iter()
.map(|&p| {
if p_isotropic > 1e-60 && p > 1e-60 {
10.0 * (p / p_isotropic).log10()
} else {
-999.0
}
})
.collect()
})
.collect()
}
pub fn total_radiated_power(&self) -> f64 {
let eta0 = 376.730_313_461_77_f64;
let mut total = 0.0_f64;
let sum_j_sq = |v: &[Complex64]| v.iter().map(|c| c.norm_sqr()).sum::<f64>();
total += sum_j_sq(&self.jx_bot) + sum_j_sq(&self.jy_bot);
total += sum_j_sq(&self.jx_top) + sum_j_sq(&self.jy_top);
total += sum_j_sq(&self.jx_fnt) + sum_j_sq(&self.jz_fnt);
total += sum_j_sq(&self.jx_bck) + sum_j_sq(&self.jz_bck);
total += sum_j_sq(&self.jy_lft) + sum_j_sq(&self.jz_lft);
total += sum_j_sq(&self.jy_rgt) + sum_j_sq(&self.jz_rgt);
total * eta0 / 2.0 * self.dx * self.dy * self.dz
}
pub fn face_sizes(&self) -> (usize, usize, usize) {
(self.ni * self.nj, self.ni * self.nk, self.nj * self.nk)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_ntff() -> NearToFarField2d {
let nx = 80;
let ny = 80;
let dx = 20e-9;
let dy = 20e-9;
NearToFarField2d::new(nx, ny, dx, dy, 15, 65, 15, 65, 2.0 * PI * 200e12)
}
#[test]
fn ntff_initializes_zero() {
let m = make_ntff();
assert!(m.hz_bot.iter().all(|v| v.norm() == 0.0));
assert!(m.ex_bot.iter().all(|v| v.norm() == 0.0));
}
#[test]
fn ntff_accumulate_does_not_panic() {
let mut m = make_ntff();
let nx = m.nx;
let ny = m.ny;
let hz = vec![0.0f64; nx * ny];
let ex = vec![0.0f64; (nx + 1) * ny];
let ey = vec![0.0f64; nx * (ny + 1)];
m.accumulate(&hz, &ex, &ey, 1e-15, 1e-17);
assert!(m.hz_bot.iter().all(|v| v.norm() == 0.0));
}
#[test]
fn ntff_radiation_pattern_returns_correct_size() {
let m = make_ntff();
let angles: Vec<f64> = (0..36).map(|i| i as f64 * 10.0).collect();
let pattern = m.radiation_pattern(&angles);
assert_eq!(pattern.len(), 36);
assert!(pattern.iter().all(|&v| v >= 0.0));
}
#[test]
fn ntff_zero_fields_give_zero_pattern() {
let m = make_ntff();
let angles = vec![0.0, 90.0, 180.0, 270.0];
let pattern = m.radiation_pattern(&angles);
for &p in &pattern {
assert!(
p.abs() < 1e-60,
"pattern should be ~0 with no field accumulated"
);
}
}
#[test]
fn ntff_3d_new_correct_dimensions() {
let ntff = NearToFarField3d::new(
5,
15,
5,
15,
5,
15,
2.0 * PI * 200e12,
1e-17,
20e-9,
20e-9,
20e-9,
20,
20,
20,
);
let (n_bt, n_fb, n_lr) = ntff.face_sizes();
assert_eq!(n_bt, 11 * 11, "Bottom/top face should be ni*nj");
assert_eq!(n_fb, 11 * 11, "Front/back face should be ni*nk");
assert_eq!(n_lr, 11 * 11, "Left/right face should be nj*nk");
assert_eq!(ntff.n_samples, 0);
}
#[test]
fn ntff_3d_accumulate_with_zero_fields() {
let mut ntff = NearToFarField3d::new(
5,
15,
5,
15,
5,
15,
2.0 * PI * 200e12,
1e-17,
20e-9,
20e-9,
20e-9,
20,
20,
20,
);
let n = 20 * 20 * 20;
let ex = vec![0.0f64; n];
let ey = vec![0.0f64; n];
let ez = vec![0.0f64; n];
let hx = vec![0.0f64; n];
let hy = vec![0.0f64; n];
let hz = vec![0.0f64; n];
ntff.accumulate(0, &ex, &ey, &ez, &hx, &hy, &hz);
assert_eq!(ntff.n_samples, 1);
assert!(ntff.jx_bot.iter().all(|c| c.norm() == 0.0));
}
#[test]
fn ntff_3d_accumulate_with_nonzero_fields() {
let mut ntff = NearToFarField3d::new(
3,
8,
3,
8,
3,
8,
2.0 * PI * 200e12,
1e-17,
20e-9,
20e-9,
20e-9,
12,
12,
12,
);
let n = 12 * 12 * 12;
let ex = vec![1.0f64; n];
let ey = vec![0.5f64; n];
let ez = vec![0.0f64; n];
let hx = vec![0.0f64; n];
let hy = vec![1.0f64; n];
let hz = vec![0.0f64; n];
ntff.accumulate(0, &ex, &ey, &ez, &hx, &hy, &hz);
let any_nonzero = ntff.jx_bot.iter().any(|c| c.norm() > 0.0)
|| ntff.jy_bot.iter().any(|c| c.norm() > 0.0)
|| ntff.mx_bot.iter().any(|c| c.norm() > 0.0);
assert!(
any_nonzero,
"Nonzero fields should produce nonzero surface currents"
);
}
#[test]
fn ntff_3d_far_field_returns_finite() {
let ntff = NearToFarField3d::new(
5,
15,
5,
15,
5,
15,
2.0 * PI * 200e12,
1e-17,
20e-9,
20e-9,
20e-9,
20,
20,
20,
);
let ([et_re, et_im], [ep_re, ep_im]) = ntff.far_field(PI / 2.0, 0.0, 1.0);
assert!(et_re.is_finite() && et_im.is_finite());
assert!(ep_re.is_finite() && ep_im.is_finite());
}
#[test]
fn ntff_3d_directivity_pattern_shape() {
let ntff = NearToFarField3d::new(
5,
10,
5,
10,
5,
10,
2.0 * PI * 200e12,
1e-17,
20e-9,
20e-9,
20e-9,
15,
15,
15,
);
let n_theta = 10;
let n_phi = 12;
let pattern = ntff.directivity_pattern(n_theta, n_phi);
assert_eq!(pattern.len(), n_theta, "Pattern should have n_theta rows");
assert_eq!(pattern[0].len(), n_phi, "Pattern should have n_phi columns");
}
#[test]
fn ntff_3d_total_radiated_power_nonnegative() {
let ntff = NearToFarField3d::new(
5,
10,
5,
10,
5,
10,
2.0 * PI * 200e12,
1e-17,
20e-9,
20e-9,
20e-9,
15,
15,
15,
);
let p = ntff.total_radiated_power();
assert!(p >= 0.0, "Total radiated power should be non-negative: {p}");
}
#[test]
fn ntff_3d_wavelength_computed_correctly() {
let omega = 2.0 * PI * 200e12;
let ntff = NearToFarField3d::new(
5, 10, 5, 10, 5, 10, omega, 1e-17, 20e-9, 20e-9, 20e-9, 15, 15, 15,
);
let expected_lambda = C / 200e12;
assert!(
(ntff.wavelength - expected_lambda).abs() < 1e-12,
"Wavelength mismatch: {} vs {}",
ntff.wavelength,
expected_lambda
);
}
}