1#![allow(clippy::missing_safety_doc)]
29
30use std::ffi::{CStr, CString, c_char};
31use std::panic::{AssertUnwindSafe, catch_unwind};
32
33use powerio::{IndexCore, IndexedNetwork, Network, TargetFormat};
34
35#[cfg(feature = "arrow")]
36mod arrow_export;
37#[cfg(feature = "arrow")]
38pub use arrow_export::{
39 PIO_ARROW_TABLE_BRANCH, PIO_ARROW_TABLE_BUS, PIO_ARROW_TABLE_GEN, PIO_ARROW_TABLE_LOAD,
40 PIO_ARROW_TABLE_SHUNT, PIO_ARROW_TABLE_SOLVER_ARC, PIO_ARROW_TABLE_SOLVER_BRANCH,
41 PIO_ARROW_TABLE_SOLVER_BUS, PIO_ARROW_TABLE_SOLVER_GEN, PIO_ARROW_TABLE_SOLVER_HVDC,
42 PIO_ARROW_TABLE_SOLVER_LOAD, PIO_ARROW_TABLE_SOLVER_SHUNT, PIO_ARROW_TABLE_SOLVER_STORAGE,
43 PIO_ARROW_TABLE_SOLVER_SWITCH, PIO_ARROW_TABLE_SWITCH,
44};
45
46pub struct PioNetwork {
51 net: Network,
52 core: IndexCore,
53 warnings: Vec<String>,
54}
55
56const _: fn() = || {
61 fn assert_send_sync<T: Send + Sync>() {}
62 assert_send_sync::<PioNetwork>();
63};
64
65unsafe fn copy_to_buf(buf: *mut c_char, len: usize, msg: &str) {
75 unsafe {
76 if buf.is_null() || len == 0 {
77 return;
78 }
79 let bytes = msg.as_bytes();
80 let mut n = bytes.len().min(len - 1);
81 while n > 0 && !msg.is_char_boundary(n) {
82 n -= 1;
83 }
84 std::ptr::copy_nonoverlapping(bytes.as_ptr().cast::<c_char>(), buf, n);
85 *buf.add(n) = 0;
86 }
87}
88
89unsafe fn cstr<'a>(p: *const c_char) -> Option<&'a str> {
90 unsafe {
91 if p.is_null() {
92 return None;
93 }
94 CStr::from_ptr(p).to_str().ok()
95 }
96}
97
98fn into_cstring(s: String) -> Option<*mut c_char> {
102 CString::new(s).ok().map(CString::into_raw)
103}
104
105fn finish_cstring(s: String, errbuf: *mut c_char, errlen: usize) -> *mut c_char {
109 match into_cstring(s) {
110 Some(p) => p,
111 None => {
112 unsafe { copy_to_buf(errbuf, errlen, "output contained an interior NUL byte") };
113 std::ptr::null_mut()
114 }
115 }
116}
117
118unsafe fn guard<R>(fallback: R, f: impl FnOnce() -> R) -> R {
121 catch_unwind(AssertUnwindSafe(f)).unwrap_or(fallback)
122}
123
124fn make_network(net: Network, warnings: Vec<String>) -> *mut PioNetwork {
127 let core = IndexCore::build(&net);
128 Box::into_raw(Box::new(PioNetwork {
129 net,
130 core,
131 warnings,
132 }))
133}
134
135unsafe fn finish_network(
141 errbuf: *mut c_char,
142 errlen: usize,
143 panic_msg: &str,
144 f: impl FnOnce() -> Result<(Network, Vec<String>), String>,
145) -> *mut PioNetwork {
146 unsafe {
147 match catch_unwind(AssertUnwindSafe(|| {
151 f().map(|(net, warnings)| make_network(net, warnings))
152 })) {
153 Ok(Ok(handle)) => handle,
154 Ok(Err(msg)) => {
155 copy_to_buf(errbuf, errlen, &msg);
156 std::ptr::null_mut()
157 }
158 Err(_) => {
159 copy_to_buf(errbuf, errlen, panic_msg);
160 std::ptr::null_mut()
161 }
162 }
163 }
164}
165
166pub const PIO_ABI_VERSION: u32 = 4;
177
178#[cfg(feature = "dist")]
183pub const PIO_DIST_ABI_VERSION: u32 = 1;
184
185pub const PIO_ERRBUF_MIN: usize = 256;
188
189#[unsafe(no_mangle)]
192pub extern "C" fn pio_abi_version() -> u32 {
193 PIO_ABI_VERSION
194}
195
196#[cfg(feature = "dist")]
200#[unsafe(no_mangle)]
201pub extern "C" fn pio_dist_abi_version() -> u32 {
202 PIO_DIST_ABI_VERSION
203}
204
205#[unsafe(no_mangle)]
214pub unsafe extern "C" fn pio_has_feature(feature: *const c_char) -> i32 {
215 unsafe {
216 guard(0, || {
217 let Some(name) = cstr(feature) else { return 0 };
218 let features: &[(&str, bool)] = &[
219 ("arrow", cfg!(feature = "arrow")),
220 ("gridfm", cfg!(feature = "gridfm")),
221 ("dist", cfg!(feature = "dist")),
222 ("pkg", cfg!(feature = "pkg")),
223 ];
224 i32::from(features.iter().any(|&(n, on)| n == name && on))
225 })
226 }
227}
228
229#[unsafe(no_mangle)]
233pub extern "C" fn pio_version() -> *const c_char {
234 concat!(env!("CARGO_PKG_VERSION"), "\0")
238 .as_ptr()
239 .cast::<c_char>()
240}
241
242fn target_format_from_c(to: *const c_char) -> Result<TargetFormat, String> {
243 let to = unsafe { cstr(to) }.ok_or_else(|| "to is NULL or not UTF-8".to_string())?;
244 to.parse::<TargetFormat>().map_err(|e| e.to_string())
245}
246
247fn optional_cstr<'a>(p: *const c_char, name: &str) -> Result<Option<&'a str>, String> {
248 if p.is_null() {
249 Ok(None)
250 } else {
251 unsafe { cstr(p) }
252 .map(Some)
253 .ok_or_else(|| format!("{name} is not UTF-8"))
254 }
255}
256
257#[cfg(any(feature = "dist", feature = "pkg"))]
260fn required_cstr<'a>(p: *const c_char, name: &str) -> Result<&'a str, String> {
261 unsafe { cstr(p) }.ok_or_else(|| format!("{name} is NULL or not UTF-8"))
262}
263
264#[unsafe(no_mangle)]
273pub unsafe extern "C" fn pio_parse_file(
274 path: *const c_char,
275 from: *const c_char,
276 errbuf: *mut c_char,
277 errlen: usize,
278) -> *mut PioNetwork {
279 unsafe {
280 finish_network(errbuf, errlen, "panic while parsing", || {
281 let path = cstr(path).ok_or_else(|| "path is NULL or not UTF-8".to_string())?;
282 let from = optional_cstr(from, "from")?;
283 powerio::parse_file(std::path::Path::new(path), from)
284 .map(|p| (p.network, p.warnings))
285 .map_err(|e| e.to_string())
286 })
287 }
288}
289
290#[unsafe(no_mangle)]
301pub unsafe extern "C" fn pio_parse_str(
302 text: *const c_char,
303 format: *const c_char,
304 errbuf: *mut c_char,
305 errlen: usize,
306) -> *mut PioNetwork {
307 unsafe {
308 finish_network(errbuf, errlen, "panic while parsing", || {
309 let text = cstr(text).ok_or_else(|| "text is NULL or not UTF-8".to_string())?;
310 let format = cstr(format).ok_or_else(|| "format is NULL or not UTF-8".to_string())?;
311 powerio::parse_str(text, format)
312 .map(|p| (p.network, p.warnings))
313 .map_err(|e| e.to_string())
314 })
315 }
316}
317
318#[cfg(feature = "gridfm")]
329#[unsafe(no_mangle)]
330pub unsafe extern "C" fn pio_read_dir(
331 dir: *const c_char,
332 from: *const c_char,
333 scenario: i64,
334 errbuf: *mut c_char,
335 errlen: usize,
336) -> *mut PioNetwork {
337 unsafe {
338 finish_network(errbuf, errlen, "panic while reading dataset", || {
339 let dir = cstr(dir).ok_or_else(|| "dir is NULL or not UTF-8".to_string())?;
340 let from = cstr(from).ok_or_else(|| "from is NULL or not UTF-8".to_string())?;
341 powerio_matrix::read_dataset_dir(std::path::Path::new(dir), from, scenario)
342 .map(|read| (read.network, read.warnings))
343 .map_err(|e| e.to_string())
344 })
345 }
346}
347
348#[cfg(feature = "gridfm")]
355#[unsafe(no_mangle)]
356pub unsafe extern "C" fn pio_scenario_ids(
357 dir: *const c_char,
358 from: *const c_char,
359 out: *mut i64,
360 cap: usize,
361 errbuf: *mut c_char,
362 errlen: usize,
363) -> isize {
364 unsafe {
365 let r = catch_unwind(AssertUnwindSafe(|| {
366 let dir = cstr(dir).ok_or_else(|| "dir is NULL or not UTF-8".to_string())?;
367 let from = cstr(from).ok_or_else(|| "from is NULL or not UTF-8".to_string())?;
368 powerio_matrix::dataset_scenario_ids(std::path::Path::new(dir), from)
369 .map_err(|e| e.to_string())
370 }));
371 match r {
372 Ok(Ok(ids)) => {
373 let Ok(total) = isize::try_from(ids.len()) else {
374 copy_to_buf(errbuf, errlen, "scenario count exceeds isize");
375 return -1;
376 };
377 fill(out, cap, ids.iter().copied());
378 total
379 }
380 Ok(Err(msg)) => {
381 copy_to_buf(errbuf, errlen, &msg);
382 -1
383 }
384 Err(_) => {
385 copy_to_buf(errbuf, errlen, "panic while reading scenario ids");
386 -1
387 }
388 }
389 }
390}
391
392#[unsafe(no_mangle)]
400pub unsafe extern "C" fn pio_warnings(
401 net: *const PioNetwork,
402 warnbuf: *mut c_char,
403 warnlen: usize,
404) -> usize {
405 unsafe {
406 guard(0, || {
407 let Some(c) = network_ref(net) else { return 0 };
408 let msg = c.warnings.join("\n");
409 copy_to_buf(warnbuf, warnlen, &msg);
410 msg.len()
411 })
412 }
413}
414
415#[unsafe(no_mangle)]
418pub unsafe extern "C" fn pio_network_free(net: *mut PioNetwork) {
419 unsafe {
420 guard((), || {
424 if !net.is_null() {
425 drop(Box::from_raw(net));
426 }
427 });
428 }
429}
430
431unsafe fn network_ref<'a>(net: *const PioNetwork) -> Option<&'a PioNetwork> {
432 unsafe { net.as_ref() }
433}
434
435unsafe fn view<'a>(net: *const PioNetwork) -> Option<IndexedNetwork<'a>> {
437 unsafe {
438 net.as_ref()
439 .map(|c| IndexedNetwork::with_core(&c.net, &c.core))
440 }
441}
442
443#[unsafe(no_mangle)]
452pub unsafe extern "C" fn pio_normalize(
453 net: *const PioNetwork,
454 errbuf: *mut c_char,
455 errlen: usize,
456) -> *mut PioNetwork {
457 unsafe {
458 finish_network(errbuf, errlen, "panic while normalizing", || {
459 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
460 c.net
461 .to_normalized()
462 .map(|n| (n, c.warnings.clone()))
463 .map_err(|e| e.to_string())
464 })
465 }
466}
467
468#[unsafe(no_mangle)]
469pub unsafe extern "C" fn pio_n_buses(net: *const PioNetwork) -> usize {
470 unsafe { guard(0, || network_ref(net).map_or(0, |c| c.net.buses.len())) }
471}
472
473#[unsafe(no_mangle)]
474pub unsafe extern "C" fn pio_n_branches(net: *const PioNetwork) -> usize {
475 unsafe { guard(0, || network_ref(net).map_or(0, |c| c.net.branches.len())) }
476}
477
478#[unsafe(no_mangle)]
479pub unsafe extern "C" fn pio_n_switches(net: *const PioNetwork) -> usize {
480 unsafe { guard(0, || network_ref(net).map_or(0, |c| c.net.switches.len())) }
481}
482
483#[unsafe(no_mangle)]
484pub unsafe extern "C" fn pio_n_gens(net: *const PioNetwork) -> usize {
485 unsafe { guard(0, || network_ref(net).map_or(0, |c| c.net.generators.len())) }
486}
487
488#[unsafe(no_mangle)]
489pub unsafe extern "C" fn pio_base_mva(net: *const PioNetwork) -> f64 {
490 unsafe { guard(0.0, || network_ref(net).map_or(0.0, |c| c.net.base_mva)) }
491}
492
493#[unsafe(no_mangle)]
500pub unsafe extern "C" fn pio_ref_bus_index(net: *const PioNetwork) -> i64 {
501 unsafe {
502 guard(-1, || match view(net) {
503 Some(v) => v
504 .reference_bus_index()
505 .map_or(-1, |i| i64::try_from(i).unwrap_or(-1)),
506 None => -1,
507 })
508 }
509}
510
511#[unsafe(no_mangle)]
517pub unsafe extern "C" fn pio_ref_bus_indices(
518 net: *const PioNetwork,
519 out: *mut i64,
520 cap: usize,
521) -> usize {
522 unsafe {
523 guard(0, || {
524 view(net).map_or(0, |v| {
525 fill(
526 out,
527 cap,
528 v.reference_bus_indices()
529 .into_iter()
530 .map(|i| i64::try_from(i).unwrap_or(-1)),
531 )
532 })
533 })
534 }
535}
536
537#[unsafe(no_mangle)]
539pub unsafe extern "C" fn pio_n_islands(net: *const PioNetwork) -> usize {
540 unsafe { guard(0, || view(net).map_or(0, |v| v.n_connected_components())) }
541}
542
543#[unsafe(no_mangle)]
545pub unsafe extern "C" fn pio_is_radial(net: *const PioNetwork) -> i32 {
546 unsafe { guard(0, || view(net).map_or(0, |v| i32::from(v.is_radial()))) }
547}
548
549#[unsafe(no_mangle)]
563pub unsafe extern "C" fn pio_to_format(
564 net: *const PioNetwork,
565 to: *const c_char,
566 warnbuf: *mut c_char,
567 warnlen: usize,
568 errbuf: *mut c_char,
569 errlen: usize,
570) -> *mut c_char {
571 unsafe {
572 finish_conversion(warnbuf, warnlen, errbuf, errlen, || {
573 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
574 let target = target_format_from_c(to)?;
575 let conv = c.net.to_format(target).map_err(|e| e.to_string())?;
576 Ok((conv.text, conv.warnings))
577 })
578 }
579}
580
581unsafe fn finish_conversion(
587 warnbuf: *mut c_char,
588 warnlen: usize,
589 errbuf: *mut c_char,
590 errlen: usize,
591 f: impl FnOnce() -> Result<(String, Vec<String>), String>,
592) -> *mut c_char {
593 unsafe {
594 match catch_unwind(AssertUnwindSafe(f)) {
595 Ok(Ok((text, warnings))) => {
596 copy_to_buf(warnbuf, warnlen, &warnings.join("\n"));
597 finish_cstring(text, errbuf, errlen)
598 }
599 Ok(Err(msg)) => {
600 copy_to_buf(errbuf, errlen, &msg);
601 std::ptr::null_mut()
602 }
603 Err(_) => {
604 copy_to_buf(errbuf, errlen, "panic while converting");
605 std::ptr::null_mut()
606 }
607 }
608 }
609}
610
611#[unsafe(no_mangle)]
617pub unsafe extern "C" fn pio_convert_file(
618 path: *const c_char,
619 from: *const c_char,
620 to: *const c_char,
621 warnbuf: *mut c_char,
622 warnlen: usize,
623 errbuf: *mut c_char,
624 errlen: usize,
625) -> *mut c_char {
626 unsafe {
627 finish_conversion(warnbuf, warnlen, errbuf, errlen, || {
628 let path = cstr(path).ok_or_else(|| "path is NULL or not UTF-8".to_string())?;
629 let from = optional_cstr(from, "from")?;
630 let target = target_format_from_c(to)?;
631 let conv = powerio::convert_file(std::path::Path::new(path), target, from)
632 .map_err(|e| e.to_string())?;
633 Ok((conv.text, conv.warnings))
634 })
635 }
636}
637
638#[unsafe(no_mangle)]
644pub unsafe extern "C" fn pio_convert_str(
645 text: *const c_char,
646 from: *const c_char,
647 to: *const c_char,
648 warnbuf: *mut c_char,
649 warnlen: usize,
650 errbuf: *mut c_char,
651 errlen: usize,
652) -> *mut c_char {
653 unsafe {
654 finish_conversion(warnbuf, warnlen, errbuf, errlen, || {
655 let text = cstr(text).ok_or_else(|| "text is NULL or not UTF-8".to_string())?;
656 let from = cstr(from).ok_or_else(|| "from is NULL or not UTF-8".to_string())?;
657 let target = target_format_from_c(to)?;
658 let conv = powerio::convert_str(text, target, from).map_err(|e| e.to_string())?;
659 Ok((conv.text, conv.warnings))
660 })
661 }
662}
663
664#[unsafe(no_mangle)]
671pub unsafe extern "C" fn pio_write_dir(
672 net: *const PioNetwork,
673 to: *const c_char,
674 out_dir: *const c_char,
675 warnbuf: *mut c_char,
676 warnlen: usize,
677 errbuf: *mut c_char,
678 errlen: usize,
679) -> i32 {
680 unsafe {
681 let r = catch_unwind(AssertUnwindSafe(|| {
682 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
683 let to = cstr(to).ok_or_else(|| "to is NULL or not UTF-8".to_string())?;
684 let out_dir =
685 cstr(out_dir).ok_or_else(|| "out_dir is NULL or not UTF-8".to_string())?;
686 powerio::write_dir(&c.net, to, std::path::Path::new(out_dir)).map_err(|e| e.to_string())
687 }));
688 match r {
689 Ok(Ok(warnings)) => {
690 copy_to_buf(warnbuf, warnlen, &warnings.join("\n"));
691 0
692 }
693 Ok(Err(msg)) => {
694 copy_to_buf(errbuf, errlen, &msg);
695 -1
696 }
697 Err(_) => {
698 copy_to_buf(errbuf, errlen, "panic while writing directory");
699 -1
700 }
701 }
702 }
703}
704
705#[unsafe(no_mangle)]
708pub unsafe extern "C" fn pio_string_free(s: *mut c_char) {
709 unsafe {
710 guard((), || {
712 if !s.is_null() {
713 drop(CString::from_raw(s));
714 }
715 });
716 }
717}
718
719unsafe fn fill<T: Copy>(out: *mut T, cap: usize, vals: impl ExactSizeIterator<Item = T>) -> usize {
723 unsafe {
724 let total = vals.len();
725 if !out.is_null() {
726 for (i, v) in vals.take(cap).enumerate() {
727 *out.add(i) = v;
728 }
729 }
730 total
731 }
732}
733
734#[unsafe(no_mangle)]
741pub unsafe extern "C" fn pio_bus_ids(net: *const PioNetwork, out: *mut i64, cap: usize) -> usize {
742 unsafe {
743 guard(0, || {
744 network_ref(net).map_or(0, |c| {
745 fill(
746 out,
747 cap,
748 c.net
749 .buses
750 .iter()
751 .map(|b| i64::try_from(b.id.0).unwrap_or(-1)),
752 )
753 })
754 })
755 }
756}
757
758#[unsafe(no_mangle)]
765pub unsafe extern "C" fn pio_branches(
766 net: *const PioNetwork,
767 from: *mut i64,
768 to: *mut i64,
769 r: *mut f64,
770 x: *mut f64,
771 b: *mut f64,
772 tap: *mut f64,
773 shift: *mut f64,
774 in_service: *mut u8,
775 cap: usize,
776) -> usize {
777 unsafe {
778 guard(0, || {
779 let Some(c) = network_ref(net) else { return 0 };
780 let net = &c.net;
781 fill(
782 from,
783 cap,
784 net.branches
785 .iter()
786 .map(|br| i64::try_from(br.from.0).unwrap_or(-1)),
787 );
788 fill(
789 to,
790 cap,
791 net.branches
792 .iter()
793 .map(|br| i64::try_from(br.to.0).unwrap_or(-1)),
794 );
795 fill(r, cap, net.branches.iter().map(|br| br.r));
796 fill(x, cap, net.branches.iter().map(|br| br.x));
797 fill(
798 b,
799 cap,
800 net.branches.iter().map(|br| br.legacy_total_charging_b()),
801 );
802 fill(tap, cap, net.branches.iter().map(|br| br.tap));
803 fill(shift, cap, net.branches.iter().map(|br| br.shift));
804 fill(
805 in_service,
806 cap,
807 net.branches.iter().map(|br| u8::from(br.in_service)),
808 );
809 net.branches.len()
810 })
811 }
812}
813
814#[unsafe(no_mangle)]
817pub unsafe extern "C" fn pio_branch_charging(
818 net: *const PioNetwork,
819 g_fr: *mut f64,
820 b_fr: *mut f64,
821 g_to: *mut f64,
822 b_to: *mut f64,
823 cap: usize,
824) -> usize {
825 unsafe {
826 guard(0, || {
827 let Some(c) = network_ref(net) else { return 0 };
828 let net = &c.net;
829 fill(
830 g_fr,
831 cap,
832 net.branches.iter().map(|br| br.terminal_charging().g_fr),
833 );
834 fill(
835 b_fr,
836 cap,
837 net.branches.iter().map(|br| br.terminal_charging().b_fr),
838 );
839 fill(
840 g_to,
841 cap,
842 net.branches.iter().map(|br| br.terminal_charging().g_to),
843 );
844 fill(
845 b_to,
846 cap,
847 net.branches.iter().map(|br| br.terminal_charging().b_to),
848 );
849 net.branches.len()
850 })
851 }
852}
853
854#[unsafe(no_mangle)]
857pub unsafe extern "C" fn pio_switches(
858 net: *const PioNetwork,
859 from: *mut i64,
860 to: *mut i64,
861 closed: *mut u8,
862 thermal_rating: *mut f64,
863 current_rating: *mut f64,
864 pf: *mut f64,
865 qf: *mut f64,
866 pt: *mut f64,
867 qt: *mut f64,
868 cap: usize,
869) -> usize {
870 unsafe {
871 guard(0, || {
872 let Some(c) = network_ref(net) else { return 0 };
873 let net = &c.net;
874 fill(
875 from,
876 cap,
877 net.switches
878 .iter()
879 .map(|sw| i64::try_from(sw.from.0).unwrap_or(-1)),
880 );
881 fill(
882 to,
883 cap,
884 net.switches
885 .iter()
886 .map(|sw| i64::try_from(sw.to.0).unwrap_or(-1)),
887 );
888 fill(
889 closed,
890 cap,
891 net.switches.iter().map(|sw| u8::from(sw.closed)),
892 );
893 fill(
894 thermal_rating,
895 cap,
896 net.switches
897 .iter()
898 .map(|sw| sw.thermal_rating.unwrap_or(0.0)),
899 );
900 fill(
901 current_rating,
902 cap,
903 net.switches
904 .iter()
905 .map(|sw| sw.current_rating.unwrap_or(0.0)),
906 );
907 fill(pf, cap, net.switches.iter().map(|sw| sw.pf.unwrap_or(0.0)));
908 fill(qf, cap, net.switches.iter().map(|sw| sw.qf.unwrap_or(0.0)));
909 fill(pt, cap, net.switches.iter().map(|sw| sw.pt.unwrap_or(0.0)));
910 fill(qt, cap, net.switches.iter().map(|sw| sw.qt.unwrap_or(0.0)));
911 net.switches.len()
912 })
913 }
914}
915
916#[unsafe(no_mangle)]
920pub unsafe extern "C" fn pio_gens(
921 net: *const PioNetwork,
922 bus: *mut i64,
923 pg: *mut f64,
924 pmax: *mut f64,
925 pmin: *mut f64,
926 in_service: *mut u8,
927 cap: usize,
928) -> usize {
929 unsafe {
930 guard(0, || {
931 let Some(c) = network_ref(net) else { return 0 };
932 let net = &c.net;
933 fill(
934 bus,
935 cap,
936 net.generators
937 .iter()
938 .map(|g| i64::try_from(g.bus.0).unwrap_or(-1)),
939 );
940 fill(pg, cap, net.generators.iter().map(|g| g.pg));
941 fill(pmax, cap, net.generators.iter().map(|g| g.pmax));
942 fill(pmin, cap, net.generators.iter().map(|g| g.pmin));
943 fill(
944 in_service,
945 cap,
946 net.generators.iter().map(|g| u8::from(g.in_service)),
947 );
948 net.generators.len()
949 })
950 }
951}
952
953#[unsafe(no_mangle)]
957pub unsafe extern "C" fn pio_bus_demand(
958 net: *const PioNetwork,
959 pd: *mut f64,
960 qd: *mut f64,
961 cap: usize,
962) -> usize {
963 unsafe {
964 guard(0, || {
965 view(net).map_or(0, |v| {
966 let n = fill(pd, cap, v.pd().iter().copied());
969 fill(qd, cap, v.qd().iter().copied());
970 n
971 })
972 })
973 }
974}
975
976#[unsafe(no_mangle)]
980pub unsafe extern "C" fn pio_bus_shunt(
981 net: *const PioNetwork,
982 gs: *mut f64,
983 bs: *mut f64,
984 cap: usize,
985) -> usize {
986 unsafe {
987 guard(0, || {
988 view(net).map_or(0, |v| {
989 let n = fill(gs, cap, v.gs().iter().copied());
991 fill(bs, cap, v.bs().iter().copied());
992 n
993 })
994 })
995 }
996}
997
998#[cfg(feature = "arrow")]
1015#[unsafe(no_mangle)]
1016pub unsafe extern "C" fn pio_to_arrow(
1017 net: *const PioNetwork,
1018 table: i32,
1019 out_array: *mut arrow::ffi::FFI_ArrowArray,
1020 out_schema: *mut arrow::ffi::FFI_ArrowSchema,
1021 errbuf: *mut c_char,
1022 errlen: usize,
1023) -> i32 {
1024 unsafe {
1025 let r = catch_unwind(AssertUnwindSafe(|| {
1026 if out_array.is_null() || out_schema.is_null() {
1027 return Err("out_array or out_schema is NULL".to_string());
1028 }
1029 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
1030 arrow_export::export(&c.net, table)
1031 }));
1032 match r {
1033 Ok(Ok((array, schema))) => {
1034 std::ptr::write(out_array, array);
1039 std::ptr::write(out_schema, schema);
1040 0
1041 }
1042 Ok(Err(msg)) => {
1043 copy_to_buf(errbuf, errlen, &msg);
1044 -1
1045 }
1046 Err(_) => {
1047 copy_to_buf(errbuf, errlen, "panic while exporting Arrow");
1048 -1
1049 }
1050 }
1051 }
1052}
1053
1054#[cfg(feature = "pkg")]
1064pub struct PioPackage {
1065 package: powerio_pkg::NetworkPackage,
1066}
1067
1068#[cfg(feature = "pkg")]
1069const _: fn() = || {
1070 fn assert_send_sync<T: Send + Sync>() {}
1071 assert_send_sync::<PioPackage>();
1072};
1073
1074#[cfg(feature = "pkg")]
1075fn lowering_options(base_mva: f64) -> powerio_pkg::MulticonductorToBalancedOptions {
1076 powerio_pkg::MulticonductorToBalancedOptions {
1077 base_mva,
1078 ..Default::default()
1079 }
1080}
1081
1082#[cfg(feature = "pkg")]
1083unsafe fn finish_package(
1084 errbuf: *mut c_char,
1085 errlen: usize,
1086 panic_msg: &str,
1087 f: impl FnOnce() -> Result<powerio_pkg::NetworkPackage, String>,
1088) -> *mut PioPackage {
1089 unsafe {
1090 match catch_unwind(AssertUnwindSafe(f)) {
1091 Ok(Ok(package)) => Box::into_raw(Box::new(PioPackage { package })),
1092 Ok(Err(msg)) => {
1093 copy_to_buf(errbuf, errlen, &msg);
1094 std::ptr::null_mut()
1095 }
1096 Err(_) => {
1097 copy_to_buf(errbuf, errlen, panic_msg);
1098 std::ptr::null_mut()
1099 }
1100 }
1101 }
1102}
1103
1104#[cfg(feature = "pkg")]
1110#[unsafe(no_mangle)]
1111pub unsafe extern "C" fn pio_package_parse_file(
1112 path: *const c_char,
1113 errbuf: *mut c_char,
1114 errlen: usize,
1115) -> *mut PioPackage {
1116 unsafe {
1117 finish_package(errbuf, errlen, "panic while parsing package", || {
1118 let path = required_cstr(path, "path")?;
1119 let text = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
1120 powerio_pkg::NetworkPackage::from_json(&text).map_err(|e| e.to_string())
1121 })
1122 }
1123}
1124
1125#[cfg(feature = "pkg")]
1129#[unsafe(no_mangle)]
1130pub unsafe extern "C" fn pio_package_parse_str(
1131 text: *const c_char,
1132 errbuf: *mut c_char,
1133 errlen: usize,
1134) -> *mut PioPackage {
1135 unsafe {
1136 finish_package(errbuf, errlen, "panic while parsing package", || {
1137 let text = required_cstr(text, "text")?;
1138 powerio_pkg::NetworkPackage::from_json(text).map_err(|e| e.to_string())
1139 })
1140 }
1141}
1142
1143#[cfg(feature = "pkg")]
1146#[unsafe(no_mangle)]
1147pub unsafe extern "C" fn pio_package_free(pkg: *mut PioPackage) {
1148 unsafe {
1149 guard((), || {
1150 if !pkg.is_null() {
1151 drop(Box::from_raw(pkg));
1152 }
1153 });
1154 }
1155}
1156
1157#[cfg(feature = "pkg")]
1162unsafe fn finish_package_json(
1163 pkg: *const PioPackage,
1164 errbuf: *mut c_char,
1165 errlen: usize,
1166 panic_msg: &str,
1167 f: impl FnOnce(&PioPackage) -> Result<String, String>,
1168) -> *mut c_char {
1169 unsafe {
1170 let result = catch_unwind(AssertUnwindSafe(|| {
1171 let pkg = pkg
1172 .as_ref()
1173 .ok_or_else(|| "package handle is NULL".to_string())?;
1174 f(pkg)
1175 }));
1176 match result {
1177 Ok(Ok(text)) => finish_cstring(text, errbuf, errlen),
1178 Ok(Err(msg)) => {
1179 copy_to_buf(errbuf, errlen, &msg);
1180 std::ptr::null_mut()
1181 }
1182 Err(_) => {
1183 copy_to_buf(errbuf, errlen, panic_msg);
1184 std::ptr::null_mut()
1185 }
1186 }
1187 }
1188}
1189
1190#[cfg(feature = "pkg")]
1193#[unsafe(no_mangle)]
1194pub unsafe extern "C" fn pio_package_to_json(
1195 pkg: *const PioPackage,
1196 errbuf: *mut c_char,
1197 errlen: usize,
1198) -> *mut c_char {
1199 unsafe {
1200 finish_package_json(
1201 pkg,
1202 errbuf,
1203 errlen,
1204 "panic while serializing package",
1205 |p| p.package.to_json().map_err(|e| e.to_string()),
1206 )
1207 }
1208}
1209
1210#[cfg(feature = "pkg")]
1215#[unsafe(no_mangle)]
1216pub unsafe extern "C" fn pio_package_from_balanced_network(
1217 net: *const PioNetwork,
1218 include_solver_metadata: i32,
1219 errbuf: *mut c_char,
1220 errlen: usize,
1221) -> *mut PioPackage {
1222 unsafe {
1223 finish_package(
1224 errbuf,
1225 errlen,
1226 "panic while packaging balanced network",
1227 || {
1228 let net = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
1229 let mut package = powerio_pkg::NetworkPackage::from_balanced(net.net.clone());
1230 if include_solver_metadata != 0 {
1231 package
1232 .attach_normalized_solver_table_metadata()
1233 .map_err(|e| e.to_string())?;
1234 }
1235 Ok(package)
1236 },
1237 )
1238 }
1239}
1240
1241#[cfg(all(feature = "pkg", feature = "dist"))]
1245#[unsafe(no_mangle)]
1246pub unsafe extern "C" fn pio_package_from_multiconductor_network(
1247 net: *const PioDistNetwork,
1248 errbuf: *mut c_char,
1249 errlen: usize,
1250) -> *mut PioPackage {
1251 unsafe {
1252 finish_package(
1253 errbuf,
1254 errlen,
1255 "panic while packaging multiconductor network",
1256 || {
1257 let net = net
1258 .as_ref()
1259 .ok_or_else(|| "distribution network handle is NULL".to_string())?;
1260 Ok(powerio_pkg::NetworkPackage::from_multiconductor(
1261 net.net.clone(),
1262 ))
1263 },
1264 )
1265 }
1266}
1267
1268#[cfg(feature = "pkg")]
1277#[unsafe(no_mangle)]
1278pub unsafe extern "C" fn pio_package_validate(
1279 pkg: *mut PioPackage,
1280 errbuf: *mut c_char,
1281 errlen: usize,
1282) -> i32 {
1283 unsafe {
1284 let result = catch_unwind(AssertUnwindSafe(|| {
1285 let pkg = pkg
1286 .as_mut()
1287 .ok_or_else(|| "package handle is NULL".to_string())?;
1288 pkg.package.run_sane_validation();
1289 Ok::<_, String>(())
1290 }));
1291 match result {
1292 Ok(Ok(())) => 0,
1293 Ok(Err(msg)) => {
1294 copy_to_buf(errbuf, errlen, &msg);
1295 -1
1296 }
1297 Err(_) => {
1298 copy_to_buf(errbuf, errlen, "panic while validating package");
1299 -1
1300 }
1301 }
1302 }
1303}
1304
1305#[cfg(feature = "pkg")]
1308#[unsafe(no_mangle)]
1309pub unsafe extern "C" fn pio_package_validation_json(
1310 pkg: *const PioPackage,
1311 errbuf: *mut c_char,
1312 errlen: usize,
1313) -> *mut c_char {
1314 unsafe {
1315 finish_package_json(
1316 pkg,
1317 errbuf,
1318 errlen,
1319 "panic while reading package validation",
1320 |p| serde_json::to_string(&p.package.validation).map_err(|e| e.to_string()),
1321 )
1322 }
1323}
1324
1325#[cfg(feature = "pkg")]
1328#[unsafe(no_mangle)]
1329pub unsafe extern "C" fn pio_package_diagnostics_json(
1330 pkg: *const PioPackage,
1331 errbuf: *mut c_char,
1332 errlen: usize,
1333) -> *mut c_char {
1334 unsafe {
1335 finish_package_json(
1336 pkg,
1337 errbuf,
1338 errlen,
1339 "panic while reading package diagnostics",
1340 |p| serde_json::to_string(&p.package.diagnostics).map_err(|e| e.to_string()),
1341 )
1342 }
1343}
1344
1345#[cfg(feature = "pkg")]
1349#[unsafe(no_mangle)]
1350pub unsafe extern "C" fn pio_package_operating_points_json(
1351 pkg: *const PioPackage,
1352 errbuf: *mut c_char,
1353 errlen: usize,
1354) -> *mut c_char {
1355 unsafe {
1356 finish_package_json(
1357 pkg,
1358 errbuf,
1359 errlen,
1360 "panic while reading package operating points",
1361 |p| serde_json::to_string(&p.package.operating_points).map_err(|e| e.to_string()),
1362 )
1363 }
1364}
1365
1366#[cfg(feature = "pkg")]
1371#[unsafe(no_mangle)]
1372pub unsafe extern "C" fn pio_package_materialize_operating_point(
1373 pkg: *const PioPackage,
1374 index: i64,
1375 errbuf: *mut c_char,
1376 errlen: usize,
1377) -> *mut PioPackage {
1378 unsafe {
1379 finish_package(
1380 errbuf,
1381 errlen,
1382 "panic while materializing package operating point",
1383 || {
1384 let pkg = pkg
1385 .as_ref()
1386 .ok_or_else(|| "package handle is NULL".to_string())?;
1387 let index = usize::try_from(index)
1388 .map_err(|_| "operating point index must be non-negative".to_string())?;
1389 pkg.package
1390 .materialize_operating_point(index)
1391 .map_err(|e| e.to_string())
1392 },
1393 )
1394 }
1395}
1396
1397#[cfg(feature = "pkg")]
1401#[unsafe(no_mangle)]
1402pub unsafe extern "C" fn pio_package_multiconductor_to_balanced_preflight_json(
1403 pkg: *const PioPackage,
1404 base_mva: f64,
1405 errbuf: *mut c_char,
1406 errlen: usize,
1407) -> *mut c_char {
1408 unsafe {
1409 let result = catch_unwind(AssertUnwindSafe(|| {
1410 let pkg = pkg
1411 .as_ref()
1412 .ok_or_else(|| "package handle is NULL".to_string())?;
1413 let net = pkg.package.as_multiconductor().ok_or_else(|| {
1414 format!(
1415 "multiconductor preflight requires a multiconductor package, got {:?}",
1416 pkg.package.model_kind()
1417 )
1418 })?;
1419 let report = powerio_pkg::check_multiconductor_to_balanced_lowering(
1420 net,
1421 lowering_options(base_mva),
1422 );
1423 serde_json::to_string(&report).map_err(|e| e.to_string())
1424 }));
1425 match result {
1426 Ok(Ok(text)) => finish_cstring(text, errbuf, errlen),
1427 Ok(Err(msg)) => {
1428 copy_to_buf(errbuf, errlen, &msg);
1429 std::ptr::null_mut()
1430 }
1431 Err(_) => {
1432 copy_to_buf(errbuf, errlen, "panic while preflighting package lowering");
1433 std::ptr::null_mut()
1434 }
1435 }
1436 }
1437}
1438
1439#[cfg(feature = "pkg")]
1444#[unsafe(no_mangle)]
1445pub unsafe extern "C" fn pio_package_lower_multiconductor_to_balanced(
1446 pkg: *const PioPackage,
1447 base_mva: f64,
1448 errbuf: *mut c_char,
1449 errlen: usize,
1450) -> *mut PioPackage {
1451 unsafe {
1452 finish_package(errbuf, errlen, "panic while lowering package", || {
1453 let pkg = pkg
1454 .as_ref()
1455 .ok_or_else(|| "package handle is NULL".to_string())?;
1456 pkg.package
1457 .lower_multiconductor_to_balanced(lowering_options(base_mva))
1458 .map_err(|e| e.to_string())
1459 })
1460 }
1461}
1462
1463#[cfg(feature = "dist")]
1477unsafe fn finish_handle<H>(
1478 errbuf: *mut c_char,
1479 errlen: usize,
1480 panic_msg: &str,
1481 f: impl FnOnce() -> Result<H, String>,
1482) -> *mut H {
1483 unsafe {
1484 match catch_unwind(AssertUnwindSafe(f)) {
1485 Ok(Ok(h)) => Box::into_raw(Box::new(h)),
1486 Ok(Err(msg)) => {
1487 copy_to_buf(errbuf, errlen, &msg);
1488 std::ptr::null_mut()
1489 }
1490 Err(_) => {
1491 copy_to_buf(errbuf, errlen, panic_msg);
1492 std::ptr::null_mut()
1493 }
1494 }
1495 }
1496}
1497
1498#[cfg(feature = "dist")]
1503pub struct PioDistNetwork {
1504 net: powerio_dist::DistNetwork,
1505}
1506
1507#[cfg(feature = "dist")]
1510const _: fn() = || {
1511 fn assert_send_sync<T: Send + Sync>() {}
1512 assert_send_sync::<PioDistNetwork>();
1513};
1514
1515#[cfg(feature = "dist")]
1521#[unsafe(no_mangle)]
1522pub unsafe extern "C" fn pio_dist_parse_file(
1523 path: *const c_char,
1524 from: *const c_char,
1525 errbuf: *mut c_char,
1526 errlen: usize,
1527) -> *mut PioDistNetwork {
1528 unsafe {
1529 finish_handle(errbuf, errlen, "panic while parsing", || {
1530 let path = required_cstr(path, "path")?;
1531 let from = optional_cstr(from, "from")?;
1532 powerio_dist::parse_file(std::path::Path::new(path), from)
1533 .map(|net| PioDistNetwork { net })
1534 .map_err(|e| e.to_string())
1535 })
1536 }
1537}
1538
1539#[cfg(feature = "dist")]
1545#[unsafe(no_mangle)]
1546pub unsafe extern "C" fn pio_dist_parse_str(
1547 text: *const c_char,
1548 format: *const c_char,
1549 errbuf: *mut c_char,
1550 errlen: usize,
1551) -> *mut PioDistNetwork {
1552 unsafe {
1553 finish_handle(errbuf, errlen, "panic while parsing", || {
1554 let text = required_cstr(text, "text")?;
1555 let format = required_cstr(format, "format")?;
1556 powerio_dist::parse_str(text, format)
1557 .map(|net| PioDistNetwork { net })
1558 .map_err(|e| e.to_string())
1559 })
1560 }
1561}
1562
1563#[cfg(feature = "dist")]
1566#[unsafe(no_mangle)]
1567pub unsafe extern "C" fn pio_dist_network_free(net: *mut PioDistNetwork) {
1568 unsafe {
1569 guard((), || {
1572 if !net.is_null() {
1573 drop(Box::from_raw(net));
1574 }
1575 });
1576 }
1577}
1578
1579#[cfg(feature = "dist")]
1585#[unsafe(no_mangle)]
1586pub unsafe extern "C" fn pio_dist_warnings(
1587 net: *const PioDistNetwork,
1588 warnbuf: *mut c_char,
1589 warnlen: usize,
1590) -> usize {
1591 unsafe {
1592 guard(0, || {
1593 let Some(c) = net.as_ref() else { return 0 };
1594 let msg = c.net.warnings.join("\n");
1595 copy_to_buf(warnbuf, warnlen, &msg);
1596 msg.len()
1597 })
1598 }
1599}
1600
1601#[cfg(feature = "dist")]
1607#[unsafe(no_mangle)]
1608pub unsafe extern "C" fn pio_dist_to_format(
1609 net: *const PioDistNetwork,
1610 to: *const c_char,
1611 warnbuf: *mut c_char,
1612 warnlen: usize,
1613 errbuf: *mut c_char,
1614 errlen: usize,
1615) -> *mut c_char {
1616 unsafe {
1617 finish_conversion(warnbuf, warnlen, errbuf, errlen, || {
1618 let c = net
1619 .as_ref()
1620 .ok_or_else(|| "network handle is NULL".to_string())?;
1621 let target = dist_target_from_c(to)?;
1622 let conv = c.net.to_format(target);
1623 Ok((conv.text, conv.warnings))
1624 })
1625 }
1626}
1627
1628#[cfg(feature = "dist")]
1634#[unsafe(no_mangle)]
1635pub unsafe extern "C" fn pio_dist_convert_file(
1636 path: *const c_char,
1637 from: *const c_char,
1638 to: *const c_char,
1639 warnbuf: *mut c_char,
1640 warnlen: usize,
1641 errbuf: *mut c_char,
1642 errlen: usize,
1643) -> *mut c_char {
1644 unsafe {
1645 finish_conversion(warnbuf, warnlen, errbuf, errlen, || {
1646 let path = required_cstr(path, "path")?;
1647 let from = optional_cstr(from, "from")?;
1648 let to = dist_target_from_c(to)?;
1649 let conv = powerio_dist::convert_file(std::path::Path::new(path), to, from)
1650 .map_err(|e| e.to_string())?;
1651 Ok((conv.text, conv.warnings))
1652 })
1653 }
1654}
1655
1656#[cfg(feature = "dist")]
1663#[unsafe(no_mangle)]
1664pub unsafe extern "C" fn pio_dist_convert_str(
1665 text: *const c_char,
1666 from: *const c_char,
1667 to: *const c_char,
1668 warnbuf: *mut c_char,
1669 warnlen: usize,
1670 errbuf: *mut c_char,
1671 errlen: usize,
1672) -> *mut c_char {
1673 unsafe {
1674 finish_conversion(warnbuf, warnlen, errbuf, errlen, || {
1675 let text = required_cstr(text, "text")?;
1676 let to = dist_target_from_c(to)?;
1677 let from = required_cstr(from, "from")?;
1678 let conv = powerio_dist::convert_str(text, to, from).map_err(|e| e.to_string())?;
1679 Ok((conv.text, conv.warnings))
1680 })
1681 }
1682}
1683
1684#[cfg(feature = "dist")]
1685fn dist_target_from_c(to: *const c_char) -> Result<powerio_dist::DistTargetFormat, String> {
1686 let to = required_cstr(to, "to")?;
1687 to.parse::<powerio_dist::DistTargetFormat>()
1690 .map_err(|e| e.to_string())
1691}
1692
1693#[cfg(test)]
1694mod tests {
1695 use super::*;
1696 use std::ffi::CString;
1697
1698 fn data_path(name: &str) -> CString {
1699 CString::new(
1700 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1701 .join("../tests/data")
1702 .join(name)
1703 .to_str()
1704 .unwrap(),
1705 )
1706 .unwrap()
1707 }
1708
1709 fn close(actual: f64, expected: f64) {
1710 assert!((actual - expected).abs() < 1e-12, "{actual} != {expected}");
1711 }
1712
1713 fn case9() -> *mut PioNetwork {
1714 let path = data_path("case9.m");
1715 let mut err = [0 as c_char; 256];
1716 let c =
1717 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
1718 assert!(!c.is_null(), "parse returned null");
1719 c
1720 }
1721
1722 fn terminal_projection_case() -> *mut PioNetwork {
1723 use powerio::{Branch, BranchCharging, Bus, BusId, BusType};
1724
1725 let mut branch = Branch::new(BusId(1), BusId(2), 0.01, 0.1);
1726 branch.charging = Some(BranchCharging::new(0.01, 0.02, 0.03, 0.05));
1727 branch.rate_a = 100.0;
1728 let net = Network::in_memory(
1729 "terminal-projection",
1730 100.0,
1731 vec![
1732 Bus::new(BusId(1), BusType::Ref, 230.0),
1733 Bus::new(BusId(2), BusType::Pq, 230.0),
1734 ],
1735 vec![branch],
1736 );
1737 let text = CString::new(net.to_json().unwrap()).unwrap();
1738 let format = CString::new("powerio-json").unwrap();
1739 let mut err = [0 as c_char; 256];
1740 let c =
1741 unsafe { pio_parse_str(text.as_ptr(), format.as_ptr(), err.as_mut_ptr(), err.len()) };
1742 assert!(
1743 !c.is_null(),
1744 "parse returned null: {}",
1745 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
1746 );
1747 c
1748 }
1749
1750 unsafe fn to_format(net: *const PioNetwork, to: &str) -> String {
1752 let to = CString::new(to).unwrap();
1753 let mut warn = [0 as c_char; 512];
1754 let mut err = [0 as c_char; 256];
1755 unsafe {
1756 let s = pio_to_format(
1757 net,
1758 to.as_ptr(),
1759 warn.as_mut_ptr(),
1760 warn.len(),
1761 err.as_mut_ptr(),
1762 err.len(),
1763 );
1764 assert!(
1765 !s.is_null(),
1766 "to_format failed: {}",
1767 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
1768 );
1769 let text = CStr::from_ptr(s).to_str().unwrap().to_owned();
1770 pio_string_free(s);
1771 text
1772 }
1773 }
1774
1775 #[cfg(feature = "pkg")]
1776 unsafe fn package_json_text(pkg: *const PioPackage) -> String {
1777 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
1778 unsafe {
1779 let s = pio_package_to_json(pkg, err.as_mut_ptr(), err.len());
1780 assert!(
1781 !s.is_null(),
1782 "package to json failed: {}",
1783 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
1784 );
1785 let text = CStr::from_ptr(s).to_str().unwrap().to_owned();
1786 pio_string_free(s);
1787 text
1788 }
1789 }
1790
1791 #[cfg(feature = "pkg")]
1792 unsafe fn package_json(pkg: *const PioPackage) -> serde_json::Value {
1793 unsafe { serde_json::from_str(&package_json_text(pkg)).unwrap() }
1794 }
1795
1796 #[cfg(feature = "pkg")]
1797 unsafe fn package_report_json(
1798 f: unsafe extern "C" fn(*const PioPackage, *mut c_char, usize) -> *mut c_char,
1799 pkg: *const PioPackage,
1800 ) -> serde_json::Value {
1801 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
1802 unsafe {
1803 let s = f(pkg, err.as_mut_ptr(), err.len());
1804 assert!(
1805 !s.is_null(),
1806 "package report failed: {}",
1807 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
1808 );
1809 let text = CStr::from_ptr(s).to_str().unwrap().to_owned();
1810 pio_string_free(s);
1811 serde_json::from_str(&text).unwrap()
1812 }
1813 }
1814
1815 #[test]
1816 fn version_surface() {
1817 assert_eq!(pio_abi_version(), PIO_ABI_VERSION);
1820 assert_eq!(PIO_ABI_VERSION, 4);
1821 let v = unsafe { CStr::from_ptr(pio_version()) }.to_str().unwrap();
1822 assert_eq!(v, env!("CARGO_PKG_VERSION"));
1823 assert!(!v.is_empty());
1824 }
1825
1826 fn strip_c_comments(input: &str) -> String {
1827 let mut out = String::with_capacity(input.len());
1828 let mut chars = input.chars().peekable();
1829 let mut in_block = false;
1830 while let Some(ch) = chars.next() {
1831 if in_block {
1832 if ch == '*' && chars.peek() == Some(&'/') {
1833 chars.next();
1834 in_block = false;
1835 } else if ch == '\n' {
1836 out.push('\n');
1837 }
1838 } else if ch == '/' && chars.peek() == Some(&'*') {
1839 chars.next();
1840 in_block = true;
1841 } else if ch == '/' && chars.peek() == Some(&'/') {
1842 chars.next();
1843 for tail in chars.by_ref() {
1844 if tail == '\n' {
1845 out.push('\n');
1846 break;
1847 }
1848 }
1849 } else {
1850 out.push(ch);
1851 }
1852 }
1853 out
1854 }
1855
1856 fn collapse_ws(s: &str) -> String {
1857 s.split_whitespace().collect::<Vec<_>>().join(" ")
1858 }
1859
1860 fn c_header_abi_manifest(header: &str) -> Vec<String> {
1861 let clean = strip_c_comments(header);
1862 let mut entries = Vec::new();
1863 let mut prototype = String::new();
1864 for line in clean.lines().map(str::trim).filter(|line| !line.is_empty()) {
1865 if !prototype.is_empty() {
1866 prototype.push(' ');
1867 prototype.push_str(line);
1868 if line.ends_with(';') {
1869 entries.push(collapse_ws(&prototype));
1870 prototype.clear();
1871 }
1872 continue;
1873 }
1874
1875 if line.starts_with("#define PIO_") || line.starts_with("typedef struct Pio") {
1876 entries.push(collapse_ws(line));
1877 } else if line.contains("pio_") {
1878 if line.ends_with(';') {
1879 entries.push(collapse_ws(line));
1880 } else {
1881 prototype.push_str(line);
1882 }
1883 }
1884 }
1885 assert!(prototype.is_empty(), "unterminated prototype: {prototype}");
1886 entries
1887 }
1888
1889 fn pio_symbol_names_from_manifest(manifest: &[String]) -> Vec<String> {
1890 let mut names = std::collections::BTreeSet::new();
1891 for entry in manifest {
1892 if let Some(start) = entry.find("pio_") {
1893 let tail = &entry[start..];
1894 if let Some(end) = tail.find('(') {
1895 names.insert(tail[..end].to_string());
1896 }
1897 }
1898 }
1899 names.into_iter().collect()
1900 }
1901
1902 fn source_exported_pio_symbols(source: &str) -> Vec<String> {
1903 let mut names = std::collections::BTreeSet::new();
1904 let mut saw_no_mangle = false;
1905 for line in source.lines() {
1906 let trimmed = line.trim();
1907 if trimmed == "#[unsafe(no_mangle)]" {
1908 saw_no_mangle = true;
1909 continue;
1910 }
1911 if !trimmed.contains("extern \"C\"") {
1912 if !trimmed.is_empty()
1913 && !trimmed.starts_with("#[")
1914 && !trimmed.starts_with("//")
1915 && !trimmed.starts_with("///")
1916 {
1917 saw_no_mangle = false;
1918 }
1919 continue;
1920 }
1921 if let Some(start) = trimmed.find("fn pio_") {
1922 let tail = &trimmed[start + "fn ".len()..];
1923 let end = tail
1924 .find('(')
1925 .unwrap_or_else(|| panic!("unterminated extern fn line: {trimmed}"));
1926 let name = &tail[..end];
1927 assert!(
1928 saw_no_mangle,
1929 "{name} is exported in Rust source without #[unsafe(no_mangle)]"
1930 );
1931 names.insert(name.to_string());
1932 saw_no_mangle = false;
1933 continue;
1934 }
1935 if !trimmed.is_empty() && !trimmed.starts_with("#[") && !trimmed.starts_with("//") {
1936 saw_no_mangle = false;
1937 }
1938 }
1939 names.into_iter().collect()
1940 }
1941
1942 #[test]
1943 fn c_header_abi_manifest_is_pinned() {
1944 let actual = c_header_abi_manifest(include_str!("../include/powerio.h"));
1945 let expected = [
1946 "#define PIO_ABI_VERSION 4",
1947 "#define PIO_DIST_ABI_VERSION 1",
1948 "#define PIO_ERRBUF_MIN 256",
1949 "#define PIO_ARROW_TABLE_BUS 0",
1950 "#define PIO_ARROW_TABLE_BRANCH 1",
1951 "#define PIO_ARROW_TABLE_GEN 2",
1952 "#define PIO_ARROW_TABLE_LOAD 3",
1953 "#define PIO_ARROW_TABLE_SHUNT 4",
1954 "#define PIO_ARROW_TABLE_SWITCH 5",
1955 "#define PIO_ARROW_TABLE_SOLVER_BUS 6",
1956 "#define PIO_ARROW_TABLE_SOLVER_LOAD 7",
1957 "#define PIO_ARROW_TABLE_SOLVER_SHUNT 8",
1958 "#define PIO_ARROW_TABLE_SOLVER_BRANCH 9",
1959 "#define PIO_ARROW_TABLE_SOLVER_SWITCH 10",
1960 "#define PIO_ARROW_TABLE_SOLVER_ARC 11",
1961 "#define PIO_ARROW_TABLE_SOLVER_GEN 12",
1962 "#define PIO_ARROW_TABLE_SOLVER_STORAGE 13",
1963 "#define PIO_ARROW_TABLE_SOLVER_HVDC 14",
1964 "typedef struct PioDistNetwork PioDistNetwork;",
1965 "typedef struct PioNetwork PioNetwork;",
1966 "typedef struct PioPackage PioPackage;",
1967 "uint32_t pio_abi_version(void);",
1968 "uint32_t pio_dist_abi_version(void);",
1969 "int32_t pio_has_feature(const char *feature);",
1970 "const char *pio_version(void);",
1971 "PioNetwork *pio_parse_file(const char *path, const char *from, char *errbuf, size_t errlen);",
1972 "PioNetwork *pio_parse_str(const char *text, const char *format, char *errbuf, size_t errlen);",
1973 "PioNetwork *pio_read_dir(const char *dir, const char *from, int64_t scenario, char *errbuf, size_t errlen);",
1974 "ptrdiff_t pio_scenario_ids(const char *dir, const char *from, int64_t *out, size_t cap, char *errbuf, size_t errlen);",
1975 "size_t pio_warnings(const PioNetwork *net, char *warnbuf, size_t warnlen);",
1976 "void pio_network_free(PioNetwork *net);",
1977 "PioNetwork *pio_normalize(const PioNetwork *net, char *errbuf, size_t errlen);",
1978 "size_t pio_n_buses(const PioNetwork *net);",
1979 "size_t pio_n_branches(const PioNetwork *net);",
1980 "size_t pio_n_switches(const PioNetwork *net);",
1981 "size_t pio_n_gens(const PioNetwork *net);",
1982 "double pio_base_mva(const PioNetwork *net);",
1983 "int64_t pio_ref_bus_index(const PioNetwork *net);",
1984 "size_t pio_ref_bus_indices(const PioNetwork *net, int64_t *out, size_t cap);",
1985 "size_t pio_n_islands(const PioNetwork *net);",
1986 "int32_t pio_is_radial(const PioNetwork *net);",
1987 "char *pio_to_format(const PioNetwork *net, const char *to, char *warnbuf, size_t warnlen, char *errbuf, size_t errlen);",
1988 "char *pio_convert_file(const char *path, const char *from, const char *to, char *warnbuf, size_t warnlen, char *errbuf, size_t errlen);",
1989 "char *pio_convert_str(const char *text, const char *from, const char *to, char *warnbuf, size_t warnlen, char *errbuf, size_t errlen);",
1990 "int32_t pio_write_dir(const PioNetwork *net, const char *to, const char *out_dir, char *warnbuf, size_t warnlen, char *errbuf, size_t errlen);",
1991 "void pio_string_free(char *s);",
1992 "size_t pio_bus_ids(const PioNetwork *net, int64_t *out, size_t cap);",
1993 "size_t pio_branches(const PioNetwork *net, int64_t *from, int64_t *to, double *r, double *x, double *b, double *tap, double *shift, uint8_t *in_service, size_t cap);",
1994 "size_t pio_branch_charging(const PioNetwork *net, double *g_fr, double *b_fr, double *g_to, double *b_to, size_t cap);",
1995 "size_t pio_switches(const PioNetwork *net, int64_t *from, int64_t *to, uint8_t *closed, double *thermal_rating, double *current_rating, double *pf, double *qf, double *pt, double *qt, size_t cap);",
1996 "size_t pio_gens(const PioNetwork *net, int64_t *bus, double *pg, double *pmax, double *pmin, uint8_t *in_service, size_t cap);",
1997 "size_t pio_bus_demand(const PioNetwork *net, double *pd, double *qd, size_t cap);",
1998 "size_t pio_bus_shunt(const PioNetwork *net, double *gs, double *bs, size_t cap);",
1999 "int32_t pio_to_arrow(const PioNetwork *net, int32_t table, struct ArrowArray *out_array, struct ArrowSchema *out_schema, char *errbuf, size_t errlen);",
2000 "PioPackage *pio_package_parse_file(const char *path, char *errbuf, size_t errlen);",
2001 "PioPackage *pio_package_parse_str(const char *text, char *errbuf, size_t errlen);",
2002 "void pio_package_free(PioPackage *pkg);",
2003 "char *pio_package_to_json(const PioPackage *pkg, char *errbuf, size_t errlen);",
2004 "PioPackage *pio_package_from_balanced_network(const PioNetwork *net, int32_t include_solver_metadata, char *errbuf, size_t errlen);",
2005 "PioPackage *pio_package_from_multiconductor_network(const PioDistNetwork *net, char *errbuf, size_t errlen);",
2006 "int32_t pio_package_validate(PioPackage *pkg, char *errbuf, size_t errlen);",
2007 "char *pio_package_validation_json(const PioPackage *pkg, char *errbuf, size_t errlen);",
2008 "char *pio_package_diagnostics_json(const PioPackage *pkg, char *errbuf, size_t errlen);",
2009 "char *pio_package_operating_points_json(const PioPackage *pkg, char *errbuf, size_t errlen);",
2010 "PioPackage *pio_package_materialize_operating_point(const PioPackage *pkg, int64_t index, char *errbuf, size_t errlen);",
2011 "char *pio_package_multiconductor_to_balanced_preflight_json(const PioPackage *pkg, double base_mva, char *errbuf, size_t errlen);",
2012 "PioPackage *pio_package_lower_multiconductor_to_balanced(const PioPackage *pkg, double base_mva, char *errbuf, size_t errlen);",
2013 "PioDistNetwork *pio_dist_parse_file(const char *path, const char *from, char *errbuf, size_t errlen);",
2014 "PioDistNetwork *pio_dist_parse_str(const char *text, const char *format, char *errbuf, size_t errlen);",
2015 "void pio_dist_network_free(PioDistNetwork *net);",
2016 "size_t pio_dist_warnings(const PioDistNetwork *net, char *warnbuf, size_t warnlen);",
2017 "char *pio_dist_to_format(const PioDistNetwork *net, const char *to, char *warnbuf, size_t warnlen, char *errbuf, size_t errlen);",
2018 "char *pio_dist_convert_file(const char *path, const char *from, const char *to, char *warnbuf, size_t warnlen, char *errbuf, size_t errlen);",
2019 "char *pio_dist_convert_str(const char *text, const char *from, const char *to, char *warnbuf, size_t warnlen, char *errbuf, size_t errlen);",
2020 ]
2021 .into_iter()
2022 .map(str::to_string)
2023 .collect::<Vec<_>>();
2024 assert_eq!(actual, expected);
2025 }
2026
2027 #[test]
2028 fn c_header_and_rust_exported_symbols_match() {
2029 let manifest = c_header_abi_manifest(include_str!("../include/powerio.h"));
2030 let header_symbols = pio_symbol_names_from_manifest(&manifest);
2031 let rust_symbols = source_exported_pio_symbols(include_str!("lib.rs"));
2032 assert_eq!(rust_symbols, header_symbols);
2033 }
2034
2035 #[test]
2036 fn parse_query_free() {
2037 let c = case9();
2038 unsafe {
2039 assert_eq!(pio_n_buses(c), 9);
2040 assert_eq!(pio_n_branches(c), 9);
2041 assert_eq!(pio_n_gens(c), 3);
2042 assert_eq!(pio_base_mva(c), 100.0);
2043 assert_eq!(pio_n_islands(c), 1);
2044 assert!(pio_ref_bus_index(c) >= 0);
2045 assert_eq!(pio_warnings(c, std::ptr::null_mut(), 0), 0);
2047 pio_network_free(c);
2048 }
2049 }
2050
2051 #[test]
2052 fn warnings_size_then_fill_exactly() {
2053 let path = data_path("pandapower/example.json");
2057 let mut err = [0 as c_char; 256];
2058 let c =
2059 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
2060 assert!(
2061 !c.is_null(),
2062 "parse failed: {}",
2063 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
2064 );
2065 unsafe {
2066 let len = pio_warnings(c, std::ptr::null_mut(), 0);
2067 assert!(len > 0, "expected read warnings");
2068 let mut warn = vec![0x7f as c_char; len + 1];
2069 assert_eq!(pio_warnings(c, warn.as_mut_ptr(), warn.len()), len);
2070 let w = CStr::from_ptr(warn.as_ptr()).to_str().unwrap();
2071 assert_eq!(w.len(), len, "buffer sized from the return holds it all");
2072 assert!(w.contains("switch"), "expected a switch warning, got {w:?}");
2073 assert_eq!(
2075 pio_warnings(std::ptr::null(), warn.as_mut_ptr(), warn.len()),
2076 0
2077 );
2078 pio_network_free(c);
2079 }
2080 }
2081
2082 #[test]
2083 fn matpower_write_is_byte_exact() {
2084 let src = std::fs::read_to_string(
2085 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case9.m"),
2086 )
2087 .unwrap();
2088 let c = case9();
2089 unsafe {
2090 assert_eq!(to_format(c, "matpower"), src);
2091
2092 let to = CString::new("matpower").unwrap();
2094 let mut err = [0 as c_char; 256];
2095 let null = pio_to_format(
2096 std::ptr::null(),
2097 to.as_ptr(),
2098 std::ptr::null_mut(),
2099 0,
2100 err.as_mut_ptr(),
2101 err.len(),
2102 );
2103 assert!(null.is_null());
2104 assert_eq!(
2105 CStr::from_ptr(err.as_ptr()).to_str().unwrap(),
2106 "network handle is NULL"
2107 );
2108 pio_network_free(c);
2109 }
2110 }
2111
2112 #[test]
2113 fn extract_branch_tables() {
2114 let c = case9();
2115 unsafe {
2116 let nb = pio_branches(
2118 c,
2119 std::ptr::null_mut(),
2120 std::ptr::null_mut(),
2121 std::ptr::null_mut(),
2122 std::ptr::null_mut(),
2123 std::ptr::null_mut(),
2124 std::ptr::null_mut(),
2125 std::ptr::null_mut(),
2126 std::ptr::null_mut(),
2127 0,
2128 );
2129 assert_eq!(nb, pio_n_branches(c));
2130 let mut from = vec![0i64; nb];
2131 let mut x = vec![0f64; nb];
2132 let total = pio_branches(
2133 c,
2134 from.as_mut_ptr(),
2135 std::ptr::null_mut(),
2136 std::ptr::null_mut(),
2137 x.as_mut_ptr(),
2138 std::ptr::null_mut(),
2139 std::ptr::null_mut(),
2140 std::ptr::null_mut(),
2141 std::ptr::null_mut(),
2142 nb,
2143 );
2144 assert_eq!(total, nb);
2145 assert!(from.iter().all(|&f| f >= 1));
2148 assert!(x.iter().all(|&xx| xx > 0.0));
2149 pio_network_free(c);
2150 }
2151 }
2152
2153 #[test]
2154 fn branch_tables_project_terminal_charging_to_legacy_b() {
2155 let c = terminal_projection_case();
2156 unsafe {
2157 let mut b = [0.0];
2158 let nb = pio_branches(
2159 c,
2160 std::ptr::null_mut(),
2161 std::ptr::null_mut(),
2162 std::ptr::null_mut(),
2163 std::ptr::null_mut(),
2164 b.as_mut_ptr(),
2165 std::ptr::null_mut(),
2166 std::ptr::null_mut(),
2167 std::ptr::null_mut(),
2168 1,
2169 );
2170 assert_eq!(nb, 1);
2171 close(b[0], 0.07);
2172
2173 let mut g_fr = [0.0];
2174 let mut b_fr = [0.0];
2175 let mut g_to = [0.0];
2176 let mut b_to = [0.0];
2177 let nb = pio_branch_charging(
2178 c,
2179 g_fr.as_mut_ptr(),
2180 b_fr.as_mut_ptr(),
2181 g_to.as_mut_ptr(),
2182 b_to.as_mut_ptr(),
2183 1,
2184 );
2185 assert_eq!(nb, 1);
2186 close(g_fr[0], 0.01);
2187 close(b_fr[0], 0.02);
2188 close(g_to[0], 0.03);
2189 close(b_to[0], 0.05);
2190 pio_network_free(c);
2191 }
2192 }
2193
2194 #[test]
2195 fn cap_clamps_the_write_and_returns_the_total() {
2196 let c = case9();
2197 unsafe {
2198 let total = pio_bus_ids(c, std::ptr::null_mut(), 0);
2199 assert_eq!(total, 9);
2200 let mut ids = [-1i64; 2];
2203 assert_eq!(pio_bus_ids(c, ids.as_mut_ptr(), ids.len()), 9);
2204 assert!(ids.iter().all(|&id| id >= 1));
2205 pio_network_free(c);
2206 }
2207 }
2208
2209 #[test]
2210 fn convert_matpower_echo() {
2211 let path = data_path("case14.m");
2212 let to = CString::new("matpower").unwrap();
2213 let mut warn = [0 as c_char; 256];
2214 let mut err = [0 as c_char; 256];
2215 unsafe {
2216 let s = pio_convert_file(
2217 path.as_ptr(),
2218 std::ptr::null(),
2219 to.as_ptr(),
2220 warn.as_mut_ptr(),
2221 warn.len(),
2222 err.as_mut_ptr(),
2223 err.len(),
2224 );
2225 assert!(!s.is_null());
2226 let got = CStr::from_ptr(s).to_str().unwrap();
2227 let src = std::fs::read_to_string(
2228 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case14.m"),
2229 )
2230 .unwrap();
2231 assert_eq!(got, src);
2232 pio_string_free(s);
2233 }
2234 }
2235
2236 #[test]
2237 fn convert_file_rejects_target_before_source_order() {
2238 let path = data_path("case14.m");
2239 let old_target = CString::new("powermodels-json").unwrap();
2240 let old_source = CString::new("matpower").unwrap();
2241 let mut warn = [0 as c_char; 512];
2242 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
2243 unsafe {
2244 let s = pio_convert_file(
2245 path.as_ptr(),
2246 old_target.as_ptr(),
2247 old_source.as_ptr(),
2248 warn.as_mut_ptr(),
2249 warn.len(),
2250 err.as_mut_ptr(),
2251 err.len(),
2252 );
2253 assert!(
2254 s.is_null(),
2255 "legacy target-before-source order unexpectedly succeeded"
2256 );
2257 let msg = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
2258 assert!(!msg.is_empty(), "expected an explanatory parse error");
2259 }
2260 }
2261
2262 #[test]
2263 fn convert_str_round_trips_in_memory() {
2264 let src = std::fs::read_to_string(
2267 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case9.m"),
2268 )
2269 .unwrap();
2270 let text = CString::new(src).unwrap();
2271 let from = CString::new("matpower").unwrap();
2272 let to = CString::new("powermodels-json").unwrap();
2273 let mut warn = [0 as c_char; 512];
2274 let mut err = [0 as c_char; 256];
2275 unsafe {
2276 let s = pio_convert_str(
2277 text.as_ptr(),
2278 from.as_ptr(),
2279 to.as_ptr(),
2280 warn.as_mut_ptr(),
2281 warn.len(),
2282 err.as_mut_ptr(),
2283 err.len(),
2284 );
2285 assert!(
2286 !s.is_null(),
2287 "convert_str failed: {}",
2288 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2289 );
2290 let out = CStr::from_ptr(s).to_str().unwrap();
2291 assert!(out.contains("\"bus\""));
2292 pio_string_free(s);
2293 }
2294 }
2295
2296 #[test]
2297 fn convert_str_rejects_target_before_source_order() {
2298 let src = std::fs::read_to_string(
2299 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case9.m"),
2300 )
2301 .unwrap();
2302 let text = CString::new(src).unwrap();
2303 let old_target = CString::new("powermodels-json").unwrap();
2304 let old_source = CString::new("matpower").unwrap();
2305 let mut warn = [0 as c_char; 512];
2306 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
2307 unsafe {
2308 let s = pio_convert_str(
2309 text.as_ptr(),
2310 old_target.as_ptr(),
2311 old_source.as_ptr(),
2312 warn.as_mut_ptr(),
2313 warn.len(),
2314 err.as_mut_ptr(),
2315 err.len(),
2316 );
2317 assert!(
2318 s.is_null(),
2319 "legacy target-before-source order unexpectedly succeeded"
2320 );
2321 let msg = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
2322 assert!(!msg.is_empty(), "expected an explanatory parse error");
2323 }
2324 }
2325
2326 #[test]
2327 fn to_format_converts_live_handle() {
2328 let c = case9();
2329 unsafe {
2330 let text = to_format(c, "powermodels-json");
2331 assert!(text.contains("\"bus\""));
2332 pio_network_free(c);
2333 }
2334 }
2335
2336 #[test]
2337 fn parse_error_sets_message_not_null_handle() {
2338 let path = CString::new("/no/such/case.m").unwrap();
2339 let mut err = [0 as c_char; 256];
2340 let c =
2341 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
2342 assert!(c.is_null());
2343 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
2344 assert!(!msg.is_empty(), "expected an error message");
2345 }
2346
2347 #[test]
2348 fn non_utf8_from_hint_errors_instead_of_falling_back() {
2349 let path = data_path("case9.m");
2350 let to = CString::new("matpower").unwrap();
2351 let bad_from = [0xff_u8, 0];
2352 let mut err = [0 as c_char; 256];
2353 let c = unsafe {
2354 pio_parse_file(
2355 path.as_ptr(),
2356 bad_from.as_ptr().cast::<c_char>(),
2357 err.as_mut_ptr(),
2358 err.len(),
2359 )
2360 };
2361 assert!(c.is_null());
2362 assert_eq!(
2363 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap(),
2364 "from is not UTF-8"
2365 );
2366
2367 let mut warn = [0 as c_char; 256];
2368 err.fill(0);
2369 let s = unsafe {
2370 pio_convert_file(
2371 path.as_ptr(),
2372 bad_from.as_ptr().cast::<c_char>(),
2373 to.as_ptr(),
2374 warn.as_mut_ptr(),
2375 warn.len(),
2376 err.as_mut_ptr(),
2377 err.len(),
2378 )
2379 };
2380 assert!(s.is_null());
2381 assert_eq!(
2382 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap(),
2383 "from is not UTF-8"
2384 );
2385 }
2386
2387 #[test]
2388 fn extract_gen_and_bus_aggregate_tables() {
2389 let path = data_path("case30.m");
2393 let mut err = [0 as c_char; 256];
2394 let c =
2395 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
2396 assert!(!c.is_null());
2397 unsafe {
2398 let nb = pio_n_buses(c);
2399 let ng = pio_n_gens(c);
2400 assert_eq!(nb, 30);
2401 assert!(ng > 0);
2402
2403 let mut gbus = vec![-9i64; ng];
2404 let mut pmax = vec![0f64; ng];
2405 let total = pio_gens(
2406 c,
2407 gbus.as_mut_ptr(),
2408 std::ptr::null_mut(),
2409 pmax.as_mut_ptr(),
2410 std::ptr::null_mut(),
2411 std::ptr::null_mut(),
2412 ng,
2413 );
2414 assert_eq!(total, ng);
2415 assert!(gbus.iter().all(|&b| (1..=nb as i64).contains(&b)));
2417 assert!(pmax.iter().any(|&p| p > 0.0));
2418
2419 let mut ids = vec![0i64; nb];
2420 assert_eq!(pio_bus_ids(c, ids.as_mut_ptr(), nb), nb);
2421 assert!(ids.iter().all(|&id| id >= 1)); let mut pd = vec![0f64; nb];
2424 let mut qd = vec![0f64; nb];
2425 assert_eq!(pio_bus_demand(c, pd.as_mut_ptr(), qd.as_mut_ptr(), nb), nb);
2426 assert!(pd.iter().sum::<f64>() > 0.0, "case30 has active demand");
2427
2428 let mut gs = vec![0f64; nb];
2429 let mut bs = vec![0f64; nb];
2430 assert_eq!(pio_bus_shunt(c, gs.as_mut_ptr(), bs.as_mut_ptr(), nb), nb);
2431 assert!(gs.iter().chain(bs.iter()).all(|x| x.is_finite()));
2432
2433 pio_network_free(c);
2434 }
2435 }
2436
2437 #[test]
2438 fn null_handle_and_null_out_are_safe() {
2439 unsafe {
2442 let nil: *const PioNetwork = std::ptr::null();
2443 assert_eq!(pio_n_buses(nil), 0);
2444 assert_eq!(pio_n_branches(nil), 0);
2445 assert_eq!(pio_n_gens(nil), 0);
2446 assert_eq!(pio_base_mva(nil), 0.0);
2447 assert_eq!(pio_ref_bus_index(nil), -1);
2448 assert_eq!(pio_ref_bus_indices(nil, std::ptr::null_mut(), 0), 0);
2449 assert_eq!(pio_is_radial(nil), 0);
2450 assert_eq!(pio_n_islands(nil), 0);
2451
2452 let mut err = [0 as c_char; 128];
2454 assert!(pio_normalize(nil, err.as_mut_ptr(), err.len()).is_null());
2455 let fmt = CString::new("matpower").unwrap();
2456 assert!(
2457 pio_parse_str(std::ptr::null(), fmt.as_ptr(), err.as_mut_ptr(), err.len())
2458 .is_null()
2459 );
2460
2461 let c = case9();
2462 assert_eq!(pio_bus_ids(c, std::ptr::null_mut(), 0), 9);
2463 pio_ref_bus_indices(c, std::ptr::null_mut(), 0);
2464 pio_bus_demand(c, std::ptr::null_mut(), std::ptr::null_mut(), 0);
2465 pio_gens(
2466 c,
2467 std::ptr::null_mut(),
2468 std::ptr::null_mut(),
2469 std::ptr::null_mut(),
2470 std::ptr::null_mut(),
2471 std::ptr::null_mut(),
2472 0,
2473 );
2474 pio_network_free(c);
2475 }
2476 }
2477
2478 #[test]
2479 fn normalized_multi_ref_is_legible() {
2480 let src = "\
2485function mpc = tworef
2486mpc.version = '2';
2487mpc.baseMVA = 100;
2488mpc.bus = [
2489\t1\t3\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
2490\t2\t3\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
2491\t3\t1\t50\t10\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
2492];
2493mpc.gen = [
2494\t1\t0\t0\t100\t-100\t1\t100\t1\t100\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0;
2495\t2\t0\t0\t100\t-100\t1\t100\t1\t300\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0;
2496];
2497mpc.branch = [
2498\t1\t2\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
2499\t2\t3\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
2500];
2501";
2502 let text = CString::new(src).unwrap();
2503 let fmt = CString::new("matpower").unwrap();
2504 let mut err = [0 as c_char; 256];
2505 unsafe {
2506 let cs = pio_parse_str(text.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len());
2507 assert!(!cs.is_null(), "parse_str returned null");
2508 let cn = pio_normalize(cs, err.as_mut_ptr(), err.len());
2509 assert!(!cn.is_null(), "normalize returned null");
2510
2511 assert_eq!(pio_ref_bus_indices(cn, std::ptr::null_mut(), 0), 2);
2513 assert_eq!(pio_ref_bus_index(cn), -1);
2515 let mut refs = [-1i64; 2];
2516 assert_eq!(pio_ref_bus_indices(cn, refs.as_mut_ptr(), refs.len()), 2);
2517 assert_eq!(refs, [0, 1]);
2518
2519 pio_network_free(cn);
2520 pio_network_free(cs);
2521 }
2522 }
2523
2524 #[test]
2525 fn normalized_preserves_source_bus_ids() {
2526 let src = "\
2527function mpc = sparseids
2528mpc.version = '2';
2529mpc.baseMVA = 100;
2530mpc.bus = [
2531\t1\t3\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
2532\t2\t1\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
2533\t3\t1\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
2534\t4\t1\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
2535\t10\t1\t50\t10\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
2536];
2537mpc.gen = [
2538\t1\t0\t0\t100\t-100\t1\t100\t1\t200\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0;
2539];
2540mpc.branch = [
2541\t1\t2\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
2542\t2\t3\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
2543\t3\t4\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
2544\t4\t10\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
2545];
2546";
2547 let text = CString::new(src).unwrap();
2548 let fmt = CString::new("matpower").unwrap();
2549 let mut err = [0 as c_char; 256];
2550 unsafe {
2551 let cs = pio_parse_str(text.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len());
2552 assert!(!cs.is_null(), "parse_str returned null");
2553 let cn = pio_normalize(cs, err.as_mut_ptr(), err.len());
2554 assert!(!cn.is_null(), "normalize returned null");
2555
2556 let mut ids = vec![0i64; pio_n_buses(cn)];
2557 pio_bus_ids(cn, ids.as_mut_ptr(), ids.len());
2558 assert_eq!(ids, vec![1, 2, 3, 4, 10]);
2559
2560 let mut from = vec![0i64; pio_n_branches(cn)];
2561 let mut to = vec![0i64; pio_n_branches(cn)];
2562 pio_branches(
2563 cn,
2564 from.as_mut_ptr(),
2565 to.as_mut_ptr(),
2566 std::ptr::null_mut(),
2567 std::ptr::null_mut(),
2568 std::ptr::null_mut(),
2569 std::ptr::null_mut(),
2570 std::ptr::null_mut(),
2571 std::ptr::null_mut(),
2572 from.len(),
2573 );
2574 assert_eq!((from[3], to[3]), (4, 10));
2575
2576 pio_network_free(cn);
2577 pio_network_free(cs);
2578 }
2579 }
2580
2581 #[test]
2582 fn convert_emits_warning_into_buffer() {
2583 let path = data_path("t_case9_dcline.m");
2587 let to = CString::new("psse").unwrap();
2588 let mut warn = [0 as c_char; 512];
2589 let mut err = [0 as c_char; 256];
2590 unsafe {
2591 let s = pio_convert_file(
2592 path.as_ptr(),
2593 std::ptr::null(),
2594 to.as_ptr(),
2595 warn.as_mut_ptr(),
2596 warn.len(),
2597 err.as_mut_ptr(),
2598 err.len(),
2599 );
2600 assert!(!s.is_null());
2601 let w = CStr::from_ptr(warn.as_ptr()).to_str().unwrap();
2602 assert!(
2603 w.contains("converter detail"),
2604 "expected an HVDC converter-detail warning, got {w:?}"
2605 );
2606 pio_string_free(s);
2607 }
2608 }
2609
2610 #[test]
2611 fn snapshot_round_trip_preserves_structure() {
2612 let path = data_path("case30.m");
2616 let mut err = [0 as c_char; 256];
2617 let c =
2618 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
2619 assert!(!c.is_null());
2620 unsafe {
2621 let json = to_format(c, "powerio-json");
2622 assert!(json.contains("\"buses\""));
2623
2624 let text = CString::new(json).unwrap();
2625 let fmt = CString::new("powerio-json").unwrap();
2626 let back = pio_parse_str(text.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len());
2627 assert!(
2628 !back.is_null(),
2629 "snapshot parse failed: {}",
2630 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2631 );
2632 assert_eq!(pio_warnings(back, std::ptr::null_mut(), 0), 0);
2634 assert_eq!(pio_n_buses(back), pio_n_buses(c));
2636 assert_eq!(pio_n_branches(back), pio_n_branches(c));
2637 assert_eq!(pio_n_gens(back), pio_n_gens(c));
2638 assert_eq!(pio_base_mva(back), pio_base_mva(c));
2639 assert_eq!(pio_ref_bus_index(back), pio_ref_bus_index(c));
2640
2641 let alias = CString::new("json").unwrap();
2643 let again = pio_parse_str(text.as_ptr(), alias.as_ptr(), err.as_mut_ptr(), err.len());
2644 assert!(!again.is_null());
2645 assert_eq!(pio_n_buses(again), pio_n_buses(c));
2646
2647 pio_network_free(again);
2648 pio_network_free(back);
2649 pio_network_free(c);
2650 }
2651 }
2652
2653 #[test]
2654 fn snapshot_rejects_garbage() {
2655 let bad = CString::new("{ not json").unwrap();
2656 let fmt = CString::new("powerio-json").unwrap();
2657 let mut err = [0 as c_char; 256];
2658 let h = unsafe { pio_parse_str(bad.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len()) };
2659 assert!(h.is_null());
2660 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
2661 assert!(!msg.is_empty(), "expected a JSON parse error message");
2662 }
2663
2664 #[test]
2665 fn error_buffer_truncates_and_nul_terminates() {
2666 let path = CString::new("/no/such/directory/deeply/nested/missing/case.m").unwrap();
2669 let mut err = [0x7f as c_char; 16]; let c =
2671 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
2672 assert!(c.is_null());
2673 let nul = err
2674 .iter()
2675 .position(|&b| b == 0)
2676 .expect("buffer must be NUL-terminated");
2677 assert!(nul <= 15);
2678 }
2679
2680 #[test]
2681 fn truncation_lands_on_a_utf8_char_boundary() {
2682 let mut buf = [0x7f as c_char; 6];
2686 unsafe { copy_to_buf(buf.as_mut_ptr(), buf.len(), "aé€") };
2687 let s = unsafe { CStr::from_ptr(buf.as_ptr()) }
2688 .to_str()
2689 .expect("truncated message must be valid UTF-8");
2690 assert_eq!(s, "aé");
2691
2692 let mut buf = [0x7f as c_char; 8];
2694 unsafe { copy_to_buf(buf.as_mut_ptr(), buf.len(), "aé€") };
2695 let s = unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap();
2696 assert_eq!(s, "aé€");
2697 }
2698
2699 #[cfg(feature = "pkg")]
2700 #[test]
2701 fn package_feature_is_reported() {
2702 let pkg = CString::new("pkg").unwrap();
2703 let nope = CString::new("nope").unwrap();
2704 unsafe {
2705 assert_eq!(pio_has_feature(pkg.as_ptr()), 1);
2706 assert_eq!(pio_has_feature(nope.as_ptr()), 0);
2707 }
2708 }
2709
2710 #[cfg(feature = "pkg")]
2711 #[test]
2712 fn package_materialize_reports_unknown_identity() {
2713 use powerio_pkg::{
2714 ElementRef, ElementUpdate, NetworkPackage, OperatingPoint, OperatingPointSeries,
2715 TimeAxis,
2716 };
2717
2718 let case = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
2719 .join("../tests/data")
2720 .join("case9.m");
2721 let net = powerio::parse_str(&std::fs::read_to_string(case).unwrap(), "matpower")
2722 .unwrap()
2723 .network;
2724 let mut point = OperatingPoint::new(0);
2725 point.updates.push(ElementUpdate::new(
2726 ElementRef::by_source_uid("generators", "no-such-uid"),
2727 std::collections::BTreeMap::from([("pg".to_owned(), serde_json::json!(1.0))]),
2728 ));
2729 let package = NetworkPackage::from_balanced(net).with_operating_points(
2730 OperatingPointSeries::new(TimeAxis::new(1).with_duration_hours(vec![1.0]), vec![point]),
2731 );
2732 let json = CString::new(package.to_json().unwrap()).unwrap();
2733
2734 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
2735 unsafe {
2736 let pkg = pio_package_parse_str(json.as_ptr(), err.as_mut_ptr(), err.len());
2737 assert!(
2738 !pkg.is_null(),
2739 "package parse_str failed: {}",
2740 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2741 );
2742 let materialized =
2743 pio_package_materialize_operating_point(pkg, 0, err.as_mut_ptr(), err.len());
2744 assert!(materialized.is_null(), "unknown identity must fail");
2745 let message = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
2746 assert!(
2747 message.contains("unknown identity"),
2748 "unexpected error: {message}"
2749 );
2750 pio_package_free(pkg);
2751 }
2752 }
2753
2754 #[cfg(feature = "pkg")]
2755 #[test]
2756 fn package_parse_free_to_json_and_reports() {
2757 let net = case9();
2758 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
2759 unsafe {
2760 let pkg = pio_package_from_balanced_network(net, 1, err.as_mut_ptr(), err.len());
2761 assert!(
2762 !pkg.is_null(),
2763 "package constructor failed: {}",
2764 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2765 );
2766 let v = package_json(pkg);
2767 assert_eq!(v["schema_version"], serde_json::json!("0.1.1"));
2768 assert_eq!(v["model_kind"], serde_json::json!("balanced"));
2769 assert_eq!(v["model"]["kind"], serde_json::json!("balanced"));
2770 assert_eq!(
2771 v["payload_schema"],
2772 serde_json::json!(powerio_pkg::PIO_PAYLOAD_BALANCED_SCHEMA_URL)
2773 );
2774 assert_eq!(
2775 v["payload_schema_version"],
2776 serde_json::json!(powerio_pkg::PIO_PAYLOAD_BALANCED_SCHEMA_VERSION)
2777 );
2778 assert_eq!(
2779 v["derived"]["normalized_solver_tables"]["row_counts"]["buses"],
2780 serde_json::json!(9)
2781 );
2782
2783 let json = CString::new(package_json_text(pkg)).unwrap();
2784 let parsed = pio_package_parse_str(json.as_ptr(), err.as_mut_ptr(), err.len());
2785 assert!(
2786 !parsed.is_null(),
2787 "package parse_str failed: {}",
2788 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2789 );
2790
2791 let tmp = tempfile::tempdir().unwrap();
2792 let path = tmp.path().join("case9.pio.json");
2793 std::fs::write(&path, CStr::from_ptr(json.as_ptr()).to_bytes()).unwrap();
2794 let path = CString::new(path.to_str().unwrap()).unwrap();
2795 let parsed_file = pio_package_parse_file(path.as_ptr(), err.as_mut_ptr(), err.len());
2796 assert!(
2797 !parsed_file.is_null(),
2798 "package parse_file failed: {}",
2799 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2800 );
2801
2802 assert_eq!(
2803 pio_package_validate(parsed_file, err.as_mut_ptr(), err.len()),
2804 0
2805 );
2806 let validation = package_report_json(pio_package_validation_json, parsed_file);
2807 assert_eq!(validation["status"], serde_json::json!("ok"));
2808 assert!(
2809 validation["passes"]
2810 .as_array()
2811 .unwrap()
2812 .iter()
2813 .any(|p| p["name"] == "balanced.structure")
2814 );
2815 let diagnostics = package_report_json(pio_package_diagnostics_json, parsed_file);
2816 assert!(diagnostics.as_array().unwrap().is_empty());
2817
2818 pio_package_free(parsed_file);
2819 pio_package_free(parsed);
2820 pio_package_free(pkg);
2821 pio_network_free(net);
2822 }
2823 }
2824
2825 #[cfg(feature = "pkg")]
2826 #[test]
2827 fn package_balanced_constructor_omits_solver_metadata_by_default() {
2828 let net = case9();
2829 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
2830 unsafe {
2831 let pkg = pio_package_from_balanced_network(net, 0, err.as_mut_ptr(), err.len());
2832 assert!(
2833 !pkg.is_null(),
2834 "package constructor failed: {}",
2835 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2836 );
2837 let v = package_json(pkg);
2838 assert!(v["derived"].get("normalized_solver_tables").is_none());
2839 pio_package_free(pkg);
2840 pio_network_free(net);
2841 }
2842 }
2843
2844 #[cfg(all(feature = "pkg", feature = "dist"))]
2845 mod package_dist {
2846 use super::*;
2847
2848 fn strings(values: &[&str]) -> Vec<String> {
2849 values.iter().map(|v| (*v).to_owned()).collect()
2850 }
2851
2852 fn zero_matrix(n: usize) -> powerio_dist::Mat {
2853 vec![vec![0.0; n]; n]
2854 }
2855
2856 fn diagonal_matrix(n: usize, value: f64) -> powerio_dist::Mat {
2857 let mut matrix = zero_matrix(n);
2858 for (idx, row) in matrix.iter_mut().enumerate() {
2859 row[idx] = value;
2860 }
2861 matrix
2862 }
2863
2864 fn phase_reference(terminals: &[&str], grounded: &[&str]) -> (Vec<f64>, Vec<f64>) {
2865 let phase_angles = [
2866 0.0,
2867 -2.0 * std::f64::consts::PI / 3.0,
2868 2.0 * std::f64::consts::PI / 3.0,
2869 ];
2870 let mut magnitudes = vec![0.0; terminals.len()];
2871 let mut angles = vec![0.0; terminals.len()];
2872 let mut active = 0;
2873 for (idx, terminal) in terminals.iter().enumerate() {
2874 if grounded.contains(terminal) || *terminal == "0" {
2875 continue;
2876 }
2877 magnitudes[idx] = 240.0;
2878 if active < phase_angles.len() {
2879 angles[idx] = phase_angles[active];
2880 }
2881 active += 1;
2882 }
2883 (magnitudes, angles)
2884 }
2885
2886 fn preflight_network(terminals: &[&str], grounded: &[&str]) -> powerio_dist::DistNetwork {
2887 use powerio_dist::{DistBus, DistLine, DistLineCode, DistNetwork, VoltageSource};
2888
2889 let n = terminals.len();
2890 let terminal_map = strings(terminals);
2891 let (v_magnitude, v_angle) = phase_reference(terminals, grounded);
2892 let mut net = DistNetwork::default();
2893 for id in ["sourcebus", "loadbus"] {
2894 let mut bus = DistBus::new(id, terminal_map.clone());
2895 bus.grounded = strings(grounded);
2896 net.buses.push(bus);
2897 }
2898 let mut linecode =
2899 DistLineCode::new("lc", diagonal_matrix(n, 0.01), diagonal_matrix(n, 0.10));
2900 linecode.g_from = zero_matrix(n);
2901 linecode.b_from = zero_matrix(n);
2902 linecode.g_to = zero_matrix(n);
2903 linecode.b_to = zero_matrix(n);
2904 net.linecodes.push(linecode);
2905 net.lines.push(DistLine::new(
2906 "l1",
2907 "sourcebus",
2908 "loadbus",
2909 terminal_map.clone(),
2910 terminal_map.clone(),
2911 "lc",
2912 1.0,
2913 ));
2914 net.sources.push(VoltageSource::new(
2915 "source",
2916 "sourcebus",
2917 terminal_map,
2918 v_magnitude,
2919 v_angle,
2920 ));
2921 net
2922 }
2923
2924 #[test]
2925 fn multiconductor_package_preflight_and_lowering() {
2926 let dist = PioDistNetwork {
2927 net: preflight_network(&["1", "2", "3"], &[]),
2928 };
2929 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
2930 unsafe {
2931 let pkg =
2932 pio_package_from_multiconductor_network(&dist, err.as_mut_ptr(), err.len());
2933 assert!(
2934 !pkg.is_null(),
2935 "multiconductor package constructor failed: {}",
2936 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2937 );
2938 let v = package_json(pkg);
2939 assert_eq!(v["model_kind"], serde_json::json!("multiconductor"));
2940
2941 let report = pio_package_multiconductor_to_balanced_preflight_json(
2942 pkg,
2943 50.0,
2944 err.as_mut_ptr(),
2945 err.len(),
2946 );
2947 assert!(
2948 !report.is_null(),
2949 "preflight failed: {}",
2950 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2951 );
2952 let report_json: serde_json::Value =
2953 serde_json::from_str(CStr::from_ptr(report).to_str().unwrap()).unwrap();
2954 assert_eq!(report_json["status"], serde_json::json!("ok"));
2955 assert_eq!(report_json["base_mva"], serde_json::json!(50.0));
2956 pio_string_free(report);
2957
2958 let lowered = pio_package_lower_multiconductor_to_balanced(
2959 pkg,
2960 75.0,
2961 err.as_mut_ptr(),
2962 err.len(),
2963 );
2964 assert!(
2965 !lowered.is_null(),
2966 "lowering failed: {}",
2967 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2968 );
2969 let lowered_json = package_json(lowered);
2970 assert_eq!(lowered_json["model_kind"], serde_json::json!("balanced"));
2971 assert_eq!(
2972 lowered_json["model"]["balanced_network"]["base_mva"],
2973 serde_json::json!(75.0)
2974 );
2975 assert_eq!(
2976 lowered_json["lowering_history"][0]["pass"],
2977 serde_json::json!("multiconductor-to-balanced")
2978 );
2979
2980 let invalid_report = pio_package_multiconductor_to_balanced_preflight_json(
2981 pkg,
2982 0.0,
2983 err.as_mut_ptr(),
2984 err.len(),
2985 );
2986 assert!(
2987 !invalid_report.is_null(),
2988 "invalid-base preflight failed: {}",
2989 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2990 );
2991 let invalid_report_json: serde_json::Value =
2992 serde_json::from_str(CStr::from_ptr(invalid_report).to_str().unwrap()).unwrap();
2993 assert_eq!(invalid_report_json["status"], serde_json::json!("error"));
2994 assert!(
2995 invalid_report_json["diagnostics"]
2996 .as_array()
2997 .unwrap()
2998 .iter()
2999 .any(|d| d["code"] == "LOWER.MULTI_TO_BALANCED.INVALID_BASE_MVA")
3000 );
3001 pio_string_free(invalid_report);
3002
3003 let invalid_lowered = pio_package_lower_multiconductor_to_balanced(
3004 pkg,
3005 0.0,
3006 err.as_mut_ptr(),
3007 err.len(),
3008 );
3009 assert!(invalid_lowered.is_null());
3010 let msg = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
3011 assert!(msg.contains("base_mva must be positive"), "got: {msg}");
3012
3013 pio_package_free(lowered);
3014 pio_package_free(pkg);
3015 }
3016 }
3017 }
3018
3019 #[cfg(feature = "arrow")]
3020 #[test]
3021 fn to_arrow_null_out_params_return_error() {
3022 let c = case9();
3024 let mut err = [0 as c_char; 256];
3025 let rc = unsafe {
3026 pio_to_arrow(
3027 c,
3028 PIO_ARROW_TABLE_BUS,
3029 std::ptr::null_mut(),
3030 std::ptr::null_mut(),
3031 err.as_mut_ptr(),
3032 err.len(),
3033 )
3034 };
3035 assert_eq!(rc, -1);
3036 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
3037 assert!(!msg.is_empty(), "expected an error message");
3038 unsafe { pio_network_free(c) };
3039 }
3040
3041 #[cfg(feature = "gridfm")]
3042 #[test]
3043 fn read_dir_round_trips_and_enumerates_scenarios() {
3044 use powerio_matrix::{GridfmOptions, write_gridfm_dataset};
3045 let net = powerio::parse_file(
3047 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case14.m"),
3048 None,
3049 )
3050 .unwrap()
3051 .network;
3052 let tmp = tempfile::tempdir().unwrap();
3053 let out = write_gridfm_dataset(&net, 0, tmp.path(), &GridfmOptions::default()).unwrap();
3054 let dir = CString::new(out.dir.to_str().unwrap()).unwrap();
3055 let from = CString::new("gridfm").unwrap();
3056
3057 let mut err = [0 as c_char; 256];
3058 unsafe {
3059 let h = pio_read_dir(dir.as_ptr(), from.as_ptr(), 0, err.as_mut_ptr(), err.len());
3060 assert!(
3061 !h.is_null(),
3062 "read failed: {}",
3063 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
3064 );
3065 assert_eq!(pio_n_buses(h), 14);
3066 assert!(
3069 pio_warnings(h, std::ptr::null_mut(), 0) > 0,
3070 "expected fidelity warnings on the handle"
3071 );
3072 pio_network_free(h);
3073
3074 let count = pio_scenario_ids(
3076 dir.as_ptr(),
3077 from.as_ptr(),
3078 std::ptr::null_mut(),
3079 0,
3080 err.as_mut_ptr(),
3081 err.len(),
3082 );
3083 assert_eq!(count, 1);
3084 let mut ids = [-1i64; 4];
3085 let n = pio_scenario_ids(
3086 dir.as_ptr(),
3087 from.as_ptr(),
3088 ids.as_mut_ptr(),
3089 ids.len(),
3090 err.as_mut_ptr(),
3091 err.len(),
3092 );
3093 assert_eq!(n, 1);
3094 assert_eq!(ids[0], 0);
3095
3096 let bad = CString::new("pypsa").unwrap();
3098 let h = pio_read_dir(dir.as_ptr(), bad.as_ptr(), 0, err.as_mut_ptr(), err.len());
3099 assert!(h.is_null());
3100 let msg = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
3101 assert!(msg.contains("gridfm"), "got: {msg}");
3102
3103 let missing = CString::new(tmp.path().join("nope").to_str().unwrap()).unwrap();
3105 let bad = pio_read_dir(
3106 missing.as_ptr(),
3107 from.as_ptr(),
3108 0,
3109 err.as_mut_ptr(),
3110 err.len(),
3111 );
3112 assert!(bad.is_null());
3113 assert!(!CStr::from_ptr(err.as_ptr()).to_str().unwrap().is_empty());
3114 }
3115 }
3116
3117 #[test]
3118 fn write_dir_rejects_text_formats_by_name() {
3119 let c = case9();
3120 let to = CString::new("matpower").unwrap();
3121 let dir = CString::new("/tmp/unused").unwrap();
3122 let mut err = [0 as c_char; 256];
3123 unsafe {
3124 let rc = pio_write_dir(
3125 c,
3126 to.as_ptr(),
3127 dir.as_ptr(),
3128 std::ptr::null_mut(),
3129 0,
3130 err.as_mut_ptr(),
3131 err.len(),
3132 );
3133 assert_eq!(rc, -1);
3134 let msg = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
3135 assert!(msg.contains("pypsa"), "got: {msg}");
3136 pio_network_free(c);
3137 }
3138 }
3139
3140 #[cfg(feature = "dist")]
3141 mod dist {
3142 use super::*;
3143 use std::ffi::CStr;
3144
3145 fn fourwire() -> std::path::PathBuf {
3146 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
3147 .join("../tests/data/dist/micro/fourwire_linecode.dss")
3148 }
3149
3150 fn fourwire_cstr() -> CString {
3151 CString::new(fourwire().to_str().unwrap()).unwrap()
3152 }
3153
3154 #[test]
3155 fn dist_abi_version_is_separate() {
3156 assert_eq!(pio_abi_version(), PIO_ABI_VERSION);
3157 assert_eq!(PIO_ABI_VERSION, 4);
3158 assert_eq!(pio_dist_abi_version(), PIO_DIST_ABI_VERSION);
3159 assert_eq!(PIO_DIST_ABI_VERSION, 1);
3160 let feature = CString::new("dist").unwrap();
3161 assert_eq!(unsafe { pio_has_feature(feature.as_ptr()) }, 1);
3162 }
3163
3164 #[test]
3165 fn parse_file_convert_and_echo() {
3166 let path = fourwire_cstr();
3167 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
3168 let net = unsafe {
3169 pio_dist_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len())
3170 };
3171 assert!(
3172 !net.is_null(),
3173 "{}",
3174 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
3175 );
3176
3177 let to = CString::new("bmopf").unwrap();
3179 let mut warn = [0 as c_char; 4096];
3180 let s = unsafe {
3181 pio_dist_to_format(
3182 net,
3183 to.as_ptr(),
3184 warn.as_mut_ptr(),
3185 warn.len(),
3186 err.as_mut_ptr(),
3187 err.len(),
3188 )
3189 };
3190 assert!(!s.is_null());
3191 let text = unsafe { CStr::from_ptr(s) }.to_str().unwrap();
3192 assert!(text.contains("\"bus\""));
3193 unsafe { pio_string_free(s) };
3194
3195 let to = CString::new("dss").unwrap();
3197 let s = unsafe {
3198 pio_dist_to_format(
3199 net,
3200 to.as_ptr(),
3201 warn.as_mut_ptr(),
3202 warn.len(),
3203 err.as_mut_ptr(),
3204 err.len(),
3205 )
3206 };
3207 assert!(!s.is_null());
3208 let echoed = unsafe { CStr::from_ptr(s) }.to_str().unwrap();
3209 let source = std::fs::read_to_string(fourwire()).unwrap();
3210 assert_eq!(echoed, source);
3211 assert_eq!(
3212 unsafe { CStr::from_ptr(warn.as_ptr()) }.to_str().unwrap(),
3213 ""
3214 );
3215 unsafe { pio_string_free(s) };
3216
3217 unsafe { pio_dist_network_free(net) };
3218 }
3219
3220 #[test]
3221 fn convert_str_round_trips_through_pmd() {
3222 let source = std::fs::read_to_string(fourwire()).unwrap();
3223 let text = CString::new(source).unwrap();
3224 let from = CString::new("dss").unwrap();
3225 let to = CString::new("pmd").unwrap();
3226 let mut warn = [0 as c_char; 4096];
3227 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
3228 let s = unsafe {
3229 pio_dist_convert_str(
3230 text.as_ptr(),
3231 from.as_ptr(),
3232 to.as_ptr(),
3233 warn.as_mut_ptr(),
3234 warn.len(),
3235 err.as_mut_ptr(),
3236 err.len(),
3237 )
3238 };
3239 assert!(
3240 !s.is_null(),
3241 "{}",
3242 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
3243 );
3244 let pmd = unsafe { CStr::from_ptr(s) }.to_str().unwrap();
3245 assert!(pmd.contains("\"data_model\": \"ENGINEERING\""));
3246 unsafe { pio_string_free(s) };
3247 }
3248
3249 #[test]
3250 fn convert_str_rejects_target_before_source_order() {
3251 let source = std::fs::read_to_string(fourwire()).unwrap();
3252 let text = CString::new(source).unwrap();
3253 let old_target = CString::new("pmd").unwrap();
3254 let old_source = CString::new("dss").unwrap();
3255 let mut warn = [0 as c_char; 4096];
3256 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
3257 let s = unsafe {
3258 pio_dist_convert_str(
3259 text.as_ptr(),
3260 old_target.as_ptr(),
3261 old_source.as_ptr(),
3262 warn.as_mut_ptr(),
3263 warn.len(),
3264 err.as_mut_ptr(),
3265 err.len(),
3266 )
3267 };
3268 assert!(
3269 s.is_null(),
3270 "legacy target-before-source order unexpectedly succeeded"
3271 );
3272 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
3273 assert!(!msg.is_empty(), "expected an explanatory parse error");
3274 }
3275
3276 #[test]
3277 fn warnings_report_count_and_text() {
3278 let text = CString::new(
3281 "clear\nnew circuit.w basekv=12.47 bus1=src\nnew line.l1 bus1=src bus2=b2 length=1 units=furlong\n",
3282 )
3283 .unwrap();
3284 let fmt = CString::new("dss").unwrap();
3285 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
3286 let net = unsafe {
3287 pio_dist_parse_str(text.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len())
3288 };
3289 assert!(!net.is_null());
3290 let mut warn = [0 as c_char; 4096];
3291 let n = unsafe { pio_dist_warnings(net, warn.as_mut_ptr(), warn.len()) };
3292 assert!(n > 0, "expected a nonzero warning length");
3293 let msg = unsafe { CStr::from_ptr(warn.as_ptr()) }.to_str().unwrap();
3294 assert!(
3295 msg.lines().any(|w| w.contains("furlong")),
3296 "expected the units warning, got: {msg}"
3297 );
3298 assert_eq!(
3300 unsafe { pio_dist_warnings(std::ptr::null(), warn.as_mut_ptr(), warn.len()) },
3301 0
3302 );
3303 unsafe { pio_dist_network_free(net) };
3304 }
3305
3306 #[test]
3307 fn convert_file_round_trips_through_bmopf() {
3308 let path = fourwire_cstr();
3309 let to = CString::new("bmopf-json").unwrap();
3310 let mut warn = [0 as c_char; 4096];
3311 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
3312 let s = unsafe {
3313 pio_dist_convert_file(
3314 path.as_ptr(),
3315 std::ptr::null(),
3316 to.as_ptr(),
3317 warn.as_mut_ptr(),
3318 warn.len(),
3319 err.as_mut_ptr(),
3320 err.len(),
3321 )
3322 };
3323 assert!(
3324 !s.is_null(),
3325 "{}",
3326 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
3327 );
3328 let text = unsafe { CStr::from_ptr(s) }.to_str().unwrap();
3329 assert!(text.contains("\"bus\""));
3330 unsafe { pio_string_free(s) };
3331 }
3332
3333 #[test]
3334 fn convert_file_rejects_target_before_source_order() {
3335 let path = fourwire_cstr();
3336 let old_target = CString::new("pmd").unwrap();
3337 let old_source = CString::new("dss").unwrap();
3338 let mut warn = [0 as c_char; 4096];
3339 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
3340 let s = unsafe {
3341 pio_dist_convert_file(
3342 path.as_ptr(),
3343 old_target.as_ptr(),
3344 old_source.as_ptr(),
3345 warn.as_mut_ptr(),
3346 warn.len(),
3347 err.as_mut_ptr(),
3348 err.len(),
3349 )
3350 };
3351 assert!(
3352 s.is_null(),
3353 "legacy target-before-source order unexpectedly succeeded"
3354 );
3355 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
3356 assert!(!msg.is_empty(), "expected an explanatory parse error");
3357 }
3358
3359 #[test]
3360 fn unknown_format_is_an_error_not_a_crash() {
3361 let text = CString::new("clear\n").unwrap();
3362 let fmt = CString::new("matpower").unwrap();
3363 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
3364 let net = unsafe {
3365 pio_dist_parse_str(text.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len())
3366 };
3367 assert!(net.is_null());
3368 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
3369 assert!(msg.contains("unknown distribution format"));
3370 }
3371
3372 #[test]
3373 fn has_feature_reports_dist() {
3374 let dist = CString::new("dist").unwrap();
3375 assert_eq!(unsafe { pio_has_feature(dist.as_ptr()) }, 1);
3376 let nope = CString::new("nope").unwrap();
3377 assert_eq!(unsafe { pio_has_feature(nope.as_ptr()) }, 0);
3378 }
3379 }
3380}