1#![allow(clippy::missing_safety_doc)]
28
29use std::ffi::{CStr, CString, c_char};
30use std::panic::{AssertUnwindSafe, catch_unwind};
31
32use powerio::{BalancedNetwork, IndexCore, IndexedNetwork, NormalizeOptions, TargetFormat};
33
34#[cfg(feature = "arrow")]
35mod arrow_export;
36#[cfg(feature = "arrow")]
37pub use arrow_export::{
38 PIO_ARROW_TABLE_BDOUBLEPRIME, PIO_ARROW_TABLE_BPRIME, PIO_ARROW_TABLE_BRANCH,
39 PIO_ARROW_TABLE_BUS, PIO_ARROW_TABLE_GEN, PIO_ARROW_TABLE_INCIDENCE, PIO_ARROW_TABLE_LOAD,
40 PIO_ARROW_TABLE_MATRIX_BRANCH, PIO_ARROW_TABLE_MATRIX_BUS, PIO_ARROW_TABLE_SHUNT,
41 PIO_ARROW_TABLE_SOLVER_ARC, PIO_ARROW_TABLE_SOLVER_BRANCH, PIO_ARROW_TABLE_SOLVER_BUS,
42 PIO_ARROW_TABLE_SOLVER_GEN, PIO_ARROW_TABLE_SOLVER_HVDC, PIO_ARROW_TABLE_SOLVER_LOAD,
43 PIO_ARROW_TABLE_SOLVER_SHUNT, PIO_ARROW_TABLE_SOLVER_STORAGE, PIO_ARROW_TABLE_SOLVER_SWITCH,
44 PIO_ARROW_TABLE_SWITCH, PIO_ARROW_TABLE_YBUS,
45};
46
47pub struct PioNetwork {
52 net: BalancedNetwork,
53 core: IndexCore,
54 warnings: Vec<String>,
55}
56
57const _: fn() = || {
62 fn assert_send_sync<T: Send + Sync>() {}
63 assert_send_sync::<PioNetwork>();
64};
65
66unsafe fn copy_to_buf(buf: *mut c_char, len: usize, msg: &str) {
76 unsafe {
77 if buf.is_null() || len == 0 {
78 return;
79 }
80 let bytes = msg.as_bytes();
81 let mut n = bytes.len().min(len - 1);
82 while n > 0 && !msg.is_char_boundary(n) {
83 n -= 1;
84 }
85 std::ptr::copy_nonoverlapping(bytes.as_ptr().cast::<c_char>(), buf, n);
86 *buf.add(n) = 0;
87 }
88}
89
90unsafe fn cstr<'a>(p: *const c_char) -> Option<&'a str> {
91 unsafe {
92 if p.is_null() {
93 return None;
94 }
95 CStr::from_ptr(p).to_str().ok()
96 }
97}
98
99fn into_cstring(s: String) -> Option<*mut c_char> {
103 CString::new(s).ok().map(CString::into_raw)
104}
105
106fn finish_cstring(s: String, errbuf: *mut c_char, errlen: usize) -> *mut c_char {
110 match into_cstring(s) {
111 Some(p) => p,
112 None => {
113 unsafe { copy_to_buf(errbuf, errlen, "output contained an interior NUL byte") };
114 std::ptr::null_mut()
115 }
116 }
117}
118
119unsafe fn finish_string(
126 errbuf: *mut c_char,
127 errlen: usize,
128 panic_msg: &str,
129 f: impl FnOnce() -> Result<String, String>,
130) -> *mut c_char {
131 unsafe {
132 match catch_unwind(AssertUnwindSafe(f)) {
133 Ok(Ok(text)) => finish_cstring(text, errbuf, errlen),
134 Ok(Err(msg)) => {
135 copy_to_buf(errbuf, errlen, &msg);
136 std::ptr::null_mut()
137 }
138 Err(_) => {
139 copy_to_buf(errbuf, errlen, panic_msg);
140 std::ptr::null_mut()
141 }
142 }
143 }
144}
145
146unsafe fn guard<R>(fallback: R, f: impl FnOnce() -> R) -> R {
149 catch_unwind(AssertUnwindSafe(f)).unwrap_or(fallback)
150}
151
152fn make_network(net: BalancedNetwork, warnings: Vec<String>) -> *mut PioNetwork {
155 let core = IndexCore::build(&net);
156 Box::into_raw(Box::new(PioNetwork {
157 net,
158 core,
159 warnings,
160 }))
161}
162
163unsafe fn finish_network(
169 errbuf: *mut c_char,
170 errlen: usize,
171 panic_msg: &str,
172 f: impl FnOnce() -> Result<(BalancedNetwork, Vec<String>), String>,
173) -> *mut PioNetwork {
174 unsafe {
175 match catch_unwind(AssertUnwindSafe(|| {
179 f().map(|(net, warnings)| make_network(net, warnings))
180 })) {
181 Ok(Ok(handle)) => handle,
182 Ok(Err(msg)) => {
183 copy_to_buf(errbuf, errlen, &msg);
184 std::ptr::null_mut()
185 }
186 Err(_) => {
187 copy_to_buf(errbuf, errlen, panic_msg);
188 std::ptr::null_mut()
189 }
190 }
191 }
192}
193
194pub const PIO_ABI_VERSION: u32 = 5;
211
212#[cfg(feature = "dist")]
223pub const PIO_DIST_ABI_VERSION: u32 = 1;
224
225pub const PIO_ERRBUF_MIN: usize = 256;
228
229#[unsafe(no_mangle)]
232pub extern "C" fn pio_abi_version() -> u32 {
233 PIO_ABI_VERSION
234}
235
236#[cfg(feature = "dist")]
240#[unsafe(no_mangle)]
241pub extern "C" fn pio_dist_abi_version() -> u32 {
242 PIO_DIST_ABI_VERSION
243}
244
245#[cfg(feature = "dist")]
246fn dist_capabilities_json() -> String {
247 serde_json::json!({
248 "dist": true,
249 powerio::version::VERSION_KEY: powerio::VERSION,
253 "bmopf_fixed_taps": true,
254 "bmopf_center_tap_leakage": true,
255 "bmopf_delta_wye_leakage": true,
256 "bmopf_delta_roll": true,
257 "bmopf_voltage_source_merge": true,
258 "bmopf_transformer_diagnostics": true,
259 "bmopf_schema_id": powerio_dist::BMOPF_SCHEMA_ID,
261 "bmopf_schema_version": powerio_dist::BMOPF_SCHEMA_VERSION,
262 "typed_capacitors": true,
263 "line_and_generator_ratings": true,
264 "per_sequence_bus_bounds": true,
265 "transformer_extras_relocation": true,
266 })
267 .to_string()
268}
269
270#[cfg(feature = "dist")]
277#[unsafe(no_mangle)]
278pub extern "C" fn pio_dist_capabilities_json() -> *mut c_char {
279 unsafe {
283 guard(std::ptr::null_mut(), || {
284 into_cstring(dist_capabilities_json()).unwrap_or(std::ptr::null_mut())
285 })
286 }
287}
288
289#[unsafe(no_mangle)]
299pub extern "C" fn pio_schema_versions_json() -> *mut c_char {
300 unsafe { guard(std::ptr::null_mut(), schema_versions_json_ptr) }
301}
302
303fn schema_versions_json_ptr() -> *mut c_char {
305 #[cfg(feature = "dist")]
310 let bmopf_schema = Some(powerio_dist::BMOPF_SCHEMA_VERSION);
311 #[cfg(not(feature = "dist"))]
313 let bmopf_schema: Option<&str> = None;
314
315 let doc = serde_json::json!({
316 powerio::version::VERSION_KEY: powerio::VERSION,
317 "abi": PIO_ABI_VERSION,
318 "bmopf_schema": bmopf_schema,
319 });
320 into_cstring(doc.to_string()).unwrap_or(std::ptr::null_mut())
321}
322
323#[unsafe(no_mangle)]
336pub extern "C" fn pio_build_info() -> *mut c_char {
337 unsafe { guard(std::ptr::null_mut(), build_info_ptr) }
338}
339
340fn build_info_ptr() -> *mut c_char {
342 #[cfg(feature = "dist")]
343 let bmopf_schema = Some(powerio_dist::BMOPF_SCHEMA_VERSION);
344 #[cfg(not(feature = "dist"))]
345 let bmopf_schema: Option<&str> = None;
346
347 let doc = serde_json::json!({
348 powerio::version::VERSION_KEY: powerio::VERSION,
349 "abi": PIO_ABI_VERSION,
350 "features": {
351 "arrow": cfg!(feature = "arrow"),
352 "matrix": cfg!(feature = "matrix"),
353 "gridfm": cfg!(feature = "gridfm"),
354 "dist": cfg!(feature = "dist"),
355 "pkg": cfg!(feature = "pkg"),
356 "prob": cfg!(feature = "prob"),
357 },
358 "foreign_schemas": { "bmopf": bmopf_schema },
362 "error_categories": powerio::ErrorCategory::TOKENS,
363 });
364 into_cstring(doc.to_string()).unwrap_or(std::ptr::null_mut())
365}
366
367#[unsafe(no_mangle)]
371pub extern "C" fn pio_matrix_available() -> i32 {
372 i32::from(cfg!(all(feature = "arrow", feature = "matrix")))
373}
374
375#[unsafe(no_mangle)]
384pub unsafe extern "C" fn pio_has_feature(feature: *const c_char) -> i32 {
385 unsafe {
386 guard(0, || {
387 let Some(name) = cstr(feature) else { return 0 };
388 let features: &[(&str, bool)] = &[
389 ("arrow", cfg!(feature = "arrow")),
390 ("matrix", cfg!(feature = "matrix")),
391 ("gridfm", cfg!(feature = "gridfm")),
392 ("dist", cfg!(feature = "dist")),
393 ("pkg", cfg!(feature = "pkg")),
394 ("prob", cfg!(feature = "prob")),
395 ];
396 i32::from(features.iter().any(|&(n, on)| n == name && on))
397 })
398 }
399}
400
401#[unsafe(no_mangle)]
405pub extern "C" fn pio_version() -> *const c_char {
406 concat!(env!("CARGO_PKG_VERSION"), "\0")
410 .as_ptr()
411 .cast::<c_char>()
412}
413
414fn target_format_from_c(to: *const c_char) -> Result<TargetFormat, String> {
415 let to = unsafe { cstr(to) }.ok_or_else(|| "to is NULL or not UTF-8".to_string())?;
416 to.parse::<TargetFormat>().map_err(|e| e.to_string())
417}
418
419fn optional_cstr<'a>(p: *const c_char, name: &str) -> Result<Option<&'a str>, String> {
420 if p.is_null() {
421 Ok(None)
422 } else {
423 unsafe { cstr(p) }
424 .map(Some)
425 .ok_or_else(|| format!("{name} is not UTF-8"))
426 }
427}
428
429fn required_cstr<'a>(p: *const c_char, name: &str) -> Result<&'a str, String> {
434 unsafe { cstr(p) }.ok_or_else(|| format!("{name} is NULL or not UTF-8"))
435}
436
437#[unsafe(no_mangle)]
446pub unsafe extern "C" fn pio_parse_file(
447 path: *const c_char,
448 from: *const c_char,
449 errbuf: *mut c_char,
450 errlen: usize,
451) -> *mut PioNetwork {
452 unsafe {
453 finish_network(errbuf, errlen, "panic while parsing", || {
454 let path = cstr(path).ok_or_else(|| "path is NULL or not UTF-8".to_string())?;
455 let from = optional_cstr(from, "from")?;
456 powerio::parse_file(std::path::Path::new(path), from)
457 .map(|p| (p.network, p.warnings))
458 .map_err(|e| e.to_string())
459 })
460 }
461}
462
463#[unsafe(no_mangle)]
474pub unsafe extern "C" fn pio_parse_str(
475 text: *const c_char,
476 format: *const c_char,
477 errbuf: *mut c_char,
478 errlen: usize,
479) -> *mut PioNetwork {
480 unsafe {
481 finish_network(errbuf, errlen, "panic while parsing", || {
482 let text = cstr(text).ok_or_else(|| "text is NULL or not UTF-8".to_string())?;
483 let format = cstr(format).ok_or_else(|| "format is NULL or not UTF-8".to_string())?;
484 powerio::parse_str(text, format)
485 .map(|p| (p.network, p.warnings))
486 .map_err(|e| e.to_string())
487 })
488 }
489}
490
491#[unsafe(no_mangle)]
502pub unsafe extern "C" fn pio_parse_bytes(
503 bytes: *const u8,
504 len: usize,
505 format: *const c_char,
506 errbuf: *mut c_char,
507 errlen: usize,
508) -> *mut PioNetwork {
509 unsafe {
510 finish_network(errbuf, errlen, "panic while parsing", || {
511 let format = required_cstr(format, "format")?;
512 let slice = if len == 0 {
515 &[][..]
516 } else if bytes.is_null() {
517 return Err("bytes is NULL".to_string());
518 } else {
519 std::slice::from_raw_parts(bytes, len)
520 };
521 powerio::parse_bytes(slice, format)
522 .map(|p| (p.network, p.warnings))
523 .map_err(|e| e.to_string())
524 })
525 }
526}
527
528#[unsafe(no_mangle)]
543pub unsafe extern "C" fn pio_classify_str(
544 text: *const c_char,
545 outbuf: *mut c_char,
546 outlen: usize,
547) -> usize {
548 unsafe {
549 copy_to_buf(outbuf, outlen, "");
552 guard(0, || {
553 let Ok(text) = required_cstr(text, "text") else {
554 return 0;
555 };
556 let label = classify_label(text);
557 copy_to_buf(outbuf, outlen, &label);
558 label.len()
559 })
560 }
561}
562
563fn classify_label(text: &str) -> String {
564 use powerio::format::routing::{self, Detection, Domain, JsonClass};
565 match routing::classify_json_text(text) {
566 JsonClass::Package => "package".to_string(),
567 JsonClass::Case(Detection::Known(format)) => {
568 let domain = match format.domain() {
569 Domain::Transmission => "transmission",
570 Domain::Distribution => "distribution",
571 _ => return "unknown".to_string(),
572 };
573 format!("{domain}:{}", format.name())
574 }
575 JsonClass::Case(Detection::Ambiguous) => "ambiguous".to_string(),
576 JsonClass::Case(Detection::Unknown) => "unknown".to_string(),
577 }
578}
579
580#[unsafe(no_mangle)]
587pub unsafe extern "C" fn pio_to_json(
588 net: *const PioNetwork,
589 errbuf: *mut c_char,
590 errlen: usize,
591) -> *mut c_char {
592 unsafe {
593 finish_string(errbuf, errlen, "panic while serializing model JSON", || {
594 let net = net
595 .as_ref()
596 .ok_or_else(|| "network handle is NULL".to_string())?;
597 net.net.to_json().map_err(|e| e.to_string())
598 })
599 }
600}
601
602#[unsafe(no_mangle)]
608pub unsafe extern "C" fn pio_from_json(
609 text: *const c_char,
610 errbuf: *mut c_char,
611 errlen: usize,
612) -> *mut PioNetwork {
613 unsafe {
614 finish_network(errbuf, errlen, "panic while parsing model JSON", || {
615 let text = required_cstr(text, "text")?;
616 BalancedNetwork::from_json(text)
617 .map(|net| (net, Vec::new()))
618 .map_err(|e| e.to_string())
619 })
620 }
621}
622
623#[cfg(feature = "gridfm")]
634#[unsafe(no_mangle)]
635pub unsafe extern "C" fn pio_read_dir(
636 dir: *const c_char,
637 from: *const c_char,
638 scenario: i64,
639 errbuf: *mut c_char,
640 errlen: usize,
641) -> *mut PioNetwork {
642 unsafe {
643 finish_network(errbuf, errlen, "panic while reading dataset", || {
644 let dir = cstr(dir).ok_or_else(|| "dir is NULL or not UTF-8".to_string())?;
645 let from = cstr(from).ok_or_else(|| "from is NULL or not UTF-8".to_string())?;
646 powerio_matrix::read_dataset_dir(std::path::Path::new(dir), from, scenario)
647 .map(|read| (read.network, read.warnings))
648 .map_err(|e| e.to_string())
649 })
650 }
651}
652
653#[cfg(feature = "gridfm")]
660#[unsafe(no_mangle)]
661pub unsafe extern "C" fn pio_scenario_ids(
662 dir: *const c_char,
663 from: *const c_char,
664 out: *mut i64,
665 cap: usize,
666 errbuf: *mut c_char,
667 errlen: usize,
668) -> isize {
669 unsafe {
670 let r = catch_unwind(AssertUnwindSafe(|| {
671 let dir = cstr(dir).ok_or_else(|| "dir is NULL or not UTF-8".to_string())?;
672 let from = cstr(from).ok_or_else(|| "from is NULL or not UTF-8".to_string())?;
673 powerio_matrix::dataset_scenario_ids(std::path::Path::new(dir), from)
674 .map_err(|e| e.to_string())
675 }));
676 match r {
677 Ok(Ok(ids)) => {
678 let Ok(total) = isize::try_from(ids.len()) else {
679 copy_to_buf(errbuf, errlen, "scenario count exceeds isize");
680 return -1;
681 };
682 fill(out, cap, ids.iter().copied());
683 total
684 }
685 Ok(Err(msg)) => {
686 copy_to_buf(errbuf, errlen, &msg);
687 -1
688 }
689 Err(_) => {
690 copy_to_buf(errbuf, errlen, "panic while reading scenario ids");
691 -1
692 }
693 }
694 }
695}
696
697#[unsafe(no_mangle)]
705pub unsafe extern "C" fn pio_warnings(
706 net: *const PioNetwork,
707 warnbuf: *mut c_char,
708 warnlen: usize,
709) -> usize {
710 unsafe {
711 guard(0, || {
712 let Some(c) = network_ref(net) else { return 0 };
713 let msg = c.warnings.join("\n");
714 copy_to_buf(warnbuf, warnlen, &msg);
715 msg.len()
716 })
717 }
718}
719
720#[unsafe(no_mangle)]
723pub unsafe extern "C" fn pio_network_free(net: *mut PioNetwork) {
724 unsafe {
725 guard((), || {
729 if !net.is_null() {
730 drop(Box::from_raw(net));
731 }
732 });
733 }
734}
735
736unsafe fn network_ref<'a>(net: *const PioNetwork) -> Option<&'a PioNetwork> {
737 unsafe { net.as_ref() }
738}
739
740unsafe fn view<'a>(net: *const PioNetwork) -> Option<IndexedNetwork<'a>> {
742 unsafe {
743 net.as_ref()
744 .map(|c| IndexedNetwork::with_core(&c.net, &c.core))
745 }
746}
747
748#[unsafe(no_mangle)]
757pub unsafe extern "C" fn pio_normalize(
758 net: *const PioNetwork,
759 errbuf: *mut c_char,
760 errlen: usize,
761) -> *mut PioNetwork {
762 unsafe {
763 finish_network(errbuf, errlen, "panic while normalizing", || {
764 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
765 c.net
766 .to_normalized()
767 .map(|n| (n, c.warnings.clone()))
768 .map_err(|e| e.to_string())
769 })
770 }
771}
772
773#[unsafe(no_mangle)]
783pub unsafe extern "C" fn pio_normalize_with_options(
784 net: *const PioNetwork,
785 clamp_angle_bounds: i32,
786 angle_bound_pad: f64,
787 errbuf: *mut c_char,
788 errlen: usize,
789) -> *mut PioNetwork {
790 unsafe {
791 finish_network(
792 errbuf,
793 errlen,
794 "panic while normalizing with options",
795 || {
796 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
797 let options = NormalizeOptions {
798 clamp_angle_bounds: clamp_angle_bounds != 0,
799 angle_bound_pad,
800 };
801 let out = c
802 .net
803 .to_normalized_with_options(&options)
804 .map_err(|e| e.to_string())?;
805 let mut warnings = c.warnings.clone();
806 warnings.extend(out.warnings);
807 Ok((out.network, warnings))
808 },
809 )
810 }
811}
812
813#[unsafe(no_mangle)]
814pub unsafe extern "C" fn pio_n_buses(net: *const PioNetwork) -> usize {
815 unsafe { guard(0, || view(net).map_or(0, |v| v.n())) }
821}
822
823#[unsafe(no_mangle)]
824pub unsafe extern "C" fn pio_n_branches(net: *const PioNetwork) -> usize {
825 unsafe { guard(0, || view(net).map_or(0, |v| v.branches().len())) }
830}
831
832#[unsafe(no_mangle)]
833pub unsafe extern "C" fn pio_n_switches(net: *const PioNetwork) -> usize {
834 unsafe { guard(0, || network_ref(net).map_or(0, |c| c.net.switches.len())) }
835}
836
837#[unsafe(no_mangle)]
838pub unsafe extern "C" fn pio_n_gens(net: *const PioNetwork) -> usize {
839 unsafe { guard(0, || network_ref(net).map_or(0, |c| c.net.generators.len())) }
840}
841
842#[unsafe(no_mangle)]
843pub unsafe extern "C" fn pio_base_mva(net: *const PioNetwork) -> f64 {
844 unsafe { guard(0.0, || network_ref(net).map_or(0.0, |c| c.net.base_mva)) }
845}
846
847#[unsafe(no_mangle)]
851pub unsafe extern "C" fn pio_network_name(
852 net: *const PioNetwork,
853 out: *mut c_char,
854 cap: usize,
855) -> usize {
856 unsafe {
857 guard(0, || {
858 let Some(c) = network_ref(net) else { return 0 };
859 copy_to_buf(out, cap, &c.net.name);
860 c.net.name.len()
861 })
862 }
863}
864
865#[unsafe(no_mangle)]
869pub unsafe extern "C" fn pio_source_format(
870 net: *const PioNetwork,
871 out: *mut c_char,
872 cap: usize,
873) -> usize {
874 unsafe {
875 guard(0, || {
876 let Some(c) = network_ref(net) else { return 0 };
877 let name = format!("{:?}", c.net.source_format);
878 copy_to_buf(out, cap, &name);
879 name.len()
880 })
881 }
882}
883
884#[unsafe(no_mangle)]
894pub unsafe extern "C" fn pio_summary_json(
895 net: *const PioNetwork,
896 errbuf: *mut c_char,
897 errlen: usize,
898) -> *mut c_char {
899 unsafe {
900 finish_string(
901 errbuf,
902 errlen,
903 "panic while serializing summary JSON",
904 || {
905 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
906 let v = IndexedNetwork::with_core(&c.net, &c.core);
907 let reference_bus_indices = v.reference_bus_indices();
908 let reference_bus_ids: Vec<usize> = reference_bus_indices
909 .iter()
910 .map(|&idx| v.bus_id(idx).0)
911 .collect();
912 let summary = serde_json::json!({
913 powerio::version::VERSION_KEY: powerio::VERSION,
914 "name": c.net.name,
915 "source_format": format!("{:?}", c.net.source_format),
916 "base_mva": c.net.base_mva,
917 "base_frequency": c.net.base_frequency,
918 "counts": {
919 "buses": c.net.buses.len(),
920 "loads": c.net.loads.len(),
921 "shunts": c.net.shunts.len(),
922 "branches": c.net.branches.len(),
923 "switches": c.net.switches.len(),
924 "generators": c.net.generators.len(),
925 "storage": c.net.storage.len(),
926 "hvdc": c.net.hvdc.len(),
927 "transformers_3w": c.net.transformers_3w.len(),
928 "areas": c.net.areas.len(),
929 "warnings": c.warnings.len(),
930 },
931 "topology": {
932 "n_buses": v.n(),
933 "n_branches": v.branches().len(),
934 "reference_bus_ids": reference_bus_ids,
935 "reference_bus_indices": reference_bus_indices,
936 "n_components": v.n_connected_components(),
937 "is_radial": v.is_radial(),
938 },
939 });
940 serde_json::to_string(&summary).map_err(|e| e.to_string())
941 },
942 )
943 }
944}
945
946#[unsafe(no_mangle)]
953pub unsafe extern "C" fn pio_ref_bus_index(net: *const PioNetwork) -> i64 {
954 unsafe {
955 guard(-1, || match view(net) {
956 Some(v) => v
957 .reference_bus_index()
958 .map_or(-1, |i| i64::try_from(i).unwrap_or(-1)),
959 None => -1,
960 })
961 }
962}
963
964#[unsafe(no_mangle)]
970pub unsafe extern "C" fn pio_ref_bus_indices(
971 net: *const PioNetwork,
972 out: *mut i64,
973 cap: usize,
974) -> usize {
975 unsafe {
976 guard(0, || {
977 view(net).map_or(0, |v| {
978 fill(
979 out,
980 cap,
981 v.reference_bus_indices()
982 .into_iter()
983 .map(|i| i64::try_from(i).unwrap_or(-1)),
984 )
985 })
986 })
987 }
988}
989
990#[unsafe(no_mangle)]
992pub unsafe extern "C" fn pio_n_islands(net: *const PioNetwork) -> usize {
993 unsafe { guard(0, || view(net).map_or(0, |v| v.n_connected_components())) }
994}
995
996#[unsafe(no_mangle)]
998pub unsafe extern "C" fn pio_is_radial(net: *const PioNetwork) -> i32 {
999 unsafe { guard(0, || view(net).map_or(0, |v| i32::from(v.is_radial()))) }
1000}
1001
1002#[unsafe(no_mangle)]
1016pub unsafe extern "C" fn pio_to_format(
1017 net: *const PioNetwork,
1018 to: *const c_char,
1019 out_warnings: *mut *mut c_char,
1020 errbuf: *mut c_char,
1021 errlen: usize,
1022) -> *mut c_char {
1023 unsafe {
1024 finish_conversion(out_warnings, errbuf, errlen, || {
1025 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
1026 let target = target_format_from_c(to)?;
1027 let conv = c.net.to_format(target).map_err(|e| e.to_string())?;
1028 Ok((conv.text, conv.warnings))
1029 })
1030 }
1031}
1032
1033unsafe fn set_out_warnings(out_warnings: *mut *mut c_char, warnings: &[String]) {
1042 unsafe {
1043 if out_warnings.is_null() {
1044 return;
1045 }
1046 *out_warnings = if warnings.is_empty() {
1047 std::ptr::null_mut()
1048 } else {
1049 into_cstring(warnings.join("\n")).unwrap_or(std::ptr::null_mut())
1053 };
1054 }
1055}
1056
1057unsafe fn finish_conversion(
1064 out_warnings: *mut *mut c_char,
1065 errbuf: *mut c_char,
1066 errlen: usize,
1067 f: impl FnOnce() -> Result<(String, Vec<String>), String>,
1068) -> *mut c_char {
1069 unsafe {
1070 set_out_warnings(out_warnings, &[]);
1073 match catch_unwind(AssertUnwindSafe(f)) {
1074 Ok(Ok((text, warnings))) => {
1075 set_out_warnings(out_warnings, &warnings);
1076 finish_cstring(text, errbuf, errlen)
1077 }
1078 Ok(Err(msg)) => {
1079 copy_to_buf(errbuf, errlen, &msg);
1080 std::ptr::null_mut()
1081 }
1082 Err(_) => {
1083 copy_to_buf(errbuf, errlen, "panic while converting");
1084 std::ptr::null_mut()
1085 }
1086 }
1087 }
1088}
1089
1090#[unsafe(no_mangle)]
1097pub unsafe extern "C" fn pio_convert_file(
1098 path: *const c_char,
1099 from: *const c_char,
1100 to: *const c_char,
1101 out_warnings: *mut *mut c_char,
1102 errbuf: *mut c_char,
1103 errlen: usize,
1104) -> *mut c_char {
1105 unsafe {
1106 finish_conversion(out_warnings, errbuf, errlen, || {
1107 let path = cstr(path).ok_or_else(|| "path is NULL or not UTF-8".to_string())?;
1108 let from = optional_cstr(from, "from")?;
1109 let target = target_format_from_c(to)?;
1110 let conv = powerio::convert_file(std::path::Path::new(path), target, from)
1111 .map_err(|e| e.to_string())?;
1112 Ok((conv.text, conv.warnings))
1113 })
1114 }
1115}
1116
1117#[unsafe(no_mangle)]
1123pub unsafe extern "C" fn pio_convert_str(
1124 text: *const c_char,
1125 from: *const c_char,
1126 to: *const c_char,
1127 out_warnings: *mut *mut c_char,
1128 errbuf: *mut c_char,
1129 errlen: usize,
1130) -> *mut c_char {
1131 unsafe {
1132 finish_conversion(out_warnings, errbuf, errlen, || {
1133 let text = cstr(text).ok_or_else(|| "text is NULL or not UTF-8".to_string())?;
1134 let from = cstr(from).ok_or_else(|| "from is NULL or not UTF-8".to_string())?;
1135 let target = target_format_from_c(to)?;
1136 let conv = powerio::convert_str(text, target, from).map_err(|e| e.to_string())?;
1137 Ok((conv.text, conv.warnings))
1138 })
1139 }
1140}
1141
1142#[unsafe(no_mangle)]
1149pub unsafe extern "C" fn pio_write_dir(
1150 net: *const PioNetwork,
1151 to: *const c_char,
1152 out_dir: *const c_char,
1153 out_warnings: *mut *mut c_char,
1154 errbuf: *mut c_char,
1155 errlen: usize,
1156) -> i32 {
1157 unsafe {
1158 set_out_warnings(out_warnings, &[]);
1159 let r = catch_unwind(AssertUnwindSafe(|| {
1160 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
1161 let to = cstr(to).ok_or_else(|| "to is NULL or not UTF-8".to_string())?;
1162 let out_dir =
1163 cstr(out_dir).ok_or_else(|| "out_dir is NULL or not UTF-8".to_string())?;
1164 powerio::write_dir(&c.net, to, std::path::Path::new(out_dir)).map_err(|e| e.to_string())
1165 }));
1166 match r {
1167 Ok(Ok(warnings)) => {
1168 set_out_warnings(out_warnings, &warnings);
1169 0
1170 }
1171 Ok(Err(msg)) => {
1172 copy_to_buf(errbuf, errlen, &msg);
1173 -1
1174 }
1175 Err(_) => {
1176 copy_to_buf(errbuf, errlen, "panic while writing directory");
1177 -1
1178 }
1179 }
1180 }
1181}
1182
1183#[unsafe(no_mangle)]
1185pub unsafe extern "C" fn pio_string_free(s: *mut c_char) {
1186 unsafe {
1187 guard((), || {
1189 if !s.is_null() {
1190 drop(CString::from_raw(s));
1191 }
1192 });
1193 }
1194}
1195
1196unsafe fn fill<T: Copy>(out: *mut T, cap: usize, vals: impl ExactSizeIterator<Item = T>) -> usize {
1200 unsafe {
1201 let total = vals.len();
1202 if !out.is_null() {
1203 for (i, v) in vals.take(cap).enumerate() {
1204 *out.add(i) = v;
1205 }
1206 }
1207 total
1208 }
1209}
1210
1211#[unsafe(no_mangle)]
1218pub unsafe extern "C" fn pio_bus_ids(net: *const PioNetwork, out: *mut i64, cap: usize) -> usize {
1219 unsafe {
1220 guard(0, || {
1221 view(net).map_or(0, |v| {
1225 fill(
1226 out,
1227 cap,
1228 (0..v.n()).map(|i| i64::try_from(v.bus_id(i).0).unwrap_or(-1)),
1229 )
1230 })
1231 })
1232 }
1233}
1234
1235#[unsafe(no_mangle)]
1242pub unsafe extern "C" fn pio_branches(
1243 net: *const PioNetwork,
1244 from: *mut i64,
1245 to: *mut i64,
1246 r: *mut f64,
1247 x: *mut f64,
1248 b: *mut f64,
1249 tap: *mut f64,
1250 shift: *mut f64,
1251 in_service: *mut u8,
1252 cap: usize,
1253) -> usize {
1254 unsafe {
1255 guard(0, || {
1256 let Some(v) = view(net) else { return 0 };
1257 let branches = v.branches();
1258 fill(
1259 from,
1260 cap,
1261 branches
1262 .iter()
1263 .map(|br| i64::try_from(br.from.0).unwrap_or(-1)),
1264 );
1265 fill(
1266 to,
1267 cap,
1268 branches
1269 .iter()
1270 .map(|br| i64::try_from(br.to.0).unwrap_or(-1)),
1271 );
1272 fill(r, cap, branches.iter().map(|br| br.r));
1273 fill(x, cap, branches.iter().map(|br| br.x));
1274 fill(b, cap, branches.iter().map(|br| br.total_charging_b()));
1275 fill(tap, cap, branches.iter().map(|br| br.tap));
1276 fill(shift, cap, branches.iter().map(|br| br.shift));
1277 fill(
1278 in_service,
1279 cap,
1280 branches.iter().map(|br| u8::from(br.in_service)),
1281 );
1282 branches.len()
1283 })
1284 }
1285}
1286
1287#[unsafe(no_mangle)]
1290pub unsafe extern "C" fn pio_branch_charging(
1291 net: *const PioNetwork,
1292 g_fr: *mut f64,
1293 b_fr: *mut f64,
1294 g_to: *mut f64,
1295 b_to: *mut f64,
1296 cap: usize,
1297) -> usize {
1298 unsafe {
1299 guard(0, || {
1300 let Some(v) = view(net) else { return 0 };
1301 let branches = v.branches();
1302 fill(
1303 g_fr,
1304 cap,
1305 branches.iter().map(|br| br.terminal_charging().g_fr),
1306 );
1307 fill(
1308 b_fr,
1309 cap,
1310 branches.iter().map(|br| br.terminal_charging().b_fr),
1311 );
1312 fill(
1313 g_to,
1314 cap,
1315 branches.iter().map(|br| br.terminal_charging().g_to),
1316 );
1317 fill(
1318 b_to,
1319 cap,
1320 branches.iter().map(|br| br.terminal_charging().b_to),
1321 );
1322 branches.len()
1323 })
1324 }
1325}
1326
1327#[unsafe(no_mangle)]
1330pub unsafe extern "C" fn pio_switches(
1331 net: *const PioNetwork,
1332 from: *mut i64,
1333 to: *mut i64,
1334 closed: *mut u8,
1335 thermal_rating: *mut f64,
1336 current_rating: *mut f64,
1337 pf: *mut f64,
1338 qf: *mut f64,
1339 pt: *mut f64,
1340 qt: *mut f64,
1341 cap: usize,
1342) -> usize {
1343 unsafe {
1344 guard(0, || {
1345 let Some(c) = network_ref(net) else { return 0 };
1346 let net = &c.net;
1347 fill(
1348 from,
1349 cap,
1350 net.switches
1351 .iter()
1352 .map(|sw| i64::try_from(sw.from.0).unwrap_or(-1)),
1353 );
1354 fill(
1355 to,
1356 cap,
1357 net.switches
1358 .iter()
1359 .map(|sw| i64::try_from(sw.to.0).unwrap_or(-1)),
1360 );
1361 fill(
1362 closed,
1363 cap,
1364 net.switches.iter().map(|sw| u8::from(sw.closed)),
1365 );
1366 fill(
1367 thermal_rating,
1368 cap,
1369 net.switches
1370 .iter()
1371 .map(|sw| sw.thermal_rating.unwrap_or(0.0)),
1372 );
1373 fill(
1374 current_rating,
1375 cap,
1376 net.switches
1377 .iter()
1378 .map(|sw| sw.current_rating.unwrap_or(0.0)),
1379 );
1380 fill(pf, cap, net.switches.iter().map(|sw| sw.pf.unwrap_or(0.0)));
1381 fill(qf, cap, net.switches.iter().map(|sw| sw.qf.unwrap_or(0.0)));
1382 fill(pt, cap, net.switches.iter().map(|sw| sw.pt.unwrap_or(0.0)));
1383 fill(qt, cap, net.switches.iter().map(|sw| sw.qt.unwrap_or(0.0)));
1384 net.switches.len()
1385 })
1386 }
1387}
1388
1389#[unsafe(no_mangle)]
1393pub unsafe extern "C" fn pio_gens(
1394 net: *const PioNetwork,
1395 bus: *mut i64,
1396 pg: *mut f64,
1397 pmax: *mut f64,
1398 pmin: *mut f64,
1399 in_service: *mut u8,
1400 cap: usize,
1401) -> usize {
1402 unsafe {
1403 guard(0, || {
1404 let Some(c) = network_ref(net) else { return 0 };
1405 let net = &c.net;
1406 fill(
1407 bus,
1408 cap,
1409 net.generators
1410 .iter()
1411 .map(|g| i64::try_from(g.bus.0).unwrap_or(-1)),
1412 );
1413 fill(pg, cap, net.generators.iter().map(|g| g.pg));
1414 fill(pmax, cap, net.generators.iter().map(|g| g.pmax));
1415 fill(pmin, cap, net.generators.iter().map(|g| g.pmin));
1416 fill(
1417 in_service,
1418 cap,
1419 net.generators.iter().map(|g| u8::from(g.in_service)),
1420 );
1421 net.generators.len()
1422 })
1423 }
1424}
1425
1426#[unsafe(no_mangle)]
1430pub unsafe extern "C" fn pio_bus_demand(
1431 net: *const PioNetwork,
1432 pd: *mut f64,
1433 qd: *mut f64,
1434 cap: usize,
1435) -> usize {
1436 unsafe {
1437 guard(0, || {
1438 view(net).map_or(0, |v| {
1439 let n = fill(pd, cap, v.pd().iter().copied());
1442 fill(qd, cap, v.qd().iter().copied());
1443 n
1444 })
1445 })
1446 }
1447}
1448
1449#[unsafe(no_mangle)]
1453pub unsafe extern "C" fn pio_bus_shunt(
1454 net: *const PioNetwork,
1455 gs: *mut f64,
1456 bs: *mut f64,
1457 cap: usize,
1458) -> usize {
1459 unsafe {
1460 guard(0, || {
1461 view(net).map_or(0, |v| {
1462 let n = fill(gs, cap, v.gs().iter().copied());
1464 fill(bs, cap, v.bs().iter().copied());
1465 n
1466 })
1467 })
1468 }
1469}
1470
1471#[cfg(feature = "arrow")]
1489#[unsafe(no_mangle)]
1490pub unsafe extern "C" fn pio_to_arrow(
1491 net: *const PioNetwork,
1492 table: i32,
1493 out_array: *mut arrow::ffi::FFI_ArrowArray,
1494 out_schema: *mut arrow::ffi::FFI_ArrowSchema,
1495 errbuf: *mut c_char,
1496 errlen: usize,
1497) -> i32 {
1498 unsafe {
1499 let r = catch_unwind(AssertUnwindSafe(|| {
1500 if out_array.is_null() || out_schema.is_null() {
1501 return Err("out_array or out_schema is NULL".to_string());
1502 }
1503 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
1504 arrow_export::export(&c.net, &c.core, table)
1505 }));
1506 match r {
1507 Ok(Ok((array, schema))) => {
1508 std::ptr::write(out_array, array);
1513 std::ptr::write(out_schema, schema);
1514 0
1515 }
1516 Ok(Err(msg)) => {
1517 copy_to_buf(errbuf, errlen, &msg);
1518 -1
1519 }
1520 Err(_) => {
1521 copy_to_buf(errbuf, errlen, "panic while exporting Arrow");
1522 -1
1523 }
1524 }
1525 }
1526}
1527
1528#[cfg(feature = "arrow")]
1543#[unsafe(no_mangle)]
1544pub unsafe extern "C" fn pio_arrow_catalog_json(errbuf: *mut c_char, errlen: usize) -> *mut c_char {
1545 unsafe {
1546 finish_string(errbuf, errlen, "panic while building Arrow catalog", || {
1547 Ok(arrow_export::catalog_json())
1548 })
1549 }
1550}
1551
1552#[cfg(feature = "pkg")]
1562pub struct PioPackage {
1563 package: powerio_pkg::NetworkPackage,
1564}
1565
1566#[cfg(feature = "pkg")]
1567const _: fn() = || {
1568 fn assert_send_sync<T: Send + Sync>() {}
1569 assert_send_sync::<PioPackage>();
1570};
1571
1572#[cfg(feature = "pkg")]
1573fn lowering_options(base_mva: f64) -> powerio_pkg::MulticonductorToBalancedOptions {
1574 powerio_pkg::MulticonductorToBalancedOptions {
1575 base_mva,
1576 ..Default::default()
1577 }
1578}
1579
1580#[cfg(feature = "pkg")]
1581unsafe fn finish_package(
1582 errbuf: *mut c_char,
1583 errlen: usize,
1584 panic_msg: &str,
1585 f: impl FnOnce() -> Result<powerio_pkg::NetworkPackage, String>,
1586) -> *mut PioPackage {
1587 unsafe {
1588 match catch_unwind(AssertUnwindSafe(f)) {
1589 Ok(Ok(package)) => Box::into_raw(Box::new(PioPackage { package })),
1590 Ok(Err(msg)) => {
1591 copy_to_buf(errbuf, errlen, &msg);
1592 std::ptr::null_mut()
1593 }
1594 Err(_) => {
1595 copy_to_buf(errbuf, errlen, panic_msg);
1596 std::ptr::null_mut()
1597 }
1598 }
1599 }
1600}
1601
1602#[cfg(feature = "pkg")]
1608#[unsafe(no_mangle)]
1609pub unsafe extern "C" fn pio_package_parse_file(
1610 path: *const c_char,
1611 errbuf: *mut c_char,
1612 errlen: usize,
1613) -> *mut PioPackage {
1614 unsafe {
1615 finish_package(errbuf, errlen, "panic while parsing package", || {
1616 let path = required_cstr(path, "path")?;
1617 let text = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
1618 powerio_pkg::NetworkPackage::from_json(&text).map_err(|e| e.to_string())
1619 })
1620 }
1621}
1622
1623#[cfg(feature = "pkg")]
1627#[unsafe(no_mangle)]
1628pub unsafe extern "C" fn pio_package_parse_str(
1629 text: *const c_char,
1630 errbuf: *mut c_char,
1631 errlen: usize,
1632) -> *mut PioPackage {
1633 unsafe {
1634 finish_package(errbuf, errlen, "panic while parsing package", || {
1635 let text = required_cstr(text, "text")?;
1636 powerio_pkg::NetworkPackage::from_json(text).map_err(|e| e.to_string())
1637 })
1638 }
1639}
1640
1641#[cfg(feature = "pkg")]
1644#[unsafe(no_mangle)]
1645pub unsafe extern "C" fn pio_package_free(pkg: *mut PioPackage) {
1646 unsafe {
1647 guard((), || {
1648 if !pkg.is_null() {
1649 drop(Box::from_raw(pkg));
1650 }
1651 });
1652 }
1653}
1654
1655#[cfg(feature = "pkg")]
1660unsafe fn finish_package_json(
1661 pkg: *const PioPackage,
1662 errbuf: *mut c_char,
1663 errlen: usize,
1664 panic_msg: &str,
1665 f: impl FnOnce(&PioPackage) -> Result<String, String>,
1666) -> *mut c_char {
1667 unsafe {
1668 finish_string(errbuf, errlen, panic_msg, || {
1669 let pkg = pkg
1670 .as_ref()
1671 .ok_or_else(|| "package handle is NULL".to_string())?;
1672 f(pkg)
1673 })
1674 }
1675}
1676
1677#[cfg(feature = "pkg")]
1680#[unsafe(no_mangle)]
1681pub unsafe extern "C" fn pio_package_to_json(
1682 pkg: *const PioPackage,
1683 errbuf: *mut c_char,
1684 errlen: usize,
1685) -> *mut c_char {
1686 unsafe {
1687 finish_package_json(
1688 pkg,
1689 errbuf,
1690 errlen,
1691 "panic while serializing package",
1692 |p| p.package.to_json().map_err(|e| e.to_string()),
1693 )
1694 }
1695}
1696
1697#[cfg(feature = "pkg")]
1702#[unsafe(no_mangle)]
1703pub unsafe extern "C" fn pio_package_from_balanced_network(
1704 net: *const PioNetwork,
1705 include_solver_metadata: i32,
1706 errbuf: *mut c_char,
1707 errlen: usize,
1708) -> *mut PioPackage {
1709 unsafe {
1710 finish_package(
1711 errbuf,
1712 errlen,
1713 "panic while packaging balanced network",
1714 || {
1715 let net = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
1716 let mut package = powerio_pkg::NetworkPackage::from_balanced_with_read_warnings(
1717 net.net.clone(),
1718 powerio_pkg::READ_TRANSMISSION_PARSE_WARNING,
1719 net.warnings.clone(),
1720 );
1721 if include_solver_metadata != 0 {
1722 package
1723 .attach_normalized_solver_table_metadata()
1724 .map_err(|e| e.to_string())?;
1725 }
1726 Ok(package)
1727 },
1728 )
1729 }
1730}
1731
1732#[cfg(all(feature = "pkg", feature = "dist"))]
1736#[unsafe(no_mangle)]
1737pub unsafe extern "C" fn pio_package_from_multiconductor_network(
1738 net: *const PioDistNetwork,
1739 errbuf: *mut c_char,
1740 errlen: usize,
1741) -> *mut PioPackage {
1742 unsafe {
1743 finish_package(
1744 errbuf,
1745 errlen,
1746 "panic while packaging multiconductor network",
1747 || {
1748 let net = net
1749 .as_ref()
1750 .ok_or_else(|| "distribution network handle is NULL".to_string())?;
1751 Ok(powerio_pkg::NetworkPackage::from_multiconductor(
1752 net.net.clone(),
1753 ))
1754 },
1755 )
1756 }
1757}
1758
1759#[cfg(feature = "pkg")]
1766#[unsafe(no_mangle)]
1767pub unsafe extern "C" fn pio_package_to_balanced_network(
1768 pkg: *const PioPackage,
1769 errbuf: *mut c_char,
1770 errlen: usize,
1771) -> *mut PioNetwork {
1772 unsafe {
1773 finish_network(
1774 errbuf,
1775 errlen,
1776 "panic while extracting balanced network",
1777 || {
1778 let pkg = pkg
1779 .as_ref()
1780 .ok_or_else(|| "package handle is NULL".to_string())?;
1781 let net = pkg.package.model.as_balanced().ok_or_else(|| {
1782 if cfg!(feature = "dist") {
1783 "package holds a multiconductor model, not balanced; use \
1784 pio_package_to_multiconductor_network"
1785 .to_string()
1786 } else {
1787 "package holds a multiconductor model, not balanced; extracting \
1788 it needs a build with the `dist` feature \
1789 (pio_package_to_multiconductor_network)"
1790 .to_string()
1791 }
1792 })?;
1793 let mut net = net.clone();
1794 net.source = None;
1799 Ok((net, Vec::new()))
1800 },
1801 )
1802 }
1803}
1804
1805#[cfg(all(feature = "pkg", feature = "dist"))]
1813#[unsafe(no_mangle)]
1814pub unsafe extern "C" fn pio_package_to_multiconductor_network(
1815 pkg: *const PioPackage,
1816 errbuf: *mut c_char,
1817 errlen: usize,
1818) -> *mut PioDistNetwork {
1819 unsafe {
1820 finish_handle(
1821 errbuf,
1822 errlen,
1823 "panic while extracting multiconductor network",
1824 || {
1825 let pkg = pkg
1826 .as_ref()
1827 .ok_or_else(|| "package handle is NULL".to_string())?;
1828 let net = pkg.package.model.as_multiconductor().ok_or_else(|| {
1829 "package holds a balanced model, not multiconductor; use \
1830 pio_package_to_balanced_network"
1831 .to_string()
1832 })?;
1833 let mut net = net.clone();
1834 net.source = None;
1838 net.defaulted = Default::default();
1839 Ok(PioDistNetwork { net })
1840 },
1841 )
1842 }
1843}
1844
1845#[cfg(feature = "pkg")]
1854#[unsafe(no_mangle)]
1855pub unsafe extern "C" fn pio_package_validate(
1856 pkg: *mut PioPackage,
1857 errbuf: *mut c_char,
1858 errlen: usize,
1859) -> i32 {
1860 unsafe {
1861 let result = catch_unwind(AssertUnwindSafe(|| {
1862 let pkg = pkg
1863 .as_mut()
1864 .ok_or_else(|| "package handle is NULL".to_string())?;
1865 pkg.package.run_sane_validation();
1866 Ok::<_, String>(())
1867 }));
1868 match result {
1869 Ok(Ok(())) => 0,
1870 Ok(Err(msg)) => {
1871 copy_to_buf(errbuf, errlen, &msg);
1872 -1
1873 }
1874 Err(_) => {
1875 copy_to_buf(errbuf, errlen, "panic while validating package");
1876 -1
1877 }
1878 }
1879 }
1880}
1881
1882#[cfg(feature = "pkg")]
1885#[unsafe(no_mangle)]
1886pub unsafe extern "C" fn pio_package_validation_json(
1887 pkg: *const PioPackage,
1888 errbuf: *mut c_char,
1889 errlen: usize,
1890) -> *mut c_char {
1891 unsafe {
1892 finish_package_json(
1893 pkg,
1894 errbuf,
1895 errlen,
1896 "panic while reading package validation",
1897 |p| serde_json::to_string(&p.package.validation).map_err(|e| e.to_string()),
1898 )
1899 }
1900}
1901
1902#[cfg(feature = "pkg")]
1905#[unsafe(no_mangle)]
1906pub unsafe extern "C" fn pio_package_diagnostics_json(
1907 pkg: *const PioPackage,
1908 errbuf: *mut c_char,
1909 errlen: usize,
1910) -> *mut c_char {
1911 unsafe {
1912 finish_package_json(
1913 pkg,
1914 errbuf,
1915 errlen,
1916 "panic while reading package diagnostics",
1917 |p| serde_json::to_string(&p.package.diagnostics).map_err(|e| e.to_string()),
1918 )
1919 }
1920}
1921
1922#[cfg(feature = "pkg")]
1926#[unsafe(no_mangle)]
1927pub unsafe extern "C" fn pio_package_operating_points_json(
1928 pkg: *const PioPackage,
1929 errbuf: *mut c_char,
1930 errlen: usize,
1931) -> *mut c_char {
1932 unsafe {
1933 finish_package_json(
1934 pkg,
1935 errbuf,
1936 errlen,
1937 "panic while reading package operating points",
1938 |p| serde_json::to_string(&p.package.operating_points).map_err(|e| e.to_string()),
1939 )
1940 }
1941}
1942
1943#[cfg(feature = "pkg")]
1951#[unsafe(no_mangle)]
1952pub unsafe extern "C" fn pio_package_set_operating_points(
1953 pkg: *mut PioPackage,
1954 json: *const c_char,
1955 errbuf: *mut c_char,
1956 errlen: usize,
1957) -> i32 {
1958 unsafe {
1959 let result = catch_unwind(AssertUnwindSafe(|| {
1960 let pkg = pkg
1961 .as_mut()
1962 .ok_or_else(|| "package handle is NULL".to_string())?;
1963 let json = required_cstr(json, "json")?;
1964 let series: Option<powerio_pkg::OperatingPointSeries> =
1965 serde_json::from_str(json).map_err(|e| e.to_string())?;
1966 match series {
1967 Some(series) => pkg.package.set_operating_points(series),
1968 None => pkg.package.clear_operating_points(),
1969 }
1970 pkg.package.run_sane_validation();
1971 Ok::<_, String>(())
1972 }));
1973 match result {
1974 Ok(Ok(())) => 0,
1975 Ok(Err(msg)) => {
1976 copy_to_buf(errbuf, errlen, &msg);
1977 -1
1978 }
1979 Err(_) => {
1980 copy_to_buf(
1981 errbuf,
1982 errlen,
1983 "panic while setting package operating points",
1984 );
1985 -1
1986 }
1987 }
1988 }
1989}
1990
1991#[cfg(feature = "pkg")]
1994#[unsafe(no_mangle)]
1995pub unsafe extern "C" fn pio_package_study_json(
1996 pkg: *const PioPackage,
1997 errbuf: *mut c_char,
1998 errlen: usize,
1999) -> *mut c_char {
2000 unsafe {
2001 finish_package_json(
2002 pkg,
2003 errbuf,
2004 errlen,
2005 "panic while reading package study block",
2006 |p| serde_json::to_string(&p.package.study).map_err(|e| e.to_string()),
2007 )
2008 }
2009}
2010
2011#[cfg(feature = "pkg")]
2016#[unsafe(no_mangle)]
2017pub unsafe extern "C" fn pio_package_materialize_operating_point(
2018 pkg: *const PioPackage,
2019 index: i64,
2020 errbuf: *mut c_char,
2021 errlen: usize,
2022) -> *mut PioPackage {
2023 unsafe {
2024 finish_package(
2025 errbuf,
2026 errlen,
2027 "panic while materializing package operating point",
2028 || {
2029 let pkg = pkg
2030 .as_ref()
2031 .ok_or_else(|| "package handle is NULL".to_string())?;
2032 let index = usize::try_from(index)
2033 .map_err(|_| "operating point index must be non-negative".to_string())?;
2034 pkg.package
2035 .materialize_operating_point(index)
2036 .map_err(|e| e.to_string())
2037 },
2038 )
2039 }
2040}
2041
2042#[cfg(feature = "pkg")]
2047#[unsafe(no_mangle)]
2048pub unsafe extern "C" fn pio_package_materialize_study_commit(
2049 pkg: *const PioPackage,
2050 index: i64,
2051 errbuf: *mut c_char,
2052 errlen: usize,
2053) -> *mut PioPackage {
2054 unsafe {
2055 finish_package(
2056 errbuf,
2057 errlen,
2058 "panic while materializing package study commit",
2059 || {
2060 let pkg = pkg
2061 .as_ref()
2062 .ok_or_else(|| "package handle is NULL".to_string())?;
2063 let index = usize::try_from(index)
2064 .map_err(|_| "study commit index must be non-negative".to_string())?;
2065 pkg.package
2066 .materialize_study_commit(index)
2067 .map_err(|e| e.to_string())
2068 },
2069 )
2070 }
2071}
2072
2073#[cfg(feature = "pkg")]
2077#[unsafe(no_mangle)]
2078pub unsafe extern "C" fn pio_package_multiconductor_to_balanced_preflight_json(
2079 pkg: *const PioPackage,
2080 base_mva: f64,
2081 errbuf: *mut c_char,
2082 errlen: usize,
2083) -> *mut c_char {
2084 unsafe {
2085 finish_string(
2086 errbuf,
2087 errlen,
2088 "panic while preflighting package lowering",
2089 || {
2090 let pkg = pkg
2091 .as_ref()
2092 .ok_or_else(|| "package handle is NULL".to_string())?;
2093 let net = pkg.package.as_multiconductor().ok_or_else(|| {
2094 format!(
2095 "multiconductor preflight requires a multiconductor package, got {:?}",
2096 pkg.package.model_kind()
2097 )
2098 })?;
2099 let report = powerio_pkg::check_multiconductor_to_balanced_lowering(
2100 net,
2101 lowering_options(base_mva),
2102 );
2103 serde_json::to_string(&report).map_err(|e| e.to_string())
2104 },
2105 )
2106 }
2107}
2108
2109#[cfg(feature = "pkg")]
2114#[unsafe(no_mangle)]
2115pub unsafe extern "C" fn pio_package_lower_multiconductor_to_balanced(
2116 pkg: *const PioPackage,
2117 base_mva: f64,
2118 errbuf: *mut c_char,
2119 errlen: usize,
2120) -> *mut PioPackage {
2121 unsafe {
2122 finish_package(errbuf, errlen, "panic while lowering package", || {
2123 let pkg = pkg
2124 .as_ref()
2125 .ok_or_else(|| "package handle is NULL".to_string())?;
2126 pkg.package
2127 .lower_multiconductor_to_balanced(lowering_options(base_mva))
2128 .map_err(|e| e.to_string())
2129 })
2130 }
2131}
2132
2133#[unsafe(no_mangle)]
2147pub unsafe extern "C" fn pio_geo_parse(
2148 text: *const c_char,
2149 name_hint: *const c_char,
2150 errbuf: *mut c_char,
2151 errlen: usize,
2152) -> *mut c_char {
2153 unsafe {
2154 finish_string(errbuf, errlen, "panic while parsing geo layer", || {
2155 let text = required_cstr(text, "text")?;
2156 let name_hint = optional_cstr(name_hint, "name_hint")?;
2157 let parsed = powerio::GeoLayer::parse_bytes(text.as_bytes(), name_hint)
2158 .map_err(|e| e.to_string())?;
2159 Ok(parsed.layer.to_geojson())
2160 })
2161 }
2162}
2163
2164#[unsafe(no_mangle)]
2169pub unsafe extern "C" fn pio_geo_extract(
2170 net: *const PioNetwork,
2171 errbuf: *mut c_char,
2172 errlen: usize,
2173) -> *mut c_char {
2174 unsafe {
2175 finish_string(errbuf, errlen, "panic while extracting geo layer", || {
2176 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
2177 c.net
2178 .geo_layer()
2179 .extracted_geojson()
2180 .map_err(|e| e.to_string())
2181 })
2182 }
2183}
2184
2185#[unsafe(no_mangle)]
2195pub unsafe extern "C" fn pio_geo_apply(
2196 net: *const PioNetwork,
2197 layer: *const c_char,
2198 name_hint: *const c_char,
2199 errbuf: *mut c_char,
2200 errlen: usize,
2201) -> *mut PioNetwork {
2202 unsafe {
2203 finish_network(errbuf, errlen, "panic while applying geo layer", || {
2204 let c = network_ref(net).ok_or_else(|| "network handle is NULL".to_string())?;
2205 let layer = required_cstr(layer, "layer")?;
2206 let name_hint = optional_cstr(name_hint, "name_hint")?;
2207 let parsed = powerio::GeoLayer::parse_bytes(layer.as_bytes(), name_hint)
2208 .map_err(|e| e.to_string())?;
2209 let mut out = c.net.clone();
2210 let report = out.apply_geo_layer(&parsed.layer);
2211 out.source = None;
2212 let mut warnings = c.warnings.clone();
2213 warnings.extend(parsed.warnings);
2214 warnings.push(geo_apply_summary(&report));
2215 warnings.extend(report.notes);
2216 Ok((out, warnings))
2217 })
2218 }
2219}
2220
2221fn geo_apply_summary(report: &powerio::GeoApplyReport) -> String {
2223 format!(
2224 "geo apply: {} bus point(s), {} branch route(s), {} unmatched feature(s), \
2225 {} bus(es) with no location, {} branch(es) with no route",
2226 report.matched_buses,
2227 report.matched_branches,
2228 report.unmatched_features,
2229 report.unlocated_buses,
2230 report.unlocated_branches
2231 )
2232}
2233
2234#[cfg(feature = "prob")]
2240pub struct PioScopfInstance {
2241 instance: powerio_prob::ScopfInstance,
2242}
2243
2244#[cfg(feature = "prob")]
2245const _: fn() = || {
2246 fn assert_send_sync<T: Send + Sync>() {}
2247 assert_send_sync::<PioScopfInstance>();
2248};
2249
2250#[cfg(feature = "prob")]
2254#[unsafe(no_mangle)]
2255pub unsafe extern "C" fn pio_scopf_parse_str(
2256 text: *const c_char,
2257 from: *const c_char,
2258 errbuf: *mut c_char,
2259 errlen: usize,
2260) -> *mut PioScopfInstance {
2261 unsafe {
2262 finish_handle(errbuf, errlen, "panic while parsing SCOPF instance", || {
2263 let text = required_cstr(text, "text")?;
2264 let from = required_cstr(from, "from")?;
2265 let instance =
2266 powerio_prob::parse_scopf_str(text, from).map_err(|error| error.to_string())?;
2267 Ok(PioScopfInstance { instance })
2268 })
2269 }
2270}
2271
2272#[cfg(feature = "prob")]
2276#[unsafe(no_mangle)]
2277pub unsafe extern "C" fn pio_scopf_to_json(
2278 instance: *const PioScopfInstance,
2279 errbuf: *mut c_char,
2280 errlen: usize,
2281) -> *mut c_char {
2282 unsafe {
2283 finish_string(
2284 errbuf,
2285 errlen,
2286 "panic while serializing SCOPF instance",
2287 || {
2288 let instance = instance
2289 .as_ref()
2290 .ok_or_else(|| "SCOPF instance handle is NULL".to_string())?;
2291 powerio_prob::scopf::json::to_json(&instance.instance)
2292 .map_err(|error| error.to_string())
2293 },
2294 )
2295 }
2296}
2297
2298#[cfg(feature = "prob")]
2300#[unsafe(no_mangle)]
2301pub unsafe extern "C" fn pio_scopf_instance_free(instance: *mut PioScopfInstance) {
2302 unsafe {
2303 guard((), || {
2304 if !instance.is_null() {
2305 drop(Box::from_raw(instance));
2306 }
2307 });
2308 }
2309}
2310
2311#[cfg(any(feature = "dist", feature = "prob"))]
2326unsafe fn finish_handle<H>(
2327 errbuf: *mut c_char,
2328 errlen: usize,
2329 panic_msg: &str,
2330 f: impl FnOnce() -> Result<H, String>,
2331) -> *mut H {
2332 unsafe {
2333 match catch_unwind(AssertUnwindSafe(f)) {
2334 Ok(Ok(h)) => Box::into_raw(Box::new(h)),
2335 Ok(Err(msg)) => {
2336 copy_to_buf(errbuf, errlen, &msg);
2337 std::ptr::null_mut()
2338 }
2339 Err(_) => {
2340 copy_to_buf(errbuf, errlen, panic_msg);
2341 std::ptr::null_mut()
2342 }
2343 }
2344 }
2345}
2346
2347#[cfg(feature = "dist")]
2352pub struct PioDistNetwork {
2353 net: powerio_dist::MulticonductorNetwork,
2354}
2355
2356#[cfg(feature = "dist")]
2359const _: fn() = || {
2360 fn assert_send_sync<T: Send + Sync>() {}
2361 assert_send_sync::<PioDistNetwork>();
2362};
2363
2364#[cfg(feature = "dist")]
2370#[unsafe(no_mangle)]
2371pub unsafe extern "C" fn pio_dist_parse_file(
2372 path: *const c_char,
2373 from: *const c_char,
2374 errbuf: *mut c_char,
2375 errlen: usize,
2376) -> *mut PioDistNetwork {
2377 unsafe {
2378 finish_handle(errbuf, errlen, "panic while parsing", || {
2379 let path = required_cstr(path, "path")?;
2380 let from = optional_cstr(from, "from")?;
2381 powerio_dist::parse_file(std::path::Path::new(path), from)
2382 .map(|net| PioDistNetwork { net })
2383 .map_err(|e| e.to_string())
2384 })
2385 }
2386}
2387
2388#[cfg(feature = "dist")]
2394#[unsafe(no_mangle)]
2395pub unsafe extern "C" fn pio_dist_parse_str(
2396 text: *const c_char,
2397 format: *const c_char,
2398 errbuf: *mut c_char,
2399 errlen: usize,
2400) -> *mut PioDistNetwork {
2401 unsafe {
2402 finish_handle(errbuf, errlen, "panic while parsing", || {
2403 let text = required_cstr(text, "text")?;
2404 let format = required_cstr(format, "format")?;
2405 powerio_dist::parse_str(text, format)
2406 .map(|net| PioDistNetwork { net })
2407 .map_err(|e| e.to_string())
2408 })
2409 }
2410}
2411
2412#[cfg(feature = "dist")]
2415#[unsafe(no_mangle)]
2416pub unsafe extern "C" fn pio_dist_network_free(net: *mut PioDistNetwork) {
2417 unsafe {
2418 guard((), || {
2421 if !net.is_null() {
2422 drop(Box::from_raw(net));
2423 }
2424 });
2425 }
2426}
2427
2428#[cfg(feature = "dist")]
2434#[unsafe(no_mangle)]
2435pub unsafe extern "C" fn pio_dist_warnings(
2436 net: *const PioDistNetwork,
2437 warnbuf: *mut c_char,
2438 warnlen: usize,
2439) -> usize {
2440 unsafe {
2441 guard(0, || {
2442 let Some(c) = net.as_ref() else { return 0 };
2443 let msg = c.net.warnings.join("\n");
2444 copy_to_buf(warnbuf, warnlen, &msg);
2445 msg.len()
2446 })
2447 }
2448}
2449
2450#[cfg(feature = "dist")]
2454#[unsafe(no_mangle)]
2455pub unsafe extern "C" fn pio_dist_summary_json(
2456 net: *const PioDistNetwork,
2457 errbuf: *mut c_char,
2458 errlen: usize,
2459) -> *mut c_char {
2460 unsafe {
2461 finish_string(
2462 errbuf,
2463 errlen,
2464 "panic while serializing summary JSON",
2465 || {
2466 let net = net
2467 .as_ref()
2468 .ok_or_else(|| "distribution network handle is NULL".to_string())?;
2469 let summary = serde_json::json!({
2470 powerio::version::VERSION_KEY: powerio::VERSION,
2471 "name": net.net.name.as_deref(),
2472 "source_format": net.net.source_format.map(|f| f.name()),
2473 "base_frequency": net.net.base_frequency,
2474 "counts": {
2475 "buses": net.net.buses.len(),
2476 "linecodes": net.net.linecodes.len(),
2477 "lines": net.net.lines.len(),
2478 "switches": net.net.switches.len(),
2479 "transformers": net.net.transformers.len(),
2480 "loads": net.net.loads.len(),
2481 "generators": net.net.generators.len(),
2482 "ibrs": net.net.ibrs.len(),
2483 "control_profiles": net.net.control_profiles.len(),
2484 "shunts": net.net.shunts.len(),
2485 "capacitors": net.net.capacitors.len(),
2486 "sources": net.net.sources.len(),
2487 "untyped": net.net.untyped.len(),
2488 "warnings": net.net.warnings.len(),
2489 },
2490 });
2491 serde_json::to_string(&summary).map_err(|e| e.to_string())
2492 },
2493 )
2494 }
2495}
2496
2497#[cfg(feature = "dist")]
2504#[unsafe(no_mangle)]
2505pub unsafe extern "C" fn pio_dist_to_json(
2506 net: *const PioDistNetwork,
2507 errbuf: *mut c_char,
2508 errlen: usize,
2509) -> *mut c_char {
2510 unsafe {
2511 finish_string(errbuf, errlen, "panic while serializing model JSON", || {
2512 let net = net
2513 .as_ref()
2514 .ok_or_else(|| "distribution network handle is NULL".to_string())?;
2515 serde_json::to_string(&net.net).map_err(|e| e.to_string())
2516 })
2517 }
2518}
2519
2520#[cfg(feature = "dist")]
2524#[unsafe(no_mangle)]
2525pub unsafe extern "C" fn pio_dist_graph_json(
2526 net: *const PioDistNetwork,
2527 errbuf: *mut c_char,
2528 errlen: usize,
2529) -> *mut c_char {
2530 unsafe {
2531 finish_string(errbuf, errlen, "panic while serializing graph JSON", || {
2532 let net = net
2533 .as_ref()
2534 .ok_or_else(|| "distribution network handle is NULL".to_string())?;
2535 serde_json::to_string(&net.net.graph()).map_err(|e| e.to_string())
2536 })
2537 }
2538}
2539
2540#[cfg(feature = "dist")]
2547#[unsafe(no_mangle)]
2548pub unsafe extern "C" fn pio_dist_from_json(
2549 text: *const c_char,
2550 errbuf: *mut c_char,
2551 errlen: usize,
2552) -> *mut PioDistNetwork {
2553 unsafe {
2554 finish_handle(errbuf, errlen, "panic while parsing model JSON", || {
2555 let text = required_cstr(text, "text")?;
2556 serde_json::from_str::<powerio_dist::MulticonductorNetwork>(text)
2557 .map(|net| PioDistNetwork { net })
2558 .map_err(|e| format!("model JSON: {e}"))
2559 })
2560 }
2561}
2562
2563#[cfg(feature = "dist")]
2567fn warn_dropped_sidecars(
2568 mut warnings: Vec<String>,
2569 sidecars: &[powerio_dist::ConversionSidecar],
2570) -> Vec<String> {
2571 for sidecar in sidecars {
2572 warnings.push(sidecar.dropped_warning(
2573 "this entry point returns the case text only, and that text refers to the file; \
2574 write it beside the case before loading",
2575 ));
2576 }
2577 warnings
2578}
2579
2580#[cfg(feature = "dist")]
2586#[unsafe(no_mangle)]
2587pub unsafe extern "C" fn pio_dist_to_format(
2588 net: *const PioDistNetwork,
2589 to: *const c_char,
2590 out_warnings: *mut *mut c_char,
2591 errbuf: *mut c_char,
2592 errlen: usize,
2593) -> *mut c_char {
2594 unsafe {
2595 finish_conversion(out_warnings, errbuf, errlen, || {
2596 let c = net
2597 .as_ref()
2598 .ok_or_else(|| "network handle is NULL".to_string())?;
2599 let target = dist_target_from_c(to)?;
2600 let conv = c.net.to_format(target);
2601 Ok((
2602 conv.text,
2603 warn_dropped_sidecars(conv.warnings, &conv.sidecars),
2604 ))
2605 })
2606 }
2607}
2608
2609#[cfg(feature = "dist")]
2615#[unsafe(no_mangle)]
2616pub unsafe extern "C" fn pio_dist_convert_file(
2617 path: *const c_char,
2618 from: *const c_char,
2619 to: *const c_char,
2620 out_warnings: *mut *mut c_char,
2621 errbuf: *mut c_char,
2622 errlen: usize,
2623) -> *mut c_char {
2624 unsafe {
2625 finish_conversion(out_warnings, errbuf, errlen, || {
2626 let path = required_cstr(path, "path")?;
2627 let from = optional_cstr(from, "from")?;
2628 let to = dist_target_from_c(to)?;
2629 let conv = powerio_dist::convert_file(std::path::Path::new(path), to, from)
2630 .map_err(|e| e.to_string())?;
2631 Ok((
2632 conv.text,
2633 warn_dropped_sidecars(conv.warnings, &conv.sidecars),
2634 ))
2635 })
2636 }
2637}
2638
2639#[cfg(feature = "dist")]
2646#[unsafe(no_mangle)]
2647pub unsafe extern "C" fn pio_dist_convert_str(
2648 text: *const c_char,
2649 from: *const c_char,
2650 to: *const c_char,
2651 out_warnings: *mut *mut c_char,
2652 errbuf: *mut c_char,
2653 errlen: usize,
2654) -> *mut c_char {
2655 unsafe {
2656 finish_conversion(out_warnings, errbuf, errlen, || {
2657 let text = required_cstr(text, "text")?;
2658 let to = dist_target_from_c(to)?;
2659 let from = required_cstr(from, "from")?;
2660 let conv = powerio_dist::convert_str(text, to, from).map_err(|e| e.to_string())?;
2661 Ok((
2662 conv.text,
2663 warn_dropped_sidecars(conv.warnings, &conv.sidecars),
2664 ))
2665 })
2666 }
2667}
2668
2669#[cfg(feature = "dist")]
2670fn dist_target_from_c(to: *const c_char) -> Result<powerio_dist::DistTargetFormat, String> {
2671 let to = required_cstr(to, "to")?;
2672 to.parse::<powerio_dist::DistTargetFormat>()
2675 .map_err(|e| e.to_string())
2676}
2677
2678#[cfg(all(feature = "dist", feature = "pkg"))]
2683#[unsafe(no_mangle)]
2684pub unsafe extern "C" fn pio_dist_geo_extract(
2685 net: *const PioDistNetwork,
2686 errbuf: *mut c_char,
2687 errlen: usize,
2688) -> *mut c_char {
2689 unsafe {
2690 finish_string(errbuf, errlen, "panic while extracting geo layer", || {
2691 let c = net
2692 .as_ref()
2693 .ok_or_else(|| "distribution network handle is NULL".to_string())?;
2694 powerio_pkg::dist_geo_layer(&c.net)
2695 .extracted_geojson()
2696 .map_err(|e| e.to_string())
2697 })
2698 }
2699}
2700
2701#[cfg(all(feature = "dist", feature = "pkg"))]
2709#[unsafe(no_mangle)]
2710pub unsafe extern "C" fn pio_dist_geo_apply(
2711 net: *const PioDistNetwork,
2712 layer: *const c_char,
2713 name_hint: *const c_char,
2714 errbuf: *mut c_char,
2715 errlen: usize,
2716) -> *mut PioDistNetwork {
2717 unsafe {
2718 finish_handle(errbuf, errlen, "panic while applying geo layer", || {
2719 let c = net
2720 .as_ref()
2721 .ok_or_else(|| "distribution network handle is NULL".to_string())?;
2722 let layer = required_cstr(layer, "layer")?;
2723 let name_hint = optional_cstr(name_hint, "name_hint")?;
2724 let parsed = powerio::GeoLayer::parse_bytes(layer.as_bytes(), name_hint)
2725 .map_err(|e| e.to_string())?;
2726 let mut out = c.net.clone();
2727 let report = powerio_pkg::apply_dist_geo_layer(&mut out, &parsed.layer);
2728 out.source = None;
2729 out.source_format = None;
2730 out.warnings.extend(parsed.warnings);
2731 out.warnings.push(geo_apply_summary(&report));
2732 out.warnings.extend(report.notes);
2733 Ok(PioDistNetwork { net: out })
2734 })
2735 }
2736}
2737
2738#[cfg(test)]
2739mod tests {
2740 use super::*;
2741 use powerio::POWER_MODELS_ANGLE_BOUND_PAD;
2742 use std::ffi::CString;
2743
2744 fn data_path(name: &str) -> CString {
2745 CString::new(
2746 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
2747 .join("../tests/data")
2748 .join(name)
2749 .to_str()
2750 .unwrap(),
2751 )
2752 .unwrap()
2753 }
2754
2755 fn close(actual: f64, expected: f64) {
2756 assert!((actual - expected).abs() < 1e-12, "{actual} != {expected}");
2757 }
2758
2759 #[test]
2760 fn parse_bytes_reads_binary_and_text_without_a_file() {
2761 let read = |name: &str| {
2762 std::fs::read(
2763 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
2764 .join("../tests/data")
2765 .join(name),
2766 )
2767 .unwrap()
2768 };
2769 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
2770 unsafe {
2771 let pwb = read("powerworld/ACTIVSg200.pwb");
2774 let fmt = CString::new("pwb").unwrap();
2775 let net = pio_parse_bytes(
2776 pwb.as_ptr(),
2777 pwb.len(),
2778 fmt.as_ptr(),
2779 err.as_mut_ptr(),
2780 err.len(),
2781 );
2782 assert!(
2783 !net.is_null(),
2784 "pwb bytes: {}",
2785 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2786 );
2787 assert_eq!(pio_n_buses(net), 200);
2788 pio_network_free(net);
2789
2790 let m = read("case9.m");
2792 let fmt = CString::new("matpower").unwrap();
2793 let from_bytes = pio_parse_bytes(
2794 m.as_ptr(),
2795 m.len(),
2796 fmt.as_ptr(),
2797 err.as_mut_ptr(),
2798 err.len(),
2799 );
2800 assert!(!from_bytes.is_null());
2801 let from_path = case9();
2802 assert_eq!(pio_n_buses(from_bytes), pio_n_buses(from_path));
2803 assert_eq!(pio_n_branches(from_bytes), pio_n_branches(from_path));
2804 pio_network_free(from_bytes);
2805 pio_network_free(from_path);
2806
2807 let junk = [0xffu8, 0xfe, 0x00, 0x01];
2809 let bad = pio_parse_bytes(
2810 junk.as_ptr(),
2811 junk.len(),
2812 fmt.as_ptr(),
2813 err.as_mut_ptr(),
2814 err.len(),
2815 );
2816 assert!(bad.is_null());
2817 assert!(
2818 CStr::from_ptr(err.as_ptr())
2819 .to_str()
2820 .unwrap()
2821 .contains("UTF-8"),
2822 "expected a UTF-8 message, got {}",
2823 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2824 );
2825
2826 let null_bytes = pio_parse_bytes(
2829 std::ptr::null(),
2830 8,
2831 fmt.as_ptr(),
2832 err.as_mut_ptr(),
2833 err.len(),
2834 );
2835 assert!(null_bytes.is_null());
2836 }
2837 }
2838
2839 fn case9() -> *mut PioNetwork {
2840 let path = data_path("case9.m");
2841 let mut err = [0 as c_char; 256];
2842 let c =
2843 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
2844 assert!(!c.is_null(), "parse returned null");
2845 c
2846 }
2847
2848 fn angle_bounds_case() -> *mut PioNetwork {
2849 let path = data_path("angle_bounds_clamp.m");
2850 let mut err = [0 as c_char; 256];
2851 let c =
2852 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
2853 assert!(!c.is_null(), "parse returned null");
2854 c
2855 }
2856
2857 fn terminal_projection_case() -> *mut PioNetwork {
2858 use powerio::{Branch, BranchCharging, Bus, BusId, BusType};
2859
2860 let mut branch = Branch::new(BusId(1), BusId(2), 0.01, 0.1);
2861 branch.charging = Some(BranchCharging::new(0.01, 0.02, 0.03, 0.05));
2862 branch.rate_a = 100.0;
2863 let net = BalancedNetwork::in_memory(
2864 "terminal-projection",
2865 100.0,
2866 vec![
2867 Bus::new(BusId(1), BusType::Ref, 230.0),
2868 Bus::new(BusId(2), BusType::Pq, 230.0),
2869 ],
2870 vec![branch],
2871 );
2872 let text = CString::new(net.to_json().unwrap()).unwrap();
2873 let format = CString::new("powerio-json").unwrap();
2874 let mut err = [0 as c_char; 256];
2875 let c =
2876 unsafe { pio_parse_str(text.as_ptr(), format.as_ptr(), err.as_mut_ptr(), err.len()) };
2877 assert!(
2878 !c.is_null(),
2879 "parse returned null: {}",
2880 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
2881 );
2882 c
2883 }
2884
2885 unsafe fn to_format(net: *const PioNetwork, to: &str) -> String {
2887 let to = CString::new(to).unwrap();
2888 let mut warn_out: *mut c_char = std::ptr::null_mut();
2889 let mut err = [0 as c_char; 256];
2890 unsafe {
2891 let s = pio_to_format(net, to.as_ptr(), &mut warn_out, err.as_mut_ptr(), err.len());
2892 assert!(
2893 !s.is_null(),
2894 "to_format failed: {}",
2895 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2896 );
2897 let text = CStr::from_ptr(s).to_str().unwrap().to_owned();
2898 pio_string_free(s);
2899 text
2900 }
2901 }
2902
2903 unsafe fn network_json(net: *const PioNetwork) -> serde_json::Value {
2904 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
2905 unsafe {
2906 let s = pio_to_json(net, err.as_mut_ptr(), err.len());
2907 assert!(
2908 !s.is_null(),
2909 "to_json failed: {}",
2910 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2911 );
2912 let text = CStr::from_ptr(s).to_str().unwrap().to_owned();
2913 pio_string_free(s);
2914 serde_json::from_str(&text).unwrap()
2915 }
2916 }
2917
2918 unsafe fn warning_text(net: *const PioNetwork) -> String {
2919 let n = unsafe { pio_warnings(net, std::ptr::null_mut(), 0) };
2920 let mut buf = vec![0 as c_char; n + 1];
2921 unsafe {
2922 pio_warnings(net, buf.as_mut_ptr(), buf.len());
2923 CStr::from_ptr(buf.as_ptr()).to_str().unwrap().to_owned()
2924 }
2925 }
2926
2927 #[test]
2928 fn deepmind_opfdata_uses_shared_c_api() {
2931 let path = data_path("opfdataset/example_0.json");
2932 let mut err = [0 as c_char; 512];
2933 let net =
2934 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
2935 assert!(
2936 !net.is_null(),
2937 "parse returned null: {}",
2938 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
2939 );
2940
2941 unsafe {
2942 assert_eq!(pio_n_buses(net), 14);
2943 assert_eq!(pio_n_branches(net), 20);
2944 assert_eq!(pio_n_gens(net), 5);
2945 close(pio_base_mva(net), 100.0);
2946
2947 let mut source_format = [0 as c_char; 64];
2948 let len = pio_source_format(net, source_format.as_mut_ptr(), source_format.len());
2949 assert_eq!(
2950 CStr::from_ptr(source_format.as_ptr()).to_str().unwrap(),
2951 "DeepMindOpfDataJson"
2952 );
2953 assert_eq!(len, "DeepMindOpfDataJson".len());
2954 assert!(warning_text(net).contains("solver initial values"));
2955 assert!(to_format(net, "matpower").contains("mpc.bus"));
2956 pio_network_free(net);
2957 }
2958 }
2959
2960 #[cfg(feature = "pkg")]
2961 unsafe fn package_json_text(pkg: *const PioPackage) -> String {
2962 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
2963 unsafe {
2964 let s = pio_package_to_json(pkg, err.as_mut_ptr(), err.len());
2965 assert!(
2966 !s.is_null(),
2967 "package to json failed: {}",
2968 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2969 );
2970 let text = CStr::from_ptr(s).to_str().unwrap().to_owned();
2971 pio_string_free(s);
2972 text
2973 }
2974 }
2975
2976 #[cfg(feature = "pkg")]
2977 unsafe fn package_json(pkg: *const PioPackage) -> serde_json::Value {
2978 unsafe { serde_json::from_str(&package_json_text(pkg)).unwrap() }
2979 }
2980
2981 #[cfg(feature = "pkg")]
2982 unsafe fn package_report_json(
2983 f: unsafe extern "C" fn(*const PioPackage, *mut c_char, usize) -> *mut c_char,
2984 pkg: *const PioPackage,
2985 ) -> serde_json::Value {
2986 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
2987 unsafe {
2988 let s = f(pkg, err.as_mut_ptr(), err.len());
2989 assert!(
2990 !s.is_null(),
2991 "package report failed: {}",
2992 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
2993 );
2994 let text = CStr::from_ptr(s).to_str().unwrap().to_owned();
2995 pio_string_free(s);
2996 serde_json::from_str(&text).unwrap()
2997 }
2998 }
2999
3000 #[test]
3001 fn every_extractor_reports_the_star_lowered_space() {
3002 let case = case9_json_with_a_3w_transformer();
3010 let text = CString::new(case).unwrap();
3011 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
3012 unsafe {
3013 let net = pio_from_json(text.as_ptr(), err.as_mut_ptr(), err.len());
3014 assert!(
3015 !net.is_null(),
3016 "from_json failed: {}",
3017 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
3018 );
3019
3020 let n = pio_n_buses(net);
3021 let demand = pio_bus_demand(net, std::ptr::null_mut(), std::ptr::null_mut(), 0);
3022 let shunt = pio_bus_shunt(net, std::ptr::null_mut(), std::ptr::null_mut(), 0);
3023 let ids = pio_bus_ids(net, std::ptr::null_mut(), 0);
3024
3025 assert_eq!(n, 10, "9 buses plus one star point");
3026 assert_eq!(demand, n, "pio_bus_demand must agree with pio_n_buses");
3027 assert_eq!(shunt, n, "pio_bus_shunt must agree with pio_n_buses");
3028 assert_eq!(ids, n, "pio_bus_ids must agree with pio_n_buses");
3029
3030 let m = pio_n_branches(net);
3031 let (ni, nf, nb) = (
3032 std::ptr::null_mut::<i64>(),
3033 std::ptr::null_mut::<f64>(),
3034 std::ptr::null_mut::<u8>(),
3035 );
3036 let rows = pio_branches(net, ni, ni, nf, nf, nf, nf, nf, nb, 0);
3037 let charging = pio_branch_charging(net, nf, nf, nf, nf, 0);
3038 assert_eq!(m, 12, "9 branches plus three star legs");
3039 assert_eq!(rows, m, "pio_branches must agree with pio_n_branches");
3040 assert_eq!(charging, m, "pio_branch_charging must agree too");
3041
3042 let mut buf = vec![-1i64; n];
3045 assert_eq!(pio_bus_ids(net, buf.as_mut_ptr(), buf.len()), n);
3046 assert!(
3047 buf.iter().all(|&id| id > 0),
3048 "every dense index carries an id, got {buf:?}"
3049 );
3050
3051 let mut from = vec![-1i64; m];
3055 let mut to = vec![-1i64; m];
3056 assert_eq!(
3057 pio_branches(
3058 net,
3059 from.as_mut_ptr(),
3060 to.as_mut_ptr(),
3061 nf,
3062 nf,
3063 nf,
3064 nf,
3065 nf,
3066 nb,
3067 m
3068 ),
3069 m
3070 );
3071 for id in from.iter().chain(&to) {
3072 assert!(
3073 buf.contains(id),
3074 "branch endpoint {id} is not in pio_bus_ids"
3075 );
3076 }
3077 for id in &buf {
3078 assert!(
3079 from.contains(id) || to.contains(id),
3080 "bus {id} has no incident branch"
3081 );
3082 }
3083
3084 let raw = pio_summary_json(net, err.as_mut_ptr(), err.len());
3088 assert!(!raw.is_null());
3089 let summary: serde_json::Value =
3090 serde_json::from_str(CStr::from_ptr(raw).to_str().unwrap()).unwrap();
3091 pio_string_free(raw);
3092 assert_eq!(summary["counts"]["buses"], 9);
3093 assert_eq!(summary["counts"]["branches"], 9);
3094 assert_eq!(summary["counts"]["transformers_3w"], 1);
3095 assert_eq!(summary["topology"]["n_buses"], n);
3096 assert_eq!(summary["topology"]["n_branches"], m);
3097
3098 pio_network_free(net);
3099 }
3100 }
3101
3102 fn case9_json_with_a_3w_transformer() -> String {
3106 let net = case9();
3107 let mut doc: serde_json::Value = unsafe {
3108 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
3109 let raw = pio_to_json(net, err.as_mut_ptr(), err.len());
3110 assert!(!raw.is_null());
3111 let text = CStr::from_ptr(raw).to_str().unwrap().to_owned();
3112 pio_string_free(raw);
3113 pio_network_free(net);
3114 serde_json::from_str(&text).unwrap()
3115 };
3116 let winding = |bus: i64| {
3117 serde_json::json!({
3118 "bus": bus, "tap": 1.0, "shift": 0.0, "nominal_kv": 0.0,
3119 "rate_a": 0.0, "rate_b": 0.0, "rate_c": 0.0
3120 })
3121 };
3122 let z = serde_json::json!({ "r": 0.0, "x": 0.05, "base_mva": 100.0 });
3123 doc["transformers_3w"] = serde_json::json!([{
3124 "windings": [winding(4), winding(5), winding(6)],
3125 "z": [z, z, z],
3126 "star_vm": 1.0, "star_va": 0.0, "mag_g": 0.0, "mag_b": 0.0,
3127 "in_service": true, "name": "t3", "extras": {}
3128 }]);
3129 doc.to_string()
3130 }
3131
3132 #[test]
3133 fn build_info_reports_the_build_in_one_document() {
3134 let raw = pio_build_info();
3135 assert!(!raw.is_null());
3136 let doc: serde_json::Value =
3137 serde_json::from_str(unsafe { CStr::from_ptr(raw) }.to_str().unwrap()).unwrap();
3138 unsafe { pio_string_free(raw) };
3139
3140 assert_eq!(doc["abi"], serde_json::json!(PIO_ABI_VERSION));
3141 assert_eq!(
3142 doc[powerio::version::VERSION_KEY],
3143 serde_json::json!(powerio::VERSION)
3144 );
3145 for name in ["arrow", "matrix", "gridfm", "dist", "pkg", "prob"] {
3148 let c = CString::new(name).unwrap();
3149 let probed = unsafe { pio_has_feature(c.as_ptr()) } == 1;
3150 assert_eq!(
3151 doc["features"][name],
3152 serde_json::json!(probed),
3153 "{name} disagrees between pio_build_info and pio_has_feature"
3154 );
3155 }
3156 assert_eq!(
3157 doc["error_categories"],
3158 serde_json::json!(powerio::ErrorCategory::TOKENS)
3159 );
3160 }
3161
3162 #[test]
3163 fn version_api() {
3164 assert_eq!(pio_abi_version(), PIO_ABI_VERSION);
3167 assert_eq!(PIO_ABI_VERSION, 5);
3168 let v = unsafe { CStr::from_ptr(pio_version()) }.to_str().unwrap();
3169 assert_eq!(v, env!("CARGO_PKG_VERSION"));
3170 assert!(!v.is_empty());
3171 }
3172
3173 #[test]
3174 fn matrix_probe_matches_build_features() {
3175 let matrix = CString::new("matrix").unwrap();
3176 unsafe {
3177 assert_eq!(
3178 pio_has_feature(matrix.as_ptr()),
3179 i32::from(cfg!(feature = "matrix"))
3180 );
3181 }
3182 assert_eq!(
3183 pio_matrix_available(),
3184 i32::from(cfg!(all(feature = "arrow", feature = "matrix")))
3185 );
3186 }
3187
3188 #[test]
3189 fn prob_probe_matches_build_features() {
3190 let prob = CString::new("prob").unwrap();
3191 unsafe {
3192 assert_eq!(
3193 pio_has_feature(prob.as_ptr()),
3194 i32::from(cfg!(feature = "prob"))
3195 );
3196 }
3197 }
3198
3199 fn strip_c_comments(input: &str) -> String {
3200 let mut out = String::with_capacity(input.len());
3201 let mut chars = input.chars().peekable();
3202 let mut in_block = false;
3203 while let Some(ch) = chars.next() {
3204 if in_block {
3205 if ch == '*' && chars.peek() == Some(&'/') {
3206 chars.next();
3207 in_block = false;
3208 } else if ch == '\n' {
3209 out.push('\n');
3210 }
3211 } else if ch == '/' && chars.peek() == Some(&'*') {
3212 chars.next();
3213 in_block = true;
3214 } else if ch == '/' && chars.peek() == Some(&'/') {
3215 chars.next();
3216 for tail in chars.by_ref() {
3217 if tail == '\n' {
3218 out.push('\n');
3219 break;
3220 }
3221 }
3222 } else {
3223 out.push(ch);
3224 }
3225 }
3226 out
3227 }
3228
3229 fn collapse_ws(s: &str) -> String {
3230 s.split_whitespace().collect::<Vec<_>>().join(" ")
3231 }
3232
3233 fn c_header_abi_manifest(header: &str) -> Vec<String> {
3234 let clean = strip_c_comments(header);
3235 let mut entries = Vec::new();
3236 let mut prototype = String::new();
3237 for line in clean.lines().map(str::trim).filter(|line| !line.is_empty()) {
3238 if !prototype.is_empty() {
3239 prototype.push(' ');
3240 prototype.push_str(line);
3241 if line.ends_with(';') {
3242 entries.push(collapse_ws(&prototype));
3243 prototype.clear();
3244 }
3245 continue;
3246 }
3247
3248 if line.starts_with("#define PIO_") || line.starts_with("typedef struct Pio") {
3249 entries.push(collapse_ws(line));
3250 } else if line.contains("pio_") {
3251 if line.ends_with(';') {
3252 entries.push(collapse_ws(line));
3253 } else {
3254 prototype.push_str(line);
3255 }
3256 }
3257 }
3258 assert!(prototype.is_empty(), "unterminated prototype: {prototype}");
3259 entries
3260 }
3261
3262 fn pio_symbol_names_from_manifest(manifest: &[String]) -> Vec<String> {
3263 let mut names = std::collections::BTreeSet::new();
3264 for entry in manifest {
3265 if let Some(start) = entry.find("pio_") {
3266 let tail = &entry[start..];
3267 if let Some(end) = tail.find('(') {
3268 names.insert(tail[..end].to_string());
3269 }
3270 }
3271 }
3272 names.into_iter().collect()
3273 }
3274
3275 fn source_exported_pio_symbols(source: &str) -> Vec<String> {
3276 let mut names = std::collections::BTreeSet::new();
3277 let mut saw_no_mangle = false;
3278 for line in source.lines() {
3279 let trimmed = line.trim();
3280 if trimmed == "#[unsafe(no_mangle)]" {
3281 saw_no_mangle = true;
3282 continue;
3283 }
3284 if !trimmed.contains("extern \"C\"") {
3285 if !trimmed.is_empty()
3286 && !trimmed.starts_with("#[")
3287 && !trimmed.starts_with("//")
3288 && !trimmed.starts_with("///")
3289 {
3290 saw_no_mangle = false;
3291 }
3292 continue;
3293 }
3294 if let Some(start) = trimmed.find("fn pio_") {
3295 let tail = &trimmed[start + "fn ".len()..];
3296 let end = tail
3297 .find('(')
3298 .unwrap_or_else(|| panic!("unterminated extern fn line: {trimmed}"));
3299 let name = &tail[..end];
3300 assert!(
3301 saw_no_mangle,
3302 "{name} is exported in Rust source without #[unsafe(no_mangle)]"
3303 );
3304 names.insert(name.to_string());
3305 saw_no_mangle = false;
3306 continue;
3307 }
3308 if !trimmed.is_empty() && !trimmed.starts_with("#[") && !trimmed.starts_with("//") {
3309 saw_no_mangle = false;
3310 }
3311 }
3312 names.into_iter().collect()
3313 }
3314
3315 #[test]
3316 fn c_header_abi_manifest_is_pinned() {
3317 let actual = c_header_abi_manifest(include_str!("../include/powerio.h"));
3318 let expected = [
3319 "#define PIO_ABI_VERSION 5",
3320 "#define PIO_DIST_ABI_VERSION 1",
3321 "#define PIO_ERRBUF_MIN 256",
3322 "#define PIO_ARROW_TABLE_BUS 0",
3323 "#define PIO_ARROW_TABLE_BRANCH 1",
3324 "#define PIO_ARROW_TABLE_GEN 2",
3325 "#define PIO_ARROW_TABLE_LOAD 3",
3326 "#define PIO_ARROW_TABLE_SHUNT 4",
3327 "#define PIO_ARROW_TABLE_SWITCH 5",
3328 "#define PIO_ARROW_TABLE_SOLVER_BUS 6",
3329 "#define PIO_ARROW_TABLE_SOLVER_LOAD 7",
3330 "#define PIO_ARROW_TABLE_SOLVER_SHUNT 8",
3331 "#define PIO_ARROW_TABLE_SOLVER_BRANCH 9",
3332 "#define PIO_ARROW_TABLE_SOLVER_SWITCH 10",
3333 "#define PIO_ARROW_TABLE_SOLVER_ARC 11",
3334 "#define PIO_ARROW_TABLE_SOLVER_GEN 12",
3335 "#define PIO_ARROW_TABLE_SOLVER_STORAGE 13",
3336 "#define PIO_ARROW_TABLE_SOLVER_HVDC 14",
3337 "#define PIO_ARROW_TABLE_YBUS 15",
3338 "#define PIO_ARROW_TABLE_INCIDENCE 16",
3339 "#define PIO_ARROW_TABLE_BPRIME 17",
3340 "#define PIO_ARROW_TABLE_BDOUBLEPRIME 18",
3341 "#define PIO_ARROW_TABLE_MATRIX_BUS 19",
3342 "#define PIO_ARROW_TABLE_MATRIX_BRANCH 20",
3343 "typedef struct PioDistNetwork PioDistNetwork;",
3344 "typedef struct PioNetwork PioNetwork;",
3345 "typedef struct PioPackage PioPackage;",
3346 "typedef struct PioScopfInstance PioScopfInstance;",
3347 "uint32_t pio_abi_version(void);",
3348 "uint32_t pio_dist_abi_version(void);",
3349 "char *pio_dist_capabilities_json(void);",
3350 "char *pio_schema_versions_json(void);",
3351 "char *pio_build_info(void);",
3352 "int32_t pio_matrix_available(void);",
3353 "int32_t pio_has_feature(const char *feature);",
3354 "const char *pio_version(void);",
3355 "PioNetwork *pio_parse_file(const char *path, const char *from, char *errbuf, size_t errlen);",
3356 "PioNetwork *pio_parse_str(const char *text, const char *format, char *errbuf, size_t errlen);",
3357 "PioNetwork *pio_parse_bytes(const uint8_t *bytes, size_t len, const char *format, char *errbuf, size_t errlen);",
3358 "size_t pio_classify_str(const char *text, char *outbuf, size_t outlen);",
3359 "char *pio_to_json(const PioNetwork *net, char *errbuf, size_t errlen);",
3360 "PioNetwork *pio_from_json(const char *text, char *errbuf, size_t errlen);",
3361 "PioNetwork *pio_read_dir(const char *dir, const char *from, int64_t scenario, char *errbuf, size_t errlen);",
3362 "ptrdiff_t pio_scenario_ids(const char *dir, const char *from, int64_t *out, size_t cap, char *errbuf, size_t errlen);",
3363 "size_t pio_warnings(const PioNetwork *net, char *warnbuf, size_t warnlen);",
3364 "void pio_network_free(PioNetwork *net);",
3365 "PioNetwork *pio_normalize(const PioNetwork *net, char *errbuf, size_t errlen);",
3366 "PioNetwork *pio_normalize_with_options(const PioNetwork *net, int32_t clamp_angle_bounds, double angle_bound_pad, char *errbuf, size_t errlen);",
3367 "size_t pio_n_buses(const PioNetwork *net);",
3368 "size_t pio_n_branches(const PioNetwork *net);",
3369 "size_t pio_n_switches(const PioNetwork *net);",
3370 "size_t pio_n_gens(const PioNetwork *net);",
3371 "double pio_base_mva(const PioNetwork *net);",
3372 "size_t pio_network_name(const PioNetwork *net, char *out, size_t cap);",
3373 "size_t pio_source_format(const PioNetwork *net, char *out, size_t cap);",
3374 "char *pio_summary_json(const PioNetwork *net, char *errbuf, size_t errlen);",
3375 "int64_t pio_ref_bus_index(const PioNetwork *net);",
3376 "size_t pio_ref_bus_indices(const PioNetwork *net, int64_t *out, size_t cap);",
3377 "size_t pio_n_islands(const PioNetwork *net);",
3378 "int32_t pio_is_radial(const PioNetwork *net);",
3379 "char *pio_to_format(const PioNetwork *net, const char *to, char **out_warnings, char *errbuf, size_t errlen);",
3380 "char *pio_convert_file(const char *path, const char *from, const char *to, char **out_warnings, char *errbuf, size_t errlen);",
3381 "char *pio_convert_str(const char *text, const char *from, const char *to, char **out_warnings, char *errbuf, size_t errlen);",
3382 "int32_t pio_write_dir(const PioNetwork *net, const char *to, const char *out_dir, char **out_warnings, char *errbuf, size_t errlen);",
3383 "void pio_string_free(char *s);",
3384 "size_t pio_bus_ids(const PioNetwork *net, int64_t *out, size_t cap);",
3385 "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);",
3386 "size_t pio_branch_charging(const PioNetwork *net, double *g_fr, double *b_fr, double *g_to, double *b_to, size_t cap);",
3387 "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);",
3388 "size_t pio_gens(const PioNetwork *net, int64_t *bus, double *pg, double *pmax, double *pmin, uint8_t *in_service, size_t cap);",
3389 "size_t pio_bus_demand(const PioNetwork *net, double *pd, double *qd, size_t cap);",
3390 "size_t pio_bus_shunt(const PioNetwork *net, double *gs, double *bs, size_t cap);",
3391 "int32_t pio_to_arrow(const PioNetwork *net, int32_t table, struct ArrowArray *out_array, struct ArrowSchema *out_schema, char *errbuf, size_t errlen);",
3392 "char *pio_arrow_catalog_json(char *errbuf, size_t errlen);",
3393 "PioPackage *pio_package_parse_file(const char *path, char *errbuf, size_t errlen);",
3394 "PioPackage *pio_package_parse_str(const char *text, char *errbuf, size_t errlen);",
3395 "void pio_package_free(PioPackage *pkg);",
3396 "char *pio_package_to_json(const PioPackage *pkg, char *errbuf, size_t errlen);",
3397 "PioPackage *pio_package_from_balanced_network(const PioNetwork *net, int32_t include_solver_metadata, char *errbuf, size_t errlen);",
3398 "PioPackage *pio_package_from_multiconductor_network(const PioDistNetwork *net, char *errbuf, size_t errlen);",
3399 "PioNetwork *pio_package_to_balanced_network(const PioPackage *pkg, char *errbuf, size_t errlen);",
3400 "PioDistNetwork *pio_package_to_multiconductor_network(const PioPackage *pkg, char *errbuf, size_t errlen);",
3401 "int32_t pio_package_validate(PioPackage *pkg, char *errbuf, size_t errlen);",
3402 "char *pio_package_validation_json(const PioPackage *pkg, char *errbuf, size_t errlen);",
3403 "char *pio_package_diagnostics_json(const PioPackage *pkg, char *errbuf, size_t errlen);",
3404 "char *pio_package_operating_points_json(const PioPackage *pkg, char *errbuf, size_t errlen);",
3405 "int32_t pio_package_set_operating_points(PioPackage *pkg, const char *json, char *errbuf, size_t errlen);",
3406 "char *pio_package_study_json(const PioPackage *pkg, char *errbuf, size_t errlen);",
3407 "PioPackage *pio_package_materialize_operating_point(const PioPackage *pkg, int64_t index, char *errbuf, size_t errlen);",
3408 "PioPackage *pio_package_materialize_study_commit(const PioPackage *pkg, int64_t index, char *errbuf, size_t errlen);",
3409 "char *pio_package_multiconductor_to_balanced_preflight_json(const PioPackage *pkg, double base_mva, char *errbuf, size_t errlen);",
3410 "PioPackage *pio_package_lower_multiconductor_to_balanced(const PioPackage *pkg, double base_mva, char *errbuf, size_t errlen);",
3411 "char *pio_geo_parse(const char *text, const char *name_hint, char *errbuf, size_t errlen);",
3412 "char *pio_geo_extract(const PioNetwork *net, char *errbuf, size_t errlen);",
3413 "PioNetwork *pio_geo_apply(const PioNetwork *net, const char *layer, const char *name_hint, char *errbuf, size_t errlen);",
3414 "PioScopfInstance *pio_scopf_parse_str(const char *text, const char *from, char *errbuf, size_t errlen);",
3415 "char *pio_scopf_to_json(const PioScopfInstance *instance, char *errbuf, size_t errlen);",
3416 "void pio_scopf_instance_free(PioScopfInstance *instance);",
3417 "PioDistNetwork *pio_dist_parse_file(const char *path, const char *from, char *errbuf, size_t errlen);",
3418 "PioDistNetwork *pio_dist_parse_str(const char *text, const char *format, char *errbuf, size_t errlen);",
3419 "void pio_dist_network_free(PioDistNetwork *net);",
3420 "size_t pio_dist_warnings(const PioDistNetwork *net, char *warnbuf, size_t warnlen);",
3421 "char *pio_dist_summary_json(const PioDistNetwork *net, char *errbuf, size_t errlen);",
3422 "char *pio_dist_to_json(const PioDistNetwork *net, char *errbuf, size_t errlen);",
3423 "char *pio_dist_graph_json(const PioDistNetwork *net, char *errbuf, size_t errlen);",
3424 "PioDistNetwork *pio_dist_from_json(const char *text, char *errbuf, size_t errlen);",
3425 "char *pio_dist_to_format(const PioDistNetwork *net, const char *to, char **out_warnings, char *errbuf, size_t errlen);",
3426 "char *pio_dist_convert_file(const char *path, const char *from, const char *to, char **out_warnings, char *errbuf, size_t errlen);",
3427 "char *pio_dist_convert_str(const char *text, const char *from, const char *to, char **out_warnings, char *errbuf, size_t errlen);",
3428 "char *pio_dist_geo_extract(const PioDistNetwork *net, char *errbuf, size_t errlen);",
3429 "PioDistNetwork *pio_dist_geo_apply(const PioDistNetwork *net, const char *layer, const char *name_hint, char *errbuf, size_t errlen);",
3430 ]
3431 .into_iter()
3432 .map(str::to_string)
3433 .collect::<Vec<_>>();
3434 assert_eq!(actual, expected);
3435 }
3436
3437 #[test]
3438 fn c_header_and_rust_exported_symbols_match() {
3439 let manifest = c_header_abi_manifest(include_str!("../include/powerio.h"));
3440 let header_symbols = pio_symbol_names_from_manifest(&manifest);
3441 let rust_symbols = source_exported_pio_symbols(include_str!("lib.rs"));
3442 assert_eq!(rust_symbols, header_symbols);
3443 }
3444
3445 #[test]
3446 fn parse_query_free() {
3447 let c = case9();
3448 unsafe {
3449 assert_eq!(pio_n_buses(c), 9);
3450 assert_eq!(pio_n_branches(c), 9);
3451 assert_eq!(pio_n_gens(c), 3);
3452 assert_eq!(pio_base_mva(c), 100.0);
3453 let mut name = [0 as c_char; 64];
3454 let name_len = pio_network_name(c, name.as_mut_ptr(), name.len());
3455 assert_eq!(CStr::from_ptr(name.as_ptr()).to_str().unwrap(), "case9");
3456 assert_eq!(name_len, 5);
3457 let mut source_format = [0 as c_char; 64];
3458 let fmt_len = pio_source_format(c, source_format.as_mut_ptr(), source_format.len());
3459 assert_eq!(
3460 CStr::from_ptr(source_format.as_ptr()).to_str().unwrap(),
3461 "Matpower"
3462 );
3463 assert_eq!(fmt_len, 8);
3464 let mut err = [0 as c_char; 256];
3465 let summary = pio_summary_json(c, err.as_mut_ptr(), err.len());
3466 assert!(
3467 !summary.is_null(),
3468 "summary json failed: {}",
3469 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
3470 );
3471 let summary_value: serde_json::Value =
3472 serde_json::from_str(CStr::from_ptr(summary).to_str().unwrap()).unwrap();
3473 assert_eq!(summary_value["name"], "case9");
3474 assert_eq!(summary_value["source_format"], "Matpower");
3475 assert_eq!(summary_value["base_mva"], 100.0);
3476 assert_eq!(summary_value["counts"]["buses"], 9);
3477 assert_eq!(summary_value["counts"]["branches"], 9);
3478 assert_eq!(summary_value["counts"]["generators"], 3);
3479 assert_eq!(
3480 summary_value["topology"]["reference_bus_ids"],
3481 serde_json::json!([1])
3482 );
3483 assert_eq!(summary_value["topology"]["n_components"], 1);
3484 assert_eq!(summary_value["topology"]["is_radial"], false);
3485 pio_string_free(summary);
3486 assert_eq!(pio_n_islands(c), 1);
3487 assert!(pio_ref_bus_index(c) >= 0);
3488 assert_eq!(pio_warnings(c, std::ptr::null_mut(), 0), 0);
3490 pio_network_free(c);
3491 }
3492 }
3493
3494 #[test]
3495 fn warnings_size_then_fill_exactly() {
3496 let path = data_path("pandapower/example.json");
3500 let mut err = [0 as c_char; 256];
3501 let c =
3502 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
3503 assert!(
3504 !c.is_null(),
3505 "parse failed: {}",
3506 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
3507 );
3508 unsafe {
3509 let len = pio_warnings(c, std::ptr::null_mut(), 0);
3510 assert!(len > 0, "expected read warnings");
3511 let mut warn = vec![0x7f as c_char; len + 1];
3512 assert_eq!(pio_warnings(c, warn.as_mut_ptr(), warn.len()), len);
3513 let w = CStr::from_ptr(warn.as_ptr()).to_str().unwrap();
3514 assert_eq!(w.len(), len, "buffer sized from the return holds it all");
3515 assert!(w.contains("switch"), "expected a switch warning, got {w:?}");
3516 assert_eq!(
3518 pio_warnings(std::ptr::null(), warn.as_mut_ptr(), warn.len()),
3519 0
3520 );
3521 pio_network_free(c);
3522 }
3523 }
3524
3525 #[test]
3526 fn matpower_write_is_byte_exact() {
3527 let src = std::fs::read_to_string(
3528 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case9.m"),
3529 )
3530 .unwrap();
3531 let c = case9();
3532 unsafe {
3533 assert_eq!(to_format(c, "matpower"), src);
3534
3535 let to = CString::new("matpower").unwrap();
3537 let mut err = [0 as c_char; 256];
3538 let null = pio_to_format(
3539 std::ptr::null(),
3540 to.as_ptr(),
3541 std::ptr::null_mut(),
3542 err.as_mut_ptr(),
3543 err.len(),
3544 );
3545 assert!(null.is_null());
3546 assert_eq!(
3547 CStr::from_ptr(err.as_ptr()).to_str().unwrap(),
3548 "network handle is NULL"
3549 );
3550 pio_network_free(c);
3551 }
3552 }
3553
3554 #[test]
3555 fn extract_branch_tables() {
3556 let c = case9();
3557 unsafe {
3558 let nb = pio_branches(
3560 c,
3561 std::ptr::null_mut(),
3562 std::ptr::null_mut(),
3563 std::ptr::null_mut(),
3564 std::ptr::null_mut(),
3565 std::ptr::null_mut(),
3566 std::ptr::null_mut(),
3567 std::ptr::null_mut(),
3568 std::ptr::null_mut(),
3569 0,
3570 );
3571 assert_eq!(nb, pio_n_branches(c));
3572 let mut from = vec![0i64; nb];
3573 let mut x = vec![0f64; nb];
3574 let total = pio_branches(
3575 c,
3576 from.as_mut_ptr(),
3577 std::ptr::null_mut(),
3578 std::ptr::null_mut(),
3579 x.as_mut_ptr(),
3580 std::ptr::null_mut(),
3581 std::ptr::null_mut(),
3582 std::ptr::null_mut(),
3583 std::ptr::null_mut(),
3584 nb,
3585 );
3586 assert_eq!(total, nb);
3587 assert!(from.iter().all(|&f| f >= 1));
3590 assert!(x.iter().all(|&xx| xx > 0.0));
3591 pio_network_free(c);
3592 }
3593 }
3594
3595 #[test]
3596 fn branch_tables_project_terminal_charging_to_legacy_b() {
3597 let c = terminal_projection_case();
3598 unsafe {
3599 let mut b = [0.0];
3600 let nb = pio_branches(
3601 c,
3602 std::ptr::null_mut(),
3603 std::ptr::null_mut(),
3604 std::ptr::null_mut(),
3605 std::ptr::null_mut(),
3606 b.as_mut_ptr(),
3607 std::ptr::null_mut(),
3608 std::ptr::null_mut(),
3609 std::ptr::null_mut(),
3610 1,
3611 );
3612 assert_eq!(nb, 1);
3613 close(b[0], 0.07);
3614
3615 let mut g_fr = [0.0];
3616 let mut b_fr = [0.0];
3617 let mut g_to = [0.0];
3618 let mut b_to = [0.0];
3619 let nb = pio_branch_charging(
3620 c,
3621 g_fr.as_mut_ptr(),
3622 b_fr.as_mut_ptr(),
3623 g_to.as_mut_ptr(),
3624 b_to.as_mut_ptr(),
3625 1,
3626 );
3627 assert_eq!(nb, 1);
3628 close(g_fr[0], 0.01);
3629 close(b_fr[0], 0.02);
3630 close(g_to[0], 0.03);
3631 close(b_to[0], 0.05);
3632 pio_network_free(c);
3633 }
3634 }
3635
3636 #[test]
3637 fn cap_clamps_the_write_and_returns_the_total() {
3638 let c = case9();
3639 unsafe {
3640 let total = pio_bus_ids(c, std::ptr::null_mut(), 0);
3641 assert_eq!(total, 9);
3642 let mut ids = [-1i64; 2];
3645 assert_eq!(pio_bus_ids(c, ids.as_mut_ptr(), ids.len()), 9);
3646 assert!(ids.iter().all(|&id| id >= 1));
3647 pio_network_free(c);
3648 }
3649 }
3650
3651 #[test]
3652 fn convert_matpower_echo() {
3653 let path = data_path("case14.m");
3654 let to = CString::new("matpower").unwrap();
3655 let mut warn_out: *mut c_char = std::ptr::null_mut();
3656 let mut err = [0 as c_char; 256];
3657 unsafe {
3658 let s = pio_convert_file(
3659 path.as_ptr(),
3660 std::ptr::null(),
3661 to.as_ptr(),
3662 &mut warn_out,
3663 err.as_mut_ptr(),
3664 err.len(),
3665 );
3666 assert!(!s.is_null());
3667 let got = CStr::from_ptr(s).to_str().unwrap();
3668 let src = std::fs::read_to_string(
3669 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case14.m"),
3670 )
3671 .unwrap();
3672 assert_eq!(got, src);
3673 pio_string_free(s);
3674 }
3675 }
3676
3677 #[test]
3678 fn convert_file_rejects_target_before_source_order() {
3679 let path = data_path("case14.m");
3680 let old_target = CString::new("powermodels-json").unwrap();
3681 let old_source = CString::new("matpower").unwrap();
3682 let mut warn_out: *mut c_char = std::ptr::null_mut();
3683 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
3684 unsafe {
3685 let s = pio_convert_file(
3686 path.as_ptr(),
3687 old_target.as_ptr(),
3688 old_source.as_ptr(),
3689 &mut warn_out,
3690 err.as_mut_ptr(),
3691 err.len(),
3692 );
3693 assert!(
3694 s.is_null(),
3695 "legacy target-before-source order unexpectedly succeeded"
3696 );
3697 let msg = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
3698 assert!(!msg.is_empty(), "expected an explanatory parse error");
3699 }
3700 }
3701
3702 #[test]
3703 fn convert_str_round_trips_in_memory() {
3704 let src = std::fs::read_to_string(
3707 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case9.m"),
3708 )
3709 .unwrap();
3710 let text = CString::new(src).unwrap();
3711 let from = CString::new("matpower").unwrap();
3712 let to = CString::new("powermodels-json").unwrap();
3713 let mut warn_out: *mut c_char = std::ptr::null_mut();
3714 let mut err = [0 as c_char; 256];
3715 unsafe {
3716 let s = pio_convert_str(
3717 text.as_ptr(),
3718 from.as_ptr(),
3719 to.as_ptr(),
3720 &mut warn_out,
3721 err.as_mut_ptr(),
3722 err.len(),
3723 );
3724 assert!(
3725 !s.is_null(),
3726 "convert_str failed: {}",
3727 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
3728 );
3729 let out = CStr::from_ptr(s).to_str().unwrap();
3730 assert!(out.contains("\"bus\""));
3731 pio_string_free(s);
3732 }
3733 }
3734
3735 #[test]
3736 fn convert_str_rejects_target_before_source_order() {
3737 let src = std::fs::read_to_string(
3738 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case9.m"),
3739 )
3740 .unwrap();
3741 let text = CString::new(src).unwrap();
3742 let old_target = CString::new("powermodels-json").unwrap();
3743 let old_source = CString::new("matpower").unwrap();
3744 let mut warn_out: *mut c_char = std::ptr::null_mut();
3745 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
3746 unsafe {
3747 let s = pio_convert_str(
3748 text.as_ptr(),
3749 old_target.as_ptr(),
3750 old_source.as_ptr(),
3751 &mut warn_out,
3752 err.as_mut_ptr(),
3753 err.len(),
3754 );
3755 assert!(
3756 s.is_null(),
3757 "legacy target-before-source order unexpectedly succeeded"
3758 );
3759 let msg = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
3760 assert!(!msg.is_empty(), "expected an explanatory parse error");
3761 }
3762 }
3763
3764 #[test]
3765 fn to_format_converts_live_handle() {
3766 let c = case9();
3767 unsafe {
3768 let text = to_format(c, "powermodels-json");
3769 assert!(text.contains("\"bus\""));
3770 pio_network_free(c);
3771 }
3772 }
3773
3774 #[test]
3775 fn parse_error_sets_message_not_null_handle() {
3776 let path = CString::new("/no/such/case.m").unwrap();
3777 let mut err = [0 as c_char; 256];
3778 let c =
3779 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
3780 assert!(c.is_null());
3781 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
3782 assert!(!msg.is_empty(), "expected an error message");
3783 }
3784
3785 #[test]
3786 fn non_utf8_from_hint_errors_instead_of_falling_back() {
3787 let path = data_path("case9.m");
3788 let to = CString::new("matpower").unwrap();
3789 let bad_from = [0xff_u8, 0];
3790 let mut err = [0 as c_char; 256];
3791 let c = unsafe {
3792 pio_parse_file(
3793 path.as_ptr(),
3794 bad_from.as_ptr().cast::<c_char>(),
3795 err.as_mut_ptr(),
3796 err.len(),
3797 )
3798 };
3799 assert!(c.is_null());
3800 assert_eq!(
3801 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap(),
3802 "from is not UTF-8"
3803 );
3804
3805 let mut warn_out: *mut c_char = std::ptr::null_mut();
3806 err.fill(0);
3807 let s = unsafe {
3808 pio_convert_file(
3809 path.as_ptr(),
3810 bad_from.as_ptr().cast::<c_char>(),
3811 to.as_ptr(),
3812 &mut warn_out,
3813 err.as_mut_ptr(),
3814 err.len(),
3815 )
3816 };
3817 assert!(s.is_null());
3818 assert_eq!(
3819 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap(),
3820 "from is not UTF-8"
3821 );
3822 }
3823
3824 #[test]
3825 fn extract_gen_and_bus_aggregate_tables() {
3826 let path = data_path("case30.m");
3830 let mut err = [0 as c_char; 256];
3831 let c =
3832 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
3833 assert!(!c.is_null());
3834 unsafe {
3835 let nb = pio_n_buses(c);
3836 let ng = pio_n_gens(c);
3837 assert_eq!(nb, 30);
3838 assert!(ng > 0);
3839
3840 let mut gbus = vec![-9i64; ng];
3841 let mut pmax = vec![0f64; ng];
3842 let total = pio_gens(
3843 c,
3844 gbus.as_mut_ptr(),
3845 std::ptr::null_mut(),
3846 pmax.as_mut_ptr(),
3847 std::ptr::null_mut(),
3848 std::ptr::null_mut(),
3849 ng,
3850 );
3851 assert_eq!(total, ng);
3852 assert!(gbus.iter().all(|&b| (1..=nb as i64).contains(&b)));
3854 assert!(pmax.iter().any(|&p| p > 0.0));
3855
3856 let mut ids = vec![0i64; nb];
3857 assert_eq!(pio_bus_ids(c, ids.as_mut_ptr(), nb), nb);
3858 assert!(ids.iter().all(|&id| id >= 1)); let mut pd = vec![0f64; nb];
3861 let mut qd = vec![0f64; nb];
3862 assert_eq!(pio_bus_demand(c, pd.as_mut_ptr(), qd.as_mut_ptr(), nb), nb);
3863 assert!(pd.iter().sum::<f64>() > 0.0, "case30 has active demand");
3864
3865 let mut gs = vec![0f64; nb];
3866 let mut bs = vec![0f64; nb];
3867 assert_eq!(pio_bus_shunt(c, gs.as_mut_ptr(), bs.as_mut_ptr(), nb), nb);
3868 assert!(gs.iter().chain(bs.iter()).all(|x| x.is_finite()));
3869
3870 pio_network_free(c);
3871 }
3872 }
3873
3874 #[test]
3875 fn null_handle_and_null_out_are_safe() {
3876 unsafe {
3879 let nil: *const PioNetwork = std::ptr::null();
3880 assert_eq!(pio_n_buses(nil), 0);
3881 assert_eq!(pio_n_branches(nil), 0);
3882 assert_eq!(pio_n_gens(nil), 0);
3883 assert_eq!(pio_base_mva(nil), 0.0);
3884 assert_eq!(pio_ref_bus_index(nil), -1);
3885 assert_eq!(pio_ref_bus_indices(nil, std::ptr::null_mut(), 0), 0);
3886 assert_eq!(pio_is_radial(nil), 0);
3887 assert_eq!(pio_n_islands(nil), 0);
3888
3889 let mut err = [0 as c_char; 128];
3891 assert!(pio_normalize(nil, err.as_mut_ptr(), err.len()).is_null());
3892 assert!(
3893 pio_normalize_with_options(
3894 nil,
3895 1,
3896 POWER_MODELS_ANGLE_BOUND_PAD,
3897 err.as_mut_ptr(),
3898 err.len()
3899 )
3900 .is_null()
3901 );
3902 let fmt = CString::new("matpower").unwrap();
3903 assert!(
3904 pio_parse_str(std::ptr::null(), fmt.as_ptr(), err.as_mut_ptr(), err.len())
3905 .is_null()
3906 );
3907
3908 let c = case9();
3909 assert_eq!(pio_bus_ids(c, std::ptr::null_mut(), 0), 9);
3910 pio_ref_bus_indices(c, std::ptr::null_mut(), 0);
3911 pio_bus_demand(c, std::ptr::null_mut(), std::ptr::null_mut(), 0);
3912 pio_gens(
3913 c,
3914 std::ptr::null_mut(),
3915 std::ptr::null_mut(),
3916 std::ptr::null_mut(),
3917 std::ptr::null_mut(),
3918 std::ptr::null_mut(),
3919 0,
3920 );
3921 pio_network_free(c);
3922 }
3923 }
3924
3925 #[test]
3926 fn normalized_multi_ref_is_legible() {
3927 let src = "\
3932function mpc = tworef
3933mpc.version = '2';
3934mpc.baseMVA = 100;
3935mpc.bus = [
3936\t1\t3\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
3937\t2\t3\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
3938\t3\t1\t50\t10\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
3939];
3940mpc.gen = [
3941\t1\t0\t0\t100\t-100\t1\t100\t1\t100\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0;
3942\t2\t0\t0\t100\t-100\t1\t100\t1\t300\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0;
3943];
3944mpc.branch = [
3945\t1\t2\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
3946\t2\t3\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
3947];
3948";
3949 let text = CString::new(src).unwrap();
3950 let fmt = CString::new("matpower").unwrap();
3951 let mut err = [0 as c_char; 256];
3952 unsafe {
3953 let cs = pio_parse_str(text.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len());
3954 assert!(!cs.is_null(), "parse_str returned null");
3955 let cn = pio_normalize(cs, err.as_mut_ptr(), err.len());
3956 assert!(!cn.is_null(), "normalize returned null");
3957
3958 assert_eq!(pio_ref_bus_indices(cn, std::ptr::null_mut(), 0), 2);
3960 assert_eq!(pio_ref_bus_index(cn), -1);
3962 let mut refs = [-1i64; 2];
3963 assert_eq!(pio_ref_bus_indices(cn, refs.as_mut_ptr(), refs.len()), 2);
3964 assert_eq!(refs, [0, 1]);
3965
3966 pio_network_free(cn);
3967 pio_network_free(cs);
3968 }
3969 }
3970
3971 #[test]
3972 fn normalized_preserves_source_bus_ids() {
3973 let src = "\
3974function mpc = sparseids
3975mpc.version = '2';
3976mpc.baseMVA = 100;
3977mpc.bus = [
3978\t1\t3\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
3979\t2\t1\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
3980\t3\t1\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
3981\t4\t1\t0\t0\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
3982\t10\t1\t50\t10\t0\t0\t1\t1\t0\t230\t1\t1.1\t0.9;
3983];
3984mpc.gen = [
3985\t1\t0\t0\t100\t-100\t1\t100\t1\t200\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0;
3986];
3987mpc.branch = [
3988\t1\t2\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
3989\t2\t3\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
3990\t3\t4\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
3991\t4\t10\t0.01\t0.1\t0\t0\t0\t0\t0\t0\t1\t-360\t360;
3992];
3993";
3994 let text = CString::new(src).unwrap();
3995 let fmt = CString::new("matpower").unwrap();
3996 let mut err = [0 as c_char; 256];
3997 unsafe {
3998 let cs = pio_parse_str(text.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len());
3999 assert!(!cs.is_null(), "parse_str returned null");
4000 let cn = pio_normalize(cs, err.as_mut_ptr(), err.len());
4001 assert!(!cn.is_null(), "normalize returned null");
4002
4003 let mut ids = vec![0i64; pio_n_buses(cn)];
4004 pio_bus_ids(cn, ids.as_mut_ptr(), ids.len());
4005 assert_eq!(ids, vec![1, 2, 3, 4, 10]);
4006
4007 let mut from = vec![0i64; pio_n_branches(cn)];
4008 let mut to = vec![0i64; pio_n_branches(cn)];
4009 pio_branches(
4010 cn,
4011 from.as_mut_ptr(),
4012 to.as_mut_ptr(),
4013 std::ptr::null_mut(),
4014 std::ptr::null_mut(),
4015 std::ptr::null_mut(),
4016 std::ptr::null_mut(),
4017 std::ptr::null_mut(),
4018 std::ptr::null_mut(),
4019 from.len(),
4020 );
4021 assert_eq!((from[3], to[3]), (4, 10));
4022
4023 pio_network_free(cn);
4024 pio_network_free(cs);
4025 }
4026 }
4027
4028 #[test]
4029 fn normalize_with_options_clamps_angle_bounds_and_warns() {
4030 let c = angle_bounds_case();
4031 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
4032 unsafe {
4033 let cn = pio_normalize_with_options(
4034 c,
4035 1,
4036 POWER_MODELS_ANGLE_BOUND_PAD,
4037 err.as_mut_ptr(),
4038 err.len(),
4039 );
4040 assert!(
4041 !cn.is_null(),
4042 "normalize with options returned null: {}",
4043 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4044 );
4045 let v = network_json(cn);
4046 close(
4047 v["branches"][0]["angmin"].as_f64().unwrap(),
4048 -POWER_MODELS_ANGLE_BOUND_PAD,
4049 );
4050 close(
4051 v["branches"][0]["angmax"].as_f64().unwrap(),
4052 POWER_MODELS_ANGLE_BOUND_PAD,
4053 );
4054 close(
4055 v["branches"][1]["angmin"].as_f64().unwrap(),
4056 -POWER_MODELS_ANGLE_BOUND_PAD,
4057 );
4058 close(
4059 v["branches"][1]["angmax"].as_f64().unwrap(),
4060 POWER_MODELS_ANGLE_BOUND_PAD,
4061 );
4062 close(
4063 v["branches"][2]["angmin"].as_f64().unwrap(),
4064 -std::f64::consts::PI / 6.0,
4065 );
4066 close(
4067 v["branches"][2]["angmax"].as_f64().unwrap(),
4068 std::f64::consts::PI / 6.0,
4069 );
4070 close(
4071 v["branches"][3]["angmin"].as_f64().unwrap(),
4072 -POWER_MODELS_ANGLE_BOUND_PAD,
4073 );
4074 close(
4075 v["branches"][3]["angmax"].as_f64().unwrap(),
4076 POWER_MODELS_ANGLE_BOUND_PAD,
4077 );
4078 close(
4079 v["branches"][4]["angmin"].as_f64().unwrap(),
4080 -POWER_MODELS_ANGLE_BOUND_PAD,
4081 );
4082 close(
4083 v["branches"][4]["angmax"].as_f64().unwrap(),
4084 POWER_MODELS_ANGLE_BOUND_PAD,
4085 );
4086 for branch in v["branches"].as_array().unwrap() {
4087 assert!(branch["angmin"].as_f64().unwrap() <= branch["angmax"].as_f64().unwrap());
4088 }
4089
4090 let warnings = warning_text(cn);
4091 assert!(warnings.contains("branch 0 angle difference bounds clamped"));
4092 assert!(warnings.contains("branch 1 angle difference bounds clamped"));
4093 assert!(warnings.contains("branch 3 angle difference bounds clamped"));
4094 assert!(warnings.contains("branch 4 angle difference bounds clamped"));
4095
4096 pio_network_free(cn);
4097 pio_network_free(c);
4098 }
4099 }
4100
4101 #[test]
4102 fn normalize_with_options_rejects_invalid_angle_pad() {
4103 let c = angle_bounds_case();
4104 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
4105 unsafe {
4106 let cn = pio_normalize_with_options(
4107 c,
4108 1,
4109 std::f64::consts::FRAC_PI_2,
4110 err.as_mut_ptr(),
4111 err.len(),
4112 );
4113 assert!(cn.is_null());
4114 let msg = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
4115 assert!(msg.contains("angle_bound_pad"), "{msg}");
4116 pio_network_free(c);
4117 }
4118 }
4119
4120 #[test]
4121 fn convert_emits_warning_into_buffer() {
4122 let path = data_path("t_case9_dcline.m");
4126 let to = CString::new("psse").unwrap();
4127 let mut warn_out: *mut c_char = std::ptr::null_mut();
4128 let mut err = [0 as c_char; 256];
4129 unsafe {
4130 let s = pio_convert_file(
4131 path.as_ptr(),
4132 std::ptr::null(),
4133 to.as_ptr(),
4134 &mut warn_out,
4135 err.as_mut_ptr(),
4136 err.len(),
4137 );
4138 assert!(!s.is_null());
4139 assert!(!warn_out.is_null(), "expected fidelity warnings");
4140 let w = CStr::from_ptr(warn_out).to_str().unwrap().to_owned();
4141 assert!(
4142 w.contains("converter detail"),
4143 "expected an HVDC converter-detail warning, got {w:?}"
4144 );
4145 pio_string_free(warn_out);
4146 pio_string_free(s);
4147 }
4148 }
4149
4150 #[test]
4151 fn snapshot_round_trip_preserves_structure() {
4152 let path = data_path("case30.m");
4156 let mut err = [0 as c_char; 256];
4157 let c =
4158 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
4159 assert!(!c.is_null());
4160 unsafe {
4161 let json = to_format(c, "powerio-json");
4162 assert!(json.contains("\"buses\""));
4163
4164 let text = CString::new(json).unwrap();
4165 let fmt = CString::new("powerio-json").unwrap();
4166 let back = pio_parse_str(text.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len());
4167 assert!(
4168 !back.is_null(),
4169 "snapshot parse failed: {}",
4170 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4171 );
4172 assert_eq!(pio_warnings(back, std::ptr::null_mut(), 0), 0);
4174 assert_eq!(pio_n_buses(back), pio_n_buses(c));
4176 assert_eq!(pio_n_branches(back), pio_n_branches(c));
4177 assert_eq!(pio_n_gens(back), pio_n_gens(c));
4178 assert_eq!(pio_base_mva(back), pio_base_mva(c));
4179 assert_eq!(pio_ref_bus_index(back), pio_ref_bus_index(c));
4180
4181 let alias = CString::new("json").unwrap();
4183 let again = pio_parse_str(text.as_ptr(), alias.as_ptr(), err.as_mut_ptr(), err.len());
4184 assert!(!again.is_null());
4185 assert_eq!(pio_n_buses(again), pio_n_buses(c));
4186
4187 pio_network_free(again);
4188 pio_network_free(back);
4189 pio_network_free(c);
4190 }
4191 }
4192
4193 #[test]
4194 fn snapshot_rejects_garbage() {
4195 let bad = CString::new("{ not json").unwrap();
4196 let fmt = CString::new("powerio-json").unwrap();
4197 let mut err = [0 as c_char; 256];
4198 let h = unsafe { pio_parse_str(bad.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len()) };
4199 assert!(h.is_null());
4200 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
4201 assert!(!msg.is_empty(), "expected a JSON parse error message");
4202 }
4203
4204 #[test]
4205 fn error_buffer_truncates_and_nul_terminates() {
4206 let path = CString::new("/no/such/directory/deeply/nested/missing/case.m").unwrap();
4209 let mut err = [0x7f as c_char; 16]; let c =
4211 unsafe { pio_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len()) };
4212 assert!(c.is_null());
4213 let nul = err
4214 .iter()
4215 .position(|&b| b == 0)
4216 .expect("buffer must be NUL-terminated");
4217 assert!(nul <= 15);
4218 }
4219
4220 #[test]
4221 fn truncation_lands_on_a_utf8_char_boundary() {
4222 let mut buf = [0x7f as c_char; 6];
4226 unsafe { copy_to_buf(buf.as_mut_ptr(), buf.len(), "aé€") };
4227 let s = unsafe { CStr::from_ptr(buf.as_ptr()) }
4228 .to_str()
4229 .expect("truncated message must be valid UTF-8");
4230 assert_eq!(s, "aé");
4231
4232 let mut buf = [0x7f as c_char; 8];
4234 unsafe { copy_to_buf(buf.as_mut_ptr(), buf.len(), "aé€") };
4235 let s = unsafe { CStr::from_ptr(buf.as_ptr()) }.to_str().unwrap();
4236 assert_eq!(s, "aé€");
4237 }
4238
4239 #[cfg(feature = "pkg")]
4240 #[test]
4241 fn package_feature_is_reported() {
4242 let pkg = CString::new("pkg").unwrap();
4243 let nope = CString::new("nope").unwrap();
4244 unsafe {
4245 assert_eq!(pio_has_feature(pkg.as_ptr()), 1);
4246 assert_eq!(pio_has_feature(nope.as_ptr()), 0);
4247 }
4248 }
4249
4250 #[cfg(feature = "pkg")]
4251 #[test]
4252 fn package_materialize_reports_unknown_identity() {
4253 use powerio_pkg::{
4254 ElementRef, ElementUpdate, NetworkPackage, OperatingPoint, OperatingPointSeries,
4255 TimeAxis,
4256 };
4257
4258 let case = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4259 .join("../tests/data")
4260 .join("case9.m");
4261 let net = powerio::parse_str(&std::fs::read_to_string(case).unwrap(), "matpower")
4262 .unwrap()
4263 .network;
4264 let mut point = OperatingPoint::new(0);
4265 point.updates.push(ElementUpdate::new(
4266 ElementRef::by_source_uid("generators", "no-such-uid"),
4267 std::collections::BTreeMap::from([("pg".to_owned(), serde_json::json!(1.0))]),
4268 ));
4269 let package = NetworkPackage::from_balanced(net).with_operating_points(
4270 OperatingPointSeries::new(TimeAxis::new(1).with_duration_hours(vec![1.0]), vec![point]),
4271 );
4272 let json = CString::new(package.to_json().unwrap()).unwrap();
4273
4274 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
4275 unsafe {
4276 let pkg = pio_package_parse_str(json.as_ptr(), err.as_mut_ptr(), err.len());
4277 assert!(
4278 !pkg.is_null(),
4279 "package parse_str failed: {}",
4280 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4281 );
4282 let materialized =
4283 pio_package_materialize_operating_point(pkg, 0, err.as_mut_ptr(), err.len());
4284 assert!(materialized.is_null(), "unknown identity must fail");
4285 let message = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
4286 assert!(
4287 message.contains("unknown identity"),
4288 "unexpected error: {message}"
4289 );
4290 pio_package_free(pkg);
4291 }
4292 }
4293
4294 #[cfg(feature = "pkg")]
4295 #[test]
4296 fn package_set_operating_points_round_trips() {
4297 use powerio_pkg::NetworkPackage;
4298
4299 let case = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4300 .join("../tests/data")
4301 .join("case9.m");
4302 let net = powerio::parse_str(&std::fs::read_to_string(case).unwrap(), "matpower")
4303 .unwrap()
4304 .network;
4305 let json = CString::new(NetworkPackage::from_balanced(net).to_json().unwrap()).unwrap();
4306 let series_text = r#"{
4307 "time_axis": {"periods": 1, "duration_hours": [1.0]},
4308 "points": [
4309 {
4310 "index": 0,
4311 "updates": [
4312 {
4313 "element": {"table": "generators", "source_uid": "generators:0"},
4314 "fields": {"pg": 1.5}
4315 }
4316 ]
4317 }
4318 ]
4319 }"#;
4320 let series = CString::new(series_text).unwrap();
4321
4322 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
4323 unsafe {
4324 let pkg = pio_package_parse_str(json.as_ptr(), err.as_mut_ptr(), err.len());
4325 assert!(
4326 !pkg.is_null(),
4327 "package parse_str failed: {}",
4328 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4329 );
4330 assert!(
4331 package_report_json(pio_package_operating_points_json, pkg).is_null(),
4332 "freshly parsed package must start with no operating points"
4333 );
4334
4335 let status =
4336 pio_package_set_operating_points(pkg, series.as_ptr(), err.as_mut_ptr(), err.len());
4337 assert_eq!(
4338 status,
4339 0,
4340 "set_operating_points failed: {}",
4341 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4342 );
4343
4344 let expected: serde_json::Value = serde_json::from_str(series_text).unwrap();
4345 let echoed = package_report_json(pio_package_operating_points_json, pkg);
4346 assert_eq!(echoed, expected, "attached series did not echo back");
4347
4348 let invalid = CString::new("not json").unwrap();
4349 let status = pio_package_set_operating_points(
4350 pkg,
4351 invalid.as_ptr(),
4352 err.as_mut_ptr(),
4353 err.len(),
4354 );
4355 assert_eq!(status, -1);
4356 assert_eq!(
4357 package_report_json(pio_package_operating_points_json, pkg),
4358 expected,
4359 "a parse error must not replace the existing series"
4360 );
4361
4362 let materialized =
4363 pio_package_materialize_operating_point(pkg, 0, err.as_mut_ptr(), err.len());
4364 assert!(
4365 !materialized.is_null(),
4366 "materialize failed: {}",
4367 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4368 );
4369 let materialized_json = package_json(materialized);
4370 assert_eq!(
4371 materialized_json["model"]["balanced_network"]["generators"][0]["pg"],
4372 serde_json::json!(1.5)
4373 );
4374 pio_package_free(materialized);
4375
4376 let clear = CString::new("null").unwrap();
4378 let status =
4379 pio_package_set_operating_points(pkg, clear.as_ptr(), err.as_mut_ptr(), err.len());
4380 assert_eq!(status, 0);
4381 assert!(package_report_json(pio_package_operating_points_json, pkg).is_null());
4382 assert_eq!(
4383 package_report_json(pio_package_validation_json, pkg)["status"],
4384 serde_json::json!("ok")
4385 );
4386
4387 pio_package_free(pkg);
4388 }
4389 }
4390
4391 #[cfg(feature = "pkg")]
4392 #[test]
4393 fn package_study_json_and_materialize_commit() {
4394 use powerio_pkg::{ElementRef, NetworkPackage, StudyBlock, StudyCommit, StudyEdit};
4395
4396 let case = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4397 .join("../tests/data")
4398 .join("case9.m");
4399 let net = powerio::parse_str(&std::fs::read_to_string(case).unwrap(), "matpower")
4400 .unwrap()
4401 .network;
4402 let mut commit = StudyCommit::default();
4403 commit.label = Some("load step".to_owned());
4404 commit.edits.push(StudyEdit::DemandDelta {
4405 bus: ElementRef::by_source_uid("buses", "buses:0"),
4406 p_mw: 7.0,
4407 q_mvar: Some(3.0),
4408 });
4409 let mut study = StudyBlock::default();
4410 study.label = Some("binding study".to_owned());
4411 study.commits.push(commit);
4412 let package = NetworkPackage::from_balanced(net).with_study(study);
4413 let json = CString::new(package.to_json().unwrap()).unwrap();
4414
4415 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
4416 unsafe {
4417 let pkg = pio_package_parse_str(json.as_ptr(), err.as_mut_ptr(), err.len());
4418 assert!(
4419 !pkg.is_null(),
4420 "package parse_str failed: {}",
4421 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4422 );
4423
4424 let study = package_report_json(pio_package_study_json, pkg);
4425 assert_eq!(study["label"], serde_json::json!("binding study"));
4426 assert_eq!(
4427 study["commits"][0]["edits"][0]["kind"],
4428 serde_json::json!("demand_delta")
4429 );
4430
4431 let materialized =
4432 pio_package_materialize_study_commit(pkg, 0, err.as_mut_ptr(), err.len());
4433 assert!(
4434 !materialized.is_null(),
4435 "study materialization failed: {}",
4436 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4437 );
4438 let materialized_json = package_json(materialized);
4439 assert!(materialized_json.get("study").is_none());
4440 assert!(materialized_json.get("operating_points").is_none());
4441 let loads = materialized_json["model"]["balanced_network"]["loads"]
4442 .as_array()
4443 .unwrap();
4444 assert!(loads.iter().any(|load| {
4445 load["uid"] == serde_json::json!("study:load:buses:0")
4446 && load["p"] == serde_json::json!(7.0)
4447 && load["q"] == serde_json::json!(3.0)
4448 }));
4449
4450 let negative =
4451 pio_package_materialize_study_commit(pkg, -1, err.as_mut_ptr(), err.len());
4452 assert!(negative.is_null());
4453 let message = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
4454 assert!(
4455 message.contains("study commit index must be non-negative"),
4456 "unexpected error: {message}"
4457 );
4458
4459 pio_package_free(materialized);
4460 pio_package_free(pkg);
4461 }
4462 }
4463
4464 #[cfg(feature = "pkg")]
4465 #[test]
4466 fn package_parse_free_to_json_and_reports() {
4467 let net = case9();
4468 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
4469 unsafe {
4470 let pkg = pio_package_from_balanced_network(net, 1, err.as_mut_ptr(), err.len());
4471 assert!(
4472 !pkg.is_null(),
4473 "package constructor failed: {}",
4474 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4475 );
4476 let v = package_json(pkg);
4477 assert_eq!(
4478 v[powerio::version::VERSION_KEY],
4479 serde_json::json!(powerio::VERSION)
4480 );
4481 assert_eq!(v["model_kind"], serde_json::json!("balanced"));
4482 assert_eq!(v["model"]["kind"], serde_json::json!("balanced"));
4483 assert_eq!(
4484 v["derived"]["normalized_solver_tables"]["row_counts"]["buses"],
4485 serde_json::json!(9)
4486 );
4487
4488 let json = CString::new(package_json_text(pkg)).unwrap();
4489 let parsed = pio_package_parse_str(json.as_ptr(), err.as_mut_ptr(), err.len());
4490 assert!(
4491 !parsed.is_null(),
4492 "package parse_str failed: {}",
4493 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4494 );
4495
4496 let tmp = tempfile::tempdir().unwrap();
4497 let path = tmp.path().join("case9.pio.json");
4498 std::fs::write(&path, CStr::from_ptr(json.as_ptr()).to_bytes()).unwrap();
4499 let path = CString::new(path.to_str().unwrap()).unwrap();
4500 let parsed_file = pio_package_parse_file(path.as_ptr(), err.as_mut_ptr(), err.len());
4501 assert!(
4502 !parsed_file.is_null(),
4503 "package parse_file failed: {}",
4504 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4505 );
4506
4507 assert_eq!(
4508 pio_package_validate(parsed_file, err.as_mut_ptr(), err.len()),
4509 0
4510 );
4511 let validation = package_report_json(pio_package_validation_json, parsed_file);
4512 assert_eq!(validation["status"], serde_json::json!("ok"));
4513 assert!(
4514 validation["passes"]
4515 .as_array()
4516 .unwrap()
4517 .iter()
4518 .any(|p| p["name"] == "balanced.structure")
4519 );
4520 let diagnostics = package_report_json(pio_package_diagnostics_json, parsed_file);
4521 assert!(diagnostics.as_array().unwrap().is_empty());
4522
4523 pio_package_free(parsed_file);
4524 pio_package_free(parsed);
4525 pio_package_free(pkg);
4526 pio_network_free(net);
4527 }
4528 }
4529
4530 #[cfg(feature = "pkg")]
4531 #[test]
4532 fn package_balanced_constructor_omits_solver_metadata_by_default() {
4533 let net = case9();
4534 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
4535 unsafe {
4536 let pkg = pio_package_from_balanced_network(net, 0, err.as_mut_ptr(), err.len());
4537 assert!(
4538 !pkg.is_null(),
4539 "package constructor failed: {}",
4540 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4541 );
4542 let v = package_json(pkg);
4543 assert!(v["derived"].get("normalized_solver_tables").is_none());
4544 pio_package_free(pkg);
4545 pio_network_free(net);
4546 }
4547 }
4548
4549 #[cfg(feature = "prob")]
4550 const GOC3_SMALL_FIXTURE: &str = include_str!("../../powerio-prob/tests/data/goc3_small.json");
4551
4552 #[cfg(feature = "prob")]
4553 #[test]
4554 fn scopf_handle_serializes_its_julia_document() {
4555 let text = CString::new(GOC3_SMALL_FIXTURE).unwrap();
4556 let from = CString::new("goc3-json").unwrap();
4557 let feature = CString::new("prob").unwrap();
4558 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
4559 unsafe {
4560 assert_eq!(pio_has_feature(feature.as_ptr()), 1);
4561 let instance =
4562 pio_scopf_parse_str(text.as_ptr(), from.as_ptr(), err.as_mut_ptr(), err.len());
4563 assert!(
4564 !instance.is_null(),
4565 "pio_scopf_parse_str failed: {}",
4566 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4567 );
4568 let json = pio_scopf_to_json(instance, err.as_mut_ptr(), err.len());
4569 assert!(!json.is_null());
4570 let text = CStr::from_ptr(json).to_str().unwrap().to_owned();
4571 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
4572
4573 assert_eq!(v["schema"], "powerio.scopf.julia");
4574 assert_eq!(v[powerio::version::VERSION_KEY], powerio::VERSION);
4575 assert_eq!(v["index_base"], 1);
4576 assert_eq!(v["instance"]["lengths"]["I"], 2);
4577 assert_eq!(v["instance"]["static"]["acl_branch"][0]["j_ln"], 1);
4578
4579 pio_string_free(json);
4580 pio_scopf_instance_free(instance);
4581 pio_scopf_instance_free(std::ptr::null_mut());
4582 }
4583 }
4584
4585 #[cfg(feature = "prob")]
4586 #[test]
4587 fn scopf_handle_reports_format_parse_and_null_errors() {
4588 let valid = CString::new(GOC3_SMALL_FIXTURE).unwrap();
4589 let text = CString::new("not json").unwrap();
4590 let from = CString::new("goc3-json").unwrap();
4591 let unsupported = CString::new("matpower").unwrap();
4592 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
4593 unsafe {
4594 let instance =
4595 pio_scopf_parse_str(text.as_ptr(), from.as_ptr(), err.as_mut_ptr(), err.len());
4596 assert!(instance.is_null());
4597 assert!(!CStr::from_ptr(err.as_ptr()).to_bytes().is_empty());
4598
4599 let instance = pio_scopf_parse_str(
4600 valid.as_ptr(),
4601 unsupported.as_ptr(),
4602 err.as_mut_ptr(),
4603 err.len(),
4604 );
4605 assert!(instance.is_null());
4606 assert!(
4607 CStr::from_ptr(err.as_ptr())
4608 .to_str()
4609 .unwrap()
4610 .contains("unsupported SCOPF source format")
4611 );
4612
4613 let json = pio_scopf_to_json(std::ptr::null(), err.as_mut_ptr(), err.len());
4614 assert!(json.is_null());
4615 assert!(
4616 CStr::from_ptr(err.as_ptr())
4617 .to_str()
4618 .unwrap()
4619 .contains("handle is NULL")
4620 );
4621 }
4622 }
4623
4624 #[test]
4625 fn geo_parse_normalizes_and_apply_returns_a_placed_handle() {
4626 let net = case9();
4627 let coords = CString::new("1, -89.6, 40.6\n2, -89.2, 39.8\n").unwrap();
4628 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
4629 unsafe {
4630 let empty = pio_geo_extract(net, err.as_mut_ptr(), err.len());
4632 assert!(empty.is_null());
4633
4634 let canonical = pio_geo_parse(
4635 coords.as_ptr(),
4636 std::ptr::null(),
4637 err.as_mut_ptr(),
4638 err.len(),
4639 );
4640 assert!(
4641 !canonical.is_null(),
4642 "pio_geo_parse failed: {}",
4643 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4644 );
4645 let v: serde_json::Value =
4646 serde_json::from_str(CStr::from_ptr(canonical).to_str().unwrap()).unwrap();
4647 assert_eq!(v["type"], "FeatureCollection");
4648 assert_eq!(v["powerio_geo"]["space"], "geographic");
4649
4650 let placed = pio_geo_apply(
4651 net,
4652 coords.as_ptr(),
4653 std::ptr::null(),
4654 err.as_mut_ptr(),
4655 err.len(),
4656 );
4657 assert!(
4658 !placed.is_null(),
4659 "pio_geo_apply failed: {}",
4660 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4661 );
4662 let layer = pio_geo_extract(placed, err.as_mut_ptr(), err.len());
4663 assert!(!layer.is_null());
4664 let v: serde_json::Value =
4665 serde_json::from_str(CStr::from_ptr(layer).to_str().unwrap()).unwrap();
4666 assert_eq!(v["features"].as_array().unwrap().len(), 2);
4667
4668 let count = pio_warnings(placed, std::ptr::null_mut(), 0);
4670 assert!(count > 0);
4671
4672 pio_string_free(layer);
4673 pio_string_free(canonical);
4674 pio_network_free(placed);
4675 pio_network_free(net);
4676
4677 let garbage = CString::new("not a geo file").unwrap();
4678 let out = pio_geo_parse(
4679 garbage.as_ptr(),
4680 std::ptr::null(),
4681 err.as_mut_ptr(),
4682 err.len(),
4683 );
4684 assert!(out.is_null());
4685 assert!(!CStr::from_ptr(err.as_ptr()).to_bytes().is_empty());
4686 }
4687 }
4688
4689 #[cfg(all(feature = "pkg", feature = "dist"))]
4690 mod package_dist {
4691 use super::*;
4692
4693 fn strings(values: &[&str]) -> Vec<String> {
4694 values.iter().map(|v| (*v).to_owned()).collect()
4695 }
4696
4697 fn zero_matrix(n: usize) -> powerio_dist::Mat {
4698 vec![vec![0.0; n]; n]
4699 }
4700
4701 fn diagonal_matrix(n: usize, value: f64) -> powerio_dist::Mat {
4702 let mut matrix = zero_matrix(n);
4703 for (idx, row) in matrix.iter_mut().enumerate() {
4704 row[idx] = value;
4705 }
4706 matrix
4707 }
4708
4709 fn phase_reference(terminals: &[&str], grounded: &[&str]) -> (Vec<f64>, Vec<f64>) {
4710 let phase_angles = [
4711 0.0,
4712 -2.0 * std::f64::consts::PI / 3.0,
4713 2.0 * std::f64::consts::PI / 3.0,
4714 ];
4715 let mut magnitudes = vec![0.0; terminals.len()];
4716 let mut angles = vec![0.0; terminals.len()];
4717 let mut active = 0;
4718 for (idx, terminal) in terminals.iter().enumerate() {
4719 if grounded.contains(terminal) || *terminal == "0" {
4720 continue;
4721 }
4722 magnitudes[idx] = 240.0;
4723 if active < phase_angles.len() {
4724 angles[idx] = phase_angles[active];
4725 }
4726 active += 1;
4727 }
4728 (magnitudes, angles)
4729 }
4730
4731 fn preflight_network(
4732 terminals: &[&str],
4733 grounded: &[&str],
4734 ) -> powerio_dist::MulticonductorNetwork {
4735 use powerio_dist::{
4736 DistBus, DistLine, DistLineCode, MulticonductorNetwork, VoltageSource,
4737 };
4738
4739 let n = terminals.len();
4740 let terminal_map = strings(terminals);
4741 let (v_magnitude, v_angle) = phase_reference(terminals, grounded);
4742 let mut net = MulticonductorNetwork::default();
4743 for id in ["sourcebus", "loadbus"] {
4744 let mut bus = DistBus::new(id, terminal_map.clone());
4745 bus.grounded = strings(grounded);
4746 net.buses.push(bus);
4747 }
4748 let mut linecode =
4749 DistLineCode::new("lc", diagonal_matrix(n, 0.01), diagonal_matrix(n, 0.10));
4750 linecode.g_from = zero_matrix(n);
4751 linecode.b_from = zero_matrix(n);
4752 linecode.g_to = zero_matrix(n);
4753 linecode.b_to = zero_matrix(n);
4754 net.linecodes.push(linecode);
4755 net.lines.push(DistLine::new(
4756 "l1",
4757 "sourcebus",
4758 "loadbus",
4759 terminal_map.clone(),
4760 terminal_map.clone(),
4761 "lc",
4762 1.0,
4763 ));
4764 net.sources.push(VoltageSource::new(
4765 "source",
4766 "sourcebus",
4767 terminal_map,
4768 v_magnitude,
4769 v_angle,
4770 ));
4771 net
4772 }
4773
4774 #[test]
4775 fn multiconductor_package_preflight_and_lowering() {
4776 let dist = PioDistNetwork {
4777 net: preflight_network(&["1", "2", "3"], &[]),
4778 };
4779 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
4780 unsafe {
4781 let pkg =
4782 pio_package_from_multiconductor_network(&dist, err.as_mut_ptr(), err.len());
4783 assert!(
4784 !pkg.is_null(),
4785 "multiconductor package constructor failed: {}",
4786 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4787 );
4788 let v = package_json(pkg);
4789 assert_eq!(v["model_kind"], serde_json::json!("multiconductor"));
4790
4791 let report = pio_package_multiconductor_to_balanced_preflight_json(
4792 pkg,
4793 50.0,
4794 err.as_mut_ptr(),
4795 err.len(),
4796 );
4797 assert!(
4798 !report.is_null(),
4799 "preflight failed: {}",
4800 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4801 );
4802 let report_json: serde_json::Value =
4803 serde_json::from_str(CStr::from_ptr(report).to_str().unwrap()).unwrap();
4804 assert_eq!(report_json["status"], serde_json::json!("ok"));
4805 assert_eq!(report_json["base_mva"], serde_json::json!(50.0));
4806 pio_string_free(report);
4807
4808 let lowered = pio_package_lower_multiconductor_to_balanced(
4809 pkg,
4810 75.0,
4811 err.as_mut_ptr(),
4812 err.len(),
4813 );
4814 assert!(
4815 !lowered.is_null(),
4816 "lowering failed: {}",
4817 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4818 );
4819 let lowered_json = package_json(lowered);
4820 assert_eq!(lowered_json["model_kind"], serde_json::json!("balanced"));
4821 assert_eq!(
4822 lowered_json["model"]["balanced_network"]["base_mva"],
4823 serde_json::json!(75.0)
4824 );
4825 assert_eq!(
4826 lowered_json["lowering_history"][0]["pass"],
4827 serde_json::json!("multiconductor-to-balanced")
4828 );
4829
4830 let invalid_report = pio_package_multiconductor_to_balanced_preflight_json(
4831 pkg,
4832 0.0,
4833 err.as_mut_ptr(),
4834 err.len(),
4835 );
4836 assert!(
4837 !invalid_report.is_null(),
4838 "invalid-base preflight failed: {}",
4839 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4840 );
4841 let invalid_report_json: serde_json::Value =
4842 serde_json::from_str(CStr::from_ptr(invalid_report).to_str().unwrap()).unwrap();
4843 assert_eq!(invalid_report_json["status"], serde_json::json!("error"));
4844 assert!(
4845 invalid_report_json["diagnostics"]
4846 .as_array()
4847 .unwrap()
4848 .iter()
4849 .any(|d| d["code"] == "LOWER.MULTI_TO_BALANCED.INVALID_BASE_MVA")
4850 );
4851 pio_string_free(invalid_report);
4852
4853 let invalid_lowered = pio_package_lower_multiconductor_to_balanced(
4854 pkg,
4855 0.0,
4856 err.as_mut_ptr(),
4857 err.len(),
4858 );
4859 assert!(invalid_lowered.is_null());
4860 let msg = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
4861 assert!(msg.contains("base_mva must be positive"), "got: {msg}");
4862
4863 pio_package_free(lowered);
4864 pio_package_free(pkg);
4865 }
4866 }
4867 }
4868
4869 #[cfg(feature = "arrow")]
4870 #[test]
4871 fn to_arrow_null_out_params_return_error() {
4872 let c = case9();
4874 let mut err = [0 as c_char; 256];
4875 let rc = unsafe {
4876 pio_to_arrow(
4877 c,
4878 PIO_ARROW_TABLE_BUS,
4879 std::ptr::null_mut(),
4880 std::ptr::null_mut(),
4881 err.as_mut_ptr(),
4882 err.len(),
4883 )
4884 };
4885 assert_eq!(rc, -1);
4886 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
4887 assert!(!msg.is_empty(), "expected an error message");
4888 unsafe { pio_network_free(c) };
4889 }
4890
4891 #[cfg(feature = "arrow")]
4892 #[test]
4893 fn arrow_catalog_json_symbol_returns_table_catalog() {
4894 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
4895 let ptr = unsafe { pio_arrow_catalog_json(err.as_mut_ptr(), err.len()) };
4896 assert!(
4897 !ptr.is_null(),
4898 "{}",
4899 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
4900 );
4901 let text = unsafe { CStr::from_ptr(ptr) }.to_str().unwrap().to_owned();
4902 unsafe { pio_string_free(ptr) };
4903 let catalog: serde_json::Value = serde_json::from_str(&text).unwrap();
4904 assert_eq!(catalog[powerio::version::VERSION_KEY], powerio::VERSION);
4905 assert!(catalog["tables"].as_array().unwrap().iter().any(|table| {
4906 table["id"] == serde_json::json!(PIO_ARROW_TABLE_MATRIX_BUS)
4907 && table["name"] == serde_json::json!("matrix_bus")
4908 }));
4909 }
4910
4911 #[cfg(feature = "gridfm")]
4912 #[test]
4913 fn read_dir_round_trips_and_enumerates_scenarios() {
4914 use powerio_matrix::{GridfmOptions, write_gridfm_dataset};
4915 let net = powerio::parse_file(
4917 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case14.m"),
4918 None,
4919 )
4920 .unwrap()
4921 .network;
4922 let tmp = tempfile::tempdir().unwrap();
4923 let out = write_gridfm_dataset(&net, 0, tmp.path(), &GridfmOptions::default()).unwrap();
4924 let dir = CString::new(out.dir.to_str().unwrap()).unwrap();
4925 let from = CString::new("gridfm").unwrap();
4926
4927 let mut err = [0 as c_char; 256];
4928 unsafe {
4929 let h = pio_read_dir(dir.as_ptr(), from.as_ptr(), 0, err.as_mut_ptr(), err.len());
4930 assert!(
4931 !h.is_null(),
4932 "read failed: {}",
4933 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
4934 );
4935 assert_eq!(pio_n_buses(h), 14);
4936 assert!(
4939 pio_warnings(h, std::ptr::null_mut(), 0) > 0,
4940 "expected fidelity warnings on the handle"
4941 );
4942 pio_network_free(h);
4943
4944 let count = pio_scenario_ids(
4946 dir.as_ptr(),
4947 from.as_ptr(),
4948 std::ptr::null_mut(),
4949 0,
4950 err.as_mut_ptr(),
4951 err.len(),
4952 );
4953 assert_eq!(count, 1);
4954 let mut ids = [-1i64; 4];
4955 let n = pio_scenario_ids(
4956 dir.as_ptr(),
4957 from.as_ptr(),
4958 ids.as_mut_ptr(),
4959 ids.len(),
4960 err.as_mut_ptr(),
4961 err.len(),
4962 );
4963 assert_eq!(n, 1);
4964 assert_eq!(ids[0], 0);
4965
4966 let bad = CString::new("pypsa").unwrap();
4968 let h = pio_read_dir(dir.as_ptr(), bad.as_ptr(), 0, err.as_mut_ptr(), err.len());
4969 assert!(h.is_null());
4970 let msg = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
4971 assert!(msg.contains("gridfm"), "got: {msg}");
4972
4973 let missing = CString::new(tmp.path().join("nope").to_str().unwrap()).unwrap();
4975 let bad = pio_read_dir(
4976 missing.as_ptr(),
4977 from.as_ptr(),
4978 0,
4979 err.as_mut_ptr(),
4980 err.len(),
4981 );
4982 assert!(bad.is_null());
4983 assert!(!CStr::from_ptr(err.as_ptr()).to_str().unwrap().is_empty());
4984 }
4985 }
4986
4987 #[test]
4988 fn write_dir_rejects_text_formats_by_name() {
4989 let c = case9();
4990 let to = CString::new("matpower").unwrap();
4991 let dir = CString::new("/tmp/unused").unwrap();
4992 let mut err = [0 as c_char; 256];
4993 unsafe {
4994 let rc = pio_write_dir(
4995 c,
4996 to.as_ptr(),
4997 dir.as_ptr(),
4998 std::ptr::null_mut(),
4999 err.as_mut_ptr(),
5000 err.len(),
5001 );
5002 assert_eq!(rc, -1);
5003 let msg = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
5004 assert!(msg.contains("pypsa"), "got: {msg}");
5005 pio_network_free(c);
5006 }
5007 }
5008
5009 #[cfg(feature = "dist")]
5010 fn fourwire() -> std::path::PathBuf {
5011 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
5012 .join("../tests/data/dist/micro/fourwire_linecode.dss")
5013 }
5014
5015 #[cfg(feature = "dist")]
5016 fn fourwire_cstr() -> CString {
5017 CString::new(fourwire().to_str().unwrap()).unwrap()
5018 }
5019
5020 #[cfg(feature = "dist")]
5023 unsafe fn bmopf(net: *const PioDistNetwork) -> String {
5024 let to = CString::new("bmopf").unwrap();
5025 let mut warn_out: *mut c_char = std::ptr::null_mut();
5026 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5027 let s = unsafe {
5028 pio_dist_to_format(net, to.as_ptr(), &mut warn_out, err.as_mut_ptr(), err.len())
5029 };
5030 assert!(!s.is_null());
5031 let text = unsafe { std::ffi::CStr::from_ptr(s) }
5032 .to_str()
5033 .unwrap()
5034 .to_owned();
5035 unsafe { pio_string_free(s) };
5036 text
5037 }
5038
5039 #[cfg(feature = "dist")]
5040 mod dist {
5041 use super::*;
5042 use std::ffi::CStr;
5043
5044 #[cfg(feature = "pkg")]
5045 #[test]
5046 fn dist_geo_apply_returns_a_placed_handle() {
5047 let master = CString::new(
5048 "New Circuit.c1 bus1=sourcebus basekv=12.47\n\
5049 New Line.l1 bus1=sourcebus bus2=loadbus length=1 units=km\n",
5050 )
5051 .unwrap();
5052 let format = CString::new("dss").unwrap();
5053 let coords = CString::new("sourcebus, -89.6, 40.6\nloadbus, -89.2, 39.8\n").unwrap();
5054 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5055 unsafe {
5056 let net = pio_dist_parse_str(
5057 master.as_ptr(),
5058 format.as_ptr(),
5059 err.as_mut_ptr(),
5060 err.len(),
5061 );
5062 assert!(!net.is_null());
5063 let empty = pio_dist_geo_extract(net, err.as_mut_ptr(), err.len());
5064 assert!(empty.is_null());
5065
5066 let placed = pio_dist_geo_apply(
5067 net,
5068 coords.as_ptr(),
5069 std::ptr::null(),
5070 err.as_mut_ptr(),
5071 err.len(),
5072 );
5073 assert!(
5074 !placed.is_null(),
5075 "pio_dist_geo_apply failed: {}",
5076 CStr::from_ptr(err.as_ptr()).to_str().unwrap()
5077 );
5078 let layer = pio_dist_geo_extract(placed, err.as_mut_ptr(), err.len());
5079 assert!(!layer.is_null());
5080 let v: serde_json::Value =
5081 serde_json::from_str(CStr::from_ptr(layer).to_str().unwrap()).unwrap();
5082 assert_eq!(v["features"].as_array().unwrap().len(), 2);
5083 pio_string_free(layer);
5084 pio_dist_network_free(placed);
5085 pio_dist_network_free(net);
5086 }
5087 }
5088
5089 #[test]
5090 fn dist_abi_version_is_frozen_at_one() {
5091 assert_eq!(pio_abi_version(), PIO_ABI_VERSION);
5092 assert_eq!(PIO_ABI_VERSION, 5);
5093 assert_eq!(pio_dist_abi_version(), PIO_DIST_ABI_VERSION);
5094 assert_eq!(PIO_DIST_ABI_VERSION, 1);
5095 let feature = CString::new("dist").unwrap();
5096 assert_eq!(unsafe { pio_has_feature(feature.as_ptr()) }, 1);
5097 }
5098
5099 #[test]
5100 fn version_report_states_one_powerio_version_and_the_foreign_ones() {
5101 let raw = pio_schema_versions_json();
5102 assert!(!raw.is_null());
5103 let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
5104 unsafe { pio_string_free(raw) };
5105 let doc: serde_json::Value = serde_json::from_str(&text).unwrap();
5106
5107 assert_eq!(
5110 doc[powerio::version::VERSION_KEY],
5111 serde_json::json!(powerio::VERSION)
5112 );
5113 assert_eq!(doc["abi"], serde_json::json!(PIO_ABI_VERSION));
5114
5115 for retired in ["schema_version", "package", "arrow", "dist_capabilities"] {
5118 assert_eq!(doc[retired], serde_json::Value::Null, "{retired}");
5119 }
5120
5121 #[cfg(feature = "dist")]
5124 assert_eq!(
5125 doc["bmopf_schema"],
5126 serde_json::json!(powerio_dist::BMOPF_SCHEMA_VERSION)
5127 );
5128 #[cfg(not(feature = "dist"))]
5129 assert_eq!(doc["bmopf_schema"], serde_json::Value::Null);
5130 }
5131
5132 #[test]
5133 fn dist_capabilities_json_reports_bmopf_fidelity_flags() {
5134 let ptr = pio_dist_capabilities_json();
5135 assert!(!ptr.is_null(), "dist capabilities JSON returned NULL");
5136 let text = unsafe { CStr::from_ptr(ptr) }.to_str().unwrap().to_owned();
5137 unsafe { pio_string_free(ptr) };
5138
5139 let caps: serde_json::Value = serde_json::from_str(&text).unwrap();
5140 assert_eq!(
5143 caps,
5144 serde_json::json!({
5145 "dist": true,
5146 powerio::version::VERSION_KEY: powerio::VERSION,
5147 "bmopf_fixed_taps": true,
5148 "bmopf_center_tap_leakage": true,
5149 "bmopf_delta_wye_leakage": true,
5150 "bmopf_delta_roll": true,
5151 "bmopf_voltage_source_merge": true,
5152 "bmopf_transformer_diagnostics": true,
5153 "bmopf_schema_id": powerio_dist::BMOPF_SCHEMA_ID,
5154 "bmopf_schema_version": powerio_dist::BMOPF_SCHEMA_VERSION,
5155 "typed_capacitors": true,
5156 "line_and_generator_ratings": true,
5157 "per_sequence_bus_bounds": true,
5158 "transformer_extras_relocation": true,
5159 })
5160 );
5161 }
5162
5163 #[test]
5164 fn parse_file_convert_and_echo() {
5165 let path = fourwire_cstr();
5166 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5167 let net = unsafe {
5168 pio_dist_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len())
5169 };
5170 assert!(
5171 !net.is_null(),
5172 "{}",
5173 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
5174 );
5175
5176 let to = CString::new("bmopf").unwrap();
5178 let mut warn_out: *mut c_char = std::ptr::null_mut();
5179 let s = unsafe {
5180 pio_dist_to_format(net, to.as_ptr(), &mut warn_out, err.as_mut_ptr(), err.len())
5181 };
5182 assert!(!s.is_null());
5183 let text = unsafe { CStr::from_ptr(s) }.to_str().unwrap();
5184 assert!(text.contains("\"bus\""));
5185 unsafe { pio_string_free(s) };
5186
5187 let to = CString::new("dss").unwrap();
5189 let s = unsafe {
5190 pio_dist_to_format(net, to.as_ptr(), &mut warn_out, err.as_mut_ptr(), err.len())
5191 };
5192 assert!(!s.is_null());
5193 let echoed = unsafe { CStr::from_ptr(s) }.to_str().unwrap();
5194 let source = std::fs::read_to_string(fourwire()).unwrap();
5195 assert_eq!(echoed, source);
5196 assert!(warn_out.is_null(), "a byte exact echo loses nothing");
5197 unsafe { pio_string_free(s) };
5198
5199 unsafe { pio_dist_network_free(net) };
5200 }
5201
5202 #[test]
5203 fn graph_json_reports_bus_edge_projection() {
5204 let path = fourwire_cstr();
5205 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5206 let net = unsafe {
5207 pio_dist_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len())
5208 };
5209 assert!(
5210 !net.is_null(),
5211 "{}",
5212 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
5213 );
5214
5215 let graph = unsafe { pio_dist_graph_json(net, err.as_mut_ptr(), err.len()) };
5216 assert!(
5217 !graph.is_null(),
5218 "graph json failed: {}",
5219 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
5220 );
5221 let graph_json: serde_json::Value =
5222 serde_json::from_str(unsafe { CStr::from_ptr(graph) }.to_str().unwrap()).unwrap();
5223 let buses = graph_json["buses"].as_array().unwrap();
5224 assert_eq!(buses.len(), 2);
5225 assert!(buses.iter().any(|bus| {
5226 bus["id"] == serde_json::json!("sourcebus")
5227 && bus["has_source"] == serde_json::json!(true)
5228 }));
5229 let edges = graph_json["edges"].as_array().unwrap();
5230 assert!(edges.iter().any(|edge| {
5231 edge["kind"] == serde_json::json!("line")
5232 && edge["id"] == serde_json::json!("l1")
5233 && edge["from"] == serde_json::json!("sourcebus")
5234 && edge["to"] == serde_json::json!("loadbus")
5235 && edge["n_phases"] == serde_json::json!(4)
5236 && edge["conductors"].as_array().unwrap().len() == 4
5237 }));
5238
5239 unsafe {
5240 pio_string_free(graph);
5241 pio_dist_network_free(net);
5242 }
5243 }
5244
5245 #[test]
5246 fn summary_json_reports_counts_without_model_payload() {
5247 let path = fourwire_cstr();
5248 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5249 let net = unsafe {
5250 pio_dist_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len())
5251 };
5252 assert!(
5253 !net.is_null(),
5254 "{}",
5255 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
5256 );
5257
5258 let summary = unsafe { pio_dist_summary_json(net, err.as_mut_ptr(), err.len()) };
5259 assert!(
5260 !summary.is_null(),
5261 "summary json failed: {}",
5262 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
5263 );
5264 let value: serde_json::Value =
5265 serde_json::from_str(unsafe { CStr::from_ptr(summary) }.to_str().unwrap()).unwrap();
5266 assert_eq!(
5267 value[powerio::version::VERSION_KEY],
5268 serde_json::json!(powerio::VERSION)
5269 );
5270 assert_eq!(value["source_format"], serde_json::json!("dss"));
5271 assert_eq!(value["base_frequency"], serde_json::json!(60.0));
5272 assert_eq!(value["counts"]["buses"], serde_json::json!(2));
5273 assert_eq!(value["counts"]["lines"], serde_json::json!(1));
5274 assert_eq!(value["counts"]["loads"], serde_json::json!(3));
5275
5276 unsafe {
5277 pio_string_free(summary);
5278 pio_dist_network_free(net);
5279 }
5280 }
5281
5282 #[test]
5283 fn convert_str_round_trips_through_pmd() {
5284 let source = std::fs::read_to_string(fourwire()).unwrap();
5285 let text = CString::new(source).unwrap();
5286 let from = CString::new("dss").unwrap();
5287 let to = CString::new("pmd").unwrap();
5288 let mut warn_out: *mut c_char = std::ptr::null_mut();
5289 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5290 let s = unsafe {
5291 pio_dist_convert_str(
5292 text.as_ptr(),
5293 from.as_ptr(),
5294 to.as_ptr(),
5295 &mut warn_out,
5296 err.as_mut_ptr(),
5297 err.len(),
5298 )
5299 };
5300 assert!(
5301 !s.is_null(),
5302 "{}",
5303 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
5304 );
5305 let pmd = unsafe { CStr::from_ptr(s) }.to_str().unwrap();
5306 assert!(pmd.contains("\"data_model\": \"ENGINEERING\""));
5307 unsafe { pio_string_free(s) };
5308 }
5309
5310 #[test]
5311 fn capabilities_json_reports_the_bmopf_vintage_the_writer_targets() {
5312 let raw = pio_dist_capabilities_json();
5313 assert!(!raw.is_null());
5314 let text = unsafe { CStr::from_ptr(raw) }.to_str().unwrap().to_owned();
5315 unsafe { pio_string_free(raw) };
5316 let caps: serde_json::Value = serde_json::from_str(&text).unwrap();
5317
5318 let vendored: serde_json::Value = serde_json::from_str(
5321 &std::fs::read_to_string("../tests/data/dist/bmopf/draft_bmopf_schema.json")
5322 .unwrap(),
5323 )
5324 .unwrap();
5325 assert_eq!(vendored["version"], caps["bmopf_schema_version"]);
5326
5327 assert_eq!(
5328 caps[powerio::version::VERSION_KEY],
5329 serde_json::json!(powerio::VERSION)
5330 );
5331 }
5332
5333 #[test]
5334 fn convert_str_warns_that_the_buscoords_sidecar_was_dropped() {
5335 let source = "\
5339New Circuit.c basekv=12.47
5340New Line.l1 bus1=a bus2=b phases=3
5341";
5342 let text = CString::new(source).unwrap();
5343 let from = CString::new("dss").unwrap();
5344 let to = CString::new("dss").unwrap();
5345 let mut warn_out: *mut c_char = std::ptr::null_mut();
5346 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5347 let bmopf_target = CString::new("bmopf").unwrap();
5350 let as_bmopf = unsafe {
5351 pio_dist_convert_str(
5352 text.as_ptr(),
5353 from.as_ptr(),
5354 bmopf_target.as_ptr(),
5355 &mut warn_out,
5356 err.as_mut_ptr(),
5357 err.len(),
5358 )
5359 };
5360 assert!(!as_bmopf.is_null());
5361 let bmopf_text = unsafe { CStr::from_ptr(as_bmopf) }
5362 .to_str()
5363 .unwrap()
5364 .to_owned();
5365 unsafe { pio_string_free(as_bmopf) };
5366
5367 let mut doc: serde_json::Value = serde_json::from_str(&bmopf_text).unwrap();
5369 if let Some(buses) = doc["bus"].as_object_mut() {
5370 for (i, (_, bus)) in buses.iter_mut().enumerate() {
5371 bus["longitude"] = serde_json::json!(i as f64);
5372 bus["latitude"] = serde_json::json!(i as f64);
5373 }
5374 }
5375 let with_coords = CString::new(doc.to_string()).unwrap();
5376 let bmopf_from = CString::new("bmopf").unwrap();
5377 let mut warn2_out: *mut c_char = std::ptr::null_mut();
5378 let s = unsafe {
5379 pio_dist_convert_str(
5380 with_coords.as_ptr(),
5381 bmopf_from.as_ptr(),
5382 to.as_ptr(),
5383 &mut warn2_out,
5384 err.as_mut_ptr(),
5385 err.len(),
5386 )
5387 };
5388 assert!(
5389 !s.is_null(),
5390 "{}",
5391 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
5392 );
5393 let dss = unsafe { CStr::from_ptr(s) }.to_str().unwrap().to_owned();
5394 unsafe { pio_string_free(s) };
5395 assert!(!warn2_out.is_null(), "expected a dropped sidecar warning");
5396 let warnings = unsafe { CStr::from_ptr(warn2_out) }
5397 .to_str()
5398 .unwrap()
5399 .to_owned();
5400 unsafe { pio_string_free(warn2_out) };
5401
5402 assert!(
5405 dss.to_lowercase().contains("buscoords"),
5406 "expected the dss writer to reference a buscoords file; output was: {dss}"
5407 );
5408 assert!(
5410 warnings.contains("was not written"),
5411 "dss output references a buscoords file but no warning reported the drop; \
5412 warnings were: {warnings}"
5413 );
5414 }
5415
5416 #[test]
5417 fn convert_str_rejects_target_before_source_order() {
5418 let source = std::fs::read_to_string(fourwire()).unwrap();
5419 let text = CString::new(source).unwrap();
5420 let old_target = CString::new("pmd").unwrap();
5421 let old_source = CString::new("dss").unwrap();
5422 let mut warn_out: *mut c_char = std::ptr::null_mut();
5423 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5424 let s = unsafe {
5425 pio_dist_convert_str(
5426 text.as_ptr(),
5427 old_target.as_ptr(),
5428 old_source.as_ptr(),
5429 &mut warn_out,
5430 err.as_mut_ptr(),
5431 err.len(),
5432 )
5433 };
5434 assert!(
5435 s.is_null(),
5436 "legacy target-before-source order unexpectedly succeeded"
5437 );
5438 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
5439 assert!(!msg.is_empty(), "expected an explanatory parse error");
5440 }
5441
5442 #[test]
5443 fn warnings_report_count_and_text() {
5444 let text = CString::new(
5447 "clear\nnew circuit.w basekv=12.47 bus1=src\nnew line.l1 bus1=src bus2=b2 length=1 units=furlong\n",
5448 )
5449 .unwrap();
5450 let fmt = CString::new("dss").unwrap();
5451 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5452 let net = unsafe {
5453 pio_dist_parse_str(text.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len())
5454 };
5455 assert!(!net.is_null());
5456 let mut warn = [0 as c_char; 4096];
5457 let n = unsafe { pio_dist_warnings(net, warn.as_mut_ptr(), warn.len()) };
5458 assert!(n > 0, "expected a nonzero warning length");
5459 let msg = unsafe { CStr::from_ptr(warn.as_ptr()) }.to_str().unwrap();
5460 assert!(
5461 msg.lines().any(|w| w.contains("furlong")),
5462 "expected the units warning, got: {msg}"
5463 );
5464 assert_eq!(
5466 unsafe { pio_dist_warnings(std::ptr::null(), warn.as_mut_ptr(), warn.len()) },
5467 0
5468 );
5469 unsafe { pio_dist_network_free(net) };
5470 }
5471
5472 #[test]
5473 fn convert_file_round_trips_through_bmopf() {
5474 let path = fourwire_cstr();
5475 let to = CString::new("bmopf-json").unwrap();
5476 let mut warn_out: *mut c_char = std::ptr::null_mut();
5477 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5478 let s = unsafe {
5479 pio_dist_convert_file(
5480 path.as_ptr(),
5481 std::ptr::null(),
5482 to.as_ptr(),
5483 &mut warn_out,
5484 err.as_mut_ptr(),
5485 err.len(),
5486 )
5487 };
5488 assert!(
5489 !s.is_null(),
5490 "{}",
5491 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
5492 );
5493 let text = unsafe { CStr::from_ptr(s) }.to_str().unwrap();
5494 assert!(text.contains("\"bus\""));
5495 unsafe { pio_string_free(s) };
5496 }
5497
5498 #[test]
5499 fn convert_file_rejects_target_before_source_order() {
5500 let path = fourwire_cstr();
5501 let old_target = CString::new("pmd").unwrap();
5502 let old_source = CString::new("dss").unwrap();
5503 let mut warn_out: *mut c_char = std::ptr::null_mut();
5504 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5505 let s = unsafe {
5506 pio_dist_convert_file(
5507 path.as_ptr(),
5508 old_target.as_ptr(),
5509 old_source.as_ptr(),
5510 &mut warn_out,
5511 err.as_mut_ptr(),
5512 err.len(),
5513 )
5514 };
5515 assert!(
5516 s.is_null(),
5517 "legacy target-before-source order unexpectedly succeeded"
5518 );
5519 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
5520 assert!(!msg.is_empty(), "expected an explanatory parse error");
5521 }
5522
5523 #[test]
5524 fn unknown_format_is_an_error_not_a_crash() {
5525 let text = CString::new("clear\n").unwrap();
5526 let fmt = CString::new("matpower").unwrap();
5527 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5528 let net = unsafe {
5529 pio_dist_parse_str(text.as_ptr(), fmt.as_ptr(), err.as_mut_ptr(), err.len())
5530 };
5531 assert!(net.is_null());
5532 let msg = unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap();
5533 assert!(msg.contains("unknown distribution format"));
5534 }
5535
5536 #[test]
5537 fn has_feature_reports_dist() {
5538 let dist = CString::new("dist").unwrap();
5539 assert_eq!(unsafe { pio_has_feature(dist.as_ptr()) }, 1);
5540 let nope = CString::new("nope").unwrap();
5541 assert_eq!(unsafe { pio_has_feature(nope.as_ptr()) }, 0);
5542 }
5543 }
5544
5545 #[test]
5548 fn balanced_model_json_round_trip_matches_token_writer() {
5549 let net = case9();
5550 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5551 let json = unsafe { pio_to_json(net, err.as_mut_ptr(), err.len()) };
5552 assert!(!json.is_null());
5553 let text = unsafe { CStr::from_ptr(json) }.to_str().unwrap().to_owned();
5554 unsafe { pio_string_free(json) };
5555 assert_eq!(text, unsafe { to_format(net, "powerio-json") });
5556
5557 let c = CString::new(text).unwrap();
5558 let back = unsafe { pio_from_json(c.as_ptr(), err.as_mut_ptr(), err.len()) };
5559 assert!(!back.is_null());
5560 unsafe {
5561 assert_eq!(pio_n_buses(back), pio_n_buses(net));
5562 close(pio_base_mva(back), pio_base_mva(net));
5563 pio_network_free(back);
5564 pio_network_free(net);
5565 }
5566 }
5567
5568 #[test]
5570 fn classify_str_labels() {
5571 fn classify(text: &str) -> String {
5572 let c = CString::new(text).unwrap();
5573 let mut out = [0 as c_char; 64];
5574 let n = unsafe { pio_classify_str(c.as_ptr(), out.as_mut_ptr(), out.len()) };
5575 let label = unsafe { CStr::from_ptr(out.as_ptr()) }.to_str().unwrap();
5576 assert_eq!(n, label.len(), "size-then-fill length disagrees");
5577 label.to_string()
5578 }
5579 assert_eq!(
5580 classify(r#"{"baseMVA": 100.0, "bus": {}}"#),
5581 "transmission:powermodels-json"
5582 );
5583 assert_eq!(
5584 classify(r#"{"data_model": "ENGINEERING"}"#),
5585 "distribution:pmd-json"
5586 );
5587 assert_eq!(
5588 classify(r#"{"line": {}, "bus": {}}"#),
5589 "distribution:bmopf-json"
5590 );
5591 assert_eq!(
5592 classify(r#"{"model_kind": "balanced", "model": {}}"#),
5593 "package"
5594 );
5595 assert_eq!(classify("not json"), "unknown");
5596 assert_eq!(classify(r#"{"nothing": 1}"#), "unknown");
5597 assert_eq!(
5598 unsafe { pio_classify_str(std::ptr::null(), std::ptr::null_mut(), 0) },
5599 0
5600 );
5601 }
5602
5603 #[cfg(feature = "pkg")]
5606 mod package_inverse {
5607 use super::*;
5608 use std::ffi::CStr;
5609
5610 unsafe fn package_round_trip(pkg: *mut PioPackage) -> *mut PioPackage {
5611 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5612 let json = unsafe { pio_package_to_json(pkg, err.as_mut_ptr(), err.len()) };
5613 assert!(!json.is_null());
5614 let text = unsafe { CStr::from_ptr(json) }.to_str().unwrap().to_owned();
5615 unsafe { pio_string_free(json) };
5616 let c = CString::new(text).unwrap();
5617 let reread = unsafe { pio_package_parse_str(c.as_ptr(), err.as_mut_ptr(), err.len()) };
5618 assert!(!reread.is_null());
5619 unsafe { pio_package_free(pkg) };
5620 reread
5621 }
5622
5623 #[test]
5624 fn balanced_wrap_extract_across_json() {
5625 let net = case9();
5626 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5627 let pkg =
5628 unsafe { pio_package_from_balanced_network(net, 0, err.as_mut_ptr(), err.len()) };
5629 assert!(!pkg.is_null());
5630
5631 unsafe {
5635 let mem = pio_package_to_balanced_network(pkg, err.as_mut_ptr(), err.len());
5636 assert!(!mem.is_null());
5637 assert!((*mem).net.source.is_none());
5638 pio_network_free(mem);
5639 }
5640
5641 let pkg = unsafe { package_round_trip(pkg) };
5642
5643 #[cfg(feature = "dist")]
5645 unsafe {
5646 let wrong = pio_package_to_multiconductor_network(pkg, err.as_mut_ptr(), err.len());
5647 assert!(wrong.is_null());
5648 let msg = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
5649 assert!(msg.contains("balanced"), "got: {msg}");
5650 }
5651
5652 let back = unsafe { pio_package_to_balanced_network(pkg, err.as_mut_ptr(), err.len()) };
5653 assert!(
5654 !back.is_null(),
5655 "{}",
5656 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
5657 );
5658 unsafe {
5659 assert_eq!(pio_n_buses(back), pio_n_buses(net));
5660 assert_eq!(pio_n_gens(back), pio_n_gens(net));
5661 close(pio_base_mva(back), pio_base_mva(net));
5662 pio_package_free(pkg);
5663 pio_network_free(back);
5664 pio_network_free(net);
5665 }
5666 }
5667
5668 #[cfg(feature = "dist")]
5669 #[test]
5670 fn dist_model_json_round_trip_matches_package_field() {
5671 let path = fourwire_cstr();
5672 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5673 let net = unsafe {
5674 pio_dist_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len())
5675 };
5676 assert!(!net.is_null());
5677
5678 let json = unsafe { pio_dist_to_json(net, err.as_mut_ptr(), err.len()) };
5680 assert!(!json.is_null());
5681 let text = unsafe { CStr::from_ptr(json) }.to_str().unwrap().to_owned();
5682 unsafe { pio_string_free(json) };
5683 let c = CString::new(text.clone()).unwrap();
5684 let back = unsafe { pio_dist_from_json(c.as_ptr(), err.as_mut_ptr(), err.len()) };
5685 assert!(!back.is_null());
5686
5687 unsafe { assert_eq!(bmopf(back), bmopf(net)) };
5688
5689 let pkg = unsafe {
5692 pio_package_from_multiconductor_network(net, err.as_mut_ptr(), err.len())
5693 };
5694 assert!(!pkg.is_null());
5695 let pkg_json = unsafe { pio_package_to_json(pkg, err.as_mut_ptr(), err.len()) };
5696 assert!(!pkg_json.is_null());
5697 let doc: serde_json::Value =
5698 serde_json::from_str(unsafe { CStr::from_ptr(pkg_json) }.to_str().unwrap())
5699 .unwrap();
5700 let direct: serde_json::Value = serde_json::from_str(&text).unwrap();
5701 assert_eq!(doc["model"]["multiconductor_network"], direct);
5702
5703 unsafe {
5704 pio_string_free(pkg_json);
5705 pio_package_free(pkg);
5706 pio_dist_network_free(back);
5707 pio_dist_network_free(net);
5708 }
5709 }
5710
5711 #[cfg(feature = "dist")]
5712 #[test]
5713 fn multiconductor_wrap_extract_across_json() {
5714 let path = fourwire_cstr();
5715 let mut err = [0 as c_char; PIO_ERRBUF_MIN];
5716 let net = unsafe {
5717 pio_dist_parse_file(path.as_ptr(), std::ptr::null(), err.as_mut_ptr(), err.len())
5718 };
5719 assert!(!net.is_null());
5720 let pkg = unsafe {
5721 pio_package_from_multiconductor_network(net, err.as_mut_ptr(), err.len())
5722 };
5723 assert!(!pkg.is_null());
5724
5725 unsafe {
5728 let mem = pio_package_to_multiconductor_network(pkg, err.as_mut_ptr(), err.len());
5729 assert!(!mem.is_null());
5730 assert!((*mem).net.source.is_none());
5731 assert!((*mem).net.defaulted.is_empty());
5732 pio_dist_network_free(mem);
5733 }
5734
5735 let pkg = unsafe { package_round_trip(pkg) };
5736 let back =
5737 unsafe { pio_package_to_multiconductor_network(pkg, err.as_mut_ptr(), err.len()) };
5738 assert!(
5739 !back.is_null(),
5740 "{}",
5741 unsafe { CStr::from_ptr(err.as_ptr()) }.to_str().unwrap()
5742 );
5743
5744 unsafe {
5747 assert_eq!(bmopf(back), bmopf(net));
5748 let wrong = pio_package_to_balanced_network(pkg, err.as_mut_ptr(), err.len());
5749 assert!(wrong.is_null());
5750 let msg = CStr::from_ptr(err.as_ptr()).to_str().unwrap();
5751 assert!(msg.contains("multiconductor"), "got: {msg}");
5752 pio_package_free(pkg);
5753 pio_dist_network_free(back);
5754 pio_dist_network_free(net);
5755 }
5756 }
5757 }
5758}