199 lines
7.1 KiB
Rust
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);
|
|
}
|