Skip to main content

gainlineup/
lib.rs

1#![warn(missing_docs)]
2//! # gainlineup
3//!
4//! A gain lineup toolbox for RF cascade analysis and signal chain modeling.
5//!
6//! This crate provides types and functions for computing cascaded gain, noise figure,
7//! IP3, dynamic range, and compression through a chain of RF blocks (amplifiers,
8//! attenuators, filters, mixers, etc.).
9//!
10//! # Quick Start
11//!
12//! ```
13//! use gainlineup::{Input, Block, cascade_vector_return_output};
14//!
15//! let input = Input::new(1.0e9, 1.0e6, -30.0, Some(270.0));
16//! let blocks = vec![
17//!     Block {
18//!         name: "LNA".to_string(),
19//!         gain_db: 30.0,
20//!         noise_figure_db: 1.5,
21//!         output_p1db_dbm: None,
22//!         output_ip3_dbm: None,
23//!     },
24//! ];
25//! let output = cascade_vector_return_output(input, blocks);
26//! assert_eq!(output.signal_power_dbm, 0.0);
27//! ```
28
29mod block;
30
31/// Command-line interface for the gainlineup tool.
32#[cfg(feature = "cli")]
33#[allow(missing_docs)]
34pub mod cli;
35mod constants;
36mod file_operations;
37mod input;
38mod node;
39mod open;
40
41#[cfg(feature = "plot")]
42mod plot;
43
44mod amplifier_model;
45
46pub use amplifier_model::{AmplifierModel, AmplifierModelBuilder, AmplifierPoint};
47pub use block::{Block, Imd3Point};
48pub use input::Input;
49pub use node::{DynamicRange, SignalNode};
50
51/// Cascade a vector of blocks and return only the final output [`SignalNode`].
52///
53/// # Examples
54///
55/// ```
56/// use gainlineup::{Input, Block, cascade_vector_return_output};
57///
58/// let input = Input::new(1.0e9, 1.0e6, -30.0, Some(270.0));
59/// let blocks = vec![
60///     Block {
61///         name: "LNA".to_string(),
62///         gain_db: 30.0,
63///         noise_figure_db: 1.5,
64///         output_p1db_dbm: None,
65///         output_ip3_dbm: None,
66///     },
67///     Block {
68///         name: "Attenuator".to_string(),
69///         gain_db: -6.0,
70///         noise_figure_db: 6.0,
71///         output_p1db_dbm: None,
72///         output_ip3_dbm: None,
73///     },
74/// ];
75/// let output = cascade_vector_return_output(input, blocks);
76/// assert_eq!(output.signal_power_dbm, -6.0); // -30 + 30 - 6
77/// assert_eq!(output.cumulative_gain_db, 24.0);
78/// ```
79#[doc(alias = "cascade")]
80#[doc(alias = "signal chain")]
81#[doc(alias = "gain lineup")]
82#[must_use]
83pub fn cascade_vector_return_output(input: Input, blocks: Vec<Block>) -> SignalNode {
84    let mut cascading_signal: SignalNode = SignalNode::default(); // will be overwritten in first iteration
85
86    for (i, block) in blocks.iter().enumerate() {
87        if i == 0 {
88            cascading_signal = input.cascade_block(block);
89        } else {
90            cascading_signal = cascading_signal.cascade_block(block);
91        }
92    }
93
94    cascading_signal
95}
96
97/// Cascade a vector of blocks and return a [`SignalNode`] for each stage output.
98///
99/// # Examples
100///
101/// ```
102/// use gainlineup::{Input, Block, cascade_vector_return_vector};
103///
104/// let input = Input::new(1.0e9, 1.0e6, -30.0, Some(270.0));
105/// let blocks = vec![
106///     Block {
107///         name: "LNA".to_string(),
108///         gain_db: 30.0,
109///         noise_figure_db: 1.5,
110///         output_p1db_dbm: None,
111///         output_ip3_dbm: None,
112///     },
113///     Block {
114///         name: "Filter".to_string(),
115///         gain_db: -3.0,
116///         noise_figure_db: 3.0,
117///         output_p1db_dbm: None,
118///         output_ip3_dbm: None,
119///     },
120/// ];
121/// let nodes = cascade_vector_return_vector(input, blocks);
122/// assert_eq!(nodes.len(), 2);
123/// assert_eq!(nodes[0].signal_power_dbm, 0.0);  // after LNA
124/// assert_eq!(nodes[1].signal_power_dbm, -3.0);  // after filter
125/// ```
126#[doc(alias = "cascade")]
127#[doc(alias = "signal chain")]
128#[must_use]
129pub fn cascade_vector_return_vector(input: Input, blocks: Vec<Block>) -> Vec<SignalNode> {
130    let mut cascading_signal: SignalNode = SignalNode::default(); // will be overwritten in first iteration
131
132    // initialize node vector without input node, since the signal nodes are created in the loop and start with the output of the first block
133    let mut node_vector: Vec<SignalNode> = vec![];
134    for (i, block) in blocks.iter().enumerate() {
135        if i == 0 {
136            cascading_signal = input.cascade_block(block);
137        } else {
138            cascading_signal = cascading_signal.cascade_block(block);
139        }
140        node_vector.push(cascading_signal.clone());
141    }
142    node_vector
143}
144
145/// Sweep input power through a cascade of blocks and return the AM-AM curve.
146///
147/// For each input power, the signal is passed through every block in sequence
148/// using each block's compression model. Returns Vec of `(Pin_dBm, Pout_dBm)`.
149///
150/// # Examples
151///
152/// ```
153/// use gainlineup::{Block, cascade_am_am_sweep};
154///
155/// let blocks = vec![
156///     Block {
157///         name: "LNA".to_string(),
158///         gain_db: 20.0,
159///         noise_figure_db: 3.0,
160///         output_p1db_dbm: Some(10.0),
161///         output_ip3_dbm: None,
162///     },
163/// ];
164/// let sweep = cascade_am_am_sweep(&blocks, -40.0, -20.0, 10.0);
165/// assert_eq!(sweep.len(), 3);
166/// assert!((sweep[0].1 - (-20.0)).abs() < 0.01); // -40 + 20 = -20 (linear)
167/// ```
168#[doc(alias = "P1dB")]
169#[doc(alias = "compression")]
170#[must_use]
171pub fn cascade_am_am_sweep(
172    blocks: &[Block],
173    start_dbm: f64,
174    stop_dbm: f64,
175    step_db: f64,
176) -> Vec<(f64, f64)> {
177    let mut powers = vec![];
178    let mut pin = start_dbm;
179    while pin <= stop_dbm + step_db * 0.01 {
180        powers.push(pin);
181        pin += step_db;
182    }
183    powers
184        .iter()
185        .map(|&pin| {
186            let mut power = pin;
187            for block in blocks {
188                power = block.output_power(power);
189            }
190            (pin, power)
191        })
192        .collect()
193}
194
195/// Sweep input power through a cascade and return gain compression curve.
196///
197/// Returns Vec of `(Pin_dBm, Gain_dB)` showing total cascade gain vs. input power.
198///
199/// # Examples
200///
201/// ```
202/// use gainlineup::{Block, cascade_gain_compression_sweep};
203///
204/// let blocks = vec![
205///     Block {
206///         name: "Amp".to_string(),
207///         gain_db: 20.0,
208///         noise_figure_db: 3.0,
209///         output_p1db_dbm: Some(10.0),
210///         output_ip3_dbm: None,
211///     },
212/// ];
213/// let sweep = cascade_gain_compression_sweep(&blocks, -40.0, 0.0, 10.0);
214/// assert!((sweep[0].1 - 20.0).abs() < 0.01); // full gain at low power
215/// assert!(sweep.last().unwrap().1 < 20.0);    // compressed at high power
216/// ```
217#[doc(alias = "gain compression")]
218#[must_use]
219pub fn cascade_gain_compression_sweep(
220    blocks: &[Block],
221    start_dbm: f64,
222    stop_dbm: f64,
223    step_db: f64,
224) -> Vec<(f64, f64)> {
225    cascade_am_am_sweep(blocks, start_dbm, stop_dbm, step_db)
226        .iter()
227        .map(|&(pin, pout)| (pin, pout - pin))
228        .collect()
229}
230
231#[cfg(test)]
232mod tests {
233    #[test]
234    fn two_part_node_cascade_vector_return_output() {
235        let input_power: f64 = -30.0;
236        let input = super::Input {
237            power_dbm: input_power,
238            frequency_hz: 1.0e9, // 1 GHz
239            bandwidth_hz: 0.0,   // CW
240            noise_temperature_k: Some(270.0),
241        };
242        let amplifier = super::Block {
243            name: "Low Noise Amplifier".to_string(),
244            gain_db: 30.0,
245            noise_figure_db: 3.0,
246            output_p1db_dbm: None,
247            output_ip3_dbm: None,
248        };
249        let attenuator = super::Block {
250            name: "Attenuator".to_string(),
251            gain_db: -6.0,
252            noise_figure_db: 6.0,
253            output_p1db_dbm: None,
254            output_ip3_dbm: None,
255        };
256        let blocks = vec![amplifier, attenuator];
257        let output_node = super::cascade_vector_return_output(input, blocks);
258
259        assert_eq!(output_node.signal_power_dbm, -6.0);
260        assert_eq!(output_node.cumulative_gain_db, 24.0);
261
262        assert_eq!(output_node.name, "Attenuator Output");
263
264        // round to 3 decimal places for comparison, because floating point math is not exact
265        let rounded_noise_figure = (output_node.cumulative_noise_figure_db * 1e3).round() / 1e3;
266        assert_eq!(rounded_noise_figure, 3.006);
267    }
268
269    #[test]
270    fn two_part_node_cascade_vector_return_vector() {
271        let input_power: f64 = -30.0;
272        let input = super::Input {
273            power_dbm: input_power,
274            frequency_hz: 1.0e9, // 1 GHz
275            bandwidth_hz: 0.0,   // CW
276            noise_temperature_k: Some(270.0),
277        };
278        let amplifier = super::Block {
279            name: "Low Noise Amplifier".to_string(),
280            gain_db: 30.0,
281            noise_figure_db: 3.0,
282            output_p1db_dbm: None,
283            output_ip3_dbm: None,
284        };
285        let attenuator = super::Block {
286            name: "Attenuator".to_string(),
287            gain_db: -6.0,
288            noise_figure_db: 6.0,
289            output_p1db_dbm: None,
290            output_ip3_dbm: None,
291        };
292        let blocks = vec![amplifier, attenuator];
293        let cascade_vector = super::cascade_vector_return_vector(input, blocks);
294
295        let output_node = cascade_vector.last().unwrap();
296        assert_eq!(output_node.signal_power_dbm, -6.0);
297        assert_eq!(output_node.cumulative_gain_db, 24.0);
298
299        assert_eq!(output_node.name, "Attenuator Output");
300
301        // round to 3 decimal places for comparison, because floating point math is not exact
302        let rounded_noise_figure = (output_node.cumulative_noise_figure_db * 1e3).round() / 1e3;
303        assert_eq!(rounded_noise_figure, 3.006);
304    }
305
306    #[test]
307    fn cascade_am_am_linear() {
308        let blocks = vec![
309            super::Block {
310                name: "LNA".to_string(),
311                gain_db: 20.0,
312                noise_figure_db: 3.0,
313                output_p1db_dbm: None,
314                output_ip3_dbm: None,
315            },
316            super::Block {
317                name: "Atten".to_string(),
318                gain_db: -6.0,
319                noise_figure_db: 6.0,
320                output_p1db_dbm: None,
321                output_ip3_dbm: None,
322            },
323        ];
324        let sweep = super::cascade_am_am_sweep(&blocks, -40.0, -20.0, 10.0);
325        assert_eq!(sweep.len(), 3);
326        // Total gain = 20 - 6 = 14 dB
327        assert!((sweep[0].1 - (-26.0)).abs() < 0.01); // -40 + 14 = -26
328        assert!((sweep[1].1 - (-16.0)).abs() < 0.01); // -30 + 14 = -16
329        assert!((sweep[2].1 - (-6.0)).abs() < 0.01); // -20 + 14 = -6
330    }
331
332    #[test]
333    fn cascade_am_am_with_compression() {
334        let blocks = vec![
335            super::Block {
336                name: "LNA".to_string(),
337                gain_db: 30.0,
338                noise_figure_db: 3.0,
339                output_p1db_dbm: Some(5.0),
340                output_ip3_dbm: None,
341            },
342            super::Block {
343                name: "Driver".to_string(),
344                gain_db: 10.0,
345                noise_figure_db: 5.0,
346                output_p1db_dbm: Some(15.0),
347                output_ip3_dbm: None,
348            },
349        ];
350        let sweep = super::cascade_am_am_sweep(&blocks, -50.0, 0.0, 10.0);
351        // At -50: LNA out = -20, Driver out = -10 (linear)
352        assert!((sweep[0].1 - (-10.0)).abs() < 0.01);
353        // At high power, should compress
354        let last = sweep.last().unwrap();
355        assert!(last.1 <= 16.0, "Should compress at high input");
356    }
357
358    #[test]
359    fn cascade_gain_compression() {
360        let blocks = vec![super::Block {
361            name: "Amp".to_string(),
362            gain_db: 20.0,
363            noise_figure_db: 3.0,
364            output_p1db_dbm: Some(10.0),
365            output_ip3_dbm: None,
366        }];
367        let sweep = super::cascade_gain_compression_sweep(&blocks, -40.0, 0.0, 10.0);
368        // At -40: linear, gain = 20
369        assert!((sweep[0].1 - 20.0).abs() < 0.01);
370        // At 0: compressed, gain < 20
371        let last = sweep.last().unwrap();
372        assert!(last.1 < 20.0, "Gain should compress at high input");
373    }
374
375    #[test]
376    fn two_part_node_cascade_vector_return_vector_with_compression() {
377        let input_power: f64 = -30.0;
378        let input = super::Input {
379            power_dbm: input_power,
380            frequency_hz: 1.0e9, // 1 GHz
381            bandwidth_hz: 0.0,   // CW
382            noise_temperature_k: Some(270.0),
383        };
384        let low_noise_amplifier = super::Block {
385            name: "Low Noise Amplifier".to_string(),
386            gain_db: 30.0,
387            noise_figure_db: 3.0,
388            output_p1db_dbm: Some(5.0),
389            output_ip3_dbm: None,
390        };
391        let attenuator = super::Block {
392            name: "Attenuator".to_string(),
393            gain_db: -6.0,
394            noise_figure_db: 6.0,
395            output_p1db_dbm: None,
396            output_ip3_dbm: None,
397        };
398        let high_power_amplifier = super::Block {
399            name: "High Power Amplifier".to_string(),
400            gain_db: 30.0,
401            noise_figure_db: 3.0,
402            output_p1db_dbm: Some(20.0),
403            output_ip3_dbm: None,
404        };
405        let blocks = vec![low_noise_amplifier, attenuator, high_power_amplifier];
406        let cascade_vector = super::cascade_vector_return_vector(input, blocks);
407
408        let output_node = cascade_vector.last().unwrap();
409        assert_eq!(output_node.signal_power_dbm, 21.0);
410        assert_eq!(output_node.cumulative_gain_db, 51.0);
411
412        assert_eq!(output_node.name, "High Power Amplifier Output");
413
414        // round to 3 decimal places for comparison, because floating point math is not exact
415        let rounded_noise_figure = (output_node.cumulative_noise_figure_db * 1e3).round() / 1e3;
416        assert_eq!(rounded_noise_figure, 3.015);
417    }
418}