1use gridoxide::PowerFlowOptions;
3use gridoxide::dispatch::{allocate, reactive_keys};
4use gridoxide::network::{build_ybus, stamp_shunts};
5
6fn main() {
7 let path = std::env::args().nth(1).expect("path to a CGMES directory");
8 let dir = std::path::Path::new(&path);
9 let mut paths: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
10 .expect("dir")
11 .filter_map(|e| e.ok().map(|e| e.path()))
12 .filter(|p| p.extension().is_some_and(|x| x == "xml"))
13 .collect();
14 paths.sort();
15 let refs: Vec<&std::path::Path> = paths.iter().map(|p| p.as_path()).collect();
16 let ds = gridoxide::cgmes::load_profiles(&refs).expect("load");
17 let net = gridoxide::cgmes::cgmes_to_network(&ds, 100e6).expect("convert");
18 let vc = &net.voltage_control;
19 println!(
20 "{} regulated bus(es), {} shared, {} regulating machine(s)",
21 vc.regulated_buses,
22 vc.shared_buses,
23 vc.machines.len()
24 );
25
26 let report = gridoxide::run_power_flow(
27 net.buses.clone(),
28 &net.lines,
29 &net.transformers,
30 &net.shunts,
31 gridoxide::TapData::none(),
32 PowerFlowOptions {
33 enforce_q_limits: true,
34 max_iter: 60,
35 max_outer_iter: 60,
36 ..Default::default()
37 },
38 );
39 println!("solve: {:?}", report.stats.status);
40
41 let mut ybus = build_ybus(net.buses.len(), &net.lines, &net.transformers);
42 stamp_shunts(&mut ybus, &net.shunts);
43 let ybus = ybus.finish();
44 let d = allocate(&vc.machines, &vc.nonregulating_q, &report.buses, &ybus);
45
46 let mut by_bus: std::collections::BTreeMap<usize, Vec<&gridoxide::dispatch::MachineDispatch>> =
48 Default::default();
49 for bd in &d {
50 for m in &bd.machines {
51 by_bus.entry(m.controls_bus).or_default().push(m);
52 }
53 }
54 let shared: Vec<_> = by_bus.iter().filter(|(_, v)| v.len() > 1).collect();
55 println!("{} bus(es) with >1 machine\n", shared.len());
56
57 for (bus, ms) in shared.iter().take(4) {
58 let refs: Vec<&gridoxide::types::RegulatingMachine> = vc
59 .machines
60 .iter()
61 .filter(|m| m.controls_bus == **bus)
62 .collect();
63 let (_, basis) = reactive_keys(&refs);
64 let total: f64 = ms.iter().map(|m| m.q).sum();
65 println!(
66 "bus {bus}: {} machines, keys from {:?}, total Q = {:+.6} pu",
67 ms.len(),
68 basis,
69 total
70 );
71 for m in ms.iter() {
72 println!(
73 " {:<22} q={:+.6} share={:.3} range=[{:+.4},{:+.4}]{}",
74 &m.id[..m.id.len().min(22)],
75 m.q,
76 m.share,
77 m.q_min,
78 m.q_max,
79 if m.at_limit { " AT LIMIT" } else { "" }
80 );
81 }
82 println!();
83 }
84
85 let mut published: std::collections::HashMap<String, f64> = Default::default();
88 if let Some(mrids) = ds.by_type.get("Terminal") {
89 let mut eq_of: std::collections::HashMap<&str, &str> = Default::default();
90 for m in mrids {
91 if let Some(e) = ds.entries[m].get_ref("Terminal.ConductingEquipment") {
92 eq_of.insert(m.as_str(), e);
93 }
94 }
95 if let Some(fl) = ds.by_type.get("SvPowerFlow") {
96 for m in fl {
97 let sv = &ds.entries[m];
98 let (Some(t), Some(q)) = (
99 sv.get_ref("SvPowerFlow.Terminal"),
100 sv.get_f64("SvPowerFlow.q"),
101 ) else {
102 continue;
103 };
104 if let Some(e) = eq_of.get(t) {
105 published.insert((*e).to_string(), -q / 100.0);
106 }
107 }
108 }
109 }
110 let mut matched = 0usize;
111 let mut worst_m = 0.0f64;
112 let mut sum_abs = 0.0f64;
113 let mut sum_err = 0.0f64;
114 for bd in &d {
115 for m in &bd.machines {
116 if let Some(&exp) = published.get(&m.id) {
117 matched += 1;
118 worst_m = worst_m.max((m.q - exp).abs());
119 sum_abs += exp.abs();
120 sum_err += (m.q - exp).abs();
121 }
122 }
123 }
124 println!(
125 "published per-machine Q: matched {matched}/{} worst |diff| = {worst_m:.4} pu \
126 mean |diff| / mean |published| = {:.1}%",
127 d.iter().map(|b| b.machines.len()).sum::<usize>(),
128 if sum_abs > 0.0 {
129 100.0 * sum_err / sum_abs
130 } else {
131 0.0
132 }
133 );
134
135 let (mut bus_err, mut bus_abs) = (0.0f64, 0.0f64);
137 let (mut split_err, mut split_abs) = (0.0f64, 0.0f64);
138 let (mut shared_split_err, mut shared_split_abs) = (0.0f64, 0.0f64);
139 let mut worst_bus = (0.0f64, 0usize);
140 for bd in &d {
141 let pub_sum: f64 = bd
142 .machines
143 .iter()
144 .filter_map(|m| published.get(&m.id))
145 .sum();
146 let n_pub = bd
147 .machines
148 .iter()
149 .filter(|m| published.contains_key(&m.id))
150 .count();
151 if n_pub != bd.machines.len() {
152 continue;
153 }
154 bus_err += (bd.attributed - pub_sum).abs();
155 bus_abs += pub_sum.abs();
156 if (bd.attributed - pub_sum).abs() > worst_bus.0 {
157 worst_bus = ((bd.attributed - pub_sum).abs(), bd.bus);
158 }
159 let (keys, _) = reactive_keys(
162 &vc.machines
163 .iter()
164 .filter(|m| m.controls_bus == bd.bus)
165 .collect::<Vec<_>>(),
166 );
167 for (k, m) in bd.machines.iter().enumerate() {
168 let exp = published[&m.id];
169 let ours_on_pub_total = pub_sum * keys[k];
170 split_err += (ours_on_pub_total - exp).abs();
171 split_abs += exp.abs();
172 if bd.machines.len() > 1 {
173 shared_split_err += (ours_on_pub_total - exp).abs();
174 shared_split_abs += exp.abs();
175 }
176 }
177 }
178 println!(
179 " bus totals (the solve): mean rel err {:.1}% worst {:.4} pu at bus {}",
180 100.0 * bus_err / bus_abs.max(1e-12),
181 worst_bus.0,
182 worst_bus.1
183 );
184 println!(
185 " the split (the rule) : mean rel err {:.1}% (shared buses only: {:.1}%)",
186 100.0 * split_err / split_abs.max(1e-12),
187 100.0 * shared_split_err / shared_split_abs.max(1e-12)
188 );
189
190 let mut basis_counts: std::collections::BTreeMap<String, usize> = Default::default();
192 for bd in &d {
193 if bd.machines.len() > 1 {
194 *basis_counts.entry(format!("{:?}", bd.basis)).or_default() += 1;
195 }
196 }
197 println!("shared-bus key basis: {basis_counts:?}");
198 for thr in [1e-9f64, 1e-7, 1e-6, 1e-5] {
199 let n = d.iter().filter(|b| b.unattributed.abs() > thr).count();
200 let t: f64 = d
201 .iter()
202 .filter(|b| b.unattributed.abs() > thr)
203 .map(|b| b.unattributed.abs())
204 .sum();
205 println!(" unattributed > {thr:.0e}: {n} bus(es), {t:.4} pu");
206 }
207 for bd in d
209 .iter()
210 .filter(|b| b.machines.len() > 1 && format!("{:?}", b.basis) != "Capability")
211 {
212 println!(" FALLBACK bus {} -> {:?}", bd.bus, bd.basis);
213 for m in &bd.machines {
214 println!(
215 " {} range=[{:+.6},{:+.6}] width={:.6}",
216 &m.id[..12],
217 m.q_min,
218 m.q_max,
219 m.q_max - m.q_min
220 );
221 }
222 }
223
224 let (_, qall) = gridoxide::network::power_injections(&report.buses, &ybus);
225 let mut worst: Vec<(f64, usize, usize, usize, f64, f64)> = Vec::new();
226 let mut violations = 0;
227 for (bus, ms) in &by_bus {
228 let total: f64 = ms.iter().map(|m| m.q).sum();
229 let expected = qall[*bus] - vc.nonregulating_q[*bus];
230 let pinned = ms.iter().filter(|m| m.at_limit).count();
231 worst.push((
232 (total - expected).abs(),
233 *bus,
234 ms.len(),
235 pinned,
236 total,
237 expected,
238 ));
239 for m in ms.iter() {
240 if m.q > m.q_max + 1e-9 || m.q < m.q_min - 1e-9 {
241 violations += 1;
242 }
243 }
244 }
245 let n_bad = worst.iter().filter(|w| w.0 > 1e-9).count();
246 let total_bad: f64 = worst.iter().map(|w| w.0).sum();
247 let with_load = by_bus
249 .keys()
250 .filter(|b| vc.nonregulating_q[**b].abs() > 1e-9)
251 .count();
252 println!(
253 "regulated buses: {} with co-located reactive injection: {with_load} \
254 unattributable: {n_bad} total unattributed = {total_bad:.4} pu",
255 by_bus.len()
256 );
257 worst.sort_by(|a, b| b.0.total_cmp(&a.0));
258 println!("limit violations = {violations}");
259 println!("worst mismatches (err, bus, machines, pinned, sum, expected):");
260 for w in worst.iter().take(6) {
261 let bt = report.buses[w.1].bus_type;
262 println!(
263 " {:.3e} bus {:4} n={} pinned={} sum={:+.6} expected={:+.6} type={:?} qlim=[{:+.4},{:+.4}]",
264 w.0, w.1, w.2, w.3, w.4, w.5, bt, report.buses[w.1].q_min, report.buses[w.1].q_max
265 );
266 }
267}