rf: add noise temperature and cascade models
This commit is contained in:
parent
c112f02d9c
commit
9a4b5bf9e5
|
|
@ -121,6 +121,10 @@ version = "0.1.0"
|
|||
[[package]]
|
||||
name = "apsis-noise"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"apsis-core",
|
||||
"apsis-rf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "apsis-orbit"
|
||||
|
|
|
|||
|
|
@ -11,3 +11,5 @@ publish = false
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
apsis-core.workspace = true
|
||||
apsis-rf.workspace = true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
//! Noise in a cascade of stages, by the Friis formula.
|
||||
|
||||
use crate::temperature::{NoiseTemperature, REFERENCE_TEMPERATURE_K};
|
||||
use apsis_rf::Gain;
|
||||
|
||||
/// One stage of a receiver chain.
|
||||
///
|
||||
/// A stage is described by the noise it adds, referred to its own input, and
|
||||
/// the gain it applies. The noise may be given as a temperature or as a
|
||||
/// noise figure; both are converted to a temperature before the cascade is
|
||||
/// evaluated, because temperatures add across the chain while decibel
|
||||
/// figures do not.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Stage {
|
||||
/// The noise temperature added by this stage, referred to its input.
|
||||
pub temperature: NoiseTemperature,
|
||||
/// The available gain of this stage.
|
||||
pub gain: Gain,
|
||||
}
|
||||
|
||||
impl Stage {
|
||||
/// Creates a stage from a noise temperature and a gain.
|
||||
pub const fn new(temperature: NoiseTemperature, gain: Gain) -> Self {
|
||||
Self { temperature, gain }
|
||||
}
|
||||
|
||||
/// Creates a stage from a noise figure in decibels and a gain in
|
||||
/// decibels.
|
||||
///
|
||||
/// Returns `None` when either value is not finite.
|
||||
pub fn from_noise_figure(noise_figure_db: f64, gain_dbi: f64) -> Option<Self> {
|
||||
Some(Self {
|
||||
temperature: NoiseTemperature::from_noise_figure_db(noise_figure_db)?,
|
||||
gain: Gain::from_dbi(gain_dbi)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a noiseless stage with the given gain.
|
||||
///
|
||||
/// An ideal amplifier adds no noise, so it contributes nothing to the
|
||||
/// cascade except its gain.
|
||||
pub const fn ideal(gain: Gain) -> Self {
|
||||
Self {
|
||||
temperature: NoiseTemperature::from_kelvin(0.0),
|
||||
gain,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` when the stage is usable.
|
||||
pub fn is_valid(self) -> bool {
|
||||
self.temperature.is_valid() && self.gain.ratio().is_finite() && self.gain.ratio() > 0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the noise temperature of a cascade of stages, referred to the
|
||||
/// input of the first stage.
|
||||
///
|
||||
/// The Friis formula weights each stage's contribution by the gain that
|
||||
/// precedes it:
|
||||
///
|
||||
/// ```text
|
||||
/// T = T_1 + T_2 / G_1 + T_3 / (G_1 G_2) + ...
|
||||
/// ```
|
||||
///
|
||||
/// The consequence is that the first stage dominates: a noisy second stage
|
||||
/// matters much less when the first stage has gain. The total temperature is
|
||||
/// that of the chain, so it can be compared directly with the available
|
||||
/// gain to find the noise figure of the chain.
|
||||
///
|
||||
/// Returns `None` when any stage is not usable, which includes a gain ratio
|
||||
/// that is not positive.
|
||||
pub fn cascade_temperature(stages: &[Stage]) -> Option<NoiseTemperature> {
|
||||
let mut total = NoiseTemperature::from_kelvin(0.0);
|
||||
let mut cumulative_gain = 1.0_f64;
|
||||
for stage in stages {
|
||||
if !stage.is_valid() {
|
||||
return None;
|
||||
}
|
||||
total = total.plus(stage.temperature.scaled(1.0 / cumulative_gain));
|
||||
cumulative_gain *= stage.gain.ratio();
|
||||
}
|
||||
Some(total)
|
||||
}
|
||||
|
||||
/// Returns the noise temperature that corresponds to a noise figure in
|
||||
/// decibels, using the standard reference temperature.
|
||||
///
|
||||
/// This is a free function form of
|
||||
/// [`NoiseTemperature::from_noise_figure_db`](crate::NoiseTemperature::from_noise_figure_db).
|
||||
///
|
||||
/// Returns `None` when the figure is not finite.
|
||||
pub fn noise_figure_to_temperature(noise_figure_db: f64) -> Option<NoiseTemperature> {
|
||||
NoiseTemperature::from_noise_figure_db(noise_figure_db)
|
||||
}
|
||||
|
||||
/// Returns the noise figure of a cascade, in decibels.
|
||||
///
|
||||
/// The cascade is evaluated as a temperature and then converted to a figure
|
||||
/// using the standard reference temperature. This is the form in which a
|
||||
/// receiver chain is usually specified.
|
||||
///
|
||||
/// Returns `None` when any stage is not usable.
|
||||
pub fn cascade_noise_figure_db(stages: &[Stage]) -> Option<f64> {
|
||||
cascade_temperature(stages)?.noise_figure_db()
|
||||
}
|
||||
|
||||
/// Returns the reference temperature used by the noise figure definition.
|
||||
pub const fn reference_temperature() -> f64 {
|
||||
REFERENCE_TEMPERATURE_K
|
||||
}
|
||||
|
|
@ -1 +1,46 @@
|
|||
//! Noise temperature and noise figure models.
|
||||
//!
|
||||
//! Noise enters a receiver from three places: the source, the antenna and
|
||||
//! its feed, and the receiver itself. They are combined by adding noise
|
||||
//! temperatures rather than powers, because a temperature is a power
|
||||
//! referred to a fixed bandwidth through the Boltzmann relation, and a
|
||||
//! cascade of stages at different gains does not simply add powers.
|
||||
//!
|
||||
//! The central relations are:
|
||||
//!
|
||||
//! - Thermal noise power in a bandwidth, `P = k T B`, with `k` the Boltzmann
|
||||
//! constant.
|
||||
//! - Noise figure as a temperature, `T = T_0 (F - 1)`, with `T_0` the
|
||||
//! reference temperature of 290 K.
|
||||
//! - The Friis cascade, which weights each stage's noise by the gain that
|
||||
//! precedes it.
|
||||
//!
|
||||
//! Sources:
|
||||
//!
|
||||
//! - The reference temperature of 290 K and the noise figure definition are
|
||||
//! from the IEEE standard definition of noise figure, IEEE Std 161.
|
||||
//! - The cascade is the Friis formula for noise, as given in any receiver
|
||||
//! design text.
|
||||
//!
|
||||
//! ```
|
||||
//! use apsis_noise::{NoiseTemperature, thermal_noise_power};
|
||||
//!
|
||||
//! // A 36 MHz bandwidth at 290 K carries about -128 dBW of thermal noise.
|
||||
//! let temperature = NoiseTemperature::from_kelvin(290.0);
|
||||
//! let power = thermal_noise_power(temperature, 36.0e6).expect("valid arguments");
|
||||
//! assert!((power.dbw().expect("positive") + 128.0).abs() < 1.0);
|
||||
//! ```
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
mod cascade;
|
||||
mod temperature;
|
||||
|
||||
pub use cascade::{
|
||||
Stage, cascade_noise_figure_db, cascade_temperature, noise_figure_to_temperature,
|
||||
reference_temperature,
|
||||
};
|
||||
pub use temperature::{NoiseTemperature, REFERENCE_TEMPERATURE_K, thermal_noise_power};
|
||||
|
||||
pub use apsis_core::constant::BOLTZMANN;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
//! Noise temperature.
|
||||
|
||||
use apsis_core::constant::BOLTZMANN;
|
||||
use apsis_rf::Power;
|
||||
use core::fmt;
|
||||
|
||||
/// The reference temperature used to define the noise figure, in kelvin.
|
||||
///
|
||||
/// The value is 290 K, which is the standard reference temperature of the
|
||||
/// IEEE definition of noise figure. It is a convention rather than a
|
||||
/// physical temperature, and the choice affects the noise figure quoted for
|
||||
/// a given noise temperature.
|
||||
pub const REFERENCE_TEMPERATURE_K: f64 = 290.0;
|
||||
|
||||
/// A noise temperature in kelvin.
|
||||
///
|
||||
/// A noise temperature is the temperature of a resistor that would deliver
|
||||
/// the same noise power in the same bandwidth. It is a convenient way to
|
||||
/// express any noise source, including one that is not thermal in origin,
|
||||
/// because temperatures add across a cascade while powers do not.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
|
||||
pub struct NoiseTemperature(f64);
|
||||
|
||||
impl NoiseTemperature {
|
||||
/// The standard reference temperature of 290 K.
|
||||
pub const REFERENCE: Self = Self(REFERENCE_TEMPERATURE_K);
|
||||
|
||||
/// Creates a noise temperature from a value in kelvin.
|
||||
pub const fn from_kelvin(kelvin: f64) -> Self {
|
||||
Self(kelvin)
|
||||
}
|
||||
|
||||
/// Creates a noise temperature from a noise figure in decibels.
|
||||
///
|
||||
/// The relation is `T = T_0 (F - 1)` with `F` the linear noise factor,
|
||||
/// so a noise figure of zero decibels gives a temperature of zero.
|
||||
///
|
||||
/// Returns `None` when the figure is not finite.
|
||||
pub fn from_noise_figure_db(decibels: f64) -> Option<Self> {
|
||||
if !decibels.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let factor = 10f64.powf(decibels / 10.0);
|
||||
Some(Self(REFERENCE_TEMPERATURE_K * (factor - 1.0)))
|
||||
}
|
||||
|
||||
/// Creates a noise temperature from a linear noise factor.
|
||||
///
|
||||
/// Returns `None` when the factor is not finite or is below one, which
|
||||
/// would describe a stage that removes noise.
|
||||
pub fn from_noise_factor(factor: f64) -> Option<Self> {
|
||||
if factor.is_finite() && factor >= 1.0 {
|
||||
Some(Self(REFERENCE_TEMPERATURE_K * (factor - 1.0)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns this temperature in kelvin.
|
||||
pub const fn kelvin(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Returns this temperature as a linear noise factor.
|
||||
///
|
||||
/// The factor is `T / T_0 + 1`, which is one for zero noise.
|
||||
pub fn noise_factor(self) -> f64 {
|
||||
self.0 / REFERENCE_TEMPERATURE_K + 1.0
|
||||
}
|
||||
|
||||
/// Returns this temperature as a noise figure in decibels.
|
||||
///
|
||||
/// Returns `None` when the temperature is not finite or is negative,
|
||||
/// which would give a factor below one.
|
||||
pub fn noise_figure_db(self) -> Option<f64> {
|
||||
let factor = self.noise_factor();
|
||||
if factor > 0.0 && factor.is_finite() && self.0 >= 0.0 {
|
||||
Some(10.0 * factor.log10())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the sum of two temperatures.
|
||||
///
|
||||
/// Noise temperatures of independent sources add, which is the reason
|
||||
/// the quantity is useful.
|
||||
pub const fn plus(self, other: Self) -> Self {
|
||||
Self(self.0 + other.0)
|
||||
}
|
||||
|
||||
/// Returns this temperature scaled by a gain ratio.
|
||||
///
|
||||
/// A source followed by a gain contributes its temperature divided by
|
||||
/// that gain when referred to the input, so the scaling for a
|
||||
/// contribution referred to the input is the reciprocal.
|
||||
pub const fn scaled(self, factor: f64) -> Self {
|
||||
Self(self.0 * factor)
|
||||
}
|
||||
|
||||
/// Returns `true` when the temperature is finite and not negative.
|
||||
pub const fn is_valid(self) -> bool {
|
||||
self.0.is_finite() && self.0 >= 0.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for NoiseTemperature {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} K", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Add for NoiseTemperature {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self {
|
||||
self.plus(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the thermal noise power in a bandwidth at a noise temperature.
|
||||
///
|
||||
/// The relation is `P = k T B`, with `k` the Boltzmann constant. This is the
|
||||
/// power that a matched resistor at the given temperature would deliver to a
|
||||
/// load over the stated bandwidth.
|
||||
///
|
||||
/// The bandwidth must be positive. Returns `None` for an invalid
|
||||
/// temperature or bandwidth.
|
||||
pub fn thermal_noise_power(temperature: NoiseTemperature, bandwidth: f64) -> Option<Power> {
|
||||
if !temperature.is_valid() {
|
||||
return None;
|
||||
}
|
||||
if !bandwidth.is_finite() || bandwidth <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
Some(Power::from_watts(
|
||||
BOLTZMANN * temperature.kelvin() * bandwidth,
|
||||
))
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
//! Tests for the noise cascade.
|
||||
|
||||
use apsis_noise::{
|
||||
NoiseTemperature, Stage, cascade_noise_figure_db, cascade_temperature,
|
||||
noise_figure_to_temperature, reference_temperature,
|
||||
};
|
||||
use apsis_rf::Gain;
|
||||
|
||||
const TOLERANCE: f64 = 1e-9;
|
||||
|
||||
/// A stage that adds the given temperature with the given gain in decibels.
|
||||
fn stage(temperature: f64, gain_dbi: f64) -> Stage {
|
||||
Stage::new(
|
||||
NoiseTemperature::from_kelvin(temperature),
|
||||
Gain::from_dbi(gain_dbi).expect("finite gain"),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_reference_temperature_helper_matches_the_constant() {
|
||||
assert_eq!(reference_temperature(), 290.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_single_stage_contributes_its_own_temperature() {
|
||||
let only = stage(100.0, 20.0);
|
||||
let total = cascade_temperature(&[only]).expect("valid stage");
|
||||
assert!((total.kelvin() - 100.0).abs() < TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_cascade_has_no_noise() {
|
||||
let total = cascade_temperature(&[]).expect("empty is valid");
|
||||
assert_eq!(total.kelvin(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_first_stage_dominates() {
|
||||
// A 100 K first stage with 20 dB of gain followed by a 1000 K second
|
||||
// stage contributes the second stage divided by 100.
|
||||
let cascade = [stage(100.0, 20.0), stage(1_000.0, 20.0)];
|
||||
let total = cascade_temperature(&cascade).expect("valid stages");
|
||||
assert!((total.kelvin() - 110.0).abs() < 1e-6, "{}", total.kelvin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_noiseless_stage_contributes_nothing() {
|
||||
let cascade = [
|
||||
Stage::ideal(Gain::from_dbi(20.0).expect("valid")),
|
||||
stage(50.0, 10.0),
|
||||
];
|
||||
let total = cascade_temperature(&cascade).expect("valid stages");
|
||||
assert!((total.kelvin() - 0.5).abs() < 1e-9, "{}", total.kelvin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_friis_formula_matches_by_hand() {
|
||||
// Three stages: 50 K at 20 dB, 100 K at 10 dB, 200 K at 0 dB.
|
||||
let cascade = [stage(50.0, 20.0), stage(100.0, 10.0), stage(200.0, 0.0)];
|
||||
let total = cascade_temperature(&cascade).expect("valid stages");
|
||||
let expected = 50.0 + 100.0 / 100.0 + 200.0 / (100.0 * 10.0);
|
||||
assert!((total.kelvin() - expected).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lossy_component_ahead_of_the_receiver_raises_the_noise() {
|
||||
// A 3 dB loss is a gain of one half, and it contributes its own
|
||||
// temperature divided by the gain of the stages before it, which for a
|
||||
// loss is a division by a fraction and so an increase.
|
||||
let loss = Stage::new(
|
||||
NoiseTemperature::from_kelvin(0.0),
|
||||
Gain::from_dbi(-3.0).expect("finite"),
|
||||
);
|
||||
let receiver = stage(100.0, 30.0);
|
||||
let total = cascade_temperature(&[loss, receiver]).expect("valid stages");
|
||||
// The receiver contributes 100 / 0.5012 = 199.5 K.
|
||||
assert!(
|
||||
total.kelvin() > 199.0 && total.kelvin() < 200.0,
|
||||
"{}",
|
||||
total.kelvin()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cascade_figure_follows_from_the_cascade_temperature() {
|
||||
let cascade = [stage(50.0, 20.0), stage(100.0, 10.0)];
|
||||
let temperature = cascade_temperature(&cascade).expect("valid stages");
|
||||
let figure = cascade_noise_figure_db(&cascade).expect("valid stages");
|
||||
let expected = temperature.noise_figure_db().expect("valid");
|
||||
assert!((figure - expected).abs() < TOLERANCE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_noiseless_cascade_has_zero_figure() {
|
||||
let cascade = [
|
||||
Stage::ideal(Gain::from_dbi(10.0).expect("valid")),
|
||||
Stage::ideal(Gain::from_dbi(10.0).expect("valid")),
|
||||
];
|
||||
let figure = cascade_noise_figure_db(&cascade).expect("valid stages");
|
||||
assert!(figure.abs() < TOLERANCE, "figure {figure}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_known_receiver_chain_gives_a_plausible_figure() {
|
||||
// A low noise amplifier at 1 dB and 30 dB gain followed by a mixer at
|
||||
// 10 dB and 0 dB gain. The cascade figure is close to the first stage
|
||||
// plus the second divided by the first stage's gain.
|
||||
let low_noise = Stage::from_noise_figure(1.0, 30.0).expect("valid");
|
||||
let mixer = Stage::from_noise_figure(10.0, 0.0).expect("valid");
|
||||
let figure = cascade_noise_figure_db(&[low_noise, mixer]).expect("valid stages");
|
||||
assert!(figure > 1.0 && figure < 1.1, "figure {figure}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_single_stage_cascade_figure_is_its_own_figure() {
|
||||
let amplifier = Stage::from_noise_figure(2.5, 25.0).expect("valid");
|
||||
let figure = cascade_noise_figure_db(&[amplifier]).expect("valid");
|
||||
assert!((figure - 2.5).abs() < 1e-9, "figure {figure}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_invalid_stage_is_rejected() {
|
||||
let bad_gain = Stage::new(NoiseTemperature::from_kelvin(100.0), Gain::from_ratio(-1.0));
|
||||
assert!(!bad_gain.is_valid());
|
||||
assert_eq!(cascade_temperature(&[bad_gain]), None);
|
||||
|
||||
let bad_temperature = Stage::new(
|
||||
NoiseTemperature::from_kelvin(-1.0),
|
||||
Gain::from_dbi(10.0).expect("valid"),
|
||||
);
|
||||
assert!(!bad_temperature.is_valid());
|
||||
assert_eq!(cascade_temperature(&[bad_temperature]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_conversion_helper_matches_the_method() {
|
||||
let via_function = noise_figure_to_temperature(3.0).expect("finite");
|
||||
let via_method = NoiseTemperature::from_noise_figure_db(3.0).expect("finite");
|
||||
assert_eq!(via_function, via_method);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gain_ordering_matters() {
|
||||
// Placing the noisy stage first gives a worse cascade than placing it
|
||||
// second, which is why a low noise amplifier goes at the front.
|
||||
let noisy = stage(500.0, 10.0);
|
||||
let quiet = stage(20.0, 20.0);
|
||||
let quiet_first = cascade_temperature(&[quiet, noisy]).expect("valid");
|
||||
let noisy_first = cascade_temperature(&[noisy, quiet]).expect("valid");
|
||||
assert!(
|
||||
noisy_first.kelvin() > quiet_first.kelvin(),
|
||||
"{} vs {}",
|
||||
noisy_first.kelvin(),
|
||||
quiet_first.kelvin()
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
//! Tests for noise temperature and thermal noise power.
|
||||
|
||||
use apsis_core::constant::BOLTZMANN;
|
||||
use apsis_noise::{NoiseTemperature, REFERENCE_TEMPERATURE_K, thermal_noise_power};
|
||||
|
||||
const TOLERANCE: f64 = 1e-12;
|
||||
|
||||
#[test]
|
||||
fn the_reference_temperature_is_290_kelvin() {
|
||||
assert_eq!(REFERENCE_TEMPERATURE_K, 290.0);
|
||||
assert_eq!(NoiseTemperature::REFERENCE.kelvin(), 290.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_temperature_has_unit_noise_factor() {
|
||||
let quiet = NoiseTemperature::from_kelvin(0.0);
|
||||
assert_eq!(quiet.noise_factor(), 1.0);
|
||||
assert_eq!(quiet.noise_figure_db(), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_reference_temperature_is_a_noise_figure_of_three_decibels() {
|
||||
// A noise figure is defined as the ratio of the total noise to the
|
||||
// noise of a source at the reference temperature, so a stage whose
|
||||
// excess noise equals the reference temperature has a factor of two,
|
||||
// which is 3.01 dB.
|
||||
let value = NoiseTemperature::from_kelvin(REFERENCE_TEMPERATURE_K);
|
||||
assert!((value.noise_factor() - 2.0).abs() < TOLERANCE);
|
||||
assert!((value.noise_figure_db().expect("valid") - 3.010_299_956_639_812).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_figure_and_temperature_round_trip() {
|
||||
for figure in [0.0, 0.5, 1.0, 3.0, 10.0, 20.0] {
|
||||
let temperature = NoiseTemperature::from_noise_figure_db(figure).expect("finite figure");
|
||||
let recovered = temperature.noise_figure_db().expect("valid temperature");
|
||||
assert!((recovered - figure).abs() < 1e-9, "figure {figure}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_factor_and_temperature_round_trip() {
|
||||
for factor in [1.0, 1.5, 2.0, 10.0, 100.0] {
|
||||
let temperature = NoiseTemperature::from_noise_factor(factor).expect("valid factor");
|
||||
assert!(
|
||||
(temperature.noise_factor() - factor).abs() < 1e-9,
|
||||
"factor {factor}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_known_noise_figure_gives_a_known_temperature() {
|
||||
// A noise figure of 1 dB is a factor of 1.2589, so the temperature is
|
||||
// 290 * 0.2589, about 75.1 K.
|
||||
let temperature = NoiseTemperature::from_noise_figure_db(1.0).expect("finite");
|
||||
assert!(
|
||||
(temperature.kelvin() - 75.09).abs() < 0.01,
|
||||
"{}",
|
||||
temperature.kelvin()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_inputs_are_rejected() {
|
||||
assert_eq!(NoiseTemperature::from_noise_figure_db(f64::NAN), None);
|
||||
assert_eq!(NoiseTemperature::from_noise_figure_db(f64::INFINITY), None);
|
||||
assert_eq!(NoiseTemperature::from_noise_factor(0.5), None);
|
||||
assert_eq!(NoiseTemperature::from_noise_factor(f64::NAN), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn temperatures_add() {
|
||||
let first = NoiseTemperature::from_kelvin(50.0);
|
||||
let second = NoiseTemperature::from_kelvin(25.0);
|
||||
assert_eq!((first + second).kelvin(), 75.0);
|
||||
assert_eq!(first.plus(second).kelvin(), 75.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validity_is_reported() {
|
||||
assert!(NoiseTemperature::from_kelvin(0.0).is_valid());
|
||||
assert!(NoiseTemperature::from_kelvin(290.0).is_valid());
|
||||
assert!(!NoiseTemperature::from_kelvin(-1.0).is_valid());
|
||||
assert!(!NoiseTemperature::from_kelvin(f64::NAN).is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thermal_noise_power_follows_the_boltzmann_relation() {
|
||||
let temperature = NoiseTemperature::from_kelvin(290.0);
|
||||
let bandwidth = 1.0e6;
|
||||
let power = thermal_noise_power(temperature, bandwidth).expect("valid");
|
||||
let expected = BOLTZMANN * 290.0 * bandwidth;
|
||||
assert!((power.watts() - expected).abs() < 1e-30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_one_hertz_bandwidth_at_290_kelvin_is_minus_204_dbw() {
|
||||
// k T B with T = 290 K and B = 1 Hz is 4.0e-21 W, which is -203.98 dBW.
|
||||
let power = thermal_noise_power(NoiseTemperature::from_kelvin(290.0), 1.0).expect("valid");
|
||||
assert!(
|
||||
(power.dbw().expect("positive") + 203.98).abs() < 0.01,
|
||||
"{} dBW",
|
||||
power.dbw().expect("positive")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_thirty_six_megahertz_transponder_is_about_minus_128_dbw() {
|
||||
// k T B with T = 290 K and B = 36 MHz is 1.44e-13 W, which is
|
||||
// -128.41 dBW. A one hertz bandwidth at the same temperature is
|
||||
// -203.98 dBW, and 36 MHz is 75.56 dB above one hertz.
|
||||
let power = thermal_noise_power(NoiseTemperature::from_kelvin(290.0), 36.0e6).expect("valid");
|
||||
let dbw = power.dbw().expect("positive");
|
||||
assert!((dbw + 128.41).abs() < 0.01, "{dbw} dBW");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_power_scales_with_temperature_and_bandwidth() {
|
||||
let one_hertz = thermal_noise_power(NoiseTemperature::from_kelvin(100.0), 1.0).expect("valid");
|
||||
let hundred_hertz =
|
||||
thermal_noise_power(NoiseTemperature::from_kelvin(100.0), 100.0).expect("valid");
|
||||
let double_temperature =
|
||||
thermal_noise_power(NoiseTemperature::from_kelvin(200.0), 1.0).expect("valid");
|
||||
// Each doubling of either factor adds three decibels.
|
||||
assert!(
|
||||
(hundred_hertz.dbw().expect("positive") - one_hertz.dbw().expect("positive") - 20.0).abs()
|
||||
< 1e-9
|
||||
);
|
||||
assert!(
|
||||
(double_temperature.dbw().expect("positive")
|
||||
- one_hertz.dbw().expect("positive")
|
||||
- 3.010_299_956_639_812)
|
||||
.abs()
|
||||
< 1e-9
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_bandwidth_produces_no_noise() {
|
||||
assert_eq!(
|
||||
thermal_noise_power(NoiseTemperature::from_kelvin(290.0), 0.0),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_arguments_are_rejected() {
|
||||
assert_eq!(
|
||||
thermal_noise_power(NoiseTemperature::from_kelvin(-1.0), 1.0e6),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
thermal_noise_power(NoiseTemperature::from_kelvin(290.0), -1.0),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
thermal_noise_power(NoiseTemperature::from_kelvin(290.0), f64::NAN),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_includes_the_unit() {
|
||||
assert_eq!(NoiseTemperature::from_kelvin(290.0).to_string(), "290 K");
|
||||
}
|
||||
Loading…
Reference in New Issue