apsis-rs/crates/rf/apsis-atmosphere/tests/rain.rs

178 lines
6.7 KiB
Rust

//! Tests for rain attenuation.
use apsis_atmosphere::{
polarisation_tilt, rain_attenuation, rain_coefficients, specific_rain_attenuation,
};
use core::f64::consts::FRAC_PI_2;
/// Horizontal polarisation.
const HORIZONTAL: f64 = 0.0;
/// Vertical polarisation.
const VERTICAL: f64 = FRAC_PI_2;
/// A mid latitude elevation of thirty degrees.
const ELEVATION: f64 = core::f64::consts::PI / 6.0;
#[test]
fn coefficients_are_available_across_the_tabulated_range() {
for gigahertz in [1.0, 4.0, 12.0, 20.0, 30.0, 100.0, 500.0, 1_000.0] {
let coefficients =
rain_coefficients(gigahertz * 1.0e9, HORIZONTAL, ELEVATION).expect("valid");
assert!(
coefficients.0 > 0.0,
"{gigahertz} GHz gave k {}",
coefficients.0
);
assert!(
coefficients.1 > 0.5 && coefficients.1 < 1.5,
"{gigahertz} GHz gave alpha {}",
coefficients.1
);
}
}
#[test]
fn coefficients_at_a_tabulated_frequency_match_the_table() {
// At 12 GHz the table gives k = 0.0168 for horizontal polarisation,
// with alpha_h = 1.200 and alpha_v = 1.000. The combined alpha depends
// on the elevation through the `cos(2 elevation)` term, so at thirty
// degrees it is the weighted mean of the two, which is 1.15.
let (k, alpha) = rain_coefficients(12.0e9, HORIZONTAL, ELEVATION).expect("valid");
assert!((k - 0.016_8).abs() < 1e-9, "k {k}");
assert!((alpha - 1.15).abs() < 1e-9, "alpha {alpha}");
// At the zenith the `cos(2 elevation)` factor is minus one, which
// selects the vertical alpha instead, so the pair swaps behaviour.
let (_, zenith_alpha) = rain_coefficients(12.0e9, HORIZONTAL, FRAC_PI_2).expect("valid");
assert!((zenith_alpha - 1.0).abs() < 1e-9, "alpha {zenith_alpha}");
}
#[test]
fn horizontal_coefficients_exceed_vertical_at_mid_frequencies() {
// At 20 GHz the table gives k_h = 0.0751 against k_v = 0.0691.
let (horizontal, _) = rain_coefficients(20.0e9, HORIZONTAL, ELEVATION).expect("valid");
let (vertical, _) = rain_coefficients(20.0e9, VERTICAL, ELEVATION).expect("valid");
assert!(horizontal > vertical, "{horizontal} vs {vertical}");
}
#[test]
fn the_coefficients_are_interpolated_between_points() {
// At 11 GHz the value lies between the 10 and 12 GHz entries in
// logarithmic k, so it is the geometric mean of the two.
let (k, _) = rain_coefficients(11.0e9, HORIZONTAL, ELEVATION).expect("valid");
let expected = (0.008_87_f64 * 0.016_8).sqrt();
assert!((k - expected).abs() < 1e-9, "k {k} expected {expected}");
}
#[test]
fn a_frequency_outside_the_table_is_rejected() {
assert_eq!(rain_coefficients(0.5e9, HORIZONTAL, ELEVATION), None);
assert_eq!(rain_coefficients(2_000.0e9, HORIZONTAL, ELEVATION), None);
assert_eq!(rain_coefficients(f64::NAN, HORIZONTAL, ELEVATION), None);
assert_eq!(rain_coefficients(12.0e9, f64::NAN, ELEVATION), None);
}
#[test]
fn specific_attenuation_follows_the_power_law() {
let rain_rate = 20.0;
let specific =
specific_rain_attenuation(rain_rate, 12.0e9, HORIZONTAL, ELEVATION).expect("valid");
// The value is k R^alpha with the coefficients for this frequency,
// polarisation, and elevation.
let (k, alpha) = rain_coefficients(12.0e9, HORIZONTAL, ELEVATION).expect("valid");
assert!((specific - k * rain_rate.powf(alpha)).abs() < 1e-9);
// For 20 mm/h at 12 GHz and thirty degrees this is about 0.53 dB/km.
assert!((specific - 0.526_6).abs() < 0.001, "specific {specific}");
}
#[test]
fn a_higher_rain_rate_gives_more_attenuation() {
let light = specific_rain_attenuation(1.0, 12.0e9, HORIZONTAL, ELEVATION).expect("valid");
let heavy = specific_rain_attenuation(50.0, 12.0e9, HORIZONTAL, ELEVATION).expect("valid");
assert!(heavy > light);
// The ratio follows the power law, so it is 50 raised to the same alpha
// the coefficients give.
let (_, alpha) = rain_coefficients(12.0e9, HORIZONTAL, ELEVATION).expect("valid");
assert!((heavy / light - 50.0_f64.powf(alpha)).abs() < 1e-6);
}
#[test]
fn a_zero_rain_rate_gives_no_attenuation() {
assert_eq!(
specific_rain_attenuation(0.0, 12.0e9, HORIZONTAL, ELEVATION),
Some(0.0)
);
}
#[test]
fn a_negative_rain_rate_is_rejected() {
assert_eq!(
specific_rain_attenuation(-1.0, 12.0e9, HORIZONTAL, ELEVATION),
None
);
}
#[test]
fn total_attenuation_is_the_specific_value_times_the_effective_path() {
let result = rain_attenuation(20.0, 12.0e9, HORIZONTAL, ELEVATION).expect("valid");
assert!(
(result.attenuation_db - result.specific_db_per_km * result.path_length_km).abs() < 1e-9
);
assert!(result.is_valid());
}
#[test]
fn the_effective_path_is_shorter_than_the_geometric_one() {
// The reduction factor represents the fact that heavy rain does not fill
// the whole slant path, so the effective length is below the geometric
// length through a three kilometre layer.
let result = rain_attenuation(20.0, 12.0e9, HORIZONTAL, ELEVATION).expect("valid");
let geometric = 3.0 / ELEVATION.sin();
assert!((geometric - 6.0).abs() < 1e-6);
assert!(
result.path_length_km < geometric,
"{}",
result.path_length_km
);
assert!(result.path_length_km > 0.0);
}
#[test]
fn attenuation_grows_as_the_elevation_falls() {
let high = rain_attenuation(20.0, 12.0e9, HORIZONTAL, FRAC_PI_2).expect("valid");
let low = rain_attenuation(20.0, 12.0e9, HORIZONTAL, 10.0_f64.to_radians()).expect("valid");
assert!(low.attenuation_db > high.attenuation_db);
}
#[test]
fn a_heavy_rain_event_gives_a_significant_attenuation() {
// A 20 mm/h rain cell at 12 GHz and thirty degrees elevation gives a few
// decibels, which is the order of magnitude a Ku band link must budget.
let result = rain_attenuation(20.0, 12.0e9, HORIZONTAL, ELEVATION).expect("valid");
assert!(
result.attenuation_db > 0.5 && result.attenuation_db < 5.0,
"{} dB",
result.attenuation_db
);
}
#[test]
fn a_lower_frequency_suffers_less_rain_attenuation() {
let c_band = rain_attenuation(20.0, 6.0e9, HORIZONTAL, ELEVATION).expect("valid");
let ku_band = rain_attenuation(20.0, 12.0e9, HORIZONTAL, ELEVATION).expect("valid");
assert!(ku_band.attenuation_db > c_band.attenuation_db);
}
#[test]
fn an_elevation_at_the_horizon_is_rejected() {
assert_eq!(rain_attenuation(20.0, 12.0e9, HORIZONTAL, 0.0), None);
assert_eq!(rain_attenuation(20.0, 12.0e9, HORIZONTAL, -0.1), None);
}
#[test]
fn polarisation_tilt_converts_degrees() {
assert_eq!(polarisation_tilt(0.0), 0.0);
assert!((polarisation_tilt(90.0) - FRAC_PI_2).abs() < 1e-12);
assert!((polarisation_tilt(45.0) - FRAC_PI_2 / 2.0).abs() < 1e-12);
}