1use crate::codes;
3use crate::{BalancedNetwork, GeoApplyReport, GeoLayer};
4use powerio_core::{Diagnostic, HistoryEntry, HistoryId, HistoryKind};
5use powerio_dist::{MulticonductorNetwork, NeutralKronOptions, NeutralKronReport};
6#[cfg(test)]
7use powerio_tnd::transform::capped_history_notes;
8#[doc(inline)]
9pub use powerio_tnd::transform::{
10 MulticonductorToBalancedError, MulticonductorToBalancedOptions, MulticonductorToBalancedReport,
11 MulticonductorToBalancedTransformation, SequenceTransformConvention, to_balanced_network,
12 to_balanced_network_report,
13};
14use std::collections::BTreeMap;
15pub fn to_balanced_report(
21 module: &powerio_core::PioModule<crate::PioValue>,
22 options: MulticonductorToBalancedOptions,
23) -> Result<MulticonductorToBalancedReport, powerio_core::Error> {
24 let crate::PioValue::MulticonductorNetwork(net) = &module.value() else {
25 return Err(wrong_kind_error(module.value()));
26 };
27 Ok(to_balanced_network_report(net, options))
28}
29
30#[allow(clippy::result_large_err)]
46pub fn to_balanced(
47 module: powerio_core::PioModule<crate::PioValue>,
48 options: MulticonductorToBalancedOptions,
49) -> Result<
50 powerio_core::PioModule<crate::PioValue>,
51 (
52 powerio_core::PioModule<crate::PioValue>,
53 Box<MulticonductorToBalancedError>,
54 ),
55> {
56 let crate::PioValue::MulticonductorNetwork(net) = &module.value() else {
57 let error = MulticonductorToBalancedError::new(
58 options,
59 &[Diagnostic::of(
60 &codes::TRANSFORM_MULTI_TO_BALANCED_WRONG_MODEL_KIND,
61 format!(
62 "the module carries a {} value; the balanced lowering takes a \
63 multiconductor network",
64 module.value().type_name()
65 ),
66 )],
67 );
68 return Err((module, Box::new(error)));
69 };
70 let lowering = match to_balanced_network(net, options) {
71 Ok(lowering) => lowering,
72 Err(error) => return Err((module, Box::new(error))),
73 };
74 let MulticonductorToBalancedTransformation {
75 network,
76 diagnostics,
77 history,
78 ..
79 } = lowering;
80 let diagnostics_room =
84 powerio_core::limits::MAX_MODULE_DIAGNOSTICS.saturating_sub(module.diagnostics().len());
85 let history_room =
86 powerio_core::limits::MAX_MODULE_HISTORY_ENTRIES.saturating_sub(module.history().len());
87 if diagnostics.len() > diagnostics_room || history_room == 0 {
88 let error = MulticonductorToBalancedError::new(
89 options,
90 &[Diagnostic::of(
91 &codes::TRANSFORM_MULTI_TO_BALANCED_RECORD_CAP,
92 "the module cannot hold the lowering's findings and history entry; export a \
93 fresh module before lowering"
94 .to_string(),
95 )],
96 );
97 return Err((module, Box::new(error)));
98 }
99 let mut module = module
100 .map_value(|_| crate::PioValue::BalancedNetwork(network))
101 .sever_source();
102 module.sever_value_targets();
107 for diagnostic in &diagnostics {
108 let mut diagnostic = diagnostic.clone();
109 diagnostic.clear_target();
110 module
111 .add_diagnostic(diagnostic)
112 .expect("room was checked; pass diagnostics carry no identity and no span");
113 }
114 let entry = copy_history_with_id(
115 &history,
116 unused_history_id(&module, "multiconductor-to-balanced"),
117 );
118 module
119 .add_history_entry(entry)
120 .expect("room was checked and the history id is unique by construction");
121 Ok(module)
122}
123
124fn derive_balanced_calculation<I>(
125 module: &powerio_core::PioModule<crate::PioValue>,
126 operation: &'static str,
127 output_type: &'static str,
128 build: impl FnOnce(BalancedNetwork) -> Result<I, powerio_core::Error>,
129) -> Result<powerio_core::PioModule<I>, powerio_core::Error> {
130 if !matches!(module.value(), crate::PioValue::BalancedNetwork(_)) {
131 return Err(powerio_core::Error::new(
132 &codes::REQUEST_MODULE_WRONG_MODEL_KIND,
133 format!(
134 "{operation} requires powerio.BalancedNetwork; the module contains {}",
135 module.value().type_name()
136 ),
137 ));
138 }
139 let history = HistoryEntry::new(
140 unused_history_id(module, operation),
141 HistoryKind::Transform,
142 operation,
143 )?
144 .with_input_type("powerio.BalancedNetwork")?
145 .with_output_type(output_type)?;
146 let producer = powerio_core::Producer::new("powerio", crate::VERSION)?;
147 module.clone().try_derive_value(producer, history, |value| {
148 let crate::PioValue::BalancedNetwork(network) = value else {
149 unreachable!("the value type was checked before derivation")
150 };
151 build(network)
152 })
153}
154
155fn derive_calculation<I>(
159 module: &powerio_core::PioModule<crate::PioValue>,
160 operation: &'static str,
161 output_type: &'static str,
162 value: Result<I, powerio_core::Error>,
163) -> Result<powerio_core::PioModule<I>, powerio_core::Error> {
164 let value = value?;
165 let history = HistoryEntry::new(
166 unused_history_id(module, operation),
167 HistoryKind::Transform,
168 operation,
169 )?
170 .with_input_type(module.value().type_name())?
171 .with_output_type(output_type)?;
172 let producer = powerio_core::Producer::new("powerio", crate::VERSION)?;
173 module
174 .clone()
175 .try_derive_value(producer, history, |_| Ok(value))
176}
177
178fn derive_multiconductor_calculation<I>(
179 module: &powerio_core::PioModule<crate::PioValue>,
180 operation: &'static str,
181 output_type: &'static str,
182 build: impl FnOnce(MulticonductorNetwork) -> Result<I, powerio_core::Error>,
183) -> Result<powerio_core::PioModule<I>, powerio_core::Error> {
184 if !matches!(module.value(), crate::PioValue::MulticonductorNetwork(_)) {
185 return Err(powerio_core::Error::new(
186 &codes::REQUEST_MODULE_WRONG_MODEL_KIND,
187 format!(
188 "{operation} requires powerio.MulticonductorNetwork; the module contains {}",
189 module.value().type_name()
190 ),
191 ));
192 }
193 let history = HistoryEntry::new(
194 unused_history_id(module, operation),
195 HistoryKind::Transform,
196 operation,
197 )?
198 .with_input_type("powerio.MulticonductorNetwork")?
199 .with_output_type(output_type)?;
200 let producer = powerio_core::Producer::new("powerio", crate::VERSION)?;
201 module.clone().try_derive_value(producer, history, |value| {
202 let crate::PioValue::MulticonductorNetwork(network) = value else {
203 unreachable!("the value type was checked before derivation")
204 };
205 build(network)
206 })
207}
208
209pub fn apply_geo_layer(
222 module: &powerio_core::PioModule<crate::PioValue>,
223 layer: &GeoLayer,
224) -> Result<(powerio_core::PioModule<crate::PioValue>, GeoApplyReport), powerio_core::Error> {
225 let type_name = match &module.value() {
226 crate::PioValue::BalancedNetwork(_) => "powerio.BalancedNetwork",
227 crate::PioValue::MulticonductorNetwork(_) => "powerio.MulticonductorNetwork",
228 value => {
229 return Err(powerio_core::Error::new(
230 &codes::REQUEST_MODULE_WRONG_MODEL_KIND,
231 format!(
232 "apply_geo_layer requires powerio.BalancedNetwork or \
233 powerio.MulticonductorNetwork; the module contains {}",
234 value.type_name()
235 ),
236 ));
237 }
238 };
239 let history = HistoryEntry::new(
240 unused_history_id(module, "apply-geo-layer"),
241 HistoryKind::Transform,
242 "apply_geo_layer",
243 )?
244 .with_input_type(type_name)?
245 .with_output_type(type_name)?;
246 let producer = powerio_core::Producer::new("powerio", crate::VERSION)?;
247 let mut report = None;
248 let derived = module
249 .clone()
250 .try_derive_value(producer, history, |mut value| {
251 let applied = match &mut value {
252 crate::PioValue::BalancedNetwork(network) => network.apply_geo_layer(layer),
253 crate::PioValue::MulticonductorNetwork(network) => {
254 crate::dist_geo::apply_dist_geo_layer(network, layer)
255 }
256 value => {
257 return Err(powerio_core::Error::new(
258 &codes::REQUEST_MODULE_WRONG_MODEL_KIND,
259 format!(
260 "apply_geo_layer requires powerio.BalancedNetwork or \
261 powerio.MulticonductorNetwork; the module contains {}",
262 value.type_name()
263 ),
264 ));
265 }
266 };
267 report = Some(applied);
268 Ok(value)
269 })?;
270 let report = report.ok_or_else(|| {
271 powerio_core::Error::new(
272 &codes::REQUEST_MODULE_WRONG_MODEL_KIND,
273 "apply_geo_layer did not receive a network value",
274 )
275 })?;
276 Ok((derived, report))
277}
278
279pub fn neutral_kron(
286 module: &powerio_core::PioModule<crate::PioValue>,
287) -> Result<(powerio_core::PioModule<crate::PioValue>, NeutralKronReport), powerio_core::Error> {
288 neutral_kron_with_options(module, &NeutralKronOptions::default())
289}
290
291pub fn neutral_kron_with_options(
302 module: &powerio_core::PioModule<crate::PioValue>,
303 options: &NeutralKronOptions,
304) -> Result<(powerio_core::PioModule<crate::PioValue>, NeutralKronReport), powerio_core::Error> {
305 let crate::PioValue::MulticonductorNetwork(network) = module.value() else {
306 return Err(powerio_core::Error::new(
307 &codes::REQUEST_MODULE_WRONG_MODEL_KIND,
308 format!(
309 "neutral_kron requires powerio.MulticonductorNetwork; the module contains {}",
310 module.value().type_name()
311 ),
312 ));
313 };
314 let reduction = powerio_dist::neutral_kron_reduce(network, options).map_err(|cause| {
315 powerio_core::Error::new(cause.code(), cause.to_string()).with_cause(cause)
316 })?;
317 let (network, report) = reduction.into_parts();
318 let neutral_terminals = options
319 .neutral_terminals
320 .iter()
321 .map(|(bus, terminal)| (bus.clone(), serde_json::Value::String(terminal.clone())))
322 .collect();
323 let parameters = BTreeMap::from([
324 (
325 "allow_forced_ideal_ground".to_owned(),
326 serde_json::Value::Bool(options.allow_forced_ideal_ground),
327 ),
328 (
329 "neutral_terminals".to_owned(),
330 serde_json::Value::Object(neutral_terminals),
331 ),
332 ]);
333 let history = HistoryEntry::new(
334 unused_history_id(module, "neutral_kron"),
335 HistoryKind::Transform,
336 "neutral_kron",
337 )?
338 .with_input_type("powerio.MulticonductorNetwork")?
339 .with_output_type("powerio.MulticonductorNetwork")?
340 .with_parameters(parameters)?;
341 let producer = powerio_core::Producer::new("powerio", crate::VERSION)?;
342 let mut derived = module.clone().try_derive_value(
343 producer,
344 history,
345 move |value| match value {
346 crate::PioValue::MulticonductorNetwork(_) => {
347 Ok(crate::PioValue::MulticonductorNetwork(network))
348 }
349 value => Err(powerio_core::Error::new(
350 &codes::REQUEST_MODULE_WRONG_MODEL_KIND,
351 format!(
352 "neutral_kron requires powerio.MulticonductorNetwork; the module contains {}",
353 value.type_name()
354 ),
355 )),
356 },
357 )?;
358 for diagnostic in &report.diagnostics {
359 let mut diagnostic = diagnostic.clone();
360 diagnostic.clear_target();
361 derived.add_diagnostic(diagnostic)?;
362 }
363 Ok((derived, report))
364}
365
366#[must_use]
374pub fn network_with_operating_point(
375 point: &powerio_prob::OperatingPoint<BalancedNetwork>,
376) -> BalancedNetwork {
377 crate::write::network_with_balanced_operating_point(point, "network").0
378}
379
380pub fn to_dc_pf_instance(
385 module: &powerio_core::PioModule<crate::PioValue>,
386) -> Result<powerio_core::PioModule<powerio_prob::DcPfInstance>, powerio_core::Error> {
387 if matches!(module.value(), crate::PioValue::DcPfInstance(_)) {
388 return Ok(module.clone().map_value(|value| match value {
389 crate::PioValue::DcPfInstance(instance) => instance,
390 _ => unreachable!("the value type was checked before extraction"),
391 }));
392 }
393 derive_balanced_calculation(
394 module,
395 "to_dc_pf_instance",
396 "powerio.DcPfInstance",
397 powerio_prob::DcPfInstance::from_network,
398 )
399}
400
401pub fn to_ac_pf_instance(
403 module: &powerio_core::PioModule<crate::PioValue>,
404) -> Result<powerio_core::PioModule<powerio_prob::AcPfInstance>, powerio_core::Error> {
405 if matches!(module.value(), crate::PioValue::AcPfInstance(_)) {
406 return Ok(module.clone().map_value(|value| match value {
407 crate::PioValue::AcPfInstance(instance) => instance,
408 _ => unreachable!("the value type was checked before extraction"),
409 }));
410 }
411 derive_balanced_calculation(
412 module,
413 "to_ac_pf_instance",
414 "powerio.AcPfInstance",
415 powerio_prob::AcPfInstance::from_network,
416 )
417}
418
419pub fn to_dc_opf_instance(
422 module: &powerio_core::PioModule<crate::PioValue>,
423) -> Result<powerio_core::PioModule<powerio_prob::DcOpfInstance>, powerio_core::Error> {
424 if matches!(module.value(), crate::PioValue::DcOpfInstance(_)) {
425 return Ok(module.clone().map_value(|value| match value {
426 crate::PioValue::DcOpfInstance(instance) => instance,
427 _ => unreachable!("the value type was checked before extraction"),
428 }));
429 }
430 derive_balanced_calculation(
431 module,
432 "to_dc_opf_instance",
433 "powerio.DcOpfInstance",
434 powerio_prob::DcOpfInstance::from_network,
435 )
436}
437
438pub fn to_ac_opf_instance(
441 module: &powerio_core::PioModule<crate::PioValue>,
442) -> Result<powerio_core::PioModule<powerio_prob::AcOpfInstance>, powerio_core::Error> {
443 if matches!(module.value(), crate::PioValue::AcOpfInstance(_)) {
444 return Ok(module.clone().map_value(|value| match value {
445 crate::PioValue::AcOpfInstance(instance) => instance,
446 _ => unreachable!("the value type was checked before extraction"),
447 }));
448 }
449 derive_balanced_calculation(
450 module,
451 "to_ac_opf_instance",
452 "powerio.AcOpfInstance",
453 powerio_prob::AcOpfInstance::from_network,
454 )
455}
456
457pub fn to_ac_ots_instance(
469 module: &powerio_core::PioModule<crate::PioValue>,
470) -> Result<powerio_core::PioModule<powerio_prob::AcOtsInstance>, powerio_core::Error> {
471 match module.value() {
472 crate::PioValue::AcOtsInstance(_) => Ok(module.clone().map_value(|value| match value {
473 crate::PioValue::AcOtsInstance(instance) => instance,
474 _ => unreachable!("the value type was checked before extraction"),
475 })),
476 crate::PioValue::AcOpfInstance(base) => derive_calculation(
477 module,
478 "to_ac_ots_instance",
479 "powerio.AcOtsInstance",
480 powerio_prob::AcOtsInstance::from_ac_opf(base.clone()),
481 ),
482 _ => derive_balanced_calculation(
483 module,
484 "to_ac_ots_instance",
485 "powerio.AcOtsInstance",
486 powerio_prob::AcOtsInstance::from_network,
487 ),
488 }
489}
490
491pub fn to_ac_ots_instance_with_options(
502 module: &powerio_core::PioModule<crate::PioValue>,
503 options: powerio_prob::AcOtsOptions,
504) -> Result<powerio_core::PioModule<powerio_prob::AcOtsInstance>, powerio_core::Error> {
505 let configure = |instance: Result<powerio_prob::AcOtsInstance, powerio_core::Error>| {
506 instance.map(powerio_prob::AcOtsInstance::with_switching_objective)
507 };
508 match module.value() {
509 crate::PioValue::AcOtsInstance(instance) => derive_calculation(
510 module,
511 "to_ac_ots_instance",
512 "powerio.AcOtsInstance",
513 configure(instance.clone().with_options(options)),
514 ),
515 crate::PioValue::AcOpfInstance(base) => derive_calculation(
516 module,
517 "to_ac_ots_instance",
518 "powerio.AcOtsInstance",
519 configure(powerio_prob::AcOtsInstance::new(base.clone(), options)),
520 ),
521 _ => derive_balanced_calculation(
522 module,
523 "to_ac_ots_instance",
524 "powerio.AcOtsInstance",
525 |network| {
526 configure(powerio_prob::AcOtsInstance::new(
527 powerio_prob::AcOpfInstance::from_network(network)?,
528 options,
529 ))
530 },
531 ),
532 }
533}
534
535pub fn ac_ots_instance_from_base(
543 module: &powerio_core::PioModule<crate::PioValue>,
544 base: &powerio_prob::AcOpfInstance,
545 options: powerio_prob::AcOtsOptions,
546) -> Result<powerio_core::PioModule<powerio_prob::AcOtsInstance>, powerio_core::Error> {
547 derive_calculation(
548 module,
549 "ac_ots_instance",
550 "powerio.AcOtsInstance",
551 powerio_prob::AcOtsInstance::new(base.clone(), options),
552 )
553}
554
555pub fn fix_ac_ots_topology(
563 module: &powerio_core::PioModule<crate::PioValue>,
564 instance: &powerio_prob::AcOtsInstance,
565 branch_closed: &[bool],
566) -> Result<powerio_core::PioModule<powerio_prob::AcOpfInstance>, powerio_core::Error> {
567 let (fixed, diagnostics) = instance.fix_topology(branch_closed)?;
568 let mut derived = derive_calculation(
569 module,
570 "fix_ac_ots_topology",
571 "powerio.AcOpfInstance",
572 Ok(fixed),
573 )?;
574 for mut diagnostic in diagnostics {
575 diagnostic.clear_target();
576 derived.add_diagnostic(diagnostic)?;
577 }
578 Ok(derived)
579}
580
581pub fn ac_ots_solution(
589 module: &powerio_core::PioModule<crate::PioValue>,
590 instance: &powerio_prob::AcOtsInstance,
591 branch_closed: Vec<bool>,
592 ac_solution: powerio_prob::AcOpfSolution,
593) -> Result<powerio_core::PioModule<powerio_prob::AcOtsSolution>, powerio_core::Error> {
594 derive_calculation(
595 module,
596 "ac_ots_solution",
597 "powerio.AcOtsSolution",
598 powerio_prob::AcOtsSolution::new(
599 std::sync::Arc::new(instance.clone()),
600 branch_closed,
601 ac_solution,
602 ),
603 )
604}
605
606pub fn materialize_ac_ots_solution(
615 module: &powerio_core::PioModule<crate::PioValue>,
616 solution: &powerio_prob::AcOtsSolution,
617) -> Result<powerio_core::PioModule<BalancedNetwork>, powerio_core::Error> {
618 let mut network = crate::write::network_with_ac_opf_solution(solution.ac_solution());
619 network.assign_missing_component_ids();
620 derive_calculation(
621 module,
622 "materialize_ac_ots_solution",
623 "powerio.BalancedNetwork",
624 Ok(network),
625 )
626}
627
628pub fn to_mc_ac_pf_instance(
631 module: &powerio_core::PioModule<crate::PioValue>,
632) -> Result<powerio_core::PioModule<powerio_prob::McAcPfInstance>, powerio_core::Error> {
633 if matches!(module.value(), crate::PioValue::McAcPfInstance(_)) {
634 return Ok(module.clone().map_value(|value| match value {
635 crate::PioValue::McAcPfInstance(instance) => instance,
636 _ => unreachable!("the value type was checked before extraction"),
637 }));
638 }
639 derive_multiconductor_calculation(
640 module,
641 "to_mc_ac_pf_instance",
642 "powerio.McAcPfInstance",
643 powerio_prob::McAcPfInstance::from_network,
644 )
645}
646
647pub fn to_mc_ac_opf_instance(
650 module: &powerio_core::PioModule<crate::PioValue>,
651) -> Result<powerio_core::PioModule<powerio_prob::McAcOpfInstance>, powerio_core::Error> {
652 if matches!(module.value(), crate::PioValue::McAcOpfInstance(_)) {
653 return Ok(module.clone().map_value(|value| match value {
654 crate::PioValue::McAcOpfInstance(instance) => instance,
655 _ => unreachable!("the value type was checked before extraction"),
656 }));
657 }
658 derive_multiconductor_calculation(
659 module,
660 "to_mc_ac_opf_instance",
661 "powerio.McAcOpfInstance",
662 powerio_prob::McAcOpfInstance::from_network,
663 )
664}
665
666pub fn to_lindist3flow_opf_instance(
672 module: &powerio_core::PioModule<crate::PioValue>,
673) -> Result<powerio_core::PioModule<powerio_prob::LinDist3FlowOpfInstance>, powerio_core::Error> {
674 to_lindist3flow_opf_instance_with_options(
675 module,
676 powerio_prob::LinDist3FlowBuildOptions::default(),
677 )
678}
679
680pub fn to_lindist3flow_opf_instance_with_options(
687 module: &powerio_core::PioModule<crate::PioValue>,
688 options: powerio_prob::LinDist3FlowBuildOptions,
689) -> Result<powerio_core::PioModule<powerio_prob::LinDist3FlowOpfInstance>, powerio_core::Error> {
690 if matches!(module.value(), crate::PioValue::LinDist3FlowOpfInstance(_)) {
691 return Ok(module.clone().map_value(|value| match value {
692 crate::PioValue::LinDist3FlowOpfInstance(instance) => instance,
693 _ => unreachable!("the value type was checked before extraction"),
694 }));
695 }
696 if !matches!(module.value(), crate::PioValue::MulticonductorNetwork(_)) {
697 return Err(powerio_core::Error::new(
698 &codes::REQUEST_MODULE_WRONG_MODEL_KIND,
699 format!(
700 "to_lindist3flow_opf_instance requires powerio.MulticonductorNetwork; the module contains {}",
701 module.value().type_name()
702 ),
703 ));
704 }
705 let options_value = serde_json::to_value(options).map_err(|cause| {
706 powerio_core::Error::new(
707 &codes::TRANSFORM_LINDIST3FLOW_OPTIONS_SERIALIZE_FAILED,
708 "could not record LinDist3Flow build options in transform history",
709 )
710 .with_cause(cause)
711 })?;
712 let serde_json::Value::Object(parameters) = options_value else {
713 return Err(powerio_core::Error::new(
714 &codes::TRANSFORM_LINDIST3FLOW_OPTIONS_SERIALIZE_FAILED,
715 "LinDist3Flow build options did not serialize as an object",
716 ));
717 };
718 let history = HistoryEntry::new(
719 unused_history_id(module, "to_lindist3flow_opf_instance"),
720 HistoryKind::Transform,
721 "to_lindist3flow_opf_instance",
722 )?
723 .with_input_type("powerio.MulticonductorNetwork")?
724 .with_output_type("powerio.LinDist3FlowOpfInstance")?
725 .with_parameters(parameters.into_iter().collect())?;
726 let producer = powerio_core::Producer::new("powerio", crate::VERSION)?;
727 module.clone().try_derive_value(producer, history, |value| {
728 let crate::PioValue::MulticonductorNetwork(network) = value else {
729 unreachable!("the value type was checked before derivation")
730 };
731 powerio_prob::LinDist3FlowOpfInstance::from_network(network, options)
732 })
733}
734
735pub fn to_lindist3flow_pf_instance(
738 module: &powerio_core::PioModule<crate::PioValue>,
739) -> Result<powerio_core::PioModule<powerio_prob::LinDist3FlowPfInstance>, powerio_core::Error> {
740 to_lindist3flow_pf_instance_with_options(
741 module,
742 powerio_prob::LinDist3FlowBuildOptions::default(),
743 )
744}
745
746pub fn to_lindist3flow_pf_instance_with_options(
749 module: &powerio_core::PioModule<crate::PioValue>,
750 options: powerio_prob::LinDist3FlowBuildOptions,
751) -> Result<powerio_core::PioModule<powerio_prob::LinDist3FlowPfInstance>, powerio_core::Error> {
752 if matches!(module.value(), crate::PioValue::LinDist3FlowPfInstance(_)) {
753 return Ok(module.clone().map_value(|value| match value {
754 crate::PioValue::LinDist3FlowPfInstance(instance) => instance,
755 _ => unreachable!("the value type was checked before extraction"),
756 }));
757 }
758 if !matches!(module.value(), crate::PioValue::MulticonductorNetwork(_)) {
759 return Err(powerio_core::Error::new(
760 &codes::REQUEST_MODULE_WRONG_MODEL_KIND,
761 format!(
762 "to_lindist3flow_pf_instance requires powerio.MulticonductorNetwork; the module contains {}",
763 module.value().type_name()
764 ),
765 ));
766 }
767 let options_value = serde_json::to_value(options).map_err(|cause| {
768 powerio_core::Error::new(
769 &codes::TRANSFORM_LINDIST3FLOW_OPTIONS_SERIALIZE_FAILED,
770 "could not record LinDist3Flow build options in transform history",
771 )
772 .with_cause(cause)
773 })?;
774 let serde_json::Value::Object(parameters) = options_value else {
775 return Err(powerio_core::Error::new(
776 &codes::TRANSFORM_LINDIST3FLOW_OPTIONS_SERIALIZE_FAILED,
777 "LinDist3Flow build options did not serialize as an object",
778 ));
779 };
780 let history = HistoryEntry::new(
781 unused_history_id(module, "to_lindist3flow_pf_instance"),
782 HistoryKind::Transform,
783 "to_lindist3flow_pf_instance",
784 )?
785 .with_input_type("powerio.MulticonductorNetwork")?
786 .with_output_type("powerio.LinDist3FlowPfInstance")?
787 .with_parameters(parameters.into_iter().collect())?;
788 let producer = powerio_core::Producer::new("powerio", crate::VERSION)?;
789 module.clone().try_derive_value(producer, history, |value| {
790 let crate::PioValue::MulticonductorNetwork(network) = value else {
791 unreachable!("the value type was checked before derivation")
792 };
793 powerio_prob::LinDist3FlowPfInstance::from_network(network, options)
794 })
795}
796
797fn copy_history_with_id(history: &HistoryEntry, id: HistoryId) -> HistoryEntry {
803 let mut copied = HistoryEntry::new(id, history.kind(), history.name())
804 .expect("the existing history name is valid")
805 .with_parameters(history.parameters().clone())
806 .expect("the existing parameter set is valid");
807 if let Some(type_name) = history.input_type() {
808 copied = copied
809 .with_input_type(type_name)
810 .expect("the existing input type is valid");
811 }
812 if let Some(type_name) = history.output_type() {
813 copied = copied
814 .with_output_type(type_name)
815 .expect("the existing output type is valid");
816 }
817 for assumption in history.assumptions() {
818 copied = copied
819 .with_assumption(assumption.clone())
820 .expect("the existing assumption list is valid");
821 }
822 for loss in history.losses() {
823 copied = copied
824 .with_loss(loss.clone())
825 .expect("the existing loss list is valid");
826 }
827 copied
828}
829
830fn unused_history_id(
834 module: &powerio_core::PioModule<crate::PioValue>,
835 base: &str,
836) -> powerio_core::HistoryId {
837 use powerio_core::HistoryId;
838 let taken: std::collections::BTreeSet<&str> = module
839 .history()
840 .iter()
841 .map(|entry| entry.id().as_str())
842 .collect();
843 if !taken.contains(base) {
844 return HistoryId::new(base).expect("static id is valid");
845 }
846 let mut counter = 2usize;
847 loop {
848 let candidate = format!("{base}-{counter}");
849 if !taken.contains(candidate.as_str()) {
850 return HistoryId::new(candidate).expect("numbered id is valid");
851 }
852 counter += 1;
853 }
854}
855
856fn wrong_kind_error(value: &crate::PioValue) -> powerio_core::Error {
857 powerio_core::Error::new(
858 &codes::TRANSFORM_MULTI_TO_BALANCED_WRONG_MODEL_KIND,
859 format!(
860 "the module carries a {} value; the balanced lowering takes a multiconductor \
861 network",
862 value.type_name()
863 ),
864 )
865}
866
867#[cfg(test)]
868mod history_record_tests {
869 use super::{capped_history_notes, unused_history_id};
870
871 #[test]
872 fn note_overflow_is_stated_within_the_cap() {
873 let cap = powerio_core::limits::MAX_HISTORY_NOTES;
874 let notes: Vec<String> = (0..cap + 40).map(|index| format!("note {index}")).collect();
875 let kept = capped_history_notes(notes, "assumptions");
876 assert_eq!(kept.len(), cap);
877 assert_eq!(kept.last().unwrap(), "41 more assumptions elided");
878
879 let short: Vec<String> = (0..3).map(|index| format!("note {index}")).collect();
880 assert_eq!(capped_history_notes(short.clone(), "assumptions"), short);
881 }
882
883 #[test]
884 fn the_history_id_is_minted_unused() {
885 use powerio_core::{HistoryEntry, HistoryId, HistoryKind, PioModule};
886 let mut module = PioModule::new(crate::PioValue::BalancedNetwork(
887 crate::BalancedNetwork::in_memory("t", 100.0, Vec::new(), Vec::new()),
888 ));
889 assert_eq!(
890 unused_history_id(&module, "multiconductor-to-balanced").as_str(),
891 "multiconductor-to-balanced"
892 );
893 for id in ["multiconductor-to-balanced", "multiconductor-to-balanced-2"] {
894 module
895 .add_history_entry(
896 HistoryEntry::new(
897 HistoryId::new(id).unwrap(),
898 HistoryKind::Transform,
899 "to_balanced",
900 )
901 .unwrap(),
902 )
903 .unwrap();
904 }
905 assert_eq!(
906 unused_history_id(&module, "multiconductor-to-balanced").as_str(),
907 "multiconductor-to-balanced-3"
908 );
909 }
910}