1#![allow(clippy::missing_safety_doc)]
14
15use std::ffi::{CStr, CString, c_char};
16use std::panic::{AssertUnwindSafe, catch_unwind};
17
18use powerio::{IndexCore, IndexedNetwork, Network, TargetFormat};
19
20#[cfg(feature = "arrow")]
21mod arrow_export;
22#[cfg(feature = "arrow")]
23pub use arrow_export::{
24 PIO_ARROW_TABLE_BRANCH, PIO_ARROW_TABLE_BUS, PIO_ARROW_TABLE_GEN, PIO_ARROW_TABLE_LOAD,
25 PIO_ARROW_TABLE_SHUNT,
26};
27
28pub struct PioNetwork {
33 net: Network,
34 core: IndexCore,
35 warnings: Vec<String>,
36}
37
38unsafe fn copy_to_buf(buf: *mut c_char, len: usize, msg: &str) {
48 unsafe {
49 if buf.is_null() || len == 0 {
50 return;
51 }
52 let bytes = msg.as_bytes();
53 let mut n = bytes.len().min(len - 1);
54 while n > 0 && !msg.is_char_boundary(n) {
55 n -= 1;
56 }
57 std::ptr::copy_nonoverlapping(bytes.as_ptr().cast::<c_char>(), buf, n);
58 *buf.add(n) = 0;
59 }
60}
61
62unsafe fn cstr<'a>(p: *const c_char) -> Option<&'a str> {
63 unsafe {
64 if p.is_null() {
65 return None;
66 }
67 CStr::from_ptr(p).to_str().ok()
68 }
69}
70
71fn into_cstring(s: String) -> Option<*mut c_char> {
75 CString::new(s).ok().map(CString::into_raw)
76}
77
78fn finish_cstring(s: String, errbuf: *mut c_char, errlen: usize) -> *mut c_char {
82 match into_cstring(s) {
83 Some(p) => p,
84 None => {
85 unsafe { copy_to_buf(errbuf, errlen, "output contained an interior NUL byte") };
86 std::ptr::null_mut()
87 }
88 }
89}
90
91unsafe fn guard<R>(fallback: R, f: impl FnOnce() -> R) -> R {
94 catch_unwind(AssertUnwindSafe(f)).unwrap_or(fallback)
95}
96
97fn make_network(net: Network, warnings: Vec<String>) -> *mut PioNetwork {
100 let core = IndexCore::build(&net);
101 Box::into_raw(Box::new(PioNetwork {
102 net,
103 core,
104 warnings,
105 }))
106}
107
108unsafe fn finish_network(
114 errbuf: *mut c_char,
115 errlen: usize,
116 panic_msg: &str,
117 f: impl FnOnce() -> Result<(Network, Vec<String>), String>,
118) -> *mut PioNetwork {
119 unsafe {
120 match catch_unwind(AssertUnwindSafe(f)) {
121 Ok(Ok((net, warnings))) => make_network(net, warnings),
122 Ok(Err(msg)) => {
123 copy_to_buf(errbuf, errlen, &msg);
124 std::ptr::null_mut()
125 }
126 Err(_) => {
127 copy_to_buf(errbuf, errlen, panic_msg);
128 std::ptr::null_mut()
129 }
130 }
131 }
132}
133
134pub const PIO_ABI_VERSION: u32 = 3;
140
141pub const PIO_ERRBUF_MIN: usize = 256;
144
145#[unsafe(no_mangle)]
148pub extern "C" fn pio_abi_version() -> u32 {
149 PIO_ABI_VERSION
150}
151
152#[unsafe(no_mangle)]
156pub extern "C" fn pio_version() -> *const c_char {
157 concat!(env!("CARGO_PKG_VERSION"), "\0")
161 .as_ptr()
162 .cast::<c_char>()
163}
164
165fn target_format_from_c(to: *const c_char) -> Result<TargetFormat, String> {
166 let to = unsafe { cstr(to) }.ok_or_else(|| "to is NULL or not UTF-8".to_string())?;
167 to.parse::<TargetFormat>().map_err(|e| e.to_string())
168}
169
170fn optional_cstr<'a>(p: *const c_char, name: &str) -> Result<Option<&'a str>, String> {
171 if p.is_null() {
172 Ok(None)
173 } else {
174 unsafe { cstr(p) }
175 .map(Some)
176 .ok_or_else(|| format!("{name} is not UTF-8"))
177 }
178}
179
180#[unsafe(no_mangle)]
188pub unsafe extern "C" fn pio_parse_file(
189 path: *const c_char,
190 from: *const c_char,
191 errbuf: *mut c_char,
192 errlen: usize,
193) -> *mut PioNetwork {
194 unsafe {
195 finish_network(errbuf, errlen, "panic while parsing", || {
196 let path = cstr(path).ok_or_else(|| "path is NULL or not UTF-8".to_string())?;
197 let from = optional_cstr(from, "from")?;
198 powerio::parse_file(std::path::Path::new(path), from)
199 .map(|p| (p.network, p.warnings))
200 .map_err(|e| e.to_string())
201 })
202 }
203}
204
205#[unsafe(no_mangle)]
214pub unsafe extern "C" fn pio_parse_str(
215 text: *const c_char,
216 format: *const c_char,
217 errbuf: *mut c_char,
218 errlen: usize,
219) -> *mut PioNetwork {
220 unsafe {
221 finish_network(errbuf, errlen, "panic while parsing", || {
222 let text = cstr(text).ok_or_else(|| "text is NULL or not UTF-8".to_string())?;
223 let format = cstr(format).ok_or_else(|| "format is NULL or not UTF-8".to_string())?;
224 powerio::parse_str(text, format)
225 .map(|p| (p.network, p.warnings))
226 .map_err(|e| e.to_string())
227 })
228 }
229}
230
231#[unsafe(no_mangle)]
235pub unsafe extern "C" fn pio_parse_warnings(
236 net: *const PioNetwork,
237 warnbuf: *mut c_char,
238 warnlen: usize,
239) -> usize {
240 unsafe {
241 guard(0, || {
242 let Some(c) = network_ref(net) else { return 0 };
243 copy_to_buf(warnbuf, warnlen, &c.warnings.join("\n"));
244 c.warnings.len()
245 })
246 }
247}
248
249#[unsafe(no_mangle)]
252pub unsafe extern "C" fn pio_network_free(net: *mut PioNetwork) {
253 unsafe {
254 if !net.is_null() {
255 drop(Box::from_raw(net));
256 }
257 }
258}
259
260unsafe fn network_ref<'a>(net: *const PioNetwork) -> Option<&'a PioNetwork> {
261 unsafe { net.as_ref() }
262}
263
264unsafe fn view<'a>(net: *const PioNetwork) -> Option<IndexedNetwork<'a>> {
266 unsafe {
267 net.as_ref()
268 .map(|c| IndexedNetwork::with_core(&c.net, &c.core))
269 }
270}
271
272#[unsafe(no_mangle)]
280pub unsafe extern "C" fn pio_to_normalized(
281 net: *const PioNetwork,
282 errbuf: *mut c_char,
283 errlen: usize,
284) -> *mut PioNetwork {
285 unsafe {
286 finish_network(errbuf, errlen, "panic while normalizing", || {
287 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
288 c.net
289 .to_normalized()
290 .map(|n| (n, c.warnings.clone()))
291 .map_err(|e| e.to_string())
292 })
293 }
294}
295
296#[unsafe(no_mangle)]
297pub unsafe extern "C" fn pio_n_buses(net: *const PioNetwork) -> usize {
298 unsafe { guard(0, || network_ref(net).map_or(0, |c| c.net.buses.len())) }
299}
300
301#[unsafe(no_mangle)]
302pub unsafe extern "C" fn pio_n_branches(net: *const PioNetwork) -> usize {
303 unsafe { guard(0, || network_ref(net).map_or(0, |c| c.net.branches.len())) }
304}
305
306#[unsafe(no_mangle)]
307pub unsafe extern "C" fn pio_n_gens(net: *const PioNetwork) -> usize {
308 unsafe { guard(0, || network_ref(net).map_or(0, |c| c.net.generators.len())) }
309}
310
311#[unsafe(no_mangle)]
312pub unsafe extern "C" fn pio_base_mva(net: *const PioNetwork) -> f64 {
313 unsafe { guard(0.0, || network_ref(net).map_or(0.0, |c| c.net.base_mva)) }
314}
315
316#[unsafe(no_mangle)]
322pub unsafe extern "C" fn pio_reference_bus(net: *const PioNetwork) -> isize {
323 unsafe {
324 guard(-1, || match view(net) {
325 Some(v) => v
326 .reference_bus_index()
327 .map_or(-1, |i| isize::try_from(i).unwrap_or(-1)),
328 None => -1,
329 })
330 }
331}
332
333#[unsafe(no_mangle)]
337pub unsafe extern "C" fn pio_n_reference_buses(net: *const PioNetwork) -> usize {
338 unsafe {
339 guard(0, || {
340 view(net).map_or(0, |v| v.reference_bus_indices().len())
341 })
342 }
343}
344
345#[unsafe(no_mangle)]
348pub unsafe extern "C" fn pio_reference_buses(net: *const PioNetwork, out: *mut i64) {
349 unsafe {
350 guard((), || {
351 if let Some(v) = view(net) {
352 fill(
353 out,
354 v.reference_bus_indices()
355 .into_iter()
356 .map(|i| i64::try_from(i).unwrap_or(-1)),
357 );
358 }
359 })
360 }
361}
362
363#[unsafe(no_mangle)]
364pub unsafe extern "C" fn pio_n_components(net: *const PioNetwork) -> usize {
365 unsafe { guard(0, || view(net).map_or(0, |v| v.n_connected_components())) }
366}
367
368#[unsafe(no_mangle)]
370pub unsafe extern "C" fn pio_is_radial(net: *const PioNetwork) -> i32 {
371 unsafe { guard(0, || view(net).map_or(0, |v| i32::from(v.is_radial()))) }
372}
373
374#[unsafe(no_mangle)]
378pub unsafe extern "C" fn pio_to_matpower(
379 net: *const PioNetwork,
380 errbuf: *mut c_char,
381 errlen: usize,
382) -> *mut c_char {
383 unsafe {
384 let r = catch_unwind(AssertUnwindSafe(|| {
385 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
386 Ok::<_, String>(c.net.to_matpower())
387 }));
388 match r {
389 Ok(Ok(text)) => finish_cstring(text, errbuf, errlen),
390 Ok(Err(msg)) => {
391 copy_to_buf(errbuf, errlen, &msg);
392 std::ptr::null_mut()
393 }
394 Err(_) => {
395 copy_to_buf(errbuf, errlen, "panic while serializing to MATPOWER");
396 std::ptr::null_mut()
397 }
398 }
399 }
400}
401
402#[unsafe(no_mangle)]
408pub unsafe extern "C" fn pio_to_format(
409 net: *const PioNetwork,
410 to: *const c_char,
411 warnbuf: *mut c_char,
412 warnlen: usize,
413 errbuf: *mut c_char,
414 errlen: usize,
415) -> *mut c_char {
416 unsafe {
417 let r = catch_unwind(AssertUnwindSafe(|| {
418 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
419 let target = target_format_from_c(to)?;
420 let conv = c.net.to_format(target);
421 Ok::<_, String>((conv.text, conv.warnings))
422 }));
423 match r {
424 Ok(Ok((text, warnings))) => {
425 copy_to_buf(warnbuf, warnlen, &warnings.join("\n"));
426 finish_cstring(text, errbuf, errlen)
427 }
428 Ok(Err(msg)) => {
429 copy_to_buf(errbuf, errlen, &msg);
430 std::ptr::null_mut()
431 }
432 Err(_) => {
433 copy_to_buf(errbuf, errlen, "panic while converting");
434 std::ptr::null_mut()
435 }
436 }
437 }
438}
439
440#[unsafe(no_mangle)]
445pub unsafe extern "C" fn pio_convert_file(
446 path: *const c_char,
447 to: *const c_char,
448 from: *const c_char,
449 warnbuf: *mut c_char,
450 warnlen: usize,
451 errbuf: *mut c_char,
452 errlen: usize,
453) -> *mut c_char {
454 unsafe {
455 let r = catch_unwind(AssertUnwindSafe(|| {
456 let path = cstr(path).ok_or_else(|| "path is NULL or not UTF-8".to_string())?;
457 let from = optional_cstr(from, "from")?;
458 let target = target_format_from_c(to)?;
459 let conv = powerio::convert_file(std::path::Path::new(path), target, from)
460 .map_err(|e| e.to_string())?;
461 Ok::<_, String>((conv.text, conv.warnings))
462 }));
463 match r {
464 Ok(Ok((text, warnings))) => {
465 copy_to_buf(warnbuf, warnlen, &warnings.join("\n"));
466 finish_cstring(text, errbuf, errlen)
467 }
468 Ok(Err(msg)) => {
469 copy_to_buf(errbuf, errlen, &msg);
470 std::ptr::null_mut()
471 }
472 Err(_) => {
473 copy_to_buf(errbuf, errlen, "panic while converting");
474 std::ptr::null_mut()
475 }
476 }
477 }
478}
479
480#[unsafe(no_mangle)]
485pub unsafe extern "C" fn pio_write_pypsa_csv_folder(
486 net: *const PioNetwork,
487 out_dir: *const c_char,
488 warnbuf: *mut c_char,
489 warnlen: usize,
490 errbuf: *mut c_char,
491 errlen: usize,
492) -> i32 {
493 unsafe {
494 let r = catch_unwind(AssertUnwindSafe(|| {
495 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
496 let out_dir =
497 cstr(out_dir).ok_or_else(|| "out_dir is NULL or not UTF-8".to_string())?;
498 powerio::write_pypsa_csv_folder(&c.net, std::path::Path::new(out_dir))
499 .map(|outputs| outputs.warnings)
500 .map_err(|e| e.to_string())
501 }));
502 match r {
503 Ok(Ok(warnings)) => {
504 copy_to_buf(warnbuf, warnlen, &warnings.join("\n"));
505 0
506 }
507 Ok(Err(msg)) => {
508 copy_to_buf(errbuf, errlen, &msg);
509 -1
510 }
511 Err(_) => {
512 copy_to_buf(errbuf, errlen, "panic while writing PyPSA CSV folder");
513 -1
514 }
515 }
516 }
517}
518
519#[cfg(feature = "gridfm")]
527#[unsafe(no_mangle)]
528pub unsafe extern "C" fn pio_read_gridfm(
529 dir: *const c_char,
530 scenario: i64,
531 warnbuf: *mut c_char,
532 warnlen: usize,
533 errbuf: *mut c_char,
534 errlen: usize,
535) -> *mut PioNetwork {
536 unsafe {
537 let r = catch_unwind(AssertUnwindSafe(|| {
538 let dir = cstr(dir).ok_or_else(|| "dir is NULL or not UTF-8".to_string())?;
539 powerio_matrix::read_gridfm_dataset(std::path::Path::new(dir), scenario)
540 .map_err(|e| e.to_string())
541 }));
542 match r {
543 Ok(Ok(read)) => {
544 copy_to_buf(warnbuf, warnlen, &read.warnings.join("\n"));
545 make_network(read.network, read.warnings)
546 }
547 Ok(Err(msg)) => {
548 copy_to_buf(errbuf, errlen, &msg);
549 std::ptr::null_mut()
550 }
551 Err(_) => {
552 copy_to_buf(errbuf, errlen, "panic while reading gridfm dataset");
553 std::ptr::null_mut()
554 }
555 }
556 }
557}
558
559#[cfg(feature = "gridfm")]
565#[unsafe(no_mangle)]
566pub unsafe extern "C" fn pio_gridfm_scenario_ids(
567 dir: *const c_char,
568 out: *mut i64,
569 cap: usize,
570 errbuf: *mut c_char,
571 errlen: usize,
572) -> isize {
573 unsafe {
574 let r = catch_unwind(AssertUnwindSafe(|| {
575 let dir = cstr(dir).ok_or_else(|| "dir is NULL or not UTF-8".to_string())?;
576 powerio_matrix::gridfm_scenario_ids(std::path::Path::new(dir))
577 .map_err(|e| e.to_string())
578 }));
579 match r {
580 Ok(Ok(ids)) => {
581 if !out.is_null() {
582 let n = ids.len().min(cap);
583 std::ptr::copy_nonoverlapping(ids.as_ptr(), out, n);
584 }
585 ids.len() as isize
586 }
587 Ok(Err(msg)) => {
588 copy_to_buf(errbuf, errlen, &msg);
589 -1
590 }
591 Err(_) => {
592 copy_to_buf(errbuf, errlen, "panic while reading gridfm scenario ids");
593 -1
594 }
595 }
596 }
597}
598
599#[unsafe(no_mangle)]
603pub unsafe extern "C" fn pio_string_free(s: *mut c_char) {
604 unsafe {
605 if !s.is_null() {
606 drop(CString::from_raw(s));
607 }
608 }
609}
610
611#[unsafe(no_mangle)]
618pub unsafe extern "C" fn pio_to_json(
619 net: *const PioNetwork,
620 errbuf: *mut c_char,
621 errlen: usize,
622) -> *mut c_char {
623 unsafe {
624 let r = catch_unwind(AssertUnwindSafe(|| {
625 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
626 c.net.to_json().map_err(|e| e.to_string())
627 }));
628 match r {
629 Ok(Ok(json)) => finish_cstring(json, errbuf, errlen),
630 Ok(Err(msg)) => {
631 copy_to_buf(errbuf, errlen, &msg);
632 std::ptr::null_mut()
633 }
634 Err(_) => {
635 copy_to_buf(errbuf, errlen, "panic while serializing to JSON");
636 std::ptr::null_mut()
637 }
638 }
639 }
640}
641
642#[unsafe(no_mangle)]
647pub unsafe extern "C" fn pio_from_json(
648 json: *const c_char,
649 errbuf: *mut c_char,
650 errlen: usize,
651) -> *mut PioNetwork {
652 unsafe {
653 finish_network(errbuf, errlen, "panic while parsing JSON", || {
654 let json = cstr(json).ok_or_else(|| "json is NULL or not UTF-8".to_string())?;
655 Network::from_json(json)
656 .map(|n| (n, Vec::new()))
657 .map_err(|e| e.to_string())
658 })
659 }
660}
661
662unsafe fn fill<T: Copy>(ptr: *mut T, vals: impl Iterator<Item = T>) {
663 unsafe {
664 if ptr.is_null() {
665 return;
666 }
667 for (i, v) in vals.enumerate() {
668 *ptr.add(i) = v;
669 }
670 }
671}
672
673#[unsafe(no_mangle)]
675pub unsafe extern "C" fn pio_bus_ids(net: *const PioNetwork, out: *mut i64) {
676 unsafe {
677 guard((), || {
678 if let Some(c) = network_ref(net) {
679 fill(
680 out,
681 c.net
682 .buses
683 .iter()
684 .map(|b| i64::try_from(b.id.0).unwrap_or(-1)),
685 );
686 }
687 })
688 }
689}
690
691#[unsafe(no_mangle)]
696pub unsafe extern "C" fn pio_branches(
697 net: *const PioNetwork,
698 from: *mut i64,
699 to: *mut i64,
700 r: *mut f64,
701 x: *mut f64,
702 b: *mut f64,
703 tap: *mut f64,
704 shift: *mut f64,
705 in_service: *mut u8,
706) {
707 unsafe {
708 guard((), || {
709 let Some(c) = network_ref(net) else { return };
710 let net = &c.net;
711 fill(
712 from,
713 net.branches
714 .iter()
715 .map(|br| i64::try_from(br.from.0).unwrap_or(-1)),
716 );
717 fill(
718 to,
719 net.branches
720 .iter()
721 .map(|br| i64::try_from(br.to.0).unwrap_or(-1)),
722 );
723 fill(r, net.branches.iter().map(|br| br.r));
724 fill(x, net.branches.iter().map(|br| br.x));
725 fill(b, net.branches.iter().map(|br| br.b));
726 fill(tap, net.branches.iter().map(|br| br.tap));
727 fill(shift, net.branches.iter().map(|br| br.shift));
728 fill(
729 in_service,
730 net.branches.iter().map(|br| u8::from(br.in_service)),
731 );
732 })
733 }
734}
735
736#[unsafe(no_mangle)]
739pub unsafe extern "C" fn pio_gens(
740 net: *const PioNetwork,
741 bus: *mut i64,
742 pg: *mut f64,
743 pmax: *mut f64,
744 pmin: *mut f64,
745 in_service: *mut u8,
746) {
747 unsafe {
748 guard((), || {
749 let Some(c) = network_ref(net) else { return };
750 let net = &c.net;
751 fill(
752 bus,
753 net.generators
754 .iter()
755 .map(|g| i64::try_from(g.bus.0).unwrap_or(-1)),
756 );
757 fill(pg, net.generators.iter().map(|g| g.pg));
758 fill(pmax, net.generators.iter().map(|g| g.pmax));
759 fill(pmin, net.generators.iter().map(|g| g.pmin));
760 fill(
761 in_service,
762 net.generators.iter().map(|g| u8::from(g.in_service)),
763 );
764 })
765 }
766}
767
768#[unsafe(no_mangle)]
771pub unsafe extern "C" fn pio_nodal_demand(net: *const PioNetwork, pd: *mut f64, qd: *mut f64) {
772 unsafe {
773 guard((), || {
774 if let Some(v) = view(net) {
775 fill(pd, v.pd().iter().copied());
776 fill(qd, v.qd().iter().copied());
777 }
778 })
779 }
780}
781
782#[unsafe(no_mangle)]
784pub unsafe extern "C" fn pio_nodal_shunt(net: *const PioNetwork, gs: *mut f64, bs: *mut f64) {
785 unsafe {
786 guard((), || {
787 if let Some(v) = view(net) {
788 fill(gs, v.gs().iter().copied());
789 fill(bs, v.bs().iter().copied());
790 }
791 })
792 }
793}
794
795#[cfg(feature = "arrow")]
807#[unsafe(no_mangle)]
808pub unsafe extern "C" fn pio_export_arrow(
809 net: *const PioNetwork,
810 table: i32,
811 out_array: *mut arrow::ffi::FFI_ArrowArray,
812 out_schema: *mut arrow::ffi::FFI_ArrowSchema,
813 errbuf: *mut c_char,
814 errlen: usize,
815) -> i32 {
816 unsafe {
817 let r = catch_unwind(AssertUnwindSafe(|| {
818 if out_array.is_null() || out_schema.is_null() {
819 return Err("out_array or out_schema is NULL".to_string());
820 }
821 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
822 arrow_export::export(&c.net, table)
823 }));
824 match r {
825 Ok(Ok((array, schema))) => {
826 std::ptr::write(out_array, array);
831 std::ptr::write(out_schema, schema);
832 0
833 }
834 Ok(Err(msg)) => {
835 copy_to_buf(errbuf, errlen, &msg);
836 -1
837 }
838 Err(_) => {
839 copy_to_buf(errbuf, errlen, "panic while exporting Arrow");
840 -1
841 }
842 }
843 }
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849 use std::ffi::CString;
850
851 fn data_path(name: &str) -> CString {
852 CString::new(
853 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
854 .join("../tests/data")
855 .join(name)
856 .to_str()
857 .unwrap(),
858 )
859 .unwrap()
860 }
861
862 fn case9() -> *mut PioNetwork {
863 let path = CString::new(
864 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
865 .join("../tests/data/case9.m")
866 .to_str()
867 .unwrap(),
868 )
869 .unwrap();
870 let mut err = [0 as c_char; 256];
871 let c =
872 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
873 assert!(!c.is_null(), "parse returned null");
874 c
875 }
876
877 #[test]
878 fn version_surface() {
879 assert_eq!(pio_abi_version(), PIO_ABI_VERSION);
882 let v = unsafe { CStr::from_ptr(pio_version()) }.to_str().unwrap();
883 assert_eq!(v, env!("CARGO_PKG_VERSION"));
884 assert!(!v.is_empty());
885 }
886
887 #[test]
888 fn parse_query_free() {
889 let c = case9();
890 unsafe {
891 assert_eq!(pio_n_buses(c), 9);
892 assert_eq!(pio_n_branches(c), 9);
893 assert_eq!(pio_n_gens(c), 3);
894 assert_eq!(pio_base_mva(c), 100.0);
895 assert_eq!(pio_n_components(c), 1);
896 assert!(pio_reference_bus(c) >= 0);
897 assert_eq!(pio_parse_warnings(c, std::ptr::null_mut(), 0), 0);
899 pio_network_free(c);
900 }
901 }
902
903 #[test]
904 fn parse_warnings_reach_the_buffer() {
905 let path = data_path("pandapower/example.json");
908 let mut err = [0 as c_char; 256];
909 let c =
910 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
911 assert!(
912 !c.is_null(),
913 "parse failed: {}",
914 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
915 );
916 unsafe {
917 let mut warn = [0 as c_char; 4096];
918 let n = pio_parse_warnings(c, warn.as_mut_ptr(), warn.len());
919 assert!(n > 0, "expected read warnings");
920 let w = CStr::from_ptr(warn.as_ptr()).to_str().unwrap();
921 assert!(w.contains("switch"), "expected a switch warning, got {w:?}");
922 assert_eq!(
924 pio_parse_warnings(std::ptr::null(), warn.as_mut_ptr(), warn.len()),
925 0
926 );
927 pio_network_free(c);
928 }
929 }
930
931 #[test]
932 fn write_is_byte_exact() {
933 let src = std::fs::read_to_string(
934 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case9.m"),
935 )
936 .unwrap();
937 let c = case9();
938 unsafe {
939 let mut err = [0 as c_char; 256];
940 let s = pio_to_matpower(c, err.as_mut_ptr(), err.len());
941 assert!(!s.is_null());
942 let got = CStr::from_ptr(s).to_str().unwrap();
943 assert_eq!(got, src);
944 pio_string_free(s);
945
946 let null = pio_to_matpower(std::ptr::null(), err.as_mut_ptr(), err.len());
947 assert!(null.is_null());
948 assert_eq!(
949 CStr::from_ptr(err.as_ptr()).to_str().unwrap(),
950 "network handle is NULL"
951 );
952 pio_network_free(c);
953 }
954 }
955
956 #[test]
957 fn extract_branch_tables() {
958 let c = case9();
959 unsafe {
960 let nb = pio_n_branches(c);
961 let mut from = vec![0i64; nb];
962 let mut x = vec![0f64; nb];
963 pio_branches(
964 c,
965 from.as_mut_ptr(),
966 std::ptr::null_mut(),
967 std::ptr::null_mut(),
968 x.as_mut_ptr(),
969 std::ptr::null_mut(),
970 std::ptr::null_mut(),
971 std::ptr::null_mut(),
972 std::ptr::null_mut(),
973 );
974 assert!(from.iter().all(|&f| f >= 1));
977 assert!(x.iter().all(|&xx| xx > 0.0));
978 pio_network_free(c);
979 }
980 }
981
982 #[test]
983 fn convert_matpower_echo() {
984 let path = CString::new(
985 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
986 .join("../tests/data/case14.m")
987 .to_str()
988 .unwrap(),
989 )
990 .unwrap();
991 let to = CString::new("matpower").unwrap();
992 let mut warn = [0 as c_char; 256];
993 let mut err = [0 as c_char; 256];
994 unsafe {
995 let s = pio_convert_file(
996 path.as_ptr(),
997 to.as_ptr(),
998 std::ptr::null(),
999 warn.as_mut_ptr(),
1000 warn.len(),
1001 err.as_mut_ptr(),
1002 err.len(),
1003 );
1004 assert!(!s.is_null());
1005 let got = CStr::from_ptr(s).to_str().unwrap();
1006 let src = std::fs::read_to_string(
1007 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case14.m"),
1008 )
1009 .unwrap();
1010 assert_eq!(got, src);
1011 pio_string_free(s);
1012 }
1013 }
1014
1015 #[test]
1016 fn to_format_converts_live_handle() {
1017 let c = case9();
1018 let to = CString::new("powermodels-json").unwrap();
1019 let mut warn = [0 as c_char; 256];
1020 let mut err = [0 as c_char; 256];
1021 unsafe {
1022 let s = pio_to_format(
1023 c,
1024 to.as_ptr(),
1025 warn.as_mut_ptr(),
1026 warn.len(),
1027 err.as_mut_ptr(),
1028 err.len(),
1029 );
1030 assert!(!s.is_null());
1031 let text = CStr::from_ptr(s).to_str().unwrap();
1032 assert!(text.contains("\"bus\""));
1033 pio_string_free(s);
1034 pio_network_free(c);
1035 }
1036 }
1037
1038 #[test]
1039 fn parse_error_sets_message_not_null_handle() {
1040 let path = CString::new("/no/such/case.m").unwrap();
1041 let mut err = [0 as c_char; 256];
1042 let c =
1043 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
1044 assert!(c.is_null());
1045 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
1046 assert!(!msg.is_empty(), "expected an error message");
1047 }
1048
1049 #[test]
1050 fn non_utf8_from_hint_errors_instead_of_falling_back() {
1051 let path = data_path("case9.m");
1052 let to = CString::new("matpower").unwrap();
1053 let bad_from = [0xff_u8, 0];
1054 let mut err = [0 as c_char; 256];
1055 let c = unsafe {
1056 pio_parse_file(
1057 path.as_ptr(),
1058 bad_from.as_ptr().cast::<c_char>(),
1059 err.as_mut_ptr(),
1060 err.len(),
1061 )
1062 };
1063 assert!(c.is_null());
1064 assert_eq!(
1065 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap(),
1066 "from is not UTF-8"
1067 );
1068
1069 let mut warn = [0 as c_char; 256];
1070 err.fill(0);
1071 let s = unsafe {
1072 pio_convert_file(
1073 path.as_ptr(),
1074 to.as_ptr(),
1075 bad_from.as_ptr().cast::<c_char>(),
1076 warn.as_mut_ptr(),
1077 warn.len(),
1078 err.as_mut_ptr(),
1079 err.len(),
1080 )
1081 };
1082 assert!(s.is_null());
1083 assert_eq!(
1084 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap(),
1085 "from is not UTF-8"
1086 );
1087 }
1088
1089 #[test]
1090 fn extract_gen_and_nodal_tables() {
1091 let path = data_path("case30.m");
1095 let mut err = [0 as c_char; 256];
1096 let c =
1097 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
1098 assert!(!c.is_null());
1099 unsafe {
1100 let nb = pio_n_buses(c);
1101 let ng = pio_n_gens(c);
1102 assert_eq!(nb, 30);
1103 assert!(ng > 0);
1104
1105 let mut gbus = vec![-9i64; ng];
1106 let mut pmax = vec![0f64; ng];
1107 pio_gens(
1108 c,
1109 gbus.as_mut_ptr(),
1110 std::ptr::null_mut(),
1111 pmax.as_mut_ptr(),
1112 std::ptr::null_mut(),
1113 std::ptr::null_mut(),
1114 );
1115 assert!(gbus.iter().all(|&b| b >= 0 && (b as usize) < nb));
1116 assert!(pmax.iter().any(|&p| p > 0.0));
1117
1118 let mut ids = vec![0i64; nb];
1119 pio_bus_ids(c, ids.as_mut_ptr());
1120 assert!(ids.iter().all(|&id| id >= 1)); let mut pd = vec![0f64; nb];
1123 let mut qd = vec![0f64; nb];
1124 pio_nodal_demand(c, pd.as_mut_ptr(), qd.as_mut_ptr());
1125 assert!(pd.iter().sum::<f64>() > 0.0, "case30 has active demand");
1126
1127 let mut gs = vec![0f64; nb];
1128 let mut bs = vec![0f64; nb];
1129 pio_nodal_shunt(c, gs.as_mut_ptr(), bs.as_mut_ptr());
1130 assert!(gs.iter().chain(bs.iter()).all(|x| x.is_finite()));
1131
1132 pio_network_free(c);
1133 }
1134 }
1135
1136 #[test]
1137 fn null_handle_and_null_out_are_safe() {
1138 unsafe {
1141 let nil: *const PioNetwork = std::ptr::null();
1142 assert_eq!(pio_n_buses(nil), 0);
1143 assert_eq!(pio_n_branches(nil), 0);
1144 assert_eq!(pio_n_gens(nil), 0);
1145 assert_eq!(pio_base_mva(nil), 0.0);
1146 assert_eq!(pio_reference_bus(nil), -1);
1147 assert_eq!(pio_n_reference_buses(nil), 0);
1148 assert_eq!(pio_is_radial(nil), 0);
1149 assert_eq!(pio_n_components(nil), 0);
1150
1151 let mut err = [0 as c_char; 128];
1153 assert!(pio_to_normalized(nil, err.as_mut_ptr(), err.len()).is_null());
1154 let fmt = CString::new("matpower").unwrap();
1155 assert!(
1156 pio_parse_str(std::ptr::null(), fmt.as_ptr(), err.as_mut_ptr(), err.len())
1157 .is_null()
1158 );
1159
1160 let c = case9();
1161 pio_bus_ids(c, std::ptr::null_mut());
1162 pio_reference_buses(c, std::ptr::null_mut());
1163 pio_nodal_demand(c, std::ptr::null_mut(), std::ptr::null_mut());
1164 pio_gens(
1165 c,
1166 std::ptr::null_mut(),
1167 std::ptr::null_mut(),
1168 std::ptr::null_mut(),
1169 std::ptr::null_mut(),
1170 std::ptr::null_mut(),
1171 );
1172 pio_network_free(c);
1173 }
1174 }
1175
1176 #[test]
1177 fn normalized_multi_ref_is_legible() {
1178 let src = "\
1183function mpc = tworef
1184mpc.version = '2';
1185mpc.baseMVA = 100;
1186mpc.bus = [
1187\t1\t3\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
1188\t2\t3\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
1189\t3\t1\t50\t10\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
1190];
1191mpc.gen = [
1192\t1\t0\t0\t100\t-100\t1\t100\t1\t100\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0;
1193\t2\t0\t0\t100\t-100\t1\t100\t1\t300\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0;
1194];
1195mpc.branch = [
1196\t1\t2\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
1197\t2\t3\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
1198];
1199";
1200 let text = CString::new(src).unwrap();
1201 let fmt = CString::new("matpower").unwrap();
1202 let mut err = [0 as c_char; 256];
1203 unsafe {
1204 let cs = pio_parse_str(text.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len());
1205 assert!(!cs.is_null(), "parse_str returned null");
1206 let cn = pio_to_normalized(cs, err.as_mut_ptr(), err.len());
1207 assert!(!cn.is_null(), "to_normalized returned null");
1208
1209 assert_eq!(pio_n_reference_buses(cn), 2);
1210 assert_eq!(pio_reference_bus(cn), -1);
1212 let mut refs = vec![0i64; pio_n_reference_buses(cn)];
1213 pio_reference_buses(cn, refs.as_mut_ptr());
1214 assert_eq!(refs, vec![0, 1]);
1215
1216 pio_network_free(cn);
1217 pio_network_free(cs);
1218 }
1219 }
1220
1221 #[test]
1222 fn normalized_preserves_source_bus_ids() {
1223 let src = "\
1224function mpc = sparseids
1225mpc.version = '2';
1226mpc.baseMVA = 100;
1227mpc.bus = [
1228\t1\t3\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
1229\t2\t1\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
1230\t3\t1\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
1231\t4\t1\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
1232\t10\t1\t50\t10\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
1233];
1234mpc.gen = [
1235\t1\t0\t0\t100\t-100\t1\t100\t1\t200\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0;
1236];
1237mpc.branch = [
1238\t1\t2\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
1239\t2\t3\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
1240\t3\t4\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
1241\t4\t10\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
1242];
1243";
1244 let text = CString::new(src).unwrap();
1245 let fmt = CString::new("matpower").unwrap();
1246 let mut err = [0 as c_char; 256];
1247 unsafe {
1248 let cs = pio_parse_str(text.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len());
1249 assert!(!cs.is_null(), "parse_str returned null");
1250 let cn = pio_to_normalized(cs, err.as_mut_ptr(), err.len());
1251 assert!(!cn.is_null(), "to_normalized returned null");
1252
1253 let mut ids = vec![0i64; pio_n_buses(cn)];
1254 pio_bus_ids(cn, ids.as_mut_ptr());
1255 assert_eq!(ids, vec![1, 2, 3, 4, 10]);
1256
1257 let mut from = vec![0i64; pio_n_branches(cn)];
1258 let mut to = vec![0i64; pio_n_branches(cn)];
1259 pio_branches(
1260 cn,
1261 from.as_mut_ptr(),
1262 to.as_mut_ptr(),
1263 std::ptr::null_mut(),
1264 std::ptr::null_mut(),
1265 std::ptr::null_mut(),
1266 std::ptr::null_mut(),
1267 std::ptr::null_mut(),
1268 std::ptr::null_mut(),
1269 );
1270 assert_eq!((from[3], to[3]), (4, 10));
1271
1272 pio_network_free(cn);
1273 pio_network_free(cs);
1274 }
1275 }
1276
1277 #[test]
1278 fn convert_emits_warning_into_buffer() {
1279 let path = data_path("t_case9_dcline.m");
1282 let to = CString::new("psse").unwrap();
1283 let mut warn = [0 as c_char; 512];
1284 let mut err = [0 as c_char; 256];
1285 unsafe {
1286 let s = pio_convert_file(
1287 path.as_ptr(),
1288 to.as_ptr(),
1289 std::ptr::null(),
1290 warn.as_mut_ptr(),
1291 warn.len(),
1292 err.as_mut_ptr(),
1293 err.len(),
1294 );
1295 assert!(!s.is_null());
1296 let w = CStr::from_ptr(warn.as_ptr()).to_str().unwrap();
1297 assert!(
1298 w.contains("dcline"),
1299 "expected an HVDC/dcline warning, got {w:?}"
1300 );
1301 pio_string_free(s);
1302 }
1303 }
1304
1305 #[test]
1306 fn json_round_trip_preserves_structure() {
1307 let c = {
1310 let path = data_path("case30.m");
1311 let mut err = [0 as c_char; 256];
1312 let h = unsafe {
1313 pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len())
1314 };
1315 assert!(!h.is_null());
1316 h
1317 };
1318 unsafe {
1319 let mut err = [0 as c_char; 256];
1320 let json = pio_to_json(c, err.as_mut_ptr(), err.len());
1321 assert!(!json.is_null(), "to_json returned null");
1322 let text = CStr::from_ptr(json).to_str().unwrap().to_owned();
1323 assert!(text.contains("\"buses\""));
1324
1325 let back = pio_from_json(json.cast_const(), err.as_mut_ptr(), err.len());
1326 assert!(!back.is_null(), "from_json returned null");
1327 assert_eq!(pio_n_buses(back), pio_n_buses(c));
1329 assert_eq!(pio_n_branches(back), pio_n_branches(c));
1330 assert_eq!(pio_n_gens(back), pio_n_gens(c));
1331 assert_eq!(pio_base_mva(back), pio_base_mva(c));
1332 assert_eq!(pio_reference_bus(back), pio_reference_bus(c));
1333
1334 pio_string_free(json);
1335 pio_network_free(back);
1336 pio_network_free(c);
1337 }
1338 }
1339
1340 #[test]
1341 fn from_json_rejects_garbage() {
1342 let bad = CString::new("{ not json").unwrap();
1343 let mut err = [0 as c_char; 256];
1344 let h = unsafe { pio_from_json(bad.as_ptr(), err.as_mut_ptr(), err.len()) };
1345 assert!(h.is_null());
1346 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
1347 assert!(!msg.is_empty(), "expected a JSON parse error message");
1348 }
1349
1350 #[test]
1351 fn to_json_null_handle_is_safe() {
1352 let mut err = [0 as c_char; 256];
1353 let s = unsafe { pio_to_json(std::ptr::null(), err.as_mut_ptr(), err.len()) };
1354 assert!(s.is_null());
1355 }
1356
1357 #[test]
1358 fn error_buffer_truncates_and_nul_terminates() {
1359 let path = CString::new("/no/such/directory/deeply/nested/missing/case.m").unwrap();
1362 let mut err = [0x7f as c_char; 16]; let c =
1364 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
1365 assert!(c.is_null());
1366 let nul = err
1367 .iter()
1368 .position(|&b| b == 0)
1369 .expect("buffer must be NUL-terminated");
1370 assert!(nul <= 15);
1371 }
1372
1373 #[test]
1374 fn truncation_lands_on_a_utf8_char_boundary() {
1375 let mut buf = [0x7f as c_char; 6];
1379 unsafe { copy_to_buf(buf.as_mut_ptr(), buf.len(), "aé€") };
1380 let s = unsafe { CStr::from_ptr(buf.as_ptr()) }
1381 .to_str()
1382 .expect("truncated message must be valid UTF-8");
1383 assert_eq!(s, "aé");
1384
1385 let mut buf = [0x7f as c_char; 8];
1387 unsafe { copy_to_buf(buf.as_mut_ptr(), buf.len(), "aé€") };
1388 let s = unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap();
1389 assert_eq!(s, "aé€");
1390 }
1391
1392 #[cfg(feature = "arrow")]
1393 #[test]
1394 fn export_arrow_null_out_params_return_error() {
1395 let c = case9();
1397 let mut err = [0 as c_char; 256];
1398 let rc = unsafe {
1399 pio_export_arrow(
1400 c,
1401 PIO_ARROW_TABLE_BUS,
1402 std::ptr::null_mut(),
1403 std::ptr::null_mut(),
1404 err.as_mut_ptr(),
1405 err.len(),
1406 )
1407 };
1408 assert_eq!(rc, -1);
1409 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
1410 assert!(!msg.is_empty(), "expected an error message");
1411 unsafe { pio_network_free(c) };
1412 }
1413
1414 #[cfg(feature = "gridfm")]
1415 #[test]
1416 fn read_gridfm_round_trips_and_enumerates_scenarios() {
1417 use powerio_matrix::{GridfmOptions, write_gridfm_dataset};
1418 let net = powerio::parse_file(
1420 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case14.m"),
1421 None,
1422 )
1423 .unwrap()
1424 .network;
1425 let tmp = tempfile::tempdir().unwrap();
1426 let out = write_gridfm_dataset(&net, 0, tmp.path(), &GridfmOptions::default()).unwrap();
1427 let dir = CString::new(out.dir.to_str().unwrap()).unwrap();
1428
1429 let mut warn = [0 as c_char; 512];
1430 let mut err = [0 as c_char; 256];
1431 unsafe {
1432 let h = pio_read_gridfm(
1433 dir.as_ptr(),
1434 0,
1435 warn.as_mut_ptr(),
1436 warn.len(),
1437 err.as_mut_ptr(),
1438 err.len(),
1439 );
1440 assert!(
1441 !h.is_null(),
1442 "read failed: {}",
1443 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
1444 );
1445 assert_eq!(pio_n_buses(h), 14);
1446 assert!(
1448 !CStr::from_ptr(warn.as_ptr()).to_str().unwrap().is_empty(),
1449 "expected fidelity warnings"
1450 );
1451 pio_network_free(h);
1452
1453 let count = pio_gridfm_scenario_ids(
1455 dir.as_ptr(),
1456 std::ptr::null_mut(),
1457 0,
1458 err.as_mut_ptr(),
1459 err.len(),
1460 );
1461 assert_eq!(count, 1);
1462 let mut ids = [-1i64; 4];
1463 let n = pio_gridfm_scenario_ids(
1464 dir.as_ptr(),
1465 ids.as_mut_ptr(),
1466 ids.len(),
1467 err.as_mut_ptr(),
1468 err.len(),
1469 );
1470 assert_eq!(n, 1);
1471 assert_eq!(ids[0], 0);
1472
1473 let missing = CString::new(tmp.path().join("nope").to_str().unwrap()).unwrap();
1475 let bad = pio_read_gridfm(
1476 missing.as_ptr(),
1477 0,
1478 warn.as_mut_ptr(),
1479 warn.len(),
1480 err.as_mut_ptr(),
1481 err.len(),
1482 );
1483 assert!(bad.is_null());
1484 assert!(!CStr::from_ptr(err.as_ptr()).to_str().unwrap().is_empty());
1485 }
1486 }
1487}