gainlineup/
lib.rs

1use std::fs;
2use std::path::Path;
3
4use touchstone::Network;
5
6use serde::Deserialize;
7use toml;
8
9pub mod cli;
10mod file_operations;
11mod open;
12mod plot;
13
14// the input to our `create_user` handler
15#[derive(Clone, Debug)]
16pub struct Block {
17    pub name: String,
18    pub gain: f64,                                 // dB
19    pub noise_figure: f64, // dB, nf would be ambiguous between noise factor and noise figure
20    pub output_1db_compression_point: Option<f64>, // dBm
21}
22
23impl Block {
24    pub fn new(
25        name: String,
26        gain: f64,
27        noise_figure: f64,
28        output_1db_compression_point: Option<f64>,
29    ) -> Block {
30        Block {
31            name,
32            gain,
33            noise_figure,
34            output_1db_compression_point,
35        }
36    }
37}
38
39#[derive(Clone, Debug)]
40pub struct SignalNode {
41    pub name: String,
42    pub power: f64,             // dBm
43    pub noise_temperature: f64, // cumulative, dB
44    pub cumulative_gain: f64,   // cumulative, dB (set to 0 at start)
45}
46
47// the structure of the toml files
48//
49// Config is the top level toml file
50//
51#[derive(Debug)]
52pub struct Config {
53    pub input_power: f64,
54    pub frequency: f64,
55    pub blocks: Vec<Block>,
56}
57
58#[derive(Deserialize, Debug)]
59struct IncludedConfig {
60    blocks: Vec<BlockConfig>,
61}
62
63#[derive(Deserialize, Debug)]
64#[serde(tag = "type", rename_all = "snake_case")]
65enum BlockConfig {
66    Explicit {
67        name: String,
68        gain: f64,
69        noise_figure: f64,
70        output_1db_compression_point: Option<f64>,
71    },
72    Touchstone {
73        file_path: String,
74        name: String,
75        noise_figure: Option<f64>,
76        output_1db_compression_point: Option<f64>,
77    },
78    Include {
79        path: String,
80    },
81}
82
83pub fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
84    // println!("\n----------------------------\n");
85    // println!("Loading Config: {}", path);
86    let config_content = fs::read_to_string(path)?;
87    // println!("Config Content: {}", config_content);
88
89    // We need an intermediate struct to parse the TOML because Config now holds Vec<Block>
90    // but the TOML contains BlockConfigs
91    #[derive(Deserialize)]
92    struct IntermediateConfig {
93        input_power: f64,
94        frequency: f64,
95        blocks: Vec<BlockConfig>,
96    }
97
98    let intermediate_config: IntermediateConfig = toml::from_str(&config_content)?;
99    // println!("Config: {:#?}", config);
100
101    let mut blocks = Vec::new();
102    let config_path = Path::new(path);
103    let base_dir = config_path.parent().unwrap_or_else(|| Path::new("."));
104
105    load_blocks_recursive(
106        intermediate_config.blocks,
107        intermediate_config.frequency,
108        &mut blocks,
109        base_dir,
110    )?;
111
112    // println!("\n----------------------------\n");
113
114    Ok(Config {
115        input_power: intermediate_config.input_power,
116        frequency: intermediate_config.frequency,
117        blocks,
118    })
119}
120
121fn load_blocks_recursive(
122    block_configs: Vec<BlockConfig>,
123    frequency: f64,
124    blocks: &mut Vec<Block>,
125    base_dir: &Path,
126) -> Result<(), Box<dyn std::error::Error>> {
127    for block_config in block_configs {
128        match block_config {
129            BlockConfig::Explicit {
130                name,
131                gain,
132                noise_figure,
133                output_1db_compression_point,
134            } => {
135                blocks.push(Block {
136                    name,
137                    gain,
138                    noise_figure,
139                    output_1db_compression_point,
140                });
141            }
142            BlockConfig::Touchstone {
143                file_path,
144                name,
145                noise_figure,
146                output_1db_compression_point,
147            } => {
148                // Touchstone files might also be relative to the config file
149                let full_path = base_dir.join(&file_path);
150                let gain = touchstone_file_path_and_frequency_to_gain(
151                    full_path.to_string_lossy().to_string(),
152                    frequency,
153                );
154
155                let noise_figure_default = gain * -1.0; // only handles passives right now
156                let output_1db_compression_point_default = 99.0; // 99 dBm
157
158                let final_noise_figure = noise_figure.unwrap_or(noise_figure_default);
159                let final_output_1db_compression_point =
160                    output_1db_compression_point.or(Some(output_1db_compression_point_default));
161
162                blocks.push(Block {
163                    name,
164                    gain,
165                    noise_figure: final_noise_figure,
166                    output_1db_compression_point: final_output_1db_compression_point,
167                });
168            }
169            BlockConfig::Include { path } => {
170                let included_path = base_dir.join(&path);
171                // println!("Loading Included Config: {}", included_path.display());
172                let content = fs::read_to_string(&included_path)?;
173                let included: IncludedConfig = toml::from_str(&content)?;
174
175                let new_base_dir = included_path.parent().unwrap_or_else(|| Path::new("."));
176                load_blocks_recursive(included.blocks, frequency, blocks, new_base_dir)?;
177            }
178        }
179    }
180    Ok(())
181}
182
183pub fn touchstone_file_path_and_frequency_to_gain(file_path: String, frequency_in_hz: f64) -> f64 {
184    let s2p = Network::new(file_path.clone());
185
186    let gain_vector = s2p.s_db(2, 1); // uses 1-based indexing
187
188    let gain = gain_vector
189        .iter()
190        .find(|frequency_db| frequency_db.frequency == frequency_in_hz)
191        .unwrap()
192        .s_db
193        .decibel();
194
195    gain
196}
197
198// returns output power, handling compression point if present
199pub fn cascade(input_power: f64, block1: Block) -> f64 {
200    let output_power_without_compression = input_power + block1.gain;
201    if let Some(op1db) = block1.output_1db_compression_point {
202        if output_power_without_compression > op1db + 1.0 {
203            return op1db + 1.0;
204        }
205    }
206    output_power_without_compression
207}
208
209// returns output signal node, handling compression point if present
210pub fn cascade_node(signal: SignalNode, block1: Block) -> SignalNode {
211    let output_node_name = block1.name + " Output";
212    let block_noise_temperature =
213        rfconversions::noise::noise_temperature_from_noise_figure(block1.noise_figure);
214    let cumulative_gain_linear = rfconversions::power::db_to_linear(signal.cumulative_gain)
215        + rfconversions::power::db_to_linear(block1.gain);
216
217    // handle compression point
218    let output_power_without_compression = signal.power + block1.gain;
219    let output_power = if let Some(op1db) = block1.output_1db_compression_point {
220        if output_power_without_compression > op1db + 1.0 {
221            op1db + 1.0
222        } else {
223            output_power_without_compression
224        }
225    } else {
226        output_power_without_compression
227    };
228
229    let stage_gain = output_power - signal.power;
230
231    SignalNode {
232        name: output_node_name,
233        power: output_power,
234        noise_temperature: signal.noise_temperature
235            + block_noise_temperature / cumulative_gain_linear,
236        cumulative_gain: signal.cumulative_gain + stage_gain,
237    }
238}
239
240// returns final output signal node, handling compression point if present
241pub fn cascade_vector_return_output(input_signal: SignalNode, blocks: Vec<Block>) -> SignalNode {
242    let mut cascading_signal = input_signal;
243
244    for block in blocks {
245        cascading_signal = cascade_node(cascading_signal, block);
246    }
247    cascading_signal
248}
249
250// returns vector of output signal nodes, handling compression point if present
251pub fn cascade_vector_return_vector(
252    input_signal: SignalNode,
253    blocks: Vec<Block>,
254) -> Vec<SignalNode> {
255    let mut cascading_signal = input_signal;
256    let mut node_vector: Vec<SignalNode> = vec![cascading_signal.clone()];
257    for block in blocks.iter() {
258        cascading_signal = cascade_node(cascading_signal, block.clone());
259        node_vector.push(cascading_signal.clone());
260    }
261    node_vector
262}
263
264// This module contains tests for the cascade function and the Node struct
265
266#[cfg(test)]
267mod tests {
268
269    use super::*;
270
271    #[test]
272    fn one_part() {
273        let input_power: f64 = -30.0;
274        let amplifier = super::Block {
275            name: "Simple Amplifier".to_string(),
276            gain: 10.0,
277            noise_figure: 3.0,
278            output_1db_compression_point: None,
279        };
280        let output_power = super::cascade(input_power, amplifier);
281
282        assert_eq!(output_power, -20.0);
283    }
284
285    #[test]
286    fn one_part_new() {
287        let input_power: f64 = -30.0;
288        let name = "Simple Amplifier".to_string();
289        let gain = 10.0;
290        let noise_figure = 3.0;
291        let amplifier = super::Block::new(name, gain, noise_figure, None);
292        let output_power = super::cascade(input_power, amplifier);
293
294        assert_eq!(output_power, -20.0);
295    }
296
297    #[test]
298    fn one_part_node() {
299        let input_power: f64 = -30.0;
300        let input_node = super::SignalNode {
301            name: "Input".to_string(),
302            power: input_power,
303            noise_temperature: 290.0,
304            cumulative_gain: 0.0, // starting/initial/input node of cascade
305        };
306        let amplifier = super::Block {
307            name: "Simple Amplifier".to_string(),
308            gain: 10.0,
309            noise_figure: 3.0,
310            output_1db_compression_point: None,
311        };
312        let output_node = super::cascade_node(input_node, amplifier);
313
314        assert_eq!(output_node.power, -20.0);
315        assert_eq!(output_node.name, "Simple Amplifier Output");
316        // assert_eq!(output_node.noise_temperature, rfconversions::noise::noise_temperature_from_noise_figure(3.0));
317        let output_noise_figure = rfconversions::noise::noise_figure_from_noise_temperature(
318            output_node.noise_temperature,
319        );
320        assert_eq!(output_noise_figure, 3.202456829285537);
321    }
322
323    #[test]
324    fn one_part_lna_node() {
325        let input_power: f64 = -30.0;
326        let input_node = super::SignalNode {
327            name: "Input".to_string(),
328            power: input_power,
329            noise_temperature: 290.0,
330            cumulative_gain: 0.0, // starting/initial/input node of cascade
331        };
332        let amplifier = super::Block {
333            name: "Low Noise Amplifier".to_string(),
334            gain: 30.0,
335            noise_figure: 3.0,
336            output_1db_compression_point: None,
337        };
338
339        let output_node = super::cascade_node(input_node, amplifier);
340
341        assert_eq!(output_node.power, 0.0);
342        assert_eq!(output_node.name, "Low Noise Amplifier Output");
343        // assert_eq!(output_node.noise_temperature, rfconversions::noise::noise_temperature_from_noise_figure(3.0));
344        let output_noise_figure = rfconversions::noise::noise_figure_from_noise_temperature(
345            output_node.noise_temperature,
346        );
347        assert_eq!(output_noise_figure, 3.0124584457866126);
348    }
349
350    #[test]
351    fn two_part_node() {
352        let input_power: f64 = -30.0;
353        let input_node = super::SignalNode {
354            name: "Input".to_string(),
355            power: input_power,
356            noise_temperature: 290.0,
357            cumulative_gain: 0.0, // starting/initial/input node of cascade
358        };
359        let amplifier = super::Block {
360            name: "Low Noise Amplifier".to_string(),
361            gain: 30.0,
362            noise_figure: 3.0,
363            output_1db_compression_point: None,
364        };
365        let attenuator = super::Block {
366            name: "Attenuator".to_string(),
367            gain: -6.0,
368            noise_figure: 6.0,
369            output_1db_compression_point: None,
370        };
371        let intermediate_node = super::cascade_node(input_node, amplifier);
372
373        assert_eq!(intermediate_node.cumulative_gain, 30.0);
374
375        let output_node = super::cascade_node(intermediate_node, attenuator);
376
377        assert_eq!(output_node.power, -6.0);
378        assert_eq!(output_node.cumulative_gain, 24.0);
379
380        assert_eq!(output_node.name, "Attenuator Output");
381        // assert_eq!(output_node.noise_temperature, rfconversions::noise::noise_temperature_from_noise_figure(3.0));
382        let output_noise_figure = rfconversions::noise::noise_figure_from_noise_temperature(
383            output_node.noise_temperature,
384        );
385        assert_eq!(output_noise_figure, 3.018922107070044);
386    }
387
388    #[test]
389    fn two_part_node_cascade_vector_return_output() {
390        let input_power: f64 = -30.0;
391        let input_node = super::SignalNode {
392            name: "Input".to_string(),
393            power: input_power,
394            noise_temperature: 290.0,
395            cumulative_gain: 0.0, // starting/initial/input node of cascade
396        };
397        let amplifier = super::Block {
398            name: "Low Noise Amplifier".to_string(),
399            gain: 30.0,
400            noise_figure: 3.0,
401            output_1db_compression_point: None,
402        };
403        let attenuator = super::Block {
404            name: "Attenuator".to_string(),
405            gain: -6.0,
406            noise_figure: 6.0,
407            output_1db_compression_point: None,
408        };
409        let blocks = vec![amplifier, attenuator];
410        let output_node = super::cascade_vector_return_output(input_node, blocks);
411
412        assert_eq!(output_node.power, -6.0);
413        assert_eq!(output_node.cumulative_gain, 24.0);
414
415        assert_eq!(output_node.name, "Attenuator Output");
416        // assert_eq!(output_node.noise_temperature, rfconversions::noise::noise_temperature_from_noise_figure(3.0));
417        let output_noise_figure = rfconversions::noise::noise_figure_from_noise_temperature(
418            output_node.noise_temperature,
419        );
420        assert_eq!(output_noise_figure, 3.018922107070044);
421    }
422
423    #[test]
424    fn two_part_node_cascade_vector_return_vector() {
425        let input_power: f64 = -30.0;
426        let input_node = super::SignalNode {
427            name: "Input".to_string(),
428            power: input_power,
429            noise_temperature: 290.0,
430            cumulative_gain: 0.0, // starting/initial/input node of cascade
431        };
432        let amplifier = super::Block {
433            name: "Low Noise Amplifier".to_string(),
434            gain: 30.0,
435            noise_figure: 3.0,
436            output_1db_compression_point: None,
437        };
438        let attenuator = super::Block {
439            name: "Attenuator".to_string(),
440            gain: -6.0,
441            noise_figure: 6.0,
442            output_1db_compression_point: None,
443        };
444        let blocks = vec![amplifier, attenuator];
445        let cascade_vector = super::cascade_vector_return_vector(input_node, blocks);
446
447        let output_node = cascade_vector.last().unwrap();
448        assert_eq!(output_node.power, -6.0);
449        assert_eq!(output_node.cumulative_gain, 24.0);
450
451        assert_eq!(output_node.name, "Attenuator Output");
452        // assert_eq!(output_node.noise_temperature, rfconversions::noise::noise_temperature_from_noise_figure(3.0));
453        let output_noise_figure = rfconversions::noise::noise_figure_from_noise_temperature(
454            output_node.noise_temperature,
455        );
456        assert_eq!(output_noise_figure, 3.018922107070044);
457    }
458
459    #[test]
460    fn two_part_node_cascade_vector_return_vector_with_compression() {
461        let input_power: f64 = -30.0;
462        let input_node = super::SignalNode {
463            name: "Input".to_string(),
464            power: input_power,
465            noise_temperature: 290.0,
466            cumulative_gain: 0.0, // starting/initial/input node of cascade
467        };
468        let low_noise_amplifier = super::Block {
469            name: "Low Noise Amplifier".to_string(),
470            gain: 30.0,
471            noise_figure: 3.0,
472            output_1db_compression_point: Some(5.0),
473        };
474        let attenuator = super::Block {
475            name: "Attenuator".to_string(),
476            gain: -6.0,
477            noise_figure: 6.0,
478            output_1db_compression_point: None,
479        };
480        let high_power_amplifier = super::Block {
481            name: "High Power Amplifier".to_string(),
482            gain: 30.0,
483            noise_figure: 3.0,
484            output_1db_compression_point: Some(20.0),
485        };
486        let blocks = vec![low_noise_amplifier, attenuator, high_power_amplifier];
487        let cascade_vector = super::cascade_vector_return_vector(input_node, blocks);
488
489        let output_node = cascade_vector.last().unwrap();
490        assert_eq!(output_node.power, 21.0);
491        assert_eq!(output_node.cumulative_gain, 51.0);
492
493        assert_eq!(output_node.name, "High Power Amplifier Output");
494        // assert_eq!(output_node.noise_temperature, rfconversions::noise::noise_temperature_from_noise_figure(3.0));
495        let output_noise_figure = rfconversions::noise::noise_figure_from_noise_temperature(
496            output_node.noise_temperature,
497        );
498        assert_eq!(output_noise_figure, 3.020645644372404);
499    }
500
501    use std::fs;
502    use std::path::Path;
503    use toml;
504
505    use crate::{cascade_vector_return_vector, SignalNode};
506
507    // Helper to parse config for tests
508    fn parse_test_config(content: &str) -> Result<Config, Box<dyn std::error::Error>> {
509        #[derive(Deserialize)]
510        struct IntermediateConfig {
511            input_power: f64,
512            frequency: f64,
513            blocks: Vec<BlockConfig>,
514        }
515        let intermediate_config: IntermediateConfig = toml::from_str(content)?;
516        let mut blocks = Vec::new();
517        // For tests, we assume base_dir is current dir or not important for explicit blocks
518        let base_dir = Path::new(".");
519        load_blocks_recursive(
520            intermediate_config.blocks,
521            intermediate_config.frequency,
522            &mut blocks,
523            base_dir,
524        )?;
525        Ok(Config {
526            input_power: intermediate_config.input_power,
527            frequency: intermediate_config.frequency,
528            blocks,
529        })
530    }
531
532    #[test]
533    fn test_load_simple_config() {
534        let cwd = std::env::current_dir().unwrap();
535        let config_path = std::env::args()
536            .nth(1)
537            .unwrap_or_else(|| "files/simple_config.toml".to_string());
538        let full_path_to_config = cwd.join(config_path);
539        let config_content = fs::read_to_string(full_path_to_config.display().to_string()).unwrap();
540        let config = parse_test_config(&config_content).unwrap();
541        assert_eq!(config.input_power, -70.0);
542        assert_eq!(config.frequency, 6.0e9);
543        assert_eq!(config.blocks.len(), 3);
544    }
545
546    #[test]
547    fn test_load_include_config() {
548        let cwd = std::env::current_dir().unwrap();
549        let config_path = std::env::args()
550            .nth(1)
551            .unwrap_or_else(|| "files/include_directive/config.toml".to_string());
552        let full_path_to_config = cwd.join(config_path);
553        // We need to use load_config here to handle includes correctly relative to file path
554        let config = load_config(&full_path_to_config.display().to_string()).unwrap();
555        assert_eq!(config.blocks.len(), 6);
556    }
557
558    #[test]
559    fn test_compression() {
560        let cwd = std::env::current_dir().unwrap();
561        let config_path = std::env::args()
562            .nth(1)
563            .unwrap_or_else(|| "files/compression/compression_test.toml".to_string());
564        let full_path_to_config = cwd.join(config_path);
565        // We need to use load_config here to handle includes correctly relative to file path
566        let config = load_config(&full_path_to_config.display().to_string()).unwrap();
567        assert_eq!(config.blocks.len(), 3);
568
569        let input_node = SignalNode {
570            name: "Input".to_string(),
571            power: config.input_power,
572            noise_temperature: 290.0,
573            cumulative_gain: 0.0,
574        };
575        let cascade = cascade_vector_return_vector(input_node, config.blocks);
576
577        assert_eq!(cascade.last().unwrap().power, 21.0);
578    }
579
580    #[test]
581    fn test_touchstone_options() {
582        let cwd = std::env::current_dir().unwrap();
583        let config_path = std::env::args()
584            .nth(1)
585            .unwrap_or_else(|| "files/touchstone_options/config.toml".to_string());
586        let full_path_to_config = cwd.join(config_path);
587        // We need to use load_config here to handle includes correctly relative to file path
588        let config = load_config(&full_path_to_config.display().to_string()).unwrap();
589        assert_eq!(config.blocks.len(), 3);
590    }
591}