Skip to main content

powerio_tx/format/
opfdata.rs

1//! Read DeepMind OPFData JSON documents into [`BalancedNetwork`].
2//!
3//! OPFData stores one solved AC-OPF problem per JSON file. The `grid` section
4//! carries the problem inputs, `solution` carries the solved operating point,
5//! and `metadata.objective` carries the solved quadratic objective. This reader
6//! maps the solved snapshot into the balanced MW and degree model and retains
7//! the original JSON for an exact write back to the same format.
8//!
9//! The published FullTop and N-1 datasets use the same schema at every grid
10//! size. Element counts and topology come exclusively from each document; N-1
11//! outages therefore need no case-specific handling.
12
13use std::collections::BTreeMap;
14
15use serde::Deserialize;
16
17use crate::diagnostics::{Diagnostics, codes};
18use crate::network::{
19    BalancedNetwork, Branch, BranchCharging, BranchSolution, Bus, BusId, BusType, GenCost,
20    Generator, Load, Shunt, SourceFormat,
21};
22use crate::normalize::{RAD_TO_DEG, cost_from_pu};
23use crate::{Error, Result};
24
25const FMT: &str = "DeepMind OPFData JSON";
26type ExtraFields = BTreeMap<String, serde_json::Value>;
27
28#[derive(Debug, Deserialize)]
29struct Document {
30    grid: Grid,
31    solution: Solution,
32    metadata: Metadata,
33    #[serde(flatten)]
34    extra: ExtraFields,
35}
36
37#[derive(Debug, Deserialize)]
38struct Grid {
39    nodes: GridNodes,
40    edges: GridEdges,
41    context: Vec<Vec<Vec<f64>>>,
42    #[serde(flatten)]
43    extra: ExtraFields,
44}
45
46#[derive(Debug, Deserialize)]
47struct GridNodes {
48    bus: Vec<BusRow>,
49    generator: Vec<GeneratorRow>,
50    load: Vec<LoadRow>,
51    shunt: Vec<ShuntRow>,
52    #[serde(flatten)]
53    extra: ExtraFields,
54}
55
56#[derive(Debug, Deserialize)]
57struct GridEdges {
58    ac_line: AcLineEdges,
59    transformer: TransformerEdges,
60    generator_link: LinkEdges,
61    load_link: LinkEdges,
62    shunt_link: LinkEdges,
63    #[serde(flatten)]
64    extra: ExtraFields,
65}
66
67#[derive(Debug, Deserialize)]
68struct AcLineEdges {
69    senders: Vec<usize>,
70    receivers: Vec<usize>,
71    features: Vec<AcLineRow>,
72    #[serde(flatten)]
73    extra: ExtraFields,
74}
75
76#[derive(Debug, Deserialize)]
77struct TransformerEdges {
78    senders: Vec<usize>,
79    receivers: Vec<usize>,
80    features: Vec<TransformerRow>,
81    #[serde(flatten)]
82    extra: ExtraFields,
83}
84
85#[derive(Debug, Deserialize)]
86struct LinkEdges {
87    senders: Vec<usize>,
88    receivers: Vec<usize>,
89    #[serde(flatten)]
90    extra: ExtraFields,
91}
92
93#[derive(Debug, Deserialize)]
94struct Solution {
95    nodes: SolutionNodes,
96    edges: SolutionEdges,
97    #[serde(flatten)]
98    extra: ExtraFields,
99}
100
101#[derive(Debug, Deserialize)]
102struct SolutionNodes {
103    bus: Vec<BusSolutionRow>,
104    generator: Vec<GeneratorSolutionRow>,
105    #[serde(flatten)]
106    extra: ExtraFields,
107}
108
109#[derive(Debug, Deserialize)]
110struct SolutionEdges {
111    ac_line: SolutionBranchEdges,
112    transformer: SolutionBranchEdges,
113    #[serde(flatten)]
114    extra: ExtraFields,
115}
116
117#[derive(Debug, Deserialize)]
118struct SolutionBranchEdges {
119    senders: Vec<usize>,
120    receivers: Vec<usize>,
121    features: Vec<BranchSolutionRow>,
122    #[serde(flatten)]
123    extra: ExtraFields,
124}
125
126#[derive(Debug, Deserialize)]
127struct Metadata {
128    objective: f64,
129    #[serde(flatten)]
130    extra: ExtraFields,
131}
132
133// These transparent row types make the documented feature widths part of what
134// serde enforces, while keeping column meaning out of the mapping code's magic
135// indices. A row with too few or too many values fails deserialization.
136#[derive(Debug, Deserialize)]
137#[serde(transparent)]
138struct BusRow([f64; 4]);
139
140impl BusRow {
141    fn base_kv(&self) -> f64 {
142        self.0[0]
143    }
144
145    fn bus_type(&self) -> f64 {
146        self.0[1]
147    }
148
149    fn vmin(&self) -> f64 {
150        self.0[2]
151    }
152
153    fn vmax(&self) -> f64 {
154        self.0[3]
155    }
156}
157
158#[derive(Debug, Deserialize)]
159#[serde(transparent)]
160struct GeneratorRow([f64; 11]);
161
162impl GeneratorRow {
163    fn mbase(&self) -> f64 {
164        self.0[0]
165    }
166
167    fn pmin(&self) -> f64 {
168        self.0[2]
169    }
170
171    fn pmax(&self) -> f64 {
172        self.0[3]
173    }
174
175    fn qmin(&self) -> f64 {
176        self.0[5]
177    }
178
179    fn qmax(&self) -> f64 {
180        self.0[6]
181    }
182
183    fn initial_pg(&self) -> f64 {
184        self.0[1]
185    }
186
187    fn initial_qg(&self) -> f64 {
188        self.0[4]
189    }
190
191    fn initial_vg(&self) -> f64 {
192        self.0[7]
193    }
194
195    fn cost_coefficients(&self) -> &[f64] {
196        &self.0[8..11]
197    }
198
199    fn objective_at(&self, pg: f64) -> f64 {
200        self.0[8] * pg * pg + self.0[9] * pg + self.0[10]
201    }
202}
203
204#[derive(Debug, Deserialize)]
205#[serde(transparent)]
206struct LoadRow([f64; 2]);
207
208impl LoadRow {
209    fn pd(&self) -> f64 {
210        self.0[0]
211    }
212
213    fn qd(&self) -> f64 {
214        self.0[1]
215    }
216}
217
218#[derive(Debug, Deserialize)]
219#[serde(transparent)]
220struct ShuntRow([f64; 2]);
221
222impl ShuntRow {
223    fn bs(&self) -> f64 {
224        self.0[0]
225    }
226
227    fn gs(&self) -> f64 {
228        self.0[1]
229    }
230}
231
232#[derive(Debug, Deserialize)]
233#[serde(transparent)]
234struct AcLineRow([f64; 9]);
235
236impl AcLineRow {
237    fn angmin(&self) -> f64 {
238        self.0[0]
239    }
240
241    fn angmax(&self) -> f64 {
242        self.0[1]
243    }
244
245    fn b_fr(&self) -> f64 {
246        self.0[2]
247    }
248
249    fn b_to(&self) -> f64 {
250        self.0[3]
251    }
252
253    fn r(&self) -> f64 {
254        self.0[4]
255    }
256
257    fn x(&self) -> f64 {
258        self.0[5]
259    }
260
261    fn rate_a(&self) -> f64 {
262        self.0[6]
263    }
264
265    fn rate_b(&self) -> f64 {
266        self.0[7]
267    }
268
269    fn rate_c(&self) -> f64 {
270        self.0[8]
271    }
272}
273
274#[derive(Debug, Deserialize)]
275#[serde(transparent)]
276struct TransformerRow([f64; 11]);
277
278impl TransformerRow {
279    fn angmin(&self) -> f64 {
280        self.0[0]
281    }
282
283    fn angmax(&self) -> f64 {
284        self.0[1]
285    }
286
287    fn r(&self) -> f64 {
288        self.0[2]
289    }
290
291    fn x(&self) -> f64 {
292        self.0[3]
293    }
294
295    fn rate_a(&self) -> f64 {
296        self.0[4]
297    }
298
299    fn rate_b(&self) -> f64 {
300        self.0[5]
301    }
302
303    fn rate_c(&self) -> f64 {
304        self.0[6]
305    }
306
307    fn tap(&self) -> f64 {
308        self.0[7]
309    }
310
311    fn shift(&self) -> f64 {
312        self.0[8]
313    }
314
315    fn b_fr(&self) -> f64 {
316        self.0[9]
317    }
318
319    fn b_to(&self) -> f64 {
320        self.0[10]
321    }
322}
323
324#[derive(Debug, Deserialize)]
325#[serde(transparent)]
326struct BusSolutionRow([f64; 2]);
327
328impl BusSolutionRow {
329    fn va(&self) -> f64 {
330        self.0[0]
331    }
332
333    fn vm(&self) -> f64 {
334        self.0[1]
335    }
336}
337
338#[derive(Debug, Deserialize)]
339#[serde(transparent)]
340struct GeneratorSolutionRow([f64; 2]);
341
342impl GeneratorSolutionRow {
343    fn pg(&self) -> f64 {
344        self.0[0]
345    }
346
347    fn qg(&self) -> f64 {
348        self.0[1]
349    }
350}
351
352#[derive(Debug, Deserialize)]
353#[serde(transparent)]
354struct BranchSolutionRow([f64; 4]);
355
356impl BranchSolutionRow {
357    fn to_network(&self, base_mva: f64) -> BranchSolution {
358        // OPFData orders solved flows [pt, qt, pf, qf].
359        BranchSolution::new(
360            self.0[2] * base_mva,
361            self.0[3] * base_mva,
362            self.0[0] * base_mva,
363            self.0[1] * base_mva,
364        )
365    }
366}
367
368fn bad(message: impl Into<String>) -> Error {
369    Error::FormatRead {
370        format: FMT,
371        message: message.into(),
372    }
373}
374
375fn base_mva(context: &[Vec<Vec<f64>>]) -> Result<f64> {
376    if context.len() != 1 || context[0].len() != 1 || context[0][0].len() != 1 {
377        return Err(bad(format!(
378            "`grid.context` must have shape [1, 1, 1], got outer lengths [{}, {}, {}]",
379            context.len(),
380            context.first().map_or(0, Vec::len),
381            context
382                .first()
383                .and_then(|row| row.first())
384                .map_or(0, Vec::len)
385        )));
386    }
387    let base = context[0][0][0];
388    if !base.is_finite() || base <= 0.0 {
389        return Err(bad(format!(
390            "`grid.context` baseMVA must be positive and finite, got {base}"
391        )));
392    }
393    Ok(base)
394}
395
396fn equal_len(
397    what: &str,
398    left_name: &str,
399    left: usize,
400    right_name: &str,
401    right: usize,
402) -> Result<()> {
403    if left != right {
404        return Err(bad(format!(
405            "`{what}` length mismatch: `{left_name}` has {left} rows but `{right_name}` has {right}"
406        )));
407    }
408    Ok(())
409}
410
411fn validate_edge_arrays(
412    what: &str,
413    senders: &[usize],
414    receivers: &[usize],
415    features: usize,
416    buses: usize,
417) -> Result<()> {
418    equal_len(what, "senders", senders.len(), "receivers", receivers.len())?;
419    equal_len(what, "senders", senders.len(), "features", features)?;
420    for (index, (&from, &to)) in senders.iter().zip(receivers).enumerate() {
421        if from >= buses || to >= buses {
422            return Err(bad(format!(
423                "`{what}` row {index} references bus indices ({from}, {to}) but there are {buses} buses"
424            )));
425        }
426    }
427    Ok(())
428}
429
430fn validate_solution_edges(
431    what: &str,
432    grid_senders: &[usize],
433    grid_receivers: &[usize],
434    solution: &SolutionBranchEdges,
435    buses: usize,
436) -> Result<()> {
437    validate_edge_arrays(
438        what,
439        &solution.senders,
440        &solution.receivers,
441        solution.features.len(),
442        buses,
443    )?;
444    equal_len(
445        what,
446        "grid edges",
447        grid_senders.len(),
448        "solution edges",
449        solution.senders.len(),
450    )?;
451    for (index, ((&grid_from, &grid_to), (&sol_from, &sol_to))) in grid_senders
452        .iter()
453        .zip(grid_receivers)
454        .zip(solution.senders.iter().zip(&solution.receivers))
455        .enumerate()
456    {
457        if (grid_from, grid_to) != (sol_from, sol_to) {
458            return Err(bad(format!(
459                "`{what}` row {index} topology differs between grid ({grid_from}, {grid_to}) and solution ({sol_from}, {sol_to})"
460            )));
461        }
462    }
463    Ok(())
464}
465
466fn linked_buses(what: &str, link: &LinkEdges, rows: usize, buses: usize) -> Result<Vec<BusId>> {
467    equal_len(
468        what,
469        "senders",
470        link.senders.len(),
471        "receivers",
472        link.receivers.len(),
473    )?;
474    equal_len(what, "links", link.senders.len(), "node rows", rows)?;
475
476    let mut mapped = vec![None; rows];
477    for (index, (&sender, &receiver)) in link.senders.iter().zip(&link.receivers).enumerate() {
478        if sender >= rows {
479            return Err(bad(format!(
480                "`{what}` row {index} references node index {sender} but there are {rows} node rows"
481            )));
482        }
483        if receiver >= buses {
484            return Err(bad(format!(
485                "`{what}` row {index} references bus index {receiver} but there are {buses} buses"
486            )));
487        }
488        if mapped[sender].replace(BusId(receiver + 1)).is_some() {
489            return Err(bad(format!(
490                "`{what}` contains more than one link for node index {sender}"
491            )));
492        }
493    }
494
495    mapped
496        .into_iter()
497        .enumerate()
498        .map(|(index, bus)| {
499            bus.ok_or_else(|| bad(format!("`{what}` has no link for node index {index}")))
500        })
501        .collect()
502}
503
504fn bus_type(value: f64, row: usize) -> Result<BusType> {
505    match value {
506        1.0 => Ok(BusType::Pq),
507        2.0 => Ok(BusType::Pv),
508        3.0 => Ok(BusType::Ref),
509        4.0 => Ok(BusType::Isolated),
510        _ => Err(bad(format!(
511            "`grid.nodes.bus` row {row} has invalid bus type {value}; expected 1, 2, 3, or 4"
512        ))),
513    }
514}
515
516fn warn_extra_fields(path: &str, extra: &ExtraFields, warnings: &mut Diagnostics) {
517    if extra.is_empty() {
518        return;
519    }
520    let fields = extra
521        .keys()
522        .map(|field| {
523            if path.is_empty() {
524                format!("`{field}`")
525            } else {
526                format!("`{path}.{field}`")
527            }
528        })
529        .collect::<Vec<_>>()
530        .join(", ");
531    warnings.push(&codes::READ_OPFDATA_FIELD_DROPPED, format!(
532        "OPFData fields {fields} are not part of the published schema; they remain in the retained source but are not represented in the canonical snapshot"
533    ));
534}
535
536fn warn_document_extras(document: &Document, warnings: &mut Diagnostics) {
537    warn_extra_fields("", &document.extra, warnings);
538    warn_extra_fields("grid", &document.grid.extra, warnings);
539    warn_extra_fields("grid.nodes", &document.grid.nodes.extra, warnings);
540    warn_extra_fields("grid.edges", &document.grid.edges.extra, warnings);
541    warn_extra_fields(
542        "grid.edges.ac_line",
543        &document.grid.edges.ac_line.extra,
544        warnings,
545    );
546    warn_extra_fields(
547        "grid.edges.transformer",
548        &document.grid.edges.transformer.extra,
549        warnings,
550    );
551    warn_extra_fields(
552        "grid.edges.generator_link",
553        &document.grid.edges.generator_link.extra,
554        warnings,
555    );
556    warn_extra_fields(
557        "grid.edges.load_link",
558        &document.grid.edges.load_link.extra,
559        warnings,
560    );
561    warn_extra_fields(
562        "grid.edges.shunt_link",
563        &document.grid.edges.shunt_link.extra,
564        warnings,
565    );
566    warn_extra_fields("solution", &document.solution.extra, warnings);
567    warn_extra_fields("solution.nodes", &document.solution.nodes.extra, warnings);
568    warn_extra_fields("solution.edges", &document.solution.edges.extra, warnings);
569    warn_extra_fields(
570        "solution.edges.ac_line",
571        &document.solution.edges.ac_line.extra,
572        warnings,
573    );
574    warn_extra_fields(
575        "solution.edges.transformer",
576        &document.solution.edges.transformer.extra,
577        warnings,
578    );
579    warn_extra_fields("metadata", &document.metadata.extra, warnings);
580}
581
582fn objective_warning(document: &Document) -> Option<String> {
583    let calculated = document
584        .grid
585        .nodes
586        .generator
587        .iter()
588        .zip(&document.solution.nodes.generator)
589        .map(|(generator, solution)| generator.objective_at(solution.pg()))
590        .sum::<f64>();
591    let stated = document.metadata.objective;
592    let tolerance = 1.0e-8 * stated.abs().max(calculated.abs()).max(1.0);
593    (!calculated.is_finite() || !stated.is_finite() || (calculated - stated).abs() > tolerance)
594        .then(|| {
595            format!(
596                "`metadata.objective` is {stated}, but the solved generator dispatch and costs evaluate to {calculated}"
597            )
598        })
599}
600
601struct NodeLinks {
602    generators: Vec<BusId>,
603    loads: Vec<BusId>,
604    shunts: Vec<BusId>,
605}
606
607fn validate_document(document: &Document, bus_count: usize) -> Result<NodeLinks> {
608    equal_len(
609        "nodes.bus",
610        "grid rows",
611        bus_count,
612        "solution rows",
613        document.solution.nodes.bus.len(),
614    )?;
615    equal_len(
616        "nodes.generator",
617        "grid rows",
618        document.grid.nodes.generator.len(),
619        "solution rows",
620        document.solution.nodes.generator.len(),
621    )?;
622
623    validate_edge_arrays(
624        "grid.edges.ac_line",
625        &document.grid.edges.ac_line.senders,
626        &document.grid.edges.ac_line.receivers,
627        document.grid.edges.ac_line.features.len(),
628        bus_count,
629    )?;
630    validate_edge_arrays(
631        "grid.edges.transformer",
632        &document.grid.edges.transformer.senders,
633        &document.grid.edges.transformer.receivers,
634        document.grid.edges.transformer.features.len(),
635        bus_count,
636    )?;
637    validate_solution_edges(
638        "solution.edges.ac_line",
639        &document.grid.edges.ac_line.senders,
640        &document.grid.edges.ac_line.receivers,
641        &document.solution.edges.ac_line,
642        bus_count,
643    )?;
644    validate_solution_edges(
645        "solution.edges.transformer",
646        &document.grid.edges.transformer.senders,
647        &document.grid.edges.transformer.receivers,
648        &document.solution.edges.transformer,
649        bus_count,
650    )?;
651
652    Ok(NodeLinks {
653        generators: linked_buses(
654            "grid.edges.generator_link",
655            &document.grid.edges.generator_link,
656            document.grid.nodes.generator.len(),
657            bus_count,
658        )?,
659        loads: linked_buses(
660            "grid.edges.load_link",
661            &document.grid.edges.load_link,
662            document.grid.nodes.load.len(),
663            bus_count,
664        )?,
665        shunts: linked_buses(
666            "grid.edges.shunt_link",
667            &document.grid.edges.shunt_link,
668            document.grid.nodes.shunt.len(),
669            bus_count,
670        )?,
671    })
672}
673
674/// The solved columns and stated objective of one OPFData document, in
675/// network units (per unit voltages, degrees, MW, MVAr), each vector in its
676/// network table's order. OPFData explicitly represents a solved AC OPF, so
677/// these columns exist for every valid document; the solver initial `pg`,
678/// `qg`, and `vg` the `grid` section supplies ride along for solve metadata.
679#[derive(Debug, Clone)]
680pub struct OpfDataSolution {
681    /// Solved bus voltage magnitude, per unit, bus order.
682    pub bus_voltage_magnitude: Vec<f64>,
683    /// Solved bus voltage angle, degrees, bus order.
684    pub bus_voltage_angle: Vec<f64>,
685    /// Solved from-side active flow, MW, branch order.
686    pub branch_from_active_flow: Vec<f64>,
687    /// Solved from-side reactive flow, MVAr, branch order.
688    pub branch_from_reactive_flow: Vec<f64>,
689    /// Solved to-side active flow, MW, branch order.
690    pub branch_to_active_flow: Vec<f64>,
691    /// Solved to-side reactive flow, MVAr, branch order.
692    pub branch_to_reactive_flow: Vec<f64>,
693    /// Solved generator active dispatch, MW, generator order.
694    pub generator_active_power: Vec<f64>,
695    /// Solved generator reactive dispatch, MVAr, generator order.
696    pub generator_reactive_power: Vec<f64>,
697    /// Solver initial generator active power, MW, generator order.
698    pub initial_generator_active_power: Vec<f64>,
699    /// Solver initial generator reactive power, MVAr, generator order.
700    pub initial_generator_reactive_power: Vec<f64>,
701    /// Solver initial generator voltage setpoint, per unit, generator order.
702    pub initial_generator_voltage_setpoint: Vec<f64>,
703    /// The stated solved objective, from `metadata.objective`.
704    pub objective: f64,
705}
706
707/// Parse one OPFData document into the solved network snapshot plus the
708/// solved columns a typed solution assembly reads directly, without scraping
709/// them back off the network.
710///
711/// # Errors
712/// An invalid document: a schema mismatch, a bad base MVA, or inconsistent
713/// table links.
714pub fn parse_opfdata_json(
715    content: &str,
716) -> Result<(BalancedNetwork, OpfDataSolution, Vec<super::Diagnostic>)> {
717    let mut warnings = Diagnostics::new();
718    let (network, solution) = parse_opfdata_document(content, None, &mut warnings)?;
719    Ok((network, solution, warnings.into_records()))
720}
721
722pub(crate) fn parse_opfdata_source(
723    source: &str,
724    name_hint: Option<&str>,
725    warnings: &mut Diagnostics,
726) -> Result<BalancedNetwork> {
727    parse_opfdata_document(source, name_hint, warnings).map(|(network, _)| network)
728}
729
730#[allow(clippy::too_many_lines)]
731fn parse_opfdata_document(
732    source: &str,
733    name_hint: Option<&str>,
734    warnings: &mut Diagnostics,
735) -> Result<(BalancedNetwork, OpfDataSolution)> {
736    let document: Document = serde_json::from_str(source)
737        .map_err(|error| bad(format!("invalid OPFData schema: {error}")))?;
738    let base = base_mva(&document.grid.context)?;
739    let bus_count = document.grid.nodes.bus.len();
740    let links = validate_document(&document, bus_count)?;
741    warn_document_extras(&document, warnings);
742
743    let buses = document
744        .grid
745        .nodes
746        .bus
747        .iter()
748        .zip(&document.solution.nodes.bus)
749        .enumerate()
750        .map(|(index, (grid, solution))| {
751            let mut bus = Bus::new(
752                BusId(index + 1),
753                bus_type(grid.bus_type(), index)?,
754                grid.base_kv(),
755            );
756            bus.vmin = grid.vmin();
757            bus.vmax = grid.vmax();
758            bus.va = solution.va() * RAD_TO_DEG;
759            bus.vm = solution.vm();
760            Ok(bus)
761        })
762        .collect::<Result<Vec<_>>>()?;
763
764    let generators: Vec<Generator> = document
765        .grid
766        .nodes
767        .generator
768        .iter()
769        .zip(&document.solution.nodes.generator)
770        .zip(links.generators)
771        .map(|((grid, solution), bus)| {
772            let mut generator = Generator::new(bus);
773            generator.mbase = grid.mbase();
774            generator.pg = solution.pg() * base;
775            generator.pmin = grid.pmin() * base;
776            generator.pmax = grid.pmax() * base;
777            generator.qg = solution.qg() * base;
778            generator.qmin = grid.qmin() * base;
779            generator.qmax = grid.qmax() * base;
780            generator.vg = buses[bus.0 - 1].vm;
781            generator.cost = Some(GenCost::new(
782                2,
783                0.0,
784                0.0,
785                cost_from_pu(grid.cost_coefficients(), 2, base),
786            ));
787            generator
788        })
789        .collect();
790
791    let loads = document
792        .grid
793        .nodes
794        .load
795        .iter()
796        .zip(links.loads)
797        .map(|(row, bus)| Load::new(bus, row.pd() * base, row.qd() * base))
798        .collect();
799
800    let shunts = document
801        .grid
802        .nodes
803        .shunt
804        .iter()
805        .zip(links.shunts)
806        .map(|(row, bus)| Shunt::new(bus, row.gs() * base, row.bs() * base))
807        .collect();
808
809    let mut branches = Vec::with_capacity(
810        document.grid.edges.ac_line.features.len() + document.grid.edges.transformer.features.len(),
811    );
812    for (((&from, &to), grid), solution) in document
813        .grid
814        .edges
815        .ac_line
816        .senders
817        .iter()
818        .zip(&document.grid.edges.ac_line.receivers)
819        .zip(&document.grid.edges.ac_line.features)
820        .zip(&document.solution.edges.ac_line.features)
821    {
822        let mut branch = Branch::new(BusId(from + 1), BusId(to + 1), grid.r(), grid.x());
823        branch.b = grid.b_fr() + grid.b_to();
824        branch.charging = Some(BranchCharging::new(0.0, grid.b_fr(), 0.0, grid.b_to()));
825        branch.rate_a = grid.rate_a() * base;
826        branch.rate_b = grid.rate_b() * base;
827        branch.rate_c = grid.rate_c() * base;
828        branch.angmin = grid.angmin() * RAD_TO_DEG;
829        branch.angmax = grid.angmax() * RAD_TO_DEG;
830        branch.solution = Some(solution.to_network(base));
831        branches.push(branch);
832    }
833    for (((&from, &to), grid), solution) in document
834        .grid
835        .edges
836        .transformer
837        .senders
838        .iter()
839        .zip(&document.grid.edges.transformer.receivers)
840        .zip(&document.grid.edges.transformer.features)
841        .zip(&document.solution.edges.transformer.features)
842    {
843        let mut branch = Branch::new(BusId(from + 1), BusId(to + 1), grid.r(), grid.x());
844        branch.rate_a = grid.rate_a() * base;
845        branch.rate_b = grid.rate_b() * base;
846        branch.rate_c = grid.rate_c() * base;
847        branch.tap = grid.tap();
848        branch.shift = grid.shift() * RAD_TO_DEG;
849        branch.b = grid.b_fr() + grid.b_to();
850        branch.charging = Some(BranchCharging::new(0.0, grid.b_fr(), 0.0, grid.b_to()));
851        branch.angmin = grid.angmin() * RAD_TO_DEG;
852        branch.angmax = grid.angmax() * RAD_TO_DEG;
853        branch.solution = Some(solution.to_network(base));
854        branches.push(branch);
855    }
856
857    if !document.grid.nodes.generator.is_empty() {
858        warnings.push(&codes::READ_OPFDATA_RETAINED_SOURCE_ONLY,
859            "OPFData generator pg/qg/vg grid features are solver initial values; the canonical snapshot uses solved pg/qg and terminal-bus voltage, so the initial values are carried in the parsed solution's initial generator columns instead of the network snapshot"
860                ,
861        );
862    }
863    warnings.push(&codes::READ_OPFDATA_VALUE_INFERRED, format!(
864        "OPFData does not carry original bus IDs/names, areas/zones, or base frequency; synthesized IDs 1..{bus_count}, area/zone 1, and {} Hz",
865        crate::network::DEFAULT_BASE_FREQUENCY
866    ));
867    if let Some(warning) = objective_warning(&document) {
868        warnings.push(&codes::READ_OPFDATA_FIELD_DROPPED, warning);
869    }
870
871    let solved = OpfDataSolution {
872        bus_voltage_magnitude: buses.iter().map(|bus| bus.vm).collect(),
873        bus_voltage_angle: buses.iter().map(|bus| bus.va).collect(),
874        branch_from_active_flow: branch_solution_column(&branches, |s| s.pf),
875        branch_from_reactive_flow: branch_solution_column(&branches, |s| s.qf),
876        branch_to_active_flow: branch_solution_column(&branches, |s| s.pt),
877        branch_to_reactive_flow: branch_solution_column(&branches, |s| s.qt),
878        generator_active_power: generators.iter().map(|g| g.pg).collect(),
879        generator_reactive_power: generators.iter().map(|g| g.qg).collect(),
880        initial_generator_active_power: document
881            .grid
882            .nodes
883            .generator
884            .iter()
885            .map(|row| row.initial_pg() * base)
886            .collect(),
887        initial_generator_reactive_power: document
888            .grid
889            .nodes
890            .generator
891            .iter()
892            .map(|row| row.initial_qg() * base)
893            .collect(),
894        initial_generator_voltage_setpoint: document
895            .grid
896            .nodes
897            .generator
898            .iter()
899            .map(GeneratorRow::initial_vg)
900            .collect(),
901        objective: document.metadata.objective,
902    };
903
904    let mut network = BalancedNetwork::new(name_hint.unwrap_or("opfdata"), base);
905    *network.buses_mut() = buses;
906    *network.loads_mut() = loads;
907    *network.shunts_mut() = shunts;
908    *network.branches_mut() = branches;
909    *network.generators_mut() = generators;
910    *network.source_format_mut() = SourceFormat::DeepMindOpfDataJson;
911    Ok((network, solved))
912}
913
914fn branch_solution_column(branches: &[Branch], pick: fn(&BranchSolution) -> f64) -> Vec<f64> {
915    branches
916        .iter()
917        .map(|branch| branch.solution.as_ref().map_or(f64::NAN, pick))
918        .collect()
919}