use crate::data_line;
use crate::data_pairs::{RealImaginary, RealImaginaryMatrix};
use crate::{Network, NetworkPoint, ReferenceImpedance, SMatrix, TouchstoneError};
#[derive(Debug, Clone)]
pub struct NetworkBuilder {
name: String,
rank: usize,
frequency_unit: String,
z0: f64,
comments: Vec<String>,
comments_after_option_line: Vec<String>,
points: Vec<NetworkPoint>,
}
impl NetworkBuilder {
#[must_use]
pub fn new<S: Into<String>>(name: S, rank: usize) -> Self {
Self {
name: name.into(),
rank,
frequency_unit: "Hz".to_string(),
z0: 50.0,
comments: Vec::new(),
comments_after_option_line: Vec::new(),
points: Vec::new(),
}
}
#[must_use]
pub fn frequency_unit<S: Into<String>>(mut self, frequency_unit: S) -> Self {
self.frequency_unit = frequency_unit.into();
self
}
#[must_use]
pub fn z0(mut self, z0: f64) -> Self {
self.z0 = z0;
self
}
#[must_use]
pub fn comment<S: Into<String>>(mut self, comment: S) -> Self {
self.comments.push(normalize_comment(comment.into()));
self
}
#[must_use]
pub fn network_data_comment<S: Into<String>>(mut self, comment: S) -> Self {
self.comments_after_option_line
.push(normalize_comment(comment.into()));
self
}
#[must_use]
pub fn point(mut self, frequency: f64, s: SMatrix) -> Self {
self.points.push(NetworkPoint { frequency, s });
self
}
pub fn push_point(&mut self, frequency: f64, s: SMatrix) -> &mut Self {
self.points.push(NetworkPoint { frequency, s });
self
}
pub fn build(self) -> Result<Network, TouchstoneError> {
let rank = validate_rank(self.rank)?;
validate_extension_rank(&self.name, self.rank)?;
let frequency_unit = canonical_frequency_unit(&self.frequency_unit)
.ok_or_else(|| TouchstoneError::UnsupportedFrequencyUnit {
unit: self.frequency_unit.clone(),
})?
.to_string();
if !self.z0.is_finite() || self.z0 <= 0.0 {
return Err(TouchstoneError::InvalidReferenceImpedance { z0: self.z0 });
}
if self.points.is_empty() {
return Err(TouchstoneError::EmptyNetworkData);
}
for (point_index, point) in self.points.iter().enumerate() {
validate_frequency(point_index, point.frequency)?;
validate_matrix(point_index, self.rank, &point.s)?;
}
let f = self
.points
.iter()
.map(|point| point.frequency)
.collect::<Vec<_>>();
let s = self
.points
.iter()
.map(|point| parsed_data_line_from_matrix(point.frequency, &point.s))
.collect::<Vec<_>>();
Ok(Network {
name: self.name,
rank,
frequency_unit,
parameter: "S".to_string(),
format: "RI".to_string(),
resistance_string: "R".to_string(),
z0: self.z0,
reference_impedance: ReferenceImpedance::Common(self.z0),
comments: self.comments,
comments_after_option_line: self.comments_after_option_line,
warnings: Vec::new(),
f,
s,
})
}
}
fn validate_rank(rank: usize) -> Result<i32, TouchstoneError> {
if rank == 0 || rank > i32::MAX as usize {
return Err(TouchstoneError::InvalidNetworkRank { rank });
}
Ok(rank as i32)
}
fn validate_extension_rank(name: &str, rank: usize) -> Result<(), TouchstoneError> {
if let Some(extension_rank) = infer_touchstone_extension_rank(name) {
if extension_rank != rank {
return Err(TouchstoneError::NetworkRankExtensionMismatch {
rank,
extension_rank,
});
}
}
Ok(())
}
fn validate_frequency(point_index: usize, frequency: f64) -> Result<(), TouchstoneError> {
if frequency.is_finite() {
Ok(())
} else {
Err(TouchstoneError::InvalidFrequency {
point_index,
frequency,
})
}
}
fn validate_matrix(
point_index: usize,
expected_rank: usize,
matrix: &SMatrix,
) -> Result<(), TouchstoneError> {
if matrix.rank != expected_rank {
return Err(TouchstoneError::InvalidMatrixRank {
point_index,
matrix_rank: matrix.rank,
expected_rank,
});
}
if matrix.data.len() != expected_rank {
return Err(TouchstoneError::InvalidMatrixShape {
point_index,
rows: matrix.data.len(),
row_index: None,
columns: 0,
expected_rank,
});
}
for (row_index, row) in matrix.data.iter().enumerate() {
if row.len() != expected_rank {
return Err(TouchstoneError::InvalidMatrixShape {
point_index,
rows: matrix.data.len(),
row_index: Some(row_index),
columns: row.len(),
expected_rank,
});
}
for (column_index, value) in row.iter().enumerate() {
if !value.re.is_finite() || !value.im.is_finite() {
return Err(TouchstoneError::InvalidSParameterValue {
point_index,
to_port: row_index + 1,
from_port: column_index + 1,
re: value.re,
im: value.im,
});
}
}
}
Ok(())
}
fn parsed_data_line_from_matrix(frequency: f64, matrix: &SMatrix) -> data_line::ParsedDataLine {
let s_ri_data = matrix
.data
.iter()
.map(|row| {
row.iter()
.map(|value| RealImaginary(value.re, value.im))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let s_ri = RealImaginaryMatrix::from_vec(s_ri_data);
data_line::parsed_data_line_from_ri_matrix(frequency, s_ri)
}
fn infer_touchstone_extension_rank(name: &str) -> Option<usize> {
let extension = name.rsplit_once('.')?.1;
if !extension.starts_with('s') || !extension.ends_with('p') {
return None;
}
let digits = &extension[1..extension.len() - 1];
if digits.is_empty() || digits.starts_with('0') || !digits.chars().all(|c| c.is_ascii_digit()) {
return None;
}
digits.parse::<usize>().ok()
}
fn canonical_frequency_unit(unit: &str) -> Option<&'static str> {
match unit.trim().to_ascii_lowercase().as_str() {
"hz" => Some("Hz"),
"khz" => Some("kHz"),
"mhz" => Some("MHz"),
"ghz" => Some("GHz"),
"thz" => Some("THz"),
_ => None,
}
}
fn normalize_comment(comment: String) -> String {
if comment.trim_start().starts_with('!') {
comment
} else {
format!("! {comment}")
}
}