474 lines
16 KiB
Rust
474 lines
16 KiB
Rust
//! Rain attenuation.
|
|
//!
|
|
//! The specific attenuation of rain is modelled as a power law in the rain
|
|
//! rate, `gamma = k R^alpha`, where `k` and `alpha` depend on the frequency,
|
|
//! on the polarisation, and on the elevation. The coefficients are given by
|
|
//! ITU-R Recommendation P.838 as closed form curve fits rather than as a
|
|
//! table, which is what the current revision of that recommendation uses.
|
|
//!
|
|
//! The attenuation along a slant path follows ITU-R Recommendation P.618.
|
|
//! The path length through the rain is limited by the height of the rain
|
|
//! layer above the station, and two reduction factors account for the fact
|
|
//! that the rain rate is not uniform along the path: a horizontal reduction
|
|
//! and a vertical adjustment. Both depend on the specific attenuation, so
|
|
//! the calculation is not a simple product of a rate and a length.
|
|
//!
|
|
//! Sources:
|
|
//!
|
|
//! - Specific attenuation coefficients: ITU-R Recommendation P.838-3,
|
|
//! *Specific attenuation model for rain for use in prediction methods*,
|
|
//! equations 2 and 3 and tables 1 and 2.
|
|
//! - Slant path method: ITU-R Recommendation P.618-13, *Propagation data and
|
|
//! prediction methods required for the design of Earth-space
|
|
//! telecommunication systems*, equations 1 through 10.
|
|
|
|
use apsis_rf::Decibel;
|
|
|
|
/// The effective radius of the Earth used by the path geometry, in
|
|
/// kilometers.
|
|
///
|
|
/// The value accounts for the refraction of the ray in the lower atmosphere,
|
|
/// which makes the Earth appear larger than it is.
|
|
const EFFECTIVE_EARTH_RADIUS_KM: f64 = 8_500.0;
|
|
|
|
/// A mid latitude rain height, in kilometers above sea level.
|
|
///
|
|
/// Recommendation P.618 takes the rain height from the zero degree isotherm
|
|
/// height of Recommendation P.839, which is tabulated as a function of
|
|
/// latitude and longitude. Prefer [`rain_attenuation_at`], which looks the
|
|
/// value up, over [`rain_attenuation`] with this constant.
|
|
pub const DEFAULT_RAIN_HEIGHT_KM: f64 = 3.0;
|
|
|
|
/// One term of a P.838-3 curve fit.
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct CurveTerm {
|
|
/// The amplitude coefficient.
|
|
amplitude: f64,
|
|
/// The centre of the term, on a base ten logarithmic frequency axis.
|
|
centre: f64,
|
|
/// The width of the term, on a base ten logarithmic frequency axis.
|
|
width: f64,
|
|
}
|
|
|
|
/// The coefficients of the `k` curve fit for horizontal polarisation.
|
|
const K_HORIZONTAL_TERMS: &[CurveTerm] = &[
|
|
CurveTerm {
|
|
amplitude: -5.339_80,
|
|
centre: -0.100_08,
|
|
width: 1.130_98,
|
|
},
|
|
CurveTerm {
|
|
amplitude: -0.353_51,
|
|
centre: 1.269_70,
|
|
width: 0.454_00,
|
|
},
|
|
CurveTerm {
|
|
amplitude: -0.237_89,
|
|
centre: 0.860_36,
|
|
width: 0.153_54,
|
|
},
|
|
CurveTerm {
|
|
amplitude: -0.941_58,
|
|
centre: 0.645_52,
|
|
width: 0.168_17,
|
|
},
|
|
];
|
|
|
|
/// The coefficients of the `k` curve fit for vertical polarisation.
|
|
const K_VERTICAL_TERMS: &[CurveTerm] = &[
|
|
CurveTerm {
|
|
amplitude: -3.805_95,
|
|
centre: 0.569_34,
|
|
width: 0.810_61,
|
|
},
|
|
CurveTerm {
|
|
amplitude: -3.449_65,
|
|
centre: -0.229_11,
|
|
width: 0.510_59,
|
|
},
|
|
CurveTerm {
|
|
amplitude: -0.399_02,
|
|
centre: 0.730_42,
|
|
width: 0.118_99,
|
|
},
|
|
CurveTerm {
|
|
amplitude: 0.501_67,
|
|
centre: 1.073_19,
|
|
width: 0.271_95,
|
|
},
|
|
];
|
|
|
|
/// The coefficients of the `alpha` curve fit for horizontal polarisation.
|
|
const ALPHA_HORIZONTAL_TERMS: &[CurveTerm] = &[
|
|
CurveTerm {
|
|
amplitude: -0.143_18,
|
|
centre: 1.824_42,
|
|
width: -0.551_87,
|
|
},
|
|
CurveTerm {
|
|
amplitude: 0.295_91,
|
|
centre: 0.775_64,
|
|
width: 0.198_22,
|
|
},
|
|
CurveTerm {
|
|
amplitude: 0.321_77,
|
|
centre: 0.637_73,
|
|
width: 0.131_64,
|
|
},
|
|
CurveTerm {
|
|
amplitude: -5.376_10,
|
|
centre: -0.962_30,
|
|
width: 1.478_28,
|
|
},
|
|
CurveTerm {
|
|
amplitude: 16.172_1,
|
|
centre: -3.299_80,
|
|
width: 3.439_90,
|
|
},
|
|
];
|
|
|
|
/// The coefficients of the `alpha` curve fit for vertical polarisation.
|
|
const ALPHA_VERTICAL_TERMS: &[CurveTerm] = &[
|
|
CurveTerm {
|
|
amplitude: -0.077_71,
|
|
centre: 2.338_40,
|
|
width: -0.762_84,
|
|
},
|
|
CurveTerm {
|
|
amplitude: 0.567_27,
|
|
centre: 0.955_45,
|
|
width: 0.540_39,
|
|
},
|
|
CurveTerm {
|
|
amplitude: -0.202_38,
|
|
centre: 1.145_20,
|
|
width: 0.268_09,
|
|
},
|
|
CurveTerm {
|
|
amplitude: -48.299_1,
|
|
centre: 0.791_669,
|
|
width: 0.116_226,
|
|
},
|
|
CurveTerm {
|
|
amplitude: 48.583_3,
|
|
centre: 0.791_459,
|
|
width: 0.116_479,
|
|
},
|
|
];
|
|
|
|
/// The linear coefficients of the `k` curve fits.
|
|
const K_SLOPE_HORIZONTAL: f64 = -0.189_61;
|
|
/// The constant of the horizontal `k` curve fit.
|
|
const K_OFFSET_HORIZONTAL: f64 = 0.711_47;
|
|
/// The linear coefficient of the vertical `k` curve fit.
|
|
const K_SLOPE_VERTICAL: f64 = -0.163_98;
|
|
/// The constant of the vertical `k` curve fit.
|
|
const K_OFFSET_VERTICAL: f64 = 0.632_97;
|
|
/// The linear coefficient of the horizontal `alpha` curve fit.
|
|
const ALPHA_SLOPE_HORIZONTAL: f64 = 0.678_49;
|
|
/// The constant of the horizontal `alpha` curve fit.
|
|
const ALPHA_OFFSET_HORIZONTAL: f64 = -1.955_37;
|
|
/// The linear coefficient of the vertical `alpha` curve fit.
|
|
const ALPHA_SLOPE_VERTICAL: f64 = -0.053_739;
|
|
/// The constant of the vertical `alpha` curve fit.
|
|
const ALPHA_OFFSET_VERTICAL: f64 = 0.834_33;
|
|
|
|
/// A rain attenuation result at a given elevation.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub struct RainAttenuation {
|
|
/// The specific attenuation, in decibels per kilometer.
|
|
pub specific_db_per_km: f64,
|
|
/// The effective path length through the rain, in kilometers.
|
|
pub path_length_km: f64,
|
|
/// The total attenuation along the path, in decibels.
|
|
pub attenuation_db: f64,
|
|
/// The horizontal reduction factor, dimensionless and at most one.
|
|
pub horizontal_reduction: f64,
|
|
/// The vertical adjustment factor, dimensionless.
|
|
///
|
|
/// The factor is the ratio of the effective path length to the longer
|
|
/// of the two geometric path estimates. It is at most one when the path
|
|
/// meets the rain layer at a steep angle and can exceed one at a shallow
|
|
/// angle, which is the behaviour Recommendation P.618 gives.
|
|
pub vertical_adjustment: f64,
|
|
}
|
|
|
|
impl RainAttenuation {
|
|
/// Returns the total attenuation.
|
|
pub fn attenuation(self) -> Decibel {
|
|
Decibel::from_db(self.attenuation_db)
|
|
}
|
|
|
|
/// Returns `true` when every value is finite and not negative.
|
|
pub fn is_valid(self) -> bool {
|
|
self.specific_db_per_km.is_finite()
|
|
&& self.specific_db_per_km >= 0.0
|
|
&& self.path_length_km.is_finite()
|
|
&& self.path_length_km >= 0.0
|
|
&& self.attenuation_db.is_finite()
|
|
&& self.attenuation_db >= 0.0
|
|
}
|
|
}
|
|
|
|
/// Evaluates one P.838-3 curve term.
|
|
///
|
|
/// The term is a Gaussian on a base ten logarithmic frequency axis, so the
|
|
/// frequency must be in gigahertz and positive.
|
|
fn curve_term(term: CurveTerm, frequency_ghz: f64) -> f64 {
|
|
let logarithm = frequency_ghz.log10();
|
|
term.amplitude * (-((logarithm - term.centre) / term.width).powi(2)).exp()
|
|
}
|
|
|
|
/// Returns the `k` for a polarisation, before the elevation correction.
|
|
fn base_k(terms: &[CurveTerm], slope: f64, offset: f64, frequency_ghz: f64) -> f64 {
|
|
let gaussian: f64 = terms
|
|
.iter()
|
|
.map(|term| curve_term(*term, frequency_ghz))
|
|
.sum();
|
|
10f64.powf(gaussian + slope * frequency_ghz.log10() + offset)
|
|
}
|
|
|
|
/// Returns the `alpha` for a polarisation, before the elevation correction.
|
|
fn base_alpha(terms: &[CurveTerm], slope: f64, offset: f64, frequency_ghz: f64) -> f64 {
|
|
let gaussian: f64 = terms
|
|
.iter()
|
|
.map(|term| curve_term(*term, frequency_ghz))
|
|
.sum();
|
|
gaussian + slope * frequency_ghz.log10() + offset
|
|
}
|
|
|
|
/// Returns the P.838-3 coefficients of the specific attenuation power law.
|
|
///
|
|
/// The polarisation is an angle in radians between the electric field and
|
|
/// the horizontal, so zero is horizontal and a right angle is vertical. The
|
|
/// elevation is in radians.
|
|
///
|
|
/// The result is a pair of the coefficient `k` and the exponent `alpha` of
|
|
/// the relation `gamma = k R^alpha`, with the rain rate in millimeters per
|
|
/// hour and the result in decibels per kilometer.
|
|
///
|
|
/// Returns `None` when the frequency, polarisation, or elevation is not
|
|
/// usable. The recommendation is stated for frequencies from 1 GHz to
|
|
/// 1000 GHz.
|
|
///
|
|
/// Source: ITU-R Recommendation P.838-3, equations 2 and 3.
|
|
pub fn rain_coefficients(frequency: f64, polarisation: f64, elevation: f64) -> Option<(f64, f64)> {
|
|
let frequency_ghz = frequency / 1.0e9;
|
|
if !frequency_ghz.is_finite() || !(1.0..=1_000.0).contains(&frequency_ghz) {
|
|
return None;
|
|
}
|
|
if !polarisation.is_finite() || !elevation.is_finite() {
|
|
return None;
|
|
}
|
|
|
|
let k_horizontal = base_k(
|
|
K_HORIZONTAL_TERMS,
|
|
K_SLOPE_HORIZONTAL,
|
|
K_OFFSET_HORIZONTAL,
|
|
frequency_ghz,
|
|
);
|
|
let k_vertical = base_k(
|
|
K_VERTICAL_TERMS,
|
|
K_SLOPE_VERTICAL,
|
|
K_OFFSET_VERTICAL,
|
|
frequency_ghz,
|
|
);
|
|
let alpha_horizontal = base_alpha(
|
|
ALPHA_HORIZONTAL_TERMS,
|
|
ALPHA_SLOPE_HORIZONTAL,
|
|
ALPHA_OFFSET_HORIZONTAL,
|
|
frequency_ghz,
|
|
);
|
|
let alpha_vertical = base_alpha(
|
|
ALPHA_VERTICAL_TERMS,
|
|
ALPHA_SLOPE_VERTICAL,
|
|
ALPHA_OFFSET_VERTICAL,
|
|
frequency_ghz,
|
|
);
|
|
|
|
// The elevation and polarisation enter through the square of the cosine
|
|
// of the elevation angle and the cosine of twice the polarisation tilt.
|
|
// The elevation term vanishes at the zenith, where the distinction
|
|
// between horizontal and vertical polarisation disappears.
|
|
let geometry_factor = elevation.cos().powi(2) * (2.0 * polarisation).cos();
|
|
let k = 0.5 * (k_horizontal + k_vertical + (k_horizontal - k_vertical) * geometry_factor);
|
|
let alpha = (k_horizontal * alpha_horizontal
|
|
+ k_vertical * alpha_vertical
|
|
+ (k_horizontal * alpha_horizontal - k_vertical * alpha_vertical) * geometry_factor)
|
|
/ (2.0 * k);
|
|
Some((k, alpha))
|
|
}
|
|
|
|
/// Returns the specific attenuation in rain, in decibels per kilometer.
|
|
///
|
|
/// The rain rate is in millimeters per hour.
|
|
///
|
|
/// Returns `None` when the rain rate is negative, or when the coefficients
|
|
/// are not available.
|
|
pub fn specific_rain_attenuation(
|
|
rain_rate: f64,
|
|
frequency: f64,
|
|
polarisation: f64,
|
|
elevation: f64,
|
|
) -> Option<f64> {
|
|
if !rain_rate.is_finite() || rain_rate < 0.0 {
|
|
return None;
|
|
}
|
|
let (k, alpha) = rain_coefficients(frequency, polarisation, elevation)?;
|
|
Some(k * rain_rate.powf(alpha))
|
|
}
|
|
|
|
/// Returns the total rain attenuation along a slant path.
|
|
///
|
|
/// The rain rate is the value exceeded for the percentage of time of
|
|
/// interest, in millimeters per hour. The station height above sea level and
|
|
/// the rain layer height are in kilometers; pass [`DEFAULT_RAIN_HEIGHT_KM`]
|
|
/// for the latter when no local value is available.
|
|
///
|
|
/// The method is the one of Recommendation P.618. The slant path length
|
|
/// follows from the rain height, the station height, and the elevation, with
|
|
/// a correction for the curvature of the Earth. Two reduction factors then
|
|
/// account for the non-uniformity of the rain: a horizontal factor that
|
|
/// depends on the specific attenuation, the frequency, and the horizontal
|
|
/// projection of the path, and a vertical factor that depends on the
|
|
/// latitude through the angle at which the path crosses the rain layer.
|
|
///
|
|
/// Returns `None` when the rain rate, frequency, or elevation is not usable,
|
|
/// when the station is above the rain layer, or when the elevation is at or
|
|
/// below the horizon.
|
|
///
|
|
/// Source: ITU-R Recommendation P.618-13, equations 1 through 10.
|
|
pub fn rain_attenuation(
|
|
rain_rate: f64,
|
|
frequency: f64,
|
|
polarisation: f64,
|
|
elevation: f64,
|
|
station_height_km: f64,
|
|
rain_height_km: f64,
|
|
latitude: f64,
|
|
) -> Option<RainAttenuation> {
|
|
if !rain_rate.is_finite() || rain_rate < 0.0 {
|
|
return None;
|
|
}
|
|
if !elevation.is_finite() || elevation.sin() <= 0.0 {
|
|
return None;
|
|
}
|
|
if !station_height_km.is_finite() || !rain_height_km.is_finite() {
|
|
return None;
|
|
}
|
|
if !latitude.is_finite() {
|
|
return None;
|
|
}
|
|
let specific = specific_rain_attenuation(rain_rate, frequency, polarisation, elevation)?;
|
|
|
|
// Equation 2: the slant path length, with the Earth curvature term.
|
|
let height_difference = rain_height_km - station_height_km;
|
|
if height_difference <= 0.0 {
|
|
// The station is at or above the rain layer, so the path is empty.
|
|
return Some(RainAttenuation {
|
|
specific_db_per_km: specific,
|
|
path_length_km: 0.0,
|
|
attenuation_db: 0.0,
|
|
horizontal_reduction: 1.0,
|
|
vertical_adjustment: 1.0,
|
|
});
|
|
}
|
|
let sine = elevation.sin();
|
|
let slant_length = 2.0 * height_difference
|
|
/ ((sine * sine + 2.0 * height_difference / EFFECTIVE_EARTH_RADIUS_KM).sqrt() + sine);
|
|
|
|
// Equation 3: the horizontal projection of the slant path.
|
|
let horizontal_length = (slant_length * elevation.cos()).abs();
|
|
|
|
// Equation 6: the horizontal reduction factor.
|
|
let frequency_ghz = frequency / 1.0e9;
|
|
let horizontal_reduction = 1.0
|
|
/ (1.0 + 0.78 * (horizontal_length * specific / frequency_ghz).sqrt()
|
|
- 0.38 * (1.0 - (-2.0 * horizontal_length).exp()));
|
|
|
|
// Equation 7: the angle at which the path crosses the rain layer.
|
|
let eta = (height_difference / (horizontal_length * horizontal_reduction)).atan();
|
|
|
|
// Equation 8: the longer of the two path estimates.
|
|
let path_length = if eta > elevation {
|
|
horizontal_length * horizontal_reduction / elevation.cos()
|
|
} else {
|
|
height_difference / sine
|
|
};
|
|
|
|
// Equation 9: the vertical adjustment factor.
|
|
//
|
|
// The exponential takes the elevation in degrees, not radians, because
|
|
// the coefficients of 31 and 0.45 were fitted with degrees. The sine
|
|
// takes the elevation in radians, as usual.
|
|
let latitude_magnitude = latitude.abs();
|
|
let xi = if latitude_magnitude < 36.0 {
|
|
36.0 - latitude_magnitude
|
|
} else {
|
|
0.0
|
|
};
|
|
let elevation_degrees = elevation.to_degrees();
|
|
let vertical_adjustment = 1.0
|
|
/ (1.0
|
|
+ sine.sqrt()
|
|
* (31.0
|
|
* (1.0 - (-(elevation_degrees / (1.0 + xi))).exp())
|
|
* (path_length * specific).sqrt()
|
|
/ frequency_ghz.powi(2)
|
|
- 0.45));
|
|
|
|
// Equation 10: the effective path length and the attenuation.
|
|
let effective_length = path_length * vertical_adjustment;
|
|
Some(RainAttenuation {
|
|
specific_db_per_km: specific,
|
|
path_length_km: effective_length,
|
|
attenuation_db: specific * effective_length,
|
|
horizontal_reduction,
|
|
vertical_adjustment,
|
|
})
|
|
}
|
|
|
|
/// Returns the angle in radians for a polarisation described by a tilt from
|
|
/// horizontal.
|
|
///
|
|
/// This is a convenience for the two cases that are quoted by name.
|
|
pub const fn polarisation_tilt(degrees_from_horizontal: f64) -> f64 {
|
|
degrees_from_horizontal * core::f64::consts::PI / 180.0
|
|
}
|
|
|
|
/// Returns the total rain attenuation along a slant path, taking the rain
|
|
/// height from the Recommendation P.839 model.
|
|
///
|
|
/// This is the same calculation as [`rain_attenuation`], with the rain layer
|
|
/// height looked up from the tabulated zero degree isotherm height at the
|
|
/// given latitude and longitude rather than supplied by the caller. It is
|
|
/// the form to use unless a locally measured isotherm height is available.
|
|
///
|
|
/// The station height must be given; pass zero for a station at sea level.
|
|
///
|
|
/// Returns `None` when the coordinates are not usable, or when any of the
|
|
/// other arguments is rejected by [`rain_attenuation`].
|
|
///
|
|
/// Source: the rain height is from ITU-R Recommendation P.839-4 and the path
|
|
/// method from ITU-R Recommendation P.618-13.
|
|
pub fn rain_attenuation_at(
|
|
rain_rate: f64,
|
|
frequency: f64,
|
|
polarisation: f64,
|
|
elevation: f64,
|
|
station_height_km: f64,
|
|
latitude: f64,
|
|
longitude: f64,
|
|
) -> Option<RainAttenuation> {
|
|
let rain_height_km = crate::p839::rain_height(latitude, longitude)?;
|
|
rain_attenuation(
|
|
rain_rate,
|
|
frequency,
|
|
polarisation,
|
|
elevation,
|
|
station_height_km,
|
|
rain_height_km,
|
|
latitude,
|
|
)
|
|
}
|