Skip to main content

powerio_capi/
lib.rs

1//! PowerIO C ABI, version 7.
2//!
3//! The ABI exposes the same four representation operations as Rust:
4//! [`pio_parse`], [`pio_emit`], [`pio_module_serialize`], and
5//! [`pio_module_deserialize`]. Files and memory enter through [`PioSource`];
6//! files, directories, and memory output use [`PioDestination`]. Values are
7//! identified by canonical structural type names rather than an ordinal enum.
8
9#![allow(clippy::missing_safety_doc)]
10
11use std::ffi::c_char;
12use std::panic::{AssertUnwindSafe, catch_unwind};
13use std::path::PathBuf;
14use std::sync::Arc;
15
16use powerio::{
17    BalancedNetwork, Destination, Diagnostic, EmitResult, EmittedOutput, PioScenarioSet,
18    PioTimeSeries, PioValue, Source,
19};
20use powerio_core::{ComponentId, HistoryEntry, HistoryId, HistoryKind, Producer};
21use powerio_matrix::{
22    AcOpfAssemblyOptions, AcOpfPreparation, AnalysisBranchSource, DcOperators,
23    DcOpfAssemblyOptions, DcOpfPreparation, PreparedObjective, SparseMatrix, Units,
24    build_ac_opf_preparation, build_dc_opf_preparation,
25};
26use powerio_prob::{
27    ActivePower, ActivePowerUnit, ApparentPower, ApparentPowerUnit, CalculationUpdate,
28    DcPfInstance, LoadAllocation, NetworkUpdate, OperatingPointUpdate, ReactivePower,
29    ReactivePowerUnit, UpdateChange, UpdatedField, apply_bus_load_active_power, apply_updates,
30};
31use powerio_tx::BranchSusceptanceFormula;
32
33use crate::diagnostics::codes;
34
35pub mod diagnostics;
36
37/// C ABI version.
38pub const PIO_ABI_VERSION: u32 = 7;
39
40// ---- views -----------------------------------------------------------------
41
42/// Borrowed UTF-8 bytes. The bytes need not end in NUL.
43#[repr(C)]
44#[derive(Clone, Copy)]
45pub struct PioStringView {
46    pub data: *const c_char,
47    pub len: usize,
48}
49
50impl PioStringView {
51    const EMPTY: Self = Self {
52        data: std::ptr::null(),
53        len: 0,
54    };
55
56    fn new(text: &str) -> Self {
57        Self {
58            data: text.as_ptr().cast(),
59            len: text.len(),
60        }
61    }
62}
63
64/// One borrowed source byte range from a diagnostic.
65#[repr(C)]
66#[derive(Clone, Copy)]
67pub struct PioDiagnosticSpanView {
68    pub source: PioStringView,
69    pub byte_start: u64,
70    pub byte_end: u64,
71}
72
73/// Program identity recorded with one module.
74#[repr(C)]
75#[derive(Clone, Copy)]
76pub struct PioModuleProducerView {
77    pub name: PioStringView,
78    pub version: PioStringView,
79}
80
81/// One durable source descriptor recorded with a module.
82#[repr(C)]
83#[derive(Clone, Copy)]
84pub struct PioModuleSourceView {
85    pub id: PioStringView,
86    pub name: PioStringView,
87    pub byte_length: u64,
88    pub format: PioStringView,
89    pub has_format: bool,
90    pub digest_algorithm: PioStringView,
91    pub digest: PioStringView,
92    pub has_digest: bool,
93}
94
95/// One borrowed source byte range from a source map entry.
96#[repr(C)]
97#[derive(Clone, Copy)]
98pub struct PioSourceSpanView {
99    pub source: PioStringView,
100    pub byte_start: u64,
101    pub byte_end: u64,
102}
103
104/// One typed value target and its relation to source bytes.
105#[repr(C)]
106#[derive(Clone, Copy)]
107pub struct PioModuleSourceMapEntryView {
108    pub target: PioStringView,
109    pub relation: PioStringView,
110    pub span_count: usize,
111}
112
113/// One operation recorded in module history.
114#[repr(C)]
115#[derive(Clone, Copy)]
116pub struct PioModuleHistoryEntryView {
117    pub id: PioStringView,
118    pub kind: PioStringView,
119    pub name: PioStringView,
120    pub input_type: PioStringView,
121    pub has_input_type: bool,
122    pub output_type: PioStringView,
123    pub has_output_type: bool,
124    pub parameter_count: usize,
125    pub assumption_count: usize,
126    pub loss_count: usize,
127}
128
129/// One named structured parameter in a module history entry.
130#[repr(C)]
131#[derive(Clone, Copy)]
132pub struct PioModuleHistoryParameterView {
133    pub name: PioStringView,
134    pub value_kind: PioStringView,
135}
136
137/// One namespaced structured module extension.
138#[repr(C)]
139#[derive(Clone, Copy)]
140pub struct PioModuleExtensionView {
141    pub namespace: PioStringView,
142    pub value_kind: PioStringView,
143}
144
145/// One structured JSON value stored in module history or extensions.
146#[repr(C)]
147#[derive(Clone, Copy)]
148pub struct PioJsonValueView {
149    pub kind: PioStringView,
150    pub boolean_value: bool,
151    pub number_kind: PioStringView,
152    pub signed_integer_value: i64,
153    pub unsigned_integer_value: u64,
154    pub floating_point_value: f64,
155    pub string_value: PioStringView,
156    pub element_count: usize,
157}
158
159/// One key and value type in a structured JSON object.
160#[repr(C)]
161#[derive(Clone, Copy)]
162pub struct PioJsonObjectEntryView {
163    pub key: PioStringView,
164    pub value_kind: PioStringView,
165}
166
167/// Borrowed binary bytes.
168#[repr(C)]
169#[derive(Clone, Copy)]
170pub struct PioByteView {
171    pub data: *const u8,
172    pub len: usize,
173}
174
175impl PioByteView {
176    const EMPTY: Self = Self {
177        data: std::ptr::null(),
178        len: 0,
179    };
180
181    fn new(bytes: &[u8]) -> Self {
182        Self {
183            data: bytes.as_ptr(),
184            len: bytes.len(),
185        }
186    }
187}
188
189/// Borrowed `double` values.
190#[repr(C)]
191#[derive(Clone, Copy)]
192pub struct PioF64View {
193    pub data: *const f64,
194    pub len: usize,
195}
196
197impl PioF64View {
198    const EMPTY: Self = Self {
199        data: std::ptr::null(),
200        len: 0,
201    };
202
203    fn new(values: &[f64]) -> Self {
204        Self {
205            data: values.as_ptr(),
206            len: values.len(),
207        }
208    }
209}
210
211/// Borrowed `size_t` values.
212#[repr(C)]
213#[derive(Clone, Copy)]
214pub struct PioSizeView {
215    pub data: *const usize,
216    pub len: usize,
217}
218
219impl PioSizeView {
220    const EMPTY: Self = Self {
221        data: std::ptr::null(),
222        len: 0,
223    };
224
225    fn new(values: &[usize]) -> Self {
226        Self {
227            data: values.as_ptr(),
228            len: values.len(),
229        }
230    }
231}
232
233/// One point in a balanced network coordinate space.
234#[repr(C)]
235#[derive(Clone, Copy)]
236pub struct PioBalancedLocationView {
237    pub x: f64,
238    pub y: f64,
239    pub kind: PioStringView,
240    pub has_kind: bool,
241}
242
243/// Coordinate metadata for a balanced network.
244#[repr(C)]
245#[derive(Clone, Copy)]
246pub struct PioBalancedGeoView {
247    pub has_geo: bool,
248    pub space: PioStringView,
249    pub crs: PioStringView,
250    pub has_crs: bool,
251    pub kind: PioStringView,
252    pub has_kind: bool,
253    pub has_canvas: bool,
254    pub canvas_width: f64,
255    pub has_canvas_width: bool,
256    pub canvas_height: f64,
257    pub has_canvas_height: bool,
258    pub canvas_units: PioStringView,
259    pub has_canvas_units: bool,
260}
261
262/// One balanced bus. String and coefficient spans borrow from the network.
263#[repr(C)]
264#[derive(Clone, Copy)]
265pub struct PioBalancedBusView {
266    pub component_id: PioStringView,
267    pub has_component_id: bool,
268    pub id: usize,
269    pub bus_type: PioStringView,
270    pub vm_pu: f64,
271    pub va_degrees: f64,
272    pub base_kv: f64,
273    pub vmax_pu: f64,
274    pub vmin_pu: f64,
275    pub has_emergency_voltage_limits: bool,
276    pub emergency_vmax_pu: f64,
277    pub emergency_vmin_pu: f64,
278    pub area: usize,
279    pub zone: usize,
280    pub name: PioStringView,
281    pub has_name: bool,
282    pub location: PioBalancedLocationView,
283    pub has_location: bool,
284}
285
286/// Voltage dependence attached to one balanced load.
287#[repr(C)]
288#[derive(Clone, Copy)]
289pub struct PioBalancedLoadVoltageModelView {
290    pub kind: PioStringView,
291    pub p_constant_power_mw: f64,
292    pub q_constant_power_mvar: f64,
293    pub p_constant_current_mw: f64,
294    pub q_constant_current_mvar: f64,
295    pub p_constant_impedance_mw: f64,
296    pub q_constant_impedance_mvar: f64,
297    pub exponential_p_mw: f64,
298    pub exponential_q_mvar: f64,
299    pub gamma_p: f64,
300    pub gamma_q: f64,
301    pub nominal_voltage_pu: f64,
302    pub has_nominal_voltage: bool,
303    pub load_type: i32,
304    pub has_load_type: bool,
305    pub scaling: f64,
306    pub has_scaling: bool,
307}
308
309/// One balanced load.
310#[repr(C)]
311#[derive(Clone, Copy)]
312pub struct PioBalancedLoadView {
313    pub component_id: PioStringView,
314    pub has_component_id: bool,
315    pub bus_id: usize,
316    pub p_mw: f64,
317    pub q_mvar: f64,
318    pub in_service: bool,
319    pub voltage_model: PioBalancedLoadVoltageModelView,
320}
321
322/// One switched shunt block.
323#[repr(C)]
324#[derive(Clone, Copy)]
325pub struct PioShuntBlockView {
326    pub steps: u32,
327    pub conductance_mw: f64,
328    pub susceptance_mvar: f64,
329}
330
331/// One balanced shunt.
332#[repr(C)]
333#[derive(Clone, Copy)]
334pub struct PioBalancedShuntView {
335    pub component_id: PioStringView,
336    pub has_component_id: bool,
337    pub bus_id: usize,
338    pub conductance_mw: f64,
339    pub susceptance_mvar: f64,
340    pub in_service: bool,
341    pub section_count: u32,
342    pub has_section_count: bool,
343    pub has_control: bool,
344    pub control_mode: PioStringView,
345    pub control_vmax_pu: f64,
346    pub control_vmin_pu: f64,
347    pub control_bus_id: usize,
348    pub has_control_bus: bool,
349    pub control_reactive_range_percent: f64,
350    pub control_block_count: usize,
351}
352
353/// One named branch MVA rating beyond rating A, B, and C.
354#[repr(C)]
355#[derive(Clone, Copy)]
356pub struct PioBranchRatingView {
357    pub name: PioStringView,
358    pub rate_mva: f64,
359}
360
361/// One balanced branch or two winding transformer.
362#[repr(C)]
363#[derive(Clone, Copy)]
364pub struct PioBalancedBranchView {
365    pub component_id: PioStringView,
366    pub has_component_id: bool,
367    pub name: PioStringView,
368    pub has_name: bool,
369    pub from_bus_id: usize,
370    pub to_bus_id: usize,
371    pub resistance_pu: f64,
372    pub reactance_pu: f64,
373    pub total_charging_susceptance_pu: f64,
374    pub terminal_charging_is_explicit: bool,
375    pub from_conductance_pu: f64,
376    pub from_susceptance_pu: f64,
377    pub to_conductance_pu: f64,
378    pub to_susceptance_pu: f64,
379    pub rate_a_mva: f64,
380    pub rate_b_mva: f64,
381    pub rate_c_mva: f64,
382    pub additional_rating_count: usize,
383    pub has_current_ratings: bool,
384    pub current_rating_a: f64,
385    pub current_rating_b: f64,
386    pub current_rating_c: f64,
387    pub tap_ratio: f64,
388    pub effective_tap_ratio: f64,
389    pub phase_shift_degrees: f64,
390    pub in_service: bool,
391    pub angle_min_degrees: f64,
392    pub angle_max_degrees: f64,
393    pub control: PioTransformerControlView,
394    pub has_control: bool,
395    pub route_point_count: usize,
396    pub has_route: bool,
397}
398
399/// One generator cost curve.
400#[repr(C)]
401#[derive(Clone, Copy)]
402pub struct PioGeneratorCostView {
403    pub model: u8,
404    pub startup: f64,
405    pub shutdown: f64,
406    pub ncost: usize,
407    pub coefficients: PioF64View,
408}
409
410/// One optional generator capability or ramp field.
411#[repr(C)]
412#[derive(Clone, Copy)]
413pub struct PioGeneratorCapabilityView {
414    pub name: PioStringView,
415    pub value: f64,
416    pub has_value: bool,
417}
418
419/// Governor and distributed slack settings for a generator or storage element.
420#[repr(C)]
421#[derive(Clone, Copy)]
422pub struct PioActivePowerControlView {
423    pub participate: bool,
424    pub droop_percent: f64,
425    pub has_droop_percent: bool,
426    pub participation_factor: f64,
427    pub has_participation_factor: bool,
428    pub minimum_target_active_power_mw: f64,
429    pub has_minimum_target_active_power: bool,
430    pub maximum_target_active_power_mw: f64,
431    pub has_maximum_target_active_power: bool,
432}
433
434/// One balanced generator.
435#[repr(C)]
436#[derive(Clone, Copy)]
437pub struct PioBalancedGeneratorView {
438    pub component_id: PioStringView,
439    pub has_component_id: bool,
440    pub bus_id: usize,
441    pub energy_source: PioStringView,
442    pub active_power_mw: f64,
443    pub reactive_power_mvar: f64,
444    pub active_power_max_mw: f64,
445    pub active_power_min_mw: f64,
446    pub reactive_power_max_mvar: f64,
447    pub reactive_power_min_mvar: f64,
448    pub voltage_setpoint_pu: f64,
449    pub machine_base_mva: f64,
450    pub in_service: bool,
451    pub has_cost: bool,
452    pub cost: PioGeneratorCostView,
453    pub regulated_bus_id: usize,
454    pub has_regulated_bus: bool,
455    pub capability_count: usize,
456    pub active_power_control: PioActivePowerControlView,
457    pub has_active_power_control: bool,
458    pub voltage_regulation_on: bool,
459    pub regulating_terminal: PioTerminalReferenceView,
460    pub has_regulating_terminal: bool,
461}
462
463/// One balanced storage element.
464#[repr(C)]
465#[derive(Clone, Copy)]
466pub struct PioBalancedStorageView {
467    pub component_id: PioStringView,
468    pub has_component_id: bool,
469    pub bus_id: usize,
470    pub active_power_mw: f64,
471    pub reactive_power_mvar: f64,
472    pub energy_mwh: f64,
473    pub energy_rating_mwh: f64,
474    pub charge_rating_mw: f64,
475    pub discharge_rating_mw: f64,
476    pub charge_efficiency: f64,
477    pub discharge_efficiency: f64,
478    pub thermal_rating_mva: f64,
479    pub current_rating: f64,
480    pub has_current_rating: bool,
481    pub reactive_power_min_mvar: f64,
482    pub reactive_power_max_mvar: f64,
483    pub resistance_pu: f64,
484    pub reactance_pu: f64,
485    pub active_power_loss_mw: f64,
486    pub reactive_power_loss_mvar: f64,
487    pub in_service: bool,
488    pub active_power_control: PioActivePowerControlView,
489    pub has_active_power_control: bool,
490}
491
492/// Borrowed reference to one numbered equipment terminal.
493#[repr(C)]
494#[derive(Clone, Copy)]
495pub struct PioTerminalReferenceView {
496    pub equipment: PioComponentIdView,
497    pub terminal: u8,
498}
499
500/// Automatic transformer tap or phase control.
501#[repr(C)]
502#[derive(Clone, Copy)]
503pub struct PioTransformerControlView {
504    pub mode: PioStringView,
505    pub enabled: bool,
506    pub controlled_bus_id: usize,
507    pub has_controlled_bus: bool,
508    pub controlled_bus_on_winding_side: bool,
509    pub regulating_terminal: PioTerminalReferenceView,
510    pub has_regulating_terminal: bool,
511    pub tap_min: f64,
512    pub tap_max: f64,
513    pub band_min: f64,
514    pub band_max: f64,
515    pub tap_position_count: u32,
516    pub mva_base: f64,
517    pub winding_connection_angle: f64,
518    pub has_winding_connection_angle: bool,
519}
520
521/// One balanced static VAR compensator.
522#[repr(C)]
523#[derive(Clone, Copy)]
524pub struct PioBalancedStaticVarCompensatorView {
525    pub component_id: PioStringView,
526    pub has_component_id: bool,
527    pub bus_id: usize,
528    pub minimum_susceptance_siemens: f64,
529    pub maximum_susceptance_siemens: f64,
530    pub voltage_setpoint_kv: f64,
531    pub reactive_power_setpoint_mvar: f64,
532    pub regulation_mode: PioStringView,
533    pub regulating: bool,
534    pub regulating_terminal: PioTerminalReferenceView,
535    pub has_regulating_terminal: bool,
536    pub active_power_mw: f64,
537    pub reactive_power_mvar: f64,
538    pub in_service: bool,
539}
540
541/// One balanced transmission switch.
542#[repr(C)]
543#[derive(Clone, Copy)]
544pub struct PioBalancedSwitchView {
545    pub component_id: PioStringView,
546    pub has_component_id: bool,
547    pub from_bus_id: usize,
548    pub to_bus_id: usize,
549    pub closed: bool,
550    pub thermal_rating_mva: f64,
551    pub has_thermal_rating: bool,
552    pub current_rating_a: f64,
553    pub has_current_rating: bool,
554    pub from_active_power_mw: f64,
555    pub has_from_active_power: bool,
556    pub from_reactive_power_mvar: f64,
557    pub has_from_reactive_power: bool,
558    pub to_active_power_mw: f64,
559    pub has_to_active_power: bool,
560    pub to_reactive_power_mvar: f64,
561    pub has_to_reactive_power: bool,
562}
563
564/// One AC terminal converter station of a balanced HVDC line.
565#[repr(C)]
566#[derive(Clone, Copy)]
567pub struct PioBalancedHvdcConverterView {
568    pub component: PioComponentIdView,
569    pub kind: PioStringView,
570    pub loss_factor_percent: f64,
571    pub voltage_regulator_on: bool,
572    pub has_voltage_regulator_on: bool,
573    pub voltage_setpoint_kv: f64,
574    pub has_voltage_setpoint: bool,
575    pub reactive_power_setpoint_mvar: f64,
576    pub has_reactive_power_setpoint: bool,
577    pub power_factor: f64,
578    pub has_power_factor: bool,
579    pub regulating_terminal: PioTerminalReferenceView,
580    pub has_regulating_terminal: bool,
581}
582
583/// One balanced two terminal HVDC line.
584#[repr(C)]
585#[derive(Clone, Copy)]
586pub struct PioBalancedHvdcView {
587    pub component_id: PioStringView,
588    pub has_component_id: bool,
589    pub from_bus_id: usize,
590    pub to_bus_id: usize,
591    pub in_service: bool,
592    pub from_active_power_mw: f64,
593    pub to_active_power_mw: f64,
594    pub from_reactive_power_mvar: f64,
595    pub to_reactive_power_mvar: f64,
596    pub from_voltage_pu: f64,
597    pub to_voltage_pu: f64,
598    pub minimum_active_power_mw: f64,
599    pub maximum_active_power_mw: f64,
600    pub minimum_from_reactive_power_mvar: f64,
601    pub maximum_from_reactive_power_mvar: f64,
602    pub minimum_to_reactive_power_mvar: f64,
603    pub maximum_to_reactive_power_mvar: f64,
604    pub constant_loss_mw: f64,
605    pub proportional_loss: f64,
606    pub resistance_ohm: f64,
607    pub has_resistance: bool,
608    pub nominal_voltage_kv: f64,
609    pub has_nominal_voltage: bool,
610    pub converters_mode: PioStringView,
611    pub has_converters_mode: bool,
612    pub converter1: PioBalancedHvdcConverterView,
613    pub has_converter1: bool,
614    pub converter2: PioBalancedHvdcConverterView,
615    pub has_converter2: bool,
616    pub cost: PioGeneratorCostView,
617    pub has_cost: bool,
618}
619
620/// One winding of a balanced three winding transformer.
621#[repr(C)]
622#[derive(Clone, Copy)]
623pub struct PioThreeWindingTransformerWindingView {
624    pub bus_id: usize,
625    pub tap_ratio: f64,
626    pub phase_shift_degrees: f64,
627    pub nominal_voltage_kv: f64,
628    pub rating_a_mva: f64,
629    pub rating_b_mva: f64,
630    pub rating_c_mva: f64,
631    pub control: PioTransformerControlView,
632    pub has_control: bool,
633}
634
635/// One pairwise impedance of a balanced three winding transformer.
636#[repr(C)]
637#[derive(Clone, Copy)]
638pub struct PioThreeWindingTransformerImpedanceView {
639    pub resistance_pu: f64,
640    pub reactance_pu: f64,
641    pub base_mva: f64,
642}
643
644/// One balanced three winding transformer.
645#[repr(C)]
646#[derive(Clone, Copy)]
647pub struct PioBalancedThreeWindingTransformerView {
648    pub component_id: PioStringView,
649    pub has_component_id: bool,
650    pub name: PioStringView,
651    pub has_name: bool,
652    pub winding_count: usize,
653    pub impedance_count: usize,
654    pub star_voltage_magnitude_pu: f64,
655    pub star_voltage_angle_degrees: f64,
656    pub magnetizing_conductance_pu: f64,
657    pub magnetizing_susceptance_pu: f64,
658    pub in_service: bool,
659}
660
661/// One balanced control area.
662#[repr(C)]
663#[derive(Clone, Copy)]
664pub struct PioBalancedAreaView {
665    pub number: usize,
666    pub slack_bus_id: usize,
667    pub has_slack_bus: bool,
668    pub net_interchange_mw: f64,
669    pub tolerance_mw: f64,
670    pub name: PioStringView,
671    pub has_name: bool,
672    pub component_id: PioStringView,
673    pub has_component_id: bool,
674    pub area_type: PioStringView,
675    pub has_area_type: bool,
676}
677
678/// Exact table lengths in source neutral detailed connectivity.
679#[repr(C)]
680#[derive(Clone, Copy)]
681pub struct PioDetailedConnectivityCountsView {
682    pub omitted_fields: usize,
683    pub component_metadata: usize,
684    pub subnetworks: usize,
685    pub substations: usize,
686    pub voltage_levels: usize,
687    pub bus_breaker_buses: usize,
688    pub calculated_buses: usize,
689    pub connectivity_nodes: usize,
690    pub busbar_sections: usize,
691    pub junctions: usize,
692    pub terminals: usize,
693    pub switches: usize,
694    pub internal_connections: usize,
695    pub operational_limit_groups: usize,
696    pub tap_changers: usize,
697    pub equipment_reactive_limits: usize,
698    pub boundary_lines: usize,
699    pub tie_lines: usize,
700    pub dc_converter_units: usize,
701    pub dc_topological_nodes: usize,
702    pub dc_nodes: usize,
703    pub dc_grounds: usize,
704    pub dc_busbars: usize,
705    pub dc_lines: usize,
706    pub dc_series_devices: usize,
707    pub dc_switches: usize,
708    pub voltage_source_converters: usize,
709    pub line_commutated_converters: usize,
710}
711
712/// One source field that was absent rather than explicitly assigned a value.
713#[repr(C)]
714#[derive(Clone, Copy)]
715pub struct PioOmittedFieldView {
716    pub component: PioComponentIdView,
717    pub field: PioStringView,
718}
719
720/// Reactive limits retained for one equipment record.
721#[repr(C)]
722#[derive(Clone, Copy)]
723pub struct PioEquipmentReactiveLimitsView {
724    pub equipment: PioComponentIdView,
725    pub limits: PioReactiveLimitsView,
726}
727
728/// Source neutral case metadata attached to one subnetwork.
729#[repr(C)]
730#[derive(Clone, Copy)]
731pub struct PioCaseMetadataView {
732    pub case_date: PioStringView,
733    pub has_case_date: bool,
734    pub forecast_distance: i32,
735    pub has_forecast_distance: bool,
736    pub source_model_format: PioStringView,
737    pub has_source_model_format: bool,
738    pub minimum_validation_level: PioStringView,
739    pub has_minimum_validation_level: bool,
740}
741
742/// One PowSybl subnetwork contained directly by the balanced network.
743#[repr(C)]
744#[derive(Clone, Copy)]
745pub struct PioSubnetworkView {
746    pub component: PioComponentIdView,
747    pub parent: PioComponentIdView,
748    pub case_metadata: PioCaseMetadataView,
749    pub component_count: usize,
750}
751
752/// One point of a reactive capability curve.
753#[repr(C)]
754#[derive(Clone, Copy)]
755pub struct PioReactiveCapabilityCurvePointView {
756    pub active_power_mw: f64,
757    pub minimum_reactive_power_mvar: f64,
758    pub maximum_reactive_power_mvar: f64,
759    pub property_count: usize,
760}
761
762/// Min/max or active power dependent reactive limits.
763#[repr(C)]
764#[derive(Clone, Copy)]
765pub struct PioReactiveLimitsView {
766    pub kind: PioStringView,
767    pub minimum_reactive_power_mvar: f64,
768    pub maximum_reactive_power_mvar: f64,
769    pub has_minimum_and_maximum: bool,
770    pub curve_style: PioStringView,
771    pub has_curve_style: bool,
772    pub property_count: usize,
773    pub point_count: usize,
774}
775
776/// Optional generation attached to one PowSybl boundary line.
777#[repr(C)]
778#[derive(Clone, Copy)]
779pub struct PioBoundaryLineGenerationView {
780    pub voltage_regulation_on: bool,
781    pub minimum_active_power_mw: f64,
782    pub has_minimum_active_power: bool,
783    pub maximum_active_power_mw: f64,
784    pub has_maximum_active_power: bool,
785    pub target_active_power_mw: f64,
786    pub has_target_active_power: bool,
787    pub target_reactive_power_mvar: f64,
788    pub has_target_reactive_power: bool,
789    pub target_voltage_kv: f64,
790    pub has_target_voltage: bool,
791    pub reactive_limits: PioReactiveLimitsView,
792    pub has_reactive_limits: bool,
793}
794
795/// One PowSybl boundary line retained beside the balanced calculation view.
796#[repr(C)]
797#[derive(Clone, Copy)]
798pub struct PioBoundaryLineView {
799    pub component: PioComponentIdView,
800    pub voltage_level: PioComponentIdView,
801    pub active_power_setpoint_mw: f64,
802    pub reactive_power_setpoint_mvar: f64,
803    pub resistance_ohm: f64,
804    pub reactance_ohm: f64,
805    pub conductance_siemens: f64,
806    pub susceptance_siemens: f64,
807    pub pairing_key: PioStringView,
808    pub has_pairing_key: bool,
809    pub generation: PioBoundaryLineGenerationView,
810    pub has_generation: bool,
811    pub calculation_load: PioComponentIdView,
812    pub has_calculation_load: bool,
813    pub calculation_generator: PioComponentIdView,
814    pub has_calculation_generator: bool,
815}
816
817/// One PowSybl tie line and the two boundary lines that define it.
818#[repr(C)]
819#[derive(Clone, Copy)]
820pub struct PioTieLineView {
821    pub component: PioComponentIdView,
822    pub boundary_line1: PioComponentIdView,
823    pub boundary_line2: PioComponentIdView,
824    pub calculation_branch: PioComponentIdView,
825    pub has_calculation_branch: bool,
826}
827
828/// Source neutral metadata attached to one stable component identity.
829#[repr(C)]
830#[derive(Clone, Copy)]
831pub struct PioComponentMetadataView {
832    pub component: PioComponentIdView,
833    pub name: PioStringView,
834    pub has_name: bool,
835    pub equipment_container: PioComponentIdView,
836    pub has_equipment_container: bool,
837    pub fictitious: bool,
838    pub alias_count: usize,
839    pub external_identifier_count: usize,
840    pub property_count: usize,
841}
842
843/// One source neutral component alias.
844#[repr(C)]
845#[derive(Clone, Copy)]
846pub struct PioComponentAliasView {
847    pub value: PioStringView,
848    pub alias_type: PioStringView,
849    pub has_alias_type: bool,
850}
851
852/// One source neutral external component identifier.
853#[repr(C)]
854#[derive(Clone, Copy)]
855pub struct PioExternalIdentifierView {
856    pub value: PioStringView,
857    pub authority: PioStringView,
858    pub has_authority: bool,
859}
860
861/// One source neutral string property.
862#[repr(C)]
863#[derive(Clone, Copy)]
864pub struct PioStringPropertyView {
865    pub name: PioStringView,
866    pub value: PioStringView,
867}
868
869/// One source neutral substation.
870#[repr(C)]
871#[derive(Clone, Copy)]
872pub struct PioSubstationView {
873    pub component: PioComponentIdView,
874    pub country: PioStringView,
875    pub has_country: bool,
876    pub operator_name: PioStringView,
877    pub has_operator_name: bool,
878    pub geographical_tag_count: usize,
879}
880
881/// One source neutral voltage level.
882#[repr(C)]
883#[derive(Clone, Copy)]
884pub struct PioVoltageLevelView {
885    pub component: PioComponentIdView,
886    pub substation: PioComponentIdView,
887    pub has_substation: bool,
888    pub nominal_voltage_kv: f64,
889    pub low_voltage_limit_kv: f64,
890    pub has_low_voltage_limit: bool,
891    pub high_voltage_limit_kv: f64,
892    pub has_high_voltage_limit: bool,
893    pub topology_kind: PioStringView,
894    pub bus_count: usize,
895}
896
897/// One source neutral connectivity node.
898#[repr(C)]
899#[derive(Clone, Copy)]
900pub struct PioConnectivityNodeView {
901    pub component: PioComponentIdView,
902    pub voltage_level: PioComponentIdView,
903    pub node_number: i32,
904    pub has_node_number: bool,
905    pub calculated_bus_id: usize,
906    pub has_calculated_bus: bool,
907}
908
909/// One configured bus in bus breaker topology.
910#[repr(C)]
911#[derive(Clone, Copy)]
912pub struct PioBusBreakerBusView {
913    pub component: PioComponentIdView,
914    pub voltage_level: PioComponentIdView,
915    pub calculated_bus_id: usize,
916    pub has_calculated_bus: bool,
917    pub voltage_kv: f64,
918    pub has_voltage: bool,
919    pub angle_degrees: f64,
920    pub has_angle: bool,
921}
922
923/// One calculated bus explicitly recorded in node breaker topology.
924#[repr(C)]
925#[derive(Clone, Copy)]
926pub struct PioCalculatedBusView {
927    pub voltage_level: PioComponentIdView,
928    pub calculated_bus_id: usize,
929    pub node_count: usize,
930    pub voltage_kv: f64,
931    pub has_voltage: bool,
932    pub angle_degrees: f64,
933    pub has_angle: bool,
934}
935
936/// One source neutral busbar section.
937#[repr(C)]
938#[derive(Clone, Copy)]
939pub struct PioBusbarSectionView {
940    pub component: PioComponentIdView,
941    pub voltage_level: PioComponentIdView,
942    pub node: PioComponentIdView,
943}
944
945/// One source neutral CIM junction.
946#[repr(C)]
947#[derive(Clone, Copy)]
948pub struct PioJunctionView {
949    pub component: PioComponentIdView,
950}
951
952/// One source neutral AC terminal.
953#[repr(C)]
954#[derive(Clone, Copy)]
955pub struct PioDetailedTerminalView {
956    pub component: PioComponentIdView,
957    pub has_component: bool,
958    pub equipment: PioComponentIdView,
959    pub terminal: u8,
960    pub voltage_level: PioComponentIdView,
961    pub bus: PioComponentIdView,
962    pub has_bus: bool,
963    pub connectable_bus: PioComponentIdView,
964    pub has_connectable_bus: bool,
965    pub node: PioComponentIdView,
966    pub has_node: bool,
967    pub connected: bool,
968    pub active_power_mw: f64,
969    pub has_active_power: bool,
970    pub reactive_power_mvar: f64,
971    pub has_reactive_power: bool,
972}
973
974/// One source neutral bus breaker or node breaker switch.
975#[repr(C)]
976#[derive(Clone, Copy)]
977pub struct PioTopologySwitchView {
978    pub component: PioComponentIdView,
979    pub voltage_level: PioComponentIdView,
980    pub kind: PioStringView,
981    pub endpoint1_kind: PioStringView,
982    pub endpoint1: PioComponentIdView,
983    pub endpoint2_kind: PioStringView,
984    pub endpoint2: PioComponentIdView,
985    pub open: bool,
986    pub retained: bool,
987}
988
989/// One permanent connection between two node breaker connectivity nodes.
990#[repr(C)]
991#[derive(Clone, Copy)]
992pub struct PioInternalConnectionView {
993    pub voltage_level: PioComponentIdView,
994    pub node1: PioComponentIdView,
995    pub node2: PioComponentIdView,
996}
997
998/// One named source neutral loading limit set at an equipment terminal.
999#[repr(C)]
1000#[derive(Clone, Copy)]
1001pub struct PioOperationalLimitGroupView {
1002    pub equipment: PioComponentIdView,
1003    pub terminal: u8,
1004    pub id: PioStringView,
1005    pub selected: bool,
1006    pub property_count: usize,
1007    pub has_current_limits: bool,
1008    pub current_permanent_limit_a: f64,
1009    pub current_permanent_limit_name: PioStringView,
1010    pub has_current_permanent_limit: bool,
1011    pub has_current_permanent_limit_name: bool,
1012    pub current_temporary_limit_count: usize,
1013    pub has_active_power_limits: bool,
1014    pub active_power_permanent_limit_mw: f64,
1015    pub active_power_permanent_limit_name: PioStringView,
1016    pub has_active_power_permanent_limit: bool,
1017    pub has_active_power_permanent_limit_name: bool,
1018    pub active_power_temporary_limit_count: usize,
1019    pub has_apparent_power_limits: bool,
1020    pub apparent_power_permanent_limit_mva: f64,
1021    pub apparent_power_permanent_limit_name: PioStringView,
1022    pub has_apparent_power_permanent_limit: bool,
1023    pub has_apparent_power_permanent_limit_name: bool,
1024    pub apparent_power_temporary_limit_count: usize,
1025}
1026
1027/// One segment of an AC/DC converter DC voltage droop curve.
1028#[repr(C)]
1029#[derive(Clone, Copy)]
1030pub struct PioDroopCurveSegmentView {
1031    pub minimum_voltage_kv: f64,
1032    pub maximum_voltage_kv: f64,
1033    pub k: f64,
1034}
1035
1036/// One temporary source neutral loading limit.
1037#[repr(C)]
1038#[derive(Clone, Copy)]
1039pub struct PioTemporaryLimitView {
1040    pub name: PioStringView,
1041    pub value: f64,
1042    pub acceptable_duration_seconds: u64,
1043    pub fictitious: bool,
1044}
1045
1046/// One source neutral transformer tap changer.
1047#[repr(C)]
1048#[derive(Clone, Copy)]
1049pub struct PioTapChangerView {
1050    pub component: PioComponentIdView,
1051    pub has_component: bool,
1052    pub transformer: PioComponentIdView,
1053    pub winding: u8,
1054    pub kind: PioStringView,
1055    pub tap_position: i32,
1056    pub has_tap_position: bool,
1057    pub solved_tap_position: i32,
1058    pub has_solved_tap_position: bool,
1059    pub low_tap_position: i32,
1060    pub neutral_tap_position: i32,
1061    pub has_neutral_tap_position: bool,
1062    pub normal_tap_position: i32,
1063    pub has_normal_tap_position: bool,
1064    pub voltage_step_increment_percent: f64,
1065    pub has_voltage_step_increment_percent: bool,
1066    pub load_tap_changing_capabilities: bool,
1067    pub regulating: bool,
1068    pub regulation_mode: PioStringView,
1069    pub has_regulation_mode: bool,
1070    pub regulation_value: f64,
1071    pub has_regulation_value: bool,
1072    pub target_deadband: f64,
1073    pub has_target_deadband: bool,
1074    pub regulation_terminal: PioTerminalReferenceView,
1075    pub has_regulation_terminal: bool,
1076    pub step_count: usize,
1077}
1078
1079/// One source neutral transformer tap changer step.
1080#[repr(C)]
1081#[derive(Clone, Copy)]
1082pub struct PioTapChangerStepView {
1083    pub position: i32,
1084    pub ratio_pu: f64,
1085    pub phase_shift_degrees: f64,
1086    pub resistance_deviation_percent: f64,
1087    pub reactance_deviation_percent: f64,
1088    pub conductance_deviation_percent: f64,
1089    pub susceptance_deviation_percent: f64,
1090}
1091
1092/// One terminal of source neutral DC conducting equipment.
1093#[repr(C)]
1094#[derive(Clone, Copy)]
1095pub struct PioDcTerminalView {
1096    pub component: PioComponentIdView,
1097    pub has_component: bool,
1098    pub sequence_number: u32,
1099    pub has_sequence_number: bool,
1100    pub dc_node: PioComponentIdView,
1101    pub has_dc_node: bool,
1102    pub dc_topological_node: PioComponentIdView,
1103    pub has_dc_topological_node: bool,
1104    pub polarity: PioStringView,
1105    pub has_polarity: bool,
1106    pub connected: bool,
1107    pub has_connected: bool,
1108    pub active_power_mw: f64,
1109    pub has_active_power: bool,
1110    pub current_a: f64,
1111    pub has_current: bool,
1112}
1113
1114/// One source neutral DC conducting equipment record.
1115#[repr(C)]
1116#[derive(Clone, Copy)]
1117pub struct PioDcEquipmentView {
1118    pub component: PioComponentIdView,
1119    pub equipment_container: PioComponentIdView,
1120    pub has_equipment_container: bool,
1121    pub kind: PioStringView,
1122    pub terminal_count: usize,
1123    pub terminal1: PioDcTerminalView,
1124    pub terminal2: PioDcTerminalView,
1125    pub rated_dc_voltage_kv: f64,
1126    pub has_rated_dc_voltage: bool,
1127    pub resistance_ohm: f64,
1128    pub has_resistance: bool,
1129    pub inductance_h: f64,
1130    pub has_inductance: bool,
1131    pub capacitance_f: f64,
1132    pub has_capacitance: bool,
1133    pub length_km: f64,
1134    pub has_length: bool,
1135    pub switch_kind: PioStringView,
1136    pub has_switch_kind: bool,
1137    pub open: bool,
1138    pub has_open: bool,
1139}
1140
1141/// One physical or energized node in source neutral DC connectivity.
1142#[repr(C)]
1143#[derive(Clone, Copy)]
1144pub struct PioDcNodeView {
1145    pub component: PioComponentIdView,
1146    pub kind: PioStringView,
1147    pub nominal_voltage_kv: f64,
1148    pub has_nominal_voltage: bool,
1149    pub voltage_kv: f64,
1150    pub has_voltage: bool,
1151    pub dc_converter_unit: PioComponentIdView,
1152    pub has_dc_converter_unit: bool,
1153    pub dc_topological_node: PioComponentIdView,
1154    pub has_dc_topological_node: bool,
1155}
1156
1157/// One source neutral DC converter unit.
1158#[repr(C)]
1159#[derive(Clone, Copy)]
1160pub struct PioDcConverterUnitView {
1161    pub component: PioComponentIdView,
1162    pub substation: PioComponentIdView,
1163    pub has_substation: bool,
1164    pub operation_mode: PioStringView,
1165}
1166
1167/// One source neutral AC/DC converter.
1168#[repr(C)]
1169#[derive(Clone, Copy)]
1170pub struct PioAcDcConverterView {
1171    pub component: PioComponentIdView,
1172    pub kind: PioStringView,
1173    pub dc_converter_unit: PioComponentIdView,
1174    pub has_dc_converter_unit: bool,
1175    pub dc_terminal1: PioDcTerminalView,
1176    pub dc_terminal2: PioDcTerminalView,
1177    pub base_apparent_power_mva: f64,
1178    pub has_base_apparent_power: bool,
1179    pub minimum_active_power_mw: f64,
1180    pub has_minimum_active_power: bool,
1181    pub maximum_active_power_mw: f64,
1182    pub has_maximum_active_power: bool,
1183    pub minimum_dc_voltage_kv: f64,
1184    pub has_minimum_dc_voltage: bool,
1185    pub maximum_dc_voltage_kv: f64,
1186    pub has_maximum_dc_voltage: bool,
1187    pub rated_dc_voltage_kv: f64,
1188    pub has_rated_dc_voltage: bool,
1189    pub valve_u0_kv: f64,
1190    pub has_valve_u0: bool,
1191    pub number_of_valves: u32,
1192    pub has_number_of_valves: bool,
1193    pub idle_loss_mw: f64,
1194    pub has_idle_loss: bool,
1195    pub switching_loss_mw_per_ampere: f64,
1196    pub has_switching_loss: bool,
1197    pub resistive_loss_ohm: f64,
1198    pub has_resistive_loss: bool,
1199    pub control_mode: PioStringView,
1200    pub has_control_mode: bool,
1201    pub active_power_at_pcc_mw: f64,
1202    pub has_active_power_at_pcc: bool,
1203    pub reactive_power_at_pcc_mvar: f64,
1204    pub has_reactive_power_at_pcc: bool,
1205    pub target_active_power_mw: f64,
1206    pub has_target_active_power: bool,
1207    pub target_dc_voltage_kv: f64,
1208    pub has_target_dc_voltage: bool,
1209    pub pcc_terminal: PioTerminalReferenceView,
1210    pub has_pcc_terminal: bool,
1211    pub droop_curve_segment_count: usize,
1212    pub has_droop_curve: bool,
1213    pub droop: f64,
1214    pub has_droop: bool,
1215    pub droop_compensation: f64,
1216    pub has_droop_compensation: bool,
1217    pub q_share: f64,
1218    pub has_q_share: bool,
1219    pub maximum_modulation_index: f64,
1220    pub has_maximum_modulation_index: bool,
1221    pub maximum_valve_current_a: f64,
1222    pub has_maximum_valve_current: bool,
1223    pub dc_current_a: f64,
1224    pub has_dc_current: bool,
1225    pub ac_voltage_kv: f64,
1226    pub has_ac_voltage: bool,
1227    pub dc_voltage_kv: f64,
1228    pub has_dc_voltage: bool,
1229    pub voltage_regulator_on: bool,
1230    pub has_voltage_regulator_on: bool,
1231    pub voltage_setpoint_kv: f64,
1232    pub has_voltage_setpoint: bool,
1233    pub reactive_power_setpoint_mvar: f64,
1234    pub has_reactive_power_setpoint: bool,
1235    pub reactive_limits: PioReactiveLimitsView,
1236    pub has_reactive_limits: bool,
1237    pub pole_loss_active_power_mw: f64,
1238    pub has_pole_loss_active_power: bool,
1239    pub reactive_model: PioStringView,
1240    pub has_reactive_model: bool,
1241    pub power_factor: f64,
1242    pub has_power_factor: bool,
1243    pub operating_mode: PioStringView,
1244    pub has_operating_mode: bool,
1245    pub rated_dc_current_a: f64,
1246    pub has_rated_dc_current: bool,
1247    pub minimum_alpha_degrees: f64,
1248    pub has_minimum_alpha: bool,
1249    pub maximum_alpha_degrees: f64,
1250    pub has_maximum_alpha: bool,
1251    pub minimum_gamma_degrees: f64,
1252    pub has_minimum_gamma: bool,
1253    pub maximum_gamma_degrees: f64,
1254    pub has_maximum_gamma: bool,
1255    pub target_alpha_degrees: f64,
1256    pub has_target_alpha: bool,
1257    pub target_gamma_degrees: f64,
1258    pub has_target_gamma: bool,
1259    pub target_dc_current_a: f64,
1260    pub has_target_dc_current: bool,
1261    pub alpha_degrees: f64,
1262    pub has_alpha: bool,
1263    pub gamma_degrees: f64,
1264    pub has_gamma: bool,
1265    pub delta_degrees: f64,
1266    pub has_delta: bool,
1267    pub uf_kv: f64,
1268    pub has_uf: bool,
1269    pub uv_kv: f64,
1270    pub has_uv: bool,
1271}
1272
1273/// One point in a multiconductor network coordinate space.
1274#[repr(C)]
1275#[derive(Clone, Copy)]
1276pub struct PioMulticonductorLocationView {
1277    pub x: f64,
1278    pub y: f64,
1279    pub kind: PioStringView,
1280    pub has_kind: bool,
1281}
1282
1283/// Coordinate metadata for a multiconductor network.
1284#[repr(C)]
1285#[derive(Clone, Copy)]
1286pub struct PioMulticonductorGeoView {
1287    pub has_geo: bool,
1288    pub space: PioStringView,
1289    pub crs: PioStringView,
1290    pub has_crs: bool,
1291    pub kind: PioStringView,
1292    pub has_kind: bool,
1293    pub has_canvas: bool,
1294    pub canvas_width: f64,
1295    pub has_canvas_width: bool,
1296    pub canvas_height: f64,
1297    pub has_canvas_height: bool,
1298    pub canvas_units: PioStringView,
1299    pub has_canvas_units: bool,
1300}
1301
1302/// Exact table lengths in a multiconductor network.
1303///
1304/// Source extension `extras` maps are not exposed through ABI 7. They are
1305/// retained by PowerIO for same format emission but are not PowerIO domain data.
1306#[repr(C)]
1307#[derive(Clone, Copy)]
1308pub struct PioMulticonductorNetworkCountsView {
1309    pub buses: usize,
1310    pub line_codes: usize,
1311    pub lines: usize,
1312    pub switches: usize,
1313    pub transformers: usize,
1314    pub loads: usize,
1315    pub generators: usize,
1316    pub inverter_based_resources: usize,
1317    pub control_profiles: usize,
1318    pub shunts: usize,
1319    pub capacitors: usize,
1320    pub voltage_sources: usize,
1321    pub untyped_objects: usize,
1322    pub commands: usize,
1323    pub options: usize,
1324}
1325
1326/// One multiconductor bus.
1327#[repr(C)]
1328#[derive(Clone, Copy)]
1329pub struct PioMulticonductorBusView {
1330    pub id: PioStringView,
1331    pub terminal_count: usize,
1332    pub grounded_terminal_count: usize,
1333    pub voltage_min_v: f64,
1334    pub has_voltage_min: bool,
1335    pub voltage_max_v: f64,
1336    pub has_voltage_max: bool,
1337    /// Nonuniform phase bounds, in phase-terminal order, used when the scalar is absent.
1338    pub phase_to_ground_voltage_min_v: PioF64View,
1339    pub has_phase_to_ground_voltage_min: bool,
1340    pub phase_to_ground_voltage_max_v: PioF64View,
1341    pub has_phase_to_ground_voltage_max: bool,
1342    pub phase_to_neutral_voltage_min_v: PioF64View,
1343    pub has_phase_to_neutral_voltage_min: bool,
1344    pub phase_to_neutral_voltage_max_v: PioF64View,
1345    pub has_phase_to_neutral_voltage_max: bool,
1346    pub phase_to_phase_voltage_min_v: PioF64View,
1347    pub has_phase_to_phase_voltage_min: bool,
1348    pub phase_to_phase_voltage_max_v: PioF64View,
1349    pub has_phase_to_phase_voltage_max: bool,
1350    pub positive_sequence_voltage_min_v: f64,
1351    pub has_positive_sequence_voltage_min: bool,
1352    pub positive_sequence_voltage_max_v: f64,
1353    pub has_positive_sequence_voltage_max: bool,
1354    pub negative_sequence_voltage_max_v: f64,
1355    pub has_negative_sequence_voltage_max: bool,
1356    pub zero_sequence_voltage_max_v: f64,
1357    pub has_zero_sequence_voltage_max: bool,
1358    pub neutral_to_ground_voltage_max_v: f64,
1359    pub has_neutral_to_ground_voltage_max: bool,
1360    pub location: PioMulticonductorLocationView,
1361    pub has_location: bool,
1362}
1363
1364/// One multiconductor line code.
1365#[repr(C)]
1366#[derive(Clone, Copy)]
1367pub struct PioMulticonductorLineCodeView {
1368    pub name: PioStringView,
1369    pub conductor_count: usize,
1370    pub resistance_matrix_row_count: usize,
1371    pub reactance_matrix_row_count: usize,
1372    pub conductance_from_matrix_row_count: usize,
1373    pub susceptance_from_matrix_row_count: usize,
1374    pub conductance_to_matrix_row_count: usize,
1375    pub susceptance_to_matrix_row_count: usize,
1376    pub current_limit_a: PioF64View,
1377    pub has_current_limit: bool,
1378    pub apparent_power_limit_va: PioF64View,
1379    pub has_apparent_power_limit: bool,
1380    pub source: PioStringView,
1381    pub has_source: bool,
1382}
1383
1384/// One multiconductor line.
1385#[repr(C)]
1386#[derive(Clone, Copy)]
1387pub struct PioMulticonductorLineView {
1388    pub name: PioStringView,
1389    pub bus_from: PioStringView,
1390    pub bus_to: PioStringView,
1391    pub terminal_map_from_count: usize,
1392    pub terminal_map_to_count: usize,
1393    pub line_code: PioStringView,
1394    pub length_m: f64,
1395    pub route_point_count: usize,
1396    pub has_route: bool,
1397    pub current_limit_a: PioF64View,
1398    pub has_current_limit: bool,
1399    pub apparent_power_limit_va: PioF64View,
1400    pub has_apparent_power_limit: bool,
1401}
1402
1403/// One multiconductor switch.
1404#[repr(C)]
1405#[derive(Clone, Copy)]
1406pub struct PioMulticonductorSwitchView {
1407    pub name: PioStringView,
1408    pub bus_from: PioStringView,
1409    pub bus_to: PioStringView,
1410    pub terminal_map_from_count: usize,
1411    pub terminal_map_to_count: usize,
1412    pub open: bool,
1413    pub current_limit_a: PioF64View,
1414    pub has_current_limit: bool,
1415}
1416
1417/// One multiconductor transformer.
1418#[repr(C)]
1419#[derive(Clone, Copy)]
1420pub struct PioMulticonductorTransformerView {
1421    pub name: PioStringView,
1422    pub winding_count: usize,
1423    pub short_circuit_reactance_percent: PioF64View,
1424    pub phase_count: usize,
1425}
1426
1427/// One winding of a multiconductor transformer.
1428#[repr(C)]
1429#[derive(Clone, Copy)]
1430pub struct PioMulticonductorTransformerWindingView {
1431    pub bus: PioStringView,
1432    pub terminal_map_count: usize,
1433    pub connection: PioStringView,
1434    pub rated_voltage_v: f64,
1435    pub apparent_power_rating_va: f64,
1436    pub resistance_percent: f64,
1437    pub tap: f64,
1438    pub neutral_resistance_ohm: f64,
1439    pub has_neutral_resistance: bool,
1440    pub neutral_reactance_ohm: f64,
1441    pub has_neutral_reactance: bool,
1442}
1443
1444/// One multiconductor load.
1445#[repr(C)]
1446#[derive(Clone, Copy)]
1447pub struct PioMulticonductorLoadView {
1448    pub name: PioStringView,
1449    pub bus: PioStringView,
1450    pub terminal_map_count: usize,
1451    pub configuration: PioStringView,
1452    pub active_power_nominal_w: PioF64View,
1453    pub reactive_power_nominal_var: PioF64View,
1454    pub voltage_model: PioStringView,
1455    pub nominal_voltage_v: PioF64View,
1456    pub active_power_constant_impedance: PioF64View,
1457    pub active_power_constant_current: PioF64View,
1458    pub active_power_constant_power: PioF64View,
1459    pub reactive_power_constant_impedance: PioF64View,
1460    pub reactive_power_constant_current: PioF64View,
1461    pub reactive_power_constant_power: PioF64View,
1462    pub active_power_exponent: PioF64View,
1463    pub reactive_power_exponent: PioF64View,
1464}
1465
1466/// One multiconductor generator.
1467#[repr(C)]
1468#[derive(Clone, Copy)]
1469pub struct PioMulticonductorGeneratorView {
1470    pub name: PioStringView,
1471    pub bus: PioStringView,
1472    pub terminal_map_count: usize,
1473    pub configuration: PioStringView,
1474    pub active_power_nominal_w: PioF64View,
1475    pub reactive_power_nominal_var: PioF64View,
1476    pub active_power_min_w: PioF64View,
1477    pub has_active_power_min: bool,
1478    pub active_power_max_w: PioF64View,
1479    pub has_active_power_max: bool,
1480    pub reactive_power_min_var: PioF64View,
1481    pub has_reactive_power_min: bool,
1482    pub reactive_power_max_var: PioF64View,
1483    pub has_reactive_power_max: bool,
1484    pub active_power_dispatch_cost_per_kwh: PioF64View,
1485    pub has_active_power_dispatch_cost: bool,
1486    pub apparent_power_limit_va: PioF64View,
1487    pub has_apparent_power_limit: bool,
1488    pub current_limit_a: PioF64View,
1489    pub has_current_limit: bool,
1490}
1491
1492/// One inverter based resource.
1493#[repr(C)]
1494#[derive(Clone, Copy)]
1495pub struct PioInverterBasedResourceView {
1496    pub name: PioStringView,
1497    pub bus: PioStringView,
1498    pub terminal_map_count: usize,
1499    pub topology: PioStringView,
1500    pub prime_mover: PioStringView,
1501    pub apparent_power_limit_va: PioF64View,
1502    pub current_limit_a: PioF64View,
1503    pub has_current_limit: bool,
1504    pub active_power_available_w: f64,
1505    pub has_active_power_available: bool,
1506    pub active_power_min_w: PioF64View,
1507    pub has_active_power_min: bool,
1508    pub active_power_max_w: PioF64View,
1509    pub has_active_power_max: bool,
1510    pub reactive_power_min_var: PioF64View,
1511    pub has_reactive_power_min: bool,
1512    pub reactive_power_max_var: PioF64View,
1513    pub has_reactive_power_max: bool,
1514    pub control_profile: PioStringView,
1515    pub has_control_profile: bool,
1516    pub voltage_aggregation: PioStringView,
1517    pub has_voltage_aggregation: bool,
1518}
1519
1520/// One inverter control profile.
1521#[repr(C)]
1522#[derive(Clone, Copy)]
1523pub struct PioControlProfileView {
1524    pub name: PioStringView,
1525    pub has_power_factor: bool,
1526    pub power_factor: f64,
1527    pub has_volt_var: bool,
1528    pub volt_var_voltage_reference: PioStringView,
1529    pub has_volt_var_voltage_reference: bool,
1530    pub volt_var_breakpoints: PioF64View,
1531    pub volt_var_reactive_power_limits: PioF64View,
1532    pub volt_var_reactive_power_unit: PioStringView,
1533    pub has_volt_var_reactive_power_unit: bool,
1534    pub volt_var_reactive_power_reference: PioStringView,
1535    pub has_volt_var_reactive_power_reference: bool,
1536    pub volt_var_active_power_min_for_reactive_power_w: f64,
1537    pub has_volt_var_active_power_min_for_reactive_power: bool,
1538    pub volt_var_active_power_min_for_max_reactive_power_w: f64,
1539    pub has_volt_var_active_power_min_for_max_reactive_power: bool,
1540    pub has_volt_watt: bool,
1541    pub volt_watt_voltage_reference: PioStringView,
1542    pub has_volt_watt_voltage_reference: bool,
1543    pub volt_watt_breakpoints: PioF64View,
1544    pub volt_watt_active_power_limits: PioF64View,
1545    pub volt_watt_active_power_unit: PioStringView,
1546    pub has_volt_watt_active_power_unit: bool,
1547    pub volt_watt_active_power_reference: PioStringView,
1548    pub has_volt_watt_active_power_reference: bool,
1549}
1550
1551/// One multiconductor shunt.
1552#[repr(C)]
1553#[derive(Clone, Copy)]
1554pub struct PioMulticonductorShuntView {
1555    pub name: PioStringView,
1556    pub bus: PioStringView,
1557    pub terminal_map_count: usize,
1558    pub conductance_matrix_row_count: usize,
1559    pub susceptance_matrix_row_count: usize,
1560}
1561
1562/// One multiconductor capacitor.
1563#[repr(C)]
1564#[derive(Clone, Copy)]
1565pub struct PioMulticonductorCapacitorView {
1566    pub name: PioStringView,
1567    pub bus: PioStringView,
1568    pub terminal_map_count: usize,
1569    pub configuration: PioStringView,
1570    pub rated_reactive_power_var: f64,
1571    pub nominal_voltage_v: f64,
1572}
1573
1574/// One multiconductor voltage source.
1575#[repr(C)]
1576#[derive(Clone, Copy)]
1577pub struct PioVoltageSourceView {
1578    pub name: PioStringView,
1579    pub bus: PioStringView,
1580    pub terminal_map_count: usize,
1581    pub voltage_magnitude_v: PioF64View,
1582    pub voltage_angle_rad: PioF64View,
1583    /// Dollars/kWh in phase order, excluding neutral terminals.
1584    pub energy_cost_rate_per_kwh: PioF64View,
1585    pub has_energy_cost_rate: bool,
1586}
1587
1588/// One source object retained without a typed PowerIO representation.
1589#[repr(C)]
1590#[derive(Clone, Copy)]
1591pub struct PioMulticonductorUntypedObjectView {
1592    pub class_name: PioStringView,
1593    pub name: PioStringView,
1594    pub property_count: usize,
1595}
1596
1597/// One property of an untyped source object.
1598#[repr(C)]
1599#[derive(Clone, Copy)]
1600pub struct PioMulticonductorUntypedPropertyView {
1601    pub name: PioStringView,
1602    pub has_name: bool,
1603    pub value: PioStringView,
1604}
1605
1606/// One retained source command.
1607#[repr(C)]
1608#[derive(Clone, Copy)]
1609pub struct PioMulticonductorCommandView {
1610    pub verb: PioStringView,
1611    pub args: PioStringView,
1612}
1613
1614/// One bus boundary specification in a DC power flow instance.
1615#[repr(C)]
1616#[derive(Clone, Copy)]
1617pub struct PioDcBusSpecificationView {
1618    pub bus_id: usize,
1619    pub kind: PioStringView,
1620    pub net_active_power_mw: f64,
1621    pub voltage_angle_degrees: f64,
1622}
1623
1624/// One bus boundary specification in an AC power flow instance.
1625#[repr(C)]
1626#[derive(Clone, Copy)]
1627pub struct PioAcBusSpecificationView {
1628    pub bus_id: usize,
1629    pub kind: PioStringView,
1630    pub net_active_power_mw: f64,
1631    pub net_reactive_power_mvar: f64,
1632    pub voltage_magnitude_pu: f64,
1633    pub voltage_angle_degrees: f64,
1634}
1635
1636/// Shape and conventions of one prepared DC OPF calculation.
1637#[repr(C)]
1638#[derive(Clone, Copy)]
1639pub struct PioDcOpfPreparationView {
1640    pub name: PioStringView,
1641    pub bus_count: usize,
1642    pub generator_count: usize,
1643    pub branch_count: usize,
1644    pub source_generator_count: usize,
1645    pub source_branch_count: usize,
1646    pub base_mva: f64,
1647    pub units: PioStringView,
1648    pub branch_susceptance_formula: PioStringView,
1649    pub objective: PioStringView,
1650    pub skip_zero_impedance: bool,
1651    pub synthesize_unrated_limits: bool,
1652    pub correct_angle_difference_bounds: bool,
1653    pub reference_bus_count: usize,
1654    pub skipped_zero_impedance_count: usize,
1655}
1656
1657/// One dense bus row in a DC OPF preparation.
1658#[repr(C)]
1659#[derive(Clone, Copy)]
1660pub struct PioDcOpfBusView {
1661    pub bus_id: usize,
1662    pub analysis_row: usize,
1663    pub source_row: usize,
1664    pub has_source_row: bool,
1665    pub active_power_demand: f64,
1666    pub shunt_conductance: f64,
1667    pub phase_shift_injection: f64,
1668}
1669
1670/// One generator row in a DC OPF preparation.
1671#[repr(C)]
1672#[derive(Clone, Copy)]
1673pub struct PioDcOpfGeneratorView {
1674    pub component_id: PioStringView,
1675    pub bus_index: usize,
1676    pub analysis_row: usize,
1677    pub source_row: usize,
1678    pub has_source_row: bool,
1679    pub quadratic_cost: f64,
1680    pub linear_cost: f64,
1681    pub constant_cost: f64,
1682    pub has_piecewise_linear_cost: bool,
1683    pub piecewise_linear_power: PioF64View,
1684    pub piecewise_linear_value: PioF64View,
1685    pub active_power_max: f64,
1686    pub active_power_min: f64,
1687    pub capability_active: bool,
1688}
1689
1690/// One active branch row in a DC OPF preparation.
1691#[repr(C)]
1692#[derive(Clone, Copy)]
1693pub struct PioDcOpfBranchView {
1694    pub component_id: PioStringView,
1695    pub from_bus_index: usize,
1696    pub to_bus_index: usize,
1697    pub susceptance_magnitude: f64,
1698    pub phase_shift_radians: f64,
1699    pub active_power_max: f64,
1700    pub angle_difference_min_radians: f64,
1701    pub angle_difference_max_radians: f64,
1702    pub analysis_row: usize,
1703    pub source_kind: PioStringView,
1704    pub source_row: usize,
1705    pub winding: usize,
1706    pub has_winding: bool,
1707    pub thermal_limit_active: bool,
1708    pub angle_bound_active: bool,
1709}
1710
1711/// Shape and conventions of one prepared AC OPF calculation.
1712#[repr(C)]
1713#[derive(Clone, Copy)]
1714pub struct PioAcOpfPreparationView {
1715    pub name: PioStringView,
1716    pub bus_count: usize,
1717    pub generator_count: usize,
1718    pub storage_count: usize,
1719    pub branch_count: usize,
1720    pub source_generator_count: usize,
1721    pub source_branch_count: usize,
1722    pub base_mva: f64,
1723    pub units: PioStringView,
1724    pub objective: PioStringView,
1725    pub skip_zero_impedance: bool,
1726    pub synthesize_unrated_limits: bool,
1727    pub correct_angle_difference_bounds: bool,
1728    pub reference_bus_count: usize,
1729    pub skipped_zero_impedance_count: usize,
1730}
1731
1732/// One dense bus row in an AC OPF preparation.
1733#[repr(C)]
1734#[derive(Clone, Copy)]
1735pub struct PioAcOpfBusView {
1736    pub bus_id: usize,
1737    pub analysis_row: usize,
1738    pub source_row: usize,
1739    pub has_source_row: bool,
1740    pub active_power_demand: f64,
1741    pub reactive_power_demand: f64,
1742    pub shunt_conductance: f64,
1743    pub shunt_susceptance: f64,
1744    pub voltage_magnitude_min_pu: f64,
1745    pub voltage_magnitude_max_pu: f64,
1746    pub initial_voltage_magnitude_pu: f64,
1747    pub initial_voltage_angle_radians: f64,
1748    pub voltage_bound_active: bool,
1749}
1750
1751/// One generator row in an AC OPF preparation.
1752#[repr(C)]
1753#[derive(Clone, Copy)]
1754pub struct PioAcOpfGeneratorView {
1755    pub component_id: PioStringView,
1756    pub bus_index: usize,
1757    pub analysis_row: usize,
1758    pub source_row: usize,
1759    pub has_source_row: bool,
1760    pub quadratic_cost: f64,
1761    pub linear_cost: f64,
1762    pub constant_cost: f64,
1763    pub has_piecewise_linear_cost: bool,
1764    pub piecewise_linear_power: PioF64View,
1765    pub piecewise_linear_value: PioF64View,
1766    pub active_power_max: f64,
1767    pub active_power_min: f64,
1768    pub reactive_power_max: f64,
1769    pub reactive_power_min: f64,
1770    pub initial_active_power: f64,
1771    pub initial_reactive_power: f64,
1772    pub voltage_magnitude_setpoint_pu: f64,
1773    pub capability_active: bool,
1774}
1775
1776/// One storage row in an AC OPF preparation.
1777#[repr(C)]
1778#[derive(Clone, Copy)]
1779pub struct PioAcOpfStorageView {
1780    pub component_id: PioStringView,
1781    pub bus_index: usize,
1782    pub source_row: usize,
1783    pub initial_active_power: f64,
1784    pub initial_reactive_power: f64,
1785    pub energy: f64,
1786    pub energy_rating: f64,
1787    pub charge_rating: f64,
1788    pub discharge_rating: f64,
1789    pub charge_efficiency: f64,
1790    pub discharge_efficiency: f64,
1791    pub apparent_power_max: f64,
1792    pub reactive_power_min: f64,
1793    pub reactive_power_max: f64,
1794    pub resistance_pu: f64,
1795    pub reactance_pu: f64,
1796    pub active_power_loss: f64,
1797    pub reactive_power_loss: f64,
1798    pub in_service: bool,
1799}
1800
1801/// One active branch row in an AC OPF preparation.
1802#[repr(C)]
1803#[derive(Clone, Copy)]
1804pub struct PioAcOpfBranchView {
1805    pub component_id: PioStringView,
1806    pub from_bus_index: usize,
1807    pub to_bus_index: usize,
1808    pub series_conductance: f64,
1809    pub series_susceptance: f64,
1810    pub from_conductance: f64,
1811    pub from_susceptance: f64,
1812    pub to_conductance: f64,
1813    pub to_susceptance: f64,
1814    pub tap_ratio: f64,
1815    pub phase_shift_radians: f64,
1816    pub apparent_power_max: f64,
1817    pub angle_difference_min_radians: f64,
1818    pub angle_difference_max_radians: f64,
1819    pub analysis_row: usize,
1820    pub source_kind: PioStringView,
1821    pub source_row: usize,
1822    pub winding: usize,
1823    pub has_winding: bool,
1824    pub thermal_limit_active: bool,
1825    pub angle_bound_active: bool,
1826}
1827
1828/// One typed objective term.
1829#[repr(C)]
1830#[derive(Clone, Copy)]
1831pub struct PioObjectiveTermView {
1832    pub kind: PioStringView,
1833}
1834
1835/// One active constraint family and its element selection.
1836#[repr(C)]
1837#[derive(Clone, Copy)]
1838pub struct PioActiveConstraintView {
1839    pub family: PioStringView,
1840    pub selection: PioStringView,
1841    pub identity_count: usize,
1842}
1843
1844/// One prescribed multiconductor load terminal power.
1845#[repr(C)]
1846#[derive(Clone, Copy)]
1847pub struct PioPrescribedTerminalPowerView {
1848    pub load: PioStringView,
1849    pub terminal_count: usize,
1850    pub voltage_model: PioStringView,
1851}
1852
1853/// One terminal of a prescribed multiconductor load.
1854#[repr(C)]
1855#[derive(Clone, Copy)]
1856pub struct PioTerminalPowerView {
1857    pub terminal: PioStringView,
1858    pub active_power_w: f64,
1859    pub reactive_power_var: f64,
1860    pub nominal_voltage_v: f64,
1861    pub has_nominal_voltage: bool,
1862    pub active_impedance_fraction: f64,
1863    pub active_current_fraction: f64,
1864    pub active_power_fraction: f64,
1865    pub reactive_impedance_fraction: f64,
1866    pub reactive_current_fraction: f64,
1867    pub reactive_power_fraction: f64,
1868    pub active_power_exponent: f64,
1869    pub reactive_power_exponent: f64,
1870}
1871
1872/// One prescribed multiconductor source terminal voltage.
1873#[repr(C)]
1874#[derive(Clone, Copy)]
1875pub struct PioPrescribedSourceVoltageView {
1876    pub source: PioStringView,
1877    pub terminal_count: usize,
1878}
1879
1880/// One terminal of a prescribed multiconductor source.
1881#[repr(C)]
1882#[derive(Clone, Copy)]
1883pub struct PioTerminalVoltageView {
1884    pub terminal: PioStringView,
1885    pub magnitude_v: f64,
1886    pub angle_radians: f64,
1887}
1888
1889/// One isolated multiconductor terminal.
1890#[repr(C)]
1891#[derive(Clone, Copy)]
1892pub struct PioIsolatedTerminalView {
1893    pub bus: PioStringView,
1894    pub terminal: PioStringView,
1895}
1896
1897/// One active multiconductor equipment control.
1898#[repr(C)]
1899#[derive(Clone, Copy)]
1900pub struct PioActiveControlView {
1901    pub kind: PioStringView,
1902    pub component_id: PioStringView,
1903}
1904
1905/// Set sizes and time horizon of one AC SCUC instance.
1906#[repr(C)]
1907#[derive(Clone, Copy)]
1908pub struct PioScucDimensionsView {
1909    pub period_count: usize,
1910    pub device_count: usize,
1911    pub producer_count: usize,
1912    pub consumer_count: usize,
1913    pub shunt_count: usize,
1914    pub branch_switching_cost_count: usize,
1915    pub transformer_control_count: usize,
1916    pub active_reserve_zone_count: usize,
1917    pub reactive_reserve_zone_count: usize,
1918    pub contingency_count: usize,
1919}
1920
1921/// Required SCUC violation costs.
1922#[repr(C)]
1923#[derive(Clone, Copy)]
1924pub struct PioScucViolationCostView {
1925    pub active_power_balance: f64,
1926    pub reactive_power_balance: f64,
1927    pub branch_thermal_limit: f64,
1928    pub energy_requirement: f64,
1929}
1930
1931/// Borrowed stable component identity.
1932#[repr(C)]
1933#[derive(Clone, Copy)]
1934pub struct PioComponentIdView {
1935    pub component_type: PioStringView,
1936    pub local_id: PioStringView,
1937}
1938
1939/// Active power ramp limits for one SCUC device, in per unit per hour.
1940#[repr(C)]
1941#[derive(Clone, Copy)]
1942pub struct PioScucRampLimitsView {
1943    pub up_pu_per_hour: f64,
1944    pub down_pu_per_hour: f64,
1945    pub startup_pu_per_hour: f64,
1946    pub shutdown_pu_per_hour: f64,
1947}
1948
1949/// Reserve quantity limits for one SCUC device.
1950#[repr(C)]
1951#[derive(Clone, Copy)]
1952pub struct PioScucReserveLimitsView {
1953    pub regulation_up_pu: f64,
1954    pub regulation_down_pu: f64,
1955    pub synchronized_pu: f64,
1956    pub nonsynchronized_pu: f64,
1957    pub ramping_up_online_pu: f64,
1958    pub ramping_down_online_pu: f64,
1959    pub ramping_up_offline_pu: f64,
1960    pub ramping_down_offline_pu: f64,
1961}
1962
1963/// Initial commitment durations for one SCUC device.
1964#[repr(C)]
1965#[derive(Clone, Copy)]
1966pub struct PioScucInitialCommitmentView {
1967    pub accumulated_up_time_hours: f64,
1968    pub accumulated_down_time_hours: f64,
1969}
1970
1971/// Additional active and reactive power capability relation for one device.
1972#[repr(C)]
1973#[derive(Clone, Copy)]
1974pub struct PioScucReactiveCapabilityView {
1975    pub kind: PioStringView,
1976    pub reactive_power_at_zero_active_power_pu: f64,
1977    pub reactive_power_at_zero_active_power_min_pu: f64,
1978    pub reactive_power_at_zero_active_power_max_pu: f64,
1979    pub slope: f64,
1980    pub slope_min: f64,
1981    pub slope_max: f64,
1982}
1983
1984/// One SCUC producer or consumer.
1985#[repr(C)]
1986#[derive(Clone, Copy)]
1987pub struct PioScucDeviceView {
1988    pub id: PioComponentIdView,
1989    pub kind: PioStringView,
1990    pub initial_on_status: bool,
1991    pub on_cost: f64,
1992    pub startup_cost: f64,
1993    pub shutdown_cost: f64,
1994    pub minimum_up_time_hours: f64,
1995    pub minimum_down_time_hours: f64,
1996    pub ramp_limits: PioScucRampLimitsView,
1997    pub reserve_limits: PioScucReserveLimitsView,
1998    pub initial_commitment: PioScucInitialCommitmentView,
1999    pub reactive_capability: PioScucReactiveCapabilityView,
2000    pub period_count: usize,
2001    pub startup_cost_adjustment_count: usize,
2002    pub startup_limit_count: usize,
2003    pub energy_upper_bound_count: usize,
2004    pub energy_lower_bound_count: usize,
2005}
2006
2007/// One downtime dependent startup cost adjustment.
2008#[repr(C)]
2009#[derive(Clone, Copy)]
2010pub struct PioScucStartupCostAdjustmentView {
2011    pub cost: f64,
2012    pub maximum_down_time_hours: f64,
2013}
2014
2015/// One SCUC device period.
2016#[repr(C)]
2017#[derive(Clone, Copy)]
2018pub struct PioScucDevicePeriodView {
2019    pub on_status_min: bool,
2020    pub on_status_max: bool,
2021    pub active_power_min_pu: f64,
2022    pub active_power_max_pu: f64,
2023    pub reactive_power_min_pu: f64,
2024    pub reactive_power_max_pu: f64,
2025    pub energy_cost_block_count: usize,
2026    pub reserve_costs: PioScucReserveCostsView,
2027}
2028
2029/// One limit on the number of device startups during a time window.
2030#[repr(C)]
2031#[derive(Clone, Copy)]
2032pub struct PioScucStartupLimitView {
2033    pub start_time_hours: f64,
2034    pub end_time_hours: f64,
2035    pub maximum_startups: u64,
2036}
2037
2038/// One energy requirement over a time window, in per unit as defined by GOC3.
2039#[repr(C)]
2040#[derive(Clone, Copy)]
2041pub struct PioScucEnergyRequirementView {
2042    pub start_time_hours: f64,
2043    pub end_time_hours: f64,
2044    pub energy_pu: f64,
2045}
2046
2047/// One piecewise linear active energy cost block.
2048#[repr(C)]
2049#[derive(Clone, Copy)]
2050pub struct PioScucEnergyCostBlockView {
2051    pub marginal_cost: f64,
2052    pub block_size_pu: f64,
2053}
2054
2055/// Reserve costs for one device and one interval, in $/(p.u. h).
2056#[repr(C)]
2057#[derive(Clone, Copy)]
2058pub struct PioScucReserveCostsView {
2059    pub regulation_up: f64,
2060    pub regulation_down: f64,
2061    pub synchronized: f64,
2062    pub nonsynchronized: f64,
2063    pub ramping_up_online: f64,
2064    pub ramping_down_online: f64,
2065    pub ramping_up_offline: f64,
2066    pub ramping_down_offline: f64,
2067    pub reactive_up: f64,
2068    pub reactive_down: f64,
2069}
2070
2071/// Discrete step limits for one SCUC shunt.
2072#[repr(C)]
2073#[derive(Clone, Copy)]
2074pub struct PioScucShuntView {
2075    pub id: PioComponentIdView,
2076    pub conductance_per_step_pu: f64,
2077    pub susceptance_per_step_pu: f64,
2078    pub step_min: i64,
2079    pub step_max: i64,
2080    pub initial_step: i64,
2081}
2082
2083/// Connection and disconnection costs for one switchable AC branch.
2084#[repr(C)]
2085#[derive(Clone, Copy)]
2086pub struct PioScucBranchSwitchingCostView {
2087    pub id: PioComponentIdView,
2088    pub connection_cost: f64,
2089    pub disconnection_cost: f64,
2090}
2091
2092/// Tap ratio and phase shift bounds for one transformer.
2093#[repr(C)]
2094#[derive(Clone, Copy)]
2095pub struct PioScucTransformerControlView {
2096    pub id: PioComponentIdView,
2097    pub tap_ratio_min: f64,
2098    pub tap_ratio_max: f64,
2099    pub phase_shift_min_radians: f64,
2100    pub phase_shift_max_radians: f64,
2101}
2102
2103/// One active power reserve zone.
2104#[repr(C)]
2105#[derive(Clone, Copy)]
2106pub struct PioScucActiveReserveZoneView {
2107    pub id: PioComponentIdView,
2108    pub regulation_up_requirement_fraction: f64,
2109    pub regulation_down_requirement_fraction: f64,
2110    pub synchronized_requirement_fraction: f64,
2111    pub nonsynchronized_requirement_fraction: f64,
2112    pub regulation_up_violation_cost: f64,
2113    pub regulation_down_violation_cost: f64,
2114    pub synchronized_violation_cost: f64,
2115    pub nonsynchronized_violation_cost: f64,
2116    pub ramping_up_violation_cost: f64,
2117    pub ramping_down_violation_cost: f64,
2118    pub period_count: usize,
2119    pub bus_count: usize,
2120}
2121
2122/// One period of an active power reserve zone.
2123#[repr(C)]
2124#[derive(Clone, Copy)]
2125pub struct PioScucActiveReservePeriodView {
2126    pub ramping_up_requirement_pu: f64,
2127    pub ramping_down_requirement_pu: f64,
2128}
2129
2130/// One reactive power reserve zone.
2131#[repr(C)]
2132#[derive(Clone, Copy)]
2133pub struct PioScucReactiveReserveZoneView {
2134    pub id: PioComponentIdView,
2135    pub reactive_up_violation_cost: f64,
2136    pub reactive_down_violation_cost: f64,
2137    pub period_count: usize,
2138    pub bus_count: usize,
2139}
2140
2141/// One period of a reactive power reserve zone.
2142#[repr(C)]
2143#[derive(Clone, Copy)]
2144pub struct PioScucReactiveReservePeriodView {
2145    pub reactive_up_requirement_pu: f64,
2146    pub reactive_down_requirement_pu: f64,
2147}
2148
2149/// One named SCUC contingency.
2150#[repr(C)]
2151#[derive(Clone, Copy)]
2152pub struct PioScucContingencyView {
2153    pub id: PioComponentIdView,
2154    pub component_count: usize,
2155}
2156
2157/// One component removed by a SCUC contingency.
2158#[repr(C)]
2159#[derive(Clone, Copy)]
2160pub struct PioScucContingencyComponentView {
2161    pub id: PioComponentIdView,
2162}
2163
2164// ---- shared handle machinery ----------------------------------------------
2165
2166/// Every handle payload must be shareable across threads: the header lets
2167/// callers move and retain handles from any thread.
2168const fn assert_send_sync<T: Send + Sync>() {}
2169
2170#[repr(transparent)]
2171struct HandleBox<T> {
2172    inner: Arc<T>,
2173}
2174
2175fn handle_new<T>(value: T) -> *mut HandleBox<T> {
2176    Box::into_raw(Box::new(HandleBox {
2177        inner: Arc::new(value),
2178    }))
2179}
2180
2181fn handle_from_arc<T>(inner: Arc<T>) -> *mut HandleBox<T> {
2182    Box::into_raw(Box::new(HandleBox { inner }))
2183}
2184
2185unsafe fn handle_get<'a, T>(raw: *const HandleBox<T>) -> Option<&'a T> {
2186    unsafe { raw.as_ref() }.map(|handle| handle.inner.as_ref())
2187}
2188
2189unsafe fn handle_arc<T>(raw: *const HandleBox<T>) -> Option<Arc<T>> {
2190    unsafe { raw.as_ref() }.map(|handle| Arc::clone(&handle.inner))
2191}
2192
2193unsafe fn handle_retain<T>(raw: *const HandleBox<T>) -> *mut HandleBox<T> {
2194    unsafe { handle_arc(raw) }.map_or(std::ptr::null_mut(), handle_from_arc)
2195}
2196
2197unsafe fn handle_release<T>(raw: *mut HandleBox<T>) {
2198    if !raw.is_null() {
2199        drop(unsafe { Box::from_raw(raw) });
2200    }
2201}
2202
2203macro_rules! opaque_handle {
2204    ($(#[$doc:meta])* $name:ident, $inner:ty) => {
2205        $(#[$doc])*
2206        #[repr(transparent)]
2207        pub struct $name(HandleBox<$inner>);
2208        const _: () = assert_send_sync::<$inner>();
2209
2210        #[allow(dead_code)]
2211        impl $name {
2212            fn new_raw(value: $inner) -> *mut Self {
2213                handle_new(value).cast()
2214            }
2215
2216            fn from_arc(inner: Arc<$inner>) -> *mut Self {
2217                handle_from_arc(inner).cast()
2218            }
2219
2220            unsafe fn get<'a>(raw: *const Self) -> Option<&'a $inner> {
2221                unsafe { handle_get(raw.cast()) }
2222            }
2223
2224            unsafe fn arc(raw: *const Self) -> Option<Arc<$inner>> {
2225                unsafe { handle_arc(raw.cast()) }
2226            }
2227
2228            unsafe fn retain_raw(raw: *const Self) -> *mut Self {
2229                unsafe { handle_retain(raw.cast::<HandleBox<$inner>>()) }.cast::<Self>()
2230            }
2231
2232            unsafe fn release_raw(raw: *mut Self) {
2233                unsafe { handle_release(raw.cast::<HandleBox<$inner>>()) }
2234            }
2235        }
2236    };
2237}
2238
2239// ---- errors and diagnostics ------------------------------------------------
2240
2241struct ErrorInner {
2242    code: String,
2243    message: String,
2244    diagnostics: Arc<DiagnosticsInner>,
2245}
2246
2247struct DiagnosticsInner {
2248    owner: DiagnosticsOwner,
2249}
2250
2251enum DiagnosticsOwner {
2252    Owned(Vec<Diagnostic>),
2253    Module(Arc<ModuleInner>),
2254}
2255
2256impl DiagnosticsInner {
2257    fn records(&self) -> &[Diagnostic] {
2258        match &self.owner {
2259            DiagnosticsOwner::Owned(records) => records,
2260            DiagnosticsOwner::Module(module) => module.module.diagnostics(),
2261        }
2262    }
2263}
2264
2265opaque_handle!(
2266    /// Structured operation failure.
2267    PioError,
2268    ErrorInner
2269);
2270opaque_handle!(
2271    /// Immutable diagnostic list.
2272    PioDiagnostics,
2273    DiagnosticsInner
2274);
2275
2276fn boundary_diagnostic(
2277    info: &'static powerio_core::DiagnosticInfo,
2278    message: impl Into<String>,
2279) -> Diagnostic {
2280    Diagnostic::of(info, message)
2281}
2282
2283fn error_from_diagnostics(message: String, mut records: Vec<Diagnostic>) -> *mut PioError {
2284    if records.is_empty() {
2285        records.push(boundary_diagnostic(
2286            &codes::BIND_CAPI_UNCODED_FAILURE,
2287            message.clone(),
2288        ));
2289    }
2290    let code = records[0].code().to_owned();
2291    PioError::new_raw(ErrorInner {
2292        code,
2293        message,
2294        diagnostics: Arc::new(DiagnosticsInner {
2295            owner: DiagnosticsOwner::Owned(records),
2296        }),
2297    })
2298}
2299
2300fn error_from_core(error: &powerio_core::Error) -> *mut PioError {
2301    error_from_diagnostics(error.to_string(), error.diagnostics().to_vec())
2302}
2303
2304fn error_from_tx(error: &powerio_tx::Error) -> *mut PioError {
2305    let diagnostic = Diagnostic::of(error.code(), error.to_string());
2306    error_from_diagnostics(error.to_string(), vec![diagnostic])
2307}
2308
2309fn error_from_matrix(error: &powerio_matrix::Error) -> *mut PioError {
2310    let diagnostic = Diagnostic::of(error.code(), error.to_string());
2311    error_from_diagnostics(error.to_string(), vec![diagnostic])
2312}
2313
2314fn boundary_error(
2315    info: &'static powerio_core::DiagnosticInfo,
2316    message: impl Into<String>,
2317) -> *mut PioError {
2318    let diagnostic = boundary_diagnostic(info, message);
2319    error_from_diagnostics(
2320        powerio_core::render_diagnostic(&diagnostic),
2321        vec![diagnostic],
2322    )
2323}
2324
2325unsafe fn store_error(slot: *mut *mut PioError, error: *mut PioError) {
2326    if slot.is_null() {
2327        unsafe { PioError::release_raw(error) };
2328    } else {
2329        unsafe { *slot = error };
2330    }
2331}
2332
2333unsafe fn entry<R>(
2334    error: *mut *mut PioError,
2335    fallback: R,
2336    operation: impl FnOnce() -> Result<R, *mut PioError>,
2337) -> R {
2338    if !error.is_null() {
2339        unsafe { *error = std::ptr::null_mut() };
2340    }
2341    match catch_unwind(AssertUnwindSafe(operation)) {
2342        Ok(Ok(value)) => value,
2343        Ok(Err(failure)) => {
2344            unsafe { store_error(error, failure) };
2345            fallback
2346        }
2347        Err(_) => {
2348            let failure = boundary_error(
2349                &codes::BIND_CAPI_PANIC,
2350                "the operation panicked and made no C-visible change",
2351            );
2352            unsafe { store_error(error, failure) };
2353            fallback
2354        }
2355    }
2356}
2357
2358unsafe fn input_bytes<'a>(
2359    data: *const u8,
2360    len: usize,
2361    argument: &str,
2362) -> Result<&'a [u8], *mut PioError> {
2363    if data.is_null() {
2364        if len == 0 {
2365            return Ok(&[]);
2366        }
2367        return Err(boundary_error(
2368            &codes::BIND_CAPI_NULL_ARGUMENT,
2369            format!("{argument} is NULL with a nonzero length"),
2370        ));
2371    }
2372    Ok(unsafe { std::slice::from_raw_parts(data, len) })
2373}
2374
2375unsafe fn input_str<'a>(
2376    data: *const c_char,
2377    len: usize,
2378    argument: &str,
2379) -> Result<&'a str, *mut PioError> {
2380    let bytes = unsafe { input_bytes(data.cast(), len, argument) }?;
2381    std::str::from_utf8(bytes).map_err(|_| {
2382        boundary_error(
2383            &codes::BIND_CAPI_INVALID_UTF8,
2384            format!("{argument} is not valid UTF-8"),
2385        )
2386    })
2387}
2388
2389unsafe fn required_str<'a>(
2390    data: *const c_char,
2391    len: usize,
2392    argument: &str,
2393) -> Result<&'a str, *mut PioError> {
2394    if data.is_null() || len == 0 {
2395        return Err(boundary_error(
2396            &codes::BIND_CAPI_NULL_ARGUMENT,
2397            format!("{argument} is required"),
2398        ));
2399    }
2400    unsafe { input_str(data, len, argument) }
2401}
2402
2403unsafe fn optional_str<'a>(
2404    data: *const c_char,
2405    len: usize,
2406    argument: &str,
2407) -> Result<Option<&'a str>, *mut PioError> {
2408    if data.is_null() {
2409        if len == 0 {
2410            return Ok(None);
2411        }
2412        return Err(boundary_error(
2413            &codes::BIND_CAPI_NULL_ARGUMENT,
2414            format!("{argument} is NULL with a nonzero length"),
2415        ));
2416    }
2417    unsafe { input_str(data, len, argument) }.map(Some)
2418}
2419
2420/// Return the ABI number compiled into this library.
2421#[unsafe(no_mangle)]
2422pub extern "C" fn pio_abi_version() -> u32 {
2423    PIO_ABI_VERSION
2424}
2425
2426/// Return the PowerIO crate version.
2427#[unsafe(no_mangle)]
2428pub extern "C" fn pio_version() -> PioStringView {
2429    PioStringView::new(powerio::VERSION)
2430}
2431
2432/// The failure's stable diagnostic code.
2433#[unsafe(no_mangle)]
2434pub unsafe extern "C" fn pio_error_code(error: *const PioError) -> PioStringView {
2435    unsafe { PioError::get(error) }.map_or(PioStringView::EMPTY, |error| {
2436        PioStringView::new(&error.code)
2437    })
2438}
2439
2440/// The rendered failure message.
2441#[unsafe(no_mangle)]
2442pub unsafe extern "C" fn pio_error_message(error: *const PioError) -> PioStringView {
2443    unsafe { PioError::get(error) }.map_or(PioStringView::EMPTY, |error| {
2444        PioStringView::new(&error.message)
2445    })
2446}
2447
2448/// The structured diagnostics that caused the failure.
2449#[unsafe(no_mangle)]
2450pub unsafe extern "C" fn pio_error_diagnostics(error: *const PioError) -> *mut PioDiagnostics {
2451    unsafe { PioError::get(error) }.map_or(std::ptr::null_mut(), |error| {
2452        PioDiagnostics::from_arc(Arc::clone(&error.diagnostics))
2453    })
2454}
2455
2456#[unsafe(no_mangle)]
2457pub unsafe extern "C" fn pio_error_retain(error: *const PioError) -> *mut PioError {
2458    unsafe { PioError::retain_raw(error) }
2459}
2460
2461#[unsafe(no_mangle)]
2462pub unsafe extern "C" fn pio_error_release(error: *mut PioError) {
2463    unsafe { PioError::release_raw(error) };
2464}
2465
2466#[unsafe(no_mangle)]
2467pub unsafe extern "C" fn pio_diagnostics_len(diagnostics: *const PioDiagnostics) -> usize {
2468    unsafe { PioDiagnostics::get(diagnostics) }.map_or(0, |values| values.records().len())
2469}
2470
2471#[unsafe(no_mangle)]
2472pub unsafe extern "C" fn pio_diagnostic_code(
2473    diagnostics: *const PioDiagnostics,
2474    index: usize,
2475) -> PioStringView {
2476    unsafe { PioDiagnostics::get(diagnostics) }
2477        .and_then(|values| values.records().get(index))
2478        .map_or(PioStringView::EMPTY, |record| {
2479            PioStringView::new(record.code())
2480        })
2481}
2482
2483#[unsafe(no_mangle)]
2484pub unsafe extern "C" fn pio_diagnostic_severity(
2485    diagnostics: *const PioDiagnostics,
2486    index: usize,
2487) -> PioStringView {
2488    unsafe { PioDiagnostics::get(diagnostics) }
2489        .and_then(|values| values.records().get(index))
2490        .map_or(PioStringView::EMPTY, |record| {
2491            PioStringView::new(record.severity().as_str())
2492        })
2493}
2494
2495#[unsafe(no_mangle)]
2496pub unsafe extern "C" fn pio_diagnostic_message(
2497    diagnostics: *const PioDiagnostics,
2498    index: usize,
2499) -> PioStringView {
2500    unsafe { PioDiagnostics::get(diagnostics) }
2501        .and_then(|values| values.records().get(index))
2502        .map_or(PioStringView::EMPTY, |record| {
2503            PioStringView::new(record.message())
2504        })
2505}
2506
2507/// Whether this diagnostic has a durable identity.
2508#[unsafe(no_mangle)]
2509pub unsafe extern "C" fn pio_diagnostic_has_id(
2510    diagnostics: *const PioDiagnostics,
2511    index: usize,
2512) -> bool {
2513    unsafe { PioDiagnostics::get(diagnostics) }
2514        .and_then(|values| values.records().get(index))
2515        .is_some_and(|record| record.id().is_some())
2516}
2517
2518/// Borrow this diagnostic's durable identity, or an empty view when absent.
2519#[unsafe(no_mangle)]
2520pub unsafe extern "C" fn pio_diagnostic_id(
2521    diagnostics: *const PioDiagnostics,
2522    index: usize,
2523) -> PioStringView {
2524    unsafe { PioDiagnostics::get(diagnostics) }
2525        .and_then(|values| values.records().get(index))
2526        .and_then(Diagnostic::id)
2527        .map_or(PioStringView::EMPTY, |id| PioStringView::new(id.as_str()))
2528}
2529
2530/// Whether this diagnostic names a value element.
2531#[unsafe(no_mangle)]
2532pub unsafe extern "C" fn pio_diagnostic_has_target(
2533    diagnostics: *const PioDiagnostics,
2534    index: usize,
2535) -> bool {
2536    unsafe { PioDiagnostics::get(diagnostics) }
2537        .and_then(|values| values.records().get(index))
2538        .is_some_and(|record| record.target().is_some())
2539}
2540
2541/// Borrow this diagnostic's value element locator, or an empty view when absent.
2542#[unsafe(no_mangle)]
2543pub unsafe extern "C" fn pio_diagnostic_target(
2544    diagnostics: *const PioDiagnostics,
2545    index: usize,
2546) -> PioStringView {
2547    unsafe { PioDiagnostics::get(diagnostics) }
2548        .and_then(|values| values.records().get(index))
2549        .and_then(Diagnostic::target)
2550        .map_or(PioStringView::EMPTY, PioStringView::new)
2551}
2552
2553/// Whether this diagnostic carries a suggested action.
2554#[unsafe(no_mangle)]
2555pub unsafe extern "C" fn pio_diagnostic_has_suggested_action(
2556    diagnostics: *const PioDiagnostics,
2557    index: usize,
2558) -> bool {
2559    unsafe { PioDiagnostics::get(diagnostics) }
2560        .and_then(|values| values.records().get(index))
2561        .is_some_and(|record| record.suggested_action().is_some())
2562}
2563
2564/// Borrow this diagnostic's suggested action, or an empty view when absent.
2565#[unsafe(no_mangle)]
2566pub unsafe extern "C" fn pio_diagnostic_suggested_action(
2567    diagnostics: *const PioDiagnostics,
2568    index: usize,
2569) -> PioStringView {
2570    unsafe { PioDiagnostics::get(diagnostics) }
2571        .and_then(|values| values.records().get(index))
2572        .and_then(Diagnostic::suggested_action)
2573        .map_or(PioStringView::EMPTY, PioStringView::new)
2574}
2575
2576/// Number of source byte ranges attached to this diagnostic.
2577#[unsafe(no_mangle)]
2578pub unsafe extern "C" fn pio_diagnostic_n_spans(
2579    diagnostics: *const PioDiagnostics,
2580    index: usize,
2581) -> usize {
2582    unsafe { PioDiagnostics::get(diagnostics) }
2583        .and_then(|values| values.records().get(index))
2584        .map_or(0, |record| record.spans().len())
2585}
2586
2587/// Read one source byte range. The source string borrows from `diagnostics`.
2588#[unsafe(no_mangle)]
2589pub unsafe extern "C" fn pio_diagnostic_span(
2590    diagnostics: *const PioDiagnostics,
2591    index: usize,
2592    span_index: usize,
2593    output: *mut PioDiagnosticSpanView,
2594    error: *mut *mut PioError,
2595) -> bool {
2596    unsafe {
2597        entry(error, false, || {
2598            let values = PioDiagnostics::get(diagnostics).ok_or_else(|| {
2599                boundary_error(
2600                    &codes::BIND_CAPI_NULL_HANDLE,
2601                    "PioDiagnostics must not be NULL",
2602                )
2603            })?;
2604            let record = values.records().get(index).ok_or_else(|| {
2605                boundary_error(
2606                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
2607                    format!("diagnostic index {index} is out of range"),
2608                )
2609            })?;
2610            let span = record.spans().get(span_index).ok_or_else(|| {
2611                boundary_error(
2612                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
2613                    format!("diagnostic span index {span_index} is out of range"),
2614                )
2615            })?;
2616            *require_output(output, "output")? = PioDiagnosticSpanView {
2617                source: PioStringView::new(span.source().as_str()),
2618                byte_start: span.byte_start(),
2619                byte_end: span.byte_end(),
2620            };
2621            Ok(true)
2622        })
2623    }
2624}
2625
2626/// Number of other diagnostic identities referenced by this diagnostic.
2627#[unsafe(no_mangle)]
2628pub unsafe extern "C" fn pio_diagnostic_n_related(
2629    diagnostics: *const PioDiagnostics,
2630    index: usize,
2631) -> usize {
2632    unsafe { PioDiagnostics::get(diagnostics) }
2633        .and_then(|values| values.records().get(index))
2634        .map_or(0, |record| record.related().len())
2635}
2636
2637/// Borrow one related diagnostic identity, or an empty view when out of range.
2638#[unsafe(no_mangle)]
2639pub unsafe extern "C" fn pio_diagnostic_related(
2640    diagnostics: *const PioDiagnostics,
2641    index: usize,
2642    related_index: usize,
2643) -> PioStringView {
2644    unsafe { PioDiagnostics::get(diagnostics) }
2645        .and_then(|values| values.records().get(index))
2646        .and_then(|record| record.related().get(related_index))
2647        .map_or(PioStringView::EMPTY, |id| PioStringView::new(id.as_str()))
2648}
2649
2650/// Serialize this diagnostic's structured details as an owned JSON object.
2651#[unsafe(no_mangle)]
2652pub unsafe extern "C" fn pio_diagnostic_details_json(
2653    diagnostics: *const PioDiagnostics,
2654    index: usize,
2655    error: *mut *mut PioError,
2656) -> *mut PioString {
2657    unsafe {
2658        entry(error, std::ptr::null_mut(), || {
2659            let values = PioDiagnostics::get(diagnostics).ok_or_else(|| {
2660                boundary_error(
2661                    &codes::BIND_CAPI_NULL_HANDLE,
2662                    "PioDiagnostics must not be NULL",
2663                )
2664            })?;
2665            let record = values.records().get(index).ok_or_else(|| {
2666                boundary_error(
2667                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
2668                    format!("diagnostic index {index} is out of range"),
2669                )
2670            })?;
2671            serde_json::to_string(record.details())
2672                .map(|text| PioString::new_raw(StringInner { text }))
2673                .map_err(|failure| {
2674                    boundary_error(
2675                        &codes::EMIT_CAPI_SERIALIZE_FAILED,
2676                        format!("cannot serialize diagnostic details: {failure}"),
2677                    )
2678                })
2679        })
2680    }
2681}
2682
2683#[unsafe(no_mangle)]
2684pub unsafe extern "C" fn pio_diagnostics_retain(
2685    diagnostics: *const PioDiagnostics,
2686) -> *mut PioDiagnostics {
2687    unsafe { PioDiagnostics::retain_raw(diagnostics) }
2688}
2689
2690#[unsafe(no_mangle)]
2691pub unsafe extern "C" fn pio_diagnostics_release(diagnostics: *mut PioDiagnostics) {
2692    unsafe { PioDiagnostics::release_raw(diagnostics) };
2693}
2694
2695// ---- source and destination ------------------------------------------------
2696
2697opaque_handle!(
2698    /// Acquired file, directory, or named memory bytes.
2699    PioSource,
2700    Source
2701);
2702
2703enum DestinationSpec {
2704    Path(PathBuf),
2705    Memory(String),
2706}
2707
2708impl DestinationSpec {
2709    fn build(&self) -> Result<Destination, powerio_core::Error> {
2710        match self {
2711            Self::Path(path) => Ok(Destination::path(path)),
2712            Self::Memory(root) => Destination::memory(root.clone()),
2713        }
2714    }
2715}
2716
2717opaque_handle!(
2718    /// File, directory, or memory output destination.
2719    PioDestination,
2720    DestinationSpec
2721);
2722
2723struct GeoLayerInner {
2724    layer: powerio::GeoLayer,
2725    diagnostics: Vec<Diagnostic>,
2726}
2727
2728opaque_handle!(
2729    /// Parsed geographic sidecar.
2730    PioGeoLayer,
2731    GeoLayerInner
2732);
2733opaque_handle!(
2734    /// Counts and notes from applying one geographic layer.
2735    PioGeoApplyReport,
2736    powerio::GeoApplyReport
2737);
2738
2739/// Acquire a file or directory path.
2740#[unsafe(no_mangle)]
2741pub unsafe extern "C" fn pio_source_open(
2742    path: *const c_char,
2743    path_len: usize,
2744    error: *mut *mut PioError,
2745) -> *mut PioSource {
2746    unsafe {
2747        entry(error, std::ptr::null_mut(), || {
2748            let path = required_str(path, path_len, "path")?;
2749            Source::open(path)
2750                .map(PioSource::new_raw)
2751                .map_err(|failure| error_from_core(&failure))
2752        })
2753    }
2754}
2755
2756/// Retain named bytes as an in-memory source. Binary content is supported.
2757#[unsafe(no_mangle)]
2758pub unsafe extern "C" fn pio_source_from_memory(
2759    name: *const c_char,
2760    name_len: usize,
2761    data: *const u8,
2762    data_len: usize,
2763    error: *mut *mut PioError,
2764) -> *mut PioSource {
2765    unsafe {
2766        entry(error, std::ptr::null_mut(), || {
2767            let name = required_str(name, name_len, "name")?;
2768            let bytes = input_bytes(data, data_len, "data")?;
2769            Source::from_memory(name, bytes.to_vec())
2770                .map(PioSource::new_raw)
2771                .map_err(|failure| error_from_core(&failure))
2772        })
2773    }
2774}
2775
2776#[unsafe(no_mangle)]
2777pub unsafe extern "C" fn pio_source_retain(source: *const PioSource) -> *mut PioSource {
2778    unsafe { PioSource::retain_raw(source) }
2779}
2780
2781#[unsafe(no_mangle)]
2782pub unsafe extern "C" fn pio_source_release(source: *mut PioSource) {
2783    unsafe { PioSource::release_raw(source) };
2784}
2785
2786/// Select a filesystem output path.
2787#[unsafe(no_mangle)]
2788pub unsafe extern "C" fn pio_destination_path(
2789    path: *const c_char,
2790    path_len: usize,
2791    error: *mut *mut PioError,
2792) -> *mut PioDestination {
2793    unsafe {
2794        entry(error, std::ptr::null_mut(), || {
2795            let path = required_str(path, path_len, "path")?;
2796            Ok(PioDestination::new_raw(DestinationSpec::Path(
2797                PathBuf::from(path),
2798            )))
2799        })
2800    }
2801}
2802
2803/// Select memory output and prefix returned artifact names with `root`.
2804#[unsafe(no_mangle)]
2805pub unsafe extern "C" fn pio_destination_memory(
2806    root: *const c_char,
2807    root_len: usize,
2808    error: *mut *mut PioError,
2809) -> *mut PioDestination {
2810    unsafe {
2811        entry(error, std::ptr::null_mut(), || {
2812            let root = required_str(root, root_len, "root")?;
2813            Destination::memory(root).map_err(|failure| error_from_core(&failure))?;
2814            Ok(PioDestination::new_raw(DestinationSpec::Memory(
2815                root.to_owned(),
2816            )))
2817        })
2818    }
2819}
2820
2821#[unsafe(no_mangle)]
2822pub unsafe extern "C" fn pio_destination_retain(
2823    destination: *const PioDestination,
2824) -> *mut PioDestination {
2825    unsafe { PioDestination::retain_raw(destination) }
2826}
2827
2828#[unsafe(no_mangle)]
2829pub unsafe extern "C" fn pio_destination_release(destination: *mut PioDestination) {
2830    unsafe { PioDestination::release_raw(destination) };
2831}
2832
2833/// Parse one geographic sidecar from an acquired source.
2834#[unsafe(no_mangle)]
2835pub unsafe extern "C" fn pio_geo_layer_parse(
2836    source: *const PioSource,
2837    error: *mut *mut PioError,
2838) -> *mut PioGeoLayer {
2839    unsafe {
2840        entry(error, std::ptr::null_mut(), || {
2841            let source = PioSource::get(source).ok_or_else(|| {
2842                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioSource must not be NULL")
2843            })?;
2844            let buffer = source
2845                .primary_buffer()
2846                .map_err(|failure| error_from_core(&failure))?;
2847            let text = std::str::from_utf8(buffer.content_bytes()).map_err(|_| {
2848                boundary_error(
2849                    &codes::BIND_CAPI_INVALID_UTF8,
2850                    "a geographic sidecar must be valid UTF-8",
2851                )
2852            })?;
2853            powerio::GeoLayer::parse(text, Some(source.name()))
2854                .map(|parsed| {
2855                    PioGeoLayer::new_raw(GeoLayerInner {
2856                        layer: parsed.layer,
2857                        diagnostics: parsed.diagnostics,
2858                    })
2859                })
2860                .map_err(|failure| error_from_tx(&failure))
2861        })
2862    }
2863}
2864
2865/// Return diagnostics produced while parsing a geographic sidecar.
2866#[unsafe(no_mangle)]
2867pub unsafe extern "C" fn pio_geo_layer_diagnostics(
2868    layer: *const PioGeoLayer,
2869) -> *mut PioDiagnostics {
2870    unsafe { PioGeoLayer::get(layer) }.map_or(std::ptr::null_mut(), |layer| {
2871        PioDiagnostics::new_raw(DiagnosticsInner {
2872            owner: DiagnosticsOwner::Owned(layer.diagnostics.clone()),
2873        })
2874    })
2875}
2876
2877#[unsafe(no_mangle)]
2878pub unsafe extern "C" fn pio_geo_layer_retain(layer: *const PioGeoLayer) -> *mut PioGeoLayer {
2879    unsafe { PioGeoLayer::retain_raw(layer) }
2880}
2881
2882#[unsafe(no_mangle)]
2883pub unsafe extern "C" fn pio_geo_layer_release(layer: *mut PioGeoLayer) {
2884    unsafe { PioGeoLayer::release_raw(layer) };
2885}
2886
2887// ---- modules and values ----------------------------------------------------
2888
2889#[derive(Clone)]
2890struct ModuleInner {
2891    module: powerio::PioModule<PioValue>,
2892}
2893
2894opaque_handle!(
2895    /// PowerIO value with diagnostics, source mappings, and history.
2896    PioModule,
2897    ModuleInner
2898);
2899
2900#[derive(Clone, Copy)]
2901enum ModuleJsonRoot {
2902    Extension(usize),
2903    HistoryParameter {
2904        history_index: usize,
2905        parameter_index: usize,
2906    },
2907}
2908
2909#[derive(Clone, Copy)]
2910enum JsonValueStep {
2911    Array(usize),
2912    Object(usize),
2913}
2914
2915struct JsonValueInner {
2916    owner: Arc<ModuleInner>,
2917    root: ModuleJsonRoot,
2918    steps: Vec<JsonValueStep>,
2919}
2920
2921impl JsonValueInner {
2922    fn child(&self, step: JsonValueStep) -> Self {
2923        let mut steps = self.steps.clone();
2924        steps.push(step);
2925        Self {
2926            owner: Arc::clone(&self.owner),
2927            root: self.root,
2928            steps,
2929        }
2930    }
2931
2932    fn value(&self) -> Option<&serde_json::Value> {
2933        let mut value = match self.root {
2934            ModuleJsonRoot::Extension(index) => {
2935                self.owner.module.extensions().values().nth(index)?
2936            }
2937            ModuleJsonRoot::HistoryParameter {
2938                history_index,
2939                parameter_index,
2940            } => self
2941                .owner
2942                .module
2943                .history()
2944                .get(history_index)?
2945                .parameters()
2946                .values()
2947                .nth(parameter_index)?,
2948        };
2949        for step in &self.steps {
2950            value = match (step, value) {
2951                (JsonValueStep::Array(index), serde_json::Value::Array(values)) => {
2952                    values.get(*index)?
2953                }
2954                (JsonValueStep::Object(index), serde_json::Value::Object(values)) => {
2955                    values.values().nth(*index)?
2956                }
2957                _ => return None,
2958            };
2959        }
2960        Some(value)
2961    }
2962}
2963
2964opaque_handle!(
2965    /// Owner-rooted structured value from module history or extensions.
2966    PioJsonValue,
2967    JsonValueInner
2968);
2969
2970#[derive(Clone, Copy)]
2971enum ValueStep {
2972    TimeSeries(usize),
2973    Scenario(usize),
2974}
2975
2976struct ValueInner {
2977    owner: Arc<ModuleInner>,
2978    steps: Vec<ValueStep>,
2979}
2980
2981impl ValueInner {
2982    fn root(owner: Arc<ModuleInner>) -> Self {
2983        Self {
2984            owner,
2985            steps: Vec::new(),
2986        }
2987    }
2988
2989    fn child(&self, step: ValueStep) -> Self {
2990        let mut steps = self.steps.clone();
2991        steps.push(step);
2992        Self {
2993            owner: Arc::clone(&self.owner),
2994            steps,
2995        }
2996    }
2997
2998    fn value(&self) -> Option<&PioValue> {
2999        let mut value = self.owner.module.value();
3000        for step in &self.steps {
3001            value = match (step, value) {
3002                (ValueStep::TimeSeries(index), PioValue::TimeSeries(series)) => {
3003                    series.get(*index)?
3004                }
3005                (ValueStep::Scenario(index), PioValue::ScenarioSet(scenarios)) => {
3006                    scenarios.get_at(*index)?
3007                }
3008                _ => return None,
3009            };
3010        }
3011        Some(value)
3012    }
3013}
3014
3015opaque_handle!(
3016    /// Owner-rooted view of one module or collection value.
3017    PioValueHandle,
3018    ValueInner
3019);
3020
3021#[derive(Clone, Copy)]
3022enum BalancedNetworkProjection {
3023    Direct,
3024    OperatingPoint,
3025    DcPfInstance,
3026    AcPfInstance,
3027    DcOpfInstance,
3028    AcOpfInstance,
3029    AcScucInstance,
3030    DcPfSolution,
3031    AcPfSolution,
3032    DcOpfSolution,
3033    AcOpfSolution,
3034    SocwrOpfSolution,
3035    AcScucSolution,
3036}
3037
3038struct BalancedNetworkInner {
3039    value: ValueInner,
3040    projection: BalancedNetworkProjection,
3041}
3042
3043impl BalancedNetworkInner {
3044    fn network(&self) -> Option<&BalancedNetwork> {
3045        match (self.projection, self.value.value()?) {
3046            (BalancedNetworkProjection::Direct, PioValue::BalancedNetwork(network)) => {
3047                Some(network)
3048            }
3049            (
3050                BalancedNetworkProjection::OperatingPoint,
3051                PioValue::BalancedOperatingPoint(point),
3052            ) => Some(point.network()),
3053            (BalancedNetworkProjection::DcPfInstance, PioValue::DcPfInstance(instance)) => {
3054                Some(instance.network())
3055            }
3056            (BalancedNetworkProjection::AcPfInstance, PioValue::AcPfInstance(instance)) => {
3057                Some(instance.network())
3058            }
3059            (BalancedNetworkProjection::DcOpfInstance, PioValue::DcOpfInstance(instance)) => {
3060                Some(instance.network())
3061            }
3062            (BalancedNetworkProjection::AcOpfInstance, PioValue::AcOpfInstance(instance)) => {
3063                Some(instance.network())
3064            }
3065            (BalancedNetworkProjection::AcScucInstance, PioValue::AcScucInstance(instance)) => {
3066                Some(instance.network())
3067            }
3068            (BalancedNetworkProjection::DcPfSolution, PioValue::DcPfSolution(solution)) => {
3069                Some(solution.network())
3070            }
3071            (BalancedNetworkProjection::AcPfSolution, PioValue::AcPfSolution(solution)) => {
3072                Some(solution.network())
3073            }
3074            (BalancedNetworkProjection::DcOpfSolution, PioValue::DcOpfSolution(solution)) => {
3075                Some(solution.network())
3076            }
3077            (BalancedNetworkProjection::AcOpfSolution, PioValue::AcOpfSolution(solution)) => {
3078                Some(solution.network())
3079            }
3080            (BalancedNetworkProjection::SocwrOpfSolution, PioValue::SocwrOpfSolution(solution)) => {
3081                Some(solution.network())
3082            }
3083            (BalancedNetworkProjection::AcScucSolution, PioValue::AcScucSolution(solution)) => {
3084                Some(solution.instance().network())
3085            }
3086            _ => None,
3087        }
3088    }
3089}
3090
3091#[derive(Clone, Copy)]
3092enum MulticonductorNetworkProjection {
3093    Direct,
3094    OperatingPoint,
3095    McAcPfInstance,
3096    McAcOpfInstance,
3097    McAcPfSolution,
3098    McAcOpfSolution,
3099}
3100
3101struct MulticonductorNetworkInner {
3102    value: ValueInner,
3103    projection: MulticonductorNetworkProjection,
3104}
3105
3106impl MulticonductorNetworkInner {
3107    fn network(&self) -> Option<&powerio::MulticonductorNetwork> {
3108        match (self.projection, self.value.value()?) {
3109            (MulticonductorNetworkProjection::Direct, PioValue::MulticonductorNetwork(network)) => {
3110                Some(network)
3111            }
3112            (
3113                MulticonductorNetworkProjection::OperatingPoint,
3114                PioValue::MulticonductorOperatingPoint(point),
3115            ) => Some(point.network()),
3116            (
3117                MulticonductorNetworkProjection::McAcPfInstance,
3118                PioValue::McAcPfInstance(instance),
3119            ) => Some(instance.network()),
3120            (
3121                MulticonductorNetworkProjection::McAcOpfInstance,
3122                PioValue::McAcOpfInstance(instance),
3123            ) => Some(instance.network()),
3124            (
3125                MulticonductorNetworkProjection::McAcPfSolution,
3126                PioValue::McAcPfSolution(solution),
3127            ) => Some(solution.network()),
3128            (
3129                MulticonductorNetworkProjection::McAcOpfSolution,
3130                PioValue::McAcOpfSolution(solution),
3131            ) => Some(solution.network()),
3132            _ => None,
3133        }
3134    }
3135}
3136
3137#[derive(Clone, Copy)]
3138enum OperatingPointProjection {
3139    DirectBalanced,
3140    DirectMulticonductor,
3141    DcPfInitial,
3142    AcPfInitial,
3143    DcOpfInitial,
3144    AcOpfInitial,
3145    McAcPfInitial,
3146    McAcOpfInitial,
3147    DcPfSolutionInitial,
3148    AcPfSolutionInitial,
3149    DcOpfSolutionInitial,
3150    AcOpfSolutionInitial,
3151    SocwrOpfSolutionInitial,
3152    McAcPfSolutionInitial,
3153    McAcOpfSolutionInitial,
3154}
3155
3156struct OperatingPointInner {
3157    value: ValueInner,
3158    projection: OperatingPointProjection,
3159}
3160
3161impl OperatingPointInner {
3162    fn balanced(&self) -> Option<&powerio_prob::OperatingPoint<BalancedNetwork>> {
3163        match (self.projection, self.value.value()?) {
3164            (OperatingPointProjection::DirectBalanced, PioValue::BalancedOperatingPoint(point)) => {
3165                Some(point)
3166            }
3167            (OperatingPointProjection::DcPfInitial, PioValue::DcPfInstance(instance)) => {
3168                instance.initial_point()
3169            }
3170            (OperatingPointProjection::AcPfInitial, PioValue::AcPfInstance(instance)) => {
3171                instance.initial_point()
3172            }
3173            (OperatingPointProjection::DcOpfInitial, PioValue::DcOpfInstance(instance)) => {
3174                instance.initial_point()
3175            }
3176            (OperatingPointProjection::AcOpfInitial, PioValue::AcOpfInstance(instance)) => {
3177                instance.initial_point()
3178            }
3179            (OperatingPointProjection::DcPfSolutionInitial, PioValue::DcPfSolution(solution)) => {
3180                solution.instance().initial_point()
3181            }
3182            (OperatingPointProjection::AcPfSolutionInitial, PioValue::AcPfSolution(solution)) => {
3183                solution.instance().initial_point()
3184            }
3185            (OperatingPointProjection::DcOpfSolutionInitial, PioValue::DcOpfSolution(solution)) => {
3186                solution.instance().initial_point()
3187            }
3188            (OperatingPointProjection::AcOpfSolutionInitial, PioValue::AcOpfSolution(solution)) => {
3189                solution.instance().initial_point()
3190            }
3191            (
3192                OperatingPointProjection::SocwrOpfSolutionInitial,
3193                PioValue::SocwrOpfSolution(solution),
3194            ) => solution.instance().initial_point(),
3195            _ => None,
3196        }
3197    }
3198
3199    fn multiconductor(
3200        &self,
3201    ) -> Option<&powerio_prob::OperatingPoint<powerio::MulticonductorNetwork>> {
3202        match (self.projection, self.value.value()?) {
3203            (
3204                OperatingPointProjection::DirectMulticonductor,
3205                PioValue::MulticonductorOperatingPoint(point),
3206            ) => Some(point),
3207            (OperatingPointProjection::McAcPfInitial, PioValue::McAcPfInstance(instance)) => {
3208                instance.initial_point()
3209            }
3210            (OperatingPointProjection::McAcOpfInitial, PioValue::McAcOpfInstance(instance)) => {
3211                instance.initial_point()
3212            }
3213            (
3214                OperatingPointProjection::McAcPfSolutionInitial,
3215                PioValue::McAcPfSolution(solution),
3216            ) => solution.instance().initial_point(),
3217            (
3218                OperatingPointProjection::McAcOpfSolutionInitial,
3219                PioValue::McAcOpfSolution(solution),
3220            ) => solution.instance().initial_point(),
3221            _ => None,
3222        }
3223    }
3224
3225    fn type_name(&self) -> Option<&'static str> {
3226        if self.balanced().is_some() {
3227            Some("powerio.OperatingPoint<powerio.BalancedNetwork>")
3228        } else if self.multiconductor().is_some() {
3229            Some("powerio.OperatingPoint<powerio.MulticonductorNetwork>")
3230        } else {
3231            None
3232        }
3233    }
3234
3235    fn balanced_network_projection(&self) -> Option<BalancedNetworkProjection> {
3236        self.balanced()?;
3237        Some(match self.projection {
3238            OperatingPointProjection::DirectBalanced => BalancedNetworkProjection::OperatingPoint,
3239            OperatingPointProjection::DcPfInitial => BalancedNetworkProjection::DcPfInstance,
3240            OperatingPointProjection::AcPfInitial => BalancedNetworkProjection::AcPfInstance,
3241            OperatingPointProjection::DcOpfInitial => BalancedNetworkProjection::DcOpfInstance,
3242            OperatingPointProjection::AcOpfInitial => BalancedNetworkProjection::AcOpfInstance,
3243            OperatingPointProjection::DcPfSolutionInitial => {
3244                BalancedNetworkProjection::DcPfSolution
3245            }
3246            OperatingPointProjection::AcPfSolutionInitial => {
3247                BalancedNetworkProjection::AcPfSolution
3248            }
3249            OperatingPointProjection::DcOpfSolutionInitial => {
3250                BalancedNetworkProjection::DcOpfSolution
3251            }
3252            OperatingPointProjection::AcOpfSolutionInitial => {
3253                BalancedNetworkProjection::AcOpfSolution
3254            }
3255            OperatingPointProjection::SocwrOpfSolutionInitial => {
3256                BalancedNetworkProjection::SocwrOpfSolution
3257            }
3258            _ => return None,
3259        })
3260    }
3261
3262    fn multiconductor_network_projection(&self) -> Option<MulticonductorNetworkProjection> {
3263        self.multiconductor()?;
3264        Some(match self.projection {
3265            OperatingPointProjection::DirectMulticonductor => {
3266                MulticonductorNetworkProjection::OperatingPoint
3267            }
3268            OperatingPointProjection::McAcPfInitial => {
3269                MulticonductorNetworkProjection::McAcPfInstance
3270            }
3271            OperatingPointProjection::McAcOpfInitial => {
3272                MulticonductorNetworkProjection::McAcOpfInstance
3273            }
3274            OperatingPointProjection::McAcPfSolutionInitial => {
3275                MulticonductorNetworkProjection::McAcPfSolution
3276            }
3277            OperatingPointProjection::McAcOpfSolutionInitial => {
3278                MulticonductorNetworkProjection::McAcOpfSolution
3279            }
3280            _ => return None,
3281        })
3282    }
3283}
3284
3285#[derive(Clone, Copy)]
3286enum CalculationInstanceProjection {
3287    Direct,
3288    DcPfSolution,
3289    AcPfSolution,
3290    DcOpfSolution,
3291    AcOpfSolution,
3292    SocwrOpfSolution,
3293    McAcPfSolution,
3294    McAcOpfSolution,
3295    AcScucSolution,
3296}
3297
3298#[derive(Clone, Copy)]
3299enum CalculationInstanceRef<'a> {
3300    DcPf(&'a powerio_prob::DcPfInstance),
3301    AcPf(&'a powerio_prob::AcPfInstance),
3302    DcOpf(&'a powerio_prob::DcOpfInstance),
3303    AcOpf(&'a powerio_prob::AcOpfInstance),
3304    McAcPf(&'a powerio_prob::McAcPfInstance),
3305    McAcOpf(&'a powerio_prob::McAcOpfInstance),
3306    AcScuc(&'a powerio_prob::AcScucInstance),
3307}
3308
3309impl CalculationInstanceRef<'_> {
3310    fn type_name(self) -> &'static str {
3311        match self {
3312            Self::DcPf(_) => "powerio.DcPfInstance",
3313            Self::AcPf(_) => "powerio.AcPfInstance",
3314            Self::DcOpf(_) => "powerio.DcOpfInstance",
3315            Self::AcOpf(_) => "powerio.AcOpfInstance",
3316            Self::McAcPf(_) => "powerio.McAcPfInstance",
3317            Self::McAcOpf(_) => "powerio.McAcOpfInstance",
3318            Self::AcScuc(_) => "powerio.AcScucInstance",
3319        }
3320    }
3321}
3322
3323struct CalculationInstanceInner {
3324    value: ValueInner,
3325    projection: CalculationInstanceProjection,
3326}
3327
3328impl CalculationInstanceInner {
3329    fn instance(&self) -> Option<CalculationInstanceRef<'_>> {
3330        match (self.projection, self.value.value()?) {
3331            (CalculationInstanceProjection::Direct, PioValue::DcPfInstance(instance)) => {
3332                Some(CalculationInstanceRef::DcPf(instance))
3333            }
3334            (CalculationInstanceProjection::Direct, PioValue::AcPfInstance(instance)) => {
3335                Some(CalculationInstanceRef::AcPf(instance))
3336            }
3337            (CalculationInstanceProjection::Direct, PioValue::DcOpfInstance(instance)) => {
3338                Some(CalculationInstanceRef::DcOpf(instance))
3339            }
3340            (CalculationInstanceProjection::Direct, PioValue::AcOpfInstance(instance)) => {
3341                Some(CalculationInstanceRef::AcOpf(instance))
3342            }
3343            (CalculationInstanceProjection::Direct, PioValue::McAcPfInstance(instance)) => {
3344                Some(CalculationInstanceRef::McAcPf(instance))
3345            }
3346            (CalculationInstanceProjection::Direct, PioValue::McAcOpfInstance(instance)) => {
3347                Some(CalculationInstanceRef::McAcOpf(instance))
3348            }
3349            (CalculationInstanceProjection::Direct, PioValue::AcScucInstance(instance)) => {
3350                Some(CalculationInstanceRef::AcScuc(instance))
3351            }
3352            (CalculationInstanceProjection::DcPfSolution, PioValue::DcPfSolution(solution)) => {
3353                Some(CalculationInstanceRef::DcPf(solution.instance()))
3354            }
3355            (CalculationInstanceProjection::AcPfSolution, PioValue::AcPfSolution(solution)) => {
3356                Some(CalculationInstanceRef::AcPf(solution.instance()))
3357            }
3358            (CalculationInstanceProjection::DcOpfSolution, PioValue::DcOpfSolution(solution)) => {
3359                Some(CalculationInstanceRef::DcOpf(solution.instance()))
3360            }
3361            (CalculationInstanceProjection::AcOpfSolution, PioValue::AcOpfSolution(solution)) => {
3362                Some(CalculationInstanceRef::AcOpf(solution.instance()))
3363            }
3364            (
3365                CalculationInstanceProjection::SocwrOpfSolution,
3366                PioValue::SocwrOpfSolution(solution),
3367            ) => Some(CalculationInstanceRef::AcOpf(solution.instance())),
3368            (CalculationInstanceProjection::McAcPfSolution, PioValue::McAcPfSolution(solution)) => {
3369                Some(CalculationInstanceRef::McAcPf(solution.instance()))
3370            }
3371            (
3372                CalculationInstanceProjection::McAcOpfSolution,
3373                PioValue::McAcOpfSolution(solution),
3374            ) => Some(CalculationInstanceRef::McAcOpf(solution.instance())),
3375            (CalculationInstanceProjection::AcScucSolution, PioValue::AcScucSolution(solution)) => {
3376                Some(CalculationInstanceRef::AcScuc(solution.instance()))
3377            }
3378            _ => None,
3379        }
3380    }
3381
3382    fn type_name(&self) -> Option<&'static str> {
3383        self.instance().map(CalculationInstanceRef::type_name)
3384    }
3385
3386    fn dc_pf(&self) -> Option<&powerio_prob::DcPfInstance> {
3387        match self.instance()? {
3388            CalculationInstanceRef::DcPf(instance) => Some(instance),
3389            _ => None,
3390        }
3391    }
3392
3393    fn ac_pf(&self) -> Option<&powerio_prob::AcPfInstance> {
3394        match self.instance()? {
3395            CalculationInstanceRef::AcPf(instance) => Some(instance),
3396            _ => None,
3397        }
3398    }
3399
3400    fn dc_opf(&self) -> Option<&powerio_prob::DcOpfInstance> {
3401        match self.instance()? {
3402            CalculationInstanceRef::DcOpf(instance) => Some(instance),
3403            _ => None,
3404        }
3405    }
3406
3407    fn ac_opf(&self) -> Option<&powerio_prob::AcOpfInstance> {
3408        match self.instance()? {
3409            CalculationInstanceRef::AcOpf(instance) => Some(instance),
3410            _ => None,
3411        }
3412    }
3413
3414    fn mc_ac_pf(&self) -> Option<&powerio_prob::McAcPfInstance> {
3415        match self.instance()? {
3416            CalculationInstanceRef::McAcPf(instance) => Some(instance),
3417            _ => None,
3418        }
3419    }
3420
3421    fn ac_scuc(&self) -> Option<&powerio_prob::AcScucInstance> {
3422        match self.instance()? {
3423            CalculationInstanceRef::AcScuc(instance) => Some(instance),
3424            _ => None,
3425        }
3426    }
3427
3428    fn has_initial_point(&self) -> bool {
3429        match self.instance() {
3430            Some(CalculationInstanceRef::DcPf(instance)) => instance.initial_point().is_some(),
3431            Some(CalculationInstanceRef::AcPf(instance)) => instance.initial_point().is_some(),
3432            Some(CalculationInstanceRef::DcOpf(instance)) => instance.initial_point().is_some(),
3433            Some(CalculationInstanceRef::AcOpf(instance)) => instance.initial_point().is_some(),
3434            Some(CalculationInstanceRef::McAcPf(instance)) => instance.initial_point().is_some(),
3435            Some(CalculationInstanceRef::McAcOpf(instance)) => instance.initial_point().is_some(),
3436            _ => false,
3437        }
3438    }
3439}
3440
3441opaque_handle!(
3442    /// Owner-rooted balanced network view.
3443    PioBalancedNetwork,
3444    BalancedNetworkInner
3445);
3446
3447struct DetailedConnectivityInner {
3448    owner: Arc<BalancedNetworkInner>,
3449}
3450
3451impl DetailedConnectivityInner {
3452    fn details(&self) -> Option<&powerio_tx::DetailedConnectivity> {
3453        self.owner.network()?.detailed_connectivity().as_deref()
3454    }
3455}
3456
3457opaque_handle!(
3458    /// Owner-rooted source neutral hierarchy and detailed connectivity view.
3459    PioDetailedConnectivity,
3460    DetailedConnectivityInner
3461);
3462opaque_handle!(
3463    /// Owner-rooted multiconductor network view.
3464    PioMulticonductorNetwork,
3465    MulticonductorNetworkInner
3466);
3467opaque_handle!(
3468    /// Owner-rooted time series view.
3469    PioTimeSeriesHandle,
3470    ValueInner
3471);
3472opaque_handle!(
3473    /// Owner-rooted scenario set view.
3474    PioScenarioSetHandle,
3475    ValueInner
3476);
3477opaque_handle!(
3478    /// Owner-rooted operating point view.
3479    PioOperatingPoint,
3480    OperatingPointInner
3481);
3482opaque_handle!(
3483    /// Owner-rooted calculation instance view.
3484    PioCalculationInstance,
3485    CalculationInstanceInner
3486);
3487opaque_handle!(
3488    /// Owned DC OPF preparation whose borrowed row views remain valid until release.
3489    PioDcOpfPreparation,
3490    DcOpfPreparation
3491);
3492opaque_handle!(
3493    /// Owned AC OPF preparation whose borrowed row views remain valid until release.
3494    PioAcOpfPreparation,
3495    AcOpfPreparation
3496);
3497opaque_handle!(
3498    /// Owner-rooted calculation solution view.
3499    PioCalculationSolution,
3500    ValueInner
3501);
3502
3503fn module_handle(module: powerio::PioModule<PioValue>) -> *mut PioModule {
3504    PioModule::new_raw(ModuleInner { module })
3505}
3506
3507fn value_handle(value: ValueInner) -> *mut PioValueHandle {
3508    PioValueHandle::new_raw(value)
3509}
3510
3511unsafe fn require_value<'a>(value: *const PioValueHandle) -> Result<&'a ValueInner, *mut PioError> {
3512    unsafe { PioValueHandle::get(value) }.ok_or_else(|| {
3513        boundary_error(
3514            &codes::BIND_CAPI_NULL_HANDLE,
3515            "PioValueHandle must not be NULL",
3516        )
3517    })
3518}
3519
3520/// Parse one acquired grid exchange source.
3521#[unsafe(no_mangle)]
3522pub unsafe extern "C" fn pio_parse(
3523    source: *const PioSource,
3524    format: *const c_char,
3525    format_len: usize,
3526    error: *mut *mut PioError,
3527) -> *mut PioModule {
3528    unsafe {
3529        entry(error, std::ptr::null_mut(), || {
3530            let source = PioSource::get(source).ok_or_else(|| {
3531                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioSource must not be NULL")
3532            })?;
3533            let format = optional_str(format, format_len, "format")?;
3534            let mut options = powerio::ParseOptions::default();
3535            if let Some(format) = format {
3536                options = options
3537                    .format(format)
3538                    .map_err(|failure| error_from_core(&failure))?;
3539            }
3540            powerio::parse_with_options(source.clone(), &options)
3541                .map(module_handle)
3542                .map_err(|failure| error_from_core(&failure))
3543        })
3544    }
3545}
3546
3547/// Deserialize one PowerIO IR source.
3548///
3549/// A document carries the independent PowerIO IR generation reported by
3550/// `pio_schema_report`. This library refuses any unsupported identity or
3551/// generation through `error`, naming what it found.
3552#[unsafe(no_mangle)]
3553pub unsafe extern "C" fn pio_module_deserialize(
3554    source: *const PioSource,
3555    error: *mut *mut PioError,
3556) -> *mut PioModule {
3557    unsafe {
3558        entry(error, std::ptr::null_mut(), || {
3559            let source = PioSource::get(source).ok_or_else(|| {
3560                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioSource must not be NULL")
3561            })?;
3562            powerio::deserialize(source.clone())
3563                .map(module_handle)
3564                .map_err(|failure| error_from_core(&failure))
3565        })
3566    }
3567}
3568
3569unsafe fn transform_module<T>(
3570    module: *const PioModule,
3571    error: *mut *mut PioError,
3572    transform: impl FnOnce(
3573        &powerio::PioModule<PioValue>,
3574    ) -> Result<powerio::PioModule<T>, powerio_core::Error>,
3575    wrap: impl FnOnce(T) -> PioValue,
3576) -> *mut PioModule {
3577    unsafe {
3578        entry(error, std::ptr::null_mut(), || {
3579            let module = PioModule::get(module).ok_or_else(|| {
3580                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
3581            })?;
3582            transform(&module.module)
3583                .map(|derived| module_handle(derived.map_value(wrap)))
3584                .map_err(|failure| error_from_core(&failure))
3585        })
3586    }
3587}
3588
3589/// Construct a DC power flow calculation module from a balanced network module.
3590#[unsafe(no_mangle)]
3591pub unsafe extern "C" fn pio_module_to_dc_pf_instance(
3592    module: *const PioModule,
3593    error: *mut *mut PioError,
3594) -> *mut PioModule {
3595    unsafe {
3596        transform_module(
3597            module,
3598            error,
3599            powerio::transform::to_dc_pf_instance,
3600            PioValue::DcPfInstance,
3601        )
3602    }
3603}
3604
3605/// Construct an AC power flow calculation module from a balanced network module.
3606#[unsafe(no_mangle)]
3607pub unsafe extern "C" fn pio_module_to_ac_pf_instance(
3608    module: *const PioModule,
3609    error: *mut *mut PioError,
3610) -> *mut PioModule {
3611    unsafe {
3612        transform_module(
3613            module,
3614            error,
3615            powerio::transform::to_ac_pf_instance,
3616            PioValue::AcPfInstance,
3617        )
3618    }
3619}
3620
3621/// Construct a DC optimal power flow calculation module from a balanced network module.
3622#[unsafe(no_mangle)]
3623pub unsafe extern "C" fn pio_module_to_dc_opf_instance(
3624    module: *const PioModule,
3625    error: *mut *mut PioError,
3626) -> *mut PioModule {
3627    unsafe {
3628        transform_module(
3629            module,
3630            error,
3631            powerio::transform::to_dc_opf_instance,
3632            PioValue::DcOpfInstance,
3633        )
3634    }
3635}
3636
3637/// Construct an AC optimal power flow calculation module from a balanced network module.
3638#[unsafe(no_mangle)]
3639pub unsafe extern "C" fn pio_module_to_ac_opf_instance(
3640    module: *const PioModule,
3641    error: *mut *mut PioError,
3642) -> *mut PioModule {
3643    unsafe {
3644        transform_module(
3645            module,
3646            error,
3647            powerio::transform::to_ac_opf_instance,
3648            PioValue::AcOpfInstance,
3649        )
3650    }
3651}
3652
3653/// Construct a multiconductor AC power flow calculation module from a
3654/// multiconductor network module.
3655#[unsafe(no_mangle)]
3656pub unsafe extern "C" fn pio_module_to_mc_ac_pf_instance(
3657    module: *const PioModule,
3658    error: *mut *mut PioError,
3659) -> *mut PioModule {
3660    unsafe {
3661        transform_module(
3662            module,
3663            error,
3664            powerio::transform::to_mc_ac_pf_instance,
3665            PioValue::McAcPfInstance,
3666        )
3667    }
3668}
3669
3670/// Construct a multiconductor AC optimal power flow calculation module from a
3671/// multiconductor network module.
3672#[unsafe(no_mangle)]
3673pub unsafe extern "C" fn pio_module_to_mc_ac_opf_instance(
3674    module: *const PioModule,
3675    error: *mut *mut PioError,
3676) -> *mut PioModule {
3677    unsafe {
3678        transform_module(
3679            module,
3680            error,
3681            powerio::transform::to_mc_ac_opf_instance,
3682            PioValue::McAcOpfInstance,
3683        )
3684    }
3685}
3686
3687/// Apply one geographic layer to a balanced or multiconductor network module.
3688/// The input module is unchanged. When `out_report` is not NULL, it receives
3689/// an independently owned report handle.
3690#[unsafe(no_mangle)]
3691pub unsafe extern "C" fn pio_module_apply_geo_layer(
3692    module: *const PioModule,
3693    layer: *const PioGeoLayer,
3694    out_report: *mut *mut PioGeoApplyReport,
3695    error: *mut *mut PioError,
3696) -> *mut PioModule {
3697    unsafe {
3698        if !out_report.is_null() {
3699            *out_report = std::ptr::null_mut();
3700        }
3701        entry(error, std::ptr::null_mut(), || {
3702            let module = PioModule::get(module).ok_or_else(|| {
3703                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
3704            })?;
3705            let layer = PioGeoLayer::get(layer).ok_or_else(|| {
3706                boundary_error(
3707                    &codes::BIND_CAPI_NULL_HANDLE,
3708                    "PioGeoLayer must not be NULL",
3709                )
3710            })?;
3711            let (mut derived, report) = powerio::apply_geo_layer(&module.module, &layer.layer)
3712                .map_err(|failure| error_from_core(&failure))?;
3713            for diagnostic in &layer.diagnostics {
3714                derived
3715                    .add_diagnostic(diagnostic.clone())
3716                    .map_err(|failure| error_from_core(&failure))?;
3717            }
3718            if !out_report.is_null() {
3719                *out_report = PioGeoApplyReport::new_raw(report);
3720            }
3721            Ok(module_handle(derived))
3722        })
3723    }
3724}
3725
3726#[unsafe(no_mangle)]
3727pub unsafe extern "C" fn pio_geo_apply_report_matched_buses(
3728    report: *const PioGeoApplyReport,
3729) -> usize {
3730    unsafe { PioGeoApplyReport::get(report) }.map_or(0, |report| report.matched_buses)
3731}
3732
3733#[unsafe(no_mangle)]
3734pub unsafe extern "C" fn pio_geo_apply_report_matched_branches(
3735    report: *const PioGeoApplyReport,
3736) -> usize {
3737    unsafe { PioGeoApplyReport::get(report) }.map_or(0, |report| report.matched_branches)
3738}
3739
3740#[unsafe(no_mangle)]
3741pub unsafe extern "C" fn pio_geo_apply_report_unmatched_features(
3742    report: *const PioGeoApplyReport,
3743) -> usize {
3744    unsafe { PioGeoApplyReport::get(report) }.map_or(0, |report| report.unmatched_features)
3745}
3746
3747#[unsafe(no_mangle)]
3748pub unsafe extern "C" fn pio_geo_apply_report_unlocated_buses(
3749    report: *const PioGeoApplyReport,
3750) -> usize {
3751    unsafe { PioGeoApplyReport::get(report) }.map_or(0, |report| report.unlocated_buses)
3752}
3753
3754#[unsafe(no_mangle)]
3755pub unsafe extern "C" fn pio_geo_apply_report_unlocated_branches(
3756    report: *const PioGeoApplyReport,
3757) -> usize {
3758    unsafe { PioGeoApplyReport::get(report) }.map_or(0, |report| report.unlocated_branches)
3759}
3760
3761#[unsafe(no_mangle)]
3762pub unsafe extern "C" fn pio_geo_apply_report_note_count(
3763    report: *const PioGeoApplyReport,
3764) -> usize {
3765    unsafe { PioGeoApplyReport::get(report) }.map_or(0, |report| report.notes.len())
3766}
3767
3768#[unsafe(no_mangle)]
3769pub unsafe extern "C" fn pio_geo_apply_report_note_at(
3770    report: *const PioGeoApplyReport,
3771    index: usize,
3772    error: *mut *mut PioError,
3773) -> PioStringView {
3774    unsafe {
3775        entry(error, PioStringView::EMPTY, || {
3776            let report = PioGeoApplyReport::get(report).ok_or_else(|| {
3777                boundary_error(
3778                    &codes::BIND_CAPI_NULL_HANDLE,
3779                    "PioGeoApplyReport must not be NULL",
3780                )
3781            })?;
3782            let note = report.notes.get(index).ok_or_else(|| {
3783                boundary_error(
3784                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
3785                    format!("geo apply note index {index} is out of range"),
3786                )
3787            })?;
3788            Ok(PioStringView::new(note))
3789        })
3790    }
3791}
3792
3793#[unsafe(no_mangle)]
3794pub unsafe extern "C" fn pio_geo_apply_report_retain(
3795    report: *const PioGeoApplyReport,
3796) -> *mut PioGeoApplyReport {
3797    unsafe { PioGeoApplyReport::retain_raw(report) }
3798}
3799
3800#[unsafe(no_mangle)]
3801pub unsafe extern "C" fn pio_geo_apply_report_release(report: *mut PioGeoApplyReport) {
3802    unsafe { PioGeoApplyReport::release_raw(report) };
3803}
3804
3805/// Return an owner-rooted view of the module's value.
3806#[unsafe(no_mangle)]
3807pub unsafe extern "C" fn pio_module_value(module: *const PioModule) -> *mut PioValueHandle {
3808    unsafe { PioModule::arc(module) }.map_or(std::ptr::null_mut(), |owner| {
3809        value_handle(ValueInner::root(owner))
3810    })
3811}
3812
3813/// Return the module's stored diagnostics.
3814#[unsafe(no_mangle)]
3815pub unsafe extern "C" fn pio_module_diagnostics(module: *const PioModule) -> *mut PioDiagnostics {
3816    unsafe { PioModule::arc(module) }.map_or(std::ptr::null_mut(), |module| {
3817        PioDiagnostics::new_raw(DiagnosticsInner {
3818            owner: DiagnosticsOwner::Module(module),
3819        })
3820    })
3821}
3822
3823fn source_relation_name(relation: powerio_core::SourceRelation) -> &'static str {
3824    match relation {
3825        powerio_core::SourceRelation::Exact => "exact",
3826        powerio_core::SourceRelation::Defaulted => "defaulted",
3827        powerio_core::SourceRelation::Inferred => "inferred",
3828        powerio_core::SourceRelation::ConvertedUnits => "converted_units",
3829        powerio_core::SourceRelation::Aggregated => "aggregated",
3830        powerio_core::SourceRelation::Split => "split",
3831        powerio_core::SourceRelation::Synthetic => "synthetic",
3832        powerio_core::SourceRelation::Transformed => "transformed",
3833        powerio_core::SourceRelation::RetainedExtra => "retained_extra",
3834        _ => "unknown",
3835    }
3836}
3837
3838fn history_kind_name(kind: HistoryKind) -> &'static str {
3839    match kind {
3840        HistoryKind::Parse => "parse",
3841        HistoryKind::Transform => "transform",
3842        HistoryKind::Edit => "edit",
3843        HistoryKind::Repair => "repair",
3844        HistoryKind::Solve => "solve",
3845        _ => "unknown",
3846    }
3847}
3848
3849fn json_value_kind(value: &serde_json::Value) -> &'static str {
3850    match value {
3851        serde_json::Value::Null => "null",
3852        serde_json::Value::Bool(_) => "boolean",
3853        serde_json::Value::Number(_) => "number",
3854        serde_json::Value::String(_) => "string",
3855        serde_json::Value::Array(_) => "array",
3856        serde_json::Value::Object(_) => "object",
3857    }
3858}
3859
3860/// Read the program identity recorded with a module.
3861#[unsafe(no_mangle)]
3862pub unsafe extern "C" fn pio_module_producer(
3863    module: *const PioModule,
3864    output: *mut PioModuleProducerView,
3865    error: *mut *mut PioError,
3866) -> bool {
3867    unsafe {
3868        entry(error, false, || {
3869            let module = PioModule::get(module).ok_or_else(|| {
3870                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
3871            })?;
3872            let producer = module.module.producer();
3873            *require_output(output, "output")? = PioModuleProducerView {
3874                name: PioStringView::new(producer.name()),
3875                version: PioStringView::new(producer.version()),
3876            };
3877            Ok(true)
3878        })
3879    }
3880}
3881
3882#[unsafe(no_mangle)]
3883pub unsafe extern "C" fn pio_module_source_count(module: *const PioModule) -> usize {
3884    unsafe { PioModule::get(module) }.map_or(0, |module| module.module.sources().len())
3885}
3886
3887/// Read one durable source descriptor by zero based position.
3888#[unsafe(no_mangle)]
3889pub unsafe extern "C" fn pio_module_source_at(
3890    module: *const PioModule,
3891    index: usize,
3892    output: *mut PioModuleSourceView,
3893    error: *mut *mut PioError,
3894) -> bool {
3895    unsafe {
3896        entry(error, false, || {
3897            let module = PioModule::get(module).ok_or_else(|| {
3898                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
3899            })?;
3900            let source = module.module.sources().get(index).ok_or_else(|| {
3901                boundary_error(
3902                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
3903                    format!("module source index {index} is out of range"),
3904                )
3905            })?;
3906            let (format, has_format) = source
3907                .format()
3908                .map_or((PioStringView::EMPTY, false), |format| {
3909                    (PioStringView::new(format.as_str()), true)
3910                });
3911            let digest = source.digest();
3912            *require_output(output, "output")? = PioModuleSourceView {
3913                id: PioStringView::new(source.id().as_str()),
3914                name: PioStringView::new(source.name()),
3915                byte_length: source.byte_length(),
3916                format,
3917                has_format,
3918                digest_algorithm: digest.map_or(PioStringView::EMPTY, |digest| {
3919                    PioStringView::new(digest.algorithm().as_str())
3920                }),
3921                digest: digest.map_or(PioStringView::EMPTY, |digest| {
3922                    PioStringView::new(digest.value())
3923                }),
3924                has_digest: digest.is_some(),
3925            };
3926            Ok(true)
3927        })
3928    }
3929}
3930
3931#[unsafe(no_mangle)]
3932pub unsafe extern "C" fn pio_module_source_map_count(module: *const PioModule) -> usize {
3933    unsafe { PioModule::get(module) }.map_or(0, |module| module.module.source_map().len())
3934}
3935
3936/// Read one source map entry by zero based position.
3937#[unsafe(no_mangle)]
3938pub unsafe extern "C" fn pio_module_source_map_at(
3939    module: *const PioModule,
3940    index: usize,
3941    output: *mut PioModuleSourceMapEntryView,
3942    error: *mut *mut PioError,
3943) -> bool {
3944    unsafe {
3945        entry(error, false, || {
3946            let module = PioModule::get(module).ok_or_else(|| {
3947                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
3948            })?;
3949            let source_map = module.module.source_map().get(index).ok_or_else(|| {
3950                boundary_error(
3951                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
3952                    format!("module source map index {index} is out of range"),
3953                )
3954            })?;
3955            *require_output(output, "output")? = PioModuleSourceMapEntryView {
3956                target: PioStringView::new(source_map.target()),
3957                relation: PioStringView::new(source_relation_name(source_map.relation())),
3958                span_count: source_map.spans().len(),
3959            };
3960            Ok(true)
3961        })
3962    }
3963}
3964
3965/// Read one byte range from a source map entry.
3966#[unsafe(no_mangle)]
3967pub unsafe extern "C" fn pio_module_source_map_span_at(
3968    module: *const PioModule,
3969    entry_index: usize,
3970    span_index: usize,
3971    output: *mut PioSourceSpanView,
3972    error: *mut *mut PioError,
3973) -> bool {
3974    unsafe {
3975        entry(error, false, || {
3976            let module = PioModule::get(module).ok_or_else(|| {
3977                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
3978            })?;
3979            let source_map = module.module.source_map().get(entry_index).ok_or_else(|| {
3980                boundary_error(
3981                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
3982                    format!("module source map index {entry_index} is out of range"),
3983                )
3984            })?;
3985            let span = source_map.spans().get(span_index).ok_or_else(|| {
3986                boundary_error(
3987                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
3988                    format!(
3989                        "source map span index {span_index} is out of range for entry {entry_index}"
3990                    ),
3991                )
3992            })?;
3993            *require_output(output, "output")? = PioSourceSpanView {
3994                source: PioStringView::new(span.source().as_str()),
3995                byte_start: span.byte_start(),
3996                byte_end: span.byte_end(),
3997            };
3998            Ok(true)
3999        })
4000    }
4001}
4002
4003#[unsafe(no_mangle)]
4004pub unsafe extern "C" fn pio_module_history_count(module: *const PioModule) -> usize {
4005    unsafe { PioModule::get(module) }.map_or(0, |module| module.module.history().len())
4006}
4007
4008/// Read one operation from module history by zero based position.
4009#[unsafe(no_mangle)]
4010pub unsafe extern "C" fn pio_module_history_at(
4011    module: *const PioModule,
4012    index: usize,
4013    output: *mut PioModuleHistoryEntryView,
4014    error: *mut *mut PioError,
4015) -> bool {
4016    unsafe {
4017        entry(error, false, || {
4018            let module = PioModule::get(module).ok_or_else(|| {
4019                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
4020            })?;
4021            let history = module.module.history().get(index).ok_or_else(|| {
4022                boundary_error(
4023                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4024                    format!("module history index {index} is out of range"),
4025                )
4026            })?;
4027            let (input_type, has_input_type) = optional_string_view(history.input_type());
4028            let (output_type, has_output_type) = optional_string_view(history.output_type());
4029            *require_output(output, "output")? = PioModuleHistoryEntryView {
4030                id: PioStringView::new(history.id().as_str()),
4031                kind: PioStringView::new(history_kind_name(history.kind())),
4032                name: PioStringView::new(history.name()),
4033                input_type,
4034                has_input_type,
4035                output_type,
4036                has_output_type,
4037                parameter_count: history.parameters().len(),
4038                assumption_count: history.assumptions().len(),
4039                loss_count: history.losses().len(),
4040            };
4041            Ok(true)
4042        })
4043    }
4044}
4045
4046/// Read one named structured history parameter by zero based position.
4047#[unsafe(no_mangle)]
4048pub unsafe extern "C" fn pio_module_history_parameter_at(
4049    module: *const PioModule,
4050    history_index: usize,
4051    parameter_index: usize,
4052    output: *mut PioModuleHistoryParameterView,
4053    error: *mut *mut PioError,
4054) -> bool {
4055    unsafe {
4056        entry(error, false, || {
4057            let module = PioModule::get(module).ok_or_else(|| {
4058                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
4059            })?;
4060            let history = module.module.history().get(history_index).ok_or_else(|| {
4061                boundary_error(
4062                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4063                    format!("module history index {history_index} is out of range"),
4064                )
4065            })?;
4066            let (name, value) = history
4067                .parameters()
4068                .iter()
4069                .nth(parameter_index)
4070                .ok_or_else(|| {
4071                    boundary_error(
4072                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4073                        format!(
4074                            "history parameter index {parameter_index} is out of range for entry {history_index}"
4075                        ),
4076                    )
4077                })?;
4078            *require_output(output, "output")? = PioModuleHistoryParameterView {
4079                name: PioStringView::new(name),
4080                value_kind: PioStringView::new(json_value_kind(value)),
4081            };
4082            Ok(true)
4083        })
4084    }
4085}
4086
4087/// Return an owner-rooted structured history parameter value.
4088#[unsafe(no_mangle)]
4089pub unsafe extern "C" fn pio_module_history_parameter_value_at(
4090    module: *const PioModule,
4091    history_index: usize,
4092    parameter_index: usize,
4093    error: *mut *mut PioError,
4094) -> *mut PioJsonValue {
4095    unsafe {
4096        entry(error, std::ptr::null_mut(), || {
4097            let owner = PioModule::arc(module).ok_or_else(|| {
4098                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
4099            })?;
4100            let history = owner.module.history().get(history_index).ok_or_else(|| {
4101                boundary_error(
4102                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4103                    format!("module history index {history_index} is out of range"),
4104                )
4105            })?;
4106            if history.parameters().values().nth(parameter_index).is_none() {
4107                return Err(boundary_error(
4108                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4109                    format!(
4110                        "history parameter index {parameter_index} is out of range for entry {history_index}"
4111                    ),
4112                ));
4113            }
4114            Ok(PioJsonValue::new_raw(JsonValueInner {
4115                owner,
4116                root: ModuleJsonRoot::HistoryParameter {
4117                    history_index,
4118                    parameter_index,
4119                },
4120                steps: Vec::new(),
4121            }))
4122        })
4123    }
4124}
4125
4126/// Read one assumption attached to a history entry.
4127#[unsafe(no_mangle)]
4128pub unsafe extern "C" fn pio_module_history_assumption_at(
4129    module: *const PioModule,
4130    history_index: usize,
4131    assumption_index: usize,
4132    error: *mut *mut PioError,
4133) -> PioStringView {
4134    unsafe {
4135        entry(error, PioStringView::EMPTY, || {
4136            let module = PioModule::get(module).ok_or_else(|| {
4137                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
4138            })?;
4139            let history = module.module.history().get(history_index).ok_or_else(|| {
4140                boundary_error(
4141                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4142                    format!("module history index {history_index} is out of range"),
4143                )
4144            })?;
4145            history
4146                .assumptions()
4147                .get(assumption_index)
4148                .map(|value| PioStringView::new(value))
4149                .ok_or_else(|| {
4150                    boundary_error(
4151                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4152                        format!(
4153                            "history assumption index {assumption_index} is out of range for entry {history_index}"
4154                        ),
4155                    )
4156                })
4157        })
4158    }
4159}
4160
4161/// Read one declared loss attached to a history entry.
4162#[unsafe(no_mangle)]
4163pub unsafe extern "C" fn pio_module_history_loss_at(
4164    module: *const PioModule,
4165    history_index: usize,
4166    loss_index: usize,
4167    error: *mut *mut PioError,
4168) -> PioStringView {
4169    unsafe {
4170        entry(error, PioStringView::EMPTY, || {
4171            let module = PioModule::get(module).ok_or_else(|| {
4172                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
4173            })?;
4174            let history = module.module.history().get(history_index).ok_or_else(|| {
4175                boundary_error(
4176                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4177                    format!("module history index {history_index} is out of range"),
4178                )
4179            })?;
4180            history
4181                .losses()
4182                .get(loss_index)
4183                .map(|value| PioStringView::new(value))
4184                .ok_or_else(|| {
4185                    boundary_error(
4186                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4187                        format!(
4188                            "history loss index {loss_index} is out of range for entry {history_index}"
4189                        ),
4190                    )
4191                })
4192        })
4193    }
4194}
4195
4196#[unsafe(no_mangle)]
4197pub unsafe extern "C" fn pio_module_extension_count(module: *const PioModule) -> usize {
4198    unsafe { PioModule::get(module) }.map_or(0, |module| module.module.extensions().len())
4199}
4200
4201/// Read one namespaced structured module extension by zero based position.
4202#[unsafe(no_mangle)]
4203pub unsafe extern "C" fn pio_module_extension_at(
4204    module: *const PioModule,
4205    index: usize,
4206    output: *mut PioModuleExtensionView,
4207    error: *mut *mut PioError,
4208) -> bool {
4209    unsafe {
4210        entry(error, false, || {
4211            let module = PioModule::get(module).ok_or_else(|| {
4212                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
4213            })?;
4214            let (namespace, value) =
4215                module
4216                    .module
4217                    .extensions()
4218                    .iter()
4219                    .nth(index)
4220                    .ok_or_else(|| {
4221                        boundary_error(
4222                            &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4223                            format!("module extension index {index} is out of range"),
4224                        )
4225                    })?;
4226            *require_output(output, "output")? = PioModuleExtensionView {
4227                namespace: PioStringView::new(namespace),
4228                value_kind: PioStringView::new(json_value_kind(value)),
4229            };
4230            Ok(true)
4231        })
4232    }
4233}
4234
4235/// Return an owner-rooted structured module extension value.
4236#[unsafe(no_mangle)]
4237pub unsafe extern "C" fn pio_module_extension_value_at(
4238    module: *const PioModule,
4239    index: usize,
4240    error: *mut *mut PioError,
4241) -> *mut PioJsonValue {
4242    unsafe {
4243        entry(error, std::ptr::null_mut(), || {
4244            let owner = PioModule::arc(module).ok_or_else(|| {
4245                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
4246            })?;
4247            if owner.module.extensions().values().nth(index).is_none() {
4248                return Err(boundary_error(
4249                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4250                    format!("module extension index {index} is out of range"),
4251                ));
4252            }
4253            Ok(PioJsonValue::new_raw(JsonValueInner {
4254                owner,
4255                root: ModuleJsonRoot::Extension(index),
4256                steps: Vec::new(),
4257            }))
4258        })
4259    }
4260}
4261
4262fn json_value_view(value: &serde_json::Value) -> PioJsonValueView {
4263    let mut view = PioJsonValueView {
4264        kind: PioStringView::new(json_value_kind(value)),
4265        boolean_value: false,
4266        number_kind: PioStringView::EMPTY,
4267        signed_integer_value: 0,
4268        unsigned_integer_value: 0,
4269        floating_point_value: 0.0,
4270        string_value: PioStringView::EMPTY,
4271        element_count: 0,
4272    };
4273    match value {
4274        serde_json::Value::Bool(value) => view.boolean_value = *value,
4275        serde_json::Value::Number(value) if value.is_i64() => {
4276            view.number_kind = PioStringView::new("signed_integer");
4277            view.signed_integer_value = value.as_i64().unwrap_or_default();
4278        }
4279        serde_json::Value::Number(value) if value.is_u64() => {
4280            view.number_kind = PioStringView::new("unsigned_integer");
4281            view.unsigned_integer_value = value.as_u64().unwrap_or_default();
4282        }
4283        serde_json::Value::Number(value) => {
4284            view.number_kind = PioStringView::new("floating_point");
4285            view.floating_point_value = value.as_f64().unwrap_or(f64::NAN);
4286        }
4287        serde_json::Value::String(value) => view.string_value = PioStringView::new(value),
4288        serde_json::Value::Array(values) => view.element_count = values.len(),
4289        serde_json::Value::Object(values) => view.element_count = values.len(),
4290        serde_json::Value::Null => {}
4291    }
4292    view
4293}
4294
4295/// Read the type and scalar or collection data for a structured value.
4296#[unsafe(no_mangle)]
4297pub unsafe extern "C" fn pio_json_value_get(
4298    value: *const PioJsonValue,
4299    output: *mut PioJsonValueView,
4300    error: *mut *mut PioError,
4301) -> bool {
4302    unsafe {
4303        entry(error, false, || {
4304            let value = PioJsonValue::get(value).ok_or_else(|| {
4305                boundary_error(
4306                    &codes::BIND_CAPI_NULL_HANDLE,
4307                    "PioJsonValue must not be NULL",
4308                )
4309            })?;
4310            let value = value.value().ok_or_else(|| {
4311                boundary_error(
4312                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4313                    "the structured value path is out of range",
4314                )
4315            })?;
4316            *require_output(output, "output")? = json_value_view(value);
4317            Ok(true)
4318        })
4319    }
4320}
4321
4322/// Return one owner-rooted element from a structured JSON array.
4323#[unsafe(no_mangle)]
4324pub unsafe extern "C" fn pio_json_value_array_at(
4325    value: *const PioJsonValue,
4326    index: usize,
4327    error: *mut *mut PioError,
4328) -> *mut PioJsonValue {
4329    unsafe {
4330        entry(error, std::ptr::null_mut(), || {
4331            let value = PioJsonValue::get(value).ok_or_else(|| {
4332                boundary_error(
4333                    &codes::BIND_CAPI_NULL_HANDLE,
4334                    "PioJsonValue must not be NULL",
4335                )
4336            })?;
4337            let values = value
4338                .value()
4339                .and_then(serde_json::Value::as_array)
4340                .ok_or_else(|| {
4341                    boundary_error(
4342                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
4343                        "the structured value is not an array",
4344                    )
4345                })?;
4346            if values.get(index).is_none() {
4347                return Err(boundary_error(
4348                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4349                    format!("structured array index {index} is out of range"),
4350                ));
4351            }
4352            Ok(PioJsonValue::new_raw(
4353                value.child(JsonValueStep::Array(index)),
4354            ))
4355        })
4356    }
4357}
4358
4359/// Read one key and value type from a structured JSON object.
4360#[unsafe(no_mangle)]
4361pub unsafe extern "C" fn pio_json_value_object_entry_at(
4362    value: *const PioJsonValue,
4363    index: usize,
4364    output: *mut PioJsonObjectEntryView,
4365    error: *mut *mut PioError,
4366) -> bool {
4367    unsafe {
4368        entry(error, false, || {
4369            let value = PioJsonValue::get(value).ok_or_else(|| {
4370                boundary_error(
4371                    &codes::BIND_CAPI_NULL_HANDLE,
4372                    "PioJsonValue must not be NULL",
4373                )
4374            })?;
4375            let values = value
4376                .value()
4377                .and_then(serde_json::Value::as_object)
4378                .ok_or_else(|| {
4379                    boundary_error(
4380                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
4381                        "the structured value is not an object",
4382                    )
4383                })?;
4384            let (key, value) = values.iter().nth(index).ok_or_else(|| {
4385                boundary_error(
4386                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4387                    format!("structured object index {index} is out of range"),
4388                )
4389            })?;
4390            *require_output(output, "output")? = PioJsonObjectEntryView {
4391                key: PioStringView::new(key),
4392                value_kind: PioStringView::new(json_value_kind(value)),
4393            };
4394            Ok(true)
4395        })
4396    }
4397}
4398
4399/// Return one owner-rooted value from a structured JSON object by position.
4400#[unsafe(no_mangle)]
4401pub unsafe extern "C" fn pio_json_value_object_value_at(
4402    value: *const PioJsonValue,
4403    index: usize,
4404    error: *mut *mut PioError,
4405) -> *mut PioJsonValue {
4406    unsafe {
4407        entry(error, std::ptr::null_mut(), || {
4408            let value = PioJsonValue::get(value).ok_or_else(|| {
4409                boundary_error(
4410                    &codes::BIND_CAPI_NULL_HANDLE,
4411                    "PioJsonValue must not be NULL",
4412                )
4413            })?;
4414            let values = value
4415                .value()
4416                .and_then(serde_json::Value::as_object)
4417                .ok_or_else(|| {
4418                    boundary_error(
4419                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
4420                        "the structured value is not an object",
4421                    )
4422                })?;
4423            if values.values().nth(index).is_none() {
4424                return Err(boundary_error(
4425                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
4426                    format!("structured object index {index} is out of range"),
4427                ));
4428            }
4429            Ok(PioJsonValue::new_raw(
4430                value.child(JsonValueStep::Object(index)),
4431            ))
4432        })
4433    }
4434}
4435
4436#[unsafe(no_mangle)]
4437pub unsafe extern "C" fn pio_json_value_retain(value: *const PioJsonValue) -> *mut PioJsonValue {
4438    unsafe { PioJsonValue::retain_raw(value) }
4439}
4440
4441#[unsafe(no_mangle)]
4442pub unsafe extern "C" fn pio_json_value_release(value: *mut PioJsonValue) {
4443    unsafe { PioJsonValue::release_raw(value) };
4444}
4445
4446#[unsafe(no_mangle)]
4447pub unsafe extern "C" fn pio_module_retain(module: *const PioModule) -> *mut PioModule {
4448    unsafe { PioModule::retain_raw(module) }
4449}
4450
4451#[unsafe(no_mangle)]
4452pub unsafe extern "C" fn pio_module_release(module: *mut PioModule) {
4453    unsafe { PioModule::release_raw(module) };
4454}
4455
4456/// Canonical structural type name, such as `powerio.BalancedNetwork`.
4457#[unsafe(no_mangle)]
4458pub unsafe extern "C" fn pio_value_type_name(value: *const PioValueHandle) -> PioStringView {
4459    unsafe { PioValueHandle::get(value) }
4460        .and_then(ValueInner::value)
4461        .map_or(PioStringView::EMPTY, |value| {
4462            PioStringView::new(value.type_name())
4463        })
4464}
4465
4466/// Exact structural type predicate.
4467#[unsafe(no_mangle)]
4468pub unsafe extern "C" fn pio_value_is_type(
4469    value: *const PioValueHandle,
4470    type_name: *const c_char,
4471    type_name_len: usize,
4472) -> bool {
4473    let Some(value) = unsafe { PioValueHandle::get(value) }.and_then(ValueInner::value) else {
4474        return false;
4475    };
4476    let Ok(type_name) = (unsafe { input_str(type_name, type_name_len, "type_name") }) else {
4477        return false;
4478    };
4479    value.type_name() == type_name
4480}
4481
4482/// Borrow the value as a balanced network without serialization or copying.
4483#[unsafe(no_mangle)]
4484pub unsafe extern "C" fn pio_value_balanced_network(
4485    value: *const PioValueHandle,
4486    error: *mut *mut PioError,
4487) -> *mut PioBalancedNetwork {
4488    unsafe {
4489        entry(error, std::ptr::null_mut(), || {
4490            let value = require_value(value)?;
4491            if !matches!(value.value(), Some(PioValue::BalancedNetwork(_))) {
4492                return Err(boundary_error(
4493                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
4494                    "the value is not powerio.BalancedNetwork",
4495                ));
4496            }
4497            Ok(PioBalancedNetwork::new_raw(BalancedNetworkInner {
4498                value: ValueInner {
4499                    owner: Arc::clone(&value.owner),
4500                    steps: value.steps.clone(),
4501                },
4502                projection: BalancedNetworkProjection::Direct,
4503            }))
4504        })
4505    }
4506}
4507
4508/// Take the value as a geographic layer handle. The layer is copied out of
4509/// the value, so the handle outlives the module the way
4510/// `pio_geo_layer_parse` produces one.
4511#[unsafe(no_mangle)]
4512pub unsafe extern "C" fn pio_value_geo_layer(
4513    value: *const PioValueHandle,
4514    error: *mut *mut PioError,
4515) -> *mut PioGeoLayer {
4516    unsafe {
4517        entry(error, std::ptr::null_mut(), || {
4518            let value = require_value(value)?;
4519            let Some(PioValue::GeoLayer(layer)) = value.value() else {
4520                return Err(boundary_error(
4521                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
4522                    "the value is not powerio.GeoLayer",
4523                ));
4524            };
4525            Ok(PioGeoLayer::new_raw(GeoLayerInner {
4526                layer: layer.clone(),
4527                diagnostics: Vec::new(),
4528            }))
4529        })
4530    }
4531}
4532
4533/// Borrow the value as a multiconductor network without serialization or copying.
4534#[unsafe(no_mangle)]
4535pub unsafe extern "C" fn pio_value_multiconductor_network(
4536    value: *const PioValueHandle,
4537    error: *mut *mut PioError,
4538) -> *mut PioMulticonductorNetwork {
4539    unsafe {
4540        entry(error, std::ptr::null_mut(), || {
4541            let value = require_value(value)?;
4542            if !matches!(value.value(), Some(PioValue::MulticonductorNetwork(_))) {
4543                return Err(boundary_error(
4544                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
4545                    "the value is not powerio.MulticonductorNetwork",
4546                ));
4547            }
4548            Ok(PioMulticonductorNetwork::new_raw(
4549                MulticonductorNetworkInner {
4550                    value: ValueInner {
4551                        owner: Arc::clone(&value.owner),
4552                        steps: value.steps.clone(),
4553                    },
4554                    projection: MulticonductorNetworkProjection::Direct,
4555                },
4556            ))
4557        })
4558    }
4559}
4560
4561/// Borrow the value as a time series.
4562#[unsafe(no_mangle)]
4563pub unsafe extern "C" fn pio_value_time_series(
4564    value: *const PioValueHandle,
4565    error: *mut *mut PioError,
4566) -> *mut PioTimeSeriesHandle {
4567    unsafe {
4568        entry(error, std::ptr::null_mut(), || {
4569            let value = require_value(value)?;
4570            if !matches!(value.value(), Some(PioValue::TimeSeries(_))) {
4571                return Err(boundary_error(
4572                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
4573                    "the value is not powerio.TimeSeries<T>",
4574                ));
4575            }
4576            Ok(PioTimeSeriesHandle::new_raw(ValueInner {
4577                owner: Arc::clone(&value.owner),
4578                steps: value.steps.clone(),
4579            }))
4580        })
4581    }
4582}
4583
4584/// Borrow the value as a scenario set.
4585#[unsafe(no_mangle)]
4586pub unsafe extern "C" fn pio_value_scenario_set(
4587    value: *const PioValueHandle,
4588    error: *mut *mut PioError,
4589) -> *mut PioScenarioSetHandle {
4590    unsafe {
4591        entry(error, std::ptr::null_mut(), || {
4592            let value = require_value(value)?;
4593            if !matches!(value.value(), Some(PioValue::ScenarioSet(_))) {
4594                return Err(boundary_error(
4595                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
4596                    "the value is not powerio.ScenarioSet<T>",
4597                ));
4598            }
4599            Ok(PioScenarioSetHandle::new_raw(ValueInner {
4600                owner: Arc::clone(&value.owner),
4601                steps: value.steps.clone(),
4602            }))
4603        })
4604    }
4605}
4606
4607#[derive(Clone, Copy)]
4608enum ExpectedValue {
4609    BalancedOperatingPoint,
4610    MulticonductorOperatingPoint,
4611    DcPfInstance,
4612    AcPfInstance,
4613    DcOpfInstance,
4614    AcOpfInstance,
4615    McAcPfInstance,
4616    McAcOpfInstance,
4617    AcScucInstance,
4618    DcPfSolution,
4619    AcPfSolution,
4620    DcOpfSolution,
4621    AcOpfSolution,
4622    SocwrOpfSolution,
4623    McAcPfSolution,
4624    McAcOpfSolution,
4625    AcScucSolution,
4626}
4627
4628impl ExpectedValue {
4629    fn matches(self, value: &PioValue) -> bool {
4630        matches!(
4631            (self, value),
4632            (
4633                Self::BalancedOperatingPoint,
4634                PioValue::BalancedOperatingPoint(_)
4635            ) | (
4636                Self::MulticonductorOperatingPoint,
4637                PioValue::MulticonductorOperatingPoint(_)
4638            ) | (Self::DcPfInstance, PioValue::DcPfInstance(_))
4639                | (Self::AcPfInstance, PioValue::AcPfInstance(_))
4640                | (Self::DcOpfInstance, PioValue::DcOpfInstance(_))
4641                | (Self::AcOpfInstance, PioValue::AcOpfInstance(_))
4642                | (Self::McAcPfInstance, PioValue::McAcPfInstance(_))
4643                | (Self::McAcOpfInstance, PioValue::McAcOpfInstance(_))
4644                | (Self::AcScucInstance, PioValue::AcScucInstance(_))
4645                | (Self::DcPfSolution, PioValue::DcPfSolution(_))
4646                | (Self::AcPfSolution, PioValue::AcPfSolution(_))
4647                | (Self::DcOpfSolution, PioValue::DcOpfSolution(_))
4648                | (Self::AcOpfSolution, PioValue::AcOpfSolution(_))
4649                | (Self::SocwrOpfSolution, PioValue::SocwrOpfSolution(_))
4650                | (Self::McAcPfSolution, PioValue::McAcPfSolution(_))
4651                | (Self::McAcOpfSolution, PioValue::McAcOpfSolution(_))
4652                | (Self::AcScucSolution, PioValue::AcScucSolution(_))
4653        )
4654    }
4655
4656    fn type_name(self) -> &'static str {
4657        match self {
4658            Self::BalancedOperatingPoint => "powerio.OperatingPoint<powerio.BalancedNetwork>",
4659            Self::MulticonductorOperatingPoint => {
4660                "powerio.OperatingPoint<powerio.MulticonductorNetwork>"
4661            }
4662            Self::DcPfInstance => "powerio.DcPfInstance",
4663            Self::AcPfInstance => "powerio.AcPfInstance",
4664            Self::DcOpfInstance => "powerio.DcOpfInstance",
4665            Self::AcOpfInstance => "powerio.AcOpfInstance",
4666            Self::McAcPfInstance => "powerio.McAcPfInstance",
4667            Self::McAcOpfInstance => "powerio.McAcOpfInstance",
4668            Self::AcScucInstance => "powerio.AcScucInstance",
4669            Self::DcPfSolution => "powerio.DcPfSolution",
4670            Self::AcPfSolution => "powerio.AcPfSolution",
4671            Self::DcOpfSolution => "powerio.DcOpfSolution",
4672            Self::AcOpfSolution => "powerio.AcOpfSolution",
4673            Self::SocwrOpfSolution => "powerio.SocwrOpfSolution",
4674            Self::McAcPfSolution => "powerio.McAcPfSolution",
4675            Self::McAcOpfSolution => "powerio.McAcOpfSolution",
4676            Self::AcScucSolution => "powerio.AcScucSolution",
4677        }
4678    }
4679}
4680
4681unsafe fn checked_typed_value(
4682    value: *const PioValueHandle,
4683    expected: ExpectedValue,
4684) -> Result<ValueInner, *mut PioError> {
4685    let value = unsafe { require_value(value) }?;
4686    if !value.value().is_some_and(|value| expected.matches(value)) {
4687        return Err(boundary_error(
4688            &codes::REQUEST_CAPI_TYPE_MISMATCH,
4689            format!("the value is not {}", expected.type_name()),
4690        ));
4691    }
4692    Ok(ValueInner {
4693        owner: Arc::clone(&value.owner),
4694        steps: value.steps.clone(),
4695    })
4696}
4697
4698unsafe fn operating_point_accessor(
4699    value: *const PioValueHandle,
4700    expected: ExpectedValue,
4701    error: *mut *mut PioError,
4702) -> *mut PioOperatingPoint {
4703    unsafe {
4704        entry(error, std::ptr::null_mut(), || {
4705            let value = checked_typed_value(value, expected)?;
4706            let projection = match expected {
4707                ExpectedValue::BalancedOperatingPoint => OperatingPointProjection::DirectBalanced,
4708                ExpectedValue::MulticonductorOperatingPoint => {
4709                    OperatingPointProjection::DirectMulticonductor
4710                }
4711                _ => {
4712                    return Err(boundary_error(
4713                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
4714                        "the requested value is not an operating point",
4715                    ));
4716                }
4717            };
4718            Ok(PioOperatingPoint::new_raw(OperatingPointInner {
4719                value,
4720                projection,
4721            }))
4722        })
4723    }
4724}
4725
4726unsafe fn instance_accessor(
4727    value: *const PioValueHandle,
4728    expected: ExpectedValue,
4729    error: *mut *mut PioError,
4730) -> *mut PioCalculationInstance {
4731    unsafe {
4732        entry(error, std::ptr::null_mut(), || {
4733            checked_typed_value(value, expected).map(|value| {
4734                PioCalculationInstance::new_raw(CalculationInstanceInner {
4735                    value,
4736                    projection: CalculationInstanceProjection::Direct,
4737                })
4738            })
4739        })
4740    }
4741}
4742
4743unsafe fn solution_accessor(
4744    value: *const PioValueHandle,
4745    expected: ExpectedValue,
4746    error: *mut *mut PioError,
4747) -> *mut PioCalculationSolution {
4748    unsafe {
4749        entry(error, std::ptr::null_mut(), || {
4750            checked_typed_value(value, expected).map(PioCalculationSolution::new_raw)
4751        })
4752    }
4753}
4754
4755#[unsafe(no_mangle)]
4756pub unsafe extern "C" fn pio_value_balanced_operating_point(
4757    value: *const PioValueHandle,
4758    error: *mut *mut PioError,
4759) -> *mut PioOperatingPoint {
4760    unsafe { operating_point_accessor(value, ExpectedValue::BalancedOperatingPoint, error) }
4761}
4762
4763#[unsafe(no_mangle)]
4764pub unsafe extern "C" fn pio_value_multiconductor_operating_point(
4765    value: *const PioValueHandle,
4766    error: *mut *mut PioError,
4767) -> *mut PioOperatingPoint {
4768    unsafe { operating_point_accessor(value, ExpectedValue::MulticonductorOperatingPoint, error) }
4769}
4770
4771#[unsafe(no_mangle)]
4772pub unsafe extern "C" fn pio_value_dc_pf_instance(
4773    value: *const PioValueHandle,
4774    error: *mut *mut PioError,
4775) -> *mut PioCalculationInstance {
4776    unsafe { instance_accessor(value, ExpectedValue::DcPfInstance, error) }
4777}
4778
4779#[unsafe(no_mangle)]
4780pub unsafe extern "C" fn pio_value_ac_pf_instance(
4781    value: *const PioValueHandle,
4782    error: *mut *mut PioError,
4783) -> *mut PioCalculationInstance {
4784    unsafe { instance_accessor(value, ExpectedValue::AcPfInstance, error) }
4785}
4786
4787#[unsafe(no_mangle)]
4788pub unsafe extern "C" fn pio_value_dc_opf_instance(
4789    value: *const PioValueHandle,
4790    error: *mut *mut PioError,
4791) -> *mut PioCalculationInstance {
4792    unsafe { instance_accessor(value, ExpectedValue::DcOpfInstance, error) }
4793}
4794
4795#[unsafe(no_mangle)]
4796pub unsafe extern "C" fn pio_value_ac_opf_instance(
4797    value: *const PioValueHandle,
4798    error: *mut *mut PioError,
4799) -> *mut PioCalculationInstance {
4800    unsafe { instance_accessor(value, ExpectedValue::AcOpfInstance, error) }
4801}
4802
4803#[unsafe(no_mangle)]
4804pub unsafe extern "C" fn pio_value_mc_ac_pf_instance(
4805    value: *const PioValueHandle,
4806    error: *mut *mut PioError,
4807) -> *mut PioCalculationInstance {
4808    unsafe { instance_accessor(value, ExpectedValue::McAcPfInstance, error) }
4809}
4810
4811#[unsafe(no_mangle)]
4812pub unsafe extern "C" fn pio_value_mc_ac_opf_instance(
4813    value: *const PioValueHandle,
4814    error: *mut *mut PioError,
4815) -> *mut PioCalculationInstance {
4816    unsafe { instance_accessor(value, ExpectedValue::McAcOpfInstance, error) }
4817}
4818
4819#[unsafe(no_mangle)]
4820pub unsafe extern "C" fn pio_value_ac_scuc_instance(
4821    value: *const PioValueHandle,
4822    error: *mut *mut PioError,
4823) -> *mut PioCalculationInstance {
4824    unsafe { instance_accessor(value, ExpectedValue::AcScucInstance, error) }
4825}
4826
4827#[unsafe(no_mangle)]
4828pub unsafe extern "C" fn pio_value_dc_pf_solution(
4829    value: *const PioValueHandle,
4830    error: *mut *mut PioError,
4831) -> *mut PioCalculationSolution {
4832    unsafe { solution_accessor(value, ExpectedValue::DcPfSolution, error) }
4833}
4834
4835#[unsafe(no_mangle)]
4836pub unsafe extern "C" fn pio_value_ac_pf_solution(
4837    value: *const PioValueHandle,
4838    error: *mut *mut PioError,
4839) -> *mut PioCalculationSolution {
4840    unsafe { solution_accessor(value, ExpectedValue::AcPfSolution, error) }
4841}
4842
4843#[unsafe(no_mangle)]
4844pub unsafe extern "C" fn pio_value_dc_opf_solution(
4845    value: *const PioValueHandle,
4846    error: *mut *mut PioError,
4847) -> *mut PioCalculationSolution {
4848    unsafe { solution_accessor(value, ExpectedValue::DcOpfSolution, error) }
4849}
4850
4851#[unsafe(no_mangle)]
4852pub unsafe extern "C" fn pio_value_ac_opf_solution(
4853    value: *const PioValueHandle,
4854    error: *mut *mut PioError,
4855) -> *mut PioCalculationSolution {
4856    unsafe { solution_accessor(value, ExpectedValue::AcOpfSolution, error) }
4857}
4858
4859#[unsafe(no_mangle)]
4860pub unsafe extern "C" fn pio_value_socwr_opf_solution(
4861    value: *const PioValueHandle,
4862    error: *mut *mut PioError,
4863) -> *mut PioCalculationSolution {
4864    unsafe { solution_accessor(value, ExpectedValue::SocwrOpfSolution, error) }
4865}
4866
4867#[unsafe(no_mangle)]
4868pub unsafe extern "C" fn pio_value_mc_ac_pf_solution(
4869    value: *const PioValueHandle,
4870    error: *mut *mut PioError,
4871) -> *mut PioCalculationSolution {
4872    unsafe { solution_accessor(value, ExpectedValue::McAcPfSolution, error) }
4873}
4874
4875#[unsafe(no_mangle)]
4876pub unsafe extern "C" fn pio_value_mc_ac_opf_solution(
4877    value: *const PioValueHandle,
4878    error: *mut *mut PioError,
4879) -> *mut PioCalculationSolution {
4880    unsafe { solution_accessor(value, ExpectedValue::McAcOpfSolution, error) }
4881}
4882
4883#[unsafe(no_mangle)]
4884pub unsafe extern "C" fn pio_value_ac_scuc_solution(
4885    value: *const PioValueHandle,
4886    error: *mut *mut PioError,
4887) -> *mut PioCalculationSolution {
4888    unsafe { solution_accessor(value, ExpectedValue::AcScucSolution, error) }
4889}
4890
4891#[unsafe(no_mangle)]
4892pub unsafe extern "C" fn pio_operating_point_type_name(
4893    point: *const PioOperatingPoint,
4894) -> PioStringView {
4895    unsafe { PioOperatingPoint::get(point) }
4896        .and_then(OperatingPointInner::type_name)
4897        .map_or(PioStringView::EMPTY, PioStringView::new)
4898}
4899
4900#[unsafe(no_mangle)]
4901pub unsafe extern "C" fn pio_calculation_instance_type_name(
4902    instance: *const PioCalculationInstance,
4903) -> PioStringView {
4904    unsafe { PioCalculationInstance::get(instance) }
4905        .and_then(CalculationInstanceInner::type_name)
4906        .map_or(PioStringView::EMPTY, PioStringView::new)
4907}
4908
4909#[unsafe(no_mangle)]
4910pub unsafe extern "C" fn pio_calculation_solution_type_name(
4911    solution: *const PioCalculationSolution,
4912) -> PioStringView {
4913    unsafe { PioCalculationSolution::get(solution) }
4914        .and_then(ValueInner::value)
4915        .map_or(PioStringView::EMPTY, |value| {
4916            PioStringView::new(value.type_name())
4917        })
4918}
4919
4920/// Return an owner-rooted view of the exact calculation instance retained by
4921/// a calculation solution.
4922#[unsafe(no_mangle)]
4923pub unsafe extern "C" fn pio_calculation_solution_instance(
4924    solution: *const PioCalculationSolution,
4925    error: *mut *mut PioError,
4926) -> *mut PioCalculationInstance {
4927    unsafe {
4928        entry(error, std::ptr::null_mut(), || {
4929            let solution = PioCalculationSolution::get(solution).ok_or_else(|| {
4930                boundary_error(
4931                    &codes::BIND_CAPI_NULL_HANDLE,
4932                    "PioCalculationSolution must not be NULL",
4933                )
4934            })?;
4935            let projection = match solution.value() {
4936                Some(PioValue::DcPfSolution(_)) => CalculationInstanceProjection::DcPfSolution,
4937                Some(PioValue::AcPfSolution(_)) => CalculationInstanceProjection::AcPfSolution,
4938                Some(PioValue::DcOpfSolution(_)) => CalculationInstanceProjection::DcOpfSolution,
4939                Some(PioValue::AcOpfSolution(_)) => CalculationInstanceProjection::AcOpfSolution,
4940                Some(PioValue::SocwrOpfSolution(_)) => {
4941                    CalculationInstanceProjection::SocwrOpfSolution
4942                }
4943                Some(PioValue::McAcPfSolution(_)) => CalculationInstanceProjection::McAcPfSolution,
4944                Some(PioValue::McAcOpfSolution(_)) => {
4945                    CalculationInstanceProjection::McAcOpfSolution
4946                }
4947                Some(PioValue::AcScucSolution(_)) => CalculationInstanceProjection::AcScucSolution,
4948                _ => {
4949                    return Err(boundary_error(
4950                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
4951                        "the handle does not refer to a calculation solution",
4952                    ));
4953                }
4954            };
4955            Ok(PioCalculationInstance::new_raw(CalculationInstanceInner {
4956                value: ValueInner {
4957                    owner: Arc::clone(&solution.owner),
4958                    steps: solution.steps.clone(),
4959                },
4960                projection,
4961            }))
4962        })
4963    }
4964}
4965
4966fn make_balanced_network_view(
4967    value: &ValueInner,
4968    projection: BalancedNetworkProjection,
4969) -> *mut PioBalancedNetwork {
4970    PioBalancedNetwork::new_raw(BalancedNetworkInner {
4971        value: ValueInner {
4972            owner: Arc::clone(&value.owner),
4973            steps: value.steps.clone(),
4974        },
4975        projection,
4976    })
4977}
4978
4979fn make_multiconductor_network_view(
4980    value: &ValueInner,
4981    projection: MulticonductorNetworkProjection,
4982) -> *mut PioMulticonductorNetwork {
4983    PioMulticonductorNetwork::new_raw(MulticonductorNetworkInner {
4984        value: ValueInner {
4985            owner: Arc::clone(&value.owner),
4986            steps: value.steps.clone(),
4987        },
4988        projection,
4989    })
4990}
4991
4992#[unsafe(no_mangle)]
4993pub unsafe extern "C" fn pio_operating_point_balanced_network(
4994    point: *const PioOperatingPoint,
4995    error: *mut *mut PioError,
4996) -> *mut PioBalancedNetwork {
4997    unsafe {
4998        entry(error, std::ptr::null_mut(), || {
4999            let point = PioOperatingPoint::get(point).ok_or_else(|| {
5000                boundary_error(
5001                    &codes::BIND_CAPI_NULL_HANDLE,
5002                    "PioOperatingPoint must not be NULL",
5003                )
5004            })?;
5005            let projection = point.balanced_network_projection().ok_or_else(|| {
5006                boundary_error(
5007                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
5008                    "the operating point does not use powerio.BalancedNetwork",
5009                )
5010            })?;
5011            Ok(make_balanced_network_view(&point.value, projection))
5012        })
5013    }
5014}
5015
5016#[unsafe(no_mangle)]
5017pub unsafe extern "C" fn pio_operating_point_multiconductor_network(
5018    point: *const PioOperatingPoint,
5019    error: *mut *mut PioError,
5020) -> *mut PioMulticonductorNetwork {
5021    unsafe {
5022        entry(error, std::ptr::null_mut(), || {
5023            let point = PioOperatingPoint::get(point).ok_or_else(|| {
5024                boundary_error(
5025                    &codes::BIND_CAPI_NULL_HANDLE,
5026                    "PioOperatingPoint must not be NULL",
5027                )
5028            })?;
5029            let projection = point.multiconductor_network_projection().ok_or_else(|| {
5030                boundary_error(
5031                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
5032                    "the operating point does not use powerio.MulticonductorNetwork",
5033                )
5034            })?;
5035            Ok(make_multiconductor_network_view(&point.value, projection))
5036        })
5037    }
5038}
5039
5040#[unsafe(no_mangle)]
5041pub unsafe extern "C" fn pio_calculation_instance_balanced_network(
5042    instance: *const PioCalculationInstance,
5043    error: *mut *mut PioError,
5044) -> *mut PioBalancedNetwork {
5045    unsafe {
5046        entry(error, std::ptr::null_mut(), || {
5047            let instance = PioCalculationInstance::get(instance).ok_or_else(|| {
5048                boundary_error(
5049                    &codes::BIND_CAPI_NULL_HANDLE,
5050                    "PioCalculationInstance must not be NULL",
5051                )
5052            })?;
5053            let projection = match instance.projection {
5054                CalculationInstanceProjection::Direct => match instance.instance() {
5055                    Some(CalculationInstanceRef::DcPf(_)) => {
5056                        BalancedNetworkProjection::DcPfInstance
5057                    }
5058                    Some(CalculationInstanceRef::AcPf(_)) => {
5059                        BalancedNetworkProjection::AcPfInstance
5060                    }
5061                    Some(CalculationInstanceRef::DcOpf(_)) => {
5062                        BalancedNetworkProjection::DcOpfInstance
5063                    }
5064                    Some(CalculationInstanceRef::AcOpf(_)) => {
5065                        BalancedNetworkProjection::AcOpfInstance
5066                    }
5067                    Some(CalculationInstanceRef::AcScuc(_)) => {
5068                        BalancedNetworkProjection::AcScucInstance
5069                    }
5070                    _ => {
5071                        return Err(boundary_error(
5072                            &codes::REQUEST_CAPI_TYPE_MISMATCH,
5073                            "the calculation instance does not use powerio.BalancedNetwork",
5074                        ));
5075                    }
5076                },
5077                CalculationInstanceProjection::DcPfSolution => {
5078                    BalancedNetworkProjection::DcPfSolution
5079                }
5080                CalculationInstanceProjection::AcPfSolution => {
5081                    BalancedNetworkProjection::AcPfSolution
5082                }
5083                CalculationInstanceProjection::DcOpfSolution => {
5084                    BalancedNetworkProjection::DcOpfSolution
5085                }
5086                CalculationInstanceProjection::AcOpfSolution => {
5087                    BalancedNetworkProjection::AcOpfSolution
5088                }
5089                CalculationInstanceProjection::SocwrOpfSolution => {
5090                    BalancedNetworkProjection::SocwrOpfSolution
5091                }
5092                CalculationInstanceProjection::AcScucSolution => {
5093                    BalancedNetworkProjection::AcScucSolution
5094                }
5095                _ => {
5096                    return Err(boundary_error(
5097                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
5098                        "the calculation instance does not use powerio.BalancedNetwork",
5099                    ));
5100                }
5101            };
5102            Ok(make_balanced_network_view(&instance.value, projection))
5103        })
5104    }
5105}
5106
5107#[unsafe(no_mangle)]
5108pub unsafe extern "C" fn pio_calculation_instance_multiconductor_network(
5109    instance: *const PioCalculationInstance,
5110    error: *mut *mut PioError,
5111) -> *mut PioMulticonductorNetwork {
5112    unsafe {
5113        entry(error, std::ptr::null_mut(), || {
5114            let instance = PioCalculationInstance::get(instance).ok_or_else(|| {
5115                boundary_error(
5116                    &codes::BIND_CAPI_NULL_HANDLE,
5117                    "PioCalculationInstance must not be NULL",
5118                )
5119            })?;
5120            let projection = match instance.projection {
5121                CalculationInstanceProjection::Direct => match instance.instance() {
5122                    Some(CalculationInstanceRef::McAcPf(_)) => {
5123                        MulticonductorNetworkProjection::McAcPfInstance
5124                    }
5125                    Some(CalculationInstanceRef::McAcOpf(_)) => {
5126                        MulticonductorNetworkProjection::McAcOpfInstance
5127                    }
5128                    _ => {
5129                        return Err(boundary_error(
5130                            &codes::REQUEST_CAPI_TYPE_MISMATCH,
5131                            "the calculation instance does not use powerio.MulticonductorNetwork",
5132                        ));
5133                    }
5134                },
5135                CalculationInstanceProjection::McAcPfSolution => {
5136                    MulticonductorNetworkProjection::McAcPfSolution
5137                }
5138                CalculationInstanceProjection::McAcOpfSolution => {
5139                    MulticonductorNetworkProjection::McAcOpfSolution
5140                }
5141                _ => {
5142                    return Err(boundary_error(
5143                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
5144                        "the calculation instance does not use powerio.MulticonductorNetwork",
5145                    ));
5146                }
5147            };
5148            Ok(make_multiconductor_network_view(
5149                &instance.value,
5150                projection,
5151            ))
5152        })
5153    }
5154}
5155
5156fn branch_susceptance_formula_name(formula: BranchSusceptanceFormula) -> &'static str {
5157    match formula {
5158        BranchSusceptanceFormula::ReactanceOnly => "reactance_only",
5159        BranchSusceptanceFormula::TapAdjustedReactance => "tap_adjusted_reactance",
5160        BranchSusceptanceFormula::SeriesSusceptance => "series_susceptance",
5161        _ => "unknown",
5162    }
5163}
5164
5165unsafe fn require_calculation_instance<'a>(
5166    instance: *const PioCalculationInstance,
5167) -> Result<&'a CalculationInstanceInner, *mut PioError> {
5168    unsafe { PioCalculationInstance::get(instance) }.ok_or_else(|| {
5169        boundary_error(
5170            &codes::BIND_CAPI_NULL_HANDLE,
5171            "PioCalculationInstance must not be NULL",
5172        )
5173    })
5174}
5175
5176fn opf_preparation_units(name: &str) -> Result<Units, *mut PioError> {
5177    match name {
5178        "per_unit" => Ok(Units::PerUnit),
5179        "native" => Ok(Units::Native),
5180        _ => Err(boundary_error(
5181            &codes::BIND_CAPI_INVALID_OPTIONS,
5182            format!("unknown OPF preparation units '{name}'; expected 'per_unit' or 'native'"),
5183        )),
5184    }
5185}
5186
5187fn opf_preparation_units_name(units: Units) -> Option<&'static str> {
5188    match units {
5189        Units::PerUnit => Some("per_unit"),
5190        Units::Native => Some("native"),
5191        _ => None,
5192    }
5193}
5194
5195fn prepared_objective_name(objective: PreparedObjective) -> Option<&'static str> {
5196    match objective {
5197        PreparedObjective::Feasibility => Some("feasibility"),
5198        PreparedObjective::NetworkGeneratorCost => Some("network_generator_cost"),
5199        _ => None,
5200    }
5201}
5202
5203fn opf_analysis_branch_source(
5204    source: AnalysisBranchSource,
5205    preparation: &str,
5206) -> Result<(&'static str, usize, Option<usize>), *mut PioError> {
5207    match source {
5208        AnalysisBranchSource::Branch { row } => Ok(("branch", row, None)),
5209        AnalysisBranchSource::ThreeWindingTransformerWinding {
5210            transformer_row,
5211            winding,
5212        } => Ok((
5213            "three_winding_transformer_winding",
5214            transformer_row,
5215            Some(winding),
5216        )),
5217        _ => Err(boundary_error(
5218            &codes::REQUEST_CAPI_TYPE_MISMATCH,
5219            format!(
5220                "the {preparation} OPF preparation has an analysis branch source unsupported by ABI 7"
5221            ),
5222        )),
5223    }
5224}
5225
5226/// Build the matrix free DC OPF inputs from one typed instance.
5227#[unsafe(no_mangle)]
5228pub unsafe extern "C" fn pio_build_dc_opf_preparation(
5229    instance: *const PioCalculationInstance,
5230    units: *const c_char,
5231    units_len: usize,
5232    skip_zero_impedance: bool,
5233    synthesize_unrated_limits: bool,
5234    correct_angle_difference_bounds: bool,
5235    error: *mut *mut PioError,
5236) -> *mut PioDcOpfPreparation {
5237    unsafe {
5238        entry(error, std::ptr::null_mut(), || {
5239            let instance = require_calculation_instance(instance)?
5240                .dc_opf()
5241                .ok_or_else(|| {
5242                    boundary_error(
5243                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
5244                        "DC OPF preparation requires powerio.DcOpfInstance",
5245                    )
5246                })?;
5247            let units = opf_preparation_units(required_str(units, units_len, "units")?)?;
5248            let options = DcOpfAssemblyOptions::default()
5249                .with_units(units)
5250                .with_skip_zero_impedance(skip_zero_impedance)
5251                .with_synthesize_unrated_limits(synthesize_unrated_limits)
5252                .with_correct_angle_difference_bounds(correct_angle_difference_bounds);
5253            build_dc_opf_preparation(instance, &options)
5254                .map(PioDcOpfPreparation::new_raw)
5255                .map_err(|failure| error_from_matrix(&failure))
5256        })
5257    }
5258}
5259
5260/// Read the dimensions and conventions of a DC OPF preparation.
5261#[unsafe(no_mangle)]
5262pub unsafe extern "C" fn pio_dc_opf_preparation_summary(
5263    preparation: *const PioDcOpfPreparation,
5264    output: *mut PioDcOpfPreparationView,
5265    error: *mut *mut PioError,
5266) -> bool {
5267    unsafe {
5268        entry(error, false, || {
5269            let preparation = PioDcOpfPreparation::get(preparation).ok_or_else(|| {
5270                boundary_error(
5271                    &codes::BIND_CAPI_NULL_HANDLE,
5272                    "PioDcOpfPreparation must not be NULL",
5273                )
5274            })?;
5275            let units = opf_preparation_units_name(preparation.units).ok_or_else(|| {
5276                boundary_error(
5277                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
5278                    "the DC OPF preparation uses units unsupported by ABI 7",
5279                )
5280            })?;
5281            let objective = prepared_objective_name(preparation.objective).ok_or_else(|| {
5282                boundary_error(
5283                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
5284                    "the DC OPF preparation uses an objective unsupported by ABI 7",
5285                )
5286            })?;
5287            *require_output(output, "output")? = PioDcOpfPreparationView {
5288                name: PioStringView::new(&preparation.name),
5289                bus_count: preparation.n_buses,
5290                generator_count: preparation.n_generators(),
5291                branch_count: preparation.n_branches(),
5292                source_generator_count: preparation.n_source_generators,
5293                source_branch_count: preparation.n_source_branches,
5294                base_mva: preparation.base_mva,
5295                units: PioStringView::new(units),
5296                branch_susceptance_formula: PioStringView::new(branch_susceptance_formula_name(
5297                    preparation.formula,
5298                )),
5299                objective: PioStringView::new(objective),
5300                skip_zero_impedance: preparation.skip_zero_impedance,
5301                synthesize_unrated_limits: preparation.synthesize_unrated_limits,
5302                correct_angle_difference_bounds: preparation.correct_angle_difference_bounds,
5303                reference_bus_count: preparation.reference_buses.len(),
5304                skipped_zero_impedance_count: preparation.branches.skipped_zero_impedance.len(),
5305            };
5306            Ok(true)
5307        })
5308    }
5309}
5310
5311/// Borrow the dense reference bus indices of a DC OPF preparation.
5312#[unsafe(no_mangle)]
5313pub unsafe extern "C" fn pio_dc_opf_preparation_reference_buses(
5314    preparation: *const PioDcOpfPreparation,
5315) -> PioSizeView {
5316    unsafe { PioDcOpfPreparation::get(preparation) }.map_or(PioSizeView::EMPTY, |value| {
5317        PioSizeView::new(value.reference_buses.as_ref())
5318    })
5319}
5320
5321/// Borrow the analysis rows skipped for zero impedance.
5322#[unsafe(no_mangle)]
5323pub unsafe extern "C" fn pio_dc_opf_preparation_skipped_zero_impedance(
5324    preparation: *const PioDcOpfPreparation,
5325) -> PioSizeView {
5326    unsafe { PioDcOpfPreparation::get(preparation) }.map_or(PioSizeView::EMPTY, |value| {
5327        PioSizeView::new(&value.branches.skipped_zero_impedance)
5328    })
5329}
5330
5331/// Read one dense bus row of a DC OPF preparation.
5332#[unsafe(no_mangle)]
5333pub unsafe extern "C" fn pio_dc_opf_preparation_bus_at(
5334    preparation: *const PioDcOpfPreparation,
5335    index: usize,
5336    output: *mut PioDcOpfBusView,
5337    error: *mut *mut PioError,
5338) -> bool {
5339    unsafe {
5340        entry(error, false, || {
5341            let value = PioDcOpfPreparation::get(preparation).ok_or_else(|| {
5342                boundary_error(
5343                    &codes::BIND_CAPI_NULL_HANDLE,
5344                    "PioDcOpfPreparation must not be NULL",
5345                )
5346            })?;
5347            if index >= value.n_buses {
5348                return Err(boundary_error(
5349                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
5350                    format!("DC OPF preparation bus index {index} is out of range"),
5351                ));
5352            }
5353            let source_row = value.bus_source_rows[index];
5354            *require_output(output, "output")? = PioDcOpfBusView {
5355                bus_id: value.bus_ids[index].0,
5356                analysis_row: value.bus_analysis_rows[index],
5357                source_row: source_row.unwrap_or(0),
5358                has_source_row: source_row.is_some(),
5359                active_power_demand: value.p_d[index],
5360                shunt_conductance: value.g_s[index],
5361                phase_shift_injection: value.p_shift[index],
5362            };
5363            Ok(true)
5364        })
5365    }
5366}
5367
5368/// Read one generator row of a DC OPF preparation.
5369#[unsafe(no_mangle)]
5370pub unsafe extern "C" fn pio_dc_opf_preparation_generator_at(
5371    preparation: *const PioDcOpfPreparation,
5372    index: usize,
5373    output: *mut PioDcOpfGeneratorView,
5374    error: *mut *mut PioError,
5375) -> bool {
5376    unsafe {
5377        entry(error, false, || {
5378            let value = PioDcOpfPreparation::get(preparation).ok_or_else(|| {
5379                boundary_error(
5380                    &codes::BIND_CAPI_NULL_HANDLE,
5381                    "PioDcOpfPreparation must not be NULL",
5382                )
5383            })?;
5384            if index >= value.n_generators() {
5385                return Err(boundary_error(
5386                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
5387                    format!("DC OPF preparation generator index {index} is out of range"),
5388                ));
5389            }
5390            let source_row = value.generators.source_rows[index];
5391            let piecewise = value.generators.piecewise_linear[index].as_ref();
5392            *require_output(output, "output")? = PioDcOpfGeneratorView {
5393                component_id: PioStringView::new(&value.generators.identities[index]),
5394                bus_index: value.generators.bus_of_gen[index],
5395                analysis_row: value.generators.analysis_rows[index],
5396                source_row: source_row.unwrap_or(0),
5397                has_source_row: source_row.is_some(),
5398                quadratic_cost: value.generators.q[index],
5399                linear_cost: value.generators.c[index],
5400                constant_cost: value.generators.c0[index],
5401                has_piecewise_linear_cost: piecewise.is_some(),
5402                piecewise_linear_power: piecewise
5403                    .map_or(PioF64View::EMPTY, |cost| PioF64View::new(&cost.power)),
5404                piecewise_linear_value: piecewise
5405                    .map_or(PioF64View::EMPTY, |cost| PioF64View::new(&cost.value)),
5406                active_power_max: value.generators.pmax[index],
5407                active_power_min: value.generators.pmin[index],
5408                capability_active: value.generators.capability_active[index],
5409            };
5410            Ok(true)
5411        })
5412    }
5413}
5414
5415/// Read one active branch row of a DC OPF preparation.
5416#[unsafe(no_mangle)]
5417pub unsafe extern "C" fn pio_dc_opf_preparation_branch_at(
5418    preparation: *const PioDcOpfPreparation,
5419    index: usize,
5420    output: *mut PioDcOpfBranchView,
5421    error: *mut *mut PioError,
5422) -> bool {
5423    unsafe {
5424        entry(error, false, || {
5425            let value = PioDcOpfPreparation::get(preparation).ok_or_else(|| {
5426                boundary_error(
5427                    &codes::BIND_CAPI_NULL_HANDLE,
5428                    "PioDcOpfPreparation must not be NULL",
5429                )
5430            })?;
5431            if index >= value.n_branches() {
5432                return Err(boundary_error(
5433                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
5434                    format!("DC OPF preparation branch index {index} is out of range"),
5435                ));
5436            }
5437            let (source_kind, source_row, winding) =
5438                opf_analysis_branch_source(value.branches.analysis_sources[index], "DC")?;
5439            *require_output(output, "output")? = PioDcOpfBranchView {
5440                component_id: PioStringView::new(&value.branches.identities[index]),
5441                from_bus_index: value.branches.from_bus[index],
5442                to_bus_index: value.branches.to_bus[index],
5443                susceptance_magnitude: value.branches.susceptance_magnitude[index],
5444                phase_shift_radians: value.branches.shift[index],
5445                active_power_max: value.branches.f_max[index],
5446                angle_difference_min_radians: value.branches.angle_min[index],
5447                angle_difference_max_radians: value.branches.angle_max[index],
5448                analysis_row: value.branches.analysis_rows[index],
5449                source_kind: PioStringView::new(source_kind),
5450                source_row,
5451                winding: winding.unwrap_or(0),
5452                has_winding: winding.is_some(),
5453                thermal_limit_active: value.branches.thermal_limit_active[index],
5454                angle_bound_active: value.branches.angle_bound_active[index],
5455            };
5456            Ok(true)
5457        })
5458    }
5459}
5460
5461/// Build the matrix free AC OPF inputs from one typed instance.
5462#[unsafe(no_mangle)]
5463pub unsafe extern "C" fn pio_build_ac_opf_preparation(
5464    instance: *const PioCalculationInstance,
5465    units: *const c_char,
5466    units_len: usize,
5467    skip_zero_impedance: bool,
5468    synthesize_unrated_limits: bool,
5469    correct_angle_difference_bounds: bool,
5470    error: *mut *mut PioError,
5471) -> *mut PioAcOpfPreparation {
5472    unsafe {
5473        entry(error, std::ptr::null_mut(), || {
5474            let instance = require_calculation_instance(instance)?
5475                .ac_opf()
5476                .ok_or_else(|| {
5477                    boundary_error(
5478                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
5479                        "AC OPF preparation requires powerio.AcOpfInstance",
5480                    )
5481                })?;
5482            let units = opf_preparation_units(required_str(units, units_len, "units")?)?;
5483            let options = AcOpfAssemblyOptions::default()
5484                .with_units(units)
5485                .with_skip_zero_impedance(skip_zero_impedance)
5486                .with_synthesize_unrated_limits(synthesize_unrated_limits)
5487                .with_correct_angle_difference_bounds(correct_angle_difference_bounds);
5488            build_ac_opf_preparation(instance, &options)
5489                .map(PioAcOpfPreparation::new_raw)
5490                .map_err(|failure| error_from_matrix(&failure))
5491        })
5492    }
5493}
5494
5495/// Read the dimensions and conventions of an AC OPF preparation.
5496#[unsafe(no_mangle)]
5497pub unsafe extern "C" fn pio_ac_opf_preparation_summary(
5498    preparation: *const PioAcOpfPreparation,
5499    output: *mut PioAcOpfPreparationView,
5500    error: *mut *mut PioError,
5501) -> bool {
5502    unsafe {
5503        entry(error, false, || {
5504            let preparation = PioAcOpfPreparation::get(preparation).ok_or_else(|| {
5505                boundary_error(
5506                    &codes::BIND_CAPI_NULL_HANDLE,
5507                    "PioAcOpfPreparation must not be NULL",
5508                )
5509            })?;
5510            let units = opf_preparation_units_name(preparation.units).ok_or_else(|| {
5511                boundary_error(
5512                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
5513                    "the AC OPF preparation uses units unsupported by ABI 7",
5514                )
5515            })?;
5516            let objective = prepared_objective_name(preparation.objective).ok_or_else(|| {
5517                boundary_error(
5518                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
5519                    "the AC OPF preparation uses an objective unsupported by ABI 7",
5520                )
5521            })?;
5522            *require_output(output, "output")? = PioAcOpfPreparationView {
5523                name: PioStringView::new(&preparation.name),
5524                bus_count: preparation.n_buses,
5525                generator_count: preparation.n_generators(),
5526                storage_count: preparation.n_storage(),
5527                branch_count: preparation.n_branches(),
5528                source_generator_count: preparation.n_source_generators,
5529                source_branch_count: preparation.n_source_branches,
5530                base_mva: preparation.base_mva,
5531                units: PioStringView::new(units),
5532                objective: PioStringView::new(objective),
5533                skip_zero_impedance: preparation.skip_zero_impedance,
5534                synthesize_unrated_limits: preparation.synthesize_unrated_limits,
5535                correct_angle_difference_bounds: preparation.correct_angle_difference_bounds,
5536                reference_bus_count: preparation.reference_buses.len(),
5537                skipped_zero_impedance_count: preparation.branches.skipped_zero_impedance.len(),
5538            };
5539            Ok(true)
5540        })
5541    }
5542}
5543
5544/// Borrow the dense reference bus indices of an AC OPF preparation.
5545#[unsafe(no_mangle)]
5546pub unsafe extern "C" fn pio_ac_opf_preparation_reference_buses(
5547    preparation: *const PioAcOpfPreparation,
5548) -> PioSizeView {
5549    unsafe { PioAcOpfPreparation::get(preparation) }.map_or(PioSizeView::EMPTY, |value| {
5550        PioSizeView::new(value.reference_buses.as_ref())
5551    })
5552}
5553
5554/// Borrow the analysis rows skipped for zero impedance.
5555#[unsafe(no_mangle)]
5556pub unsafe extern "C" fn pio_ac_opf_preparation_skipped_zero_impedance(
5557    preparation: *const PioAcOpfPreparation,
5558) -> PioSizeView {
5559    unsafe { PioAcOpfPreparation::get(preparation) }.map_or(PioSizeView::EMPTY, |value| {
5560        PioSizeView::new(&value.branches.skipped_zero_impedance)
5561    })
5562}
5563
5564/// Read one dense bus row of an AC OPF preparation.
5565#[unsafe(no_mangle)]
5566pub unsafe extern "C" fn pio_ac_opf_preparation_bus_at(
5567    preparation: *const PioAcOpfPreparation,
5568    index: usize,
5569    output: *mut PioAcOpfBusView,
5570    error: *mut *mut PioError,
5571) -> bool {
5572    unsafe {
5573        entry(error, false, || {
5574            let value = PioAcOpfPreparation::get(preparation).ok_or_else(|| {
5575                boundary_error(
5576                    &codes::BIND_CAPI_NULL_HANDLE,
5577                    "PioAcOpfPreparation must not be NULL",
5578                )
5579            })?;
5580            if index >= value.n_buses {
5581                return Err(boundary_error(
5582                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
5583                    format!("AC OPF preparation bus index {index} is out of range"),
5584                ));
5585            }
5586            let source_row = value.bus_source_rows[index];
5587            *require_output(output, "output")? = PioAcOpfBusView {
5588                bus_id: value.bus_ids[index].0,
5589                analysis_row: value.bus_analysis_rows[index],
5590                source_row: source_row.unwrap_or(0),
5591                has_source_row: source_row.is_some(),
5592                active_power_demand: value.buses.p_d[index],
5593                reactive_power_demand: value.buses.q_d[index],
5594                shunt_conductance: value.buses.g_s[index],
5595                shunt_susceptance: value.buses.b_s[index],
5596                voltage_magnitude_min_pu: value.buses.vm_min[index],
5597                voltage_magnitude_max_pu: value.buses.vm_max[index],
5598                initial_voltage_magnitude_pu: value.buses.initial_vm[index],
5599                initial_voltage_angle_radians: value.buses.initial_va[index],
5600                voltage_bound_active: value.buses.voltage_bound_active[index],
5601            };
5602            Ok(true)
5603        })
5604    }
5605}
5606
5607/// Read one generator row of an AC OPF preparation.
5608#[unsafe(no_mangle)]
5609pub unsafe extern "C" fn pio_ac_opf_preparation_generator_at(
5610    preparation: *const PioAcOpfPreparation,
5611    index: usize,
5612    output: *mut PioAcOpfGeneratorView,
5613    error: *mut *mut PioError,
5614) -> bool {
5615    unsafe {
5616        entry(error, false, || {
5617            let value = PioAcOpfPreparation::get(preparation).ok_or_else(|| {
5618                boundary_error(
5619                    &codes::BIND_CAPI_NULL_HANDLE,
5620                    "PioAcOpfPreparation must not be NULL",
5621                )
5622            })?;
5623            if index >= value.n_generators() {
5624                return Err(boundary_error(
5625                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
5626                    format!("AC OPF preparation generator index {index} is out of range"),
5627                ));
5628            }
5629            let source_row = value.generators.source_rows[index];
5630            let piecewise = value.generators.piecewise_linear[index].as_ref();
5631            *require_output(output, "output")? = PioAcOpfGeneratorView {
5632                component_id: PioStringView::new(&value.generators.identities[index]),
5633                bus_index: value.generators.bus_of_gen[index],
5634                analysis_row: value.generators.analysis_rows[index],
5635                source_row: source_row.unwrap_or(0),
5636                has_source_row: source_row.is_some(),
5637                quadratic_cost: value.generators.q[index],
5638                linear_cost: value.generators.c[index],
5639                constant_cost: value.generators.c0[index],
5640                has_piecewise_linear_cost: piecewise.is_some(),
5641                piecewise_linear_power: piecewise
5642                    .map_or(PioF64View::EMPTY, |cost| PioF64View::new(&cost.power)),
5643                piecewise_linear_value: piecewise
5644                    .map_or(PioF64View::EMPTY, |cost| PioF64View::new(&cost.value)),
5645                active_power_max: value.generators.pmax[index],
5646                active_power_min: value.generators.pmin[index],
5647                reactive_power_max: value.generators.qmax[index],
5648                reactive_power_min: value.generators.qmin[index],
5649                initial_active_power: value.generators.pg[index],
5650                initial_reactive_power: value.generators.qg[index],
5651                voltage_magnitude_setpoint_pu: value.generators.vg[index],
5652                capability_active: value.generators.capability_active[index],
5653            };
5654            Ok(true)
5655        })
5656    }
5657}
5658
5659/// Read one storage row of an AC OPF preparation.
5660#[unsafe(no_mangle)]
5661pub unsafe extern "C" fn pio_ac_opf_preparation_storage_at(
5662    preparation: *const PioAcOpfPreparation,
5663    index: usize,
5664    output: *mut PioAcOpfStorageView,
5665    error: *mut *mut PioError,
5666) -> bool {
5667    unsafe {
5668        entry(error, false, || {
5669            let value = PioAcOpfPreparation::get(preparation).ok_or_else(|| {
5670                boundary_error(
5671                    &codes::BIND_CAPI_NULL_HANDLE,
5672                    "PioAcOpfPreparation must not be NULL",
5673                )
5674            })?;
5675            if index >= value.n_storage() {
5676                return Err(boundary_error(
5677                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
5678                    format!("AC OPF preparation storage index {index} is out of range"),
5679                ));
5680            }
5681            let storage = &value.storage;
5682            *require_output(output, "output")? = PioAcOpfStorageView {
5683                component_id: PioStringView::new(&storage.identities[index]),
5684                bus_index: storage.bus_of_storage[index],
5685                source_row: storage.source_rows[index],
5686                initial_active_power: storage.p[index],
5687                initial_reactive_power: storage.q[index],
5688                energy: storage.energy[index],
5689                energy_rating: storage.energy_rating[index],
5690                charge_rating: storage.charge_rating[index],
5691                discharge_rating: storage.discharge_rating[index],
5692                charge_efficiency: storage.charge_efficiency[index],
5693                discharge_efficiency: storage.discharge_efficiency[index],
5694                apparent_power_max: storage.s_max[index],
5695                reactive_power_min: storage.qmin[index],
5696                reactive_power_max: storage.qmax[index],
5697                resistance_pu: storage.r[index],
5698                reactance_pu: storage.x[index],
5699                active_power_loss: storage.p_loss[index],
5700                reactive_power_loss: storage.q_loss[index],
5701                in_service: storage.in_service[index],
5702            };
5703            Ok(true)
5704        })
5705    }
5706}
5707
5708/// Read one active branch row of an AC OPF preparation.
5709#[unsafe(no_mangle)]
5710pub unsafe extern "C" fn pio_ac_opf_preparation_branch_at(
5711    preparation: *const PioAcOpfPreparation,
5712    index: usize,
5713    output: *mut PioAcOpfBranchView,
5714    error: *mut *mut PioError,
5715) -> bool {
5716    unsafe {
5717        entry(error, false, || {
5718            let value = PioAcOpfPreparation::get(preparation).ok_or_else(|| {
5719                boundary_error(
5720                    &codes::BIND_CAPI_NULL_HANDLE,
5721                    "PioAcOpfPreparation must not be NULL",
5722                )
5723            })?;
5724            if index >= value.n_branches() {
5725                return Err(boundary_error(
5726                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
5727                    format!("AC OPF preparation branch index {index} is out of range"),
5728                ));
5729            }
5730            let (source_kind, source_row, winding) =
5731                opf_analysis_branch_source(value.branches.analysis_sources[index], "AC")?;
5732            *require_output(output, "output")? = PioAcOpfBranchView {
5733                component_id: PioStringView::new(&value.branches.identities[index]),
5734                from_bus_index: value.branches.from_bus[index],
5735                to_bus_index: value.branches.to_bus[index],
5736                series_conductance: value.branches.g[index],
5737                series_susceptance: value.branches.b[index],
5738                from_conductance: value.branches.g_fr[index],
5739                from_susceptance: value.branches.b_fr[index],
5740                to_conductance: value.branches.g_to[index],
5741                to_susceptance: value.branches.b_to[index],
5742                tap_ratio: value.branches.tap[index],
5743                phase_shift_radians: value.branches.shift[index],
5744                apparent_power_max: value.branches.s_max[index],
5745                angle_difference_min_radians: value.branches.angle_min[index],
5746                angle_difference_max_radians: value.branches.angle_max[index],
5747                analysis_row: value.branches.analysis_rows[index],
5748                source_kind: PioStringView::new(source_kind),
5749                source_row,
5750                winding: winding.unwrap_or(0),
5751                has_winding: winding.is_some(),
5752                thermal_limit_active: value.branches.thermal_limit_active[index],
5753                angle_bound_active: value.branches.angle_bound_active[index],
5754            };
5755            Ok(true)
5756        })
5757    }
5758}
5759
5760#[unsafe(no_mangle)]
5761pub unsafe extern "C" fn pio_dc_pf_instance_bus_specification_count(
5762    instance: *const PioCalculationInstance,
5763) -> usize {
5764    unsafe { PioCalculationInstance::get(instance) }
5765        .and_then(CalculationInstanceInner::dc_pf)
5766        .map_or(0, |instance| instance.specifications().len())
5767}
5768
5769/// Read one DC power flow bus specification by zero based bus table position.
5770#[unsafe(no_mangle)]
5771pub unsafe extern "C" fn pio_dc_pf_instance_bus_specification_at(
5772    instance: *const PioCalculationInstance,
5773    index: usize,
5774    output: *mut PioDcBusSpecificationView,
5775    error: *mut *mut PioError,
5776) -> bool {
5777    unsafe {
5778        entry(error, false, || {
5779            let instance = require_calculation_instance(instance)?
5780                .dc_pf()
5781                .ok_or_else(|| {
5782                    boundary_error(
5783                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
5784                        "the calculation instance is not powerio.DcPfInstance",
5785                    )
5786                })?;
5787            let specification = instance.specifications().get(index).ok_or_else(|| {
5788                boundary_error(
5789                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
5790                    format!("DC bus specification index {index} is out of range"),
5791                )
5792            })?;
5793            let bus_id = instance
5794                .network()
5795                .buses()
5796                .get(index)
5797                .ok_or_else(|| {
5798                    boundary_error(
5799                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
5800                        format!("bus specification index {index} has no bus"),
5801                    )
5802                })?
5803                .id
5804                .0;
5805            let (kind, net_active_power_mw, voltage_angle_degrees) = match *specification {
5806                powerio_prob::DcBusSpecification::NetActivePower { p_mw } => {
5807                    ("net_active_power", p_mw, 0.0)
5808                }
5809                powerio_prob::DcBusSpecification::Reference { va_degrees } => {
5810                    ("reference", 0.0, va_degrees)
5811                }
5812                powerio_prob::DcBusSpecification::Isolated => ("isolated", 0.0, 0.0),
5813                _ => ("unknown", 0.0, 0.0),
5814            };
5815            *require_output(output, "output")? = PioDcBusSpecificationView {
5816                bus_id,
5817                kind: PioStringView::new(kind),
5818                net_active_power_mw,
5819                voltage_angle_degrees,
5820            };
5821            Ok(true)
5822        })
5823    }
5824}
5825
5826#[unsafe(no_mangle)]
5827pub unsafe extern "C" fn pio_dc_pf_instance_branch_susceptance_formula(
5828    instance: *const PioCalculationInstance,
5829) -> PioStringView {
5830    unsafe { PioCalculationInstance::get(instance) }
5831        .and_then(CalculationInstanceInner::dc_pf)
5832        .map_or(PioStringView::EMPTY, |instance| {
5833            PioStringView::new(branch_susceptance_formula_name(
5834                instance.branch_susceptance_formula(),
5835            ))
5836        })
5837}
5838
5839#[unsafe(no_mangle)]
5840pub unsafe extern "C" fn pio_ac_pf_instance_bus_specification_count(
5841    instance: *const PioCalculationInstance,
5842) -> usize {
5843    unsafe { PioCalculationInstance::get(instance) }
5844        .and_then(CalculationInstanceInner::ac_pf)
5845        .map_or(0, |instance| instance.specifications().len())
5846}
5847
5848/// Read one AC power flow bus specification by zero based bus table position.
5849#[unsafe(no_mangle)]
5850pub unsafe extern "C" fn pio_ac_pf_instance_bus_specification_at(
5851    instance: *const PioCalculationInstance,
5852    index: usize,
5853    output: *mut PioAcBusSpecificationView,
5854    error: *mut *mut PioError,
5855) -> bool {
5856    unsafe {
5857        entry(error, false, || {
5858            let instance = require_calculation_instance(instance)?
5859                .ac_pf()
5860                .ok_or_else(|| {
5861                    boundary_error(
5862                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
5863                        "the calculation instance is not powerio.AcPfInstance",
5864                    )
5865                })?;
5866            let specification = instance.specifications().get(index).ok_or_else(|| {
5867                boundary_error(
5868                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
5869                    format!("AC bus specification index {index} is out of range"),
5870                )
5871            })?;
5872            let bus_id = instance
5873                .network()
5874                .buses()
5875                .get(index)
5876                .ok_or_else(|| {
5877                    boundary_error(
5878                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
5879                        format!("bus specification index {index} has no bus"),
5880                    )
5881                })?
5882                .id
5883                .0;
5884            let (kind, p, q, vm, va) = match *specification {
5885                powerio_prob::AcBusSpecification::Pq { p, q } => ("pq", p, q, 0.0, 0.0),
5886                powerio_prob::AcBusSpecification::Pv { p, vm } => ("pv", p, 0.0, vm, 0.0),
5887                powerio_prob::AcBusSpecification::Reference { vm, va } => {
5888                    ("reference", 0.0, 0.0, vm, va)
5889                }
5890                powerio_prob::AcBusSpecification::Isolated => ("isolated", 0.0, 0.0, 0.0, 0.0),
5891                _ => ("unknown", 0.0, 0.0, 0.0, 0.0),
5892            };
5893            *require_output(output, "output")? = PioAcBusSpecificationView {
5894                bus_id,
5895                kind: PioStringView::new(kind),
5896                net_active_power_mw: p,
5897                net_reactive_power_mvar: q,
5898                voltage_magnitude_pu: vm,
5899                voltage_angle_degrees: va,
5900            };
5901            Ok(true)
5902        })
5903    }
5904}
5905
5906#[unsafe(no_mangle)]
5907pub unsafe extern "C" fn pio_dc_opf_instance_branch_susceptance_formula(
5908    instance: *const PioCalculationInstance,
5909) -> PioStringView {
5910    unsafe { PioCalculationInstance::get(instance) }
5911        .and_then(CalculationInstanceInner::dc_opf)
5912        .map_or(PioStringView::EMPTY, |instance| {
5913            PioStringView::new(branch_susceptance_formula_name(
5914                instance.branch_susceptance_formula(),
5915            ))
5916        })
5917}
5918
5919fn objective(instance: &CalculationInstanceInner) -> Option<&powerio_prob::Objective> {
5920    match instance.instance()? {
5921        CalculationInstanceRef::DcOpf(instance) => Some(instance.objective()),
5922        CalculationInstanceRef::AcOpf(instance) => Some(instance.objective()),
5923        CalculationInstanceRef::McAcOpf(instance) => Some(instance.objective()),
5924        _ => None,
5925    }
5926}
5927
5928fn objective_term_name(term: &powerio_prob::ObjectiveTerm) -> &'static str {
5929    match term {
5930        powerio_prob::ObjectiveTerm::NetworkGeneratorCost => "network_generator_cost",
5931        powerio_prob::ObjectiveTerm::ActivePowerDispatchCost => "active_power_dispatch_cost",
5932        _ => "unknown",
5933    }
5934}
5935
5936#[unsafe(no_mangle)]
5937pub unsafe extern "C" fn pio_calculation_instance_objective_term_count(
5938    instance: *const PioCalculationInstance,
5939) -> usize {
5940    unsafe { PioCalculationInstance::get(instance) }
5941        .and_then(objective)
5942        .map_or(0, |objective| objective.terms().len())
5943}
5944
5945/// Read one typed objective term by zero based position.
5946#[unsafe(no_mangle)]
5947pub unsafe extern "C" fn pio_calculation_instance_objective_term_at(
5948    instance: *const PioCalculationInstance,
5949    index: usize,
5950    output: *mut PioObjectiveTermView,
5951    error: *mut *mut PioError,
5952) -> bool {
5953    unsafe {
5954        entry(error, false, || {
5955            let value = require_calculation_instance(instance)?;
5956            let objective = objective(value).ok_or_else(|| {
5957                boundary_error(
5958                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
5959                    "the calculation instance has no optimization objective",
5960                )
5961            })?;
5962            let term = objective.terms().get(index).ok_or_else(|| {
5963                boundary_error(
5964                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
5965                    format!("objective term index {index} is out of range"),
5966                )
5967            })?;
5968            *require_output(output, "output")? = PioObjectiveTermView {
5969                kind: PioStringView::new(objective_term_name(term)),
5970            };
5971            Ok(true)
5972        })
5973    }
5974}
5975
5976fn active_constraint(
5977    instance: &CalculationInstanceInner,
5978    index: usize,
5979) -> Option<(&'static str, &powerio_prob::ConstraintSelection)> {
5980    match instance.instance()? {
5981        CalculationInstanceRef::DcOpf(instance) => match index {
5982            0 => Some((
5983                "generator_capability",
5984                &instance.constraints().generator_capability,
5985            )),
5986            1 => Some(("voltage_bounds", &instance.constraints().voltage_bounds)),
5987            2 => Some(("thermal_limits", &instance.constraints().thermal_limits)),
5988            3 => Some(("angle_bounds", &instance.constraints().angle_bounds)),
5989            _ => None,
5990        },
5991        CalculationInstanceRef::AcOpf(instance) => match index {
5992            0 => Some((
5993                "generator_capability",
5994                &instance.constraints().generator_capability,
5995            )),
5996            1 => Some(("voltage_bounds", &instance.constraints().voltage_bounds)),
5997            2 => Some(("thermal_limits", &instance.constraints().thermal_limits)),
5998            3 => Some(("angle_bounds", &instance.constraints().angle_bounds)),
5999            _ => None,
6000        },
6001        CalculationInstanceRef::McAcOpf(instance) => match index {
6002            0 => Some((
6003                "terminal_voltage_bounds",
6004                &instance.constraints().terminal_voltage_bounds,
6005            )),
6006            1 => Some(("conductor_limits", &instance.constraints().conductor_limits)),
6007            2 => Some((
6008                "generator_capability",
6009                &instance.constraints().generator_capability,
6010            )),
6011            _ => None,
6012        },
6013        _ => None,
6014    }
6015}
6016
6017fn constraint_selection_parts(
6018    selection: &powerio_prob::ConstraintSelection,
6019) -> (&'static str, &[String]) {
6020    match selection {
6021        powerio_prob::ConstraintSelection::All => ("all", &[]),
6022        powerio_prob::ConstraintSelection::None => ("none", &[]),
6023        powerio_prob::ConstraintSelection::Only(identities) => ("only", identities),
6024        _ => ("unknown", &[]),
6025    }
6026}
6027
6028#[unsafe(no_mangle)]
6029pub unsafe extern "C" fn pio_calculation_instance_active_constraint_count(
6030    instance: *const PioCalculationInstance,
6031) -> usize {
6032    match unsafe { PioCalculationInstance::get(instance) }
6033        .and_then(CalculationInstanceInner::instance)
6034    {
6035        Some(CalculationInstanceRef::DcOpf(_) | CalculationInstanceRef::AcOpf(_)) => 4,
6036        Some(CalculationInstanceRef::McAcOpf(_)) => 3,
6037        _ => 0,
6038    }
6039}
6040
6041/// Read one active constraint family by zero based position.
6042#[unsafe(no_mangle)]
6043pub unsafe extern "C" fn pio_calculation_instance_active_constraint_at(
6044    instance: *const PioCalculationInstance,
6045    index: usize,
6046    output: *mut PioActiveConstraintView,
6047    error: *mut *mut PioError,
6048) -> bool {
6049    unsafe {
6050        entry(error, false, || {
6051            let instance = require_calculation_instance(instance)?;
6052            let (family, selection) = active_constraint(instance, index).ok_or_else(|| {
6053                boundary_error(
6054                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6055                    format!("active constraint index {index} is out of range"),
6056                )
6057            })?;
6058            let (selection, identities) = constraint_selection_parts(selection);
6059            *require_output(output, "output")? = PioActiveConstraintView {
6060                family: PioStringView::new(family),
6061                selection: PioStringView::new(selection),
6062                identity_count: identities.len(),
6063            };
6064            Ok(true)
6065        })
6066    }
6067}
6068
6069/// Read one selected component identity from an `only` constraint selection.
6070#[unsafe(no_mangle)]
6071pub unsafe extern "C" fn pio_calculation_instance_active_constraint_identity_at(
6072    instance: *const PioCalculationInstance,
6073    constraint_index: usize,
6074    identity_index: usize,
6075    error: *mut *mut PioError,
6076) -> PioStringView {
6077    unsafe {
6078        entry(error, PioStringView::EMPTY, || {
6079            let instance = require_calculation_instance(instance)?;
6080            let (_, selection) =
6081                active_constraint(instance, constraint_index).ok_or_else(|| {
6082                    boundary_error(
6083                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6084                        format!("active constraint index {constraint_index} is out of range"),
6085                    )
6086                })?;
6087            let (_, identities) = constraint_selection_parts(selection);
6088            let identity = identities.get(identity_index).ok_or_else(|| {
6089                boundary_error(
6090                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6091                    format!("constraint identity index {identity_index} is out of range"),
6092                )
6093            })?;
6094            Ok(PioStringView::new(identity))
6095        })
6096    }
6097}
6098
6099#[unsafe(no_mangle)]
6100pub unsafe extern "C" fn pio_calculation_instance_has_initial_point(
6101    instance: *const PioCalculationInstance,
6102) -> bool {
6103    unsafe { PioCalculationInstance::get(instance) }
6104        .is_some_and(CalculationInstanceInner::has_initial_point)
6105}
6106
6107/// Return the optional owner-rooted initial operating point. A calculation
6108/// instance with no initial point returns NULL without setting an error.
6109#[unsafe(no_mangle)]
6110pub unsafe extern "C" fn pio_calculation_instance_initial_point(
6111    instance: *const PioCalculationInstance,
6112    error: *mut *mut PioError,
6113) -> *mut PioOperatingPoint {
6114    unsafe {
6115        entry(error, std::ptr::null_mut(), || {
6116            let instance = PioCalculationInstance::get(instance).ok_or_else(|| {
6117                boundary_error(
6118                    &codes::BIND_CAPI_NULL_HANDLE,
6119                    "PioCalculationInstance must not be NULL",
6120                )
6121            })?;
6122            let has_initial_point = instance.has_initial_point();
6123            let projection = if has_initial_point {
6124                Some(match (instance.projection, instance.instance()) {
6125                    (
6126                        CalculationInstanceProjection::Direct,
6127                        Some(CalculationInstanceRef::DcPf(_)),
6128                    ) => OperatingPointProjection::DcPfInitial,
6129                    (
6130                        CalculationInstanceProjection::Direct,
6131                        Some(CalculationInstanceRef::AcPf(_)),
6132                    ) => OperatingPointProjection::AcPfInitial,
6133                    (
6134                        CalculationInstanceProjection::Direct,
6135                        Some(CalculationInstanceRef::DcOpf(_)),
6136                    ) => OperatingPointProjection::DcOpfInitial,
6137                    (
6138                        CalculationInstanceProjection::Direct,
6139                        Some(CalculationInstanceRef::AcOpf(_)),
6140                    ) => OperatingPointProjection::AcOpfInitial,
6141                    (
6142                        CalculationInstanceProjection::Direct,
6143                        Some(CalculationInstanceRef::McAcPf(_)),
6144                    ) => OperatingPointProjection::McAcPfInitial,
6145                    (
6146                        CalculationInstanceProjection::Direct,
6147                        Some(CalculationInstanceRef::McAcOpf(_)),
6148                    ) => OperatingPointProjection::McAcOpfInitial,
6149                    (CalculationInstanceProjection::DcPfSolution, _) => {
6150                        OperatingPointProjection::DcPfSolutionInitial
6151                    }
6152                    (CalculationInstanceProjection::AcPfSolution, _) => {
6153                        OperatingPointProjection::AcPfSolutionInitial
6154                    }
6155                    (CalculationInstanceProjection::DcOpfSolution, _) => {
6156                        OperatingPointProjection::DcOpfSolutionInitial
6157                    }
6158                    (CalculationInstanceProjection::AcOpfSolution, _) => {
6159                        OperatingPointProjection::AcOpfSolutionInitial
6160                    }
6161                    (CalculationInstanceProjection::SocwrOpfSolution, _) => {
6162                        OperatingPointProjection::SocwrOpfSolutionInitial
6163                    }
6164                    (CalculationInstanceProjection::McAcPfSolution, _) => {
6165                        OperatingPointProjection::McAcPfSolutionInitial
6166                    }
6167                    (CalculationInstanceProjection::McAcOpfSolution, _) => {
6168                        OperatingPointProjection::McAcOpfSolutionInitial
6169                    }
6170                    _ => {
6171                        return Err(boundary_error(
6172                            &codes::REQUEST_CAPI_TYPE_MISMATCH,
6173                            "the handle does not refer to a calculation instance with an operating point",
6174                        ));
6175                    }
6176                })
6177            } else {
6178                None
6179            };
6180            Ok(projection.map_or(std::ptr::null_mut(), |projection| {
6181                PioOperatingPoint::new_raw(OperatingPointInner {
6182                    value: ValueInner {
6183                        owner: Arc::clone(&instance.value.owner),
6184                        steps: instance.value.steps.clone(),
6185                    },
6186                    projection,
6187                })
6188            }))
6189        })
6190    }
6191}
6192
6193fn dist_load_voltage_model_name(model: &powerio_dist::DistLoadVoltageModel) -> &'static str {
6194    match model {
6195        powerio_dist::DistLoadVoltageModel::ConstantPower { .. } => "constant_power",
6196        powerio_dist::DistLoadVoltageModel::ConstantCurrent { .. } => "constant_current",
6197        powerio_dist::DistLoadVoltageModel::ConstantImpedance { .. } => "constant_impedance",
6198        powerio_dist::DistLoadVoltageModel::Zip { .. } => "zip",
6199        powerio_dist::DistLoadVoltageModel::Exponential { .. } => "exponential",
6200        _ => "unknown",
6201    }
6202}
6203
6204#[unsafe(no_mangle)]
6205pub unsafe extern "C" fn pio_mc_ac_pf_instance_load_count(
6206    instance: *const PioCalculationInstance,
6207) -> usize {
6208    unsafe { PioCalculationInstance::get(instance) }
6209        .and_then(CalculationInstanceInner::mc_ac_pf)
6210        .map_or(0, |instance| instance.loads().len())
6211}
6212
6213#[unsafe(no_mangle)]
6214pub unsafe extern "C" fn pio_mc_ac_pf_instance_load_at(
6215    instance: *const PioCalculationInstance,
6216    index: usize,
6217    output: *mut PioPrescribedTerminalPowerView,
6218    error: *mut *mut PioError,
6219) -> bool {
6220    unsafe {
6221        entry(error, false, || {
6222            let instance = require_calculation_instance(instance)?
6223                .mc_ac_pf()
6224                .ok_or_else(|| {
6225                    boundary_error(
6226                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
6227                        "the calculation instance is not powerio.McAcPfInstance",
6228                    )
6229                })?;
6230            let load = instance.loads().get(index).ok_or_else(|| {
6231                boundary_error(
6232                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6233                    format!("prescribed load index {index} is out of range"),
6234                )
6235            })?;
6236            *require_output(output, "output")? = PioPrescribedTerminalPowerView {
6237                load: PioStringView::new(&load.load),
6238                terminal_count: load.terminals.len(),
6239                voltage_model: PioStringView::new(dist_load_voltage_model_name(
6240                    &load.voltage_model,
6241                )),
6242            };
6243            Ok(true)
6244        })
6245    }
6246}
6247
6248fn model_value(values: &[f64], index: usize) -> (f64, bool) {
6249    values
6250        .get(index)
6251        .copied()
6252        .map_or((0.0, false), |value| (value, true))
6253}
6254
6255#[unsafe(no_mangle)]
6256pub unsafe extern "C" fn pio_mc_ac_pf_instance_load_terminal_at(
6257    instance: *const PioCalculationInstance,
6258    load_index: usize,
6259    terminal_index: usize,
6260    output: *mut PioTerminalPowerView,
6261    error: *mut *mut PioError,
6262) -> bool {
6263    unsafe {
6264        entry(error, false, || {
6265            let instance = require_calculation_instance(instance)?
6266                .mc_ac_pf()
6267                .ok_or_else(|| {
6268                    boundary_error(
6269                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
6270                        "the calculation instance is not powerio.McAcPfInstance",
6271                    )
6272                })?;
6273            let load = instance.loads().get(load_index).ok_or_else(|| {
6274                boundary_error(
6275                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6276                    format!("prescribed load index {load_index} is out of range"),
6277                )
6278            })?;
6279            let terminal = load.terminals.get(terminal_index).ok_or_else(|| {
6280                boundary_error(
6281                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6282                    format!("load terminal index {terminal_index} is out of range"),
6283                )
6284            })?;
6285            let p = *load.p_w.get(terminal_index).ok_or_else(|| {
6286                boundary_error(
6287                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
6288                    "the prescribed load active power does not align with its terminals",
6289                )
6290            })?;
6291            let q = *load.q_var.get(terminal_index).ok_or_else(|| {
6292                boundary_error(
6293                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
6294                    "the prescribed load reactive power does not align with its terminals",
6295                )
6296            })?;
6297            let mut model_values = (0.0, false, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
6298            match &load.voltage_model {
6299                powerio_dist::DistLoadVoltageModel::ConstantPower { v_nom } => {
6300                    let (v, has_v) = model_value(v_nom, terminal_index);
6301                    model_values.0 = v;
6302                    model_values.1 = has_v;
6303                }
6304                powerio_dist::DistLoadVoltageModel::ConstantCurrent { v_nom } => {
6305                    let (v, has_v) = model_value(v_nom, terminal_index);
6306                    model_values = (v, has_v, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0);
6307                }
6308                powerio_dist::DistLoadVoltageModel::ConstantImpedance { v_nom } => {
6309                    let (v, has_v) = model_value(v_nom, terminal_index);
6310                    model_values = (v, has_v, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0);
6311                }
6312                powerio_dist::DistLoadVoltageModel::Zip {
6313                    v_nom,
6314                    alpha_z,
6315                    alpha_i,
6316                    alpha_p,
6317                    beta_z,
6318                    beta_i,
6319                    beta_p,
6320                } => {
6321                    let (v, has_v) = model_value(v_nom, terminal_index);
6322                    model_values = (
6323                        v,
6324                        has_v,
6325                        alpha_z.get(terminal_index).copied().unwrap_or(0.0),
6326                        alpha_i.get(terminal_index).copied().unwrap_or(0.0),
6327                        alpha_p.get(terminal_index).copied().unwrap_or(0.0),
6328                        beta_z.get(terminal_index).copied().unwrap_or(0.0),
6329                        beta_i.get(terminal_index).copied().unwrap_or(0.0),
6330                        beta_p.get(terminal_index).copied().unwrap_or(0.0),
6331                        0.0,
6332                        0.0,
6333                    );
6334                }
6335                powerio_dist::DistLoadVoltageModel::Exponential {
6336                    v_nom,
6337                    gamma_p,
6338                    gamma_q,
6339                } => {
6340                    let (v, has_v) = model_value(v_nom, terminal_index);
6341                    model_values = (
6342                        v,
6343                        has_v,
6344                        0.0,
6345                        0.0,
6346                        0.0,
6347                        0.0,
6348                        0.0,
6349                        0.0,
6350                        gamma_p.get(terminal_index).copied().unwrap_or(0.0),
6351                        gamma_q.get(terminal_index).copied().unwrap_or(0.0),
6352                    );
6353                }
6354                _ => {}
6355            }
6356            *require_output(output, "output")? = PioTerminalPowerView {
6357                terminal: PioStringView::new(terminal),
6358                active_power_w: p,
6359                reactive_power_var: q,
6360                nominal_voltage_v: model_values.0,
6361                has_nominal_voltage: model_values.1,
6362                active_impedance_fraction: model_values.2,
6363                active_current_fraction: model_values.3,
6364                active_power_fraction: model_values.4,
6365                reactive_impedance_fraction: model_values.5,
6366                reactive_current_fraction: model_values.6,
6367                reactive_power_fraction: model_values.7,
6368                active_power_exponent: model_values.8,
6369                reactive_power_exponent: model_values.9,
6370            };
6371            Ok(true)
6372        })
6373    }
6374}
6375
6376#[unsafe(no_mangle)]
6377pub unsafe extern "C" fn pio_mc_ac_pf_instance_source_count(
6378    instance: *const PioCalculationInstance,
6379) -> usize {
6380    unsafe { PioCalculationInstance::get(instance) }
6381        .and_then(CalculationInstanceInner::mc_ac_pf)
6382        .map_or(0, |instance| instance.sources().len())
6383}
6384
6385#[unsafe(no_mangle)]
6386pub unsafe extern "C" fn pio_mc_ac_pf_instance_source_at(
6387    instance: *const PioCalculationInstance,
6388    index: usize,
6389    output: *mut PioPrescribedSourceVoltageView,
6390    error: *mut *mut PioError,
6391) -> bool {
6392    unsafe {
6393        entry(error, false, || {
6394            let instance = require_calculation_instance(instance)?
6395                .mc_ac_pf()
6396                .ok_or_else(|| {
6397                    boundary_error(
6398                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
6399                        "the calculation instance is not powerio.McAcPfInstance",
6400                    )
6401                })?;
6402            let source = instance.sources().get(index).ok_or_else(|| {
6403                boundary_error(
6404                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6405                    format!("prescribed source index {index} is out of range"),
6406                )
6407            })?;
6408            *require_output(output, "output")? = PioPrescribedSourceVoltageView {
6409                source: PioStringView::new(&source.source),
6410                terminal_count: source.terminals.len(),
6411            };
6412            Ok(true)
6413        })
6414    }
6415}
6416
6417#[unsafe(no_mangle)]
6418pub unsafe extern "C" fn pio_mc_ac_pf_instance_source_terminal_at(
6419    instance: *const PioCalculationInstance,
6420    source_index: usize,
6421    terminal_index: usize,
6422    output: *mut PioTerminalVoltageView,
6423    error: *mut *mut PioError,
6424) -> bool {
6425    unsafe {
6426        entry(error, false, || {
6427            let instance = require_calculation_instance(instance)?
6428                .mc_ac_pf()
6429                .ok_or_else(|| {
6430                    boundary_error(
6431                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
6432                        "the calculation instance is not powerio.McAcPfInstance",
6433                    )
6434                })?;
6435            let source = instance.sources().get(source_index).ok_or_else(|| {
6436                boundary_error(
6437                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6438                    format!("prescribed source index {source_index} is out of range"),
6439                )
6440            })?;
6441            let terminal = source.terminals.get(terminal_index).ok_or_else(|| {
6442                boundary_error(
6443                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6444                    format!("source terminal index {terminal_index} is out of range"),
6445                )
6446            })?;
6447            let magnitude = source.v_magnitude.get(terminal_index).ok_or_else(|| {
6448                boundary_error(
6449                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
6450                    "the prescribed source magnitudes do not align with its terminals",
6451                )
6452            })?;
6453            let angle = source.v_angle.get(terminal_index).ok_or_else(|| {
6454                boundary_error(
6455                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
6456                    "the prescribed source angles do not align with its terminals",
6457                )
6458            })?;
6459            *require_output(output, "output")? = PioTerminalVoltageView {
6460                terminal: PioStringView::new(terminal),
6461                magnitude_v: *magnitude,
6462                angle_radians: *angle,
6463            };
6464            Ok(true)
6465        })
6466    }
6467}
6468
6469#[unsafe(no_mangle)]
6470pub unsafe extern "C" fn pio_mc_ac_pf_instance_isolated_terminal_count(
6471    instance: *const PioCalculationInstance,
6472) -> usize {
6473    unsafe { PioCalculationInstance::get(instance) }
6474        .and_then(CalculationInstanceInner::mc_ac_pf)
6475        .map_or(0, |instance| instance.isolated_terminals().len())
6476}
6477
6478#[unsafe(no_mangle)]
6479pub unsafe extern "C" fn pio_mc_ac_pf_instance_isolated_terminal_at(
6480    instance: *const PioCalculationInstance,
6481    index: usize,
6482    output: *mut PioIsolatedTerminalView,
6483    error: *mut *mut PioError,
6484) -> bool {
6485    unsafe {
6486        entry(error, false, || {
6487            let instance = require_calculation_instance(instance)?
6488                .mc_ac_pf()
6489                .ok_or_else(|| {
6490                    boundary_error(
6491                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
6492                        "the calculation instance is not powerio.McAcPfInstance",
6493                    )
6494                })?;
6495            let (bus, terminal) = instance.isolated_terminals().get(index).ok_or_else(|| {
6496                boundary_error(
6497                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6498                    format!("isolated terminal index {index} is out of range"),
6499                )
6500            })?;
6501            *require_output(output, "output")? = PioIsolatedTerminalView {
6502                bus: PioStringView::new(bus),
6503                terminal: PioStringView::new(terminal),
6504            };
6505            Ok(true)
6506        })
6507    }
6508}
6509
6510#[unsafe(no_mangle)]
6511pub unsafe extern "C" fn pio_mc_ac_pf_instance_active_control_count(
6512    instance: *const PioCalculationInstance,
6513) -> usize {
6514    unsafe { PioCalculationInstance::get(instance) }
6515        .and_then(CalculationInstanceInner::mc_ac_pf)
6516        .map_or(0, |instance| instance.control_modes().len())
6517}
6518
6519#[unsafe(no_mangle)]
6520pub unsafe extern "C" fn pio_mc_ac_pf_instance_active_control_at(
6521    instance: *const PioCalculationInstance,
6522    index: usize,
6523    output: *mut PioActiveControlView,
6524    error: *mut *mut PioError,
6525) -> bool {
6526    unsafe {
6527        entry(error, false, || {
6528            let instance = require_calculation_instance(instance)?
6529                .mc_ac_pf()
6530                .ok_or_else(|| {
6531                    boundary_error(
6532                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
6533                        "the calculation instance is not powerio.McAcPfInstance",
6534                    )
6535                })?;
6536            let control = instance.control_modes().get(index).ok_or_else(|| {
6537                boundary_error(
6538                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6539                    format!("active control index {index} is out of range"),
6540                )
6541            })?;
6542            let (kind, component_id) = match control {
6543                powerio_prob::ActiveControlMode::RegulatorTap { transformer } => {
6544                    ("regulator_tap", transformer.as_str())
6545                }
6546                powerio_prob::ActiveControlMode::CapacitorSteps { capacitor } => {
6547                    ("capacitor_steps", capacitor.as_str())
6548                }
6549                _ => ("unknown", ""),
6550            };
6551            *require_output(output, "output")? = PioActiveControlView {
6552                kind: PioStringView::new(kind),
6553                component_id: PioStringView::new(component_id),
6554            };
6555            Ok(true)
6556        })
6557    }
6558}
6559
6560fn scuc_inputs(instance: &CalculationInstanceInner) -> Option<&powerio_prob::ScucInputs> {
6561    instance.ac_scuc().map(powerio_prob::AcScucInstance::inputs)
6562}
6563
6564fn scuc_inputs_or_error(
6565    instance: &CalculationInstanceInner,
6566) -> Result<&powerio_prob::ScucInputs, *mut PioError> {
6567    scuc_inputs(instance).ok_or_else(|| {
6568        boundary_error(
6569            &codes::REQUEST_CAPI_TYPE_MISMATCH,
6570            "the calculation instance is not powerio.AcScucInstance",
6571        )
6572    })
6573}
6574
6575fn component_id_view(id: &ComponentId) -> PioComponentIdView {
6576    PioComponentIdView {
6577        component_type: PioStringView::new(id.component_type()),
6578        local_id: PioStringView::new(id.local_id()),
6579    }
6580}
6581
6582fn empty_component_id_view() -> PioComponentIdView {
6583    PioComponentIdView {
6584        component_type: PioStringView::EMPTY,
6585        local_id: PioStringView::EMPTY,
6586    }
6587}
6588
6589fn optional_component_id_view(value: Option<&ComponentId>) -> (PioComponentIdView, bool) {
6590    value.map_or((empty_component_id_view(), false), |value| {
6591        (component_id_view(value), true)
6592    })
6593}
6594
6595fn terminal_reference_view(
6596    value: Option<&powerio_tx::TerminalReference>,
6597) -> (PioTerminalReferenceView, bool) {
6598    value.map_or(
6599        (
6600            PioTerminalReferenceView {
6601                equipment: empty_component_id_view(),
6602                terminal: 0,
6603            },
6604            false,
6605        ),
6606        |value| {
6607            (
6608                PioTerminalReferenceView {
6609                    equipment: component_id_view(&value.equipment),
6610                    terminal: value.terminal,
6611                },
6612                true,
6613            )
6614        },
6615    )
6616}
6617
6618fn transformer_control_mode_name(mode: powerio_tx::TransformerControlMode) -> &'static str {
6619    match mode {
6620        powerio_tx::TransformerControlMode::Fixed => "fixed",
6621        powerio_tx::TransformerControlMode::Voltage => "voltage",
6622        powerio_tx::TransformerControlMode::ReactiveFlow => "reactive_flow",
6623        powerio_tx::TransformerControlMode::ActiveFlow => "active_flow",
6624        powerio_tx::TransformerControlMode::DcLineQuantity => "dc_line_quantity",
6625        powerio_tx::TransformerControlMode::AsymmetricActiveFlow => "asymmetric_active_flow",
6626        _ => "unknown",
6627    }
6628}
6629
6630fn transformer_control_view(
6631    value: Option<&powerio_tx::TransformerControl>,
6632) -> (PioTransformerControlView, bool) {
6633    let empty_terminal = terminal_reference_view(None).0;
6634    value.map_or(
6635        (
6636            PioTransformerControlView {
6637                mode: PioStringView::EMPTY,
6638                enabled: false,
6639                controlled_bus_id: 0,
6640                has_controlled_bus: false,
6641                controlled_bus_on_winding_side: false,
6642                regulating_terminal: empty_terminal,
6643                has_regulating_terminal: false,
6644                tap_min: 0.0,
6645                tap_max: 0.0,
6646                band_min: 0.0,
6647                band_max: 0.0,
6648                tap_position_count: 0,
6649                mva_base: 0.0,
6650                winding_connection_angle: 0.0,
6651                has_winding_connection_angle: false,
6652            },
6653            false,
6654        ),
6655        |control| {
6656            let (regulating_terminal, has_regulating_terminal) =
6657                terminal_reference_view(control.regulating_terminal.as_ref());
6658            (
6659                PioTransformerControlView {
6660                    mode: PioStringView::new(transformer_control_mode_name(control.mode)),
6661                    enabled: control.enabled,
6662                    controlled_bus_id: control.controlled_bus.map_or(0, |bus| bus.0),
6663                    has_controlled_bus: control.controlled_bus.is_some(),
6664                    controlled_bus_on_winding_side: control.controlled_bus_on_winding_side,
6665                    regulating_terminal,
6666                    has_regulating_terminal,
6667                    tap_min: control.tap_min,
6668                    tap_max: control.tap_max,
6669                    band_min: control.band_min,
6670                    band_max: control.band_max,
6671                    tap_position_count: control.ntp,
6672                    mva_base: control.mva_base,
6673                    winding_connection_angle: control.winding_connection_angle.unwrap_or(0.0),
6674                    has_winding_connection_angle: control.winding_connection_angle.is_some(),
6675                },
6676                true,
6677            )
6678        },
6679    )
6680}
6681
6682fn scuc_device_kind_name(kind: powerio_prob::ScucDeviceKind) -> &'static str {
6683    match kind {
6684        powerio_prob::ScucDeviceKind::Producer => "producer",
6685        powerio_prob::ScucDeviceKind::Consumer => "consumer",
6686        _ => "unknown",
6687    }
6688}
6689
6690fn scuc_reactive_capability_view(
6691    capability: &powerio_prob::ScucReactiveCapability,
6692) -> PioScucReactiveCapabilityView {
6693    match capability {
6694        powerio_prob::ScucReactiveCapability::None => PioScucReactiveCapabilityView {
6695            kind: PioStringView::new("none"),
6696            reactive_power_at_zero_active_power_pu: 0.0,
6697            reactive_power_at_zero_active_power_min_pu: 0.0,
6698            reactive_power_at_zero_active_power_max_pu: 0.0,
6699            slope: 0.0,
6700            slope_min: 0.0,
6701            slope_max: 0.0,
6702        },
6703        powerio_prob::ScucReactiveCapability::Linear {
6704            reactive_power_at_zero_active_power,
6705            slope,
6706        } => PioScucReactiveCapabilityView {
6707            kind: PioStringView::new("linear"),
6708            reactive_power_at_zero_active_power_pu: *reactive_power_at_zero_active_power,
6709            reactive_power_at_zero_active_power_min_pu: 0.0,
6710            reactive_power_at_zero_active_power_max_pu: 0.0,
6711            slope: *slope,
6712            slope_min: 0.0,
6713            slope_max: 0.0,
6714        },
6715        powerio_prob::ScucReactiveCapability::Bounded {
6716            reactive_power_at_zero_active_power_min,
6717            reactive_power_at_zero_active_power_max,
6718            slope_min,
6719            slope_max,
6720        } => PioScucReactiveCapabilityView {
6721            kind: PioStringView::new("bounded"),
6722            reactive_power_at_zero_active_power_pu: 0.0,
6723            reactive_power_at_zero_active_power_min_pu: *reactive_power_at_zero_active_power_min,
6724            reactive_power_at_zero_active_power_max_pu: *reactive_power_at_zero_active_power_max,
6725            slope: 0.0,
6726            slope_min: *slope_min,
6727            slope_max: *slope_max,
6728        },
6729        _ => PioScucReactiveCapabilityView {
6730            kind: PioStringView::new("unknown"),
6731            reactive_power_at_zero_active_power_pu: 0.0,
6732            reactive_power_at_zero_active_power_min_pu: 0.0,
6733            reactive_power_at_zero_active_power_max_pu: 0.0,
6734            slope: 0.0,
6735            slope_min: 0.0,
6736            slope_max: 0.0,
6737        },
6738    }
6739}
6740
6741fn scuc_reserve_costs_view(costs: &powerio_prob::ScucReserveCosts) -> PioScucReserveCostsView {
6742    PioScucReserveCostsView {
6743        regulation_up: costs.regulation_up,
6744        regulation_down: costs.regulation_down,
6745        synchronized: costs.synchronized,
6746        nonsynchronized: costs.nonsynchronized,
6747        ramping_up_online: costs.ramping_up_online,
6748        ramping_down_online: costs.ramping_down_online,
6749        ramping_up_offline: costs.ramping_up_offline,
6750        ramping_down_offline: costs.ramping_down_offline,
6751        reactive_up: costs.reactive_up,
6752        reactive_down: costs.reactive_down,
6753    }
6754}
6755
6756fn scuc_device_view(device: &powerio_prob::ScucDevice) -> PioScucDeviceView {
6757    PioScucDeviceView {
6758        id: component_id_view(&device.id),
6759        kind: PioStringView::new(scuc_device_kind_name(device.kind)),
6760        initial_on_status: device.initial_on_status,
6761        on_cost: device.on_cost,
6762        startup_cost: device.startup_cost,
6763        shutdown_cost: device.shutdown_cost,
6764        minimum_up_time_hours: device.minimum_up_time,
6765        minimum_down_time_hours: device.minimum_down_time,
6766        ramp_limits: PioScucRampLimitsView {
6767            up_pu_per_hour: device.ramp_limits.up,
6768            down_pu_per_hour: device.ramp_limits.down,
6769            startup_pu_per_hour: device.ramp_limits.startup,
6770            shutdown_pu_per_hour: device.ramp_limits.shutdown,
6771        },
6772        reserve_limits: PioScucReserveLimitsView {
6773            regulation_up_pu: device.reserve_limits.regulation_up,
6774            regulation_down_pu: device.reserve_limits.regulation_down,
6775            synchronized_pu: device.reserve_limits.synchronized,
6776            nonsynchronized_pu: device.reserve_limits.nonsynchronized,
6777            ramping_up_online_pu: device.reserve_limits.ramping_up_online,
6778            ramping_down_online_pu: device.reserve_limits.ramping_down_online,
6779            ramping_up_offline_pu: device.reserve_limits.ramping_up_offline,
6780            ramping_down_offline_pu: device.reserve_limits.ramping_down_offline,
6781        },
6782        initial_commitment: PioScucInitialCommitmentView {
6783            accumulated_up_time_hours: device.initial_commitment.accumulated_up_time,
6784            accumulated_down_time_hours: device.initial_commitment.accumulated_down_time,
6785        },
6786        reactive_capability: scuc_reactive_capability_view(&device.reactive_capability),
6787        period_count: device.periods.len(),
6788        startup_cost_adjustment_count: device.startup_cost_adjustments.len(),
6789        startup_limit_count: device.startup_limits.len(),
6790        energy_upper_bound_count: device.energy_upper_bounds.len(),
6791        energy_lower_bound_count: device.energy_lower_bounds.len(),
6792    }
6793}
6794
6795fn scuc_device_or_error(
6796    inputs: &powerio_prob::ScucInputs,
6797    device_index: usize,
6798) -> Result<&powerio_prob::ScucDevice, *mut PioError> {
6799    inputs.devices.get(device_index).ok_or_else(|| {
6800        boundary_error(
6801            &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6802            format!("SCUC device index {device_index} is out of range"),
6803        )
6804    })
6805}
6806
6807/// Read semantic collection sizes for one AC SCUC instance.
6808#[unsafe(no_mangle)]
6809pub unsafe extern "C" fn pio_ac_scuc_instance_dimensions(
6810    instance: *const PioCalculationInstance,
6811    output: *mut PioScucDimensionsView,
6812    error: *mut *mut PioError,
6813) -> bool {
6814    unsafe {
6815        entry(error, false, || {
6816            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
6817            *require_output(output, "output")? = PioScucDimensionsView {
6818                period_count: inputs.interval_durations.len(),
6819                device_count: inputs.devices.len(),
6820                producer_count: inputs.producers().count(),
6821                consumer_count: inputs.consumers().count(),
6822                shunt_count: inputs.shunts.len(),
6823                branch_switching_cost_count: inputs.branch_switching_costs.len(),
6824                transformer_control_count: inputs.transformer_controls.len(),
6825                active_reserve_zone_count: inputs.active_reserve_zones.len(),
6826                reactive_reserve_zone_count: inputs.reactive_reserve_zones.len(),
6827                contingency_count: inputs.contingencies.len(),
6828            };
6829            Ok(true)
6830        })
6831    }
6832}
6833
6834/// Borrow interval durations in hours, in chronological order.
6835#[unsafe(no_mangle)]
6836pub unsafe extern "C" fn pio_ac_scuc_instance_interval_durations(
6837    instance: *const PioCalculationInstance,
6838) -> PioF64View {
6839    unsafe { PioCalculationInstance::get(instance) }
6840        .and_then(scuc_inputs)
6841        .map_or(PioF64View::EMPTY, |inputs| {
6842            PioF64View::new(&inputs.interval_durations)
6843        })
6844}
6845
6846/// Read the four required violation costs.
6847#[unsafe(no_mangle)]
6848pub unsafe extern "C" fn pio_ac_scuc_instance_violation_costs(
6849    instance: *const PioCalculationInstance,
6850    output: *mut PioScucViolationCostView,
6851    error: *mut *mut PioError,
6852) -> bool {
6853    unsafe {
6854        entry(error, false, || {
6855            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
6856            let costs = inputs.violation_costs;
6857            *require_output(output, "output")? = PioScucViolationCostView {
6858                active_power_balance: costs.active_power_balance,
6859                reactive_power_balance: costs.reactive_power_balance,
6860                branch_thermal_limit: costs.branch_thermal_limit,
6861                energy_requirement: costs.energy_requirement,
6862            };
6863            Ok(true)
6864        })
6865    }
6866}
6867
6868#[unsafe(no_mangle)]
6869pub unsafe extern "C" fn pio_ac_scuc_instance_device_count(
6870    instance: *const PioCalculationInstance,
6871) -> usize {
6872    unsafe { PioCalculationInstance::get(instance) }
6873        .and_then(scuc_inputs)
6874        .map_or(0, |inputs| inputs.devices.len())
6875}
6876
6877#[unsafe(no_mangle)]
6878pub unsafe extern "C" fn pio_ac_scuc_instance_device_at(
6879    instance: *const PioCalculationInstance,
6880    index: usize,
6881    output: *mut PioScucDeviceView,
6882    error: *mut *mut PioError,
6883) -> bool {
6884    unsafe {
6885        entry(error, false, || {
6886            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
6887            let device = scuc_device_or_error(inputs, index)?;
6888            *require_output(output, "output")? = scuc_device_view(device);
6889            Ok(true)
6890        })
6891    }
6892}
6893
6894/// Read one device by its exact source UID.
6895#[unsafe(no_mangle)]
6896pub unsafe extern "C" fn pio_ac_scuc_instance_device_get(
6897    instance: *const PioCalculationInstance,
6898    uid: *const c_char,
6899    uid_len: usize,
6900    output: *mut PioScucDeviceView,
6901    error: *mut *mut PioError,
6902) -> bool {
6903    unsafe {
6904        entry(error, false, || {
6905            let uid = required_str(uid, uid_len, "device_uid")?;
6906            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
6907            let device = inputs.device(uid).ok_or_else(|| {
6908                boundary_error(
6909                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6910                    format!("SCUC device UID '{uid}' does not exist"),
6911                )
6912            })?;
6913            *require_output(output, "output")? = scuc_device_view(device);
6914            Ok(true)
6915        })
6916    }
6917}
6918
6919#[unsafe(no_mangle)]
6920pub unsafe extern "C" fn pio_ac_scuc_instance_device_startup_cost_adjustment_count(
6921    instance: *const PioCalculationInstance,
6922    device_index: usize,
6923) -> usize {
6924    unsafe { PioCalculationInstance::get(instance) }
6925        .and_then(scuc_inputs)
6926        .and_then(|inputs| inputs.devices.get(device_index))
6927        .map_or(0, |device| device.startup_cost_adjustments.len())
6928}
6929
6930#[unsafe(no_mangle)]
6931pub unsafe extern "C" fn pio_ac_scuc_instance_device_startup_cost_adjustment_at(
6932    instance: *const PioCalculationInstance,
6933    device_index: usize,
6934    adjustment_index: usize,
6935    output: *mut PioScucStartupCostAdjustmentView,
6936    error: *mut *mut PioError,
6937) -> bool {
6938    unsafe {
6939        entry(error, false, || {
6940            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
6941            let device = scuc_device_or_error(inputs, device_index)?;
6942            let adjustment = device
6943                .startup_cost_adjustments
6944                .get(adjustment_index)
6945                .ok_or_else(|| {
6946                    boundary_error(
6947                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6948                        format!(
6949                            "SCUC device {device_index} startup cost adjustment index {adjustment_index} is out of range"
6950                        ),
6951                    )
6952                })?;
6953            *require_output(output, "output")? = PioScucStartupCostAdjustmentView {
6954                cost: adjustment.cost,
6955                maximum_down_time_hours: adjustment.maximum_down_time,
6956            };
6957            Ok(true)
6958        })
6959    }
6960}
6961
6962#[unsafe(no_mangle)]
6963pub unsafe extern "C" fn pio_ac_scuc_instance_device_startup_limit_count(
6964    instance: *const PioCalculationInstance,
6965    device_index: usize,
6966) -> usize {
6967    unsafe { PioCalculationInstance::get(instance) }
6968        .and_then(scuc_inputs)
6969        .and_then(|inputs| inputs.devices.get(device_index))
6970        .map_or(0, |device| device.startup_limits.len())
6971}
6972
6973#[unsafe(no_mangle)]
6974pub unsafe extern "C" fn pio_ac_scuc_instance_device_startup_limit_at(
6975    instance: *const PioCalculationInstance,
6976    device_index: usize,
6977    limit_index: usize,
6978    output: *mut PioScucStartupLimitView,
6979    error: *mut *mut PioError,
6980) -> bool {
6981    unsafe {
6982        entry(error, false, || {
6983            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
6984            let device = scuc_device_or_error(inputs, device_index)?;
6985            let limit = device.startup_limits.get(limit_index).ok_or_else(|| {
6986                boundary_error(
6987                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
6988                    format!(
6989                        "SCUC device {device_index} startup limit index {limit_index} is out of range"
6990                    ),
6991                )
6992            })?;
6993            *require_output(output, "output")? = PioScucStartupLimitView {
6994                start_time_hours: limit.start_time,
6995                end_time_hours: limit.end_time,
6996                maximum_startups: limit.maximum_startups,
6997            };
6998            Ok(true)
6999        })
7000    }
7001}
7002
7003unsafe fn scuc_energy_requirement_count(
7004    instance: *const PioCalculationInstance,
7005    device_index: usize,
7006    upper: bool,
7007) -> usize {
7008    unsafe { PioCalculationInstance::get(instance) }
7009        .and_then(scuc_inputs)
7010        .and_then(|inputs| inputs.devices.get(device_index))
7011        .map_or(0, |device| {
7012            if upper {
7013                device.energy_upper_bounds.len()
7014            } else {
7015                device.energy_lower_bounds.len()
7016            }
7017        })
7018}
7019
7020unsafe fn scuc_energy_requirement_at(
7021    instance: *const PioCalculationInstance,
7022    device_index: usize,
7023    requirement_index: usize,
7024    upper: bool,
7025    output: *mut PioScucEnergyRequirementView,
7026    error: *mut *mut PioError,
7027) -> bool {
7028    unsafe {
7029        entry(error, false, || {
7030            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7031            let device = scuc_device_or_error(inputs, device_index)?;
7032            let requirements = if upper {
7033                &device.energy_upper_bounds
7034            } else {
7035                &device.energy_lower_bounds
7036            };
7037            let requirement = requirements.get(requirement_index).ok_or_else(|| {
7038                boundary_error(
7039                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7040                    format!(
7041                        "SCUC device {device_index} {} energy requirement index {requirement_index} is out of range",
7042                        if upper { "upper" } else { "lower" }
7043                    ),
7044                )
7045            })?;
7046            *require_output(output, "output")? = PioScucEnergyRequirementView {
7047                start_time_hours: requirement.start_time,
7048                end_time_hours: requirement.end_time,
7049                energy_pu: requirement.energy,
7050            };
7051            Ok(true)
7052        })
7053    }
7054}
7055
7056#[unsafe(no_mangle)]
7057pub unsafe extern "C" fn pio_ac_scuc_instance_device_energy_upper_bound_count(
7058    instance: *const PioCalculationInstance,
7059    device_index: usize,
7060) -> usize {
7061    unsafe { scuc_energy_requirement_count(instance, device_index, true) }
7062}
7063
7064#[unsafe(no_mangle)]
7065pub unsafe extern "C" fn pio_ac_scuc_instance_device_energy_upper_bound_at(
7066    instance: *const PioCalculationInstance,
7067    device_index: usize,
7068    requirement_index: usize,
7069    output: *mut PioScucEnergyRequirementView,
7070    error: *mut *mut PioError,
7071) -> bool {
7072    unsafe {
7073        scuc_energy_requirement_at(
7074            instance,
7075            device_index,
7076            requirement_index,
7077            true,
7078            output,
7079            error,
7080        )
7081    }
7082}
7083
7084#[unsafe(no_mangle)]
7085pub unsafe extern "C" fn pio_ac_scuc_instance_device_energy_lower_bound_count(
7086    instance: *const PioCalculationInstance,
7087    device_index: usize,
7088) -> usize {
7089    unsafe { scuc_energy_requirement_count(instance, device_index, false) }
7090}
7091
7092#[unsafe(no_mangle)]
7093pub unsafe extern "C" fn pio_ac_scuc_instance_device_energy_lower_bound_at(
7094    instance: *const PioCalculationInstance,
7095    device_index: usize,
7096    requirement_index: usize,
7097    output: *mut PioScucEnergyRequirementView,
7098    error: *mut *mut PioError,
7099) -> bool {
7100    unsafe {
7101        scuc_energy_requirement_at(
7102            instance,
7103            device_index,
7104            requirement_index,
7105            false,
7106            output,
7107            error,
7108        )
7109    }
7110}
7111
7112#[unsafe(no_mangle)]
7113pub unsafe extern "C" fn pio_ac_scuc_instance_device_period_at(
7114    instance: *const PioCalculationInstance,
7115    device_index: usize,
7116    period_index: usize,
7117    output: *mut PioScucDevicePeriodView,
7118    error: *mut *mut PioError,
7119) -> bool {
7120    unsafe {
7121        entry(error, false, || {
7122            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7123            let device = scuc_device_or_error(inputs, device_index)?;
7124            let period = device.periods.get(period_index).ok_or_else(|| {
7125                boundary_error(
7126                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7127                    format!(
7128                        "SCUC device {device_index} period index {period_index} is out of range"
7129                    ),
7130                )
7131            })?;
7132            *require_output(output, "output")? = PioScucDevicePeriodView {
7133                on_status_min: period.on_status_min,
7134                on_status_max: period.on_status_max,
7135                active_power_min_pu: period.active_power_min,
7136                active_power_max_pu: period.active_power_max,
7137                reactive_power_min_pu: period.reactive_power_min,
7138                reactive_power_max_pu: period.reactive_power_max,
7139                energy_cost_block_count: period.energy_cost_blocks.len(),
7140                reserve_costs: scuc_reserve_costs_view(&period.reserve_costs),
7141            };
7142            Ok(true)
7143        })
7144    }
7145}
7146
7147#[unsafe(no_mangle)]
7148pub unsafe extern "C" fn pio_ac_scuc_instance_device_energy_cost_block_count(
7149    instance: *const PioCalculationInstance,
7150    device_index: usize,
7151    period_index: usize,
7152) -> usize {
7153    unsafe { PioCalculationInstance::get(instance) }
7154        .and_then(scuc_inputs)
7155        .and_then(|inputs| inputs.devices.get(device_index))
7156        .and_then(|device| device.periods.get(period_index))
7157        .map_or(0, |period| period.energy_cost_blocks.len())
7158}
7159
7160#[unsafe(no_mangle)]
7161pub unsafe extern "C" fn pio_ac_scuc_instance_device_energy_cost_block_at(
7162    instance: *const PioCalculationInstance,
7163    device_index: usize,
7164    period_index: usize,
7165    block_index: usize,
7166    output: *mut PioScucEnergyCostBlockView,
7167    error: *mut *mut PioError,
7168) -> bool {
7169    unsafe {
7170        entry(error, false, || {
7171            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7172            let device = scuc_device_or_error(inputs, device_index)?;
7173            let period = device.periods.get(period_index).ok_or_else(|| {
7174                boundary_error(
7175                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7176                    format!(
7177                        "SCUC device {device_index} period index {period_index} is out of range"
7178                    ),
7179                )
7180            })?;
7181            let block = period.energy_cost_blocks.get(block_index).ok_or_else(|| {
7182                boundary_error(
7183                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7184                    format!(
7185                        "SCUC device {device_index} period {period_index} energy cost block index {block_index} is out of range"
7186                    ),
7187                )
7188            })?;
7189            *require_output(output, "output")? = PioScucEnergyCostBlockView {
7190                marginal_cost: block.marginal_cost,
7191                block_size_pu: block.block_size,
7192            };
7193            Ok(true)
7194        })
7195    }
7196}
7197
7198#[unsafe(no_mangle)]
7199pub unsafe extern "C" fn pio_ac_scuc_instance_shunt_count(
7200    instance: *const PioCalculationInstance,
7201) -> usize {
7202    unsafe { PioCalculationInstance::get(instance) }
7203        .and_then(scuc_inputs)
7204        .map_or(0, |inputs| inputs.shunts.len())
7205}
7206
7207fn scuc_shunt_view(shunt: &powerio_prob::ScucShunt) -> PioScucShuntView {
7208    PioScucShuntView {
7209        id: component_id_view(&shunt.id),
7210        conductance_per_step_pu: shunt.conductance_per_step,
7211        susceptance_per_step_pu: shunt.susceptance_per_step,
7212        step_min: shunt.step_min,
7213        step_max: shunt.step_max,
7214        initial_step: shunt.initial_step,
7215    }
7216}
7217
7218#[unsafe(no_mangle)]
7219pub unsafe extern "C" fn pio_ac_scuc_instance_shunt_at(
7220    instance: *const PioCalculationInstance,
7221    index: usize,
7222    output: *mut PioScucShuntView,
7223    error: *mut *mut PioError,
7224) -> bool {
7225    unsafe {
7226        entry(error, false, || {
7227            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7228            let shunt = inputs.shunts.get(index).ok_or_else(|| {
7229                boundary_error(
7230                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7231                    format!("SCUC shunt index {index} is out of range"),
7232                )
7233            })?;
7234            *require_output(output, "output")? = scuc_shunt_view(shunt);
7235            Ok(true)
7236        })
7237    }
7238}
7239
7240/// Read one shunt by its exact source UID.
7241#[unsafe(no_mangle)]
7242pub unsafe extern "C" fn pio_ac_scuc_instance_shunt_get(
7243    instance: *const PioCalculationInstance,
7244    uid: *const c_char,
7245    uid_len: usize,
7246    output: *mut PioScucShuntView,
7247    error: *mut *mut PioError,
7248) -> bool {
7249    unsafe {
7250        entry(error, false, || {
7251            let uid = required_str(uid, uid_len, "shunt_uid")?;
7252            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7253            let shunt = inputs.shunt(uid).ok_or_else(|| {
7254                boundary_error(
7255                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7256                    format!("SCUC shunt UID '{uid}' does not exist"),
7257                )
7258            })?;
7259            *require_output(output, "output")? = scuc_shunt_view(shunt);
7260            Ok(true)
7261        })
7262    }
7263}
7264
7265#[unsafe(no_mangle)]
7266pub unsafe extern "C" fn pio_ac_scuc_instance_branch_switching_cost_count(
7267    instance: *const PioCalculationInstance,
7268) -> usize {
7269    unsafe { PioCalculationInstance::get(instance) }
7270        .and_then(scuc_inputs)
7271        .map_or(0, |inputs| inputs.branch_switching_costs.len())
7272}
7273
7274#[unsafe(no_mangle)]
7275pub unsafe extern "C" fn pio_ac_scuc_instance_branch_switching_cost_at(
7276    instance: *const PioCalculationInstance,
7277    index: usize,
7278    output: *mut PioScucBranchSwitchingCostView,
7279    error: *mut *mut PioError,
7280) -> bool {
7281    unsafe {
7282        entry(error, false, || {
7283            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7284            let cost = inputs.branch_switching_costs.get(index).ok_or_else(|| {
7285                boundary_error(
7286                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7287                    format!("SCUC branch switching cost index {index} is out of range"),
7288                )
7289            })?;
7290            *require_output(output, "output")? = PioScucBranchSwitchingCostView {
7291                id: component_id_view(&cost.id),
7292                connection_cost: cost.connection_cost,
7293                disconnection_cost: cost.disconnection_cost,
7294            };
7295            Ok(true)
7296        })
7297    }
7298}
7299
7300#[unsafe(no_mangle)]
7301pub unsafe extern "C" fn pio_ac_scuc_instance_transformer_control_count(
7302    instance: *const PioCalculationInstance,
7303) -> usize {
7304    unsafe { PioCalculationInstance::get(instance) }
7305        .and_then(scuc_inputs)
7306        .map_or(0, |inputs| inputs.transformer_controls.len())
7307}
7308
7309#[unsafe(no_mangle)]
7310pub unsafe extern "C" fn pio_ac_scuc_instance_transformer_control_at(
7311    instance: *const PioCalculationInstance,
7312    index: usize,
7313    output: *mut PioScucTransformerControlView,
7314    error: *mut *mut PioError,
7315) -> bool {
7316    unsafe {
7317        entry(error, false, || {
7318            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7319            let control = inputs.transformer_controls.get(index).ok_or_else(|| {
7320                boundary_error(
7321                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7322                    format!("SCUC transformer control index {index} is out of range"),
7323                )
7324            })?;
7325            *require_output(output, "output")? = PioScucTransformerControlView {
7326                id: component_id_view(&control.id),
7327                tap_ratio_min: control.tap_ratio_min,
7328                tap_ratio_max: control.tap_ratio_max,
7329                phase_shift_min_radians: control.phase_shift_min,
7330                phase_shift_max_radians: control.phase_shift_max,
7331            };
7332            Ok(true)
7333        })
7334    }
7335}
7336
7337#[unsafe(no_mangle)]
7338pub unsafe extern "C" fn pio_ac_scuc_instance_active_reserve_zone_count(
7339    instance: *const PioCalculationInstance,
7340) -> usize {
7341    unsafe { PioCalculationInstance::get(instance) }
7342        .and_then(scuc_inputs)
7343        .map_or(0, |inputs| inputs.active_reserve_zones.len())
7344}
7345
7346#[unsafe(no_mangle)]
7347pub unsafe extern "C" fn pio_ac_scuc_instance_active_reserve_zone_at(
7348    instance: *const PioCalculationInstance,
7349    index: usize,
7350    output: *mut PioScucActiveReserveZoneView,
7351    error: *mut *mut PioError,
7352) -> bool {
7353    unsafe {
7354        entry(error, false, || {
7355            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7356            let zone = inputs.active_reserve_zones.get(index).ok_or_else(|| {
7357                boundary_error(
7358                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7359                    format!("SCUC active reserve zone index {index} is out of range"),
7360                )
7361            })?;
7362            *require_output(output, "output")? = PioScucActiveReserveZoneView {
7363                id: component_id_view(&zone.id),
7364                regulation_up_requirement_fraction: zone.regulation_up_requirement_fraction,
7365                regulation_down_requirement_fraction: zone.regulation_down_requirement_fraction,
7366                synchronized_requirement_fraction: zone.synchronized_requirement_fraction,
7367                nonsynchronized_requirement_fraction: zone.nonsynchronized_requirement_fraction,
7368                regulation_up_violation_cost: zone.regulation_up_violation_cost,
7369                regulation_down_violation_cost: zone.regulation_down_violation_cost,
7370                synchronized_violation_cost: zone.synchronized_violation_cost,
7371                nonsynchronized_violation_cost: zone.nonsynchronized_violation_cost,
7372                ramping_up_violation_cost: zone.ramping_up_violation_cost,
7373                ramping_down_violation_cost: zone.ramping_down_violation_cost,
7374                period_count: zone.ramping_up_requirement.len(),
7375                bus_count: zone.buses.len(),
7376            };
7377            Ok(true)
7378        })
7379    }
7380}
7381
7382#[unsafe(no_mangle)]
7383pub unsafe extern "C" fn pio_ac_scuc_instance_active_reserve_zone_period_at(
7384    instance: *const PioCalculationInstance,
7385    zone_index: usize,
7386    period_index: usize,
7387    output: *mut PioScucActiveReservePeriodView,
7388    error: *mut *mut PioError,
7389) -> bool {
7390    unsafe {
7391        entry(error, false, || {
7392            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7393            let zone = inputs.active_reserve_zones.get(zone_index).ok_or_else(|| {
7394                boundary_error(
7395                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7396                    format!("SCUC active reserve zone index {zone_index} is out of range"),
7397                )
7398            })?;
7399            let up = zone
7400                .ramping_up_requirement
7401                .get(period_index)
7402                .copied()
7403                .ok_or_else(|| {
7404                    boundary_error(
7405                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7406                        format!("SCUC reserve period index {period_index} is out of range"),
7407                    )
7408                })?;
7409            let down = zone
7410                .ramping_down_requirement
7411                .get(period_index)
7412                .copied()
7413                .ok_or_else(|| {
7414                    boundary_error(
7415                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7416                        format!("SCUC reserve period index {period_index} is out of range"),
7417                    )
7418                })?;
7419            *require_output(output, "output")? = PioScucActiveReservePeriodView {
7420                ramping_up_requirement_pu: up,
7421                ramping_down_requirement_pu: down,
7422            };
7423            Ok(true)
7424        })
7425    }
7426}
7427
7428#[unsafe(no_mangle)]
7429pub unsafe extern "C" fn pio_ac_scuc_instance_active_reserve_zone_bus_at(
7430    instance: *const PioCalculationInstance,
7431    zone_index: usize,
7432    bus_index: usize,
7433    output: *mut PioComponentIdView,
7434    error: *mut *mut PioError,
7435) -> bool {
7436    unsafe {
7437        entry(error, false, || {
7438            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7439            let zone = inputs.active_reserve_zones.get(zone_index).ok_or_else(|| {
7440                boundary_error(
7441                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7442                    format!("SCUC active reserve zone index {zone_index} is out of range"),
7443                )
7444            })?;
7445            let bus = zone.buses.get(bus_index).ok_or_else(|| {
7446                boundary_error(
7447                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7448                    format!("SCUC reserve zone bus index {bus_index} is out of range"),
7449                )
7450            })?;
7451            *require_output(output, "output")? = component_id_view(bus);
7452            Ok(true)
7453        })
7454    }
7455}
7456
7457#[unsafe(no_mangle)]
7458pub unsafe extern "C" fn pio_ac_scuc_instance_reactive_reserve_zone_count(
7459    instance: *const PioCalculationInstance,
7460) -> usize {
7461    unsafe { PioCalculationInstance::get(instance) }
7462        .and_then(scuc_inputs)
7463        .map_or(0, |inputs| inputs.reactive_reserve_zones.len())
7464}
7465
7466#[unsafe(no_mangle)]
7467pub unsafe extern "C" fn pio_ac_scuc_instance_reactive_reserve_zone_at(
7468    instance: *const PioCalculationInstance,
7469    index: usize,
7470    output: *mut PioScucReactiveReserveZoneView,
7471    error: *mut *mut PioError,
7472) -> bool {
7473    unsafe {
7474        entry(error, false, || {
7475            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7476            let zone = inputs.reactive_reserve_zones.get(index).ok_or_else(|| {
7477                boundary_error(
7478                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7479                    format!("SCUC reactive reserve zone index {index} is out of range"),
7480                )
7481            })?;
7482            *require_output(output, "output")? = PioScucReactiveReserveZoneView {
7483                id: component_id_view(&zone.id),
7484                reactive_up_violation_cost: zone.reactive_up_violation_cost,
7485                reactive_down_violation_cost: zone.reactive_down_violation_cost,
7486                period_count: zone.reactive_up_requirement.len(),
7487                bus_count: zone.buses.len(),
7488            };
7489            Ok(true)
7490        })
7491    }
7492}
7493
7494#[unsafe(no_mangle)]
7495pub unsafe extern "C" fn pio_ac_scuc_instance_reactive_reserve_zone_period_at(
7496    instance: *const PioCalculationInstance,
7497    zone_index: usize,
7498    period_index: usize,
7499    output: *mut PioScucReactiveReservePeriodView,
7500    error: *mut *mut PioError,
7501) -> bool {
7502    unsafe {
7503        entry(error, false, || {
7504            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7505            let zone = inputs
7506                .reactive_reserve_zones
7507                .get(zone_index)
7508                .ok_or_else(|| {
7509                    boundary_error(
7510                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7511                        format!("SCUC reactive reserve zone index {zone_index} is out of range"),
7512                    )
7513                })?;
7514            let up = zone
7515                .reactive_up_requirement
7516                .get(period_index)
7517                .copied()
7518                .ok_or_else(|| {
7519                    boundary_error(
7520                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7521                        format!("SCUC reserve period index {period_index} is out of range"),
7522                    )
7523                })?;
7524            let down = zone
7525                .reactive_down_requirement
7526                .get(period_index)
7527                .copied()
7528                .ok_or_else(|| {
7529                    boundary_error(
7530                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7531                        format!("SCUC reserve period index {period_index} is out of range"),
7532                    )
7533                })?;
7534            *require_output(output, "output")? = PioScucReactiveReservePeriodView {
7535                reactive_up_requirement_pu: up,
7536                reactive_down_requirement_pu: down,
7537            };
7538            Ok(true)
7539        })
7540    }
7541}
7542
7543#[unsafe(no_mangle)]
7544pub unsafe extern "C" fn pio_ac_scuc_instance_reactive_reserve_zone_bus_at(
7545    instance: *const PioCalculationInstance,
7546    zone_index: usize,
7547    bus_index: usize,
7548    output: *mut PioComponentIdView,
7549    error: *mut *mut PioError,
7550) -> bool {
7551    unsafe {
7552        entry(error, false, || {
7553            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7554            let zone = inputs
7555                .reactive_reserve_zones
7556                .get(zone_index)
7557                .ok_or_else(|| {
7558                    boundary_error(
7559                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7560                        format!("SCUC reactive reserve zone index {zone_index} is out of range"),
7561                    )
7562                })?;
7563            let bus = zone.buses.get(bus_index).ok_or_else(|| {
7564                boundary_error(
7565                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7566                    format!("SCUC reserve zone bus index {bus_index} is out of range"),
7567                )
7568            })?;
7569            *require_output(output, "output")? = component_id_view(bus);
7570            Ok(true)
7571        })
7572    }
7573}
7574
7575/// Return the number of named contingencies.
7576#[unsafe(no_mangle)]
7577pub unsafe extern "C" fn pio_ac_scuc_instance_contingency_count(
7578    instance: *const PioCalculationInstance,
7579) -> usize {
7580    unsafe { PioCalculationInstance::get(instance) }
7581        .and_then(scuc_inputs)
7582        .map_or(0, |inputs| inputs.contingencies.len())
7583}
7584
7585fn scuc_contingency_view(contingency: &powerio_prob::ScucContingency) -> PioScucContingencyView {
7586    PioScucContingencyView {
7587        id: component_id_view(&contingency.id),
7588        component_count: contingency.components.len(),
7589    }
7590}
7591
7592/// Read one named contingency in source order.
7593#[unsafe(no_mangle)]
7594pub unsafe extern "C" fn pio_ac_scuc_instance_contingency_at(
7595    instance: *const PioCalculationInstance,
7596    contingency_index: usize,
7597    output: *mut PioScucContingencyView,
7598    error: *mut *mut PioError,
7599) -> bool {
7600    unsafe {
7601        entry(error, false, || {
7602            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7603            let contingency = inputs.contingencies.get(contingency_index).ok_or_else(|| {
7604                boundary_error(
7605                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7606                    format!("SCUC contingency index {contingency_index} is out of range"),
7607                )
7608            })?;
7609            *require_output(output, "output")? = scuc_contingency_view(contingency);
7610            Ok(true)
7611        })
7612    }
7613}
7614
7615/// Read one named contingency by its exact source UID.
7616#[unsafe(no_mangle)]
7617pub unsafe extern "C" fn pio_ac_scuc_instance_contingency_get(
7618    instance: *const PioCalculationInstance,
7619    uid: *const c_char,
7620    uid_len: usize,
7621    output: *mut PioScucContingencyView,
7622    error: *mut *mut PioError,
7623) -> bool {
7624    unsafe {
7625        entry(error, false, || {
7626            let uid = required_str(uid, uid_len, "contingency_uid")?;
7627            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7628            let contingency = inputs.contingency(uid).ok_or_else(|| {
7629                boundary_error(
7630                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7631                    format!("SCUC contingency UID '{uid}' does not exist"),
7632                )
7633            })?;
7634            *require_output(output, "output")? = scuc_contingency_view(contingency);
7635            Ok(true)
7636        })
7637    }
7638}
7639
7640/// Read one stable component identity from a named contingency.
7641#[unsafe(no_mangle)]
7642pub unsafe extern "C" fn pio_ac_scuc_instance_contingency_component_at(
7643    instance: *const PioCalculationInstance,
7644    contingency_index: usize,
7645    component_index: usize,
7646    output: *mut PioScucContingencyComponentView,
7647    error: *mut *mut PioError,
7648) -> bool {
7649    unsafe {
7650        entry(error, false, || {
7651            let inputs = scuc_inputs_or_error(require_calculation_instance(instance)?)?;
7652            let contingency = inputs.contingencies.get(contingency_index).ok_or_else(|| {
7653                boundary_error(
7654                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7655                    format!("SCUC contingency index {contingency_index} is out of range"),
7656                )
7657            })?;
7658            let component = contingency.components.get(component_index).ok_or_else(|| {
7659                boundary_error(
7660                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
7661                    format!(
7662                        "SCUC contingency {contingency_index} component index {component_index} is out of range"
7663                    ),
7664                )
7665            })?;
7666            *require_output(output, "output")? = PioScucContingencyComponentView {
7667                id: component_id_view(component),
7668            };
7669            Ok(true)
7670        })
7671    }
7672}
7673
7674#[unsafe(no_mangle)]
7675pub unsafe extern "C" fn pio_calculation_solution_balanced_network(
7676    solution: *const PioCalculationSolution,
7677    error: *mut *mut PioError,
7678) -> *mut PioBalancedNetwork {
7679    unsafe {
7680        entry(error, std::ptr::null_mut(), || {
7681            let solution = PioCalculationSolution::get(solution).ok_or_else(|| {
7682                boundary_error(
7683                    &codes::BIND_CAPI_NULL_HANDLE,
7684                    "PioCalculationSolution must not be NULL",
7685                )
7686            })?;
7687            let projection = match solution.value() {
7688                Some(PioValue::DcPfSolution(_)) => BalancedNetworkProjection::DcPfSolution,
7689                Some(PioValue::AcPfSolution(_)) => BalancedNetworkProjection::AcPfSolution,
7690                Some(PioValue::DcOpfSolution(_)) => BalancedNetworkProjection::DcOpfSolution,
7691                Some(PioValue::AcOpfSolution(_)) => BalancedNetworkProjection::AcOpfSolution,
7692                Some(PioValue::SocwrOpfSolution(_)) => BalancedNetworkProjection::SocwrOpfSolution,
7693                Some(PioValue::AcScucSolution(_)) => BalancedNetworkProjection::AcScucSolution,
7694                _ => {
7695                    return Err(boundary_error(
7696                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
7697                        "the calculation solution does not use powerio.BalancedNetwork",
7698                    ));
7699                }
7700            };
7701            Ok(make_balanced_network_view(solution, projection))
7702        })
7703    }
7704}
7705
7706#[unsafe(no_mangle)]
7707pub unsafe extern "C" fn pio_calculation_solution_multiconductor_network(
7708    solution: *const PioCalculationSolution,
7709    error: *mut *mut PioError,
7710) -> *mut PioMulticonductorNetwork {
7711    unsafe {
7712        entry(error, std::ptr::null_mut(), || {
7713            let solution = PioCalculationSolution::get(solution).ok_or_else(|| {
7714                boundary_error(
7715                    &codes::BIND_CAPI_NULL_HANDLE,
7716                    "PioCalculationSolution must not be NULL",
7717                )
7718            })?;
7719            let projection = match solution.value() {
7720                Some(PioValue::McAcPfSolution(_)) => {
7721                    MulticonductorNetworkProjection::McAcPfSolution
7722                }
7723                Some(PioValue::McAcOpfSolution(_)) => {
7724                    MulticonductorNetworkProjection::McAcOpfSolution
7725                }
7726                _ => {
7727                    return Err(boundary_error(
7728                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
7729                        "the calculation solution does not use powerio.MulticonductorNetwork",
7730                    ));
7731                }
7732            };
7733            Ok(make_multiconductor_network_view(solution, projection))
7734        })
7735    }
7736}
7737
7738/// Read one operating point quantity by its PowerIO quantity name and stable
7739/// component identity. Multiconductor terminal identities use
7740/// component/terminal. Returns false when the point does not contain the
7741/// quantity or identity.
7742#[unsafe(no_mangle)]
7743pub unsafe extern "C" fn pio_operating_point_get_value(
7744    point: *const PioOperatingPoint,
7745    quantity: *const c_char,
7746    quantity_len: usize,
7747    identity: *const c_char,
7748    identity_len: usize,
7749    out_value: *mut f64,
7750    error: *mut *mut PioError,
7751) -> bool {
7752    unsafe {
7753        entry(error, false, || {
7754            if out_value.is_null() {
7755                return Err(boundary_error(
7756                    &codes::BIND_CAPI_NULL_ARGUMENT,
7757                    "out_value must not be NULL",
7758                ));
7759            }
7760            let quantity = required_str(quantity, quantity_len, "quantity")?;
7761            let identity = required_str(identity, identity_len, "identity")?;
7762            let point = PioOperatingPoint::get(point).ok_or_else(|| {
7763                boundary_error(
7764                    &codes::BIND_CAPI_NULL_HANDLE,
7765                    "PioOperatingPoint must not be NULL",
7766                )
7767            })?;
7768            let value = if let Some(point) = point.balanced() {
7769                use powerio_prob::{
7770                    BalancedOperatingPointFlag as F, BalancedOperatingPointQuantity as Q,
7771                };
7772                match quantity {
7773                    "bus_voltage_magnitude" => point.values(Q::BusVoltageMagnitude),
7774                    "bus_voltage_angle" => point.values(Q::BusVoltageAngle),
7775                    "bus_active_injection" => point.values(Q::BusActiveInjection),
7776                    "bus_reactive_injection" => point.values(Q::BusReactiveInjection),
7777                    "generator_active_power" => point.values(Q::GeneratorActivePower),
7778                    "generator_reactive_power" => point.values(Q::GeneratorReactivePower),
7779                    "generator_voltage_setpoint" => point.values(Q::GeneratorVoltageSetpoint),
7780                    "load_active_power" => point.values(Q::LoadActivePower),
7781                    "load_reactive_power" => point.values(Q::LoadReactivePower),
7782                    "branch_tap_ratio" => point.values(Q::BranchTapRatio),
7783                    "branch_phase_shift" => point.values(Q::BranchPhaseShift),
7784                    "generator_in_service" => {
7785                        let found = point
7786                            .flags(F::GeneratorInService)
7787                            .and_then(|mut values| values.find(|(key, _)| *key == identity))
7788                            .map(|(_, value)| if value { 1.0 } else { 0.0 });
7789                        if let Some(found) = found {
7790                            *out_value = found;
7791                            return Ok(true);
7792                        }
7793                        return Ok(false);
7794                    }
7795                    "branch_in_service" => {
7796                        let found = point
7797                            .flags(F::BranchInService)
7798                            .and_then(|mut values| values.find(|(key, _)| *key == identity))
7799                            .map(|(_, value)| if value { 1.0 } else { 0.0 });
7800                        if let Some(found) = found {
7801                            *out_value = found;
7802                            return Ok(true);
7803                        }
7804                        return Ok(false);
7805                    }
7806                    "switch_closed" => {
7807                        let found = point
7808                            .flags(F::SwitchClosed)
7809                            .and_then(|mut values| values.find(|(key, _)| *key == identity))
7810                            .map(|(_, value)| if value { 1.0 } else { 0.0 });
7811                        if let Some(found) = found {
7812                            *out_value = found;
7813                            return Ok(true);
7814                        }
7815                        return Ok(false);
7816                    }
7817                    _ => {
7818                        return Err(boundary_error(
7819                            &codes::REQUEST_CAPI_QUANTITY_UNKNOWN,
7820                            format!("unknown balanced operating point quantity '{quantity}'"),
7821                        ));
7822                    }
7823                }
7824                .and_then(|mut values| values.find(|(key, _)| *key == identity))
7825                .map(|(_, value)| value)
7826            } else if let Some(point) = point.multiconductor() {
7827                use powerio_prob::{
7828                    MulticonductorOperatingPointFlag as F,
7829                    MulticonductorOperatingPointQuantity as Q,
7830                };
7831                match quantity {
7832                    "terminal_voltage_magnitude" => point.values(Q::TerminalVoltageMagnitude),
7833                    "terminal_voltage_angle" => point.values(Q::TerminalVoltageAngle),
7834                    "load_active_power" => point.values(Q::LoadActivePower),
7835                    "load_reactive_power" => point.values(Q::LoadReactivePower),
7836                    "generator_active_power" => point.values(Q::GeneratorActivePower),
7837                    "generator_reactive_power" => point.values(Q::GeneratorReactivePower),
7838                    "transformer_tap" => point.values(Q::TransformerTap),
7839                    "capacitor_steps" => point.values(Q::CapacitorSteps),
7840                    "switch_closed" => {
7841                        let found = point
7842                            .flags(F::SwitchClosed)
7843                            .and_then(|mut values| values.find(|(key, _)| *key == identity))
7844                            .map(|(_, value)| if value { 1.0 } else { 0.0 });
7845                        if let Some(found) = found {
7846                            *out_value = found;
7847                            return Ok(true);
7848                        }
7849                        return Ok(false);
7850                    }
7851                    _ => {
7852                        return Err(boundary_error(
7853                            &codes::REQUEST_CAPI_QUANTITY_UNKNOWN,
7854                            format!("unknown multiconductor operating point quantity '{quantity}'"),
7855                        ));
7856                    }
7857                }
7858                .and_then(|mut values| values.find(|(key, _)| *key == identity))
7859                .map(|(_, value)| value)
7860            } else {
7861                return Err(boundary_error(
7862                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
7863                    "the handle does not refer to an operating point",
7864                ));
7865            };
7866            if let Some(value) = value {
7867                *out_value = value;
7868                Ok(true)
7869            } else {
7870                Ok(false)
7871            }
7872        })
7873    }
7874}
7875
7876#[unsafe(no_mangle)]
7877pub unsafe extern "C" fn pio_operating_point_retain(
7878    point: *const PioOperatingPoint,
7879) -> *mut PioOperatingPoint {
7880    unsafe { PioOperatingPoint::retain_raw(point) }
7881}
7882
7883#[unsafe(no_mangle)]
7884pub unsafe extern "C" fn pio_operating_point_release(point: *mut PioOperatingPoint) {
7885    unsafe { PioOperatingPoint::release_raw(point) };
7886}
7887
7888#[unsafe(no_mangle)]
7889pub unsafe extern "C" fn pio_calculation_instance_retain(
7890    instance: *const PioCalculationInstance,
7891) -> *mut PioCalculationInstance {
7892    unsafe { PioCalculationInstance::retain_raw(instance) }
7893}
7894
7895#[unsafe(no_mangle)]
7896pub unsafe extern "C" fn pio_calculation_instance_release(instance: *mut PioCalculationInstance) {
7897    unsafe { PioCalculationInstance::release_raw(instance) };
7898}
7899
7900#[unsafe(no_mangle)]
7901pub unsafe extern "C" fn pio_dc_opf_preparation_retain(
7902    preparation: *const PioDcOpfPreparation,
7903) -> *mut PioDcOpfPreparation {
7904    unsafe { PioDcOpfPreparation::retain_raw(preparation) }
7905}
7906
7907#[unsafe(no_mangle)]
7908pub unsafe extern "C" fn pio_dc_opf_preparation_release(preparation: *mut PioDcOpfPreparation) {
7909    unsafe { PioDcOpfPreparation::release_raw(preparation) };
7910}
7911
7912#[unsafe(no_mangle)]
7913pub unsafe extern "C" fn pio_ac_opf_preparation_retain(
7914    preparation: *const PioAcOpfPreparation,
7915) -> *mut PioAcOpfPreparation {
7916    unsafe { PioAcOpfPreparation::retain_raw(preparation) }
7917}
7918
7919#[unsafe(no_mangle)]
7920pub unsafe extern "C" fn pio_ac_opf_preparation_release(preparation: *mut PioAcOpfPreparation) {
7921    unsafe { PioAcOpfPreparation::release_raw(preparation) };
7922}
7923
7924#[unsafe(no_mangle)]
7925pub unsafe extern "C" fn pio_calculation_solution_retain(
7926    solution: *const PioCalculationSolution,
7927) -> *mut PioCalculationSolution {
7928    unsafe { PioCalculationSolution::retain_raw(solution) }
7929}
7930
7931#[unsafe(no_mangle)]
7932pub unsafe extern "C" fn pio_calculation_solution_release(solution: *mut PioCalculationSolution) {
7933    unsafe { PioCalculationSolution::release_raw(solution) };
7934}
7935
7936#[unsafe(no_mangle)]
7937pub unsafe extern "C" fn pio_value_retain(value: *const PioValueHandle) -> *mut PioValueHandle {
7938    unsafe { PioValueHandle::retain_raw(value) }
7939}
7940
7941#[unsafe(no_mangle)]
7942pub unsafe extern "C" fn pio_value_release(value: *mut PioValueHandle) {
7943    unsafe { PioValueHandle::release_raw(value) };
7944}
7945
7946// ---- collections -----------------------------------------------------------
7947
7948fn time_series(value: &ValueInner) -> Option<&PioTimeSeries> {
7949    match value.value()? {
7950        PioValue::TimeSeries(series) => Some(series),
7951        _ => None,
7952    }
7953}
7954
7955fn scenario_set(value: &ValueInner) -> Option<&PioScenarioSet> {
7956    match value.value()? {
7957        PioValue::ScenarioSet(scenarios) => Some(scenarios),
7958        _ => None,
7959    }
7960}
7961
7962#[unsafe(no_mangle)]
7963pub unsafe extern "C" fn pio_time_series_len(series: *const PioTimeSeriesHandle) -> usize {
7964    unsafe { PioTimeSeriesHandle::get(series) }
7965        .and_then(time_series)
7966        .map_or(0, PioTimeSeries::len)
7967}
7968
7969#[unsafe(no_mangle)]
7970pub unsafe extern "C" fn pio_time_series_element_type(
7971    series: *const PioTimeSeriesHandle,
7972) -> PioStringView {
7973    unsafe { PioTimeSeriesHandle::get(series) }
7974        .and_then(time_series)
7975        .map_or(PioStringView::EMPTY, |series| {
7976            PioStringView::new(series.element_type())
7977        })
7978}
7979
7980/// Return an owner-rooted entry by zero-based position.
7981#[unsafe(no_mangle)]
7982pub unsafe extern "C" fn pio_time_series_get(
7983    series: *const PioTimeSeriesHandle,
7984    index: usize,
7985    error: *mut *mut PioError,
7986) -> *mut PioValueHandle {
7987    unsafe {
7988        entry(error, std::ptr::null_mut(), || {
7989            let series = PioTimeSeriesHandle::get(series).ok_or_else(|| {
7990                boundary_error(
7991                    &codes::BIND_CAPI_NULL_HANDLE,
7992                    "PioTimeSeriesHandle must not be NULL",
7993                )
7994            })?;
7995            let values = time_series(series).ok_or_else(|| {
7996                boundary_error(
7997                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
7998                    "the handle does not refer to a time series",
7999                )
8000            })?;
8001            if values.get(index).is_none() {
8002                return Err(boundary_error(
8003                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8004                    format!("time series index {index} is out of range"),
8005                ));
8006            }
8007            Ok(value_handle(series.child(ValueStep::TimeSeries(index))))
8008        })
8009    }
8010}
8011
8012#[unsafe(no_mangle)]
8013pub unsafe extern "C" fn pio_time_series_retain(
8014    series: *const PioTimeSeriesHandle,
8015) -> *mut PioTimeSeriesHandle {
8016    unsafe { PioTimeSeriesHandle::retain_raw(series) }
8017}
8018
8019#[unsafe(no_mangle)]
8020pub unsafe extern "C" fn pio_time_series_release(series: *mut PioTimeSeriesHandle) {
8021    unsafe { PioTimeSeriesHandle::release_raw(series) };
8022}
8023
8024#[unsafe(no_mangle)]
8025pub unsafe extern "C" fn pio_scenario_set_len(set: *const PioScenarioSetHandle) -> usize {
8026    unsafe { PioScenarioSetHandle::get(set) }
8027        .and_then(scenario_set)
8028        .map_or(0, PioScenarioSet::len)
8029}
8030
8031#[unsafe(no_mangle)]
8032pub unsafe extern "C" fn pio_scenario_set_element_type(
8033    set: *const PioScenarioSetHandle,
8034) -> PioStringView {
8035    unsafe { PioScenarioSetHandle::get(set) }
8036        .and_then(scenario_set)
8037        .map_or(PioStringView::EMPTY, |set| {
8038            PioStringView::new(set.element_type())
8039        })
8040}
8041
8042#[unsafe(no_mangle)]
8043pub unsafe extern "C" fn pio_scenario_set_id_at(
8044    set: *const PioScenarioSetHandle,
8045    index: usize,
8046) -> PioStringView {
8047    unsafe { PioScenarioSetHandle::get(set) }
8048        .and_then(scenario_set)
8049        .and_then(|set| set.iter().nth(index))
8050        .map_or(PioStringView::EMPTY, |scenario| {
8051            PioStringView::new(scenario.id().as_str())
8052        })
8053}
8054
8055/// Return an owner-rooted scenario value by zero-based position.
8056#[unsafe(no_mangle)]
8057pub unsafe extern "C" fn pio_scenario_set_get_at(
8058    set: *const PioScenarioSetHandle,
8059    index: usize,
8060    error: *mut *mut PioError,
8061) -> *mut PioValueHandle {
8062    unsafe {
8063        entry(error, std::ptr::null_mut(), || {
8064            let set = PioScenarioSetHandle::get(set).ok_or_else(|| {
8065                boundary_error(
8066                    &codes::BIND_CAPI_NULL_HANDLE,
8067                    "PioScenarioSetHandle must not be NULL",
8068                )
8069            })?;
8070            let values = scenario_set(set).ok_or_else(|| {
8071                boundary_error(
8072                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
8073                    "the handle does not refer to a scenario set",
8074                )
8075            })?;
8076            if values.get_at(index).is_none() {
8077                return Err(boundary_error(
8078                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8079                    format!("scenario position {index} is out of range"),
8080                ));
8081            }
8082            Ok(value_handle(set.child(ValueStep::Scenario(index))))
8083        })
8084    }
8085}
8086
8087/// Return an owner-rooted scenario value by exact scenario ID.
8088#[unsafe(no_mangle)]
8089pub unsafe extern "C" fn pio_scenario_set_get(
8090    set: *const PioScenarioSetHandle,
8091    id: *const c_char,
8092    id_len: usize,
8093    error: *mut *mut PioError,
8094) -> *mut PioValueHandle {
8095    unsafe {
8096        entry(error, std::ptr::null_mut(), || {
8097            let id = required_str(id, id_len, "scenario_id")?;
8098            let set = PioScenarioSetHandle::get(set).ok_or_else(|| {
8099                boundary_error(
8100                    &codes::BIND_CAPI_NULL_HANDLE,
8101                    "PioScenarioSetHandle must not be NULL",
8102                )
8103            })?;
8104            let values = scenario_set(set).ok_or_else(|| {
8105                boundary_error(
8106                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
8107                    "the handle does not refer to a scenario set",
8108                )
8109            })?;
8110            let Some(position) = values
8111                .iter()
8112                .position(|scenario| scenario.id().as_str() == id)
8113            else {
8114                return Err(boundary_error(
8115                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8116                    format!("scenario ID '{id}' does not exist"),
8117                ));
8118            };
8119            Ok(value_handle(set.child(ValueStep::Scenario(position))))
8120        })
8121    }
8122}
8123
8124#[unsafe(no_mangle)]
8125pub unsafe extern "C" fn pio_scenario_set_retain(
8126    set: *const PioScenarioSetHandle,
8127) -> *mut PioScenarioSetHandle {
8128    unsafe { PioScenarioSetHandle::retain_raw(set) }
8129}
8130
8131#[unsafe(no_mangle)]
8132pub unsafe extern "C" fn pio_scenario_set_release(set: *mut PioScenarioSetHandle) {
8133    unsafe { PioScenarioSetHandle::release_raw(set) };
8134}
8135
8136// ---- network access --------------------------------------------------------
8137
8138#[unsafe(no_mangle)]
8139pub unsafe extern "C" fn pio_balanced_network_name(
8140    network: *const PioBalancedNetwork,
8141) -> PioStringView {
8142    unsafe { PioBalancedNetwork::get(network) }
8143        .and_then(BalancedNetworkInner::network)
8144        .map_or(PioStringView::EMPTY, |network| {
8145            PioStringView::new(network.name())
8146        })
8147}
8148
8149#[unsafe(no_mangle)]
8150pub unsafe extern "C" fn pio_balanced_network_base_mva(network: *const PioBalancedNetwork) -> f64 {
8151    unsafe { PioBalancedNetwork::get(network) }
8152        .and_then(BalancedNetworkInner::network)
8153        .map_or(f64::NAN, BalancedNetwork::base_mva)
8154}
8155
8156#[unsafe(no_mangle)]
8157pub unsafe extern "C" fn pio_balanced_network_base_frequency_hz(
8158    network: *const PioBalancedNetwork,
8159) -> f64 {
8160    unsafe { PioBalancedNetwork::get(network) }
8161        .and_then(BalancedNetworkInner::network)
8162        .map_or(f64::NAN, BalancedNetwork::base_frequency)
8163}
8164
8165/// Read the optional coordinate space metadata for a balanced network.
8166#[unsafe(no_mangle)]
8167pub unsafe extern "C" fn pio_balanced_network_geo(
8168    network: *const PioBalancedNetwork,
8169    output: *mut PioBalancedGeoView,
8170    error: *mut *mut PioError,
8171) -> bool {
8172    unsafe {
8173        entry(error, false, || {
8174            let network = require_balanced_network(network)?;
8175            *require_output(output, "output")? = balanced_geo_view(network.geo().as_ref());
8176            Ok(true)
8177        })
8178    }
8179}
8180
8181#[unsafe(no_mangle)]
8182pub unsafe extern "C" fn pio_balanced_network_has_detailed_connectivity(
8183    network: *const PioBalancedNetwork,
8184) -> bool {
8185    unsafe { PioBalancedNetwork::get(network) }
8186        .and_then(BalancedNetworkInner::network)
8187        .is_some_and(|network| network.detailed_connectivity().is_some())
8188}
8189
8190/// Return the optional owner-rooted detailed connectivity view.
8191#[unsafe(no_mangle)]
8192pub unsafe extern "C" fn pio_balanced_network_detailed_connectivity(
8193    network: *const PioBalancedNetwork,
8194) -> *mut PioDetailedConnectivity {
8195    let Some(owner) = (unsafe { PioBalancedNetwork::arc(network) }) else {
8196        return std::ptr::null_mut();
8197    };
8198    if owner
8199        .network()
8200        .is_none_or(|network| network.detailed_connectivity().is_none())
8201    {
8202        return std::ptr::null_mut();
8203    }
8204    PioDetailedConnectivity::new_raw(DetailedConnectivityInner { owner })
8205}
8206
8207/// Read every detailed connectivity table length.
8208#[unsafe(no_mangle)]
8209pub unsafe extern "C" fn pio_detailed_connectivity_counts(
8210    details: *const PioDetailedConnectivity,
8211    output: *mut PioDetailedConnectivityCountsView,
8212    error: *mut *mut PioError,
8213) -> bool {
8214    unsafe {
8215        entry(error, false, || {
8216            let details = require_detailed_connectivity(details)?;
8217            *require_output(output, "output")? = PioDetailedConnectivityCountsView {
8218                omitted_fields: details.omitted_fields.len(),
8219                component_metadata: details.component_metadata.len(),
8220                subnetworks: details.subnetworks.len(),
8221                substations: details.substations.len(),
8222                voltage_levels: details.voltage_levels.len(),
8223                bus_breaker_buses: details.bus_breaker_buses.len(),
8224                calculated_buses: details.calculated_buses.len(),
8225                connectivity_nodes: details.connectivity_nodes.len(),
8226                busbar_sections: details.busbar_sections.len(),
8227                junctions: details.junctions.len(),
8228                terminals: details.terminals.len(),
8229                switches: details.switches.len(),
8230                internal_connections: details.internal_connections.len(),
8231                operational_limit_groups: details.operational_limit_groups.len(),
8232                tap_changers: details.tap_changers.len(),
8233                equipment_reactive_limits: details.equipment_reactive_limits.len(),
8234                boundary_lines: details.boundary_lines.len(),
8235                tie_lines: details.tie_lines.len(),
8236                dc_converter_units: details.dc_converter_units.len(),
8237                dc_topological_nodes: details.dc_topological_nodes.len(),
8238                dc_nodes: details.dc_nodes.len(),
8239                dc_grounds: details.dc_grounds.len(),
8240                dc_busbars: details.dc_busbars.len(),
8241                dc_lines: details.dc_lines.len(),
8242                dc_series_devices: details.dc_series_devices.len(),
8243                dc_switches: details.dc_switches.len(),
8244                voltage_source_converters: details.voltage_source_converters.len(),
8245                line_commutated_converters: details.line_commutated_converters.len(),
8246            };
8247            Ok(true)
8248        })
8249    }
8250}
8251
8252fn omitted_field_name(value: powerio_tx::OmittedFieldName) -> &'static str {
8253    match value {
8254        powerio_tx::OmittedFieldName::ActivePower => "active_power",
8255        powerio_tx::OmittedFieldName::ReactivePower => "reactive_power",
8256        powerio_tx::OmittedFieldName::VoltageSetpoint => "voltage_setpoint",
8257        powerio_tx::OmittedFieldName::RatedApparentPower => "rated_apparent_power",
8258        powerio_tx::OmittedFieldName::ShuntConductancePerSection => "shunt_conductance_per_section",
8259        _ => "unknown",
8260    }
8261}
8262
8263fn topology_kind_name(kind: powerio_tx::TopologyKind) -> &'static str {
8264    match kind {
8265        powerio_tx::TopologyKind::BusBreaker => "bus_breaker",
8266        powerio_tx::TopologyKind::NodeBreaker => "node_breaker",
8267        _ => "unknown",
8268    }
8269}
8270
8271fn topology_switch_kind_name(kind: powerio_tx::SwitchKind) -> &'static str {
8272    match kind {
8273        powerio_tx::SwitchKind::Breaker => "breaker",
8274        powerio_tx::SwitchKind::Disconnector => "disconnector",
8275        powerio_tx::SwitchKind::LoadBreakSwitch => "load_break_switch",
8276        _ => "unknown",
8277    }
8278}
8279
8280fn curve_style_name(style: powerio_tx::CurveStyle) -> &'static str {
8281    match style {
8282        powerio_tx::CurveStyle::ConstantYValue => "constant_y_value",
8283        powerio_tx::CurveStyle::StraightLineYValues => "straight_line_y_values",
8284        _ => "unknown",
8285    }
8286}
8287
8288fn empty_reactive_limits_view() -> PioReactiveLimitsView {
8289    PioReactiveLimitsView {
8290        kind: PioStringView::EMPTY,
8291        minimum_reactive_power_mvar: 0.0,
8292        maximum_reactive_power_mvar: 0.0,
8293        has_minimum_and_maximum: false,
8294        curve_style: PioStringView::EMPTY,
8295        has_curve_style: false,
8296        property_count: 0,
8297        point_count: 0,
8298    }
8299}
8300
8301fn reactive_limits_view(
8302    value: Option<&powerio_tx::ReactiveLimits>,
8303) -> (PioReactiveLimitsView, bool) {
8304    match value {
8305        Some(powerio_tx::ReactiveLimits::MinMax(limits)) => (
8306            PioReactiveLimitsView {
8307                kind: PioStringView::new("min_max"),
8308                minimum_reactive_power_mvar: limits.minimum_reactive_power_mvar,
8309                maximum_reactive_power_mvar: limits.maximum_reactive_power_mvar,
8310                has_minimum_and_maximum: true,
8311                curve_style: PioStringView::EMPTY,
8312                has_curve_style: false,
8313                property_count: limits.properties.len(),
8314                point_count: 0,
8315            },
8316            true,
8317        ),
8318        Some(powerio_tx::ReactiveLimits::CapabilityCurve(curve)) => (
8319            PioReactiveLimitsView {
8320                kind: PioStringView::new("capability_curve"),
8321                minimum_reactive_power_mvar: 0.0,
8322                maximum_reactive_power_mvar: 0.0,
8323                has_minimum_and_maximum: false,
8324                curve_style: PioStringView::new(curve_style_name(curve.curve_style)),
8325                has_curve_style: true,
8326                property_count: curve.properties.len(),
8327                point_count: curve.points.len(),
8328            },
8329            true,
8330        ),
8331        Some(_) => (empty_reactive_limits_view(), true),
8332        None => (empty_reactive_limits_view(), false),
8333    }
8334}
8335
8336fn reactive_limit_properties(
8337    limits: &powerio_tx::ReactiveLimits,
8338) -> &std::collections::BTreeMap<String, String> {
8339    match limits {
8340        powerio_tx::ReactiveLimits::MinMax(limits) => &limits.properties,
8341        powerio_tx::ReactiveLimits::CapabilityCurve(curve) => &curve.properties,
8342        _ => unreachable!("all reactive limit forms are handled"),
8343    }
8344}
8345
8346fn reactive_capability_curve(
8347    limits: &powerio_tx::ReactiveLimits,
8348) -> Option<&powerio_tx::ReactiveCapabilityCurve> {
8349    match limits {
8350        powerio_tx::ReactiveLimits::CapabilityCurve(curve) => Some(curve),
8351        _ => None,
8352    }
8353}
8354
8355fn string_property_view(
8356    properties: &std::collections::BTreeMap<String, String>,
8357    index: usize,
8358) -> Result<PioStringPropertyView, *mut PioError> {
8359    let (name, value) = properties.iter().nth(index).ok_or_else(|| {
8360        boundary_error(
8361            &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8362            format!("property index {index} is out of range"),
8363        )
8364    })?;
8365    Ok(PioStringPropertyView {
8366        name: PioStringView::new(name),
8367        value: PioStringView::new(value),
8368    })
8369}
8370
8371fn reactive_capability_point_view(
8372    curve: &powerio_tx::ReactiveCapabilityCurve,
8373    index: usize,
8374) -> Result<PioReactiveCapabilityCurvePointView, *mut PioError> {
8375    let point = curve.points.get(index).ok_or_else(|| {
8376        boundary_error(
8377            &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8378            format!("reactive capability point index {index} is out of range"),
8379        )
8380    })?;
8381    Ok(PioReactiveCapabilityCurvePointView {
8382        active_power_mw: point.active_power_mw,
8383        minimum_reactive_power_mvar: point.minimum_reactive_power_mvar,
8384        maximum_reactive_power_mvar: point.maximum_reactive_power_mvar,
8385        property_count: point.properties.len(),
8386    })
8387}
8388
8389fn empty_boundary_line_generation_view() -> PioBoundaryLineGenerationView {
8390    PioBoundaryLineGenerationView {
8391        voltage_regulation_on: false,
8392        minimum_active_power_mw: 0.0,
8393        has_minimum_active_power: false,
8394        maximum_active_power_mw: 0.0,
8395        has_maximum_active_power: false,
8396        target_active_power_mw: 0.0,
8397        has_target_active_power: false,
8398        target_reactive_power_mvar: 0.0,
8399        has_target_reactive_power: false,
8400        target_voltage_kv: 0.0,
8401        has_target_voltage: false,
8402        reactive_limits: empty_reactive_limits_view(),
8403        has_reactive_limits: false,
8404    }
8405}
8406
8407fn boundary_line_generation_view(
8408    value: Option<&powerio_tx::BoundaryLineGeneration>,
8409) -> (PioBoundaryLineGenerationView, bool) {
8410    let Some(value) = value else {
8411        return (empty_boundary_line_generation_view(), false);
8412    };
8413    let (reactive_limits, has_reactive_limits) =
8414        reactive_limits_view(value.reactive_limits.as_ref());
8415    (
8416        PioBoundaryLineGenerationView {
8417            voltage_regulation_on: value.voltage_regulation_on,
8418            minimum_active_power_mw: value.minimum_active_power_mw.unwrap_or(0.0),
8419            has_minimum_active_power: value.minimum_active_power_mw.is_some(),
8420            maximum_active_power_mw: value.maximum_active_power_mw.unwrap_or(0.0),
8421            has_maximum_active_power: value.maximum_active_power_mw.is_some(),
8422            target_active_power_mw: value.target_active_power_mw.unwrap_or(0.0),
8423            has_target_active_power: value.target_active_power_mw.is_some(),
8424            target_reactive_power_mvar: value.target_reactive_power_mvar.unwrap_or(0.0),
8425            has_target_reactive_power: value.target_reactive_power_mvar.is_some(),
8426            target_voltage_kv: value.target_voltage_kv.unwrap_or(0.0),
8427            has_target_voltage: value.target_voltage_kv.is_some(),
8428            reactive_limits,
8429            has_reactive_limits,
8430        },
8431        true,
8432    )
8433}
8434
8435fn topology_endpoint_view(
8436    endpoint: &powerio_tx::TopologyEndpoint,
8437) -> (PioStringView, PioComponentIdView) {
8438    match endpoint {
8439        powerio_tx::TopologyEndpoint::Bus(component) => {
8440            (PioStringView::new("bus"), component_id_view(component))
8441        }
8442        powerio_tx::TopologyEndpoint::Node(component) => {
8443            (PioStringView::new("node"), component_id_view(component))
8444        }
8445        _ => (PioStringView::new("unknown"), empty_component_id_view()),
8446    }
8447}
8448
8449fn tap_changer_kind_name(kind: powerio_tx::TapChangerKind) -> &'static str {
8450    match kind {
8451        powerio_tx::TapChangerKind::Ratio => "ratio",
8452        powerio_tx::TapChangerKind::Phase => "phase",
8453        _ => "unknown",
8454    }
8455}
8456
8457fn tap_changer_regulation_mode_name(mode: powerio_tx::TapChangerRegulationMode) -> &'static str {
8458    match mode {
8459        powerio_tx::TapChangerRegulationMode::Voltage => "voltage",
8460        powerio_tx::TapChangerRegulationMode::ReactivePower => "reactive_power",
8461        powerio_tx::TapChangerRegulationMode::ActivePower => "active_power",
8462        powerio_tx::TapChangerRegulationMode::Current => "current",
8463        _ => "unknown",
8464    }
8465}
8466
8467fn dc_polarity_name(value: powerio_tx::DcPolarity) -> &'static str {
8468    match value {
8469        powerio_tx::DcPolarity::Positive => "positive",
8470        powerio_tx::DcPolarity::Middle => "middle",
8471        powerio_tx::DcPolarity::Negative => "negative",
8472        _ => "unknown",
8473    }
8474}
8475
8476fn dc_switch_kind_name(value: powerio_tx::DcSwitchKind) -> &'static str {
8477    match value {
8478        powerio_tx::DcSwitchKind::Switch => "switch",
8479        powerio_tx::DcSwitchKind::Breaker => "breaker",
8480        powerio_tx::DcSwitchKind::Disconnector => "disconnector",
8481        _ => "unknown",
8482    }
8483}
8484
8485fn dc_converter_operation_mode_name(value: powerio_tx::DcConverterOperatingMode) -> &'static str {
8486    match value {
8487        powerio_tx::DcConverterOperatingMode::Bipolar => "bipolar",
8488        powerio_tx::DcConverterOperatingMode::MonopolarGroundReturn => "monopolar_ground_return",
8489        powerio_tx::DcConverterOperatingMode::MonopolarMetallicReturn => {
8490            "monopolar_metallic_return"
8491        }
8492        _ => "unknown",
8493    }
8494}
8495
8496fn ac_dc_converter_control_mode_name(value: powerio_tx::AcDcConverterControlMode) -> &'static str {
8497    match value {
8498        powerio_tx::AcDcConverterControlMode::ActivePowerAtPcc => "active_power_at_pcc",
8499        powerio_tx::AcDcConverterControlMode::DcVoltage => "dc_voltage",
8500        powerio_tx::AcDcConverterControlMode::DcCurrent => "dc_current",
8501        powerio_tx::AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopCurve => {
8502            "active_power_at_pcc_and_dc_voltage_droop_curve"
8503        }
8504        powerio_tx::AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroop => {
8505            "active_power_at_pcc_and_dc_voltage_droop"
8506        }
8507        powerio_tx::AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopWithCompensation => {
8508            "active_power_at_pcc_and_dc_voltage_droop_with_compensation"
8509        }
8510        powerio_tx::AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopPilot => {
8511            "active_power_at_pcc_and_dc_voltage_droop_pilot"
8512        }
8513        _ => "unknown",
8514    }
8515}
8516
8517fn line_commutated_converter_reactive_model_name(
8518    value: powerio_tx::LineCommutatedConverterReactiveModel,
8519) -> &'static str {
8520    match value {
8521        powerio_tx::LineCommutatedConverterReactiveModel::FixedPowerFactor => "fixed_power_factor",
8522        powerio_tx::LineCommutatedConverterReactiveModel::CalculatedPowerFactor => {
8523            "calculated_power_factor"
8524        }
8525        _ => "unknown",
8526    }
8527}
8528
8529fn line_commutated_converter_operating_mode_name(
8530    value: powerio_tx::LineCommutatedConverterOperatingMode,
8531) -> &'static str {
8532    match value {
8533        powerio_tx::LineCommutatedConverterOperatingMode::Rectifier => "rectifier",
8534        powerio_tx::LineCommutatedConverterOperatingMode::Inverter => "inverter",
8535        _ => "unknown",
8536    }
8537}
8538
8539fn empty_dc_terminal_view() -> PioDcTerminalView {
8540    PioDcTerminalView {
8541        component: empty_component_id_view(),
8542        has_component: false,
8543        sequence_number: 0,
8544        has_sequence_number: false,
8545        dc_node: empty_component_id_view(),
8546        has_dc_node: false,
8547        dc_topological_node: empty_component_id_view(),
8548        has_dc_topological_node: false,
8549        polarity: PioStringView::EMPTY,
8550        has_polarity: false,
8551        connected: false,
8552        has_connected: false,
8553        active_power_mw: 0.0,
8554        has_active_power: false,
8555        current_a: 0.0,
8556        has_current: false,
8557    }
8558}
8559
8560fn dc_terminal_view(value: &powerio_tx::DcTerminal) -> PioDcTerminalView {
8561    let (component, has_component) = optional_component_id_view(value.component.as_ref());
8562    let (dc_node, has_dc_node) = optional_component_id_view(value.dc_node.as_ref());
8563    let (dc_topological_node, has_dc_topological_node) =
8564        optional_component_id_view(value.dc_topological_node.as_ref());
8565    let (polarity, has_polarity) = value
8566        .polarity
8567        .map_or((PioStringView::EMPTY, false), |polarity| {
8568            (PioStringView::new(dc_polarity_name(polarity)), true)
8569        });
8570    PioDcTerminalView {
8571        component,
8572        has_component,
8573        sequence_number: value.sequence_number.unwrap_or(0),
8574        has_sequence_number: value.sequence_number.is_some(),
8575        dc_node,
8576        has_dc_node,
8577        dc_topological_node,
8578        has_dc_topological_node,
8579        polarity,
8580        has_polarity,
8581        connected: value.connected.unwrap_or(false),
8582        has_connected: value.connected.is_some(),
8583        active_power_mw: value.active_power_mw.unwrap_or(0.0),
8584        has_active_power: value.active_power_mw.is_some(),
8585        current_a: value.current_a.unwrap_or(0.0),
8586        has_current: value.current_a.is_some(),
8587    }
8588}
8589
8590#[allow(clippy::too_many_arguments)]
8591fn dc_equipment_view(
8592    component: &ComponentId,
8593    equipment_container: Option<&ComponentId>,
8594    kind: &'static str,
8595    terminal1: &powerio_tx::DcTerminal,
8596    terminal2: Option<&powerio_tx::DcTerminal>,
8597    rated_dc_voltage_kv: Option<f64>,
8598    resistance_ohm: Option<f64>,
8599    inductance_h: Option<f64>,
8600    capacitance_f: Option<f64>,
8601    length_km: Option<f64>,
8602    switch_kind: Option<powerio_tx::DcSwitchKind>,
8603    open: Option<bool>,
8604) -> PioDcEquipmentView {
8605    let (equipment_container, has_equipment_container) =
8606        optional_component_id_view(equipment_container);
8607    let (switch_kind, has_switch_kind) = switch_kind
8608        .map_or((PioStringView::EMPTY, false), |kind| {
8609            (PioStringView::new(dc_switch_kind_name(kind)), true)
8610        });
8611    PioDcEquipmentView {
8612        component: component_id_view(component),
8613        equipment_container,
8614        has_equipment_container,
8615        kind: PioStringView::new(kind),
8616        terminal_count: usize::from(terminal2.is_some()) + 1,
8617        terminal1: dc_terminal_view(terminal1),
8618        terminal2: terminal2.map_or_else(empty_dc_terminal_view, dc_terminal_view),
8619        rated_dc_voltage_kv: rated_dc_voltage_kv.unwrap_or(0.0),
8620        has_rated_dc_voltage: rated_dc_voltage_kv.is_some(),
8621        resistance_ohm: resistance_ohm.unwrap_or(0.0),
8622        has_resistance: resistance_ohm.is_some(),
8623        inductance_h: inductance_h.unwrap_or(0.0),
8624        has_inductance: inductance_h.is_some(),
8625        capacitance_f: capacitance_f.unwrap_or(0.0),
8626        has_capacitance: capacitance_f.is_some(),
8627        length_km: length_km.unwrap_or(0.0),
8628        has_length: length_km.is_some(),
8629        switch_kind,
8630        has_switch_kind,
8631        open: open.unwrap_or(false),
8632        has_open: open.is_some(),
8633    }
8634}
8635
8636/// Read one field that was absent from the source representation.
8637#[unsafe(no_mangle)]
8638pub unsafe extern "C" fn pio_detailed_connectivity_omitted_field_at(
8639    details: *const PioDetailedConnectivity,
8640    index: usize,
8641    output: *mut PioOmittedFieldView,
8642    error: *mut *mut PioError,
8643) -> bool {
8644    unsafe {
8645        entry(error, false, || {
8646            let details = require_detailed_connectivity(details)?;
8647            let field = details.omitted_fields.get(index).ok_or_else(|| {
8648                boundary_error(
8649                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8650                    format!("omitted field index {index} is out of range"),
8651                )
8652            })?;
8653            *require_output(output, "output")? = PioOmittedFieldView {
8654                component: component_id_view(&field.component),
8655                field: PioStringView::new(omitted_field_name(field.field)),
8656            };
8657            Ok(true)
8658        })
8659    }
8660}
8661
8662/// Read one component metadata record by zero based table position.
8663#[unsafe(no_mangle)]
8664pub unsafe extern "C" fn pio_detailed_connectivity_component_metadata_at(
8665    details: *const PioDetailedConnectivity,
8666    index: usize,
8667    output: *mut PioComponentMetadataView,
8668    error: *mut *mut PioError,
8669) -> bool {
8670    unsafe {
8671        entry(error, false, || {
8672            let details = require_detailed_connectivity(details)?;
8673            let metadata = details.component_metadata.get(index).ok_or_else(|| {
8674                boundary_error(
8675                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8676                    format!("component metadata index {index} is out of range"),
8677                )
8678            })?;
8679            let (name, has_name) = optional_string_view(metadata.name.as_deref());
8680            let (equipment_container, has_equipment_container) =
8681                optional_component_id_view(metadata.equipment_container.as_ref());
8682            *require_output(output, "output")? = PioComponentMetadataView {
8683                component: component_id_view(&metadata.component),
8684                name,
8685                has_name,
8686                equipment_container,
8687                has_equipment_container,
8688                fictitious: metadata.fictitious,
8689                alias_count: metadata.aliases.len(),
8690                external_identifier_count: metadata.external_identifiers.len(),
8691                property_count: metadata.properties.len(),
8692            };
8693            Ok(true)
8694        })
8695    }
8696}
8697
8698/// Read one alias from a component metadata record.
8699#[unsafe(no_mangle)]
8700pub unsafe extern "C" fn pio_detailed_connectivity_component_alias_at(
8701    details: *const PioDetailedConnectivity,
8702    metadata_index: usize,
8703    alias_index: usize,
8704    output: *mut PioComponentAliasView,
8705    error: *mut *mut PioError,
8706) -> bool {
8707    unsafe {
8708        entry(error, false, || {
8709            let details = require_detailed_connectivity(details)?;
8710            let metadata = details
8711                .component_metadata
8712                .get(metadata_index)
8713                .ok_or_else(|| {
8714                    boundary_error(
8715                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8716                        format!("component metadata index {metadata_index} is out of range"),
8717                    )
8718                })?;
8719            let alias = metadata.aliases.get(alias_index).ok_or_else(|| {
8720                boundary_error(
8721                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8722                    format!("component alias index {alias_index} is out of range"),
8723                )
8724            })?;
8725            let (alias_type, has_alias_type) = optional_string_view(alias.alias_type.as_deref());
8726            *require_output(output, "output")? = PioComponentAliasView {
8727                value: PioStringView::new(&alias.value),
8728                alias_type,
8729                has_alias_type,
8730            };
8731            Ok(true)
8732        })
8733    }
8734}
8735
8736/// Read one external identifier from a component metadata record.
8737#[unsafe(no_mangle)]
8738pub unsafe extern "C" fn pio_detailed_connectivity_external_identifier_at(
8739    details: *const PioDetailedConnectivity,
8740    metadata_index: usize,
8741    identifier_index: usize,
8742    output: *mut PioExternalIdentifierView,
8743    error: *mut *mut PioError,
8744) -> bool {
8745    unsafe {
8746        entry(error, false, || {
8747            let details = require_detailed_connectivity(details)?;
8748            let metadata = details
8749                .component_metadata
8750                .get(metadata_index)
8751                .ok_or_else(|| {
8752                    boundary_error(
8753                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8754                        format!("component metadata index {metadata_index} is out of range"),
8755                    )
8756                })?;
8757            let identifier = metadata
8758                .external_identifiers
8759                .get(identifier_index)
8760                .ok_or_else(|| {
8761                    boundary_error(
8762                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8763                        format!("external identifier index {identifier_index} is out of range"),
8764                    )
8765                })?;
8766            let (authority, has_authority) = optional_string_view(identifier.authority.as_deref());
8767            *require_output(output, "output")? = PioExternalIdentifierView {
8768                value: PioStringView::new(&identifier.value),
8769                authority,
8770                has_authority,
8771            };
8772            Ok(true)
8773        })
8774    }
8775}
8776
8777/// Read one string property from a component metadata record.
8778#[unsafe(no_mangle)]
8779pub unsafe extern "C" fn pio_detailed_connectivity_component_property_at(
8780    details: *const PioDetailedConnectivity,
8781    metadata_index: usize,
8782    property_index: usize,
8783    output: *mut PioStringPropertyView,
8784    error: *mut *mut PioError,
8785) -> bool {
8786    unsafe {
8787        entry(error, false, || {
8788            let details = require_detailed_connectivity(details)?;
8789            let metadata = details
8790                .component_metadata
8791                .get(metadata_index)
8792                .ok_or_else(|| {
8793                    boundary_error(
8794                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8795                        format!("component metadata index {metadata_index} is out of range"),
8796                    )
8797                })?;
8798            let (name, value) =
8799                metadata
8800                    .properties
8801                    .iter()
8802                    .nth(property_index)
8803                    .ok_or_else(|| {
8804                        boundary_error(
8805                            &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8806                            format!("component property index {property_index} is out of range"),
8807                        )
8808                    })?;
8809            *require_output(output, "output")? = PioStringPropertyView {
8810                name: PioStringView::new(name),
8811                value: PioStringView::new(value),
8812            };
8813            Ok(true)
8814        })
8815    }
8816}
8817
8818fn case_metadata_view(metadata: &powerio_tx::CaseMetadata) -> PioCaseMetadataView {
8819    let (case_date, has_case_date) = optional_string_view(metadata.case_date.as_deref());
8820    let (source_model_format, has_source_model_format) =
8821        optional_string_view(metadata.source_model_format.as_deref());
8822    let (minimum_validation_level, has_minimum_validation_level) =
8823        optional_string_view(metadata.minimum_validation_level.as_deref());
8824    PioCaseMetadataView {
8825        case_date,
8826        has_case_date,
8827        forecast_distance: metadata.forecast_distance.unwrap_or(0),
8828        has_forecast_distance: metadata.forecast_distance.is_some(),
8829        source_model_format,
8830        has_source_model_format,
8831        minimum_validation_level,
8832        has_minimum_validation_level,
8833    }
8834}
8835
8836/// Read one PowSybl subnetwork by zero based table position.
8837#[unsafe(no_mangle)]
8838pub unsafe extern "C" fn pio_detailed_connectivity_subnetwork_at(
8839    details: *const PioDetailedConnectivity,
8840    index: usize,
8841    output: *mut PioSubnetworkView,
8842    error: *mut *mut PioError,
8843) -> bool {
8844    unsafe {
8845        entry(error, false, || {
8846            let details = require_detailed_connectivity(details)?;
8847            let subnetwork = details.subnetworks.get(index).ok_or_else(|| {
8848                boundary_error(
8849                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8850                    format!("subnetwork index {index} is out of range"),
8851                )
8852            })?;
8853            *require_output(output, "output")? = PioSubnetworkView {
8854                component: component_id_view(&subnetwork.component),
8855                parent: component_id_view(&subnetwork.parent),
8856                case_metadata: case_metadata_view(&subnetwork.case_metadata),
8857                component_count: subnetwork.components.len(),
8858            };
8859            Ok(true)
8860        })
8861    }
8862}
8863
8864/// Read one component identity contained by a PowSybl subnetwork.
8865#[unsafe(no_mangle)]
8866pub unsafe extern "C" fn pio_detailed_connectivity_subnetwork_component_at(
8867    details: *const PioDetailedConnectivity,
8868    subnetwork_index: usize,
8869    component_index: usize,
8870    output: *mut PioComponentIdView,
8871    error: *mut *mut PioError,
8872) -> bool {
8873    unsafe {
8874        entry(error, false, || {
8875            let details = require_detailed_connectivity(details)?;
8876            let subnetwork = details.subnetworks.get(subnetwork_index).ok_or_else(|| {
8877                boundary_error(
8878                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8879                    format!("subnetwork index {subnetwork_index} is out of range"),
8880                )
8881            })?;
8882            let component = subnetwork.components.get(component_index).ok_or_else(|| {
8883                boundary_error(
8884                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8885                    format!("subnetwork component index {component_index} is out of range"),
8886                )
8887            })?;
8888            *require_output(output, "output")? = component_id_view(component);
8889            Ok(true)
8890        })
8891    }
8892}
8893
8894/// Read one substation by zero based table position.
8895#[unsafe(no_mangle)]
8896pub unsafe extern "C" fn pio_detailed_connectivity_substation_at(
8897    details: *const PioDetailedConnectivity,
8898    index: usize,
8899    output: *mut PioSubstationView,
8900    error: *mut *mut PioError,
8901) -> bool {
8902    unsafe {
8903        entry(error, false, || {
8904            let details = require_detailed_connectivity(details)?;
8905            let substation = details.substations.get(index).ok_or_else(|| {
8906                boundary_error(
8907                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8908                    format!("substation index {index} is out of range"),
8909                )
8910            })?;
8911            let (country, has_country) = optional_string_view(substation.country.as_deref());
8912            let (operator_name, has_operator_name) =
8913                optional_string_view(substation.operator.as_deref());
8914            *require_output(output, "output")? = PioSubstationView {
8915                component: component_id_view(&substation.component),
8916                country,
8917                has_country,
8918                operator_name,
8919                has_operator_name,
8920                geographical_tag_count: substation.geographical_tags.len(),
8921            };
8922            Ok(true)
8923        })
8924    }
8925}
8926
8927/// Read one geographical tag of a substation.
8928#[unsafe(no_mangle)]
8929pub unsafe extern "C" fn pio_detailed_connectivity_substation_geographical_tag_at(
8930    details: *const PioDetailedConnectivity,
8931    substation_index: usize,
8932    tag_index: usize,
8933    output: *mut PioStringView,
8934    error: *mut *mut PioError,
8935) -> bool {
8936    unsafe {
8937        entry(error, false, || {
8938            let details = require_detailed_connectivity(details)?;
8939            let substation = details.substations.get(substation_index).ok_or_else(|| {
8940                boundary_error(
8941                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8942                    format!("substation index {substation_index} is out of range"),
8943                )
8944            })?;
8945            let tag = substation.geographical_tags.get(tag_index).ok_or_else(|| {
8946                boundary_error(
8947                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8948                    format!("geographical tag index {tag_index} is out of range"),
8949                )
8950            })?;
8951            *require_output(output, "output")? = PioStringView::new(tag);
8952            Ok(true)
8953        })
8954    }
8955}
8956
8957/// Read one voltage level by zero based table position.
8958#[unsafe(no_mangle)]
8959pub unsafe extern "C" fn pio_detailed_connectivity_voltage_level_at(
8960    details: *const PioDetailedConnectivity,
8961    index: usize,
8962    output: *mut PioVoltageLevelView,
8963    error: *mut *mut PioError,
8964) -> bool {
8965    unsafe {
8966        entry(error, false, || {
8967            let details = require_detailed_connectivity(details)?;
8968            let level = details.voltage_levels.get(index).ok_or_else(|| {
8969                boundary_error(
8970                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
8971                    format!("voltage level index {index} is out of range"),
8972                )
8973            })?;
8974            let (substation, has_substation) =
8975                optional_component_id_view(level.substation.as_ref());
8976            *require_output(output, "output")? = PioVoltageLevelView {
8977                component: component_id_view(&level.component),
8978                substation,
8979                has_substation,
8980                nominal_voltage_kv: level.nominal_kv,
8981                low_voltage_limit_kv: level.low_voltage_limit_kv.unwrap_or(0.0),
8982                has_low_voltage_limit: level.low_voltage_limit_kv.is_some(),
8983                high_voltage_limit_kv: level.high_voltage_limit_kv.unwrap_or(0.0),
8984                has_high_voltage_limit: level.high_voltage_limit_kv.is_some(),
8985                topology_kind: PioStringView::new(topology_kind_name(level.topology_kind)),
8986                bus_count: level.buses.len(),
8987            };
8988            Ok(true)
8989        })
8990    }
8991}
8992
8993/// Read one balanced bus ID assigned to a voltage level.
8994#[unsafe(no_mangle)]
8995pub unsafe extern "C" fn pio_detailed_connectivity_voltage_level_bus_at(
8996    details: *const PioDetailedConnectivity,
8997    voltage_level_index: usize,
8998    bus_index: usize,
8999    output: *mut usize,
9000    error: *mut *mut PioError,
9001) -> bool {
9002    unsafe {
9003        entry(error, false, || {
9004            let details = require_detailed_connectivity(details)?;
9005            let level = details
9006                .voltage_levels
9007                .get(voltage_level_index)
9008                .ok_or_else(|| {
9009                    boundary_error(
9010                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9011                        format!("voltage level index {voltage_level_index} is out of range"),
9012                    )
9013                })?;
9014            let bus = level.buses.get(bus_index).ok_or_else(|| {
9015                boundary_error(
9016                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9017                    format!("voltage level bus index {bus_index} is out of range"),
9018                )
9019            })?;
9020            *require_output(output, "output")? = bus.0;
9021            Ok(true)
9022        })
9023    }
9024}
9025
9026/// Read one configured bus breaker bus by zero based table position.
9027#[unsafe(no_mangle)]
9028pub unsafe extern "C" fn pio_detailed_connectivity_bus_breaker_bus_at(
9029    details: *const PioDetailedConnectivity,
9030    index: usize,
9031    output: *mut PioBusBreakerBusView,
9032    error: *mut *mut PioError,
9033) -> bool {
9034    unsafe {
9035        entry(error, false, || {
9036            let details = require_detailed_connectivity(details)?;
9037            let bus = details.bus_breaker_buses.get(index).ok_or_else(|| {
9038                boundary_error(
9039                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9040                    format!("bus breaker bus index {index} is out of range"),
9041                )
9042            })?;
9043            *require_output(output, "output")? = PioBusBreakerBusView {
9044                component: component_id_view(&bus.component),
9045                voltage_level: component_id_view(&bus.voltage_level),
9046                calculated_bus_id: bus.calculated_bus.map_or(0, |value| value.0),
9047                has_calculated_bus: bus.calculated_bus.is_some(),
9048                voltage_kv: bus.voltage_kv.unwrap_or(0.0),
9049                has_voltage: bus.voltage_kv.is_some(),
9050                angle_degrees: bus.angle_degrees.unwrap_or(0.0),
9051                has_angle: bus.angle_degrees.is_some(),
9052            };
9053            Ok(true)
9054        })
9055    }
9056}
9057
9058/// Read one calculated bus by zero based table position.
9059#[unsafe(no_mangle)]
9060pub unsafe extern "C" fn pio_detailed_connectivity_calculated_bus_at(
9061    details: *const PioDetailedConnectivity,
9062    index: usize,
9063    output: *mut PioCalculatedBusView,
9064    error: *mut *mut PioError,
9065) -> bool {
9066    unsafe {
9067        entry(error, false, || {
9068            let details = require_detailed_connectivity(details)?;
9069            let bus = details.calculated_buses.get(index).ok_or_else(|| {
9070                boundary_error(
9071                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9072                    format!("calculated bus index {index} is out of range"),
9073                )
9074            })?;
9075            *require_output(output, "output")? = PioCalculatedBusView {
9076                voltage_level: component_id_view(&bus.voltage_level),
9077                calculated_bus_id: bus.calculated_bus.0,
9078                node_count: bus.nodes.len(),
9079                voltage_kv: bus.voltage_kv.unwrap_or(0.0),
9080                has_voltage: bus.voltage_kv.is_some(),
9081                angle_degrees: bus.angle_degrees.unwrap_or(0.0),
9082                has_angle: bus.angle_degrees.is_some(),
9083            };
9084            Ok(true)
9085        })
9086    }
9087}
9088
9089/// Read one node identity from a calculated bus.
9090#[unsafe(no_mangle)]
9091pub unsafe extern "C" fn pio_detailed_connectivity_calculated_bus_node_at(
9092    details: *const PioDetailedConnectivity,
9093    calculated_bus_index: usize,
9094    node_index: usize,
9095    output: *mut PioComponentIdView,
9096    error: *mut *mut PioError,
9097) -> bool {
9098    unsafe {
9099        entry(error, false, || {
9100            let details = require_detailed_connectivity(details)?;
9101            let bus = details
9102                .calculated_buses
9103                .get(calculated_bus_index)
9104                .ok_or_else(|| {
9105                    boundary_error(
9106                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9107                        format!("calculated bus index {calculated_bus_index} is out of range"),
9108                    )
9109                })?;
9110            let node = bus.nodes.get(node_index).ok_or_else(|| {
9111                boundary_error(
9112                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9113                    format!("calculated bus node index {node_index} is out of range"),
9114                )
9115            })?;
9116            *require_output(output, "output")? = component_id_view(node);
9117            Ok(true)
9118        })
9119    }
9120}
9121
9122/// Read one connectivity node by zero based table position.
9123#[unsafe(no_mangle)]
9124pub unsafe extern "C" fn pio_detailed_connectivity_node_at(
9125    details: *const PioDetailedConnectivity,
9126    index: usize,
9127    output: *mut PioConnectivityNodeView,
9128    error: *mut *mut PioError,
9129) -> bool {
9130    unsafe {
9131        entry(error, false, || {
9132            let details = require_detailed_connectivity(details)?;
9133            let node = details.connectivity_nodes.get(index).ok_or_else(|| {
9134                boundary_error(
9135                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9136                    format!("connectivity node index {index} is out of range"),
9137                )
9138            })?;
9139            *require_output(output, "output")? = PioConnectivityNodeView {
9140                component: component_id_view(&node.component),
9141                voltage_level: component_id_view(&node.voltage_level),
9142                node_number: node.node_number.unwrap_or(0),
9143                has_node_number: node.node_number.is_some(),
9144                calculated_bus_id: node.calculated_bus.map_or(0, |bus| bus.0),
9145                has_calculated_bus: node.calculated_bus.is_some(),
9146            };
9147            Ok(true)
9148        })
9149    }
9150}
9151
9152/// Read one busbar section by zero based table position.
9153#[unsafe(no_mangle)]
9154pub unsafe extern "C" fn pio_detailed_connectivity_busbar_section_at(
9155    details: *const PioDetailedConnectivity,
9156    index: usize,
9157    output: *mut PioBusbarSectionView,
9158    error: *mut *mut PioError,
9159) -> bool {
9160    unsafe {
9161        entry(error, false, || {
9162            let details = require_detailed_connectivity(details)?;
9163            let section = details.busbar_sections.get(index).ok_or_else(|| {
9164                boundary_error(
9165                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9166                    format!("busbar section index {index} is out of range"),
9167                )
9168            })?;
9169            *require_output(output, "output")? = PioBusbarSectionView {
9170                component: component_id_view(&section.component),
9171                voltage_level: component_id_view(&section.voltage_level),
9172                node: component_id_view(&section.node),
9173            };
9174            Ok(true)
9175        })
9176    }
9177}
9178
9179/// Read one CIM junction by zero based table position.
9180#[unsafe(no_mangle)]
9181pub unsafe extern "C" fn pio_detailed_connectivity_junction_at(
9182    details: *const PioDetailedConnectivity,
9183    index: usize,
9184    output: *mut PioJunctionView,
9185    error: *mut *mut PioError,
9186) -> bool {
9187    unsafe {
9188        entry(error, false, || {
9189            let details = require_detailed_connectivity(details)?;
9190            let junction = details.junctions.get(index).ok_or_else(|| {
9191                boundary_error(
9192                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9193                    format!("junction index {index} is out of range"),
9194                )
9195            })?;
9196            *require_output(output, "output")? = PioJunctionView {
9197                component: component_id_view(&junction.component),
9198            };
9199            Ok(true)
9200        })
9201    }
9202}
9203
9204/// Read one AC terminal by zero based table position.
9205#[unsafe(no_mangle)]
9206pub unsafe extern "C" fn pio_detailed_connectivity_terminal_at(
9207    details: *const PioDetailedConnectivity,
9208    index: usize,
9209    output: *mut PioDetailedTerminalView,
9210    error: *mut *mut PioError,
9211) -> bool {
9212    unsafe {
9213        entry(error, false, || {
9214            let details = require_detailed_connectivity(details)?;
9215            let terminal = details.terminals.get(index).ok_or_else(|| {
9216                boundary_error(
9217                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9218                    format!("terminal index {index} is out of range"),
9219                )
9220            })?;
9221            let (bus, has_bus) = optional_component_id_view(terminal.bus.as_ref());
9222            let (connectable_bus, has_connectable_bus) =
9223                optional_component_id_view(terminal.connectable_bus.as_ref());
9224            let (node, has_node) = optional_component_id_view(terminal.node.as_ref());
9225            let (component, has_component) =
9226                optional_component_id_view(terminal.component.as_ref());
9227            *require_output(output, "output")? = PioDetailedTerminalView {
9228                component,
9229                has_component,
9230                equipment: component_id_view(&terminal.equipment),
9231                terminal: terminal.terminal,
9232                voltage_level: component_id_view(&terminal.voltage_level),
9233                bus,
9234                has_bus,
9235                connectable_bus,
9236                has_connectable_bus,
9237                node,
9238                has_node,
9239                connected: terminal.connected,
9240                active_power_mw: terminal.active_power_mw.unwrap_or(0.0),
9241                has_active_power: terminal.active_power_mw.is_some(),
9242                reactive_power_mvar: terminal.reactive_power_mvar.unwrap_or(0.0),
9243                has_reactive_power: terminal.reactive_power_mvar.is_some(),
9244            };
9245            Ok(true)
9246        })
9247    }
9248}
9249
9250/// Read one detailed topology switch by zero based table position.
9251#[unsafe(no_mangle)]
9252pub unsafe extern "C" fn pio_detailed_connectivity_switch_at(
9253    details: *const PioDetailedConnectivity,
9254    index: usize,
9255    output: *mut PioTopologySwitchView,
9256    error: *mut *mut PioError,
9257) -> bool {
9258    unsafe {
9259        entry(error, false, || {
9260            let details = require_detailed_connectivity(details)?;
9261            let switch = details.switches.get(index).ok_or_else(|| {
9262                boundary_error(
9263                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9264                    format!("topology switch index {index} is out of range"),
9265                )
9266            })?;
9267            let (endpoint1_kind, endpoint1) = topology_endpoint_view(&switch.endpoint1);
9268            let (endpoint2_kind, endpoint2) = topology_endpoint_view(&switch.endpoint2);
9269            *require_output(output, "output")? = PioTopologySwitchView {
9270                component: component_id_view(&switch.component),
9271                voltage_level: component_id_view(&switch.voltage_level),
9272                kind: PioStringView::new(topology_switch_kind_name(switch.kind)),
9273                endpoint1_kind,
9274                endpoint1,
9275                endpoint2_kind,
9276                endpoint2,
9277                open: switch.open,
9278                retained: switch.retained,
9279            };
9280            Ok(true)
9281        })
9282    }
9283}
9284
9285/// Read one node breaker internal connection by zero based table position.
9286#[unsafe(no_mangle)]
9287pub unsafe extern "C" fn pio_detailed_connectivity_internal_connection_at(
9288    details: *const PioDetailedConnectivity,
9289    index: usize,
9290    output: *mut PioInternalConnectionView,
9291    error: *mut *mut PioError,
9292) -> bool {
9293    unsafe {
9294        entry(error, false, || {
9295            let details = require_detailed_connectivity(details)?;
9296            let connection = details.internal_connections.get(index).ok_or_else(|| {
9297                boundary_error(
9298                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9299                    format!("internal connection index {index} is out of range"),
9300                )
9301            })?;
9302            *require_output(output, "output")? = PioInternalConnectionView {
9303                voltage_level: component_id_view(&connection.voltage_level),
9304                node1: component_id_view(&connection.node1),
9305                node2: component_id_view(&connection.node2),
9306            };
9307            Ok(true)
9308        })
9309    }
9310}
9311
9312/// Read one operational limit group by zero based table position.
9313#[unsafe(no_mangle)]
9314pub unsafe extern "C" fn pio_detailed_connectivity_operational_limit_group_at(
9315    details: *const PioDetailedConnectivity,
9316    index: usize,
9317    output: *mut PioOperationalLimitGroupView,
9318    error: *mut *mut PioError,
9319) -> bool {
9320    unsafe {
9321        entry(error, false, || {
9322            let details = require_detailed_connectivity(details)?;
9323            let group = details.operational_limit_groups.get(index).ok_or_else(|| {
9324                boundary_error(
9325                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9326                    format!("operational limit group index {index} is out of range"),
9327                )
9328            })?;
9329            let current = group.current_limits.as_ref();
9330            let active = group.active_power_limits.as_ref();
9331            let apparent = group.apparent_power_limits.as_ref();
9332            let (current_name, has_current_name) = optional_string_view(
9333                current.and_then(|limits| limits.permanent_limit_name.as_deref()),
9334            );
9335            let (active_name, has_active_name) = optional_string_view(
9336                active.and_then(|limits| limits.permanent_limit_name.as_deref()),
9337            );
9338            let (apparent_name, has_apparent_name) = optional_string_view(
9339                apparent.and_then(|limits| limits.permanent_limit_name.as_deref()),
9340            );
9341            *require_output(output, "output")? = PioOperationalLimitGroupView {
9342                equipment: component_id_view(&group.equipment),
9343                terminal: group.terminal,
9344                id: PioStringView::new(&group.id),
9345                selected: group.selected,
9346                property_count: group.properties.len(),
9347                has_current_limits: current.is_some(),
9348                current_permanent_limit_a: current
9349                    .and_then(|limits| limits.permanent_limit)
9350                    .unwrap_or(0.0),
9351                current_permanent_limit_name: current_name,
9352                has_current_permanent_limit: current
9353                    .is_some_and(|limits| limits.permanent_limit.is_some()),
9354                has_current_permanent_limit_name: has_current_name,
9355                current_temporary_limit_count: current
9356                    .map_or(0, |limits| limits.temporary_limits.len()),
9357                has_active_power_limits: active.is_some(),
9358                active_power_permanent_limit_mw: active
9359                    .and_then(|limits| limits.permanent_limit)
9360                    .unwrap_or(0.0),
9361                active_power_permanent_limit_name: active_name,
9362                has_active_power_permanent_limit: active
9363                    .is_some_and(|limits| limits.permanent_limit.is_some()),
9364                has_active_power_permanent_limit_name: has_active_name,
9365                active_power_temporary_limit_count: active
9366                    .map_or(0, |limits| limits.temporary_limits.len()),
9367                has_apparent_power_limits: apparent.is_some(),
9368                apparent_power_permanent_limit_mva: apparent
9369                    .and_then(|limits| limits.permanent_limit)
9370                    .unwrap_or(0.0),
9371                apparent_power_permanent_limit_name: apparent_name,
9372                has_apparent_power_permanent_limit: apparent
9373                    .is_some_and(|limits| limits.permanent_limit.is_some()),
9374                has_apparent_power_permanent_limit_name: has_apparent_name,
9375                apparent_power_temporary_limit_count: apparent
9376                    .map_or(0, |limits| limits.temporary_limits.len()),
9377            };
9378            Ok(true)
9379        })
9380    }
9381}
9382
9383/// Read one string property from an operational limit group.
9384#[unsafe(no_mangle)]
9385pub unsafe extern "C" fn pio_detailed_connectivity_operational_limit_group_property_at(
9386    details: *const PioDetailedConnectivity,
9387    group_index: usize,
9388    property_index: usize,
9389    output: *mut PioStringPropertyView,
9390    error: *mut *mut PioError,
9391) -> bool {
9392    unsafe {
9393        entry(error, false, || {
9394            let details = require_detailed_connectivity(details)?;
9395            let group = details
9396                .operational_limit_groups
9397                .get(group_index)
9398                .ok_or_else(|| {
9399                    boundary_error(
9400                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9401                        format!("operational limit group index {group_index} is out of range"),
9402                    )
9403                })?;
9404            *require_output(output, "output")? =
9405                string_property_view(&group.properties, property_index)?;
9406            Ok(true)
9407        })
9408    }
9409}
9410
9411/// Read one temporary current, active power, or apparent power limit.
9412#[unsafe(no_mangle)]
9413pub unsafe extern "C" fn pio_detailed_connectivity_temporary_limit_at(
9414    details: *const PioDetailedConnectivity,
9415    group_index: usize,
9416    quantity: *const c_char,
9417    quantity_len: usize,
9418    limit_index: usize,
9419    output: *mut PioTemporaryLimitView,
9420    error: *mut *mut PioError,
9421) -> bool {
9422    unsafe {
9423        entry(error, false, || {
9424            let details = require_detailed_connectivity(details)?;
9425            let group = details
9426                .operational_limit_groups
9427                .get(group_index)
9428                .ok_or_else(|| {
9429                    boundary_error(
9430                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9431                        format!("operational limit group index {group_index} is out of range"),
9432                    )
9433                })?;
9434            let quantity = required_str(quantity, quantity_len, "quantity")?;
9435            let limits = match quantity {
9436                "current" => group.current_limits.as_ref(),
9437                "active_power" => group.active_power_limits.as_ref(),
9438                "apparent_power" => group.apparent_power_limits.as_ref(),
9439                _ => {
9440                    return Err(boundary_error(
9441                        &codes::REQUEST_CAPI_QUANTITY_UNKNOWN,
9442                        "quantity must be current, active_power, or apparent_power",
9443                    ));
9444                }
9445            }
9446            .ok_or_else(|| {
9447                boundary_error(
9448                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9449                    format!("operational limit group {group_index} has no {quantity} limits"),
9450                )
9451            })?;
9452            let limit = limits.temporary_limits.get(limit_index).ok_or_else(|| {
9453                boundary_error(
9454                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9455                    format!("temporary limit index {limit_index} is out of range"),
9456                )
9457            })?;
9458            *require_output(output, "output")? = PioTemporaryLimitView {
9459                name: PioStringView::new(&limit.name),
9460                value: limit.value,
9461                acceptable_duration_seconds: limit.acceptable_duration_seconds,
9462                fictitious: limit.fictitious,
9463            };
9464            Ok(true)
9465        })
9466    }
9467}
9468
9469/// Read one PowSybl boundary line by zero based table position.
9470#[unsafe(no_mangle)]
9471pub unsafe extern "C" fn pio_detailed_connectivity_boundary_line_at(
9472    details: *const PioDetailedConnectivity,
9473    index: usize,
9474    output: *mut PioBoundaryLineView,
9475    error: *mut *mut PioError,
9476) -> bool {
9477    unsafe {
9478        entry(error, false, || {
9479            let details = require_detailed_connectivity(details)?;
9480            let line = details.boundary_lines.get(index).ok_or_else(|| {
9481                boundary_error(
9482                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9483                    format!("boundary line index {index} is out of range"),
9484                )
9485            })?;
9486            let (pairing_key, has_pairing_key) = optional_string_view(line.pairing_key.as_deref());
9487            let (generation, has_generation) =
9488                boundary_line_generation_view(line.generation.as_ref());
9489            let (calculation_load, has_calculation_load) =
9490                optional_component_id_view(line.calculation_load.as_ref());
9491            let (calculation_generator, has_calculation_generator) =
9492                optional_component_id_view(line.calculation_generator.as_ref());
9493            *require_output(output, "output")? = PioBoundaryLineView {
9494                component: component_id_view(&line.component),
9495                voltage_level: component_id_view(&line.voltage_level),
9496                active_power_setpoint_mw: line.active_power_setpoint_mw,
9497                reactive_power_setpoint_mvar: line.reactive_power_setpoint_mvar,
9498                resistance_ohm: line.resistance_ohm,
9499                reactance_ohm: line.reactance_ohm,
9500                conductance_siemens: line.conductance_siemens,
9501                susceptance_siemens: line.susceptance_siemens,
9502                pairing_key,
9503                has_pairing_key,
9504                generation,
9505                has_generation,
9506                calculation_load,
9507                has_calculation_load,
9508                calculation_generator,
9509                has_calculation_generator,
9510            };
9511            Ok(true)
9512        })
9513    }
9514}
9515
9516/// Read one property on a boundary line generation reactive limit record.
9517#[unsafe(no_mangle)]
9518pub unsafe extern "C" fn pio_detailed_connectivity_boundary_line_reactive_limit_property_at(
9519    details: *const PioDetailedConnectivity,
9520    boundary_line_index: usize,
9521    property_index: usize,
9522    output: *mut PioStringPropertyView,
9523    error: *mut *mut PioError,
9524) -> bool {
9525    unsafe {
9526        entry(error, false, || {
9527            let details = require_detailed_connectivity(details)?;
9528            let line = details
9529                .boundary_lines
9530                .get(boundary_line_index)
9531                .ok_or_else(|| {
9532                    boundary_error(
9533                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9534                        format!("boundary line index {boundary_line_index} is out of range"),
9535                    )
9536                })?;
9537            let limits = line
9538                .generation
9539                .as_ref()
9540                .and_then(|generation| generation.reactive_limits.as_ref())
9541                .ok_or_else(|| {
9542                    boundary_error(
9543                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9544                        format!(
9545                            "boundary line {boundary_line_index} has no generation reactive limits"
9546                        ),
9547                    )
9548                })?;
9549            let properties = match limits {
9550                powerio_tx::ReactiveLimits::MinMax(limits) => &limits.properties,
9551                powerio_tx::ReactiveLimits::CapabilityCurve(curve) => &curve.properties,
9552                _ => {
9553                    return Err(boundary_error(
9554                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
9555                        "unsupported reactive limit record",
9556                    ));
9557                }
9558            };
9559            let (name, value) = properties.iter().nth(property_index).ok_or_else(|| {
9560                boundary_error(
9561                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9562                    format!("reactive limit property index {property_index} is out of range"),
9563                )
9564            })?;
9565            *require_output(output, "output")? = PioStringPropertyView {
9566                name: PioStringView::new(name),
9567                value: PioStringView::new(value),
9568            };
9569            Ok(true)
9570        })
9571    }
9572}
9573
9574/// Read one point from a boundary line generation reactive capability curve.
9575#[unsafe(no_mangle)]
9576pub unsafe extern "C" fn pio_detailed_connectivity_boundary_line_reactive_capability_point_at(
9577    details: *const PioDetailedConnectivity,
9578    boundary_line_index: usize,
9579    point_index: usize,
9580    output: *mut PioReactiveCapabilityCurvePointView,
9581    error: *mut *mut PioError,
9582) -> bool {
9583    unsafe {
9584        entry(error, false, || {
9585            let details = require_detailed_connectivity(details)?;
9586            let line = details
9587                .boundary_lines
9588                .get(boundary_line_index)
9589                .ok_or_else(|| {
9590                    boundary_error(
9591                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9592                        format!("boundary line index {boundary_line_index} is out of range"),
9593                    )
9594                })?;
9595            let curve = line
9596                .generation
9597                .as_ref()
9598                .and_then(|generation| generation.reactive_limits.as_ref())
9599                .and_then(|limits| match limits {
9600                    powerio_tx::ReactiveLimits::CapabilityCurve(curve) => Some(curve),
9601                    _ => None,
9602                })
9603                .ok_or_else(|| {
9604                    boundary_error(
9605                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
9606                        format!(
9607                            "boundary line {boundary_line_index} has no generation reactive capability curve"
9608                        ),
9609                    )
9610                })?;
9611            let point = curve.points.get(point_index).ok_or_else(|| {
9612                boundary_error(
9613                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9614                    format!("reactive capability point index {point_index} is out of range"),
9615                )
9616            })?;
9617            *require_output(output, "output")? = PioReactiveCapabilityCurvePointView {
9618                active_power_mw: point.active_power_mw,
9619                minimum_reactive_power_mvar: point.minimum_reactive_power_mvar,
9620                maximum_reactive_power_mvar: point.maximum_reactive_power_mvar,
9621                property_count: point.properties.len(),
9622            };
9623            Ok(true)
9624        })
9625    }
9626}
9627
9628/// Read one property from one boundary line reactive capability curve point.
9629#[unsafe(no_mangle)]
9630pub unsafe extern "C" fn pio_detailed_connectivity_boundary_line_reactive_capability_point_property_at(
9631    details: *const PioDetailedConnectivity,
9632    boundary_line_index: usize,
9633    point_index: usize,
9634    property_index: usize,
9635    output: *mut PioStringPropertyView,
9636    error: *mut *mut PioError,
9637) -> bool {
9638    unsafe {
9639        entry(error, false, || {
9640            let details = require_detailed_connectivity(details)?;
9641            let line = details
9642                .boundary_lines
9643                .get(boundary_line_index)
9644                .ok_or_else(|| {
9645                    boundary_error(
9646                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9647                        format!("boundary line index {boundary_line_index} is out of range"),
9648                    )
9649                })?;
9650            let curve = line
9651                .generation
9652                .as_ref()
9653                .and_then(|generation| generation.reactive_limits.as_ref())
9654                .and_then(|limits| match limits {
9655                    powerio_tx::ReactiveLimits::CapabilityCurve(curve) => Some(curve),
9656                    _ => None,
9657                })
9658                .ok_or_else(|| {
9659                    boundary_error(
9660                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
9661                        format!(
9662                            "boundary line {boundary_line_index} has no generation reactive capability curve"
9663                        ),
9664                    )
9665                })?;
9666            let point = curve.points.get(point_index).ok_or_else(|| {
9667                boundary_error(
9668                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9669                    format!("reactive capability point index {point_index} is out of range"),
9670                )
9671            })?;
9672            let (name, value) = point.properties.iter().nth(property_index).ok_or_else(|| {
9673                boundary_error(
9674                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9675                    format!(
9676                        "reactive capability point property index {property_index} is out of range"
9677                    ),
9678                )
9679            })?;
9680            *require_output(output, "output")? = PioStringPropertyView {
9681                name: PioStringView::new(name),
9682                value: PioStringView::new(value),
9683            };
9684            Ok(true)
9685        })
9686    }
9687}
9688
9689/// Read one PowSybl tie line by zero based table position.
9690#[unsafe(no_mangle)]
9691pub unsafe extern "C" fn pio_detailed_connectivity_tie_line_at(
9692    details: *const PioDetailedConnectivity,
9693    index: usize,
9694    output: *mut PioTieLineView,
9695    error: *mut *mut PioError,
9696) -> bool {
9697    unsafe {
9698        entry(error, false, || {
9699            let details = require_detailed_connectivity(details)?;
9700            let tie = details.tie_lines.get(index).ok_or_else(|| {
9701                boundary_error(
9702                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9703                    format!("tie line index {index} is out of range"),
9704                )
9705            })?;
9706            let (calculation_branch, has_calculation_branch) =
9707                optional_component_id_view(tie.calculation_branch.as_ref());
9708            *require_output(output, "output")? = PioTieLineView {
9709                component: component_id_view(&tie.component),
9710                boundary_line1: component_id_view(&tie.boundary_line1),
9711                boundary_line2: component_id_view(&tie.boundary_line2),
9712                calculation_branch,
9713                has_calculation_branch,
9714            };
9715            Ok(true)
9716        })
9717    }
9718}
9719
9720/// Read one transformer tap changer by zero based table position.
9721#[unsafe(no_mangle)]
9722pub unsafe extern "C" fn pio_detailed_connectivity_tap_changer_at(
9723    details: *const PioDetailedConnectivity,
9724    index: usize,
9725    output: *mut PioTapChangerView,
9726    error: *mut *mut PioError,
9727) -> bool {
9728    unsafe {
9729        entry(error, false, || {
9730            let details = require_detailed_connectivity(details)?;
9731            let changer = details.tap_changers.get(index).ok_or_else(|| {
9732                boundary_error(
9733                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9734                    format!("tap changer index {index} is out of range"),
9735                )
9736            })?;
9737            let (regulation_mode, has_regulation_mode) =
9738                changer
9739                    .regulation_mode
9740                    .map_or((PioStringView::EMPTY, false), |mode| {
9741                        (
9742                            PioStringView::new(tap_changer_regulation_mode_name(mode)),
9743                            true,
9744                        )
9745                    });
9746            let (regulation_terminal, has_regulation_terminal) =
9747                terminal_reference_view(changer.regulation_terminal.as_ref());
9748            let (component, has_component) = optional_component_id_view(changer.component.as_ref());
9749            *require_output(output, "output")? = PioTapChangerView {
9750                component,
9751                has_component,
9752                transformer: component_id_view(&changer.transformer),
9753                winding: changer.winding,
9754                kind: PioStringView::new(tap_changer_kind_name(changer.kind)),
9755                tap_position: changer.tap_position.unwrap_or(0),
9756                has_tap_position: changer.tap_position.is_some(),
9757                solved_tap_position: changer.solved_tap_position.unwrap_or(0),
9758                has_solved_tap_position: changer.solved_tap_position.is_some(),
9759                low_tap_position: changer.low_tap_position,
9760                neutral_tap_position: changer.neutral_tap_position.unwrap_or(0),
9761                has_neutral_tap_position: changer.neutral_tap_position.is_some(),
9762                normal_tap_position: changer.normal_tap_position.unwrap_or(0),
9763                has_normal_tap_position: changer.normal_tap_position.is_some(),
9764                voltage_step_increment_percent: changer
9765                    .voltage_step_increment_percent
9766                    .unwrap_or(0.0),
9767                has_voltage_step_increment_percent: changer
9768                    .voltage_step_increment_percent
9769                    .is_some(),
9770                load_tap_changing_capabilities: changer.load_tap_changing_capabilities,
9771                regulating: changer.regulating,
9772                regulation_mode,
9773                has_regulation_mode,
9774                regulation_value: changer.regulation_value.unwrap_or(0.0),
9775                has_regulation_value: changer.regulation_value.is_some(),
9776                target_deadband: changer.target_deadband.unwrap_or(0.0),
9777                has_target_deadband: changer.target_deadband.is_some(),
9778                regulation_terminal,
9779                has_regulation_terminal,
9780                step_count: changer.steps.len(),
9781            };
9782            Ok(true)
9783        })
9784    }
9785}
9786
9787/// Read one transformer tap changer step by zero based position.
9788#[unsafe(no_mangle)]
9789pub unsafe extern "C" fn pio_detailed_connectivity_tap_changer_step_at(
9790    details: *const PioDetailedConnectivity,
9791    tap_changer_index: usize,
9792    step_index: usize,
9793    output: *mut PioTapChangerStepView,
9794    error: *mut *mut PioError,
9795) -> bool {
9796    unsafe {
9797        entry(error, false, || {
9798            let details = require_detailed_connectivity(details)?;
9799            let changer = details.tap_changers.get(tap_changer_index).ok_or_else(|| {
9800                boundary_error(
9801                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9802                    format!("tap changer index {tap_changer_index} is out of range"),
9803                )
9804            })?;
9805            let step = changer.steps.get(step_index).ok_or_else(|| {
9806                boundary_error(
9807                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9808                    format!("tap changer step index {step_index} is out of range"),
9809                )
9810            })?;
9811            *require_output(output, "output")? = PioTapChangerStepView {
9812                position: step.position,
9813                ratio_pu: step.rho,
9814                phase_shift_degrees: step.alpha_degrees,
9815                resistance_deviation_percent: step.resistance_deviation_percent,
9816                reactance_deviation_percent: step.reactance_deviation_percent,
9817                conductance_deviation_percent: step.conductance_deviation_percent,
9818                susceptance_deviation_percent: step.susceptance_deviation_percent,
9819            };
9820            Ok(true)
9821        })
9822    }
9823}
9824
9825/// Read reactive limits retained for one equipment record.
9826#[unsafe(no_mangle)]
9827pub unsafe extern "C" fn pio_detailed_connectivity_equipment_reactive_limits_at(
9828    details: *const PioDetailedConnectivity,
9829    index: usize,
9830    output: *mut PioEquipmentReactiveLimitsView,
9831    error: *mut *mut PioError,
9832) -> bool {
9833    unsafe {
9834        entry(error, false, || {
9835            let details = require_detailed_connectivity(details)?;
9836            let record = details
9837                .equipment_reactive_limits
9838                .get(index)
9839                .ok_or_else(|| {
9840                    boundary_error(
9841                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9842                        format!("equipment reactive limits index {index} is out of range"),
9843                    )
9844                })?;
9845            *require_output(output, "output")? = PioEquipmentReactiveLimitsView {
9846                equipment: component_id_view(&record.equipment),
9847                limits: reactive_limits_view(Some(&record.limits)).0,
9848            };
9849            Ok(true)
9850        })
9851    }
9852}
9853
9854/// Read one property from an equipment reactive limit record.
9855#[unsafe(no_mangle)]
9856pub unsafe extern "C" fn pio_detailed_connectivity_equipment_reactive_limit_property_at(
9857    details: *const PioDetailedConnectivity,
9858    equipment_index: usize,
9859    property_index: usize,
9860    output: *mut PioStringPropertyView,
9861    error: *mut *mut PioError,
9862) -> bool {
9863    unsafe {
9864        entry(error, false, || {
9865            let details = require_detailed_connectivity(details)?;
9866            let record = details
9867                .equipment_reactive_limits
9868                .get(equipment_index)
9869                .ok_or_else(|| {
9870                    boundary_error(
9871                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9872                        format!(
9873                            "equipment reactive limits index {equipment_index} is out of range"
9874                        ),
9875                    )
9876                })?;
9877            *require_output(output, "output")? =
9878                string_property_view(reactive_limit_properties(&record.limits), property_index)?;
9879            Ok(true)
9880        })
9881    }
9882}
9883
9884/// Read one point from an equipment reactive capability curve.
9885#[unsafe(no_mangle)]
9886pub unsafe extern "C" fn pio_detailed_connectivity_equipment_reactive_capability_point_at(
9887    details: *const PioDetailedConnectivity,
9888    equipment_index: usize,
9889    point_index: usize,
9890    output: *mut PioReactiveCapabilityCurvePointView,
9891    error: *mut *mut PioError,
9892) -> bool {
9893    unsafe {
9894        entry(error, false, || {
9895            let details = require_detailed_connectivity(details)?;
9896            let record = details
9897                .equipment_reactive_limits
9898                .get(equipment_index)
9899                .ok_or_else(|| {
9900                    boundary_error(
9901                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9902                        format!(
9903                            "equipment reactive limits index {equipment_index} is out of range"
9904                        ),
9905                    )
9906                })?;
9907            let curve = reactive_capability_curve(&record.limits).ok_or_else(|| {
9908                boundary_error(
9909                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
9910                    format!(
9911                        "equipment reactive limits {equipment_index} is not a capability curve"
9912                    ),
9913                )
9914            })?;
9915            *require_output(output, "output")? =
9916                reactive_capability_point_view(curve, point_index)?;
9917            Ok(true)
9918        })
9919    }
9920}
9921
9922/// Read one property from an equipment reactive capability curve point.
9923#[unsafe(no_mangle)]
9924pub unsafe extern "C" fn pio_detailed_connectivity_equipment_reactive_capability_point_property_at(
9925    details: *const PioDetailedConnectivity,
9926    equipment_index: usize,
9927    point_index: usize,
9928    property_index: usize,
9929    output: *mut PioStringPropertyView,
9930    error: *mut *mut PioError,
9931) -> bool {
9932    unsafe {
9933        entry(error, false, || {
9934            let details = require_detailed_connectivity(details)?;
9935            let record = details
9936                .equipment_reactive_limits
9937                .get(equipment_index)
9938                .ok_or_else(|| {
9939                    boundary_error(
9940                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9941                        format!(
9942                            "equipment reactive limits index {equipment_index} is out of range"
9943                        ),
9944                    )
9945                })?;
9946            let curve = reactive_capability_curve(&record.limits).ok_or_else(|| {
9947                boundary_error(
9948                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
9949                    format!(
9950                        "equipment reactive limits {equipment_index} is not a capability curve"
9951                    ),
9952                )
9953            })?;
9954            let point = curve.points.get(point_index).ok_or_else(|| {
9955                boundary_error(
9956                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9957                    format!("reactive capability point index {point_index} is out of range"),
9958                )
9959            })?;
9960            *require_output(output, "output")? =
9961                string_property_view(&point.properties, property_index)?;
9962            Ok(true)
9963        })
9964    }
9965}
9966
9967/// Read one DC converter unit by zero based table position.
9968#[unsafe(no_mangle)]
9969pub unsafe extern "C" fn pio_detailed_connectivity_dc_converter_unit_at(
9970    details: *const PioDetailedConnectivity,
9971    index: usize,
9972    output: *mut PioDcConverterUnitView,
9973    error: *mut *mut PioError,
9974) -> bool {
9975    unsafe {
9976        entry(error, false, || {
9977            let details = require_detailed_connectivity(details)?;
9978            let unit = details.dc_converter_units.get(index).ok_or_else(|| {
9979                boundary_error(
9980                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
9981                    format!("DC converter unit index {index} is out of range"),
9982                )
9983            })?;
9984            let (substation, has_substation) = optional_component_id_view(unit.substation.as_ref());
9985            *require_output(output, "output")? = PioDcConverterUnitView {
9986                component: component_id_view(&unit.component),
9987                substation,
9988                has_substation,
9989                operation_mode: PioStringView::new(dc_converter_operation_mode_name(
9990                    unit.operation_mode,
9991                )),
9992            };
9993            Ok(true)
9994        })
9995    }
9996}
9997
9998/// Read one DC topological node by zero based table position.
9999#[unsafe(no_mangle)]
10000pub unsafe extern "C" fn pio_detailed_connectivity_dc_topological_node_at(
10001    details: *const PioDetailedConnectivity,
10002    index: usize,
10003    output: *mut PioDcNodeView,
10004    error: *mut *mut PioError,
10005) -> bool {
10006    unsafe {
10007        entry(error, false, || {
10008            let details = require_detailed_connectivity(details)?;
10009            let node = details.dc_topological_nodes.get(index).ok_or_else(|| {
10010                boundary_error(
10011                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10012                    format!("DC topological node index {index} is out of range"),
10013                )
10014            })?;
10015            let (dc_converter_unit, has_dc_converter_unit) =
10016                optional_component_id_view(node.dc_converter_unit.as_ref());
10017            *require_output(output, "output")? = PioDcNodeView {
10018                component: component_id_view(&node.component),
10019                kind: PioStringView::new("topological_node"),
10020                nominal_voltage_kv: 0.0,
10021                has_nominal_voltage: false,
10022                voltage_kv: 0.0,
10023                has_voltage: false,
10024                dc_converter_unit,
10025                has_dc_converter_unit,
10026                dc_topological_node: empty_component_id_view(),
10027                has_dc_topological_node: false,
10028            };
10029            Ok(true)
10030        })
10031    }
10032}
10033
10034/// Read one physical DC node by zero based table position.
10035#[unsafe(no_mangle)]
10036pub unsafe extern "C" fn pio_detailed_connectivity_dc_node_at(
10037    details: *const PioDetailedConnectivity,
10038    index: usize,
10039    output: *mut PioDcNodeView,
10040    error: *mut *mut PioError,
10041) -> bool {
10042    unsafe {
10043        entry(error, false, || {
10044            let details = require_detailed_connectivity(details)?;
10045            let node = details.dc_nodes.get(index).ok_or_else(|| {
10046                boundary_error(
10047                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10048                    format!("DC node index {index} is out of range"),
10049                )
10050            })?;
10051            let (dc_converter_unit, has_dc_converter_unit) =
10052                optional_component_id_view(node.dc_converter_unit.as_ref());
10053            let (dc_topological_node, has_dc_topological_node) =
10054                optional_component_id_view(node.dc_topological_node.as_ref());
10055            *require_output(output, "output")? = PioDcNodeView {
10056                component: component_id_view(&node.component),
10057                kind: PioStringView::new("node"),
10058                nominal_voltage_kv: node.nominal_voltage_kv.unwrap_or(0.0),
10059                has_nominal_voltage: node.nominal_voltage_kv.is_some(),
10060                voltage_kv: node.voltage_kv.unwrap_or(0.0),
10061                has_voltage: node.voltage_kv.is_some(),
10062                dc_converter_unit,
10063                has_dc_converter_unit,
10064                dc_topological_node,
10065                has_dc_topological_node,
10066            };
10067            Ok(true)
10068        })
10069    }
10070}
10071
10072/// Read one DC ground by zero based table position.
10073#[unsafe(no_mangle)]
10074pub unsafe extern "C" fn pio_detailed_connectivity_dc_ground_at(
10075    details: *const PioDetailedConnectivity,
10076    index: usize,
10077    output: *mut PioDcEquipmentView,
10078    error: *mut *mut PioError,
10079) -> bool {
10080    unsafe {
10081        entry(error, false, || {
10082            let details = require_detailed_connectivity(details)?;
10083            let record = details.dc_grounds.get(index).ok_or_else(|| {
10084                boundary_error(
10085                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10086                    format!("DC ground index {index} is out of range"),
10087                )
10088            })?;
10089            *require_output(output, "output")? = dc_equipment_view(
10090                &record.component,
10091                record.equipment_container.as_ref(),
10092                "ground",
10093                &record.dc_terminal,
10094                None,
10095                record.rated_dc_voltage_kv,
10096                record.resistance_ohm,
10097                record.inductance_h,
10098                None,
10099                None,
10100                None,
10101                None,
10102            );
10103            Ok(true)
10104        })
10105    }
10106}
10107
10108/// Read one DC busbar by zero based table position.
10109#[unsafe(no_mangle)]
10110pub unsafe extern "C" fn pio_detailed_connectivity_dc_busbar_at(
10111    details: *const PioDetailedConnectivity,
10112    index: usize,
10113    output: *mut PioDcEquipmentView,
10114    error: *mut *mut PioError,
10115) -> bool {
10116    unsafe {
10117        entry(error, false, || {
10118            let details = require_detailed_connectivity(details)?;
10119            let record = details.dc_busbars.get(index).ok_or_else(|| {
10120                boundary_error(
10121                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10122                    format!("DC busbar index {index} is out of range"),
10123                )
10124            })?;
10125            *require_output(output, "output")? = dc_equipment_view(
10126                &record.component,
10127                record.equipment_container.as_ref(),
10128                "busbar",
10129                &record.dc_terminal,
10130                None,
10131                record.rated_dc_voltage_kv,
10132                None,
10133                None,
10134                None,
10135                None,
10136                None,
10137                None,
10138            );
10139            Ok(true)
10140        })
10141    }
10142}
10143
10144/// Read one DC line by zero based table position.
10145#[unsafe(no_mangle)]
10146pub unsafe extern "C" fn pio_detailed_connectivity_dc_line_at(
10147    details: *const PioDetailedConnectivity,
10148    index: usize,
10149    output: *mut PioDcEquipmentView,
10150    error: *mut *mut PioError,
10151) -> bool {
10152    unsafe {
10153        entry(error, false, || {
10154            let details = require_detailed_connectivity(details)?;
10155            let record = details.dc_lines.get(index).ok_or_else(|| {
10156                boundary_error(
10157                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10158                    format!("DC line index {index} is out of range"),
10159                )
10160            })?;
10161            *require_output(output, "output")? = dc_equipment_view(
10162                &record.component,
10163                record.equipment_container.as_ref(),
10164                "line",
10165                &record.dc_terminal1,
10166                Some(&record.dc_terminal2),
10167                record.rated_dc_voltage_kv,
10168                record.resistance_ohm,
10169                record.inductance_h,
10170                record.capacitance_f,
10171                record.length_km,
10172                None,
10173                None,
10174            );
10175            Ok(true)
10176        })
10177    }
10178}
10179
10180/// Read one DC series device by zero based table position.
10181#[unsafe(no_mangle)]
10182pub unsafe extern "C" fn pio_detailed_connectivity_dc_series_device_at(
10183    details: *const PioDetailedConnectivity,
10184    index: usize,
10185    output: *mut PioDcEquipmentView,
10186    error: *mut *mut PioError,
10187) -> bool {
10188    unsafe {
10189        entry(error, false, || {
10190            let details = require_detailed_connectivity(details)?;
10191            let record = details.dc_series_devices.get(index).ok_or_else(|| {
10192                boundary_error(
10193                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10194                    format!("DC series device index {index} is out of range"),
10195                )
10196            })?;
10197            *require_output(output, "output")? = dc_equipment_view(
10198                &record.component,
10199                record.equipment_container.as_ref(),
10200                "series_device",
10201                &record.dc_terminal1,
10202                Some(&record.dc_terminal2),
10203                record.rated_dc_voltage_kv,
10204                record.resistance_ohm,
10205                record.inductance_h,
10206                None,
10207                None,
10208                None,
10209                None,
10210            );
10211            Ok(true)
10212        })
10213    }
10214}
10215
10216/// Read one DC switch by zero based table position.
10217#[unsafe(no_mangle)]
10218pub unsafe extern "C" fn pio_detailed_connectivity_dc_switch_at(
10219    details: *const PioDetailedConnectivity,
10220    index: usize,
10221    output: *mut PioDcEquipmentView,
10222    error: *mut *mut PioError,
10223) -> bool {
10224    unsafe {
10225        entry(error, false, || {
10226            let details = require_detailed_connectivity(details)?;
10227            let record = details.dc_switches.get(index).ok_or_else(|| {
10228                boundary_error(
10229                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10230                    format!("DC switch index {index} is out of range"),
10231                )
10232            })?;
10233            *require_output(output, "output")? = dc_equipment_view(
10234                &record.component,
10235                record.equipment_container.as_ref(),
10236                "switch",
10237                &record.dc_terminal1,
10238                Some(&record.dc_terminal2),
10239                record.rated_dc_voltage_kv,
10240                record.resistance_ohm,
10241                None,
10242                None,
10243                None,
10244                Some(record.kind),
10245                record.open,
10246            );
10247            Ok(true)
10248        })
10249    }
10250}
10251
10252/// Read one voltage source converter by zero based table position.
10253#[unsafe(no_mangle)]
10254pub unsafe extern "C" fn pio_detailed_connectivity_voltage_source_converter_at(
10255    details: *const PioDetailedConnectivity,
10256    index: usize,
10257    output: *mut PioAcDcConverterView,
10258    error: *mut *mut PioError,
10259) -> bool {
10260    unsafe {
10261        entry(error, false, || {
10262            let details = require_detailed_connectivity(details)?;
10263            let converter = details
10264                .voltage_source_converters
10265                .get(index)
10266                .ok_or_else(|| {
10267                    boundary_error(
10268                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10269                        format!("voltage source converter index {index} is out of range"),
10270                    )
10271                })?;
10272            let (dc_converter_unit, has_dc_converter_unit) =
10273                optional_component_id_view(converter.dc_converter_unit.as_ref());
10274            let (control_mode, has_control_mode) =
10275                converter
10276                    .control_mode
10277                    .map_or((PioStringView::EMPTY, false), |mode| {
10278                        (
10279                            PioStringView::new(ac_dc_converter_control_mode_name(mode)),
10280                            true,
10281                        )
10282                    });
10283            let (pcc_terminal, has_pcc_terminal) =
10284                terminal_reference_view(converter.pcc_terminal.as_ref());
10285            let (reactive_limits, has_reactive_limits) =
10286                reactive_limits_view(converter.reactive_limits.as_ref());
10287            *require_output(output, "output")? = PioAcDcConverterView {
10288                component: component_id_view(&converter.component),
10289                kind: PioStringView::new("voltage_source"),
10290                dc_converter_unit,
10291                has_dc_converter_unit,
10292                dc_terminal1: dc_terminal_view(&converter.dc_terminal1),
10293                dc_terminal2: dc_terminal_view(&converter.dc_terminal2),
10294                base_apparent_power_mva: converter.base_apparent_power_mva.unwrap_or(0.0),
10295                has_base_apparent_power: converter.base_apparent_power_mva.is_some(),
10296                minimum_active_power_mw: converter.minimum_active_power_mw.unwrap_or(0.0),
10297                has_minimum_active_power: converter.minimum_active_power_mw.is_some(),
10298                maximum_active_power_mw: converter.maximum_active_power_mw.unwrap_or(0.0),
10299                has_maximum_active_power: converter.maximum_active_power_mw.is_some(),
10300                minimum_dc_voltage_kv: converter.minimum_dc_voltage_kv.unwrap_or(0.0),
10301                has_minimum_dc_voltage: converter.minimum_dc_voltage_kv.is_some(),
10302                maximum_dc_voltage_kv: converter.maximum_dc_voltage_kv.unwrap_or(0.0),
10303                has_maximum_dc_voltage: converter.maximum_dc_voltage_kv.is_some(),
10304                rated_dc_voltage_kv: converter.rated_dc_voltage_kv.unwrap_or(0.0),
10305                has_rated_dc_voltage: converter.rated_dc_voltage_kv.is_some(),
10306                valve_u0_kv: converter.valve_u0_kv.unwrap_or(0.0),
10307                has_valve_u0: converter.valve_u0_kv.is_some(),
10308                number_of_valves: converter.number_of_valves.unwrap_or(0),
10309                has_number_of_valves: converter.number_of_valves.is_some(),
10310                idle_loss_mw: converter.idle_loss_mw.unwrap_or(0.0),
10311                has_idle_loss: converter.idle_loss_mw.is_some(),
10312                switching_loss_mw_per_ampere: converter.switching_loss_mw_per_ampere.unwrap_or(0.0),
10313                has_switching_loss: converter.switching_loss_mw_per_ampere.is_some(),
10314                resistive_loss_ohm: converter.resistive_loss_ohm.unwrap_or(0.0),
10315                has_resistive_loss: converter.resistive_loss_ohm.is_some(),
10316                control_mode,
10317                has_control_mode,
10318                active_power_at_pcc_mw: converter.active_power_at_pcc_mw.unwrap_or(0.0),
10319                has_active_power_at_pcc: converter.active_power_at_pcc_mw.is_some(),
10320                reactive_power_at_pcc_mvar: converter.reactive_power_at_pcc_mvar.unwrap_or(0.0),
10321                has_reactive_power_at_pcc: converter.reactive_power_at_pcc_mvar.is_some(),
10322                target_active_power_mw: converter.target_active_power_mw.unwrap_or(0.0),
10323                has_target_active_power: converter.target_active_power_mw.is_some(),
10324                target_dc_voltage_kv: converter.target_dc_voltage_kv.unwrap_or(0.0),
10325                has_target_dc_voltage: converter.target_dc_voltage_kv.is_some(),
10326                pcc_terminal,
10327                has_pcc_terminal,
10328                droop_curve_segment_count: converter
10329                    .droop_curve
10330                    .as_ref()
10331                    .map_or(0, |curve| curve.segments.len()),
10332                has_droop_curve: converter.droop_curve.is_some(),
10333                droop: converter.droop.unwrap_or(0.0),
10334                has_droop: converter.droop.is_some(),
10335                droop_compensation: converter.droop_compensation.unwrap_or(0.0),
10336                has_droop_compensation: converter.droop_compensation.is_some(),
10337                q_share: converter.q_share.unwrap_or(0.0),
10338                has_q_share: converter.q_share.is_some(),
10339                maximum_modulation_index: converter.maximum_modulation_index.unwrap_or(0.0),
10340                has_maximum_modulation_index: converter.maximum_modulation_index.is_some(),
10341                maximum_valve_current_a: converter.maximum_valve_current_a.unwrap_or(0.0),
10342                has_maximum_valve_current: converter.maximum_valve_current_a.is_some(),
10343                dc_current_a: converter.dc_current_a.unwrap_or(0.0),
10344                has_dc_current: converter.dc_current_a.is_some(),
10345                ac_voltage_kv: converter.ac_voltage_kv.unwrap_or(0.0),
10346                has_ac_voltage: converter.ac_voltage_kv.is_some(),
10347                dc_voltage_kv: converter.dc_voltage_kv.unwrap_or(0.0),
10348                has_dc_voltage: converter.dc_voltage_kv.is_some(),
10349                voltage_regulator_on: converter.voltage_regulator_on.unwrap_or(false),
10350                has_voltage_regulator_on: converter.voltage_regulator_on.is_some(),
10351                voltage_setpoint_kv: converter.voltage_setpoint_kv.unwrap_or(0.0),
10352                has_voltage_setpoint: converter.voltage_setpoint_kv.is_some(),
10353                reactive_power_setpoint_mvar: converter.reactive_power_setpoint_mvar.unwrap_or(0.0),
10354                has_reactive_power_setpoint: converter.reactive_power_setpoint_mvar.is_some(),
10355                reactive_limits,
10356                has_reactive_limits,
10357                pole_loss_active_power_mw: converter.pole_loss_active_power_mw.unwrap_or(0.0),
10358                has_pole_loss_active_power: converter.pole_loss_active_power_mw.is_some(),
10359                reactive_model: PioStringView::EMPTY,
10360                has_reactive_model: false,
10361                power_factor: 0.0,
10362                has_power_factor: false,
10363                operating_mode: PioStringView::EMPTY,
10364                has_operating_mode: false,
10365                rated_dc_current_a: 0.0,
10366                has_rated_dc_current: false,
10367                minimum_alpha_degrees: 0.0,
10368                has_minimum_alpha: false,
10369                maximum_alpha_degrees: 0.0,
10370                has_maximum_alpha: false,
10371                minimum_gamma_degrees: 0.0,
10372                has_minimum_gamma: false,
10373                maximum_gamma_degrees: 0.0,
10374                has_maximum_gamma: false,
10375                target_alpha_degrees: 0.0,
10376                has_target_alpha: false,
10377                target_gamma_degrees: 0.0,
10378                has_target_gamma: false,
10379                target_dc_current_a: 0.0,
10380                has_target_dc_current: false,
10381                alpha_degrees: 0.0,
10382                has_alpha: false,
10383                gamma_degrees: 0.0,
10384                has_gamma: false,
10385                delta_degrees: converter.delta_degrees.unwrap_or(0.0),
10386                has_delta: converter.delta_degrees.is_some(),
10387                uf_kv: converter.uf_kv.unwrap_or(0.0),
10388                has_uf: converter.uf_kv.is_some(),
10389                uv_kv: converter.uv_kv.unwrap_or(0.0),
10390                has_uv: converter.uv_kv.is_some(),
10391            };
10392            Ok(true)
10393        })
10394    }
10395}
10396
10397/// Read one line commutated converter by zero based table position.
10398#[unsafe(no_mangle)]
10399pub unsafe extern "C" fn pio_detailed_connectivity_line_commutated_converter_at(
10400    details: *const PioDetailedConnectivity,
10401    index: usize,
10402    output: *mut PioAcDcConverterView,
10403    error: *mut *mut PioError,
10404) -> bool {
10405    unsafe {
10406        entry(error, false, || {
10407            let details = require_detailed_connectivity(details)?;
10408            let converter = details
10409                .line_commutated_converters
10410                .get(index)
10411                .ok_or_else(|| {
10412                    boundary_error(
10413                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10414                        format!("line commutated converter index {index} is out of range"),
10415                    )
10416                })?;
10417            let (dc_converter_unit, has_dc_converter_unit) =
10418                optional_component_id_view(converter.dc_converter_unit.as_ref());
10419            let (control_mode, has_control_mode) =
10420                converter
10421                    .control_mode
10422                    .map_or((PioStringView::EMPTY, false), |mode| {
10423                        (
10424                            PioStringView::new(ac_dc_converter_control_mode_name(mode)),
10425                            true,
10426                        )
10427                    });
10428            let (reactive_model, has_reactive_model) =
10429                converter
10430                    .reactive_model
10431                    .map_or((PioStringView::EMPTY, false), |model| {
10432                        (
10433                            PioStringView::new(line_commutated_converter_reactive_model_name(
10434                                model,
10435                            )),
10436                            true,
10437                        )
10438                    });
10439            let (operating_mode, has_operating_mode) =
10440                converter
10441                    .operating_mode
10442                    .map_or((PioStringView::EMPTY, false), |mode| {
10443                        (
10444                            PioStringView::new(line_commutated_converter_operating_mode_name(mode)),
10445                            true,
10446                        )
10447                    });
10448            let (pcc_terminal, has_pcc_terminal) =
10449                terminal_reference_view(converter.pcc_terminal.as_ref());
10450            *require_output(output, "output")? = PioAcDcConverterView {
10451                component: component_id_view(&converter.component),
10452                kind: PioStringView::new("line_commutated"),
10453                dc_converter_unit,
10454                has_dc_converter_unit,
10455                dc_terminal1: dc_terminal_view(&converter.dc_terminal1),
10456                dc_terminal2: dc_terminal_view(&converter.dc_terminal2),
10457                base_apparent_power_mva: converter.base_apparent_power_mva.unwrap_or(0.0),
10458                has_base_apparent_power: converter.base_apparent_power_mva.is_some(),
10459                minimum_active_power_mw: converter.minimum_active_power_mw.unwrap_or(0.0),
10460                has_minimum_active_power: converter.minimum_active_power_mw.is_some(),
10461                maximum_active_power_mw: converter.maximum_active_power_mw.unwrap_or(0.0),
10462                has_maximum_active_power: converter.maximum_active_power_mw.is_some(),
10463                minimum_dc_voltage_kv: converter.minimum_dc_voltage_kv.unwrap_or(0.0),
10464                has_minimum_dc_voltage: converter.minimum_dc_voltage_kv.is_some(),
10465                maximum_dc_voltage_kv: converter.maximum_dc_voltage_kv.unwrap_or(0.0),
10466                has_maximum_dc_voltage: converter.maximum_dc_voltage_kv.is_some(),
10467                rated_dc_voltage_kv: converter.rated_dc_voltage_kv.unwrap_or(0.0),
10468                has_rated_dc_voltage: converter.rated_dc_voltage_kv.is_some(),
10469                valve_u0_kv: converter.valve_u0_kv.unwrap_or(0.0),
10470                has_valve_u0: converter.valve_u0_kv.is_some(),
10471                number_of_valves: converter.number_of_valves.unwrap_or(0),
10472                has_number_of_valves: converter.number_of_valves.is_some(),
10473                idle_loss_mw: converter.idle_loss_mw.unwrap_or(0.0),
10474                has_idle_loss: converter.idle_loss_mw.is_some(),
10475                switching_loss_mw_per_ampere: converter.switching_loss_mw_per_ampere.unwrap_or(0.0),
10476                has_switching_loss: converter.switching_loss_mw_per_ampere.is_some(),
10477                resistive_loss_ohm: converter.resistive_loss_ohm.unwrap_or(0.0),
10478                has_resistive_loss: converter.resistive_loss_ohm.is_some(),
10479                control_mode,
10480                has_control_mode,
10481                active_power_at_pcc_mw: converter.active_power_at_pcc_mw.unwrap_or(0.0),
10482                has_active_power_at_pcc: converter.active_power_at_pcc_mw.is_some(),
10483                reactive_power_at_pcc_mvar: converter.reactive_power_at_pcc_mvar.unwrap_or(0.0),
10484                has_reactive_power_at_pcc: converter.reactive_power_at_pcc_mvar.is_some(),
10485                target_active_power_mw: converter.target_active_power_mw.unwrap_or(0.0),
10486                has_target_active_power: converter.target_active_power_mw.is_some(),
10487                target_dc_voltage_kv: converter.target_dc_voltage_kv.unwrap_or(0.0),
10488                has_target_dc_voltage: converter.target_dc_voltage_kv.is_some(),
10489                pcc_terminal,
10490                has_pcc_terminal,
10491                droop_curve_segment_count: converter
10492                    .droop_curve
10493                    .as_ref()
10494                    .map_or(0, |curve| curve.segments.len()),
10495                has_droop_curve: converter.droop_curve.is_some(),
10496                droop: 0.0,
10497                has_droop: false,
10498                droop_compensation: 0.0,
10499                has_droop_compensation: false,
10500                q_share: 0.0,
10501                has_q_share: false,
10502                maximum_modulation_index: 0.0,
10503                has_maximum_modulation_index: false,
10504                maximum_valve_current_a: 0.0,
10505                has_maximum_valve_current: false,
10506                dc_current_a: converter.dc_current_a.unwrap_or(0.0),
10507                has_dc_current: converter.dc_current_a.is_some(),
10508                ac_voltage_kv: converter.ac_voltage_kv.unwrap_or(0.0),
10509                has_ac_voltage: converter.ac_voltage_kv.is_some(),
10510                dc_voltage_kv: converter.dc_voltage_kv.unwrap_or(0.0),
10511                has_dc_voltage: converter.dc_voltage_kv.is_some(),
10512                voltage_regulator_on: false,
10513                has_voltage_regulator_on: false,
10514                voltage_setpoint_kv: 0.0,
10515                has_voltage_setpoint: false,
10516                reactive_power_setpoint_mvar: 0.0,
10517                has_reactive_power_setpoint: false,
10518                reactive_limits: empty_reactive_limits_view(),
10519                has_reactive_limits: false,
10520                pole_loss_active_power_mw: converter.pole_loss_active_power_mw.unwrap_or(0.0),
10521                has_pole_loss_active_power: converter.pole_loss_active_power_mw.is_some(),
10522                reactive_model,
10523                has_reactive_model,
10524                power_factor: converter.power_factor.unwrap_or(0.0),
10525                has_power_factor: converter.power_factor.is_some(),
10526                operating_mode,
10527                has_operating_mode,
10528                rated_dc_current_a: converter.rated_dc_current_a.unwrap_or(0.0),
10529                has_rated_dc_current: converter.rated_dc_current_a.is_some(),
10530                minimum_alpha_degrees: converter.minimum_alpha_degrees.unwrap_or(0.0),
10531                has_minimum_alpha: converter.minimum_alpha_degrees.is_some(),
10532                maximum_alpha_degrees: converter.maximum_alpha_degrees.unwrap_or(0.0),
10533                has_maximum_alpha: converter.maximum_alpha_degrees.is_some(),
10534                minimum_gamma_degrees: converter.minimum_gamma_degrees.unwrap_or(0.0),
10535                has_minimum_gamma: converter.minimum_gamma_degrees.is_some(),
10536                maximum_gamma_degrees: converter.maximum_gamma_degrees.unwrap_or(0.0),
10537                has_maximum_gamma: converter.maximum_gamma_degrees.is_some(),
10538                target_alpha_degrees: converter.target_alpha_degrees.unwrap_or(0.0),
10539                has_target_alpha: converter.target_alpha_degrees.is_some(),
10540                target_gamma_degrees: converter.target_gamma_degrees.unwrap_or(0.0),
10541                has_target_gamma: converter.target_gamma_degrees.is_some(),
10542                target_dc_current_a: converter.target_dc_current_a.unwrap_or(0.0),
10543                has_target_dc_current: converter.target_dc_current_a.is_some(),
10544                alpha_degrees: converter.alpha_degrees.unwrap_or(0.0),
10545                has_alpha: converter.alpha_degrees.is_some(),
10546                gamma_degrees: converter.gamma_degrees.unwrap_or(0.0),
10547                has_gamma: converter.gamma_degrees.is_some(),
10548                delta_degrees: 0.0,
10549                has_delta: false,
10550                uf_kv: 0.0,
10551                has_uf: false,
10552                uv_kv: 0.0,
10553                has_uv: false,
10554            };
10555            Ok(true)
10556        })
10557    }
10558}
10559
10560fn droop_curve_segment_view(segment: &powerio_tx::DroopCurveSegment) -> PioDroopCurveSegmentView {
10561    PioDroopCurveSegmentView {
10562        minimum_voltage_kv: segment.minimum_voltage_kv,
10563        maximum_voltage_kv: segment.maximum_voltage_kv,
10564        k: segment.k,
10565    }
10566}
10567
10568/// Read one DC voltage droop curve segment from a voltage source converter.
10569#[unsafe(no_mangle)]
10570pub unsafe extern "C" fn pio_detailed_connectivity_voltage_source_converter_droop_curve_segment_at(
10571    details: *const PioDetailedConnectivity,
10572    converter_index: usize,
10573    segment_index: usize,
10574    output: *mut PioDroopCurveSegmentView,
10575    error: *mut *mut PioError,
10576) -> bool {
10577    unsafe {
10578        entry(error, false, || {
10579            let details = require_detailed_connectivity(details)?;
10580            let converter = details
10581                .voltage_source_converters
10582                .get(converter_index)
10583                .ok_or_else(|| {
10584                    boundary_error(
10585                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10586                        format!("voltage source converter index {converter_index} is out of range"),
10587                    )
10588                })?;
10589            let curve = converter.droop_curve.as_ref().ok_or_else(|| {
10590                boundary_error(
10591                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
10592                    format!("voltage source converter {converter_index} has no droop curve"),
10593                )
10594            })?;
10595            let segment = curve.segments.get(segment_index).ok_or_else(|| {
10596                boundary_error(
10597                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10598                    format!("droop curve segment index {segment_index} is out of range"),
10599                )
10600            })?;
10601            *require_output(output, "output")? = droop_curve_segment_view(segment);
10602            Ok(true)
10603        })
10604    }
10605}
10606
10607/// Read one DC voltage droop curve segment from a line commutated converter.
10608#[unsafe(no_mangle)]
10609pub unsafe extern "C" fn pio_detailed_connectivity_line_commutated_converter_droop_curve_segment_at(
10610    details: *const PioDetailedConnectivity,
10611    converter_index: usize,
10612    segment_index: usize,
10613    output: *mut PioDroopCurveSegmentView,
10614    error: *mut *mut PioError,
10615) -> bool {
10616    unsafe {
10617        entry(error, false, || {
10618            let details = require_detailed_connectivity(details)?;
10619            let converter = details
10620                .line_commutated_converters
10621                .get(converter_index)
10622                .ok_or_else(|| {
10623                    boundary_error(
10624                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10625                        format!(
10626                            "line commutated converter index {converter_index} is out of range"
10627                        ),
10628                    )
10629                })?;
10630            let curve = converter.droop_curve.as_ref().ok_or_else(|| {
10631                boundary_error(
10632                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
10633                    format!("line commutated converter {converter_index} has no droop curve"),
10634                )
10635            })?;
10636            let segment = curve.segments.get(segment_index).ok_or_else(|| {
10637                boundary_error(
10638                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10639                    format!("droop curve segment index {segment_index} is out of range"),
10640                )
10641            })?;
10642            *require_output(output, "output")? = droop_curve_segment_view(segment);
10643            Ok(true)
10644        })
10645    }
10646}
10647
10648/// Read one property from a voltage source converter reactive limit record.
10649#[unsafe(no_mangle)]
10650pub unsafe extern "C" fn pio_detailed_connectivity_voltage_source_converter_reactive_limit_property_at(
10651    details: *const PioDetailedConnectivity,
10652    converter_index: usize,
10653    property_index: usize,
10654    output: *mut PioStringPropertyView,
10655    error: *mut *mut PioError,
10656) -> bool {
10657    unsafe {
10658        entry(error, false, || {
10659            let details = require_detailed_connectivity(details)?;
10660            let converter = details
10661                .voltage_source_converters
10662                .get(converter_index)
10663                .ok_or_else(|| {
10664                    boundary_error(
10665                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10666                        format!("voltage source converter index {converter_index} is out of range"),
10667                    )
10668                })?;
10669            let limits = converter.reactive_limits.as_ref().ok_or_else(|| {
10670                boundary_error(
10671                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
10672                    format!("voltage source converter {converter_index} has no reactive limits"),
10673                )
10674            })?;
10675            *require_output(output, "output")? =
10676                string_property_view(reactive_limit_properties(limits), property_index)?;
10677            Ok(true)
10678        })
10679    }
10680}
10681
10682/// Read one point from a voltage source converter reactive capability curve.
10683#[unsafe(no_mangle)]
10684pub unsafe extern "C" fn pio_detailed_connectivity_voltage_source_converter_reactive_capability_point_at(
10685    details: *const PioDetailedConnectivity,
10686    converter_index: usize,
10687    point_index: usize,
10688    output: *mut PioReactiveCapabilityCurvePointView,
10689    error: *mut *mut PioError,
10690) -> bool {
10691    unsafe {
10692        entry(error, false, || {
10693            let details = require_detailed_connectivity(details)?;
10694            let converter = details
10695                .voltage_source_converters
10696                .get(converter_index)
10697                .ok_or_else(|| {
10698                    boundary_error(
10699                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10700                        format!("voltage source converter index {converter_index} is out of range"),
10701                    )
10702                })?;
10703            let curve = converter
10704                .reactive_limits
10705                .as_ref()
10706                .and_then(reactive_capability_curve)
10707                .ok_or_else(|| {
10708                    boundary_error(
10709                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
10710                        format!(
10711                            "voltage source converter {converter_index} has no reactive capability curve"
10712                        ),
10713                    )
10714                })?;
10715            *require_output(output, "output")? =
10716                reactive_capability_point_view(curve, point_index)?;
10717            Ok(true)
10718        })
10719    }
10720}
10721
10722/// Read one property from a voltage source converter reactive capability point.
10723#[unsafe(no_mangle)]
10724pub unsafe extern "C" fn pio_detailed_connectivity_voltage_source_converter_reactive_capability_point_property_at(
10725    details: *const PioDetailedConnectivity,
10726    converter_index: usize,
10727    point_index: usize,
10728    property_index: usize,
10729    output: *mut PioStringPropertyView,
10730    error: *mut *mut PioError,
10731) -> bool {
10732    unsafe {
10733        entry(error, false, || {
10734            let details = require_detailed_connectivity(details)?;
10735            let converter = details
10736                .voltage_source_converters
10737                .get(converter_index)
10738                .ok_or_else(|| {
10739                    boundary_error(
10740                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10741                        format!("voltage source converter index {converter_index} is out of range"),
10742                    )
10743                })?;
10744            let curve = converter
10745                .reactive_limits
10746                .as_ref()
10747                .and_then(reactive_capability_curve)
10748                .ok_or_else(|| {
10749                    boundary_error(
10750                        &codes::REQUEST_CAPI_TYPE_MISMATCH,
10751                        format!(
10752                            "voltage source converter {converter_index} has no reactive capability curve"
10753                        ),
10754                    )
10755                })?;
10756            let point = curve.points.get(point_index).ok_or_else(|| {
10757                boundary_error(
10758                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
10759                    format!("reactive capability point index {point_index} is out of range"),
10760                )
10761            })?;
10762            *require_output(output, "output")? =
10763                string_property_view(&point.properties, property_index)?;
10764            Ok(true)
10765        })
10766    }
10767}
10768
10769#[unsafe(no_mangle)]
10770pub unsafe extern "C" fn pio_detailed_connectivity_retain(
10771    details: *const PioDetailedConnectivity,
10772) -> *mut PioDetailedConnectivity {
10773    unsafe { PioDetailedConnectivity::retain_raw(details) }
10774}
10775
10776#[unsafe(no_mangle)]
10777pub unsafe extern "C" fn pio_detailed_connectivity_release(details: *mut PioDetailedConnectivity) {
10778    unsafe { PioDetailedConnectivity::release_raw(details) };
10779}
10780
10781#[unsafe(no_mangle)]
10782pub unsafe extern "C" fn pio_balanced_network_bus_count(
10783    network: *const PioBalancedNetwork,
10784) -> usize {
10785    unsafe { PioBalancedNetwork::get(network) }
10786        .and_then(BalancedNetworkInner::network)
10787        .map_or(0, |network| network.buses().len())
10788}
10789
10790#[unsafe(no_mangle)]
10791pub unsafe extern "C" fn pio_balanced_network_branch_count(
10792    network: *const PioBalancedNetwork,
10793) -> usize {
10794    unsafe { PioBalancedNetwork::get(network) }
10795        .and_then(BalancedNetworkInner::network)
10796        .map_or(0, |network| network.branches().len())
10797}
10798
10799#[unsafe(no_mangle)]
10800pub unsafe extern "C" fn pio_balanced_network_load_count(
10801    network: *const PioBalancedNetwork,
10802) -> usize {
10803    unsafe { PioBalancedNetwork::get(network) }
10804        .and_then(BalancedNetworkInner::network)
10805        .map_or(0, |network| network.loads().len())
10806}
10807
10808#[unsafe(no_mangle)]
10809pub unsafe extern "C" fn pio_balanced_network_shunt_count(
10810    network: *const PioBalancedNetwork,
10811) -> usize {
10812    unsafe { PioBalancedNetwork::get(network) }
10813        .and_then(BalancedNetworkInner::network)
10814        .map_or(0, |network| network.shunts().len())
10815}
10816
10817#[unsafe(no_mangle)]
10818pub unsafe extern "C" fn pio_balanced_network_static_var_compensator_count(
10819    network: *const PioBalancedNetwork,
10820) -> usize {
10821    unsafe { PioBalancedNetwork::get(network) }
10822        .and_then(BalancedNetworkInner::network)
10823        .map_or(0, |network| network.static_var_compensators().len())
10824}
10825
10826#[unsafe(no_mangle)]
10827pub unsafe extern "C" fn pio_balanced_network_generator_count(
10828    network: *const PioBalancedNetwork,
10829) -> usize {
10830    unsafe { PioBalancedNetwork::get(network) }
10831        .and_then(BalancedNetworkInner::network)
10832        .map_or(0, |network| network.generators().len())
10833}
10834
10835#[unsafe(no_mangle)]
10836pub unsafe extern "C" fn pio_balanced_network_storage_count(
10837    network: *const PioBalancedNetwork,
10838) -> usize {
10839    unsafe { PioBalancedNetwork::get(network) }
10840        .and_then(BalancedNetworkInner::network)
10841        .map_or(0, |network| network.storage().len())
10842}
10843
10844#[unsafe(no_mangle)]
10845pub unsafe extern "C" fn pio_balanced_network_switch_count(
10846    network: *const PioBalancedNetwork,
10847) -> usize {
10848    unsafe { PioBalancedNetwork::get(network) }
10849        .and_then(BalancedNetworkInner::network)
10850        .map_or(0, |network| network.switches().len())
10851}
10852
10853#[unsafe(no_mangle)]
10854pub unsafe extern "C" fn pio_balanced_network_hvdc_count(
10855    network: *const PioBalancedNetwork,
10856) -> usize {
10857    unsafe { PioBalancedNetwork::get(network) }
10858        .and_then(BalancedNetworkInner::network)
10859        .map_or(0, |network| network.hvdc().len())
10860}
10861
10862#[unsafe(no_mangle)]
10863pub unsafe extern "C" fn pio_balanced_network_three_winding_transformer_count(
10864    network: *const PioBalancedNetwork,
10865) -> usize {
10866    unsafe { PioBalancedNetwork::get(network) }
10867        .and_then(BalancedNetworkInner::network)
10868        .map_or(0, |network| network.transformers_3w().len())
10869}
10870
10871#[unsafe(no_mangle)]
10872pub unsafe extern "C" fn pio_balanced_network_area_count(
10873    network: *const PioBalancedNetwork,
10874) -> usize {
10875    unsafe { PioBalancedNetwork::get(network) }
10876        .and_then(BalancedNetworkInner::network)
10877        .map_or(0, |network| network.areas().len())
10878}
10879
10880unsafe fn require_balanced_network<'a>(
10881    network: *const PioBalancedNetwork,
10882) -> Result<&'a BalancedNetwork, *mut PioError> {
10883    unsafe { PioBalancedNetwork::get(network) }
10884        .and_then(BalancedNetworkInner::network)
10885        .ok_or_else(|| {
10886            boundary_error(
10887                &codes::BIND_CAPI_NULL_HANDLE,
10888                "PioBalancedNetwork must not be NULL",
10889            )
10890        })
10891}
10892
10893unsafe fn require_output<'a, T>(
10894    output: *mut T,
10895    argument: &str,
10896) -> Result<&'a mut T, *mut PioError> {
10897    unsafe { output.as_mut() }.ok_or_else(|| {
10898        boundary_error(
10899            &codes::BIND_CAPI_NULL_ARGUMENT,
10900            format!("{argument} must not be NULL"),
10901        )
10902    })
10903}
10904
10905unsafe fn require_detailed_connectivity<'a>(
10906    details: *const PioDetailedConnectivity,
10907) -> Result<&'a powerio_tx::DetailedConnectivity, *mut PioError> {
10908    unsafe { PioDetailedConnectivity::get(details) }
10909        .and_then(DetailedConnectivityInner::details)
10910        .ok_or_else(|| {
10911            boundary_error(
10912                &codes::BIND_CAPI_NULL_HANDLE,
10913                "PioDetailedConnectivity must not be NULL",
10914            )
10915        })
10916}
10917
10918fn optional_string_view(value: Option<&str>) -> (PioStringView, bool) {
10919    value.map_or((PioStringView::EMPTY, false), |value| {
10920        (PioStringView::new(value), true)
10921    })
10922}
10923
10924fn balanced_coords_kind_name(kind: powerio_tx::CoordsKind) -> &'static str {
10925    match kind {
10926        powerio_tx::CoordsKind::Source => "source",
10927        powerio_tx::CoordsKind::Synthetic => "synthetic",
10928        powerio_tx::CoordsKind::Manual => "manual",
10929        powerio_tx::CoordsKind::Derived => "derived",
10930        _ => "unknown",
10931    }
10932}
10933
10934fn balanced_location_view(location: &powerio_tx::Location) -> PioBalancedLocationView {
10935    let (kind, has_kind) = location.kind.map_or((PioStringView::EMPTY, false), |kind| {
10936        (PioStringView::new(balanced_coords_kind_name(kind)), true)
10937    });
10938    PioBalancedLocationView {
10939        x: location.x,
10940        y: location.y,
10941        kind,
10942        has_kind,
10943    }
10944}
10945
10946fn empty_balanced_location_view() -> PioBalancedLocationView {
10947    PioBalancedLocationView {
10948        x: 0.0,
10949        y: 0.0,
10950        kind: PioStringView::EMPTY,
10951        has_kind: false,
10952    }
10953}
10954
10955fn balanced_geo_view(geo: Option<&powerio_tx::GeoMeta>) -> PioBalancedGeoView {
10956    let Some(geo) = geo else {
10957        return PioBalancedGeoView {
10958            has_geo: false,
10959            space: PioStringView::EMPTY,
10960            crs: PioStringView::EMPTY,
10961            has_crs: false,
10962            kind: PioStringView::EMPTY,
10963            has_kind: false,
10964            has_canvas: false,
10965            canvas_width: 0.0,
10966            has_canvas_width: false,
10967            canvas_height: 0.0,
10968            has_canvas_height: false,
10969            canvas_units: PioStringView::EMPTY,
10970            has_canvas_units: false,
10971        };
10972    };
10973    let (space, crs, has_crs, canvas) = match &geo.space {
10974        powerio_tx::CoordinateSpace::Geographic { crs } => (
10975            "geographic",
10976            crs.as_deref()
10977                .map_or(PioStringView::EMPTY, PioStringView::new),
10978            crs.is_some(),
10979            None,
10980        ),
10981        powerio_tx::CoordinateSpace::Projected { crs } => (
10982            "projected",
10983            crs.as_deref()
10984                .map_or(PioStringView::EMPTY, PioStringView::new),
10985            crs.is_some(),
10986            None,
10987        ),
10988        powerio_tx::CoordinateSpace::Diagram { canvas } => {
10989            ("diagram", PioStringView::EMPTY, false, canvas.as_ref())
10990        }
10991        powerio_tx::CoordinateSpace::Unknown => ("unknown", PioStringView::EMPTY, false, None),
10992        _ => ("unknown", PioStringView::EMPTY, false, None),
10993    };
10994    let (kind, has_kind) = geo.kind.map_or((PioStringView::EMPTY, false), |kind| {
10995        (PioStringView::new(balanced_coords_kind_name(kind)), true)
10996    });
10997    PioBalancedGeoView {
10998        has_geo: true,
10999        space: PioStringView::new(space),
11000        crs,
11001        has_crs,
11002        kind,
11003        has_kind,
11004        has_canvas: canvas.is_some(),
11005        canvas_width: canvas.and_then(|canvas| canvas.width).unwrap_or(0.0),
11006        has_canvas_width: canvas.is_some_and(|canvas| canvas.width.is_some()),
11007        canvas_height: canvas.and_then(|canvas| canvas.height).unwrap_or(0.0),
11008        has_canvas_height: canvas.is_some_and(|canvas| canvas.height.is_some()),
11009        canvas_units: canvas
11010            .and_then(|canvas| canvas.units.as_deref())
11011            .map_or(PioStringView::EMPTY, PioStringView::new),
11012        has_canvas_units: canvas.is_some_and(|canvas| canvas.units.is_some()),
11013    }
11014}
11015
11016fn load_voltage_model_view(load: &powerio_tx::Load) -> PioBalancedLoadVoltageModelView {
11017    let mut view = PioBalancedLoadVoltageModelView {
11018        kind: PioStringView::new("constant_power"),
11019        p_constant_power_mw: load.p,
11020        q_constant_power_mvar: load.q,
11021        p_constant_current_mw: 0.0,
11022        q_constant_current_mvar: 0.0,
11023        p_constant_impedance_mw: 0.0,
11024        q_constant_impedance_mvar: 0.0,
11025        exponential_p_mw: 0.0,
11026        exponential_q_mvar: 0.0,
11027        gamma_p: 0.0,
11028        gamma_q: 0.0,
11029        nominal_voltage_pu: 0.0,
11030        has_nominal_voltage: false,
11031        load_type: 0,
11032        has_load_type: false,
11033        scaling: 0.0,
11034        has_scaling: false,
11035    };
11036    match &load.voltage_model {
11037        None | Some(powerio_tx::LoadVoltageModel::ConstantPower) => {}
11038        Some(powerio_tx::LoadVoltageModel::Zip {
11039            p_constant_power,
11040            q_constant_power,
11041            p_constant_current,
11042            q_constant_current,
11043            p_constant_impedance,
11044            q_constant_impedance,
11045            v_nom,
11046            load_type,
11047            scaling,
11048        }) => {
11049            view.kind = PioStringView::new("zip");
11050            view.p_constant_power_mw = *p_constant_power;
11051            view.q_constant_power_mvar = *q_constant_power;
11052            view.p_constant_current_mw = *p_constant_current;
11053            view.q_constant_current_mvar = *q_constant_current;
11054            view.p_constant_impedance_mw = *p_constant_impedance;
11055            view.q_constant_impedance_mvar = *q_constant_impedance;
11056            if let Some(value) = v_nom {
11057                view.nominal_voltage_pu = *value;
11058                view.has_nominal_voltage = true;
11059            }
11060            if let Some(value) = load_type {
11061                view.load_type = *value;
11062                view.has_load_type = true;
11063            }
11064            if let Some(value) = scaling {
11065                view.scaling = *value;
11066                view.has_scaling = true;
11067            }
11068        }
11069        Some(powerio_tx::LoadVoltageModel::Exponential {
11070            p,
11071            q,
11072            v_nom,
11073            gamma_p,
11074            gamma_q,
11075        }) => {
11076            view.kind = PioStringView::new("exponential");
11077            view.p_constant_power_mw = 0.0;
11078            view.q_constant_power_mvar = 0.0;
11079            view.exponential_p_mw = *p;
11080            view.exponential_q_mvar = *q;
11081            view.gamma_p = *gamma_p;
11082            view.gamma_q = *gamma_q;
11083            if let Some(value) = v_nom {
11084                view.nominal_voltage_pu = *value;
11085                view.has_nominal_voltage = true;
11086            }
11087        }
11088        Some(_) => view.kind = PioStringView::new("unknown"),
11089    }
11090    view
11091}
11092
11093fn switched_shunt_mode_name(mode: powerio_tx::SwitchedShuntMode) -> &'static str {
11094    match mode {
11095        powerio_tx::SwitchedShuntMode::Locked => "locked",
11096        powerio_tx::SwitchedShuntMode::Continuous => "continuous",
11097        powerio_tx::SwitchedShuntMode::Discrete => "discrete",
11098        _ => "unknown",
11099    }
11100}
11101
11102const GENERATOR_CAPABILITY_NAMES: [&str; 11] = [
11103    "pc1", "pc2", "qc1min", "qc1max", "qc2min", "qc2max", "ramp_agc", "ramp_10", "ramp_30",
11104    "ramp_q", "apf",
11105];
11106
11107fn static_var_compensator_regulation_mode_name(
11108    mode: powerio_tx::StaticVarCompensatorRegulationMode,
11109) -> &'static str {
11110    match mode {
11111        powerio_tx::StaticVarCompensatorRegulationMode::Voltage => "voltage",
11112        powerio_tx::StaticVarCompensatorRegulationMode::ReactivePower => "reactive_power",
11113        _ => "unknown",
11114    }
11115}
11116
11117fn hvdc_converter_kind_name(kind: powerio_tx::HvdcConverterKind) -> &'static str {
11118    match kind {
11119        powerio_tx::HvdcConverterKind::Vsc => "vsc",
11120        powerio_tx::HvdcConverterKind::Lcc => "lcc",
11121        _ => "unknown",
11122    }
11123}
11124
11125fn hvdc_converters_mode_name(mode: powerio_tx::HvdcConvertersMode) -> &'static str {
11126    match mode {
11127        powerio_tx::HvdcConvertersMode::Side1RectifierSide2Inverter => {
11128            "side1_rectifier_side2_inverter"
11129        }
11130        powerio_tx::HvdcConvertersMode::Side1InverterSide2Rectifier => {
11131            "side1_inverter_side2_rectifier"
11132        }
11133        _ => "unknown",
11134    }
11135}
11136
11137fn hvdc_converter_view(
11138    converter: Option<&powerio_tx::HvdcConverter>,
11139) -> (PioBalancedHvdcConverterView, bool) {
11140    let empty_terminal = terminal_reference_view(None).0;
11141    converter.map_or(
11142        (
11143            PioBalancedHvdcConverterView {
11144                component: empty_component_id_view(),
11145                kind: PioStringView::EMPTY,
11146                loss_factor_percent: 0.0,
11147                voltage_regulator_on: false,
11148                has_voltage_regulator_on: false,
11149                voltage_setpoint_kv: 0.0,
11150                has_voltage_setpoint: false,
11151                reactive_power_setpoint_mvar: 0.0,
11152                has_reactive_power_setpoint: false,
11153                power_factor: 0.0,
11154                has_power_factor: false,
11155                regulating_terminal: empty_terminal,
11156                has_regulating_terminal: false,
11157            },
11158            false,
11159        ),
11160        |converter| {
11161            let (regulating_terminal, has_regulating_terminal) =
11162                terminal_reference_view(converter.regulating_terminal.as_ref());
11163            (
11164                PioBalancedHvdcConverterView {
11165                    component: component_id_view(&converter.component),
11166                    kind: PioStringView::new(hvdc_converter_kind_name(converter.kind)),
11167                    loss_factor_percent: converter.loss_factor_percent,
11168                    voltage_regulator_on: converter.voltage_regulator_on.unwrap_or(false),
11169                    has_voltage_regulator_on: converter.voltage_regulator_on.is_some(),
11170                    voltage_setpoint_kv: converter.voltage_setpoint_kv.unwrap_or(0.0),
11171                    has_voltage_setpoint: converter.voltage_setpoint_kv.is_some(),
11172                    reactive_power_setpoint_mvar: converter
11173                        .reactive_power_setpoint_mvar
11174                        .unwrap_or(0.0),
11175                    has_reactive_power_setpoint: converter.reactive_power_setpoint_mvar.is_some(),
11176                    power_factor: converter.power_factor.unwrap_or(0.0),
11177                    has_power_factor: converter.power_factor.is_some(),
11178                    regulating_terminal,
11179                    has_regulating_terminal,
11180                },
11181                true,
11182            )
11183        },
11184    )
11185}
11186
11187/// Read one bus by zero based table position.
11188#[unsafe(no_mangle)]
11189pub unsafe extern "C" fn pio_balanced_network_bus_at(
11190    network: *const PioBalancedNetwork,
11191    index: usize,
11192    output: *mut PioBalancedBusView,
11193    error: *mut *mut PioError,
11194) -> bool {
11195    unsafe {
11196        entry(error, false, || {
11197            let network = require_balanced_network(network)?;
11198            let bus = network.buses().get(index).ok_or_else(|| {
11199                boundary_error(
11200                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11201                    format!("bus index {index} is out of range"),
11202                )
11203            })?;
11204            let output = require_output(output, "output")?;
11205            let (component_id, has_component_id) = optional_string_view(bus.uid.as_deref());
11206            let (name, has_name) = optional_string_view(bus.name.as_deref());
11207            let (location, has_location) = bus
11208                .location
11209                .as_ref()
11210                .map_or((empty_balanced_location_view(), false), |location| {
11211                    (balanced_location_view(location), true)
11212                });
11213            *output = PioBalancedBusView {
11214                component_id,
11215                has_component_id,
11216                id: bus.id.0,
11217                bus_type: PioStringView::new(bus.kind.as_str()),
11218                vm_pu: bus.vm,
11219                va_degrees: bus.va,
11220                base_kv: bus.base_kv,
11221                vmax_pu: bus.vmax,
11222                vmin_pu: bus.vmin,
11223                has_emergency_voltage_limits: bus.evhi.is_some() || bus.evlo.is_some(),
11224                emergency_vmax_pu: bus.evhi.unwrap_or(bus.vmax),
11225                emergency_vmin_pu: bus.evlo.unwrap_or(bus.vmin),
11226                area: bus.area,
11227                zone: bus.zone,
11228                name,
11229                has_name,
11230                location,
11231                has_location,
11232            };
11233            Ok(true)
11234        })
11235    }
11236}
11237
11238/// Read one load by zero based table position.
11239#[unsafe(no_mangle)]
11240pub unsafe extern "C" fn pio_balanced_network_load_at(
11241    network: *const PioBalancedNetwork,
11242    index: usize,
11243    output: *mut PioBalancedLoadView,
11244    error: *mut *mut PioError,
11245) -> bool {
11246    unsafe {
11247        entry(error, false, || {
11248            let network = require_balanced_network(network)?;
11249            let load = network.loads().get(index).ok_or_else(|| {
11250                boundary_error(
11251                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11252                    format!("load index {index} is out of range"),
11253                )
11254            })?;
11255            let output = require_output(output, "output")?;
11256            let (component_id, has_component_id) = optional_string_view(load.uid.as_deref());
11257            *output = PioBalancedLoadView {
11258                component_id,
11259                has_component_id,
11260                bus_id: load.bus.0,
11261                p_mw: load.p,
11262                q_mvar: load.q,
11263                in_service: load.in_service,
11264                voltage_model: load_voltage_model_view(load),
11265            };
11266            Ok(true)
11267        })
11268    }
11269}
11270
11271/// Read one shunt by zero based table position.
11272#[unsafe(no_mangle)]
11273pub unsafe extern "C" fn pio_balanced_network_shunt_at(
11274    network: *const PioBalancedNetwork,
11275    index: usize,
11276    output: *mut PioBalancedShuntView,
11277    error: *mut *mut PioError,
11278) -> bool {
11279    unsafe {
11280        entry(error, false, || {
11281            let network = require_balanced_network(network)?;
11282            let shunt = network.shunts().get(index).ok_or_else(|| {
11283                boundary_error(
11284                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11285                    format!("shunt index {index} is out of range"),
11286                )
11287            })?;
11288            let output = require_output(output, "output")?;
11289            let (component_id, has_component_id) = optional_string_view(shunt.uid.as_deref());
11290            let control = shunt.control.as_ref();
11291            *output = PioBalancedShuntView {
11292                component_id,
11293                has_component_id,
11294                bus_id: shunt.bus.0,
11295                conductance_mw: shunt.g,
11296                susceptance_mvar: shunt.b,
11297                in_service: shunt.in_service,
11298                section_count: shunt.section_count.unwrap_or(0),
11299                has_section_count: shunt.section_count.is_some(),
11300                has_control: control.is_some(),
11301                control_mode: control.map_or(PioStringView::EMPTY, |control| {
11302                    PioStringView::new(switched_shunt_mode_name(control.mode))
11303                }),
11304                control_vmax_pu: control.map_or(0.0, |control| control.vhigh),
11305                control_vmin_pu: control.map_or(0.0, |control| control.vlow),
11306                control_bus_id: control
11307                    .and_then(|control| control.control_bus)
11308                    .map_or(0, |bus| bus.0),
11309                has_control_bus: control.is_some_and(|control| control.control_bus.is_some()),
11310                control_reactive_range_percent: control.map_or(0.0, |control| control.rmpct),
11311                control_block_count: control.map_or(0, |control| control.blocks.len()),
11312            };
11313            Ok(true)
11314        })
11315    }
11316}
11317
11318/// Read one switched shunt block by zero based position.
11319#[unsafe(no_mangle)]
11320pub unsafe extern "C" fn pio_balanced_network_shunt_block_at(
11321    network: *const PioBalancedNetwork,
11322    shunt_index: usize,
11323    block_index: usize,
11324    output: *mut PioShuntBlockView,
11325    error: *mut *mut PioError,
11326) -> bool {
11327    unsafe {
11328        entry(error, false, || {
11329            let network = require_balanced_network(network)?;
11330            let shunt = network.shunts().get(shunt_index).ok_or_else(|| {
11331                boundary_error(
11332                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11333                    format!("shunt index {shunt_index} is out of range"),
11334                )
11335            })?;
11336            let block = shunt
11337                .control
11338                .as_ref()
11339                .and_then(|control| control.blocks.get(block_index))
11340                .ok_or_else(|| {
11341                    boundary_error(
11342                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11343                        format!(
11344                            "shunt block index {block_index} is out of range for shunt {shunt_index}"
11345                        ),
11346                    )
11347                })?;
11348            *require_output(output, "output")? = PioShuntBlockView {
11349                steps: block.steps,
11350                conductance_mw: block.g,
11351                susceptance_mvar: block.b,
11352            };
11353            Ok(true)
11354        })
11355    }
11356}
11357
11358/// Read one static VAR compensator by zero based table position.
11359#[unsafe(no_mangle)]
11360pub unsafe extern "C" fn pio_balanced_network_static_var_compensator_at(
11361    network: *const PioBalancedNetwork,
11362    index: usize,
11363    output: *mut PioBalancedStaticVarCompensatorView,
11364    error: *mut *mut PioError,
11365) -> bool {
11366    unsafe {
11367        entry(error, false, || {
11368            let network = require_balanced_network(network)?;
11369            let svc = network
11370                .static_var_compensators()
11371                .get(index)
11372                .ok_or_else(|| {
11373                    boundary_error(
11374                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11375                        format!("static VAR compensator index {index} is out of range"),
11376                    )
11377                })?;
11378            let (component_id, has_component_id) = optional_string_view(svc.uid.as_deref());
11379            let (regulating_terminal, has_regulating_terminal) =
11380                terminal_reference_view(svc.regulating_terminal.as_ref());
11381            *require_output(output, "output")? = PioBalancedStaticVarCompensatorView {
11382                component_id,
11383                has_component_id,
11384                bus_id: svc.bus.0,
11385                minimum_susceptance_siemens: svc.b_min_siemens,
11386                maximum_susceptance_siemens: svc.b_max_siemens,
11387                voltage_setpoint_kv: svc.voltage_setpoint_kv,
11388                reactive_power_setpoint_mvar: svc.reactive_power_setpoint_mvar,
11389                regulation_mode: PioStringView::new(static_var_compensator_regulation_mode_name(
11390                    svc.regulation_mode,
11391                )),
11392                regulating: svc.regulating,
11393                regulating_terminal,
11394                has_regulating_terminal,
11395                active_power_mw: svc.p,
11396                reactive_power_mvar: svc.q,
11397                in_service: svc.in_service,
11398            };
11399            Ok(true)
11400        })
11401    }
11402}
11403
11404/// Read one branch by zero based table position.
11405#[unsafe(no_mangle)]
11406pub unsafe extern "C" fn pio_balanced_network_branch_at(
11407    network: *const PioBalancedNetwork,
11408    index: usize,
11409    output: *mut PioBalancedBranchView,
11410    error: *mut *mut PioError,
11411) -> bool {
11412    unsafe {
11413        entry(error, false, || {
11414            let network = require_balanced_network(network)?;
11415            let branch = network.branches().get(index).ok_or_else(|| {
11416                boundary_error(
11417                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11418                    format!("branch index {index} is out of range"),
11419                )
11420            })?;
11421            let output = require_output(output, "output")?;
11422            let (component_id, has_component_id) = optional_string_view(branch.uid.as_deref());
11423            let (name, has_name) = optional_string_view(branch.name.as_deref());
11424            let charging = branch.calc_terminal_charging();
11425            let current = branch.current_ratings;
11426            let (control, has_control) = transformer_control_view(branch.control.as_ref());
11427            *output = PioBalancedBranchView {
11428                component_id,
11429                has_component_id,
11430                name,
11431                has_name,
11432                from_bus_id: branch.from.0,
11433                to_bus_id: branch.to.0,
11434                resistance_pu: branch.r,
11435                reactance_pu: branch.x,
11436                total_charging_susceptance_pu: branch.b,
11437                terminal_charging_is_explicit: branch.charging.is_some(),
11438                from_conductance_pu: charging.g_fr,
11439                from_susceptance_pu: charging.b_fr,
11440                to_conductance_pu: charging.g_to,
11441                to_susceptance_pu: charging.b_to,
11442                rate_a_mva: branch.rate_a,
11443                rate_b_mva: branch.rate_b,
11444                rate_c_mva: branch.rate_c,
11445                additional_rating_count: branch.rating_sets.len(),
11446                has_current_ratings: current.is_some(),
11447                current_rating_a: current.map_or(0.0, |rating| rating.c_rating_a),
11448                current_rating_b: current.map_or(0.0, |rating| rating.c_rating_b),
11449                current_rating_c: current.map_or(0.0, |rating| rating.c_rating_c),
11450                tap_ratio: branch.tap,
11451                effective_tap_ratio: branch.calc_effective_tap(),
11452                phase_shift_degrees: branch.shift,
11453                in_service: branch.in_service,
11454                angle_min_degrees: branch.angmin,
11455                angle_max_degrees: branch.angmax,
11456                control,
11457                has_control,
11458                route_point_count: branch.route.as_ref().map_or(0, Vec::len),
11459                has_route: branch.route.is_some(),
11460            };
11461            Ok(true)
11462        })
11463    }
11464}
11465
11466/// Read one point from an explicitly stored balanced branch route.
11467#[unsafe(no_mangle)]
11468pub unsafe extern "C" fn pio_balanced_network_branch_route_point_at(
11469    network: *const PioBalancedNetwork,
11470    branch_index: usize,
11471    point_index: usize,
11472    output: *mut PioBalancedLocationView,
11473    error: *mut *mut PioError,
11474) -> bool {
11475    unsafe {
11476        entry(error, false, || {
11477            let network = require_balanced_network(network)?;
11478            let branch = network.branches().get(branch_index).ok_or_else(|| {
11479                boundary_error(
11480                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11481                    format!("branch index {branch_index} is out of range"),
11482                )
11483            })?;
11484            let route = branch.route.as_ref().ok_or_else(|| {
11485                boundary_error(
11486                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11487                    format!("balanced branch {branch_index} has no route"),
11488                )
11489            })?;
11490            let point = route.get(point_index).ok_or_else(|| {
11491                boundary_error(
11492                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11493                    format!("branch route point index {point_index} is out of range"),
11494                )
11495            })?;
11496            *require_output(output, "output")? = balanced_location_view(point);
11497            Ok(true)
11498        })
11499    }
11500}
11501
11502/// Read one additional named branch MVA rating.
11503#[unsafe(no_mangle)]
11504pub unsafe extern "C" fn pio_balanced_network_branch_rating_at(
11505    network: *const PioBalancedNetwork,
11506    branch_index: usize,
11507    rating_index: usize,
11508    output: *mut PioBranchRatingView,
11509    error: *mut *mut PioError,
11510) -> bool {
11511    unsafe {
11512        entry(error, false, || {
11513            let network = require_balanced_network(network)?;
11514            let branch = network.branches().get(branch_index).ok_or_else(|| {
11515                boundary_error(
11516                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11517                    format!("branch index {branch_index} is out of range"),
11518                )
11519            })?;
11520            let rating = branch.rating_sets.get(rating_index).ok_or_else(|| {
11521                boundary_error(
11522                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11523                    format!(
11524                        "rating index {rating_index} is out of range for branch {branch_index}"
11525                    ),
11526                )
11527            })?;
11528            *require_output(output, "output")? = PioBranchRatingView {
11529                name: PioStringView::new(&rating.name),
11530                rate_mva: rating.rate_mva,
11531            };
11532            Ok(true)
11533        })
11534    }
11535}
11536
11537fn active_power_control_view(
11538    value: Option<&powerio_tx::ActivePowerControl>,
11539) -> (PioActivePowerControlView, bool) {
11540    let Some(value) = value else {
11541        return (
11542            PioActivePowerControlView {
11543                participate: false,
11544                droop_percent: 0.0,
11545                has_droop_percent: false,
11546                participation_factor: 0.0,
11547                has_participation_factor: false,
11548                minimum_target_active_power_mw: 0.0,
11549                has_minimum_target_active_power: false,
11550                maximum_target_active_power_mw: 0.0,
11551                has_maximum_target_active_power: false,
11552            },
11553            false,
11554        );
11555    };
11556    (
11557        PioActivePowerControlView {
11558            participate: value.participate,
11559            droop_percent: value.droop_percent.unwrap_or(0.0),
11560            has_droop_percent: value.droop_percent.is_some(),
11561            participation_factor: value.participation_factor.unwrap_or(0.0),
11562            has_participation_factor: value.participation_factor.is_some(),
11563            minimum_target_active_power_mw: value.minimum_target_active_power_mw.unwrap_or(0.0),
11564            has_minimum_target_active_power: value.minimum_target_active_power_mw.is_some(),
11565            maximum_target_active_power_mw: value.maximum_target_active_power_mw.unwrap_or(0.0),
11566            has_maximum_target_active_power: value.maximum_target_active_power_mw.is_some(),
11567        },
11568        true,
11569    )
11570}
11571
11572fn generator_energy_source_name(value: powerio_tx::GeneratorEnergySource) -> &'static str {
11573    match value {
11574        powerio_tx::GeneratorEnergySource::Hydro => "hydro",
11575        powerio_tx::GeneratorEnergySource::Nuclear => "nuclear",
11576        powerio_tx::GeneratorEnergySource::Wind => "wind",
11577        powerio_tx::GeneratorEnergySource::Thermal => "thermal",
11578        powerio_tx::GeneratorEnergySource::Solar => "solar",
11579        powerio_tx::GeneratorEnergySource::Other => "other",
11580        _ => "unknown",
11581    }
11582}
11583
11584/// Read one generator by zero based table position.
11585#[unsafe(no_mangle)]
11586pub unsafe extern "C" fn pio_balanced_network_generator_at(
11587    network: *const PioBalancedNetwork,
11588    index: usize,
11589    output: *mut PioBalancedGeneratorView,
11590    error: *mut *mut PioError,
11591) -> bool {
11592    unsafe {
11593        entry(error, false, || {
11594            let network = require_balanced_network(network)?;
11595            let generator = network.generators().get(index).ok_or_else(|| {
11596                boundary_error(
11597                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11598                    format!("generator index {index} is out of range"),
11599                )
11600            })?;
11601            let output = require_output(output, "output")?;
11602            let (component_id, has_component_id) = optional_string_view(generator.uid.as_deref());
11603            let cost = generator.cost.as_ref();
11604            let (active_power_control, has_active_power_control) =
11605                active_power_control_view(generator.active_power_control.as_ref());
11606            let (regulating_terminal, has_regulating_terminal) =
11607                terminal_reference_view(generator.regulating_terminal.as_ref());
11608            *output = PioBalancedGeneratorView {
11609                component_id,
11610                has_component_id,
11611                bus_id: generator.bus.0,
11612                energy_source: PioStringView::new(generator_energy_source_name(
11613                    generator.energy_source,
11614                )),
11615                active_power_mw: generator.pg,
11616                reactive_power_mvar: generator.qg,
11617                active_power_max_mw: generator.pmax,
11618                active_power_min_mw: generator.pmin,
11619                reactive_power_max_mvar: generator.qmax,
11620                reactive_power_min_mvar: generator.qmin,
11621                voltage_setpoint_pu: generator.vg,
11622                machine_base_mva: generator.mbase,
11623                in_service: generator.in_service,
11624                has_cost: cost.is_some(),
11625                cost: cost.map_or(
11626                    PioGeneratorCostView {
11627                        model: 0,
11628                        startup: 0.0,
11629                        shutdown: 0.0,
11630                        ncost: 0,
11631                        coefficients: PioF64View::EMPTY,
11632                    },
11633                    |cost| PioGeneratorCostView {
11634                        model: cost.model,
11635                        startup: cost.startup,
11636                        shutdown: cost.shutdown,
11637                        ncost: cost.ncost,
11638                        coefficients: PioF64View::new(&cost.coeffs),
11639                    },
11640                ),
11641                regulated_bus_id: generator.regulated_bus.map_or(0, |bus| bus.0),
11642                has_regulated_bus: generator.regulated_bus.is_some(),
11643                capability_count: GENERATOR_CAPABILITY_NAMES.len(),
11644                active_power_control,
11645                has_active_power_control,
11646                voltage_regulation_on: generator.voltage_regulation_on,
11647                regulating_terminal,
11648                has_regulating_terminal,
11649            };
11650            Ok(true)
11651        })
11652    }
11653}
11654
11655/// Read one named generator capability or ramp field.
11656#[unsafe(no_mangle)]
11657pub unsafe extern "C" fn pio_balanced_network_generator_capability_at(
11658    network: *const PioBalancedNetwork,
11659    generator_index: usize,
11660    capability_index: usize,
11661    output: *mut PioGeneratorCapabilityView,
11662    error: *mut *mut PioError,
11663) -> bool {
11664    unsafe {
11665        entry(error, false, || {
11666            let network = require_balanced_network(network)?;
11667            let generator = network.generators().get(generator_index).ok_or_else(|| {
11668                boundary_error(
11669                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11670                    format!("generator index {generator_index} is out of range"),
11671                )
11672            })?;
11673            let name = GENERATOR_CAPABILITY_NAMES
11674                .get(capability_index)
11675                .ok_or_else(|| {
11676                    boundary_error(
11677                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11678                        format!("generator capability index {capability_index} is out of range"),
11679                    )
11680                })?;
11681            let value = generator.caps[capability_index];
11682            *require_output(output, "output")? = PioGeneratorCapabilityView {
11683                name: PioStringView::new(name),
11684                value: value.unwrap_or(0.0),
11685                has_value: value.is_some(),
11686            };
11687            Ok(true)
11688        })
11689    }
11690}
11691
11692/// Read one storage element by zero based table position.
11693#[unsafe(no_mangle)]
11694pub unsafe extern "C" fn pio_balanced_network_storage_at(
11695    network: *const PioBalancedNetwork,
11696    index: usize,
11697    output: *mut PioBalancedStorageView,
11698    error: *mut *mut PioError,
11699) -> bool {
11700    unsafe {
11701        entry(error, false, || {
11702            let network = require_balanced_network(network)?;
11703            let storage = network.storage().get(index).ok_or_else(|| {
11704                boundary_error(
11705                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11706                    format!("storage index {index} is out of range"),
11707                )
11708            })?;
11709            let output = require_output(output, "output")?;
11710            let (component_id, has_component_id) = optional_string_view(storage.uid.as_deref());
11711            let (active_power_control, has_active_power_control) =
11712                active_power_control_view(storage.active_power_control.as_ref());
11713            *output = PioBalancedStorageView {
11714                component_id,
11715                has_component_id,
11716                bus_id: storage.bus.0,
11717                active_power_mw: storage.ps,
11718                reactive_power_mvar: storage.qs,
11719                energy_mwh: storage.energy,
11720                energy_rating_mwh: storage.energy_rating,
11721                charge_rating_mw: storage.charge_rating,
11722                discharge_rating_mw: storage.discharge_rating,
11723                charge_efficiency: storage.charge_efficiency,
11724                discharge_efficiency: storage.discharge_efficiency,
11725                thermal_rating_mva: storage.thermal_rating,
11726                current_rating: storage.current_rating.unwrap_or(0.0),
11727                has_current_rating: storage.current_rating.is_some(),
11728                reactive_power_min_mvar: storage.qmin,
11729                reactive_power_max_mvar: storage.qmax,
11730                resistance_pu: storage.r,
11731                reactance_pu: storage.x,
11732                active_power_loss_mw: storage.p_loss,
11733                reactive_power_loss_mvar: storage.q_loss,
11734                in_service: storage.in_service,
11735                active_power_control,
11736                has_active_power_control,
11737            };
11738            Ok(true)
11739        })
11740    }
11741}
11742
11743/// Read one transmission switch by zero based table position.
11744#[unsafe(no_mangle)]
11745pub unsafe extern "C" fn pio_balanced_network_switch_at(
11746    network: *const PioBalancedNetwork,
11747    index: usize,
11748    output: *mut PioBalancedSwitchView,
11749    error: *mut *mut PioError,
11750) -> bool {
11751    unsafe {
11752        entry(error, false, || {
11753            let network = require_balanced_network(network)?;
11754            let switch = network.switches().get(index).ok_or_else(|| {
11755                boundary_error(
11756                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11757                    format!("switch index {index} is out of range"),
11758                )
11759            })?;
11760            let (component_id, has_component_id) = optional_string_view(switch.uid.as_deref());
11761            *require_output(output, "output")? = PioBalancedSwitchView {
11762                component_id,
11763                has_component_id,
11764                from_bus_id: switch.from.0,
11765                to_bus_id: switch.to.0,
11766                closed: switch.closed,
11767                thermal_rating_mva: switch.thermal_rating.unwrap_or(0.0),
11768                has_thermal_rating: switch.thermal_rating.is_some(),
11769                current_rating_a: switch.current_rating.unwrap_or(0.0),
11770                has_current_rating: switch.current_rating.is_some(),
11771                from_active_power_mw: switch.pf.unwrap_or(0.0),
11772                has_from_active_power: switch.pf.is_some(),
11773                from_reactive_power_mvar: switch.qf.unwrap_or(0.0),
11774                has_from_reactive_power: switch.qf.is_some(),
11775                to_active_power_mw: switch.pt.unwrap_or(0.0),
11776                has_to_active_power: switch.pt.is_some(),
11777                to_reactive_power_mvar: switch.qt.unwrap_or(0.0),
11778                has_to_reactive_power: switch.qt.is_some(),
11779            };
11780            Ok(true)
11781        })
11782    }
11783}
11784
11785/// Read one HVDC line by zero based table position.
11786#[unsafe(no_mangle)]
11787pub unsafe extern "C" fn pio_balanced_network_hvdc_at(
11788    network: *const PioBalancedNetwork,
11789    index: usize,
11790    output: *mut PioBalancedHvdcView,
11791    error: *mut *mut PioError,
11792) -> bool {
11793    unsafe {
11794        entry(error, false, || {
11795            let network = require_balanced_network(network)?;
11796            let hvdc = network.hvdc().get(index).ok_or_else(|| {
11797                boundary_error(
11798                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11799                    format!("HVDC index {index} is out of range"),
11800                )
11801            })?;
11802            let (component_id, has_component_id) = optional_string_view(hvdc.uid.as_deref());
11803            let (converter1, has_converter1) = hvdc_converter_view(hvdc.converter1.as_ref());
11804            let (converter2, has_converter2) = hvdc_converter_view(hvdc.converter2.as_ref());
11805            let (converters_mode, has_converters_mode) = hvdc
11806                .converters_mode
11807                .map_or((PioStringView::EMPTY, false), |mode| {
11808                    (PioStringView::new(hvdc_converters_mode_name(mode)), true)
11809                });
11810            let cost = hvdc.cost.as_ref();
11811            *require_output(output, "output")? = PioBalancedHvdcView {
11812                component_id,
11813                has_component_id,
11814                from_bus_id: hvdc.from.0,
11815                to_bus_id: hvdc.to.0,
11816                in_service: hvdc.in_service,
11817                from_active_power_mw: hvdc.pf,
11818                to_active_power_mw: hvdc.pt,
11819                from_reactive_power_mvar: hvdc.qf,
11820                to_reactive_power_mvar: hvdc.qt,
11821                from_voltage_pu: hvdc.vf,
11822                to_voltage_pu: hvdc.vt,
11823                minimum_active_power_mw: hvdc.pmin,
11824                maximum_active_power_mw: hvdc.pmax,
11825                minimum_from_reactive_power_mvar: hvdc.qminf,
11826                maximum_from_reactive_power_mvar: hvdc.qmaxf,
11827                minimum_to_reactive_power_mvar: hvdc.qmint,
11828                maximum_to_reactive_power_mvar: hvdc.qmaxt,
11829                constant_loss_mw: hvdc.loss0,
11830                proportional_loss: hvdc.loss1,
11831                resistance_ohm: hvdc.resistance_ohm.unwrap_or(0.0),
11832                has_resistance: hvdc.resistance_ohm.is_some(),
11833                nominal_voltage_kv: hvdc.nominal_voltage_kv.unwrap_or(0.0),
11834                has_nominal_voltage: hvdc.nominal_voltage_kv.is_some(),
11835                converters_mode,
11836                has_converters_mode,
11837                converter1,
11838                has_converter1,
11839                converter2,
11840                has_converter2,
11841                cost: cost.map_or(
11842                    PioGeneratorCostView {
11843                        model: 0,
11844                        startup: 0.0,
11845                        shutdown: 0.0,
11846                        ncost: 0,
11847                        coefficients: PioF64View::EMPTY,
11848                    },
11849                    |cost| PioGeneratorCostView {
11850                        model: cost.model,
11851                        startup: cost.startup,
11852                        shutdown: cost.shutdown,
11853                        ncost: cost.ncost,
11854                        coefficients: PioF64View::new(&cost.coeffs),
11855                    },
11856                ),
11857                has_cost: cost.is_some(),
11858            };
11859            Ok(true)
11860        })
11861    }
11862}
11863
11864/// Read one three winding transformer by zero based table position.
11865#[unsafe(no_mangle)]
11866pub unsafe extern "C" fn pio_balanced_network_three_winding_transformer_at(
11867    network: *const PioBalancedNetwork,
11868    index: usize,
11869    output: *mut PioBalancedThreeWindingTransformerView,
11870    error: *mut *mut PioError,
11871) -> bool {
11872    unsafe {
11873        entry(error, false, || {
11874            let network = require_balanced_network(network)?;
11875            let transformer = network.transformers_3w().get(index).ok_or_else(|| {
11876                boundary_error(
11877                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11878                    format!("three winding transformer index {index} is out of range"),
11879                )
11880            })?;
11881            let (component_id, has_component_id) = optional_string_view(transformer.uid.as_deref());
11882            let (name, has_name) = optional_string_view(transformer.name.as_deref());
11883            *require_output(output, "output")? = PioBalancedThreeWindingTransformerView {
11884                component_id,
11885                has_component_id,
11886                name,
11887                has_name,
11888                winding_count: transformer.windings.len(),
11889                impedance_count: transformer.z.len(),
11890                star_voltage_magnitude_pu: transformer.star_vm,
11891                star_voltage_angle_degrees: transformer.star_va,
11892                magnetizing_conductance_pu: transformer.mag_g,
11893                magnetizing_susceptance_pu: transformer.mag_b,
11894                in_service: transformer.in_service,
11895            };
11896            Ok(true)
11897        })
11898    }
11899}
11900
11901/// Read one winding of a three winding transformer.
11902#[unsafe(no_mangle)]
11903pub unsafe extern "C" fn pio_balanced_network_three_winding_transformer_winding_at(
11904    network: *const PioBalancedNetwork,
11905    transformer_index: usize,
11906    winding_index: usize,
11907    output: *mut PioThreeWindingTransformerWindingView,
11908    error: *mut *mut PioError,
11909) -> bool {
11910    unsafe {
11911        entry(error, false, || {
11912            let network = require_balanced_network(network)?;
11913            let transformer = network
11914                .transformers_3w()
11915                .get(transformer_index)
11916                .ok_or_else(|| {
11917                    boundary_error(
11918                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11919                        format!(
11920                            "three winding transformer index {transformer_index} is out of range"
11921                        ),
11922                    )
11923                })?;
11924            let winding = transformer.windings.get(winding_index).ok_or_else(|| {
11925                boundary_error(
11926                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11927                    format!("winding index {winding_index} is out of range"),
11928                )
11929            })?;
11930            let (control, has_control) = transformer_control_view(winding.control.as_ref());
11931            *require_output(output, "output")? = PioThreeWindingTransformerWindingView {
11932                bus_id: winding.bus.0,
11933                tap_ratio: winding.tap,
11934                phase_shift_degrees: winding.shift,
11935                nominal_voltage_kv: winding.nominal_kv,
11936                rating_a_mva: winding.rate_a,
11937                rating_b_mva: winding.rate_b,
11938                rating_c_mva: winding.rate_c,
11939                control,
11940                has_control,
11941            };
11942            Ok(true)
11943        })
11944    }
11945}
11946
11947/// Read one pairwise impedance of a three winding transformer.
11948#[unsafe(no_mangle)]
11949pub unsafe extern "C" fn pio_balanced_network_three_winding_transformer_impedance_at(
11950    network: *const PioBalancedNetwork,
11951    transformer_index: usize,
11952    impedance_index: usize,
11953    output: *mut PioThreeWindingTransformerImpedanceView,
11954    error: *mut *mut PioError,
11955) -> bool {
11956    unsafe {
11957        entry(error, false, || {
11958            let network = require_balanced_network(network)?;
11959            let transformer = network
11960                .transformers_3w()
11961                .get(transformer_index)
11962                .ok_or_else(|| {
11963                    boundary_error(
11964                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11965                        format!(
11966                            "three winding transformer index {transformer_index} is out of range"
11967                        ),
11968                    )
11969                })?;
11970            let impedance = transformer.z.get(impedance_index).ok_or_else(|| {
11971                boundary_error(
11972                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
11973                    format!("impedance index {impedance_index} is out of range"),
11974                )
11975            })?;
11976            *require_output(output, "output")? = PioThreeWindingTransformerImpedanceView {
11977                resistance_pu: impedance.r,
11978                reactance_pu: impedance.x,
11979                base_mva: impedance.base_mva,
11980            };
11981            Ok(true)
11982        })
11983    }
11984}
11985
11986/// Read one control area by zero based table position.
11987#[unsafe(no_mangle)]
11988pub unsafe extern "C" fn pio_balanced_network_area_at(
11989    network: *const PioBalancedNetwork,
11990    index: usize,
11991    output: *mut PioBalancedAreaView,
11992    error: *mut *mut PioError,
11993) -> bool {
11994    unsafe {
11995        entry(error, false, || {
11996            let network = require_balanced_network(network)?;
11997            let area = network.areas().get(index).ok_or_else(|| {
11998                boundary_error(
11999                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
12000                    format!("area index {index} is out of range"),
12001                )
12002            })?;
12003            let (name, has_name) = optional_string_view(area.name.as_deref());
12004            let (component_id, has_component_id) = optional_string_view(area.uid.as_deref());
12005            let (area_type, has_area_type) = optional_string_view(area.area_type.as_deref());
12006            *require_output(output, "output")? = PioBalancedAreaView {
12007                number: area.number,
12008                slack_bus_id: area.slack_bus.map_or(0, |bus| bus.0),
12009                has_slack_bus: area.slack_bus.is_some(),
12010                net_interchange_mw: area.net_interchange,
12011                tolerance_mw: area.tolerance,
12012                name,
12013                has_name,
12014                component_id,
12015                has_component_id,
12016                area_type,
12017                has_area_type,
12018            };
12019            Ok(true)
12020        })
12021    }
12022}
12023
12024#[unsafe(no_mangle)]
12025pub unsafe extern "C" fn pio_balanced_network_retain(
12026    network: *const PioBalancedNetwork,
12027) -> *mut PioBalancedNetwork {
12028    unsafe { PioBalancedNetwork::retain_raw(network) }
12029}
12030
12031#[unsafe(no_mangle)]
12032pub unsafe extern "C" fn pio_balanced_network_release(network: *mut PioBalancedNetwork) {
12033    unsafe { PioBalancedNetwork::release_raw(network) };
12034}
12035
12036unsafe fn require_multiconductor_network<'a>(
12037    network: *const PioMulticonductorNetwork,
12038) -> Result<&'a powerio_dist::MulticonductorNetwork, *mut PioError> {
12039    unsafe { PioMulticonductorNetwork::get(network) }
12040        .and_then(MulticonductorNetworkInner::network)
12041        .ok_or_else(|| {
12042            boundary_error(
12043                &codes::BIND_CAPI_NULL_HANDLE,
12044                "PioMulticonductorNetwork must not be NULL",
12045            )
12046        })
12047}
12048
12049fn optional_f64_view(value: Option<&[f64]>) -> (PioF64View, bool) {
12050    value.map_or((PioF64View::EMPTY, false), |value| {
12051        (PioF64View::new(value), true)
12052    })
12053}
12054
12055fn dist_coords_kind_name(kind: powerio_dist::DistCoordsKind) -> &'static str {
12056    match kind {
12057        powerio_dist::DistCoordsKind::Source => "source",
12058        powerio_dist::DistCoordsKind::Synthetic => "synthetic",
12059        powerio_dist::DistCoordsKind::Manual => "manual",
12060        powerio_dist::DistCoordsKind::Derived => "derived",
12061        _ => "unknown",
12062    }
12063}
12064
12065fn multiconductor_location_view(
12066    location: &powerio_dist::DistLocation,
12067) -> PioMulticonductorLocationView {
12068    let (kind, has_kind) = location.kind.map_or((PioStringView::EMPTY, false), |kind| {
12069        (PioStringView::new(dist_coords_kind_name(kind)), true)
12070    });
12071    PioMulticonductorLocationView {
12072        x: location.x,
12073        y: location.y,
12074        kind,
12075        has_kind,
12076    }
12077}
12078
12079fn empty_multiconductor_location_view() -> PioMulticonductorLocationView {
12080    PioMulticonductorLocationView {
12081        x: 0.0,
12082        y: 0.0,
12083        kind: PioStringView::EMPTY,
12084        has_kind: false,
12085    }
12086}
12087
12088fn multiconductor_geo_view(geo: Option<&powerio_dist::DistGeoMeta>) -> PioMulticonductorGeoView {
12089    let Some(geo) = geo else {
12090        return PioMulticonductorGeoView {
12091            has_geo: false,
12092            space: PioStringView::EMPTY,
12093            crs: PioStringView::EMPTY,
12094            has_crs: false,
12095            kind: PioStringView::EMPTY,
12096            has_kind: false,
12097            has_canvas: false,
12098            canvas_width: 0.0,
12099            has_canvas_width: false,
12100            canvas_height: 0.0,
12101            has_canvas_height: false,
12102            canvas_units: PioStringView::EMPTY,
12103            has_canvas_units: false,
12104        };
12105    };
12106    let (space, crs, has_crs, canvas) = match &geo.space {
12107        powerio_dist::CoordinateSpace::Geographic { crs } => (
12108            "geographic",
12109            crs.as_deref()
12110                .map_or(PioStringView::EMPTY, PioStringView::new),
12111            crs.is_some(),
12112            None,
12113        ),
12114        powerio_dist::CoordinateSpace::Projected { crs } => (
12115            "projected",
12116            crs.as_deref()
12117                .map_or(PioStringView::EMPTY, PioStringView::new),
12118            crs.is_some(),
12119            None,
12120        ),
12121        powerio_dist::CoordinateSpace::Diagram { canvas } => {
12122            ("diagram", PioStringView::EMPTY, false, canvas.as_ref())
12123        }
12124        powerio_dist::CoordinateSpace::Unknown => ("unknown", PioStringView::EMPTY, false, None),
12125        _ => ("unknown", PioStringView::EMPTY, false, None),
12126    };
12127    let (kind, has_kind) = geo.kind.map_or((PioStringView::EMPTY, false), |kind| {
12128        (PioStringView::new(dist_coords_kind_name(kind)), true)
12129    });
12130    PioMulticonductorGeoView {
12131        has_geo: true,
12132        space: PioStringView::new(space),
12133        crs,
12134        has_crs,
12135        kind,
12136        has_kind,
12137        has_canvas: canvas.is_some(),
12138        canvas_width: canvas.and_then(|canvas| canvas.width).unwrap_or(0.0),
12139        has_canvas_width: canvas.is_some_and(|canvas| canvas.width.is_some()),
12140        canvas_height: canvas.and_then(|canvas| canvas.height).unwrap_or(0.0),
12141        has_canvas_height: canvas.is_some_and(|canvas| canvas.height.is_some()),
12142        canvas_units: canvas
12143            .and_then(|canvas| canvas.units.as_deref())
12144            .map_or(PioStringView::EMPTY, PioStringView::new),
12145        has_canvas_units: canvas.is_some_and(|canvas| canvas.units.is_some()),
12146    }
12147}
12148
12149fn multiconductor_configuration_name(configuration: powerio_dist::Configuration) -> &'static str {
12150    match configuration {
12151        powerio_dist::Configuration::Wye => "wye",
12152        powerio_dist::Configuration::Delta => "delta",
12153        powerio_dist::Configuration::SinglePhase => "single_phase",
12154        _ => "unknown",
12155    }
12156}
12157
12158fn multiconductor_winding_connection_name(
12159    connection: powerio_dist::DistWindingConn,
12160) -> &'static str {
12161    match connection {
12162        powerio_dist::DistWindingConn::Wye => "wye",
12163        powerio_dist::DistWindingConn::Delta => "delta",
12164        _ => "unknown",
12165    }
12166}
12167
12168fn inverter_topology_name(topology: powerio_dist::IbrTopology) -> &'static str {
12169    match topology {
12170        powerio_dist::IbrTopology::SinglePhase => "SINGLE_PHASE",
12171        powerio_dist::IbrTopology::ThreeLeg => "THREE_LEG",
12172        powerio_dist::IbrTopology::FourLeg => "FOUR_LEG",
12173        _ => "UNKNOWN",
12174    }
12175}
12176
12177fn inverter_prime_mover_name(prime_mover: powerio_dist::IbrPrimeMover) -> &'static str {
12178    match prime_mover {
12179        powerio_dist::IbrPrimeMover::Pv => "PV",
12180        powerio_dist::IbrPrimeMover::Battery => "BATTERY",
12181        powerio_dist::IbrPrimeMover::Generic => "GENERIC",
12182        powerio_dist::IbrPrimeMover::Statcom => "STATCOM",
12183        powerio_dist::IbrPrimeMover::Dstatcom => "DSTATCOM",
12184        _ => "UNKNOWN",
12185    }
12186}
12187
12188fn inverter_voltage_aggregation_name(
12189    aggregation: powerio_dist::IbrVoltageAggregation,
12190) -> &'static str {
12191    match aggregation {
12192        powerio_dist::IbrVoltageAggregation::PerPhase => "PER_PHASE",
12193        powerio_dist::IbrVoltageAggregation::Average => "AVERAGE",
12194        _ => "UNKNOWN",
12195    }
12196}
12197
12198fn control_voltage_reference_name(
12199    reference: powerio_dist::ControlVoltageReference,
12200) -> &'static str {
12201    match reference {
12202        powerio_dist::ControlVoltageReference::PnPerPhase => "PN_PER_PHASE",
12203        powerio_dist::ControlVoltageReference::PpPerPhase => "PP_PER_PHASE",
12204        powerio_dist::ControlVoltageReference::PpAveraged => "PP_AVERAGED",
12205        powerio_dist::ControlVoltageReference::PgAveraged => "PG_AVERAGED",
12206        powerio_dist::ControlVoltageReference::PnAveraged => "PN_AVERAGED",
12207        powerio_dist::ControlVoltageReference::PgPerPhase => "PG_PER_PHASE",
12208        _ => "UNKNOWN",
12209    }
12210}
12211
12212fn reactive_power_unit_name(unit: powerio_dist::ReactivePowerUnit) -> &'static str {
12213    match unit {
12214        powerio_dist::ReactivePowerUnit::VaFraction => "VA_FRACTION",
12215        powerio_dist::ReactivePowerUnit::Var => "VAR",
12216        _ => "UNKNOWN",
12217    }
12218}
12219
12220fn active_power_unit_name(unit: powerio_dist::ActivePowerUnit) -> &'static str {
12221    match unit {
12222        powerio_dist::ActivePowerUnit::VaFraction => "VA_FRACTION",
12223        powerio_dist::ActivePowerUnit::W => "W",
12224        _ => "UNKNOWN",
12225    }
12226}
12227
12228fn reactive_power_reference_name(reference: powerio_dist::ReactivePowerReference) -> &'static str {
12229    match reference {
12230        powerio_dist::ReactivePowerReference::VarMax => "VAR_MAX",
12231        powerio_dist::ReactivePowerReference::VarAvailable => "VAR_AVAILABLE",
12232        _ => "UNKNOWN",
12233    }
12234}
12235
12236fn active_power_reference_name(reference: powerio_dist::ActivePowerReference) -> &'static str {
12237    match reference {
12238        powerio_dist::ActivePowerReference::PAvailable => "P_AVAILABLE",
12239        powerio_dist::ActivePowerReference::PMax => "P_MAX",
12240        powerio_dist::ActivePowerReference::SMax => "S_MAX",
12241        _ => "UNKNOWN",
12242    }
12243}
12244
12245fn string_slice_at(
12246    values: &[String],
12247    index: usize,
12248    description: &str,
12249) -> Result<PioStringView, *mut PioError> {
12250    values
12251        .get(index)
12252        .map(|value| PioStringView::new(value))
12253        .ok_or_else(|| {
12254            boundary_error(
12255                &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
12256                format!("{description} index {index} is out of range"),
12257            )
12258        })
12259}
12260
12261fn conductor_matrix_row(
12262    matrix: &powerio_dist::ConductorMatrix,
12263    row_index: usize,
12264    description: &str,
12265) -> Result<PioF64View, *mut PioError> {
12266    matrix
12267        .get(row_index)
12268        .map(|row| PioF64View::new(row))
12269        .ok_or_else(|| {
12270            boundary_error(
12271                &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
12272                format!("{description} row index {row_index} is out of range"),
12273            )
12274        })
12275}
12276
12277fn multiconductor_bus_view(bus: &powerio_dist::DistBus) -> PioMulticonductorBusView {
12278    let (vpn_min, has_vpn_min) = optional_f64_view(bus.vpn_min.as_deref());
12279    let (vpn_max, has_vpn_max) = optional_f64_view(bus.vpn_max.as_deref());
12280    let (vpp_min, has_vpp_min) = optional_f64_view(bus.vpp_min.as_deref());
12281    let (vpp_max, has_vpp_max) = optional_f64_view(bus.vpp_max.as_deref());
12282    let (location, has_location) = bus
12283        .location
12284        .as_ref()
12285        .map_or((empty_multiconductor_location_view(), false), |location| {
12286            (multiconductor_location_view(location), true)
12287        });
12288    PioMulticonductorBusView {
12289        id: PioStringView::new(&bus.id),
12290        terminal_count: bus.terminals.len(),
12291        grounded_terminal_count: bus.grounded.len(),
12292        voltage_min_v: bus.v_min.unwrap_or(0.0),
12293        has_voltage_min: bus.v_min.is_some(),
12294        voltage_max_v: bus.v_max.unwrap_or(0.0),
12295        has_voltage_max: bus.v_max.is_some(),
12296        phase_to_ground_voltage_min_v: optional_f64_view(bus.v_min_phase.as_deref()).0,
12297        has_phase_to_ground_voltage_min: bus.v_min_phase.is_some(),
12298        phase_to_ground_voltage_max_v: optional_f64_view(bus.v_max_phase.as_deref()).0,
12299        has_phase_to_ground_voltage_max: bus.v_max_phase.is_some(),
12300        phase_to_neutral_voltage_min_v: vpn_min,
12301        has_phase_to_neutral_voltage_min: has_vpn_min,
12302        phase_to_neutral_voltage_max_v: vpn_max,
12303        has_phase_to_neutral_voltage_max: has_vpn_max,
12304        phase_to_phase_voltage_min_v: vpp_min,
12305        has_phase_to_phase_voltage_min: has_vpp_min,
12306        phase_to_phase_voltage_max_v: vpp_max,
12307        has_phase_to_phase_voltage_max: has_vpp_max,
12308        positive_sequence_voltage_min_v: bus.vpos_min.unwrap_or(0.0),
12309        has_positive_sequence_voltage_min: bus.vpos_min.is_some(),
12310        positive_sequence_voltage_max_v: bus.vpos_max.unwrap_or(0.0),
12311        has_positive_sequence_voltage_max: bus.vpos_max.is_some(),
12312        negative_sequence_voltage_max_v: bus.vneg_max.unwrap_or(0.0),
12313        has_negative_sequence_voltage_max: bus.vneg_max.is_some(),
12314        zero_sequence_voltage_max_v: bus.vzero_max.unwrap_or(0.0),
12315        has_zero_sequence_voltage_max: bus.vzero_max.is_some(),
12316        neutral_to_ground_voltage_max_v: bus.vn_max.unwrap_or(0.0),
12317        has_neutral_to_ground_voltage_max: bus.vn_max.is_some(),
12318        location,
12319        has_location,
12320    }
12321}
12322
12323fn multiconductor_line_code_view(
12324    line_code: &powerio_dist::DistLineCode,
12325) -> PioMulticonductorLineCodeView {
12326    let (current_limit, has_current_limit) = optional_f64_view(line_code.i_max.as_deref());
12327    let (apparent_power_limit, has_apparent_power_limit) =
12328        optional_f64_view(line_code.s_max.as_deref());
12329    let (source, has_source) = optional_string_view(line_code.source.as_deref());
12330    PioMulticonductorLineCodeView {
12331        name: PioStringView::new(&line_code.name),
12332        conductor_count: line_code.n_conductors,
12333        resistance_matrix_row_count: line_code.r_series.len(),
12334        reactance_matrix_row_count: line_code.x_series.len(),
12335        conductance_from_matrix_row_count: line_code.g_from.len(),
12336        susceptance_from_matrix_row_count: line_code.b_from.len(),
12337        conductance_to_matrix_row_count: line_code.g_to.len(),
12338        susceptance_to_matrix_row_count: line_code.b_to.len(),
12339        current_limit_a: current_limit,
12340        has_current_limit,
12341        apparent_power_limit_va: apparent_power_limit,
12342        has_apparent_power_limit,
12343        source,
12344        has_source,
12345    }
12346}
12347
12348fn multiconductor_line_view(line: &powerio_dist::DistLine) -> PioMulticonductorLineView {
12349    let (current_limit, has_current_limit) = optional_f64_view(line.i_max.as_deref());
12350    let (apparent_power_limit, has_apparent_power_limit) = optional_f64_view(line.s_max.as_deref());
12351    PioMulticonductorLineView {
12352        name: PioStringView::new(&line.name),
12353        bus_from: PioStringView::new(&line.bus_from),
12354        bus_to: PioStringView::new(&line.bus_to),
12355        terminal_map_from_count: line.terminal_map_from.len(),
12356        terminal_map_to_count: line.terminal_map_to.len(),
12357        line_code: PioStringView::new(&line.linecode),
12358        length_m: line.length,
12359        route_point_count: line.route.as_ref().map_or(0, Vec::len),
12360        has_route: line.route.is_some(),
12361        current_limit_a: current_limit,
12362        has_current_limit,
12363        apparent_power_limit_va: apparent_power_limit,
12364        has_apparent_power_limit,
12365    }
12366}
12367
12368fn multiconductor_switch_view(switch: &powerio_dist::DistSwitch) -> PioMulticonductorSwitchView {
12369    let (current_limit, has_current_limit) = optional_f64_view(switch.i_max.as_deref());
12370    PioMulticonductorSwitchView {
12371        name: PioStringView::new(&switch.name),
12372        bus_from: PioStringView::new(&switch.bus_from),
12373        bus_to: PioStringView::new(&switch.bus_to),
12374        terminal_map_from_count: switch.terminal_map_from.len(),
12375        terminal_map_to_count: switch.terminal_map_to.len(),
12376        open: switch.open,
12377        current_limit_a: current_limit,
12378        has_current_limit,
12379    }
12380}
12381
12382fn multiconductor_load_view(load: &powerio_dist::DistLoad) -> PioMulticonductorLoadView {
12383    let empty = PioF64View::EMPTY;
12384    let (
12385        voltage_model,
12386        nominal_voltage,
12387        alpha_z,
12388        alpha_i,
12389        alpha_p,
12390        beta_z,
12391        beta_i,
12392        beta_p,
12393        gamma_p,
12394        gamma_q,
12395    ) = match &load.voltage_model {
12396        powerio_dist::DistLoadVoltageModel::ConstantPower { v_nom } => (
12397            "constant_power",
12398            PioF64View::new(v_nom),
12399            empty,
12400            empty,
12401            empty,
12402            empty,
12403            empty,
12404            empty,
12405            empty,
12406            empty,
12407        ),
12408        powerio_dist::DistLoadVoltageModel::ConstantCurrent { v_nom } => (
12409            "constant_current",
12410            PioF64View::new(v_nom),
12411            empty,
12412            empty,
12413            empty,
12414            empty,
12415            empty,
12416            empty,
12417            empty,
12418            empty,
12419        ),
12420        powerio_dist::DistLoadVoltageModel::ConstantImpedance { v_nom } => (
12421            "constant_impedance",
12422            PioF64View::new(v_nom),
12423            empty,
12424            empty,
12425            empty,
12426            empty,
12427            empty,
12428            empty,
12429            empty,
12430            empty,
12431        ),
12432        powerio_dist::DistLoadVoltageModel::Zip {
12433            v_nom,
12434            alpha_z,
12435            alpha_i,
12436            alpha_p,
12437            beta_z,
12438            beta_i,
12439            beta_p,
12440        } => (
12441            "zip",
12442            PioF64View::new(v_nom),
12443            PioF64View::new(alpha_z),
12444            PioF64View::new(alpha_i),
12445            PioF64View::new(alpha_p),
12446            PioF64View::new(beta_z),
12447            PioF64View::new(beta_i),
12448            PioF64View::new(beta_p),
12449            empty,
12450            empty,
12451        ),
12452        powerio_dist::DistLoadVoltageModel::Exponential {
12453            v_nom,
12454            gamma_p,
12455            gamma_q,
12456        } => (
12457            "exponential",
12458            PioF64View::new(v_nom),
12459            empty,
12460            empty,
12461            empty,
12462            empty,
12463            empty,
12464            empty,
12465            PioF64View::new(gamma_p),
12466            PioF64View::new(gamma_q),
12467        ),
12468        _ => (
12469            "unknown", empty, empty, empty, empty, empty, empty, empty, empty, empty,
12470        ),
12471    };
12472    PioMulticonductorLoadView {
12473        name: PioStringView::new(&load.name),
12474        bus: PioStringView::new(&load.bus),
12475        terminal_map_count: load.terminal_map.len(),
12476        configuration: PioStringView::new(multiconductor_configuration_name(load.configuration)),
12477        active_power_nominal_w: PioF64View::new(&load.p_nom),
12478        reactive_power_nominal_var: PioF64View::new(&load.q_nom),
12479        voltage_model: PioStringView::new(voltage_model),
12480        nominal_voltage_v: nominal_voltage,
12481        active_power_constant_impedance: alpha_z,
12482        active_power_constant_current: alpha_i,
12483        active_power_constant_power: alpha_p,
12484        reactive_power_constant_impedance: beta_z,
12485        reactive_power_constant_current: beta_i,
12486        reactive_power_constant_power: beta_p,
12487        active_power_exponent: gamma_p,
12488        reactive_power_exponent: gamma_q,
12489    }
12490}
12491
12492fn multiconductor_generator_view(
12493    generator: &powerio_dist::DistGenerator,
12494) -> PioMulticonductorGeneratorView {
12495    let (p_min, has_p_min) = optional_f64_view(generator.p_min.as_deref());
12496    let (p_max, has_p_max) = optional_f64_view(generator.p_max.as_deref());
12497    let (q_min, has_q_min) = optional_f64_view(generator.q_min.as_deref());
12498    let (q_max, has_q_max) = optional_f64_view(generator.q_max.as_deref());
12499    let (cost, has_cost) = optional_f64_view(generator.cost.as_deref());
12500    let (s_max, has_s_max) = optional_f64_view(generator.s_max.as_deref());
12501    let (i_max, has_i_max) = optional_f64_view(generator.i_max.as_deref());
12502    PioMulticonductorGeneratorView {
12503        name: PioStringView::new(&generator.name),
12504        bus: PioStringView::new(&generator.bus),
12505        terminal_map_count: generator.terminal_map.len(),
12506        configuration: PioStringView::new(multiconductor_configuration_name(
12507            generator.configuration,
12508        )),
12509        active_power_nominal_w: PioF64View::new(&generator.p_nom),
12510        reactive_power_nominal_var: PioF64View::new(&generator.q_nom),
12511        active_power_min_w: p_min,
12512        has_active_power_min: has_p_min,
12513        active_power_max_w: p_max,
12514        has_active_power_max: has_p_max,
12515        reactive_power_min_var: q_min,
12516        has_reactive_power_min: has_q_min,
12517        reactive_power_max_var: q_max,
12518        has_reactive_power_max: has_q_max,
12519        active_power_dispatch_cost_per_kwh: cost,
12520        has_active_power_dispatch_cost: has_cost,
12521        apparent_power_limit_va: s_max,
12522        has_apparent_power_limit: has_s_max,
12523        current_limit_a: i_max,
12524        has_current_limit: has_i_max,
12525    }
12526}
12527
12528fn inverter_based_resource_view(ibr: &powerio_dist::DistIbr) -> PioInverterBasedResourceView {
12529    let (i_max, has_i_max) = optional_f64_view(ibr.i_max.as_deref());
12530    let (p_min, has_p_min) = optional_f64_view(ibr.p_min.as_deref());
12531    let (p_max, has_p_max) = optional_f64_view(ibr.p_max.as_deref());
12532    let (q_min, has_q_min) = optional_f64_view(ibr.q_min.as_deref());
12533    let (q_max, has_q_max) = optional_f64_view(ibr.q_max.as_deref());
12534    let (control_profile, has_control_profile) =
12535        optional_string_view(ibr.control_profile.as_deref());
12536    let (voltage_aggregation, has_voltage_aggregation) =
12537        ibr.voltage_aggregation
12538            .map_or((PioStringView::EMPTY, false), |aggregation| {
12539                (
12540                    PioStringView::new(inverter_voltage_aggregation_name(aggregation)),
12541                    true,
12542                )
12543            });
12544    PioInverterBasedResourceView {
12545        name: PioStringView::new(&ibr.name),
12546        bus: PioStringView::new(&ibr.bus),
12547        terminal_map_count: ibr.terminal_map.len(),
12548        topology: PioStringView::new(inverter_topology_name(ibr.topology)),
12549        prime_mover: PioStringView::new(inverter_prime_mover_name(ibr.prime_mover)),
12550        apparent_power_limit_va: PioF64View::new(&ibr.s_max),
12551        current_limit_a: i_max,
12552        has_current_limit: has_i_max,
12553        active_power_available_w: ibr.p_avail.unwrap_or(0.0),
12554        has_active_power_available: ibr.p_avail.is_some(),
12555        active_power_min_w: p_min,
12556        has_active_power_min: has_p_min,
12557        active_power_max_w: p_max,
12558        has_active_power_max: has_p_max,
12559        reactive_power_min_var: q_min,
12560        has_reactive_power_min: has_q_min,
12561        reactive_power_max_var: q_max,
12562        has_reactive_power_max: has_q_max,
12563        control_profile,
12564        has_control_profile,
12565        voltage_aggregation,
12566        has_voltage_aggregation,
12567    }
12568}
12569
12570fn control_profile_view(profile: &powerio_dist::DistControlProfile) -> PioControlProfileView {
12571    let power_factor = profile.power_factor.as_ref();
12572    let volt_var = profile.volt_var.as_ref();
12573    let volt_watt = profile.volt_watt.as_ref();
12574    let (vv_voltage_reference, has_vv_voltage_reference) = volt_var
12575        .and_then(|control| control.voltage_reference)
12576        .map_or((PioStringView::EMPTY, false), |reference| {
12577            (
12578                PioStringView::new(control_voltage_reference_name(reference)),
12579                true,
12580            )
12581        });
12582    let (vv_q_unit, has_vv_q_unit) = volt_var
12583        .and_then(|control| control.q_unit)
12584        .map_or((PioStringView::EMPTY, false), |unit| {
12585            (PioStringView::new(reactive_power_unit_name(unit)), true)
12586        });
12587    let (vv_q_ref, has_vv_q_ref) = volt_var.and_then(|control| control.q_ref).map_or(
12588        (PioStringView::EMPTY, false),
12589        |reference| {
12590            (
12591                PioStringView::new(reactive_power_reference_name(reference)),
12592                true,
12593            )
12594        },
12595    );
12596    let (vw_voltage_reference, has_vw_voltage_reference) = volt_watt
12597        .and_then(|control| control.voltage_reference)
12598        .map_or((PioStringView::EMPTY, false), |reference| {
12599            (
12600                PioStringView::new(control_voltage_reference_name(reference)),
12601                true,
12602            )
12603        });
12604    let (vw_p_unit, has_vw_p_unit) = volt_watt
12605        .and_then(|control| control.p_unit)
12606        .map_or((PioStringView::EMPTY, false), |unit| {
12607            (PioStringView::new(active_power_unit_name(unit)), true)
12608        });
12609    let (vw_p_ref, has_vw_p_ref) = volt_watt.and_then(|control| control.p_ref).map_or(
12610        (PioStringView::EMPTY, false),
12611        |reference| {
12612            (
12613                PioStringView::new(active_power_reference_name(reference)),
12614                true,
12615            )
12616        },
12617    );
12618    PioControlProfileView {
12619        name: PioStringView::new(&profile.name),
12620        has_power_factor: power_factor.is_some(),
12621        power_factor: power_factor.map_or(0.0, |control| control.pf),
12622        has_volt_var: volt_var.is_some(),
12623        volt_var_voltage_reference: vv_voltage_reference,
12624        has_volt_var_voltage_reference: has_vv_voltage_reference,
12625        volt_var_breakpoints: volt_var.map_or(PioF64View::EMPTY, |control| {
12626            PioF64View::new(&control.breakpoints)
12627        }),
12628        volt_var_reactive_power_limits: volt_var.map_or(PioF64View::EMPTY, |control| {
12629            PioF64View::new(&control.q_limits)
12630        }),
12631        volt_var_reactive_power_unit: vv_q_unit,
12632        has_volt_var_reactive_power_unit: has_vv_q_unit,
12633        volt_var_reactive_power_reference: vv_q_ref,
12634        has_volt_var_reactive_power_reference: has_vv_q_ref,
12635        volt_var_active_power_min_for_reactive_power_w: volt_var
12636            .and_then(|control| control.p_min_for_q)
12637            .unwrap_or(0.0),
12638        has_volt_var_active_power_min_for_reactive_power: volt_var
12639            .is_some_and(|control| control.p_min_for_q.is_some()),
12640        volt_var_active_power_min_for_max_reactive_power_w: volt_var
12641            .and_then(|control| control.p_min_for_q_max)
12642            .unwrap_or(0.0),
12643        has_volt_var_active_power_min_for_max_reactive_power: volt_var
12644            .is_some_and(|control| control.p_min_for_q_max.is_some()),
12645        has_volt_watt: volt_watt.is_some(),
12646        volt_watt_voltage_reference: vw_voltage_reference,
12647        has_volt_watt_voltage_reference: has_vw_voltage_reference,
12648        volt_watt_breakpoints: volt_watt.map_or(PioF64View::EMPTY, |control| {
12649            PioF64View::new(&control.breakpoints)
12650        }),
12651        volt_watt_active_power_limits: volt_watt.map_or(PioF64View::EMPTY, |control| {
12652            PioF64View::new(&control.p_limits)
12653        }),
12654        volt_watt_active_power_unit: vw_p_unit,
12655        has_volt_watt_active_power_unit: has_vw_p_unit,
12656        volt_watt_active_power_reference: vw_p_ref,
12657        has_volt_watt_active_power_reference: has_vw_p_ref,
12658    }
12659}
12660
12661#[unsafe(no_mangle)]
12662pub unsafe extern "C" fn pio_multiconductor_network_name(
12663    network: *const PioMulticonductorNetwork,
12664) -> PioStringView {
12665    unsafe { PioMulticonductorNetwork::get(network) }
12666        .and_then(MulticonductorNetworkInner::network)
12667        .and_then(|network| network.name().as_deref())
12668        .map_or(PioStringView::EMPTY, PioStringView::new)
12669}
12670
12671#[unsafe(no_mangle)]
12672pub unsafe extern "C" fn pio_multiconductor_network_has_name(
12673    network: *const PioMulticonductorNetwork,
12674) -> bool {
12675    unsafe { PioMulticonductorNetwork::get(network) }
12676        .and_then(MulticonductorNetworkInner::network)
12677        .is_some_and(|network| network.name().is_some())
12678}
12679
12680#[unsafe(no_mangle)]
12681pub unsafe extern "C" fn pio_multiconductor_network_source_format(
12682    network: *const PioMulticonductorNetwork,
12683) -> PioStringView {
12684    unsafe { PioMulticonductorNetwork::get(network) }
12685        .and_then(MulticonductorNetworkInner::network)
12686        .and_then(|network| *network.source_format())
12687        .map_or(PioStringView::EMPTY, |format| {
12688            PioStringView::new(format.name())
12689        })
12690}
12691
12692#[unsafe(no_mangle)]
12693pub unsafe extern "C" fn pio_multiconductor_network_has_source_format(
12694    network: *const PioMulticonductorNetwork,
12695) -> bool {
12696    unsafe { PioMulticonductorNetwork::get(network) }
12697        .and_then(MulticonductorNetworkInner::network)
12698        .is_some_and(|network| network.source_format().is_some())
12699}
12700
12701/// Read the network coordinate metadata, including absence through `has_geo`.
12702#[unsafe(no_mangle)]
12703pub unsafe extern "C" fn pio_multiconductor_network_geo(
12704    network: *const PioMulticonductorNetwork,
12705    output: *mut PioMulticonductorGeoView,
12706    error: *mut *mut PioError,
12707) -> bool {
12708    unsafe {
12709        entry(error, false, || {
12710            let network = require_multiconductor_network(network)?;
12711            *require_output(output, "output")? = multiconductor_geo_view(network.geo().as_ref());
12712            Ok(true)
12713        })
12714    }
12715}
12716
12717/// Read exact table lengths. Defaulted source fields and arbitrary extension
12718/// maps are retained internally and are not separate domain tables.
12719#[unsafe(no_mangle)]
12720pub unsafe extern "C" fn pio_multiconductor_network_counts(
12721    network: *const PioMulticonductorNetwork,
12722    output: *mut PioMulticonductorNetworkCountsView,
12723    error: *mut *mut PioError,
12724) -> bool {
12725    unsafe {
12726        entry(error, false, || {
12727            let network = require_multiconductor_network(network)?;
12728            *require_output(output, "output")? = PioMulticonductorNetworkCountsView {
12729                buses: network.buses().len(),
12730                line_codes: network.line_codes().len(),
12731                lines: network.lines().len(),
12732                switches: network.switches().len(),
12733                transformers: network.transformers().len(),
12734                loads: network.loads().len(),
12735                generators: network.generators().len(),
12736                inverter_based_resources: network.ibrs().len(),
12737                control_profiles: network.control_profiles().len(),
12738                shunts: network.shunts().len(),
12739                capacitors: network.capacitors().len(),
12740                voltage_sources: network.sources().len(),
12741                untyped_objects: network.untyped_objects().len(),
12742                commands: network.commands().len(),
12743                options: network.options().len(),
12744            };
12745            Ok(true)
12746        })
12747    }
12748}
12749
12750#[unsafe(no_mangle)]
12751pub unsafe extern "C" fn pio_multiconductor_network_base_frequency_hz(
12752    network: *const PioMulticonductorNetwork,
12753) -> f64 {
12754    unsafe { PioMulticonductorNetwork::get(network) }
12755        .and_then(MulticonductorNetworkInner::network)
12756        .map_or(f64::NAN, |network| network.base_frequency())
12757}
12758
12759#[unsafe(no_mangle)]
12760pub unsafe extern "C" fn pio_multiconductor_network_bus_count(
12761    network: *const PioMulticonductorNetwork,
12762) -> usize {
12763    unsafe { PioMulticonductorNetwork::get(network) }
12764        .and_then(MulticonductorNetworkInner::network)
12765        .map_or(0, |network| network.buses().len())
12766}
12767
12768#[unsafe(no_mangle)]
12769pub unsafe extern "C" fn pio_multiconductor_network_line_count(
12770    network: *const PioMulticonductorNetwork,
12771) -> usize {
12772    unsafe { PioMulticonductorNetwork::get(network) }
12773        .and_then(MulticonductorNetworkInner::network)
12774        .map_or(0, |network| network.lines().len())
12775}
12776
12777#[unsafe(no_mangle)]
12778pub unsafe extern "C" fn pio_multiconductor_network_load_count(
12779    network: *const PioMulticonductorNetwork,
12780) -> usize {
12781    unsafe { PioMulticonductorNetwork::get(network) }
12782        .and_then(MulticonductorNetworkInner::network)
12783        .map_or(0, |network| network.loads().len())
12784}
12785
12786#[unsafe(no_mangle)]
12787pub unsafe extern "C" fn pio_multiconductor_network_generator_count(
12788    network: *const PioMulticonductorNetwork,
12789) -> usize {
12790    unsafe { PioMulticonductorNetwork::get(network) }
12791        .and_then(MulticonductorNetworkInner::network)
12792        .map_or(0, |network| network.generators().len())
12793}
12794
12795/// Read one multiconductor bus by zero based table position. Borrowed strings
12796/// and numeric spans remain valid while the network handle is alive.
12797#[unsafe(no_mangle)]
12798pub unsafe extern "C" fn pio_multiconductor_network_bus_at(
12799    network: *const PioMulticonductorNetwork,
12800    index: usize,
12801    output: *mut PioMulticonductorBusView,
12802    error: *mut *mut PioError,
12803) -> bool {
12804    unsafe {
12805        entry(error, false, || {
12806            let network = require_multiconductor_network(network)?;
12807            let bus = network.buses().get(index).ok_or_else(|| {
12808                boundary_error(
12809                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
12810                    format!("multiconductor bus index {index} is out of range"),
12811                )
12812            })?;
12813            *require_output(output, "output")? = multiconductor_bus_view(bus);
12814            Ok(true)
12815        })
12816    }
12817}
12818
12819#[unsafe(no_mangle)]
12820pub unsafe extern "C" fn pio_multiconductor_network_bus_terminal_at(
12821    network: *const PioMulticonductorNetwork,
12822    bus_index: usize,
12823    terminal_index: usize,
12824    output: *mut PioStringView,
12825    error: *mut *mut PioError,
12826) -> bool {
12827    unsafe {
12828        entry(error, false, || {
12829            let network = require_multiconductor_network(network)?;
12830            let bus = network.buses().get(bus_index).ok_or_else(|| {
12831                boundary_error(
12832                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
12833                    format!("multiconductor bus index {bus_index} is out of range"),
12834                )
12835            })?;
12836            *require_output(output, "output")? =
12837                string_slice_at(&bus.terminals, terminal_index, "bus terminal")?;
12838            Ok(true)
12839        })
12840    }
12841}
12842
12843#[unsafe(no_mangle)]
12844pub unsafe extern "C" fn pio_multiconductor_network_bus_grounded_terminal_at(
12845    network: *const PioMulticonductorNetwork,
12846    bus_index: usize,
12847    terminal_index: usize,
12848    output: *mut PioStringView,
12849    error: *mut *mut PioError,
12850) -> bool {
12851    unsafe {
12852        entry(error, false, || {
12853            let network = require_multiconductor_network(network)?;
12854            let bus = network.buses().get(bus_index).ok_or_else(|| {
12855                boundary_error(
12856                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
12857                    format!("multiconductor bus index {bus_index} is out of range"),
12858                )
12859            })?;
12860            *require_output(output, "output")? =
12861                string_slice_at(&bus.grounded, terminal_index, "grounded bus terminal")?;
12862            Ok(true)
12863        })
12864    }
12865}
12866
12867#[unsafe(no_mangle)]
12868pub unsafe extern "C" fn pio_multiconductor_network_line_code_at(
12869    network: *const PioMulticonductorNetwork,
12870    index: usize,
12871    output: *mut PioMulticonductorLineCodeView,
12872    error: *mut *mut PioError,
12873) -> bool {
12874    unsafe {
12875        entry(error, false, || {
12876            let network = require_multiconductor_network(network)?;
12877            let line_code = network.line_codes().get(index).ok_or_else(|| {
12878                boundary_error(
12879                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
12880                    format!("multiconductor line code index {index} is out of range"),
12881                )
12882            })?;
12883            *require_output(output, "output")? = multiconductor_line_code_view(line_code);
12884            Ok(true)
12885        })
12886    }
12887}
12888
12889fn multiconductor_line_code_matrix<'a>(
12890    network: &'a powerio_dist::MulticonductorNetwork,
12891    line_code_index: usize,
12892    matrix: &str,
12893) -> Result<&'a powerio_dist::ConductorMatrix, *mut PioError> {
12894    let line_code = network.line_codes().get(line_code_index).ok_or_else(|| {
12895        boundary_error(
12896            &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
12897            format!("multiconductor line code index {line_code_index} is out of range"),
12898        )
12899    })?;
12900    Ok(match matrix {
12901        "resistance" => &line_code.r_series,
12902        "reactance" => &line_code.x_series,
12903        "conductance_from" => &line_code.g_from,
12904        "susceptance_from" => &line_code.b_from,
12905        "conductance_to" => &line_code.g_to,
12906        "susceptance_to" => &line_code.b_to,
12907        _ => unreachable!("all line code matrices are handled"),
12908    })
12909}
12910
12911unsafe fn write_multiconductor_line_code_matrix_row(
12912    network: *const PioMulticonductorNetwork,
12913    line_code_index: usize,
12914    row_index: usize,
12915    matrix: &str,
12916    output: *mut PioF64View,
12917    error: *mut *mut PioError,
12918) -> bool {
12919    unsafe {
12920        entry(error, false, || {
12921            let network = require_multiconductor_network(network)?;
12922            let values = multiconductor_line_code_matrix(network, line_code_index, matrix)?;
12923            *require_output(output, "output")? =
12924                conductor_matrix_row(values, row_index, &format!("line code {matrix} matrix"))?;
12925            Ok(true)
12926        })
12927    }
12928}
12929
12930#[unsafe(no_mangle)]
12931pub unsafe extern "C" fn pio_multiconductor_network_line_code_resistance_matrix_row_at(
12932    network: *const PioMulticonductorNetwork,
12933    line_code_index: usize,
12934    row_index: usize,
12935    output: *mut PioF64View,
12936    error: *mut *mut PioError,
12937) -> bool {
12938    unsafe {
12939        write_multiconductor_line_code_matrix_row(
12940            network,
12941            line_code_index,
12942            row_index,
12943            "resistance",
12944            output,
12945            error,
12946        )
12947    }
12948}
12949
12950#[unsafe(no_mangle)]
12951pub unsafe extern "C" fn pio_multiconductor_network_line_code_reactance_matrix_row_at(
12952    network: *const PioMulticonductorNetwork,
12953    line_code_index: usize,
12954    row_index: usize,
12955    output: *mut PioF64View,
12956    error: *mut *mut PioError,
12957) -> bool {
12958    unsafe {
12959        write_multiconductor_line_code_matrix_row(
12960            network,
12961            line_code_index,
12962            row_index,
12963            "reactance",
12964            output,
12965            error,
12966        )
12967    }
12968}
12969
12970#[unsafe(no_mangle)]
12971pub unsafe extern "C" fn pio_multiconductor_network_line_code_conductance_from_matrix_row_at(
12972    network: *const PioMulticonductorNetwork,
12973    line_code_index: usize,
12974    row_index: usize,
12975    output: *mut PioF64View,
12976    error: *mut *mut PioError,
12977) -> bool {
12978    unsafe {
12979        write_multiconductor_line_code_matrix_row(
12980            network,
12981            line_code_index,
12982            row_index,
12983            "conductance_from",
12984            output,
12985            error,
12986        )
12987    }
12988}
12989
12990#[unsafe(no_mangle)]
12991pub unsafe extern "C" fn pio_multiconductor_network_line_code_susceptance_from_matrix_row_at(
12992    network: *const PioMulticonductorNetwork,
12993    line_code_index: usize,
12994    row_index: usize,
12995    output: *mut PioF64View,
12996    error: *mut *mut PioError,
12997) -> bool {
12998    unsafe {
12999        write_multiconductor_line_code_matrix_row(
13000            network,
13001            line_code_index,
13002            row_index,
13003            "susceptance_from",
13004            output,
13005            error,
13006        )
13007    }
13008}
13009
13010#[unsafe(no_mangle)]
13011pub unsafe extern "C" fn pio_multiconductor_network_line_code_conductance_to_matrix_row_at(
13012    network: *const PioMulticonductorNetwork,
13013    line_code_index: usize,
13014    row_index: usize,
13015    output: *mut PioF64View,
13016    error: *mut *mut PioError,
13017) -> bool {
13018    unsafe {
13019        write_multiconductor_line_code_matrix_row(
13020            network,
13021            line_code_index,
13022            row_index,
13023            "conductance_to",
13024            output,
13025            error,
13026        )
13027    }
13028}
13029
13030#[unsafe(no_mangle)]
13031pub unsafe extern "C" fn pio_multiconductor_network_line_code_susceptance_to_matrix_row_at(
13032    network: *const PioMulticonductorNetwork,
13033    line_code_index: usize,
13034    row_index: usize,
13035    output: *mut PioF64View,
13036    error: *mut *mut PioError,
13037) -> bool {
13038    unsafe {
13039        write_multiconductor_line_code_matrix_row(
13040            network,
13041            line_code_index,
13042            row_index,
13043            "susceptance_to",
13044            output,
13045            error,
13046        )
13047    }
13048}
13049
13050#[unsafe(no_mangle)]
13051pub unsafe extern "C" fn pio_multiconductor_network_line_at(
13052    network: *const PioMulticonductorNetwork,
13053    index: usize,
13054    output: *mut PioMulticonductorLineView,
13055    error: *mut *mut PioError,
13056) -> bool {
13057    unsafe {
13058        entry(error, false, || {
13059            let network = require_multiconductor_network(network)?;
13060            let line = network.lines().get(index).ok_or_else(|| {
13061                boundary_error(
13062                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13063                    format!("multiconductor line index {index} is out of range"),
13064                )
13065            })?;
13066            *require_output(output, "output")? = multiconductor_line_view(line);
13067            Ok(true)
13068        })
13069    }
13070}
13071
13072unsafe fn write_multiconductor_line_terminal(
13073    network: *const PioMulticonductorNetwork,
13074    line_index: usize,
13075    terminal_index: usize,
13076    from: bool,
13077    output: *mut PioStringView,
13078    error: *mut *mut PioError,
13079) -> bool {
13080    unsafe {
13081        entry(error, false, || {
13082            let network = require_multiconductor_network(network)?;
13083            let line = network.lines().get(line_index).ok_or_else(|| {
13084                boundary_error(
13085                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13086                    format!("multiconductor line index {line_index} is out of range"),
13087                )
13088            })?;
13089            let (terminals, description) = if from {
13090                (&line.terminal_map_from, "line from terminal")
13091            } else {
13092                (&line.terminal_map_to, "line to terminal")
13093            };
13094            *require_output(output, "output")? =
13095                string_slice_at(terminals, terminal_index, description)?;
13096            Ok(true)
13097        })
13098    }
13099}
13100
13101#[unsafe(no_mangle)]
13102pub unsafe extern "C" fn pio_multiconductor_network_line_terminal_from_at(
13103    network: *const PioMulticonductorNetwork,
13104    line_index: usize,
13105    terminal_index: usize,
13106    output: *mut PioStringView,
13107    error: *mut *mut PioError,
13108) -> bool {
13109    unsafe {
13110        write_multiconductor_line_terminal(network, line_index, terminal_index, true, output, error)
13111    }
13112}
13113
13114#[unsafe(no_mangle)]
13115pub unsafe extern "C" fn pio_multiconductor_network_line_terminal_to_at(
13116    network: *const PioMulticonductorNetwork,
13117    line_index: usize,
13118    terminal_index: usize,
13119    output: *mut PioStringView,
13120    error: *mut *mut PioError,
13121) -> bool {
13122    unsafe {
13123        write_multiconductor_line_terminal(
13124            network,
13125            line_index,
13126            terminal_index,
13127            false,
13128            output,
13129            error,
13130        )
13131    }
13132}
13133
13134#[unsafe(no_mangle)]
13135pub unsafe extern "C" fn pio_multiconductor_network_line_route_point_at(
13136    network: *const PioMulticonductorNetwork,
13137    line_index: usize,
13138    point_index: usize,
13139    output: *mut PioMulticonductorLocationView,
13140    error: *mut *mut PioError,
13141) -> bool {
13142    unsafe {
13143        entry(error, false, || {
13144            let network = require_multiconductor_network(network)?;
13145            let line = network.lines().get(line_index).ok_or_else(|| {
13146                boundary_error(
13147                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13148                    format!("multiconductor line index {line_index} is out of range"),
13149                )
13150            })?;
13151            let route = line.route.as_ref().ok_or_else(|| {
13152                boundary_error(
13153                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
13154                    format!("multiconductor line {line_index} has no route"),
13155                )
13156            })?;
13157            let point = route.get(point_index).ok_or_else(|| {
13158                boundary_error(
13159                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13160                    format!("line route point index {point_index} is out of range"),
13161                )
13162            })?;
13163            *require_output(output, "output")? = multiconductor_location_view(point);
13164            Ok(true)
13165        })
13166    }
13167}
13168
13169#[unsafe(no_mangle)]
13170pub unsafe extern "C" fn pio_multiconductor_network_switch_at(
13171    network: *const PioMulticonductorNetwork,
13172    index: usize,
13173    output: *mut PioMulticonductorSwitchView,
13174    error: *mut *mut PioError,
13175) -> bool {
13176    unsafe {
13177        entry(error, false, || {
13178            let network = require_multiconductor_network(network)?;
13179            let switch = network.switches().get(index).ok_or_else(|| {
13180                boundary_error(
13181                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13182                    format!("multiconductor switch index {index} is out of range"),
13183                )
13184            })?;
13185            *require_output(output, "output")? = multiconductor_switch_view(switch);
13186            Ok(true)
13187        })
13188    }
13189}
13190
13191unsafe fn write_multiconductor_switch_terminal(
13192    network: *const PioMulticonductorNetwork,
13193    switch_index: usize,
13194    terminal_index: usize,
13195    from: bool,
13196    output: *mut PioStringView,
13197    error: *mut *mut PioError,
13198) -> bool {
13199    unsafe {
13200        entry(error, false, || {
13201            let network = require_multiconductor_network(network)?;
13202            let switch = network.switches().get(switch_index).ok_or_else(|| {
13203                boundary_error(
13204                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13205                    format!("multiconductor switch index {switch_index} is out of range"),
13206                )
13207            })?;
13208            let (terminals, description) = if from {
13209                (&switch.terminal_map_from, "switch from terminal")
13210            } else {
13211                (&switch.terminal_map_to, "switch to terminal")
13212            };
13213            *require_output(output, "output")? =
13214                string_slice_at(terminals, terminal_index, description)?;
13215            Ok(true)
13216        })
13217    }
13218}
13219
13220#[unsafe(no_mangle)]
13221pub unsafe extern "C" fn pio_multiconductor_network_switch_terminal_from_at(
13222    network: *const PioMulticonductorNetwork,
13223    switch_index: usize,
13224    terminal_index: usize,
13225    output: *mut PioStringView,
13226    error: *mut *mut PioError,
13227) -> bool {
13228    unsafe {
13229        write_multiconductor_switch_terminal(
13230            network,
13231            switch_index,
13232            terminal_index,
13233            true,
13234            output,
13235            error,
13236        )
13237    }
13238}
13239
13240#[unsafe(no_mangle)]
13241pub unsafe extern "C" fn pio_multiconductor_network_switch_terminal_to_at(
13242    network: *const PioMulticonductorNetwork,
13243    switch_index: usize,
13244    terminal_index: usize,
13245    output: *mut PioStringView,
13246    error: *mut *mut PioError,
13247) -> bool {
13248    unsafe {
13249        write_multiconductor_switch_terminal(
13250            network,
13251            switch_index,
13252            terminal_index,
13253            false,
13254            output,
13255            error,
13256        )
13257    }
13258}
13259
13260#[unsafe(no_mangle)]
13261pub unsafe extern "C" fn pio_multiconductor_network_transformer_at(
13262    network: *const PioMulticonductorNetwork,
13263    index: usize,
13264    output: *mut PioMulticonductorTransformerView,
13265    error: *mut *mut PioError,
13266) -> bool {
13267    unsafe {
13268        entry(error, false, || {
13269            let network = require_multiconductor_network(network)?;
13270            let transformer = network.transformers().get(index).ok_or_else(|| {
13271                boundary_error(
13272                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13273                    format!("multiconductor transformer index {index} is out of range"),
13274                )
13275            })?;
13276            *require_output(output, "output")? = PioMulticonductorTransformerView {
13277                name: PioStringView::new(&transformer.name),
13278                winding_count: transformer.windings.len(),
13279                short_circuit_reactance_percent: PioF64View::new(&transformer.xsc_pct),
13280                phase_count: transformer.phases,
13281            };
13282            Ok(true)
13283        })
13284    }
13285}
13286
13287#[unsafe(no_mangle)]
13288pub unsafe extern "C" fn pio_multiconductor_network_transformer_winding_at(
13289    network: *const PioMulticonductorNetwork,
13290    transformer_index: usize,
13291    winding_index: usize,
13292    output: *mut PioMulticonductorTransformerWindingView,
13293    error: *mut *mut PioError,
13294) -> bool {
13295    unsafe {
13296        entry(error, false, || {
13297            let network = require_multiconductor_network(network)?;
13298            let transformer = network
13299                .transformers()
13300                .get(transformer_index)
13301                .ok_or_else(|| {
13302                    boundary_error(
13303                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13304                        format!(
13305                            "multiconductor transformer index {transformer_index} is out of range"
13306                        ),
13307                    )
13308                })?;
13309            let winding = transformer.windings.get(winding_index).ok_or_else(|| {
13310                boundary_error(
13311                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13312                    format!("transformer winding index {winding_index} is out of range"),
13313                )
13314            })?;
13315            *require_output(output, "output")? = PioMulticonductorTransformerWindingView {
13316                bus: PioStringView::new(&winding.bus),
13317                terminal_map_count: winding.terminal_map.len(),
13318                connection: PioStringView::new(multiconductor_winding_connection_name(
13319                    winding.conn,
13320                )),
13321                rated_voltage_v: winding.v_ref,
13322                apparent_power_rating_va: winding.s_rating,
13323                resistance_percent: winding.r_pct,
13324                tap: winding.tap,
13325                neutral_resistance_ohm: winding.r_neutral.unwrap_or(0.0),
13326                has_neutral_resistance: winding.r_neutral.is_some(),
13327                neutral_reactance_ohm: winding.x_neutral.unwrap_or(0.0),
13328                has_neutral_reactance: winding.x_neutral.is_some(),
13329            };
13330            Ok(true)
13331        })
13332    }
13333}
13334
13335#[unsafe(no_mangle)]
13336pub unsafe extern "C" fn pio_multiconductor_network_transformer_winding_terminal_at(
13337    network: *const PioMulticonductorNetwork,
13338    transformer_index: usize,
13339    winding_index: usize,
13340    terminal_index: usize,
13341    output: *mut PioStringView,
13342    error: *mut *mut PioError,
13343) -> bool {
13344    unsafe {
13345        entry(error, false, || {
13346            let network = require_multiconductor_network(network)?;
13347            let transformer = network
13348                .transformers()
13349                .get(transformer_index)
13350                .ok_or_else(|| {
13351                    boundary_error(
13352                        &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13353                        format!(
13354                            "multiconductor transformer index {transformer_index} is out of range"
13355                        ),
13356                    )
13357                })?;
13358            let winding = transformer.windings.get(winding_index).ok_or_else(|| {
13359                boundary_error(
13360                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13361                    format!("transformer winding index {winding_index} is out of range"),
13362                )
13363            })?;
13364            *require_output(output, "output")? = string_slice_at(
13365                &winding.terminal_map,
13366                terminal_index,
13367                "transformer winding terminal",
13368            )?;
13369            Ok(true)
13370        })
13371    }
13372}
13373
13374#[derive(Clone, Copy)]
13375enum MulticonductorTerminalTable {
13376    Load,
13377    Generator,
13378    InverterBasedResource,
13379    Shunt,
13380    Capacitor,
13381    VoltageSource,
13382}
13383
13384unsafe fn write_multiconductor_terminal(
13385    network: *const PioMulticonductorNetwork,
13386    table: MulticonductorTerminalTable,
13387    element_index: usize,
13388    terminal_index: usize,
13389    output: *mut PioStringView,
13390    error: *mut *mut PioError,
13391) -> bool {
13392    unsafe {
13393        entry(error, false, || {
13394            let network = require_multiconductor_network(network)?;
13395            let (terminals, description): (&[String], &str) = match table {
13396                MulticonductorTerminalTable::Load => (
13397                    &network
13398                        .loads()
13399                        .get(element_index)
13400                        .ok_or_else(|| {
13401                            boundary_error(
13402                                &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13403                                format!(
13404                                    "multiconductor load index {element_index} is out of range"
13405                                ),
13406                            )
13407                        })?
13408                        .terminal_map,
13409                    "load terminal",
13410                ),
13411                MulticonductorTerminalTable::Generator => (
13412                    &network
13413                        .generators()
13414                        .get(element_index)
13415                        .ok_or_else(|| {
13416                            boundary_error(
13417                                &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13418                                format!(
13419                                    "multiconductor generator index {element_index} is out of range"
13420                                ),
13421                            )
13422                        })?
13423                        .terminal_map,
13424                    "generator terminal",
13425                ),
13426                MulticonductorTerminalTable::InverterBasedResource => (
13427                    &network
13428                        .ibrs()
13429                        .get(element_index)
13430                        .ok_or_else(|| {
13431                            boundary_error(
13432                                &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13433                                format!(
13434                                    "inverter based resource index {element_index} is out of range"
13435                                ),
13436                            )
13437                        })?
13438                        .terminal_map,
13439                    "inverter based resource terminal",
13440                ),
13441                MulticonductorTerminalTable::Shunt => (
13442                    &network
13443                        .shunts()
13444                        .get(element_index)
13445                        .ok_or_else(|| {
13446                            boundary_error(
13447                                &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13448                                format!(
13449                                    "multiconductor shunt index {element_index} is out of range"
13450                                ),
13451                            )
13452                        })?
13453                        .terminal_map,
13454                    "shunt terminal",
13455                ),
13456                MulticonductorTerminalTable::Capacitor => (
13457                    &network
13458                        .capacitors()
13459                        .get(element_index)
13460                        .ok_or_else(|| {
13461                            boundary_error(
13462                                &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13463                                format!(
13464                                    "multiconductor capacitor index {element_index} is out of range"
13465                                ),
13466                            )
13467                        })?
13468                        .terminal_map,
13469                    "capacitor terminal",
13470                ),
13471                MulticonductorTerminalTable::VoltageSource => (
13472                    &network
13473                        .sources()
13474                        .get(element_index)
13475                        .ok_or_else(|| {
13476                            boundary_error(
13477                                &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13478                                format!("voltage source index {element_index} is out of range"),
13479                            )
13480                        })?
13481                        .terminal_map,
13482                    "voltage source terminal",
13483                ),
13484            };
13485            *require_output(output, "output")? =
13486                string_slice_at(terminals, terminal_index, description)?;
13487            Ok(true)
13488        })
13489    }
13490}
13491
13492#[unsafe(no_mangle)]
13493pub unsafe extern "C" fn pio_multiconductor_network_load_at(
13494    network: *const PioMulticonductorNetwork,
13495    index: usize,
13496    output: *mut PioMulticonductorLoadView,
13497    error: *mut *mut PioError,
13498) -> bool {
13499    unsafe {
13500        entry(error, false, || {
13501            let network = require_multiconductor_network(network)?;
13502            let load = network.loads().get(index).ok_or_else(|| {
13503                boundary_error(
13504                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13505                    format!("multiconductor load index {index} is out of range"),
13506                )
13507            })?;
13508            *require_output(output, "output")? = multiconductor_load_view(load);
13509            Ok(true)
13510        })
13511    }
13512}
13513
13514#[unsafe(no_mangle)]
13515pub unsafe extern "C" fn pio_multiconductor_network_load_terminal_at(
13516    network: *const PioMulticonductorNetwork,
13517    load_index: usize,
13518    terminal_index: usize,
13519    output: *mut PioStringView,
13520    error: *mut *mut PioError,
13521) -> bool {
13522    unsafe {
13523        write_multiconductor_terminal(
13524            network,
13525            MulticonductorTerminalTable::Load,
13526            load_index,
13527            terminal_index,
13528            output,
13529            error,
13530        )
13531    }
13532}
13533
13534#[unsafe(no_mangle)]
13535pub unsafe extern "C" fn pio_multiconductor_network_generator_at(
13536    network: *const PioMulticonductorNetwork,
13537    index: usize,
13538    output: *mut PioMulticonductorGeneratorView,
13539    error: *mut *mut PioError,
13540) -> bool {
13541    unsafe {
13542        entry(error, false, || {
13543            let network = require_multiconductor_network(network)?;
13544            let generator = network.generators().get(index).ok_or_else(|| {
13545                boundary_error(
13546                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13547                    format!("multiconductor generator index {index} is out of range"),
13548                )
13549            })?;
13550            *require_output(output, "output")? = multiconductor_generator_view(generator);
13551            Ok(true)
13552        })
13553    }
13554}
13555
13556#[unsafe(no_mangle)]
13557pub unsafe extern "C" fn pio_multiconductor_network_generator_terminal_at(
13558    network: *const PioMulticonductorNetwork,
13559    generator_index: usize,
13560    terminal_index: usize,
13561    output: *mut PioStringView,
13562    error: *mut *mut PioError,
13563) -> bool {
13564    unsafe {
13565        write_multiconductor_terminal(
13566            network,
13567            MulticonductorTerminalTable::Generator,
13568            generator_index,
13569            terminal_index,
13570            output,
13571            error,
13572        )
13573    }
13574}
13575
13576#[unsafe(no_mangle)]
13577pub unsafe extern "C" fn pio_multiconductor_network_inverter_based_resource_at(
13578    network: *const PioMulticonductorNetwork,
13579    index: usize,
13580    output: *mut PioInverterBasedResourceView,
13581    error: *mut *mut PioError,
13582) -> bool {
13583    unsafe {
13584        entry(error, false, || {
13585            let network = require_multiconductor_network(network)?;
13586            let resource = network.ibrs().get(index).ok_or_else(|| {
13587                boundary_error(
13588                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13589                    format!("inverter based resource index {index} is out of range"),
13590                )
13591            })?;
13592            *require_output(output, "output")? = inverter_based_resource_view(resource);
13593            Ok(true)
13594        })
13595    }
13596}
13597
13598#[unsafe(no_mangle)]
13599pub unsafe extern "C" fn pio_multiconductor_network_inverter_based_resource_terminal_at(
13600    network: *const PioMulticonductorNetwork,
13601    resource_index: usize,
13602    terminal_index: usize,
13603    output: *mut PioStringView,
13604    error: *mut *mut PioError,
13605) -> bool {
13606    unsafe {
13607        write_multiconductor_terminal(
13608            network,
13609            MulticonductorTerminalTable::InverterBasedResource,
13610            resource_index,
13611            terminal_index,
13612            output,
13613            error,
13614        )
13615    }
13616}
13617
13618#[unsafe(no_mangle)]
13619pub unsafe extern "C" fn pio_multiconductor_network_control_profile_at(
13620    network: *const PioMulticonductorNetwork,
13621    index: usize,
13622    output: *mut PioControlProfileView,
13623    error: *mut *mut PioError,
13624) -> bool {
13625    unsafe {
13626        entry(error, false, || {
13627            let network = require_multiconductor_network(network)?;
13628            let profile = network.control_profiles().get(index).ok_or_else(|| {
13629                boundary_error(
13630                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13631                    format!("control profile index {index} is out of range"),
13632                )
13633            })?;
13634            *require_output(output, "output")? = control_profile_view(profile);
13635            Ok(true)
13636        })
13637    }
13638}
13639
13640#[unsafe(no_mangle)]
13641pub unsafe extern "C" fn pio_multiconductor_network_shunt_at(
13642    network: *const PioMulticonductorNetwork,
13643    index: usize,
13644    output: *mut PioMulticonductorShuntView,
13645    error: *mut *mut PioError,
13646) -> bool {
13647    unsafe {
13648        entry(error, false, || {
13649            let network = require_multiconductor_network(network)?;
13650            let shunt = network.shunts().get(index).ok_or_else(|| {
13651                boundary_error(
13652                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13653                    format!("multiconductor shunt index {index} is out of range"),
13654                )
13655            })?;
13656            *require_output(output, "output")? = PioMulticonductorShuntView {
13657                name: PioStringView::new(&shunt.name),
13658                bus: PioStringView::new(&shunt.bus),
13659                terminal_map_count: shunt.terminal_map.len(),
13660                conductance_matrix_row_count: shunt.g.len(),
13661                susceptance_matrix_row_count: shunt.b.len(),
13662            };
13663            Ok(true)
13664        })
13665    }
13666}
13667
13668#[unsafe(no_mangle)]
13669pub unsafe extern "C" fn pio_multiconductor_network_shunt_terminal_at(
13670    network: *const PioMulticonductorNetwork,
13671    shunt_index: usize,
13672    terminal_index: usize,
13673    output: *mut PioStringView,
13674    error: *mut *mut PioError,
13675) -> bool {
13676    unsafe {
13677        write_multiconductor_terminal(
13678            network,
13679            MulticonductorTerminalTable::Shunt,
13680            shunt_index,
13681            terminal_index,
13682            output,
13683            error,
13684        )
13685    }
13686}
13687
13688unsafe fn write_multiconductor_shunt_matrix_row(
13689    network: *const PioMulticonductorNetwork,
13690    shunt_index: usize,
13691    row_index: usize,
13692    conductance: bool,
13693    output: *mut PioF64View,
13694    error: *mut *mut PioError,
13695) -> bool {
13696    unsafe {
13697        entry(error, false, || {
13698            let network = require_multiconductor_network(network)?;
13699            let shunt = network.shunts().get(shunt_index).ok_or_else(|| {
13700                boundary_error(
13701                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13702                    format!("multiconductor shunt index {shunt_index} is out of range"),
13703                )
13704            })?;
13705            let (matrix, description) = if conductance {
13706                (&shunt.g, "shunt conductance matrix")
13707            } else {
13708                (&shunt.b, "shunt susceptance matrix")
13709            };
13710            *require_output(output, "output")? =
13711                conductor_matrix_row(matrix, row_index, description)?;
13712            Ok(true)
13713        })
13714    }
13715}
13716
13717#[unsafe(no_mangle)]
13718pub unsafe extern "C" fn pio_multiconductor_network_shunt_conductance_matrix_row_at(
13719    network: *const PioMulticonductorNetwork,
13720    shunt_index: usize,
13721    row_index: usize,
13722    output: *mut PioF64View,
13723    error: *mut *mut PioError,
13724) -> bool {
13725    unsafe {
13726        write_multiconductor_shunt_matrix_row(network, shunt_index, row_index, true, output, error)
13727    }
13728}
13729
13730#[unsafe(no_mangle)]
13731pub unsafe extern "C" fn pio_multiconductor_network_shunt_susceptance_matrix_row_at(
13732    network: *const PioMulticonductorNetwork,
13733    shunt_index: usize,
13734    row_index: usize,
13735    output: *mut PioF64View,
13736    error: *mut *mut PioError,
13737) -> bool {
13738    unsafe {
13739        write_multiconductor_shunt_matrix_row(network, shunt_index, row_index, false, output, error)
13740    }
13741}
13742
13743#[unsafe(no_mangle)]
13744pub unsafe extern "C" fn pio_multiconductor_network_capacitor_at(
13745    network: *const PioMulticonductorNetwork,
13746    index: usize,
13747    output: *mut PioMulticonductorCapacitorView,
13748    error: *mut *mut PioError,
13749) -> bool {
13750    unsafe {
13751        entry(error, false, || {
13752            let network = require_multiconductor_network(network)?;
13753            let capacitor = network.capacitors().get(index).ok_or_else(|| {
13754                boundary_error(
13755                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13756                    format!("multiconductor capacitor index {index} is out of range"),
13757                )
13758            })?;
13759            *require_output(output, "output")? = PioMulticonductorCapacitorView {
13760                name: PioStringView::new(&capacitor.name),
13761                bus: PioStringView::new(&capacitor.bus),
13762                terminal_map_count: capacitor.terminal_map.len(),
13763                configuration: PioStringView::new(multiconductor_configuration_name(
13764                    capacitor.configuration,
13765                )),
13766                rated_reactive_power_var: capacitor.q_rated,
13767                nominal_voltage_v: capacitor.v_nom,
13768            };
13769            Ok(true)
13770        })
13771    }
13772}
13773
13774#[unsafe(no_mangle)]
13775pub unsafe extern "C" fn pio_multiconductor_network_capacitor_terminal_at(
13776    network: *const PioMulticonductorNetwork,
13777    capacitor_index: usize,
13778    terminal_index: usize,
13779    output: *mut PioStringView,
13780    error: *mut *mut PioError,
13781) -> bool {
13782    unsafe {
13783        write_multiconductor_terminal(
13784            network,
13785            MulticonductorTerminalTable::Capacitor,
13786            capacitor_index,
13787            terminal_index,
13788            output,
13789            error,
13790        )
13791    }
13792}
13793
13794#[unsafe(no_mangle)]
13795pub unsafe extern "C" fn pio_multiconductor_network_voltage_source_at(
13796    network: *const PioMulticonductorNetwork,
13797    index: usize,
13798    output: *mut PioVoltageSourceView,
13799    error: *mut *mut PioError,
13800) -> bool {
13801    unsafe {
13802        entry(error, false, || {
13803            let network = require_multiconductor_network(network)?;
13804            let source = network.sources().get(index).ok_or_else(|| {
13805                boundary_error(
13806                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13807                    format!("voltage source index {index} is out of range"),
13808                )
13809            })?;
13810            *require_output(output, "output")? = PioVoltageSourceView {
13811                name: PioStringView::new(&source.name),
13812                bus: PioStringView::new(&source.bus),
13813                terminal_map_count: source.terminal_map.len(),
13814                voltage_magnitude_v: PioF64View::new(&source.v_magnitude),
13815                voltage_angle_rad: PioF64View::new(&source.v_angle),
13816                energy_cost_rate_per_kwh: PioF64View::new(
13817                    source.energy_cost_rate.as_deref().unwrap_or(&[]),
13818                ),
13819                has_energy_cost_rate: source.energy_cost_rate.is_some(),
13820            };
13821            Ok(true)
13822        })
13823    }
13824}
13825
13826#[unsafe(no_mangle)]
13827pub unsafe extern "C" fn pio_multiconductor_network_voltage_source_terminal_at(
13828    network: *const PioMulticonductorNetwork,
13829    source_index: usize,
13830    terminal_index: usize,
13831    output: *mut PioStringView,
13832    error: *mut *mut PioError,
13833) -> bool {
13834    unsafe {
13835        write_multiconductor_terminal(
13836            network,
13837            MulticonductorTerminalTable::VoltageSource,
13838            source_index,
13839            terminal_index,
13840            output,
13841            error,
13842        )
13843    }
13844}
13845
13846#[unsafe(no_mangle)]
13847pub unsafe extern "C" fn pio_multiconductor_network_untyped_object_at(
13848    network: *const PioMulticonductorNetwork,
13849    index: usize,
13850    output: *mut PioMulticonductorUntypedObjectView,
13851    error: *mut *mut PioError,
13852) -> bool {
13853    unsafe {
13854        entry(error, false, || {
13855            let network = require_multiconductor_network(network)?;
13856            let object = network.untyped_objects().get(index).ok_or_else(|| {
13857                boundary_error(
13858                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13859                    format!("untyped object index {index} is out of range"),
13860                )
13861            })?;
13862            *require_output(output, "output")? = PioMulticonductorUntypedObjectView {
13863                class_name: PioStringView::new(&object.class),
13864                name: PioStringView::new(&object.name),
13865                property_count: object.props.len(),
13866            };
13867            Ok(true)
13868        })
13869    }
13870}
13871
13872#[unsafe(no_mangle)]
13873pub unsafe extern "C" fn pio_multiconductor_network_untyped_object_property_at(
13874    network: *const PioMulticonductorNetwork,
13875    object_index: usize,
13876    property_index: usize,
13877    output: *mut PioMulticonductorUntypedPropertyView,
13878    error: *mut *mut PioError,
13879) -> bool {
13880    unsafe {
13881        entry(error, false, || {
13882            let network = require_multiconductor_network(network)?;
13883            let object = network.untyped_objects().get(object_index).ok_or_else(|| {
13884                boundary_error(
13885                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13886                    format!("untyped object index {object_index} is out of range"),
13887                )
13888            })?;
13889            let (name, value) = object.props.get(property_index).ok_or_else(|| {
13890                boundary_error(
13891                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13892                    format!("untyped object property index {property_index} is out of range"),
13893                )
13894            })?;
13895            let (name, has_name) = optional_string_view(name.as_deref());
13896            *require_output(output, "output")? = PioMulticonductorUntypedPropertyView {
13897                name,
13898                has_name,
13899                value: PioStringView::new(value),
13900            };
13901            Ok(true)
13902        })
13903    }
13904}
13905
13906#[unsafe(no_mangle)]
13907pub unsafe extern "C" fn pio_multiconductor_network_command_at(
13908    network: *const PioMulticonductorNetwork,
13909    index: usize,
13910    output: *mut PioMulticonductorCommandView,
13911    error: *mut *mut PioError,
13912) -> bool {
13913    unsafe {
13914        entry(error, false, || {
13915            let network = require_multiconductor_network(network)?;
13916            let (verb, args) = network.commands().get(index).ok_or_else(|| {
13917                boundary_error(
13918                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13919                    format!("source command index {index} is out of range"),
13920                )
13921            })?;
13922            *require_output(output, "output")? = PioMulticonductorCommandView {
13923                verb: PioStringView::new(verb),
13924                args: PioStringView::new(args),
13925            };
13926            Ok(true)
13927        })
13928    }
13929}
13930
13931#[unsafe(no_mangle)]
13932pub unsafe extern "C" fn pio_multiconductor_network_option_at(
13933    network: *const PioMulticonductorNetwork,
13934    index: usize,
13935    output: *mut PioStringPropertyView,
13936    error: *mut *mut PioError,
13937) -> bool {
13938    unsafe {
13939        entry(error, false, || {
13940            let network = require_multiconductor_network(network)?;
13941            let (name, value) = network.options().get(index).ok_or_else(|| {
13942                boundary_error(
13943                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
13944                    format!("source option index {index} is out of range"),
13945                )
13946            })?;
13947            *require_output(output, "output")? = PioStringPropertyView {
13948                name: PioStringView::new(name),
13949                value: PioStringView::new(value),
13950            };
13951            Ok(true)
13952        })
13953    }
13954}
13955
13956#[unsafe(no_mangle)]
13957pub unsafe extern "C" fn pio_multiconductor_network_retain(
13958    network: *const PioMulticonductorNetwork,
13959) -> *mut PioMulticonductorNetwork {
13960    unsafe { PioMulticonductorNetwork::retain_raw(network) }
13961}
13962
13963#[unsafe(no_mangle)]
13964pub unsafe extern "C" fn pio_multiconductor_network_release(
13965    network: *mut PioMulticonductorNetwork,
13966) {
13967    unsafe { PioMulticonductorNetwork::release_raw(network) };
13968}
13969
13970// ---- typed updates ---------------------------------------------------------
13971
13972opaque_handle!(
13973    /// Stable, type-qualified component identity.
13974    PioComponentId,
13975    ComponentId
13976);
13977opaque_handle!(
13978    /// Active power replacement with an explicit unit.
13979    PioActivePower,
13980    ActivePower
13981);
13982opaque_handle!(
13983    /// Reactive power replacement with an explicit unit.
13984    PioReactivePower,
13985    ReactivePower
13986);
13987opaque_handle!(
13988    /// Apparent power replacement with an explicit unit.
13989    PioApparentPower,
13990    ApparentPower
13991);
13992opaque_handle!(
13993    /// Typed update to an operating point.
13994    PioOperatingPointUpdate,
13995    OperatingPointUpdate
13996);
13997opaque_handle!(
13998    /// Typed update to physical network data.
13999    PioNetworkUpdate,
14000    NetworkUpdate
14001);
14002opaque_handle!(
14003    /// Typed update to a calculation instance.
14004    PioCalculationUpdate,
14005    CalculationUpdate
14006);
14007
14008struct UpdateReportInner {
14009    changes: Vec<UpdateChange>,
14010    connectivity_changed: bool,
14011}
14012
14013struct UpdateChangeInner {
14014    owner: Arc<UpdateReportInner>,
14015    index: usize,
14016}
14017
14018impl UpdateChangeInner {
14019    fn change(&self) -> Option<&UpdateChange> {
14020        self.owner.changes.get(self.index)
14021    }
14022}
14023
14024opaque_handle!(
14025    /// Exact changes made by one atomic update batch.
14026    PioUpdateReport,
14027    UpdateReportInner
14028);
14029opaque_handle!(
14030    /// Owner-rooted view of one changed component field.
14031    PioUpdateChange,
14032    UpdateChangeInner
14033);
14034
14035unsafe fn require_handle<'a, T>(
14036    handle: *const HandleBox<T>,
14037    name: &str,
14038) -> Result<&'a T, *mut PioError> {
14039    unsafe { handle_get(handle) }.ok_or_else(|| {
14040        boundary_error(
14041            &codes::BIND_CAPI_NULL_HANDLE,
14042            format!("{name} must not be NULL"),
14043        )
14044    })
14045}
14046
14047unsafe fn optional_owned_string(
14048    value: *const c_char,
14049    value_len: usize,
14050    name: &str,
14051) -> Result<Option<String>, *mut PioError> {
14052    unsafe { optional_str(value, value_len, name) }.map(|value| value.map(str::to_owned))
14053}
14054
14055/// Construct a stable component identity.
14056#[unsafe(no_mangle)]
14057pub unsafe extern "C" fn pio_component_id_new(
14058    component_type: *const c_char,
14059    component_type_len: usize,
14060    local_id: *const c_char,
14061    local_id_len: usize,
14062    error: *mut *mut PioError,
14063) -> *mut PioComponentId {
14064    unsafe {
14065        entry(error, std::ptr::null_mut(), || {
14066            let component_type =
14067                required_str(component_type, component_type_len, "component_type")?;
14068            let local_id = required_str(local_id, local_id_len, "local_id")?;
14069            ComponentId::new(component_type, local_id)
14070                .map(PioComponentId::new_raw)
14071                .map_err(|failure| error_from_core(&failure))
14072        })
14073    }
14074}
14075
14076#[unsafe(no_mangle)]
14077pub unsafe extern "C" fn pio_component_id_type(component: *const PioComponentId) -> PioStringView {
14078    unsafe { PioComponentId::get(component) }.map_or(PioStringView::EMPTY, |component| {
14079        PioStringView::new(component.component_type())
14080    })
14081}
14082
14083#[unsafe(no_mangle)]
14084pub unsafe extern "C" fn pio_component_id_local_id(
14085    component: *const PioComponentId,
14086) -> PioStringView {
14087    unsafe { PioComponentId::get(component) }.map_or(PioStringView::EMPTY, |component| {
14088        PioStringView::new(component.local_id())
14089    })
14090}
14091
14092#[unsafe(no_mangle)]
14093pub unsafe extern "C" fn pio_component_id_retain(
14094    component: *const PioComponentId,
14095) -> *mut PioComponentId {
14096    unsafe { PioComponentId::retain_raw(component) }
14097}
14098
14099#[unsafe(no_mangle)]
14100pub unsafe extern "C" fn pio_component_id_release(component: *mut PioComponentId) {
14101    unsafe { PioComponentId::release_raw(component) };
14102}
14103
14104#[unsafe(no_mangle)]
14105pub extern "C" fn pio_active_power_from_watts(value: f64) -> *mut PioActivePower {
14106    PioActivePower::new_raw(ActivePower::from_watts(value))
14107}
14108
14109#[unsafe(no_mangle)]
14110pub extern "C" fn pio_active_power_from_megawatts(value: f64) -> *mut PioActivePower {
14111    PioActivePower::new_raw(ActivePower::from_megawatts(value))
14112}
14113
14114#[unsafe(no_mangle)]
14115pub unsafe extern "C" fn pio_active_power_value(power: *const PioActivePower) -> f64 {
14116    unsafe { PioActivePower::get(power) }.map_or(f64::NAN, |power| power.value())
14117}
14118
14119#[unsafe(no_mangle)]
14120pub unsafe extern "C" fn pio_active_power_unit(power: *const PioActivePower) -> PioStringView {
14121    unsafe { PioActivePower::get(power) }.map_or(PioStringView::EMPTY, |power| {
14122        PioStringView::new(match power.unit() {
14123            ActivePowerUnit::Watts => "watts",
14124            ActivePowerUnit::Megawatts => "megawatts",
14125            _ => "unknown",
14126        })
14127    })
14128}
14129
14130#[unsafe(no_mangle)]
14131pub unsafe extern "C" fn pio_active_power_retain(
14132    power: *const PioActivePower,
14133) -> *mut PioActivePower {
14134    unsafe { PioActivePower::retain_raw(power) }
14135}
14136
14137#[unsafe(no_mangle)]
14138pub unsafe extern "C" fn pio_active_power_release(power: *mut PioActivePower) {
14139    unsafe { PioActivePower::release_raw(power) };
14140}
14141
14142#[unsafe(no_mangle)]
14143pub extern "C" fn pio_reactive_power_from_vars(value: f64) -> *mut PioReactivePower {
14144    PioReactivePower::new_raw(ReactivePower::from_vars(value))
14145}
14146
14147#[unsafe(no_mangle)]
14148pub extern "C" fn pio_reactive_power_from_megavars(value: f64) -> *mut PioReactivePower {
14149    PioReactivePower::new_raw(ReactivePower::from_megavars(value))
14150}
14151
14152#[unsafe(no_mangle)]
14153pub unsafe extern "C" fn pio_reactive_power_value(power: *const PioReactivePower) -> f64 {
14154    unsafe { PioReactivePower::get(power) }.map_or(f64::NAN, |power| power.value())
14155}
14156
14157#[unsafe(no_mangle)]
14158pub unsafe extern "C" fn pio_reactive_power_unit(power: *const PioReactivePower) -> PioStringView {
14159    unsafe { PioReactivePower::get(power) }.map_or(PioStringView::EMPTY, |power| {
14160        PioStringView::new(match power.unit() {
14161            ReactivePowerUnit::Vars => "vars",
14162            ReactivePowerUnit::Megavars => "megavars",
14163            _ => "unknown",
14164        })
14165    })
14166}
14167
14168#[unsafe(no_mangle)]
14169pub unsafe extern "C" fn pio_reactive_power_retain(
14170    power: *const PioReactivePower,
14171) -> *mut PioReactivePower {
14172    unsafe { PioReactivePower::retain_raw(power) }
14173}
14174
14175#[unsafe(no_mangle)]
14176pub unsafe extern "C" fn pio_reactive_power_release(power: *mut PioReactivePower) {
14177    unsafe { PioReactivePower::release_raw(power) };
14178}
14179
14180#[unsafe(no_mangle)]
14181pub extern "C" fn pio_apparent_power_from_volt_amperes(value: f64) -> *mut PioApparentPower {
14182    PioApparentPower::new_raw(ApparentPower::from_volt_amperes(value))
14183}
14184
14185#[unsafe(no_mangle)]
14186pub extern "C" fn pio_apparent_power_from_megavolt_amperes(value: f64) -> *mut PioApparentPower {
14187    PioApparentPower::new_raw(ApparentPower::from_megavolt_amperes(value))
14188}
14189
14190#[unsafe(no_mangle)]
14191pub unsafe extern "C" fn pio_apparent_power_value(power: *const PioApparentPower) -> f64 {
14192    unsafe { PioApparentPower::get(power) }.map_or(f64::NAN, |power| power.value())
14193}
14194
14195#[unsafe(no_mangle)]
14196pub unsafe extern "C" fn pio_apparent_power_unit(power: *const PioApparentPower) -> PioStringView {
14197    unsafe { PioApparentPower::get(power) }.map_or(PioStringView::EMPTY, |power| {
14198        PioStringView::new(match power.unit() {
14199            ApparentPowerUnit::VoltAmperes => "volt_amperes",
14200            ApparentPowerUnit::MegavoltAmperes => "megavolt_amperes",
14201            _ => "unknown",
14202        })
14203    })
14204}
14205
14206#[unsafe(no_mangle)]
14207pub unsafe extern "C" fn pio_apparent_power_retain(
14208    power: *const PioApparentPower,
14209) -> *mut PioApparentPower {
14210    unsafe { PioApparentPower::retain_raw(power) }
14211}
14212
14213#[unsafe(no_mangle)]
14214pub unsafe extern "C" fn pio_apparent_power_release(power: *mut PioApparentPower) {
14215    unsafe { PioApparentPower::release_raw(power) };
14216}
14217
14218unsafe fn component_clone(component: *const PioComponentId) -> Result<ComponentId, *mut PioError> {
14219    unsafe { require_handle(component.cast(), "PioComponentId") }.cloned()
14220}
14221
14222unsafe fn active_power_copy(power: *const PioActivePower) -> Result<ActivePower, *mut PioError> {
14223    unsafe { require_handle(power.cast(), "PioActivePower") }.copied()
14224}
14225
14226unsafe fn reactive_power_copy(
14227    power: *const PioReactivePower,
14228) -> Result<ReactivePower, *mut PioError> {
14229    unsafe { require_handle(power.cast(), "PioReactivePower") }.copied()
14230}
14231
14232unsafe fn apparent_power_copy(
14233    power: *const PioApparentPower,
14234) -> Result<ApparentPower, *mut PioError> {
14235    unsafe { require_handle(power.cast(), "PioApparentPower") }.copied()
14236}
14237
14238#[unsafe(no_mangle)]
14239pub unsafe extern "C" fn pio_operating_point_update_set_load_active_power(
14240    load: *const PioComponentId,
14241    terminal: *const c_char,
14242    terminal_len: usize,
14243    power: *const PioActivePower,
14244    error: *mut *mut PioError,
14245) -> *mut PioOperatingPointUpdate {
14246    unsafe {
14247        entry(error, std::ptr::null_mut(), || {
14248            Ok(PioOperatingPointUpdate::new_raw(
14249                OperatingPointUpdate::LoadActivePower {
14250                    load: component_clone(load)?,
14251                    terminal: optional_owned_string(terminal, terminal_len, "terminal")?,
14252                    p: active_power_copy(power)?,
14253                },
14254            ))
14255        })
14256    }
14257}
14258
14259#[unsafe(no_mangle)]
14260pub unsafe extern "C" fn pio_operating_point_update_set_load_reactive_power(
14261    load: *const PioComponentId,
14262    terminal: *const c_char,
14263    terminal_len: usize,
14264    power: *const PioReactivePower,
14265    error: *mut *mut PioError,
14266) -> *mut PioOperatingPointUpdate {
14267    unsafe {
14268        entry(error, std::ptr::null_mut(), || {
14269            Ok(PioOperatingPointUpdate::new_raw(
14270                OperatingPointUpdate::LoadReactivePower {
14271                    load: component_clone(load)?,
14272                    terminal: optional_owned_string(terminal, terminal_len, "terminal")?,
14273                    q: reactive_power_copy(power)?,
14274                },
14275            ))
14276        })
14277    }
14278}
14279
14280#[unsafe(no_mangle)]
14281pub unsafe extern "C" fn pio_operating_point_update_set_generator_active_power(
14282    generator: *const PioComponentId,
14283    terminal: *const c_char,
14284    terminal_len: usize,
14285    power: *const PioActivePower,
14286    error: *mut *mut PioError,
14287) -> *mut PioOperatingPointUpdate {
14288    unsafe {
14289        entry(error, std::ptr::null_mut(), || {
14290            Ok(PioOperatingPointUpdate::new_raw(
14291                OperatingPointUpdate::GeneratorActivePower {
14292                    generator: component_clone(generator)?,
14293                    terminal: optional_owned_string(terminal, terminal_len, "terminal")?,
14294                    p: active_power_copy(power)?,
14295                },
14296            ))
14297        })
14298    }
14299}
14300
14301#[unsafe(no_mangle)]
14302pub unsafe extern "C" fn pio_operating_point_update_set_generator_reactive_power(
14303    generator: *const PioComponentId,
14304    terminal: *const c_char,
14305    terminal_len: usize,
14306    power: *const PioReactivePower,
14307    error: *mut *mut PioError,
14308) -> *mut PioOperatingPointUpdate {
14309    unsafe {
14310        entry(error, std::ptr::null_mut(), || {
14311            Ok(PioOperatingPointUpdate::new_raw(
14312                OperatingPointUpdate::GeneratorReactivePower {
14313                    generator: component_clone(generator)?,
14314                    terminal: optional_owned_string(terminal, terminal_len, "terminal")?,
14315                    q: reactive_power_copy(power)?,
14316                },
14317            ))
14318        })
14319    }
14320}
14321
14322#[unsafe(no_mangle)]
14323pub unsafe extern "C" fn pio_operating_point_update_set_generator_voltage_magnitude(
14324    generator: *const PioComponentId,
14325    voltage_magnitude_per_unit: f64,
14326    error: *mut *mut PioError,
14327) -> *mut PioOperatingPointUpdate {
14328    unsafe {
14329        entry(error, std::ptr::null_mut(), || {
14330            Ok(PioOperatingPointUpdate::new_raw(
14331                OperatingPointUpdate::GeneratorVoltageMagnitude {
14332                    generator: component_clone(generator)?,
14333                    vm_pu: voltage_magnitude_per_unit,
14334                },
14335            ))
14336        })
14337    }
14338}
14339
14340#[unsafe(no_mangle)]
14341pub unsafe extern "C" fn pio_operating_point_update_set_generator_in_service(
14342    generator: *const PioComponentId,
14343    in_service: bool,
14344    error: *mut *mut PioError,
14345) -> *mut PioOperatingPointUpdate {
14346    unsafe {
14347        entry(error, std::ptr::null_mut(), || {
14348            Ok(PioOperatingPointUpdate::new_raw(
14349                OperatingPointUpdate::GeneratorInService {
14350                    generator: component_clone(generator)?,
14351                    in_service,
14352                },
14353            ))
14354        })
14355    }
14356}
14357
14358#[unsafe(no_mangle)]
14359pub unsafe extern "C" fn pio_operating_point_update_set_branch_in_service(
14360    branch: *const PioComponentId,
14361    in_service: bool,
14362    error: *mut *mut PioError,
14363) -> *mut PioOperatingPointUpdate {
14364    unsafe {
14365        entry(error, std::ptr::null_mut(), || {
14366            Ok(PioOperatingPointUpdate::new_raw(
14367                OperatingPointUpdate::BranchInService {
14368                    branch: component_clone(branch)?,
14369                    in_service,
14370                },
14371            ))
14372        })
14373    }
14374}
14375
14376#[unsafe(no_mangle)]
14377pub unsafe extern "C" fn pio_operating_point_update_set_transformer_tap_ratio(
14378    transformer: *const PioComponentId,
14379    tap_ratio: f64,
14380    error: *mut *mut PioError,
14381) -> *mut PioOperatingPointUpdate {
14382    unsafe {
14383        entry(error, std::ptr::null_mut(), || {
14384            Ok(PioOperatingPointUpdate::new_raw(
14385                OperatingPointUpdate::TransformerTapRatio {
14386                    transformer: component_clone(transformer)?,
14387                    tap_ratio,
14388                },
14389            ))
14390        })
14391    }
14392}
14393
14394#[unsafe(no_mangle)]
14395pub unsafe extern "C" fn pio_operating_point_update_set_transformer_phase_shift_degrees(
14396    transformer: *const PioComponentId,
14397    phase_shift_degrees: f64,
14398    error: *mut *mut PioError,
14399) -> *mut PioOperatingPointUpdate {
14400    unsafe {
14401        entry(error, std::ptr::null_mut(), || {
14402            Ok(PioOperatingPointUpdate::new_raw(
14403                OperatingPointUpdate::TransformerPhaseShift {
14404                    transformer: component_clone(transformer)?,
14405                    shift_degrees: phase_shift_degrees,
14406                },
14407            ))
14408        })
14409    }
14410}
14411
14412#[unsafe(no_mangle)]
14413pub unsafe extern "C" fn pio_operating_point_update_set_switch_closed(
14414    switch_id: *const PioComponentId,
14415    closed: bool,
14416    error: *mut *mut PioError,
14417) -> *mut PioOperatingPointUpdate {
14418    unsafe {
14419        entry(error, std::ptr::null_mut(), || {
14420            Ok(PioOperatingPointUpdate::new_raw(
14421                OperatingPointUpdate::SwitchClosed {
14422                    switch: component_clone(switch_id)?,
14423                    closed,
14424                },
14425            ))
14426        })
14427    }
14428}
14429
14430#[unsafe(no_mangle)]
14431pub unsafe extern "C" fn pio_network_update_set_branch_thermal_rating(
14432    branch: *const PioComponentId,
14433    terminal: *const c_char,
14434    terminal_len: usize,
14435    rating: *const PioApparentPower,
14436    error: *mut *mut PioError,
14437) -> *mut PioNetworkUpdate {
14438    unsafe {
14439        entry(error, std::ptr::null_mut(), || {
14440            Ok(PioNetworkUpdate::new_raw(
14441                NetworkUpdate::BranchThermalRating {
14442                    branch: component_clone(branch)?,
14443                    terminal: optional_owned_string(terminal, terminal_len, "terminal")?,
14444                    rating: apparent_power_copy(rating)?,
14445                },
14446            ))
14447        })
14448    }
14449}
14450
14451#[unsafe(no_mangle)]
14452pub unsafe extern "C" fn pio_calculation_update_from_operating_point(
14453    update: *const PioOperatingPointUpdate,
14454    error: *mut *mut PioError,
14455) -> *mut PioCalculationUpdate {
14456    unsafe {
14457        entry(error, std::ptr::null_mut(), || {
14458            let update = PioOperatingPointUpdate::get(update)
14459                .ok_or_else(|| {
14460                    boundary_error(
14461                        &codes::BIND_CAPI_NULL_HANDLE,
14462                        "PioOperatingPointUpdate must not be NULL",
14463                    )
14464                })?
14465                .clone();
14466            Ok(PioCalculationUpdate::new_raw(
14467                CalculationUpdate::OperatingPoint(update),
14468            ))
14469        })
14470    }
14471}
14472
14473#[unsafe(no_mangle)]
14474pub unsafe extern "C" fn pio_calculation_update_from_network(
14475    update: *const PioNetworkUpdate,
14476    error: *mut *mut PioError,
14477) -> *mut PioCalculationUpdate {
14478    unsafe {
14479        entry(error, std::ptr::null_mut(), || {
14480            let update = PioNetworkUpdate::get(update)
14481                .ok_or_else(|| {
14482                    boundary_error(
14483                        &codes::BIND_CAPI_NULL_HANDLE,
14484                        "PioNetworkUpdate must not be NULL",
14485                    )
14486                })?
14487                .clone();
14488            Ok(PioCalculationUpdate::new_raw(CalculationUpdate::Network(
14489                update,
14490            )))
14491        })
14492    }
14493}
14494
14495#[unsafe(no_mangle)]
14496pub unsafe extern "C" fn pio_operating_point_update_retain(
14497    update: *const PioOperatingPointUpdate,
14498) -> *mut PioOperatingPointUpdate {
14499    unsafe { PioOperatingPointUpdate::retain_raw(update) }
14500}
14501
14502#[unsafe(no_mangle)]
14503pub unsafe extern "C" fn pio_operating_point_update_release(update: *mut PioOperatingPointUpdate) {
14504    unsafe { PioOperatingPointUpdate::release_raw(update) };
14505}
14506
14507#[unsafe(no_mangle)]
14508pub unsafe extern "C" fn pio_network_update_retain(
14509    update: *const PioNetworkUpdate,
14510) -> *mut PioNetworkUpdate {
14511    unsafe { PioNetworkUpdate::retain_raw(update) }
14512}
14513
14514#[unsafe(no_mangle)]
14515pub unsafe extern "C" fn pio_network_update_release(update: *mut PioNetworkUpdate) {
14516    unsafe { PioNetworkUpdate::release_raw(update) };
14517}
14518
14519#[unsafe(no_mangle)]
14520pub unsafe extern "C" fn pio_calculation_update_retain(
14521    update: *const PioCalculationUpdate,
14522) -> *mut PioCalculationUpdate {
14523    unsafe { PioCalculationUpdate::retain_raw(update) }
14524}
14525
14526#[unsafe(no_mangle)]
14527pub unsafe extern "C" fn pio_calculation_update_release(update: *mut PioCalculationUpdate) {
14528    unsafe { PioCalculationUpdate::release_raw(update) };
14529}
14530
14531fn append_update_report(output: &mut UpdateReportInner, report: powerio_prob::UpdateReport) {
14532    output.connectivity_changed |= report.connectivity_changed();
14533    output.changes.extend_from_slice(report.changes());
14534}
14535
14536fn apply_bus_load_to_typed_module<T>(
14537    module: &mut powerio::PioModule<PioValue>,
14538    bus: powerio_tx::BusId,
14539    total: ActivePower,
14540    allocation: LoadAllocation,
14541    narrow: impl FnOnce(PioValue) -> Result<T, PioValue>,
14542    wrap: impl FnOnce(T) -> PioValue,
14543) -> Result<UpdateReportInner, powerio_core::Error>
14544where
14545    T: powerio_prob::BalancedCalculationInstance,
14546{
14547    let mut typed = module.clone().__try_map_value(narrow).map_err(|value| {
14548        powerio_core::Error::new(
14549            &codes::REQUEST_CAPI_TYPE_MISMATCH,
14550            format!(
14551                "{} does not accept aggregate bus active demand",
14552                value.value().type_name()
14553            ),
14554        )
14555    })?;
14556    let report = apply_bus_load_active_power(&mut typed, bus, total, allocation)?;
14557    *module = typed.map_value(wrap);
14558    let mut output = UpdateReportInner {
14559        changes: Vec::new(),
14560        connectivity_changed: false,
14561    };
14562    append_update_report(&mut output, report);
14563    Ok(output)
14564}
14565
14566// Recoverable narrowing returns the original dynamic value on a type mismatch,
14567// preserving the module records without serializing or cloning the value again.
14568#[allow(clippy::result_large_err)]
14569fn apply_dynamic_bus_load_active_power(
14570    module: &mut powerio::PioModule<PioValue>,
14571    bus: powerio_tx::BusId,
14572    total: ActivePower,
14573    allocation: LoadAllocation,
14574) -> Result<UpdateReportInner, powerio_core::Error> {
14575    match &module.value() {
14576        PioValue::DcPfInstance(_) => apply_bus_load_to_typed_module(
14577            module,
14578            bus,
14579            total,
14580            allocation,
14581            |value| match value {
14582                PioValue::DcPfInstance(instance) => Ok(instance),
14583                other => Err(other),
14584            },
14585            PioValue::DcPfInstance,
14586        ),
14587        PioValue::AcPfInstance(_) => apply_bus_load_to_typed_module(
14588            module,
14589            bus,
14590            total,
14591            allocation,
14592            |value| match value {
14593                PioValue::AcPfInstance(instance) => Ok(instance),
14594                other => Err(other),
14595            },
14596            PioValue::AcPfInstance,
14597        ),
14598        PioValue::DcOpfInstance(_) => apply_bus_load_to_typed_module(
14599            module,
14600            bus,
14601            total,
14602            allocation,
14603            |value| match value {
14604                PioValue::DcOpfInstance(instance) => Ok(instance),
14605                other => Err(other),
14606            },
14607            PioValue::DcOpfInstance,
14608        ),
14609        PioValue::AcOpfInstance(_) => apply_bus_load_to_typed_module(
14610            module,
14611            bus,
14612            total,
14613            allocation,
14614            |value| match value {
14615                PioValue::AcOpfInstance(instance) => Ok(instance),
14616                other => Err(other),
14617            },
14618            PioValue::AcOpfInstance,
14619        ),
14620        value => Err(powerio_core::Error::new(
14621            &codes::REQUEST_CAPI_TYPE_MISMATCH,
14622            format!(
14623                "{} does not accept aggregate bus active demand",
14624                value.type_name()
14625            ),
14626        )),
14627    }
14628}
14629
14630fn parse_load_allocation(value: &str) -> Result<LoadAllocation, *mut PioError> {
14631    match value {
14632        "equal" => Ok(LoadAllocation::Equal),
14633        "proportional_to_current_active_power" => {
14634            Ok(LoadAllocation::ProportionalToCurrentActivePower)
14635        }
14636        other => Err(boundary_error(
14637            &codes::REQUEST_CAPI_ALLOCATION_UNKNOWN,
14638            format!("unknown load allocation rule '{other}'"),
14639        )),
14640    }
14641}
14642
14643fn separated_updates(
14644    updates: &[CalculationUpdate],
14645) -> (Vec<OperatingPointUpdate>, Vec<NetworkUpdate>) {
14646    let mut operating = Vec::new();
14647    let mut network = Vec::new();
14648    for update in updates {
14649        match update {
14650            CalculationUpdate::OperatingPoint(update) => operating.push(update.clone()),
14651            CalculationUpdate::Network(update) => network.push(update.clone()),
14652            _ => unreachable!("unsupported calculation update from this PowerIO build"),
14653        }
14654    }
14655    (operating, network)
14656}
14657
14658fn reject_network_updates(
14659    updates: &[CalculationUpdate],
14660) -> Result<Vec<OperatingPointUpdate>, powerio_core::Error> {
14661    updates
14662        .iter()
14663        .map(|update| match update {
14664            CalculationUpdate::OperatingPoint(update) => Ok(update.clone()),
14665            CalculationUpdate::Network(_) => Err(powerio_core::Error::new(
14666                &codes::REQUEST_CAPI_TYPE_MISMATCH,
14667                "a network update cannot be applied to an operating point",
14668            )),
14669            _ => unreachable!("unsupported calculation update from this PowerIO build"),
14670        })
14671        .collect()
14672}
14673
14674fn apply_dynamic_updates(
14675    value: &mut PioValue,
14676    updates: &[CalculationUpdate],
14677) -> Result<UpdateReportInner, powerio_core::Error> {
14678    let mut output = UpdateReportInner {
14679        changes: Vec::new(),
14680        connectivity_changed: false,
14681    };
14682    match value {
14683        PioValue::BalancedNetwork(network) => {
14684            let (operating, physical) = separated_updates(updates);
14685            append_update_report(&mut output, apply_updates(network, &operating)?);
14686            append_update_report(&mut output, apply_updates(network, &physical)?);
14687        }
14688        PioValue::MulticonductorNetwork(network) => {
14689            let (operating, physical) = separated_updates(updates);
14690            append_update_report(&mut output, apply_updates(network, &operating)?);
14691            append_update_report(&mut output, apply_updates(network, &physical)?);
14692        }
14693        PioValue::BalancedOperatingPoint(point) => {
14694            let operating = reject_network_updates(updates)?;
14695            append_update_report(&mut output, apply_updates(point, &operating)?);
14696        }
14697        PioValue::MulticonductorOperatingPoint(point) => {
14698            let operating = reject_network_updates(updates)?;
14699            append_update_report(&mut output, apply_updates(point, &operating)?);
14700        }
14701        PioValue::DcPfInstance(instance) => {
14702            append_update_report(&mut output, apply_updates(instance, updates)?);
14703        }
14704        PioValue::AcPfInstance(instance) => {
14705            append_update_report(&mut output, apply_updates(instance, updates)?);
14706        }
14707        PioValue::DcOpfInstance(instance) => {
14708            append_update_report(&mut output, apply_updates(instance, updates)?);
14709        }
14710        PioValue::AcOpfInstance(instance) => {
14711            append_update_report(&mut output, apply_updates(instance, updates)?);
14712        }
14713        PioValue::McAcPfInstance(instance) => {
14714            append_update_report(&mut output, apply_updates(instance, updates)?);
14715        }
14716        PioValue::McAcOpfInstance(instance) => {
14717            append_update_report(&mut output, apply_updates(instance, updates)?);
14718        }
14719        _ => {
14720            return Err(powerio_core::Error::new(
14721                &codes::REQUEST_CAPI_TYPE_MISMATCH,
14722                format!("{} does not accept calculation updates", value.type_name()),
14723            ));
14724        }
14725    }
14726    Ok(output)
14727}
14728
14729fn update_history_id(
14730    module: &powerio::PioModule<PioValue>,
14731) -> Result<HistoryId, powerio_core::Error> {
14732    let mut suffix = 1usize;
14733    loop {
14734        let value = if suffix == 1 {
14735            "apply-updates".to_owned()
14736        } else {
14737            format!("apply-updates-{suffix}")
14738        };
14739        if module
14740            .history()
14741            .iter()
14742            .all(|entry| entry.id().as_str() != value)
14743        {
14744            return HistoryId::new(value);
14745        }
14746        suffix += 1;
14747    }
14748}
14749
14750fn apply_module_updates(
14751    module: &mut powerio::PioModule<PioValue>,
14752    updates: &[CalculationUpdate],
14753) -> Result<UpdateReportInner, powerio_core::Error> {
14754    let history_id = update_history_id(module)?;
14755    let mut edit = module.stage_edit();
14756    let report = apply_dynamic_updates(edit.value_mut(), updates)?;
14757    if report.changes.is_empty() {
14758        return Ok(report);
14759    }
14760
14761    let mut parameters = std::collections::BTreeMap::new();
14762    parameters.insert(
14763        "updates".to_owned(),
14764        serde_json::to_value(updates).map_err(|failure| {
14765            powerio_core::Error::new(
14766                &codes::EMIT_CAPI_SERIALIZE_FAILED,
14767                format!("cannot record applied updates: {failure}"),
14768            )
14769        })?,
14770    );
14771    parameters.insert(
14772        "changes".to_owned(),
14773        serde_json::to_value(&report.changes).map_err(|failure| {
14774            powerio_core::Error::new(
14775                &codes::EMIT_CAPI_SERIALIZE_FAILED,
14776                format!("cannot record update changes: {failure}"),
14777            )
14778        })?,
14779    );
14780    parameters.insert(
14781        "connectivity_changed".to_owned(),
14782        serde_json::Value::Bool(report.connectivity_changed),
14783    );
14784    let history = HistoryEntry::new(history_id, HistoryKind::Edit, "apply_updates")?
14785        .with_parameters(parameters)?;
14786    edit.commit(Producer::new("powerio-capi", powerio::VERSION)?, history)?;
14787    Ok(report)
14788}
14789
14790unsafe fn module_make_mut<'a>(
14791    module: *mut PioModule,
14792) -> Result<&'a mut ModuleInner, *mut PioError> {
14793    let handle = unsafe { module.cast::<HandleBox<ModuleInner>>().as_mut() }.ok_or_else(|| {
14794        boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
14795    })?;
14796    Ok(Arc::make_mut(&mut handle.inner))
14797}
14798
14799/// Apply a complete typed update batch atomically.
14800///
14801/// Owner rooted handles obtained before the call (values, networks, collection
14802/// entries, artifacts) keep the pre-update module alive. Plain view structs read
14803/// from this module handle (`PioStringView`, `PioModuleSourceView`, history and
14804/// source map views) are invalidated by a successful call and must be read
14805/// again. The caller must hold exclusive access to `module` for the duration of
14806/// the call: no concurrent call of any kind on this handle, including retain.
14807#[unsafe(no_mangle)]
14808pub unsafe extern "C" fn pio_apply_updates(
14809    module: *mut PioModule,
14810    updates: *const *const PioCalculationUpdate,
14811    updates_len: usize,
14812    error: *mut *mut PioError,
14813) -> *mut PioUpdateReport {
14814    unsafe {
14815        entry(error, std::ptr::null_mut(), || {
14816            if updates.is_null() && updates_len != 0 {
14817                return Err(boundary_error(
14818                    &codes::BIND_CAPI_NULL_ARGUMENT,
14819                    "updates is NULL with a nonzero length",
14820                ));
14821            }
14822            let handles = if updates_len == 0 {
14823                &[]
14824            } else {
14825                std::slice::from_raw_parts(updates, updates_len)
14826            };
14827            let mut values = Vec::with_capacity(handles.len());
14828            for (index, handle) in handles.iter().copied().enumerate() {
14829                let update = PioCalculationUpdate::get(handle).ok_or_else(|| {
14830                    boundary_error(
14831                        &codes::BIND_CAPI_NULL_HANDLE,
14832                        format!("updates[{index}] must not be NULL"),
14833                    )
14834                })?;
14835                values.push(update.clone());
14836            }
14837            let module = module_make_mut(module)?;
14838            apply_module_updates(&mut module.module, &values)
14839                .map(PioUpdateReport::new_raw)
14840                .map_err(|failure| error_from_core(&failure))
14841        })
14842    }
14843}
14844
14845/// Replace aggregate active demand at one bus using the named allocation rule.
14846///
14847/// The same view invalidation and exclusivity rules as `pio_apply_updates`
14848/// apply.
14849#[unsafe(no_mangle)]
14850pub unsafe extern "C" fn pio_apply_bus_load_active_power(
14851    module: *mut PioModule,
14852    bus_id: usize,
14853    power: *const PioActivePower,
14854    allocation: *const c_char,
14855    allocation_len: usize,
14856    error: *mut *mut PioError,
14857) -> *mut PioUpdateReport {
14858    unsafe {
14859        entry(error, std::ptr::null_mut(), || {
14860            let total = active_power_copy(power)?;
14861            let allocation = required_str(allocation, allocation_len, "allocation")?;
14862            let allocation = parse_load_allocation(allocation)?;
14863            let module = module_make_mut(module)?;
14864            apply_dynamic_bus_load_active_power(
14865                &mut module.module,
14866                powerio_tx::BusId::new(bus_id),
14867                total,
14868                allocation,
14869            )
14870            .map(PioUpdateReport::new_raw)
14871            .map_err(|failure| error_from_core(&failure))
14872        })
14873    }
14874}
14875
14876#[unsafe(no_mangle)]
14877pub unsafe extern "C" fn pio_update_report_len(report: *const PioUpdateReport) -> usize {
14878    unsafe { PioUpdateReport::get(report) }.map_or(0, |report| report.changes.len())
14879}
14880
14881#[unsafe(no_mangle)]
14882pub unsafe extern "C" fn pio_update_report_connectivity_changed(
14883    report: *const PioUpdateReport,
14884) -> bool {
14885    unsafe { PioUpdateReport::get(report) }.is_some_and(|report| report.connectivity_changed)
14886}
14887
14888#[unsafe(no_mangle)]
14889pub unsafe extern "C" fn pio_update_report_change(
14890    report: *const PioUpdateReport,
14891    index: usize,
14892    error: *mut *mut PioError,
14893) -> *mut PioUpdateChange {
14894    unsafe {
14895        entry(error, std::ptr::null_mut(), || {
14896            let owner = PioUpdateReport::arc(report).ok_or_else(|| {
14897                boundary_error(
14898                    &codes::BIND_CAPI_NULL_HANDLE,
14899                    "PioUpdateReport must not be NULL",
14900                )
14901            })?;
14902            if index >= owner.changes.len() {
14903                return Err(boundary_error(
14904                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
14905                    format!("update change index {index} is out of range"),
14906                ));
14907            }
14908            Ok(PioUpdateChange::new_raw(UpdateChangeInner { owner, index }))
14909        })
14910    }
14911}
14912
14913#[unsafe(no_mangle)]
14914pub unsafe extern "C" fn pio_update_change_component_id(
14915    change: *const PioUpdateChange,
14916) -> *mut PioComponentId {
14917    unsafe { PioUpdateChange::get(change) }
14918        .and_then(UpdateChangeInner::change)
14919        .map_or(std::ptr::null_mut(), |change| {
14920            PioComponentId::new_raw(change.component_id().clone())
14921        })
14922}
14923
14924fn updated_field_name(field: UpdatedField) -> &'static str {
14925    match field {
14926        UpdatedField::LoadActivePower => "load_active_power",
14927        UpdatedField::LoadReactivePower => "load_reactive_power",
14928        UpdatedField::GeneratorActivePower => "generator_active_power",
14929        UpdatedField::GeneratorReactivePower => "generator_reactive_power",
14930        UpdatedField::GeneratorVoltageMagnitude => "generator_voltage_magnitude",
14931        UpdatedField::GeneratorInService => "generator_in_service",
14932        UpdatedField::BranchThermalRating => "branch_thermal_rating",
14933        UpdatedField::BranchInService => "branch_in_service",
14934        UpdatedField::TransformerTapRatio => "transformer_tap_ratio",
14935        UpdatedField::TransformerPhaseShift => "transformer_phase_shift",
14936        UpdatedField::SwitchClosed => "switch_closed",
14937        _ => "unknown",
14938    }
14939}
14940
14941#[unsafe(no_mangle)]
14942pub unsafe extern "C" fn pio_update_change_field(change: *const PioUpdateChange) -> PioStringView {
14943    unsafe { PioUpdateChange::get(change) }
14944        .and_then(UpdateChangeInner::change)
14945        .map_or(PioStringView::EMPTY, |change| {
14946            PioStringView::new(updated_field_name(change.field()))
14947        })
14948}
14949
14950#[unsafe(no_mangle)]
14951pub unsafe extern "C" fn pio_update_change_terminal(
14952    change: *const PioUpdateChange,
14953) -> PioStringView {
14954    unsafe { PioUpdateChange::get(change) }
14955        .and_then(UpdateChangeInner::change)
14956        .and_then(UpdateChange::terminal)
14957        .map_or(PioStringView::EMPTY, PioStringView::new)
14958}
14959
14960#[unsafe(no_mangle)]
14961pub unsafe extern "C" fn pio_update_report_retain(
14962    report: *const PioUpdateReport,
14963) -> *mut PioUpdateReport {
14964    unsafe { PioUpdateReport::retain_raw(report) }
14965}
14966
14967#[unsafe(no_mangle)]
14968pub unsafe extern "C" fn pio_update_report_release(report: *mut PioUpdateReport) {
14969    unsafe { PioUpdateReport::release_raw(report) };
14970}
14971
14972#[unsafe(no_mangle)]
14973pub unsafe extern "C" fn pio_update_change_retain(
14974    change: *const PioUpdateChange,
14975) -> *mut PioUpdateChange {
14976    unsafe { PioUpdateChange::retain_raw(change) }
14977}
14978
14979#[unsafe(no_mangle)]
14980pub unsafe extern "C" fn pio_update_change_release(change: *mut PioUpdateChange) {
14981    unsafe { PioUpdateChange::release_raw(change) };
14982}
14983
14984// ---- emission --------------------------------------------------------------
14985
14986struct ArtifactRecord {
14987    name: String,
14988    bytes: Option<Vec<u8>>,
14989}
14990
14991struct EmitResultInner {
14992    layout: &'static str,
14993    fidelity: &'static str,
14994    artifacts: Vec<ArtifactRecord>,
14995    diagnostics: Arc<DiagnosticsInner>,
14996}
14997
14998struct ArtifactInner {
14999    owner: Arc<EmitResultInner>,
15000    index: usize,
15001}
15002
15003impl ArtifactInner {
15004    fn artifact(&self) -> Option<&ArtifactRecord> {
15005        self.owner.artifacts.get(self.index)
15006    }
15007}
15008
15009opaque_handle!(
15010    /// Completed artifact inventory and emission diagnostics.
15011    PioEmitResult,
15012    EmitResultInner
15013);
15014opaque_handle!(
15015    /// Owner-rooted emitted artifact.
15016    PioArtifact,
15017    ArtifactInner
15018);
15019
15020fn emit_result_handle(result: EmitResult) -> *mut PioEmitResult {
15021    let layout = match result.layout() {
15022        powerio::OutputLayout::File => "file",
15023        powerio::OutputLayout::Directory => "directory",
15024    };
15025    let fidelity = match result.fidelity() {
15026        powerio::Fidelity::ExactSameFormat => "exact_same_format",
15027        powerio::Fidelity::Canonical => "canonical",
15028    };
15029    let diagnostics = Arc::new(DiagnosticsInner {
15030        owner: DiagnosticsOwner::Owned(result.diagnostics().to_vec()),
15031    });
15032    let artifacts = match result.into_output() {
15033        EmittedOutput::Memory { artifacts } => artifacts
15034            .into_iter()
15035            .map(|artifact| {
15036                let name = artifact.name().as_str().to_owned();
15037                ArtifactRecord {
15038                    name,
15039                    bytes: Some(artifact.into_bytes()),
15040                }
15041            })
15042            .collect(),
15043        EmittedOutput::Path { artifacts, .. } => artifacts
15044            .into_iter()
15045            .map(|path| ArtifactRecord {
15046                name: path.to_string_lossy().into_owned(),
15047                bytes: None,
15048            })
15049            .collect(),
15050        _ => unreachable!("unsupported emitted output from this PowerIO build"),
15051    };
15052    PioEmitResult::new_raw(EmitResultInner {
15053        layout,
15054        fidelity,
15055        artifacts,
15056        diagnostics,
15057    })
15058}
15059
15060unsafe fn run_output_operation(
15061    module: *const PioModule,
15062    destination: *const PioDestination,
15063    error: *mut *mut PioError,
15064    operation: impl FnOnce(
15065        &powerio::PioModule<PioValue>,
15066        Destination,
15067    ) -> Result<EmitResult, *mut PioError>,
15068) -> *mut PioEmitResult {
15069    unsafe {
15070        entry(error, std::ptr::null_mut(), || {
15071            let module = PioModule::get(module).ok_or_else(|| {
15072                boundary_error(&codes::BIND_CAPI_NULL_HANDLE, "PioModule must not be NULL")
15073            })?;
15074            let destination = PioDestination::get(destination).ok_or_else(|| {
15075                boundary_error(
15076                    &codes::BIND_CAPI_NULL_HANDLE,
15077                    "PioDestination must not be NULL",
15078                )
15079            })?;
15080            let destination = destination
15081                .build()
15082                .map_err(|failure| error_from_core(&failure))?;
15083            operation(&module.module, destination).map(emit_result_handle)
15084        })
15085    }
15086}
15087
15088/// Emit one module as a grid exchange format.
15089#[unsafe(no_mangle)]
15090pub unsafe extern "C" fn pio_emit(
15091    module: *const PioModule,
15092    format: *const c_char,
15093    format_len: usize,
15094    destination: *const PioDestination,
15095    error: *mut *mut PioError,
15096) -> *mut PioEmitResult {
15097    unsafe {
15098        run_output_operation(module, destination, error, |module, destination| {
15099            let format = required_str(format, format_len, "format")?;
15100            powerio::emit(module, format, destination).map_err(|failure| error_from_core(&failure))
15101        })
15102    }
15103}
15104
15105/// Serialize one module as PowerIO IR.
15106#[unsafe(no_mangle)]
15107pub unsafe extern "C" fn pio_module_serialize(
15108    module: *const PioModule,
15109    destination: *const PioDestination,
15110    error: *mut *mut PioError,
15111) -> *mut PioEmitResult {
15112    unsafe {
15113        run_output_operation(module, destination, error, |module, destination| {
15114            powerio::serialize(module, destination).map_err(|failure| error_from_core(&failure))
15115        })
15116    }
15117}
15118
15119#[unsafe(no_mangle)]
15120pub unsafe extern "C" fn pio_emit_result_layout(result: *const PioEmitResult) -> PioStringView {
15121    unsafe { PioEmitResult::get(result) }.map_or(PioStringView::EMPTY, |result| {
15122        PioStringView::new(result.layout)
15123    })
15124}
15125
15126#[unsafe(no_mangle)]
15127pub unsafe extern "C" fn pio_emit_result_fidelity(result: *const PioEmitResult) -> PioStringView {
15128    unsafe { PioEmitResult::get(result) }.map_or(PioStringView::EMPTY, |result| {
15129        PioStringView::new(result.fidelity)
15130    })
15131}
15132
15133#[unsafe(no_mangle)]
15134pub unsafe extern "C" fn pio_emit_result_artifact_count(result: *const PioEmitResult) -> usize {
15135    unsafe { PioEmitResult::get(result) }.map_or(0, |result| result.artifacts.len())
15136}
15137
15138#[unsafe(no_mangle)]
15139pub unsafe extern "C" fn pio_emit_result_artifact(
15140    result: *const PioEmitResult,
15141    index: usize,
15142    error: *mut *mut PioError,
15143) -> *mut PioArtifact {
15144    unsafe {
15145        entry(error, std::ptr::null_mut(), || {
15146            let owner = PioEmitResult::arc(result).ok_or_else(|| {
15147                boundary_error(
15148                    &codes::BIND_CAPI_NULL_HANDLE,
15149                    "PioEmitResult must not be NULL",
15150                )
15151            })?;
15152            if index >= owner.artifacts.len() {
15153                return Err(boundary_error(
15154                    &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
15155                    format!("artifact index {index} is out of range"),
15156                ));
15157            }
15158            Ok(PioArtifact::new_raw(ArtifactInner { owner, index }))
15159        })
15160    }
15161}
15162
15163#[unsafe(no_mangle)]
15164pub unsafe extern "C" fn pio_emit_result_diagnostics(
15165    result: *const PioEmitResult,
15166) -> *mut PioDiagnostics {
15167    unsafe { PioEmitResult::get(result) }.map_or(std::ptr::null_mut(), |result| {
15168        PioDiagnostics::from_arc(Arc::clone(&result.diagnostics))
15169    })
15170}
15171
15172#[unsafe(no_mangle)]
15173pub unsafe extern "C" fn pio_emit_result_retain(
15174    result: *const PioEmitResult,
15175) -> *mut PioEmitResult {
15176    unsafe { PioEmitResult::retain_raw(result) }
15177}
15178
15179#[unsafe(no_mangle)]
15180pub unsafe extern "C" fn pio_emit_result_release(result: *mut PioEmitResult) {
15181    unsafe { PioEmitResult::release_raw(result) };
15182}
15183
15184#[unsafe(no_mangle)]
15185pub unsafe extern "C" fn pio_artifact_name(artifact: *const PioArtifact) -> PioStringView {
15186    unsafe { PioArtifact::get(artifact) }
15187        .and_then(ArtifactInner::artifact)
15188        .map_or(PioStringView::EMPTY, |artifact| {
15189            PioStringView::new(&artifact.name)
15190        })
15191}
15192
15193/// Return emitted memory bytes. A path destination has no memory bytes and
15194/// returns an empty view.
15195#[unsafe(no_mangle)]
15196pub unsafe extern "C" fn pio_artifact_bytes(artifact: *const PioArtifact) -> PioByteView {
15197    unsafe { PioArtifact::get(artifact) }
15198        .and_then(ArtifactInner::artifact)
15199        .and_then(|artifact| artifact.bytes.as_deref())
15200        .map_or(PioByteView::EMPTY, PioByteView::new)
15201}
15202
15203#[unsafe(no_mangle)]
15204pub unsafe extern "C" fn pio_artifact_retain(artifact: *const PioArtifact) -> *mut PioArtifact {
15205    unsafe { PioArtifact::retain_raw(artifact) }
15206}
15207
15208#[unsafe(no_mangle)]
15209pub unsafe extern "C" fn pio_artifact_release(artifact: *mut PioArtifact) {
15210    unsafe { PioArtifact::release_raw(artifact) };
15211}
15212
15213// ---- sparse matrices and vectors ------------------------------------------
15214
15215struct SparseMatrixInner {
15216    rows: usize,
15217    columns: usize,
15218    row_offsets: Vec<usize>,
15219    column_indices: Vec<usize>,
15220    values: Vec<f64>,
15221}
15222
15223impl From<SparseMatrix> for SparseMatrixInner {
15224    fn from(matrix: SparseMatrix) -> Self {
15225        let matrix = matrix.to_csr();
15226        Self {
15227            rows: matrix.rows(),
15228            columns: matrix.cols(),
15229            row_offsets: matrix.indptr().raw_storage().to_vec(),
15230            column_indices: matrix.indices().to_vec(),
15231            values: matrix.data().to_vec(),
15232        }
15233    }
15234}
15235
15236struct VectorInner {
15237    values: Vec<f64>,
15238}
15239
15240opaque_handle!(
15241    /// Immutable CSR sparse matrix.
15242    PioSparseMatrix,
15243    SparseMatrixInner
15244);
15245opaque_handle!(
15246    /// Immutable dense vector.
15247    PioVector,
15248    VectorInner
15249);
15250
15251fn termination_name(termination: &powerio_prob::Termination) -> &'static str {
15252    use powerio_prob::Termination;
15253    match termination {
15254        Termination::Converged => "converged",
15255        Termination::IterationLimit => "iteration_limit",
15256        Termination::Infeasible => "infeasible",
15257        Termination::Unbounded => "unbounded",
15258        Termination::Failed => "failed",
15259        Termination::NotReported => "not_reported",
15260        _ => "not_reported",
15261    }
15262}
15263
15264#[unsafe(no_mangle)]
15265pub unsafe extern "C" fn pio_calculation_solution_termination(
15266    solution: *const PioCalculationSolution,
15267) -> PioStringView {
15268    let Some(solution) =
15269        (unsafe { PioCalculationSolution::get(solution) }).and_then(ValueInner::value)
15270    else {
15271        return PioStringView::EMPTY;
15272    };
15273    let termination = match solution {
15274        PioValue::DcPfSolution(solution) => solution.termination(),
15275        PioValue::AcPfSolution(solution) => solution.termination(),
15276        PioValue::DcOpfSolution(solution) => solution.termination(),
15277        PioValue::AcOpfSolution(solution) => solution.termination(),
15278        PioValue::SocwrOpfSolution(solution) => solution.termination(),
15279        PioValue::McAcPfSolution(solution) => solution.termination(),
15280        PioValue::McAcOpfSolution(solution) => solution.termination(),
15281        PioValue::AcScucSolution(solution) => solution.termination(),
15282        _ => return PioStringView::EMPTY,
15283    };
15284    PioStringView::new(termination_name(termination))
15285}
15286
15287/// Return an OPF or SCUC objective. SOCWR reports a lower bound through
15288/// pio_socwr_opf_solution_get_objective_lower_bound instead.
15289#[unsafe(no_mangle)]
15290pub unsafe extern "C" fn pio_calculation_solution_get_objective(
15291    solution: *const PioCalculationSolution,
15292    out_objective: *mut f64,
15293) -> bool {
15294    if out_objective.is_null() {
15295        return false;
15296    }
15297    // No error slot: a missing objective is an ordinary false. `entry` still
15298    // turns a panic into false instead of aborting the caller.
15299    unsafe {
15300        entry(std::ptr::null_mut(), false, || {
15301            let Some(solution) = PioCalculationSolution::get(solution).and_then(ValueInner::value)
15302            else {
15303                return Ok(false);
15304            };
15305            let objective = match solution {
15306                PioValue::DcOpfSolution(solution) => Some(solution.objective()),
15307                PioValue::AcOpfSolution(solution) => Some(solution.objective()),
15308                PioValue::McAcOpfSolution(solution) => Some(solution.objective()),
15309                PioValue::AcScucSolution(solution) => solution.objective(),
15310                _ => None,
15311            };
15312            Ok(match objective {
15313                Some(objective) => {
15314                    *out_objective = objective;
15315                    true
15316                }
15317                None => false,
15318            })
15319        })
15320    }
15321}
15322
15323#[unsafe(no_mangle)]
15324pub unsafe extern "C" fn pio_socwr_opf_solution_get_objective_lower_bound(
15325    solution: *const PioCalculationSolution,
15326    out_lower_bound: *mut f64,
15327) -> bool {
15328    if out_lower_bound.is_null() {
15329        return false;
15330    }
15331    // No error slot, as for pio_calculation_solution_get_objective.
15332    unsafe {
15333        entry(std::ptr::null_mut(), false, || {
15334            let Some(PioValue::SocwrOpfSolution(solution)) =
15335                PioCalculationSolution::get(solution).and_then(ValueInner::value)
15336            else {
15337                return Ok(false);
15338            };
15339            *out_lower_bound = solution.objective_lower_bound();
15340            Ok(true)
15341        })
15342    }
15343}
15344
15345fn balanced_branch_identities(network: &BalancedNetwork) -> impl Iterator<Item = &str> {
15346    network
15347        .branches()
15348        .iter()
15349        .map(|branch| branch.uid.as_deref().expect("facade assigns component IDs"))
15350}
15351
15352fn balanced_generator_identities(network: &BalancedNetwork) -> impl Iterator<Item = &str> {
15353    network.generators().iter().map(|generator| {
15354        generator
15355            .uid
15356            .as_deref()
15357            .expect("facade assigns component IDs")
15358    })
15359}
15360
15361fn collect_solution_values(solution: &PioValue, quantity: &str) -> Result<Vec<f64>, *mut PioError> {
15362    let unknown = || {
15363        boundary_error(
15364            &codes::REQUEST_CAPI_QUANTITY_UNKNOWN,
15365            format!(
15366                "{} does not define solution quantity '{quantity}'",
15367                solution.type_name()
15368            ),
15369        )
15370    };
15371    let values = match solution {
15372        PioValue::DcPfSolution(solution) => match quantity {
15373            "bus_voltage_angle" => solution.bus_voltage_angles().to_vec(),
15374            "bus_active_injection" => solution.bus_active_injections().to_vec(),
15375            "branch_from_active_flow" => solution.branch_from_active_flows().to_vec(),
15376            "branch_to_active_flow" => solution.branch_to_active_flows().to_vec(),
15377            "generator_active_power" => solution
15378                .generator_dispatch()
15379                .map(|dispatch| dispatch.p_mw.clone())
15380                .ok_or_else(unknown)?,
15381            "generator_reactive_power" => solution
15382                .generator_dispatch()
15383                .map(|dispatch| dispatch.q_mvar.clone())
15384                .ok_or_else(unknown)?,
15385            _ => return Err(unknown()),
15386        },
15387        PioValue::AcPfSolution(solution) => match quantity {
15388            "bus_voltage_magnitude" => solution
15389                .network()
15390                .buses()
15391                .iter()
15392                .map(|bus| {
15393                    solution
15394                        .bus_voltage_magnitude(bus.id)
15395                        .expect("complete solution")
15396                })
15397                .collect(),
15398            "bus_voltage_angle" => solution
15399                .network()
15400                .buses()
15401                .iter()
15402                .map(|bus| {
15403                    solution
15404                        .bus_voltage_angle(bus.id)
15405                        .expect("complete solution")
15406                })
15407                .collect(),
15408            "bus_active_injection" => solution
15409                .network()
15410                .buses()
15411                .iter()
15412                .map(|bus| {
15413                    solution
15414                        .bus_active_injection(bus.id)
15415                        .expect("complete solution")
15416                })
15417                .collect(),
15418            "bus_reactive_injection" => solution
15419                .network()
15420                .buses()
15421                .iter()
15422                .map(|bus| {
15423                    solution
15424                        .bus_reactive_injection(bus.id)
15425                        .expect("complete solution")
15426                })
15427                .collect(),
15428            "branch_from_active_flow" => balanced_branch_identities(solution.network())
15429                .map(|id| {
15430                    solution
15431                        .branch_from_active_flow(id)
15432                        .expect("complete solution")
15433                })
15434                .collect(),
15435            "branch_from_reactive_flow" => balanced_branch_identities(solution.network())
15436                .map(|id| {
15437                    solution
15438                        .branch_from_reactive_flow(id)
15439                        .expect("complete solution")
15440                })
15441                .collect(),
15442            "branch_to_active_flow" => balanced_branch_identities(solution.network())
15443                .map(|id| {
15444                    solution
15445                        .branch_to_active_flow(id)
15446                        .expect("complete solution")
15447                })
15448                .collect(),
15449            "branch_to_reactive_flow" => balanced_branch_identities(solution.network())
15450                .map(|id| {
15451                    solution
15452                        .branch_to_reactive_flow(id)
15453                        .expect("complete solution")
15454                })
15455                .collect(),
15456            "generator_active_power" => solution
15457                .generator_dispatch()
15458                .map(|dispatch| dispatch.p_mw.clone())
15459                .ok_or_else(unknown)?,
15460            "generator_reactive_power" => solution
15461                .generator_dispatch()
15462                .map(|dispatch| dispatch.q_mvar.clone())
15463                .ok_or_else(unknown)?,
15464            _ => return Err(unknown()),
15465        },
15466        PioValue::DcOpfSolution(solution) => match quantity {
15467            "bus_voltage_angle" => solution
15468                .network()
15469                .buses()
15470                .iter()
15471                .map(|bus| {
15472                    solution
15473                        .bus_voltage_angle(bus.id)
15474                        .expect("complete solution")
15475                })
15476                .collect(),
15477            "bus_active_injection" => solution
15478                .network()
15479                .buses()
15480                .iter()
15481                .map(|bus| {
15482                    solution
15483                        .bus_active_injection(bus.id)
15484                        .expect("complete solution")
15485                })
15486                .collect(),
15487            "branch_from_active_flow" => balanced_branch_identities(solution.network())
15488                .map(|id| {
15489                    solution
15490                        .branch_from_active_flow(id)
15491                        .expect("complete solution")
15492                })
15493                .collect(),
15494            "branch_to_active_flow" => balanced_branch_identities(solution.network())
15495                .map(|id| {
15496                    solution
15497                        .branch_to_active_flow(id)
15498                        .expect("complete solution")
15499                })
15500                .collect(),
15501            "generator_active_power" => balanced_generator_identities(solution.network())
15502                .map(|id| {
15503                    solution
15504                        .generator_active_power(id)
15505                        .expect("complete solution")
15506                })
15507                .collect(),
15508            "bus_active_power_marginal" => solution
15509                .bus_active_power_marginals()
15510                .map(<[f64]>::to_vec)
15511                .ok_or_else(unknown)?,
15512            "branch_from_limit_multiplier" => solution
15513                .branch_from_limit_multipliers()
15514                .map(<[f64]>::to_vec)
15515                .ok_or_else(unknown)?,
15516            "branch_to_limit_multiplier" => solution
15517                .branch_to_limit_multipliers()
15518                .map(<[f64]>::to_vec)
15519                .ok_or_else(unknown)?,
15520            _ => return Err(unknown()),
15521        },
15522        PioValue::AcOpfSolution(solution) => match quantity {
15523            "bus_voltage_magnitude" => solution.bus_voltage_magnitudes().to_vec(),
15524            "bus_voltage_angle" => solution.bus_voltage_angles().to_vec(),
15525            "bus_active_injection" => solution.bus_active_injections().to_vec(),
15526            "bus_reactive_injection" => solution.bus_reactive_injections().to_vec(),
15527            "branch_from_active_flow" => solution.branch_from_active_flows().to_vec(),
15528            "branch_from_reactive_flow" => solution.branch_from_reactive_flows().to_vec(),
15529            "branch_to_active_flow" => solution.branch_to_active_flows().to_vec(),
15530            "branch_to_reactive_flow" => solution.branch_to_reactive_flows().to_vec(),
15531            "generator_active_power" => solution.generator_active_powers().to_vec(),
15532            "generator_reactive_power" => solution.generator_reactive_powers().to_vec(),
15533            "bus_active_power_marginal" => solution
15534                .bus_active_power_marginals()
15535                .map(<[f64]>::to_vec)
15536                .ok_or_else(unknown)?,
15537            "bus_reactive_power_marginal" => solution
15538                .bus_reactive_power_marginals()
15539                .map(<[f64]>::to_vec)
15540                .ok_or_else(unknown)?,
15541            "branch_from_limit_multiplier" => solution
15542                .branch_from_limit_multipliers()
15543                .map(<[f64]>::to_vec)
15544                .ok_or_else(unknown)?,
15545            "branch_to_limit_multiplier" => solution
15546                .branch_to_limit_multipliers()
15547                .map(<[f64]>::to_vec)
15548                .ok_or_else(unknown)?,
15549            _ => return Err(unknown()),
15550        },
15551        PioValue::SocwrOpfSolution(solution) => {
15552            let values = solution.values();
15553            match quantity {
15554                "bus_voltage_magnitude_squared" => values.bus_voltage_magnitude_squared.clone(),
15555                "branch_voltage_product_real" => values.branch_voltage_product_real.clone(),
15556                "branch_voltage_product_imaginary" => {
15557                    values.branch_voltage_product_imaginary.clone()
15558                }
15559                "generator_active_power" => values.generator_active_power.clone(),
15560                "generator_reactive_power" => values.generator_reactive_power.clone(),
15561                "branch_from_active_power" => values.branch_from_active_power.clone(),
15562                "branch_from_reactive_power" => values.branch_from_reactive_power.clone(),
15563                "branch_to_active_power" => values.branch_to_active_power.clone(),
15564                "branch_to_reactive_power" => values.branch_to_reactive_power.clone(),
15565                _ => return Err(unknown()),
15566            }
15567        }
15568        PioValue::McAcPfSolution(solution) => match quantity {
15569            "terminal_voltage_magnitude" => solution
15570                .network()
15571                .buses()
15572                .iter()
15573                .flat_map(|bus| {
15574                    bus.terminals.iter().map(move |terminal| {
15575                        solution
15576                            .terminal_voltage_magnitude(&bus.id, terminal)
15577                            .expect("complete solution")
15578                    })
15579                })
15580                .collect(),
15581            "terminal_voltage_angle" => solution
15582                .network()
15583                .buses()
15584                .iter()
15585                .flat_map(|bus| {
15586                    bus.terminals.iter().map(move |terminal| {
15587                        solution
15588                            .terminal_voltage_angle(&bus.id, terminal)
15589                            .expect("complete solution")
15590                    })
15591                })
15592                .collect(),
15593            "source_active_injection" => solution.source_active_injections().to_vec(),
15594            _ => return Err(unknown()),
15595        },
15596        PioValue::McAcOpfSolution(solution) => match quantity {
15597            "terminal_voltage_magnitude" => solution
15598                .network()
15599                .buses()
15600                .iter()
15601                .flat_map(|bus| {
15602                    bus.terminals.iter().map(move |terminal| {
15603                        solution
15604                            .terminal_voltage_magnitude(&bus.id, terminal)
15605                            .expect("complete solution")
15606                    })
15607                })
15608                .collect(),
15609            "terminal_voltage_angle" => solution
15610                .network()
15611                .buses()
15612                .iter()
15613                .flat_map(|bus| {
15614                    bus.terminals.iter().map(move |terminal| {
15615                        solution
15616                            .terminal_voltage_angle(&bus.id, terminal)
15617                            .expect("complete solution")
15618                    })
15619                })
15620                .collect(),
15621            "source_active_injection" => solution.source_active_injections().to_vec(),
15622            "generator_active_power" => solution.generator_active_powers().to_vec(),
15623            _ => return Err(unknown()),
15624        },
15625        _ => {
15626            return Err(boundary_error(
15627                &codes::REQUEST_CAPI_TYPE_MISMATCH,
15628                "the handle does not refer to a supported calculation solution",
15629            ));
15630        }
15631    };
15632    Ok(values)
15633}
15634
15635/// Copy one named solution quantity into an independently owned vector.
15636#[unsafe(no_mangle)]
15637pub unsafe extern "C" fn pio_calculation_solution_get_values(
15638    solution: *const PioCalculationSolution,
15639    quantity: *const c_char,
15640    quantity_len: usize,
15641    error: *mut *mut PioError,
15642) -> *mut PioVector {
15643    unsafe {
15644        entry(error, std::ptr::null_mut(), || {
15645            let quantity = required_str(quantity, quantity_len, "quantity")?;
15646            let solution = PioCalculationSolution::get(solution)
15647                .and_then(ValueInner::value)
15648                .ok_or_else(|| {
15649                    boundary_error(
15650                        &codes::BIND_CAPI_NULL_HANDLE,
15651                        "PioCalculationSolution must not be NULL",
15652                    )
15653                })?;
15654            collect_solution_values(solution, quantity)
15655                .map(|values| PioVector::new_raw(VectorInner { values }))
15656        })
15657    }
15658}
15659
15660#[unsafe(no_mangle)]
15661pub unsafe extern "C" fn pio_ac_scuc_solution_time_count(
15662    solution: *const PioCalculationSolution,
15663) -> usize {
15664    let Some(PioValue::AcScucSolution(solution)) =
15665        (unsafe { PioCalculationSolution::get(solution) }).and_then(ValueInner::value)
15666    else {
15667        return 0;
15668    };
15669    solution.instance().inputs().interval_durations.len()
15670}
15671
15672/// Copy one AC SCUC output row for one time position into an owned vector.
15673#[unsafe(no_mangle)]
15674pub unsafe extern "C" fn pio_ac_scuc_solution_get_values_at(
15675    solution: *const PioCalculationSolution,
15676    quantity: *const c_char,
15677    quantity_len: usize,
15678    time_index: usize,
15679    error: *mut *mut PioError,
15680) -> *mut PioVector {
15681    unsafe {
15682        entry(error, std::ptr::null_mut(), || {
15683            let quantity = required_str(quantity, quantity_len, "quantity")?;
15684            let Some(PioValue::AcScucSolution(solution)) =
15685                PioCalculationSolution::get(solution).and_then(ValueInner::value)
15686            else {
15687                return Err(boundary_error(
15688                    &codes::REQUEST_CAPI_TYPE_MISMATCH,
15689                    "the handle does not refer to powerio.AcScucSolution",
15690                ));
15691            };
15692            let network = solution.network_outputs();
15693            let device = solution.device_outputs();
15694            macro_rules! row {
15695                ($rows:expr, $convert:expr) => {{
15696                    $rows
15697                        .get(time_index)
15698                        .ok_or_else(|| {
15699                            boundary_error(
15700                                &codes::BIND_CAPI_INDEX_OUT_OF_RANGE,
15701                                format!("AC SCUC time index {time_index} is out of range"),
15702                            )
15703                        })?
15704                        .iter()
15705                        .copied()
15706                        .map($convert)
15707                        .collect::<Vec<f64>>()
15708                }};
15709            }
15710            let values = match quantity {
15711                "bus_voltage_magnitude" => row!(&network.bus_vm, |value| value),
15712                "bus_voltage_angle" => row!(&network.bus_va, |value| value),
15713                "shunt_step" => row!(&network.shunt_step, |value| value as f64),
15714                "ac_line_on_status" => row!(&network.ac_line_on_status, f64::from),
15715                "transformer_tap_ratio" => row!(&network.transformer_tm, |value| value),
15716                "transformer_phase_shift" => row!(&network.transformer_ta, |value| value),
15717                "transformer_on_status" => row!(&network.transformer_on_status, f64::from),
15718                "dc_line_from_active_power" => row!(&network.dc_line_pdc_fr, |value| value),
15719                "dc_line_from_reactive_power" => row!(&network.dc_line_qdc_fr, |value| value),
15720                "dc_line_to_reactive_power" => row!(&network.dc_line_qdc_to, |value| value),
15721                "device_on_status" => row!(&device.on_status, f64::from),
15722                "device_startup_status" => row!(&device.startup_status, f64::from),
15723                "device_shutdown_status" => row!(&device.shutdown_status, f64::from),
15724                "device_active_power" => row!(&device.p_on, |value| value),
15725                "device_reactive_power" => row!(&device.q, |value| value),
15726                "regulation_reserve_up" => row!(&device.p_reg_res_up, |value| value),
15727                "regulation_reserve_down" => row!(&device.p_reg_res_down, |value| value),
15728                "synchronized_reserve" => row!(&device.p_syn_res, |value| value),
15729                "nonsynchronized_reserve" => row!(&device.p_nsyn_res, |value| value),
15730                "ramping_reserve_up_online" => row!(&device.p_ramp_res_up_online, |value| value),
15731                "ramping_reserve_up_offline" => {
15732                    row!(&device.p_ramp_res_up_offline, |value| value)
15733                }
15734                "ramping_reserve_down_online" => {
15735                    row!(&device.p_ramp_res_down_online, |value| value)
15736                }
15737                "ramping_reserve_down_offline" => {
15738                    row!(&device.p_ramp_res_down_offline, |value| value)
15739                }
15740                "reactive_reserve_up" => row!(&device.q_res_up, |value| value),
15741                "reactive_reserve_down" => row!(&device.q_res_down, |value| value),
15742                _ => {
15743                    return Err(boundary_error(
15744                        &codes::REQUEST_CAPI_QUANTITY_UNKNOWN,
15745                        format!("unknown AC SCUC solution quantity '{quantity}'"),
15746                    ));
15747                }
15748            };
15749            Ok(PioVector::new_raw(VectorInner { values }))
15750        })
15751    }
15752}
15753
15754fn formula(name: Option<&str>) -> Result<BranchSusceptanceFormula, *mut PioError> {
15755    match name.unwrap_or("series_susceptance") {
15756        "series_susceptance" => Ok(BranchSusceptanceFormula::SeriesSusceptance),
15757        "tap_adjusted_reactance" => Ok(BranchSusceptanceFormula::TapAdjustedReactance),
15758        "reactance_only" => Ok(BranchSusceptanceFormula::ReactanceOnly),
15759        other => Err(boundary_error(
15760            &codes::REQUEST_CAPI_UNKNOWN_FORMULA,
15761            format!("unknown branch susceptance formula '{other}'"),
15762        )),
15763    }
15764}
15765
15766unsafe fn dc_operators(
15767    network: *const PioBalancedNetwork,
15768    formula_name: *const c_char,
15769    formula_name_len: usize,
15770) -> Result<DcOperators, *mut PioError> {
15771    let network = unsafe { PioBalancedNetwork::get(network) }
15772        .and_then(BalancedNetworkInner::network)
15773        .ok_or_else(|| {
15774            boundary_error(
15775                &codes::BIND_CAPI_NULL_HANDLE,
15776                "PioBalancedNetwork must not be NULL",
15777            )
15778        })?;
15779    let formula_name = unsafe { optional_str(formula_name, formula_name_len, "formula") }?;
15780    let formula = formula(formula_name)?;
15781    let instance = DcPfInstance::from_network(network.clone())
15782        .map_err(|failure| error_from_core(&failure))?
15783        .with_branch_susceptance_formula(formula);
15784    DcOperators::build(&instance).map_err(|failure| error_from_core(&failure))
15785}
15786
15787unsafe fn dc_matrix(
15788    network: *const PioBalancedNetwork,
15789    formula: *const c_char,
15790    formula_len: usize,
15791    error: *mut *mut PioError,
15792    calculation: impl FnOnce(&DcOperators) -> SparseMatrix,
15793) -> *mut PioSparseMatrix {
15794    unsafe {
15795        entry(error, std::ptr::null_mut(), || {
15796            let operators = dc_operators(network, formula, formula_len)?;
15797            Ok(PioSparseMatrix::new_raw(SparseMatrixInner::from(
15798                calculation(&operators),
15799            )))
15800        })
15801    }
15802}
15803
15804unsafe fn dc_vector(
15805    network: *const PioBalancedNetwork,
15806    formula: *const c_char,
15807    formula_len: usize,
15808    error: *mut *mut PioError,
15809    calculation: impl FnOnce(&DcOperators) -> Vec<f64>,
15810) -> *mut PioVector {
15811    unsafe {
15812        entry(error, std::ptr::null_mut(), || {
15813            let operators = dc_operators(network, formula, formula_len)?;
15814            Ok(PioVector::new_raw(VectorInner {
15815                values: calculation(&operators),
15816            }))
15817        })
15818    }
15819}
15820
15821#[unsafe(no_mangle)]
15822pub unsafe extern "C" fn pio_calc_incidence_matrix(
15823    network: *const PioBalancedNetwork,
15824    formula: *const c_char,
15825    formula_len: usize,
15826    error: *mut *mut PioError,
15827) -> *mut PioSparseMatrix {
15828    unsafe {
15829        dc_matrix(network, formula, formula_len, error, |operators| {
15830            operators.calc_incidence_matrix()
15831        })
15832    }
15833}
15834
15835#[unsafe(no_mangle)]
15836pub unsafe extern "C" fn pio_calc_bus_susceptance_matrix(
15837    network: *const PioBalancedNetwork,
15838    formula: *const c_char,
15839    formula_len: usize,
15840    error: *mut *mut PioError,
15841) -> *mut PioSparseMatrix {
15842    unsafe {
15843        dc_matrix(network, formula, formula_len, error, |operators| {
15844            operators.calc_bus_susceptance_matrix()
15845        })
15846    }
15847}
15848
15849#[unsafe(no_mangle)]
15850pub unsafe extern "C" fn pio_calc_branch_flow_matrix(
15851    network: *const PioBalancedNetwork,
15852    formula: *const c_char,
15853    formula_len: usize,
15854    error: *mut *mut PioError,
15855) -> *mut PioSparseMatrix {
15856    unsafe {
15857        dc_matrix(network, formula, formula_len, error, |operators| {
15858            operators.calc_branch_flow_matrix()
15859        })
15860    }
15861}
15862
15863#[unsafe(no_mangle)]
15864pub unsafe extern "C" fn pio_calc_branch_susceptances(
15865    network: *const PioBalancedNetwork,
15866    formula: *const c_char,
15867    formula_len: usize,
15868    error: *mut *mut PioError,
15869) -> *mut PioVector {
15870    unsafe {
15871        dc_vector(network, formula, formula_len, error, |operators| {
15872            operators.calc_branch_susceptances().to_vec()
15873        })
15874    }
15875}
15876
15877#[unsafe(no_mangle)]
15878pub unsafe extern "C" fn pio_calc_branch_phase_shift_injection(
15879    network: *const PioBalancedNetwork,
15880    formula: *const c_char,
15881    formula_len: usize,
15882    error: *mut *mut PioError,
15883) -> *mut PioVector {
15884    unsafe {
15885        dc_vector(network, formula, formula_len, error, |operators| {
15886            operators.calc_branch_phase_shift_injection()
15887        })
15888    }
15889}
15890
15891#[unsafe(no_mangle)]
15892pub unsafe extern "C" fn pio_calc_bus_phase_shift_injection(
15893    network: *const PioBalancedNetwork,
15894    formula: *const c_char,
15895    formula_len: usize,
15896    error: *mut *mut PioError,
15897) -> *mut PioVector {
15898    unsafe {
15899        dc_vector(network, formula, formula_len, error, |operators| {
15900            operators.calc_bus_phase_shift_injection()
15901        })
15902    }
15903}
15904
15905unsafe fn dc_vector_from_angles(
15906    network: *const PioBalancedNetwork,
15907    formula_name: *const c_char,
15908    formula_name_len: usize,
15909    voltage_angles: *const f64,
15910    voltage_angles_len: usize,
15911    error: *mut *mut PioError,
15912    calculation: impl FnOnce(&DcOperators, &[f64]) -> Result<Vec<f64>, powerio_core::Error>,
15913) -> *mut PioVector {
15914    unsafe {
15915        entry(error, std::ptr::null_mut(), || {
15916            if voltage_angles.is_null() && voltage_angles_len != 0 {
15917                return Err(boundary_error(
15918                    &codes::BIND_CAPI_NULL_ARGUMENT,
15919                    "voltage_angles is NULL with a nonzero length",
15920                ));
15921            }
15922            let angles = if voltage_angles_len == 0 {
15923                &[]
15924            } else {
15925                std::slice::from_raw_parts(voltage_angles, voltage_angles_len)
15926            };
15927            let operators = dc_operators(network, formula_name, formula_name_len)?;
15928            calculation(&operators, angles)
15929                .map(|values| PioVector::new_raw(VectorInner { values }))
15930                .map_err(|failure| error_from_core(&failure))
15931        })
15932    }
15933}
15934
15935#[unsafe(no_mangle)]
15936pub unsafe extern "C" fn pio_calc_branch_flow_dc(
15937    network: *const PioBalancedNetwork,
15938    formula: *const c_char,
15939    formula_len: usize,
15940    voltage_angles: *const f64,
15941    voltage_angles_len: usize,
15942    error: *mut *mut PioError,
15943) -> *mut PioVector {
15944    unsafe {
15945        dc_vector_from_angles(
15946            network,
15947            formula,
15948            formula_len,
15949            voltage_angles,
15950            voltage_angles_len,
15951            error,
15952            DcOperators::calc_branch_flow_dc,
15953        )
15954    }
15955}
15956
15957#[unsafe(no_mangle)]
15958pub unsafe extern "C" fn pio_calc_bus_injection_dc(
15959    network: *const PioBalancedNetwork,
15960    formula: *const c_char,
15961    formula_len: usize,
15962    voltage_angles: *const f64,
15963    voltage_angles_len: usize,
15964    error: *mut *mut PioError,
15965) -> *mut PioVector {
15966    unsafe {
15967        dc_vector_from_angles(
15968            network,
15969            formula,
15970            formula_len,
15971            voltage_angles,
15972            voltage_angles_len,
15973            error,
15974            DcOperators::calc_bus_injection_dc,
15975        )
15976    }
15977}
15978
15979#[unsafe(no_mangle)]
15980pub unsafe extern "C" fn pio_sparse_matrix_rows(matrix: *const PioSparseMatrix) -> usize {
15981    unsafe { PioSparseMatrix::get(matrix) }.map_or(0, |matrix| matrix.rows)
15982}
15983
15984#[unsafe(no_mangle)]
15985pub unsafe extern "C" fn pio_sparse_matrix_columns(matrix: *const PioSparseMatrix) -> usize {
15986    unsafe { PioSparseMatrix::get(matrix) }.map_or(0, |matrix| matrix.columns)
15987}
15988
15989#[unsafe(no_mangle)]
15990pub unsafe extern "C" fn pio_sparse_matrix_row_offsets(
15991    matrix: *const PioSparseMatrix,
15992) -> PioSizeView {
15993    unsafe { PioSparseMatrix::get(matrix) }.map_or(PioSizeView::EMPTY, |matrix| {
15994        PioSizeView::new(&matrix.row_offsets)
15995    })
15996}
15997
15998#[unsafe(no_mangle)]
15999pub unsafe extern "C" fn pio_sparse_matrix_column_indices(
16000    matrix: *const PioSparseMatrix,
16001) -> PioSizeView {
16002    unsafe { PioSparseMatrix::get(matrix) }.map_or(PioSizeView::EMPTY, |matrix| {
16003        PioSizeView::new(&matrix.column_indices)
16004    })
16005}
16006
16007#[unsafe(no_mangle)]
16008pub unsafe extern "C" fn pio_sparse_matrix_values(matrix: *const PioSparseMatrix) -> PioF64View {
16009    unsafe { PioSparseMatrix::get(matrix) }
16010        .map_or(PioF64View::EMPTY, |matrix| PioF64View::new(&matrix.values))
16011}
16012
16013#[unsafe(no_mangle)]
16014pub unsafe extern "C" fn pio_sparse_matrix_retain(
16015    matrix: *const PioSparseMatrix,
16016) -> *mut PioSparseMatrix {
16017    unsafe { PioSparseMatrix::retain_raw(matrix) }
16018}
16019
16020#[unsafe(no_mangle)]
16021pub unsafe extern "C" fn pio_sparse_matrix_release(matrix: *mut PioSparseMatrix) {
16022    unsafe { PioSparseMatrix::release_raw(matrix) };
16023}
16024
16025#[unsafe(no_mangle)]
16026pub unsafe extern "C" fn pio_vector_values(vector: *const PioVector) -> PioF64View {
16027    unsafe { PioVector::get(vector) }
16028        .map_or(PioF64View::EMPTY, |vector| PioF64View::new(&vector.values))
16029}
16030
16031#[unsafe(no_mangle)]
16032pub unsafe extern "C" fn pio_vector_retain(vector: *const PioVector) -> *mut PioVector {
16033    unsafe { PioVector::retain_raw(vector) }
16034}
16035
16036#[unsafe(no_mangle)]
16037pub unsafe extern "C" fn pio_vector_release(vector: *mut PioVector) {
16038    unsafe { PioVector::release_raw(vector) };
16039}
16040
16041// ---- schema report ---------------------------------------------------------
16042
16043struct StringInner {
16044    text: String,
16045}
16046
16047opaque_handle!(
16048    /// Owned UTF-8 text returned by report functions.
16049    PioString,
16050    StringInner
16051);
16052
16053/// Return version information for this ABI and the PowerIO IR serializer/deserializer.
16054#[unsafe(no_mangle)]
16055pub unsafe extern "C" fn pio_schema_report(error: *mut *mut PioError) -> *mut PioString {
16056    unsafe {
16057        entry(error, std::ptr::null_mut(), || {
16058            serde_json::to_string(&serde_json::json!({
16059                "powerio_version": powerio::VERSION,
16060                "abi": PIO_ABI_VERSION,
16061                "powerio_ir": {
16062                    "schema": powerio::IR_SCHEMA_NAME,
16063                    "version": powerio::IR_VERSION
16064                },
16065                "bmopf_schema": powerio_dist::BMOPF_SCHEMA_VERSION,
16066                // ABI 7 exports one fixed symbol set. Matrices, the
16067                // multiconductor model, and calculation types are always
16068                // present; only GridFM Parquet support is a build option.
16069                "features": {
16070                    "matrix": true,
16071                    "gridfm": cfg!(feature = "gridfm"),
16072                    "dist": true,
16073                    "prob": true
16074                },
16075                "foreign_schemas": {
16076                    "bmopf": powerio_dist::BMOPF_SCHEMA_VERSION
16077                },
16078                "error_categories": powerio_core::ErrorCategory::TOKENS,
16079                "diagnostic_namespaces": powerio_core::DiagnosticStage::NAMESPACES,
16080                "json_classes": powerio::JSON_CLASSES
16081            }))
16082            .map(|text| PioString::new_raw(StringInner { text }))
16083            .map_err(|failure| {
16084                boundary_error(
16085                    &codes::EMIT_CAPI_SERIALIZE_FAILED,
16086                    format!("cannot serialize the schema report: {failure}"),
16087                )
16088            })
16089        })
16090    }
16091}
16092
16093#[unsafe(no_mangle)]
16094pub unsafe extern "C" fn pio_string_view(string: *const PioString) -> PioStringView {
16095    unsafe { PioString::get(string) }.map_or(PioStringView::EMPTY, |string| {
16096        PioStringView::new(&string.text)
16097    })
16098}
16099
16100#[unsafe(no_mangle)]
16101pub unsafe extern "C" fn pio_string_retain(string: *const PioString) -> *mut PioString {
16102    unsafe { PioString::retain_raw(string) }
16103}
16104
16105#[unsafe(no_mangle)]
16106pub unsafe extern "C" fn pio_string_release(string: *mut PioString) {
16107    unsafe { PioString::release_raw(string) };
16108}
16109
16110#[cfg(test)]
16111mod tests {
16112    use super::*;
16113
16114    unsafe fn view_text(view: PioStringView) -> String {
16115        if view.data.is_null() {
16116            return String::new();
16117        }
16118        String::from_utf8_lossy(unsafe {
16119            std::slice::from_raw_parts(view.data.cast::<u8>(), view.len)
16120        })
16121        .into_owned()
16122    }
16123
16124    unsafe fn parse_case9() -> *mut PioModule {
16125        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/case9.m");
16126        let path = path.to_string_lossy();
16127        let mut error = std::ptr::null_mut();
16128        let source = unsafe { pio_source_open(path.as_ptr().cast(), path.len(), &mut error) };
16129        assert!(!source.is_null(), "{}", unsafe { error_text(error) });
16130        let module = unsafe { pio_parse(source, std::ptr::null(), 0, &mut error) };
16131        unsafe { pio_source_release(source) };
16132        assert!(!module.is_null(), "{}", unsafe { error_text(error) });
16133        module
16134    }
16135
16136    fn parse_xiidm_text(text: &str) -> *mut PioModule {
16137        unsafe {
16138            let name = b"case.xiidm";
16139            let format = b"xiidm";
16140            let mut error = std::ptr::null_mut();
16141            let source = pio_source_from_memory(
16142                name.as_ptr().cast(),
16143                name.len(),
16144                text.as_ptr(),
16145                text.len(),
16146                &mut error,
16147            );
16148            assert!(!source.is_null(), "{}", error_text(error));
16149            let module = pio_parse(source, format.as_ptr().cast(), format.len(), &mut error);
16150            pio_source_release(source);
16151            assert!(!module.is_null(), "{}", error_text(error));
16152            module
16153        }
16154    }
16155
16156    unsafe fn parse_bmopf() -> *mut PioModule {
16157        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
16158            .join("../tests/data/dist/bmopf/example_ieee13.json");
16159        let path = path.to_string_lossy();
16160        let mut error = std::ptr::null_mut();
16161        let source = unsafe { pio_source_open(path.as_ptr().cast(), path.len(), &mut error) };
16162        assert!(!source.is_null(), "{}", unsafe { error_text(error) });
16163        let module = unsafe { pio_parse(source, std::ptr::null(), 0, &mut error) };
16164        unsafe { pio_source_release(source) };
16165        assert!(!module.is_null(), "{}", unsafe { error_text(error) });
16166        module
16167    }
16168
16169    unsafe fn error_text(error: *const PioError) -> String {
16170        let view = unsafe { pio_error_message(error) };
16171        if view.data.is_null() {
16172            return "missing PioError".to_owned();
16173        }
16174        String::from_utf8_lossy(unsafe {
16175            std::slice::from_raw_parts(view.data.cast::<u8>(), view.len)
16176        })
16177        .into_owned()
16178    }
16179
16180    unsafe fn case9_network() -> BalancedNetwork {
16181        let module = unsafe { parse_case9() };
16182        let network = match &unsafe { PioModule::get(module) }.unwrap().module.value() {
16183            PioValue::BalancedNetwork(network) => network.clone(),
16184            value => panic!("expected balanced network, got {}", value.type_name()),
16185        };
16186        unsafe { pio_module_release(module) };
16187        network
16188    }
16189
16190    fn module_with_complete_records() -> *mut PioModule {
16191        use std::collections::BTreeMap;
16192
16193        let mut module = powerio::PioModule::new(PioValue::BalancedNetwork(BalancedNetwork::new(
16194            "metadata", 100.0,
16195        )))
16196        .with_producer(Producer::new("record-test", "1.2.3").unwrap());
16197        let source_id = powerio_core::SourceId::new("source-1").unwrap();
16198        let source =
16199            powerio_core::SourceDescriptor::new(source_id.clone(), "record-test.xiidm", 128)
16200                .unwrap()
16201                .with_format(powerio_core::FormatId::new("xiidm").unwrap())
16202                .with_digest(
16203                    powerio_core::Digest::sha256(
16204                        "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
16205                    )
16206                    .unwrap(),
16207                );
16208        module.add_source_descriptor(source).unwrap();
16209        module
16210            .add_source_map_entry(
16211                powerio_core::SourceMapEntry::new(
16212                    "/value/buses/0",
16213                    powerio_core::SourceRelation::Exact,
16214                    vec![powerio_core::SourceSpan::new(source_id, 4, 12).unwrap()],
16215                )
16216                .unwrap(),
16217            )
16218            .unwrap();
16219        let parameters = BTreeMap::from([(
16220            "settings".to_owned(),
16221            serde_json::json!({
16222                "items": [null, true, "named", 18446744073709551615u64, 1.25]
16223            }),
16224        )]);
16225        let history = HistoryEntry::new(
16226            HistoryId::new("history-1").unwrap(),
16227            HistoryKind::Transform,
16228            "normalize",
16229        )
16230        .unwrap()
16231        .with_input_type("powerio.BalancedNetwork")
16232        .unwrap()
16233        .with_output_type("powerio.BalancedNetwork")
16234        .unwrap()
16235        .with_parameters(parameters)
16236        .unwrap()
16237        .with_assumption("base power is known")
16238        .unwrap()
16239        .with_loss("source token spelling")
16240        .unwrap();
16241        module.add_history_entry(history).unwrap();
16242        module
16243            .insert_extension("org.example.record", serde_json::json!({"enabled": true}))
16244            .unwrap();
16245        module_handle(module)
16246    }
16247
16248    fn multiconductor_network() -> powerio_dist::MulticonductorNetwork {
16249        let mut network = powerio_dist::MulticonductorNetwork::named("capi-solution-instance");
16250        network.buses_mut().push(powerio_dist::DistBus::new(
16251            "source",
16252            vec!["1".into(), "2".into(), "3".into()],
16253        ));
16254        network.sources_mut().push(powerio_dist::VoltageSource::new(
16255            "source",
16256            "source",
16257            vec!["1".into(), "2".into(), "3".into()],
16258            vec![240.0, 240.0, 240.0],
16259            vec![0.0, -2.094, 2.094],
16260        ));
16261        network
16262    }
16263
16264    fn complete_multiconductor_network() -> powerio_dist::MulticonductorNetwork {
16265        let mut network = powerio_dist::MulticonductorNetwork::named("");
16266        *network.source_format_mut() = Some(powerio_dist::DistSourceFormat::Dss);
16267        *network.geo_mut() = Some(powerio_dist::DistGeoMeta {
16268            space: powerio_dist::CoordinateSpace::Diagram {
16269                canvas: Some(powerio_dist::DistCanvas {
16270                    width: Some(640.0),
16271                    height: None,
16272                    units: Some(String::new()),
16273                }),
16274            },
16275            kind: Some(powerio_dist::DistCoordsKind::Manual),
16276        });
16277
16278        let mut bus = powerio_dist::DistBus::new("bus", vec!["1".into(), "2".into()]);
16279        bus.grounded.push("2".into());
16280        bus.v_min = Some(0.0);
16281        bus.vpn_min = Some(Vec::new());
16282        bus.vpp_max = Some(vec![240.0, 240.0]);
16283        bus.location = Some(powerio_dist::DistLocation {
16284            x: 10.0,
16285            y: 20.0,
16286            kind: None,
16287        });
16288        network.buses_mut().push(bus);
16289
16290        let mut line_code = powerio_dist::DistLineCode::new(
16291            "code",
16292            vec![vec![1.0, 2.0], vec![3.0]],
16293            vec![vec![4.0]],
16294        );
16295        line_code.g_from = vec![vec![5.0]];
16296        line_code.b_from = vec![vec![6.0]];
16297        line_code.g_to = vec![vec![7.0]];
16298        line_code.b_to = vec![vec![8.0]];
16299        line_code.i_max = Some(Vec::new());
16300        line_code.s_max = Some(vec![900.0]);
16301        line_code.source = Some(String::new());
16302        network.line_codes_mut().push(line_code);
16303
16304        let mut line = powerio_dist::DistLine::new(
16305            "line",
16306            "bus",
16307            "bus",
16308            vec!["1".into()],
16309            vec!["2".into()],
16310            "code",
16311            12.0,
16312        );
16313        line.route = Some(Vec::new());
16314        line.i_max = Some(vec![10.0]);
16315        network.lines_mut().push(line);
16316
16317        let mut switch = powerio_dist::DistSwitch::new(
16318            "switch",
16319            "bus",
16320            "bus",
16321            vec!["1".into()],
16322            vec!["2".into()],
16323            true,
16324        );
16325        switch.i_max = Some(Vec::new());
16326        network.switches_mut().push(switch);
16327
16328        let mut winding = powerio_dist::DistWinding::new(
16329            "bus",
16330            vec!["1".into(), "2".into()],
16331            powerio_dist::DistWindingConn::Wye,
16332            240.0,
16333            1_000.0,
16334        );
16335        winding.r_pct = 0.5;
16336        winding.r_neutral = Some(0.0);
16337        network
16338            .transformers_mut()
16339            .push(powerio_dist::DistTransformer::new(
16340                "transformer",
16341                vec![winding],
16342                vec![2.5],
16343                1,
16344            ));
16345
16346        let mut load = powerio_dist::DistLoad::new(
16347            "load",
16348            "bus",
16349            vec!["1".into()],
16350            powerio_dist::Configuration::Wye,
16351            vec![100.0],
16352            vec![20.0],
16353        );
16354        load.voltage_model = powerio_dist::DistLoadVoltageModel::Zip {
16355            v_nom: vec![240.0],
16356            alpha_z: vec![0.1],
16357            alpha_i: vec![0.2],
16358            alpha_p: vec![0.7],
16359            beta_z: vec![0.2],
16360            beta_i: vec![0.3],
16361            beta_p: vec![0.5],
16362        };
16363        network.loads_mut().push(load);
16364
16365        let mut generator = powerio_dist::DistGenerator::new(
16366            "generator",
16367            "bus",
16368            vec!["1".into()],
16369            powerio_dist::Configuration::SinglePhase,
16370            vec![50.0],
16371            vec![5.0],
16372        );
16373        generator.p_min = Some(Vec::new());
16374        generator.cost = Some(vec![0.25]);
16375        network.generators_mut().push(generator);
16376
16377        let mut resource = powerio_dist::DistIbr::new(
16378            "ibr",
16379            "bus",
16380            vec!["1".into()],
16381            powerio_dist::IbrTopology::SinglePhase,
16382            powerio_dist::IbrPrimeMover::Pv,
16383            vec![100.0],
16384        );
16385        resource.i_max = Some(Vec::new());
16386        resource.p_avail = Some(75.0);
16387        resource.control_profile = Some(String::new());
16388        resource.voltage_aggregation = Some(powerio_dist::IbrVoltageAggregation::PerPhase);
16389        network.ibrs_mut().push(resource);
16390
16391        let profile: powerio_dist::DistControlProfile = serde_json::from_value(serde_json::json!({
16392            "name": "profile",
16393            "power_factor": { "pf": 0.97 },
16394            "volt_var": {
16395                "voltage_reference": "PN_PER_PHASE",
16396                "breakpoints": [0.95, 1.05],
16397                "q_limits": [-0.4, 0.4],
16398                "q_unit": "VA_FRACTION",
16399                "q_ref": "VAR_MAX",
16400                "p_min_for_q": 10.0,
16401                "p_min_for_q_max": null
16402            },
16403            "volt_watt": {
16404                "voltage_reference": null,
16405                "breakpoints": [1.0],
16406                "p_limits": [0.8],
16407                "p_unit": "W",
16408                "p_ref": "P_AVAILABLE"
16409            },
16410            "extras": {}
16411        }))
16412        .unwrap();
16413        network.control_profiles_mut().push(profile);
16414
16415        network.shunts_mut().push(powerio_dist::DistShunt::new(
16416            "shunt",
16417            "bus",
16418            vec!["2".into()],
16419            vec![vec![0.01, 0.02], vec![0.03]],
16420            vec![vec![0.04]],
16421        ));
16422        network
16423            .capacitors_mut()
16424            .push(powerio_dist::DistCapacitor::new(
16425                "capacitor",
16426                "bus",
16427                vec!["1".into()],
16428                powerio_dist::Configuration::SinglePhase,
16429                100.0,
16430                240.0,
16431            ));
16432        network.sources_mut().push(powerio_dist::VoltageSource::new(
16433            "source",
16434            "bus",
16435            vec!["1".into()],
16436            vec![240.0],
16437            vec![0.0],
16438        ));
16439        network.sources_mut()[0].energy_cost_rate = Some(vec![0.125]);
16440        network
16441            .untyped_objects_mut()
16442            .push(powerio_dist::UntypedObject::new(
16443                "curve",
16444                "raw",
16445                vec![
16446                    (None, "first".into()),
16447                    (Some(String::new()), "second".into()),
16448                ],
16449            ));
16450        network
16451            .commands_mut()
16452            .push(("solve".into(), "mode=daily".into()));
16453        network
16454            .options_mut()
16455            .push(("frequency".into(), "60".into()));
16456        network
16457    }
16458
16459    fn goc3_instance(source: Source) -> powerio_prob::AcScucInstance {
16460        let module = powerio::parse_with_options(
16461            source,
16462            &powerio::ParseOptions::default()
16463                .format("goc3-json")
16464                .unwrap(),
16465        )
16466        .unwrap();
16467        let PioValue::AcScucInstance(instance) = module.into_value() else {
16468            panic!("GO Challenge 3 problem did not produce powerio.AcScucInstance");
16469        };
16470        instance
16471    }
16472
16473    #[test]
16474    fn reports_abi_seven() {
16475        assert_eq!(pio_abi_version(), 7);
16476        assert_eq!(PIO_ABI_VERSION, 7);
16477        unsafe {
16478            let mut error = std::ptr::null_mut();
16479            let report = pio_schema_report(&mut error);
16480            assert!(!report.is_null(), "{}", error_text(error));
16481            let parsed: serde_json::Value =
16482                serde_json::from_str(&view_text(pio_string_view(report))).unwrap();
16483            assert_eq!(parsed["abi"], 7);
16484            assert_eq!(parsed["powerio_ir"]["schema"], powerio::IR_SCHEMA_NAME);
16485            assert_eq!(parsed["powerio_ir"]["version"], powerio::IR_VERSION);
16486            assert_eq!(
16487                parsed["bmopf_schema"],
16488                serde_json::json!(powerio_dist::BMOPF_SCHEMA_VERSION)
16489            );
16490            assert_eq!(parsed["foreign_schemas"]["bmopf"], parsed["bmopf_schema"]);
16491            assert_eq!(
16492                parsed["error_categories"],
16493                serde_json::json!(powerio_core::ErrorCategory::TOKENS)
16494            );
16495            assert_eq!(
16496                parsed["diagnostic_namespaces"],
16497                serde_json::json!(powerio_core::DiagnosticStage::NAMESPACES)
16498            );
16499            assert_eq!(
16500                parsed["json_classes"],
16501                serde_json::json!(powerio::JSON_CLASSES)
16502            );
16503            for feature in ["matrix", "gridfm", "dist", "prob"] {
16504                assert!(parsed["features"][feature].is_boolean());
16505            }
16506            pio_string_release(report);
16507        }
16508    }
16509
16510    #[test]
16511    fn structured_diagnostic_fields_keep_their_owner_alive() {
16512        unsafe {
16513            let source = powerio_core::SourceId::new("input").unwrap();
16514            let span = powerio_core::SourceSpan::new(source, 2, 7).unwrap();
16515            let mut details = serde_json::Map::new();
16516            details.insert("count".to_owned(), serde_json::json!(3));
16517            details.insert("nested".to_owned(), serde_json::json!({ "ok": true }));
16518            let diagnostic = Diagnostic::new(
16519                powerio_core::DiagnosticCode::new("PARTNER.TEST.STRUCTURED").unwrap(),
16520                powerio_core::DiagnosticSeverity::Warning,
16521                "structured finding",
16522            )
16523            .with_id(powerio_core::DiagnosticId::new("d1").unwrap())
16524            .with_target("/value/data")
16525            .unwrap()
16526            .with_span(span)
16527            .unwrap()
16528            .with_related(powerio_core::DiagnosticId::new("d0").unwrap())
16529            .unwrap()
16530            .with_details(details)
16531            .unwrap()
16532            .with_suggested_action("fix it");
16533            let diagnostics = PioDiagnostics::new_raw(DiagnosticsInner {
16534                owner: DiagnosticsOwner::Owned(vec![diagnostic]),
16535            });
16536            let retained = pio_diagnostics_retain(diagnostics);
16537            pio_diagnostics_release(diagnostics);
16538
16539            assert_eq!(pio_diagnostics_len(retained), 1);
16540            assert_eq!(
16541                view_text(pio_diagnostic_code(retained, 0)),
16542                "PARTNER.TEST.STRUCTURED"
16543            );
16544            assert_eq!(view_text(pio_diagnostic_severity(retained, 0)), "warning");
16545            assert_eq!(
16546                view_text(pio_diagnostic_message(retained, 0)),
16547                "structured finding"
16548            );
16549            assert!(pio_diagnostic_has_id(retained, 0));
16550            assert_eq!(view_text(pio_diagnostic_id(retained, 0)), "d1");
16551            assert!(pio_diagnostic_has_target(retained, 0));
16552            assert_eq!(view_text(pio_diagnostic_target(retained, 0)), "/value/data");
16553            assert!(pio_diagnostic_has_suggested_action(retained, 0));
16554            assert_eq!(
16555                view_text(pio_diagnostic_suggested_action(retained, 0)),
16556                "fix it"
16557            );
16558
16559            assert_eq!(pio_diagnostic_n_spans(retained, 0), 1);
16560            let mut output = PioDiagnosticSpanView {
16561                source: PioStringView::EMPTY,
16562                byte_start: 0,
16563                byte_end: 0,
16564            };
16565            let mut error = std::ptr::null_mut();
16566            assert!(pio_diagnostic_span(retained, 0, 0, &mut output, &mut error));
16567            assert!(error.is_null());
16568            assert_eq!(view_text(output.source), "input");
16569            assert_eq!(output.byte_start, 2);
16570            assert_eq!(output.byte_end, 7);
16571
16572            assert_eq!(pio_diagnostic_n_related(retained, 0), 1);
16573            assert_eq!(view_text(pio_diagnostic_related(retained, 0, 0)), "d0");
16574
16575            let details_text = pio_diagnostic_details_json(retained, 0, &mut error);
16576            assert!(!details_text.is_null(), "{}", error_text(error));
16577            let details: serde_json::Value =
16578                serde_json::from_str(&view_text(pio_string_view(details_text))).unwrap();
16579            assert_eq!(details["count"], 3);
16580            assert_eq!(details["nested"]["ok"], true);
16581            pio_string_release(details_text);
16582
16583            pio_diagnostics_release(retained);
16584        }
16585    }
16586
16587    #[test]
16588    fn module_records_are_typed_and_structured_values_keep_the_owner_alive() {
16589        unsafe {
16590            let module = module_with_complete_records();
16591            let mut error = std::ptr::null_mut();
16592
16593            let mut producer = std::mem::MaybeUninit::<PioModuleProducerView>::uninit();
16594            assert!(pio_module_producer(
16595                module,
16596                producer.as_mut_ptr(),
16597                &mut error
16598            ));
16599            let producer = producer.assume_init();
16600            assert_eq!(view_text(producer.name), "record-test");
16601            assert_eq!(view_text(producer.version), "1.2.3");
16602
16603            assert_eq!(pio_module_source_count(module), 1);
16604            let mut source = std::mem::MaybeUninit::<PioModuleSourceView>::uninit();
16605            assert!(pio_module_source_at(
16606                module,
16607                0,
16608                source.as_mut_ptr(),
16609                &mut error
16610            ));
16611            let source = source.assume_init();
16612            assert_eq!(view_text(source.id), "source-1");
16613            assert_eq!(view_text(source.name), "record-test.xiidm");
16614            assert_eq!(source.byte_length, 128);
16615            assert!(source.has_format);
16616            assert_eq!(view_text(source.format), "xiidm");
16617            assert!(source.has_digest);
16618            assert_eq!(view_text(source.digest_algorithm), "sha256");
16619            assert_eq!(source.digest.len, 64);
16620
16621            assert_eq!(pio_module_source_map_count(module), 1);
16622            let mut mapping = std::mem::MaybeUninit::<PioModuleSourceMapEntryView>::uninit();
16623            assert!(pio_module_source_map_at(
16624                module,
16625                0,
16626                mapping.as_mut_ptr(),
16627                &mut error
16628            ));
16629            let mapping = mapping.assume_init();
16630            assert_eq!(view_text(mapping.target), "/value/buses/0");
16631            assert_eq!(view_text(mapping.relation), "exact");
16632            assert_eq!(mapping.span_count, 1);
16633            let mut span = std::mem::MaybeUninit::<PioSourceSpanView>::uninit();
16634            assert!(pio_module_source_map_span_at(
16635                module,
16636                0,
16637                0,
16638                span.as_mut_ptr(),
16639                &mut error
16640            ));
16641            let span = span.assume_init();
16642            assert_eq!(view_text(span.source), "source-1");
16643            assert_eq!((span.byte_start, span.byte_end), (4, 12));
16644
16645            assert_eq!(pio_module_history_count(module), 1);
16646            let mut history = std::mem::MaybeUninit::<PioModuleHistoryEntryView>::uninit();
16647            assert!(pio_module_history_at(
16648                module,
16649                0,
16650                history.as_mut_ptr(),
16651                &mut error
16652            ));
16653            let history = history.assume_init();
16654            assert_eq!(view_text(history.id), "history-1");
16655            assert_eq!(view_text(history.kind), "transform");
16656            assert_eq!(view_text(history.name), "normalize");
16657            assert!(history.has_input_type);
16658            assert_eq!(view_text(history.input_type), "powerio.BalancedNetwork");
16659            assert!(history.has_output_type);
16660            assert_eq!(view_text(history.output_type), "powerio.BalancedNetwork");
16661            assert_eq!(history.parameter_count, 1);
16662            assert_eq!(history.assumption_count, 1);
16663            assert_eq!(history.loss_count, 1);
16664            assert_eq!(
16665                view_text(pio_module_history_assumption_at(module, 0, 0, &mut error)),
16666                "base power is known"
16667            );
16668            assert_eq!(
16669                view_text(pio_module_history_loss_at(module, 0, 0, &mut error)),
16670                "source token spelling"
16671            );
16672            let mut parameter = std::mem::MaybeUninit::<PioModuleHistoryParameterView>::uninit();
16673            assert!(pio_module_history_parameter_at(
16674                module,
16675                0,
16676                0,
16677                parameter.as_mut_ptr(),
16678                &mut error
16679            ));
16680            let parameter = parameter.assume_init();
16681            assert_eq!(view_text(parameter.name), "settings");
16682            assert_eq!(view_text(parameter.value_kind), "object");
16683
16684            assert_eq!(pio_module_extension_count(module), 1);
16685            let mut extension = std::mem::MaybeUninit::<PioModuleExtensionView>::uninit();
16686            assert!(pio_module_extension_at(
16687                module,
16688                0,
16689                extension.as_mut_ptr(),
16690                &mut error
16691            ));
16692            let extension = extension.assume_init();
16693            assert_eq!(view_text(extension.namespace), "org.example.record");
16694            assert_eq!(view_text(extension.value_kind), "object");
16695
16696            let parameter_value = pio_module_history_parameter_value_at(module, 0, 0, &mut error);
16697            assert!(!parameter_value.is_null(), "{}", error_text(error));
16698            let items = pio_json_value_object_value_at(parameter_value, 0, &mut error);
16699            assert!(!items.is_null(), "{}", error_text(error));
16700            let unsigned = pio_json_value_array_at(items, 3, &mut error);
16701            assert!(!unsigned.is_null(), "{}", error_text(error));
16702            let retained_unsigned = pio_json_value_retain(unsigned);
16703            let extension_value = pio_module_extension_value_at(module, 0, &mut error);
16704            assert!(!extension_value.is_null(), "{}", error_text(error));
16705
16706            let mut ignored = std::mem::MaybeUninit::<PioModuleSourceView>::uninit();
16707            assert!(!pio_module_source_at(
16708                module,
16709                1,
16710                ignored.as_mut_ptr(),
16711                &mut error
16712            ));
16713            assert!(!error.is_null());
16714            pio_error_release(error);
16715            error = std::ptr::null_mut();
16716
16717            pio_module_release(module);
16718            pio_json_value_release(parameter_value);
16719            pio_json_value_release(items);
16720            pio_json_value_release(unsigned);
16721
16722            let mut number = std::mem::MaybeUninit::<PioJsonValueView>::uninit();
16723            assert!(pio_json_value_get(
16724                retained_unsigned,
16725                number.as_mut_ptr(),
16726                &mut error
16727            ));
16728            let number = number.assume_init();
16729            assert_eq!(view_text(number.kind), "number");
16730            assert_eq!(view_text(number.number_kind), "unsigned_integer");
16731            assert_eq!(number.unsigned_integer_value, u64::MAX);
16732
16733            let enabled = pio_json_value_object_value_at(extension_value, 0, &mut error);
16734            assert!(!enabled.is_null(), "{}", error_text(error));
16735            let mut boolean = std::mem::MaybeUninit::<PioJsonValueView>::uninit();
16736            assert!(pio_json_value_get(
16737                enabled,
16738                boolean.as_mut_ptr(),
16739                &mut error
16740            ));
16741            let boolean = boolean.assume_init();
16742            assert_eq!(view_text(boolean.kind), "boolean");
16743            assert!(boolean.boolean_value);
16744
16745            let mut null_output = std::mem::MaybeUninit::<PioModuleProducerView>::uninit();
16746            assert!(!pio_module_producer(
16747                std::ptr::null(),
16748                null_output.as_mut_ptr(),
16749                &mut error
16750            ));
16751            assert!(!error.is_null());
16752            pio_error_release(error);
16753            pio_json_value_release(enabled);
16754            pio_json_value_release(extension_value);
16755            pio_json_value_release(retained_unsigned);
16756        }
16757    }
16758
16759    #[test]
16760    fn structural_access_keeps_the_module_owner_alive() {
16761        unsafe {
16762            let module = parse_case9();
16763            let diagnostics = pio_module_diagnostics(module);
16764            let value = pio_module_value(module);
16765            assert!(pio_value_is_type(
16766                value,
16767                c"powerio.BalancedNetwork".as_ptr(),
16768                "powerio.BalancedNetwork".len(),
16769            ));
16770            let mut error = std::ptr::null_mut();
16771            let network = pio_value_balanced_network(value, &mut error);
16772            assert!(!network.is_null(), "{}", error_text(error));
16773
16774            pio_module_release(module);
16775            pio_value_release(value);
16776            assert_eq!(pio_balanced_network_bus_count(network), 9);
16777            assert_eq!(pio_balanced_network_branch_count(network), 9);
16778            assert_eq!(pio_diagnostics_len(diagnostics), 0);
16779
16780            pio_balanced_network_release(network);
16781            pio_diagnostics_release(diagnostics);
16782        }
16783    }
16784
16785    #[test]
16786    fn abi_seven_bus_views_preserve_phase_bounds_after_owner_release() {
16787        unsafe {
16788            let mut network = complete_multiconductor_network();
16789            network.buses_mut()[0].v_min = None;
16790            network.buses_mut()[0].v_min_phase = Some(vec![210.0, 215.0]);
16791            let module = module_handle(powerio::PioModule::new(PioValue::from(network)));
16792            let value = pio_module_value(module);
16793            let mut error = std::ptr::null_mut();
16794            let network = pio_value_multiconductor_network(value, &mut error);
16795            assert!(!network.is_null());
16796            let mut bus = std::mem::MaybeUninit::<PioMulticonductorBusView>::uninit();
16797            pio_module_release(module);
16798            pio_value_release(value);
16799            assert!(pio_multiconductor_network_bus_at(
16800                network,
16801                0,
16802                bus.as_mut_ptr(),
16803                &mut error
16804            ));
16805            let bus = bus.assume_init();
16806            assert!(bus.has_phase_to_ground_voltage_min);
16807            assert!(!bus.has_voltage_min);
16808            assert_eq!(
16809                std::slice::from_raw_parts(
16810                    bus.phase_to_ground_voltage_min_v.data,
16811                    bus.phase_to_ground_voltage_min_v.len
16812                ),
16813                &[210.0, 215.0]
16814            );
16815            pio_multiconductor_network_release(network);
16816        }
16817    }
16818
16819    #[test]
16820    #[allow(clippy::too_many_lines)]
16821    fn multiconductor_typed_views_preserve_ownership_and_optional_values() {
16822        unsafe {
16823            let module = module_handle(powerio::PioModule::new(PioValue::from(
16824                complete_multiconductor_network(),
16825            )));
16826            let value = pio_module_value(module);
16827            let mut error = std::ptr::null_mut();
16828            let network = pio_value_multiconductor_network(value, &mut error);
16829            assert!(!network.is_null(), "{}", error_text(error));
16830            let retained = pio_multiconductor_network_retain(network);
16831            pio_module_release(module);
16832            pio_value_release(value);
16833            pio_multiconductor_network_release(network);
16834
16835            assert!(pio_multiconductor_network_has_name(retained));
16836            assert_eq!(pio_multiconductor_network_name(retained).len, 0);
16837            assert!(pio_multiconductor_network_has_source_format(retained));
16838            assert_eq!(
16839                view_text(pio_multiconductor_network_source_format(retained)),
16840                "dss"
16841            );
16842
16843            let mut geo = std::mem::MaybeUninit::<PioMulticonductorGeoView>::uninit();
16844            assert!(pio_multiconductor_network_geo(
16845                retained,
16846                geo.as_mut_ptr(),
16847                &mut error,
16848            ));
16849            let geo = geo.assume_init();
16850            assert!(geo.has_geo);
16851            assert_eq!(view_text(geo.space), "diagram");
16852            assert!(geo.has_canvas);
16853            assert!(geo.has_canvas_width);
16854            assert!(!geo.has_canvas_height);
16855            assert!(geo.has_canvas_units);
16856            assert_eq!(geo.canvas_units.len, 0);
16857
16858            let mut counts = std::mem::MaybeUninit::<PioMulticonductorNetworkCountsView>::uninit();
16859            assert!(pio_multiconductor_network_counts(
16860                retained,
16861                counts.as_mut_ptr(),
16862                &mut error,
16863            ));
16864            let counts = counts.assume_init();
16865            for count in [
16866                counts.buses,
16867                counts.line_codes,
16868                counts.lines,
16869                counts.switches,
16870                counts.transformers,
16871                counts.loads,
16872                counts.generators,
16873                counts.inverter_based_resources,
16874                counts.control_profiles,
16875                counts.shunts,
16876                counts.capacitors,
16877                counts.voltage_sources,
16878                counts.untyped_objects,
16879                counts.commands,
16880                counts.options,
16881            ] {
16882                assert_eq!(count, 1);
16883            }
16884
16885            let mut bus = std::mem::MaybeUninit::<PioMulticonductorBusView>::uninit();
16886            assert!(pio_multiconductor_network_bus_at(
16887                retained,
16888                0,
16889                bus.as_mut_ptr(),
16890                &mut error,
16891            ));
16892            let bus = bus.assume_init();
16893            assert_eq!(view_text(bus.id), "bus");
16894            assert!(bus.has_phase_to_neutral_voltage_min);
16895            assert_eq!(bus.phase_to_neutral_voltage_min_v.len, 0);
16896            assert!(bus.has_location);
16897            assert!(!bus.location.has_kind);
16898            let mut text = std::mem::MaybeUninit::<PioStringView>::uninit();
16899            assert!(pio_multiconductor_network_bus_terminal_at(
16900                retained,
16901                0,
16902                1,
16903                text.as_mut_ptr(),
16904                &mut error,
16905            ));
16906            assert_eq!(view_text(text.assume_init()), "2");
16907
16908            let mut line_code = std::mem::MaybeUninit::<PioMulticonductorLineCodeView>::uninit();
16909            assert!(pio_multiconductor_network_line_code_at(
16910                retained,
16911                0,
16912                line_code.as_mut_ptr(),
16913                &mut error,
16914            ));
16915            let line_code = line_code.assume_init();
16916            assert_eq!(line_code.resistance_matrix_row_count, 2);
16917            assert!(line_code.has_current_limit);
16918            assert_eq!(line_code.current_limit_a.len, 0);
16919            assert!(line_code.has_source);
16920            assert_eq!(line_code.source.len, 0);
16921            let mut matrix_row = std::mem::MaybeUninit::<PioF64View>::uninit();
16922            assert!(
16923                pio_multiconductor_network_line_code_resistance_matrix_row_at(
16924                    retained,
16925                    0,
16926                    1,
16927                    matrix_row.as_mut_ptr(),
16928                    &mut error,
16929                )
16930            );
16931            let matrix_row = matrix_row.assume_init();
16932            assert_eq!(matrix_row.len, 1);
16933            assert_eq!(*matrix_row.data, 3.0);
16934
16935            let mut line = std::mem::MaybeUninit::<PioMulticonductorLineView>::uninit();
16936            assert!(pio_multiconductor_network_line_at(
16937                retained,
16938                0,
16939                line.as_mut_ptr(),
16940                &mut error,
16941            ));
16942            let line = line.assume_init();
16943            assert!(line.has_route);
16944            assert_eq!(line.route_point_count, 0);
16945
16946            let mut switch = std::mem::MaybeUninit::<PioMulticonductorSwitchView>::uninit();
16947            assert!(pio_multiconductor_network_switch_at(
16948                retained,
16949                0,
16950                switch.as_mut_ptr(),
16951                &mut error,
16952            ));
16953            let switch = switch.assume_init();
16954            assert!(switch.open);
16955            assert!(switch.has_current_limit);
16956            assert_eq!(switch.current_limit_a.len, 0);
16957
16958            let mut transformer =
16959                std::mem::MaybeUninit::<PioMulticonductorTransformerView>::uninit();
16960            assert!(pio_multiconductor_network_transformer_at(
16961                retained,
16962                0,
16963                transformer.as_mut_ptr(),
16964                &mut error,
16965            ));
16966            assert_eq!(transformer.assume_init().winding_count, 1);
16967            let mut winding =
16968                std::mem::MaybeUninit::<PioMulticonductorTransformerWindingView>::uninit();
16969            assert!(pio_multiconductor_network_transformer_winding_at(
16970                retained,
16971                0,
16972                0,
16973                winding.as_mut_ptr(),
16974                &mut error,
16975            ));
16976            let winding = winding.assume_init();
16977            assert!(winding.has_neutral_resistance);
16978            assert!(!winding.has_neutral_reactance);
16979
16980            let mut load = std::mem::MaybeUninit::<PioMulticonductorLoadView>::uninit();
16981            assert!(pio_multiconductor_network_load_at(
16982                retained,
16983                0,
16984                load.as_mut_ptr(),
16985                &mut error,
16986            ));
16987            let load = load.assume_init();
16988            assert_eq!(view_text(load.voltage_model), "zip");
16989            assert_eq!(*load.active_power_constant_power.data, 0.7);
16990
16991            let mut generator = std::mem::MaybeUninit::<PioMulticonductorGeneratorView>::uninit();
16992            assert!(pio_multiconductor_network_generator_at(
16993                retained,
16994                0,
16995                generator.as_mut_ptr(),
16996                &mut error,
16997            ));
16998            let generator = generator.assume_init();
16999            assert!(generator.has_active_power_min);
17000            assert_eq!(generator.active_power_min_w.len, 0);
17001            assert!(generator.has_active_power_dispatch_cost);
17002
17003            let mut resource = std::mem::MaybeUninit::<PioInverterBasedResourceView>::uninit();
17004            assert!(pio_multiconductor_network_inverter_based_resource_at(
17005                retained,
17006                0,
17007                resource.as_mut_ptr(),
17008                &mut error,
17009            ));
17010            let resource = resource.assume_init();
17011            assert_eq!(view_text(resource.topology), "SINGLE_PHASE");
17012            assert!(resource.has_control_profile);
17013            assert_eq!(resource.control_profile.len, 0);
17014
17015            let mut profile = std::mem::MaybeUninit::<PioControlProfileView>::uninit();
17016            assert!(pio_multiconductor_network_control_profile_at(
17017                retained,
17018                0,
17019                profile.as_mut_ptr(),
17020                &mut error,
17021            ));
17022            let profile = profile.assume_init();
17023            assert!(profile.has_power_factor);
17024            assert!(profile.has_volt_var);
17025            assert!(profile.has_volt_watt);
17026            assert_eq!(
17027                view_text(profile.volt_var_voltage_reference),
17028                "PN_PER_PHASE"
17029            );
17030
17031            let mut shunt = std::mem::MaybeUninit::<PioMulticonductorShuntView>::uninit();
17032            assert!(pio_multiconductor_network_shunt_at(
17033                retained,
17034                0,
17035                shunt.as_mut_ptr(),
17036                &mut error,
17037            ));
17038            assert_eq!(shunt.assume_init().conductance_matrix_row_count, 2);
17039            let mut shunt_matrix_row = std::mem::MaybeUninit::<PioF64View>::uninit();
17040            assert!(pio_multiconductor_network_shunt_conductance_matrix_row_at(
17041                retained,
17042                0,
17043                1,
17044                shunt_matrix_row.as_mut_ptr(),
17045                &mut error,
17046            ));
17047            assert_eq!(shunt_matrix_row.assume_init().len, 1);
17048
17049            let mut capacitor = std::mem::MaybeUninit::<PioMulticonductorCapacitorView>::uninit();
17050            assert!(pio_multiconductor_network_capacitor_at(
17051                retained,
17052                0,
17053                capacitor.as_mut_ptr(),
17054                &mut error,
17055            ));
17056            assert_eq!(capacitor.assume_init().rated_reactive_power_var, 100.0);
17057
17058            let mut source = std::mem::MaybeUninit::<PioVoltageSourceView>::uninit();
17059            assert!(pio_multiconductor_network_voltage_source_at(
17060                retained,
17061                0,
17062                source.as_mut_ptr(),
17063                &mut error,
17064            ));
17065            let source = source.assume_init();
17066            assert_eq!(*source.voltage_magnitude_v.data, 240.0);
17067            assert!(source.has_energy_cost_rate);
17068            assert_eq!(source.energy_cost_rate_per_kwh.len, 1);
17069            assert_eq!(*source.energy_cost_rate_per_kwh.data, 0.125);
17070
17071            let mut object = std::mem::MaybeUninit::<PioMulticonductorUntypedObjectView>::uninit();
17072            assert!(pio_multiconductor_network_untyped_object_at(
17073                retained,
17074                0,
17075                object.as_mut_ptr(),
17076                &mut error,
17077            ));
17078            assert_eq!(object.assume_init().property_count, 2);
17079            let mut property =
17080                std::mem::MaybeUninit::<PioMulticonductorUntypedPropertyView>::uninit();
17081            assert!(pio_multiconductor_network_untyped_object_property_at(
17082                retained,
17083                0,
17084                0,
17085                property.as_mut_ptr(),
17086                &mut error,
17087            ));
17088            assert!(!property.assume_init().has_name);
17089            assert!(pio_multiconductor_network_untyped_object_property_at(
17090                retained,
17091                0,
17092                1,
17093                property.as_mut_ptr(),
17094                &mut error,
17095            ));
17096            let property = property.assume_init();
17097            assert!(property.has_name);
17098            assert_eq!(property.name.len, 0);
17099
17100            let mut command = std::mem::MaybeUninit::<PioMulticonductorCommandView>::uninit();
17101            assert!(pio_multiconductor_network_command_at(
17102                retained,
17103                0,
17104                command.as_mut_ptr(),
17105                &mut error,
17106            ));
17107            assert_eq!(view_text(command.assume_init().verb), "solve");
17108            let mut option = std::mem::MaybeUninit::<PioStringPropertyView>::uninit();
17109            assert!(pio_multiconductor_network_option_at(
17110                retained,
17111                0,
17112                option.as_mut_ptr(),
17113                &mut error,
17114            ));
17115            assert_eq!(view_text(option.assume_init().name), "frequency");
17116
17117            assert!(
17118                std::mem::offset_of!(PioMulticonductorLineView, has_route)
17119                    > std::mem::offset_of!(PioMulticonductorLineView, route_point_count)
17120            );
17121            assert!(
17122                std::mem::offset_of!(PioMulticonductorBusView, has_location)
17123                    > std::mem::offset_of!(PioMulticonductorBusView, location)
17124            );
17125
17126            let mut null_error = std::ptr::null_mut();
17127            let mut null_output = std::mem::MaybeUninit::<PioMulticonductorBusView>::uninit();
17128            assert!(!pio_multiconductor_network_bus_at(
17129                std::ptr::null(),
17130                0,
17131                null_output.as_mut_ptr(),
17132                &mut null_error,
17133            ));
17134            assert!(!null_error.is_null());
17135            assert_eq!(
17136                view_text(pio_error_code(null_error)),
17137                codes::BIND_CAPI_NULL_HANDLE.code
17138            );
17139            pio_error_release(null_error);
17140            pio_multiconductor_network_release(retained);
17141        }
17142    }
17143
17144    #[test]
17145    fn balanced_geography_is_typed_and_preserves_optional_routes() {
17146        unsafe {
17147            let mut network = case9_network();
17148            *network.geo_mut() = Some(powerio_tx::GeoMeta {
17149                space: powerio_tx::CoordinateSpace::Diagram {
17150                    canvas: Some(powerio_tx::Canvas {
17151                        width: Some(800.0),
17152                        height: None,
17153                        units: Some(String::new()),
17154                    }),
17155                },
17156                kind: Some(powerio_tx::CoordsKind::Manual),
17157            });
17158            network.buses_mut()[0].location = Some(powerio_tx::Location {
17159                x: 12.0,
17160                y: 34.0,
17161                kind: None,
17162            });
17163            network.branches_mut()[0].route = Some(Vec::new());
17164            network.branches_mut()[1].route = Some(vec![powerio_tx::Location {
17165                x: 56.0,
17166                y: 78.0,
17167                kind: Some(powerio_tx::CoordsKind::Derived),
17168            }]);
17169
17170            let module = module_handle(powerio::PioModule::new(PioValue::BalancedNetwork(network)));
17171            let value = pio_module_value(module);
17172            let mut error = std::ptr::null_mut();
17173            let balanced = pio_value_balanced_network(value, &mut error);
17174            assert!(!balanced.is_null(), "{}", error_text(error));
17175            let retained = pio_balanced_network_retain(balanced);
17176            pio_balanced_network_release(balanced);
17177            pio_value_release(value);
17178            pio_module_release(module);
17179
17180            let mut geo = std::mem::MaybeUninit::<PioBalancedGeoView>::uninit();
17181            assert!(pio_balanced_network_geo(
17182                retained,
17183                geo.as_mut_ptr(),
17184                &mut error
17185            ));
17186            let geo = geo.assume_init();
17187            assert!(geo.has_geo);
17188            assert_eq!(view_text(geo.space), "diagram");
17189            assert!(geo.has_kind);
17190            assert_eq!(view_text(geo.kind), "manual");
17191            assert!(geo.has_canvas);
17192            assert!(geo.has_canvas_width);
17193            assert_eq!(geo.canvas_width, 800.0);
17194            assert!(!geo.has_canvas_height);
17195            assert!(geo.has_canvas_units);
17196            assert_eq!(geo.canvas_units.len, 0);
17197
17198            let mut bus = std::mem::MaybeUninit::<PioBalancedBusView>::uninit();
17199            assert!(pio_balanced_network_bus_at(
17200                retained,
17201                0,
17202                bus.as_mut_ptr(),
17203                &mut error
17204            ));
17205            let bus = bus.assume_init();
17206            assert!(bus.has_location);
17207            assert_eq!((bus.location.x, bus.location.y), (12.0, 34.0));
17208            assert!(!bus.location.has_kind);
17209
17210            let mut branch = std::mem::MaybeUninit::<PioBalancedBranchView>::uninit();
17211            assert!(pio_balanced_network_branch_at(
17212                retained,
17213                0,
17214                branch.as_mut_ptr(),
17215                &mut error
17216            ));
17217            let branch = branch.assume_init();
17218            assert!(branch.has_route);
17219            assert_eq!(branch.route_point_count, 0);
17220
17221            let mut routed_branch = std::mem::MaybeUninit::<PioBalancedBranchView>::uninit();
17222            assert!(pio_balanced_network_branch_at(
17223                retained,
17224                1,
17225                routed_branch.as_mut_ptr(),
17226                &mut error
17227            ));
17228            let routed_branch = routed_branch.assume_init();
17229            assert!(routed_branch.has_route);
17230            assert_eq!(routed_branch.route_point_count, 1);
17231            let mut point = std::mem::MaybeUninit::<PioBalancedLocationView>::uninit();
17232            assert!(pio_balanced_network_branch_route_point_at(
17233                retained,
17234                1,
17235                0,
17236                point.as_mut_ptr(),
17237                &mut error
17238            ));
17239            let point = point.assume_init();
17240            assert_eq!((point.x, point.y), (56.0, 78.0));
17241            assert!(point.has_kind);
17242            assert_eq!(view_text(point.kind), "derived");
17243
17244            assert!(
17245                std::mem::offset_of!(PioBalancedBusView, has_location)
17246                    > std::mem::offset_of!(PioBalancedBusView, location)
17247            );
17248            assert!(
17249                std::mem::offset_of!(PioBalancedBranchView, has_route)
17250                    > std::mem::offset_of!(PioBalancedBranchView, route_point_count)
17251            );
17252
17253            let mut null_geo = std::mem::MaybeUninit::<PioBalancedGeoView>::uninit();
17254            assert!(!pio_balanced_network_geo(
17255                std::ptr::null(),
17256                null_geo.as_mut_ptr(),
17257                &mut error
17258            ));
17259            assert!(!error.is_null());
17260            pio_error_release(error);
17261            pio_balanced_network_release(retained);
17262        }
17263    }
17264
17265    #[test]
17266    #[allow(clippy::too_many_lines)]
17267    fn every_solution_exposes_its_owner_rooted_instance() {
17268        unsafe {
17269            let network = case9_network();
17270            let buses = network.buses().len();
17271            let branches = network.branches().len();
17272            let generators = network.generators().len();
17273            let three_winding = network.transformers_3w().len();
17274
17275            let dc_pf_instance =
17276                Arc::new(powerio_prob::DcPfInstance::from_network(network.clone()).unwrap());
17277            let dc_pf = powerio_prob::DcPfSolution::new(
17278                dc_pf_instance,
17279                powerio_prob::Termination::Converged,
17280                vec![0.0; buses],
17281                vec![0.0; buses],
17282                vec![0.0; branches],
17283                vec![0.0; branches],
17284                vec![Default::default(); three_winding],
17285            )
17286            .unwrap();
17287
17288            let ac_pf_instance =
17289                Arc::new(powerio_prob::AcPfInstance::from_network(network.clone()).unwrap());
17290            let ac_pf = powerio_prob::AcPfSolution::new(
17291                ac_pf_instance,
17292                powerio_prob::Termination::Converged,
17293                vec![1.0; buses],
17294                vec![0.0; buses],
17295                vec![0.0; buses],
17296                vec![0.0; buses],
17297                vec![0.0; branches],
17298                vec![0.0; branches],
17299                vec![0.0; branches],
17300                vec![0.0; branches],
17301                vec![Default::default(); three_winding],
17302            )
17303            .unwrap();
17304
17305            let dc_opf_instance =
17306                Arc::new(powerio_prob::DcOpfInstance::from_network(network.clone()).unwrap());
17307            let dc_opf = powerio_prob::DcOpfSolution::new(
17308                dc_opf_instance,
17309                powerio_prob::Termination::Converged,
17310                vec![0.0; buses],
17311                vec![0.0; buses],
17312                vec![0.0; branches],
17313                vec![0.0; branches],
17314                vec![0.0; generators],
17315                0.0,
17316                vec![Default::default(); three_winding],
17317            )
17318            .unwrap();
17319
17320            let ac_opf_instance =
17321                Arc::new(powerio_prob::AcOpfInstance::from_network(network.clone()).unwrap());
17322            let ac_opf = powerio_prob::AcOpfSolution::new(
17323                Arc::clone(&ac_opf_instance),
17324                powerio_prob::Termination::Converged,
17325                vec![1.0; buses],
17326                vec![0.0; buses],
17327                vec![0.0; buses],
17328                vec![0.0; buses],
17329                vec![0.0; branches],
17330                vec![0.0; branches],
17331                vec![0.0; branches],
17332                vec![0.0; branches],
17333                vec![0.0; generators],
17334                vec![0.0; generators],
17335                0.0,
17336                vec![Default::default(); three_winding],
17337            )
17338            .unwrap();
17339
17340            let mut socwr_values = powerio_prob::solution::SocwrOpfValues::default();
17341            socwr_values.bus_voltage_magnitude_squared = vec![1.0; buses];
17342            socwr_values.branch_voltage_product_real = vec![0.0; branches];
17343            socwr_values.branch_voltage_product_imaginary = vec![0.0; branches];
17344            socwr_values.generator_active_power = vec![0.0; generators];
17345            socwr_values.generator_reactive_power = vec![0.0; generators];
17346            socwr_values.branch_from_active_power = vec![0.0; branches];
17347            socwr_values.branch_from_reactive_power = vec![0.0; branches];
17348            socwr_values.branch_to_active_power = vec![0.0; branches];
17349            socwr_values.branch_to_reactive_power = vec![0.0; branches];
17350            socwr_values.three_winding_transformer_terminal_powers =
17351                vec![Default::default(); three_winding];
17352            let socwr = powerio_prob::solution::SocwrOpfSolution::new(
17353                ac_opf_instance,
17354                powerio_prob::Termination::Converged,
17355                socwr_values,
17356                0.0,
17357            )
17358            .unwrap();
17359
17360            let multiconductor = multiconductor_network();
17361            let terminals: usize = multiconductor
17362                .buses()
17363                .iter()
17364                .map(|bus| bus.terminals.len())
17365                .sum();
17366            let source_terminals: usize = multiconductor
17367                .sources()
17368                .iter()
17369                .map(|source| source.terminal_map.len())
17370                .sum();
17371            let generator_terminals: usize = multiconductor
17372                .generators()
17373                .iter()
17374                .map(|generator| generator.terminal_map.len())
17375                .sum();
17376            let mc_pf_instance = Arc::new(
17377                powerio_prob::McAcPfInstance::from_network(multiconductor.clone()).unwrap(),
17378            );
17379            let mc_pf = powerio_prob::McAcPfSolution::new(
17380                mc_pf_instance,
17381                powerio_prob::Termination::Converged,
17382                vec![1.0; terminals],
17383                vec![0.0; terminals],
17384                vec![0.0; source_terminals],
17385            )
17386            .unwrap();
17387            let mc_opf_instance =
17388                Arc::new(powerio_prob::McAcOpfInstance::from_network(multiconductor).unwrap());
17389            let mc_opf = powerio_prob::McAcOpfSolution::new(
17390                mc_opf_instance,
17391                powerio_prob::Termination::Converged,
17392                vec![1.0; terminals],
17393                vec![0.0; terminals],
17394                vec![0.0; source_terminals],
17395                vec![0.0; generator_terminals],
17396                0.0,
17397            )
17398            .unwrap();
17399
17400            let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
17401                .join("../tests/data/goc3/goc3_small.json");
17402            let scuc_instance = goc3_instance(Source::open(path).unwrap());
17403            let scuc = powerio_prob::AcScucSolution::new(
17404                Arc::new(scuc_instance),
17405                powerio_prob::Termination::Converged,
17406                powerio_prob::ScucNetworkOutputs::default(),
17407                powerio_prob::ScucDeviceOutputs::default(),
17408                None,
17409            )
17410            .unwrap();
17411
17412            type SolutionAccessor = unsafe extern "C" fn(
17413                *const PioValueHandle,
17414                *mut *mut PioError,
17415            )
17416                -> *mut PioCalculationSolution;
17417            let solutions: [(PioValue, &str, bool, SolutionAccessor); 8] = [
17418                (
17419                    PioValue::DcPfSolution(dc_pf),
17420                    "powerio.DcPfInstance",
17421                    false,
17422                    pio_value_dc_pf_solution,
17423                ),
17424                (
17425                    PioValue::AcPfSolution(ac_pf),
17426                    "powerio.AcPfInstance",
17427                    false,
17428                    pio_value_ac_pf_solution,
17429                ),
17430                (
17431                    PioValue::DcOpfSolution(dc_opf),
17432                    "powerio.DcOpfInstance",
17433                    false,
17434                    pio_value_dc_opf_solution,
17435                ),
17436                (
17437                    PioValue::AcOpfSolution(ac_opf),
17438                    "powerio.AcOpfInstance",
17439                    false,
17440                    pio_value_ac_opf_solution,
17441                ),
17442                (
17443                    PioValue::SocwrOpfSolution(socwr),
17444                    "powerio.AcOpfInstance",
17445                    false,
17446                    pio_value_socwr_opf_solution,
17447                ),
17448                (
17449                    PioValue::McAcPfSolution(mc_pf),
17450                    "powerio.McAcPfInstance",
17451                    true,
17452                    pio_value_mc_ac_pf_solution,
17453                ),
17454                (
17455                    PioValue::McAcOpfSolution(mc_opf),
17456                    "powerio.McAcOpfInstance",
17457                    true,
17458                    pio_value_mc_ac_opf_solution,
17459                ),
17460                (
17461                    PioValue::AcScucSolution(scuc),
17462                    "powerio.AcScucInstance",
17463                    false,
17464                    pio_value_ac_scuc_solution,
17465                ),
17466            ];
17467
17468            for (value, expected_instance_type, is_multiconductor, accessor) in solutions {
17469                let module = module_handle(powerio::PioModule::new(value));
17470                let value = pio_module_value(module);
17471                let mut error = std::ptr::null_mut();
17472                let solution = accessor(value, &mut error);
17473                assert!(!solution.is_null(), "{}", error_text(error));
17474                let instance = pio_calculation_solution_instance(solution, &mut error);
17475                assert!(!instance.is_null(), "{}", error_text(error));
17476
17477                pio_calculation_solution_release(solution);
17478                pio_value_release(value);
17479                pio_module_release(module);
17480
17481                assert_eq!(
17482                    view_text(pio_calculation_instance_type_name(instance)),
17483                    expected_instance_type
17484                );
17485                if is_multiconductor {
17486                    let network =
17487                        pio_calculation_instance_multiconductor_network(instance, &mut error);
17488                    assert!(!network.is_null(), "{}", error_text(error));
17489                    assert!(pio_multiconductor_network_bus_count(network) > 0);
17490                    pio_multiconductor_network_release(network);
17491                } else {
17492                    let network = pio_calculation_instance_balanced_network(instance, &mut error);
17493                    assert!(!network.is_null(), "{}", error_text(error));
17494                    assert!(pio_balanced_network_bus_count(network) > 0);
17495                    pio_balanced_network_release(network);
17496                }
17497                pio_calculation_instance_release(instance);
17498            }
17499        }
17500    }
17501
17502    #[test]
17503    fn balanced_rows_are_checked_borrowed_views() {
17504        unsafe {
17505            let module = parse_case9();
17506            let value = pio_module_value(module);
17507            let mut error = std::ptr::null_mut();
17508            let network = pio_value_balanced_network(value, &mut error);
17509            assert!(!network.is_null(), "{}", error_text(error));
17510
17511            pio_module_release(module);
17512            pio_value_release(value);
17513
17514            assert_eq!(pio_balanced_network_base_mva(network), 100.0);
17515            assert_eq!(pio_balanced_network_base_frequency_hz(network), 60.0);
17516            assert_eq!(pio_balanced_network_bus_count(network), 9);
17517            assert_eq!(pio_balanced_network_load_count(network), 3);
17518            assert_eq!(pio_balanced_network_shunt_count(network), 0);
17519            assert_eq!(pio_balanced_network_branch_count(network), 9);
17520            assert_eq!(pio_balanced_network_generator_count(network), 3);
17521            assert_eq!(pio_balanced_network_storage_count(network), 0);
17522
17523            let mut bus = std::mem::MaybeUninit::<PioBalancedBusView>::uninit();
17524            assert!(pio_balanced_network_bus_at(
17525                network,
17526                0,
17527                bus.as_mut_ptr(),
17528                &mut error,
17529            ));
17530            let bus = bus.assume_init();
17531            assert_eq!(bus.id, 1);
17532            assert_eq!(view_text(bus.bus_type), "REF");
17533            assert_eq!(bus.base_kv, 345.0);
17534            assert!(bus.has_component_id);
17535            assert_eq!(view_text(bus.component_id), "1");
17536
17537            let mut load = std::mem::MaybeUninit::<PioBalancedLoadView>::uninit();
17538            assert!(pio_balanced_network_load_at(
17539                network,
17540                0,
17541                load.as_mut_ptr(),
17542                &mut error,
17543            ));
17544            let load = load.assume_init();
17545            assert_eq!(load.bus_id, 5);
17546            assert_eq!(load.p_mw, 90.0);
17547            assert_eq!(load.q_mvar, 30.0);
17548            assert_eq!(view_text(load.voltage_model.kind), "constant_power");
17549
17550            let mut branch = std::mem::MaybeUninit::<PioBalancedBranchView>::uninit();
17551            assert!(pio_balanced_network_branch_at(
17552                network,
17553                0,
17554                branch.as_mut_ptr(),
17555                &mut error,
17556            ));
17557            let branch = branch.assume_init();
17558            assert_eq!(branch.from_bus_id, 1);
17559            assert_eq!(branch.to_bus_id, 4);
17560            assert_eq!(branch.reactance_pu, 0.0576);
17561            assert_eq!(branch.rate_a_mva, 250.0);
17562            assert_eq!(branch.tap_ratio, 0.0);
17563            assert_eq!(branch.effective_tap_ratio, 1.0);
17564            assert!(!branch.terminal_charging_is_explicit);
17565            assert_eq!(branch.additional_rating_count, 0);
17566
17567            let mut generator = std::mem::MaybeUninit::<PioBalancedGeneratorView>::uninit();
17568            assert!(pio_balanced_network_generator_at(
17569                network,
17570                0,
17571                generator.as_mut_ptr(),
17572                &mut error,
17573            ));
17574            let generator = generator.assume_init();
17575            assert_eq!(generator.bus_id, 1);
17576            assert_eq!(view_text(generator.energy_source), "other");
17577            assert_eq!(generator.active_power_mw, 72.3);
17578            assert!(generator.has_cost);
17579            assert!(generator.voltage_regulation_on);
17580            assert!(!generator.has_regulating_terminal);
17581            assert!(!generator.has_active_power_control);
17582            assert!(!generator.active_power_control.has_droop_percent);
17583            assert_eq!(generator.cost.model, 2);
17584            assert_eq!(generator.cost.ncost, 3);
17585            assert_eq!(generator.cost.coefficients.len, 3);
17586            let coefficients = std::slice::from_raw_parts(
17587                generator.cost.coefficients.data,
17588                generator.cost.coefficients.len,
17589            );
17590            assert_eq!(coefficients, &[0.11, 5.0, 150.0]);
17591
17592            let mut capability = std::mem::MaybeUninit::<PioGeneratorCapabilityView>::uninit();
17593            assert!(pio_balanced_network_generator_capability_at(
17594                network,
17595                0,
17596                0,
17597                capability.as_mut_ptr(),
17598                &mut error,
17599            ));
17600            let capability = capability.assume_init();
17601            assert_eq!(view_text(capability.name), "pc1");
17602            assert!(capability.has_value);
17603            assert_eq!(capability.value, 0.0);
17604
17605            let mut missing = std::mem::MaybeUninit::<PioBalancedBusView>::uninit();
17606            assert!(!pio_balanced_network_bus_at(
17607                network,
17608                9,
17609                missing.as_mut_ptr(),
17610                &mut error,
17611            ));
17612            assert_eq!(
17613                view_text(pio_error_code(error)),
17614                "BIND.CAPI.INDEX_OUT_OF_RANGE"
17615            );
17616            pio_error_release(error);
17617
17618            pio_balanced_network_release(network);
17619        }
17620    }
17621
17622    #[test]
17623    fn balanced_optional_row_fields_are_explicit() {
17624        unsafe {
17625            let mut network = BalancedNetwork::new("optional-fields", 100.0);
17626            network.buses_mut().push(powerio_tx::Bus::new(
17627                powerio_tx::BusId(1),
17628                powerio_tx::BusType::Ref,
17629                115.0,
17630            ));
17631            network.buses_mut().push(powerio_tx::Bus::new(
17632                powerio_tx::BusId(2),
17633                powerio_tx::BusType::Pq,
17634                115.0,
17635            ));
17636
17637            let mut load = powerio_tx::Load::new(powerio_tx::BusId(2), 6.0, 3.0);
17638            load.voltage_model = Some(powerio_tx::LoadVoltageModel::Zip {
17639                p_constant_power: 3.0,
17640                q_constant_power: 1.5,
17641                p_constant_current: 2.0,
17642                q_constant_current: 1.0,
17643                p_constant_impedance: 1.0,
17644                q_constant_impedance: 0.5,
17645                v_nom: Some(1.0),
17646                load_type: Some(7),
17647                scaling: Some(0.9),
17648            });
17649            network.loads_mut().push(load);
17650
17651            let mut shunt = powerio_tx::Shunt::new(powerio_tx::BusId(2), 0.0, 2.0);
17652            let mut control = powerio_tx::SwitchedShuntControl::new(
17653                powerio_tx::SwitchedShuntMode::Discrete,
17654                1.05,
17655                0.95,
17656                vec![powerio_tx::ShuntBlock::with_admittance(4, 0.25, 0.5)],
17657            );
17658            control.control_bus = Some(powerio_tx::BusId(1));
17659            shunt.control = Some(control);
17660            network.shunts_mut().push(shunt);
17661
17662            let mut branch =
17663                powerio_tx::Branch::new(powerio_tx::BusId(1), powerio_tx::BusId(2), 0.01, 0.1);
17664            branch.name = Some("controlled transformer".to_owned());
17665            branch.charging = Some(powerio_tx::BranchCharging::new(0.01, 0.02, 0.03, 0.04));
17666            branch
17667                .rating_sets
17668                .push(powerio_tx::BranchRatingSet::new("LTE", 175.0));
17669            branch.current_ratings = Some(powerio_tx::BranchCurrentRatings::new(1.0, 2.0, 3.0));
17670            branch.tap = 1.05;
17671            branch.shift = 4.0;
17672            branch.angmin = -30.0;
17673            branch.angmax = 30.0;
17674            let mut transformer_control = powerio_tx::TransformerControl::new(
17675                powerio_tx::TransformerControlMode::DcLineQuantity,
17676            );
17677            transformer_control.enabled = false;
17678            transformer_control.controlled_bus = Some(powerio_tx::BusId(2));
17679            transformer_control.controlled_bus_on_winding_side = true;
17680            transformer_control.regulating_terminal = Some(
17681                serde_json::from_value(serde_json::json!({
17682                    "equipment": {
17683                        "component_type": "transformer",
17684                        "local_id": "controlled-transformer"
17685                    },
17686                    "terminal": 2
17687                }))
17688                .unwrap(),
17689            );
17690            transformer_control.ntp = 17;
17691            transformer_control.winding_connection_angle = Some(12.5);
17692            branch.control = Some(transformer_control);
17693            network.branches_mut().push(branch);
17694
17695            let mut generator = powerio_tx::Generator::new(powerio_tx::BusId(1));
17696            generator.energy_source = powerio_tx::GeneratorEnergySource::Nuclear;
17697            generator.cost = Some(powerio_tx::GenCost::new(2, 10.0, 20.0, vec![1.0, 2.0]));
17698            generator.caps[8] = Some(25.0);
17699            generator.voltage_regulation_on = false;
17700            generator.regulating_terminal = Some(
17701                serde_json::from_value(serde_json::json!({
17702                    "equipment": {
17703                        "component_type": "load",
17704                        "local_id": "regulated-load"
17705                    },
17706                    "terminal": 1
17707                }))
17708                .unwrap(),
17709            );
17710            generator.regulated_bus = Some(powerio_tx::BusId(2));
17711            let mut generator_control = powerio_tx::ActivePowerControl::new(true);
17712            generator_control.droop_percent = Some(4.0);
17713            generator_control.participation_factor = Some(0.6);
17714            generator_control.minimum_target_active_power_mw = Some(10.0);
17715            generator_control.maximum_target_active_power_mw = Some(100.0);
17716            generator.active_power_control = Some(generator_control);
17717            network.generators_mut().push(generator);
17718
17719            let mut storage = powerio_tx::Storage::new(powerio_tx::BusId(2));
17720            storage.energy = 2.0;
17721            storage.energy_rating = 10.0;
17722            storage.current_rating = Some(100.0);
17723            let mut storage_control = powerio_tx::ActivePowerControl::new(false);
17724            storage_control.participation_factor = Some(0.4);
17725            storage.active_power_control = Some(storage_control);
17726            network.storage_mut().push(storage);
17727
17728            let module = module_handle(powerio::PioModule::new(PioValue::from(network)));
17729            let value = pio_module_value(module);
17730            let mut error = std::ptr::null_mut();
17731            let network = pio_value_balanced_network(value, &mut error);
17732
17733            let mut load = std::mem::MaybeUninit::<PioBalancedLoadView>::uninit();
17734            assert!(pio_balanced_network_load_at(
17735                network,
17736                0,
17737                load.as_mut_ptr(),
17738                &mut error
17739            ));
17740            let load = load.assume_init();
17741            assert_eq!(view_text(load.voltage_model.kind), "zip");
17742            assert_eq!(load.voltage_model.p_constant_current_mw, 2.0);
17743            assert!(load.voltage_model.has_nominal_voltage);
17744            assert!(load.voltage_model.has_load_type);
17745            assert!(load.voltage_model.has_scaling);
17746
17747            let mut shunt = std::mem::MaybeUninit::<PioBalancedShuntView>::uninit();
17748            assert!(pio_balanced_network_shunt_at(
17749                network,
17750                0,
17751                shunt.as_mut_ptr(),
17752                &mut error
17753            ));
17754            let shunt = shunt.assume_init();
17755            assert!(!shunt.has_section_count);
17756            assert_eq!(shunt.section_count, 0);
17757            assert!(shunt.has_control);
17758            assert_eq!(view_text(shunt.control_mode), "discrete");
17759            assert_eq!(shunt.control_block_count, 1);
17760            assert!(shunt.has_control_bus);
17761            let mut block = std::mem::MaybeUninit::<PioShuntBlockView>::uninit();
17762            assert!(pio_balanced_network_shunt_block_at(
17763                network,
17764                0,
17765                0,
17766                block.as_mut_ptr(),
17767                &mut error,
17768            ));
17769            let block = block.assume_init();
17770            assert_eq!(block.steps, 4);
17771            assert_eq!(block.conductance_mw, 0.25);
17772            assert_eq!(block.susceptance_mvar, 0.5);
17773
17774            let mut branch = std::mem::MaybeUninit::<PioBalancedBranchView>::uninit();
17775            assert!(pio_balanced_network_branch_at(
17776                network,
17777                0,
17778                branch.as_mut_ptr(),
17779                &mut error,
17780            ));
17781            let branch = branch.assume_init();
17782            assert!(branch.has_name);
17783            assert_eq!(view_text(branch.name), "controlled transformer");
17784            assert!(branch.has_control);
17785            assert_eq!(view_text(branch.control.mode), "dc_line_quantity");
17786            assert!(!branch.control.enabled);
17787            assert!(branch.control.has_controlled_bus);
17788            assert_eq!(branch.control.controlled_bus_id, 2);
17789            assert!(branch.control.controlled_bus_on_winding_side);
17790            assert!(branch.control.has_regulating_terminal);
17791            assert_eq!(branch.control.regulating_terminal.terminal, 2);
17792            assert_eq!(branch.control.tap_position_count, 17);
17793            assert!(branch.control.has_winding_connection_angle);
17794            assert_eq!(branch.control.winding_connection_angle, 12.5);
17795            assert!(branch.terminal_charging_is_explicit);
17796            assert_eq!(branch.from_conductance_pu, 0.01);
17797            assert_eq!(branch.to_susceptance_pu, 0.04);
17798            assert!(branch.has_current_ratings);
17799            assert_eq!(branch.current_rating_c, 3.0);
17800            assert_eq!(branch.additional_rating_count, 1);
17801            let mut rating = std::mem::MaybeUninit::<PioBranchRatingView>::uninit();
17802            assert!(pio_balanced_network_branch_rating_at(
17803                network,
17804                0,
17805                0,
17806                rating.as_mut_ptr(),
17807                &mut error,
17808            ));
17809            let rating = rating.assume_init();
17810            assert_eq!(view_text(rating.name), "LTE");
17811            assert_eq!(rating.rate_mva, 175.0);
17812
17813            let mut generator = std::mem::MaybeUninit::<PioBalancedGeneratorView>::uninit();
17814            assert!(pio_balanced_network_generator_at(
17815                network,
17816                0,
17817                generator.as_mut_ptr(),
17818                &mut error,
17819            ));
17820            let generator = generator.assume_init();
17821            assert!(generator.has_regulated_bus);
17822            assert_eq!(generator.regulated_bus_id, 2);
17823            assert!(!generator.voltage_regulation_on);
17824            assert!(generator.has_regulating_terminal);
17825            assert_eq!(view_text(generator.energy_source), "nuclear");
17826            assert_eq!(
17827                view_text(generator.regulating_terminal.equipment.component_type),
17828                "load"
17829            );
17830            assert_eq!(
17831                view_text(generator.regulating_terminal.equipment.local_id),
17832                "regulated-load"
17833            );
17834            assert_eq!(generator.regulating_terminal.terminal, 1);
17835            assert!(generator.has_active_power_control);
17836            assert!(generator.active_power_control.participate);
17837            assert!(generator.active_power_control.has_droop_percent);
17838            assert_eq!(generator.active_power_control.droop_percent, 4.0);
17839            assert!(generator.active_power_control.has_participation_factor);
17840            assert_eq!(generator.active_power_control.participation_factor, 0.6);
17841            assert!(
17842                generator
17843                    .active_power_control
17844                    .has_minimum_target_active_power
17845            );
17846            assert_eq!(
17847                generator
17848                    .active_power_control
17849                    .minimum_target_active_power_mw,
17850                10.0
17851            );
17852            assert!(
17853                generator
17854                    .active_power_control
17855                    .has_maximum_target_active_power
17856            );
17857            assert_eq!(
17858                generator
17859                    .active_power_control
17860                    .maximum_target_active_power_mw,
17861                100.0
17862            );
17863            let mut capability = std::mem::MaybeUninit::<PioGeneratorCapabilityView>::uninit();
17864            assert!(pio_balanced_network_generator_capability_at(
17865                network,
17866                0,
17867                8,
17868                capability.as_mut_ptr(),
17869                &mut error,
17870            ));
17871            let capability = capability.assume_init();
17872            assert_eq!(view_text(capability.name), "ramp_30");
17873            assert!(capability.has_value);
17874            assert_eq!(capability.value, 25.0);
17875
17876            let mut storage = std::mem::MaybeUninit::<PioBalancedStorageView>::uninit();
17877            assert!(pio_balanced_network_storage_at(
17878                network,
17879                0,
17880                storage.as_mut_ptr(),
17881                &mut error,
17882            ));
17883            let storage = storage.assume_init();
17884            assert_eq!(storage.energy_mwh, 2.0);
17885            assert_eq!(storage.energy_rating_mwh, 10.0);
17886            assert!(storage.has_current_rating);
17887            assert_eq!(storage.current_rating, 100.0);
17888            assert!(storage.has_active_power_control);
17889            assert!(!storage.active_power_control.participate);
17890            assert!(!storage.active_power_control.has_droop_percent);
17891            assert!(storage.active_power_control.has_participation_factor);
17892            assert_eq!(storage.active_power_control.participation_factor, 0.4);
17893
17894            pio_balanced_network_release(network);
17895            pio_value_release(value);
17896            pio_module_release(module);
17897        }
17898    }
17899
17900    #[test]
17901    fn powsybl_balanced_tables_are_borrowed_without_serialization() {
17902        unsafe {
17903            let mut network = BalancedNetwork::new("powsybl-tables", 100.0);
17904            for (id, kind, kv) in [
17905                (1, powerio_tx::BusType::Ref, 400.0),
17906                (2, powerio_tx::BusType::Pq, 225.0),
17907                (3, powerio_tx::BusType::Pq, 63.0),
17908            ] {
17909                network
17910                    .buses_mut()
17911                    .push(powerio_tx::Bus::new(powerio_tx::BusId(id), kind, kv));
17912            }
17913
17914            let mut shunt = powerio_tx::Shunt::new(powerio_tx::BusId(2), 0.0, 12.0);
17915            shunt.uid = Some("SH".to_owned());
17916            shunt.section_count = Some(2);
17917            network.shunts_mut().push(shunt);
17918
17919            let mut svc = powerio_tx::StaticVarCompensator::new(powerio_tx::BusId(2), -0.02, 0.03);
17920            svc.uid = Some("SVC".to_owned());
17921            svc.regulating = true;
17922            svc.regulation_mode = powerio_tx::StaticVarCompensatorRegulationMode::ReactivePower;
17923            svc.reactive_power_setpoint_mvar = 12.0;
17924            svc.regulating_terminal = Some(
17925                serde_json::from_value(serde_json::json!({
17926                    "equipment": {
17927                        "component_type": "static_var_compensator",
17928                        "local_id": "SVC"
17929                    },
17930                    "terminal": 1
17931                }))
17932                .unwrap(),
17933            );
17934            network.static_var_compensators_mut().push(svc);
17935
17936            let mut switch =
17937                powerio_tx::Switch::new(powerio_tx::BusId(1), powerio_tx::BusId(2), true);
17938            switch.uid = Some("SW".to_owned());
17939            switch.thermal_rating = Some(500.0);
17940            switch.pf = Some(75.0);
17941            network.switches_mut().push(switch);
17942
17943            let mut hvdc = powerio_tx::Hvdc::new(powerio_tx::BusId(1), powerio_tx::BusId(2));
17944            hvdc.uid = Some("HVDC".to_owned());
17945            hvdc.resistance_ohm = Some(1.5);
17946            hvdc.nominal_voltage_kv = Some(320.0);
17947            hvdc.converters_mode =
17948                Some(powerio_tx::HvdcConvertersMode::Side1RectifierSide2Inverter);
17949            hvdc.converter1 = Some(
17950                serde_json::from_value(serde_json::json!({
17951                    "component": {
17952                        "component_type": "hvdc_converter",
17953                        "local_id": "C1"
17954                    },
17955                    "kind": "vsc",
17956                    "loss_factor_percent": 1.0,
17957                    "voltage_regulator_on": true,
17958                    "voltage_setpoint_kv": 400.0
17959                }))
17960                .unwrap(),
17961            );
17962            network.hvdc_mut().push(hvdc);
17963
17964            let mut windings = [
17965                powerio_tx::Winding::new(powerio_tx::BusId(1)),
17966                powerio_tx::Winding::new(powerio_tx::BusId(2)),
17967                powerio_tx::Winding::new(powerio_tx::BusId(3)),
17968            ];
17969            let mut winding_control =
17970                powerio_tx::TransformerControl::new(powerio_tx::TransformerControlMode::ActiveFlow);
17971            winding_control.controlled_bus = Some(powerio_tx::BusId(3));
17972            winding_control.regulating_terminal = Some(
17973                serde_json::from_value(serde_json::json!({
17974                    "equipment": {
17975                        "component_type": "transformer",
17976                        "local_id": "T3"
17977                    },
17978                    "terminal": 3
17979                }))
17980                .unwrap(),
17981            );
17982            winding_control.ntp = 21;
17983            windings[2].control = Some(winding_control);
17984            let mut transformer = powerio_tx::Transformer3W::new(
17985                windings,
17986                [
17987                    powerio_tx::Impedance::new(0.01, 0.1, 100.0),
17988                    powerio_tx::Impedance::new(0.02, 0.2, 100.0),
17989                    powerio_tx::Impedance::new(0.03, 0.3, 100.0),
17990                ],
17991            );
17992            transformer.uid = Some("T3".to_owned());
17993            transformer.name = Some("three winding".to_owned());
17994            network.transformers_3w_mut().push(transformer);
17995
17996            let mut area = powerio_tx::Area::new(7);
17997            area.uid = Some("A7".to_owned());
17998            area.name = Some("control area".to_owned());
17999            area.area_type = Some("control_area".to_owned());
18000            area.slack_bus = Some(powerio_tx::BusId(1));
18001            area.net_interchange = 12.5;
18002            network.areas_mut().push(area);
18003
18004            let module = module_handle(powerio::PioModule::new(PioValue::from(network)));
18005            let value = pio_module_value(module);
18006            let mut error = std::ptr::null_mut();
18007            let network = pio_value_balanced_network(value, &mut error);
18008            pio_value_release(value);
18009            pio_module_release(module);
18010
18011            assert_eq!(
18012                pio_balanced_network_static_var_compensator_count(network),
18013                1
18014            );
18015            assert_eq!(pio_balanced_network_shunt_count(network), 1);
18016            assert_eq!(pio_balanced_network_switch_count(network), 1);
18017            assert_eq!(pio_balanced_network_hvdc_count(network), 1);
18018            assert_eq!(
18019                pio_balanced_network_three_winding_transformer_count(network),
18020                1
18021            );
18022            assert_eq!(pio_balanced_network_area_count(network), 1);
18023
18024            let mut svc = std::mem::MaybeUninit::<PioBalancedStaticVarCompensatorView>::uninit();
18025            assert!(pio_balanced_network_static_var_compensator_at(
18026                network,
18027                0,
18028                svc.as_mut_ptr(),
18029                &mut error,
18030            ));
18031            let svc = svc.assume_init();
18032            assert_eq!(view_text(svc.component_id), "SVC");
18033            assert_eq!(view_text(svc.regulation_mode), "reactive_power");
18034            assert!(svc.has_regulating_terminal);
18035
18036            let mut shunt = std::mem::MaybeUninit::<PioBalancedShuntView>::uninit();
18037            assert!(pio_balanced_network_shunt_at(
18038                network,
18039                0,
18040                shunt.as_mut_ptr(),
18041                &mut error,
18042            ));
18043            let shunt = shunt.assume_init();
18044            assert!(shunt.has_section_count);
18045            assert_eq!(shunt.section_count, 2);
18046
18047            let mut switch = std::mem::MaybeUninit::<PioBalancedSwitchView>::uninit();
18048            assert!(pio_balanced_network_switch_at(
18049                network,
18050                0,
18051                switch.as_mut_ptr(),
18052                &mut error,
18053            ));
18054            let switch = switch.assume_init();
18055            assert!(switch.closed);
18056            assert_eq!(switch.thermal_rating_mva, 500.0);
18057            assert!(switch.has_from_active_power);
18058
18059            let mut hvdc = std::mem::MaybeUninit::<PioBalancedHvdcView>::uninit();
18060            assert!(pio_balanced_network_hvdc_at(
18061                network,
18062                0,
18063                hvdc.as_mut_ptr(),
18064                &mut error,
18065            ));
18066            let hvdc = hvdc.assume_init();
18067            assert!(hvdc.has_converter1);
18068            assert_eq!(view_text(hvdc.converter1.kind), "vsc");
18069            assert_eq!(hvdc.nominal_voltage_kv, 320.0);
18070
18071            let mut transformer =
18072                std::mem::MaybeUninit::<PioBalancedThreeWindingTransformerView>::uninit();
18073            assert!(pio_balanced_network_three_winding_transformer_at(
18074                network,
18075                0,
18076                transformer.as_mut_ptr(),
18077                &mut error,
18078            ));
18079            assert_eq!(transformer.assume_init().winding_count, 3);
18080            let mut winding =
18081                std::mem::MaybeUninit::<PioThreeWindingTransformerWindingView>::uninit();
18082            assert!(pio_balanced_network_three_winding_transformer_winding_at(
18083                network,
18084                0,
18085                2,
18086                winding.as_mut_ptr(),
18087                &mut error,
18088            ));
18089            let winding = winding.assume_init();
18090            assert_eq!(winding.bus_id, 3);
18091            assert!(winding.has_control);
18092            assert_eq!(view_text(winding.control.mode), "active_flow");
18093            assert!(winding.control.enabled);
18094            assert_eq!(winding.control.controlled_bus_id, 3);
18095            assert!(winding.control.has_regulating_terminal);
18096            assert_eq!(winding.control.regulating_terminal.terminal, 3);
18097            assert_eq!(winding.control.tap_position_count, 21);
18098
18099            let mut area = std::mem::MaybeUninit::<PioBalancedAreaView>::uninit();
18100            assert!(pio_balanced_network_area_at(
18101                network,
18102                0,
18103                area.as_mut_ptr(),
18104                &mut error,
18105            ));
18106            let area = area.assume_init();
18107            assert_eq!(view_text(area.component_id), "A7");
18108            assert_eq!(area.slack_bus_id, 1);
18109
18110            pio_balanced_network_release(network);
18111        }
18112    }
18113
18114    #[test]
18115    fn detailed_connectivity_tables_are_owner_rooted_views() {
18116        const HIERARCHY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
18117<iidm:network xmlns:iidm="http://www.powsybl.org/schema/iidm/1_17" id="details" caseDate="2026-01-01T00:00:00Z" forecastDistance="0" sourceFormat="test" minimumValidationLevel="STEADY_STATE_HYPOTHESIS">
18118  <iidm:substation id="S" country="US" tso="MISO" geographicalTags="east">
18119    <iidm:voltageLevel id="VL1" nominalV="132" topologyKind="BUS_BREAKER"><iidm:busBreakerTopology><iidm:bus id="B1"/></iidm:busBreakerTopology></iidm:voltageLevel>
18120    <iidm:voltageLevel id="VL2" nominalV="33" topologyKind="BUS_BREAKER"><iidm:busBreakerTopology><iidm:bus id="B2"/></iidm:busBreakerTopology></iidm:voltageLevel>
18121    <iidm:voltageLevel id="VL3" nominalV="11" topologyKind="BUS_BREAKER"><iidm:busBreakerTopology><iidm:bus id="B3"/></iidm:busBreakerTopology></iidm:voltageLevel>
18122    <iidm:threeWindingsTransformer id="T3" ratedU0="132" ratedU1="132" ratedU2="33" ratedU3="11" r1="17.424" x1="34.848" r2="1.7424" x2="3.4848" r3="0.8712" x3="1.7424" bus1="B1" connectableBus1="B1" voltageLevelId1="VL1" bus2="B2" connectableBus2="B2" voltageLevelId2="VL2" bus3="B3" connectableBus3="B3" voltageLevelId3="VL3" selectedOperationalLimitsGroupIds1="normal">
18123      <iidm:ratioTapChanger2 tapPosition="0" lowTapPosition="0" loadTapChangingCapabilities="false"><iidm:step rho="1.05"/></iidm:ratioTapChanger2>
18124      <iidm:operationalLimitsGroup1 id="normal"><iidm:apparentPowerLimits permanentLimit="90"><iidm:temporaryLimit name="emergency" acceptableDuration="600" value="100"/></iidm:apparentPowerLimits></iidm:operationalLimitsGroup1>
18125    </iidm:threeWindingsTransformer>
18126  </iidm:substation>
18127</iidm:network>"#;
18128
18129        unsafe {
18130            let module = parse_xiidm_text(HIERARCHY);
18131            let mut identified_network = match &PioModule::get(module).unwrap().module.value() {
18132                PioValue::BalancedNetwork(network) => network.clone(),
18133                value => panic!("expected balanced network, got {}", value.type_name()),
18134            };
18135            let identified_details = Arc::make_mut(
18136                identified_network
18137                    .detailed_connectivity_mut()
18138                    .as_mut()
18139                    .unwrap(),
18140            );
18141            identified_details.terminals[0].component =
18142                Some(ComponentId::new("terminal", "terminal-T3-1").unwrap());
18143            identified_details.tap_changers[0].component =
18144                Some(ComponentId::new("tap_changer", "tap-T3-2").unwrap());
18145            let value = pio_module_value(module);
18146            let mut error = std::ptr::null_mut();
18147            let network = pio_value_balanced_network(value, &mut error);
18148            assert!(pio_balanced_network_has_detailed_connectivity(network));
18149            let details = pio_balanced_network_detailed_connectivity(network);
18150            assert!(!details.is_null());
18151            pio_value_release(value);
18152            pio_module_release(module);
18153            pio_balanced_network_release(network);
18154
18155            let mut counts = std::mem::MaybeUninit::<PioDetailedConnectivityCountsView>::uninit();
18156            assert!(pio_detailed_connectivity_counts(
18157                details,
18158                counts.as_mut_ptr(),
18159                &mut error,
18160            ));
18161            let counts = counts.assume_init();
18162            assert_eq!(counts.substations, 1);
18163            assert_eq!(counts.voltage_levels, 3);
18164            assert_eq!(counts.terminals, 3);
18165            assert_eq!(counts.operational_limit_groups, 1);
18166            assert_eq!(counts.tap_changers, 1);
18167
18168            let mut substation = std::mem::MaybeUninit::<PioSubstationView>::uninit();
18169            assert!(pio_detailed_connectivity_substation_at(
18170                details,
18171                0,
18172                substation.as_mut_ptr(),
18173                &mut error,
18174            ));
18175            let substation = substation.assume_init();
18176            assert_eq!(view_text(substation.component.local_id), "S");
18177            assert_eq!(substation.geographical_tag_count, 1);
18178
18179            let mut level = std::mem::MaybeUninit::<PioVoltageLevelView>::uninit();
18180            assert!(pio_detailed_connectivity_voltage_level_at(
18181                details,
18182                0,
18183                level.as_mut_ptr(),
18184                &mut error,
18185            ));
18186            let level = level.assume_init();
18187            assert_eq!(view_text(level.topology_kind), "bus_breaker");
18188            assert_eq!(level.nominal_voltage_kv, 132.0);
18189
18190            let mut terminal = std::mem::MaybeUninit::<PioDetailedTerminalView>::uninit();
18191            assert!(pio_detailed_connectivity_terminal_at(
18192                details,
18193                0,
18194                terminal.as_mut_ptr(),
18195                &mut error,
18196            ));
18197            let terminal = terminal.assume_init();
18198            assert!(terminal.connected);
18199            assert!(!terminal.has_component);
18200
18201            let mut limit = std::mem::MaybeUninit::<PioOperationalLimitGroupView>::uninit();
18202            assert!(pio_detailed_connectivity_operational_limit_group_at(
18203                details,
18204                0,
18205                limit.as_mut_ptr(),
18206                &mut error,
18207            ));
18208            let limit = limit.assume_init();
18209            assert!(limit.has_apparent_power_limits);
18210            assert_eq!(limit.apparent_power_permanent_limit_mva, 90.0);
18211            assert_eq!(limit.apparent_power_temporary_limit_count, 1);
18212
18213            let mut changer = std::mem::MaybeUninit::<PioTapChangerView>::uninit();
18214            assert!(pio_detailed_connectivity_tap_changer_at(
18215                details,
18216                0,
18217                changer.as_mut_ptr(),
18218                &mut error,
18219            ));
18220            let changer = changer.assume_init();
18221            assert!(!changer.has_component);
18222            assert_eq!(view_text(changer.kind), "ratio");
18223            assert!(changer.has_tap_position);
18224            assert_eq!(changer.tap_position, 0);
18225            assert_eq!(changer.step_count, 1);
18226
18227            pio_detailed_connectivity_release(details);
18228
18229            let module = module_handle(powerio::PioModule::new(PioValue::BalancedNetwork(
18230                identified_network,
18231            )));
18232            let value = pio_module_value(module);
18233            let network = pio_value_balanced_network(value, &mut error);
18234            let details = pio_balanced_network_detailed_connectivity(network);
18235            pio_value_release(value);
18236            pio_module_release(module);
18237            pio_balanced_network_release(network);
18238
18239            let mut terminal = std::mem::MaybeUninit::<PioDetailedTerminalView>::uninit();
18240            assert!(pio_detailed_connectivity_terminal_at(
18241                details,
18242                0,
18243                terminal.as_mut_ptr(),
18244                &mut error,
18245            ));
18246            let terminal = terminal.assume_init();
18247            assert!(terminal.has_component);
18248            assert_eq!(view_text(terminal.component.component_type), "terminal");
18249            assert_eq!(view_text(terminal.component.local_id), "terminal-T3-1");
18250
18251            let mut changer = std::mem::MaybeUninit::<PioTapChangerView>::uninit();
18252            assert!(pio_detailed_connectivity_tap_changer_at(
18253                details,
18254                0,
18255                changer.as_mut_ptr(),
18256                &mut error,
18257            ));
18258            let changer = changer.assume_init();
18259            assert!(changer.has_component);
18260            assert_eq!(view_text(changer.component.component_type), "tap_changer");
18261            assert_eq!(view_text(changer.component.local_id), "tap-T3-2");
18262            pio_detailed_connectivity_release(details);
18263
18264            let without_assigned_tap = HIERARCHY.replace(r#" tapPosition="0""#, "");
18265            let module = parse_xiidm_text(&without_assigned_tap);
18266            let value = pio_module_value(module);
18267            let network = pio_value_balanced_network(value, &mut error);
18268            let details = pio_balanced_network_detailed_connectivity(network);
18269            let mut changer = std::mem::MaybeUninit::<PioTapChangerView>::uninit();
18270            assert!(pio_detailed_connectivity_tap_changer_at(
18271                details,
18272                0,
18273                changer.as_mut_ptr(),
18274                &mut error,
18275            ));
18276            let changer = changer.assume_init();
18277            assert!(!changer.has_tap_position);
18278            assert_eq!(changer.tap_position, 0);
18279            pio_detailed_connectivity_release(details);
18280            pio_balanced_network_release(network);
18281            pio_value_release(value);
18282            pio_module_release(module);
18283        }
18284    }
18285
18286    #[test]
18287    fn subnetworks_boundary_lines_and_tie_lines_are_owner_rooted_views() {
18288        const XIIDM: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
18289<iidm:network xmlns:iidm="http://www.powsybl.org/schema/iidm/1_17" id="Merged" caseDate="2026-01-01T00:00:00Z" forecastDistance="0" sourceFormat="root" minimumValidationLevel="STEADY_STATE_HYPOTHESIS">
18290  <iidm:network id="A" caseDate="2026-01-01T01:00:00Z" forecastDistance="1" sourceFormat="part-a" minimumValidationLevel="STEADY_STATE_HYPOTHESIS">
18291    <iidm:substation id="SA"><iidm:voltageLevel id="VA" nominalV="100" topologyKind="BUS_BREAKER"><iidm:busBreakerTopology><iidm:bus id="BA"/></iidm:busBreakerTopology><iidm:boundaryLine id="DLA" p0="5" q0="6" r="1" x="2" generationVoltageRegulationOn="true" generationMinP="0" generationMaxP="20" generationTargetP="10" generationTargetV="100" bus="BA" connectableBus="BA"><iidm:reactiveCapabilityCurve><iidm:property name="owner" value="RTE"/><iidm:point p="0" minQ="-10" maxQ="10"/><iidm:point p="10" minQ="0" maxQ="20"/></iidm:reactiveCapabilityCurve></iidm:boundaryLine></iidm:voltageLevel></iidm:substation>
18292  </iidm:network>
18293  <iidm:network id="B" caseDate="2026-01-01T02:00:00Z" forecastDistance="2" sourceFormat="part-b" minimumValidationLevel="STEADY_STATE_HYPOTHESIS">
18294    <iidm:substation id="SB"><iidm:voltageLevel id="VB" nominalV="100" topologyKind="BUS_BREAKER"><iidm:busBreakerTopology><iidm:bus id="BB"/></iidm:busBreakerTopology><iidm:boundaryLine id="DLB" p0="-5" q0="-6" r="3" x="4" bus="BB" connectableBus="BB"/></iidm:voltageLevel></iidm:substation>
18295  </iidm:network>
18296  <iidm:tieLine id="TL" boundaryLineId1="DLA" boundaryLineId2="DLB"/>
18297</iidm:network>"#;
18298
18299        unsafe {
18300            let module = parse_xiidm_text(XIIDM);
18301            let value = pio_module_value(module);
18302            let mut error = std::ptr::null_mut();
18303            let network = pio_value_balanced_network(value, &mut error);
18304            let details = pio_balanced_network_detailed_connectivity(network);
18305            pio_value_release(value);
18306            pio_module_release(module);
18307            pio_balanced_network_release(network);
18308
18309            let mut counts = std::mem::MaybeUninit::<PioDetailedConnectivityCountsView>::uninit();
18310            assert!(pio_detailed_connectivity_counts(
18311                details,
18312                counts.as_mut_ptr(),
18313                &mut error,
18314            ));
18315            let counts = counts.assume_init();
18316            assert_eq!(counts.subnetworks, 2);
18317            assert_eq!(counts.boundary_lines, 2);
18318            assert_eq!(counts.tie_lines, 1);
18319
18320            let mut subnetwork = std::mem::MaybeUninit::<PioSubnetworkView>::uninit();
18321            assert!(pio_detailed_connectivity_subnetwork_at(
18322                details,
18323                0,
18324                subnetwork.as_mut_ptr(),
18325                &mut error,
18326            ));
18327            let subnetwork = subnetwork.assume_init();
18328            assert_eq!(view_text(subnetwork.component.local_id), "A");
18329            assert_eq!(view_text(subnetwork.parent.local_id), "Merged");
18330            assert!(subnetwork.case_metadata.has_forecast_distance);
18331            assert_eq!(subnetwork.case_metadata.forecast_distance, 1);
18332            assert_eq!(
18333                view_text(subnetwork.case_metadata.source_model_format),
18334                "part-a"
18335            );
18336            assert!(subnetwork.component_count >= 4);
18337
18338            let mut member = std::mem::MaybeUninit::<PioComponentIdView>::uninit();
18339            assert!(pio_detailed_connectivity_subnetwork_component_at(
18340                details,
18341                0,
18342                0,
18343                member.as_mut_ptr(),
18344                &mut error,
18345            ));
18346            assert!(!view_text(member.assume_init().local_id).is_empty());
18347
18348            let mut boundary = std::mem::MaybeUninit::<PioBoundaryLineView>::uninit();
18349            assert!(pio_detailed_connectivity_boundary_line_at(
18350                details,
18351                0,
18352                boundary.as_mut_ptr(),
18353                &mut error,
18354            ));
18355            let boundary = boundary.assume_init();
18356            assert_eq!(view_text(boundary.component.local_id), "DLA");
18357            assert_eq!(boundary.active_power_setpoint_mw, 5.0);
18358            assert!(boundary.has_generation);
18359            assert!(boundary.generation.voltage_regulation_on);
18360            assert_eq!(boundary.generation.target_active_power_mw, 10.0);
18361            assert!(boundary.generation.has_reactive_limits);
18362            assert_eq!(
18363                view_text(boundary.generation.reactive_limits.kind),
18364                "capability_curve"
18365            );
18366            assert_eq!(boundary.generation.reactive_limits.point_count, 2);
18367
18368            let mut property = std::mem::MaybeUninit::<PioStringPropertyView>::uninit();
18369            assert!(
18370                pio_detailed_connectivity_boundary_line_reactive_limit_property_at(
18371                    details,
18372                    0,
18373                    0,
18374                    property.as_mut_ptr(),
18375                    &mut error,
18376                )
18377            );
18378            let property = property.assume_init();
18379            assert_eq!(view_text(property.name), "owner");
18380            assert_eq!(view_text(property.value), "RTE");
18381
18382            let mut point = std::mem::MaybeUninit::<PioReactiveCapabilityCurvePointView>::uninit();
18383            assert!(
18384                pio_detailed_connectivity_boundary_line_reactive_capability_point_at(
18385                    details,
18386                    0,
18387                    1,
18388                    point.as_mut_ptr(),
18389                    &mut error,
18390                )
18391            );
18392            let point = point.assume_init();
18393            assert_eq!(point.active_power_mw, 10.0);
18394            assert_eq!(point.maximum_reactive_power_mvar, 20.0);
18395
18396            let mut tie = std::mem::MaybeUninit::<PioTieLineView>::uninit();
18397            assert!(pio_detailed_connectivity_tie_line_at(
18398                details,
18399                0,
18400                tie.as_mut_ptr(),
18401                &mut error,
18402            ));
18403            let tie = tie.assume_init();
18404            assert_eq!(view_text(tie.component.local_id), "TL");
18405            assert_eq!(view_text(tie.boundary_line1.local_id), "DLA");
18406            assert_eq!(view_text(tie.boundary_line2.local_id), "DLB");
18407            assert!(tie.has_calculation_branch);
18408
18409            pio_detailed_connectivity_release(details);
18410        }
18411    }
18412
18413    #[test]
18414    fn connectivity_node_numbers_are_optional_borrowed_fields() {
18415        const XIIDM: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
18416<iidm:network xmlns:iidm="http://www.powsybl.org/schema/iidm/1_17" id="nodes" caseDate="2025-01-01T00:00:00Z" forecastDistance="0" sourceFormat="test" minimumValidationLevel="STEADY_STATE_HYPOTHESIS">
18417  <iidm:substation id="S">
18418    <iidm:voltageLevel id="VL" nominalV="110" topologyKind="NODE_BREAKER">
18419      <iidm:nodeBreakerTopology>
18420        <iidm:bus v="110" angle="0" nodes="0,1,2"/>
18421        <iidm:busbarSection id="BBS" node="2"/>
18422        <iidm:switch id="BR" kind="BREAKER" open="false" node1="1" node2="2"/>
18423        <iidm:internalConnection node1="0" node2="1"/>
18424      </iidm:nodeBreakerTopology>
18425      <iidm:generator id="G" energySource="OTHER" minP="0" maxP="10" voltageRegulatorOn="true" targetP="5" node="0"><iidm:minMaxReactiveLimits minQ="-2" maxQ="2"/></iidm:generator>
18426    </iidm:voltageLevel>
18427  </iidm:substation>
18428</iidm:network>"#;
18429
18430        unsafe {
18431            let module = parse_xiidm_text(XIIDM);
18432            let value = pio_module_value(module);
18433            let mut error = std::ptr::null_mut();
18434            let network = pio_value_balanced_network(value, &mut error);
18435            let details = pio_balanced_network_detailed_connectivity(network);
18436
18437            let mut numbers = Vec::new();
18438            for index in 0..3 {
18439                let mut node = std::mem::MaybeUninit::<PioConnectivityNodeView>::uninit();
18440                assert!(pio_detailed_connectivity_node_at(
18441                    details,
18442                    index,
18443                    node.as_mut_ptr(),
18444                    &mut error,
18445                ));
18446                let node = node.assume_init();
18447                assert!(node.has_node_number);
18448                assert!(node.has_calculated_bus);
18449                numbers.push(node.node_number);
18450            }
18451            numbers.sort_unstable();
18452            assert_eq!(numbers, [0, 1, 2]);
18453
18454            pio_detailed_connectivity_release(details);
18455            pio_balanced_network_release(network);
18456            pio_value_release(value);
18457            pio_module_release(module);
18458        }
18459    }
18460
18461    #[test]
18462    fn omitted_fields_and_equipment_reactive_limits_are_typed_views() {
18463        const XIIDM: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
18464<iidm:network xmlns:iidm="http://www.powsybl.org/schema/iidm/equipment/1_12" id="equipment" caseDate="2021-01-03T00:00:00Z" forecastDistance="0" sourceFormat="test" minimumValidationLevel="EQUIPMENT">
18465  <iidm:substation id="S"><iidm:voltageLevel id="VL" nominalV="225" topologyKind="NODE_BREAKER">
18466    <iidm:nodeBreakerTopology><iidm:busbarSection id="BBS" node="0"/></iidm:nodeBreakerTopology>
18467    <iidm:generator id="G" energySource="SOLAR" minP="0" maxP="100" voltageRegulatorOn="true" node="0">
18468      <iidm:reactiveCapabilityCurve><iidm:property name="curve" value="retained"/><iidm:point p="0" minQ="-20" maxQ="20"><iidm:property name="point" value="first"/></iidm:point><iidm:point p="100" minQ="-10" maxQ="10"/></iidm:reactiveCapabilityCurve>
18469    </iidm:generator>
18470  </iidm:voltageLevel></iidm:substation>
18471</iidm:network>"#;
18472
18473        unsafe {
18474            let module = parse_xiidm_text(XIIDM);
18475            let value = pio_module_value(module);
18476            let mut error = std::ptr::null_mut();
18477            let network = pio_value_balanced_network(value, &mut error);
18478            let details = pio_balanced_network_detailed_connectivity(network);
18479
18480            let mut counts = std::mem::MaybeUninit::<PioDetailedConnectivityCountsView>::uninit();
18481            assert!(pio_detailed_connectivity_counts(
18482                details,
18483                counts.as_mut_ptr(),
18484                &mut error,
18485            ));
18486            let counts = counts.assume_init();
18487            assert_eq!(counts.omitted_fields, 4);
18488            assert_eq!(counts.equipment_reactive_limits, 1);
18489
18490            let mut omitted = std::mem::MaybeUninit::<PioOmittedFieldView>::uninit();
18491            assert!(pio_detailed_connectivity_omitted_field_at(
18492                details,
18493                2,
18494                omitted.as_mut_ptr(),
18495                &mut error,
18496            ));
18497            let omitted = omitted.assume_init();
18498            assert_eq!(view_text(omitted.component.local_id), "G");
18499            assert_eq!(view_text(omitted.field), "voltage_setpoint");
18500
18501            let mut limits = std::mem::MaybeUninit::<PioEquipmentReactiveLimitsView>::uninit();
18502            assert!(pio_detailed_connectivity_equipment_reactive_limits_at(
18503                details,
18504                0,
18505                limits.as_mut_ptr(),
18506                &mut error,
18507            ));
18508            let limits = limits.assume_init();
18509            assert_eq!(view_text(limits.equipment.local_id), "G");
18510            assert_eq!(view_text(limits.limits.kind), "capability_curve");
18511            assert_eq!(limits.limits.property_count, 1);
18512            assert_eq!(limits.limits.point_count, 2);
18513
18514            let mut point = std::mem::MaybeUninit::<PioReactiveCapabilityCurvePointView>::uninit();
18515            assert!(
18516                pio_detailed_connectivity_equipment_reactive_capability_point_at(
18517                    details,
18518                    0,
18519                    0,
18520                    point.as_mut_ptr(),
18521                    &mut error,
18522                )
18523            );
18524            let point = point.assume_init();
18525            assert_eq!(point.minimum_reactive_power_mvar, -20.0);
18526            assert_eq!(point.property_count, 1);
18527
18528            let mut property = std::mem::MaybeUninit::<PioStringPropertyView>::uninit();
18529            assert!(
18530                pio_detailed_connectivity_equipment_reactive_capability_point_property_at(
18531                    details,
18532                    0,
18533                    0,
18534                    0,
18535                    property.as_mut_ptr(),
18536                    &mut error,
18537                )
18538            );
18539            let property = property.assume_init();
18540            assert_eq!(view_text(property.name), "point");
18541            assert_eq!(view_text(property.value), "first");
18542
18543            let mut generator = std::mem::MaybeUninit::<PioBalancedGeneratorView>::uninit();
18544            assert!(pio_balanced_network_generator_at(
18545                network,
18546                0,
18547                generator.as_mut_ptr(),
18548                &mut error,
18549            ));
18550            assert_eq!(view_text(generator.assume_init().energy_source), "solar");
18551
18552            pio_detailed_connectivity_release(details);
18553            pio_balanced_network_release(network);
18554            pio_value_release(value);
18555            pio_module_release(module);
18556        }
18557    }
18558
18559    #[test]
18560    fn detailed_dc_and_converter_records_are_typed_views() {
18561        const DC: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
18562<iidm:network xmlns:iidm="http://www.powsybl.org/schema/iidm/1_17" id="dc" caseDate="2026-01-01T00:00:00Z" forecastDistance="0" sourceFormat="test" minimumValidationLevel="STEADY_STATE_HYPOTHESIS">
18563  <iidm:dcNode id="N1" nominalV="500" v="498"/><iidm:dcNode id="N2" nominalV="500"/>
18564  <iidm:dcSwitch id="S" dcNode1="N1" dcNode2="N2" kind="DISCONNECTOR" open="true" r="0.9"/>
18565  <iidm:dcGround id="G" dcNode="N1" r="0.1" connected="false"/>
18566  <iidm:dcLine id="L" dcNode1="N1" dcNode2="N2" r="4" connected1="true" connected2="true" dcP1="100" dcI1="200" dcP2="-98" dcI2="-195"/>
18567  <iidm:substation id="SUB"><iidm:voltageLevel id="VL" nominalV="400" topologyKind="BUS_BREAKER">
18568    <iidm:busBreakerTopology><iidm:bus id="B1"/><iidm:bus id="B2"/></iidm:busBreakerTopology>
18569    <iidm:voltageSourceConverter id="VSC" dcNode1="N1" dcConnected1="true" dcNode2="N2" dcConnected2="false" idleLoss="2" switchingLoss="0.2" resistiveLoss="0.000002" controlMode="P_PCC_DROOP" targetP="301" targetVdc="502" bus1="B1" connectableBus1="B1" bus2="B2" connectableBus2="B2" voltageRegulatorOn="true" voltageSetpoint="397"><iidm:pccTerminal id="VSC" number="ONE"/><iidm:droopCurve><iidm:segment minV="-100" maxV="100" k="-5"/></iidm:droopCurve><iidm:reactiveCapabilityCurve><iidm:property name="curve" value="retained"/><iidm:point p="-200" minQ="-190" maxQ="192"><iidm:property name="point" value="one"/></iidm:point><iidm:point p="200" minQ="-189" maxQ="191"/></iidm:reactiveCapabilityCurve></iidm:voltageSourceConverter>
18570  </iidm:voltageLevel></iidm:substation>
18571</iidm:network>"#;
18572
18573        unsafe {
18574            let module = parse_xiidm_text(DC);
18575            let value = pio_module_value(module);
18576            let mut error = std::ptr::null_mut();
18577            let network = pio_value_balanced_network(value, &mut error);
18578            let details = pio_balanced_network_detailed_connectivity(network);
18579
18580            let mut counts = std::mem::MaybeUninit::<PioDetailedConnectivityCountsView>::uninit();
18581            assert!(pio_detailed_connectivity_counts(
18582                details,
18583                counts.as_mut_ptr(),
18584                &mut error,
18585            ));
18586            let counts = counts.assume_init();
18587            assert_eq!(counts.dc_nodes, 2);
18588            assert_eq!(counts.dc_grounds, 1);
18589            assert_eq!(counts.dc_lines, 1);
18590            assert_eq!(counts.dc_switches, 1);
18591            assert_eq!(counts.voltage_source_converters, 1);
18592
18593            let mut line = std::mem::MaybeUninit::<PioDcEquipmentView>::uninit();
18594            assert!(pio_detailed_connectivity_dc_line_at(
18595                details,
18596                0,
18597                line.as_mut_ptr(),
18598                &mut error,
18599            ));
18600            let line = line.assume_init();
18601            assert_eq!(view_text(line.kind), "line");
18602            assert_eq!(line.terminal_count, 2);
18603            assert_eq!(line.resistance_ohm, 4.0);
18604            assert_eq!(line.terminal1.current_a, 200.0);
18605
18606            let mut converter = std::mem::MaybeUninit::<PioAcDcConverterView>::uninit();
18607            assert!(pio_detailed_connectivity_voltage_source_converter_at(
18608                details,
18609                0,
18610                converter.as_mut_ptr(),
18611                &mut error,
18612            ));
18613            let converter = converter.assume_init();
18614            assert_eq!(view_text(converter.kind), "voltage_source");
18615            assert_eq!(
18616                view_text(converter.control_mode),
18617                "active_power_at_pcc_and_dc_voltage_droop_curve"
18618            );
18619            assert!(converter.voltage_regulator_on);
18620            assert_eq!(converter.idle_loss_mw, 2.0);
18621            assert!(converter.has_switching_loss);
18622            assert_eq!(converter.switching_loss_mw_per_ampere, 0.2);
18623            assert!(converter.has_resistive_loss);
18624            assert!(converter.has_pcc_terminal);
18625            assert_eq!(converter.pcc_terminal.terminal, 1);
18626            assert!(converter.has_reactive_limits);
18627            assert_eq!(
18628                view_text(converter.reactive_limits.kind),
18629                "capability_curve"
18630            );
18631            assert_eq!(converter.droop_curve_segment_count, 1);
18632            assert!(converter.has_droop_curve);
18633
18634            let mut segment = std::mem::MaybeUninit::<PioDroopCurveSegmentView>::uninit();
18635            assert!(
18636                pio_detailed_connectivity_voltage_source_converter_droop_curve_segment_at(
18637                    details,
18638                    0,
18639                    0,
18640                    segment.as_mut_ptr(),
18641                    &mut error,
18642                )
18643            );
18644            let segment = segment.assume_init();
18645            assert_eq!(segment.minimum_voltage_kv, -100.0);
18646            assert_eq!(segment.maximum_voltage_kv, 100.0);
18647            assert_eq!(segment.k, -5.0);
18648
18649            let mut point = std::mem::MaybeUninit::<PioReactiveCapabilityCurvePointView>::uninit();
18650            assert!(
18651                pio_detailed_connectivity_voltage_source_converter_reactive_capability_point_at(
18652                    details,
18653                    0,
18654                    0,
18655                    point.as_mut_ptr(),
18656                    &mut error,
18657                )
18658            );
18659            let point = point.assume_init();
18660            assert_eq!(point.active_power_mw, -200.0);
18661            assert_eq!(point.property_count, 1);
18662
18663            let base_network = match &PioModule::get(module).unwrap().module.value() {
18664                PioValue::BalancedNetwork(network) => network.clone(),
18665                value => panic!("expected balanced network, got {}", value.type_name()),
18666            };
18667
18668            pio_detailed_connectivity_release(details);
18669            pio_balanced_network_release(network);
18670            pio_value_release(value);
18671            pio_module_release(module);
18672
18673            let mut empty_curve_network = base_network.clone();
18674            std::sync::Arc::make_mut(
18675                empty_curve_network
18676                    .detailed_connectivity_mut()
18677                    .as_mut()
18678                    .unwrap(),
18679            )
18680            .voltage_source_converters[0]
18681                .droop_curve = Some(serde_json::from_str(r#"{"segments":[]}"#).unwrap());
18682            let mut absent_curve_network = base_network;
18683            std::sync::Arc::make_mut(
18684                absent_curve_network
18685                    .detailed_connectivity_mut()
18686                    .as_mut()
18687                    .unwrap(),
18688            )
18689            .voltage_source_converters[0]
18690                .droop_curve = None;
18691
18692            for (network, has_droop_curve) in
18693                [(empty_curve_network, true), (absent_curve_network, false)]
18694            {
18695                let module = module_handle(powerio::PioModule::new(PioValue::from(network)));
18696                let value = pio_module_value(module);
18697                let network = pio_value_balanced_network(value, &mut error);
18698                let details = pio_balanced_network_detailed_connectivity(network);
18699                let mut converter = std::mem::MaybeUninit::<PioAcDcConverterView>::uninit();
18700                assert!(pio_detailed_connectivity_voltage_source_converter_at(
18701                    details,
18702                    0,
18703                    converter.as_mut_ptr(),
18704                    &mut error,
18705                ));
18706                let converter = converter.assume_init();
18707                assert_eq!(converter.droop_curve_segment_count, 0);
18708                assert_eq!(converter.has_droop_curve, has_droop_curve);
18709                pio_detailed_connectivity_release(details);
18710                pio_balanced_network_release(network);
18711                pio_value_release(value);
18712                pio_module_release(module);
18713            }
18714        }
18715    }
18716
18717    #[test]
18718    fn balanced_calculation_instances_expose_typed_inputs() {
18719        unsafe {
18720            let network = case9_network();
18721            let dc_pf = powerio_prob::DcPfInstance::from_network(network.clone()).unwrap();
18722            let dc_pf_module =
18723                module_handle(powerio::PioModule::new(PioValue::DcPfInstance(dc_pf)));
18724            let dc_pf_value = pio_module_value(dc_pf_module);
18725            let mut error = std::ptr::null_mut();
18726            let dc_pf = pio_value_dc_pf_instance(dc_pf_value, &mut error);
18727            assert!(!dc_pf.is_null(), "{}", error_text(error));
18728            assert_eq!(pio_dc_pf_instance_bus_specification_count(dc_pf), 9);
18729            assert_eq!(
18730                view_text(pio_dc_pf_instance_branch_susceptance_formula(dc_pf)),
18731                "series_susceptance"
18732            );
18733            assert!(!pio_calculation_instance_has_initial_point(dc_pf));
18734            assert!(pio_calculation_instance_initial_point(dc_pf, &mut error).is_null());
18735            assert!(error.is_null());
18736
18737            let mut reference = std::mem::MaybeUninit::<PioDcBusSpecificationView>::uninit();
18738            assert!(pio_dc_pf_instance_bus_specification_at(
18739                dc_pf,
18740                0,
18741                reference.as_mut_ptr(),
18742                &mut error,
18743            ));
18744            let reference = reference.assume_init();
18745            assert_eq!(reference.bus_id, 1);
18746            assert_eq!(view_text(reference.kind), "reference");
18747            assert_eq!(reference.voltage_angle_degrees, 0.0);
18748
18749            let mut load_bus = std::mem::MaybeUninit::<PioDcBusSpecificationView>::uninit();
18750            assert!(pio_dc_pf_instance_bus_specification_at(
18751                dc_pf,
18752                4,
18753                load_bus.as_mut_ptr(),
18754                &mut error,
18755            ));
18756            let load_bus = load_bus.assume_init();
18757            assert_eq!(load_bus.bus_id, 5);
18758            assert_eq!(view_text(load_bus.kind), "net_active_power");
18759            assert_eq!(load_bus.net_active_power_mw, -90.0);
18760
18761            pio_calculation_instance_release(dc_pf);
18762            pio_value_release(dc_pf_value);
18763            pio_module_release(dc_pf_module);
18764
18765            let initial = powerio_prob::BalancedOperatingPointBuilder::for_point(network.clone())
18766                .bus_voltage_magnitudes(vec![1.01; network.buses().len()])
18767                .build_point()
18768                .unwrap();
18769            let dc_pf = powerio_prob::DcPfInstance::from_network(network.clone())
18770                .unwrap()
18771                .with_initial_point(initial);
18772            let dc_pf_module =
18773                module_handle(powerio::PioModule::new(PioValue::DcPfInstance(dc_pf)));
18774            let dc_pf_value = pio_module_value(dc_pf_module);
18775            let dc_pf = pio_value_dc_pf_instance(dc_pf_value, &mut error);
18776            let initial = pio_calculation_instance_initial_point(dc_pf, &mut error);
18777            assert!(!initial.is_null(), "{}", error_text(error));
18778            assert_eq!(
18779                view_text(pio_operating_point_type_name(initial)),
18780                "powerio.OperatingPoint<powerio.BalancedNetwork>"
18781            );
18782            pio_calculation_instance_release(dc_pf);
18783            pio_value_release(dc_pf_value);
18784            pio_module_release(dc_pf_module);
18785            let mut initial_vm = 0.0;
18786            assert!(pio_operating_point_get_value(
18787                initial,
18788                c"bus_voltage_magnitude".as_ptr(),
18789                "bus_voltage_magnitude".len(),
18790                c"1".as_ptr(),
18791                1,
18792                &mut initial_vm,
18793                &mut error,
18794            ));
18795            assert_eq!(initial_vm, 1.01);
18796            let initial_network = pio_operating_point_balanced_network(initial, &mut error);
18797            assert_eq!(pio_balanced_network_bus_count(initial_network), 9);
18798            pio_balanced_network_release(initial_network);
18799            pio_operating_point_release(initial);
18800
18801            let ac_pf = powerio_prob::AcPfInstance::from_network(network.clone()).unwrap();
18802            let ac_pf_module =
18803                module_handle(powerio::PioModule::new(PioValue::AcPfInstance(ac_pf)));
18804            let ac_pf_value = pio_module_value(ac_pf_module);
18805            let ac_pf = pio_value_ac_pf_instance(ac_pf_value, &mut error);
18806            let mut pv = std::mem::MaybeUninit::<PioAcBusSpecificationView>::uninit();
18807            assert!(pio_ac_pf_instance_bus_specification_at(
18808                ac_pf,
18809                1,
18810                pv.as_mut_ptr(),
18811                &mut error,
18812            ));
18813            let pv = pv.assume_init();
18814            assert_eq!(pv.bus_id, 2);
18815            assert_eq!(view_text(pv.kind), "pv");
18816            assert_eq!(pv.net_active_power_mw, 163.0);
18817            assert_eq!(pv.voltage_magnitude_pu, 1.025);
18818            pio_calculation_instance_release(ac_pf);
18819            pio_value_release(ac_pf_value);
18820            pio_module_release(ac_pf_module);
18821
18822            let dc_opf = powerio_prob::DcOpfInstance::from_network(network).unwrap();
18823            let dc_opf_module =
18824                module_handle(powerio::PioModule::new(PioValue::DcOpfInstance(dc_opf)));
18825            let dc_opf_value = pio_module_value(dc_opf_module);
18826            let dc_opf = pio_value_dc_opf_instance(dc_opf_value, &mut error);
18827            assert_eq!(pio_calculation_instance_objective_term_count(dc_opf), 1);
18828            let mut term = std::mem::MaybeUninit::<PioObjectiveTermView>::uninit();
18829            assert!(pio_calculation_instance_objective_term_at(
18830                dc_opf,
18831                0,
18832                term.as_mut_ptr(),
18833                &mut error,
18834            ));
18835            assert_eq!(view_text(term.assume_init().kind), "network_generator_cost");
18836            assert_eq!(pio_calculation_instance_active_constraint_count(dc_opf), 4);
18837            let mut constraint = std::mem::MaybeUninit::<PioActiveConstraintView>::uninit();
18838            assert!(pio_calculation_instance_active_constraint_at(
18839                dc_opf,
18840                2,
18841                constraint.as_mut_ptr(),
18842                &mut error,
18843            ));
18844            let constraint = constraint.assume_init();
18845            assert_eq!(view_text(constraint.family), "thermal_limits");
18846            assert_eq!(view_text(constraint.selection), "all");
18847            assert_eq!(constraint.identity_count, 0);
18848
18849            pio_calculation_instance_release(dc_opf);
18850            pio_value_release(dc_opf_value);
18851            pio_module_release(dc_opf_module);
18852        }
18853    }
18854
18855    #[test]
18856    fn dc_opf_preparation_rows_keep_their_owner_alive() {
18857        unsafe {
18858            let source_module = parse_case9();
18859            let mut error = std::ptr::null_mut();
18860            let instance_module = pio_module_to_dc_opf_instance(source_module, &mut error);
18861            assert!(!instance_module.is_null(), "{}", error_text(error));
18862            let value = pio_module_value(instance_module);
18863            let instance = pio_value_dc_opf_instance(value, &mut error);
18864            assert!(!instance.is_null(), "{}", error_text(error));
18865            let units = "per_unit";
18866            let preparation = pio_build_dc_opf_preparation(
18867                instance,
18868                units.as_ptr().cast(),
18869                units.len(),
18870                false,
18871                false,
18872                true,
18873                &mut error,
18874            );
18875            assert!(!preparation.is_null(), "{}", error_text(error));
18876
18877            pio_calculation_instance_release(instance);
18878            pio_value_release(value);
18879            pio_module_release(instance_module);
18880            pio_module_release(source_module);
18881
18882            let mut summary = std::mem::MaybeUninit::<PioDcOpfPreparationView>::uninit();
18883            assert!(pio_dc_opf_preparation_summary(
18884                preparation,
18885                summary.as_mut_ptr(),
18886                &mut error,
18887            ));
18888            let summary = summary.assume_init();
18889            assert_eq!(summary.bus_count, 9);
18890            assert_eq!(summary.generator_count, 3);
18891            assert_eq!(summary.branch_count, 9);
18892            assert_eq!(summary.base_mva, 100.0);
18893            assert_eq!(view_text(summary.units), "per_unit");
18894            assert!(summary.correct_angle_difference_bounds);
18895            assert_eq!(
18896                view_text(summary.branch_susceptance_formula),
18897                "series_susceptance"
18898            );
18899            assert_eq!(view_text(summary.objective), "network_generator_cost");
18900
18901            let reference = pio_dc_opf_preparation_reference_buses(preparation);
18902            assert_eq!(
18903                std::slice::from_raw_parts(reference.data, reference.len),
18904                &[0]
18905            );
18906
18907            let mut bus = std::mem::MaybeUninit::<PioDcOpfBusView>::uninit();
18908            assert!(pio_dc_opf_preparation_bus_at(
18909                preparation,
18910                4,
18911                bus.as_mut_ptr(),
18912                &mut error,
18913            ));
18914            let bus = bus.assume_init();
18915            assert_eq!(bus.bus_id, 5);
18916            assert_eq!(bus.active_power_demand, 0.9);
18917            assert_eq!(bus.shunt_conductance, 0.0);
18918            assert_eq!(bus.phase_shift_injection, 0.0);
18919
18920            let mut generator = std::mem::MaybeUninit::<PioDcOpfGeneratorView>::uninit();
18921            assert!(pio_dc_opf_preparation_generator_at(
18922                preparation,
18923                0,
18924                generator.as_mut_ptr(),
18925                &mut error,
18926            ));
18927            let generator = generator.assume_init();
18928            assert_eq!(generator.bus_index, 0);
18929            assert_eq!(generator.quadratic_cost, 2200.0);
18930            assert!(!generator.has_piecewise_linear_cost);
18931
18932            let mut branch = std::mem::MaybeUninit::<PioDcOpfBranchView>::uninit();
18933            assert!(pio_dc_opf_preparation_branch_at(
18934                preparation,
18935                0,
18936                branch.as_mut_ptr(),
18937                &mut error,
18938            ));
18939            let branch = branch.assume_init();
18940            assert_eq!(branch.from_bus_index, 0);
18941            assert_eq!(branch.to_bus_index, 3);
18942            assert!(branch.susceptance_magnitude > 0.0);
18943            assert_eq!(view_text(branch.source_kind), "branch");
18944            assert_eq!(branch.source_row, 0);
18945            assert!(!branch.has_winding);
18946
18947            let mut missing = std::mem::MaybeUninit::<PioDcOpfBusView>::uninit();
18948            assert!(!pio_dc_opf_preparation_bus_at(
18949                preparation,
18950                summary.bus_count,
18951                missing.as_mut_ptr(),
18952                &mut error,
18953            ));
18954            assert_eq!(
18955                view_text(pio_error_code(error)),
18956                "BIND.CAPI.INDEX_OUT_OF_RANGE"
18957            );
18958            pio_error_release(error);
18959            pio_dc_opf_preparation_release(preparation);
18960        }
18961    }
18962
18963    #[test]
18964    fn ac_opf_preparation_rows_keep_their_owner_alive() {
18965        unsafe {
18966            let source_module = parse_case9();
18967            let mut error = std::ptr::null_mut();
18968            let instance_module = pio_module_to_ac_opf_instance(source_module, &mut error);
18969            assert!(!instance_module.is_null(), "{}", error_text(error));
18970            let value = pio_module_value(instance_module);
18971            let instance = pio_value_ac_opf_instance(value, &mut error);
18972            assert!(!instance.is_null(), "{}", error_text(error));
18973            let units = "per_unit";
18974            let preparation = pio_build_ac_opf_preparation(
18975                instance,
18976                units.as_ptr().cast(),
18977                units.len(),
18978                false,
18979                false,
18980                true,
18981                &mut error,
18982            );
18983            assert!(!preparation.is_null(), "{}", error_text(error));
18984
18985            pio_calculation_instance_release(instance);
18986            pio_value_release(value);
18987            pio_module_release(instance_module);
18988            pio_module_release(source_module);
18989
18990            let mut summary = std::mem::MaybeUninit::<PioAcOpfPreparationView>::uninit();
18991            assert!(pio_ac_opf_preparation_summary(
18992                preparation,
18993                summary.as_mut_ptr(),
18994                &mut error,
18995            ));
18996            let summary = summary.assume_init();
18997            assert_eq!(summary.bus_count, 9);
18998            assert_eq!(summary.generator_count, 3);
18999            assert_eq!(summary.branch_count, 9);
19000            assert_eq!(summary.base_mva, 100.0);
19001            assert_eq!(view_text(summary.units), "per_unit");
19002            assert!(summary.correct_angle_difference_bounds);
19003            assert_eq!(view_text(summary.objective), "network_generator_cost");
19004
19005            let reference = pio_ac_opf_preparation_reference_buses(preparation);
19006            assert_eq!(
19007                std::slice::from_raw_parts(reference.data, reference.len),
19008                &[0]
19009            );
19010
19011            let mut bus = std::mem::MaybeUninit::<PioAcOpfBusView>::uninit();
19012            assert!(pio_ac_opf_preparation_bus_at(
19013                preparation,
19014                4,
19015                bus.as_mut_ptr(),
19016                &mut error,
19017            ));
19018            let bus = bus.assume_init();
19019            assert_eq!(bus.bus_id, 5);
19020            assert_eq!(bus.active_power_demand, 0.9);
19021            assert_eq!(bus.reactive_power_demand, 0.3);
19022            assert_eq!(bus.initial_voltage_angle_radians, 0.0);
19023
19024            let mut generator = std::mem::MaybeUninit::<PioAcOpfGeneratorView>::uninit();
19025            assert!(pio_ac_opf_preparation_generator_at(
19026                preparation,
19027                0,
19028                generator.as_mut_ptr(),
19029                &mut error,
19030            ));
19031            let generator = generator.assume_init();
19032            assert_eq!(generator.bus_index, 0);
19033            assert_eq!(generator.initial_active_power, 0.723);
19034            assert_eq!(generator.quadratic_cost, 2200.0);
19035            assert!(!generator.has_piecewise_linear_cost);
19036
19037            let mut branch = std::mem::MaybeUninit::<PioAcOpfBranchView>::uninit();
19038            assert!(pio_ac_opf_preparation_branch_at(
19039                preparation,
19040                0,
19041                branch.as_mut_ptr(),
19042                &mut error,
19043            ));
19044            let branch = branch.assume_init();
19045            assert_eq!(branch.from_bus_index, 0);
19046            assert_eq!(branch.to_bus_index, 3);
19047            assert_eq!(view_text(branch.source_kind), "branch");
19048            assert_eq!(branch.source_row, 0);
19049            assert!(!branch.has_winding);
19050
19051            let mut missing = std::mem::MaybeUninit::<PioAcOpfBusView>::uninit();
19052            assert!(!pio_ac_opf_preparation_bus_at(
19053                preparation,
19054                summary.bus_count,
19055                missing.as_mut_ptr(),
19056                &mut error,
19057            ));
19058            assert_eq!(
19059                view_text(pio_error_code(error)),
19060                "BIND.CAPI.INDEX_OUT_OF_RANGE"
19061            );
19062            pio_error_release(error);
19063            pio_ac_opf_preparation_release(preparation);
19064        }
19065    }
19066
19067    #[test]
19068    fn ac_opf_storage_view_preserves_fields_and_units() {
19069        unsafe {
19070            let mut network = case9_network();
19071            let mut storage = powerio_tx::Storage::new(powerio_tx::BusId(5));
19072            storage.uid = Some("battery".into());
19073            storage.ps = 12.0;
19074            storage.qs = -3.0;
19075            storage.energy = 80.0;
19076            storage.energy_rating = 120.0;
19077            storage.charge_rating = 25.0;
19078            storage.discharge_rating = 30.0;
19079            storage.charge_efficiency = 0.91;
19080            storage.discharge_efficiency = 0.88;
19081            storage.thermal_rating = 35.0;
19082            storage.qmin = -14.0;
19083            storage.qmax = 16.0;
19084            storage.r = 0.001;
19085            storage.x = 0.002;
19086            storage.p_loss = 0.4;
19087            storage.q_loss = 0.5;
19088            network.storage_mut().push(storage);
19089            let instance = powerio_prob::AcOpfInstance::from_network(network).unwrap();
19090            let close = |actual: f64, expected: f64| assert!((actual - expected).abs() < 1e-12);
19091
19092            for (units, power_scale) in [(Units::PerUnit, 0.01), (Units::Native, 1.0)] {
19093                let prepared = build_ac_opf_preparation(
19094                    &instance,
19095                    &AcOpfAssemblyOptions::default().with_units(units),
19096                )
19097                .unwrap();
19098                let preparation = PioAcOpfPreparation::new_raw(prepared);
19099                let mut error = std::ptr::null_mut();
19100                let mut summary = std::mem::MaybeUninit::<PioAcOpfPreparationView>::uninit();
19101                assert!(pio_ac_opf_preparation_summary(
19102                    preparation,
19103                    summary.as_mut_ptr(),
19104                    &mut error,
19105                ));
19106                assert_eq!(summary.assume_init().storage_count, 1);
19107
19108                let mut row = std::mem::MaybeUninit::<PioAcOpfStorageView>::uninit();
19109                assert!(pio_ac_opf_preparation_storage_at(
19110                    preparation,
19111                    0,
19112                    row.as_mut_ptr(),
19113                    &mut error,
19114                ));
19115                let row = row.assume_init();
19116                assert_eq!(view_text(row.component_id), "battery");
19117                assert_eq!(row.bus_index, 4);
19118                assert_eq!(row.source_row, 0);
19119                close(row.initial_active_power, 12.0 * power_scale);
19120                close(row.initial_reactive_power, -3.0 * power_scale);
19121                close(row.energy, 80.0 * power_scale);
19122                close(row.energy_rating, 120.0 * power_scale);
19123                close(row.charge_rating, 25.0 * power_scale);
19124                close(row.discharge_rating, 30.0 * power_scale);
19125                assert_eq!(row.charge_efficiency, 0.91);
19126                assert_eq!(row.discharge_efficiency, 0.88);
19127                close(row.apparent_power_max, 35.0 * power_scale);
19128                close(row.reactive_power_min, -14.0 * power_scale);
19129                close(row.reactive_power_max, 16.0 * power_scale);
19130                assert_eq!(row.resistance_pu, 0.001);
19131                assert_eq!(row.reactance_pu, 0.002);
19132                close(row.active_power_loss, 0.4 * power_scale);
19133                close(row.reactive_power_loss, 0.5 * power_scale);
19134                assert!(row.in_service);
19135
19136                pio_ac_opf_preparation_release(preparation);
19137            }
19138        }
19139    }
19140
19141    #[test]
19142    fn module_transforms_build_typed_calculation_modules() {
19143        unsafe {
19144            type Transform =
19145                unsafe extern "C" fn(*const PioModule, *mut *mut PioError) -> *mut PioModule;
19146            let source = parse_case9();
19147            let operations: [(Transform, &str, &str); 4] = [
19148                (
19149                    pio_module_to_dc_pf_instance,
19150                    "powerio.DcPfInstance",
19151                    "to_dc_pf_instance",
19152                ),
19153                (
19154                    pio_module_to_ac_pf_instance,
19155                    "powerio.AcPfInstance",
19156                    "to_ac_pf_instance",
19157                ),
19158                (
19159                    pio_module_to_dc_opf_instance,
19160                    "powerio.DcOpfInstance",
19161                    "to_dc_opf_instance",
19162                ),
19163                (
19164                    pio_module_to_ac_opf_instance,
19165                    "powerio.AcOpfInstance",
19166                    "to_ac_opf_instance",
19167                ),
19168            ];
19169            for (operation, expected_type, expected_history) in operations {
19170                let mut error = std::ptr::null_mut();
19171                let derived = operation(source, &mut error);
19172                assert!(!derived.is_null(), "{}", error_text(error));
19173                let value = pio_module_value(derived);
19174                assert!(pio_value_is_type(
19175                    value,
19176                    expected_type.as_ptr().cast(),
19177                    expected_type.len(),
19178                ));
19179                let derived_module = &PioModule::get(derived).unwrap().module;
19180                assert_eq!(
19181                    derived_module.history().last().unwrap().name(),
19182                    expected_history
19183                );
19184                assert!(derived_module.source().is_none());
19185                pio_value_release(value);
19186                pio_module_release(derived);
19187            }
19188
19189            let mut error = std::ptr::null_mut();
19190            let instance = pio_module_to_dc_pf_instance(source, &mut error);
19191            let invalid = pio_module_to_ac_pf_instance(instance, &mut error);
19192            assert!(invalid.is_null());
19193            assert_eq!(
19194                view_text(pio_error_code(error)),
19195                "REQUEST.MODULE.WRONG_MODEL_KIND"
19196            );
19197            pio_error_release(error);
19198            pio_module_release(instance);
19199            pio_module_release(source);
19200        }
19201    }
19202
19203    #[test]
19204    fn multiconductor_module_transforms_build_typed_calculation_modules() {
19205        unsafe {
19206            let source = parse_bmopf();
19207            let value = pio_module_value(source);
19208            let network_type = "powerio.MulticonductorNetwork";
19209            assert!(pio_value_is_type(
19210                value,
19211                network_type.as_ptr().cast(),
19212                network_type.len(),
19213            ));
19214            pio_value_release(value);
19215
19216            type Transform =
19217                unsafe extern "C" fn(*const PioModule, *mut *mut PioError) -> *mut PioModule;
19218            let operations: [(Transform, &str, &str); 2] = [
19219                (
19220                    pio_module_to_mc_ac_pf_instance,
19221                    "powerio.McAcPfInstance",
19222                    "to_mc_ac_pf_instance",
19223                ),
19224                (
19225                    pio_module_to_mc_ac_opf_instance,
19226                    "powerio.McAcOpfInstance",
19227                    "to_mc_ac_opf_instance",
19228                ),
19229            ];
19230            for (operation, expected_type, expected_history) in operations {
19231                let mut error = std::ptr::null_mut();
19232                let derived = operation(source, &mut error);
19233                assert!(!derived.is_null(), "{}", error_text(error));
19234                let value = pio_module_value(derived);
19235                assert!(pio_value_is_type(
19236                    value,
19237                    expected_type.as_ptr().cast(),
19238                    expected_type.len(),
19239                ));
19240                let derived_module = &PioModule::get(derived).unwrap().module;
19241                assert_eq!(
19242                    derived_module.history().last().unwrap().name(),
19243                    expected_history
19244                );
19245                assert!(derived_module.source().is_none());
19246                pio_value_release(value);
19247                pio_module_release(derived);
19248            }
19249            pio_module_release(source);
19250        }
19251    }
19252
19253    #[test]
19254    fn geo_layer_derives_a_new_network_module() {
19255        unsafe {
19256            let source_module = parse_case9();
19257            let geojson = br#"{
19258                "type": "FeatureCollection",
19259                "features": [{
19260                    "type": "Feature",
19261                    "geometry": {
19262                        "type": "Point",
19263                        "coordinates": [-83.743, 42.281]
19264                    },
19265                    "properties": {"bus": "1"}
19266                }]
19267            }"#;
19268            let mut error = std::ptr::null_mut();
19269            let source = pio_source_from_memory(
19270                c"case9.geojson".as_ptr(),
19271                "case9.geojson".len(),
19272                geojson.as_ptr(),
19273                geojson.len(),
19274                &mut error,
19275            );
19276            assert!(!source.is_null(), "{}", error_text(error));
19277            let layer = pio_geo_layer_parse(source, &mut error);
19278            pio_source_release(source);
19279            assert!(!layer.is_null(), "{}", error_text(error));
19280
19281            let diagnostics = pio_geo_layer_diagnostics(layer);
19282            assert_eq!(pio_diagnostics_len(diagnostics), 0);
19283            pio_diagnostics_release(diagnostics);
19284
19285            let mut report = std::ptr::null_mut();
19286            let derived = pio_module_apply_geo_layer(source_module, layer, &mut report, &mut error);
19287            assert!(!derived.is_null(), "{}", error_text(error));
19288            assert!(!report.is_null());
19289            assert_eq!(pio_geo_apply_report_matched_buses(report), 1);
19290            assert_eq!(pio_geo_apply_report_matched_branches(report), 0);
19291            assert_eq!(pio_geo_apply_report_unmatched_features(report), 0);
19292            assert_eq!(pio_geo_apply_report_unlocated_buses(report), 8);
19293            assert_eq!(pio_geo_apply_report_unlocated_branches(report), 9);
19294
19295            let note_count = pio_geo_apply_report_note_count(report);
19296            let note = pio_geo_apply_report_note_at(report, note_count, &mut error);
19297            assert_eq!(note.len, 0);
19298            assert_eq!(
19299                view_text(pio_error_code(error)),
19300                "BIND.CAPI.INDEX_OUT_OF_RANGE"
19301            );
19302            pio_error_release(error);
19303            error = std::ptr::null_mut();
19304
19305            let derived_module = &PioModule::get(derived).unwrap().module;
19306            assert!(derived_module.source().is_none());
19307            assert_eq!(
19308                derived_module.history().last().unwrap().name(),
19309                "apply_geo_layer"
19310            );
19311            let PioValue::BalancedNetwork(derived_network) = &derived_module.value() else {
19312                panic!("geo application changed the value type")
19313            };
19314            let derived_location = derived_network
19315                .buses()
19316                .iter()
19317                .find(|bus| bus.id == powerio_tx::BusId(1))
19318                .and_then(|bus| bus.location)
19319                .unwrap();
19320            assert_eq!((derived_location.x, derived_location.y), (-83.743, 42.281));
19321
19322            let original_module = &PioModule::get(source_module).unwrap().module;
19323            let PioValue::BalancedNetwork(original_network) = &original_module.value() else {
19324                unreachable!()
19325            };
19326            assert!(
19327                original_network
19328                    .buses()
19329                    .iter()
19330                    .find(|bus| bus.id == powerio_tx::BusId(1))
19331                    .unwrap()
19332                    .location
19333                    .is_none()
19334            );
19335
19336            let calculation = pio_module_to_dc_pf_instance(source_module, &mut error);
19337            assert!(!calculation.is_null(), "{}", error_text(error));
19338            let rejected =
19339                pio_module_apply_geo_layer(calculation, layer, std::ptr::null_mut(), &mut error);
19340            assert!(rejected.is_null());
19341            assert_eq!(
19342                view_text(pio_error_code(error)),
19343                "REQUEST.MODULE.WRONG_MODEL_KIND"
19344            );
19345            pio_error_release(error);
19346
19347            pio_module_release(calculation);
19348            pio_geo_apply_report_release(report);
19349            pio_geo_layer_release(layer);
19350            pio_module_release(derived);
19351            pio_module_release(source_module);
19352        }
19353    }
19354
19355    #[test]
19356    fn aggregate_bus_active_demand_uses_explicit_proportional_allocation() {
19357        unsafe {
19358            let mut network = case9_network();
19359            let mut second = network
19360                .loads()
19361                .iter()
19362                .find(|load| load.bus == powerio_tx::BusId::new(5))
19363                .unwrap()
19364                .clone();
19365            second.p = 30.0;
19366            second.q = 10.0;
19367            second.uid = Some("extra-load-at-bus-5".to_owned());
19368            network.loads_mut().push(second);
19369            let source = module_handle(powerio::PioModule::new(PioValue::BalancedNetwork(network)));
19370            let mut error = std::ptr::null_mut();
19371            let module = pio_module_to_dc_pf_instance(source, &mut error);
19372            assert!(!module.is_null(), "{}", error_text(error));
19373            pio_module_release(source);
19374
19375            let value_before = pio_module_value(module);
19376            let instance_before = pio_value_dc_pf_instance(value_before, &mut error);
19377            let network_before =
19378                pio_calculation_instance_balanced_network(instance_before, &mut error);
19379
19380            let power = pio_active_power_from_watts(240_000_000.0);
19381            let bad_rule = "first_load";
19382            let bad_report = pio_apply_bus_load_active_power(
19383                module,
19384                5,
19385                power,
19386                bad_rule.as_ptr().cast(),
19387                bad_rule.len(),
19388                &mut error,
19389            );
19390            assert!(bad_report.is_null());
19391            assert_eq!(
19392                view_text(pio_error_code(error)),
19393                "REQUEST.CAPI.ALLOCATION_UNKNOWN"
19394            );
19395            pio_error_release(error);
19396            error = std::ptr::null_mut();
19397
19398            let rule = "proportional_to_current_active_power";
19399            let report = pio_apply_bus_load_active_power(
19400                module,
19401                5,
19402                power,
19403                rule.as_ptr().cast(),
19404                rule.len(),
19405                &mut error,
19406            );
19407            assert!(!report.is_null(), "{}", error_text(error));
19408            assert_eq!(pio_update_report_len(report), 2);
19409            assert!(!pio_update_report_connectivity_changed(report));
19410
19411            let value_after = pio_module_value(module);
19412            let instance_after = pio_value_dc_pf_instance(value_after, &mut error);
19413            let network_after =
19414                pio_calculation_instance_balanced_network(instance_after, &mut error);
19415            let mut first_before = std::mem::MaybeUninit::<PioBalancedLoadView>::uninit();
19416            let mut second_before = std::mem::MaybeUninit::<PioBalancedLoadView>::uninit();
19417            let mut first_after = std::mem::MaybeUninit::<PioBalancedLoadView>::uninit();
19418            let mut second_after = std::mem::MaybeUninit::<PioBalancedLoadView>::uninit();
19419            assert!(pio_balanced_network_load_at(
19420                network_before,
19421                0,
19422                first_before.as_mut_ptr(),
19423                &mut error,
19424            ));
19425            assert!(pio_balanced_network_load_at(
19426                network_before,
19427                3,
19428                second_before.as_mut_ptr(),
19429                &mut error,
19430            ));
19431            assert!(pio_balanced_network_load_at(
19432                network_after,
19433                0,
19434                first_after.as_mut_ptr(),
19435                &mut error,
19436            ));
19437            assert!(pio_balanced_network_load_at(
19438                network_after,
19439                3,
19440                second_after.as_mut_ptr(),
19441                &mut error,
19442            ));
19443            assert_eq!(first_before.assume_init().p_mw, 90.0);
19444            assert_eq!(second_before.assume_init().p_mw, 30.0);
19445            assert_eq!(first_after.assume_init().p_mw, 180.0);
19446            assert_eq!(second_after.assume_init().p_mw, 60.0);
19447            assert_eq!(
19448                PioModule::get(module)
19449                    .unwrap()
19450                    .module
19451                    .history()
19452                    .last()
19453                    .unwrap()
19454                    .name(),
19455                "apply_updates"
19456            );
19457
19458            pio_balanced_network_release(network_before);
19459            pio_calculation_instance_release(instance_before);
19460            pio_value_release(value_before);
19461            pio_balanced_network_release(network_after);
19462            pio_calculation_instance_release(instance_after);
19463            pio_value_release(value_after);
19464            pio_update_report_release(report);
19465            pio_active_power_release(power);
19466            pio_module_release(module);
19467        }
19468    }
19469
19470    #[test]
19471    fn scuc_instance_exposes_typed_scheduling_inputs() {
19472        unsafe {
19473            let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
19474                .join("../tests/data/goc3/goc3_small.json");
19475            let source = std::fs::read_to_string(path).unwrap().replacen(
19476                "\"startup_states\": []",
19477                "\"startup_states\": [[2.0, 3.0]]",
19478                1,
19479            );
19480            let parsed =
19481                goc3_instance(Source::from_memory("goc3_small.json", source.into_bytes()).unwrap());
19482            let module = module_handle(powerio::PioModule::new(PioValue::AcScucInstance(parsed)));
19483            let value = pio_module_value(module);
19484            let mut error = std::ptr::null_mut();
19485            let instance = pio_value_ac_scuc_instance(value, &mut error);
19486            assert!(!instance.is_null(), "{}", error_text(error));
19487            pio_value_release(value);
19488            pio_module_release(module);
19489
19490            let mut dimensions = std::mem::MaybeUninit::<PioScucDimensionsView>::uninit();
19491            assert!(pio_ac_scuc_instance_dimensions(
19492                instance,
19493                dimensions.as_mut_ptr(),
19494                &mut error,
19495            ));
19496            let dimensions = dimensions.assume_init();
19497            assert_eq!(dimensions.period_count, 2);
19498            assert_eq!(dimensions.device_count, 2);
19499            assert_eq!(dimensions.producer_count, 1);
19500            assert_eq!(dimensions.consumer_count, 1);
19501            assert_eq!(dimensions.shunt_count, 1);
19502            assert_eq!(dimensions.branch_switching_cost_count, 3);
19503            assert_eq!(dimensions.transformer_control_count, 1);
19504            assert_eq!(dimensions.active_reserve_zone_count, 1);
19505            assert_eq!(dimensions.reactive_reserve_zone_count, 1);
19506            assert_eq!(dimensions.contingency_count, 3);
19507
19508            let durations = pio_ac_scuc_instance_interval_durations(instance);
19509            assert_eq!(
19510                std::slice::from_raw_parts(durations.data, durations.len),
19511                &[1.0, 1.0]
19512            );
19513
19514            let mut violation = std::mem::MaybeUninit::<PioScucViolationCostView>::uninit();
19515            assert!(pio_ac_scuc_instance_violation_costs(
19516                instance,
19517                violation.as_mut_ptr(),
19518                &mut error,
19519            ));
19520            let violation = violation.assume_init();
19521            assert_eq!(violation.active_power_balance, 1.0);
19522            assert_eq!(violation.reactive_power_balance, 1.0);
19523            assert_eq!(violation.branch_thermal_limit, 1.0);
19524            assert_eq!(violation.energy_requirement, 1.0);
19525
19526            let mut producer = std::mem::MaybeUninit::<PioScucDeviceView>::uninit();
19527            assert!(pio_ac_scuc_instance_device_at(
19528                instance,
19529                0,
19530                producer.as_mut_ptr(),
19531                &mut error,
19532            ));
19533            let producer = producer.assume_init();
19534            assert_eq!(view_text(producer.id.component_type), "generator");
19535            assert_eq!(view_text(producer.id.local_id), "sd_00");
19536            assert_eq!(view_text(producer.kind), "producer");
19537            assert!(producer.initial_on_status);
19538            assert_eq!(producer.minimum_up_time_hours, 1.0);
19539            assert_eq!(producer.minimum_down_time_hours, 1.0);
19540            assert_eq!(producer.initial_commitment.accumulated_up_time_hours, 4.0);
19541            assert_eq!(producer.initial_commitment.accumulated_down_time_hours, 0.0);
19542            assert_eq!(producer.startup_cost_adjustment_count, 1);
19543            assert_eq!(producer.startup_limit_count, 1);
19544            assert_eq!(producer.energy_upper_bound_count, 1);
19545            assert_eq!(producer.energy_lower_bound_count, 1);
19546            assert_eq!(producer.period_count, 2);
19547
19548            let uid = "sd_00";
19549            let mut found_device = std::mem::MaybeUninit::<PioScucDeviceView>::uninit();
19550            assert!(pio_ac_scuc_instance_device_get(
19551                instance,
19552                uid.as_ptr().cast(),
19553                uid.len(),
19554                found_device.as_mut_ptr(),
19555                &mut error,
19556            ));
19557            assert_eq!(view_text(found_device.assume_init().id.local_id), "sd_00");
19558
19559            let mut adjustment =
19560                std::mem::MaybeUninit::<PioScucStartupCostAdjustmentView>::uninit();
19561            assert!(pio_ac_scuc_instance_device_startup_cost_adjustment_at(
19562                instance,
19563                0,
19564                0,
19565                adjustment.as_mut_ptr(),
19566                &mut error,
19567            ));
19568            assert_eq!(adjustment.assume_init().maximum_down_time_hours, 3.0);
19569
19570            let mut limit = std::mem::MaybeUninit::<PioScucStartupLimitView>::uninit();
19571            assert!(pio_ac_scuc_instance_device_startup_limit_at(
19572                instance,
19573                0,
19574                0,
19575                limit.as_mut_ptr(),
19576                &mut error,
19577            ));
19578            assert_eq!(limit.assume_init().maximum_startups, 1);
19579
19580            let mut energy = std::mem::MaybeUninit::<PioScucEnergyRequirementView>::uninit();
19581            assert!(pio_ac_scuc_instance_device_energy_upper_bound_at(
19582                instance,
19583                0,
19584                0,
19585                energy.as_mut_ptr(),
19586                &mut error,
19587            ));
19588            assert_eq!(energy.assume_init().energy_pu, 9.0);
19589            assert!(pio_ac_scuc_instance_device_energy_lower_bound_at(
19590                instance,
19591                0,
19592                0,
19593                energy.as_mut_ptr(),
19594                &mut error,
19595            ));
19596            assert_eq!(energy.assume_init().energy_pu, 1.0);
19597
19598            let mut period = std::mem::MaybeUninit::<PioScucDevicePeriodView>::uninit();
19599            assert!(pio_ac_scuc_instance_device_period_at(
19600                instance,
19601                0,
19602                0,
19603                period.as_mut_ptr(),
19604                &mut error,
19605            ));
19606            let period = period.assume_init();
19607            assert!(period.on_status_min);
19608            assert!(period.on_status_max);
19609            assert_eq!(period.active_power_min_pu, 2.0);
19610            assert_eq!(period.active_power_max_pu, 5.0);
19611            assert_eq!(period.energy_cost_block_count, 1);
19612
19613            let mut cost_block = std::mem::MaybeUninit::<PioScucEnergyCostBlockView>::uninit();
19614            assert!(pio_ac_scuc_instance_device_energy_cost_block_at(
19615                instance,
19616                0,
19617                0,
19618                0,
19619                cost_block.as_mut_ptr(),
19620                &mut error,
19621            ));
19622            let cost_block = cost_block.assume_init();
19623            assert_eq!(cost_block.marginal_cost, 10.0);
19624            assert_eq!(cost_block.block_size_pu, 5.0);
19625
19626            let mut shunt = std::mem::MaybeUninit::<PioScucShuntView>::uninit();
19627            assert!(pio_ac_scuc_instance_shunt_at(
19628                instance,
19629                0,
19630                shunt.as_mut_ptr(),
19631                &mut error,
19632            ));
19633            let shunt = shunt.assume_init();
19634            assert_eq!(view_text(shunt.id.local_id), "sh_00");
19635            assert_eq!(shunt.conductance_per_step_pu, 0.0);
19636            assert_eq!(shunt.susceptance_per_step_pu, 3.0);
19637            assert_eq!(
19638                (shunt.step_min, shunt.initial_step, shunt.step_max),
19639                (0, 1, 4)
19640            );
19641            let shunt_uid = "sh_00";
19642            let mut found_shunt = std::mem::MaybeUninit::<PioScucShuntView>::uninit();
19643            assert!(pio_ac_scuc_instance_shunt_get(
19644                instance,
19645                shunt_uid.as_ptr().cast(),
19646                shunt_uid.len(),
19647                found_shunt.as_mut_ptr(),
19648                &mut error,
19649            ));
19650            assert_eq!(view_text(found_shunt.assume_init().id.local_id), "sh_00");
19651
19652            let mut switching = std::mem::MaybeUninit::<PioScucBranchSwitchingCostView>::uninit();
19653            assert!(pio_ac_scuc_instance_branch_switching_cost_at(
19654                instance,
19655                0,
19656                switching.as_mut_ptr(),
19657                &mut error,
19658            ));
19659            let switching = switching.assume_init();
19660            assert_eq!(view_text(switching.id.component_type), "branch");
19661            assert_eq!(view_text(switching.id.local_id), "acl_00");
19662
19663            let mut control = std::mem::MaybeUninit::<PioScucTransformerControlView>::uninit();
19664            assert!(pio_ac_scuc_instance_transformer_control_at(
19665                instance,
19666                0,
19667                control.as_mut_ptr(),
19668                &mut error,
19669            ));
19670            let control = control.assume_init();
19671            assert_eq!(view_text(control.id.component_type), "transformer");
19672            assert_eq!(view_text(control.id.local_id), "xf_00");
19673            assert_eq!((control.tap_ratio_min, control.tap_ratio_max), (1.0, 1.0));
19674
19675            let mut active_zone = std::mem::MaybeUninit::<PioScucActiveReserveZoneView>::uninit();
19676            assert!(pio_ac_scuc_instance_active_reserve_zone_at(
19677                instance,
19678                0,
19679                active_zone.as_mut_ptr(),
19680                &mut error,
19681            ));
19682            assert_eq!(active_zone.assume_init().bus_count, 2);
19683            let mut active_period =
19684                std::mem::MaybeUninit::<PioScucActiveReservePeriodView>::uninit();
19685            assert!(pio_ac_scuc_instance_active_reserve_zone_period_at(
19686                instance,
19687                0,
19688                0,
19689                active_period.as_mut_ptr(),
19690                &mut error,
19691            ));
19692            assert_eq!(active_period.assume_init().ramping_up_requirement_pu, 0.0);
19693            let mut reserve_bus = std::mem::MaybeUninit::<PioComponentIdView>::uninit();
19694            assert!(pio_ac_scuc_instance_active_reserve_zone_bus_at(
19695                instance,
19696                0,
19697                0,
19698                reserve_bus.as_mut_ptr(),
19699                &mut error,
19700            ));
19701            assert_eq!(view_text(reserve_bus.assume_init().local_id), "bus_00");
19702
19703            let mut reactive_zone =
19704                std::mem::MaybeUninit::<PioScucReactiveReserveZoneView>::uninit();
19705            assert!(pio_ac_scuc_instance_reactive_reserve_zone_at(
19706                instance,
19707                0,
19708                reactive_zone.as_mut_ptr(),
19709                &mut error,
19710            ));
19711            assert_eq!(reactive_zone.assume_init().bus_count, 2);
19712            let mut reactive_period =
19713                std::mem::MaybeUninit::<PioScucReactiveReservePeriodView>::uninit();
19714            assert!(pio_ac_scuc_instance_reactive_reserve_zone_period_at(
19715                instance,
19716                0,
19717                0,
19718                reactive_period.as_mut_ptr(),
19719                &mut error,
19720            ));
19721            assert_eq!(
19722                reactive_period.assume_init().reactive_up_requirement_pu,
19723                0.0
19724            );
19725            assert!(pio_ac_scuc_instance_reactive_reserve_zone_bus_at(
19726                instance,
19727                0,
19728                1,
19729                reserve_bus.as_mut_ptr(),
19730                &mut error,
19731            ));
19732            assert_eq!(view_text(reserve_bus.assume_init().local_id), "bus_01");
19733
19734            let mut contingency = std::mem::MaybeUninit::<PioScucContingencyView>::uninit();
19735            assert!(pio_ac_scuc_instance_contingency_at(
19736                instance,
19737                0,
19738                contingency.as_mut_ptr(),
19739                &mut error,
19740            ));
19741            let first_contingency = contingency.assume_init();
19742            assert_eq!(
19743                view_text(first_contingency.id.component_type),
19744                "contingency"
19745            );
19746            assert_eq!(view_text(first_contingency.id.local_id), "ctg_00");
19747            let contingency_uid = "ctg_02";
19748            assert!(pio_ac_scuc_instance_contingency_get(
19749                instance,
19750                contingency_uid.as_ptr().cast(),
19751                contingency_uid.len(),
19752                contingency.as_mut_ptr(),
19753                &mut error,
19754            ));
19755            assert_eq!(view_text(contingency.assume_init().id.local_id), "ctg_02");
19756            let mut component = std::mem::MaybeUninit::<PioScucContingencyComponentView>::uninit();
19757            assert!(pio_ac_scuc_instance_contingency_component_at(
19758                instance,
19759                0,
19760                0,
19761                component.as_mut_ptr(),
19762                &mut error,
19763            ));
19764            let component = component.assume_init();
19765            assert_eq!(view_text(component.id.component_type), "branch");
19766            assert_eq!(view_text(component.id.local_id), "acl_00");
19767
19768            pio_calculation_instance_release(instance);
19769        }
19770    }
19771
19772    #[test]
19773    fn one_parse_reads_a_goc3_problem_and_solution_directory() {
19774        unsafe {
19775            let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/goc3");
19776            let path = path.to_string_lossy();
19777            let mut error = std::ptr::null_mut();
19778            let source = pio_source_open(path.as_ptr().cast(), path.len(), &mut error);
19779            assert!(!source.is_null(), "{}", error_text(error));
19780            let module = pio_parse(source, std::ptr::null(), 0, &mut error);
19781            pio_source_release(source);
19782            assert!(!module.is_null(), "{}", error_text(error));
19783
19784            let inner = PioModule::get(module).unwrap();
19785            assert_eq!(inner.module.value().type_name(), "powerio.AcScucSolution");
19786            assert_eq!(inner.module.sources().len(), 2);
19787
19788            let value = pio_module_value(module);
19789            let solution = pio_value_ac_scuc_solution(value, &mut error);
19790            pio_value_release(value);
19791            assert!(!solution.is_null(), "{}", error_text(error));
19792            assert_eq!(pio_ac_scuc_solution_time_count(solution), 2);
19793            let quantity = "bus_voltage_magnitude";
19794            let row = pio_ac_scuc_solution_get_values_at(
19795                solution,
19796                quantity.as_ptr().cast(),
19797                quantity.len(),
19798                0,
19799                &mut error,
19800            );
19801            assert!(!row.is_null(), "{}", error_text(error));
19802            let values = pio_vector_values(row);
19803            assert_eq!(
19804                std::slice::from_raw_parts(values.data, values.len),
19805                &[1.0, 0.99]
19806            );
19807            pio_vector_release(row);
19808
19809            for quantity in [
19810                "bus_voltage_magnitude",
19811                "bus_voltage_angle",
19812                "shunt_step",
19813                "ac_line_on_status",
19814                "transformer_tap_ratio",
19815                "transformer_phase_shift",
19816                "transformer_on_status",
19817                "dc_line_from_active_power",
19818                "dc_line_from_reactive_power",
19819                "dc_line_to_reactive_power",
19820                "device_on_status",
19821                "device_startup_status",
19822                "device_shutdown_status",
19823                "device_active_power",
19824                "device_reactive_power",
19825                "regulation_reserve_up",
19826                "regulation_reserve_down",
19827                "synchronized_reserve",
19828                "nonsynchronized_reserve",
19829                "ramping_reserve_up_online",
19830                "ramping_reserve_up_offline",
19831                "ramping_reserve_down_online",
19832                "ramping_reserve_down_offline",
19833                "reactive_reserve_up",
19834                "reactive_reserve_down",
19835            ] {
19836                let vector = pio_ac_scuc_solution_get_values_at(
19837                    solution,
19838                    quantity.as_ptr().cast(),
19839                    quantity.len(),
19840                    0,
19841                    &mut error,
19842                );
19843                assert!(!vector.is_null(), "{quantity}: {}", error_text(error));
19844                pio_vector_release(vector);
19845            }
19846
19847            pio_calculation_solution_release(solution);
19848            pio_module_release(module);
19849        }
19850    }
19851
19852    #[test]
19853    fn one_parse_and_one_emit_cover_powsybl_formats() {
19854        unsafe {
19855            let module = parse_case9();
19856            let directory = tempfile::tempdir().unwrap();
19857
19858            for (format, output_name) in [
19859                ("xiidm", "case9.xiidm"),
19860                ("cgmes", "case9-cgmes"),
19861                ("psse-rawx", "case9.rawx"),
19862                ("ucte", "case9.uct"),
19863            ] {
19864                let output = directory.path().join(output_name);
19865                let output_text = output.to_string_lossy();
19866                let mut error = std::ptr::null_mut();
19867                let destination = pio_destination_path(
19868                    output_text.as_ptr().cast(),
19869                    output_text.len(),
19870                    &mut error,
19871                );
19872                assert!(!destination.is_null(), "{}", error_text(error));
19873                let result = pio_emit(
19874                    module,
19875                    format.as_ptr().cast(),
19876                    format.len(),
19877                    destination,
19878                    &mut error,
19879                );
19880                pio_destination_release(destination);
19881                assert!(!result.is_null(), "{format}: {}", error_text(error));
19882                pio_emit_result_release(result);
19883
19884                let source =
19885                    pio_source_open(output_text.as_ptr().cast(), output_text.len(), &mut error);
19886                assert!(!source.is_null(), "{format}: {}", error_text(error));
19887                let reparsed = pio_parse(source, std::ptr::null(), 0, &mut error);
19888                pio_source_release(source);
19889                assert!(!reparsed.is_null(), "{format}: {}", error_text(error));
19890                assert_eq!(
19891                    PioModule::get(reparsed).unwrap().module.value().type_name(),
19892                    "powerio.BalancedNetwork"
19893                );
19894                pio_module_release(reparsed);
19895            }
19896
19897            pio_module_release(module);
19898        }
19899    }
19900
19901    #[test]
19902    fn scuc_solution_exposes_every_device_status_and_offline_reserve() {
19903        unsafe {
19904            let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
19905                .join("../tests/data/goc3/goc3_small.json");
19906            let parsed = goc3_instance(Source::open(path).unwrap());
19907            let mut outputs = powerio_prob::ScucDeviceOutputs::default();
19908            outputs.startup_status = vec![vec![false, true], vec![true, false]];
19909            outputs.shutdown_status = vec![vec![true, false], vec![false, true]];
19910            outputs.p_ramp_res_up_offline = vec![vec![1.25, 1.75], vec![1.5, 2.0]];
19911            outputs.p_ramp_res_down_offline = vec![vec![2.25, 2.75], vec![2.5, 3.0]];
19912            let solution = powerio_prob::AcScucSolution::new(
19913                Arc::new(parsed),
19914                powerio_prob::Termination::Converged,
19915                powerio_prob::ScucNetworkOutputs::default(),
19916                outputs,
19917                None,
19918            )
19919            .unwrap();
19920            let module = module_handle(powerio::PioModule::new(PioValue::AcScucSolution(solution)));
19921            let value = pio_module_value(module);
19922            let mut error = std::ptr::null_mut();
19923            let solution = pio_value_ac_scuc_solution(value, &mut error);
19924            assert!(!solution.is_null(), "{}", error_text(error));
19925            pio_value_release(value);
19926            pio_module_release(module);
19927
19928            for (quantity, expected) in [
19929                ("device_startup_status", [0.0, 1.0]),
19930                ("device_shutdown_status", [1.0, 0.0]),
19931                ("ramping_reserve_up_offline", [1.25, 1.75]),
19932                ("ramping_reserve_down_offline", [2.25, 2.75]),
19933            ] {
19934                let vector = pio_ac_scuc_solution_get_values_at(
19935                    solution,
19936                    quantity.as_ptr().cast(),
19937                    quantity.len(),
19938                    0,
19939                    &mut error,
19940                );
19941                assert!(!vector.is_null(), "{}", error_text(error));
19942                let view = pio_vector_values(vector);
19943                assert_eq!(view.len, 2);
19944                assert_eq!(std::slice::from_raw_parts(view.data, view.len), &expected);
19945                pio_vector_release(vector);
19946            }
19947
19948            pio_calculation_solution_release(solution);
19949        }
19950    }
19951
19952    #[test]
19953    fn update_detaches_from_retained_value_views() {
19954        unsafe {
19955            let module = parse_case9();
19956            let value = pio_module_value(module);
19957            let mut error = std::ptr::null_mut();
19958            let old_network = pio_value_balanced_network(value, &mut error);
19959            pio_value_release(value);
19960            assert!(!old_network.is_null(), "{}", error_text(error));
19961
19962            let old = PioBalancedNetwork::get(old_network)
19963                .and_then(BalancedNetworkInner::network)
19964                .unwrap();
19965            let load = &old.loads()[0];
19966            let old_active_power = load.p;
19967            let local_id = load.uid.as_deref().unwrap().to_owned();
19968
19969            let component = pio_component_id_new(
19970                c"load".as_ptr(),
19971                4,
19972                local_id.as_ptr().cast(),
19973                local_id.len(),
19974                &mut error,
19975            );
19976            let replacement = pio_active_power_from_megawatts(old_active_power + 1.0);
19977            let operating = pio_operating_point_update_set_load_active_power(
19978                component,
19979                std::ptr::null(),
19980                0,
19981                replacement,
19982                &mut error,
19983            );
19984            let update = pio_calculation_update_from_operating_point(operating, &mut error);
19985            let updates = [update.cast_const()];
19986            let report = pio_apply_updates(module, updates.as_ptr(), updates.len(), &mut error);
19987            assert!(!report.is_null(), "{}", error_text(error));
19988            assert_eq!(pio_update_report_len(report), 1);
19989            assert!(!pio_update_report_connectivity_changed(report));
19990
19991            let new_value = pio_module_value(module);
19992            let new_network = pio_value_balanced_network(new_value, &mut error);
19993            let new = PioBalancedNetwork::get(new_network)
19994                .and_then(BalancedNetworkInner::network)
19995                .unwrap();
19996            assert_eq!(new.loads()[0].p, old_active_power + 1.0);
19997            assert_eq!(
19998                PioBalancedNetwork::get(old_network)
19999                    .and_then(BalancedNetworkInner::network)
20000                    .unwrap()
20001                    .loads()[0]
20002                    .p,
20003                old_active_power,
20004            );
20005
20006            pio_balanced_network_release(new_network);
20007            pio_value_release(new_value);
20008            pio_update_report_release(report);
20009            pio_calculation_update_release(update);
20010            pio_operating_point_update_release(operating);
20011            pio_active_power_release(replacement);
20012            pio_component_id_release(component);
20013            pio_balanced_network_release(old_network);
20014            pio_module_release(module);
20015        }
20016    }
20017
20018    #[test]
20019    fn dc_calculations_return_named_generic_handles() {
20020        unsafe {
20021            let module = parse_case9();
20022            let value = pio_module_value(module);
20023            let mut error = std::ptr::null_mut();
20024            let network = pio_value_balanced_network(value, &mut error);
20025            let incidence = pio_calc_incidence_matrix(network, std::ptr::null(), 0, &mut error);
20026            assert!(!incidence.is_null(), "{}", error_text(error));
20027            assert_eq!(pio_sparse_matrix_rows(incidence), 9);
20028            assert_eq!(pio_sparse_matrix_columns(incidence), 9);
20029            assert_eq!(pio_sparse_matrix_values(incidence).len, 18);
20030
20031            let branch = pio_calc_branch_susceptances(
20032                network,
20033                c"reactance_only".as_ptr(),
20034                "reactance_only".len(),
20035                &mut error,
20036            );
20037            assert!(!branch.is_null(), "{}", error_text(error));
20038            assert_eq!(pio_vector_values(branch).len, 9);
20039
20040            pio_vector_release(branch);
20041            pio_sparse_matrix_release(incidence);
20042            pio_balanced_network_release(network);
20043            pio_value_release(value);
20044            pio_module_release(module);
20045        }
20046    }
20047
20048    #[test]
20049    fn powerio_ir_uses_serialize_and_deserialize() {
20050        unsafe {
20051            let module = parse_case9();
20052            let mut error = std::ptr::null_mut();
20053            let destination = pio_destination_memory(
20054                c"case.pio.json".as_ptr(),
20055                "case.pio.json".len(),
20056                &mut error,
20057            );
20058            let result = pio_module_serialize(module, destination, &mut error);
20059            assert!(!result.is_null(), "{}", error_text(error));
20060            let artifact = pio_emit_result_artifact(result, 0, &mut error);
20061            let bytes = pio_artifact_bytes(artifact);
20062            let source = pio_source_from_memory(
20063                c"case.pio.json".as_ptr(),
20064                "case.pio.json".len(),
20065                bytes.data,
20066                bytes.len,
20067                &mut error,
20068            );
20069            let decoded = pio_module_deserialize(source, &mut error);
20070            assert!(!decoded.is_null(), "{}", error_text(error));
20071
20072            pio_module_release(decoded);
20073            pio_source_release(source);
20074            pio_artifact_release(artifact);
20075            pio_emit_result_release(result);
20076            pio_destination_release(destination);
20077            pio_module_release(module);
20078        }
20079    }
20080}