rf: add the ITU-R S.465 reference antenna pattern
This commit is contained in:
parent
0110eb7eb9
commit
a2e31c32ab
|
|
@ -169,3 +169,7 @@ required-features = ["rf"]
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "dump_rain_path"
|
name = "dump_rain_path"
|
||||||
required-features = ["rf"]
|
required-features = ["rf"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "dump_s465"
|
||||||
|
required-features = ["rf"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
//! Writes the ITU-R S.465 reference pattern computed by this workspace, for
|
||||||
|
//! cross-checking against the recommendation.
|
||||||
|
//!
|
||||||
|
//! Usage:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! cargo run -p apsis-rs --features rf --example dump_s465 \
|
||||||
|
//! > validation/data/expected/rust_s465.csv
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use apsis_antenna::{main_lobe_break_angle, reference_pattern_gain, reference_peak_gain};
|
||||||
|
|
||||||
|
/// The antenna sizes sampled, as diameter to wavelength ratios.
|
||||||
|
const RATIOS: &[f64] = &[50.0, 100.0, 200.0];
|
||||||
|
|
||||||
|
/// The off-axis angles sampled, in degrees.
|
||||||
|
const ANGLES_DEGREES: &[f64] = &[0.1, 0.5, 1.0, 5.0, 20.0, 36.0, 48.0, 90.0];
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("ratio,angle_deg,peak_dbi,break_deg,gain_dbi");
|
||||||
|
for &ratio in RATIOS {
|
||||||
|
let Some(peak) = reference_peak_gain(ratio) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(break_angle) = main_lobe_break_angle(ratio) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for &angle_degrees in ANGLES_DEGREES {
|
||||||
|
let angle = angle_degrees.to_radians();
|
||||||
|
let Some(gain) = reference_pattern_gain(ratio, angle) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
println!("{ratio},{angle_degrees},{peak:.9},{break_angle:.9},{gain:.9}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -50,6 +50,11 @@
|
||||||
|
|
||||||
mod parabolic;
|
mod parabolic;
|
||||||
mod pattern;
|
mod pattern;
|
||||||
|
mod reference;
|
||||||
|
|
||||||
pub use parabolic::{ParabolicAperture, beam_width, floor_angle};
|
pub use parabolic::{ParabolicAperture, beam_width, floor_angle};
|
||||||
pub use pattern::{ElementPattern, Pattern, mask_gain, off_axis_attenuation};
|
pub use pattern::{ElementPattern, Pattern, mask_gain, off_axis_attenuation};
|
||||||
|
pub use reference::{
|
||||||
|
PEAK_GAIN_OFFSET_DB, diameter_over_wavelength_from_peak_gain, main_lobe_break_angle,
|
||||||
|
reference_beam_width, reference_pattern_decibel, reference_pattern_gain, reference_peak_gain,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,10 @@ use core::f64::consts::PI;
|
||||||
/// Returns the gain of an antenna at an angle from boresight, given a peak
|
/// Returns the gain of an antenna at an angle from boresight, given a peak
|
||||||
/// gain and a half power beam width.
|
/// gain and a half power beam width.
|
||||||
///
|
///
|
||||||
/// This is the squared angle model used for interference analysis, in the
|
/// This is the squared angle model that several interference assessments
|
||||||
/// form adopted by the Radio Regulations and by ITU-R Recommendation S.465
|
/// use, expressed in terms of the half power beam width so that any antenna
|
||||||
/// for the reference pattern of a simple Earth station antenna. The gain
|
/// can be described by the two numbers a link budget has to hand. In
|
||||||
/// falls as the square of the off-axis angle until it reaches the peak gain
|
/// decibels,
|
||||||
/// minus a fixed floor, and stays there beyond. In decibels,
|
|
||||||
///
|
///
|
||||||
/// ```text
|
/// ```text
|
||||||
/// G(theta) = G_peak - min(12 (theta / theta_0)^2, G_floor)
|
/// G(theta) = G_peak - min(12 (theta / theta_0)^2, G_floor)
|
||||||
|
|
@ -23,6 +22,14 @@ use core::f64::consts::PI;
|
||||||
/// illuminated aperture, which is a good approximation down to the first
|
/// illuminated aperture, which is a good approximation down to the first
|
||||||
/// sidelobe.
|
/// sidelobe.
|
||||||
///
|
///
|
||||||
|
/// The coefficient twelve is chosen so that the attenuation reaches three
|
||||||
|
/// decibels at `theta_0 / 2`, which is the definition of the half power beam
|
||||||
|
/// width. A pattern taken from a recommendation should be used in place of
|
||||||
|
/// this one where the recommendation applies: see
|
||||||
|
/// [`reference_pattern_gain`](crate::reference_pattern_gain) for the ITU-R
|
||||||
|
/// S.465 pattern, which differs from this model in the main lobe and also in
|
||||||
|
/// the shape of its sidelobe region.
|
||||||
|
///
|
||||||
/// Returns `None` when the peak gain has no decibel form, or when the beam
|
/// Returns `None` when the peak gain has no decibel form, or when the beam
|
||||||
/// width or floor is not positive and finite.
|
/// width or floor is not positive and finite.
|
||||||
pub fn off_axis_attenuation(
|
pub fn off_axis_attenuation(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
//! The ITU-R S.465 reference antenna pattern.
|
||||||
|
//!
|
||||||
|
//! Recommendation S.465 defines a reference pattern for an Earth station
|
||||||
|
//! antenna, used to assess interference between satellite networks. The
|
||||||
|
//! pattern has three regions: a main lobe that falls as the square of the
|
||||||
|
//! off-axis angle, a sidelobe region that falls as the logarithm of the
|
||||||
|
//! angle, and a floor.
|
||||||
|
//!
|
||||||
|
//! The pattern is selected only by the ratio of the antenna diameter to the
|
||||||
|
//! wavelength. The recommendation gives one pattern for antennas with
|
||||||
|
//! `D / lambda` of at least 100 and another for smaller antennas, whose main
|
||||||
|
//! lobe is broader and whose sidelobe region therefore begins later.
|
||||||
|
//!
|
||||||
|
//! The main lobe expression is what distinguishes this pattern from the
|
||||||
|
//! generic reference pattern of the Radio Regulations Appendix 8, which
|
||||||
|
//! models the sidelobe region only and takes the main lobe width as an
|
||||||
|
//! input. The two share the shape of the sidelobe region.
|
||||||
|
//!
|
||||||
|
//! Source: ITU-R Recommendation S.465, *Reference earth-station radiation
|
||||||
|
//! pattern for use in coordination and interference assessment in the
|
||||||
|
//! frequency range from 2 to about 30 GHz*.
|
||||||
|
|
||||||
|
use apsis_rf::Decibel;
|
||||||
|
use core::f64::consts::PI;
|
||||||
|
|
||||||
|
/// The peak gain offset of the reference pattern, in decibels.
|
||||||
|
///
|
||||||
|
/// The recommendation gives the peak gain as `20 log10(D / lambda) + 7.7`,
|
||||||
|
/// which corresponds to an aperture efficiency near 0.6. It is slightly
|
||||||
|
/// below what a well adjusted dish reaches, because the reference pattern
|
||||||
|
/// represents a typical rather than an optimal antenna.
|
||||||
|
pub const PEAK_GAIN_OFFSET_DB: f64 = 7.7;
|
||||||
|
|
||||||
|
/// The diameter to wavelength ratio at which the pattern changes form.
|
||||||
|
///
|
||||||
|
/// A larger antenna has a narrower main lobe, so its sidelobe region begins
|
||||||
|
/// earlier and reaches the floor earlier.
|
||||||
|
const LARGE_APERTURE_RATIO: f64 = 100.0;
|
||||||
|
|
||||||
|
/// The constant and floor angle of the sidelobe region.
|
||||||
|
///
|
||||||
|
/// The constant is the value of `G + 25 log10(phi_deg)` in the sidelobe
|
||||||
|
/// region, and the floor angle is the angle at which the region reaches its
|
||||||
|
/// floor.
|
||||||
|
fn sidelobe_branch(diameter_over_wavelength: f64) -> (f64, f64) {
|
||||||
|
if diameter_over_wavelength >= LARGE_APERTURE_RATIO {
|
||||||
|
(29.0, 36.0)
|
||||||
|
} else {
|
||||||
|
(32.0, 48.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the peak gain of the reference pattern, in decibels relative to an
|
||||||
|
/// isotropic radiator.
|
||||||
|
///
|
||||||
|
/// The expression is `20 log10(D / lambda) + 7.7`.
|
||||||
|
///
|
||||||
|
/// Returns `None` when the ratio is not positive and finite.
|
||||||
|
pub fn reference_peak_gain(diameter_over_wavelength: f64) -> Option<f64> {
|
||||||
|
if !diameter_over_wavelength.is_finite() || diameter_over_wavelength <= 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(20.0 * diameter_over_wavelength.log10() + PEAK_GAIN_OFFSET_DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the angle at which the main lobe meets the sidelobe region, in
|
||||||
|
/// degrees.
|
||||||
|
///
|
||||||
|
/// The two expressions intersect at this angle, and there is no closed form,
|
||||||
|
/// so the value is found by bisection. The main lobe falls as the square of
|
||||||
|
/// the angle while the sidelobe region falls as its logarithm, so the
|
||||||
|
/// difference has a single zero in the interval searched, which makes
|
||||||
|
/// bisection reliable.
|
||||||
|
///
|
||||||
|
/// The result lies between zero and the floor angle of the sidelobe region.
|
||||||
|
pub fn main_lobe_break_angle(diameter_over_wavelength: f64) -> Option<f64> {
|
||||||
|
let peak = reference_peak_gain(diameter_over_wavelength)?;
|
||||||
|
let (head, floor_angle) = sidelobe_branch(diameter_over_wavelength);
|
||||||
|
|
||||||
|
let difference = |angle_degrees: f64| {
|
||||||
|
let main_lobe = peak - 2.5e-3 * (diameter_over_wavelength * angle_degrees).powi(2);
|
||||||
|
let sidelobe = head - 25.0 * angle_degrees.log10();
|
||||||
|
main_lobe - sidelobe
|
||||||
|
};
|
||||||
|
|
||||||
|
// The main lobe dominates at very small angles and the sidelobe
|
||||||
|
// expression dominates at the floor angle, so the zero lies between.
|
||||||
|
let mut low = 1.0e-9;
|
||||||
|
let mut high = floor_angle;
|
||||||
|
if difference(high) > 0.0 {
|
||||||
|
return Some(high);
|
||||||
|
}
|
||||||
|
for _ in 0..100 {
|
||||||
|
let middle = 0.5 * (low + high);
|
||||||
|
if difference(middle) > 0.0 {
|
||||||
|
low = middle;
|
||||||
|
} else {
|
||||||
|
high = middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(0.5 * (low + high))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the gain of the S.465 reference pattern at an off-axis angle.
|
||||||
|
///
|
||||||
|
/// The angle is in radians. The three regions are
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// G = G_max - 2.5e-3 (D/lambda phi_deg)^2 for 0 <= phi < phi_break
|
||||||
|
/// G = head - 25 log10(phi_deg) for phi_break <= phi < phi_floor
|
||||||
|
/// G = -10 for phi_floor <= phi <= 180
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// where `phi_break` is the angle returned by [`main_lobe_break_angle`], the
|
||||||
|
/// head is 29 for a large aperture and 32 for a small one, and the floor
|
||||||
|
/// angle is 36 or 48 degrees accordingly.
|
||||||
|
///
|
||||||
|
/// Returns `None` when the ratio or the angle is not usable.
|
||||||
|
pub fn reference_pattern_gain(diameter_over_wavelength: f64, off_axis_angle: f64) -> Option<f64> {
|
||||||
|
if !off_axis_angle.is_finite() || off_axis_angle < 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let peak = reference_peak_gain(diameter_over_wavelength)?;
|
||||||
|
let break_angle = main_lobe_break_angle(diameter_over_wavelength)?;
|
||||||
|
let (head, floor_angle) = sidelobe_branch(diameter_over_wavelength);
|
||||||
|
let angle_degrees = off_axis_angle.to_degrees();
|
||||||
|
|
||||||
|
if angle_degrees < break_angle {
|
||||||
|
Some(peak - 2.5e-3 * (diameter_over_wavelength * angle_degrees).powi(2))
|
||||||
|
} else if angle_degrees <= floor_angle {
|
||||||
|
Some(head - 25.0 * angle_degrees.log10())
|
||||||
|
} else {
|
||||||
|
Some(-10.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the gain of the S.465 reference pattern as a decibel value.
|
||||||
|
///
|
||||||
|
/// Returns `None` when the ratio or the angle is not usable.
|
||||||
|
pub fn reference_pattern_decibel(
|
||||||
|
diameter_over_wavelength: f64,
|
||||||
|
off_axis_angle: f64,
|
||||||
|
) -> Option<Decibel> {
|
||||||
|
let gain = reference_pattern_gain(diameter_over_wavelength, off_axis_angle)?;
|
||||||
|
Decibel::from_db(gain)
|
||||||
|
.is_finite()
|
||||||
|
.then_some(Decibel::from_db(gain))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the diameter to wavelength ratio of an antenna with a given peak
|
||||||
|
/// gain.
|
||||||
|
///
|
||||||
|
/// This inverts the peak gain expression, which is useful when a station is
|
||||||
|
/// specified by its gain rather than its size.
|
||||||
|
///
|
||||||
|
/// Returns `None` when the gain is not finite.
|
||||||
|
pub fn diameter_over_wavelength_from_peak_gain(peak_gain_dbi: f64) -> Option<f64> {
|
||||||
|
if !peak_gain_dbi.is_finite() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(10f64.powf((peak_gain_dbi - PEAK_GAIN_OFFSET_DB) / 20.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the half power beam width of the reference pattern main lobe, in
|
||||||
|
/// radians.
|
||||||
|
///
|
||||||
|
/// The main lobe falls to three decibels at
|
||||||
|
/// `phi = sqrt(3 / 2.5e-3) / (D / lambda)` degrees from boresight, so the
|
||||||
|
/// full width between the two three decibel points is twice that value.
|
||||||
|
///
|
||||||
|
/// Returns `None` when the ratio is not positive and finite.
|
||||||
|
pub fn reference_beam_width(diameter_over_wavelength: f64) -> Option<f64> {
|
||||||
|
if !diameter_over_wavelength.is_finite() || diameter_over_wavelength <= 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let half_angle_degrees = (3.0_f64 / 2.5e-3).sqrt() / diameter_over_wavelength;
|
||||||
|
Some(2.0 * half_angle_degrees * PI / 180.0)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
//! 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);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue