rf: add link budget with gain to temperature figure
This commit is contained in:
parent
9a4b5bf9e5
commit
7d08f900a9
|
|
@ -101,6 +101,12 @@ version = "0.1.0"
|
|||
[[package]]
|
||||
name = "apsis-link"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"apsis-antenna",
|
||||
"apsis-core",
|
||||
"apsis-noise",
|
||||
"apsis-rf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "apsis-macros"
|
||||
|
|
|
|||
|
|
@ -11,3 +11,7 @@ publish = false
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
apsis-core.workspace = true
|
||||
apsis-rf.workspace = true
|
||||
apsis-noise.workspace = true
|
||||
apsis-antenna.workspace = true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,261 @@
|
|||
//! The link budget and the figures derived from it.
|
||||
|
||||
use apsis_core::constant::BOLTZMANN;
|
||||
use apsis_noise::NoiseTemperature;
|
||||
use apsis_rf::{Decibel, Gain, Power, free_space_path_loss, wavelength};
|
||||
|
||||
/// The geometric part of a link: how far apart the terminals are and at what
|
||||
/// frequency they operate.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct LinkGeometry {
|
||||
/// The distance between the terminals, in meters.
|
||||
pub distance: f64,
|
||||
/// The carrier frequency, in hertz.
|
||||
pub frequency: f64,
|
||||
}
|
||||
|
||||
impl LinkGeometry {
|
||||
/// Returns the free space path loss for this geometry.
|
||||
///
|
||||
/// Returns `None` when the distance or frequency is not usable.
|
||||
pub fn path_loss(self) -> Option<Decibel> {
|
||||
free_space_path_loss(self.distance, self.frequency)
|
||||
}
|
||||
|
||||
/// Returns `true` when the geometry is usable.
|
||||
pub fn is_valid(self) -> bool {
|
||||
self.distance.is_finite()
|
||||
&& self.distance > 0.0
|
||||
&& self.frequency.is_finite()
|
||||
&& self.frequency > 0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A complete link budget for one direction of a radio link.
|
||||
///
|
||||
/// The losses are given as positive decibel values that are subtracted,
|
||||
/// which is the convention in link budgets: a three decibel loss is entered
|
||||
/// as three decibels, not as minus three.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct LinkBudget {
|
||||
/// The power delivered to the transmit antenna, in watts.
|
||||
pub transmit_power: Power,
|
||||
/// The transmit antenna gain on the relevant path.
|
||||
pub transmit_gain: Gain,
|
||||
/// Every loss between the transmitter and the transmit antenna, as a
|
||||
/// positive decibel value.
|
||||
pub transmit_loss: Decibel,
|
||||
/// The receive antenna gain on the relevant path.
|
||||
pub receive_gain: Gain,
|
||||
/// Every loss between the receive antenna and the receiver input, as a
|
||||
/// positive decibel value.
|
||||
pub receive_loss: Decibel,
|
||||
/// The system noise temperature referred to the receiver input.
|
||||
pub system_temperature: NoiseTemperature,
|
||||
/// The receiver noise bandwidth, in hertz.
|
||||
pub bandwidth: f64,
|
||||
/// The distance and frequency of the link.
|
||||
pub geometry: LinkGeometry,
|
||||
}
|
||||
|
||||
impl LinkBudget {
|
||||
/// Returns the equivalent isotropically radiated power, in watts.
|
||||
///
|
||||
/// This is the transmit power reduced by the transmit losses and
|
||||
/// increased by the transmit antenna gain. It is the power that an
|
||||
/// isotropic radiator would have to radiate to produce the same flux
|
||||
/// density in the direction of the receiver.
|
||||
///
|
||||
/// Returns `None` when any input is not usable.
|
||||
pub fn eirp(&self) -> Option<Power> {
|
||||
if !self.transmit_loss.is_finite() || !self.geometry.is_valid() {
|
||||
return None;
|
||||
}
|
||||
let gain = self.transmit_gain.as_decibel()?;
|
||||
let net = gain - self.transmit_loss;
|
||||
self.transmit_power.scaled(net)
|
||||
}
|
||||
|
||||
/// Returns the carrier power at the receiver input, in watts.
|
||||
///
|
||||
/// Returns `None` when any input is not usable.
|
||||
pub fn received_power(&self) -> Option<Power> {
|
||||
if !self.receive_loss.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let eirp = self.eirp()?;
|
||||
let receive_gain = self.receive_gain.as_decibel()?;
|
||||
let loss = self.geometry.path_loss()?;
|
||||
eirp.scaled(receive_gain - loss - self.receive_loss)
|
||||
}
|
||||
|
||||
/// Returns the noise power density at the receiver input, in watts per
|
||||
/// hertz.
|
||||
///
|
||||
/// This is `k T`, the noise temperature converted to a power per unit
|
||||
/// bandwidth. It does not depend on the receiver bandwidth, which is
|
||||
/// what makes the carrier to noise density ratio a property of the link
|
||||
/// rather than of the receiver.
|
||||
///
|
||||
/// Returns `None` when the system temperature is not usable.
|
||||
pub fn noise_density(&self) -> Option<f64> {
|
||||
if !self.system_temperature.is_valid() {
|
||||
return None;
|
||||
}
|
||||
Some(BOLTZMANN * self.system_temperature.kelvin())
|
||||
}
|
||||
|
||||
/// Returns the carrier to noise density ratio.
|
||||
///
|
||||
/// This is the carrier power divided by the noise power in one hertz.
|
||||
/// It is the form used to compare a link against the energy per bit
|
||||
/// requirement of a modulation, because the data rate is accounted for
|
||||
/// separately.
|
||||
///
|
||||
/// Returns `None` when any input is not usable.
|
||||
pub fn carrier_to_noise_density(&self) -> Option<Decibel> {
|
||||
let carrier = self.received_power()?;
|
||||
let density = self.noise_density()?;
|
||||
if density <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
// The ratio is a power ratio, and the numerator and denominator
|
||||
// carry different bandwidths, so the ratio is formed directly.
|
||||
let ratio = carrier.watts() / density;
|
||||
Decibel::from_ratio(ratio)
|
||||
}
|
||||
|
||||
/// Returns the carrier to noise ratio in the receiver bandwidth.
|
||||
///
|
||||
/// This is the carrier to noise density ratio reduced by the bandwidth.
|
||||
/// Unlike the density ratio it depends on the receiver, which is why the
|
||||
/// same link can show different carrier to noise ratios in receivers of
|
||||
/// different bandwidth.
|
||||
///
|
||||
/// Returns `None` when any input is not usable.
|
||||
pub fn carrier_to_noise(&self) -> Option<Decibel> {
|
||||
if !self.bandwidth.is_finite() || self.bandwidth <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let density = self.carrier_to_noise_density()?;
|
||||
Some(density - Decibel::from_ratio(self.bandwidth)?)
|
||||
}
|
||||
|
||||
/// Returns the energy per bit to noise density ratio for a data rate.
|
||||
///
|
||||
/// This is the carrier to noise density ratio reduced by the data rate,
|
||||
/// because energy per bit is carrier power divided by bit rate.
|
||||
///
|
||||
/// Returns `None` when the data rate is not positive, or when the
|
||||
/// carrier to noise density ratio is not usable.
|
||||
pub fn energy_per_bit_over_noise(&self, data_rate: f64) -> Option<Decibel> {
|
||||
if !data_rate.is_finite() || data_rate <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let density = self.carrier_to_noise_density()?;
|
||||
Some(density - Decibel::from_ratio(data_rate)?)
|
||||
}
|
||||
|
||||
/// Returns the rise in system noise caused by the receive antenna
|
||||
/// viewing a noise source of the given temperature.
|
||||
///
|
||||
/// The antenna noise is added to the system temperature in power, which
|
||||
/// in temperature is a sum. This is the form used to account for the
|
||||
/// Earth, the Sun, or a rain cell in the beam.
|
||||
///
|
||||
/// Returns `None` when the added temperature is not usable.
|
||||
pub fn with_added_noise(&self, added: NoiseTemperature) -> Option<Self> {
|
||||
if !added.is_valid() {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
system_temperature: self.system_temperature + added,
|
||||
..*self
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the wavelength of the carrier, in meters.
|
||||
///
|
||||
/// Returns `None` when the frequency is not usable.
|
||||
pub fn carrier_wavelength(&self) -> Option<f64> {
|
||||
wavelength(self.geometry.frequency)
|
||||
}
|
||||
|
||||
/// Returns `true` when every input describes a usable link.
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.transmit_power.is_valid()
|
||||
&& self.transmit_gain.dbi().is_some()
|
||||
&& self.transmit_loss.is_finite()
|
||||
&& self.receive_gain.dbi().is_some()
|
||||
&& self.receive_loss.is_finite()
|
||||
&& self.system_temperature.is_valid()
|
||||
&& self.bandwidth.is_finite()
|
||||
&& self.bandwidth > 0.0
|
||||
&& self.geometry.is_valid()
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of comparing a link against a requirement.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct LinkMargin {
|
||||
/// The ratio the link achieves, in decibels.
|
||||
pub achieved: Decibel,
|
||||
/// The ratio the link requires, in decibels.
|
||||
pub required: Decibel,
|
||||
}
|
||||
|
||||
impl LinkMargin {
|
||||
/// Returns the margin, in decibels.
|
||||
///
|
||||
/// A positive margin means the link closes with room to spare; a
|
||||
/// negative margin means it does not.
|
||||
pub fn margin(self) -> Decibel {
|
||||
self.achieved - self.required
|
||||
}
|
||||
|
||||
/// Returns `true` when the margin is not negative.
|
||||
pub fn closes(self) -> bool {
|
||||
self.margin().db() >= 0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the margin between an achieved ratio and a required one.
|
||||
///
|
||||
/// This is the comparison that decides whether a link closes.
|
||||
pub fn link_margin(achieved: Decibel, required: Decibel) -> LinkMargin {
|
||||
LinkMargin { achieved, required }
|
||||
}
|
||||
|
||||
/// Returns the carrier to noise density ratio for a budget.
|
||||
///
|
||||
/// This is a free function form of
|
||||
/// [`LinkBudget::carrier_to_noise_density`].
|
||||
///
|
||||
/// Returns `None` when the budget is not usable.
|
||||
pub fn carrier_to_noise_density(budget: &LinkBudget) -> Option<Decibel> {
|
||||
budget.carrier_to_noise_density()
|
||||
}
|
||||
|
||||
/// Returns the gain to system temperature ratio of a receive chain, in
|
||||
/// reciprocal kelvin.
|
||||
///
|
||||
/// This figure of merit is written `G / T` and is the standard way to
|
||||
/// compare receive stations. It combines the receive antenna gain with the
|
||||
/// system noise temperature, so a station with a bigger dish and a quieter
|
||||
/// receiver scores higher.
|
||||
///
|
||||
/// Expressed in decibels, `G / T` is the receive gain in dBi minus ten times
|
||||
/// the logarithm of the temperature, which is the form usually quoted.
|
||||
///
|
||||
/// Returns `None` when the gain or temperature is not usable.
|
||||
pub fn gain_to_temperature(gain: Gain, temperature: NoiseTemperature) -> Option<f64> {
|
||||
if !temperature.is_valid() || temperature.kelvin() <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let ratio = gain.ratio() / temperature.kelvin();
|
||||
if ratio.is_finite() && ratio > 0.0 {
|
||||
Some(ratio)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1,74 @@
|
|||
//! Link budget computation.
|
||||
//!
|
||||
//! A link budget adds up every gain and loss between a transmitter and a
|
||||
//! receiver and compares the resulting carrier power with the noise that
|
||||
//! competes with it. This crate computes that sum and the figures derived
|
||||
//! from it: the carrier to noise ratio, the energy per bit to noise density
|
||||
//! ratio, and the link margin.
|
||||
//!
|
||||
//! The order of operations matters, so the chain is stated explicitly:
|
||||
//!
|
||||
//! 1. The transmitter power is reduced by the transmit losses and increased
|
||||
//! by the transmit antenna gain, giving the equivalent isotropically
|
||||
//! radiated power.
|
||||
//! 2. That is divided by the free space path loss and increased by the
|
||||
//! receive antenna gain, giving the carrier power at the receiver input.
|
||||
//! 3. The system noise temperature is converted to a noise density by
|
||||
//! multiplying by the Boltzmann constant.
|
||||
//! 4. The carrier to noise density ratio is the carrier power divided by
|
||||
//! that density, and the carrier to noise ratio divides it further by the
|
||||
//! receiver bandwidth.
|
||||
//!
|
||||
//! Sources:
|
||||
//!
|
||||
//! - The link equation in this form is the one used throughout satellite
|
||||
//! communications practice, as described for example in the ITU-R
|
||||
//! Handbook on Satellite Communications.
|
||||
//! - The Boltzmann constant and the speed of light come from the SI
|
||||
//! definitions, as re-exported by `apsis-rf`.
|
||||
//!
|
||||
//! ```
|
||||
//! use apsis_link::{LinkBudget, LinkGeometry};
|
||||
//! use apsis_rf::{Gain, Power};
|
||||
//!
|
||||
//! let budget = LinkBudget {
|
||||
//! transmit_power: Power::from_dbw(10.0).expect("finite"),
|
||||
//! transmit_gain: Gain::from_dbi(30.0).expect("finite"),
|
||||
//! transmit_loss: apsis_rf::Decibel::from_db(1.0),
|
||||
//! receive_gain: Gain::from_dbi(35.0).expect("finite"),
|
||||
//! receive_loss: apsis_rf::Decibel::from_db(0.5),
|
||||
//! system_temperature: apsis_noise::NoiseTemperature::from_kelvin(150.0),
|
||||
//! bandwidth: 36.0e6,
|
||||
//! geometry: LinkGeometry {
|
||||
//! distance: 38_000_000.0,
|
||||
//! frequency: 12.0e9,
|
||||
//! },
|
||||
//! };
|
||||
//!
|
||||
//! // The carrier to noise density ratio is available directly. For this
|
||||
//! // budget it is about 74.7 dB, which is typical of a Ku band downlink.
|
||||
//! let ratio = budget.carrier_to_noise_density().expect("valid budget");
|
||||
//! assert!(ratio.db() > 70.0);
|
||||
//! assert!(ratio.db() < 80.0);
|
||||
//! ```
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
mod budget;
|
||||
|
||||
pub use budget::{
|
||||
LinkBudget, LinkGeometry, LinkMargin, carrier_to_noise_density, gain_to_temperature,
|
||||
link_margin,
|
||||
};
|
||||
|
||||
pub use apsis_antenna::{
|
||||
ElementPattern, ParabolicAperture, Pattern, beam_width, mask_gain, off_axis_attenuation,
|
||||
};
|
||||
pub use apsis_noise::{
|
||||
NoiseTemperature, REFERENCE_TEMPERATURE_K, Stage, cascade_temperature, thermal_noise_power,
|
||||
};
|
||||
pub use apsis_rf::{
|
||||
BOLTZMANN, DIPOLE_OVER_ISOTROPIC_DB, Decibel, Gain, Power, SPEED_OF_LIGHT,
|
||||
free_space_path_loss, wavelength,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,286 @@
|
|||
//! Tests for the link budget.
|
||||
//!
|
||||
//! The expected values are computed by hand from the link equation and
|
||||
//! recorded here so that a change to any part of the chain shows up as a
|
||||
//! failing assertion rather than as a silently different budget.
|
||||
|
||||
use apsis_link::{
|
||||
Decibel, Gain, LinkBudget, LinkGeometry, NoiseTemperature, Power, carrier_to_noise_density,
|
||||
gain_to_temperature, link_margin,
|
||||
};
|
||||
|
||||
const TOLERANCE: f64 = 1e-9;
|
||||
|
||||
/// A representative Ku band downlink: a ten watt transmitter, a three meter
|
||||
/// ground station, a 36 MHz transponder, and a 150 K system temperature.
|
||||
fn downlink() -> LinkBudget {
|
||||
LinkBudget {
|
||||
transmit_power: Power::from_dbw(10.0).expect("finite"),
|
||||
transmit_gain: Gain::from_dbi(30.0).expect("finite"),
|
||||
transmit_loss: Decibel::from_db(1.0),
|
||||
receive_gain: Gain::from_dbi(35.0).expect("finite"),
|
||||
receive_loss: Decibel::from_db(0.5),
|
||||
system_temperature: NoiseTemperature::from_kelvin(150.0),
|
||||
bandwidth: 36.0e6,
|
||||
geometry: LinkGeometry {
|
||||
distance: 38_000_000.0,
|
||||
frequency: 12.0e9,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eirp_sums_the_power_gain_and_loss() {
|
||||
let budget = downlink();
|
||||
// 10 dBW + 30 dBi - 1 dB = 39 dBW.
|
||||
let eirp = budget.eirp().expect("valid budget");
|
||||
assert!((eirp.dbw().expect("positive") - 39.0).abs() < TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_path_loss_matches_the_closed_form() {
|
||||
let budget = downlink();
|
||||
let loss = budget.geometry.path_loss().expect("valid geometry");
|
||||
// 4 pi d / lambda with d = 38 000 km and lambda = 0.02498 m gives
|
||||
// 205.627 dB.
|
||||
assert!(
|
||||
(loss.db() - 205.627_080_075_172_1).abs() < 1e-9,
|
||||
"{}",
|
||||
loss.db()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_carrier_power_matches_the_hand_computation() {
|
||||
let budget = downlink();
|
||||
let carrier = budget.received_power().expect("valid budget");
|
||||
// 39 dBW - 205.627 dB + 35 dBi - 0.5 dB = -132.127 dBW.
|
||||
assert!(
|
||||
(carrier.dbw().expect("positive") + 132.127_080_075_172_1).abs() < 1e-9,
|
||||
"{}",
|
||||
carrier.dbw().expect("positive")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_noise_density_is_the_boltzmann_constant_times_temperature() {
|
||||
let budget = downlink();
|
||||
let density = budget.noise_density().expect("valid temperature");
|
||||
let expected = apsis_link::BOLTZMANN * 150.0;
|
||||
assert!((density - expected).abs() < 1e-40);
|
||||
// In decibels this is -206.838 dBW/Hz.
|
||||
let db = 10.0 * density.log10();
|
||||
assert!((db + 206.838_254_582_660_85).abs() < 1e-9, "{db}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_carrier_to_noise_density_matches_the_hand_computation() {
|
||||
let budget = downlink();
|
||||
let ratio = budget.carrier_to_noise_density().expect("valid budget");
|
||||
assert!(
|
||||
(ratio.db() - 74.711_174_507_488_75).abs() < 1e-9,
|
||||
"{}",
|
||||
ratio.db()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_carrier_to_noise_matches_the_hand_computation() {
|
||||
let budget = downlink();
|
||||
let ratio = budget.carrier_to_noise().expect("valid budget");
|
||||
assert!(
|
||||
(ratio.db() + 0.851_850_500_184_127_7).abs() < 1e-9,
|
||||
"{}",
|
||||
ratio.db()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_energy_per_bit_matches_the_hand_computation() {
|
||||
let budget = downlink();
|
||||
let ratio = budget
|
||||
.energy_per_bit_over_noise(10.0e6)
|
||||
.expect("valid budget");
|
||||
assert!(
|
||||
(ratio.db() - 4.711_174_507_488_749).abs() < 1e-9,
|
||||
"{}",
|
||||
ratio.db()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_carrier_to_noise_density_helper_matches_the_method() {
|
||||
let budget = downlink();
|
||||
assert_eq!(
|
||||
carrier_to_noise_density(&budget),
|
||||
budget.carrier_to_noise_density()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wider_bandwidth_lowers_the_carrier_to_noise() {
|
||||
let narrow = LinkBudget {
|
||||
bandwidth: 1.0e6,
|
||||
..downlink()
|
||||
};
|
||||
let wide = LinkBudget {
|
||||
bandwidth: 10.0e6,
|
||||
..downlink()
|
||||
};
|
||||
let narrow_ratio = narrow.carrier_to_noise().expect("valid");
|
||||
let wide_ratio = wide.carrier_to_noise().expect("valid");
|
||||
// A factor of ten in bandwidth is ten decibels.
|
||||
assert!((narrow_ratio.db() - wide_ratio.db() - 10.0).abs() < 1e-9);
|
||||
// The carrier to noise density ratio does not depend on bandwidth.
|
||||
assert_eq!(
|
||||
narrow.carrier_to_noise_density(),
|
||||
wide.carrier_to_noise_density()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_higher_data_rate_lowers_the_energy_per_bit() {
|
||||
let budget = downlink();
|
||||
let slow = budget.energy_per_bit_over_noise(1.0e6).expect("valid");
|
||||
let fast = budget.energy_per_bit_over_noise(10.0e6).expect("valid");
|
||||
assert!((slow.db() - fast.db() - 10.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn added_noise_raises_the_system_temperature() {
|
||||
let budget = downlink();
|
||||
let warmed = budget
|
||||
.with_added_noise(NoiseTemperature::from_kelvin(50.0))
|
||||
.expect("valid temperature");
|
||||
assert_eq!(warmed.system_temperature.kelvin(), 200.0);
|
||||
// The carrier to noise density ratio falls by 10 log10(200 / 150).
|
||||
let before = budget.carrier_to_noise_density().expect("valid");
|
||||
let after = warmed.carrier_to_noise_density().expect("valid");
|
||||
let expected_drop = 10.0 * (200.0_f64 / 150.0).log10();
|
||||
assert!((before.db() - after.db() - expected_drop).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_carrier_wavelength_matches_the_frequency() {
|
||||
let budget = downlink();
|
||||
let lambda = budget.carrier_wavelength().expect("valid frequency");
|
||||
assert!((lambda - 299_792_458.0 / 12.0e9).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_is_reported() {
|
||||
assert!(downlink().is_valid());
|
||||
|
||||
let bad_geometry = LinkBudget {
|
||||
geometry: LinkGeometry {
|
||||
distance: 0.0,
|
||||
frequency: 12.0e9,
|
||||
},
|
||||
..downlink()
|
||||
};
|
||||
assert!(!bad_geometry.is_valid());
|
||||
assert_eq!(bad_geometry.eirp(), None);
|
||||
assert_eq!(bad_geometry.received_power(), None);
|
||||
|
||||
let bad_temperature = LinkBudget {
|
||||
system_temperature: NoiseTemperature::from_kelvin(-1.0),
|
||||
..downlink()
|
||||
};
|
||||
assert!(!bad_temperature.is_valid());
|
||||
assert_eq!(bad_temperature.noise_density(), None);
|
||||
assert_eq!(bad_temperature.carrier_to_noise_density(), None);
|
||||
|
||||
let bad_bandwidth = LinkBudget {
|
||||
bandwidth: -1.0,
|
||||
..downlink()
|
||||
};
|
||||
assert!(!bad_bandwidth.is_valid());
|
||||
assert_eq!(bad_bandwidth.carrier_to_noise(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_invalid_data_rate_is_rejected() {
|
||||
let budget = downlink();
|
||||
assert_eq!(budget.energy_per_bit_over_noise(0.0), None);
|
||||
assert_eq!(budget.energy_per_bit_over_noise(-1.0), None);
|
||||
assert_eq!(budget.energy_per_bit_over_noise(f64::NAN), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_margin_reports_whether_the_link_closes() {
|
||||
let achieved = Decibel::from_db(75.0);
|
||||
let required = Decibel::from_db(70.0);
|
||||
let margin = link_margin(achieved, required);
|
||||
assert!((margin.margin().db() - 5.0).abs() < TOLERANCE);
|
||||
assert!(margin.closes());
|
||||
|
||||
// A link that exactly meets the requirement closes with zero margin.
|
||||
let exact = link_margin(required, required);
|
||||
assert_eq!(exact.margin().db(), 0.0);
|
||||
assert!(exact.closes());
|
||||
|
||||
// A link below the requirement does not close.
|
||||
let short = link_margin(Decibel::from_db(65.0), required);
|
||||
assert!(!short.closes());
|
||||
assert!((short.margin().db() + 5.0).abs() < TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_gain_to_temperature_ratio_uses_reciprocal_kelvin() {
|
||||
let gain = Gain::from_dbi(35.0).expect("finite");
|
||||
let temperature = NoiseTemperature::from_kelvin(150.0);
|
||||
let ratio = gain_to_temperature(gain, temperature).expect("valid");
|
||||
let expected = 10f64.powf(3.5) / 150.0;
|
||||
assert!((ratio - expected).abs() < 1e-9);
|
||||
|
||||
// In decibels this is 35 - 10 log10(150), which is 13.239087 dB/K.
|
||||
let db = 10.0 * ratio.log10();
|
||||
assert!((db - 13.239_087_409_443_187).abs() < 1e-9, "{db}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hotter_receiver_lowers_the_gain_to_temperature_ratio() {
|
||||
let gain = Gain::from_dbi(35.0).expect("finite");
|
||||
let cold = gain_to_temperature(gain, NoiseTemperature::from_kelvin(100.0)).expect("valid");
|
||||
let hot = gain_to_temperature(gain, NoiseTemperature::from_kelvin(200.0)).expect("valid");
|
||||
assert!(cold > hot);
|
||||
// Doubling the temperature halves the ratio, which is three decibels.
|
||||
assert!((10.0 * (cold / hot).log10() - 3.010_299_956_639_812).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_temperature_has_no_gain_to_temperature_ratio() {
|
||||
let gain = Gain::from_dbi(35.0).expect("finite");
|
||||
assert_eq!(
|
||||
gain_to_temperature(gain, NoiseTemperature::from_kelvin(0.0)),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
gain_to_temperature(gain, NoiseTemperature::from_kelvin(-1.0)),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_budget_can_be_used_with_a_computed_antenna_pattern() {
|
||||
// The receive gain of a three meter dish at 12 GHz, rounded to a single
|
||||
// decibel, is used here to confirm the budget accepts a gain derived
|
||||
// from the antenna layer.
|
||||
use apsis_link::ParabolicAperture;
|
||||
let dish = ParabolicAperture {
|
||||
diameter: 3.0,
|
||||
efficiency: 0.65,
|
||||
};
|
||||
let wavelength = 0.025;
|
||||
let gain = dish.peak_gain(wavelength).expect("valid dish");
|
||||
let budget = LinkBudget {
|
||||
receive_gain: gain,
|
||||
geometry: LinkGeometry {
|
||||
distance: 38_000_000.0,
|
||||
frequency: 12.0e9,
|
||||
},
|
||||
..downlink()
|
||||
};
|
||||
assert!(budget.is_valid());
|
||||
assert!(budget.carrier_to_noise_density().is_some());
|
||||
}
|
||||
Loading…
Reference in New Issue