use core::f64::consts::PI;
use crate::axis::Axis;
use crate::constants::{C0, Z0};
use crate::engine::{
DumpPosition, DumpQuantity, DumpRegion, FieldSampler, Fields, FreqDump, Grid, Hooks, Medium,
Operator, OperatorHandle, StepContext,
};
use crate::fdtd::Face;
use crate::post::Complex64;
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MirrorKind {
Pec,
Pmc,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Mirror {
pub axis: Axis,
pub position: f64,
pub kind: MirrorKind,
}
impl Mirror {
pub fn new(axis: Axis, position: f64, kind: MirrorKind) -> Self {
Self {
axis,
position,
kind,
}
}
}
#[derive(Debug, Clone)]
struct FaceDft {
face: Face,
e: FreqDump,
h: FreqDump,
}
#[derive(Debug, Clone)]
pub struct Nf2ff {
lo: [usize; 3],
hi: [usize; 3],
freqs: Vec<f64>,
faces: Vec<FaceDft>,
center: [f64; 3],
medium: Medium,
}
fn lossless(eps_r: f64, mu_r: f64) -> Result<Medium> {
let m = Medium::lossless(eps_r, mu_r);
m.validate().map_err(|_| {
Error::Invalid(format!(
"NF2FF: invalid background eps_r = {eps_r}, mu_r = {mu_r}"
))
})?;
Ok(m)
}
impl Nf2ff {
pub fn new(
grid: &Grid,
a: [usize; 3],
b: [usize; 3],
freqs: impl Into<Vec<f64>>,
) -> Result<Self> {
Self::with_faces(grid, a, b, freqs, [true; 6])
}
pub fn with_faces(
grid: &Grid,
a: [usize; 3],
b: [usize; 3],
freqs: impl Into<Vec<f64>>,
faces: [bool; 6],
) -> Result<Self> {
let freqs = freqs.into();
if freqs.is_empty() || freqs.iter().any(|f| !(f.is_finite() && *f > 0.0)) {
return Err(Error::Invalid(format!(
"NF2FF frequencies must be positive and finite (got {freqs:?})"
)));
}
let lo = [0, 1, 2].map(|i| a[i].min(b[i]));
let hi = [0, 1, 2].map(|i| a[i].max(b[i]));
let n = grid.nodes();
for ax in Axis::ALL {
let i = ax.index();
if lo[i] >= hi[i] {
return Err(Error::Invalid(format!(
"NF2FF box is flat along {ax} (nodes {} .. {})",
lo[i], hi[i]
)));
}
if hi[i] >= n[i] {
return Err(Error::Invalid(format!(
"NF2FF box node {} along {ax} outside the grid ({} nodes)",
hi[i], n[i]
)));
}
}
if !faces.contains(&true) {
return Err(Error::Invalid("NF2FF box records no face".into()));
}
let mut out = Vec::new();
for face in Face::ALL {
if !faces[face.index()] {
continue;
}
let ax = face.axis().index();
let plane = if face.is_max() { hi[ax] } else { lo[ax] };
if plane == 0 || plane + 1 >= n[ax] {
return Err(Error::Invalid(format!(
"NF2FF face {face:?} lies on the outer mesh plane {plane}; \
move it inside the domain (and outside any PML)"
)));
}
let (mut fa, mut fb) = (lo, hi);
fa[ax] = plane;
fb[ax] = plane;
let region = DumpRegion::new(fa, fb);
let dft = |q| -> Result<FreqDump> {
let s = FieldSampler::new(grid, region, q, DumpPosition::Nodes)?;
Ok(FreqDump::new(s, freqs.clone()))
};
out.push(FaceDft {
face,
e: dft(DumpQuantity::E)?,
h: dft(DumpQuantity::H)?,
});
}
let center = Axis::ALL.map(|ax| {
let l = grid.lines(ax);
0.5 * (l[lo[ax.index()]] + l[hi[ax.index()]])
});
Ok(Self {
lo,
hi,
freqs,
faces: out,
center,
medium: Medium::VACUUM,
})
}
pub fn with_background(mut self, eps_r: f64, mu_r: f64) -> Result<Self> {
self.medium = lossless(eps_r, mu_r)?;
Ok(self)
}
pub fn background(&self) -> Medium {
self.medium
}
pub fn interval(mut self, k: u64) -> Self {
for f in &mut self.faces {
f.e = f.e.clone().interval(k);
f.h = f.h.clone().interval(k);
}
self
}
pub fn lo(&self) -> [usize; 3] {
self.lo
}
pub fn hi(&self) -> [usize; 3] {
self.hi
}
pub fn freqs(&self) -> &[f64] {
&self.freqs
}
pub fn faces(&self) -> impl Iterator<Item = Face> + '_ {
self.faces.iter().map(|f| f.face)
}
pub fn samples(&self) -> u64 {
self.faces.first().map_or(0, |f| f.e.samples())
}
pub fn result(&self) -> Nf2ffResult {
let faces = self
.faces
.iter()
.map(|f| FaceData::new(f, self.freqs.len()))
.collect();
Nf2ffResult {
freqs: self.freqs.clone(),
center: self.center,
faces,
mirrors: Vec::new(),
medium: self.medium,
}
}
}
impl Operator for Nf2ff {
fn pre_h(&mut self, fields: &mut Fields<'_>, ctx: &StepContext<'_>) {
for f in &mut self.faces {
f.e.pre_h(fields, ctx);
f.h.pre_h(fields, ctx);
}
}
fn post_e(&mut self, fields: &mut Fields<'_>, ctx: &StepContext<'_>) {
for f in &mut self.faces {
f.e.post_e(fields, ctx);
f.h.post_e(fields, ctx);
}
}
fn hooks(&self) -> Hooks {
Hooks::PRE_H | Hooks::POST_E
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Nf2ffBox {
start: [f64; 3],
stop: [f64; 3],
freqs: Vec<f64>,
faces: [bool; 6],
mirrors: Vec<(Axis, f64, MirrorKind)>,
interval: Option<u64>,
background: Option<(f64, f64)>,
}
impl Nf2ffBox {
pub fn new(start: [f64; 3], stop: [f64; 3], freqs: impl Into<Vec<f64>>) -> Self {
Self {
start,
stop,
freqs: freqs.into(),
faces: [true; 6],
mirrors: Vec::new(),
interval: None,
background: None,
}
}
pub fn background(mut self, eps_r: f64, mu_r: f64) -> Self {
self.background = Some((eps_r, mu_r));
self
}
pub fn get_background(&self) -> Option<(f64, f64)> {
self.background
}
pub fn faces(mut self, faces: [bool; 6]) -> Self {
self.faces = faces;
self
}
pub fn without_face(mut self, face: Face) -> Self {
self.faces[face.index()] = false;
self
}
pub fn mirror(mut self, axis: Axis, position: f64, kind: MirrorKind) -> Self {
self.mirrors.push((axis, position, kind));
self
}
pub fn interval(mut self, k: u64) -> Self {
self.interval = Some(k.max(1));
self
}
pub fn corners(&self) -> ([f64; 3], [f64; 3]) {
(self.start, self.stop)
}
pub fn freqs(&self) -> &[f64] {
&self.freqs
}
pub(crate) fn build(
&self,
grid: &Grid,
unit: f64,
default_interval: u64,
) -> Result<(Nf2ff, Vec<Mirror>)> {
if grid.is_cylindrical() {
return Err(Error::Unsupported(
"NF2FF boxes need a Cartesian grid".into(),
));
}
let snap = |ax: Axis, x: f64| -> Result<usize> {
if !x.is_finite() {
return Err(Error::Invalid(format!(
"NF2FF box coordinate {x} along {ax}"
)));
}
let xm = x * unit;
let (i, _) = grid
.lines(ax)
.iter()
.enumerate()
.map(|(i, &v)| (i, (v - xm).abs()))
.fold((0, f64::INFINITY), |a, b| if b.1 < a.1 { b } else { a });
Ok(i)
};
let mut a = [0; 3];
let mut b = [0; 3];
for ax in Axis::ALL {
a[ax.index()] = snap(ax, self.start[ax.index()])?;
b[ax.index()] = snap(ax, self.stop[ax.index()])?;
}
let lo = [0, 1, 2].map(|i| a[i].min(b[i]));
let hi = [0, 1, 2].map(|i| a[i].max(b[i]));
let mut faces = self.faces;
let mut mirrors = Vec::new();
for &(ax, pos, kind) in &self.mirrors {
let p = pos * unit;
if !p.is_finite() {
return Err(Error::Invalid(format!("NF2FF mirror position {pos}")));
}
let l = grid.lines(ax);
let i = ax.index();
for (max, node) in [(false, lo[i]), (true, hi[i])] {
if (l[node] - p).abs() <= 1e-6 * grid.min_delta(ax) {
faces[Face::new(ax, max).index()] = false;
}
}
mirrors.push(Mirror::new(ax, p, kind));
}
let mut op = Nf2ff::with_faces(grid, lo, hi, self.freqs.clone(), faces)?
.interval(self.interval.unwrap_or(default_interval));
if let Some((e, m)) = self.background {
op = op.with_background(e, m)?;
}
Ok((op, mirrors))
}
}
#[derive(Debug, Clone)]
pub struct Nf2ffHandle {
pub(crate) op: OperatorHandle<Nf2ff>,
pub(crate) mirrors: Vec<Mirror>,
}
impl Nf2ffHandle {
pub fn operator(&self) -> OperatorHandle<Nf2ff> {
self.op
}
pub fn mirrors(&self) -> &[Mirror] {
&self.mirrors
}
}
#[derive(Debug, Clone)]
struct FaceData {
face: Face,
points: Vec<[f64; 3]>,
weights: Vec<f64>,
j: Vec<Vec<[Complex64; 3]>>,
m: Vec<Vec<[Complex64; 3]>>,
}
fn trapezoid(x: &[f64]) -> Vec<f64> {
let n = x.len();
(0..n)
.map(|i| {
let l = if i > 0 { x[i] - x[i - 1] } else { 0.0 };
let r = if i + 1 < n { x[i + 1] - x[i] } else { 0.0 };
0.5 * (l + r)
})
.collect()
}
fn cross(a: [Complex64; 3], b: [f64; 3]) -> [Complex64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn normal(face: Face) -> [f64; 3] {
let mut n = [0.0; 3];
n[face.axis().index()] = if face.is_max() { 1.0 } else { -1.0 };
n
}
impl FaceData {
fn new(f: &FaceDft, nf: usize) -> Self {
let s = f.e.sampler();
let c = Axis::ALL.map(|a| s.coords(a));
let w = Axis::ALL.map(|a| {
if a == f.face.axis() {
vec![1.0]
} else {
trapezoid(c[a.index()])
}
});
let [dx, dy, dz] = s.dims();
let mut points = Vec::with_capacity(s.len());
let mut weights = Vec::with_capacity(s.len());
for p in 0..dx {
for q in 0..dy {
for r in 0..dz {
points.push([c[0][p], c[1][q], c[2][r]]);
weights.push(w[0][p] * w[1][q] * w[2][r]);
}
}
}
let n = normal(f.face);
let neg_n = n.map(|x| -x);
let (j, m) = (0..nf)
.map(|k| {
let e = f.e.values(k);
let h = f.h.values(k);
let j = h.iter().map(|&h| cross(h, neg_n)).collect();
let m = e.iter().map(|&e| cross(e, n)).collect();
(j, m)
})
.unzip();
Self {
face: f.face,
points,
weights,
j,
m,
}
}
fn on_mirror(&self, mirrors: &[Mirror]) -> bool {
let ax = self.face.axis();
let x = self.points.first().map_or(f64::NAN, |p| p[ax.index()]);
mirrors.iter().any(|m| {
m.axis == ax
&& (x - m.position).abs() <= 1e-9 * x.abs().max(m.position.abs()).max(1e-30)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FarField {
pub theta: f64,
pub phi: f64,
pub e_theta: Complex64,
pub e_phi: Complex64,
}
impl FarField {
pub fn intensity(&self) -> f64 {
self.intensity_in(Z0)
}
pub fn intensity_in(&self, eta: f64) -> f64 {
(self.e_theta.norm_sqr() + self.e_phi.norm_sqr()) / (2.0 * eta)
}
pub fn at_radius(&self, k: f64, r: f64) -> [Complex64; 2] {
let p = Complex64::cis(-k * r) / r;
[self.e_theta * p, self.e_phi * p]
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MaxDirectivity {
pub directivity: f64,
pub theta: f64,
pub phi: f64,
}
impl MaxDirectivity {
pub fn dbi(&self) -> f64 {
10.0 * self.directivity.log10()
}
}
#[derive(Debug, Clone)]
pub struct Nf2ffResult {
freqs: Vec<f64>,
center: [f64; 3],
faces: Vec<FaceData>,
mirrors: Vec<Mirror>,
medium: Medium,
}
fn basis(theta: f64, phi: f64) -> ([f64; 3], [f64; 3], [f64; 3]) {
let (st, ct) = theta.sin_cos();
let (sp, cp) = phi.sin_cos();
(
[st * cp, st * sp, ct],
[ct * cp, ct * sp, -st],
[-sp, cp, 0.0],
)
}
fn dot(a: [Complex64; 3], b: [f64; 3]) -> Complex64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
impl Nf2ffResult {
pub fn freqs(&self) -> &[f64] {
&self.freqs
}
pub fn wavenumber(&self, k: usize) -> f64 {
2.0 * PI * self.freqs[k] * (self.medium.eps_r * self.medium.mu_r).sqrt() / C0
}
pub fn wave_impedance(&self) -> f64 {
self.medium.impedance()
}
pub fn background(&self) -> Medium {
self.medium
}
pub fn with_background(mut self, eps_r: f64, mu_r: f64) -> Result<Self> {
self.medium = lossless(eps_r, mu_r)?;
Ok(self)
}
pub fn center(&self) -> [f64; 3] {
self.center
}
pub fn with_center(mut self, c: [f64; 3]) -> Self {
self.center = c;
self
}
pub fn mirrors(&self) -> &[Mirror] {
&self.mirrors
}
pub fn with_mirror(mut self, m: Mirror) -> Result<Self> {
if self.mirrors.iter().any(|o| o.axis == m.axis) {
return Err(Error::Invalid(format!(
"NF2FF: a mirror normal to {} is already set",
m.axis
)));
}
let i = m.axis.index();
let (lo, hi) = self
.faces
.iter()
.flat_map(|f| f.points.iter().map(move |p| p[i]))
.fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), x| {
(a.min(x), b.max(x))
});
let tol = 1e-9 * lo.abs().max(hi.abs()).max(1e-30);
if m.position > lo + tol && m.position < hi - tol {
return Err(Error::Invalid(format!(
"NF2FF mirror plane {} = {} m cuts the box ({lo} .. {hi} m)",
m.axis, m.position
)));
}
self.mirrors.push(m);
Ok(self)
}
fn active(&self) -> impl Iterator<Item = &FaceData> {
self.faces.iter().filter(|f| !f.on_mirror(&self.mirrors))
}
fn is_real_side(&self, r: [f64; 3]) -> bool {
self.mirrors.iter().all(|m| {
let i = m.axis.index();
let side = self.center[i] - m.position;
r[i] * side >= -1e-12
})
}
fn radiation_vectors(&self, k: usize, r: [f64; 3]) -> ([Complex64; 3], [Complex64; 3]) {
let kw = self.wavenumber(k);
let c = self.center;
let mut n = [Complex64::ZERO; 3];
let mut l = [Complex64::ZERO; 3];
let nm = self.mirrors.len();
for mask in 0..(1usize << nm) {
let mut refl = [None::<f64>; 3];
let mut sj = [1.0f64; 3];
let mut sm = [1.0f64; 3];
for (b, m) in self.mirrors.iter().enumerate() {
if mask & (1 << b) == 0 {
continue;
}
let a = m.axis.index();
refl[a] = Some(m.position);
for t in 0..3 {
let tangential = t != a;
let (fj, fm) = match (m.kind, tangential) {
(MirrorKind::Pec, true) => (-1.0, 1.0),
(MirrorKind::Pec, false) => (1.0, -1.0),
(MirrorKind::Pmc, true) => (1.0, -1.0),
(MirrorKind::Pmc, false) => (-1.0, 1.0),
};
sj[t] *= fj;
sm[t] *= fm;
}
}
for f in self.active() {
let (jk, mk) = (&f.j[k], &f.m[k]);
for (q, p) in f.points.iter().enumerate() {
let mut ph = 0.0;
for a in 0..3 {
let x = match refl[a] {
Some(x0) => 2.0 * x0 - p[a],
None => p[a],
};
ph += r[a] * (x - c[a]);
}
let e = Complex64::cis(kw * ph) * f.weights[q];
for a in 0..3 {
n[a] += jk[q][a] * (e * sj[a]);
l[a] += mk[q][a] * (e * sm[a]);
}
}
}
}
(n, l)
}
pub fn far_field(&self, k: usize, theta: f64, phi: f64) -> FarField {
let (r, t, p) = basis(theta, phi);
let (n, l) = self.radiation_vectors(k, r);
let kw = self.wavenumber(k);
let eta = self.wave_impedance();
let a = Complex64::new(0.0, kw / (4.0 * PI));
let (nt, np) = (dot(n, t), dot(n, p));
let (lt, lp) = (dot(l, t), dot(l, p));
FarField {
theta,
phi,
e_theta: -(a * (lp + nt * eta)),
e_phi: a * (lt - np * eta),
}
}
pub fn pattern(&self, k: usize, dirs: &[(f64, f64)]) -> Vec<FarField> {
dirs.iter().map(|&(t, p)| self.far_field(k, t, p)).collect()
}
pub fn cut_theta(&self, k: usize, phi: f64, thetas: &[f64]) -> Vec<FarField> {
thetas.iter().map(|&t| self.far_field(k, t, phi)).collect()
}
pub fn cut_phi(&self, k: usize, theta: f64, phis: &[f64]) -> Vec<FarField> {
phis.iter().map(|&p| self.far_field(k, theta, p)).collect()
}
pub fn radiation_intensity(&self, k: usize, theta: f64, phi: f64) -> f64 {
self.far_field(k, theta, phi)
.intensity_in(self.wave_impedance())
}
pub fn radiated_power(&self, k: usize) -> f64 {
let mut p = 0.0;
for f in self.active() {
let n = normal(f.face);
for (q, w) in f.weights.iter().enumerate() {
let m = f.m[k][q];
let et = cross(m, n).map(|x| -x);
let j = f.j[k][q];
let s: f64 = (0..3).map(|a| (et[a] * j[a].conj()).re).sum();
p -= w * s;
}
}
0.5 * p
}
pub fn radiated_power_far(&self, k: usize, n_theta: usize, n_phi: usize) -> f64 {
let (nt, np) = (n_theta.max(1), n_phi.max(1));
let (dt, dp) = (PI / nt as f64, 2.0 * PI / np as f64);
let mut s = 0.0;
for a in 0..nt {
let t = (a as f64 + 0.5) * dt;
for b in 0..np {
let p = (b as f64 + 0.5) * dp;
s += self.radiation_intensity(k, t, p) * t.sin();
}
}
s * dt * dp / (1usize << self.mirrors.len()) as f64
}
pub fn directivity(&self, k: usize, theta: f64, phi: f64) -> f64 {
4.0 * PI * self.radiation_intensity(k, theta, phi) / self.radiated_power(k)
}
pub fn gain(&self, k: usize, theta: f64, phi: f64, accepted_power: f64) -> f64 {
4.0 * PI * self.radiation_intensity(k, theta, phi) / accepted_power
}
pub fn max_directivity(&self, k: usize) -> MaxDirectivity {
let p_rad = self.radiated_power(k);
let mut best = (f64::NEG_INFINITY, 0.0, 0.0);
let eval = |t: f64, p: f64, best: &mut (f64, f64, f64)| {
let t = t.clamp(0.0, PI);
if !self.is_real_side(basis(t, p).0) {
return;
}
let u = self.radiation_intensity(k, t, p);
if u > best.0 {
*best = (u, t, p);
}
};
let step = 5f64.to_radians();
for a in 0..=36 {
for b in 0..72 {
eval(a as f64 * step, b as f64 * step, &mut best);
}
}
for scale in [0.1, 0.01] {
let s = step * scale;
let (_, t0, p0) = best;
for a in -10..=10 {
for b in -10..=10 {
eval(t0 + a as f64 * s, p0 + b as f64 * s, &mut best);
}
}
}
MaxDirectivity {
directivity: 4.0 * PI * best.0 / p_rad,
theta: best.1,
phi: best.2.rem_euclid(2.0 * PI),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trapezoid_weights() {
let w = trapezoid(&[0.0, 1.0, 3.0]);
assert_eq!(w, vec![0.5, 1.5, 1.0]);
assert_eq!(w.iter().sum::<f64>(), 3.0);
}
#[test]
fn invalid_boxes() {
let g = Grid::uniform([10, 10, 10], [1e-3; 3]).unwrap();
assert!(Nf2ff::new(&g, [2, 2, 2], [8, 8, 8], [1e9]).is_ok());
assert!(Nf2ff::new(&g, [2, 2, 2], [8, 8, 2], [1e9]).is_err());
assert!(Nf2ff::new(&g, [0, 2, 2], [8, 8, 8], [1e9]).is_err());
assert!(Nf2ff::new(&g, [2, 2, 2], [8, 8, 10], [1e9]).is_err());
assert!(Nf2ff::new(&g, [2, 2, 2], [8, 8, 11], [1e9]).is_err());
assert!(Nf2ff::new(&g, [2, 2, 2], [8, 8, 8], Vec::new()).is_err());
assert!(Nf2ff::new(&g, [2, 2, 2], [8, 8, 8], [-1.0]).is_err());
assert!(Nf2ff::with_faces(&g, [2, 2, 2], [8, 8, 8], [1e9], [false; 6]).is_err());
let mut f = [true; 6];
f[Face::ZMin.index()] = false;
assert!(Nf2ff::with_faces(&g, [2, 2, 0], [8, 8, 8], [1e9], f).is_ok());
}
#[test]
fn analytic_hertzian_dipole() {
let f = 1e9;
let kw = 2.0 * PI * f / C0;
let fields = |p: [f64; 3]| -> ([Complex64; 3], [Complex64; 3]) {
let r = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
let (st, ct) = ((p[0] * p[0] + p[1] * p[1]).sqrt() / r, p[2] / r);
let phi = p[1].atan2(p[0]);
let j = Complex64::new(0.0, 1.0);
let g = Complex64::cis(-kw * r);
let kr = Complex64::new(0.0, kw * r);
let er = g * (Z0 * ct / (2.0 * PI * r * r)) * (1.0 + 1.0 / kr);
let base = g * j * (Z0 * kw * st / (4.0 * PI * r));
let et = base * (1.0 + 1.0 / kr) - base * (1.0 / (kw * r * kw * r));
let hp = g * j * (kw * st / (4.0 * PI * r)) * (1.0 + 1.0 / kr);
let (sp, cp) = phi.sin_cos();
let rh = [st * cp, st * sp, ct];
let th = [ct * cp, ct * sp, -st];
let ph = [-sp, cp, 0.0];
let e = [0, 1, 2].map(|a| er * rh[a] + et * th[a]);
let h = [0, 1, 2].map(|a| hp * ph[a]);
(e, h)
};
let n = 41;
let xs: Vec<f64> = (0..n)
.map(|i| -0.2 + 0.4 * i as f64 / (n - 1) as f64)
.collect();
let mut faces = Vec::new();
for face in Face::ALL {
let ax = face.axis().index();
let x0 = if face.is_max() { 0.2 } else { -0.2 };
let (a1, a2) = ((ax + 1) % 3, (ax + 2) % 3);
let wt = trapezoid(&xs);
let mut fd = FaceData {
face,
points: Vec::new(),
weights: Vec::new(),
j: vec![Vec::new()],
m: vec![Vec::new()],
};
let nn = normal(face);
for (p, &u) in xs.iter().enumerate() {
for (q, &v) in xs.iter().enumerate() {
let mut pt = [0.0; 3];
pt[ax] = x0;
pt[a1] = u;
pt[a2] = v;
let (e, h) = fields(pt);
fd.points.push(pt);
fd.weights.push(wt[p] * wt[q]);
fd.j[0].push(cross(h, nn.map(|x| -x)));
fd.m[0].push(cross(e, nn));
}
}
faces.push(fd);
}
let res = Nf2ffResult {
freqs: vec![f],
center: [0.0; 3],
faces,
mirrors: Vec::new(),
medium: Medium::VACUUM,
};
let p_exact = Z0 * kw * kw / (12.0 * PI);
let p_box = res.radiated_power(0);
assert!((p_box / p_exact - 1.0).abs() < 2e-3, "{p_box} vs {p_exact}");
let p_far = res.radiated_power_far(0, 45, 8);
assert!((p_far / p_exact - 1.0).abs() < 2e-3, "{p_far} vs {p_exact}");
for &t in &[0.3, 0.9, PI / 2.0, 2.2] {
let ff = res.far_field(0, t, 0.7);
let want = Complex64::new(0.0, Z0 * kw * t.sin() / (4.0 * PI));
assert!(
(ff.e_theta - want).norm() < 2e-3 * want.norm(),
"{t}: {:?} vs {want}",
ff.e_theta
);
assert!(ff.e_phi.norm() < 1e-3 * want.norm());
}
let d = res.max_directivity(0);
assert!((d.directivity - 1.5).abs() < 5e-3, "{d:?}");
assert!((d.theta - PI / 2.0).abs() < 1e-2, "{d:?}");
}
}