use crate::astro::constants::physics::SPEED_OF_LIGHT_M_S;
use crate::validate;
use std::f64::consts::PI;
const FSPL_KM_MHZ_CONSTANT_DB: f64 = 32.45;
const DB_FIELD_DECADE: f64 = 20.0;
const DB_POWER_DECADE: f64 = 10.0;
const DBM_TO_DBW_OFFSET_DB: f64 = 30.0;
const BOLTZMANN_K_DBW_HZ_K: f64 = 228.6;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum RfError {
#[error("invalid RF input {field}: {reason}")]
InvalidInput {
field: &'static str,
reason: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LinkBudget {
pub eirp_dbw: f64,
pub fspl_db: f64,
pub receiver_gt_dbk: f64,
pub other_losses_db: f64,
pub required_cn0_dbhz: f64,
}
pub fn fspl(distance_km: f64, frequency_mhz: f64) -> Result<f64, RfError> {
let distance_km = rf_positive(distance_km, "distance_km")?;
let frequency_mhz = rf_positive(frequency_mhz, "frequency_mhz")?;
rf_finite_output(
FSPL_KM_MHZ_CONSTANT_DB
+ DB_FIELD_DECADE * frequency_mhz.log10()
+ DB_FIELD_DECADE * distance_km.log10(),
"fspl_db",
)
}
pub fn eirp(tx_power_dbm: f64, tx_antenna_gain_dbi: f64) -> Result<f64, RfError> {
let tx_power_dbm = rf_finite(tx_power_dbm, "tx_power_dbm")?;
let tx_antenna_gain_dbi = rf_finite(tx_antenna_gain_dbi, "tx_antenna_gain_dbi")?;
rf_finite_output(
tx_power_dbm + tx_antenna_gain_dbi - DBM_TO_DBW_OFFSET_DB,
"eirp_dbw",
)
}
pub fn cn0(
eirp_dbw: f64,
fspl_db: f64,
receiver_gt_dbk: f64,
other_losses_db: f64,
) -> Result<f64, RfError> {
let eirp_dbw = rf_finite(eirp_dbw, "eirp_dbw")?;
let fspl_db = rf_finite(fspl_db, "fspl_db")?;
let receiver_gt_dbk = rf_finite(receiver_gt_dbk, "receiver_gt_dbk")?;
let other_losses_db = rf_finite(other_losses_db, "other_losses_db")?;
rf_finite_output(
eirp_dbw + receiver_gt_dbk - fspl_db + BOLTZMANN_K_DBW_HZ_K - other_losses_db,
"cn0_dbhz",
)
}
pub fn link_margin(budget: &LinkBudget) -> Result<f64, RfError> {
let cn0_dbhz = cn0(
budget.eirp_dbw,
budget.fspl_db,
budget.receiver_gt_dbk,
budget.other_losses_db,
)?;
let required_cn0_dbhz = rf_finite(budget.required_cn0_dbhz, "required_cn0_dbhz")?;
rf_finite_output(cn0_dbhz - required_cn0_dbhz, "link_margin_db")
}
pub fn wavelength(frequency_hz: f64) -> Result<f64, RfError> {
let frequency_hz = rf_positive(frequency_hz, "frequency_hz")?;
rf_finite_output(SPEED_OF_LIGHT_M_S / frequency_hz, "wavelength_m")
}
pub fn dish_gain(diameter_m: f64, frequency_hz: f64, efficiency: f64) -> Result<f64, RfError> {
let diameter_m = rf_positive(diameter_m, "diameter_m")?;
let lambda = wavelength(frequency_hz)?;
let efficiency = rf_unit_efficiency(efficiency)?;
rf_finite_output(
DB_POWER_DECADE * (efficiency * (PI * diameter_m / lambda).powf(2.0)).log10(),
"dish_gain_dbi",
)
}
fn rf_finite(x: f64, field: &'static str) -> Result<f64, RfError> {
validate::finite(x, field).map_err(map_rf_input)
}
fn rf_positive(x: f64, field: &'static str) -> Result<f64, RfError> {
validate::finite_positive(x, field).map_err(map_rf_input)
}
fn rf_unit_efficiency(efficiency: f64) -> Result<f64, RfError> {
let efficiency = rf_positive(efficiency, "efficiency")?;
if efficiency <= 1.0 {
Ok(efficiency)
} else {
Err(invalid_rf_input("efficiency", "out of range"))
}
}
fn rf_finite_output(value: f64, field: &'static str) -> Result<f64, RfError> {
if value.is_finite() {
Ok(value)
} else {
Err(invalid_rf_input(field, "out of range"))
}
}
fn map_rf_input(error: validate::FieldError) -> RfError {
invalid_rf_input(error.field(), error.reason())
}
fn invalid_rf_input(field: &'static str, reason: &'static str) -> RfError {
RfError::InvalidInput { field, reason }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fspl_matches_frozen_elixir_bits() {
assert_eq!(
fspl(1200.0, 1616.0).unwrap().to_bits(),
158.20245204972383_f64.to_bits()
);
}
#[test]
fn eirp_matches_frozen_elixir_bits() {
assert_eq!(eirp(27.0, 3.0).unwrap().to_bits(), 0.0_f64.to_bits());
}
#[test]
fn cn0_matches_frozen_elixir_bits() {
assert_eq!(
cn0(0.0, 165.0, -12.0, 3.0).unwrap().to_bits(),
48.599999999999994_f64.to_bits()
);
}
#[test]
fn link_margin_matches_frozen_elixir_bits() {
let budget = LinkBudget {
eirp_dbw: 0.0,
fspl_db: 165.0,
receiver_gt_dbk: -12.0,
other_losses_db: 3.0,
required_cn0_dbhz: 35.0,
};
assert_eq!(
link_margin(&budget).unwrap().to_bits(),
13.599999999999994_f64.to_bits()
);
}
#[test]
fn wavelength_matches_frozen_elixir_bits() {
assert_eq!(
wavelength(1616.0e6).unwrap().to_bits(),
0.1855151349009901_f64.to_bits()
);
}
#[test]
fn dish_gain_matches_frozen_elixir_bits() {
assert_eq!(
dish_gain(1.0, 1616.0e6, 0.55).unwrap().to_bits(),
21.97903741903791_f64.to_bits()
);
}
#[test]
fn rf_helpers_reject_invalid_physical_domains() {
assert_invalid(
fspl(f64::NAN, 1616.0).unwrap_err(),
"distance_km",
"not finite",
);
assert_invalid(
fspl(0.0, 1616.0).unwrap_err(),
"distance_km",
"not positive",
);
assert_invalid(
fspl(1200.0, -1.0).unwrap_err(),
"frequency_mhz",
"not positive",
);
assert_invalid(
wavelength(f64::INFINITY).unwrap_err(),
"frequency_hz",
"not finite",
);
assert_invalid(wavelength(0.0).unwrap_err(), "frequency_hz", "not positive");
assert_invalid(
dish_gain(0.0, 1616.0e6, 0.55).unwrap_err(),
"diameter_m",
"not positive",
);
assert_invalid(
dish_gain(1.0, 1616.0e6, 0.0).unwrap_err(),
"efficiency",
"not positive",
);
assert_invalid(
dish_gain(1.0, 1616.0e6, 1.1).unwrap_err(),
"efficiency",
"out of range",
);
assert_invalid(
eirp(f64::NAN, 3.0).unwrap_err(),
"tx_power_dbm",
"not finite",
);
assert_invalid(
cn0(0.0, f64::INFINITY, -12.0, 3.0).unwrap_err(),
"fspl_db",
"not finite",
);
let budget = LinkBudget {
eirp_dbw: 0.0,
fspl_db: 165.0,
receiver_gt_dbk: -12.0,
other_losses_db: 3.0,
required_cn0_dbhz: f64::NEG_INFINITY,
};
assert_invalid(
link_margin(&budget).unwrap_err(),
"required_cn0_dbhz",
"not finite",
);
}
fn assert_invalid(error: RfError, field: &'static str, reason: &'static str) {
assert_eq!(error, RfError::InvalidInput { field, reason });
}
}