powerio-tx 0.11.0

Compiler infrastructure for power systems: parse, convert, validate, and lower grid models
Documentation
//! Shared helpers for the converter integration tests. Each test binary
//! compiles this module and uses its own subset, hence the `allow(dead_code)`.

use std::path::{Path, PathBuf};

use serde_json::Value;

/// Structural + numeric (tolerant) equality of two JSON values: same shape and
/// keys, numbers within a small relative tolerance. The per-unit PowerModels
/// round-trip (÷base on write, ×base on read) is not bit-exact in f64, so the
/// JSON comparisons use this rather than `==`.
#[allow(dead_code)]
pub fn json_approx_eq(a: &Value, b: &Value) -> bool {
    match (a, b) {
        (Value::Number(x), Value::Number(y)) => match (x.as_f64(), y.as_f64()) {
            (Some(xf), Some(yf)) => (xf - yf).abs() <= 1e-9 * xf.abs().max(yf.abs()).max(1.0),
            _ => x == y,
        },
        (Value::Array(xs), Value::Array(ys)) => {
            xs.len() == ys.len() && xs.iter().zip(ys).all(|(p, q)| json_approx_eq(p, q))
        }
        (Value::Object(xs), Value::Object(ys)) => {
            xs.len() == ys.len()
                && xs
                    .iter()
                    .all(|(k, p)| ys.get(k).is_some_and(|q| json_approx_eq(p, q)))
        }
        _ => a == b,
    }
}

/// A vendored PowerWorld fixture under `tests/data/powerworld/`.
#[allow(dead_code)]
pub fn powerworld_vendored(name: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../tests/data/powerworld")
        .join(name)
}

/// A fetched ACTIVSg2000 fixture (`benchmarks/fetch_powerworld.sh`); `None`
/// when the fetch has not run, so tests skip instead of fail.
#[allow(dead_code)]
pub fn activsg2000_fetched(name: &str) -> Option<PathBuf> {
    let p = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../tests/data/large/ACTIVSg2000")
        .join(name);
    p.exists().then_some(p)
}

/// The root of a local directory of PowerWorld case files, which
/// `POWERIO_PWB_CASES` names; `None` when the variable is unset or names no
/// directory, so tests skip instead of fail. Such files run to hundreds of
/// kilobytes each and carry whatever licence their holder obtained them
/// under, so none is vendored and only a machine that already holds a corpus
/// runs the tests that read one.
#[allow(dead_code)]
pub fn pwb_cases_root() -> Option<PathBuf> {
    let root = PathBuf::from(std::env::var_os("POWERIO_PWB_CASES")?);
    root.is_dir().then_some(root)
}

/// A fetched RTS-GMLC fixture (`benchmarks/fetch_powerworld.sh`); `None`
/// when the fetch has not run, so tests skip instead of fail.
#[allow(dead_code)]
pub fn rts_gmlc_fetched(name: &str) -> Option<PathBuf> {
    let p = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../tests/data/large/RTS-GMLC")
        .join(name);
    p.exists().then_some(p)
}

/// Branch circuit identity: the trimmed `LineCircuit` extra, `"1"` (the
/// PowerWorld default) when absent.
#[allow(dead_code)]
/// Branch keys under the one id rule (#330): the retained circuit where the
/// file stated one the writer would not re-derive, else the per-pair ordinal
/// the readers dropped as a positional default. Aligned with `branches`, so
/// zip the result back onto the slice.
pub fn branch_keys(branches: &[powerio_tx::Branch]) -> Vec<(usize, usize, String)> {
    let mut nth: std::collections::BTreeMap<(usize, usize), usize> =
        std::collections::BTreeMap::new();
    branches
        .iter()
        .map(|b| {
            let pair = (b.from.0, b.to.0);
            let n = nth.entry(pair).or_insert(0);
            *n += 1;
            let ckt = b
                .extras
                .get("LineCircuit")
                .and_then(|v| v.as_str())
                .map_or_else(|| n.to_string(), |v| v.trim().to_string());
            (pair.0, pair.1, ckt)
        })
        .collect()
}

/// The path a label resolves to in the gitignored local corpus manifest
/// (`tests/data/local_pwb_corpus.tsv`); `None` when the manifest, the label,
/// or the file is absent, so tests on machine specific corpus files skip
/// instead of fail.
#[allow(dead_code)]
pub fn local_corpus_path(label: &str) -> Option<PathBuf> {
    let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/local_pwb_corpus.tsv");
    let text = std::fs::read_to_string(manifest).ok()?;
    let path = text.lines().find_map(|line| {
        let mut f = line.split('\t');
        (f.next() == Some(label)).then(|| f.next()).flatten()
    })?;
    let p = PathBuf::from(path);
    p.exists().then_some(p)
}