touchstone 0.13.2

Touchstone (s2p, etc.) file parser, plotter, and more
Documentation
use std::fs;

use touchstone::{Network, TouchstoneError, TouchstoneWarning};

const TWO_PORT_RI: &str = "# GHz S RI R 50\n1.0 0.1 0.0 4.0 0.0 0.01 0.0 0.2 0.0\n";

#[test]
fn new_reads_existing_touchstone_file() {
    let network = Network::new("files/ntwk1.s2p").unwrap();

    assert_eq!(network.name, "files/ntwk1.s2p");
    assert_eq!(network.rank, 2);
    assert!(!network.f.is_empty());
}

#[test]
fn from_str_parses_uploaded_touchstone_data_without_file() {
    let network = Network::from_str("uploaded.s2p", TWO_PORT_RI).unwrap();

    assert_eq!(network.name, "uploaded.s2p");
    assert_eq!(network.rank, 2);
    assert_eq!(network.frequency_unit, "GHz");
    assert_eq!(network.z0, 50.0);
    assert_eq!(network.f, vec![1.0e9]);
    assert_eq!(network.s_ri(2, 1)[0].s_ri.0, 4.0);
    assert_eq!(network.s_ri(1, 2)[0].s_ri.0, 0.01);
}

#[test]
fn from_bytes_parses_uploaded_touchstone_data_without_file() {
    let network = Network::from_bytes("uploaded.s2p", TWO_PORT_RI.as_bytes()).unwrap();

    assert_eq!(network.name, "uploaded.s2p");
    assert_eq!(network.rank, 2);
    assert_eq!(network.s_ri(2, 1)[0].s_ri.0, 4.0);
    assert_eq!(network.s_ri(1, 2)[0].s_ri.0, 0.01);
}

#[test]
fn from_memory_matches_new_for_s1p_file() {
    assert_from_memory_matches_new("files/hfss_oneport.s1p");
}

#[test]
fn from_memory_matches_new_for_s3p_file() {
    assert_from_memory_matches_new("files/hfss_18.2.s3p");
}

#[test]
fn from_bytes_returns_utf8_errors() {
    let error = Network::from_bytes("uploaded.s1p", &[0xff]).unwrap_err();

    assert!(matches!(error, TouchstoneError::InvalidUtf8(_)));
}

#[test]
fn from_str_returns_unsupported_file_type_errors() {
    let error = Network::from_str("uploaded.txt", TWO_PORT_RI).unwrap_err();

    assert!(matches!(
        error,
        TouchstoneError::UnsupportedFileType { file_type } if file_type == "txt"
    ));
}

#[test]
fn from_str_returns_malformed_data_errors() {
    let error = Network::from_str("uploaded.s1p", "# GHz S RI R 50\n1.0 0.1\n").unwrap_err();

    assert!(matches!(
        error.root_cause(),
        TouchstoneError::InvalidDataLineParts {
            expected: 3,
            actual: 2
        }
    ));

    let context = error.context().unwrap();
    assert_eq!(context.source_name, "uploaded.s1p");
    assert_eq!(context.line_number, Some(2));
    assert_eq!(context.line.as_deref(), Some("1.0 0.1"));
    assert!(error.to_string().contains("uploaded.s1p:2"));
}

#[test]
fn from_str_records_missing_option_line_warning() {
    let network = Network::from_str("uploaded.s1p", "1.0 0.1 0.0\n").unwrap();

    assert_eq!(network.frequency_unit, "GHz");
    assert_eq!(network.format, "MA");
    assert_eq!(network.z0, 50.0);
    assert!(matches!(
        network.warnings.as_slice(),
        [TouchstoneWarning::MissingOptionLine { source_name }] if source_name == "uploaded.s1p"
    ));
}

#[test]
fn from_str_records_ignored_extra_option_line_warning() {
    let network = Network::from_str(
        "uploaded.s1p",
        "# GHz S RI R 50\n# MHz S RI R 75\n1.0 0.1 0.0\n",
    )
    .unwrap();

    assert_eq!(network.frequency_unit, "GHz");
    assert_eq!(network.z0, 50.0);
    assert!(matches!(
        network.warnings.as_slice(),
        [TouchstoneWarning::AdditionalOptionLineIgnored {
            source_name,
            line_number: 2,
            line,
        }] if source_name == "uploaded.s1p" && line == "# MHz S RI R 75"
    ));
}

#[test]
fn from_str_records_unknown_keyword_warning() {
    let network = Network::from_str(
        "uploaded.s1p",
        "[Version] 2.1\n[Unsupported Keyword] ignored\n# GHz S RI R 50\n[Network Data]\n1.0 0.1 0.0\n[End]\n",
    )
    .unwrap();

    assert!(matches!(
        network.warnings.as_slice(),
        [TouchstoneWarning::UnknownKeywordIgnored {
            source_name,
            line_number: 2,
            keyword,
        }] if source_name == "uploaded.s1p" && keyword == "unsupported keyword"
    ));
}

fn assert_from_memory_matches_new(path: &str) {
    let contents = fs::read_to_string(path).unwrap();
    let bytes = fs::read(path).unwrap();

    let from_file = Network::new(path).unwrap();
    let from_str = Network::from_str(path, &contents).unwrap();
    let from_bytes = Network::from_bytes(path, &bytes).unwrap();

    assert_same_network_shape(path, &from_file, &from_str);
    assert_same_network_shape(path, &from_file, &from_bytes);
}

fn assert_same_network_shape(path: &str, left: &Network, right: &Network) {
    assert_eq!(left.name, right.name, "{path}");
    assert_eq!(left.rank, right.rank, "{path}");
    assert_eq!(left.frequency_unit, right.frequency_unit, "{path}");
    assert_eq!(left.parameter, right.parameter, "{path}");
    assert_eq!(left.format, right.format, "{path}");
    assert_eq!(left.resistance_string, right.resistance_string, "{path}");
    assert_eq!(left.z0, right.z0, "{path}");
    assert_eq!(left.comments, right.comments, "{path}");
    assert_eq!(
        left.comments_after_option_line, right.comments_after_option_line,
        "{path}"
    );
    assert_eq!(left.warnings, right.warnings, "{path}");
    assert_eq!(left.f, right.f, "{path}");
    assert_eq!(left.s.len(), right.s.len(), "{path}");

    for j in 1..=left.rank {
        for k in 1..=left.rank {
            let left_s = left.s_ri(j as i8, k as i8);
            let right_s = right.s_ri(j as i8, k as i8);
            assert_eq!(left_s.len(), right_s.len(), "{path} S{j}{k}");
            for (left_point, right_point) in left_s.iter().zip(right_s) {
                assert_eq!(
                    left_point.frequency, right_point.frequency,
                    "{path} S{j}{k}"
                );
                assert_eq!(left_point.s_ri, right_point.s_ri, "{path} S{j}{k}");
            }
        }
    }
}