apsis-rs/crates/rf/apsis-antenna/tests/reference.rs

199 lines
7.1 KiB
Rust

//! Tests for the ITU-R S.465 reference antenna pattern.
use apsis_antenna::{
PEAK_GAIN_OFFSET_DB, diameter_over_wavelength_from_peak_gain, main_lobe_break_angle,
reference_beam_width, reference_pattern_gain, reference_peak_gain,
};
const TOLERANCE: f64 = 1e-9;
#[test]
fn the_peak_gain_follows_the_recommendation() {
// The recommendation gives 20 log10(D / lambda) + 7.7.
for ratio in [10.0_f64, 50.0, 100.0, 200.0, 1_000.0] {
let expected = 20.0 * ratio.log10() + PEAK_GAIN_OFFSET_DB;
let actual = reference_peak_gain(ratio).expect("valid ratio");
assert!((actual - expected).abs() < TOLERANCE, "ratio {ratio}");
}
// A ratio of one hundred gives 47.7 dBi.
assert!((reference_peak_gain(100.0).expect("valid") - 47.7).abs() < TOLERANCE);
}
#[test]
fn the_peak_gain_is_inverted_by_the_diameter_helper() {
for ratio in [10.0, 100.0, 500.0] {
let gain = reference_peak_gain(ratio).expect("valid");
let recovered = diameter_over_wavelength_from_peak_gain(gain).expect("finite gain");
assert!((recovered / ratio - 1.0).abs() < 1e-12, "ratio {ratio}");
}
}
#[test]
fn the_pattern_at_boresight_is_the_peak_gain() {
for ratio in [50.0, 100.0, 200.0] {
let peak = reference_peak_gain(ratio).expect("valid");
let boresight = reference_pattern_gain(ratio, 0.0).expect("valid");
assert!((boresight - peak).abs() < TOLERANCE, "ratio {ratio}");
}
}
#[test]
fn the_main_lobe_falls_as_the_square_of_the_angle() {
// Inside the main lobe the attenuation is 2.5e-3 (D/lambda phi)^2.
let ratio = 100.0;
let peak = reference_peak_gain(ratio).expect("valid");
for angle_degrees in [0.05_f64, 0.1, 0.2, 0.3] {
let gain = reference_pattern_gain(ratio, angle_degrees.to_radians()).expect("valid");
let expected = peak - 2.5e-3 * (ratio * angle_degrees).powi(2);
assert!(
(gain - expected).abs() < TOLERANCE,
"{angle_degrees} degrees: got {gain}, expected {expected}"
);
}
}
#[test]
fn the_pattern_matches_recorded_values() {
// These are the values of the recommendation for a ratio of one hundred,
// computed independently from its equations.
let cases: [(f64, f64); 8] = [
(0.1, 47.45),
(0.5, 41.45),
(1.0, 29.0),
(5.0, 11.525_749_892),
(20.0, -3.525_749_892),
(36.0, -9.907_562_519),
(48.0, -10.0),
(90.0, -10.0),
];
for (angle_degrees, expected) in cases {
let gain = reference_pattern_gain(100.0, angle_degrees.to_radians()).expect("valid");
assert!(
(gain - expected).abs() < 1e-6,
"{angle_degrees} degrees: got {gain}, expected {expected}"
);
}
}
#[test]
fn the_pattern_matches_recorded_values_for_a_small_aperture() {
// A ratio of fifty uses the small aperture branch, with a head of 32 and
// a floor angle of 48 degrees.
// At exactly the floor angle the branch switches to the floor, so the
// last value of the falling branch is approached from below.
let cases: [(f64, f64); 6] = [
(0.1, 41.616_900_087),
(1.0, 35.429_400_087),
(5.0, 14.525_749_892),
(36.0, -6.907_562_519),
(47.99, -10.028_768_748_310_533),
(48.0, -10.0),
];
for (angle_degrees, expected) in cases {
let gain = reference_pattern_gain(50.0, angle_degrees.to_radians()).expect("valid");
assert!(
(gain - expected).abs() < 1e-6,
"{angle_degrees} degrees: got {gain}, expected {expected}"
);
}
}
#[test]
fn the_two_branches_meet_at_the_break_angle() {
// The main lobe and the sidelobe region must agree at the junction, or
// the pattern would jump.
for ratio in [50.0_f64, 100.0, 200.0, 500.0] {
let peak = reference_peak_gain(ratio).expect("valid");
let break_angle = main_lobe_break_angle(ratio).expect("valid");
let (head, _) = if ratio >= 100.0 {
(29.0, 36.0)
} else {
(32.0, 48.0)
};
let main_lobe = peak - 2.5e-3 * (ratio * break_angle).powi(2);
let sidelobe = head - 25.0 * break_angle.log10();
assert!(
(main_lobe - sidelobe).abs() < 1e-6,
"ratio {ratio}: {main_lobe} against {sidelobe}"
);
}
}
#[test]
fn the_break_angle_shrinks_as_the_aperture_grows() {
// A larger antenna has a narrower main lobe, so the sidelobe region
// begins sooner.
let small = main_lobe_break_angle(50.0).expect("valid");
let medium = main_lobe_break_angle(100.0).expect("valid");
let large = main_lobe_break_angle(400.0).expect("valid");
assert!(small > medium, "{small} against {medium}");
assert!(medium > large, "{medium} against {large}");
}
#[test]
fn the_pattern_floors_at_minus_ten_decibels() {
for ratio in [50.0, 100.0, 200.0] {
let gain = reference_pattern_gain(ratio, 90.0_f64.to_radians()).expect("valid");
assert!((gain + 10.0).abs() < TOLERANCE, "ratio {ratio}");
let beyond = reference_pattern_gain(ratio, core::f64::consts::PI).expect("valid");
assert!((beyond + 10.0).abs() < TOLERANCE, "ratio {ratio}");
}
}
#[test]
fn the_pattern_never_rises_above_the_peak() {
let ratio = 100.0;
let peak = reference_peak_gain(ratio).expect("valid");
for tenth in 0..=100 {
let angle = tenth as f64 * 0.01;
let gain = reference_pattern_gain(ratio, angle).expect("valid");
assert!(gain <= peak + TOLERANCE, "angle {angle} gave {gain}");
}
}
#[test]
fn the_pattern_falls_monotonically_across_the_main_lobe() {
let ratio = 100.0;
let mut previous = f64::INFINITY;
for step in 0..=80 {
let angle_degrees = step as f64 * 0.01;
let gain = reference_pattern_gain(ratio, angle_degrees.to_radians()).expect("valid");
assert!(gain <= previous + TOLERANCE, "angle {angle_degrees}");
previous = gain;
}
}
#[test]
fn the_beam_width_follows_from_the_main_lobe_law() {
// Three decibels is reached at sqrt(3 / 2.5e-3) / (D/lambda) degrees
// from boresight, so the full width is twice that.
let ratio = 100.0;
let width = reference_beam_width(ratio).expect("valid");
let half_degrees = (3.0_f64 / 2.5e-3).sqrt() / ratio;
let expected = 2.0 * half_degrees.to_radians();
assert!((width - expected).abs() < 1e-15);
// The gain at half the beam width from boresight is three decibels down.
let gain = reference_pattern_gain(ratio, width / 2.0).expect("valid");
let peak = reference_peak_gain(ratio).expect("valid");
assert!(
(peak - gain - 3.0).abs() < 1e-9,
"peak - gain = {}",
peak - gain
);
}
#[test]
fn invalid_arguments_are_rejected() {
assert_eq!(reference_peak_gain(0.0), None);
assert_eq!(reference_peak_gain(-1.0), None);
assert_eq!(reference_peak_gain(f64::NAN), None);
assert_eq!(reference_pattern_gain(100.0, -0.1), None);
assert_eq!(reference_pattern_gain(100.0, f64::NAN), None);
assert_eq!(reference_pattern_gain(0.0, 0.1), None);
assert_eq!(reference_beam_width(0.0), None);
assert_eq!(diameter_over_wavelength_from_peak_gain(f64::NAN), None);
assert_eq!(main_lobe_break_angle(0.0), None);
}