powerio-dist 0.11.7

Multiconductor distribution network model and lossless converters for OpenDSS, PMD JSON, BMOPF JSON, and distribution CIM.
Documentation
use super::*;
use crate::dss::raw::__parse_raw_with;

fn read(script: &str) -> RawDss {
    __parse_raw_with(script, "line.dss", &mut |_: &std::path::Path| {
        Err(std::io::Error::other("no includes"))
    })
}

fn point(raw: &RawDss, name: &str) -> Snapshot {
    raw.line_snapshot(
        raw.objects
            .iter()
            .position(|o| o.class == "line" && o.name == name)
            .unwrap(),
    )
}

#[test]
fn copy_before_solve_keeps_the_sources_stored_matrix() {
    let raw = read("New Circuit.c\nNew Line.a r1=0.7\nNew Line.b like=a\nSolve\nNew Line.c like=a");
    let mut b = resolve(&raw, point(&raw, "b")).unwrap();
    let mut c = resolve(&raw, point(&raw, "c")).unwrap();
    let b = b.primitive(60.0).unwrap();
    let c = c.primitive(60.0).unwrap();
    assert!((b.r[0][0] - 0.098_133_333_333_333_32).abs() < 1e-15);
    assert!((c.r[0][0] - (2.0 * 0.7 + 0.1784) / 3.0).abs() < 1e-15);
}

#[test]
fn copied_line_leaves_destination_buses_units_and_switch_flag() {
    let raw = read(
        "New Circuit.c\nNew Line.a bus1=first bus2=second switch=yes\nNew Line.b bus1=feed bus2=end units=km like=a",
    );
    let b = resolve(&raw, point(&raw, "b")).unwrap();
    assert_eq!(b.bus, ["feed", "end"]);
    assert_eq!(b.units, 3);
    assert!(!b.is_switch);
}

#[test]
fn solved_base_frequency_edits_need_primitive_invalidation() {
    let raw = read(
        "New Circuit.c\nNew Line.a\nSolve\nEdit Line.a basefreq=45\nSolve\nNew Line.b like=a\nBuildY\nEdit Line.a length=1\nSolve",
    );
    let mut a = resolve(&raw, point(&raw, "a")).unwrap();
    let a = a.primitive(60.0).unwrap();
    assert!((a.r[0][0] - (0.098_133_333_333_333_32 + 0.01805 / 3.0)).abs() < 1e-15);
}

#[test]
fn singular_lines_are_not_replaced_by_epsilon_conductance() {
    let raw = read("New Circuit.c\nNew Line.a phases=1 r1=0 x1=0 rg=0 xg=0\nSolve");
    assert!(
        resolve(&raw, point(&raw, "a"))
            .unwrap_err()
            .contains("error 183")
    );
    let raw = read("New Circuit.c\nNew Line.a phases=2 rmatrix=[1|0 1] phases=3");
    assert!(
        resolve(&raw, point(&raw, "a"))
            .unwrap_err()
            .contains("error 18101")
    );
}

#[test]
fn buildy_does_not_invalidate_an_existing_line_primitive() {
    let raw = read("New Circuit.c\nNew Line.a\nSolve\nEdit Line.a basefreq=45\nBuildY\nSolve");
    let matrix = resolve(&raw, point(&raw, "a"))
        .unwrap()
        .primitive(60.0)
        .unwrap();
    assert!((matrix.r[0][0] - 0.098_133_333_333_333_32).abs() < 1e-15);
}

#[test]
fn line_copies_do_not_recurse_through_long_empty_chains() {
    use std::fmt::Write as _;
    let mut script = "New Circuit.c\nNew Line.l0\n".to_owned();
    for i in 1..=512 {
        writeln!(script, "New Line.l{i} like=l{}", i - 1).unwrap();
    }
    let raw = read(&script);
    let matrix = resolve(&raw, point(&raw, "l512"))
        .unwrap()
        .primitive(60.0)
        .unwrap();
    assert!((matrix.r[0][0] - 0.098_133_333_333_333_32).abs() < 1e-15);
}

#[test]
fn linecode_work_shares_the_line_resolution_budget() {
    let raw = read("New Circuit.c\nNew Linecode.a r1=0.7\nNew Line.b linecode=a");
    let code = raw.line_history[&point(&raw, "b").object]
        .linecodes
        .values()
        .next()
        .unwrap()
        .unwrap();
    assert!(
        linecode::resolve_with_budget(&raw, code, &mut 1)
            .unwrap_err()
            .contains("work limit")
    );
}

