gridoxide 0.1.0

AC power flow and state estimation solver with a sparse Newton-Raphson core
Documentation
mod cgmes_common;

use gridoxide::cgmes::{
    cgmes_to_buses_and_branches, cgmes_topological_node_bus_index, load_profiles,
};
use gridoxide::network::{build_ybus, stamp_shunts};
use gridoxide::run_power_flow_analysis_from_ybus;

/// ENTSO-E's MicroGrid conformance test configuration, Belgian area
/// ("MicroGrid-BE-MAS" = Model As Supplied), referenced via the
/// `CGMES-Test-Configurations` git submodule rather than copied into this
/// repo — see tests/data/cgmes/README.md for why (its CC BY-NC-SA 4.0
/// license doesn't fit a copy inside this Apache-2.0 repo's own tree).
///
/// Tolerance is looser than the PGM fixtures' (1e-2, i.e. up to a few
/// percent, not 1e-5): this is a genuinely boundary-truncated sub-model
/// (only BE's own area is loaded, not the full BE+NL merged CGMES model),
/// solved with fixed-P/Q `EquivalentInjection`s standing in for the rest of
/// the interconnected system. Cross-checked against pypowsybl's own
/// independent CGMES import + AC load flow on the same bundled case: it
/// *also* deviates from this fixture's published SV values by a comparable
/// few percent (e.g. ~1.3% at the 10.5 kV bus), confirming that gap is
/// inherent to boundary-truncated solving with simple fixed-injection
/// equivalents, not a correctness bug in either tool. A further few percent
/// here traces to boundary `ACLineSegment`s connecting a 400 kV boundary bus
/// to this area's own 380 kV bus — CGMES's own docs explicitly allow such
/// nominal-voltage mismatches at boundary points, but gridoxide's `Line`
/// (unlike `Transformer`) has no tap ratio to absorb one.
#[test]
fn test_cgmes_microgrid_be_mas() {
    let dir = cgmes_common::fixture_dir();
    if !dir.exists() {
        eprintln!(
            "skipping: {} not found — run `git submodule update --init tests/data/CGMES-Test-Configurations`",
            dir.display()
        );
        return;
    }

    // The boundary EQ (EQ_BD) is needed too: some TopologicalNode.BaseVoltage
    // references (e.g. the 380 kV boundary bus) resolve only to objects
    // defined there, not in BE-MAS's own EQ file — confirmed by actually
    // hitting an UnresolvedReference without it, not assumed up front.
    let eq_bd = dir.join("../MicroGrid-BD-MAS/20171002T0930Z_ENTSO-E_EQ_BD_2.xml");
    let eq = dir.join("20210325T1530Z_1D_BE_EQ_001.xml");
    let ssh = dir.join("20210325T1530Z_1D_BE_SSH_001.xml");
    let tp = dir.join("20210325T1530Z_1D_BE_TP_001.xml");
    let sv = dir.join("20210325T1530Z_1D_BE_SV_001.xml");
    let ds =
        load_profiles(&[&eq_bd, &eq, &ssh, &tp, &sv]).expect("failed to decode CGMES profiles");

    let expected = cgmes_common::expected_voltages(&ds);
    assert_eq!(expected.len(), 7, "fixture should have 7 SvVoltage entries");

    let s_base_va = 100e6;
    let (buses, lines, transformers, shunts) =
        cgmes_to_buses_and_branches(&ds, s_base_va).expect("conversion failed");
    // 7 physical TopologicalNode buses, plus one synthesized bus per
    // 3-winding star point and per boundary ConnectivityNode.
    assert!(
        buses.len() > 7,
        "expected at least the 7 physical TopologicalNode buses, got {}",
        buses.len()
    );

    let n = buses.len();
    let mut ybus = build_ybus(n, &lines, &transformers);
    stamp_shunts(&mut ybus, &shunts);
    let result = run_power_flow_analysis_from_ybus(buses, ybus).buses;

    let bus_index = cgmes_topological_node_bus_index(&ds).expect("bus index lookup failed");
    cgmes_common::assert_matches_sv(&result, &bus_index, &expected, 4e-2);
}