touchstone 0.14.1

Touchstone (s2p, etc.) file parser, plotter, and more
Documentation
use touchstone::{Complex, Network, NetworkBuilder, ReferenceImpedance, SMatrix, TouchstoneError};

fn c(re: f64, im: f64) -> Complex {
    Complex { re, im }
}

fn matrix(rows: Vec<Vec<Complex>>) -> SMatrix {
    SMatrix {
        rank: rows.len(),
        data: rows,
    }
}

#[test]
fn builds_one_port_network_with_comments_and_derived_data() {
    let network = NetworkBuilder::new("generated.s1p", 1)
        .comment("generated one-port")
        .network_data_comment("measured in memory")
        .z0(75.0)
        .point(1.0e9, matrix(vec![vec![c(0.5, -0.25)]]))
        .point(2.0e9, matrix(vec![vec![c(0.6, -0.35)]]))
        .build()
        .unwrap();

    assert_eq!(network.name, "generated.s1p");
    assert_eq!(network.rank, 1);
    assert_eq!(network.frequency_unit, "Hz");
    assert_eq!(network.format, "RI");
    assert_eq!(network.z0, 75.0);
    assert_eq!(
        network.reference_impedance(),
        ReferenceImpedance::Common(75.0)
    );
    assert_eq!(network.f, vec![1.0e9, 2.0e9]);
    assert!(network.warnings.is_empty());
    assert_eq!(network.try_s_ri_at(1, 1, 1).unwrap(), c(0.6, -0.35));
    assert!(network.s_ma(1, 1)[0].s_ma.magnitude().is_finite());
    assert!(network.s_db(1, 1)[0].s_db.decibel().is_finite());

    let serialized = network.to_touchstone_string().unwrap();
    assert!(serialized.starts_with("! generated one-port\n[Version] 2.1\n"));
    assert!(!serialized.contains("[Reference]"));
    assert!(serialized.contains("[Network Data]\n! measured in memory\n"));
}

#[test]
fn builds_two_port_network_with_21_12_order_and_roundtrips_from_str() {
    let network = NetworkBuilder::new("generated.s2p", 2)
        .frequency_unit("GHz")
        .point(
            1.0e9,
            matrix(vec![
                vec![c(0.11, 0.01), c(0.12, 0.03)],
                vec![c(0.21, 0.02), c(0.22, 0.04)],
            ]),
        )
        .build()
        .unwrap();

    let serialized = network.to_touchstone_string().unwrap();
    assert!(serialized.contains("[Two-Port Data Order] 21_12\n"));

    let data_line = serialized
        .lines()
        .find(|line| line.starts_with("1 "))
        .expect("serialized two-port data line");
    let values = data_line
        .split_whitespace()
        .map(|part| part.parse::<f64>().unwrap())
        .collect::<Vec<_>>();
    assert_eq!(
        values,
        vec![1.0, 0.11, 0.01, 0.21, 0.02, 0.12, 0.03, 0.22, 0.04]
    );

    let reparsed = Network::from_str("generated.s2p", &serialized).unwrap();
    assert_eq!(reparsed.try_s_ri_at(0, 1, 2).unwrap(), c(0.12, 0.03));
    assert_eq!(reparsed.try_s_ri_at(0, 2, 1).unwrap(), c(0.21, 0.02));
}

#[test]
fn serializes_thz_frequency_unit() {
    let network = NetworkBuilder::new("generated.s1p", 1)
        .frequency_unit("THz")
        .point(1.0e12, matrix(vec![vec![c(0.5, 0.0)]]))
        .build()
        .unwrap();

    let serialized = network.to_touchstone_string().unwrap();

    assert!(serialized.contains("# THz S RI R 50\n"));
    assert!(serialized.contains("1 0.5 0\n"));
}

#[test]
fn builds_three_port_network_with_multiline_full_matrix_output() {
    let network = NetworkBuilder::new("generated.s3p", 3)
        .point(
            1.0e9,
            matrix(vec![
                vec![c(11.0, 0.1), c(12.0, 0.2), c(13.0, 0.3)],
                vec![c(21.0, 0.4), c(22.0, 0.5), c(23.0, 0.6)],
                vec![c(31.0, 0.7), c(32.0, 0.8), c(33.0, 0.9)],
            ]),
        )
        .build()
        .unwrap();

    let serialized = network.to_touchstone_string().unwrap();
    let lines = serialized.lines().collect::<Vec<_>>();
    let data_index = lines
        .iter()
        .position(|line| *line == "[Network Data]")
        .unwrap();

    assert_eq!(lines[data_index + 1], "1000000000 11 0.1 12 0.2 13 0.3");
    assert_eq!(lines[data_index + 2], " 21 0.4 22 0.5 23 0.6");
    assert_eq!(lines[data_index + 3], " 31 0.7 32 0.8 33 0.9");
    assert_eq!(lines[data_index + 4], "[End]");

    let reparsed = Network::from_str("generated.s3p", &serialized).unwrap();
    assert_eq!(reparsed.try_s_ri_at(0, 3, 2).unwrap(), c(32.0, 0.8));
}

