396 lines
12 KiB
Rust
396 lines
12 KiB
Rust
//! Tests for rain attenuation.
|
|
//!
|
|
//! The coefficient values are those of ITU-R Recommendation P.838-3,
|
|
//! produced with a reference implementation of the recommendation and
|
|
//! recorded here. The path results exercise the P.618-13 method.
|
|
|
|
use apsis_atmosphere::{
|
|
DEFAULT_RAIN_HEIGHT_KM, polarisation_tilt, rain_attenuation, rain_coefficients,
|
|
specific_rain_attenuation,
|
|
};
|
|
use core::f64::consts::{FRAC_PI_2, PI};
|
|
|
|
/// Horizontal polarisation.
|
|
const HORIZONTAL: f64 = 0.0;
|
|
/// Vertical polarisation.
|
|
const VERTICAL: f64 = FRAC_PI_2;
|
|
/// The zenith, where the two polarisations coincide.
|
|
const ZENITH: f64 = FRAC_PI_2;
|
|
/// A mid latitude elevation of thirty degrees.
|
|
const THIRTY_DEGREES: f64 = PI / 6.0;
|
|
|
|
/// The relative tolerance on the coefficient values.
|
|
const TOLERANCE: f64 = 1e-9;
|
|
|
|
fn assert_relative(actual: f64, expected: f64, context: &str) {
|
|
let relative = (actual - expected).abs() / expected.abs();
|
|
assert!(
|
|
relative < TOLERANCE,
|
|
"{context}: got {actual}, expected {expected}, relative {relative}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn coefficients_match_the_recommendation_at_the_zenith() {
|
|
// At the zenith the elevation term vanishes, so horizontal and vertical
|
|
// polarisation give the same coefficients.
|
|
let cases = [
|
|
(12.0, 0.024_203_061_156_1, 1.151_599_196_3),
|
|
(20.0, 0.093_876_937_766_6, 1.019_877_631_17),
|
|
(30.0, 0.234_699_253_968, 0.931_114_875_787),
|
|
];
|
|
for (frequency, k_expected, alpha_expected) in cases {
|
|
for polarisation in [HORIZONTAL, VERTICAL] {
|
|
let (k, alpha) =
|
|
rain_coefficients(frequency * 1.0e9, polarisation, ZENITH).expect("valid");
|
|
assert_relative(k, k_expected, &format!("{frequency} GHz k"));
|
|
assert_relative(alpha, alpha_expected, &format!("{frequency} GHz alpha"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn coefficients_match_the_recommendation_at_thirty_degrees() {
|
|
let cases = [
|
|
(12.0, 0.023_944_109_789_7, 1.174_670_745_2),
|
|
(20.0, 0.092_201_236_241_3, 1.047_387_560_19),
|
|
(30.0, 0.238_905_952_257, 0.944_198_048_818),
|
|
];
|
|
for (frequency, k_expected, alpha_expected) in cases {
|
|
let (k, alpha) =
|
|
rain_coefficients(frequency * 1.0e9, HORIZONTAL, THIRTY_DEGREES).expect("valid");
|
|
assert_relative(k, k_expected, &format!("{frequency} GHz k"));
|
|
assert_relative(alpha, alpha_expected, &format!("{frequency} GHz alpha"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn horizontal_and_vertical_differ_off_the_zenith() {
|
|
// Away from the zenith the polarisation matters. At 12 GHz and low
|
|
// elevation the vertical coefficient is the larger of the two, and the
|
|
// two converge as the elevation rises and the geometry becomes
|
|
// symmetric.
|
|
let low_elevation = 10.0_f64.to_radians();
|
|
let horizontal = rain_coefficients(12.0e9, HORIZONTAL, low_elevation).expect("valid");
|
|
let vertical = rain_coefficients(12.0e9, VERTICAL, low_elevation).expect("valid");
|
|
assert_relative(horizontal.0, 0.023_868_203_786_369_1, "horizontal k");
|
|
assert_relative(vertical.0, 0.024_537_918_525_868_188, "vertical k");
|
|
|
|
// At the zenith they coincide.
|
|
let zenith_horizontal = rain_coefficients(12.0e9, HORIZONTAL, ZENITH).expect("valid");
|
|
let zenith_vertical = rain_coefficients(12.0e9, VERTICAL, ZENITH).expect("valid");
|
|
assert_relative(zenith_horizontal.0, zenith_vertical.0, "zenith k");
|
|
}
|
|
|
|
#[test]
|
|
fn circular_polarisation_lies_between_the_linear_cases() {
|
|
// A tilt of forty five degrees is circular polarisation, whose
|
|
// coefficients are the mean of the two linear cases.
|
|
let horizontal = rain_coefficients(12.0e9, HORIZONTAL, THIRTY_DEGREES).expect("valid");
|
|
let vertical = rain_coefficients(12.0e9, VERTICAL, THIRTY_DEGREES).expect("valid");
|
|
let circular =
|
|
rain_coefficients(12.0e9, polarisation_tilt(45.0), THIRTY_DEGREES).expect("valid");
|
|
let mean = 0.5 * (horizontal.0 + vertical.0);
|
|
assert_relative(circular.0, mean, "circular k");
|
|
}
|
|
|
|
#[test]
|
|
fn the_coefficients_are_smooth_across_the_band() {
|
|
// The curve fits are continuous, so a small change in frequency gives a
|
|
// small change in the coefficients.
|
|
let mut previous = rain_coefficients(10.0e9, HORIZONTAL, ZENITH).expect("valid");
|
|
for tenth in 101..=200 {
|
|
let frequency = tenth as f64 * 0.1e9;
|
|
let current = rain_coefficients(frequency, HORIZONTAL, ZENITH).expect("valid");
|
|
assert!(
|
|
(current.0 - previous.0).abs() / previous.0 < 0.05,
|
|
"jump at {frequency} GHz"
|
|
);
|
|
previous = current;
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_frequency_outside_the_recommendation_is_rejected() {
|
|
assert_eq!(rain_coefficients(0.5e9, HORIZONTAL, ZENITH), None);
|
|
assert_eq!(rain_coefficients(2_000.0e9, HORIZONTAL, ZENITH), None);
|
|
assert_eq!(rain_coefficients(f64::NAN, HORIZONTAL, ZENITH), None);
|
|
assert_eq!(rain_coefficients(12.0e9, f64::NAN, ZENITH), None);
|
|
assert_eq!(rain_coefficients(12.0e9, HORIZONTAL, f64::NAN), None);
|
|
}
|
|
|
|
#[test]
|
|
fn specific_attenuation_follows_the_power_law() {
|
|
let rain_rate = 20.0;
|
|
let frequency = 12.0e9;
|
|
let specific =
|
|
specific_rain_attenuation(rain_rate, frequency, HORIZONTAL, THIRTY_DEGREES).expect("valid");
|
|
let (k, alpha) = rain_coefficients(frequency, HORIZONTAL, THIRTY_DEGREES).expect("valid");
|
|
assert_relative(specific, k * rain_rate.powf(alpha), "specific attenuation");
|
|
// At 12 GHz, thirty degrees elevation, and 20 mm/h this is 0.808 dB/km.
|
|
assert!(
|
|
(specific - 0.808_128_772_7).abs() < 1e-6,
|
|
"specific {specific}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_higher_rain_rate_gives_more_attenuation() {
|
|
let light = specific_rain_attenuation(1.0, 12.0e9, HORIZONTAL, THIRTY_DEGREES).expect("valid");
|
|
let heavy = specific_rain_attenuation(50.0, 12.0e9, HORIZONTAL, THIRTY_DEGREES).expect("valid");
|
|
assert!(heavy > light);
|
|
let (_, alpha) = rain_coefficients(12.0e9, HORIZONTAL, THIRTY_DEGREES).expect("valid");
|
|
assert_relative(heavy / light, 50.0_f64.powf(alpha), "rate ratio");
|
|
}
|
|
|
|
#[test]
|
|
fn a_zero_rain_rate_gives_no_attenuation() {
|
|
assert_eq!(
|
|
specific_rain_attenuation(0.0, 12.0e9, HORIZONTAL, THIRTY_DEGREES),
|
|
Some(0.0)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_negative_rain_rate_is_rejected() {
|
|
assert_eq!(
|
|
specific_rain_attenuation(-1.0, 12.0e9, HORIZONTAL, THIRTY_DEGREES),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
rain_attenuation(
|
|
-1.0,
|
|
12.0e9,
|
|
HORIZONTAL,
|
|
THIRTY_DEGREES,
|
|
0.0,
|
|
DEFAULT_RAIN_HEIGHT_KM,
|
|
45.0
|
|
),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn total_attenuation_is_the_specific_value_times_the_effective_path() {
|
|
let result = rain_attenuation(
|
|
20.0,
|
|
12.0e9,
|
|
HORIZONTAL,
|
|
THIRTY_DEGREES,
|
|
0.0,
|
|
DEFAULT_RAIN_HEIGHT_KM,
|
|
45.0,
|
|
)
|
|
.expect("valid");
|
|
assert_relative(
|
|
result.attenuation_db,
|
|
result.specific_db_per_km * result.path_length_km,
|
|
"total attenuation",
|
|
);
|
|
assert!(result.is_valid());
|
|
}
|
|
|
|
#[test]
|
|
fn the_reduction_factors_are_physical() {
|
|
// The horizontal reduction factor accounts for rain not filling the
|
|
// whole horizontal extent of the path, so it is at most one. The
|
|
// vertical adjustment is the ratio of two path estimates and can exceed
|
|
// one when the path crosses the rain layer at a shallow angle, which is
|
|
// the case Recommendation P.618 allows for.
|
|
let result = rain_attenuation(
|
|
20.0,
|
|
12.0e9,
|
|
HORIZONTAL,
|
|
THIRTY_DEGREES,
|
|
0.0,
|
|
DEFAULT_RAIN_HEIGHT_KM,
|
|
45.0,
|
|
)
|
|
.expect("valid");
|
|
assert!(
|
|
result.horizontal_reduction > 0.0 && result.horizontal_reduction <= 1.0,
|
|
"horizontal reduction {}",
|
|
result.horizontal_reduction
|
|
);
|
|
assert!(result.vertical_adjustment > 0.0, "vertical adjustment");
|
|
}
|
|
|
|
#[test]
|
|
fn attenuation_grows_as_the_elevation_falls() {
|
|
let high = rain_attenuation(
|
|
20.0,
|
|
12.0e9,
|
|
HORIZONTAL,
|
|
ZENITH,
|
|
0.0,
|
|
DEFAULT_RAIN_HEIGHT_KM,
|
|
45.0,
|
|
)
|
|
.expect("valid");
|
|
let low = rain_attenuation(
|
|
20.0,
|
|
12.0e9,
|
|
HORIZONTAL,
|
|
10.0_f64.to_radians(),
|
|
0.0,
|
|
DEFAULT_RAIN_HEIGHT_KM,
|
|
45.0,
|
|
)
|
|
.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,
|
|
THIRTY_DEGREES,
|
|
0.0,
|
|
DEFAULT_RAIN_HEIGHT_KM,
|
|
45.0,
|
|
)
|
|
.expect("valid");
|
|
assert!(
|
|
result.attenuation_db > 1.0 && result.attenuation_db < 6.0,
|
|
"{} dB",
|
|
result.attenuation_db
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_lower_frequency_suffers_less_rain_attenuation() {
|
|
let c_band = rain_attenuation(
|
|
20.0,
|
|
6.0e9,
|
|
HORIZONTAL,
|
|
THIRTY_DEGREES,
|
|
0.0,
|
|
DEFAULT_RAIN_HEIGHT_KM,
|
|
45.0,
|
|
)
|
|
.expect("valid");
|
|
let ku_band = rain_attenuation(
|
|
20.0,
|
|
12.0e9,
|
|
HORIZONTAL,
|
|
THIRTY_DEGREES,
|
|
0.0,
|
|
DEFAULT_RAIN_HEIGHT_KM,
|
|
45.0,
|
|
)
|
|
.expect("valid");
|
|
assert!(ku_band.attenuation_db > c_band.attenuation_db);
|
|
}
|
|
|
|
#[test]
|
|
fn a_station_above_the_rain_layer_has_no_rain_path() {
|
|
let result = rain_attenuation(
|
|
20.0,
|
|
12.0e9,
|
|
HORIZONTAL,
|
|
THIRTY_DEGREES,
|
|
4.0,
|
|
DEFAULT_RAIN_HEIGHT_KM,
|
|
45.0,
|
|
)
|
|
.expect("valid");
|
|
assert_eq!(result.attenuation_db, 0.0);
|
|
assert_eq!(result.path_length_km, 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn an_elevation_at_the_horizon_is_rejected() {
|
|
assert_eq!(
|
|
rain_attenuation(
|
|
20.0,
|
|
12.0e9,
|
|
HORIZONTAL,
|
|
0.0,
|
|
0.0,
|
|
DEFAULT_RAIN_HEIGHT_KM,
|
|
45.0
|
|
),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
rain_attenuation(
|
|
20.0,
|
|
12.0e9,
|
|
HORIZONTAL,
|
|
-0.1,
|
|
0.0,
|
|
DEFAULT_RAIN_HEIGHT_KM,
|
|
45.0
|
|
),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_latitude_enters_through_the_vertical_adjustment() {
|
|
// Below thirty six degrees the latitude term is nonzero, so the same
|
|
// geometry gives a different result at two latitudes.
|
|
let temperate = rain_attenuation(
|
|
20.0,
|
|
12.0e9,
|
|
HORIZONTAL,
|
|
THIRTY_DEGREES,
|
|
0.0,
|
|
DEFAULT_RAIN_HEIGHT_KM,
|
|
45.0,
|
|
)
|
|
.expect("valid");
|
|
let tropical = rain_attenuation(
|
|
20.0,
|
|
12.0e9,
|
|
HORIZONTAL,
|
|
THIRTY_DEGREES,
|
|
0.0,
|
|
DEFAULT_RAIN_HEIGHT_KM,
|
|
10.0,
|
|
)
|
|
.expect("valid");
|
|
assert!(
|
|
(temperate.vertical_adjustment - tropical.vertical_adjustment).abs() > 1e-6,
|
|
"{} vs {}",
|
|
temperate.vertical_adjustment,
|
|
tropical.vertical_adjustment
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn non_finite_geometry_is_rejected() {
|
|
assert_eq!(
|
|
rain_attenuation(
|
|
20.0,
|
|
12.0e9,
|
|
HORIZONTAL,
|
|
THIRTY_DEGREES,
|
|
f64::NAN,
|
|
3.0,
|
|
45.0
|
|
),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
rain_attenuation(20.0, 12.0e9, HORIZONTAL, THIRTY_DEGREES, 0.0, 3.0, f64::NAN),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn polarisation_tilt_converts_degrees() {
|
|
assert_eq!(polarisation_tilt(0.0), 0.0);
|
|
assert_relative(polarisation_tilt(90.0), FRAC_PI_2, "ninety degrees");
|
|
assert_relative(
|
|
polarisation_tilt(45.0),
|
|
FRAC_PI_2 / 2.0,
|
|
"forty five degrees",
|
|
);
|
|
}
|