Skip to main content

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