#[test]
fn save_matches_in_memory_serialization() {
    let network = NetworkBuilder::new("generated.s2p", 2)
        .point(
            1.0e9,
            matrix(vec![
                vec![c(0.11, 0.01), c(0.12, 0.03)],
                vec![c(0.21, 0.02), c(0.22, 0.04)],
            ]),
        )
        .build()
        .unwrap();
    let temp_dir = std::env::temp_dir().join("touchstone_network_builder_tests");
    std::fs::create_dir_all(&temp_dir).unwrap();
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let path = temp_dir.join(format!("save_matches_in_memory_{nanos}.s2p"));

    network.save(path.to_str().unwrap()).unwrap();
    let saved = std::fs::read_to_string(&path).unwrap();

    assert_eq!(saved, network.to_touchstone_string().unwrap());

    std::fs::remove_file(path).unwrap();
}

#[test]
fn rejects_invalid_network_rank() {
    let error = NetworkBuilder::new("generated.s0p", 0).build().unwrap_err();

    assert!(matches!(
        error,
        TouchstoneError::InvalidNetworkRank { rank: 0 }
    ));
}

#[test]
fn rejects_extension_rank_mismatch() {
    let error = NetworkBuilder::new("generated.s2p", 1)
        .point(1.0e9, matrix(vec![vec![c(0.5, 0.0)]]))
        .build()
        .unwrap_err();

    assert!(matches!(
        error,
        TouchstoneError::NetworkRankExtensionMismatch {
            rank: 1,
            extension_rank: 2
        }
    ));
}

#[test]
fn rejects_empty_data() {
    let error = NetworkBuilder::new("generated.s1p", 1).build().unwrap_err();

    assert!(matches!(error, TouchstoneError::EmptyNetworkData));
}

#[test]
fn rejects_matrix_rank_mismatch() {
    let error = NetworkBuilder::new("generated.s2p", 2)
        .point(1.0e9, matrix(vec![vec![c(0.5, 0.0)]]))
        .build()
        .unwrap_err();

    assert!(matches!(
        error,
        TouchstoneError::InvalidMatrixRank {
            point_index: 0,
            matrix_rank: 1,
            expected_rank: 2
        }
    ));
}

#[test]
fn rejects_matrix_shape_mismatch() {
    let error = NetworkBuilder::new("generated.s2p", 2)
        .point(
            1.0e9,
            SMatrix {
                rank: 2,
                data: vec![vec![c(0.5, 0.0)]],
            },
        )
        .build()
        .unwrap_err();

    assert!(matches!(
        error,
        TouchstoneError::InvalidMatrixShape {
            point_index: 0,
            rows: 1,
            row_index: None,
            columns: 0,
            expected_rank: 2
        }
    ));
}

#[test]
fn rejects_non_finite_frequency() {
    let error = NetworkBuilder::new("generated.s1p", 1)
        .point(f64::NAN, matrix(vec![vec![c(0.5, 0.0)]]))
        .build()
        .unwrap_err();

    assert!(matches!(
        error,
        TouchstoneError::InvalidFrequency { point_index: 0, frequency } if frequency.is_nan()
    ));
}

#[test]
fn rejects_non_finite_s_parameter_value() {
    let error = NetworkBuilder::new("generated.s1p", 1)
        .point(1.0e9, matrix(vec![vec![c(f64::INFINITY, 0.0)]]))
        .build()
        .unwrap_err();

    assert!(matches!(
        error,
        TouchstoneError::InvalidSParameterValue {
            point_index: 0,
            to_port: 1,
            from_port: 1,
            re,
            im: 0.0,
        } if re.is_infinite()
    ));
}

#[test]
fn rejects_non_positive_z0() {
    let error = NetworkBuilder::new("generated.s1p", 1)
        .z0(0.0)
        .point(1.0e9, matrix(vec![vec![c(0.5, 0.0)]]))
        .build()
        .unwrap_err();

    assert!(matches!(
        error,
        TouchstoneError::InvalidReferenceImpedance { z0: 0.0 }
    ));
}