1use 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#[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 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#[derive(Debug, Clone)]
680pub struct OpfDataSolution {
681 pub bus_voltage_magnitude: Vec<f64>,
683 pub bus_voltage_angle: Vec<f64>,
685 pub branch_from_active_flow: Vec<f64>,
687 pub branch_from_reactive_flow: Vec<f64>,
689 pub branch_to_active_flow: Vec<f64>,
691 pub branch_to_reactive_flow: Vec<f64>,
693 pub generator_active_power: Vec<f64>,
695 pub generator_reactive_power: Vec<f64>,
697 pub initial_generator_active_power: Vec<f64>,
699 pub initial_generator_reactive_power: Vec<f64>,
701 pub initial_generator_voltage_setpoint: Vec<f64>,
703 pub objective: f64,
705}
706
707pub 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}