use std::env;
use std::fs;
use std::path::Path;
use std::process;
use crate::cascade_vector_return_vector;
use crate::file_operations;
use crate::Block;
use crate::Input;
use crate::SignalNode;
use touchstone::Network;
use serde::Deserialize;
#[derive(Debug)]
pub struct Config {
pub input_power_dbm: f64,
pub frequency_hz: f64,
pub bandwidth_hz: Option<f64>,
pub noise_temperature_k: Option<f64>,
pub blocks: Vec<Block>,
}
#[derive(Deserialize, Debug)]
struct IncludedConfig {
blocks: Vec<BlockConfig>,
}
#[derive(Deserialize, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
enum BlockConfig {
Explicit {
name: String,
#[serde(alias = "gain")]
gain_db: f64,
#[serde(alias = "noise_figure", alias = "nf")]
noise_figure_db: f64,
#[serde(alias = "output_p1db", alias = "op1db")]
output_p1db_dbm: Option<f64>,
#[serde(default, alias = "output_ip3", alias = "oip3")]
output_ip3_dbm: Option<f64>,
},
Touchstone {
file_path: String,
name: String,
#[serde(alias = "noise_figure", alias = "nf")]
noise_figure_db: Option<f64>,
#[serde(alias = "output_p1db", alias = "op1db")]
output_p1db_dbm: Option<f64>,
},
Include {
path: String,
},
}
pub fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
tracing::debug!("Loading config: {}", path);
let config_content = fs::read_to_string(path)?;
tracing::trace!("Config content: {}", config_content);
#[derive(Deserialize)]
struct IntermediateConfig {
#[serde(alias = "input_power", alias = "pin")]
input_power_dbm: f64,
#[serde(alias = "frequency", alias = "f")]
frequency_hz: f64,
#[serde(alias = "bandwidth", alias = "bw")]
bandwidth_hz: Option<f64>,
#[serde(alias = "noise_temperature")]
noise_temperature_k: Option<f64>,
blocks: Vec<BlockConfig>,
}
let intermediate_config: IntermediateConfig = toml::from_str(&config_content)?;
tracing::debug!("Parsed config successfully");
let mut blocks = Vec::new();
let config_path = Path::new(path);
let base_dir = config_path.parent().unwrap_or_else(|| Path::new("."));
load_blocks_recursive(
intermediate_config.blocks,
intermediate_config.frequency_hz,
&mut blocks,
base_dir,
)?;
Ok(Config {
input_power_dbm: intermediate_config.input_power_dbm,
frequency_hz: intermediate_config.frequency_hz,
bandwidth_hz: intermediate_config.bandwidth_hz,
noise_temperature_k: intermediate_config.noise_temperature_k,
blocks,
})
}
fn load_blocks_recursive(
block_configs: Vec<BlockConfig>,
frequency: f64,
blocks: &mut Vec<Block>,
base_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
for block_config in block_configs {
match block_config {
BlockConfig::Explicit {
name,
gain_db,
noise_figure_db,
output_p1db_dbm,
output_ip3_dbm,
} => {
blocks.push(Block {
name,
gain_db,
noise_figure_db,
output_p1db_dbm,
output_ip3_dbm,
});
}
BlockConfig::Touchstone {
file_path,
name,
noise_figure_db,
output_p1db_dbm,
} => {
let full_path = base_dir.join(&file_path);
let TouchstoneValid {
contains_frequency,
gain,
} = touchstone_file_path_and_frequency_to_struct(
full_path.to_string_lossy().to_string(),
frequency,
);
if !contains_frequency {
let file_path_relative_to_config = file_path.clone();
return Err(format!(
"Frequency {} Hz not found in touchstone file {}",
frequency, file_path_relative_to_config
)
.into());
}
let gain = gain.unwrap();
let noise_figure_default = -gain; let output_p1db_default = 99.0;
let final_noise_figure = noise_figure_db.unwrap_or(noise_figure_default);
let final_output_p1db = output_p1db_dbm.or(Some(output_p1db_default));
blocks.push(Block {
name,
gain_db: gain,
noise_figure_db: final_noise_figure,
output_p1db_dbm: final_output_p1db,
output_ip3_dbm: None,
});
}
BlockConfig::Include { path } => {
let included_path = base_dir.join(&path);
tracing::debug!("Loading included config: {}", included_path.display());
let content = fs::read_to_string(&included_path)?;
let included: IncludedConfig = toml::from_str(&content)?;
let new_base_dir = included_path.parent().unwrap_or_else(|| Path::new("."));
load_blocks_recursive(included.blocks, frequency, blocks, new_base_dir)?;
}
}
}
Ok(())
}
pub struct TouchstoneValid {
contains_frequency: bool,
gain: Option<f64>,
}
pub fn touchstone_file_path_and_frequency_to_struct(
file_path: String,
frequency_in_hz: f64,
) -> TouchstoneValid {
let s2p = Network::new(file_path.clone());
let frequency_vector = s2p.f.clone();
let contains_frequency = frequency_vector.contains(&frequency_in_hz);
if !contains_frequency {
return TouchstoneValid {
contains_frequency: false,
gain: None,
};
}
let gain_vector = s2p.s_db(2, 1);
let gain = gain_vector
.iter()
.find(|frequency_db| frequency_db.frequency == frequency_in_hz)
.unwrap()
.s_db
.decibel();
TouchstoneValid {
contains_frequency: true,
gain: Some(gain),
}
}
fn calculate_gainlineup(input: Input, blocks: Vec<Block>) -> Vec<SignalNode> {
let full_cascade: Vec<SignalNode> = cascade_vector_return_vector(input, blocks);
full_cascade
}
#[derive(Debug)]
pub struct Command {}
impl Command {
pub fn run(args: &[String]) -> Result<Command, Box<dyn std::error::Error>> {
if args.len() < 2 {
return Err("not enough arguments".into());
}
if args.len() > 2 {
return Err(
"too many arguments, expecting only 2, such as `gainlineup filepath`".into(),
);
}
match args[1].as_str() {
"--version" | "-v" => {
print_version();
process::exit(0);
}
"--help" | "-h" => {
print_help();
process::exit(0);
}
_ => {
if args.len() > 2 {
return Err(
"too many arguments, expecting only 2, such as `touchstone filepath`"
.into(),
);
}
}
}
let cwd = std::env::current_dir().unwrap();
let file_path = args[1].clone();
println!("Config Path: {}", file_path);
let full_path_to_config = cwd.join(file_path);
println!("Full Path: {}", full_path_to_config.display());
match load_config(&full_path_to_config.display().to_string()) {
Ok(config) => {
let input = Input {
power_dbm: config.input_power_dbm,
frequency_hz: config.frequency_hz,
bandwidth_hz: config.bandwidth_hz.unwrap_or(100.0), noise_temperature_k: Some(config.noise_temperature_k.unwrap_or(290.0)), };
let cascade = calculate_gainlineup(input.clone(), config.blocks.clone());
print_cascade(cascade.clone(), config.blocks.clone());
let file_path = full_path_to_config.display().to_string();
let file_path_config: file_operations::FilePathConfig =
file_operations::get_file_path_config(&file_path);
let output_html_path = if file_path_config.unix_absolute_path
|| file_path_config.windows_absolute_path
{
let mut file_path_html = format!("{}.html", &file_path);
if file_path_config.windows_absolute_path && file_path_html.starts_with(r"\\?\")
{
file_path_html = file_path_html[4..].to_string();
}
file_path_html
} else if file_path_config.relative_path_with_separators {
format!("{}.html", &file_path)
} else if file_path_config.bare_filename {
format!("./{}.html", &file_path)
} else {
panic!(
"file_path_config must have one true value: {:?}",
file_path_config
);
};
println!("Generating HTML table at: {}", output_html_path);
let output_html_path_str = output_html_path.as_str();
match crate::plot::generate_html_table(
&input,
&cascade,
&config.blocks,
output_html_path_str,
) {
Ok(_) => {
crate::open::plot(output_html_path.clone());
}
Err(e) => {
eprintln!("Error generating HTML table: {}", e);
}
}
}
Err(e) => {
eprintln!("Error running calculation or plotting: {}", e);
return Err(e);
}
}
Ok(Command {})
}
}
pub fn print_version() {
println!("gainlineup {}", env!("CARGO_PKG_VERSION"));
}
pub fn print_error(error: &str) {
const RED: &str = "\x1b[31m";
const RESET: &str = "\x1b[0m";
println!("{}Problem parsing arguments: {error}{}", RED, RESET);
}
pub fn print_help() {
const BOLD: &str = "\x1b[1m";
const CYAN: &str = "\x1b[36m";
const GREEN: &str = "\x1b[32m";
const YELLOW: &str = "\x1b[33m";
const RESET: &str = "\x1b[0m";
println!(
"📡 Gainlineup parser and calculator - https://github.com/iancleary/gainlineup{}",
RESET
);
println!();
println!("{}{}VERSION:{}", BOLD, YELLOW, RESET);
println!(" {}{}{}", GREEN, env!("CARGO_PKG_VERSION"), RESET);
println!();
println!("{}{}USAGE:{}", BOLD, YELLOW, RESET);
println!(" {} gainlineup <FILE_PATH>{}", GREEN, RESET);
println!();
println!(" FILE_PATH: path to a toml config file");
println!();
println!(" The toml file is parsed and an interactive plot (html file and js/ folder) ");
println!(" is created next to the source file(s).");
println!();
println!("{}{}OPTIONS:{}", BOLD, YELLOW, RESET);
println!(
" {} -v, --version{}{} Print version information",
GREEN, RESET, RESET
);
println!(
" {} -h, --help{}{} Print help information",
GREEN, RESET, RESET
);
println!();
println!("{}{}EXAMPLES:{}", BOLD, YELLOW, RESET);
println!(" {} # Single file (Relative path){}", CYAN, RESET);
println!(" {} gainlineup files/config.toml{}", GREEN, RESET);
println!();
}
pub fn print_cascade(cascade: Vec<SignalNode>, blocks: Vec<Block>) {
println!();
for (i, node) in cascade.iter().enumerate() {
println!("\nNode {}: {}", i, node.name);
if i == 0 {
println!("Input Level {:>8.2} dBm", node.signal_power_dbm);
} else {
let block_gain = blocks[i - 1].gain_db;
let input_power = node.signal_power_dbm - block_gain;
println!("Input Power\t\t{:>8.2} dBm", input_power);
println!("Block Gain:\t\t{:>8.2} dB", block_gain);
println!("Block NF:\t\t{:>8.2} dB", blocks[i - 1].noise_figure_db);
println!("Cumulative Gain:\t{:>8.2} dB", node.cumulative_gain_db);
println!(
"Cumulative Noise Figure:{:>8.2} dB",
node.cumulative_noise_figure_db
);
println!("Output Power\t\t{:>8.2} dBm", node.signal_power_dbm);
}
}
println!();
println!("Final Cascade Summary:");
println!("----------------------");
println!("Number of Blocks: {}", cascade.len() - 1);
println!("Pin:\t{:>8.2} dBm", cascade[0].signal_power_dbm);
let final_output_power = cascade.last().unwrap().signal_power_dbm;
println!("Pout:\t{:>8.2} dBm", final_output_power);
println!(
"Gain:\t{:>8.2} dB",
cascade.last().unwrap().cumulative_gain_db
);
println!(
"NF:\t{:>8.2} dB",
cascade.last().unwrap().cumulative_noise_figure_db
);
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
use std::path::PathBuf;
fn setup_test_dir(name: &str) -> PathBuf {
let mut path = std::env::temp_dir();
path.push("gainlineup_tests");
path.push(name);
path.push(format!(
"{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&path).unwrap();
path
}
#[test]
fn test_run_function() {
let test_dir = setup_test_dir("test_run_function");
let toml_path = test_dir.join("test_cli_run.toml");
fs::copy("files/defaults_to_cw.toml", &toml_path).unwrap();
let args = vec![
String::from("program_name"),
toml_path.to_str().unwrap().to_string(),
];
let _cli_run = Command::run(&args).unwrap();
}
#[test]
fn test_config_build_not_enough_args() {
let args = vec![String::from("program_name")];
let result = Command::run(&args);
assert!(result.is_err());
}
#[test]
fn test_help_flag() {
let help_flags = vec!["--help", "-h"];
for flag in help_flags {
assert!(flag == "--help" || flag == "-h");
}
}
#[test]
fn test_version_flag() {
let version_flags = vec!["--version", "-v"];
for flag in version_flags {
assert!(flag == "--version" || flag == "-v");
}
}
#[test]
fn test_version_output_format() {
let version = env!("CARGO_PKG_VERSION");
assert!(!version.is_empty());
let parts: Vec<&str> = version.split('.').collect();
assert_eq!(parts.len(), 3, "Version should be in X.Y.Z format");
}
#[test]
fn test_touchstone_file_path_and_frequency_to_gain() {
let touchstone_file_path = "files/touchstone_options/ntwk3.s2p";
let frequency_in_hz = 6.0e9;
let TouchstoneValid {
contains_frequency,
gain,
} = touchstone_file_path_and_frequency_to_struct(
touchstone_file_path.to_string(),
frequency_in_hz,
);
let gain = gain.unwrap();
assert!(contains_frequency);
let gain_rounded_to_3_decimal_places = (gain * 1e3).round() / 1e3;
assert_eq!(gain_rounded_to_3_decimal_places, -3.932);
}
#[test]
fn test_touchstone_file_path_and_frequency_to_gain_not_found() {
let touchstone_file_path = "files/touchstone_options/ntwk3.s2p";
let frequency_in_hz = 11.0e9;
let TouchstoneValid {
contains_frequency,
gain,
} = touchstone_file_path_and_frequency_to_struct(
touchstone_file_path.to_string(),
frequency_in_hz,
);
assert!(!contains_frequency);
assert_eq!(gain, None);
}
#[test]
fn test_run_invalid_touchstone_frequency_error() {
let config_path = "files/touchstone_invalid_frequency/config.toml";
let args = vec![String::from("program_name"), config_path.to_string()];
let result = Command::run(&args);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Frequency 11000000000 Hz not found in touchstone file ntwk3.s2p"
);
}
#[test]
fn test_optional_units_parsing() {
let toml_content = r#"
pin = -30.0
f = 10.0e9
bw = 1.0e6
noise_temperature = 290.0
[[blocks]]
type = "explicit"
name = "LNA"
gain = 20.0
nf = 2.0
op1db = 10.0
"#;
#[derive(Deserialize, Debug)]
struct IntermediateConfig {
#[serde(alias = "input_power", alias = "pin")]
input_power_dbm: f64,
#[serde(alias = "frequency", alias = "f")]
frequency_hz: f64,
#[serde(alias = "bandwidth", alias = "bw")]
bandwidth_hz: Option<f64>,
#[serde(alias = "noise_temperature")]
noise_temperature_k: Option<f64>,
blocks: Vec<BlockConfig>,
}
let config: IntermediateConfig = toml::from_str(toml_content).unwrap();
assert_eq!(config.input_power_dbm, -30.0);
assert_eq!(config.frequency_hz, 10.0e9);
assert_eq!(config.bandwidth_hz, Some(1.0e6));
assert_eq!(config.noise_temperature_k, Some(290.0));
assert_eq!(config.blocks.len(), 1);
if let BlockConfig::Explicit {
name,
gain_db,
noise_figure_db,
output_p1db_dbm,
..
} = &config.blocks[0]
{
assert_eq!(name, "LNA");
assert_eq!(*gain_db, 20.0);
assert_eq!(*noise_figure_db, 2.0);
assert_eq!(*output_p1db_dbm, Some(10.0));
} else {
panic!("Expected Explicit block");
}
}
}