use std::default::Default;
use std::fmt;
use crate::constants;
#[doc(alias = "block")]
#[doc(alias = "stage")]
#[doc(alias = "cascade")]
#[derive(Clone, Debug)]
pub struct Block {
pub name: String,
pub gain_db: f64,
pub noise_figure_db: f64,
#[doc(alias = "P1dB")]
pub output_p1db_dbm: Option<f64>,
#[doc(alias = "OIP3")]
pub output_ip3_dbm: Option<f64>,
}
impl fmt::Display for Block {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Block {{ name: {}, gain: {} dB, noise_figure: {} dB",
self.name, self.gain_db, self.noise_figure_db
)?;
if let Some(output_p1db) = self.output_p1db_dbm {
write!(f, ", output_p1db: {} dBm", output_p1db)?;
}
if let Some(output_ip3) = self.output_ip3_dbm {
write!(f, ", output_ip3: {} dBm", output_ip3)?;
}
write!(f, " }}")
}
}
impl Default for Block {
fn default() -> Self {
Self {
name: String::from("default"),
gain_db: 0.0,
noise_figure_db: 0.0,
output_p1db_dbm: None,
output_ip3_dbm: None,
}
}
}
impl Block {
#[must_use]
pub fn noise_temperature(&self) -> f64 {
rfconversions::noise::noise_temperature_from_noise_figure(self.noise_figure_db)
}
#[must_use]
pub fn noise_factor(&self) -> f64 {
rfconversions::noise::noise_factor_from_noise_figure(self.noise_figure_db)
}
#[must_use]
pub fn input_noise_power(&self, bandwidth: f64) -> f64 {
let noise_factor = self.noise_factor();
let noise_temperature = self.noise_temperature();
let f_minus_1 = noise_factor - 1.0;
let ktb = constants::BOLTZMANN * noise_temperature * bandwidth;
rfconversions::power::watts_to_dbm(f_minus_1 * ktb)
}
#[must_use]
pub fn output_noise_power(&self, bandwidth: f64) -> f64 {
tracing::debug!("START BLOCK output_noise_power");
let input_noise_power = self.input_noise_power(bandwidth);
tracing::debug!(
"Input Noise Power (block.input_noise_power): (dBm) {}",
input_noise_power
);
let output_noise_power_without_compression = input_noise_power + self.gain_db;
tracing::debug!(
"Output Noise Power without compression: (dBm) {}",
output_noise_power_without_compression
);
let output_noise_power_dbm = if let Some(output_p1db_dbm) = self.output_p1db_dbm {
if output_noise_power_without_compression > output_p1db_dbm + 1.0 {
output_p1db_dbm + 1.0
} else {
output_noise_power_without_compression
}
} else {
output_noise_power_without_compression
};
let noise_power_gain = output_noise_power_dbm - input_noise_power;
tracing::debug!("Noise Power Gain: (dB) {}", noise_power_gain);
tracing::debug!("Output Noise Power: (dBm) {}", output_noise_power_dbm);
tracing::debug!("END BLOCK output_noise_power");
output_noise_power_dbm
}
#[must_use]
pub fn output_power(&self, input_power: f64) -> f64 {
let output_power_without_compression = input_power + self.gain_db;
if let Some(op1db) = self.output_p1db_dbm {
if output_power_without_compression > op1db + 1.0 {
return op1db + 1.0;
}
}
output_power_without_compression
}
#[must_use]
pub fn power_gain(&self, input_power: f64) -> f64 {
self.output_power(input_power) - input_power
}
#[must_use]
pub fn dynamic_range_db(&self, bandwidth_hz: f64) -> Option<f64> {
let p1db = self.output_p1db_dbm?;
let noise_floor = self.output_noise_power(bandwidth_hz);
Some(p1db - noise_floor)
}
#[must_use]
pub fn input_dynamic_range_db(&self, bandwidth_hz: f64) -> Option<f64> {
let input_p1db = self.output_p1db_dbm? - self.gain_db;
let input_noise = self.input_noise_power(bandwidth_hz);
Some(input_p1db - input_noise)
}
#[must_use]
pub fn am_am_curve(&self, input_powers_dbm: &[f64]) -> Vec<(f64, f64)> {
input_powers_dbm
.iter()
.map(|&pin| (pin, self.output_power(pin)))
.collect()
}
#[must_use]
pub fn am_am_sweep(&self, start_dbm: f64, stop_dbm: f64, step_db: f64) -> Vec<(f64, f64)> {
let powers = sweep_range(start_dbm, stop_dbm, step_db);
self.am_am_curve(&powers)
}
#[must_use]
pub fn gain_compression_curve(&self, input_powers_dbm: &[f64]) -> Vec<(f64, f64)> {
input_powers_dbm
.iter()
.map(|&pin| (pin, self.power_gain(pin)))
.collect()
}
#[must_use]
pub fn gain_compression_sweep(
&self,
start_dbm: f64,
stop_dbm: f64,
step_db: f64,
) -> Vec<(f64, f64)> {
let powers = sweep_range(start_dbm, stop_dbm, step_db);
self.gain_compression_curve(&powers)
}
#[must_use]
pub fn imd3_output_power_dbm(&self, input_power_per_tone_dbm: f64) -> Option<f64> {
let oip3 = self.output_ip3_dbm?;
let pout = self.output_power(input_power_per_tone_dbm);
Some(3.0 * pout - 2.0 * oip3)
}
#[must_use]
pub fn imd3_rejection_db(&self, input_power_per_tone_dbm: f64) -> Option<f64> {
let oip3 = self.output_ip3_dbm?;
let pout = self.output_power(input_power_per_tone_dbm);
Some(2.0 * (oip3 - pout))
}
#[must_use]
pub fn imd3_sweep(&self, start_dbm: f64, stop_dbm: f64, step_db: f64) -> Vec<Imd3Point> {
let oip3 = match self.output_ip3_dbm {
Some(v) => v,
None => return vec![],
};
let powers = sweep_range(start_dbm, stop_dbm, step_db);
powers
.iter()
.map(|&pin| {
let pout = self.output_power(pin);
let im3 = 3.0 * pout - 2.0 * oip3;
Imd3Point {
input_per_tone_dbm: pin,
output_per_tone_dbm: pout,
im3_output_dbm: im3,
rejection_db: pout - im3,
}
})
.collect()
}
}
#[doc(alias = "IMD3")]
#[doc(alias = "intermodulation")]
#[derive(Clone, Debug)]
pub struct Imd3Point {
pub input_per_tone_dbm: f64,
pub output_per_tone_dbm: f64,
pub im3_output_dbm: f64,
pub rejection_db: f64,
}
impl fmt::Display for Imd3Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Imd3Point {{ Pin: {:.1} dBm, Pout: {:.1} dBm, IM3: {:.1} dBm, rejection: {:.1} dB }}",
self.input_per_tone_dbm,
self.output_per_tone_dbm,
self.im3_output_dbm,
self.rejection_db
)
}
}
fn sweep_range(start_dbm: f64, stop_dbm: f64, step_db: f64) -> Vec<f64> {
let mut powers = vec![];
let mut pin = start_dbm;
while pin <= stop_dbm + step_db * 0.01 {
powers.push(pin);
pin += step_db;
}
powers
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default() {
let block = Block::default();
assert_eq!(block.gain_db, 0.0);
assert_eq!(block.noise_figure_db, 0.0);
assert_eq!(block.output_p1db_dbm, None);
assert_eq!(block.noise_temperature(), 0.0);
}
#[test]
fn output_power() {
let input_power: f64 = -30.0;
let amplifier = super::Block {
name: "Simple Amplifier".to_string(),
gain_db: 10.0,
noise_figure_db: 3.0,
output_p1db_dbm: None,
output_ip3_dbm: None,
};
let output_power = amplifier.output_power(input_power);
assert_eq!(output_power, -20.0);
}
#[test]
fn output_power_with_compression_below_threshold() {
let input_power: f64 = -30.0;
let amplifier = super::Block {
name: "Simple Amplifier".to_string(),
gain_db: 10.0,
noise_figure_db: 3.0,
output_p1db_dbm: Some(-20.0),
output_ip3_dbm: None,
};
let output_power = amplifier.output_power(input_power);
assert_eq!(output_power, -20.0);
let power_gain = amplifier.power_gain(input_power);
assert_eq!(power_gain, 10.0);
}
#[test]
fn output_power_with_compression_above_threshold() {
let input_power: f64 = -25.0;
let amplifier = super::Block {
name: "Simple Amplifier".to_string(),
gain_db: 10.0,
noise_figure_db: 3.0,
output_p1db_dbm: Some(-20.0),
output_ip3_dbm: None,
};
let output_power = amplifier.output_power(input_power);
assert_eq!(output_power, -19.0);
let power_gain = amplifier.power_gain(input_power);
assert_eq!(power_gain, 6.0);
}
#[test]
fn output_noise_power_without_compression() {
let bandwidth: f64 = 1.0e6;
let amplifier = super::Block {
name: "Simple Amplifier".to_string(),
gain_db: 10.0,
noise_figure_db: 3.0,
output_p1db_dbm: None,
output_ip3_dbm: None,
};
let output_noise_power = amplifier.output_noise_power(bandwidth);
assert!(
(output_noise_power - (-104.02)).abs() < 0.01,
"Expected output noise power around -104.02 dBm, got {}",
output_noise_power
);
}
#[test]
fn output_noise_power_with_compression_below_threshold() {
let bandwidth: f64 = 1.0e6;
let amplifier = super::Block {
name: "Simple Amplifier".to_string(),
gain_db: 10.0,
noise_figure_db: 3.0,
output_p1db_dbm: Some(-20.0), output_ip3_dbm: None,
};
let output_noise_power = amplifier.output_noise_power(bandwidth);
assert!(
(output_noise_power - (-104.02)).abs() < 0.01,
"Noise should not compress when well below P1dB. Expected -104.02 dBm, got {}",
output_noise_power
);
}
#[test]
fn dynamic_range_with_p1db() {
let amp = Block {
name: "LNA".to_string(),
gain_db: 20.0,
noise_figure_db: 3.0,
output_p1db_dbm: Some(10.0),
output_ip3_dbm: None,
};
let dr = amp.dynamic_range_db(1e6).unwrap();
assert!(
dr > 100.0 && dr < 115.0,
"Expected DR ~104 dB, got {:.1}",
dr
);
}
#[test]
fn dynamic_range_no_p1db() {
let block = Block::default();
assert!(block.dynamic_range_db(1e6).is_none());
}
#[test]
fn input_dynamic_range() {
let amp = Block {
name: "LNA".to_string(),
gain_db: 20.0,
noise_figure_db: 3.0,
output_p1db_dbm: Some(10.0), output_ip3_dbm: None,
};
let dr = amp.input_dynamic_range_db(1e6).unwrap();
assert!(
dr > 100.0 && dr < 115.0,
"Expected input DR ~104 dB, got {:.1}",
dr
);
}
#[test]
fn am_am_curve_linear() {
let amp = Block {
name: "Linear Amp".to_string(),
gain_db: 20.0,
noise_figure_db: 3.0,
output_p1db_dbm: None,
output_ip3_dbm: None,
};
let curve = amp.am_am_curve(&[-30.0, -20.0, -10.0]);
assert_eq!(curve.len(), 3);
assert_eq!(curve[0], (-30.0, -10.0));
assert_eq!(curve[1], (-20.0, 0.0));
assert_eq!(curve[2], (-10.0, 10.0));
}
#[test]
fn am_am_curve_with_compression() {
let amp = Block {
name: "Amp".to_string(),
gain_db: 20.0,
noise_figure_db: 3.0,
output_p1db_dbm: Some(10.0),
output_ip3_dbm: None,
};
let curve = amp.am_am_curve(&[-30.0, -10.0, 0.0, 10.0]);
assert_eq!(curve[0].1, -10.0);
assert_eq!(curve[1].1, 10.0);
assert_eq!(curve[2].1, 11.0);
assert_eq!(curve[3].1, 11.0);
}
#[test]
fn am_am_sweep_count() {
let amp = Block {
name: "Amp".to_string(),
gain_db: 10.0,
noise_figure_db: 3.0,
output_p1db_dbm: None,
output_ip3_dbm: None,
};
let sweep = amp.am_am_sweep(-40.0, -20.0, 5.0);
assert_eq!(sweep.len(), 5); }
#[test]
fn gain_compression_shows_compression() {
let amp = Block {
name: "Amp".to_string(),
gain_db: 20.0,
noise_figure_db: 3.0,
output_p1db_dbm: Some(10.0),
output_ip3_dbm: None,
};
let curve = amp.gain_compression_curve(&[-30.0, 0.0]);
assert_eq!(curve[0].1, 20.0);
assert_eq!(curve[1].1, 11.0);
}
#[test]
fn imd3_output_power() {
let amp = Block {
name: "Amp".to_string(),
gain_db: 20.0,
noise_figure_db: 3.0,
output_p1db_dbm: None,
output_ip3_dbm: Some(30.0),
};
let im3 = amp.imd3_output_power_dbm(-30.0).unwrap();
assert!((im3 - (-90.0)).abs() < 0.01);
}
#[test]
fn imd3_rejection() {
let amp = Block {
name: "Amp".to_string(),
gain_db: 20.0,
noise_figure_db: 3.0,
output_p1db_dbm: None,
output_ip3_dbm: Some(30.0),
};
let rejection = amp.imd3_rejection_db(-30.0).unwrap();
assert!((rejection - 80.0).abs() < 0.01);
}
#[test]
fn imd3_no_ip3() {
let amp = Block::default();
assert!(amp.imd3_output_power_dbm(-30.0).is_none());
assert!(amp.imd3_rejection_db(-30.0).is_none());
}
#[test]
fn imd3_3to1_slope() {
let amp = Block {
name: "Amp".to_string(),
gain_db: 20.0,
noise_figure_db: 3.0,
output_p1db_dbm: None, output_ip3_dbm: Some(30.0),
};
let im3_at_m30 = amp.imd3_output_power_dbm(-30.0).unwrap();
let im3_at_m29 = amp.imd3_output_power_dbm(-29.0).unwrap();
let delta = im3_at_m29 - im3_at_m30;
assert!(
(delta - 3.0).abs() < 0.01,
"IM3 should increase 3 dB per 1 dB input, got {:.2} dB",
delta
);
}
#[test]
fn imd3_sweep_points() {
let amp = Block {
name: "Amp".to_string(),
gain_db: 20.0,
noise_figure_db: 3.0,
output_p1db_dbm: None,
output_ip3_dbm: Some(30.0),
};
let sweep = amp.imd3_sweep(-40.0, -20.0, 5.0);
assert_eq!(sweep.len(), 5);
assert!(sweep[0].rejection_db > sweep[4].rejection_db);
}
#[test]
fn imd3_sweep_no_ip3_empty() {
let amp = Block::default();
let sweep = amp.imd3_sweep(-40.0, -20.0, 5.0);
assert!(sweep.is_empty());
}
#[test]
fn output_noise_power_with_compression_above_threshold() {
let bandwidth: f64 = 1.0e9; let amplifier = super::Block {
name: "High Noise Amplifier".to_string(),
gain_db: 10.0,
noise_figure_db: 3.0,
output_p1db_dbm: Some(-80.0), output_ip3_dbm: None,
};
let output_noise_power = amplifier.output_noise_power(bandwidth);
assert_eq!(
output_noise_power, -79.0,
"Noise should compress to P1dB + 1 dB when above threshold"
);
}
}