use std::f64::consts::PI;
pub fn directivity_from_pattern(
pattern_fn: impl Fn(f64, f64) -> f64,
n_theta: usize,
n_phi: usize,
) -> f64 {
if n_theta == 0 || n_phi == 0 {
return 0.0;
}
let mut p_rad = 0.0_f64;
let mut u_max = 0.0_f64;
let dtheta = PI / n_theta as f64;
let dphi = 2.0 * PI / n_phi as f64;
for it in 0..n_theta {
let theta = PI * (it as f64 + 0.5) / n_theta as f64;
let sin_theta = theta.sin();
for ip in 0..n_phi {
let phi = 2.0 * PI * ip as f64 / n_phi as f64;
let u = pattern_fn(theta, phi);
if u > u_max {
u_max = u;
}
p_rad += u * sin_theta * dtheta * dphi;
}
}
if p_rad < f64::EPSILON {
return 0.0;
}
4.0 * PI * u_max / p_rad
}
pub fn directivity_dbi_from_pattern(
pattern_fn: impl Fn(f64, f64) -> f64,
n_theta: usize,
n_phi: usize,
) -> f64 {
let d = directivity_from_pattern(pattern_fn, n_theta, n_phi);
if d < f64::MIN_POSITIVE {
return f64::NEG_INFINITY;
}
10.0 * d.log10()
}
#[derive(Debug, Clone)]
pub struct AntennaPatternMetrics {
pub directivity_dbi: f64,
pub hpbw_e_deg: f64,
pub hpbw_h_deg: f64,
pub side_lobe_level_db: f64,
pub front_to_back_ratio_db: f64,
}
impl AntennaPatternMetrics {
pub fn from_pattern_1d(pattern: &[f64], theta_deg_range: (f64, f64)) -> Self {
let n = pattern.len();
if n < 2 {
return Self {
directivity_dbi: 0.0,
hpbw_e_deg: 180.0,
hpbw_h_deg: 180.0,
side_lobe_level_db: 0.0,
front_to_back_ratio_db: 0.0,
};
}
let theta_start = theta_deg_range.0;
let theta_end = theta_deg_range.1;
let theta_span = theta_end - theta_start;
let (peak_idx, &peak_val) = pattern
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or((n / 2, &1.0));
let peak_val = peak_val.max(f64::MIN_POSITIVE);
let angle_of = |idx: usize| theta_start + theta_span * idx as f64 / (n - 1) as f64;
let half_power = peak_val * 0.5;
let left_idx = (0..peak_idx)
.rev()
.find(|&i| pattern[i] <= half_power)
.unwrap_or(0);
let right_idx = ((peak_idx + 1)..n)
.find(|&i| pattern[i] <= half_power)
.unwrap_or(n - 1);
let hpbw = (angle_of(right_idx) - angle_of(left_idx)).abs().max(0.0);
let sll_val = find_side_lobe_level(pattern, left_idx, right_idx, peak_val);
let side_lobe_level_db = if sll_val > f64::MIN_POSITIVE {
10.0 * (sll_val / peak_val).log10()
} else {
-60.0 };
let back_idx = ((peak_idx + n / 2) % n).min(n - 1);
let back_val = pattern[back_idx].max(f64::MIN_POSITIVE);
let front_to_back_ratio_db = 10.0 * (peak_val / back_val).log10();
let directivity_linear = compute_directivity_1d(pattern, theta_deg_range);
let directivity_dbi = if directivity_linear > f64::MIN_POSITIVE {
10.0 * directivity_linear.log10()
} else {
0.0
};
Self {
directivity_dbi,
hpbw_e_deg: hpbw,
hpbw_h_deg: hpbw, side_lobe_level_db,
front_to_back_ratio_db: front_to_back_ratio_db.max(0.0),
}
}
pub fn boresight_gain_dbi(&self) -> f64 {
self.directivity_dbi
}
pub fn is_low_sidelobe(&self, threshold_db: f64) -> bool {
self.side_lobe_level_db < threshold_db
}
}
fn find_side_lobe_level(pattern: &[f64], left: usize, right: usize, _peak: f64) -> f64 {
let n = pattern.len();
let mut max_side = 0.0_f64;
for i in 1..left.min(n - 1) {
if pattern[i] >= pattern[i - 1]
&& pattern[i] >= pattern[(i + 1).min(n - 1)]
&& pattern[i] > max_side
{
max_side = pattern[i];
}
}
for i in (right + 1).min(n - 1)..n - 1 {
if pattern[i] >= pattern[i - 1] && pattern[i] >= pattern[i + 1] && pattern[i] > max_side {
max_side = pattern[i];
}
}
max_side
}
fn compute_directivity_1d(pattern: &[f64], theta_deg_range: (f64, f64)) -> f64 {
let n = pattern.len();
if n < 2 {
return 1.0;
}
let theta_start = theta_deg_range.0.to_radians();
let theta_end = theta_deg_range.1.to_radians();
let dtheta = (theta_end - theta_start) / (n - 1) as f64;
let mut integral = 0.0_f64;
let mut u_max = 0.0_f64;
for (i, &u) in pattern.iter().enumerate() {
let theta = theta_start + i as f64 * dtheta;
integral += u * theta.sin() * dtheta;
if u > u_max {
u_max = u;
}
}
if integral < f64::EPSILON {
return 1.0;
}
2.0 * u_max / integral
}
pub fn effective_aperture_m2(gain_linear: f64, wavelength_m: f64) -> f64 {
gain_linear * wavelength_m * wavelength_m / (4.0 * PI)
}
pub fn friis_equation(gain_t: f64, gain_r: f64, wavelength_m: f64, range_m: f64) -> f64 {
if range_m < f64::MIN_POSITIVE {
return 0.0;
}
gain_t * gain_r * (wavelength_m / (4.0 * PI * range_m)).powi(2)
}
pub fn free_space_path_loss_db(wavelength_m: f64, range_m: f64) -> f64 {
if range_m < f64::MIN_POSITIVE || wavelength_m < f64::MIN_POSITIVE {
return f64::INFINITY;
}
-20.0 * (wavelength_m / (4.0 * PI * range_m)).log10()
}
pub fn gain_linear_to_dbi(gain_linear: f64) -> f64 {
if gain_linear <= 0.0 {
return f64::NEG_INFINITY;
}
10.0 * gain_linear.log10()
}
pub fn gain_dbi_to_linear(gain_dbi: f64) -> f64 {
10.0_f64.powf(gain_dbi / 10.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hertzian_dipole_directivity_numeric() {
let d = directivity_from_pattern(|theta, _phi| theta.sin().powi(2), 500, 200);
assert!(
(d - 1.5).abs() < 0.02,
"Dipole directivity: {d:.4} expected ~1.5"
);
}
#[test]
fn isotropic_radiator_directivity_unity() {
let d = directivity_from_pattern(|_theta, _phi| 1.0, 200, 100);
assert!((d - 1.0).abs() < 0.01, "Isotropic directivity: {d:.4}");
}
#[test]
fn friis_free_space_path_loss() {
let p_ratio = friis_equation(1.0, 1.0, 1550e-9, 100.0);
let fspl_db = -10.0 * p_ratio.log10();
assert!(
fspl_db > 165.0 && fspl_db < 190.0,
"FSPL = {fspl_db:.1} dB (expected ~178 dB)"
);
}
#[test]
fn effective_aperture_isotropic() {
let a = effective_aperture_m2(1.0, 1550.0e-9);
assert!(a > 1.0e-14 && a < 1.0e-12, "Isotropic aperture: {a:.3e} m²");
}
#[test]
fn fspl_increases_with_range() {
let loss1 = free_space_path_loss_db(1550.0e-9, 10.0);
let loss2 = free_space_path_loss_db(1550.0e-9, 100.0);
assert!(
loss2 > loss1,
"FSPL must increase with range: {loss1:.1} → {loss2:.1}"
);
assert!(
(loss2 - loss1 - 20.0).abs() < 0.01,
"10× range → +20 dB: diff={:.3}",
loss2 - loss1
);
}
#[test]
fn pattern_metrics_from_sinc_like_array() {
let n = 1001_usize;
let pattern: Vec<f64> = (0..n)
.map(|i| {
let t = -PI / 2.0 + PI * i as f64 / (n - 1) as f64;
(10.0 * t).cos().powi(2) * (t / 2.0).cos().powi(2)
})
.collect();
let metrics = AntennaPatternMetrics::from_pattern_1d(&pattern, (-90.0, 90.0));
assert!(
metrics.hpbw_e_deg < 15.0,
"HPBW should be < 15°: {:.2}°",
metrics.hpbw_e_deg
);
assert!(
metrics.side_lobe_level_db < 0.0,
"SLL must be negative: {:.2} dB",
metrics.side_lobe_level_db
);
}
#[test]
fn gain_conversion_round_trip() {
let g_dbi = 12.0_f64;
let g_lin = gain_dbi_to_linear(g_dbi);
let g_dbi2 = gain_linear_to_dbi(g_lin);
assert!(
(g_dbi2 - g_dbi).abs() < 1.0e-10,
"Round-trip conversion: {g_dbi2}"
);
}
#[test]
fn directivity_dbi_positive_for_dipole() {
let d_dbi = directivity_dbi_from_pattern(|theta, _phi| theta.sin().powi(2), 200, 100);
assert!(d_dbi > 1.5 && d_dbi < 2.0, "Dipole dBi: {d_dbi:.3}");
}
}