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

184 lines
7.2 KiB
Rust

//! 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-6, *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.
///
/// The falling sidelobe expression does not reach exactly minus ten decibels
/// at the floor angle, so the pattern has a step of a few hundredths of a
/// decibel there. This is a property of the recommendation, which defines
/// the regions separately, and it is reproduced here rather than smoothed
/// over.
///
/// 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)
}