1use std::sync::Arc;
18
19use arrow::array::{Array, ArrayRef, Float64Array, Int64Array, StructArray, UInt8Array};
20use arrow::datatypes::{Field, Schema};
21use arrow::error::ArrowError;
22use arrow::ffi::{FFI_ArrowArray, FFI_ArrowSchema, to_ffi};
23use arrow::record_batch::RecordBatch;
24use powerio::{BusId, Network, NormalizedSolverTables, SolverArcTerminal};
25
26pub const PIO_ARROW_TABLE_BUS: i32 = 0;
29pub const PIO_ARROW_TABLE_BRANCH: i32 = 1;
30pub const PIO_ARROW_TABLE_GEN: i32 = 2;
31pub const PIO_ARROW_TABLE_LOAD: i32 = 3;
32pub const PIO_ARROW_TABLE_SHUNT: i32 = 4;
33pub const PIO_ARROW_TABLE_SWITCH: i32 = 5;
34pub const PIO_ARROW_TABLE_SOLVER_BUS: i32 = 6;
35pub const PIO_ARROW_TABLE_SOLVER_LOAD: i32 = 7;
36pub const PIO_ARROW_TABLE_SOLVER_SHUNT: i32 = 8;
37pub const PIO_ARROW_TABLE_SOLVER_BRANCH: i32 = 9;
38pub const PIO_ARROW_TABLE_SOLVER_SWITCH: i32 = 10;
39pub const PIO_ARROW_TABLE_SOLVER_ARC: i32 = 11;
40pub const PIO_ARROW_TABLE_SOLVER_GEN: i32 = 12;
41pub const PIO_ARROW_TABLE_SOLVER_STORAGE: i32 = 13;
42pub const PIO_ARROW_TABLE_SOLVER_HVDC: i32 = 14;
43
44const _: () = assert!(
52 PIO_ARROW_TABLE_BUS == 0
53 && PIO_ARROW_TABLE_BRANCH == 1
54 && PIO_ARROW_TABLE_GEN == 2
55 && PIO_ARROW_TABLE_LOAD == 3
56 && PIO_ARROW_TABLE_SHUNT == 4
57 && PIO_ARROW_TABLE_SWITCH == 5
58 && PIO_ARROW_TABLE_SOLVER_BUS == 6
59 && PIO_ARROW_TABLE_SOLVER_LOAD == 7
60 && PIO_ARROW_TABLE_SOLVER_SHUNT == 8
61 && PIO_ARROW_TABLE_SOLVER_BRANCH == 9
62 && PIO_ARROW_TABLE_SOLVER_SWITCH == 10
63 && PIO_ARROW_TABLE_SOLVER_ARC == 11
64 && PIO_ARROW_TABLE_SOLVER_GEN == 12
65 && PIO_ARROW_TABLE_SOLVER_STORAGE == 13
66 && PIO_ARROW_TABLE_SOLVER_HVDC == 14
67);
68
69pub fn export(net: &Network, table: i32) -> Result<(FFI_ArrowArray, FFI_ArrowSchema), String> {
73 let rb = match table {
74 PIO_ARROW_TABLE_BUS => bus_batch(net).map_err(|e| e.to_string())?,
75 PIO_ARROW_TABLE_BRANCH => branch_batch(net).map_err(|e| e.to_string())?,
76 PIO_ARROW_TABLE_GEN => gen_batch(net).map_err(|e| e.to_string())?,
77 PIO_ARROW_TABLE_LOAD => load_batch(net).map_err(|e| e.to_string())?,
78 PIO_ARROW_TABLE_SHUNT => shunt_batch(net).map_err(|e| e.to_string())?,
79 PIO_ARROW_TABLE_SWITCH => switch_batch(net).map_err(|e| e.to_string())?,
80 PIO_ARROW_TABLE_SOLVER_BUS => {
81 solver_bus_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
82 }
83 PIO_ARROW_TABLE_SOLVER_LOAD => {
84 solver_load_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
85 }
86 PIO_ARROW_TABLE_SOLVER_SHUNT => {
87 solver_shunt_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
88 }
89 PIO_ARROW_TABLE_SOLVER_BRANCH => {
90 solver_branch_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
91 }
92 PIO_ARROW_TABLE_SOLVER_SWITCH => {
93 solver_switch_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
94 }
95 PIO_ARROW_TABLE_SOLVER_ARC => {
96 solver_arc_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
97 }
98 PIO_ARROW_TABLE_SOLVER_GEN => {
99 solver_gen_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
100 }
101 PIO_ARROW_TABLE_SOLVER_STORAGE => {
102 solver_storage_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
103 }
104 PIO_ARROW_TABLE_SOLVER_HVDC => {
105 solver_hvdc_batch(&solver_tables(net)?).map_err(|e| e.to_string())?
106 }
107 other => return Err(format!("unknown Arrow table id {other}")),
108 };
109
110 let data = StructArray::from(rb).into_data();
112 to_ffi(&data).map_err(|e| e.to_string())
113}
114
115fn solver_tables(net: &Network) -> Result<NormalizedSolverTables, String> {
116 net.to_normalized_solver_tables().map_err(|e| e.to_string())
117}
118
119fn bus_batch(net: &Network) -> Result<RecordBatch, ArrowError> {
120 let b = &net.buses;
121 batch(vec![
122 ("id", i64s(b.iter().map(|x| ext(x.id)).collect())),
123 (
124 "kind",
125 i64s(b.iter().map(|x| i64::from(x.kind as u8)).collect()),
126 ),
127 ("vm", f64s(b.iter().map(|x| x.vm).collect())),
128 ("va", f64s(b.iter().map(|x| x.va).collect())),
129 ("base_kv", f64s(b.iter().map(|x| x.base_kv).collect())),
130 ("vmax", f64s(b.iter().map(|x| x.vmax).collect())),
131 ("vmin", f64s(b.iter().map(|x| x.vmin).collect())),
132 ("area", i64s(b.iter().map(|x| usz(x.area)).collect())),
133 ("zone", i64s(b.iter().map(|x| usz(x.zone)).collect())),
134 ])
135}
136
137fn branch_batch(net: &Network) -> Result<RecordBatch, ArrowError> {
138 let br = &net.branches;
139 batch(vec![
140 ("from", i64s(br.iter().map(|x| ext(x.from)).collect())),
141 ("to", i64s(br.iter().map(|x| ext(x.to)).collect())),
142 ("r", f64s(br.iter().map(|x| x.r).collect())),
143 ("x", f64s(br.iter().map(|x| x.x).collect())),
144 (
145 "b",
146 f64s(br.iter().map(|x| x.legacy_total_charging_b()).collect()),
147 ),
148 ("rate_a", f64s(br.iter().map(|x| x.rate_a).collect())),
149 ("rate_b", f64s(br.iter().map(|x| x.rate_b).collect())),
150 ("rate_c", f64s(br.iter().map(|x| x.rate_c).collect())),
151 ("tap", f64s(br.iter().map(|x| x.tap).collect())),
152 ("shift", f64s(br.iter().map(|x| x.shift).collect())),
153 (
154 "in_service",
155 u8s(br.iter().map(|x| u8::from(x.in_service)).collect()),
156 ),
157 ("angmin", f64s(br.iter().map(|x| x.angmin).collect())),
158 ("angmax", f64s(br.iter().map(|x| x.angmax).collect())),
159 (
160 "g_fr",
161 f64s(br.iter().map(|x| x.terminal_charging().g_fr).collect()),
162 ),
163 (
164 "b_fr",
165 f64s(br.iter().map(|x| x.terminal_charging().b_fr).collect()),
166 ),
167 (
168 "g_to",
169 f64s(br.iter().map(|x| x.terminal_charging().g_to).collect()),
170 ),
171 (
172 "b_to",
173 f64s(br.iter().map(|x| x.terminal_charging().b_to).collect()),
174 ),
175 (
176 "c_rating_a",
177 f64s(
178 br.iter()
179 .map(|x| x.current_ratings.map_or(0.0, |r| r.c_rating_a))
180 .collect(),
181 ),
182 ),
183 (
184 "c_rating_b",
185 f64s(
186 br.iter()
187 .map(|x| x.current_ratings.map_or(0.0, |r| r.c_rating_b))
188 .collect(),
189 ),
190 ),
191 (
192 "c_rating_c",
193 f64s(
194 br.iter()
195 .map(|x| x.current_ratings.map_or(0.0, |r| r.c_rating_c))
196 .collect(),
197 ),
198 ),
199 (
200 "pf",
201 f64s(
202 br.iter()
203 .map(|x| x.solution.map_or(0.0, |s| s.pf))
204 .collect(),
205 ),
206 ),
207 (
208 "qf",
209 f64s(
210 br.iter()
211 .map(|x| x.solution.map_or(0.0, |s| s.qf))
212 .collect(),
213 ),
214 ),
215 (
216 "pt",
217 f64s(
218 br.iter()
219 .map(|x| x.solution.map_or(0.0, |s| s.pt))
220 .collect(),
221 ),
222 ),
223 (
224 "qt",
225 f64s(
226 br.iter()
227 .map(|x| x.solution.map_or(0.0, |s| s.qt))
228 .collect(),
229 ),
230 ),
231 ])
232}
233
234fn gen_batch(net: &Network) -> Result<RecordBatch, ArrowError> {
235 let g = &net.generators;
236 batch(vec![
237 ("bus", i64s(g.iter().map(|x| ext(x.bus)).collect())),
238 ("pg", f64s(g.iter().map(|x| x.pg).collect())),
239 ("qg", f64s(g.iter().map(|x| x.qg).collect())),
240 ("pmax", f64s(g.iter().map(|x| x.pmax).collect())),
241 ("pmin", f64s(g.iter().map(|x| x.pmin).collect())),
242 ("qmax", f64s(g.iter().map(|x| x.qmax).collect())),
243 ("qmin", f64s(g.iter().map(|x| x.qmin).collect())),
244 ("vg", f64s(g.iter().map(|x| x.vg).collect())),
245 ("mbase", f64s(g.iter().map(|x| x.mbase).collect())),
246 (
247 "in_service",
248 u8s(g.iter().map(|x| u8::from(x.in_service)).collect()),
249 ),
250 ])
251}
252
253fn load_batch(net: &Network) -> Result<RecordBatch, ArrowError> {
254 let l = &net.loads;
255 batch(vec![
256 ("bus", i64s(l.iter().map(|x| ext(x.bus)).collect())),
257 ("p", f64s(l.iter().map(|x| x.p).collect())),
258 ("q", f64s(l.iter().map(|x| x.q).collect())),
259 (
260 "in_service",
261 u8s(l.iter().map(|x| u8::from(x.in_service)).collect()),
262 ),
263 ])
264}
265
266fn shunt_batch(net: &Network) -> Result<RecordBatch, ArrowError> {
267 let s = &net.shunts;
268 batch(vec![
269 ("bus", i64s(s.iter().map(|x| ext(x.bus)).collect())),
270 ("g", f64s(s.iter().map(|x| x.g).collect())),
271 ("b", f64s(s.iter().map(|x| x.b).collect())),
272 (
273 "in_service",
274 u8s(s.iter().map(|x| u8::from(x.in_service)).collect()),
275 ),
276 ])
277}
278
279fn switch_batch(net: &Network) -> Result<RecordBatch, ArrowError> {
280 let s = &net.switches;
281 batch(vec![
282 ("from", i64s(s.iter().map(|x| ext(x.from)).collect())),
283 ("to", i64s(s.iter().map(|x| ext(x.to)).collect())),
284 (
285 "closed",
286 u8s(s.iter().map(|x| u8::from(x.closed)).collect()),
287 ),
288 (
289 "thermal_rating",
290 f64s(s.iter().map(|x| x.thermal_rating.unwrap_or(0.0)).collect()),
291 ),
292 (
293 "current_rating",
294 f64s(s.iter().map(|x| x.current_rating.unwrap_or(0.0)).collect()),
295 ),
296 ("pf", f64s(s.iter().map(|x| x.pf.unwrap_or(0.0)).collect())),
297 ("qf", f64s(s.iter().map(|x| x.qf.unwrap_or(0.0)).collect())),
298 ("pt", f64s(s.iter().map(|x| x.pt.unwrap_or(0.0)).collect())),
299 ("qt", f64s(s.iter().map(|x| x.qt.unwrap_or(0.0)).collect())),
300 ])
301}
302
303fn solver_bus_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
304 batch(vec![
305 (
306 "index",
307 i64s(t.buses.iter().map(|x| usz(x.index)).collect()),
308 ),
309 (
310 "bus_id",
311 i64s(t.buses.iter().map(|x| ext(x.bus_id)).collect()),
312 ),
313 (
314 "source_row",
315 i64s(t.buses.iter().map(|x| opt_usz(x.source_row)).collect()),
316 ),
317 (
318 "kind",
319 i64s(t.buses.iter().map(|x| i64::from(x.kind as u8)).collect()),
320 ),
321 ("vm", f64s(t.buses.iter().map(|x| x.vm).collect())),
322 ("va", f64s(t.buses.iter().map(|x| x.va).collect())),
323 ("base_kv", f64s(t.buses.iter().map(|x| x.base_kv).collect())),
324 ("vmax", f64s(t.buses.iter().map(|x| x.vmax).collect())),
325 ("vmin", f64s(t.buses.iter().map(|x| x.vmin).collect())),
326 ("pd", f64s(t.buses.iter().map(|x| x.pd).collect())),
327 ("qd", f64s(t.buses.iter().map(|x| x.qd).collect())),
328 ("gs", f64s(t.buses.iter().map(|x| x.gs).collect())),
329 ("bs", f64s(t.buses.iter().map(|x| x.bs).collect())),
330 (
331 "component_label",
332 i64s(t.index.component_labels.iter().map(|&x| usz(x)).collect()),
333 ),
334 (
335 "is_reference",
336 u8s(t
337 .buses
338 .iter()
339 .map(|x| u8::from(t.index.reference_bus_indices.contains(&x.index)))
340 .collect()),
341 ),
342 ])
343}
344
345fn solver_load_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
346 batch(vec![
347 (
348 "index",
349 i64s(t.loads.iter().map(|x| usz(x.index)).collect()),
350 ),
351 (
352 "source_row",
353 i64s(t.loads.iter().map(|x| opt_usz(x.source_row)).collect()),
354 ),
355 (
356 "bus_index",
357 i64s(t.loads.iter().map(|x| usz(x.bus_index)).collect()),
358 ),
359 ("p", f64s(t.loads.iter().map(|x| x.p).collect())),
360 ("q", f64s(t.loads.iter().map(|x| x.q).collect())),
361 ])
362}
363
364fn solver_shunt_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
365 batch(vec![
366 (
367 "index",
368 i64s(t.shunts.iter().map(|x| usz(x.index)).collect()),
369 ),
370 (
371 "source_row",
372 i64s(t.shunts.iter().map(|x| opt_usz(x.source_row)).collect()),
373 ),
374 (
375 "bus_index",
376 i64s(t.shunts.iter().map(|x| usz(x.bus_index)).collect()),
377 ),
378 ("g", f64s(t.shunts.iter().map(|x| x.g).collect())),
379 ("b", f64s(t.shunts.iter().map(|x| x.b).collect())),
380 ])
381}
382
383fn solver_branch_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
384 batch(vec![
385 (
386 "index",
387 i64s(t.branches.iter().map(|x| usz(x.index)).collect()),
388 ),
389 (
390 "source_row",
391 i64s(t.branches.iter().map(|x| opt_usz(x.source_row)).collect()),
392 ),
393 (
394 "from_bus_index",
395 i64s(t.branches.iter().map(|x| usz(x.from_bus_index)).collect()),
396 ),
397 (
398 "to_bus_index",
399 i64s(t.branches.iter().map(|x| usz(x.to_bus_index)).collect()),
400 ),
401 ("r", f64s(t.branches.iter().map(|x| x.r).collect())),
402 ("x", f64s(t.branches.iter().map(|x| x.x).collect())),
403 ("b", f64s(t.branches.iter().map(|x| x.b).collect())),
404 ("g_fr", f64s(t.branches.iter().map(|x| x.g_fr).collect())),
405 ("b_fr", f64s(t.branches.iter().map(|x| x.b_fr).collect())),
406 ("g_to", f64s(t.branches.iter().map(|x| x.g_to).collect())),
407 ("b_to", f64s(t.branches.iter().map(|x| x.b_to).collect())),
408 (
409 "rate_a",
410 f64s(t.branches.iter().map(|x| x.rate_a).collect()),
411 ),
412 (
413 "rate_b",
414 f64s(t.branches.iter().map(|x| x.rate_b).collect()),
415 ),
416 (
417 "rate_c",
418 f64s(t.branches.iter().map(|x| x.rate_c).collect()),
419 ),
420 ("tap", f64s(t.branches.iter().map(|x| x.tap).collect())),
421 ("shift", f64s(t.branches.iter().map(|x| x.shift).collect())),
422 (
423 "angmin",
424 f64s(t.branches.iter().map(|x| x.angmin).collect()),
425 ),
426 (
427 "angmax",
428 f64s(t.branches.iter().map(|x| x.angmax).collect()),
429 ),
430 ])
431}
432
433fn solver_switch_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
434 batch(vec![
435 (
436 "index",
437 i64s(t.switches.iter().map(|x| usz(x.index)).collect()),
438 ),
439 (
440 "source_row",
441 i64s(t.switches.iter().map(|x| opt_usz(x.source_row)).collect()),
442 ),
443 (
444 "from_bus_index",
445 i64s(t.switches.iter().map(|x| usz(x.from_bus_index)).collect()),
446 ),
447 (
448 "to_bus_index",
449 i64s(t.switches.iter().map(|x| usz(x.to_bus_index)).collect()),
450 ),
451 (
452 "closed",
453 u8s(t.switches.iter().map(|x| u8::from(x.closed)).collect()),
454 ),
455 (
456 "thermal_rating",
457 f64s(
458 t.switches
459 .iter()
460 .map(|x| x.thermal_rating.unwrap_or(0.0))
461 .collect(),
462 ),
463 ),
464 (
465 "current_rating",
466 f64s(
467 t.switches
468 .iter()
469 .map(|x| x.current_rating.unwrap_or(0.0))
470 .collect(),
471 ),
472 ),
473 (
474 "pf",
475 f64s(t.switches.iter().map(|x| x.pf.unwrap_or(0.0)).collect()),
476 ),
477 (
478 "qf",
479 f64s(t.switches.iter().map(|x| x.qf.unwrap_or(0.0)).collect()),
480 ),
481 (
482 "pt",
483 f64s(t.switches.iter().map(|x| x.pt.unwrap_or(0.0)).collect()),
484 ),
485 (
486 "qt",
487 f64s(t.switches.iter().map(|x| x.qt.unwrap_or(0.0)).collect()),
488 ),
489 ])
490}
491
492fn solver_arc_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
493 batch(vec![
494 ("index", i64s(t.arcs.iter().map(|x| usz(x.index)).collect())),
495 (
496 "branch_index",
497 i64s(t.arcs.iter().map(|x| usz(x.branch_index)).collect()),
498 ),
499 (
500 "terminal",
501 i64s(
502 t.arcs
503 .iter()
504 .map(|x| match x.terminal {
505 SolverArcTerminal::From => 0,
506 SolverArcTerminal::To => 1,
507 })
508 .collect(),
509 ),
510 ),
511 (
512 "from_bus_index",
513 i64s(t.arcs.iter().map(|x| usz(x.from_bus_index)).collect()),
514 ),
515 (
516 "to_bus_index",
517 i64s(t.arcs.iter().map(|x| usz(x.to_bus_index)).collect()),
518 ),
519 ("tap", f64s(t.arcs.iter().map(|x| x.tap).collect())),
520 ("shift", f64s(t.arcs.iter().map(|x| x.shift).collect())),
521 ("g_shunt", f64s(t.arcs.iter().map(|x| x.g_shunt).collect())),
522 ("b_shunt", f64s(t.arcs.iter().map(|x| x.b_shunt).collect())),
523 ("rate_a", f64s(t.arcs.iter().map(|x| x.rate_a).collect())),
524 ])
525}
526
527fn solver_gen_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
528 batch(vec![
529 (
530 "index",
531 i64s(t.generators.iter().map(|x| usz(x.index)).collect()),
532 ),
533 (
534 "source_row",
535 i64s(t.generators.iter().map(|x| opt_usz(x.source_row)).collect()),
536 ),
537 (
538 "bus_index",
539 i64s(t.generators.iter().map(|x| usz(x.bus_index)).collect()),
540 ),
541 ("pg", f64s(t.generators.iter().map(|x| x.pg).collect())),
542 ("qg", f64s(t.generators.iter().map(|x| x.qg).collect())),
543 ("pmax", f64s(t.generators.iter().map(|x| x.pmax).collect())),
544 ("pmin", f64s(t.generators.iter().map(|x| x.pmin).collect())),
545 ("qmax", f64s(t.generators.iter().map(|x| x.qmax).collect())),
546 ("qmin", f64s(t.generators.iter().map(|x| x.qmin).collect())),
547 ("vg", f64s(t.generators.iter().map(|x| x.vg).collect())),
548 (
549 "mbase",
550 f64s(t.generators.iter().map(|x| x.mbase).collect()),
551 ),
552 (
553 "regulated_bus_index",
554 i64s(
555 t.generators
556 .iter()
557 .map(|x| opt_usz(x.regulated_bus_index))
558 .collect(),
559 ),
560 ),
561 ])
562}
563
564fn solver_storage_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
565 batch(vec![
566 (
567 "index",
568 i64s(t.storage.iter().map(|x| usz(x.index)).collect()),
569 ),
570 (
571 "source_row",
572 i64s(t.storage.iter().map(|x| opt_usz(x.source_row)).collect()),
573 ),
574 (
575 "bus_index",
576 i64s(t.storage.iter().map(|x| usz(x.bus_index)).collect()),
577 ),
578 ("ps", f64s(t.storage.iter().map(|x| x.ps).collect())),
579 ("qs", f64s(t.storage.iter().map(|x| x.qs).collect())),
580 ("energy", f64s(t.storage.iter().map(|x| x.energy).collect())),
581 (
582 "energy_rating",
583 f64s(t.storage.iter().map(|x| x.energy_rating).collect()),
584 ),
585 (
586 "charge_rating",
587 f64s(t.storage.iter().map(|x| x.charge_rating).collect()),
588 ),
589 (
590 "discharge_rating",
591 f64s(t.storage.iter().map(|x| x.discharge_rating).collect()),
592 ),
593 (
594 "thermal_rating",
595 f64s(t.storage.iter().map(|x| x.thermal_rating).collect()),
596 ),
597 ("qmin", f64s(t.storage.iter().map(|x| x.qmin).collect())),
598 ("qmax", f64s(t.storage.iter().map(|x| x.qmax).collect())),
599 ("r", f64s(t.storage.iter().map(|x| x.r).collect())),
600 ("x", f64s(t.storage.iter().map(|x| x.x).collect())),
601 ("p_loss", f64s(t.storage.iter().map(|x| x.p_loss).collect())),
602 ("q_loss", f64s(t.storage.iter().map(|x| x.q_loss).collect())),
603 ])
604}
605
606fn solver_hvdc_batch(t: &NormalizedSolverTables) -> Result<RecordBatch, ArrowError> {
607 batch(vec![
608 ("index", i64s(t.hvdc.iter().map(|x| usz(x.index)).collect())),
609 (
610 "source_row",
611 i64s(t.hvdc.iter().map(|x| opt_usz(x.source_row)).collect()),
612 ),
613 (
614 "from_bus_index",
615 i64s(t.hvdc.iter().map(|x| usz(x.from_bus_index)).collect()),
616 ),
617 (
618 "to_bus_index",
619 i64s(t.hvdc.iter().map(|x| usz(x.to_bus_index)).collect()),
620 ),
621 ("pf", f64s(t.hvdc.iter().map(|x| x.pf).collect())),
622 ("pt", f64s(t.hvdc.iter().map(|x| x.pt).collect())),
623 ("qf", f64s(t.hvdc.iter().map(|x| x.qf).collect())),
624 ("qt", f64s(t.hvdc.iter().map(|x| x.qt).collect())),
625 ("vf", f64s(t.hvdc.iter().map(|x| x.vf).collect())),
626 ("vt", f64s(t.hvdc.iter().map(|x| x.vt).collect())),
627 ("pmin", f64s(t.hvdc.iter().map(|x| x.pmin).collect())),
628 ("pmax", f64s(t.hvdc.iter().map(|x| x.pmax).collect())),
629 ("qminf", f64s(t.hvdc.iter().map(|x| x.qminf).collect())),
630 ("qmaxf", f64s(t.hvdc.iter().map(|x| x.qmaxf).collect())),
631 ("qmint", f64s(t.hvdc.iter().map(|x| x.qmint).collect())),
632 ("qmaxt", f64s(t.hvdc.iter().map(|x| x.qmaxt).collect())),
633 ("loss0", f64s(t.hvdc.iter().map(|x| x.loss0).collect())),
634 ("loss1", f64s(t.hvdc.iter().map(|x| x.loss1).collect())),
635 ])
636}
637
638fn batch(cols: Vec<(&str, ArrayRef)>) -> Result<RecordBatch, ArrowError> {
639 let fields: Vec<Field> = cols
640 .iter()
641 .map(|(name, arr)| Field::new(*name, arr.data_type().clone(), false))
642 .collect();
643 let arrays: Vec<ArrayRef> = cols.into_iter().map(|(_, arr)| arr).collect();
644 RecordBatch::try_new(Arc::new(Schema::new(fields)), arrays)
645}
646
647fn ext(id: BusId) -> i64 {
649 i64::try_from(id.0).unwrap_or(-1)
650}
651
652fn usz(n: usize) -> i64 {
653 i64::try_from(n).unwrap_or(-1)
654}
655
656fn opt_usz(n: Option<usize>) -> i64 {
657 n.map_or(-1, usz)
658}
659
660fn i64s(v: Vec<i64>) -> ArrayRef {
661 Arc::new(Int64Array::from(v))
662}
663
664fn f64s(v: Vec<f64>) -> ArrayRef {
665 Arc::new(Float64Array::from(v))
666}
667
668fn u8s(v: Vec<u8>) -> ArrayRef {
669 Arc::new(UInt8Array::from(v))
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675 use arrow::ffi::from_ffi;
676
677 fn net(name: &str) -> Network {
678 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
679 .join("../tests/data")
680 .join(name);
681 powerio::parse_file(&path, None).unwrap().network
682 }
683
684 fn terminal_projection_net() -> Network {
685 use powerio::{Branch, BranchCharging, Bus, BusId, BusType};
686
687 let mut branch = Branch::new(BusId(1), BusId(2), 0.01, 0.1);
688 branch.charging = Some(BranchCharging::new(0.01, 0.02, 0.03, 0.05));
689 branch.rate_a = 100.0;
690 Network::in_memory(
691 "terminal-projection",
692 100.0,
693 vec![
694 Bus::new(BusId(1), BusType::Ref, 230.0),
695 Bus::new(BusId(2), BusType::Pq, 230.0),
696 ],
697 vec![branch],
698 )
699 }
700
701 fn round_trip(net: &Network, table: i32) -> StructArray {
702 let (array, schema) = export(net, table).unwrap();
703 let data = unsafe { from_ffi(array, &schema) }.unwrap();
705 StructArray::from(data)
706 }
707
708 fn f64_col<'a>(sa: &'a StructArray, name: &str) -> &'a Float64Array {
709 sa.column_by_name(name)
710 .unwrap()
711 .as_any()
712 .downcast_ref::<Float64Array>()
713 .unwrap()
714 }
715
716 fn i64_col<'a>(sa: &'a StructArray, name: &str) -> &'a Int64Array {
717 sa.column_by_name(name)
718 .unwrap()
719 .as_any()
720 .downcast_ref::<Int64Array>()
721 .unwrap()
722 }
723
724 #[test]
725 fn bus_table_round_trips_with_external_ids() {
726 let n = net("case9.m");
727 let sa = round_trip(&n, PIO_ARROW_TABLE_BUS);
728 assert_eq!(sa.len(), n.buses.len());
729 let ids = sa
730 .column_by_name("id")
731 .unwrap()
732 .as_any()
733 .downcast_ref::<Int64Array>()
734 .unwrap();
735 let expected: Vec<i64> = n
738 .buses
739 .iter()
740 .map(|b| i64::try_from(b.id.0).unwrap())
741 .collect();
742 assert_eq!(ids.values(), expected.as_slice());
743 }
744
745 #[test]
746 fn empty_table_exports_zero_rows() {
747 let n = net("case9.m");
750 assert_eq!(n.shunts.len(), 0);
751 assert_eq!(round_trip(&n, PIO_ARROW_TABLE_SHUNT).len(), 0);
752 }
753
754 #[test]
755 fn every_table_has_the_expected_row_count() {
756 let n = net("case30.m");
758 assert_eq!(round_trip(&n, PIO_ARROW_TABLE_BUS).len(), n.buses.len());
759 assert_eq!(
760 round_trip(&n, PIO_ARROW_TABLE_BRANCH).len(),
761 n.branches.len()
762 );
763 assert_eq!(
764 round_trip(&n, PIO_ARROW_TABLE_GEN).len(),
765 n.generators.len()
766 );
767 assert_eq!(round_trip(&n, PIO_ARROW_TABLE_LOAD).len(), n.loads.len());
768 assert_eq!(round_trip(&n, PIO_ARROW_TABLE_SHUNT).len(), n.shunts.len());
769 }
770
771 #[test]
772 fn normalized_solver_tables_export_dense_per_unit_rows() {
773 let n = net("case14.m");
774 let tables = n.to_normalized_solver_tables().unwrap();
775
776 assert_eq!(
777 round_trip(&n, PIO_ARROW_TABLE_SOLVER_BUS).len(),
778 tables.buses.len()
779 );
780 assert_eq!(
781 round_trip(&n, PIO_ARROW_TABLE_SOLVER_BRANCH).len(),
782 tables.branches.len()
783 );
784 assert_eq!(
785 round_trip(&n, PIO_ARROW_TABLE_SOLVER_ARC).len(),
786 tables.arcs.len()
787 );
788 assert_eq!(
789 round_trip(&n, PIO_ARROW_TABLE_SOLVER_GEN).len(),
790 tables.generators.len()
791 );
792
793 let bus = round_trip(&n, PIO_ARROW_TABLE_SOLVER_BUS);
794 assert_eq!(i64_col(&bus, "index").value(1), 1);
795 assert_eq!(i64_col(&bus, "bus_id").value(1), 2);
796 assert_eq!(i64_col(&bus, "source_row").value(1), 1);
797 assert!((f64_col(&bus, "pd").value(1) - 21.7 / 100.0).abs() < 1e-12);
798
799 let branch = round_trip(&n, PIO_ARROW_TABLE_SOLVER_BRANCH);
800 assert_eq!(i64_col(&branch, "from_bus_index").value(0), 0);
801 assert_eq!(i64_col(&branch, "to_bus_index").value(0), 1);
802
803 let arc = round_trip(&n, PIO_ARROW_TABLE_SOLVER_ARC);
804 assert_eq!(i64_col(&arc, "branch_index").value(0), 0);
805 assert_eq!(i64_col(&arc, "terminal").value(0), 0);
806 assert_eq!(i64_col(&arc, "branch_index").value(1), 0);
807 assert_eq!(i64_col(&arc, "terminal").value(1), 1);
808 }
809
810 #[test]
811 fn branch_table_b_is_legacy_projection() {
812 let n = terminal_projection_net();
813 let sa = round_trip(&n, PIO_ARROW_TABLE_BRANCH);
814 assert_eq!(sa.len(), 1);
815 assert!((f64_col(&sa, "b").value(0) - 0.07).abs() < 1e-12);
816 assert!((f64_col(&sa, "g_fr").value(0) - 0.01).abs() < 1e-12);
817 assert!((f64_col(&sa, "b_fr").value(0) - 0.02).abs() < 1e-12);
818 assert!((f64_col(&sa, "g_to").value(0) - 0.03).abs() < 1e-12);
819 assert!((f64_col(&sa, "b_to").value(0) - 0.05).abs() < 1e-12);
820 }
821
822 #[test]
823 fn unknown_table_id_errors() {
824 let n = net("case9.m");
825 assert!(export(&n, 99).is_err());
826 }
827}