use std::default::Default;
use std::fmt;
use crate::block::Block;
use crate::constants;
use crate::node::SignalNode;
#[doc(alias = "signal")]
#[doc(alias = "input power")]
#[derive(Clone, Debug)]
pub struct Input {
pub frequency_hz: f64,
pub bandwidth_hz: f64,
pub power_dbm: f64,
pub noise_temperature_k: Option<f64>,
}
impl fmt::Display for Input {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Input {{ frequency: {}, bandwidth: {}, power: {} }}",
self.frequency_hz, self.bandwidth_hz, self.power_dbm
)
}
}
impl Default for Input {
fn default() -> Self {
Self {
frequency_hz: 0.0, bandwidth_hz: 100.0, power_dbm: 0.0, noise_temperature_k: None,
}
}
}
impl Input {
#[must_use]
pub fn new(
frequency_hz: f64,
bandwidth_hz: f64,
power_dbm: f64,
noise_temperature_k: Option<f64>,
) -> Input {
Input {
frequency_hz,
bandwidth_hz,
power_dbm,
noise_temperature_k,
}
}
#[must_use]
pub fn noise_spectral_density(&self) -> f64 {
let k = constants::BOLTZMANN;
let t = self.noise_temperature_k.unwrap_or(270.0);
let noise_spectral_density = k * t;
tracing::debug!("Noise Spectral Density: (W/Hz) {}", noise_spectral_density);
let noise_spectral_density_dbm_per_hz =
rfconversions::power::watts_to_dbm(noise_spectral_density);
tracing::debug!(
"Noise Spectral Density: (dBm/Hz) {}",
noise_spectral_density_dbm_per_hz
);
noise_spectral_density_dbm_per_hz
}
#[must_use]
pub fn noise_power(&self) -> f64 {
let k = constants::BOLTZMANN;
let t = self.noise_temperature_k.unwrap_or(270.0);
let noise_power = k * t * self.bandwidth_hz;
tracing::debug!("Noise Power: (W) {}", noise_power);
let noise_power_dbm = rfconversions::power::watts_to_dbm(noise_power);
tracing::debug!("Noise Power: (dBm) {}", noise_power_dbm);
noise_power_dbm
}
#[must_use]
pub fn cascade_block(&self, block: &Block) -> SignalNode {
tracing::debug!("Start INPUT");
let output_node_name = block.name.clone() + " Output";
let block_noise_factor =
rfconversions::noise::noise_factor_from_noise_figure(block.noise_figure_db);
let block_noise_temperature =
rfconversions::noise::noise_temperature_from_noise_factor(block_noise_factor);
let output_power_dbm_without_compression = self.power_dbm + block.gain_db;
let output_power_dbm = if let Some(output_p1db_dbm) = block.output_p1db_dbm {
if output_power_dbm_without_compression > output_p1db_dbm + 1.0 {
output_p1db_dbm + 1.0
} else {
output_power_dbm_without_compression
}
} else {
output_power_dbm_without_compression
};
let stage_power_gain_db = output_power_dbm - self.power_dbm;
let stage_power_gain_linear = rfconversions::power::db_to_linear(stage_power_gain_db);
let cumulative_noise_factor = block_noise_factor;
let cumulative_noise_figure =
rfconversions::noise::noise_figure_from_noise_factor(cumulative_noise_factor);
let cumulative_noise_temperature =
if let Some(noise_temperature_k) = self.noise_temperature_k {
Some(noise_temperature_k + block_noise_temperature / stage_power_gain_linear)
} else {
Some(270.0 + block_noise_temperature / stage_power_gain_linear)
};
let input_noise_power = self.noise_power();
tracing::debug!("Input Noise Power: (dBm) {}", input_noise_power);
let output_noise_power_from_input_dbm = input_noise_power + stage_power_gain_db;
let output_noise_power_from_block_dbm = block.output_noise_power(self.bandwidth_hz);
tracing::debug!(
"Output Noise Power from Input: (dBm) {}",
output_noise_power_from_input_dbm
);
tracing::debug!(
"Output Noise Power from Block: (dBm) {}",
output_noise_power_from_block_dbm
);
let output_noise_power_from_input_watts =
rfconversions::power::dbm_to_watts(output_noise_power_from_input_dbm);
let output_noise_power_from_block_watts =
rfconversions::power::dbm_to_watts(output_noise_power_from_block_dbm);
let total_noise_power_at_output_watts =
output_noise_power_from_input_watts + output_noise_power_from_block_watts;
tracing::debug!(
"Total Noise Power at Output: (W) {}",
total_noise_power_at_output_watts
);
let output_noise_power_at_output_dbm =
rfconversions::power::watts_to_dbm(total_noise_power_at_output_watts);
tracing::debug!(
"Output Noise Power at Output: (dBm) {}",
output_noise_power_at_output_dbm
);
tracing::debug!("End INPUT");
let cumulative_oip3_dbm = block.output_ip3_dbm;
let sfdr_db = cumulative_oip3_dbm.map(|oip3| {
let noise_floor_dbm =
-174.0 + 10.0 * self.bandwidth_hz.log10() + cumulative_noise_figure;
2.0 / 3.0 * (oip3 - noise_floor_dbm)
});
SignalNode {
name: output_node_name,
signal_power_dbm: output_power_dbm,
signal_frequency_hz: self.frequency_hz,
signal_bandwidth_hz: self.bandwidth_hz,
cumulative_noise_figure_db: cumulative_noise_figure,
cumulative_gain_db: stage_power_gain_db,
cumulative_noise_temperature,
noise_power_dbm: output_noise_power_at_output_dbm,
cumulative_oip3_dbm,
sfdr_db,
output_p1db_dbm: block.output_p1db_dbm,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cascade_block() {
let input = Input::new(100.0, 100.0, 0.0, None);
let block = Block {
name: "Test Block".to_string(),
gain_db: 10.0,
noise_figure_db: 10.0,
output_p1db_dbm: None,
output_ip3_dbm: None,
};
let signal_node = input.cascade_block(&block);
assert_eq!(signal_node.name, "Test Block Output");
assert_eq!(signal_node.signal_power_dbm, 10.0);
assert_eq!(signal_node.signal_frequency_hz, 100.0);
assert_eq!(signal_node.signal_bandwidth_hz, 100.0);
assert_eq!(signal_node.cumulative_noise_figure_db, 10.0);
assert_eq!(signal_node.cumulative_gain_db, 10.0);
assert_eq!(signal_node.cumulative_noise_temperature, Some(531.0));
assert!(
(signal_node.noise_power_dbm - (-124.84)).abs() < 0.01,
"Expected noise power around -124.84 dBm, got {}",
signal_node.noise_power_dbm
);
}
#[test]
fn test_cascade_block_with_compression() {
let input = Input::new(1.0e9, 1.0e6, 0.0, None);
let block = Block {
name: "Compressing Amplifier".to_string(),
gain_db: 20.0,
noise_figure_db: 3.0,
output_p1db_dbm: Some(10.0),
output_ip3_dbm: None,
};
let signal_node = input.cascade_block(&block);
assert_eq!(signal_node.name, "Compressing Amplifier Output");
assert_eq!(signal_node.signal_power_dbm, 11.0);
assert_eq!(signal_node.cumulative_gain_db, 11.0);
assert!(
signal_node.noise_power_dbm < -50.0,
"Noise power should be well below P1dB (got {} dBm), indicating no compression",
signal_node.noise_power_dbm
);
assert_eq!(signal_node.signal_frequency_hz, 1.0e9);
assert_eq!(signal_node.signal_bandwidth_hz, 1.0e6);
}
#[test]
fn test_default() {
let input = Input::default();
assert_eq!(input.frequency_hz, 0.0);
assert_eq!(input.bandwidth_hz, 100.0);
assert_eq!(input.power_dbm, 0.0);
assert_eq!(input.noise_temperature_k, None);
}
#[test]
fn test_display() {
let input = Input::new(1.0e9, 1.0e6, -30.0, Some(290.0));
let s = format!("{}", input);
assert!(s.contains("1000000000")); assert!(s.contains("1000000")); assert!(s.contains("-30")); }
#[test]
fn test_noise_spectral_density_at_290k() {
let input = Input::new(1.0e9, 1.0e6, -30.0, Some(290.0));
let nsd = input.noise_spectral_density();
assert!(
(nsd - (-174.0)).abs() < 0.1,
"NSD at 290K should be ~-174 dBm/Hz, got {}",
nsd
);
}
#[test]
fn test_noise_spectral_density_defaults_to_270k() {
let input = Input::new(1.0e9, 1.0e6, -30.0, None);
let nsd = input.noise_spectral_density();
assert!(
(nsd - (-174.32)).abs() < 0.1,
"NSD at 270K should be ~-174.32 dBm/Hz, got {}",
nsd
);
}
#[test]
fn test_noise_power_at_standard_conditions() {
let input = Input::new(1.0e9, 1.0e6, -30.0, Some(290.0));
let np = input.noise_power();
assert!(
(np - (-114.0)).abs() < 0.1,
"Noise power at 290K/1MHz should be ~-114 dBm, got {}",
np
);
}
#[test]
fn test_noise_power_scales_with_bandwidth() {
let input_1mhz = Input::new(1.0e9, 1.0e6, -30.0, Some(290.0));
let input_2mhz = Input::new(1.0e9, 2.0e6, -30.0, Some(290.0));
let diff = input_2mhz.noise_power() - input_1mhz.noise_power();
assert!(
(diff - 3.01).abs() < 0.1,
"Doubling BW should add ~3 dB, got {} dB difference",
diff
);
}
#[test]
fn test_cascade_block_with_explicit_noise_temperature() {
let input_290 = Input::new(1.0e9, 1.0e6, -30.0, Some(290.0));
let input_270 = Input::new(1.0e9, 1.0e6, -30.0, Some(270.0));
let block = Block {
name: "LNA".to_string(),
gain_db: 20.0,
noise_figure_db: 2.0,
output_p1db_dbm: None,
output_ip3_dbm: None,
};
let node_290 = input_290.cascade_block(&block);
let node_270 = input_270.cascade_block(&block);
assert!(
node_290.cumulative_noise_temperature.unwrap()
> node_270.cumulative_noise_temperature.unwrap(),
"290K input should produce higher cumulative noise temp than 270K"
);
}
#[test]
fn test_cascade_block_with_ip3() {
let input = Input::new(1.0e9, 1.0e6, -30.0, Some(290.0));
let block = Block {
name: "LNA".to_string(),
gain_db: 20.0,
noise_figure_db: 2.0,
output_p1db_dbm: Some(10.0),
output_ip3_dbm: Some(25.0),
};
let node = input.cascade_block(&block);
assert_eq!(node.cumulative_oip3_dbm, Some(25.0));
assert!(node.sfdr_db.is_some());
assert!(node.sfdr_db.unwrap() > 0.0);
}
#[test]
fn test_cascade_block_no_ip3_means_no_sfdr() {
let input = Input::new(1.0e9, 1.0e6, -30.0, Some(290.0));
let block = Block {
name: "Filter".to_string(),
gain_db: -3.0,
noise_figure_db: 3.0,
output_p1db_dbm: None,
output_ip3_dbm: None,
};
let node = input.cascade_block(&block);
assert_eq!(node.cumulative_oip3_dbm, None);
assert_eq!(node.sfdr_db, None);
}
#[test]
fn test_cascade_block_attenuator() {
let input = Input::new(1.0e9, 1.0e6, -20.0, Some(290.0));
let atten = Block {
name: "10dB Pad".to_string(),
gain_db: -10.0,
noise_figure_db: 10.0,
output_p1db_dbm: None,
output_ip3_dbm: None,
};
let node = input.cascade_block(&atten);
assert!((node.signal_power_dbm - (-30.0)).abs() < 0.01);
assert!((node.cumulative_gain_db - (-10.0)).abs() < 0.01);
assert!((node.cumulative_noise_figure_db - 10.0).abs() < 0.01);
}
}