Skip to main content

tap_deadband_probe/
tap_deadband_probe.rs

1//! Is a fixture's *published* solution inside its own tap-control deadbands?
2//!
3//! The question that decides whether a tap loop moving a tap on a conformity
4//! fixture is a defect or a finding. Compares, at the positions the SSH names:
5//! the controlled bus's voltage as gridoxide solves it, and as the fixture's
6//! own SV profile publishes it, each against the control's target and deadband.
7use std::path::{Path, PathBuf};
8
9fn main() {
10    let root =
11        Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/CGMES-Test-Configurations/v3.0");
12    for name in std::env::args().skip(1) {
13        let dir = root.join(&name);
14        let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
15            .unwrap()
16            .filter_map(|e| e.ok().map(|e| e.path()))
17            .filter(|p| p.extension().is_some_and(|e| e == "xml"))
18            .collect();
19        paths.sort();
20        let refs: Vec<&Path> = paths.iter().map(|p| p.as_path()).collect();
21        let ds = gridoxide::cgmes::load_profiles(&refs).expect("load");
22        let mut net = gridoxide::cgmes::cgmes_to_network(&ds, 100e6).expect("convert");
23        // FullGrid is an HVDC model; without resolving its converters into
24        // AC-side injections the AC solve has nothing driving those buses.
25        match gridoxide::cgmes::cgmes_resolve_dc_converters(&ds, &mut net.buses, 100e6) {
26            Ok(_) => println!("    dc: resolved"),
27            Err(e) => println!("    dc: {e}"),
28        }
29        let index = gridoxide::cgmes::cgmes_topological_node_bus_index(&ds).expect("index");
30
31        // The published SvVoltage per bus, in per unit.
32        let mut published = vec![f64::NAN; net.buses.len()];
33        for mrid in &ds.by_type["SvVoltage"] {
34            let sv = &ds.entries[mrid];
35            let (Some(tn), Some(v)) = (
36                sv.get_ref("SvVoltage.TopologicalNode"),
37                sv.get_f64("SvVoltage.v"),
38            ) else {
39                continue;
40            };
41            if let Some(&b) = index.get(tn) {
42                published[b] = v * 1e3 / net.buses[b].u_rated;
43            }
44        }
45
46        // gridoxide's own solve at the positions the document names.
47        let report = gridoxide::run_power_flow(
48            net.buses.clone(),
49            &net.lines,
50            &net.transformers,
51            &net.shunts,
52            gridoxide::TapData::none(),
53            gridoxide::PowerFlowOptions {
54                tol: 1e-8,
55                max_iter: 30,
56                ..Default::default()
57            },
58        );
59        // And the same, after letting the tap loops run.
60        let after = gridoxide::run_power_flow(
61            net.buses.clone(),
62            &net.lines,
63            &net.transformers,
64            &net.shunts,
65            gridoxide::TapData {
66                changers: &net.tap_changers,
67                regulation: &net.regulation,
68            },
69            gridoxide::PowerFlowOptions {
70                control_taps: true,
71                max_outer_iter: 60,
72                tol: 1e-8,
73                max_iter: 30,
74                ..Default::default()
75            },
76        );
77        println!(
78            "=== {name}: base solve {:?}, controlled solve {:?}",
79            report.stats.status, after.stats.status
80        );
81        println!(
82            "{:>4} {:>9} {:>9} {:>9} {:>9}  {:>9} {:>9}",
83            "bus", "target", "half-db", "gridoxide", "|err|", "published", "|err|"
84        );
85        for r in &net.regulation {
86            if r.mode != gridoxide::outerloop::RegulationMode::Voltage {
87                continue;
88            }
89            let mine = report.buses[r.controlled_bus].voltage_mag;
90            let theirs = published[r.controlled_bus];
91            println!(
92                "{:>4} {:>9.5} {:>9.5} {:>9.5} {:>9.5}{} {:>9.5} {:>9.5}{}",
93                r.controlled_bus,
94                r.target,
95                r.deadband / 2.0,
96                mine,
97                (mine - r.target).abs(),
98                if (mine - r.target).abs() > r.deadband / 2.0 {
99                    " OUT"
100                } else {
101                    "  in"
102                },
103                theirs,
104                (theirs - r.target).abs(),
105                if (theirs - r.target).abs() > r.deadband / 2.0 {
106                    " OUT"
107                } else {
108                    "  in"
109                },
110            );
111            let controlled = after.buses[r.controlled_bus].voltage_mag;
112            println!(
113                "     -> after tap control: {controlled:.5}  |err| {:.5}{}",
114                (controlled - r.target).abs(),
115                if (controlled - r.target).abs() > r.deadband / 2.0 {
116                    " OUT"
117                } else {
118                    "  in"
119                },
120            );
121        }
122    }
123}