1use std::collections::HashMap;
19use std::sync::Arc;
20
21use arrow::array::{Array, ArrayRef, Float64Array, Int64Array, StructArray, UInt8Array};
22use arrow::datatypes::{Field, Schema};
23use arrow::error::ArrowError;
24use arrow::ffi::{FFI_ArrowArray, FFI_ArrowSchema};
25use arrow::record_batch::RecordBatch;
26#[cfg(feature = "matrix")]
27use powerio::IndexedNetwork;
28use powerio::{BalancedNetwork, BusId, IndexCore, NormalizedSolverTables, SolverArcTerminal};
29
30pub const PIO_ARROW_TABLE_BUS: i32 = 0;
33pub const PIO_ARROW_TABLE_BRANCH: i32 = 1;
34pub const PIO_ARROW_TABLE_GEN: i32 = 2;
35pub const PIO_ARROW_TABLE_LOAD: i32 = 3;
36pub const PIO_ARROW_TABLE_SHUNT: i32 = 4;
37pub const PIO_ARROW_TABLE_SWITCH: i32 = 5;
38pub const PIO_ARROW_TABLE_SOLVER_BUS: i32 = 6;
39pub const PIO_ARROW_TABLE_SOLVER_LOAD: i32 = 7;
40pub const PIO_ARROW_TABLE_SOLVER_SHUNT: i32 = 8;
41pub const PIO_ARROW_TABLE_SOLVER_BRANCH: i32 = 9;
42pub const PIO_ARROW_TABLE_SOLVER_SWITCH: i32 = 10;
43pub const PIO_ARROW_TABLE_SOLVER_ARC: i32 = 11;
44pub const PIO_ARROW_TABLE_SOLVER_GEN: i32 = 12;
45pub const PIO_ARROW_TABLE_SOLVER_STORAGE: i32 = 13;
46pub const PIO_ARROW_TABLE_SOLVER_HVDC: i32 = 14;
47pub const PIO_ARROW_TABLE_YBUS: i32 = 15;
48pub const PIO_ARROW_TABLE_INCIDENCE: i32 = 16;
49pub const PIO_ARROW_TABLE_BPRIME: i32 = 17;
50pub const PIO_ARROW_TABLE_BDOUBLEPRIME: i32 = 18;
51pub const PIO_ARROW_TABLE_MATRIX_BUS: i32 = 19;
52pub const PIO_ARROW_TABLE_MATRIX_BRANCH: i32 = 20;
53
54const _: () = assert!(
62 PIO_ARROW_TABLE_BUS == 0
63 && PIO_ARROW_TABLE_BRANCH == 1
64 && PIO_ARROW_TABLE_GEN == 2
65 && PIO_ARROW_TABLE_LOAD == 3
66 && PIO_ARROW_TABLE_SHUNT == 4
67 && PIO_ARROW_TABLE_SWITCH == 5
68 && PIO_ARROW_TABLE_SOLVER_BUS == 6
69 && PIO_ARROW_TABLE_SOLVER_LOAD == 7
70 && PIO_ARROW_TABLE_SOLVER_SHUNT == 8
71 && PIO_ARROW_TABLE_SOLVER_BRANCH == 9
72 && PIO_ARROW_TABLE_SOLVER_SWITCH == 10
73 && PIO_ARROW_TABLE_SOLVER_ARC == 11
74 && PIO_ARROW_TABLE_SOLVER_GEN == 12
75 && PIO_ARROW_TABLE_SOLVER_STORAGE == 13
76 && PIO_ARROW_TABLE_SOLVER_HVDC == 14
77 && PIO_ARROW_TABLE_YBUS == 15
78 && PIO_ARROW_TABLE_INCIDENCE == 16
79 && PIO_ARROW_TABLE_BPRIME == 17
80 && PIO_ARROW_TABLE_BDOUBLEPRIME == 18
81 && PIO_ARROW_TABLE_MATRIX_BUS == 19
82 && PIO_ARROW_TABLE_MATRIX_BRANCH == 20
83);
84
85pub fn export(
89 net: &BalancedNetwork,
90 core: &IndexCore,
91 table: i32,
92) -> Result<(FFI_ArrowArray, FFI_ArrowSchema), String> {
93 let rb = match table {
94 PIO_ARROW_TABLE_BUS => bus_batch(net).map_err(|e| e.to_string())?,
95 PIO_ARROW_TABLE_BRANCH => branch_batch(net).map_err(|e| e.to_string())?,
96 PIO_ARROW_TABLE_GEN => gen_batch(net).map_err(|e| e.to_string())?,
97 PIO_ARROW_TABLE_LOAD => load_batch(net).map_err(|e| e.to_string())?,
98 PIO_ARROW_TABLE_SHUNT => shunt_batch(net).map_err(|e| e.to_string())?,
99 PIO_ARROW_TABLE_SWITCH => switch_batch(net).map_err(|e| e.to_string())?,
100 PIO_ARROW_TABLE_SOLVER_BUS => {
101 solver_bus_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
102 }
103 PIO_ARROW_TABLE_SOLVER_LOAD => {
104 solver_load_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
105 }
106 PIO_ARROW_TABLE_SOLVER_SHUNT => {
107 solver_shunt_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
108 }
109 PIO_ARROW_TABLE_SOLVER_BRANCH => {
110 solver_branch_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
111 }
112 PIO_ARROW_TABLE_SOLVER_SWITCH => {
113 solver_switch_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
114 }
115 PIO_ARROW_TABLE_SOLVER_ARC => {
116 solver_arc_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
117 }
118 PIO_ARROW_TABLE_SOLVER_GEN => {
119 solver_gen_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
120 }
121 PIO_ARROW_TABLE_SOLVER_STORAGE => {
122 solver_storage_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
123 }
124 PIO_ARROW_TABLE_SOLVER_HVDC => {
125 solver_hvdc_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
126 }
127 PIO_ARROW_TABLE_YBUS => matrix_ybus_batch(net, core)?,
128 PIO_ARROW_TABLE_INCIDENCE => matrix_incidence_batch(net, core)?,
129 PIO_ARROW_TABLE_BPRIME => matrix_bprime_batch(net, core)?,
130 PIO_ARROW_TABLE_BDOUBLEPRIME => matrix_bdoubleprime_batch(net, core)?,
131 PIO_ARROW_TABLE_MATRIX_BUS => matrix_bus_batch(net, core)?,
132 PIO_ARROW_TABLE_MATRIX_BRANCH => matrix_branch_batch(net, core)?,
133 other => return Err(format!("unknown Arrow table id {other}")),
134 };
135
136 let schema = FFI_ArrowSchema::try_from(rb.schema().as_ref()).map_err(|e| e.to_string())?;
140 let data = StructArray::from(rb).into_data();
141 Ok((FFI_ArrowArray::new(&data), schema))
142}
143
144pub fn catalog_json() -> String {
146 serde_json::to_string(&catalog_value()).expect("Arrow catalog JSON is serializable")
147}
148
149fn catalog_value() -> serde_json::Value {
150 let matrix_available = cfg!(feature = "matrix");
151 let table_spec = |id: i32,
152 name: &str,
153 format: &str,
154 feature_requirements: &[&str],
155 available: bool,
156 row_axis: Option<&str>,
157 col_axis: Option<&str>,
158 units: serde_json::Value,
159 columns: &[(&str, &str)]| {
160 serde_json::json!({
161 "id": id,
162 "name": name,
163 "format": format,
164 "feature_requirements": feature_requirements,
165 "available": available,
166 "row_axis": row_axis,
167 "col_axis": col_axis,
168 "units": units,
169 "columns": columns.iter().map(|(name, dtype)| {
170 serde_json::json!({"name": name, "type": dtype, "nullable": false})
171 }).collect::<Vec<_>>(),
172 })
173 };
174 serde_json::json!({
175 powerio::version::VERSION_KEY: powerio::VERSION,
176 "producer": "powerio-capi",
177 "tables": [
178 table_spec(PIO_ARROW_TABLE_BUS, "bus", "record_batch", &["arrow"], true, None, None, units_source(), &[
179 ("id", "int64"), ("kind", "int64"), ("vm", "float64"), ("va", "float64"),
180 ("base_kv", "float64"), ("vmax", "float64"), ("vmin", "float64"),
181 ("area", "int64"), ("zone", "int64"),
182 ]),
183 table_spec(PIO_ARROW_TABLE_BRANCH, "branch", "record_batch", &["arrow"], true, None, None, units_source(), &[
184 ("from", "int64"), ("to", "int64"), ("r", "float64"), ("x", "float64"),
185 ("b", "float64"), ("rate_a", "float64"), ("rate_b", "float64"),
186 ("rate_c", "float64"), ("tap", "float64"), ("shift", "float64"),
187 ("in_service", "uint8"), ("angmin", "float64"), ("angmax", "float64"),
188 ("g_fr", "float64"), ("b_fr", "float64"), ("g_to", "float64"),
189 ("b_to", "float64"), ("c_rating_a", "float64"), ("c_rating_b", "float64"),
190 ("c_rating_c", "float64"), ("pf", "float64"), ("qf", "float64"),
191 ("pt", "float64"), ("qt", "float64"),
192 ]),
193 table_spec(PIO_ARROW_TABLE_GEN, "gen", "record_batch", &["arrow"], true, None, None, units_source(), &[
194 ("bus", "int64"), ("pg", "float64"), ("qg", "float64"),
195 ("pmax", "float64"), ("pmin", "float64"), ("qmax", "float64"),
196 ("qmin", "float64"), ("vg", "float64"), ("mbase", "float64"),
197 ("in_service", "uint8"),
198 ]),
199 table_spec(PIO_ARROW_TABLE_LOAD, "load", "record_batch", &["arrow"], true, None, None, units_source(), &[
200 ("bus", "int64"), ("p", "float64"), ("q", "float64"), ("in_service", "uint8"),
201 ]),
202 table_spec(PIO_ARROW_TABLE_SHUNT, "shunt", "record_batch", &["arrow"], true, None, None, units_source(), &[
203 ("bus", "int64"), ("g", "float64"), ("b", "float64"), ("in_service", "uint8"),
204 ]),
205 table_spec(PIO_ARROW_TABLE_SWITCH, "switch", "record_batch", &["arrow"], true, None, None, units_source(), &[
206 ("from", "int64"), ("to", "int64"), ("closed", "uint8"),
207 ("thermal_rating", "float64"), ("current_rating", "float64"),
208 ("pf", "float64"), ("qf", "float64"), ("pt", "float64"), ("qt", "float64"),
209 ]),
210 table_spec(PIO_ARROW_TABLE_SOLVER_BUS, "solver_bus", "record_batch", &["arrow"], true, Some("solver_bus"), None, units_solver(), &[
211 ("index", "int64"), ("bus_id", "int64"), ("source_row", "int64"),
212 ("kind", "int64"), ("vm", "float64"), ("va", "float64"),
213 ("base_kv", "float64"), ("vmax", "float64"), ("vmin", "float64"),
214 ("pd", "float64"), ("qd", "float64"), ("gs", "float64"),
215 ("bs", "float64"), ("component_label", "int64"), ("is_reference", "uint8"),
216 ]),
217 table_spec(PIO_ARROW_TABLE_SOLVER_LOAD, "solver_load", "record_batch", &["arrow"], true, Some("solver_load"), None, units_solver(), &[
218 ("index", "int64"), ("source_row", "int64"), ("bus_index", "int64"),
219 ("p", "float64"), ("q", "float64"),
220 ]),
221 table_spec(PIO_ARROW_TABLE_SOLVER_SHUNT, "solver_shunt", "record_batch", &["arrow"], true, Some("solver_shunt"), None, units_solver(), &[
222 ("index", "int64"), ("source_row", "int64"), ("bus_index", "int64"),
223 ("g", "float64"), ("b", "float64"),
224 ]),
225 table_spec(PIO_ARROW_TABLE_SOLVER_BRANCH, "solver_branch", "record_batch", &["arrow"], true, Some("solver_branch"), None, units_solver(), &[
226 ("index", "int64"), ("source_row", "int64"), ("from_bus_index", "int64"),
227 ("to_bus_index", "int64"), ("r", "float64"), ("x", "float64"),
228 ("b", "float64"), ("g_fr", "float64"), ("b_fr", "float64"),
229 ("g_to", "float64"), ("b_to", "float64"), ("rate_a", "float64"),
230 ("rate_b", "float64"), ("rate_c", "float64"), ("tap", "float64"),
231 ("shift", "float64"), ("angmin", "float64"), ("angmax", "float64"),
232 ]),
233 table_spec(PIO_ARROW_TABLE_SOLVER_SWITCH, "solver_switch", "record_batch", &["arrow"], true, Some("solver_switch"), None, units_solver(), &[
234 ("index", "int64"), ("source_row", "int64"), ("from_bus_index", "int64"),
235 ("to_bus_index", "int64"), ("closed", "uint8"), ("thermal_rating", "float64"),
236 ("current_rating", "float64"), ("pf", "float64"), ("qf", "float64"),
237 ("pt", "float64"), ("qt", "float64"),
238 ]),
239 table_spec(PIO_ARROW_TABLE_SOLVER_ARC, "solver_arc", "record_batch", &["arrow"], true, Some("solver_arc"), None, units_solver(), &[
240 ("index", "int64"), ("branch_index", "int64"), ("terminal", "int64"),
241 ("from_bus_index", "int64"), ("to_bus_index", "int64"), ("tap", "float64"),
242 ("shift", "float64"), ("g_shunt", "float64"), ("b_shunt", "float64"),
243 ("rate_a", "float64"),
244 ]),
245 table_spec(PIO_ARROW_TABLE_SOLVER_GEN, "solver_gen", "record_batch", &["arrow"], true, Some("solver_gen"), None, units_solver(), &[
246 ("index", "int64"), ("source_row", "int64"), ("bus_index", "int64"),
247 ("pg", "float64"), ("qg", "float64"), ("pmax", "float64"),
248 ("pmin", "float64"), ("qmax", "float64"), ("qmin", "float64"),
249 ("vg", "float64"), ("mbase", "float64"), ("regulated_bus_index", "int64"),
250 ]),
251 table_spec(PIO_ARROW_TABLE_SOLVER_STORAGE, "solver_storage", "record_batch", &["arrow"], true, Some("solver_storage"), None, units_solver(), &[
252 ("index", "int64"), ("source_row", "int64"), ("bus_index", "int64"),
253 ("ps", "float64"), ("qs", "float64"), ("energy", "float64"),
254 ("energy_rating", "float64"), ("charge_rating", "float64"),
255 ("discharge_rating", "float64"), ("thermal_rating", "float64"),
256 ("qmin", "float64"), ("qmax", "float64"), ("r", "float64"),
257 ("x", "float64"), ("p_loss", "float64"), ("q_loss", "float64"),
258 ]),
259 table_spec(PIO_ARROW_TABLE_SOLVER_HVDC, "solver_hvdc", "record_batch", &["arrow"], true, Some("solver_hvdc"), None, units_solver(), &[
260 ("index", "int64"), ("source_row", "int64"), ("from_bus_index", "int64"),
261 ("to_bus_index", "int64"), ("pf", "float64"), ("pt", "float64"),
262 ("qf", "float64"), ("qt", "float64"), ("vf", "float64"), ("vt", "float64"),
263 ("pmin", "float64"), ("pmax", "float64"), ("qminf", "float64"),
264 ("qmaxf", "float64"), ("qmint", "float64"), ("qmaxt", "float64"),
265 ("loss0", "float64"), ("loss1", "float64"),
266 ]),
267 table_spec(PIO_ARROW_TABLE_YBUS, "ybus", "coo", &["arrow", "matrix"], matrix_available, Some("matrix_bus"), Some("matrix_bus"), units_matrix(), &[
268 ("row_index", "int64"), ("col_index", "int64"), ("g", "float64"), ("b", "float64"),
269 ]),
270 table_spec(PIO_ARROW_TABLE_INCIDENCE, "incidence", "coo", &["arrow", "matrix"], matrix_available, Some("matrix_bus"), Some("matrix_branch"), units_matrix(), &[
271 ("row_index", "int64"), ("col_index", "int64"), ("value", "float64"),
272 ]),
273 table_spec(PIO_ARROW_TABLE_BPRIME, "bprime", "coo", &["arrow", "matrix"], matrix_available, Some("matrix_bus"), Some("matrix_bus"), units_matrix(), &[
274 ("row_index", "int64"), ("col_index", "int64"), ("value", "float64"),
275 ]),
276 table_spec(PIO_ARROW_TABLE_BDOUBLEPRIME, "bdoubleprime", "coo", &["arrow", "matrix"], matrix_available, Some("matrix_bus"), Some("matrix_bus"), units_matrix(), &[
277 ("row_index", "int64"), ("col_index", "int64"), ("value", "float64"),
278 ]),
279 table_spec(PIO_ARROW_TABLE_MATRIX_BUS, "matrix_bus", "axis_map", &["arrow", "matrix"], matrix_available, Some("matrix_bus"), None, units_axis(), &[
280 ("index", "int64"), ("bus_id", "int64"), ("source_row", "int64"),
281 ("is_reference", "uint8"), ("component", "int64"),
282 ]),
283 table_spec(PIO_ARROW_TABLE_MATRIX_BRANCH, "matrix_branch", "axis_map", &["arrow", "matrix"], matrix_available, Some("matrix_branch"), None, units_axis(), &[
284 ("index", "int64"), ("source_row", "int64"), ("from_bus_id", "int64"),
285 ("to_bus_id", "int64"),
286 ]),
287 ]
288 })
289}
290
291fn units_source() -> serde_json::Value {
292 serde_json::json!({
293 "power": "source",
294 "voltage": "source",
295 "angle": "degree",
296 "index_base": "external_bus_id"
297 })
298}
299
300fn units_solver() -> serde_json::Value {
301 serde_json::json!({
302 "power": "per_unit",
303 "voltage": "per_unit",
304 "angle": "radian",
305 "impedance": "per_unit",
306 "admittance": "per_unit",
307 "index_base": "zero"
308 })
309}
310
311fn units_matrix() -> serde_json::Value {
312 serde_json::json!({
313 "matrix_index_base": "zero",
314 "value": "per_unit"
315 })
316}
317
318fn units_axis() -> serde_json::Value {
319 serde_json::json!({
320 "index_base": "zero",
321 "source_row_base": "zero",
322 "missing_source_row": -1
323 })
324}
325
326fn solver_tables(net: &BalancedNetwork) -> Result<NormalizedSolverTables, String> {
327 net.to_normalized_solver_tables().map_err(|e| e.to_string())
328}
329
330fn bus_batch(net: &BalancedNetwork) -> Result<RecordBatch, ArrowError> {
331 let b = &net.buses;
332 batch(vec![
333 ("id", i64s(b.iter().map(|x| ext(x.id)).collect())),
334 (
335 "kind",
336 i64s(b.iter().map(|x| i64::from(x.kind as u8)).collect()),
337 ),
338 ("vm", f64s(b.iter().map(|x| x.vm).collect())),
339 ("va", f64s(b.iter().map(|x| x.va).collect())),
340 ("base_kv", f64s(b.iter().map(|x| x.base_kv).collect())),
341 ("vmax", f64s(b.iter().map(|x| x.vmax).collect())),
342 ("vmin", f64s(b.iter().map(|x| x.vmin).collect())),
343 ("area", i64s(b.iter().map(|x| usz(x.area)).collect())),
344 ("zone", i64s(b.iter().map(|x| usz(x.zone)).collect())),
345 ])
346}
347
348fn branch_batch(net: &BalancedNetwork) -> Result<RecordBatch, ArrowError> {
349 let br = &net.branches;
350 batch(vec![
351 ("from", i64s(br.iter().map(|x| ext(x.from)).collect())),
352 ("to", i64s(br.iter().map(|x| ext(x.to)).collect())),
353 ("r", f64s(br.iter().map(|x| x.r).collect())),
354 ("x", f64s(br.iter().map(|x| x.x).collect())),
355 ("b", f64s(br.iter().map(|x| x.total_charging_b()).collect())),
356 ("rate_a", f64s(br.iter().map(|x| x.rate_a).collect())),
357 ("rate_b", f64s(br.iter().map(|x| x.rate_b).collect())),
358 ("rate_c", f64s(br.iter().map(|x| x.rate_c).collect())),
359 ("tap", f64s(br.iter().map(|x| x.tap).collect())),
360 ("shift", f64s(br.iter().map(|x| x.shift).collect())),
361 (
362 "in_service",
363 u8s(br.iter().map(|x| u8::from(x.in_service)).collect()),
364 ),
365 ("angmin", f64s(br.iter().map(|x| x.angmin).collect())),
366 ("angmax", f64s(br.iter().map(|x| x.angmax).collect())),
367 (
368 "g_fr",
369 f64s(br.iter().map(|x| x.terminal_charging().g_fr).collect()),
370 ),
371 (
372 "b_fr",
373 f64s(br.iter().map(|x| x.terminal_charging().b_fr).collect()),
374 ),
375 (
376 "g_to",
377 f64s(br.iter().map(|x| x.terminal_charging().g_to).collect()),
378 ),
379 (
380 "b_to",
381 f64s(br.iter().map(|x| x.terminal_charging().b_to).collect()),
382 ),
383 (
384 "c_rating_a",
385 f64s(
386 br.iter()
387 .map(|x| x.current_ratings.map_or(0.0, |r| r.c_rating_a))
388 .collect(),
389 ),
390 ),
391 (
392 "c_rating_b",
393 f64s(
394 br.iter()
395 .map(|x| x.current_ratings.map_or(0.0, |r| r.c_rating_b))
396 .collect(),
397 ),
398 ),
399 (
400 "c_rating_c",
401 f64s(
402 br.iter()
403 .map(|x| x.current_ratings.map_or(0.0, |r| r.c_rating_c))
404 .collect(),
405 ),
406 ),
407 (
408 "pf",
409 f64s(
410 br.iter()
411 .map(|x| x.solution.map_or(0.0, |s| s.pf))
412 .collect(),
413 ),
414 ),
415 (
416 "qf",
417 f64s(
418 br.iter()
419 .map(|x| x.solution.map_or(0.0, |s| s.qf))
420 .collect(),
421 ),
422 ),
423 (
424 "pt",
425 f64s(
426 br.iter()
427 .map(|x| x.solution.map_or(0.0, |s| s.pt))
428 .collect(),
429 ),
430 ),
431 (
432 "qt",
433 f64s(
434 br.iter()
435 .map(|x| x.solution.map_or(0.0, |s| s.qt))
436 .collect(),
437 ),
438 ),
439 ])
440}
441
442fn gen_batch(net: &BalancedNetwork) -> Result<RecordBatch, ArrowError> {
443 let g = &net.generators;
444 batch(vec![
445 ("bus", i64s(g.iter().map(|x| ext(x.bus)).collect())),
446 ("pg", f64s(g.iter().map(|x| x.pg).collect())),
447 ("qg", f64s(g.iter().map(|x| x.qg).collect())),
448 ("pmax", f64s(g.iter().map(|x| x.pmax).collect())),
449 ("pmin", f64s(g.iter().map(|x| x.pmin).collect())),
450 ("qmax", f64s(g.iter().map(|x| x.qmax).collect())),
451 ("qmin", f64s(g.iter().map(|x| x.qmin).collect())),
452 ("vg", f64s(g.iter().map(|x| x.vg).collect())),
453 ("mbase", f64s(g.iter().map(|x| x.mbase).collect())),
454 (
455 "in_service",
456 u8s(g.iter().map(|x| u8::from(x.in_service)).collect()),
457 ),
458 ])
459}
460
461fn load_batch(net: &BalancedNetwork) -> Result<RecordBatch, ArrowError> {
462 let l = &net.loads;
463 batch(vec![
464 ("bus", i64s(l.iter().map(|x| ext(x.bus)).collect())),
465 ("p", f64s(l.iter().map(|x| x.p).collect())),
466 ("q", f64s(l.iter().map(|x| x.q).collect())),
467 (
468 "in_service",
469 u8s(l.iter().map(|x| u8::from(x.in_service)).collect()),
470 ),
471 ])
472}
473
474fn shunt_batch(net: &BalancedNetwork) -> Result<RecordBatch, ArrowError> {
475 let s = &net.shunts;
476 batch(vec![
477 ("bus", i64s(s.iter().map(|x| ext(x.bus)).collect())),
478 ("g", f64s(s.iter().map(|x| x.g).collect())),
479 ("b", f64s(s.iter().map(|x| x.b).collect())),
480 (
481 "in_service",
482 u8s(s.iter().map(|x| u8::from(x.in_service)).collect()),
483 ),
484 ])
485}
486
487fn switch_batch(net: &BalancedNetwork) -> Result<RecordBatch, ArrowError> {
488 let s = &net.switches;
489 batch(vec![
490 ("from", i64s(s.iter().map(|x| ext(x.from)).collect())),
491 ("to", i64s(s.iter().map(|x| ext(x.to)).collect())),
492 (
493 "closed",
494 u8s(s.iter().map(|x| u8::from(x.closed)).collect()),
495 ),
496 (
497 "thermal_rating",
498 f64s(s.iter().map(|x| x.thermal_rating.unwrap_or(0.0)).collect()),
499 ),
500 (
501 "current_rating",
502 f64s(s.iter().map(|x| x.current_rating.unwrap_or(0.0)).collect()),
503 ),
504 ("pf", f64s(s.iter().map(|x| x.pf.unwrap_or(0.0)).collect())),
505 ("qf", f64s(s.iter().map(|x| x.qf.unwrap_or(0.0)).collect())),
506 ("pt", f64s(s.iter().map(|x| x.pt.unwrap_or(0.0)).collect())),
507 ("qt", f64s(s.iter().map(|x| x.qt.unwrap_or(0.0)).collect())),
508 ])
509}
510
511fn solver_bus_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
512 batch(vec![
513 (
514 "index",
515 i64s(t.buses.iter().map(|x| usz(x.index)).collect()),
516 ),
517 (
518 "bus_id",
519 i64s(t.buses.iter().map(|x| ext(x.bus_id)).collect()),
520 ),
521 (
522 "source_row",
523 i64s(t.buses.iter().map(|x| opt_usz(x.source_row)).collect()),
524 ),
525 (
526 "kind",
527 i64s(t.buses.iter().map(|x| i64::from(x.kind as u8)).collect()),
528 ),
529 ("vm", f64s(t.buses.iter().map(|x| x.vm).collect())),
530 ("va", f64s(t.buses.iter().map(|x| x.va).collect())),
531 ("base_kv", f64s(t.buses.iter().map(|x| x.base_kv).collect())),
532 ("vmax", f64s(t.buses.iter().map(|x| x.vmax).collect())),
533 ("vmin", f64s(t.buses.iter().map(|x| x.vmin).collect())),
534 ("pd", f64s(t.buses.iter().map(|x| x.pd).collect())),
535 ("qd", f64s(t.buses.iter().map(|x| x.qd).collect())),
536 ("gs", f64s(t.buses.iter().map(|x| x.gs).collect())),
537 ("bs", f64s(t.buses.iter().map(|x| x.bs).collect())),
538 (
539 "component_label",
540 i64s(t.index.component_labels.iter().map(|&x| usz(x)).collect()),
541 ),
542 (
543 "is_reference",
544 u8s(t
545 .buses
546 .iter()
547 .map(|x| u8::from(t.index.reference_bus_indices.contains(&x.index)))
548 .collect()),
549 ),
550 ])
551}
552
553fn solver_load_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
554 batch(vec![
555 (
556 "index",
557 i64s(t.loads.iter().map(|x| usz(x.index)).collect()),
558 ),
559 (
560 "source_row",
561 i64s(t.loads.iter().map(|x| opt_usz(x.source_row)).collect()),
562 ),
563 (
564 "bus_index",
565 i64s(t.loads.iter().map(|x| usz(x.bus_index)).collect()),
566 ),
567 ("p", f64s(t.loads.iter().map(|x| x.p).collect())),
568 ("q", f64s(t.loads.iter().map(|x| x.q).collect())),
569 ])
570}
571
572fn solver_shunt_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
573 batch(vec![
574 (
575 "index",
576 i64s(t.shunts.iter().map(|x| usz(x.index)).collect()),
577 ),
578 (
579 "source_row",
580 i64s(t.shunts.iter().map(|x| opt_usz(x.source_row)).collect()),
581 ),
582 (
583 "bus_index",
584 i64s(t.shunts.iter().map(|x| usz(x.bus_index)).collect()),
585 ),
586 ("g", f64s(t.shunts.iter().map(|x| x.g).collect())),
587 ("b", f64s(t.shunts.iter().map(|x| x.b).collect())),
588 ])
589}
590
591fn solver_branch_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
592 batch(vec![
593 (
594 "index",
595 i64s(t.branches.iter().map(|x| usz(x.index)).collect()),
596 ),
597 (
598 "source_row",
599 i64s(t.branches.iter().map(|x| opt_usz(x.source_row)).collect()),
600 ),
601 (
602 "from_bus_index",
603 i64s(t.branches.iter().map(|x| usz(x.from_bus_index)).collect()),
604 ),
605 (
606 "to_bus_index",
607 i64s(t.branches.iter().map(|x| usz(x.to_bus_index)).collect()),
608 ),
609 ("r", f64s(t.branches.iter().map(|x| x.r).collect())),
610 ("x", f64s(t.branches.iter().map(|x| x.x).collect())),
611 ("b", f64s(t.branches.iter().map(|x| x.b).collect())),
612 ("g_fr", f64s(t.branches.iter().map(|x| x.g_fr).collect())),
613 ("b_fr", f64s(t.branches.iter().map(|x| x.b_fr).collect())),
614 ("g_to", f64s(t.branches.iter().map(|x| x.g_to).collect())),
615 ("b_to", f64s(t.branches.iter().map(|x| x.b_to).collect())),
616 (
617 "rate_a",
618 f64s(t.branches.iter().map(|x| x.rate_a).collect()),
619 ),
620 (
621 "rate_b",
622 f64s(t.branches.iter().map(|x| x.rate_b).collect()),
623 ),
624 (
625 "rate_c",
626 f64s(t.branches.iter().map(|x| x.rate_c).collect()),
627 ),
628 ("tap", f64s(t.branches.iter().map(|x| x.tap).collect())),
629 ("shift", f64s(t.branches.iter().map(|x| x.shift).collect())),
630 (
631 "angmin",
632 f64s(t.branches.iter().map(|x| x.angmin).collect()),
633 ),
634 (
635 "angmax",
636 f64s(t.branches.iter().map(|x| x.angmax).collect()),
637 ),
638 ])
639}
640
641fn solver_switch_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
642 batch(vec![
643 (
644 "index",
645 i64s(t.switches.iter().map(|x| usz(x.index)).collect()),
646 ),
647 (
648 "source_row",
649 i64s(t.switches.iter().map(|x| opt_usz(x.source_row)).collect()),
650 ),
651 (
652 "from_bus_index",
653 i64s(t.switches.iter().map(|x| usz(x.from_bus_index)).collect()),
654 ),
655 (
656 "to_bus_index",
657 i64s(t.switches.iter().map(|x| usz(x.to_bus_index)).collect()),
658 ),
659 (
660 "closed",
661 u8s(t.switches.iter().map(|x| u8::from(x.closed)).collect()),
662 ),
663 (
664 "thermal_rating",
665 f64s(
666 t.switches
667 .iter()
668 .map(|x| x.thermal_rating.unwrap_or(0.0))
669 .collect(),
670 ),
671 ),
672 (
673 "current_rating",
674 f64s(
675 t.switches
676 .iter()
677 .map(|x| x.current_rating.unwrap_or(0.0))
678 .collect(),
679 ),
680 ),
681 (
682 "pf",
683 f64s(t.switches.iter().map(|x| x.pf.unwrap_or(0.0)).collect()),
684 ),
685 (
686 "qf",
687 f64s(t.switches.iter().map(|x| x.qf.unwrap_or(0.0)).collect()),
688 ),
689 (
690 "pt",
691 f64s(t.switches.iter().map(|x| x.pt.unwrap_or(0.0)).collect()),
692 ),
693 (
694 "qt",
695 f64s(t.switches.iter().map(|x| x.qt.unwrap_or(0.0)).collect()),
696 ),
697 ])
698}
699
700fn solver_arc_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
701 batch(vec![
702 ("index", i64s(t.arcs.iter().map(|x| usz(x.index)).collect())),
703 (
704 "branch_index",
705 i64s(t.arcs.iter().map(|x| usz(x.branch_index)).collect()),
706 ),
707 (
708 "terminal",
709 i64s(
710 t.arcs
711 .iter()
712 .map(|x| match x.terminal {
713 SolverArcTerminal::From => 0,
714 SolverArcTerminal::To => 1,
715 })
716 .collect(),
717 ),
718 ),
719 (
720 "from_bus_index",
721 i64s(t.arcs.iter().map(|x| usz(x.from_bus_index)).collect()),
722 ),
723 (
724 "to_bus_index",
725 i64s(t.arcs.iter().map(|x| usz(x.to_bus_index)).collect()),
726 ),
727 ("tap", f64s(t.arcs.iter().map(|x| x.tap).collect())),
728 ("shift", f64s(t.arcs.iter().map(|x| x.shift).collect())),
729 ("g_shunt", f64s(t.arcs.iter().map(|x| x.g_shunt).collect())),
730 ("b_shunt", f64s(t.arcs.iter().map(|x| x.b_shunt).collect())),
731 ("rate_a", f64s(t.arcs.iter().map(|x| x.rate_a).collect())),
732 ])
733}
734
735fn solver_gen_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
736 batch(vec![
737 (
738 "index",
739 i64s(t.generators.iter().map(|x| usz(x.index)).collect()),
740 ),
741 (
742 "source_row",
743 i64s(t.generators.iter().map(|x| opt_usz(x.source_row)).collect()),
744 ),
745 (
746 "bus_index",
747 i64s(t.generators.iter().map(|x| usz(x.bus_index)).collect()),
748 ),
749 ("pg", f64s(t.generators.iter().map(|x| x.pg).collect())),
750 ("qg", f64s(t.generators.iter().map(|x| x.qg).collect())),
751 ("pmax", f64s(t.generators.iter().map(|x| x.pmax).collect())),
752 ("pmin", f64s(t.generators.iter().map(|x| x.pmin).collect())),
753 ("qmax", f64s(t.generators.iter().map(|x| x.qmax).collect())),
754 ("qmin", f64s(t.generators.iter().map(|x| x.qmin).collect())),
755 ("vg", f64s(t.generators.iter().map(|x| x.vg).collect())),
756 (
757 "mbase",
758 f64s(t.generators.iter().map(|x| x.mbase).collect()),
759 ),
760 (
761 "regulated_bus_index",
762 i64s(
763 t.generators
764 .iter()
765 .map(|x| opt_usz(x.regulated_bus_index))
766 .collect(),
767 ),
768 ),
769 ])
770}
771
772fn solver_storage_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
773 batch(vec![
774 (
775 "index",
776 i64s(t.storage.iter().map(|x| usz(x.index)).collect()),
777 ),
778 (
779 "source_row",
780 i64s(t.storage.iter().map(|x| opt_usz(x.source_row)).collect()),
781 ),
782 (
783 "bus_index",
784 i64s(t.storage.iter().map(|x| usz(x.bus_index)).collect()),
785 ),
786 ("ps", f64s(t.storage.iter().map(|x| x.ps).collect())),
787 ("qs", f64s(t.storage.iter().map(|x| x.qs).collect())),
788 ("energy", f64s(t.storage.iter().map(|x| x.energy).collect())),
789 (
790 "energy_rating",
791 f64s(t.storage.iter().map(|x| x.energy_rating).collect()),
792 ),
793 (
794 "charge_rating",
795 f64s(t.storage.iter().map(|x| x.charge_rating).collect()),
796 ),
797 (
798 "discharge_rating",
799 f64s(t.storage.iter().map(|x| x.discharge_rating).collect()),
800 ),
801 (
802 "thermal_rating",
803 f64s(t.storage.iter().map(|x| x.thermal_rating).collect()),
804 ),
805 ("qmin", f64s(t.storage.iter().map(|x| x.qmin).collect())),
806 ("qmax", f64s(t.storage.iter().map(|x| x.qmax).collect())),
807 ("r", f64s(t.storage.iter().map(|x| x.r).collect())),
808 ("x", f64s(t.storage.iter().map(|x| x.x).collect())),
809 ("p_loss", f64s(t.storage.iter().map(|x| x.p_loss).collect())),
810 ("q_loss", f64s(t.storage.iter().map(|x| x.q_loss).collect())),
811 ])
812}
813
814fn solver_hvdc_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
815 batch(vec![
816 ("index", i64s(t.hvdc.iter().map(|x| usz(x.index)).collect())),
817 (
818 "source_row",
819 i64s(t.hvdc.iter().map(|x| opt_usz(x.source_row)).collect()),
820 ),
821 (
822 "from_bus_index",
823 i64s(t.hvdc.iter().map(|x| usz(x.from_bus_index)).collect()),
824 ),
825 (
826 "to_bus_index",
827 i64s(t.hvdc.iter().map(|x| usz(x.to_bus_index)).collect()),
828 ),
829 ("pf", f64s(t.hvdc.iter().map(|x| x.pf).collect())),
830 ("pt", f64s(t.hvdc.iter().map(|x| x.pt).collect())),
831 ("qf", f64s(t.hvdc.iter().map(|x| x.qf).collect())),
832 ("qt", f64s(t.hvdc.iter().map(|x| x.qt).collect())),
833 ("vf", f64s(t.hvdc.iter().map(|x| x.vf).collect())),
834 ("vt", f64s(t.hvdc.iter().map(|x| x.vt).collect())),
835 ("pmin", f64s(t.hvdc.iter().map(|x| x.pmin).collect())),
836 ("pmax", f64s(t.hvdc.iter().map(|x| x.pmax).collect())),
837 ("qminf", f64s(t.hvdc.iter().map(|x| x.qminf).collect())),
838 ("qmaxf", f64s(t.hvdc.iter().map(|x| x.qmaxf).collect())),
839 ("qmint", f64s(t.hvdc.iter().map(|x| x.qmint).collect())),
840 ("qmaxt", f64s(t.hvdc.iter().map(|x| x.qmaxt).collect())),
841 ("loss0", f64s(t.hvdc.iter().map(|x| x.loss0).collect())),
842 ("loss1", f64s(t.hvdc.iter().map(|x| x.loss1).collect())),
843 ])
844}
845
846#[cfg(feature = "matrix")]
847macro_rules! real_matrix_batch {
848 ($table_name:expr, $matrix:expr, $row_axis:expr, $col_axis:expr) => {{
849 let matrix = $matrix;
850 let mut row_index = Vec::with_capacity(matrix.nnz());
851 let mut col_index = Vec::with_capacity(matrix.nnz());
852 let mut value = Vec::with_capacity(matrix.nnz());
853 for (row, vec) in matrix.outer_iterator().enumerate() {
854 for (col, &entry) in vec.iter() {
855 row_index.push(usz(row));
856 col_index.push(usz(col));
857 value.push(entry);
858 }
859 }
860 matrix_real_batch(
861 $table_name,
862 MatrixDims {
863 rows: matrix.rows(),
864 cols: matrix.cols(),
865 },
866 row_index,
867 col_index,
868 value,
869 MatrixAxes {
870 row: $row_axis,
871 col: $col_axis,
872 },
873 )
874 }};
875}
876
877#[cfg(feature = "matrix")]
878fn matrix_bus_batch(net: &BalancedNetwork, core: &IndexCore) -> Result<RecordBatch, String> {
879 let view = IndexedNetwork::with_core(net, core);
880 let refs = view.reference_bus_indices();
881 let components = view.connected_component_labels();
882 let source_rows: HashMap<BusId, usize> = net
883 .buses
884 .iter()
885 .enumerate()
886 .map(|(idx, bus)| (bus.id, idx))
887 .collect();
888
889 let buses = &view.network().buses;
890 batch_with_metadata(
891 vec![
892 ("index", i64s((0..buses.len()).map(usz).collect::<Vec<_>>())),
893 (
894 "bus_id",
895 i64s(buses.iter().map(|bus| ext(bus.id)).collect()),
896 ),
897 (
898 "source_row",
899 i64s(
900 buses
901 .iter()
902 .map(|bus| source_rows.get(&bus.id).copied().map_or(-1, usz))
903 .collect(),
904 ),
905 ),
906 (
907 "is_reference",
908 u8s((0..buses.len())
909 .map(|idx| u8::from(refs.contains(&idx)))
910 .collect()),
911 ),
912 (
913 "component",
914 i64s(components.iter().map(|&label| usz(label)).collect()),
915 ),
916 ],
917 axis_metadata("matrix_bus"),
918 )
919 .map_err(|e| e.to_string())
920}
921
922#[cfg(not(feature = "matrix"))]
923fn matrix_bus_batch(_net: &BalancedNetwork, _core: &IndexCore) -> Result<RecordBatch, String> {
924 Err(matrix_feature_error())
925}
926
927#[cfg(feature = "matrix")]
928fn matrix_branch_batch(net: &BalancedNetwork, core: &IndexCore) -> Result<RecordBatch, String> {
929 let view = IndexedNetwork::with_core(net, core);
930 let parts = powerio_matrix::build_incidence(
931 &view,
932 powerio_matrix::DcConvention::default(),
933 &powerio_matrix::BuildOptions::default(),
934 )
935 .map_err(|e| e.to_string())?;
936
937 let mut index = Vec::with_capacity(parts.branch_of_col.len());
938 let mut source_row = Vec::with_capacity(parts.branch_of_col.len());
939 let mut from_bus_id = Vec::with_capacity(parts.branch_of_col.len());
940 let mut to_bus_id = Vec::with_capacity(parts.branch_of_col.len());
941 for (col, &idx) in parts.branch_of_col.iter().enumerate() {
942 let br = view
943 .branches()
944 .get(idx)
945 .ok_or_else(|| format!("incidence branch column {col} points to missing row {idx}"))?;
946 index.push(usz(col));
947 source_row.push((idx < net.branches.len()).then_some(idx).map_or(-1, usz));
948 from_bus_id.push(ext(br.from));
949 to_bus_id.push(ext(br.to));
950 }
951
952 batch_with_metadata(
953 vec![
954 ("index", i64s(index)),
955 ("source_row", i64s(source_row)),
956 ("from_bus_id", i64s(from_bus_id)),
957 ("to_bus_id", i64s(to_bus_id)),
958 ],
959 axis_metadata("matrix_branch"),
960 )
961 .map_err(|e| e.to_string())
962}
963
964#[cfg(not(feature = "matrix"))]
965fn matrix_branch_batch(_net: &BalancedNetwork, _core: &IndexCore) -> Result<RecordBatch, String> {
966 Err(matrix_feature_error())
967}
968
969#[cfg(feature = "matrix")]
970fn matrix_ybus_batch(net: &BalancedNetwork, core: &IndexCore) -> Result<RecordBatch, String> {
971 let view = IndexedNetwork::with_core(net, core);
972 let parts = powerio_matrix::build_ybus(&view, &powerio_matrix::BuildOptions::default())
973 .map_err(|e| e.to_string())?;
974 let mut cols = YbusColumns {
975 row_index: Vec::with_capacity(parts.g.nnz() + parts.b.nnz()),
976 col_index: Vec::with_capacity(parts.g.nnz() + parts.b.nnz()),
977 g: Vec::with_capacity(parts.g.nnz() + parts.b.nnz()),
978 b: Vec::with_capacity(parts.g.nnz() + parts.b.nnz()),
979 };
980 for row in 0..parts.g.rows() {
981 match (parts.g.outer_view(row), parts.b.outer_view(row)) {
982 (Some(g_row), Some(b_row)) => push_ybus_row(
983 row,
984 g_row.indices(),
985 g_row.data(),
986 b_row.indices(),
987 b_row.data(),
988 &mut cols,
989 ),
990 (Some(g_row), None) => {
991 push_ybus_row(row, g_row.indices(), g_row.data(), &[], &[], &mut cols);
992 }
993 (None, Some(b_row)) => {
994 push_ybus_row(row, &[], &[], b_row.indices(), b_row.data(), &mut cols);
995 }
996 (None, None) => {}
997 }
998 }
999 matrix_ybus_record_batch(
1000 MatrixDims {
1001 rows: parts.g.rows(),
1002 cols: parts.g.cols(),
1003 },
1004 cols.row_index,
1005 cols.col_index,
1006 cols.g,
1007 cols.b,
1008 MatrixAxes {
1009 row: "matrix_bus",
1010 col: "matrix_bus",
1011 },
1012 )
1013 .map_err(|e| e.to_string())
1014}
1015
1016#[cfg(not(feature = "matrix"))]
1017fn matrix_ybus_batch(_net: &BalancedNetwork, _core: &IndexCore) -> Result<RecordBatch, String> {
1018 Err(matrix_feature_error())
1019}
1020
1021#[cfg(feature = "matrix")]
1022struct YbusColumns {
1023 row_index: Vec<i64>,
1024 col_index: Vec<i64>,
1025 g: Vec<f64>,
1026 b: Vec<f64>,
1027}
1028
1029#[cfg(feature = "matrix")]
1030fn push_ybus_row(
1031 row: usize,
1032 g_indices: &[usize],
1033 g_data: &[f64],
1034 b_indices: &[usize],
1035 b_data: &[f64],
1036 cols: &mut YbusColumns,
1037) {
1038 let mut gi = 0;
1039 let mut bi = 0;
1040 while gi < g_indices.len() || bi < b_indices.len() {
1041 let (col, g_value, b_value) = match (g_indices.get(gi), b_indices.get(bi)) {
1042 (Some(&g_col), Some(&b_col)) => match g_col.cmp(&b_col) {
1043 std::cmp::Ordering::Less => {
1044 gi += 1;
1045 (g_col, g_data[gi - 1], 0.0)
1046 }
1047 std::cmp::Ordering::Greater => {
1048 bi += 1;
1049 (b_col, 0.0, b_data[bi - 1])
1050 }
1051 std::cmp::Ordering::Equal => {
1052 gi += 1;
1053 bi += 1;
1054 (g_col, g_data[gi - 1], b_data[bi - 1])
1055 }
1056 },
1057 (Some(&g_col), None) => {
1058 gi += 1;
1059 (g_col, g_data[gi - 1], 0.0)
1060 }
1061 (None, Some(&b_col)) => {
1062 bi += 1;
1063 (b_col, 0.0, b_data[bi - 1])
1064 }
1065 (None, None) => unreachable!(),
1066 };
1067 cols.row_index.push(usz(row));
1068 cols.col_index.push(usz(col));
1069 cols.g.push(g_value);
1070 cols.b.push(b_value);
1071 }
1072}
1073
1074#[cfg(feature = "matrix")]
1075fn matrix_incidence_batch(net: &BalancedNetwork, core: &IndexCore) -> Result<RecordBatch, String> {
1076 let view = IndexedNetwork::with_core(net, core);
1077 let parts = powerio_matrix::build_incidence(
1078 &view,
1079 powerio_matrix::DcConvention::default(),
1080 &powerio_matrix::BuildOptions::default(),
1081 )
1082 .map_err(|e| e.to_string())?;
1083 real_matrix_batch!("incidence", parts.a, "matrix_bus", "matrix_branch")
1084 .map_err(|e| e.to_string())
1085}
1086
1087#[cfg(not(feature = "matrix"))]
1088fn matrix_incidence_batch(
1089 _net: &BalancedNetwork,
1090 _core: &IndexCore,
1091) -> Result<RecordBatch, String> {
1092 Err(matrix_feature_error())
1093}
1094
1095#[cfg(feature = "matrix")]
1096fn matrix_bprime_batch(net: &BalancedNetwork, core: &IndexCore) -> Result<RecordBatch, String> {
1097 let view = IndexedNetwork::with_core(net, core);
1098 let matrix = powerio_matrix::build_bprime(&view, &powerio_matrix::BuildOptions::default())
1099 .map_err(|e| e.to_string())?;
1100 real_matrix_batch!("bprime", matrix, "matrix_bus", "matrix_bus").map_err(|e| e.to_string())
1101}
1102
1103#[cfg(not(feature = "matrix"))]
1104fn matrix_bprime_batch(_net: &BalancedNetwork, _core: &IndexCore) -> Result<RecordBatch, String> {
1105 Err(matrix_feature_error())
1106}
1107
1108#[cfg(feature = "matrix")]
1109fn matrix_bdoubleprime_batch(
1110 net: &BalancedNetwork,
1111 core: &IndexCore,
1112) -> Result<RecordBatch, String> {
1113 let view = IndexedNetwork::with_core(net, core);
1114 let matrix =
1115 powerio_matrix::build_bdoubleprime(&view, &powerio_matrix::BuildOptions::default())
1116 .map_err(|e| e.to_string())?;
1117 real_matrix_batch!("bdoubleprime", matrix, "matrix_bus", "matrix_bus")
1118 .map_err(|e| e.to_string())
1119}
1120
1121#[cfg(not(feature = "matrix"))]
1122fn matrix_bdoubleprime_batch(
1123 _net: &BalancedNetwork,
1124 _core: &IndexCore,
1125) -> Result<RecordBatch, String> {
1126 Err(matrix_feature_error())
1127}
1128
1129#[cfg(not(feature = "matrix"))]
1130fn matrix_feature_error() -> String {
1131 "matrix Arrow tables require the matrix cargo feature".to_owned()
1132}
1133
1134#[cfg(feature = "matrix")]
1135struct MatrixDims {
1136 rows: usize,
1137 cols: usize,
1138}
1139
1140#[cfg(feature = "matrix")]
1141struct MatrixAxes<'a> {
1142 row: &'a str,
1143 col: &'a str,
1144}
1145
1146#[cfg(feature = "matrix")]
1147fn matrix_real_batch(
1148 table: &str,
1149 dims: MatrixDims,
1150 row_index: Vec<i64>,
1151 col_index: Vec<i64>,
1152 value: Vec<f64>,
1153 axes: MatrixAxes<'_>,
1154) -> Result<RecordBatch, ArrowError> {
1155 batch_with_metadata(
1156 vec![
1157 ("row_index", i64s(row_index)),
1158 ("col_index", i64s(col_index)),
1159 ("value", f64s(value)),
1160 ],
1161 matrix_metadata(table, dims.rows, dims.cols, axes.row, axes.col),
1162 )
1163}
1164
1165#[cfg(feature = "matrix")]
1166fn matrix_ybus_record_batch(
1167 dims: MatrixDims,
1168 row_index: Vec<i64>,
1169 col_index: Vec<i64>,
1170 g: Vec<f64>,
1171 b: Vec<f64>,
1172 axes: MatrixAxes<'_>,
1173) -> Result<RecordBatch, ArrowError> {
1174 batch_with_metadata(
1175 vec![
1176 ("row_index", i64s(row_index)),
1177 ("col_index", i64s(col_index)),
1178 ("g", f64s(g)),
1179 ("b", f64s(b)),
1180 ],
1181 matrix_metadata("ybus", dims.rows, dims.cols, axes.row, axes.col),
1182 )
1183}
1184
1185fn batch(cols: Vec<(&str, ArrayRef)>) -> Result<RecordBatch, ArrowError> {
1186 batch_with_metadata(cols, HashMap::new())
1187}
1188
1189fn batch_with_metadata(
1190 cols: Vec<(&str, ArrayRef)>,
1191 metadata: HashMap<String, String>,
1192) -> Result<RecordBatch, ArrowError> {
1193 let fields: Vec<Field> = cols
1194 .iter()
1195 .map(|(name, arr)| Field::new(*name, arr.data_type().clone(), false))
1196 .collect();
1197 let arrays: Vec<ArrayRef> = cols.into_iter().map(|(_, arr)| arr).collect();
1198 RecordBatch::try_new(
1199 Arc::new(Schema::new_with_metadata(fields, metadata)),
1200 arrays,
1201 )
1202}
1203
1204#[cfg(feature = "matrix")]
1205fn matrix_metadata(
1206 table: &str,
1207 rows: usize,
1208 cols: usize,
1209 row_axis: &str,
1210 col_axis: &str,
1211) -> HashMap<String, String> {
1212 HashMap::from([
1213 ("powerio.table".to_owned(), table.to_owned()),
1214 ("powerio.version".to_owned(), powerio::VERSION.to_owned()),
1215 ("powerio.format".to_owned(), "coo".to_owned()),
1216 ("powerio.index_space".to_owned(), "solver_bus".to_owned()),
1217 ("powerio.row_axis".to_owned(), row_axis.to_owned()),
1218 ("powerio.col_axis".to_owned(), col_axis.to_owned()),
1219 ("powerio.row_count".to_owned(), rows.to_string()),
1220 ("powerio.col_count".to_owned(), cols.to_string()),
1221 ])
1222}
1223
1224#[cfg(feature = "matrix")]
1225fn axis_metadata(table: &str) -> HashMap<String, String> {
1226 HashMap::from([
1227 ("powerio.table".to_owned(), table.to_owned()),
1228 ("powerio.version".to_owned(), powerio::VERSION.to_owned()),
1229 ("powerio.format".to_owned(), "axis_map".to_owned()),
1230 ("powerio.row_axis".to_owned(), table.to_owned()),
1231 ])
1232}
1233
1234fn ext(id: BusId) -> i64 {
1236 i64::try_from(id.0).unwrap_or(-1)
1237}
1238
1239fn usz(n: usize) -> i64 {
1240 i64::try_from(n).unwrap_or(-1)
1241}
1242
1243fn opt_usz(n: Option<usize>) -> i64 {
1244 n.map_or(-1, usz)
1245}
1246
1247fn i64s(v: Vec<i64>) -> ArrayRef {
1248 Arc::new(Int64Array::from(v))
1249}
1250
1251fn f64s(v: Vec<f64>) -> ArrayRef {
1252 Arc::new(Float64Array::from(v))
1253}
1254
1255fn u8s(v: Vec<u8>) -> ArrayRef {
1256 Arc::new(UInt8Array::from(v))
1257}
1258
1259#[cfg(test)]
1260mod tests {
1261 use super::*;
1262 use arrow::ffi::from_ffi;
1263
1264 fn net(name: &str) -> BalancedNetwork {
1265 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1266 .join("../tests/data")
1267 .join(name);
1268 powerio::parse_file(&path, None).unwrap().network
1269 }
1270
1271 fn terminal_projection_net() -> BalancedNetwork {
1272 use powerio::{Branch, BranchCharging, Bus, BusId, BusType};
1273
1274 let mut branch = Branch::new(BusId(1), BusId(2), 0.01, 0.1);
1275 branch.charging = Some(BranchCharging::new(0.01, 0.02, 0.03, 0.05));
1276 branch.rate_a = 100.0;
1277 BalancedNetwork::in_memory(
1278 "terminal-projection",
1279 100.0,
1280 vec![
1281 Bus::new(BusId(1), BusType::Ref, 230.0),
1282 Bus::new(BusId(2), BusType::Pq, 230.0),
1283 ],
1284 vec![branch],
1285 )
1286 }
1287
1288 #[cfg(feature = "matrix")]
1289 fn incidence_filter_net() -> BalancedNetwork {
1290 use powerio::{Branch, Bus, BusId, BusType};
1291
1292 BalancedNetwork::in_memory(
1293 "incidence-filter",
1294 100.0,
1295 vec![
1296 Bus::new(BusId(1), BusType::Ref, 230.0),
1297 Bus::new(BusId(2), BusType::Pq, 230.0),
1298 Bus::new(BusId(3), BusType::Pq, 230.0),
1299 ],
1300 vec![
1301 Branch::new(BusId(1), BusId(1), 0.0, 0.1),
1302 Branch::new(BusId(1), BusId(2), 0.0, 0.0),
1303 Branch::new(BusId(2), BusId(3), 0.0, 0.2),
1304 ],
1305 )
1306 }
1307
1308 fn round_trip(net: &BalancedNetwork, table: i32) -> StructArray {
1309 let core = IndexCore::build(net);
1310 let (array, schema) = export(net, &core, table).unwrap();
1311 let data = unsafe { from_ffi(array, &schema) }.unwrap();
1313 StructArray::from(data)
1314 }
1315
1316 fn f64_col<'a>(sa: &'a StructArray, name: &str) -> &'a Float64Array {
1317 sa.column_by_name(name)
1318 .unwrap()
1319 .as_any()
1320 .downcast_ref::<Float64Array>()
1321 .unwrap()
1322 }
1323
1324 fn i64_col<'a>(sa: &'a StructArray, name: &str) -> &'a Int64Array {
1325 sa.column_by_name(name)
1326 .unwrap()
1327 .as_any()
1328 .downcast_ref::<Int64Array>()
1329 .unwrap()
1330 }
1331
1332 #[cfg(feature = "matrix")]
1333 fn u8_col<'a>(sa: &'a StructArray, name: &str) -> &'a UInt8Array {
1334 sa.column_by_name(name)
1335 .unwrap()
1336 .as_any()
1337 .downcast_ref::<UInt8Array>()
1338 .unwrap()
1339 }
1340
1341 #[cfg(feature = "matrix")]
1342 fn rb_f64_col<'a>(rb: &'a RecordBatch, name: &str) -> &'a Float64Array {
1343 rb.column_by_name(name)
1344 .unwrap()
1345 .as_any()
1346 .downcast_ref::<Float64Array>()
1347 .unwrap()
1348 }
1349
1350 #[cfg(feature = "matrix")]
1351 fn rb_i64_col<'a>(rb: &'a RecordBatch, name: &str) -> &'a Int64Array {
1352 rb.column_by_name(name)
1353 .unwrap()
1354 .as_any()
1355 .downcast_ref::<Int64Array>()
1356 .unwrap()
1357 }
1358
1359 #[cfg(feature = "matrix")]
1360 fn matrix_record_batch(net: &BalancedNetwork, table: i32) -> RecordBatch {
1361 let core = IndexCore::build(net);
1362 match table {
1363 PIO_ARROW_TABLE_YBUS => matrix_ybus_batch(net, &core).unwrap(),
1364 PIO_ARROW_TABLE_INCIDENCE => matrix_incidence_batch(net, &core).unwrap(),
1365 PIO_ARROW_TABLE_BPRIME => matrix_bprime_batch(net, &core).unwrap(),
1366 PIO_ARROW_TABLE_BDOUBLEPRIME => matrix_bdoubleprime_batch(net, &core).unwrap(),
1367 PIO_ARROW_TABLE_MATRIX_BUS => matrix_bus_batch(net, &core).unwrap(),
1368 PIO_ARROW_TABLE_MATRIX_BRANCH => matrix_branch_batch(net, &core).unwrap(),
1369 _ => panic!("not a matrix table id: {table}"),
1370 }
1371 }
1372
1373 #[cfg(feature = "matrix")]
1374 fn f64_bits(values: &Float64Array) -> Vec<String> {
1375 values
1376 .values()
1377 .iter()
1378 .map(|value| format!("0x{:016x}", value.to_bits()))
1379 .collect()
1380 }
1381
1382 #[cfg(feature = "matrix")]
1383 fn matrix_table_json(table_name: &str, rb: &RecordBatch) -> serde_json::Value {
1384 let metadata = rb.schema();
1385 let metadata = metadata.metadata();
1386 let mut obj = serde_json::Map::new();
1387 obj.insert("table".to_owned(), serde_json::json!(table_name));
1388 obj.insert(
1389 powerio::version::VERSION_KEY.to_owned(),
1390 serde_json::json!(metadata.get("powerio.version").unwrap()),
1391 );
1392 obj.insert(
1393 "format".to_owned(),
1394 serde_json::json!(metadata.get("powerio.format").unwrap()),
1395 );
1396 obj.insert(
1397 "row_axis".to_owned(),
1398 serde_json::json!(metadata.get("powerio.row_axis").unwrap()),
1399 );
1400 obj.insert(
1401 "col_axis".to_owned(),
1402 serde_json::json!(metadata.get("powerio.col_axis").unwrap()),
1403 );
1404 obj.insert(
1405 "row_count".to_owned(),
1406 serde_json::json!(
1407 metadata
1408 .get("powerio.row_count")
1409 .unwrap()
1410 .parse::<usize>()
1411 .unwrap()
1412 ),
1413 );
1414 obj.insert(
1415 "col_count".to_owned(),
1416 serde_json::json!(
1417 metadata
1418 .get("powerio.col_count")
1419 .unwrap()
1420 .parse::<usize>()
1421 .unwrap()
1422 ),
1423 );
1424 obj.insert(
1425 "row_index".to_owned(),
1426 serde_json::json!(rb_i64_col(rb, "row_index").values().to_vec()),
1427 );
1428 obj.insert(
1429 "col_index".to_owned(),
1430 serde_json::json!(rb_i64_col(rb, "col_index").values().to_vec()),
1431 );
1432 if table_name == "ybus" {
1433 obj.insert(
1434 "g_bits".to_owned(),
1435 serde_json::json!(f64_bits(rb_f64_col(rb, "g"))),
1436 );
1437 obj.insert(
1438 "b_bits".to_owned(),
1439 serde_json::json!(f64_bits(rb_f64_col(rb, "b"))),
1440 );
1441 } else {
1442 obj.insert(
1443 "value_bits".to_owned(),
1444 serde_json::json!(f64_bits(rb_f64_col(rb, "value"))),
1445 );
1446 }
1447 serde_json::Value::Object(obj)
1448 }
1449
1450 #[cfg(feature = "matrix")]
1451 fn axis_table_json(table_name: &str, rb: &RecordBatch) -> serde_json::Value {
1452 let metadata = rb.schema();
1453 let metadata = metadata.metadata();
1454 let mut obj = serde_json::Map::new();
1455 obj.insert("table".to_owned(), serde_json::json!(table_name));
1456 obj.insert(
1457 powerio::version::VERSION_KEY.to_owned(),
1458 serde_json::json!(metadata.get("powerio.version").unwrap()),
1459 );
1460 obj.insert(
1461 "format".to_owned(),
1462 serde_json::json!(metadata.get("powerio.format").unwrap()),
1463 );
1464 obj.insert(
1465 "row_axis".to_owned(),
1466 serde_json::json!(metadata.get("powerio.row_axis").unwrap()),
1467 );
1468 obj.insert(
1469 "index".to_owned(),
1470 serde_json::json!(rb_i64_col(rb, "index").values().to_vec()),
1471 );
1472 obj.insert(
1473 "source_row".to_owned(),
1474 serde_json::json!(rb_i64_col(rb, "source_row").values().to_vec()),
1475 );
1476 if table_name == "matrix_bus" {
1477 obj.insert(
1478 "bus_id".to_owned(),
1479 serde_json::json!(rb_i64_col(rb, "bus_id").values().to_vec()),
1480 );
1481 obj.insert(
1482 "is_reference".to_owned(),
1483 serde_json::json!(
1484 rb.column_by_name("is_reference")
1485 .unwrap()
1486 .as_any()
1487 .downcast_ref::<UInt8Array>()
1488 .unwrap()
1489 .values()
1490 .to_vec()
1491 ),
1492 );
1493 obj.insert(
1494 "component".to_owned(),
1495 serde_json::json!(rb_i64_col(rb, "component").values().to_vec()),
1496 );
1497 } else {
1498 obj.insert(
1499 "from_bus_id".to_owned(),
1500 serde_json::json!(rb_i64_col(rb, "from_bus_id").values().to_vec()),
1501 );
1502 obj.insert(
1503 "to_bus_id".to_owned(),
1504 serde_json::json!(rb_i64_col(rb, "to_bus_id").values().to_vec()),
1505 );
1506 }
1507 serde_json::Value::Object(obj)
1508 }
1509
1510 #[cfg(feature = "matrix")]
1511 fn matrix_golden_json(case_file: &str) -> serde_json::Value {
1512 let n = net(case_file);
1513 let tables = [
1514 ("ybus", PIO_ARROW_TABLE_YBUS),
1515 ("incidence", PIO_ARROW_TABLE_INCIDENCE),
1516 ("bprime", PIO_ARROW_TABLE_BPRIME),
1517 ("bdoubleprime", PIO_ARROW_TABLE_BDOUBLEPRIME),
1518 ];
1519 let mut table_obj = serde_json::Map::new();
1520 for (name, table) in tables {
1521 let rb = matrix_record_batch(&n, table);
1522 table_obj.insert(name.to_owned(), matrix_table_json(name, &rb));
1523 }
1524 let axes = [
1525 ("matrix_bus", PIO_ARROW_TABLE_MATRIX_BUS),
1526 ("matrix_branch", PIO_ARROW_TABLE_MATRIX_BRANCH),
1527 ];
1528 let mut axis_obj = serde_json::Map::new();
1529 for (name, table) in axes {
1530 let rb = matrix_record_batch(&n, table);
1531 axis_obj.insert(name.to_owned(), axis_table_json(name, &rb));
1532 }
1533 serde_json::json!({
1534 "case": case_file,
1535 "axes": axis_obj,
1536 "tables": table_obj,
1537 })
1538 }
1539
1540 #[cfg(feature = "matrix")]
1541 fn assert_ffi_matches_record_batch(table: i32, value_cols: &[&str]) {
1542 let n = net("case9.m");
1543 let rb = matrix_record_batch(&n, table);
1544 let sa = round_trip(&n, table);
1545
1546 assert_eq!(sa.len(), rb.num_rows());
1547 assert_eq!(
1548 i64_col(&sa, "row_index").values(),
1549 rb_i64_col(&rb, "row_index").values()
1550 );
1551 assert_eq!(
1552 i64_col(&sa, "col_index").values(),
1553 rb_i64_col(&rb, "col_index").values()
1554 );
1555 for &name in value_cols {
1556 assert_eq!(
1557 f64_bits(f64_col(&sa, name)),
1558 f64_bits(rb_f64_col(&rb, name)),
1559 "{name} column changed through FFI"
1560 );
1561 }
1562 }
1563
1564 #[test]
1565 fn bus_table_round_trips_with_external_ids() {
1566 let n = net("case9.m");
1567 let sa = round_trip(&n, PIO_ARROW_TABLE_BUS);
1568 assert_eq!(sa.len(), n.buses.len());
1569 let ids = sa
1570 .column_by_name("id")
1571 .unwrap()
1572 .as_any()
1573 .downcast_ref::<Int64Array>()
1574 .unwrap();
1575 let expected: Vec<i64> = n
1578 .buses
1579 .iter()
1580 .map(|b| i64::try_from(b.id.0).unwrap())
1581 .collect();
1582 assert_eq!(ids.values(), expected.as_slice());
1583 }
1584
1585 #[test]
1586 fn empty_table_exports_zero_rows() {
1587 let n = net("case9.m");
1590 assert_eq!(n.shunts.len(), 0);
1591 assert_eq!(round_trip(&n, PIO_ARROW_TABLE_SHUNT).len(), 0);
1592 }
1593
1594 #[test]
1595 fn every_table_has_the_expected_row_count() {
1596 let n = net("case30.m");
1598 assert_eq!(round_trip(&n, PIO_ARROW_TABLE_BUS).len(), n.buses.len());
1599 assert_eq!(
1600 round_trip(&n, PIO_ARROW_TABLE_BRANCH).len(),
1601 n.branches.len()
1602 );
1603 assert_eq!(
1604 round_trip(&n, PIO_ARROW_TABLE_GEN).len(),
1605 n.generators.len()
1606 );
1607 assert_eq!(round_trip(&n, PIO_ARROW_TABLE_LOAD).len(), n.loads.len());
1608 assert_eq!(round_trip(&n, PIO_ARROW_TABLE_SHUNT).len(), n.shunts.len());
1609 }
1610
1611 #[test]
1612 fn normalized_solver_tables_export_dense_per_unit_rows() {
1613 let n = net("case14.m");
1614 let tables = n.to_normalized_solver_tables().unwrap();
1615
1616 assert_eq!(
1617 round_trip(&n, PIO_ARROW_TABLE_SOLVER_BUS).len(),
1618 tables.buses.len()
1619 );
1620 assert_eq!(
1621 round_trip(&n, PIO_ARROW_TABLE_SOLVER_BRANCH).len(),
1622 tables.branches.len()
1623 );
1624 assert_eq!(
1625 round_trip(&n, PIO_ARROW_TABLE_SOLVER_ARC).len(),
1626 tables.arcs.len()
1627 );
1628 assert_eq!(
1629 round_trip(&n, PIO_ARROW_TABLE_SOLVER_GEN).len(),
1630 tables.generators.len()
1631 );
1632
1633 let bus = round_trip(&n, PIO_ARROW_TABLE_SOLVER_BUS);
1634 assert_eq!(i64_col(&bus, "index").value(1), 1);
1635 assert_eq!(i64_col(&bus, "bus_id").value(1), 2);
1636 assert_eq!(i64_col(&bus, "source_row").value(1), 1);
1637 assert!((f64_col(&bus, "pd").value(1) - 21.7 / 100.0).abs() < 1e-12);
1638
1639 let branch = round_trip(&n, PIO_ARROW_TABLE_SOLVER_BRANCH);
1640 assert_eq!(i64_col(&branch, "from_bus_index").value(0), 0);
1641 assert_eq!(i64_col(&branch, "to_bus_index").value(0), 1);
1642
1643 let arc = round_trip(&n, PIO_ARROW_TABLE_SOLVER_ARC);
1644 assert_eq!(i64_col(&arc, "branch_index").value(0), 0);
1645 assert_eq!(i64_col(&arc, "terminal").value(0), 0);
1646 assert_eq!(i64_col(&arc, "branch_index").value(1), 0);
1647 assert_eq!(i64_col(&arc, "terminal").value(1), 1);
1648 }
1649
1650 #[test]
1651 fn branch_table_b_is_legacy_projection() {
1652 let n = terminal_projection_net();
1653 let sa = round_trip(&n, PIO_ARROW_TABLE_BRANCH);
1654 assert_eq!(sa.len(), 1);
1655 assert!((f64_col(&sa, "b").value(0) - 0.07).abs() < 1e-12);
1656 assert!((f64_col(&sa, "g_fr").value(0) - 0.01).abs() < 1e-12);
1657 assert!((f64_col(&sa, "b_fr").value(0) - 0.02).abs() < 1e-12);
1658 assert!((f64_col(&sa, "g_to").value(0) - 0.03).abs() < 1e-12);
1659 assert!((f64_col(&sa, "b_to").value(0) - 0.05).abs() < 1e-12);
1660 }
1661
1662 #[test]
1663 fn unknown_table_id_errors() {
1664 let n = net("case9.m");
1665 let core = IndexCore::build(&n);
1666 assert!(export(&n, &core, 99).is_err());
1667 }
1668
1669 #[test]
1670 fn arrow_table_ids_are_append_only() {
1671 assert_eq!(PIO_ARROW_TABLE_BUS, 0);
1672 assert_eq!(PIO_ARROW_TABLE_BRANCH, 1);
1673 assert_eq!(PIO_ARROW_TABLE_GEN, 2);
1674 assert_eq!(PIO_ARROW_TABLE_LOAD, 3);
1675 assert_eq!(PIO_ARROW_TABLE_SHUNT, 4);
1676 assert_eq!(PIO_ARROW_TABLE_SWITCH, 5);
1677 assert_eq!(PIO_ARROW_TABLE_SOLVER_BUS, 6);
1678 assert_eq!(PIO_ARROW_TABLE_SOLVER_LOAD, 7);
1679 assert_eq!(PIO_ARROW_TABLE_SOLVER_SHUNT, 8);
1680 assert_eq!(PIO_ARROW_TABLE_SOLVER_BRANCH, 9);
1681 assert_eq!(PIO_ARROW_TABLE_SOLVER_SWITCH, 10);
1682 assert_eq!(PIO_ARROW_TABLE_SOLVER_ARC, 11);
1683 assert_eq!(PIO_ARROW_TABLE_SOLVER_GEN, 12);
1684 assert_eq!(PIO_ARROW_TABLE_SOLVER_STORAGE, 13);
1685 assert_eq!(PIO_ARROW_TABLE_SOLVER_HVDC, 14);
1686 assert_eq!(PIO_ARROW_TABLE_YBUS, 15);
1687 assert_eq!(PIO_ARROW_TABLE_INCIDENCE, 16);
1688 assert_eq!(PIO_ARROW_TABLE_BPRIME, 17);
1689 assert_eq!(PIO_ARROW_TABLE_BDOUBLEPRIME, 18);
1690 assert_eq!(PIO_ARROW_TABLE_MATRIX_BUS, 19);
1691 assert_eq!(PIO_ARROW_TABLE_MATRIX_BRANCH, 20);
1692 }
1693
1694 #[test]
1695 fn arrow_catalog_lists_ids_columns_axes_and_features() {
1696 let catalog: serde_json::Value = serde_json::from_str(&catalog_json()).unwrap();
1697 assert_eq!(catalog[powerio::version::VERSION_KEY], powerio::VERSION);
1698 let tables = catalog["tables"].as_array().unwrap();
1699 let find = |name: &str| {
1700 tables
1701 .iter()
1702 .find(|table| table["name"] == name)
1703 .unwrap_or_else(|| panic!("missing catalog table {name}"))
1704 };
1705
1706 let bus = find("bus");
1707 assert_eq!(bus["id"], PIO_ARROW_TABLE_BUS);
1708 assert_eq!(bus["feature_requirements"], serde_json::json!(["arrow"]));
1709 assert_eq!(bus["columns"][0]["name"], "id");
1710
1711 let bprime = find("bprime");
1712 assert_eq!(bprime["id"], PIO_ARROW_TABLE_BPRIME);
1713 assert_eq!(bprime["format"], "coo");
1714 assert_eq!(bprime["row_axis"], "matrix_bus");
1715 assert_eq!(bprime["col_axis"], "matrix_bus");
1716 assert_eq!(
1717 bprime["feature_requirements"],
1718 serde_json::json!(["arrow", "matrix"])
1719 );
1720 assert_eq!(bprime["available"], cfg!(feature = "matrix"));
1721
1722 let incidence = find("incidence");
1723 assert_eq!(incidence["row_axis"], "matrix_bus");
1724 assert_eq!(incidence["col_axis"], "matrix_branch");
1725
1726 let axis = find("matrix_bus");
1727 assert_eq!(axis["id"], PIO_ARROW_TABLE_MATRIX_BUS);
1728 assert_eq!(axis["format"], "axis_map");
1729 assert_eq!(axis["columns"][1]["name"], "bus_id");
1730 }
1731
1732 #[cfg(not(feature = "matrix"))]
1733 #[test]
1734 fn matrix_table_requires_matrix_feature() {
1735 let n = net("case9.m");
1736 let core = IndexCore::build(&n);
1737 let err = export(&n, &core, PIO_ARROW_TABLE_BPRIME).unwrap_err();
1738 assert!(err.contains("matrix cargo feature"), "{err}");
1739 let err = export(&n, &core, PIO_ARROW_TABLE_MATRIX_BUS).unwrap_err();
1740 assert!(err.contains("matrix cargo feature"), "{err}");
1741 }
1742
1743 #[cfg(feature = "matrix")]
1744 #[test]
1745 fn matrix_tables_round_trip_through_ffi() {
1746 assert_ffi_matches_record_batch(PIO_ARROW_TABLE_YBUS, &["g", "b"]);
1747 assert_ffi_matches_record_batch(PIO_ARROW_TABLE_INCIDENCE, &["value"]);
1748 assert_ffi_matches_record_batch(PIO_ARROW_TABLE_BPRIME, &["value"]);
1749 assert_ffi_matches_record_batch(PIO_ARROW_TABLE_BDOUBLEPRIME, &["value"]);
1750 }
1751
1752 #[cfg(feature = "matrix")]
1753 #[test]
1754 fn matrix_tables_carry_schema_dimensions() {
1755 let n = net("case9.m");
1756 let rb = matrix_record_batch(&n, PIO_ARROW_TABLE_BPRIME);
1757 let metadata = rb.schema();
1758 let metadata = metadata.metadata();
1759 assert_eq!(metadata.get("powerio.table").unwrap(), "bprime");
1760 assert_eq!(metadata.get("powerio.version").unwrap(), powerio::VERSION);
1761 assert_eq!(metadata.get("powerio.format").unwrap(), "coo");
1762 assert_eq!(metadata.get("powerio.index_space").unwrap(), "solver_bus");
1763 assert_eq!(metadata.get("powerio.row_axis").unwrap(), "matrix_bus");
1764 assert_eq!(metadata.get("powerio.col_axis").unwrap(), "matrix_bus");
1765 assert_eq!(metadata.get("powerio.row_count").unwrap(), "9");
1766 assert_eq!(metadata.get("powerio.col_count").unwrap(), "9");
1767
1768 let core = IndexCore::build(&n);
1769 let (array, schema) = export(&n, &core, PIO_ARROW_TABLE_BPRIME).unwrap();
1770 let imported_schema = Schema::try_from(&schema).unwrap();
1771 let metadata = imported_schema.metadata();
1772 assert_eq!(metadata.get("powerio.table").unwrap(), "bprime");
1773 assert_eq!(metadata.get("powerio.version").unwrap(), powerio::VERSION);
1774 assert_eq!(metadata.get("powerio.format").unwrap(), "coo");
1775 assert_eq!(metadata.get("powerio.index_space").unwrap(), "solver_bus");
1776 assert_eq!(metadata.get("powerio.row_axis").unwrap(), "matrix_bus");
1777 assert_eq!(metadata.get("powerio.col_axis").unwrap(), "matrix_bus");
1778 assert_eq!(metadata.get("powerio.row_count").unwrap(), "9");
1779 assert_eq!(metadata.get("powerio.col_count").unwrap(), "9");
1780 let _data = unsafe { from_ffi(array, &schema) }.unwrap();
1781 }
1782
1783 #[cfg(feature = "matrix")]
1784 #[test]
1785 fn incidence_uses_branch_axis_metadata() {
1786 let n = net("case9.m");
1787 let rb = matrix_record_batch(&n, PIO_ARROW_TABLE_INCIDENCE);
1788 let metadata = rb.schema();
1789 let metadata = metadata.metadata();
1790 assert_eq!(metadata.get("powerio.table").unwrap(), "incidence");
1791 assert_eq!(metadata.get("powerio.format").unwrap(), "coo");
1792 assert_eq!(metadata.get("powerio.row_axis").unwrap(), "matrix_bus");
1793 assert_eq!(metadata.get("powerio.col_axis").unwrap(), "matrix_branch");
1794 assert_eq!(metadata.get("powerio.row_count").unwrap(), "9");
1795 assert_eq!(metadata.get("powerio.col_count").unwrap(), "9");
1796 }
1797
1798 #[cfg(feature = "matrix")]
1799 #[test]
1800 fn matrix_axis_maps_export_dense_rows() {
1801 let n = net("case14.m");
1802 let bus = round_trip(&n, PIO_ARROW_TABLE_MATRIX_BUS);
1803 assert_eq!(bus.len(), 14);
1804 assert_eq!(i64_col(&bus, "index").value(0), 0);
1805 assert_eq!(i64_col(&bus, "bus_id").value(0), 1);
1806 assert_eq!(i64_col(&bus, "source_row").value(0), 0);
1807 assert_eq!(u8_col(&bus, "is_reference").value(0), 1);
1808 assert_eq!(i64_col(&bus, "component").value(0), 0);
1809 assert_eq!(i64_col(&bus, "index").value(13), 13);
1810 assert_eq!(i64_col(&bus, "bus_id").value(13), 14);
1811
1812 let branch = round_trip(&n, PIO_ARROW_TABLE_MATRIX_BRANCH);
1813 let incidence = matrix_record_batch(&n, PIO_ARROW_TABLE_INCIDENCE);
1814 let col_count = incidence
1815 .schema()
1816 .metadata()
1817 .get("powerio.col_count")
1818 .unwrap()
1819 .parse::<usize>()
1820 .unwrap();
1821 assert_eq!(branch.len(), col_count);
1822 assert_eq!(i64_col(&branch, "index").value(0), 0);
1823 assert_eq!(i64_col(&branch, "source_row").value(0), 0);
1824 assert_eq!(i64_col(&branch, "from_bus_id").value(0), 1);
1825 assert_eq!(i64_col(&branch, "to_bus_id").value(0), 2);
1826 }
1827
1828 #[cfg(feature = "matrix")]
1829 fn transformer_3w_net() -> BalancedNetwork {
1830 let raw = "0, 100.00, 33, 0, 0, 60.00 / x\n\
1834CASE\n\
1835COMMENT\n\
18361,'B1 ', 230.0,3,1,1,1,1.00000,0.0,1.1,0.9,1.1,0.9\n\
18372,'B2 ', 138.0,1,1,1,1,1.00000,0.0,1.1,0.9,1.1,0.9\n\
18383,'B3 ', 13.8,1,1,1,1,1.00000,0.0,1.1,0.9,1.1,0.9\n\
18390 / END OF BUS DATA, BEGIN LOAD DATA\n\
18400 / END OF LOAD DATA, BEGIN FIXED SHUNT DATA\n\
18410 / END OF FIXED SHUNT DATA, BEGIN GENERATOR DATA\n\
18420 / END OF GENERATOR DATA, BEGIN BRANCH DATA\n\
18430 / END OF BRANCH DATA, BEGIN TRANSFORMER DATA\n\
18441, 2, 3, '1', 1, 1, 1, 0.0, 0.0, 2, 'T3W ', 1, 1, 1, 0, 1, 0, 1, 0, 1, ' '\n\
18450.01, 0.10, 100.0, 0.02, 0.20, 100.0, 0.03, 0.30, 100.0, 0.98, -1.5\n\
18461.0, 230.0, 0.0, 100.0, 90.0, 80.0, 0, 0, 1.1, 0.9, 1.1, 0.9, 33, 0, 0, 0, 0\n\
18471.025, 138.0, 0.0, 110.0, 0, 0, 0, 0, 1.1, 0.9, 1.1, 0.9, 33, 0, 0, 0, 0\n\
18480.95, 13.8, 30.0, 50.0, 0, 0, 0, 0, 1.1, 0.9, 1.1, 0.9, 33, 0, 0, 0, 0\n\
18490 / END OF TRANSFORMER DATA, BEGIN AREA DATA\n\
1850Q\n";
1851 powerio::parse_str(raw, "psse").unwrap().network
1852 }
1853
1854 #[cfg(feature = "matrix")]
1861 #[test]
1862 fn matrix_bus_axis_covers_star_lowered_rows() {
1863 let n = transformer_3w_net();
1864 assert_eq!(n.buses.len(), 3, "handle carries the three source buses");
1865 assert!(n.branches.is_empty(), "a 3W is not folded into branches");
1866 assert_eq!(n.transformers_3w.len(), 1);
1867
1868 let bus = matrix_record_batch(&n, PIO_ARROW_TABLE_MATRIX_BUS);
1869 assert_eq!(bus.num_rows(), n.buses.len() + 1);
1872
1873 let index = rb_i64_col(&bus, "index").values();
1874 let bus_id = rb_i64_col(&bus, "bus_id").values();
1875 let source_row = rb_i64_col(&bus, "source_row").values();
1876
1877 let expected_index: Vec<i64> = (0..bus.num_rows() as i64).collect();
1880 assert_eq!(index, expected_index.as_slice());
1881
1882 assert_eq!(bus_id, &[1, 2, 3, 4]);
1886 let handle_ids: Vec<i64> = n.buses.iter().map(|b| ext(b.id)).collect();
1887 assert!(
1888 !handle_ids.contains(&bus_id[bus.num_rows() - 1]),
1889 "star bus id must not collide with a handle bus id"
1890 );
1891
1892 assert_eq!(source_row[..3], [0, 1, 2]);
1894 assert_eq!(
1895 source_row[bus.num_rows() - 1],
1896 -1,
1897 "star-point bus has no source row"
1898 );
1899
1900 for table in [PIO_ARROW_TABLE_YBUS, PIO_ARROW_TABLE_BPRIME] {
1904 let rb = matrix_record_batch(&n, table);
1905 let meta = rb.schema();
1906 let meta = meta.metadata();
1907 assert_eq!(
1908 meta.get("powerio.row_count").unwrap(),
1909 &bus.num_rows().to_string(),
1910 "row_count must match matrix_bus length"
1911 );
1912 assert_eq!(
1913 meta.get("powerio.col_count").unwrap(),
1914 &bus.num_rows().to_string(),
1915 "col_count must match matrix_bus length"
1916 );
1917 let n_rows = bus.num_rows() as i64;
1918 for &r in rb_i64_col(&rb, "row_index").values() {
1919 assert!((0..n_rows).contains(&r), "COO row index within axis");
1920 }
1921 for &c in rb_i64_col(&rb, "col_index").values() {
1922 assert!((0..n_rows).contains(&c), "COO col index within axis");
1923 }
1924 }
1925 }
1926
1927 #[cfg(feature = "matrix")]
1928 #[test]
1929 fn matrix_branch_axis_matches_incidence_branch_columns() {
1930 let n = incidence_filter_net();
1931 let core = IndexCore::build(&n);
1932 let view = IndexedNetwork::with_core(&n, &core);
1933 let incidence = powerio_matrix::build_incidence(
1934 &view,
1935 powerio_matrix::DcConvention::default(),
1936 &powerio_matrix::BuildOptions::default(),
1937 )
1938 .unwrap();
1939
1940 let branch = matrix_record_batch(&n, PIO_ARROW_TABLE_MATRIX_BRANCH);
1941 let source_rows = rb_i64_col(&branch, "source_row").values().to_vec();
1942 let expected = incidence
1943 .branch_of_col
1944 .iter()
1945 .map(|&idx| i64::try_from(idx).unwrap())
1946 .collect::<Vec<_>>();
1947 assert_eq!(source_rows, expected);
1948 assert_eq!(source_rows, vec![2]);
1949
1950 let incidence_batch = matrix_record_batch(&n, PIO_ARROW_TABLE_INCIDENCE);
1951 assert_eq!(
1952 incidence_batch
1953 .schema()
1954 .metadata()
1955 .get("powerio.col_count")
1956 .unwrap(),
1957 "1"
1958 );
1959 assert_eq!(branch.num_rows(), 1);
1960 }
1961
1962 #[cfg(feature = "matrix")]
1963 #[test]
1964 fn matrix_arrow_golden_fixtures_match() {
1965 let dir =
1966 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/capi_matrix");
1967 for case_file in ["case9.m", "case30.m"] {
1968 let fixture = dir.join(case_file.replace(".m", "_arrow_coo.json"));
1969 let expected: serde_json::Value =
1970 serde_json::from_str(&std::fs::read_to_string(&fixture).unwrap()).unwrap();
1971 assert_eq!(matrix_golden_json(case_file), expected, "{case_file}");
1972 }
1973 }
1974
1975 #[cfg(feature = "matrix")]
1976 #[ignore = "rewrites committed matrix Arrow COO fixtures"]
1977 #[test]
1978 fn rewrite_matrix_arrow_golden_fixtures() {
1979 let dir =
1980 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/capi_matrix");
1981 std::fs::create_dir_all(&dir).unwrap();
1982 for case_file in ["case9.m", "case30.m"] {
1983 let fixture = dir.join(case_file.replace(".m", "_arrow_coo.json"));
1984 let text = serde_json::to_string_pretty(&matrix_golden_json(case_file)).unwrap();
1985 std::fs::write(fixture, format!("{text}\n")).unwrap();
1986 }
1987 }
1988}