type Term = (f64, f64, f64);
const KH: [Term; 4] = [
(-5.33980, -0.10008, 1.13098),
(-0.35351, 1.26970, 0.45400),
(-0.23789, 0.86036, 0.15354),
(-0.94158, 0.64552, 0.16817),
];
const KH_M: f64 = -0.18961;
const KH_C: f64 = 0.71147;
const KV: [Term; 4] = [
(-3.80595, 0.56934, 0.81061),
(-3.44965, -0.22911, 0.51059),
(-0.39902, 0.73042, 0.11899),
(0.50167, 1.07319, 0.27195),
];
const KV_M: f64 = -0.16398;
const KV_C: f64 = 0.63297;
const AH: [Term; 5] = [
(-0.14318, 1.82442, -0.55187),
(0.29591, 0.77564, 0.19822),
(0.32177, 0.63773, 0.13164),
(-5.37610, -0.96230, 1.47828),
(16.1721, -3.29980, 3.43990),
];
const AH_M: f64 = 0.67849;
const AH_C: f64 = -1.95537;
const AV: [Term; 5] = [
(-0.07771, 2.33840, -0.76284),
(0.56727, 0.95545, 0.54039),
(-0.20238, 1.14520, 0.26809),
(-48.2991, 0.791669, 0.116226),
(48.5833, 0.791459, 0.116479),
];
const AV_M: f64 = -0.053739;
const AV_C: f64 = 0.83433;
fn gauss_sum(terms: &[Term], lf: f64) -> f64 {
terms
.iter()
.map(|(a, b, c)| a * (-((lf - b) / c).powi(2)).exp())
.sum()
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RainCoefficients {
pub k_h: f64,
pub alpha_h: f64,
pub k_v: f64,
pub alpha_v: f64,
}
pub fn p838_coefficients(f_ghz: f64) -> RainCoefficients {
let lf = f_ghz.log10();
RainCoefficients {
k_h: 10f64.powf(gauss_sum(&KH, lf) + KH_M * lf + KH_C),
alpha_h: gauss_sum(&AH, lf) + AH_M * lf + AH_C,
k_v: 10f64.powf(gauss_sum(&KV, lf) + KV_M * lf + KV_C),
alpha_v: gauss_sum(&AV, lf) + AV_M * lf + AV_C,
}
}
pub fn p838_k_alpha(f_ghz: f64, el_deg: f64, tau_deg: f64) -> (f64, f64) {
let c = p838_coefficients(f_ghz);
let cos2el = el_deg.to_radians().cos().powi(2);
let cos2tau = (2.0 * tau_deg.to_radians()).cos();
let k = (c.k_h + c.k_v + (c.k_h - c.k_v) * cos2el * cos2tau) / 2.0;
let alpha = (c.k_h * c.alpha_h
+ c.k_v * c.alpha_v
+ (c.k_h * c.alpha_h - c.k_v * c.alpha_v) * cos2el * cos2tau)
/ (2.0 * k);
(k, alpha)
}
pub fn rain_specific_attenuation_db_km(
f_ghz: f64,
rain_mm_h: f64,
el_deg: f64,
tau_deg: f64,
) -> f64 {
if rain_mm_h <= 0.0 {
return 0.0;
}
let (k, a) = p838_k_alpha(f_ghz, el_deg, tau_deg);
k * rain_mm_h.powf(a)
}
#[derive(Clone, Copy, Debug)]
pub struct RainPath {
pub f_ghz: f64,
pub el_deg: f64,
pub lat_deg: f64,
pub hs_km: f64,
pub rain_height_km: f64,
pub r001_mm_h: f64,
pub tau_deg: f64,
}
const RE_EFF_KM: f64 = 8500.0;
pub fn p618_rain_attenuation_db(path: &RainPath, p_percent: f64) -> f64 {
let RainPath {
f_ghz: f,
el_deg,
lat_deg,
hs_km,
rain_height_km: hr,
r001_mm_h: r001,
tau_deg,
} = *path;
let dh = hr - hs_km;
if dh <= 0.0 || r001 <= 0.0 || el_deg <= 0.0 {
return 0.0;
}
let th = el_deg.to_radians();
let (s, c) = th.sin_cos();
let ls = if el_deg >= 5.0 {
dh / s
} else {
2.0 * dh / ((s * s + 2.0 * dh / RE_EFF_KM).sqrt() + s)
};
let lg = ls * c;
let gamma = rain_specific_attenuation_db_km(f, r001, el_deg, tau_deg);
let r = 1.0 / (1.0 + 0.78 * (lg * gamma / f).sqrt() - 0.38 * (1.0 - (-2.0 * lg).exp()));
let zeta = (dh / (lg * r)).atan().to_degrees();
let lr = if zeta > el_deg { lg * r / c } else { dh / s };
let chi = if lat_deg.abs() < 36.0 {
36.0 - lat_deg.abs()
} else {
0.0
};
let v = 1.0
/ (1.0
+ s.sqrt()
* (31.0 * (1.0 - (-(el_deg / (1.0 + chi))).exp()) * (lr * gamma).sqrt() / (f * f)
- 0.45));
let a001 = gamma * lr * v;
if a001 <= 0.0 {
return 0.0;
}
let p = p_percent;
let beta = if p >= 1.0 || lat_deg.abs() >= 36.0 {
0.0
} else if el_deg >= 25.0 {
-0.005 * (lat_deg.abs() - 36.0)
} else {
-0.005 * (lat_deg.abs() - 36.0) + 1.8 - 4.25 * s
};
let expo = -(0.655 + 0.033 * p.ln() - 0.045 * a001.ln() - beta * (1.0 - p) * s);
a001 * (p / 0.01).powf(expo)
}
pub fn wet_refractivity(t_c: f64, h_percent: f64, p_hpa: f64) -> f64 {
let ef = 1.0 + 1e-4 * (7.2 + p_hpa * (0.0320 + 5.9e-6 * t_c * t_c));
let es = ef * 6.1121 * ((18.678 - t_c / 234.5) * t_c / (t_c + 257.14)).exp();
let e = h_percent * es / 100.0;
let tk = t_c + 273.15;
72.0 * e / tk + 3.75e5 * e / (tk * tk)
}
#[derive(Clone, Copy, Debug)]
pub struct ScintillationPath {
pub f_ghz: f64,
pub el_deg: f64,
pub n_wet: f64,
pub diameter_m: f64,
pub efficiency: f64,
}
pub fn p618_scintillation_db(path: &ScintillationPath, p_percent: f64) -> (f64, bool) {
let in_range = (4.0..=55.0).contains(&path.f_ghz);
let th = path.el_deg.max(5.0).to_radians();
let s = th.sin();
let sigma_ref = 3.6e-3 + 1e-4 * path.n_wet;
let l = 2.0 * 1000.0 / ((s * s + 2.35e-4).sqrt() + s);
let d_eff = path.efficiency.max(0.0).sqrt() * path.diameter_m.max(0.0);
let x = 1.22 * d_eff * d_eff * (path.f_ghz / l);
let arg = if x <= 0.0 {
3.86 * (11.0 / 6.0 * std::f64::consts::FRAC_PI_2).sin()
} else {
3.86 * (x * x + 1.0).powf(11.0 / 12.0) * (11.0 / 6.0 * (1.0 / x).atan()).sin()
- 7.08 * x.powf(5.0 / 6.0)
};
if arg <= 0.0 {
return (0.0, in_range);
}
let g = arg.sqrt();
let sigma = sigma_ref * path.f_ghz.powf(7.0 / 12.0) * g / s.powf(1.2);
let lp = p_percent.log10();
let a = -0.061 * lp.powi(3) + 0.072 * lp * lp - 1.71 * lp + 3.0;
(a * sigma, in_range)
}
#[derive(Clone, Copy, Debug)]
pub struct Meteo {
pub pressure_hpa: f64,
pub temperature_c: f64,
pub water_vapour_g_m3: f64,
}
impl Default for Meteo {
fn default() -> Self {
Self {
pressure_hpa: 1013.0,
temperature_c: 15.0,
water_vapour_g_m3: 7.5,
}
}
}
fn phi(rp: f64, rt: f64, a: f64, b: f64, c: f64, d: f64) -> f64 {
rp.powf(a) * rt.powf(b) * (c * (1.0 - rp) + d * (1.0 - rt)).exp()
}
pub fn p676_gamma_oxygen_db_km(f_ghz: f64, m: &Meteo) -> f64 {
let rp = m.pressure_hpa / 1013.0;
let rt = 288.0 / (273.0 + m.temperature_c);
let f = f_ghz.min(54.0 - 1e-6);
let xi1 = phi(rp, rt, 0.0717, -1.8132, 0.0156, -1.6515);
let xi2 = phi(rp, rt, 0.5146, -4.6368, -0.1921, -5.7416);
let xi3 = phi(rp, rt, 0.3414, -6.5851, 0.2130, -8.5854);
(7.2 * rt.powf(2.8) / (f * f + 0.34 * rp * rp * rt.powf(1.6))
+ 0.62 * xi3 / ((54.0 - f).powf(1.16 * xi1) + 0.83 * xi2))
* f
* f
* rp
* rp
* 1e-3
}
pub fn p676_gamma_water_db_km(f_ghz: f64, m: &Meteo) -> f64 {
let rp = m.pressure_hpa / 1013.0;
let rt = 288.0 / (273.0 + m.temperature_c);
let rho = m.water_vapour_g_m3;
let f = f_ghz;
let eta1 = 0.955 * rp * rt.powf(0.68) + 0.006 * rho;
let eta2 = 0.735 * rp * rt.powf(0.5) + 0.0353 * rt.powi(4) * rho;
let g = |fi: f64| 1.0 + ((f - fi) / (f + fi)).powi(2);
let e1 = eta1 * eta1;
let sum = 3.98 * eta1 * (2.23 * (1.0 - rt)).exp() / ((f - 22.235).powi(2) + 9.42 * e1)
* g(22.0)
+ 11.96 * eta1 * (0.7 * (1.0 - rt)).exp() / ((f - 183.31).powi(2) + 11.14 * e1)
+ 0.081 * eta1 * (6.44 * (1.0 - rt)).exp() / ((f - 321.226).powi(2) + 6.29 * e1)
+ 3.66 * eta1 * (1.6 * (1.0 - rt)).exp() / ((f - 325.153).powi(2) + 9.22 * e1)
+ 25.37 * eta1 * (1.09 * (1.0 - rt)).exp() / (f - 380.0).powi(2)
+ 17.4 * eta1 * (1.46 * (1.0 - rt)).exp() / (f - 448.0).powi(2)
+ 844.6 * eta1 * (0.17 * (1.0 - rt)).exp() / (f - 557.0).powi(2) * g(557.0)
+ 290.0 * eta1 * (0.41 * (1.0 - rt)).exp() / (f - 752.0).powi(2) * g(752.0)
+ 8.3328e4 * eta2 * (0.99 * (1.0 - rt)).exp() / (f - 1780.0).powi(2) * g(1780.0);
sum * f * f * rt.powf(2.5) * rho * 1e-4
}
pub fn p676_oxygen_height_km(f_ghz: f64, m: &Meteo) -> f64 {
let rp = m.pressure_hpa / 1013.0;
let f = f_ghz;
let t1 = 4.64 / (1.0 + 0.066 * rp.powf(-2.3))
* (-((f - 59.7) / (2.87 + 12.4 * (-7.9 * rp).exp())).powi(2)).exp();
let t2 = 0.14 * (2.12 * rp).exp() / ((f - 118.75).powi(2) + 0.031 * (2.2 * rp).exp());
let t3 = 0.0114 / (1.0 + 0.14 * rp.powf(-2.6)) * f * (-0.0247 + 0.0001 * f + 1.61e-6 * f * f)
/ (1.0 - 0.0169 * f + 4.1e-5 * f * f + 3.2e-7 * f.powi(3));
let ho = 6.1 / (1.0 + 0.17 * rp.powf(-1.1)) * (1.0 + t1 + t2 + t3);
if f < 70.0 {
ho.min(10.7 * rp.powf(0.3))
} else {
ho
}
}
pub fn p676_water_height_km(f_ghz: f64, m: &Meteo) -> f64 {
let rp = m.pressure_hpa / 1013.0;
let f = f_ghz;
let sw = 1.013 / (1.0 + (-8.6 * (rp - 0.57)).exp());
1.66 * (1.0
+ 1.39 * sw / ((f - 22.235).powi(2) + 2.56 * sw)
+ 3.37 * sw / ((f - 183.31).powi(2) + 4.69 * sw)
+ 1.58 * sw / ((f - 325.1).powi(2) + 2.89 * sw))
}
pub fn p676_zenith_attenuation_db(f_ghz: f64, m: &Meteo) -> f64 {
p676_gamma_oxygen_db_km(f_ghz, m) * p676_oxygen_height_km(f_ghz, m)
+ p676_gamma_water_db_km(f_ghz, m) * p676_water_height_km(f_ghz, m)
}
pub fn p676_gaseous_attenuation_db(f_ghz: f64, el_deg: f64, m: &Meteo) -> f64 {
let ao = p676_gamma_oxygen_db_km(f_ghz, m) * p676_oxygen_height_km(f_ghz, m);
let aw = p676_gamma_water_db_km(f_ghz, m) * p676_water_height_km(f_ghz, m);
let el = el_deg.max(0.0);
if el >= 5.0 {
return (ao + aw) / el.to_radians().sin();
}
let airmass = |el_deg: f64, h_km: f64| {
let s = el_deg.to_radians().sin();
2.0 / ((s * s + 2.0 * h_km / 6371.0).sqrt() + s)
};
let s5 = 5f64.to_radians().sin();
let ho = p676_oxygen_height_km(f_ghz, m);
let hw = p676_water_height_km(f_ghz, m);
ao / s5 * airmass(el, ho) / airmass(5.0, ho) + aw / s5 * airmass(el, hw) / airmass(5.0, hw)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BuildingClass {
Traditional,
ThermallyEfficient,
}
impl BuildingClass {
pub fn parse(s: &str) -> Result<Self, String> {
match s {
"traditional" => Ok(Self::Traditional),
"thermally-efficient" => Ok(Self::ThermallyEfficient),
o => Err(format!(
"building class must be traditional or thermally-efficient; got '{o}'"
)),
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Traditional => "traditional",
Self::ThermallyEfficient => "thermally-efficient",
}
}
}
pub fn p2109_building_entry_loss_db(f_ghz: f64, p: f64, class: BuildingClass, el_deg: f64) -> f64 {
let (r, s, t, u, v, w, x, y, z) = match class {
BuildingClass::Traditional => (12.64, 3.72, 0.96, 9.6, 2.0, 9.1, -3.0, 4.5, -2.0),
BuildingClass::ThermallyEfficient => (28.19, -3.00, 8.48, 13.5, 3.8, 27.8, -2.9, 9.4, -2.1),
};
let lf = f_ghz.log10();
let lh = r + s * lf + t * lf * lf;
let le = 0.212 * el_deg.abs();
let mu1 = lh + le;
let mu2 = w + x * lf;
let sigma1 = u + v * lf;
let sigma2 = y + z * lf;
let finv = crate::detection::normal_inv_cdf(p);
let a = finv * sigma1 + mu1;
let b = finv * sigma2 + mu2;
let c = -3.0;
10.0 * (10f64.powf(0.1 * a) + 10f64.powf(0.1 * b) + 10f64.powf(0.1 * c)).log10()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rain_attenuation_is_zero_without_rain_or_above_the_rain_height() {
let mut p = RainPath {
f_ghz: 5.0,
el_deg: 30.0,
lat_deg: 45.0,
hs_km: 0.1,
rain_height_km: 3.0,
r001_mm_h: 0.0,
tau_deg: 45.0,
};
assert_eq!(p618_rain_attenuation_db(&p, 0.01), 0.0);
p.r001_mm_h = 40.0;
p.hs_km = 3.5;
assert_eq!(p618_rain_attenuation_db(&p, 0.01), 0.0);
}
#[test]
fn rain_grows_with_frequency_and_falls_with_percentage() {
let base = RainPath {
f_ghz: 2.5,
el_deg: 30.0,
lat_deg: 45.0,
hs_km: 0.1,
rain_height_km: 3.0,
r001_mm_h: 40.0,
tau_deg: 45.0,
};
let s = p618_rain_attenuation_db(&base, 0.01);
let c = p618_rain_attenuation_db(&RainPath { f_ghz: 5.0, ..base }, 0.01);
assert!(c > s && s > 0.0, "S {s} C {c}");
assert!(p618_rain_attenuation_db(&base, 1.0) < s);
}
#[test]
fn gaseous_attenuation_at_l_band_is_a_few_hundredths_of_a_db_at_zenith() {
let m = Meteo::default();
let z = p676_zenith_attenuation_db(1.2, &m);
assert!((0.02..0.06).contains(&z), "{z}");
let a5 = p676_gaseous_attenuation_db(1.2, 5.0, &m);
let a4999 = p676_gaseous_attenuation_db(1.2, 4.999, &m);
assert!((a5 - a4999).abs() < 0.02 * a5, "{a5} {a4999}");
assert!(p676_gaseous_attenuation_db(1.2, 0.0, &m).is_finite());
}
#[test]
fn building_entry_loss_median_rises_with_elevation_and_class() {
let t0 = p2109_building_entry_loss_db(0.465, 0.5, BuildingClass::Traditional, 0.0);
let t45 = p2109_building_entry_loss_db(0.465, 0.5, BuildingClass::Traditional, 45.0);
let e0 = p2109_building_entry_loss_db(0.465, 0.5, BuildingClass::ThermallyEfficient, 0.0);
assert!(t45 > t0 && e0 > t0, "{t0} {t45} {e0}");
}
#[test]
fn scintillation_on_a_small_antenna_uses_the_unit_averaging_factor() {
let (a, in_range) = p618_scintillation_db(
&ScintillationPath {
f_ghz: 5.0,
el_deg: 30.0,
n_wet: 60.0,
diameter_m: 0.0,
efficiency: 0.5,
},
1.0,
);
assert!(in_range && a > 0.0 && a < 1.0, "{a}");
let (_, below) = p618_scintillation_db(
&ScintillationPath {
f_ghz: 1.2,
el_deg: 30.0,
n_wet: 60.0,
diameter_m: 0.0,
efficiency: 0.5,
},
1.0,
);
assert!(!below);
}
}