#![warn(missing_docs)]
use std::{io::Write, ops};
pub mod cli;
mod data_line;
mod data_pairs;
mod error;
mod file_extension;
mod file_operations;
mod network_builder;
mod open;
mod option_line;
mod parser;
mod plot;
mod utils;
pub use error::{TouchstoneError, TouchstoneErrorContext, TouchstoneWarning};
pub use network_builder::NetworkBuilder;
const PARAMETER_CONVERSION_TOLERANCE: f64 = 1.0e-12;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ReferenceImpedance {
Common(f64),
PerPort(Vec<f64>),
}
impl ReferenceImpedance {
pub(crate) fn scalar_compatibility_value(&self) -> f64 {
match self {
Self::Common(z0) => *z0,
Self::PerPort(values) => values[0],
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum Interpolation {
Nearest,
#[default]
Linear,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum Extrapolation {
#[default]
Error,
Clamp,
}
#[doc(alias = "S-parameters")]
#[doc(alias = "S2P")]
#[doc(alias = "SNP")]
#[doc(alias = "Touchstone")]
#[doc(alias = "scattering parameters")]
#[derive(Debug, Clone)]
pub struct Network {
pub name: String,
pub rank: i32,
pub frequency_unit: String,
pub parameter: String,
pub format: String,
pub resistance_string: String,
pub z0: f64,
pub reference_impedance: ReferenceImpedance,
pub comments: Vec<String>,
pub comments_after_option_line: Vec<String>,
pub warnings: Vec<TouchstoneWarning>,
pub f: Vec<f64>,
pub s: Vec<data_line::ParsedDataLine>,
}
#[derive(Debug, Clone)]
pub struct FrequencyRI {
pub frequency: f64,
pub s_ri: data_pairs::RealImaginary,
}
#[derive(Debug, Clone)]
pub struct FrequencyDB {
pub frequency: f64,
pub s_db: data_pairs::DecibelAngle,
}
#[derive(Debug, Clone)]
pub struct FrequencyMA {
pub frequency: f64,
pub s_ma: data_pairs::MagnitudeAngle,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Complex {
pub re: f64,
pub im: f64,
}
impl Complex {
fn zero() -> Self {
Self { re: 0.0, im: 0.0 }
}
fn one() -> Self {
Self { re: 1.0, im: 0.0 }
}
fn magnitude(self) -> f64 {
self.re.hypot(self.im)
}
fn is_finite(self) -> bool {
self.re.is_finite() && self.im.is_finite()
}
}
impl ops::Add for Complex {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
re: self.re + rhs.re,
im: self.im + rhs.im,
}
}
}
impl ops::Sub for Complex {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self {
re: self.re - rhs.re,
im: self.im - rhs.im,
}
}
}
impl ops::Neg for Complex {
type Output = Self;
fn neg(self) -> Self::Output {
Self {
re: -self.re,
im: -self.im,
}
}
}
impl ops::Mul for Complex {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
Self {
re: self.re * rhs.re - self.im * rhs.im,
im: self.re * rhs.im + self.im * rhs.re,
}
}
}
impl ops::Mul<f64> for Complex {
type Output = Self;
fn mul(self, rhs: f64) -> Self::Output {
Self {
re: self.re * rhs,
im: self.im * rhs,
}
}
}
impl ops::Div for Complex {
type Output = Self;
fn div(self, rhs: Self) -> Self::Output {
let denominator = rhs.re * rhs.re + rhs.im * rhs.im;
Self {
re: (self.re * rhs.re + self.im * rhs.im) / denominator,
im: (self.im * rhs.re - self.re * rhs.im) / denominator,
}
}
}
impl ops::Div<f64> for Complex {
type Output = Self;
fn div(self, rhs: f64) -> Self::Output {
Self {
re: self.re / rhs,
im: self.im / rhs,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SMatrix {
pub rank: usize,
pub data: Vec<Vec<Complex>>,
}
impl SMatrix {
pub fn get(&self, to_port: usize, from_port: usize) -> Result<Complex, TouchstoneError> {
validate_port_indexes(to_port, from_port, self.rank)?;
self.data
.get(to_port - 1)
.and_then(|row| row.get(from_port - 1))
.copied()
.ok_or(TouchstoneError::InvalidPortIndex {
to_port,
from_port,
rank: self.rank,
})
}
pub fn to_y_matrix(&self, z0: f64) -> Result<ParameterMatrix, TouchstoneError> {
validate_reference_impedance(z0)?;
validate_matrix_data("S", self.rank, &self.data)?;
let identity = identity_matrix(self.rank);
let numerator = matrix_sub(&identity, &self.data);
let denominator = matrix_add(&identity, &self.data);
let denominator_inverse = invert_matrix(
denominator,
"S to Y matrix inversion",
PARAMETER_CONVERSION_TOLERANCE,
)?;
let data = matrix_scale(&matrix_mul(&numerator, &denominator_inverse), 1.0 / z0);
Ok(ParameterMatrix {
rank: self.rank,
data,
})
}
pub fn to_z_matrix(&self, z0: f64) -> Result<ParameterMatrix, TouchstoneError> {
validate_reference_impedance(z0)?;
validate_matrix_data("S", self.rank, &self.data)?;
let identity = identity_matrix(self.rank);
let numerator = matrix_add(&identity, &self.data);
let denominator = matrix_sub(&identity, &self.data);
let denominator_inverse = invert_matrix(
denominator,
"S to Z matrix inversion",
PARAMETER_CONVERSION_TOLERANCE,
)?;
let data = matrix_scale(&matrix_mul(&numerator, &denominator_inverse), z0);
Ok(ParameterMatrix {
rank: self.rank,
data,
})
}
pub fn to_abcd(&self, z0: f64) -> Result<ABCDMatrix, TouchstoneError> {
validate_reference_impedance(z0)?;
validate_matrix_data("S", self.rank, &self.data)?;
if self.rank != 2 {
return Err(TouchstoneError::UnsupportedConversionRank {
conversion: "S to ABCD".to_string(),
rank: self.rank,
expected_rank: 2,
});
}
let s11 = self.data[0][0];
let s12 = self.data[0][1];
let s21 = self.data[1][0];
let s22 = self.data[1][1];
let one = Complex::one();
let two_s21 = s21 * 2.0;
ensure_non_singular_value(
"S to ABCD denominator",
0,
two_s21,
PARAMETER_CONVERSION_TOLERANCE,
)?;
Ok(ABCDMatrix {
a: ((one + s11) * (one - s22) + s12 * s21) / two_s21,
b: (((one + s11) * (one + s22) - s12 * s21) * z0) / two_s21,
c: (((one - s11) * (one - s22) - s12 * s21) / z0) / two_s21,
d: ((one - s11) * (one + s22) + s12 * s21) / two_s21,
})
}
pub fn try_from_y_matrix(matrix: &ParameterMatrix, z0: f64) -> Result<Self, TouchstoneError> {
matrix.to_s_matrix_from_y(z0)
}
pub fn try_from_z_matrix(matrix: &ParameterMatrix, z0: f64) -> Result<Self, TouchstoneError> {
matrix.to_s_matrix_from_z(z0)
}
pub fn try_from_abcd(matrix: &ABCDMatrix, z0: f64) -> Result<Self, TouchstoneError> {
matrix.to_s_matrix(z0)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ParameterMatrix {
pub rank: usize,
pub data: Vec<Vec<Complex>>,
}
impl ParameterMatrix {
pub fn get(&self, to_port: usize, from_port: usize) -> Result<Complex, TouchstoneError> {
validate_port_indexes(to_port, from_port, self.rank)?;
self.data
.get(to_port - 1)
.and_then(|row| row.get(from_port - 1))
.copied()
.ok_or(TouchstoneError::InvalidPortIndex {
to_port,
from_port,
rank: self.rank,
})
}
pub fn to_s_matrix_from_y(&self, z0: f64) -> Result<SMatrix, TouchstoneError> {
validate_reference_impedance(z0)?;
validate_matrix_data("Y", self.rank, &self.data)?;
let identity = identity_matrix(self.rank);
let scaled_y = matrix_scale(&self.data, z0);
let numerator = matrix_sub(&identity, &scaled_y);
let denominator = matrix_add(&identity, &scaled_y);
let denominator_inverse = invert_matrix(
denominator,
"Y to S matrix inversion",
PARAMETER_CONVERSION_TOLERANCE,
)?;
let data = matrix_mul(&numerator, &denominator_inverse);
Ok(SMatrix {
rank: self.rank,
data,
})
}
pub fn to_s_matrix_from_z(&self, z0: f64) -> Result<SMatrix, TouchstoneError> {
validate_reference_impedance(z0)?;
validate_matrix_data("Z", self.rank, &self.data)?;
let z0_identity = matrix_scale(&identity_matrix(self.rank), z0);
let numerator = matrix_sub(&self.data, &z0_identity);
let denominator = matrix_add(&self.data, &z0_identity);
let denominator_inverse = invert_matrix(
denominator,
"Z to S matrix inversion",
PARAMETER_CONVERSION_TOLERANCE,
)?;
let data = matrix_mul(&numerator, &denominator_inverse);
Ok(SMatrix {
rank: self.rank,
data,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ABCDMatrix {
pub a: Complex,
pub b: Complex,
pub c: Complex,
pub d: Complex,
}
impl ABCDMatrix {
pub fn get(&self, row: usize, column: usize) -> Result<Complex, TouchstoneError> {
validate_port_indexes(row, column, 2)?;
Ok(match (row, column) {
(1, 1) => self.a,
(1, 2) => self.b,
(2, 1) => self.c,
(2, 2) => self.d,
_ => unreachable!("ABCD indexes were already validated"),
})
}
pub fn to_s_matrix(&self, z0: f64) -> Result<SMatrix, TouchstoneError> {
validate_reference_impedance(z0)?;
for (index, value) in [self.a, self.b, self.c, self.d].into_iter().enumerate() {
if !value.is_finite() {
return Err(TouchstoneError::InvalidParameterMatrixValue {
matrix: "ABCD".to_string(),
row: index / 2 + 1,
column: index % 2 + 1,
re: value.re,
im: value.im,
});
}
}
let denominator = self.a + self.b / z0 + self.c * z0 + self.d;
ensure_non_singular_value(
"ABCD to S denominator",
0,
denominator,
PARAMETER_CONVERSION_TOLERANCE,
)?;
Ok(SMatrix {
rank: 2,
data: vec![
vec![
(self.a + self.b / z0 - self.c * z0 - self.d) / denominator,
((self.a * self.d - self.b * self.c) * 2.0) / denominator,
],
vec![
Complex { re: 2.0, im: 0.0 } / denominator,
(-self.a + self.b / z0 - self.c * z0 + self.d) / denominator,
],
],
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct NetworkPoint {
pub frequency: f64,
pub s: SMatrix,
}
impl Network {
pub fn new<P: AsRef<std::path::Path>>(file_path: P) -> Result<Self, TouchstoneError> {
parser::try_read_file(file_path)
}
pub fn from_bytes<S: AsRef<str>>(
source_name: S,
bytes: &[u8],
) -> Result<Self, TouchstoneError> {
parser::parse_bytes(source_name.as_ref(), bytes)
}
#[allow(clippy::should_implement_trait)]
pub fn from_str<S: AsRef<str>>(
source_name: S,
contents: &str,
) -> Result<Self, TouchstoneError> {
parser::parse_str(source_name.as_ref(), contents)
}
pub fn print_summary(&self) {
println!("Network Summary:");
println!("Name: {}", self.name);
println!("Rank (number of ports): {}", self.rank);
println!("Frequency Unit: {}", self.frequency_unit);
println!("Parameter: {}", self.parameter);
println!("Format: {}", self.format);
println!("Reference Impedance (Z0): {}", self.z0);
println!("Number of Data Lines: {}", self.f.len());
println!("Comments:");
for comment in &self.comments {
println!("{}", comment);
}
}
#[must_use]
pub fn reference_impedance(&self) -> ReferenceImpedance {
match &self.reference_impedance {
ReferenceImpedance::Common(_) => ReferenceImpedance::Common(self.z0),
ReferenceImpedance::PerPort(values) => ReferenceImpedance::PerPort(values.clone()),
}
}
#[must_use]
pub fn f(&self) -> Vec<f64> {
self.f.clone()
}
#[must_use]
#[doc(alias = "S-parameters")]
#[doc(alias = "insertion loss")]
#[doc(alias = "return loss")]
pub fn s_db(&self, j: i8, k: i8) -> Vec<FrequencyDB> {
let mut s_db_vector: Vec<FrequencyDB> = Vec::new();
for i in 0..self.s.len() {
let frequency = self.s[i].frequency;
let s_db_matrix = &self.s[i].s_db;
let s_db_value = s_db_matrix.get(j as usize, k as usize);
let frequency_db = FrequencyDB {
frequency,
s_db: s_db_value,
};
s_db_vector.push(frequency_db);
}
s_db_vector
}
#[must_use]
pub fn s_ri(&self, j: i8, k: i8) -> Vec<FrequencyRI> {
let mut s_ri_vector: Vec<FrequencyRI> = Vec::new();
for i in 0..self.s.len() {
let frequency = self.s[i].frequency;
let s_ri_matrix = &self.s[i].s_ri;
let s_ri_value = s_ri_matrix.get(j as usize, k as usize);
let frequency_ri = FrequencyRI {
frequency,
s_ri: s_ri_value,
};
s_ri_vector.push(frequency_ri);
}
s_ri_vector
}
#[must_use]
pub fn s_ma(&self, j: i8, k: i8) -> Vec<FrequencyMA> {
let mut s_ma_vector: Vec<FrequencyMA> = Vec::new();
for i in 0..self.s.len() {
let frequency = self.s[i].frequency;
let s_ma_matrix = &self.s[i].s_ma;
let s_ma_value = s_ma_matrix.get(j as usize, k as usize);
let frequency_ma = FrequencyMA {
frequency,
s_ma: s_ma_value,
};
s_ma_vector.push(frequency_ma);
}
s_ma_vector
}
pub fn try_s_ri_at(
&self,
point_index: usize,
to_port: usize,
from_port: usize,
) -> Result<Complex, TouchstoneError> {
let data_line = self.data_line_at(point_index)?;
let rank = data_line.s_ri.size();
validate_port_indexes(to_port, from_port, rank)?;
Ok(complex_from_real_imaginary(
data_line.s_ri.get(to_port, from_port),
))
}
pub fn s_matrix_at(&self, point_index: usize) -> Result<SMatrix, TouchstoneError> {
let data_line = self.data_line_at(point_index)?;
let rank = data_line.s_ri.size();
let mut data = Vec::with_capacity(rank);
for to_port in 1..=rank {
let mut row = Vec::with_capacity(rank);
for from_port in 1..=rank {
row.push(complex_from_real_imaginary(
data_line.s_ri.get(to_port, from_port),
));
}
data.push(row);
}
Ok(SMatrix { rank, data })
}
pub fn y_matrix_at(&self, point_index: usize) -> Result<ParameterMatrix, TouchstoneError> {
let z0 = self.scalar_reference_impedance_for_conversions()?;
self.s_matrix_at(point_index)?.to_y_matrix(z0)
}
pub fn z_matrix_at(&self, point_index: usize) -> Result<ParameterMatrix, TouchstoneError> {
let z0 = self.scalar_reference_impedance_for_conversions()?;
self.s_matrix_at(point_index)?.to_z_matrix(z0)
}
pub fn abcd_at(&self, point_index: usize) -> Result<ABCDMatrix, TouchstoneError> {
let z0 = self.scalar_reference_impedance_for_conversions()?;
self.s_matrix_at(point_index)?.to_abcd(z0)
}
pub fn point_at(&self, point_index: usize) -> Result<NetworkPoint, TouchstoneError> {
let data_line = self.data_line_at(point_index)?;
Ok(NetworkPoint {
frequency: data_line.frequency,
s: self.s_matrix_at(point_index)?,
})
}
pub fn points(&self) -> Result<Vec<NetworkPoint>, TouchstoneError> {
(0..self.s.len())
.map(|point_index| self.point_at(point_index))
.collect()
}
fn scalar_reference_impedance_for_conversions(&self) -> Result<f64, TouchstoneError> {
if self.parameter != "S" {
return Err(TouchstoneError::UnsupportedNetworkParameter {
parameter: self.parameter.clone(),
});
}
match self.reference_impedance() {
ReferenceImpedance::Common(z0) => {
validate_reference_impedance(z0)?;
Ok(z0)
}
ReferenceImpedance::PerPort(values) => {
Err(TouchstoneError::UnsupportedReferenceImpedance { values })
}
}
}
#[doc(alias = "interpolate")]
#[doc(alias = "S-parameters")]
pub fn sample_at(
&self,
frequency_hz: f64,
interpolation: Interpolation,
extrapolation: Extrapolation,
) -> Result<NetworkPoint, TouchstoneError> {
validate_sample_frequency(0, frequency_hz)?;
self.validate_frequency_data()?;
let data_line =
self.sample_data_line_at_validated(frequency_hz, interpolation, extrapolation)?;
Ok(network_point_from_data_line(&data_line))
}
#[doc(alias = "interpolate")]
#[doc(alias = "resampling")]
pub fn resample<I>(
&self,
frequencies_hz: I,
interpolation: Interpolation,
extrapolation: Extrapolation,
) -> Result<Network, TouchstoneError>
where
I: IntoIterator<Item = f64>,
{
let frequencies = frequencies_hz.into_iter().collect::<Vec<_>>();
validate_frequency_slice(&frequencies)?;
self.validate_frequency_data()?;
let s = frequencies
.iter()
.map(|frequency| {
self.sample_data_line_at_validated(*frequency, interpolation, extrapolation)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Network {
name: self.name.clone(),
rank: self.rank,
frequency_unit: self.frequency_unit.clone(),
parameter: self.parameter.clone(),
format: self.format.clone(),
resistance_string: self.resistance_string.clone(),
z0: self.z0,
reference_impedance: self.reference_impedance(),
comments: self.comments.clone(),
comments_after_option_line: self.comments_after_option_line.clone(),
warnings: self.warnings.clone(),
f: frequencies,
s,
})
}
fn data_line_at(
&self,
point_index: usize,
) -> Result<&data_line::ParsedDataLine, TouchstoneError> {
self.s
.get(point_index)
.ok_or(TouchstoneError::InvalidPointIndex {
point_index,
point_count: self.s.len(),
})
}
fn validate_frequency_data(&self) -> Result<(), TouchstoneError> {
if self.f.len() != self.s.len() {
return Err(TouchstoneError::FrequencyDataLengthMismatch {
frequency_count: self.f.len(),
data_count: self.s.len(),
});
}
validate_frequency_slice(&self.f)
}
fn sample_data_line_at_validated(
&self,
frequency_hz: f64,
interpolation: Interpolation,
extrapolation: Extrapolation,
) -> Result<data_line::ParsedDataLine, TouchstoneError> {
let min = self.f[0];
let max = self.f[self.f.len() - 1];
let lookup_frequency = if frequency_hz < min {
match extrapolation {
Extrapolation::Error => {
return Err(TouchstoneError::FrequencyOutOfRange {
frequency: frequency_hz,
min,
max,
});
}
Extrapolation::Clamp => min,
}
} else if frequency_hz > max {
match extrapolation {
Extrapolation::Error => {
return Err(TouchstoneError::FrequencyOutOfRange {
frequency: frequency_hz,
min,
max,
});
}
Extrapolation::Clamp => max,
}
} else {
frequency_hz
};
if let Ok(index) = self
.f
.binary_search_by(|frequency| frequency.partial_cmp(&lookup_frequency).unwrap())
{
return Ok(parsed_data_line_with_frequency(
&self.s[index],
frequency_hz,
));
}
let upper_index = self
.f
.partition_point(|frequency| *frequency < lookup_frequency);
let lower_index = upper_index - 1;
let selected_index = match interpolation {
Interpolation::Nearest => {
let lower_frequency = self.f[lower_index];
let upper_frequency = self.f[upper_index];
if lookup_frequency - lower_frequency <= upper_frequency - lookup_frequency {
lower_index
} else {
upper_index
}
}
Interpolation::Linear => {
return Ok(interpolate_data_lines(
frequency_hz,
lookup_frequency,
self.f[lower_index],
self.f[upper_index],
&self.s[lower_index],
&self.s[upper_index],
));
}
};
Ok(parsed_data_line_with_frequency(
&self.s[selected_index],
frequency_hz,
))
}
#[must_use]
#[doc(alias = "network cascading")]
#[doc(alias = "ABCD parameters")]
#[doc(alias = "chain")]
pub fn cascade(&self, other: &Network) -> Network {
if self.rank != 2 || other.rank != 2 {
panic!("Cascading is only implemented for 2-port networks. Use cascade_ports() for explicit port specification.");
}
let self_z0 = common_reference_impedance_or_panic(self);
let other_z0 = common_reference_impedance_or_panic(other);
if self_z0 != other_z0 {
panic!(
"Cannot cascade networks with different reference impedances: {} and {}",
self_z0, other_z0
);
}
if self.frequency_unit != other.frequency_unit {
panic!(
"Cannot cascade networks with different frequency units: {} and {}",
self.frequency_unit, other.frequency_unit
);
}
let mut comments = Vec::<String>::new();
comments.push(format!(
"! Cascaded network of {} and {}",
self.name, other.name
));
let comment_header_self = format!("! Comments from first network ({:?}):", self.name);
comments.push(comment_header_self);
for comment in &self.comments {
comments.push(comment.clone());
}
let comment_header_other = format!("! Comments from second network ({:?}):", other.name);
comments.push(comment_header_other);
for comment in &other.comments {
comments.push(comment.clone());
}
let mut comments_after_option_line = Vec::<String>::new();
comments_after_option_line.push(format!(
"! Cascaded network of {} and {}",
self.name, other.name
));
let comments_after_option_line_header_self = format!(
"! Comments (after option line) from first network ({:?}):",
self.name
);
comments_after_option_line.push(comments_after_option_line_header_self);
for comment_after_option_line in &self.comments_after_option_line {
comments_after_option_line.push(comment_after_option_line.clone());
}
let comments_after_option_line_header_other = format!(
"! Comments (after option line) from second network ({:?}):",
other.name
);
comments_after_option_line.push(comments_after_option_line_header_other);
for comment_after_option_line in &other.comments_after_option_line {
comments_after_option_line.push(comment_after_option_line.clone());
}
let new_name = format!("Cascaded({},{})", self.name, other.name);
let mut s_new = Vec::new();
let len = std::cmp::min(self.s.len(), other.s.len());
for i in 0..len {
let freq = self.s[i].frequency;
let s1 = &self.s[i].s_ri;
let s2 = &other.s[i].s_ri;
let abcd1 = s1.to_abcd(self_z0);
let abcd2 = s2.to_abcd(other_z0);
let abcd_new = abcd1 * abcd2;
let s_new_ri = abcd_new.to_s(self_z0);
let s_new_ma = crate::data_pairs::MagnitudeAngleMatrix::from_vec(vec![
vec![
s_new_ri.get(1, 1).magnitude_angle(),
s_new_ri.get(1, 2).magnitude_angle(),
],
vec![
s_new_ri.get(2, 1).magnitude_angle(),
s_new_ri.get(2, 2).magnitude_angle(),
],
]);
let s_new_db =
crate::data_pairs::DecibelAngleMatrix::from_magnitude_angle_matrix(&s_new_ma);
s_new.push(crate::data_line::ParsedDataLine {
frequency: freq,
s_ri: s_new_ri,
s_ma: s_new_ma,
s_db: s_new_db,
});
}
Network {
name: new_name,
rank: self.rank,
frequency_unit: self.frequency_unit.clone(),
parameter: self.parameter.clone(),
format: self.format.clone(),
resistance_string: self.resistance_string.clone(),
z0: self_z0,
reference_impedance: ReferenceImpedance::Common(self_z0),
comments,
comments_after_option_line,
warnings: [self.warnings.clone(), other.warnings.clone()].concat(),
f: self.f.clone(), s: s_new,
}
}
#[must_use]
pub fn cascade_ports(&self, other: &Network, from_port: usize, to_port: usize) -> Network {
assert!(
from_port >= 1 && from_port <= self.rank as usize,
"from_port {} out of range for {}-port network (valid range: 1-{})",
from_port,
self.rank,
self.rank
);
assert!(
to_port >= 1 && to_port <= other.rank as usize,
"to_port {} out of range for {}-port network (valid range: 1-{})",
to_port,
other.rank,
other.rank
);
if self.rank == 2 && other.rank == 2 {
if from_port != 2 || to_port != 1 {
panic!(
"For 2-port networks, only standard cascade (port 2 → port 1) is currently supported.\n\
Requested connection: port {} → port {}\n\
Use cascade() method for standard 2-port cascade, or wait for future N-port cascade implementation.",
from_port, to_port
);
}
return self.cascade(other);
}
panic!(
"Cascading {}-port and {}-port networks is not yet supported.\n\
Currently only 2-port networks can be cascaded (with standard port 2 → port 1 connection).\n\
\n\
Future enhancement: Full N-port cascade with arbitrary port connections.\n\
\n\
Workaround: Extract 2-port sub-networks from your {}-port and {}-port networks,\n\
then cascade those 2-port networks.",
self.rank,
other.rank,
self.rank,
other.rank
);
}
pub fn to_touchstone_string(&self) -> std::io::Result<String> {
let mut bytes = Vec::new();
self.write_touchstone(&mut bytes)?;
String::from_utf8(bytes)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
}
pub fn write_touchstone<W: Write>(&self, mut writer: W) -> std::io::Result<()> {
for comment in &self.comments {
writeln!(writer, "{}", comment)?;
}
let n = self.rank as usize;
writeln!(writer, "[Version] 2.1")?;
let option_line = option_line::Options::new(
self.frequency_unit.clone(),
self.parameter.clone(),
self.format.clone(),
self.resistance_string.clone(),
self.z0.to_string().clone(),
);
writeln!(writer, "{}", option_line)?;
writeln!(writer, "[Number of Ports] {}", self.rank)?;
if n == 2 {
writeln!(writer, "[Two-Port Data Order] 21_12")?;
}
if let ReferenceImpedance::PerPort(values) = self.reference_impedance() {
writeln!(writer, "[Reference] {}", format_real_values(&values))?;
}
writeln!(writer, "[Number of Frequencies] {}", self.f.len())?;
writeln!(writer, "[Matrix Format] Full")?;
writeln!(writer, "[Network Data]")?;
for comment in &self.comments_after_option_line {
writeln!(writer, "{}", comment)?;
}
let single_line_order = full_matrix_data_order(n);
for data_line in &self.s {
let mut freq = data_line.frequency;
let frequency_unit = self.frequency_unit.clone();
if frequency_unit == "THz" {
freq = rfconversions::frequency::hz_to_thz(freq);
} else if frequency_unit == "GHz" {
freq = rfconversions::frequency::hz_to_ghz(freq);
} else if frequency_unit == "MHz" {
freq = rfconversions::frequency::hz_to_mhz(freq);
} else if frequency_unit == "kHz" {
freq = rfconversions::frequency::hz_to_khz(freq);
}
if n <= 2 {
let mut line = format!("{}", freq);
match self.format.as_str() {
"RI" => {
let s = &data_line.s_ri;
for (row, col) in &single_line_order {
line.push_str(&format!(
" {} {}",
s.get(*row, *col).0,
s.get(*row, *col).1
));
}
}
"MA" => {
let s = &data_line.s_ma;
for (row, col) in &single_line_order {
line.push_str(&format!(
" {} {}",
s.get(*row, *col).0,
s.get(*row, *col).1
));
}
}
"DB" => {
let s = &data_line.s_db;
for (row, col) in &single_line_order {
line.push_str(&format!(
" {} {}",
s.get(*row, *col).0,
s.get(*row, *col).1
));
}
}
_ => panic!("Unsupported format for saving: {}", self.format),
}
writeln!(writer, "{}", line)?;
} else {
let mut line = format!("{}", freq);
match self.format.as_str() {
"RI" => {
let s = &data_line.s_ri;
for col in 1..=n {
line.push_str(&format!(" {} {}", s.get(1, col).0, s.get(1, col).1));
}
writeln!(writer, "{}", line)?;
for row in 2..=n {
let mut row_line = String::new();
for col in 1..=n {
row_line.push_str(&format!(
" {} {}",
s.get(row, col).0,
s.get(row, col).1
));
}
writeln!(writer, "{}", row_line)?;
}
}
"MA" => {
let s = &data_line.s_ma;
for col in 1..=n {
line.push_str(&format!(" {} {}", s.get(1, col).0, s.get(1, col).1));
}
writeln!(writer, "{}", line)?;
for row in 2..=n {
let mut row_line = String::new();
for col in 1..=n {
row_line.push_str(&format!(
" {} {}",
s.get(row, col).0,
s.get(row, col).1
));
}
writeln!(writer, "{}", row_line)?;
}
}
"DB" => {
let s = &data_line.s_db;
for col in 1..=n {
line.push_str(&format!(" {} {}", s.get(1, col).0, s.get(1, col).1));
}
writeln!(writer, "{}", line)?;
for row in 2..=n {
let mut row_line = String::new();
for col in 1..=n {
row_line.push_str(&format!(
" {} {}",
s.get(row, col).0,
s.get(row, col).1
));
}
writeln!(writer, "{}", row_line)?;
}
}
_ => panic!("Unsupported format for saving: {}", self.format),
}
}
}
writeln!(writer, "[End]")?;
Ok(())
}
pub fn save(&self, file_path: &str) -> std::io::Result<()> {
let file = std::fs::File::create(file_path)?;
self.write_touchstone(file)
}
}
fn full_matrix_data_order(n: usize) -> Vec<(usize, usize)> {
if n == 2 {
vec![(1, 1), (2, 1), (1, 2), (2, 2)]
} else {
(1..=n)
.flat_map(|row| (1..=n).map(move |col| (row, col)))
.collect()
}
}
fn format_real_values(values: &[f64]) -> String {
values
.iter()
.map(f64::to_string)
.collect::<Vec<_>>()
.join(" ")
}
fn complex_from_real_imaginary(value: data_pairs::RealImaginary) -> Complex {
Complex {
re: value.0,
im: value.1,
}
}
fn validate_port_indexes(
to_port: usize,
from_port: usize,
rank: usize,
) -> Result<(), TouchstoneError> {
if to_port == 0 || from_port == 0 || to_port > rank || from_port > rank {
return Err(TouchstoneError::InvalidPortIndex {
to_port,
from_port,
rank,
});
}
Ok(())
}
fn validate_reference_impedance(z0: f64) -> Result<(), TouchstoneError> {
if z0.is_finite() && z0 > 0.0 {
Ok(())
} else {
Err(TouchstoneError::InvalidReferenceImpedance { z0 })
}
}
fn validate_matrix_data(
matrix: &str,
rank: usize,
data: &[Vec<Complex>],
) -> Result<(), TouchstoneError> {
if rank == 0 || data.len() != rank {
return Err(TouchstoneError::InvalidParameterMatrixShape {
matrix: matrix.to_string(),
rank,
rows: data.len(),
row_index: None,
columns: 0,
});
}
for (row_index, row) in data.iter().enumerate() {
if row.len() != rank {
return Err(TouchstoneError::InvalidParameterMatrixShape {
matrix: matrix.to_string(),
rank,
rows: data.len(),
row_index: Some(row_index),
columns: row.len(),
});
}
for (column_index, value) in row.iter().copied().enumerate() {
if !value.is_finite() {
return Err(TouchstoneError::InvalidParameterMatrixValue {
matrix: matrix.to_string(),
row: row_index + 1,
column: column_index + 1,
re: value.re,
im: value.im,
});
}
}
}
Ok(())
}
fn identity_matrix(rank: usize) -> Vec<Vec<Complex>> {
let mut data = vec![vec![Complex::zero(); rank]; rank];
for (index, row) in data.iter_mut().enumerate() {
row[index] = Complex::one();
}
data
}
fn matrix_add(left: &[Vec<Complex>], right: &[Vec<Complex>]) -> Vec<Vec<Complex>> {
left.iter()
.zip(right)
.map(|(left_row, right_row)| {
left_row
.iter()
.zip(right_row)
.map(|(left_value, right_value)| *left_value + *right_value)
.collect()
})
.collect()
}
fn matrix_sub(left: &[Vec<Complex>], right: &[Vec<Complex>]) -> Vec<Vec<Complex>> {
left.iter()
.zip(right)
.map(|(left_row, right_row)| {
left_row
.iter()
.zip(right_row)
.map(|(left_value, right_value)| *left_value - *right_value)
.collect()
})
.collect()
}
fn matrix_scale(matrix: &[Vec<Complex>], scalar: f64) -> Vec<Vec<Complex>> {
matrix
.iter()
.map(|row| row.iter().map(|value| *value * scalar).collect())
.collect()
}
fn matrix_mul(left: &[Vec<Complex>], right: &[Vec<Complex>]) -> Vec<Vec<Complex>> {
let rank = left.len();
let mut result = vec![vec![Complex::zero(); rank]; rank];
for row in 0..rank {
for column in 0..rank {
let mut sum = Complex::zero();
for inner in 0..rank {
sum = sum + left[row][inner] * right[inner][column];
}
result[row][column] = sum;
}
}
result
}
fn invert_matrix(
mut matrix: Vec<Vec<Complex>>,
operation: &str,
tolerance: f64,
) -> Result<Vec<Vec<Complex>>, TouchstoneError> {
let rank = matrix.len();
let mut inverse = identity_matrix(rank);
for pivot_index in 0..rank {
let mut pivot_row = pivot_index;
let mut pivot_magnitude = matrix[pivot_index][pivot_index].magnitude();
for (row_index, row) in matrix.iter().enumerate().skip(pivot_index + 1) {
let magnitude = row[pivot_index].magnitude();
if magnitude > pivot_magnitude {
pivot_row = row_index;
pivot_magnitude = magnitude;
}
}
if pivot_row != pivot_index {
matrix.swap(pivot_index, pivot_row);
inverse.swap(pivot_index, pivot_row);
}
let pivot = matrix[pivot_index][pivot_index];
ensure_non_singular_value(operation, pivot_index, pivot, tolerance)?;
for column in 0..rank {
matrix[pivot_index][column] = matrix[pivot_index][column] / pivot;
inverse[pivot_index][column] = inverse[pivot_index][column] / pivot;
}
let normalized_pivot_row = matrix[pivot_index].clone();
let normalized_inverse_row = inverse[pivot_index].clone();
for row in 0..rank {
if row == pivot_index {
continue;
}
let factor = matrix[row][pivot_index];
if factor.magnitude() == 0.0 {
continue;
}
for column in 0..rank {
matrix[row][column] = matrix[row][column] - factor * normalized_pivot_row[column];
inverse[row][column] =
inverse[row][column] - factor * normalized_inverse_row[column];
}
}
}
Ok(inverse)
}
fn ensure_non_singular_value(
operation: &str,
pivot_index: usize,
value: Complex,
tolerance: f64,
) -> Result<(), TouchstoneError> {
let magnitude = value.magnitude();
if magnitude.is_finite() && magnitude > tolerance {
Ok(())
} else {
Err(TouchstoneError::SingularMatrix {
operation: operation.to_string(),
pivot_index,
pivot_magnitude: magnitude,
tolerance,
})
}
}
fn common_reference_impedance_or_panic(network: &Network) -> f64 {
match network.reference_impedance() {
ReferenceImpedance::Common(z0) => z0,
ReferenceImpedance::PerPort(_) => {
panic!(
"Cannot cascade networks with per-port reference impedances; common scalar reference impedance is required"
);
}
}
}
impl ops::Mul<Network> for Network {
type Output = Network;
fn mul(self, _rhs: Network) -> Network {
tracing::debug!("Network cascade (mul) operation");
self.cascade(&_rhs)
}
}
fn validate_frequency_slice(frequencies: &[f64]) -> Result<(), TouchstoneError> {
if frequencies.is_empty() {
return Err(TouchstoneError::EmptyNetworkData);
}
for (point_index, frequency) in frequencies.iter().copied().enumerate() {
validate_sample_frequency(point_index, frequency)?;
if point_index == 0 {
continue;
}
let previous_index = point_index - 1;
let previous_frequency = frequencies[previous_index];
if frequency == previous_frequency {
return Err(TouchstoneError::DuplicateFrequency {
first_index: previous_index,
duplicate_index: point_index,
frequency,
});
}
if frequency < previous_frequency {
return Err(TouchstoneError::UnsortedFrequencies {
previous_index,
previous_frequency,
next_index: point_index,
next_frequency: frequency,
});
}
}
Ok(())
}
fn validate_sample_frequency(point_index: usize, frequency: f64) -> Result<(), TouchstoneError> {
if frequency.is_finite() {
Ok(())
} else {
Err(TouchstoneError::InvalidFrequency {
point_index,
frequency,
})
}
}
fn parsed_data_line_with_frequency(
data_line: &data_line::ParsedDataLine,
frequency: f64,
) -> data_line::ParsedDataLine {
data_line::parsed_data_line_from_ri_matrix(frequency, data_line.s_ri.clone())
}
fn interpolate_data_lines(
output_frequency: f64,
lookup_frequency: f64,
lower_frequency: f64,
upper_frequency: f64,
lower: &data_line::ParsedDataLine,
upper: &data_line::ParsedDataLine,
) -> data_line::ParsedDataLine {
let n = lower.s_ri.size();
let t = (lookup_frequency - lower_frequency) / (upper_frequency - lower_frequency);
let mut data = Vec::with_capacity(n);
for row in 1..=n {
let mut row_data = Vec::with_capacity(n);
for col in 1..=n {
let lower_value = lower.s_ri.get(row, col);
let upper_value = upper.s_ri.get(row, col);
row_data.push(data_pairs::RealImaginary(
lower_value.0 + (upper_value.0 - lower_value.0) * t,
lower_value.1 + (upper_value.1 - lower_value.1) * t,
));
}
data.push(row_data);
}
data_line::parsed_data_line_from_ri_matrix(
output_frequency,
data_pairs::RealImaginaryMatrix::from_vec(data),
)
}
fn network_point_from_data_line(data_line: &data_line::ParsedDataLine) -> NetworkPoint {
let rank = data_line.s_ri.size();
let mut data = Vec::with_capacity(rank);
for to_port in 1..=rank {
let mut row = Vec::with_capacity(rank);
for from_port in 1..=rank {
row.push(complex_from_real_imaginary(
data_line.s_ri.get(to_port, from_port),
));
}
data.push(row);
}
NetworkPoint {
frequency: data_line.frequency,
s: SMatrix { rank, data },
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_approx_eq(actual: f64, expected: f64) {
let tolerance = 1e-10;
assert!(
(actual - expected).abs() <= tolerance,
"expected {actual} to be within {tolerance} of {expected}"
);
}
fn interpolation_1port_network() -> Network {
Network::from_str("linear.s1p", "# GHz S RI R 50\n1.0 0.0 0.0\n2.0 2.0 4.0\n").unwrap()
}
fn assert_s11(point: &NetworkPoint, expected_re: f64, expected_im: f64) {
let s11 = point.s.get(1, 1).unwrap();
assert_approx_eq(s11.re, expected_re);
assert_approx_eq(s11.im, expected_im);
}
#[test]
fn f() {
let network1 = Network::new("files/ntwk1.s2p").unwrap();
let f = network1.f();
assert_eq!(f.len(), network1.f.len());
}
#[test]
fn s_db() {
let network1 = Network::new("files/ntwk1.s2p").unwrap();
let s11 = network1.s_db(1, 1);
let s12 = network1.s_db(1, 2);
let s21 = network1.s_db(2, 1);
let s22 = network1.s_db(2, 2);
assert_eq!(s11.len(), s12.len());
assert_eq!(s11.len(), s21.len());
assert_eq!(s11.len(), s22.len());
}
#[test]
fn s_ri() {
let network1 = Network::new("files/ntwk1.s2p").unwrap();
let s11 = network1.s_ri(1, 1);
let s12 = network1.s_ri(1, 2);
let s21 = network1.s_ri(2, 1);
let s22 = network1.s_ri(2, 2);
assert_eq!(s11.len(), s12.len());
assert_eq!(s11.len(), s21.len());
assert_eq!(s11.len(), s22.len());
}
#[test]
fn s_ma() {
let network1 = Network::new("files/ntwk1.s2p").unwrap();
let s11 = network1.s_ma(1, 1);
let s12 = network1.s_ma(1, 2);
let s21 = network1.s_ma(2, 1);
let s22 = network1.s_ma(2, 2);
assert_eq!(s11.len(), s12.len());
assert_eq!(s11.len(), s21.len());
assert_eq!(s11.len(), s22.len());
}
#[test]
fn cascade_2port_networks() {
let network1 = Network::new("files/ntwk1.s2p").unwrap();
let network2 = Network::new("files/ntwk2.s2p").unwrap();
let network3 = Network::new("files/ntwk3.s2p").unwrap();
let cascaded_network = network1.cascade(&network2);
assert_eq!(cascaded_network.f.len(), 91);
assert_eq!(cascaded_network.s.len(), 91);
for i in 0..cascaded_network.s.len() {
assert_eq!(cascaded_network.s[i].frequency, network3.s[i].frequency);
let s1 = &cascaded_network.s[i].s_ri;
let s2 = &network3.s[i].s_ri;
let epsilon = 1e-4;
assert!(
(s1.get(1, 1).0 - s2.get(1, 1).0).abs() < epsilon,
"S11 real mismatch at freq {}",
cascaded_network.s[i].frequency
);
assert!(
(s1.get(1, 1).1 - s2.get(1, 1).1).abs() < epsilon,
"S11 imag mismatch"
);
assert!(
(s1.get(1, 2).0 - s2.get(1, 2).0).abs() < epsilon,
"S12 real mismatch"
);
assert!(
(s1.get(1, 2).1 - s2.get(1, 2).1).abs() < epsilon,
"S12 imag mismatch"
);
assert!(
(s1.get(2, 1).0 - s2.get(2, 1).0).abs() < epsilon,
"S21 real mismatch"
);
assert!(
(s1.get(2, 1).1 - s2.get(2, 1).1).abs() < epsilon,
"S21 imag mismatch"
);
assert!(
(s1.get(2, 2).0 - s2.get(2, 2).0).abs() < epsilon,
"S22 real mismatch"
);
assert!(
(s1.get(2, 2).1 - s2.get(2, 2).1).abs() < epsilon,
"S22 imag mismatch"
);
}
}
#[test]
fn cascade_2port_networks_operator() {
let network1 = Network::new("files/ntwk1.s2p").unwrap();
let network2 = Network::new("files/ntwk2.s2p").unwrap();
let cascaded_network = network1 * network2;
let network3 = Network::new("files/ntwk3.s2p").unwrap();
assert_eq!(cascaded_network.f.len(), 91);
assert_eq!(cascaded_network.s.len(), 91);
for i in 0..cascaded_network.s.len() {
assert_eq!(cascaded_network.s[i].frequency, network3.s[i].frequency);
let f1 = cascaded_network.f[i];
let f2 = network3.f[i];
assert_eq!(f1, f2);
let s1 = &cascaded_network.s[i].s_ri;
let s2 = &network3.s[i].s_ri;
let epsilon = 1e-4;
assert!(
(s1.get(1, 1).0 - s2.get(1, 1).0).abs() < epsilon,
"S11 real mismatch at freq {}",
cascaded_network.s[i].frequency
);
assert!(
(s1.get(1, 1).1 - s2.get(1, 1).1).abs() < epsilon,
"S11 imag mismatch"
);
assert!(
(s1.get(1, 2).0 - s2.get(1, 2).0).abs() < epsilon,
"S12 real mismatch"
);
assert!(
(s1.get(1, 2).1 - s2.get(1, 2).1).abs() < epsilon,
"S12 imag mismatch"
);
assert!(
(s1.get(2, 1).0 - s2.get(2, 1).0).abs() < epsilon,
"S21 real mismatch"
);
assert!(
(s1.get(2, 1).1 - s2.get(2, 1).1).abs() < epsilon,
"S21 imag mismatch"
);
assert!(
(s1.get(2, 2).0 - s2.get(2, 2).0).abs() < epsilon,
"S22 real mismatch"
);
assert!(
(s1.get(2, 2).1 - s2.get(2, 2).1).abs() < epsilon,
"S22 imag mismatch"
);
}
}
#[test]
fn test_save_load_roundtrip() {
let network1 = Network::new("files/ntwk1.s2p").unwrap();
let temp_dir = std::env::temp_dir()
.join("touchstone_tests")
.join("roundtrip");
std::fs::create_dir_all(&temp_dir).unwrap();
let file_path = temp_dir.join("roundtrip.s2p");
let file_path_str = file_path.to_str().unwrap();
network1.save(file_path_str).unwrap();
let network2 = Network::new(file_path_str).unwrap();
assert_eq!(network1.f.len(), network2.f.len());
assert_eq!(network1.s.len(), network2.s.len());
assert_eq!(network1.format, network2.format);
assert_eq!(network1.z0, network2.z0);
let path_temp = file_path_str.to_string();
let binding = std::path::Path::new(&path_temp);
let network2_name = binding.to_str().unwrap();
assert_eq!(network2.name, network2_name);
assert_eq!(network1.parameter, network2.parameter);
assert_eq!(network1.f.len(), network2.f.len());
for i in 0..network1.f.len() {
assert_eq!(network1.f[i], network2.f[i]);
}
assert_eq!(network1.s.len(), network2.s.len());
for i in 0..network1.s.len() {
let s1 = &network1.s[i].s_ri;
let s2 = &network2.s[i].s_ri;
let epsilon = 1e-6;
assert!((s1.get(1, 1).0 - s2.get(1, 1).0).abs() < epsilon);
assert!((s1.get(1, 1).1 - s2.get(1, 1).1).abs() < epsilon);
}
std::fs::remove_file(file_path).unwrap();
}
#[test]
fn test_save_writes_touchstone_2_1_two_port_21_12_order() {
let temp_dir = std::env::temp_dir()
.join("touchstone_tests")
.join("two_port_order");
std::fs::create_dir_all(&temp_dir).unwrap();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let input_path = temp_dir.join(format!("asymmetric_input_{}.s2p", nanos));
let output_path = temp_dir.join(format!("asymmetric_output_{}.s2p", nanos));
std::fs::write(
&input_path,
"# GHz S RI R 50\n1.0 0.1 0.0 4.0 0.0 0.01 0.0 0.2 0.0\n",
)
.unwrap();
let network = Network::new(input_path.to_str().unwrap()).unwrap();
assert_eq!(
network.s_ri(2, 1)[0].s_ri,
data_pairs::RealImaginary(4.0, 0.0)
);
assert_eq!(
network.s_ri(1, 2)[0].s_ri,
data_pairs::RealImaginary(0.01, 0.0)
);
network.save(output_path.to_str().unwrap()).unwrap();
let saved = std::fs::read_to_string(&output_path).unwrap();
assert!(saved.contains("[Version] 2.1"));
assert!(saved.contains("[Number of Ports] 2"));
assert!(saved.contains("[Two-Port Data Order] 21_12"));
assert!(saved.contains("[Number of Frequencies] 1"));
assert!(saved.contains("[Network Data]"));
assert!(saved.contains("[End]"));
let data_line = saved
.lines()
.find(|line| line.starts_with('1'))
.expect("saved output should contain a data line");
let values = data_line
.split_whitespace()
.map(|part| part.parse::<f64>().unwrap())
.collect::<Vec<_>>();
assert_eq!(values, vec![1.0, 0.1, 0.0, 4.0, 0.0, 0.01, 0.0, 0.2, 0.0]);
let reloaded = Network::new(output_path.to_str().unwrap()).unwrap();
assert_eq!(
reloaded.s_ri(2, 1)[0].s_ri,
data_pairs::RealImaginary(4.0, 0.0)
);
assert_eq!(
reloaded.s_ri(1, 2)[0].s_ri,
data_pairs::RealImaginary(0.01, 0.0)
);
std::fs::remove_file(input_path).unwrap();
std::fs::remove_file(output_path).unwrap();
}
#[test]
fn test_save_load_roundtrip_3port() {
let network1 = Network::new("files/hfss_18.2.s3p").unwrap();
let temp_dir = std::env::temp_dir()
.join("touchstone_tests")
.join("roundtrip");
std::fs::create_dir_all(&temp_dir).unwrap();
let file_path = temp_dir.join("roundtrip_3port.s3p");
let file_path_str = file_path.to_str().unwrap();
network1.save(file_path_str).unwrap();
let network2 = Network::new(file_path_str).unwrap();
assert_eq!(network1.rank, network2.rank);
assert_eq!(network1.rank, 3);
assert_eq!(network1.f.len(), network2.f.len());
assert_eq!(network1.s.len(), network2.s.len());
assert_eq!(network1.format, network2.format);
assert_eq!(network1.z0, network2.z0);
for i in 0..network1.f.len() {
assert_eq!(network1.f[i], network2.f[i]);
}
let epsilon = 1e-6;
for i in 0..network1.s.len() {
for row in 1..=3 {
for col in 1..=3 {
let s1_ma = &network1.s[i].s_ma;
let s2_ma = &network2.s[i].s_ma;
assert!(
(s1_ma.get(row, col).0 - s2_ma.get(row, col).0).abs() < epsilon,
"S{}{} magnitude mismatch at frequency index {}",
row,
col,
i
);
assert!(
(s1_ma.get(row, col).1 - s2_ma.get(row, col).1).abs() < epsilon,
"S{}{} angle mismatch at frequency index {}",
row,
col,
i
);
}
}
}
std::fs::remove_file(file_path).unwrap();
}
#[test]
fn test_save_load_roundtrip_4port() {
let network1 = Network::new("files/Agilent_E5071B.s4p").unwrap();
let temp_dir = std::env::temp_dir()
.join("touchstone_tests")
.join("roundtrip");
std::fs::create_dir_all(&temp_dir).unwrap();
let file_path = temp_dir.join("roundtrip_4port.s4p");
let file_path_str = file_path.to_str().unwrap();
network1.save(file_path_str).unwrap();
let network2 = Network::new(file_path_str).unwrap();
assert_eq!(network1.rank, network2.rank);
assert_eq!(network1.rank, 4);
assert_eq!(network1.f.len(), network2.f.len());
assert_eq!(network1.s.len(), network2.s.len());
assert_eq!(network1.format, network2.format);
assert_eq!(network1.z0, network2.z0);
for i in 0..network1.f.len() {
assert_eq!(network1.f[i], network2.f[i]);
}
let epsilon = 1e-6;
for i in 0..network1.s.len() {
for row in 1..=4 {
for col in 1..=4 {
let s1_db = &network1.s[i].s_db;
let s2_db = &network2.s[i].s_db;
assert!(
(s1_db.get(row, col).0 - s2_db.get(row, col).0).abs() < epsilon,
"S{}{} dB mismatch at frequency index {}",
row,
col,
i
);
assert!(
(s1_db.get(row, col).1 - s2_db.get(row, col).1).abs() < epsilon,
"S{}{} angle mismatch at frequency index {}",
row,
col,
i
);
}
}
}
std::fs::remove_file(file_path).unwrap();
}
#[test]
fn test_cascade_ports_2port_standard() {
let network1 = Network::new("files/ntwk1.s2p").unwrap();
let network2 = Network::new("files/ntwk2.s2p").unwrap();
let network3 = Network::new("files/ntwk3.s2p").unwrap();
let result_ports = network1.cascade_ports(&network2, 2, 1);
let result_standard = network1.cascade(&network2);
assert_eq!(result_ports.rank, result_standard.rank);
assert_eq!(result_ports.f.len(), result_standard.f.len());
assert_eq!(result_ports.s.len(), result_standard.s.len());
let epsilon = 1e-4;
for i in 0..result_ports.s.len() {
let s1 = &result_ports.s[i].s_ri;
let s2 = &network3.s[i].s_ri;
assert!((s1.get(1, 1).0 - s2.get(1, 1).0).abs() < epsilon);
assert!((s1.get(2, 2).0 - s2.get(2, 2).0).abs() < epsilon);
}
}
#[test]
#[should_panic(expected = "only standard cascade (port 2 → port 1) is currently supported")]
fn test_cascade_ports_2port_nonstandard() {
let network1 = Network::new("files/ntwk1.s2p").unwrap();
let network2 = Network::new("files/ntwk2.s2p").unwrap();
let _ = network1.cascade_ports(&network2, 1, 2);
}
#[test]
#[should_panic(expected = "from_port 3 out of range for 2-port network")]
fn test_cascade_ports_invalid_from_port() {
let network1 = Network::new("files/ntwk1.s2p").unwrap();
let network2 = Network::new("files/ntwk2.s2p").unwrap();
let _ = network1.cascade_ports(&network2, 3, 1);
}
#[test]
#[should_panic(expected = "to_port 5 out of range for 2-port network")]
fn test_cascade_ports_invalid_to_port() {
let network1 = Network::new("files/ntwk1.s2p").unwrap();
let network2 = Network::new("files/ntwk2.s2p").unwrap();
let _ = network1.cascade_ports(&network2, 2, 5);
}
#[test]
#[should_panic(expected = "from_port 0 out of range")]
fn test_cascade_ports_zero_port() {
let network1 = Network::new("files/ntwk1.s2p").unwrap();
let network2 = Network::new("files/ntwk2.s2p").unwrap();
let _ = network1.cascade_ports(&network2, 0, 1);
}
#[test]
fn test_save_load_roundtrip_ma_format() {
let network1 = Network::new("files/hfss_threeport_MA.s3p").unwrap();
assert_eq!(network1.format, "MA");
let temp_dir = std::env::temp_dir()
.join("touchstone_tests")
.join("roundtrip_ma");
std::fs::create_dir_all(&temp_dir).unwrap();
let file_path = temp_dir.join("roundtrip_ma.s3p");
let file_path_str = file_path.to_str().unwrap();
network1.save(file_path_str).unwrap();
let network2 = Network::new(file_path_str).unwrap();
assert_eq!(network1.rank, network2.rank);
assert_eq!(network1.format, network2.format);
assert_eq!(network1.f.len(), network2.f.len());
let epsilon = 1e-6;
for i in 0..network1.s.len() {
for row in 1..=3 {
for col in 1..=3 {
let s1 = &network1.s[i].s_ma;
let s2 = &network2.s[i].s_ma;
assert!(
(s1.get(row, col).0 - s2.get(row, col).0).abs() < epsilon,
"S{}{} mag mismatch at index {}",
row,
col,
i
);
assert!(
(s1.get(row, col).1 - s2.get(row, col).1).abs() < epsilon,
"S{}{} angle mismatch at index {}",
row,
col,
i
);
}
}
}
std::fs::remove_file(file_path).unwrap();
}
#[test]
fn test_save_load_roundtrip_db_format() {
let network1 = Network::new("files/hfss_threeport_DB.s3p").unwrap();
assert_eq!(network1.format, "DB");
let temp_dir = std::env::temp_dir()
.join("touchstone_tests")
.join("roundtrip_db");
std::fs::create_dir_all(&temp_dir).unwrap();
let file_path = temp_dir.join("roundtrip_db.s3p");
let file_path_str = file_path.to_str().unwrap();
network1.save(file_path_str).unwrap();
let network2 = Network::new(file_path_str).unwrap();
assert_eq!(network1.rank, network2.rank);
assert_eq!(network1.format, network2.format);
assert_eq!(network1.f.len(), network2.f.len());
let epsilon = 1e-6;
for i in 0..network1.s.len() {
for row in 1..=3 {
for col in 1..=3 {
let s1 = &network1.s[i].s_db;
let s2 = &network2.s[i].s_db;
assert!(
(s1.get(row, col).0 - s2.get(row, col).0).abs() < epsilon,
"S{}{} dB mismatch at index {}",
row,
col,
i
);
assert!(
(s1.get(row, col).1 - s2.get(row, col).1).abs() < epsilon,
"S{}{} angle mismatch at index {}",
row,
col,
i
);
}
}
}
std::fs::remove_file(file_path).unwrap();
}
#[test]
fn test_save_load_roundtrip_1port() {
let network1 = Network::new("files/hfss_oneport.s1p").unwrap();
let temp_dir = std::env::temp_dir()
.join("touchstone_tests")
.join("roundtrip_1port");
std::fs::create_dir_all(&temp_dir).unwrap();
let file_path = temp_dir.join("roundtrip.s1p");
let file_path_str = file_path.to_str().unwrap();
network1.save(file_path_str).unwrap();
let network2 = Network::new(file_path_str).unwrap();
assert_eq!(network1.rank, 1);
assert_eq!(network1.rank, network2.rank);
assert_eq!(network1.f.len(), network2.f.len());
let epsilon = 1e-6;
for i in 0..network1.s.len() {
let s1 = &network1.s[i].s_ri;
let s2 = &network2.s[i].s_ri;
assert!((s1.get(1, 1).0 - s2.get(1, 1).0).abs() < epsilon);
assert!((s1.get(1, 1).1 - s2.get(1, 1).1).abs() < epsilon);
}
std::fs::remove_file(file_path).unwrap();
}
#[test]
#[should_panic(expected = "Cannot cascade networks with different reference impedances")]
fn test_cascade_different_z0() {
let mut net1 = Network::new("files/ntwk1.s2p").unwrap();
let net2 = Network::new("files/ntwk2.s2p").unwrap();
net1.z0 = 75.0;
let _ = net1.cascade(&net2);
}
#[test]
#[should_panic(expected = "Cannot cascade networks with different frequency units")]
fn test_cascade_different_freq_units() {
let mut net1 = Network::new("files/ntwk1.s2p").unwrap();
let net2 = Network::new("files/ntwk2.s2p").unwrap();
net1.frequency_unit = "MHz".to_string();
let _ = net1.cascade(&net2);
}
#[test]
#[should_panic(expected = "Cascading is only implemented for 2-port networks")]
fn test_cascade_non_2port() {
let net1 = Network::new("files/hfss_18.2.s3p").unwrap();
let net2 = Network::new("files/hfss_18.2.s3p").unwrap();
let _ = net1.cascade(&net2);
}
#[test]
fn test_print_summary() {
let net = Network::new("files/ntwk1.s2p").unwrap();
net.print_summary();
}
#[test]
fn test_clone() {
let net1 = Network::new("files/ntwk1.s2p").unwrap();
let net2 = net1.clone();
assert_eq!(net1.rank, net2.rank);
assert_eq!(net1.f.len(), net2.f.len());
assert_eq!(net1.z0, net2.z0);
}
#[test]
fn test_debug() {
let net = Network::new("files/ntwk1.s2p").unwrap();
let debug_str = format!("{:?}", net);
assert!(debug_str.contains("Network"));
}
#[test]
fn sample_at_exact_match_returns_existing_point() {
let network = interpolation_1port_network();
let point = network
.sample_at(1.0e9, Interpolation::Linear, Extrapolation::Error)
.unwrap();
assert_eq!(point.frequency, 1.0e9);
assert_s11(&point, 0.0, 0.0);
}
#[test]
fn sample_at_midpoint_linearly_interpolates_real_imaginary_space() {
let network = interpolation_1port_network();
let point = network
.sample_at(1.5e9, Interpolation::Linear, Extrapolation::Error)
.unwrap();
assert_eq!(point.frequency, 1.5e9);
assert_s11(&point, 1.0, 2.0);
}
#[test]
fn resample_derives_ma_and_db_from_interpolated_real_imaginary_values() {
let network = interpolation_1port_network();
let resampled = network
.resample([1.5e9], Interpolation::Linear, Extrapolation::Error)
.unwrap();
assert_eq!(resampled.f, vec![1.5e9]);
assert_eq!(resampled.s.len(), 1);
let s_ri = resampled.s[0].s_ri.get(1, 1);
let s_ma = resampled.s[0].s_ma.get(1, 1);
let s_db = resampled.s[0].s_db.get(1, 1);
let magnitude = f64::sqrt(5.0);
assert_approx_eq(s_ri.0, 1.0);
assert_approx_eq(s_ri.1, 2.0);
assert_approx_eq(s_ma.0, magnitude);
assert_approx_eq(s_ma.1, f64::atan2(2.0, 1.0) * 180.0 / std::f64::consts::PI);
assert_approx_eq(s_db.0, 20.0 * magnitude.log10());
assert_approx_eq(s_db.1, s_ma.1);
}
#[test]
fn sample_at_nearest_uses_closest_point() {
let network = interpolation_1port_network();
let lower = network
.sample_at(1.4e9, Interpolation::Nearest, Extrapolation::Error)
.unwrap();
let upper = network
.sample_at(1.6e9, Interpolation::Nearest, Extrapolation::Error)
.unwrap();
let tie = network
.sample_at(1.5e9, Interpolation::Nearest, Extrapolation::Error)
.unwrap();
assert_s11(&lower, 0.0, 0.0);
assert_s11(&upper, 2.0, 4.0);
assert_s11(&tie, 0.0, 0.0);
}
#[test]
fn sample_at_lower_and_upper_bounds_return_boundary_points() {
let network = interpolation_1port_network();
let lower = network
.sample_at(1.0e9, Interpolation::Linear, Extrapolation::Error)
.unwrap();
let upper = network
.sample_at(2.0e9, Interpolation::Linear, Extrapolation::Error)
.unwrap();
assert_s11(&lower, 0.0, 0.0);
assert_s11(&upper, 2.0, 4.0);
}
#[test]
fn sample_at_out_of_range_returns_structured_error() {
let network = interpolation_1port_network();
let error = network
.sample_at(0.5e9, Interpolation::Linear, Extrapolation::Error)
.unwrap_err();
assert!(matches!(
error,
TouchstoneError::FrequencyOutOfRange {
frequency,
min,
max
} if frequency == 0.5e9 && min == 1.0e9 && max == 2.0e9
));
}
#[test]
fn sample_at_clamp_holds_boundary_values_at_requested_frequency() {
let network = interpolation_1port_network();
let lower = network
.sample_at(0.5e9, Interpolation::Linear, Extrapolation::Clamp)
.unwrap();
let upper = network
.sample_at(3.0e9, Interpolation::Linear, Extrapolation::Clamp)
.unwrap();
assert_eq!(lower.frequency, 0.5e9);
assert_eq!(upper.frequency, 3.0e9);
assert_s11(&lower, 0.0, 0.0);
assert_s11(&upper, 2.0, 4.0);
}
#[test]
fn sample_at_interpolates_n_port_matrix_values() {
let network = Network::from_str(
"matrix.s3p",
"# GHz S RI R 50\n\
1.0 1 -1 2 -2 3 -3 4 -4 5 -5 6 -6 7 -7 8 -8 9 -9\n\
2.0 11 -11 12 -12 13 -13 14 -14 15 -15 16 -16 17 -17 18 -18 19 -19\n",
)
.unwrap();
let point = network
.sample_at(1.5e9, Interpolation::Linear, Extrapolation::Error)
.unwrap();
let s32 = point.s.get(3, 2).unwrap();
assert_eq!(point.s.rank, 3);
assert_approx_eq(s32.re, 13.0);
assert_approx_eq(s32.im, -13.0);
}
#[test]
fn resample_preserves_network_metadata() {
let network = Network::from_str(
"metadata.s2p",
"! before option\n\
[Version] 2.1\n\
# GHz S DB R 75\n\
[Number of Ports] 2\n\
[Reference] 45 55\n\
[Network Data]\n\
! after option\n\
1.0 0 0 0 0 0 0 0 0\n\
2.0 6 0 6 0 6 0 6 0\n\
[End]\n",
)
.unwrap();
let resampled = network
.resample(
[1.0e9, 1.5e9, 2.0e9],
Interpolation::Linear,
Extrapolation::Error,
)
.unwrap();
assert_eq!(resampled.name, network.name);
assert_eq!(resampled.rank, network.rank);
assert_eq!(resampled.frequency_unit, network.frequency_unit);
assert_eq!(resampled.parameter, network.parameter);
assert_eq!(resampled.format, network.format);
assert_eq!(resampled.resistance_string, network.resistance_string);
assert_eq!(resampled.z0, network.z0);
assert_eq!(
resampled.reference_impedance(),
network.reference_impedance()
);
assert_eq!(resampled.comments, network.comments);
assert_eq!(
resampled.comments_after_option_line,
network.comments_after_option_line
);
assert_eq!(resampled.warnings, network.warnings);
assert_eq!(resampled.f, vec![1.0e9, 1.5e9, 2.0e9]);
assert_eq!(resampled.s.len(), 3);
}
#[test]
fn sample_at_rejects_empty_network_data() {
let mut network = interpolation_1port_network();
network.f.clear();
network.s.clear();
let error = network
.sample_at(1.0e9, Interpolation::Linear, Extrapolation::Error)
.unwrap_err();
assert!(matches!(error, TouchstoneError::EmptyNetworkData));
}
#[test]
fn sample_at_rejects_duplicate_frequencies() {
let mut network = interpolation_1port_network();
network.f = vec![1.0e9, 1.0e9];
let error = network
.sample_at(1.0e9, Interpolation::Linear, Extrapolation::Error)
.unwrap_err();
assert!(matches!(
error,
TouchstoneError::DuplicateFrequency {
first_index: 0,
duplicate_index: 1,
frequency
} if frequency == 1.0e9
));
}
#[test]
fn sample_at_rejects_non_finite_frequencies() {
let mut network = interpolation_1port_network();
network.f = vec![1.0e9, f64::INFINITY];
let error = network
.sample_at(1.0e9, Interpolation::Linear, Extrapolation::Error)
.unwrap_err();
assert!(matches!(
error,
TouchstoneError::InvalidFrequency {
point_index: 1,
frequency
} if frequency.is_infinite()
));
}
#[test]
fn sample_at_rejects_unsorted_frequencies() {
let mut network = interpolation_1port_network();
network.f = vec![2.0e9, 1.0e9];
let error = network
.sample_at(1.5e9, Interpolation::Linear, Extrapolation::Error)
.unwrap_err();
assert!(matches!(
error,
TouchstoneError::UnsortedFrequencies {
previous_index: 0,
previous_frequency,
next_index: 1,
next_frequency
} if previous_frequency == 2.0e9 && next_frequency == 1.0e9
));
}
}