1use powerio_core::{Destination, Diagnostic, EmitResult, Error, PioModule};
10use powerio_prob::{
11 BalancedOperatingPointFlag, BalancedOperatingPointQuantity, MulticonductorOperatingPointFlag,
12 MulticonductorOperatingPointQuantity, OperatingPoint,
13};
14use powerio_tx::{BalancedNetwork, BranchSolution};
15
16use crate::PioValue;
17
18pub mod codes {
19 powerio_core::diagnostic_codes! {
20 REQUEST_EMIT_UNKNOWN_FORMAT = "REQUEST.EMIT.UNKNOWN_FORMAT", Error,
21 "the requested target format name is not recognized", category = Request;
22 REQUEST_EMIT_UNSUPPORTED_VALUE_TYPE = "REQUEST.EMIT.UNSUPPORTED_VALUE_TYPE", Error,
23 "the module's value type cannot be emitted in the requested format", category = Request;
24 EMIT_CALCULATION_DATA_OMITTED = "EMIT.CALCULATION.DATA_OMITTED", Warning,
25 "the target grid exchange format does not represent the complete calculation definition", category = Output;
26 EMIT_SOLUTION_DATA_OMITTED = "EMIT.SOLUTION.DATA_OMITTED", Warning,
27 "the target grid exchange format does not represent every solution result", category = Output;
28 EMIT_RELAXATION_DATA_OMITTED = "EMIT.RELAXATION.DATA_OMITTED", Warning,
29 "the target grid exchange format does not represent an SOCWR relaxation result", category = Output;
30 EMIT_OPERATING_POINT_DATA_OMITTED = "EMIT.OPERATING_POINT.DATA_OMITTED", Warning,
31 "the target grid exchange format does not represent every operating point quantity", category = Output;
32 }
33}
34
35fn is_pypsa_dir(format: &str) -> bool {
37 crate::resolve_format(format).is_some_and(|info| info.token == "pypsa-csv")
38}
39
40fn is_cgmes_dir(format: &str) -> bool {
41 crate::resolve_format(format).is_some_and(|info| info.token == "cgmes")
42}
43
44fn is_goc3(format: &str) -> bool {
45 crate::resolve_format(format).is_some_and(|info| info.token == "goc3-json")
46}
47
48fn is_gridfm_dir(format: &str) -> bool {
50 crate::resolve_format(format).is_some_and(|info| info.token == "gridfm")
51}
52
53fn same_directory_format(source: &str, requested: &str) -> bool {
58 if is_pypsa_dir(source) && is_pypsa_dir(requested) {
59 return true;
60 }
61 if is_cgmes_dir(source) && is_cgmes_dir(requested) {
62 return true;
63 }
64 #[cfg(feature = "gridfm")]
65 if source.eq_ignore_ascii_case("gridfm") && requested.eq_ignore_ascii_case("gridfm") {
66 return true;
67 }
68 false
69}
70
71fn echo_retained_directory(
77 module: &PioModule<PioValue>,
78 format: &str,
79) -> Result<Option<Vec<powerio_core::MemoryArtifact>>, Error> {
80 let Some(source) = module.source().filter(|source| source.is_directory()) else {
81 return Ok(None);
82 };
83 let Some(source_format) = source.format() else {
84 return Ok(None);
85 };
86 if !same_directory_format(source_format.as_str(), format) {
87 return Ok(None);
88 }
89
90 let mut artifacts = Vec::new();
91 for name in source.entry_names()? {
92 let buffer = source.buffer(&name)?;
93 artifacts.push(powerio_core::MemoryArtifact::new(
94 name,
95 buffer.bytes().to_vec(),
96 ));
97 }
98 Ok(Some(artifacts))
99}
100
101fn typed_sibling<T>(module: &PioModule<PioValue>, value: T) -> Result<PioModule<T>, Error> {
105 let mut out = PioModule::new(value).with_producer(module.producer().clone());
106 for descriptor in module.sources() {
107 out.add_source_descriptor(descriptor.clone())?;
108 }
109 for entry in module.source_map() {
110 out.add_source_map_entry(entry.clone())?;
111 }
112 for diagnostic in module.diagnostics() {
113 out.add_diagnostic(diagnostic.clone())?;
114 }
115 for entry in module.history() {
116 out.add_history_entry(entry.clone())?;
117 }
118 for (namespace, value) in module.extensions() {
119 out.insert_extension(namespace.clone(), value.clone())?;
120 }
121 Ok(match module.source() {
122 Some(source) => out.with_source(source.clone()),
123 None => out,
124 })
125}
126
127fn retained_source_matches_case_format(module: &PioModule<PioValue>, format: &str) -> bool {
140 use powerio_tx::format::routing::{
141 Detection, JsonClass, classify_format_name, classify_json_text,
142 };
143
144 let Some(source) = module.source() else {
145 return false;
146 };
147 if source.acquired_buffers().len() != 1 {
151 return false;
152 }
153 let Some(requested) = classify_format_name(format).known() else {
154 return false;
155 };
156 let actual = if let Some(declared) = source.format() {
157 let Some(actual) = classify_format_name(declared.as_str()).known() else {
158 return false;
159 };
160 actual
161 } else {
162 let Ok(buffer) = source.primary_buffer() else {
163 return false;
164 };
165 let Ok(text) = std::str::from_utf8(buffer.content_bytes()) else {
166 return false;
167 };
168 match classify_json_text(text) {
169 JsonClass::Case(Detection::Known(found)) => found,
170 _ => return false,
171 }
172 };
173 requested == actual
174}
175
176fn echo_retained_source(module: &PioModule<PioValue>, format: &str) -> Option<Vec<u8>> {
177 if !retained_source_matches_case_format(module, format) {
178 return None;
179 }
180 let source = module.source()?;
181 let buffer = source.primary_buffer().ok()?;
182 Some(buffer.bytes().to_vec())
183}
184
185fn unsupported_type(module: &PioModule<PioValue>, format: &str) -> Error {
186 Error::new(
187 &codes::REQUEST_EMIT_UNSUPPORTED_VALUE_TYPE,
188 format!(
189 "a {} module cannot be emitted as {format}; serialize writes PowerIO IR",
190 module.value().type_name()
191 ),
192 )
193}
194
195fn unknown_format(format: &str) -> Error {
196 if cfg!(not(feature = "gridfm")) && is_gridfm_dir(format) {
199 return Error::new(
200 &codes::REQUEST_EMIT_UNKNOWN_FORMAT,
201 format!(
202 "{format} names the GridFM Parquet directory format, which this build compiled without the `gridfm` feature"
203 ),
204 );
205 }
206 Error::new(
207 &codes::REQUEST_EMIT_UNKNOWN_FORMAT,
208 format!("{format} is not a recognized target format name"),
209 )
210}
211
212pub fn emit<T>(
230 module: &PioModule<T>,
231 format: &str,
232 output: impl powerio_core::IntoDestination,
233) -> Result<EmitResult, Error>
234where
235 T: Clone + Into<PioValue>,
236{
237 let module = module.clone().map_value(Into::into);
238 emit_dynamic(&module, format, output.into_destination()?)
239}
240
241fn calculation_data_omitted(value_type: &str, format: &str) -> Diagnostic {
242 Diagnostic::of(
243 &codes::EMIT_CALCULATION_DATA_OMITTED,
244 format!(
245 "{format} represents a grid case, not the complete {value_type}; emitted its electrical network"
246 ),
247 )
248}
249
250fn solution_data_omitted(value_type: &str, format: &str) -> Diagnostic {
251 Diagnostic::of(
252 &codes::EMIT_SOLUTION_DATA_OMITTED,
253 format!(
254 "{format} cannot represent every {value_type} result; emitted the network values it supports, while PowerIO IR retains the complete solution"
255 ),
256 )
257}
258
259fn relaxation_data_omitted(format: &str) -> Diagnostic {
260 Diagnostic::of(
261 &codes::EMIT_RELAXATION_DATA_OMITTED,
262 format!(
263 "{format} cannot represent SOCWR W-space values or the objective lower bound; emitted the instance network without treating the relaxation as an AC power flow solution"
264 ),
265 )
266}
267
268fn operating_point_data_omitted(format: &str) -> Diagnostic {
269 Diagnostic::of(
270 &codes::EMIT_OPERATING_POINT_DATA_OMITTED,
271 format!(
272 "{format} has no source-neutral field for net bus injection columns; emitted the other operating point quantities"
273 ),
274 )
275}
276
277pub(crate) fn network_with_balanced_operating_point(
278 point: &OperatingPoint<BalancedNetwork>,
279 format: &str,
280) -> (BalancedNetwork, Vec<Diagnostic>) {
281 let mut network = point.network().clone();
282
283 if let Some(values) = point.values(BalancedOperatingPointQuantity::BusVoltageMagnitude) {
284 for (bus, (_, value)) in network.buses_mut().iter_mut().zip(values) {
285 bus.vm = value;
286 }
287 }
288 if let Some(values) = point.values(BalancedOperatingPointQuantity::BusVoltageAngle) {
289 for (bus, (_, value)) in network.buses_mut().iter_mut().zip(values) {
290 bus.va = value.to_degrees();
291 }
292 }
293 if let Some(values) = point.values(BalancedOperatingPointQuantity::GeneratorActivePower) {
294 for (generator, (_, value)) in network.generators_mut().iter_mut().zip(values) {
295 generator.pg = value;
296 }
297 }
298 if let Some(values) = point.values(BalancedOperatingPointQuantity::GeneratorReactivePower) {
299 for (generator, (_, value)) in network.generators_mut().iter_mut().zip(values) {
300 generator.qg = value;
301 }
302 }
303 if let Some(values) = point.values(BalancedOperatingPointQuantity::GeneratorVoltageSetpoint) {
304 for (generator, (_, value)) in network.generators_mut().iter_mut().zip(values) {
305 generator.vg = value;
306 }
307 }
308 if let Some(flags) = point.flags(BalancedOperatingPointFlag::GeneratorInService) {
309 for (generator, (_, value)) in network.generators_mut().iter_mut().zip(flags) {
310 generator.in_service = value;
311 }
312 }
313 if let Some(values) = point.values(BalancedOperatingPointQuantity::LoadActivePower) {
314 for (load, (_, value)) in network.loads_mut().iter_mut().zip(values) {
315 load.p = value;
316 }
317 }
318 if let Some(values) = point.values(BalancedOperatingPointQuantity::LoadReactivePower) {
319 for (load, (_, value)) in network.loads_mut().iter_mut().zip(values) {
320 load.q = value;
321 }
322 }
323 if let Some(flags) = point.flags(BalancedOperatingPointFlag::BranchInService) {
324 for (branch, (_, value)) in network.branches_mut().iter_mut().zip(flags) {
325 branch.in_service = value;
326 }
327 }
328 if let Some(values) = point.values(BalancedOperatingPointQuantity::BranchTapRatio) {
329 for (branch, (_, value)) in network.branches_mut().iter_mut().zip(values) {
330 branch.tap = value;
331 }
332 }
333 if let Some(values) = point.values(BalancedOperatingPointQuantity::BranchPhaseShift) {
334 for (branch, (_, value)) in network.branches_mut().iter_mut().zip(values) {
335 branch.shift = value;
336 }
337 }
338 if let Some(flags) = point.flags(BalancedOperatingPointFlag::SwitchClosed) {
339 for (switch, (_, value)) in network.switches_mut().iter_mut().zip(flags) {
340 switch.closed = value;
341 }
342 }
343
344 let has_unrepresented_injections = point
345 .values(BalancedOperatingPointQuantity::BusActiveInjection)
346 .is_some()
347 || point
348 .values(BalancedOperatingPointQuantity::BusReactiveInjection)
349 .is_some();
350 let diagnostics = has_unrepresented_injections
351 .then(|| operating_point_data_omitted(format))
352 .into_iter()
353 .collect();
354 (network, diagnostics)
355}
356
357fn network_with_multiconductor_operating_point(
358 point: &OperatingPoint<powerio_dist::MulticonductorNetwork>,
359 format: &str,
360) -> (powerio_dist::MulticonductorNetwork, Vec<Diagnostic>) {
361 let mut network = point.network().clone();
362
363 if let Some(values) = point.values(MulticonductorOperatingPointQuantity::LoadActivePower) {
364 let mut values = values.map(|(_, value)| value);
365 for load in network.loads_mut() {
366 for value in &mut load.p_nom {
367 *value = values
368 .next()
369 .expect("an operating point has one value per load terminal");
370 }
371 }
372 debug_assert!(values.next().is_none());
373 }
374 if let Some(values) = point.values(MulticonductorOperatingPointQuantity::LoadReactivePower) {
375 let mut values = values.map(|(_, value)| value);
376 for load in network.loads_mut() {
377 for value in &mut load.q_nom {
378 *value = values
379 .next()
380 .expect("an operating point has one value per load terminal");
381 }
382 }
383 debug_assert!(values.next().is_none());
384 }
385 if let Some(values) = point.values(MulticonductorOperatingPointQuantity::GeneratorActivePower) {
386 let mut values = values.map(|(_, value)| value);
387 for generator in network.generators_mut() {
388 for value in &mut generator.p_nom {
389 *value = values
390 .next()
391 .expect("an operating point has one value per generator terminal");
392 }
393 }
394 debug_assert!(values.next().is_none());
395 }
396 if let Some(values) = point.values(MulticonductorOperatingPointQuantity::GeneratorReactivePower)
397 {
398 let mut values = values.map(|(_, value)| value);
399 for generator in network.generators_mut() {
400 for value in &mut generator.q_nom {
401 *value = values
402 .next()
403 .expect("an operating point has one value per generator terminal");
404 }
405 }
406 debug_assert!(values.next().is_none());
407 }
408 if let Some(flags) = point.flags(MulticonductorOperatingPointFlag::SwitchClosed) {
409 for (switch, (_, closed)) in network.switches_mut().iter_mut().zip(flags) {
410 switch.open = !closed;
411 }
412 }
413
414 let mut omitted = Vec::new();
415 if point
416 .values(MulticonductorOperatingPointQuantity::TerminalVoltageMagnitude)
417 .is_some()
418 {
419 omitted.push("terminal voltage magnitude");
420 }
421 if point
422 .values(MulticonductorOperatingPointQuantity::TerminalVoltageAngle)
423 .is_some()
424 {
425 omitted.push("terminal voltage angle");
426 }
427 if point
428 .values(MulticonductorOperatingPointQuantity::TransformerTap)
429 .is_some()
430 {
431 omitted.push("transformer tap");
432 }
433 if point
434 .values(MulticonductorOperatingPointQuantity::CapacitorSteps)
435 .is_some()
436 {
437 omitted.push("capacitor steps");
438 }
439 let diagnostics = if omitted.is_empty() {
440 Vec::new()
441 } else {
442 vec![Diagnostic::of(
443 &codes::EMIT_OPERATING_POINT_DATA_OMITTED,
444 format!(
445 "{format} cannot represent these operating point quantities in the source-neutral network model: {}; emitted the other quantities",
446 omitted.join(", ")
447 ),
448 )]
449 };
450 (network, diagnostics)
451}
452
453fn network_with_dc_pf_solution(solution: &powerio_prob::DcPfSolution) -> BalancedNetwork {
454 let mut network = solution.network().clone();
455 for bus in network.buses_mut() {
456 bus.va = solution
457 .bus_voltage_angle(bus.id)
458 .expect("a solution contains one angle per network bus");
459 }
460 if let Some(dispatch) = solution.generator_dispatch() {
461 for (generator, active_power) in network.generators_mut().iter_mut().zip(&dispatch.p_mw) {
462 generator.pg = *active_power;
463 }
464 if !dispatch.q_mvar.is_empty() {
465 for (generator, reactive_power) in
466 network.generators_mut().iter_mut().zip(&dispatch.q_mvar)
467 {
468 generator.qg = *reactive_power;
469 }
470 }
471 }
472 network
473}
474
475fn network_with_ac_pf_solution(solution: &powerio_prob::AcPfSolution) -> BalancedNetwork {
476 let mut network = solution.network().clone();
477 for bus in network.buses_mut() {
478 bus.vm = solution
479 .bus_voltage_magnitude(bus.id)
480 .expect("a solution contains one magnitude per network bus");
481 bus.va = solution
482 .bus_voltage_angle(bus.id)
483 .expect("a solution contains one angle per network bus");
484 }
485 for (branch, identity) in network
486 .branches_mut()
487 .iter_mut()
488 .zip(solution.branch_order())
489 {
490 branch.solution = Some(BranchSolution::new(
491 solution
492 .branch_from_active_flow(&identity)
493 .expect("a solution contains one from active flow per branch"),
494 solution
495 .branch_from_reactive_flow(&identity)
496 .expect("a solution contains one from reactive flow per branch"),
497 solution
498 .branch_to_active_flow(&identity)
499 .expect("a solution contains one to active flow per branch"),
500 solution
501 .branch_to_reactive_flow(&identity)
502 .expect("a solution contains one to reactive flow per branch"),
503 ));
504 }
505 if let Some(dispatch) = solution.generator_dispatch() {
506 for (generator, active_power) in network.generators_mut().iter_mut().zip(&dispatch.p_mw) {
507 generator.pg = *active_power;
508 }
509 if !dispatch.q_mvar.is_empty() {
510 for (generator, reactive_power) in
511 network.generators_mut().iter_mut().zip(&dispatch.q_mvar)
512 {
513 generator.qg = *reactive_power;
514 }
515 }
516 }
517 network
518}
519
520fn network_with_dc_opf_solution(solution: &powerio_prob::DcOpfSolution) -> BalancedNetwork {
521 let mut network = solution.network().clone();
522 for bus in network.buses_mut() {
523 bus.va = solution
524 .bus_voltage_angle(bus.id)
525 .expect("a solution contains one angle per network bus");
526 }
527 for (generator, identity) in network
528 .generators_mut()
529 .iter_mut()
530 .zip(solution.generator_order())
531 {
532 generator.pg = solution
533 .generator_active_power(&identity)
534 .expect("a solution contains one active power per generator");
535 }
536 network
537}
538
539fn network_with_ac_opf_solution(solution: &powerio_prob::AcOpfSolution) -> BalancedNetwork {
540 let mut network = solution.network().clone();
541 for bus in network.buses_mut() {
542 bus.vm = solution
543 .bus_voltage_magnitude(bus.id)
544 .expect("a solution contains one magnitude per network bus");
545 bus.va = solution
546 .bus_voltage_angle(bus.id)
547 .expect("a solution contains one angle per network bus");
548 }
549 for (branch, identity) in network
550 .branches_mut()
551 .iter_mut()
552 .zip(solution.branch_order())
553 {
554 branch.solution = Some(BranchSolution::new(
555 solution
556 .branch_from_active_flow(&identity)
557 .expect("a solution contains one from active flow per branch"),
558 solution
559 .branch_from_reactive_flow(&identity)
560 .expect("a solution contains one from reactive flow per branch"),
561 solution
562 .branch_to_active_flow(&identity)
563 .expect("a solution contains one to active flow per branch"),
564 solution
565 .branch_to_reactive_flow(&identity)
566 .expect("a solution contains one to reactive flow per branch"),
567 ));
568 }
569 for (generator, identity) in network
570 .generators_mut()
571 .iter_mut()
572 .zip(solution.generator_order())
573 {
574 generator.pg = solution
575 .generator_active_power(&identity)
576 .expect("a solution contains one active power per generator");
577 generator.qg = solution
578 .generator_reactive_power(&identity)
579 .expect("a solution contains one reactive power per generator");
580 }
581 network
582}
583
584fn emit_balanced_network(
585 module: &PioModule<PioValue>,
586 network: &BalancedNetwork,
587 format: &str,
588 destination: Destination,
589 preserve_retained_source: bool,
590 diagnostics: Vec<Diagnostic>,
591) -> Result<EmitResult, Error> {
592 let typed = typed_sibling(module, network.clone())?;
593 let typed = if preserve_retained_source && retained_source_matches_case_format(module, format) {
594 typed
595 } else {
596 typed.sever_source()
597 };
598 let result = if is_pypsa_dir(format) {
599 powerio_tx::__emit_pypsa_csv(&typed, destination)
600 } else {
601 #[cfg(feature = "gridfm")]
602 if is_gridfm_dir(format) {
603 let mut diagnostics = diagnostics;
604 let dataset = powerio_matrix::build_gridfm_dataset(
605 network,
606 0,
607 &powerio_matrix::GridfmOptions::default(),
608 )
609 .map_err(|error| Error::new(error.code(), error.to_string()).with_cause(error))?;
610 diagnostics.extend(dataset.diagnostics);
611 return destination.__commit_artifacts(
612 true,
613 powerio_core::Fidelity::Canonical,
614 dataset.artifacts,
615 diagnostics,
616 );
617 }
618 let Some(target) = powerio_tx::format::parse_target_format(format) else {
619 return Err(unknown_format(format));
620 };
621 powerio_tx::emit(&typed, target, destination)
622 }?;
623 Ok(result.__with_diagnostics(diagnostics))
624}
625
626fn emit_multiconductor_network(
627 module: &PioModule<PioValue>,
628 network: powerio_dist::MulticonductorNetwork,
629 format: &str,
630 destination: Destination,
631 preserve_retained_source: bool,
632 diagnostics: Vec<Diagnostic>,
633) -> Result<EmitResult, Error> {
634 let Some(target) = powerio_dist::parse_dist_target_format(format) else {
635 return Err(unknown_format(format));
636 };
637 let typed = typed_sibling(module, network)?;
638 let typed = if preserve_retained_source && retained_source_matches_case_format(module, format) {
639 typed
640 } else {
641 typed.sever_source()
642 };
643 powerio_dist::emit(&typed, target, destination)
644 .map(|result| result.__with_diagnostics(diagnostics))
645}
646
647fn emit_goc3_solution(
648 solution: &powerio_prob::AcScucSolution,
649 destination: Destination,
650) -> Result<EmitResult, Error> {
651 let text = powerio_prob::__internal::__emit_goc3_output(solution)?;
652 let artifact = powerio_core::MemoryArtifact::new(
653 powerio_core::ArtifactPath::new("solution.json")
654 .expect("static name is a valid artifact path"),
655 text.into_bytes(),
656 );
657 destination.__commit_artifacts(
658 false,
659 powerio_core::Fidelity::Canonical,
660 vec![artifact],
661 Vec::new(),
662 )
663}
664
665fn emit_geo_layer(
669 layer: &powerio_tx::GeoLayer,
670 format: &str,
671 destination: Destination,
672) -> Result<EmitResult, Error> {
673 if !crate::is_geo_layer_token(format) {
674 return Err(if crate::is_pwd_display_token(format) {
677 Error::new(
678 &codes::REQUEST_EMIT_UNSUPPORTED_VALUE_TYPE,
679 format!(
680 "{format} names the PowerWorld display file, which has no writer; write the layer as `geo-json`"
681 ),
682 )
683 } else if known_format_name(format) {
684 Error::new(
685 &codes::REQUEST_EMIT_UNSUPPORTED_VALUE_TYPE,
686 format!(
687 "{format} states a grid case, not powerio.GeoLayer; write a layer as `geo-json` or place it onto a case with apply_geo_layer"
688 ),
689 )
690 } else {
691 unknown_format(format)
692 });
693 }
694 let text = layer
695 .to_geojson_checked()
696 .map_err(|error| Error::new(error.code(), error.to_string()).with_cause(error))?;
697 let artifact = powerio_core::MemoryArtifact::new(
698 powerio_core::ArtifactPath::new(powerio_tx::geo::GEO_LAYER_EXTENSION)
699 .expect("static name is a valid artifact path"),
700 text.into_bytes(),
701 );
702 destination.__commit_artifacts(
703 false,
704 powerio_core::Fidelity::Canonical,
705 vec![artifact],
706 Vec::new(),
707 )
708}
709
710fn contingency_file_of_value(value: &PioValue) -> Option<crate::ContingencyFile> {
713 match value {
714 PioValue::ContingencySet(_) => Some(crate::ContingencyFile::Con),
715 PioValue::SubsystemSet(_) => Some(crate::ContingencyFile::Sub),
716 PioValue::MonitoredSet(_) => Some(crate::ContingencyFile::Mon),
717 _ => None,
718 }
719}
720
721fn retained_contingency_source(module: &PioModule<PioValue>, format: &str) -> Option<Vec<u8>> {
726 let kind = contingency_file_of_value(module.value())?;
727 if crate::contingency_file_of_token(format)? != kind {
728 return None;
729 }
730 let source = module.source()?;
731 if source.acquired_buffers().len() != 1 {
732 return None;
733 }
734 if crate::contingency_file_of_token(source.format()?.as_str())? != kind {
735 return None;
736 }
737 Some(source.primary_buffer().ok()?.bytes().to_vec())
738}
739
740fn emit_contingency_file(
745 value: &PioValue,
746 kind: crate::ContingencyFile,
747 format: &str,
748 destination: Destination,
749) -> Result<EmitResult, Error> {
750 if crate::contingency_file_of_token(format) != Some(kind) {
751 return Err(if crate::contingency_file_of_token(format).is_some() {
752 Error::new(
753 &codes::REQUEST_EMIT_UNSUPPORTED_VALUE_TYPE,
754 format!(
755 "{format} names another PSS/E contingency analysis file, not {}; write this one as `{}`",
756 kind.type_name(),
757 kind.token()
758 ),
759 )
760 } else if known_format_name(format) {
761 Error::new(
762 &codes::REQUEST_EMIT_UNSUPPORTED_VALUE_TYPE,
763 format!(
764 "{format} states a grid case, not {}; write this file as `{}`",
765 kind.type_name(),
766 kind.token()
767 ),
768 )
769 } else {
770 unknown_format(format)
771 });
772 }
773 let text = match value {
774 PioValue::ContingencySet(set) => set.to_con(),
775 PioValue::SubsystemSet(set) => set.to_sub(),
776 PioValue::MonitoredSet(set) => set.to_mon(),
777 _ => unreachable!("the value variant selected the file kind"),
778 };
779 let artifact = powerio_core::MemoryArtifact::new(
780 powerio_core::ArtifactPath::new(kind.artifact_name())
781 .expect("static name is a valid artifact path"),
782 text.into_bytes(),
783 );
784 destination.__commit_artifacts(
785 false,
786 powerio_core::Fidelity::Canonical,
787 vec![artifact],
788 Vec::new(),
789 )
790}
791
792fn balanced_calculation_network(value: &PioValue) -> Option<&BalancedNetwork> {
793 match value {
794 PioValue::DcPfInstance(instance) => Some(instance.network()),
795 PioValue::AcPfInstance(instance) => Some(instance.network()),
796 PioValue::DcOpfInstance(instance) => Some(instance.network()),
797 PioValue::AcOpfInstance(instance) => Some(instance.network()),
798 PioValue::AcScucInstance(instance) => Some(instance.network()),
799 _ => None,
800 }
801}
802
803fn multiconductor_calculation_network(
804 value: &PioValue,
805) -> Option<&powerio_dist::MulticonductorNetwork> {
806 match value {
807 PioValue::McAcPfInstance(instance) => Some(instance.network()),
808 PioValue::McAcOpfInstance(instance) => Some(instance.network()),
809 PioValue::LinDist3FlowOpfInstance(instance) => Some(instance.network()),
810 _ => None,
811 }
812}
813
814fn emit_network_or_calculation(
815 module: &PioModule<PioValue>,
816 format: &str,
817 destination: Destination,
818) -> Result<EmitResult, Error> {
819 if matches!(module.value(), PioValue::AcScucInstance(_)) && is_goc3(format) {
820 return Err(unsupported_type(module, format));
821 }
822 if let Some(network) = balanced_calculation_network(module.value()) {
823 return emit_balanced_network(
824 module,
825 network,
826 format,
827 destination,
828 false,
829 vec![calculation_data_omitted(module.value().type_name(), format)],
830 );
831 }
832 if let Some(network) = multiconductor_calculation_network(module.value()) {
833 return emit_multiconductor_network(
834 module,
835 network.clone(),
836 format,
837 destination,
838 false,
839 vec![calculation_data_omitted(module.value().type_name(), format)],
840 );
841 }
842 match &module.value() {
843 PioValue::BalancedNetwork(network) => {
844 emit_balanced_network(module, network, format, destination, true, Vec::new())
845 }
846 PioValue::MulticonductorNetwork(network) => emit_multiconductor_network(
847 module,
848 network.clone(),
849 format,
850 destination,
851 true,
852 Vec::new(),
853 ),
854 PioValue::BalancedOperatingPoint(point) => {
855 let (network, diagnostics) = network_with_balanced_operating_point(point, format);
856 emit_balanced_network(module, &network, format, destination, false, diagnostics)
857 }
858 PioValue::MulticonductorOperatingPoint(point) => {
859 let (network, diagnostics) = network_with_multiconductor_operating_point(point, format);
860 emit_multiconductor_network(module, network, format, destination, false, diagnostics)
861 }
862 _ => unreachable!("caller selected a value that is not a network or calculation"),
863 }
864}
865
866fn emit_balanced_solution_network(
867 module: &PioModule<PioValue>,
868 network: &BalancedNetwork,
869 format: &str,
870 destination: Destination,
871) -> Result<EmitResult, Error> {
872 emit_balanced_network(
873 module,
874 network,
875 format,
876 destination,
877 false,
878 vec![solution_data_omitted(module.value().type_name(), format)],
879 )
880}
881
882fn emit_multiconductor_solution_network(
883 module: &PioModule<PioValue>,
884 network: &powerio_dist::MulticonductorNetwork,
885 format: &str,
886 destination: Destination,
887) -> Result<EmitResult, Error> {
888 emit_multiconductor_network(
889 module,
890 network.clone(),
891 format,
892 destination,
893 false,
894 vec![solution_data_omitted(module.value().type_name(), format)],
895 )
896}
897
898fn emit_solution(
899 module: &PioModule<PioValue>,
900 format: &str,
901 destination: Destination,
902) -> Result<EmitResult, Error> {
903 match &module.value() {
904 PioValue::DcPfSolution(solution) => emit_balanced_solution_network(
905 module,
906 &network_with_dc_pf_solution(solution),
907 format,
908 destination,
909 ),
910 PioValue::AcPfSolution(solution) => emit_balanced_solution_network(
911 module,
912 &network_with_ac_pf_solution(solution),
913 format,
914 destination,
915 ),
916 PioValue::DcOpfSolution(solution) => emit_balanced_solution_network(
917 module,
918 &network_with_dc_opf_solution(solution),
919 format,
920 destination,
921 ),
922 PioValue::AcOpfSolution(solution) => emit_balanced_solution_network(
923 module,
924 &network_with_ac_opf_solution(solution),
925 format,
926 destination,
927 ),
928 PioValue::SocwrOpfSolution(solution) => emit_balanced_network(
929 module,
930 solution.network(),
931 format,
932 destination,
933 false,
934 vec![relaxation_data_omitted(format)],
935 ),
936 PioValue::McAcPfSolution(solution) => {
937 emit_multiconductor_solution_network(module, solution.network(), format, destination)
938 }
939 PioValue::McAcOpfSolution(solution) => {
940 emit_multiconductor_solution_network(module, solution.network(), format, destination)
941 }
942 PioValue::LinDist3FlowOpfSolution(solution) => {
943 emit_multiconductor_solution_network(module, solution.network(), format, destination)
944 }
945 PioValue::AcScucSolution(solution) if is_goc3(format) => {
946 emit_goc3_solution(solution, destination)
947 }
948 PioValue::AcScucSolution(solution) => emit_balanced_solution_network(
949 module,
950 solution.instance().network(),
951 format,
952 destination,
953 ),
954 _ => unreachable!("caller selected a value that is not a solution"),
955 }
956}
957
958fn emit_versioned_bmopf(
959 module: &PioModule<PioValue>,
960 version: &str,
961 destination: Destination,
962) -> Result<EmitResult, Error> {
963 let format = format!("bmopf-json@{version}");
964 let profile = match version {
965 "0.1.0" => powerio_dist::BmopfSchemaVersion::Bmopf010,
966 "0.2.0" => powerio_dist::BmopfSchemaVersion::Bmopf020,
967 _ => return Err(unknown_format(&format)),
968 };
969 let (network, diagnostics) = match module.value() {
970 PioValue::MulticonductorNetwork(network) => (network.clone(), Vec::new()),
971 PioValue::McAcPfInstance(instance) => (
972 instance.network().clone(),
973 vec![calculation_data_omitted(
974 module.value().type_name(),
975 &format,
976 )],
977 ),
978 PioValue::McAcOpfInstance(instance) => (
979 instance.network().clone(),
980 vec![calculation_data_omitted(
981 module.value().type_name(),
982 &format,
983 )],
984 ),
985 PioValue::LinDist3FlowOpfInstance(instance) => (
986 instance.network().clone(),
987 vec![calculation_data_omitted(
988 module.value().type_name(),
989 &format,
990 )],
991 ),
992 PioValue::MulticonductorOperatingPoint(point) => {
993 network_with_multiconductor_operating_point(point, &format)
994 }
995 PioValue::McAcPfSolution(solution) => (
996 solution.instance().network().clone(),
997 vec![solution_data_omitted(module.value().type_name(), &format)],
998 ),
999 PioValue::McAcOpfSolution(solution) => (
1000 solution.instance().network().clone(),
1001 vec![solution_data_omitted(module.value().type_name(), &format)],
1002 ),
1003 PioValue::LinDist3FlowOpfSolution(solution) => (
1004 solution.instance().network().clone(),
1005 vec![solution_data_omitted(module.value().type_name(), &format)],
1006 ),
1007 _ => return Err(unsupported_type(module, &format)),
1008 };
1009 let typed = typed_sibling(module, network)?.sever_source();
1010 let mut options = powerio_dist::EmitOptions::default();
1011 options.bmopf.schema_version = profile;
1012 powerio_dist::emit_with_options(
1013 &typed,
1014 powerio_dist::DistTargetFormat::BmopfJson,
1015 &options,
1016 destination,
1017 )
1018 .map(|result| result.__with_diagnostics(diagnostics))
1019}
1020
1021fn emit_dynamic(
1022 module: &PioModule<PioValue>,
1023 format: &str,
1024 destination: Destination,
1025) -> Result<EmitResult, Error> {
1026 if let Some(version) = format.strip_prefix("bmopf-json@") {
1027 return emit_versioned_bmopf(module, version, destination);
1028 }
1029 if let Some(artifacts) = echo_retained_directory(module, format)? {
1030 return destination.__commit_artifacts(
1031 true,
1032 powerio_core::Fidelity::ExactSameFormat,
1033 artifacts,
1034 Vec::new(),
1035 );
1036 }
1037
1038 if !matches!(
1042 &module.value(),
1043 PioValue::BalancedNetwork(_) | PioValue::MulticonductorNetwork(_)
1044 ) && let Some(bytes) = echo_retained_source(module, format)
1045 {
1046 let artifact = powerio_core::MemoryArtifact::new(
1047 powerio_core::ArtifactPath::new("case").expect("static name is a valid artifact path"),
1048 bytes,
1049 );
1050 return destination.__commit_artifacts(
1051 false,
1052 powerio_core::Fidelity::ExactSameFormat,
1053 vec![artifact],
1054 Vec::new(),
1055 );
1056 }
1057
1058 if let PioValue::GeoLayer(layer) = &module.value() {
1059 return emit_geo_layer(layer, format, destination);
1060 }
1061
1062 if let Some(kind) = contingency_file_of_value(module.value()) {
1063 if let Some(bytes) = retained_contingency_source(module, format) {
1064 let artifact = powerio_core::MemoryArtifact::new(
1065 powerio_core::ArtifactPath::new(kind.artifact_name())
1066 .expect("static name is a valid artifact path"),
1067 bytes,
1068 );
1069 return destination.__commit_artifacts(
1070 false,
1071 powerio_core::Fidelity::ExactSameFormat,
1072 vec![artifact],
1073 Vec::new(),
1074 );
1075 }
1076 return emit_contingency_file(module.value(), kind, format, destination);
1077 }
1078
1079 match &module.value() {
1080 PioValue::BalancedNetwork(_)
1081 | PioValue::MulticonductorNetwork(_)
1082 | PioValue::BalancedOperatingPoint(_)
1083 | PioValue::MulticonductorOperatingPoint(_)
1084 | PioValue::DcPfInstance(_)
1085 | PioValue::AcPfInstance(_)
1086 | PioValue::DcOpfInstance(_)
1087 | PioValue::AcOpfInstance(_)
1088 | PioValue::McAcPfInstance(_)
1089 | PioValue::McAcOpfInstance(_)
1090 | PioValue::LinDist3FlowOpfInstance(_)
1091 | PioValue::AcScucInstance(_) => emit_network_or_calculation(module, format, destination),
1092 PioValue::DcPfSolution(_)
1093 | PioValue::AcPfSolution(_)
1094 | PioValue::DcOpfSolution(_)
1095 | PioValue::AcOpfSolution(_)
1096 | PioValue::SocwrOpfSolution(_)
1097 | PioValue::McAcPfSolution(_)
1098 | PioValue::McAcOpfSolution(_)
1099 | PioValue::LinDist3FlowOpfSolution(_)
1100 | PioValue::AcScucSolution(_) => emit_solution(module, format, destination),
1101 _ => {
1102 if known_format_name(format) {
1103 Err(unsupported_type(module, format))
1104 } else {
1105 Err(unknown_format(format))
1106 }
1107 }
1108 }
1109}
1110
1111fn known_format_name(format: &str) -> bool {
1114 crate::resolve_format(format).is_some()
1115}
1116
1117#[cfg(test)]
1118mod tests {
1119 use super::*;
1120 use powerio_core::{
1121 DiagnosticCode, DiagnosticSeverity, HistoryEntry, HistoryId, HistoryKind, Producer,
1122 SourceDescriptor, SourceId, SourceMapEntry, SourceRelation, SourceSpan,
1123 };
1124 use powerio_tx::{BalancedNetwork, Bus, BusId, BusType};
1125
1126 #[test]
1127 fn a_typed_writer_sibling_preserves_every_common_record() {
1128 let network = BalancedNetwork::in_memory(
1129 "records",
1130 100.0,
1131 vec![Bus::new(BusId(1), BusType::Ref, 230.0)],
1132 vec![],
1133 );
1134 let source = powerio_core::Source::from_memory("case.m", b"case bytes".to_vec())
1135 .unwrap()
1136 .with_format(powerio_core::FormatId::new("matpower").unwrap());
1137 let source_id = SourceId::new("source-1").unwrap();
1138 let mut module = PioModule::new(PioValue::BalancedNetwork(network.clone()))
1139 .with_producer(Producer::new("records-test", "1").unwrap())
1140 .with_source(source);
1141 module
1142 .add_source_descriptor(SourceDescriptor::new(source_id.clone(), "case.m", 10).unwrap())
1143 .unwrap();
1144 module
1145 .add_source_map_entry(
1146 SourceMapEntry::new(
1147 "/buses/0",
1148 SourceRelation::Exact,
1149 vec![SourceSpan::new(source_id, 0, 4).unwrap()],
1150 )
1151 .unwrap(),
1152 )
1153 .unwrap();
1154 module
1155 .add_diagnostic(powerio_core::Diagnostic::new(
1156 DiagnosticCode::new("READ.TEST.RECORD").unwrap(),
1157 DiagnosticSeverity::Remark,
1158 "record carried to the family writer",
1159 ))
1160 .unwrap();
1161 module
1162 .add_history_entry(
1163 HistoryEntry::new(
1164 HistoryId::new("history-1").unwrap(),
1165 HistoryKind::Parse,
1166 "parse",
1167 )
1168 .unwrap(),
1169 )
1170 .unwrap();
1171 module
1172 .insert_extension("test.writer", serde_json::json!({"kept": true}))
1173 .unwrap();
1174
1175 let sibling = typed_sibling(&module, network).unwrap();
1176 assert_eq!(sibling.producer(), module.producer());
1177 assert_eq!(sibling.sources(), module.sources());
1178 assert_eq!(sibling.source_map(), module.source_map());
1179 assert_eq!(sibling.diagnostics(), module.diagnostics());
1180 assert_eq!(sibling.history(), module.history());
1181 assert_eq!(sibling.extensions(), module.extensions());
1182 let sibling_source = sibling.source().unwrap();
1183 assert_eq!(sibling_source.name(), "case.m");
1184 assert_eq!(
1185 sibling_source.primary_buffer().unwrap().bytes(),
1186 b"case bytes"
1187 );
1188 }
1189}