#[test]
#[ignore = "requires pinned native ordered Line observations"]
fn pinned_line_states() {
    #[derive(serde::Deserialize)]
    struct Native {
        frequency: f64,
        n: usize,
        units: usize,
        bus: Vec<String>,
        enabled: bool,
        is_switch: bool,
        normamps: f64,
        emergamps: f64,
        length: f64,
    }
    #[derive(serde::Deserialize)]
    struct Case {
        script: String,
        lines: std::collections::BTreeMap<String, Native>,
    }
    #[derive(serde::Deserialize)]
    struct Report {
        cases: std::collections::BTreeMap<String, Case>,
        failures: Vec<String>,
    }
    let report: Report = serde_json::from_slice(
        &std::fs::read(std::env::var("POWERIO_LINE_ORACLE").unwrap()).unwrap(),
    )
    .unwrap();
    assert_eq!(report.failures, [] as [std::string::String; 0]);
    assert!(!report.cases.is_empty());
    let mut output = std::collections::BTreeMap::new();
    for (name, case) in report.cases {
        let raw = read(&case.script);
        let mut results = std::collections::BTreeMap::new();
        for (line, expected) in case.lines {
            let mut state =
                resolve(&raw, point(&raw, &line)).unwrap_or_else(|e| panic!("{name}/{line}: {e}"));
            assert_eq!(
                (state.n, state.units, state.enabled, state.is_switch),
                (
                    expected.n,
                    expected.units,
                    expected.enabled,
                    expected.is_switch
                ),
                "{name}/{line}"
            );
            assert_eq!(state.bus.as_slice(), expected.bus, "{name}/{line}");
            for (a, b) in [
                state.frequency,
                state.normamps,
                state.emergamps,
                state.length,
            ]
            .into_iter()
            .zip([
                expected.frequency,
                expected.normamps,
                expected.emergamps,
                expected.length,
            ]) {
                assert!(
                    (a - b).abs() <= 1e-12 + 1e-12 * b.abs(),
                    "{name}/{line}: {a} != {b}"
                );
            }
            if state.enabled {
                let matrices = state
                    .primitive(raw.snapshot_frequency())
                    .unwrap_or_else(|e| panic!("{name}/{line}: {e}"));
                results.insert(
                    line,
                    serde_json::json!({"r":matrices.r,"x":matrices.x,"b":matrices.b}),
                );
            }
        }
        output.insert(name, results);
    }
    std::fs::write(
        std::env::var("POWERIO_LINE_MATRICES").unwrap(),
        serde_json::to_vec_pretty(&output).unwrap(),
    )
    .unwrap();
}

#[test]
fn fixed_terminal_history_follows_native_copy_and_resize_rules() {
    for (commands, expected) in [
        ("open line.a 1 2", [vec![true, false, true], vec![true; 3]]),
        (
            "open line.a 1\nclose line.a 1 2",
            [vec![false, true, false], vec![true; 3]],
        ),
        (
            "open line.a 2 1\nedit line.a phases=3",
            [vec![true; 3], vec![false, true, true]],
        ),
        (
            "open line.a 2 1\nedit line.a phases=2\nedit line.a phases=3",
            [vec![true; 3], vec![true; 3]],
        ),
        (
            "open line.a 2 1\nnew line.b\nedit line.a like=b",
            [vec![true; 3], vec![false, true, true]],
        ),
        (
            "new line.b\nopen line.b 1\nedit line.a like=b",
            [vec![true; 3], vec![true; 3]],
        ),
        (
            "open line.a 2\nclose line.a term=2 cond=0",
            [vec![true; 3], vec![true; 3]],
        ),
    ] {
        let raw = read(&format!("new circuit.c\nnew line.a\n{commands}\nsolve"));
        assert_eq!(
            resolve(&raw, point(&raw, "a")).unwrap().closed,
            expected,
            "{commands}"
        );
    }
}

#[test]
fn terminal_command_binds_before_later_declarations_and_selects_the_line() {
    let raw = read("new circuit.c\nopen line.a 1\nnew line.a");
    assert!(
        raw.warnings
            .iter()
            .any(|w| w.contains("requires an existing Line or Transformer"))
    );
    let raw = read("new circuit.c\nnew line.a\nnew line.b\nopen line.a 1 2\n~ length=7");
    assert_eq!(
        resolve(&raw, point(&raw, "a")).unwrap().length.to_bits(),
        7.0_f64.to_bits()
    );
    assert_eq!(
        resolve(&raw, point(&raw, "b")).unwrap().length.to_bits(),
        1.0_f64.to_bits()
    );
}