//! 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() ); }