1use 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#[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 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
664pub 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}