use std::f64::consts::PI;
use lox_core::units::{Angle, Decibel, Distance, Frequency};
use crate::antenna::AntennaGain;
const DIV_BY_ZERO_LIMIT: f64 = 1e-6;
const MINF_GAIN_LINEAR: f64 = 1e-12;
const SHORT_DIPOLE_LIMIT: f64 = 0.1;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DipolePattern {
pub length: Distance,
}
impl DipolePattern {
pub fn new(length: Distance) -> Self {
Self { length }
}
pub fn gain_pattern(&self, frequency: Frequency, angle: Angle) -> f64 {
let wavelength_m = frequency.wavelength().to_meters();
let l = self.length.to_meters();
let theta = angle.to_radians();
let sin_theta = theta.sin();
if sin_theta.abs() < DIV_BY_ZERO_LIMIT {
return MINF_GAIN_LINEAR;
}
if l / wavelength_m < SHORT_DIPOLE_LIMIT {
sin_theta * sin_theta
} else {
let k = 2.0 * PI / wavelength_m;
let half_kl = k * l / 2.0;
let numerator = (half_kl * theta.cos()).cos() - half_kl.cos();
(numerator / sin_theta).powi(2)
}
}
pub fn directivity(&self, frequency: Frequency) -> f64 {
let n = 1000;
let integral = simpsons_rule(0.0, PI, n, |theta| {
let angle = Angle::radians(theta);
self.gain_pattern(frequency, angle) * theta.sin()
});
2.0 / integral
}
pub fn peak_gain(&self, frequency: Frequency) -> Decibel {
let d = self.directivity(frequency);
let (_, max_pattern) = golden_section_max(0.0, PI, |theta| {
self.gain_pattern(frequency, Angle::radians(theta))
});
Decibel::from_linear(d * max_pattern)
}
}
impl AntennaGain for DipolePattern {
fn gain(&self, frequency: Frequency, angle: Angle) -> Decibel {
let d = self.directivity(frequency);
let pattern = self.gain_pattern(frequency, angle);
let gain_linear = d * pattern;
if gain_linear < MINF_GAIN_LINEAR {
Decibel::from_linear(MINF_GAIN_LINEAR)
} else {
Decibel::from_linear(gain_linear)
}
}
fn beamwidth(&self, _frequency: Frequency) -> Option<Angle> {
None
}
}
fn simpsons_rule(a: f64, b: f64, n: usize, f: impl Fn(f64) -> f64) -> f64 {
debug_assert!(
n.is_multiple_of(2),
"Simpson's rule requires an even number of intervals"
);
let h = (b - a) / n as f64;
let mut sum = f(a) + f(b);
for i in 1..n {
let x = a + i as f64 * h;
let weight = if i % 2 == 0 { 2.0 } else { 4.0 };
sum += weight * f(x);
}
sum * h / 3.0
}
fn golden_section_max(a: f64, b: f64, f: impl Fn(f64) -> f64) -> (f64, f64) {
let phi = (5.0_f64.sqrt() - 1.0) / 2.0; let tol = 1e-12;
let mut a = a;
let mut b = b;
let mut c = b - phi * (b - a);
let mut d = a + phi * (b - a);
let mut fc = f(c);
let mut fd = f(d);
while (b - a).abs() > tol {
if fc < fd {
a = c;
c = d;
fc = fd;
d = a + phi * (b - a);
fd = f(d);
} else {
b = d;
d = c;
fd = fc;
c = b - phi * (b - a);
fc = f(c);
}
}
let x = (a + b) / 2.0;
(x, f(x))
}
#[cfg(test)]
mod tests {
use lox_core::units::FrequencyUnits;
use lox_test_utils::assert_approx_eq;
use super::*;
fn test_frequency() -> Frequency {
29.0.ghz()
}
fn half_wave_dipole() -> DipolePattern {
let wavelength = test_frequency().wavelength();
DipolePattern::new(Distance::meters(wavelength.to_meters() / 2.0))
}
#[test]
fn test_half_wave_dipole_at_zero() {
let d = half_wave_dipole();
let gain = d.gain(test_frequency(), Angle::radians(0.0));
assert!(gain.as_f64() < -50.0);
}
#[test]
fn test_half_wave_dipole_at_broadside() {
let d = half_wave_dipole();
let gain = d.gain(test_frequency(), Angle::radians(PI / 2.0));
assert_approx_eq!(gain.as_f64(), 2.15, atol <= 0.01);
}
#[test]
fn test_short_dipole_peak() {
let wavelength = test_frequency().wavelength().to_meters();
let d = DipolePattern::new(Distance::meters(wavelength / 100.0));
let peak = d.peak_gain(test_frequency());
assert_approx_eq!(peak.as_f64(), 1.76, atol <= 0.1);
}
#[test]
fn test_1_25_wavelength_dipole_peak() {
let wavelength = test_frequency().wavelength().to_meters();
let d = DipolePattern::new(Distance::meters(1.25 * wavelength));
let peak = d.peak_gain(test_frequency());
assert_approx_eq!(peak.as_f64(), 5.2, atol <= 0.1);
}
#[test]
fn test_1_5_wavelength_dipole_at_45_degrees() {
let wavelength = test_frequency().wavelength().to_meters();
let d = DipolePattern::new(Distance::meters(1.5 * wavelength));
let gain = d.gain(test_frequency(), Angle::degrees(45.0));
assert_approx_eq!(gain.as_f64(), 3.5, atol <= 0.1);
}
#[test]
fn test_simpsons_rule_sine() {
let result = simpsons_rule(0.0, PI, 1000, |x| x.sin());
assert_approx_eq!(result, 2.0, atol <= 1e-10);
}
#[test]
fn test_golden_section_max_parabola() {
let (x_max, f_max) = golden_section_max(0.0, 4.0, |x| -(x - 2.0).powi(2) + 5.0);
assert_approx_eq!(x_max, 2.0, atol <= 1e-6);
assert_approx_eq!(f_max, 5.0, atol <= 1e-6);
}
}