Skip to main content

powerio/
write.rs

1//! The module emission dispatcher. One dynamic module emits a named target
2//! format; the concrete value routes to the family emitter that owns it.
3//!
4//! Same format emission keeps the family writers' echo tier: an unchanged
5//! parsed module emits its retained source bytes exactly. Cross format
6//! emission serializes the typed value and reports what the target cannot
7//! represent through the returned diagnostics.
8
9use 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
35/// True when `format` names the PyPSA CSV folder target.
36fn 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
48/// True when `format` names the GridFM directory target.
49fn is_gridfm_dir(format: &str) -> bool {
50    crate::resolve_format(format).is_some_and(|info| info.token == "gridfm")
51}
52
53/// True when two names identify the same supported directory format. The
54/// retained source carries the format selected by the parser; this comparison
55/// admits the public aliases at emission without treating an arbitrary
56/// declared directory token as an echoable format.
57fn 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
71/// The exact artifact inventory of an unchanged directory source when the
72/// requested format is that source's format. Entry names come from the
73/// source's bounded portable directory walk, and every byte is acquired
74/// through the source's confined, no-symlink path rather than by reopening a
75/// caller-controlled path here.
76fn 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
101/// A typed sibling module over `value` carrying every common record and the
102/// retained source from the dynamic module. Source descriptors are added
103/// first because source map and diagnostic spans validate against them.
104fn 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
127/// The module's retained source's exact original bytes, when that source's
128/// content is already `format`: the byte exact echo tier every kind should
129/// get, not just [`PioValue::BalancedNetwork`] and
130/// [`PioValue::MulticonductorNetwork`] (whose family emitters already carry
131/// it through `powerio_tx::emit` and `powerio_dist::emit`).
132/// `None` when there is no retained source, or its content is not `format`.
133///
134/// A source declared with an explicit format token (the C ABI and bindings
135/// route this way) is trusted directly; a source routed here by content (a
136/// bare `.json` file with no declared format, the common case for the CLI
137/// and a plain `Source::open`) is reclassified the same way `powerio::parse`'s
138/// own routing already did once to land on this kind in the first place.
139fn 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    // A one file representation can echo its primary buffer. A source set
148    // cannot: its primary file is only one input to the typed value (for
149    // example, the problem file beside a GO Challenge 3 solution).
150    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    // The descriptor still resolves the token in a build without the GridFM
197    // emitter, so the refusal names the missing feature rather than the name.
198    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
212/// Facade emission options. Defaults preserve the ordinary source-echo and
213/// canonical writer behavior for every format.
214#[derive(Clone, Debug, Default)]
215#[non_exhaustive]
216pub struct EmitOptions {
217    /// Fresh SINCAL generation is experimental and must be requested explicitly.
218    /// When present, this bypasses retained-source echo even for unchanged input.
219    #[cfg(feature = "sincal")]
220    pub sincal_experimental: Option<crate::SincalExperimentalOptions>,
221}
222
223/// Emit one module as `format` into `output`. The concrete value routes to its
224/// grid exchange format implementation. PowerIO IR uses [`crate::serialize`]
225/// instead.
226///
227/// `output` names the file or directory to write, or is a
228/// [`powerio_core::Destination`], which is how a memory destination and its
229/// artifact root name are given.
230///
231/// # Errors
232/// `REQUEST.EMIT.UNKNOWN_FORMAT` for a format name nothing recognizes,
233/// `REQUEST.EMIT.UNSUPPORTED_VALUE_TYPE` for a value the
234/// named format cannot state, and the family writer's own failure otherwise.
235///
236/// # Panics
237/// Never on external input: the fixed artifact names are valid by
238/// construction, and the solution columns a family writer reads are length
239/// checked by the solution constructors.
240pub fn emit<T>(
241    module: &PioModule<T>,
242    format: &str,
243    output: impl powerio_core::IntoDestination,
244) -> Result<EmitResult, Error>
245where
246    T: Clone + Into<PioValue>,
247{
248    emit_with_options(module, format, &EmitOptions::default(), output)
249}
250
251/// Emit with explicit facade options. An experimental SINCAL request always
252/// creates fresh typed output; it does not fall back to retained source echo.
253/// The default options are equivalent to [`emit`].
254///
255/// # Errors
256/// The same failures as [`emit`], invalid options for the selected format/value,
257/// or unsupported physics in the explicitly selected experimental profile.
258pub fn emit_with_options<T>(
259    module: &PioModule<T>,
260    format: &str,
261    options: &EmitOptions,
262    output: impl powerio_core::IntoDestination,
263) -> Result<EmitResult, Error>
264where
265    T: Clone + Into<PioValue>,
266{
267    let module = module.clone().map_value(Into::into);
268    let destination = output.into_destination()?;
269    #[cfg(feature = "sincal")]
270    if let Some(options) = &options.sincal_experimental {
271        return crate::sincal_write::emit(&module, format, options, destination);
272    }
273    // Without the sincal feature no option changes emission yet.
274    #[cfg(not(feature = "sincal"))]
275    let _: &EmitOptions = options;
276    let result = emit_dynamic(&module, format, destination)?;
277    let from_sincal = module.sources().iter().any(|source| {
278        source
279            .format()
280            .is_some_and(|f| is_sincal_format(f.as_str()))
281    });
282    if from_sincal
283        && !is_sincal_format(format)
284        && !result.diagnostics().iter().any(|d| {
285            matches!(
286                d.code(),
287                "EMIT.SINCAL.RETAINED_SOURCE_OMITTED" | "EMIT.DIST.SINCAL_RETAINED_SOURCE_OMITTED"
288            )
289        })
290    {
291        Ok(result.__with_diagnostics(vec![Diagnostic::of(
292            &powerio_tx::diagnostics::codes::EMIT_SINCAL_RETAINED_SOURCE_OMITTED,
293            "Cross-format emission includes the typed electrical profile; fault, dynamic, protection, diagram, result and other native source-only data are omitted.",
294        )]))
295    } else {
296        Ok(result)
297    }
298}
299
300fn calculation_data_omitted(value_type: &str, format: &str) -> Diagnostic {
301    Diagnostic::of(
302        &codes::EMIT_CALCULATION_DATA_OMITTED,
303        format!(
304            "{format} represents a grid case, not the complete {value_type}; emitted its electrical network"
305        ),
306    )
307}
308
309fn solution_data_omitted(value_type: &str, format: &str) -> Diagnostic {
310    Diagnostic::of(
311        &codes::EMIT_SOLUTION_DATA_OMITTED,
312        format!(
313            "{format} cannot represent every {value_type} result; emitted the network values it supports, while PowerIO IR retains the complete solution"
314        ),
315    )
316}
317
318fn relaxation_data_omitted(format: &str) -> Diagnostic {
319    Diagnostic::of(
320        &codes::EMIT_RELAXATION_DATA_OMITTED,
321        format!(
322            "{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"
323        ),
324    )
325}
326
327fn operating_point_data_omitted(format: &str) -> Diagnostic {
328    Diagnostic::of(
329        &codes::EMIT_OPERATING_POINT_DATA_OMITTED,
330        format!(
331            "{format} has no source-neutral field for net bus injection columns; emitted the other operating point quantities"
332        ),
333    )
334}
335
336pub(crate) fn network_with_balanced_operating_point(
337    point: &OperatingPoint<BalancedNetwork>,
338    format: &str,
339) -> (BalancedNetwork, Vec<Diagnostic>) {
340    let mut network = point.network().clone();
341    let mut overrides_electrical_state = false;
342
343    if let Some(values) = point.values(BalancedOperatingPointQuantity::BusVoltageMagnitude) {
344        overrides_electrical_state = true;
345        for (bus, (_, value)) in network.buses_mut().iter_mut().zip(values) {
346            bus.vm = value;
347        }
348    }
349    if let Some(values) = point.values(BalancedOperatingPointQuantity::BusVoltageAngle) {
350        overrides_electrical_state = true;
351        for (bus, (_, value)) in network.buses_mut().iter_mut().zip(values) {
352            bus.va = value.to_degrees();
353        }
354    }
355    if let Some(values) = point.values(BalancedOperatingPointQuantity::GeneratorActivePower) {
356        overrides_electrical_state = true;
357        for (generator, (_, value)) in network.generators_mut().iter_mut().zip(values) {
358            generator.pg = value;
359        }
360    }
361    if let Some(values) = point.values(BalancedOperatingPointQuantity::GeneratorReactivePower) {
362        overrides_electrical_state = true;
363        for (generator, (_, value)) in network.generators_mut().iter_mut().zip(values) {
364            generator.qg = value;
365        }
366    }
367    if let Some(values) = point.values(BalancedOperatingPointQuantity::GeneratorVoltageSetpoint) {
368        overrides_electrical_state = true;
369        for (generator, (_, value)) in network.generators_mut().iter_mut().zip(values) {
370            generator.vg = value;
371        }
372    }
373    if let Some(flags) = point.flags(BalancedOperatingPointFlag::GeneratorInService) {
374        overrides_electrical_state = true;
375        for (generator, (_, value)) in network.generators_mut().iter_mut().zip(flags) {
376            generator.in_service = value;
377        }
378    }
379    if let Some(values) = point.values(BalancedOperatingPointQuantity::LoadActivePower) {
380        overrides_electrical_state = true;
381        for (load, (_, value)) in network.loads_mut().iter_mut().zip(values) {
382            load.p = value;
383        }
384    }
385    if let Some(values) = point.values(BalancedOperatingPointQuantity::LoadReactivePower) {
386        overrides_electrical_state = true;
387        for (load, (_, value)) in network.loads_mut().iter_mut().zip(values) {
388            load.q = value;
389        }
390    }
391    if let Some(flags) = point.flags(BalancedOperatingPointFlag::BranchInService) {
392        overrides_electrical_state = true;
393        for (branch, (_, value)) in network.branches_mut().iter_mut().zip(flags) {
394            branch.in_service = value;
395        }
396    }
397    if let Some(values) = point.values(BalancedOperatingPointQuantity::BranchTapRatio) {
398        overrides_electrical_state = true;
399        for (branch, (_, value)) in network.branches_mut().iter_mut().zip(values) {
400            branch.tap = value;
401        }
402    }
403    if let Some(values) = point.values(BalancedOperatingPointQuantity::BranchPhaseShift) {
404        overrides_electrical_state = true;
405        for (branch, (_, value)) in network.branches_mut().iter_mut().zip(values) {
406            branch.shift = value;
407        }
408    }
409    if let Some(flags) = point.flags(BalancedOperatingPointFlag::SwitchClosed) {
410        overrides_electrical_state = true;
411        for (switch, (_, value)) in network.switches_mut().iter_mut().zip(flags) {
412            switch.closed = value;
413        }
414    }
415
416    let has_unrepresented_injections = point
417        .values(BalancedOperatingPointQuantity::BusActiveInjection)
418        .is_some()
419        || point
420            .values(BalancedOperatingPointQuantity::BusReactiveInjection)
421            .is_some();
422    let mut diagnostics: Vec<_> = has_unrepresented_injections
423        .then(|| operating_point_data_omitted(format))
424        .into_iter()
425        .collect();
426    // An operating-point column describes a new state, not a solved branch
427    // result. Even an unchanged first point of a series must not inherit flows
428    // that later points cannot supply. A point with no overrides keeps them.
429    if overrides_electrical_state || has_unrepresented_injections {
430        let mut cleared = false;
431        for branch in network.branches_mut() {
432            cleared |= branch.solution.take().is_some();
433            for key in ["pf", "qf", "pt", "qt"] {
434                cleared |= branch.extras.remove(key).is_some();
435            }
436        }
437        if cleared {
438            diagnostics.push(Diagnostic::of(
439                &codes::EMIT_OPERATING_POINT_DATA_OMITTED,
440                format!("{format}: base-network branch results were cleared because operating-point columns supply a new electrical state without branch results"),
441            ));
442        }
443    }
444    (network, diagnostics)
445}
446
447fn network_with_multiconductor_operating_point(
448    point: &OperatingPoint<powerio_dist::MulticonductorNetwork>,
449    format: &str,
450) -> (powerio_dist::MulticonductorNetwork, Vec<Diagnostic>) {
451    let mut network = point.network().clone();
452
453    if let Some(values) = point.values(MulticonductorOperatingPointQuantity::LoadActivePower) {
454        let mut values = values.map(|(_, value)| value);
455        for load in network.loads_mut() {
456            for value in &mut load.p_nom {
457                *value = values
458                    .next()
459                    .expect("an operating point has one value per load terminal");
460            }
461        }
462        debug_assert!(values.next().is_none());
463    }
464    if let Some(values) = point.values(MulticonductorOperatingPointQuantity::LoadReactivePower) {
465        let mut values = values.map(|(_, value)| value);
466        for load in network.loads_mut() {
467            for value in &mut load.q_nom {
468                *value = values
469                    .next()
470                    .expect("an operating point has one value per load terminal");
471            }
472        }
473        debug_assert!(values.next().is_none());
474    }
475    if let Some(values) = point.values(MulticonductorOperatingPointQuantity::GeneratorActivePower) {
476        let mut values = values.map(|(_, value)| value);
477        for generator in network.generators_mut() {
478            for value in &mut generator.p_nom {
479                *value = values
480                    .next()
481                    .expect("an operating point has one value per generator terminal");
482            }
483        }
484        debug_assert!(values.next().is_none());
485    }
486    if let Some(values) = point.values(MulticonductorOperatingPointQuantity::GeneratorReactivePower)
487    {
488        let mut values = values.map(|(_, value)| value);
489        for generator in network.generators_mut() {
490            for value in &mut generator.q_nom {
491                *value = values
492                    .next()
493                    .expect("an operating point has one value per generator terminal");
494            }
495        }
496        debug_assert!(values.next().is_none());
497    }
498    if let Some(flags) = point.flags(MulticonductorOperatingPointFlag::SwitchClosed) {
499        for (switch, (_, closed)) in network.switches_mut().iter_mut().zip(flags) {
500            switch.open = !closed;
501        }
502    }
503
504    let mut omitted = Vec::new();
505    if point
506        .values(MulticonductorOperatingPointQuantity::TerminalVoltageMagnitude)
507        .is_some()
508    {
509        omitted.push("terminal voltage magnitude");
510    }
511    if point
512        .values(MulticonductorOperatingPointQuantity::TerminalVoltageAngle)
513        .is_some()
514    {
515        omitted.push("terminal voltage angle");
516    }
517    if point
518        .values(MulticonductorOperatingPointQuantity::TransformerTap)
519        .is_some()
520    {
521        omitted.push("transformer tap");
522    }
523    if point
524        .values(MulticonductorOperatingPointQuantity::CapacitorSteps)
525        .is_some()
526    {
527        omitted.push("capacitor steps");
528    }
529    let diagnostics = if omitted.is_empty() {
530        Vec::new()
531    } else {
532        vec![Diagnostic::of(
533            &codes::EMIT_OPERATING_POINT_DATA_OMITTED,
534            format!(
535                "{format} cannot represent these operating point quantities in the source-neutral network model: {}; emitted the other quantities",
536                omitted.join(", ")
537            ),
538        )]
539    };
540    (network, diagnostics)
541}
542
543fn network_with_dc_pf_solution(solution: &powerio_prob::DcPfSolution) -> BalancedNetwork {
544    let mut network = solution.network().clone();
545    for bus in network.buses_mut() {
546        bus.va = solution
547            .bus_voltage_angle(bus.id)
548            .expect("a solution contains one angle per network bus");
549    }
550    if let Some(dispatch) = solution.generator_dispatch() {
551        for (generator, active_power) in network.generators_mut().iter_mut().zip(&dispatch.p_mw) {
552            generator.pg = *active_power;
553        }
554        if !dispatch.q_mvar.is_empty() {
555            for (generator, reactive_power) in
556                network.generators_mut().iter_mut().zip(&dispatch.q_mvar)
557            {
558                generator.qg = *reactive_power;
559            }
560        }
561    }
562    network
563}
564
565fn network_with_ac_pf_solution(solution: &powerio_prob::AcPfSolution) -> BalancedNetwork {
566    let mut network = solution.network().clone();
567    for bus in network.buses_mut() {
568        bus.vm = solution
569            .bus_voltage_magnitude(bus.id)
570            .expect("a solution contains one magnitude per network bus");
571        bus.va = solution
572            .bus_voltage_angle(bus.id)
573            .expect("a solution contains one angle per network bus");
574    }
575    for (branch, identity) in network
576        .branches_mut()
577        .iter_mut()
578        .zip(solution.branch_order())
579    {
580        branch.solution = Some(BranchSolution::new(
581            solution
582                .branch_from_active_flow(&identity)
583                .expect("a solution contains one from active flow per branch"),
584            solution
585                .branch_from_reactive_flow(&identity)
586                .expect("a solution contains one from reactive flow per branch"),
587            solution
588                .branch_to_active_flow(&identity)
589                .expect("a solution contains one to active flow per branch"),
590            solution
591                .branch_to_reactive_flow(&identity)
592                .expect("a solution contains one to reactive flow per branch"),
593        ));
594    }
595    if let Some(dispatch) = solution.generator_dispatch() {
596        for (generator, active_power) in network.generators_mut().iter_mut().zip(&dispatch.p_mw) {
597            generator.pg = *active_power;
598        }
599        if !dispatch.q_mvar.is_empty() {
600            for (generator, reactive_power) in
601                network.generators_mut().iter_mut().zip(&dispatch.q_mvar)
602            {
603                generator.qg = *reactive_power;
604            }
605        }
606    }
607    network
608}
609
610fn network_with_dc_opf_solution(solution: &powerio_prob::DcOpfSolution) -> BalancedNetwork {
611    let mut network = solution.network().clone();
612    for bus in network.buses_mut() {
613        bus.va = solution
614            .bus_voltage_angle(bus.id)
615            .expect("a solution contains one angle per network bus");
616    }
617    for (generator, identity) in network
618        .generators_mut()
619        .iter_mut()
620        .zip(solution.generator_order())
621    {
622        generator.pg = solution
623            .generator_active_power(&identity)
624            .expect("a solution contains one active power per generator");
625    }
626    network
627}
628
629pub(crate) fn network_with_ac_opf_solution(
630    solution: &powerio_prob::AcOpfSolution,
631) -> BalancedNetwork {
632    let mut network = solution.network().clone();
633    for bus in network.buses_mut() {
634        bus.vm = solution
635            .bus_voltage_magnitude(bus.id)
636            .expect("a solution contains one magnitude per network bus");
637        bus.va = solution
638            .bus_voltage_angle(bus.id)
639            .expect("a solution contains one angle per network bus");
640    }
641    for (branch, identity) in network
642        .branches_mut()
643        .iter_mut()
644        .zip(solution.branch_order())
645    {
646        branch.solution = Some(BranchSolution::new(
647            solution
648                .branch_from_active_flow(&identity)
649                .expect("a solution contains one from active flow per branch"),
650            solution
651                .branch_from_reactive_flow(&identity)
652                .expect("a solution contains one from reactive flow per branch"),
653            solution
654                .branch_to_active_flow(&identity)
655                .expect("a solution contains one to active flow per branch"),
656            solution
657                .branch_to_reactive_flow(&identity)
658                .expect("a solution contains one to reactive flow per branch"),
659        ));
660    }
661    for (generator, identity) in network
662        .generators_mut()
663        .iter_mut()
664        .zip(solution.generator_order())
665    {
666        generator.pg = solution
667            .generator_active_power(&identity)
668            .expect("a solution contains one active power per generator");
669        generator.qg = solution
670            .generator_reactive_power(&identity)
671            .expect("a solution contains one reactive power per generator");
672    }
673    network
674}
675
676fn emit_balanced_network(
677    module: &PioModule<PioValue>,
678    network: &BalancedNetwork,
679    format: &str,
680    destination: Destination,
681    preserve_retained_source: bool,
682    diagnostics: Vec<Diagnostic>,
683) -> Result<EmitResult, Error> {
684    let typed = typed_sibling(module, network.clone())?;
685    let typed = if preserve_retained_source && retained_source_matches_case_format(module, format) {
686        typed
687    } else {
688        typed.sever_source()
689    };
690    let result = if is_pypsa_dir(format) {
691        powerio_tx::__emit_pypsa_csv(&typed, destination)
692    } else {
693        #[cfg(feature = "gridfm")]
694        if is_gridfm_dir(format) {
695            let mut diagnostics = diagnostics;
696            let dataset = powerio_matrix::build_gridfm_dataset(
697                network,
698                0,
699                &powerio_matrix::GridfmOptions::default(),
700            )
701            .map_err(|error| Error::new(error.code(), error.to_string()).with_cause(error))?;
702            diagnostics.extend(dataset.diagnostics);
703            return destination.__commit_artifacts(
704                true,
705                powerio_core::Fidelity::Canonical,
706                dataset.artifacts,
707                diagnostics,
708            );
709        }
710        let Some(target) = powerio_tx::format::parse_target_format(format) else {
711            return Err(unknown_format(format));
712        };
713        powerio_tx::emit(&typed, target, destination)
714    }?;
715    Ok(result.__with_diagnostics(diagnostics))
716}
717
718fn emit_multiconductor_network(
719    module: &PioModule<PioValue>,
720    network: powerio_dist::MulticonductorNetwork,
721    format: &str,
722    destination: Destination,
723    preserve_retained_source: bool,
724    diagnostics: Vec<Diagnostic>,
725) -> Result<EmitResult, Error> {
726    let Some(target) = powerio_dist::parse_dist_target_format(format) else {
727        return Err(unknown_format(format));
728    };
729    let typed = typed_sibling(module, network)?;
730    let retained_project = matches!(
731        target,
732        powerio_dist::DistTargetFormat::Dss
733            | powerio_dist::DistTargetFormat::Gridlabd
734            | powerio_dist::DistTargetFormat::GridlabdArras
735    ) && module
736        .source()
737        .and_then(powerio_core::Source::format)
738        .and_then(|id| powerio_dist::parse_dist_target_format(id.as_str()))
739        == Some(target);
740    let typed = if preserve_retained_source
741        && (retained_project || retained_source_matches_case_format(module, format))
742    {
743        typed
744    } else {
745        typed.sever_source()
746    };
747    powerio_dist::emit(&typed, target, destination)
748        .map(|result| result.__with_diagnostics(diagnostics))
749}
750
751fn emit_goc3_solution(
752    solution: &powerio_prob::AcScucSolution,
753    destination: Destination,
754) -> Result<EmitResult, Error> {
755    let text = powerio_prob::__internal::__emit_goc3_output(solution)?;
756    let artifact = powerio_core::MemoryArtifact::new(
757        powerio_core::ArtifactPath::new("solution.json")
758            .expect("static name is a valid artifact path"),
759        text.into_bytes(),
760    );
761    destination.__commit_artifacts(
762        false,
763        powerio_core::Fidelity::Canonical,
764        vec![artifact],
765        Vec::new(),
766    )
767}
768
769/// Write one geographic layer as the canonical `.geo.json` document. No grid
770/// exchange format states a standalone layer, so any other target is refused
771/// by value type.
772fn emit_geo_layer(
773    layer: &powerio_tx::GeoLayer,
774    format: &str,
775    destination: Destination,
776) -> Result<EmitResult, Error> {
777    if !crate::is_geo_layer_token(format) {
778        // A `.pwd` display reads into a layer but has no writer, so naming it
779        // as the target is a different mistake from naming a grid case.
780        return Err(if crate::is_pwd_display_token(format) {
781            Error::new(
782                &codes::REQUEST_EMIT_UNSUPPORTED_VALUE_TYPE,
783                format!(
784                    "{format} names the PowerWorld display file, which has no writer; write the layer as `geo-json`"
785                ),
786            )
787        } else if known_format_name(format) {
788            Error::new(
789                &codes::REQUEST_EMIT_UNSUPPORTED_VALUE_TYPE,
790                format!(
791                    "{format} states a grid case, not powerio.GeoLayer; write a layer as `geo-json` or place it onto a case with apply_geo_layer"
792                ),
793            )
794        } else {
795            unknown_format(format)
796        });
797    }
798    let text = layer
799        .to_geojson_checked()
800        .map_err(|error| Error::new(error.code(), error.to_string()).with_cause(error))?;
801    let artifact = powerio_core::MemoryArtifact::new(
802        powerio_core::ArtifactPath::new(powerio_tx::geo::GEO_LAYER_EXTENSION)
803            .expect("static name is a valid artifact path"),
804        text.into_bytes(),
805    );
806    destination.__commit_artifacts(
807        false,
808        powerio_core::Fidelity::Canonical,
809        vec![artifact],
810        Vec::new(),
811    )
812}
813
814/// The three PSS/E contingency analysis files that a module value can be, or
815/// `None` for every other value.
816fn contingency_file_of_value(value: &PioValue) -> Option<crate::ContingencyFile> {
817    match value {
818        PioValue::ContingencySet(_) => Some(crate::ContingencyFile::Con),
819        PioValue::SubsystemSet(_) => Some(crate::ContingencyFile::Sub),
820        PioValue::MonitoredSet(_) => Some(crate::ContingencyFile::Mon),
821        _ => None,
822    }
823}
824
825/// The module's retained source bytes when the value is a PSS/E contingency
826/// analysis file and `format` names that same file. The three tokens are not
827/// grid case names, so `retained_source_matches_case_format` never claims
828/// them; this is the same rule stated over the contingency tokens.
829fn retained_contingency_source(module: &PioModule<PioValue>, format: &str) -> Option<Vec<u8>> {
830    let kind = contingency_file_of_value(module.value())?;
831    if crate::contingency_file_of_token(format)? != kind {
832        return None;
833    }
834    let source = module.source()?;
835    if source.acquired_buffers().len() != 1 {
836        return None;
837    }
838    if crate::contingency_file_of_token(source.format()?.as_str())? != kind {
839        return None;
840    }
841    Some(source.primary_buffer().ok()?.bytes().to_vec())
842}
843
844/// Write one PSS/E contingency analysis file as canonical text. Each of the
845/// three files is its own target: a `.con` written as `psse-sub` would state
846/// a different grammar, so only the token naming this value's own file is
847/// accepted.
848fn emit_contingency_file(
849    value: &PioValue,
850    kind: crate::ContingencyFile,
851    format: &str,
852    destination: Destination,
853) -> Result<EmitResult, Error> {
854    if crate::contingency_file_of_token(format) != Some(kind) {
855        return Err(if crate::contingency_file_of_token(format).is_some() {
856            Error::new(
857                &codes::REQUEST_EMIT_UNSUPPORTED_VALUE_TYPE,
858                format!(
859                    "{format} names another PSS/E contingency analysis file, not {}; write this one as `{}`",
860                    kind.type_name(),
861                    kind.token()
862                ),
863            )
864        } else if known_format_name(format) {
865            Error::new(
866                &codes::REQUEST_EMIT_UNSUPPORTED_VALUE_TYPE,
867                format!(
868                    "{format} states a grid case, not {}; write this file as `{}`",
869                    kind.type_name(),
870                    kind.token()
871                ),
872            )
873        } else {
874            unknown_format(format)
875        });
876    }
877    let text = match value {
878        PioValue::ContingencySet(set) => set.to_con(),
879        PioValue::SubsystemSet(set) => set.to_sub(),
880        PioValue::MonitoredSet(set) => set.to_mon(),
881        _ => unreachable!("the value variant selected the file kind"),
882    };
883    let artifact = powerio_core::MemoryArtifact::new(
884        powerio_core::ArtifactPath::new(kind.artifact_name())
885            .expect("static name is a valid artifact path"),
886        text.into_bytes(),
887    );
888    destination.__commit_artifacts(
889        false,
890        powerio_core::Fidelity::Canonical,
891        vec![artifact],
892        Vec::new(),
893    )
894}
895
896fn balanced_calculation_network(value: &PioValue) -> Option<&BalancedNetwork> {
897    match value {
898        PioValue::DcPfInstance(instance) => Some(instance.network()),
899        PioValue::AcPfInstance(instance) => Some(instance.network()),
900        PioValue::DcOpfInstance(instance) => Some(instance.network()),
901        PioValue::AcOpfInstance(instance) => Some(instance.network()),
902        PioValue::AcScucInstance(instance) => Some(instance.network()),
903        _ => None,
904    }
905}
906
907fn multiconductor_calculation_network(
908    value: &PioValue,
909) -> Option<&powerio_dist::MulticonductorNetwork> {
910    match value {
911        PioValue::McAcPfInstance(instance) => Some(instance.network()),
912        PioValue::McAcOpfInstance(instance) => Some(instance.network()),
913        // The source network, not the prepared formulation network: emitting
914        // the latter would silently drop lowered and omitted components.
915        PioValue::LinDist3FlowPfInstance(instance) => Some(instance.source_network()),
916        PioValue::LinDist3FlowOpfInstance(instance) => Some(instance.source_network()),
917        _ => None,
918    }
919}
920
921fn emit_network_or_calculation(
922    module: &PioModule<PioValue>,
923    format: &str,
924    destination: Destination,
925) -> Result<EmitResult, Error> {
926    if matches!(module.value(), PioValue::AcScucInstance(_)) && is_goc3(format) {
927        return Err(unsupported_type(module, format));
928    }
929    if let Some(network) = balanced_calculation_network(module.value()) {
930        return emit_balanced_network(
931            module,
932            network,
933            format,
934            destination,
935            false,
936            vec![calculation_data_omitted(module.value().type_name(), format)],
937        );
938    }
939    if let Some(network) = multiconductor_calculation_network(module.value()) {
940        return emit_multiconductor_network(
941            module,
942            network.clone(),
943            format,
944            destination,
945            false,
946            vec![calculation_data_omitted(module.value().type_name(), format)],
947        );
948    }
949    match &module.value() {
950        PioValue::BalancedNetwork(network) => {
951            emit_balanced_network(module, network, format, destination, true, Vec::new())
952        }
953        PioValue::MulticonductorNetwork(network) => emit_multiconductor_network(
954            module,
955            network.clone(),
956            format,
957            destination,
958            true,
959            Vec::new(),
960        ),
961        PioValue::BalancedOperatingPoint(point) => {
962            let (network, diagnostics) = network_with_balanced_operating_point(point, format);
963            emit_balanced_network(module, &network, format, destination, false, diagnostics)
964        }
965        PioValue::MulticonductorOperatingPoint(point) => {
966            let (network, diagnostics) = network_with_multiconductor_operating_point(point, format);
967            emit_multiconductor_network(module, network, format, destination, false, diagnostics)
968        }
969        _ => unreachable!("caller selected a value that is not a network or calculation"),
970    }
971}
972
973fn emit_balanced_solution_network(
974    module: &PioModule<PioValue>,
975    network: &BalancedNetwork,
976    format: &str,
977    destination: Destination,
978) -> Result<EmitResult, Error> {
979    emit_balanced_network(
980        module,
981        network,
982        format,
983        destination,
984        false,
985        vec![solution_data_omitted(module.value().type_name(), format)],
986    )
987}
988
989fn emit_multiconductor_solution_network(
990    module: &PioModule<PioValue>,
991    network: &powerio_dist::MulticonductorNetwork,
992    format: &str,
993    destination: Destination,
994) -> Result<EmitResult, Error> {
995    emit_multiconductor_network(
996        module,
997        network.clone(),
998        format,
999        destination,
1000        false,
1001        vec![solution_data_omitted(module.value().type_name(), format)],
1002    )
1003}
1004
1005fn emit_solution(
1006    module: &PioModule<PioValue>,
1007    format: &str,
1008    destination: Destination,
1009) -> Result<EmitResult, Error> {
1010    match &module.value() {
1011        PioValue::DcPfSolution(solution) => emit_balanced_solution_network(
1012            module,
1013            &network_with_dc_pf_solution(solution),
1014            format,
1015            destination,
1016        ),
1017        PioValue::AcPfSolution(solution) => emit_balanced_solution_network(
1018            module,
1019            &network_with_ac_pf_solution(solution),
1020            format,
1021            destination,
1022        ),
1023        PioValue::DcOpfSolution(solution) => emit_balanced_solution_network(
1024            module,
1025            &network_with_dc_opf_solution(solution),
1026            format,
1027            destination,
1028        ),
1029        PioValue::AcOpfSolution(solution) => emit_balanced_solution_network(
1030            module,
1031            &network_with_ac_opf_solution(solution),
1032            format,
1033            destination,
1034        ),
1035        PioValue::SocwrOpfSolution(solution) => emit_balanced_network(
1036            module,
1037            solution.network(),
1038            format,
1039            destination,
1040            false,
1041            vec![relaxation_data_omitted(format)],
1042        ),
1043        PioValue::McAcPfSolution(solution) => {
1044            emit_multiconductor_solution_network(module, solution.network(), format, destination)
1045        }
1046        PioValue::McAcOpfSolution(solution) => {
1047            emit_multiconductor_solution_network(module, solution.network(), format, destination)
1048        }
1049        PioValue::LinDist3FlowPfSolution(solution) => emit_multiconductor_solution_network(
1050            module,
1051            solution.instance().source_network(),
1052            format,
1053            destination,
1054        ),
1055        PioValue::LinDist3FlowOpfSolution(solution) => emit_multiconductor_solution_network(
1056            module,
1057            solution.instance().source_network(),
1058            format,
1059            destination,
1060        ),
1061        PioValue::AcScucSolution(solution) if is_goc3(format) => {
1062            emit_goc3_solution(solution, destination)
1063        }
1064        PioValue::AcScucSolution(solution) => emit_balanced_solution_network(
1065            module,
1066            solution.instance().network(),
1067            format,
1068            destination,
1069        ),
1070        _ => unreachable!("caller selected a value that is not a solution"),
1071    }
1072}
1073
1074fn emit_versioned_bmopf(
1075    module: &PioModule<PioValue>,
1076    version: &str,
1077    destination: Destination,
1078) -> Result<EmitResult, Error> {
1079    let format = format!("bmopf-json@{version}");
1080    let profile = match version {
1081        "0.1.0" => powerio_dist::BmopfSchemaVersion::Bmopf010,
1082        "0.2.0" => powerio_dist::BmopfSchemaVersion::Bmopf020,
1083        _ => return Err(unknown_format(&format)),
1084    };
1085    let (network, diagnostics) = match module.value() {
1086        PioValue::MulticonductorNetwork(network) => (network.clone(), Vec::new()),
1087        PioValue::McAcPfInstance(instance) => (
1088            instance.network().clone(),
1089            vec![calculation_data_omitted(
1090                module.value().type_name(),
1091                &format,
1092            )],
1093        ),
1094        PioValue::McAcOpfInstance(instance) => (
1095            instance.network().clone(),
1096            vec![calculation_data_omitted(
1097                module.value().type_name(),
1098                &format,
1099            )],
1100        ),
1101        PioValue::LinDist3FlowPfInstance(instance) => (
1102            instance.source_network().clone(),
1103            vec![calculation_data_omitted(
1104                module.value().type_name(),
1105                &format,
1106            )],
1107        ),
1108        PioValue::LinDist3FlowOpfInstance(instance) => (
1109            instance.source_network().clone(),
1110            vec![calculation_data_omitted(
1111                module.value().type_name(),
1112                &format,
1113            )],
1114        ),
1115        PioValue::MulticonductorOperatingPoint(point) => {
1116            network_with_multiconductor_operating_point(point, &format)
1117        }
1118        PioValue::McAcPfSolution(solution) => (
1119            solution.instance().network().clone(),
1120            vec![solution_data_omitted(module.value().type_name(), &format)],
1121        ),
1122        PioValue::McAcOpfSolution(solution) => (
1123            solution.instance().network().clone(),
1124            vec![solution_data_omitted(module.value().type_name(), &format)],
1125        ),
1126        PioValue::LinDist3FlowPfSolution(solution) => (
1127            solution.instance().source_network().clone(),
1128            vec![solution_data_omitted(module.value().type_name(), &format)],
1129        ),
1130        PioValue::LinDist3FlowOpfSolution(solution) => (
1131            solution.instance().source_network().clone(),
1132            vec![solution_data_omitted(module.value().type_name(), &format)],
1133        ),
1134        _ => return Err(unsupported_type(module, &format)),
1135    };
1136    let typed = typed_sibling(module, network)?.sever_source();
1137    let mut options = powerio_dist::EmitOptions::default();
1138    options.bmopf.schema_version = profile;
1139    powerio_dist::emit_with_options(
1140        &typed,
1141        powerio_dist::DistTargetFormat::BmopfJson,
1142        &options,
1143        destination,
1144    )
1145    .map(|result| result.__with_diagnostics(diagnostics))
1146}
1147
1148pub(crate) fn is_sincal_format(format: &str) -> bool {
1149    crate::resolve_format(format).is_some_and(|f| {
1150        matches!(
1151            f.token,
1152            "sincal" | "sincal-balanced" | "sincal-multiconductor"
1153        )
1154    })
1155}
1156
1157/// Binary retained-source echo is separate from fresh electrical writing.
1158#[cfg(feature = "sincal")]
1159fn echo_sincal(
1160    module: &PioModule<PioValue>,
1161    format: &str,
1162    destination: Destination,
1163) -> Result<EmitResult, Error> {
1164    if !matches!(
1165        module.value(),
1166        PioValue::BalancedNetwork(_) | PioValue::MulticonductorNetwork(_)
1167    ) {
1168        return Err(unsupported_type(module, "sincal"));
1169    }
1170    let target = crate::resolve_format(format).map(|f| f.token);
1171    if matches!(
1172        (target, module.value()),
1173        (Some("sincal-balanced"), PioValue::MulticonductorNetwork(_))
1174            | (Some("sincal-multiconductor"), PioValue::BalancedNetwork(_))
1175    ) {
1176        return Err(unsupported_type(module, format));
1177    }
1178    let source = module.source().filter(|source| source.format().is_some_and(|f| is_sincal_format(f.as_str())))
1179        .ok_or_else(|| Error::new(&powerio_tx::diagnostics::codes::EMIT_SINCAL_FRESH_UNSUPPORTED,
1180            "SINCAL currently supports unchanged retained-source echo only. Edited, constructed, or IR-restored networks require the separate experimental fresh writer."))?;
1181    let primary = source.primary_buffer()?;
1182    // deserialize retains the IR document as its acquired source. A caller
1183    // supplying a native format hint must not relabel those JSON bytes as a
1184    // SINCAL project and enable an invalid native echo.
1185    if !primary.bytes().starts_with(b"SQLite format 3\0")
1186        && !primary.bytes().starts_with(b"PK\x03\x04")
1187        && !primary.bytes().starts_with(b"\0\x01\0\0Standard Jet DB\0")
1188    {
1189        return Err(Error::new(
1190            &powerio_tx::diagnostics::codes::EMIT_SINCAL_FRESH_UNSUPPORTED,
1191            "The retained bytes are not a native SINCAL database or archive; source metadata alone cannot enable native echo.",
1192        ));
1193    }
1194    destination.__commit_artifacts(
1195        false,
1196        powerio_core::Fidelity::ExactSameFormat,
1197        vec![powerio_core::MemoryArtifact::new(
1198            powerio_core::ArtifactPath::new("case.sinx")?,
1199            primary.bytes().to_vec(),
1200        )],
1201        Vec::new(),
1202    )
1203}
1204
1205fn emit_egret_series(
1206    module: &PioModule<PioValue>,
1207    series: &crate::PioTimeSeries,
1208    format: &str,
1209    destination: Destination,
1210) -> Result<EmitResult, Error> {
1211    let mut diagnostics = Vec::new();
1212    let networks = series
1213        .iter()
1214        .map(|(_, value)| match value {
1215            PioValue::BalancedNetwork(network) => Ok(network.clone()),
1216            PioValue::BalancedOperatingPoint(point) => {
1217                let (network, findings) = network_with_balanced_operating_point(point, format);
1218                for finding in findings {
1219                    if !diagnostics.contains(&finding) {
1220                        diagnostics.push(finding);
1221                    }
1222                }
1223                Ok(network)
1224            }
1225            _ => Err(unsupported_type(module, format)),
1226        })
1227        .collect::<Result<Vec<_>, Error>>()?;
1228    let typed = powerio_core::TimeSeries::new(series.time_points().to_vec(), networks)?;
1229    powerio_tx::format::__emit_egret_time_series(&typed, destination)
1230        .map(|result| result.__with_diagnostics(diagnostics))
1231}
1232
1233fn emit_dynamic(
1234    module: &PioModule<PioValue>,
1235    format: &str,
1236    destination: Destination,
1237) -> Result<EmitResult, Error> {
1238    if is_sincal_format(format) {
1239        #[cfg(feature = "sincal")]
1240        return echo_sincal(module, format, destination);
1241        #[cfg(not(feature = "sincal"))]
1242        return Err(crate::sincal_feature_disabled());
1243    }
1244    if let Some(version) = format.strip_prefix("bmopf-json@") {
1245        return emit_versioned_bmopf(module, version, destination);
1246    }
1247    if let Some(artifacts) = echo_retained_directory(module, format)? {
1248        return destination.__commit_artifacts(
1249            true,
1250            powerio_core::Fidelity::ExactSameFormat,
1251            artifacts,
1252            Vec::new(),
1253        );
1254    }
1255
1256    // A parse-only calculation format can still reproduce an unchanged
1257    // retained source exactly. A derived module has no retained source, so a
1258    // solved result never falls through to stale input bytes here.
1259    if !matches!(
1260        &module.value(),
1261        PioValue::BalancedNetwork(_) | PioValue::MulticonductorNetwork(_)
1262    ) && let Some(bytes) = echo_retained_source(module, format)
1263    {
1264        let artifact = powerio_core::MemoryArtifact::new(
1265            powerio_core::ArtifactPath::new("case").expect("static name is a valid artifact path"),
1266            bytes,
1267        );
1268        return destination.__commit_artifacts(
1269            false,
1270            powerio_core::Fidelity::ExactSameFormat,
1271            vec![artifact],
1272            Vec::new(),
1273        );
1274    }
1275
1276    if let PioValue::GeoLayer(layer) = &module.value() {
1277        return emit_geo_layer(layer, format, destination);
1278    }
1279
1280    if let Some(kind) = contingency_file_of_value(module.value()) {
1281        if let Some(bytes) = retained_contingency_source(module, format) {
1282            let artifact = powerio_core::MemoryArtifact::new(
1283                powerio_core::ArtifactPath::new(kind.artifact_name())
1284                    .expect("static name is a valid artifact path"),
1285                bytes,
1286            );
1287            return destination.__commit_artifacts(
1288                false,
1289                powerio_core::Fidelity::ExactSameFormat,
1290                vec![artifact],
1291                Vec::new(),
1292            );
1293        }
1294        return emit_contingency_file(module.value(), kind, format, destination);
1295    }
1296
1297    match &module.value() {
1298        PioValue::TimeSeries(series)
1299            if powerio_tx::format::parse_target_format(format)
1300                == Some(powerio_tx::TargetFormat::EgretJson) =>
1301        {
1302            emit_egret_series(module, series, format, destination)
1303        }
1304        PioValue::BalancedNetwork(_)
1305        | PioValue::MulticonductorNetwork(_)
1306        | PioValue::BalancedOperatingPoint(_)
1307        | PioValue::MulticonductorOperatingPoint(_)
1308        | PioValue::DcPfInstance(_)
1309        | PioValue::AcPfInstance(_)
1310        | PioValue::DcOpfInstance(_)
1311        | PioValue::AcOpfInstance(_)
1312        | PioValue::McAcPfInstance(_)
1313        | PioValue::McAcOpfInstance(_)
1314        | PioValue::LinDist3FlowPfInstance(_)
1315        | PioValue::LinDist3FlowOpfInstance(_)
1316        | PioValue::AcScucInstance(_) => emit_network_or_calculation(module, format, destination),
1317        PioValue::DcPfSolution(_)
1318        | PioValue::AcPfSolution(_)
1319        | PioValue::DcOpfSolution(_)
1320        | PioValue::AcOpfSolution(_)
1321        | PioValue::SocwrOpfSolution(_)
1322        | PioValue::McAcPfSolution(_)
1323        | PioValue::McAcOpfSolution(_)
1324        | PioValue::LinDist3FlowPfSolution(_)
1325        | PioValue::LinDist3FlowOpfSolution(_)
1326        | PioValue::AcScucSolution(_) => emit_solution(module, format, destination),
1327        _ => {
1328            if known_format_name(format) {
1329                Err(unsupported_type(module, format))
1330            } else {
1331                Err(unknown_format(format))
1332            }
1333        }
1334    }
1335}
1336
1337/// True when `format` is a name some family recognizes, used to tell "wrong
1338/// kind for this format" apart from "no such format".
1339fn known_format_name(format: &str) -> bool {
1340    crate::resolve_format(format).is_some()
1341}
1342
1343#[cfg(test)]
1344mod tests {
1345    use super::*;
1346    use powerio_core::{
1347        DiagnosticCode, DiagnosticSeverity, HistoryEntry, HistoryId, HistoryKind, Producer,
1348        SourceDescriptor, SourceId, SourceMapEntry, SourceRelation, SourceSpan,
1349    };
1350    use powerio_tx::{BalancedNetwork, Bus, BusId, BusType};
1351
1352    #[test]
1353    fn a_typed_writer_sibling_preserves_every_common_record() {
1354        let network = BalancedNetwork::in_memory(
1355            "records",
1356            100.0,
1357            vec![Bus::new(BusId(1), BusType::Ref, 230.0)],
1358            vec![],
1359        );
1360        let source = powerio_core::Source::from_memory("case.m", b"case bytes".to_vec())
1361            .unwrap()
1362            .with_format(powerio_core::FormatId::new("matpower").unwrap());
1363        let source_id = SourceId::new("source-1").unwrap();
1364        let mut module = PioModule::new(PioValue::BalancedNetwork(network.clone()))
1365            .with_producer(Producer::new("records-test", "1").unwrap())
1366            .with_source(source);
1367        module
1368            .add_source_descriptor(SourceDescriptor::new(source_id.clone(), "case.m", 10).unwrap())
1369            .unwrap();
1370        module
1371            .add_source_map_entry(
1372                SourceMapEntry::new(
1373                    "/buses/0",
1374                    SourceRelation::Exact,
1375                    vec![SourceSpan::new(source_id, 0, 4).unwrap()],
1376                )
1377                .unwrap(),
1378            )
1379            .unwrap();
1380        module
1381            .add_diagnostic(powerio_core::Diagnostic::new(
1382                DiagnosticCode::new("READ.TEST.RECORD").unwrap(),
1383                DiagnosticSeverity::Remark,
1384                "record carried to the family writer",
1385            ))
1386            .unwrap();
1387        module
1388            .add_history_entry(
1389                HistoryEntry::new(
1390                    HistoryId::new("history-1").unwrap(),
1391                    HistoryKind::Parse,
1392                    "parse",
1393                )
1394                .unwrap(),
1395            )
1396            .unwrap();
1397        module
1398            .insert_extension("test.writer", serde_json::json!({"kept": true}))
1399            .unwrap();
1400
1401        let sibling = typed_sibling(&module, network).unwrap();
1402        assert_eq!(sibling.producer(), module.producer());
1403        assert_eq!(sibling.sources(), module.sources());
1404        assert_eq!(sibling.source_map(), module.source_map());
1405        assert_eq!(sibling.diagnostics(), module.diagnostics());
1406        assert_eq!(sibling.history(), module.history());
1407        assert_eq!(sibling.extensions(), module.extensions());
1408        let sibling_source = sibling.source().unwrap();
1409        assert_eq!(sibling_source.name(), "case.m");
1410        assert_eq!(
1411            sibling_source.primary_buffer().unwrap().bytes(),
1412            b"case bytes"
1413        );
1414    }
1415}