use super::turbulence::{
erfc_approx, AtmosphericPath, GammaGammaDistribution, LogNormalScintillation, TurbulenceRegime,
};
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FsoModulation {
OokIm,
Bppm { m: usize },
DpQpsk,
}
#[derive(Debug, Clone)]
pub enum AerosolType {
Maritime,
Continental,
Urban,
Desert,
Rain {
rate_mm_per_hr: f64,
},
Fog {
liquid_water_content: f64,
},
}
#[derive(Debug, Clone)]
pub struct AtmosphericExtinction {
pub visibility_km: f64,
pub wavelength: f64,
pub aerosol_type: AerosolType,
}
impl AtmosphericExtinction {
pub fn new(visibility_km: f64, wavelength: f64, aerosol: AerosolType) -> Self {
Self {
visibility_km: visibility_km.max(0.01),
wavelength,
aerosol_type: aerosol,
}
}
pub fn q_exponent(&self) -> f64 {
let v = self.visibility_km;
match &self.aerosol_type {
AerosolType::Rain { rate_mm_per_hr } => {
let _ = rate_mm_per_hr;
0.0
}
AerosolType::Fog { .. } => {
if v < 0.2 {
0.0
} else {
0.585 * v.powf(1.0 / 3.0)
}
}
_ => {
if v < 0.5 {
0.0
} else if v < 1.0 {
0.585 * v.powf(1.0 / 3.0)
} else if v < 6.0 {
1.3
} else {
1.6
}
}
}
}
pub fn extinction_coefficient_per_km(&self) -> f64 {
let v = self.visibility_km;
let lambda_um = self.wavelength * 1e6; let q = self.q_exponent();
match &self.aerosol_type {
AerosolType::Rain { rate_mm_per_hr } => {
let a = 1.076e-4;
let b = 0.67;
a * rate_mm_per_hr.powf(b)
}
AerosolType::Fog {
liquid_water_content,
} => {
let lwc = liquid_water_content.max(1e-6);
0.144 * lwc.powf(0.88)
}
_ => (3.91 / v) * (0.55 / lambda_um).powf(q),
}
}
pub fn transmission(&self, distance_km: f64) -> f64 {
let beta = self.extinction_coefficient_per_km();
(-beta * distance_km).exp()
}
pub fn loss_db_per_km(&self) -> f64 {
4.343 * self.extinction_coefficient_per_km()
}
}
#[derive(Debug, Clone)]
pub struct FsoLink {
pub wavelength: f64,
pub tx_power_dbm: f64,
pub tx_aperture_m: f64,
pub rx_aperture_m: f64,
pub link_distance_km: f64,
pub tx_divergence_mrad: f64,
pub pointing_loss_db: f64,
pub atmospheric: AtmosphericPath,
}
impl FsoLink {
pub fn new(wavelength: f64, tx_dbm: f64, tx_diam: f64, rx_diam: f64, dist_km: f64) -> Self {
let cn2_typical = 1e-15; let atm = AtmosphericPath::new_horizontal(dist_km, cn2_typical, wavelength);
let div_dl = 2.44 * wavelength / tx_diam; Self {
wavelength,
tx_power_dbm: tx_dbm,
tx_aperture_m: tx_diam,
rx_aperture_m: rx_diam,
link_distance_km: dist_km,
tx_divergence_mrad: div_dl * 1e3, pointing_loss_db: 0.0,
atmospheric: atm,
}
}
pub fn diffraction_limited_divergence_rad(&self) -> f64 {
2.44 * self.wavelength / self.tx_aperture_m
}
fn effective_divergence_rad(&self) -> f64 {
let dl = self.diffraction_limited_divergence_rad();
let req = self.tx_divergence_mrad * 1e-3;
req.max(dl)
}
pub fn free_space_loss_db(&self) -> f64 {
let r = self.link_distance_km * 1e3;
let lambda = self.wavelength;
20.0 * (4.0 * PI * r / lambda).log10()
}
pub fn geometric_loss_db(&self) -> f64 {
let theta = self.effective_divergence_rad(); let r = self.link_distance_km * 1e3; let spot_radius = theta * r / 2.0; let rx_radius = self.rx_aperture_m / 2.0;
if rx_radius >= spot_radius {
return 0.0; }
-20.0 * (rx_radius / spot_radius).log10()
}
pub fn atmospheric_loss_db(&self, visibility_km: f64) -> f64 {
let ext =
AtmosphericExtinction::new(visibility_km, self.wavelength, AerosolType::Continental);
ext.loss_db_per_km() * self.link_distance_km
}
pub fn received_power_dbm(&self, visibility_km: f64) -> f64 {
self.tx_power_dbm
- self.geometric_loss_db()
- self.atmospheric_loss_db(visibility_km)
- self.pointing_loss_db
}
pub fn link_margin_db(&self, sensitivity_dbm: f64, visibility_km: f64) -> f64 {
self.received_power_dbm(visibility_km) - sensitivity_dbm
}
pub fn aperture_averaging_factor(&self) -> f64 {
let l = self.link_distance_km * 1e3;
let d = self.rx_aperture_m;
let lambda = self.wavelength;
let phi = d * d / (4.0 * lambda * l); 1.0 / (1.0 + 1.062 * phi.powf(7.0 / 6.0))
}
pub fn effective_scintillation_index(&self) -> f64 {
let si = self.atmospheric.scintillation_index();
let a = self.aperture_averaging_factor();
si * a
}
pub fn fade_probability(&self, fade_depth_db: f64, visibility_km: f64) -> f64 {
let mean_p_dbm = self.received_power_dbm(visibility_km);
let threshold_dbm = mean_p_dbm - fade_depth_db;
self.outage_probability(threshold_dbm)
}
pub fn outage_probability(&self, sensitivity_dbm: f64) -> f64 {
let eff_si = self.effective_scintillation_index();
let mean_p_dbm = self.received_power_dbm(23.0); let mean_p_w = 1e-3 * 10.0_f64.powf(mean_p_dbm / 10.0);
let threshold_w = 1e-3 * 10.0_f64.powf(sensitivity_dbm / 10.0);
match self.atmospheric.regime() {
TurbulenceRegime::Weak => {
let ln_scint = LogNormalScintillation::new(eff_si, mean_p_w);
ln_scint.cdf(threshold_w)
}
_ => {
let gg = GammaGammaDistribution::from_scintillation_index(eff_si, mean_p_w);
gg.outage_probability(threshold_w)
}
}
}
pub fn average_ber(&self, sensitivity_dbm: f64, modulation: FsoModulation) -> f64 {
let eff_si = self.effective_scintillation_index();
let mean_p_dbm = self.received_power_dbm(23.0);
let snr_db = mean_p_dbm - sensitivity_dbm;
match modulation {
FsoModulation::OokIm => match self.atmospheric.regime() {
TurbulenceRegime::Weak => {
let mean_p_w = 1e-3 * 10.0_f64.powf(mean_p_dbm / 10.0);
let ln_scint = LogNormalScintillation::new(eff_si, mean_p_w);
let penalty = ln_scint.mean_snr_penalty_db();
let effective_snr_db = snr_db - penalty;
let snr = 10.0_f64.powf(effective_snr_db / 10.0);
0.5 * erfc_approx((snr / 2.0).sqrt())
}
_ => {
let mean_p_w = 1e-3 * 10.0_f64.powf(mean_p_dbm / 10.0);
let gg = GammaGammaDistribution::from_scintillation_index(eff_si, mean_p_w);
gg.mean_ber(snr_db)
}
},
FsoModulation::Bppm { m } => {
let m_f = m as f64;
let snr = 10.0_f64.powf(snr_db / 10.0);
let arg = (m_f.log2() * snr / m_f).sqrt();
(m_f / 2.0) * erfc_approx(arg)
}
FsoModulation::DpQpsk => {
let snr = 10.0_f64.powf(snr_db / 10.0);
erfc_approx(snr.sqrt())
}
}
}
pub fn max_range_km(&self, min_margin_db: f64, visibility_km: f64) -> f64 {
let mut lo = 0.1_f64;
let mut hi = 10_000.0_f64;
for _ in 0..60 {
let mid = (lo + hi) / 2.0;
let mut trial = self.clone();
trial.link_distance_km = mid;
trial.atmospheric.length_km = mid;
let margin = trial.link_margin_db(
self.tx_power_dbm - 60.0, visibility_km,
);
if margin >= min_margin_db {
lo = mid;
} else {
hi = mid;
}
}
lo
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_link() -> FsoLink {
FsoLink::new(1550e-9, 30.0, 0.1, 0.2, 1.0)
}
#[test]
fn test_geometric_loss_non_negative() {
let link = default_link();
assert!(link.geometric_loss_db() >= 0.0);
}
#[test]
fn test_received_power_less_than_tx() {
let link = default_link();
assert!(link.received_power_dbm(23.0) < link.tx_power_dbm);
}
#[test]
fn test_atmospheric_loss_vs_distance() {
let link1 = FsoLink::new(1550e-9, 30.0, 0.1, 0.2, 1.0);
let link5 = FsoLink::new(1550e-9, 30.0, 0.1, 0.2, 5.0);
assert!(link5.atmospheric_loss_db(10.0) > link1.atmospheric_loss_db(10.0));
}
#[test]
fn test_aperture_averaging_factor_range() {
let link = default_link();
let a = link.aperture_averaging_factor();
assert!(a > 0.0 && a <= 1.0, "A = {a:.4}");
}
#[test]
fn test_continental_extinction_clear() {
let ext = AtmosphericExtinction::new(10.0, 1550e-9, AerosolType::Continental);
let loss = ext.loss_db_per_km();
assert!(
loss < 1.0,
"Loss = {loss:.3} dB/km (expected < 1 in clear air)"
);
}
#[test]
fn test_rain_extinction() {
let ext_light = AtmosphericExtinction::new(
5.0,
1550e-9,
AerosolType::Rain {
rate_mm_per_hr: 1.0,
},
);
let ext_heavy = AtmosphericExtinction::new(
5.0,
1550e-9,
AerosolType::Rain {
rate_mm_per_hr: 50.0,
},
);
assert!(
ext_heavy.extinction_coefficient_per_km() > ext_light.extinction_coefficient_per_km()
);
}
#[test]
fn test_diffraction_limited_divergence() {
let link = FsoLink::new(1550e-9, 30.0, 0.1, 0.2, 1.0);
let div_rad = link.diffraction_limited_divergence_rad();
assert!((div_rad - 37.78e-6).abs() < 1e-7, "div = {div_rad:.4e} rad");
}
#[test]
fn test_margin_vs_distance() {
let link_near = FsoLink::new(1550e-9, 30.0, 0.1, 0.2, 1.0);
let link_far = FsoLink::new(1550e-9, 30.0, 0.1, 0.2, 5.0);
let m_near = link_near.link_margin_db(-40.0, 23.0);
let m_far = link_far.link_margin_db(-40.0, 23.0);
assert!(m_near > m_far);
}
}