1use std::cmp::Ordering;
9use std::collections::{HashMap, HashSet};
10use std::sync::Arc;
11
12use serde_json::{Map, Value};
13
14use crate::network::{
15 BalancedNetwork, Branch, BranchCharging, BranchRatingSet, Bus, BusId, BusType, Extras, GenCost,
16 Generator, Hvdc, Load, Shunt, SourceFormat, TransformerControl, TransformerControlMode,
17};
18use crate::normalize;
19use crate::{Error, Result};
20
21const FMT: &str = "GO Challenge 3 JSON";
22
23#[derive(Clone, Debug)]
28pub struct Goc3Document {
29 root: Map<String, Value>,
30}
31
32impl Goc3Document {
33 pub fn parse(text: &str) -> Result<Self> {
35 let value: Value = serde_json::from_str(text).map_err(|error| bad(error.to_string()))?;
36 let Value::Object(root) = value else {
37 return Err(bad("top level is not a JSON object"));
38 };
39 Ok(Self { root })
40 }
41
42 #[must_use]
43 pub fn root(&self) -> &Map<String, Value> {
44 &self.root
45 }
46
47 pub fn network(&self) -> Result<&Map<String, Value>> {
48 self.root
49 .get("network")
50 .and_then(Value::as_object)
51 .ok_or_else(|| bad("missing object `network`"))
52 }
53
54 pub fn time_series_input(&self) -> Result<&Map<String, Value>> {
55 self.root
56 .get("time_series_input")
57 .and_then(Value::as_object)
58 .ok_or_else(|| bad("missing object `time_series_input`"))
59 }
60
61 #[must_use]
62 pub fn time_series_output(&self) -> Option<&Map<String, Value>> {
63 self.root
64 .get("time_series_output")
65 .and_then(Value::as_object)
66 }
67
68 #[must_use]
69 pub fn reliability(&self) -> Option<&Map<String, Value>> {
70 self.root.get("reliability").and_then(Value::as_object)
71 }
72
73 pub fn network_records(&self, name: &'static str) -> Result<Vec<Goc3Record<'_>>> {
75 records(self.network()?, name)
76 }
77
78 pub fn time_series_input_records(&self, name: &'static str) -> Result<Vec<Goc3Record<'_>>> {
80 records(self.time_series_input()?, name)
81 }
82
83 pub fn time_series_output_records(&self, name: &'static str) -> Result<Vec<Goc3Record<'_>>> {
85 self.time_series_output()
86 .map_or_else(|| Ok(Vec::new()), |output| records(output, name))
87 }
88
89 pub fn dispatchable_devices(&self) -> Result<Vec<Goc3DeviceRecord<'_>>> {
91 device_rows(self.network()?)
92 }
93
94 pub fn bus_ids(&self) -> Result<HashMap<String, BusId>> {
101 bus_id_by_uid(§ion(self.network()?, "bus")?)
102 }
103
104 #[must_use]
106 pub fn dispatchable_device_cost_at(
107 &self,
108 device: &Map<String, Value>,
109 time_series: Option<&Value>,
110 period: usize,
111 base_mva: f64,
112 ) -> Option<GenCost> {
113 cost_at(device, time_series, period, base_mva)
114 }
115}
116
117#[derive(Clone, Debug)]
119pub struct Goc3Record<'a> {
120 pub uid: Option<String>,
121 pub value: &'a Value,
122}
123
124#[derive(Debug)]
125struct Goc3BusMap {
126 by_uid: HashMap<String, BusId>,
127}
128
129impl Goc3BusMap {
130 fn get(&self, uid: &str) -> Result<BusId> {
131 self.by_uid
132 .get(uid)
133 .copied()
134 .ok_or_else(|| bad(format!("unknown bus uid `{uid}`")))
135 }
136}
137
138pub fn parse_goc3_json(content: &str) -> Result<super::Parsed> {
140 let mut warnings = Vec::new();
141 let (network, document) = parse_goc3_source(Arc::new(content.to_owned()), None, &mut warnings)?;
142 Ok(super::Parsed {
143 network,
144 warnings,
145 document: Some(super::SourceDocument::Goc3(document)),
146 })
147}
148
149#[allow(clippy::too_many_lines)]
153pub(crate) fn parse_goc3_source(
154 source: Arc<String>,
155 name_hint: Option<&str>,
156 warnings: &mut Vec<String>,
157) -> Result<(BalancedNetwork, Arc<Goc3Document>)> {
158 let document = Arc::new(Goc3Document::parse(&source)?);
159 let root = document.root();
160 let network = document.network()?;
161
162 let base_mva = network
163 .get("general")
164 .and_then(Value::as_object)
165 .and_then(|general| number(general, "base_norm_mva"))
166 .unwrap_or_else(|| {
167 push_once(
168 warnings,
169 "missing `network.general.base_norm_mva`; using 100.0 MVA",
170 );
171 100.0
172 });
173 if !base_mva.is_finite() || base_mva <= 0.0 {
174 return Err(Error::InvalidBaseMva { base: base_mva });
175 }
176
177 let name = root
178 .get("uid")
179 .and_then(Value::as_str)
180 .or_else(|| {
181 network
182 .get("general")
183 .and_then(Value::as_object)
184 .and_then(|general| general.get("uid"))
185 .and_then(Value::as_str)
186 })
187 .or(name_hint)
188 .unwrap_or("goc3")
189 .to_owned();
190
191 warn_static_reduction(root, network, warnings);
192
193 let (mut buses, bus_map) = read_buses(network)?;
194 let bus_pos: HashMap<BusId, usize> = buses
195 .iter()
196 .enumerate()
197 .map(|(index, bus)| (bus.id, index))
198 .collect();
199 let time_series = root.get("time_series_input").and_then(Value::as_object);
200 let device_ts = device_time_series(time_series)?;
201
202 let mut branches = Vec::new();
203 branches.extend(read_branches(network, "ac_line", false, &bus_map)?);
204 branches.extend(read_branches(
205 network,
206 "two_winding_transformer",
207 true,
208 &bus_map,
209 )?);
210
211 let shunts = read_shunts(network, base_mva, &bus_map)?;
212 let mut loads = Vec::new();
213 let mut generators = Vec::new();
214 let mut generator_buses = HashSet::new();
215 let mut reference_candidate: Option<(BusId, f64)> = None;
216
217 for device in device_rows(network)? {
218 let obj = device.obj;
219 let bus = bus_ref(obj, "bus", &bus_map)?;
220 let ts = device
221 .uid
222 .as_deref()
223 .and_then(|key| device_ts.get(key).copied());
224
225 match device.kind {
226 Goc3DeviceKind::Generators => {
227 let generator = read_producer(obj, ts, bus, base_mva, device.uid.clone());
228 generator_buses.insert(bus);
229 if reference_candidate
230 .as_ref()
231 .is_none_or(|(_, pmax)| generator.pmax > *pmax)
232 {
233 reference_candidate = Some((bus, generator.pmax));
234 }
235 generators.push(generator);
236 }
237 Goc3DeviceKind::Loads => {
238 loads.push(read_consumer(obj, ts, bus, base_mva, device.uid.clone()));
239 }
240 }
241 }
242
243 assign_bus_types(
244 &mut buses,
245 &bus_pos,
246 &generator_buses,
247 reference_candidate,
248 warnings,
249 );
250
251 let hvdc = read_hvdc(network, base_mva, &bus_map)?;
252
253 let net = BalancedNetwork {
254 name,
255 base_mva,
256 base_frequency: crate::network::DEFAULT_BASE_FREQUENCY,
257 geo: None,
258 buses,
259 loads,
260 shunts,
261 branches,
262 switches: Vec::new(),
263 generators,
264 storage: Vec::new(),
265 hvdc,
266 transformers_3w: Vec::new(),
267 areas: Vec::new(),
268 solver: None,
269 source_format: SourceFormat::Goc3Json,
270 source: Some(source),
271 };
272 net.check_references(FMT)?;
273 Ok((net, document))
274}
275
276fn read_buses(network: &Map<String, Value>) -> Result<(Vec<Bus>, Goc3BusMap)> {
277 let items = section(network, "bus")?;
278 if items.is_empty() {
279 return Err(bad("missing non-empty `network.bus` section"));
280 }
281 let mut records = Vec::with_capacity(items.len());
282 let mut seen_uids = HashSet::new();
283 for item in &items {
284 let obj = item_object(*item, "bus")?;
285 let uid = item_uid(*item, obj).ok_or_else(|| bad("bus record missing `uid`"))?;
286 if !seen_uids.insert(uid.clone()) {
287 return Err(bad(format!("duplicate bus uid `{uid}`")));
288 }
289 records.push((uid, obj));
290 }
291
292 let ids = bus_id_by_uid(&items)?;
293
294 let mut by_uid = HashMap::with_capacity(records.len());
295 let mut buses = Vec::with_capacity(records.len());
296 for (uid, obj) in records {
297 let id = ids[&uid];
298 by_uid.insert(uid.clone(), id);
299 let initial = initial_status(obj);
300 buses.push(Bus {
301 id,
302 kind: BusType::Pq,
303 vm: initial.and_then(|s| number(s, "vm")).unwrap_or(1.0),
304 va: initial.and_then(|s| number(s, "va")).unwrap_or(0.0) * normalize::RAD_TO_DEG,
305 base_kv: number(obj, "base_nom_volt").unwrap_or(0.0),
306 vmax: number(obj, "vm_ub").unwrap_or(1.1),
307 vmin: number(obj, "vm_lb").unwrap_or(0.9),
308 evhi: None,
309 evlo: None,
310 area: 1,
311 zone: 1,
312 name: Some(uid.clone()),
313 uid: Some(uid),
314 location: None,
315 extras: extras(
316 obj,
317 &["uid", "base_nom_volt", "vm_ub", "vm_lb", "initial_status"],
318 ),
319 });
320 }
321 Ok((buses, Goc3BusMap { by_uid }))
322}
323
324fn read_branches(
325 network: &Map<String, Value>,
326 section_name: &'static str,
327 transformer: bool,
328 buses: &Goc3BusMap,
329) -> Result<Vec<Branch>> {
330 section(network, section_name)?
331 .into_iter()
332 .map(|item| {
333 let obj = item_object(item, section_name)?;
334 let from = bus_ref(obj, "fr_bus", buses)?;
335 let to = bus_ref(obj, "to_bus", buses)?;
336 let initial = initial_status(obj);
337 let b = number(obj, "b").unwrap_or(0.0);
338 let rate_a = number(obj, "mva_ub_nom").unwrap_or(0.0);
339 let rate_b = number(obj, "mva_ub_em").unwrap_or(rate_a);
340 let charging = if number(obj, "additional_shunt").unwrap_or(0.0) == 0.0 {
343 BranchCharging::from_total_b(b)
344 } else {
345 BranchCharging {
346 g_fr: number(obj, "g_fr").unwrap_or(0.0),
347 b_fr: b / 2.0 + number(obj, "b_fr").unwrap_or(0.0),
348 g_to: number(obj, "g_to").unwrap_or(0.0),
349 b_to: b / 2.0 + number(obj, "b_to").unwrap_or(0.0),
350 }
351 };
352 let tap = if transformer {
353 initial
354 .and_then(|s| number(s, "tm"))
355 .or_else(|| equal_bounds(obj, "tm_lb", "tm_ub"))
356 .unwrap_or(1.0)
357 } else {
358 0.0
359 };
360 let shift = if transformer {
361 initial.and_then(|s| number(s, "ta")).unwrap_or(0.0) * normalize::RAD_TO_DEG
362 } else {
363 0.0
364 };
365 Ok(Branch {
366 from,
367 to,
368 r: number(obj, "r").unwrap_or(0.0),
369 x: number(obj, "x").unwrap_or(0.0),
370 b,
371 charging: Some(charging),
372 rate_a,
373 rate_b,
374 rate_c: rate_b,
375 rating_sets: (rate_b != 0.0 && (rate_b - rate_a).abs() > f64::EPSILON)
376 .then(|| BranchRatingSet::new("mva_ub_em", rate_b))
377 .into_iter()
378 .collect(),
379 current_ratings: None,
380 tap,
381 shift,
382 in_service: initial_status_flag(obj, true),
383 angmin: -360.0,
384 angmax: 360.0,
385 control: shifter_control(obj, transformer),
386 solution: None,
387 uid: item_uid(item, obj),
388 route: None,
389 extras: extras(
390 obj,
391 &[
392 "uid",
393 "fr_bus",
394 "to_bus",
395 "r",
396 "x",
397 "b",
398 "mva_ub_nom",
399 "mva_ub_em",
400 "initial_status",
401 "additional_shunt",
402 "g_fr",
403 "g_to",
404 "b_fr",
405 "b_to",
406 "tm_lb",
407 "tm_ub",
408 "ta_lb",
409 "ta_ub",
410 ],
411 ),
412 })
413 })
414 .collect()
415}
416
417fn shifter_control(obj: &Map<String, Value>, transformer: bool) -> Option<TransformerControl> {
422 if !transformer {
423 return None;
424 }
425 let lb = number(obj, "ta_lb");
426 let ub = number(obj, "ta_ub");
427 if lb.is_none() && ub.is_none() {
428 return None;
429 }
430 let mut control = TransformerControl::new(TransformerControlMode::ActiveFlow);
431 control.tap_min = lb.unwrap_or(-std::f64::consts::TAU) * normalize::RAD_TO_DEG;
432 control.tap_max = ub.unwrap_or(std::f64::consts::TAU) * normalize::RAD_TO_DEG;
433 Some(control)
434}
435
436fn read_shunts(
437 network: &Map<String, Value>,
438 base_mva: f64,
439 buses: &Goc3BusMap,
440) -> Result<Vec<Shunt>> {
441 section(network, "shunt")?
442 .into_iter()
443 .map(|item| {
444 let obj = item_object(item, "shunt")?;
445 let step = initial_status(obj)
446 .and_then(|s| number(s, "step"))
447 .unwrap_or(1.0);
448 Ok(Shunt {
449 bus: bus_ref(obj, "bus", buses)?,
450 g: number(obj, "gs").unwrap_or(0.0) * step * base_mva,
451 b: number(obj, "bs").unwrap_or(0.0) * step * base_mva,
452 in_service: step != 0.0,
453 control: None,
454 uid: item_uid(item, obj),
455 extras: extras(
456 obj,
457 &[
458 "uid",
459 "bus",
460 "gs",
461 "bs",
462 "step_lb",
463 "step_ub",
464 "initial_status",
465 ],
466 ),
467 })
468 })
469 .collect()
470}
471
472fn read_producer(
473 obj: &Map<String, Value>,
474 ts: Option<&Value>,
475 bus: BusId,
476 base_mva: f64,
477 uid: Option<String>,
478) -> Generator {
479 let initial = initial_status(obj);
480 Generator {
481 bus,
482 pg: initial.and_then(|s| number(s, "p")).unwrap_or(0.0) * base_mva,
483 qg: initial.and_then(|s| number(s, "q")).unwrap_or(0.0) * base_mva,
484 pmax: first_number(ts, "p_ub").unwrap_or(0.0) * base_mva,
485 pmin: first_number(ts, "p_lb").unwrap_or(0.0) * base_mva,
486 qmax: first_number(ts, "q_ub").unwrap_or(0.0) * base_mva,
487 qmin: first_number(ts, "q_lb").unwrap_or(0.0) * base_mva,
488 vg: 1.0,
489 mbase: base_mva,
490 in_service: initial_status_flag(obj, true),
491 cost: cost_at(obj, ts, 0, base_mva),
492 caps: [None; crate::network::GEN_EXTRA_KEYS.len()],
493 regulated_bus: None,
494 uid,
495 }
496}
497
498fn read_consumer(
499 obj: &Map<String, Value>,
500 ts: Option<&Value>,
501 bus: BusId,
502 base_mva: f64,
503 uid: Option<String>,
504) -> Load {
505 let initial = initial_status(obj);
506 let p = initial
507 .and_then(|s| number(s, "p"))
508 .or_else(|| first_number(ts, "p_ub"))
509 .unwrap_or(0.0)
510 .abs()
511 * base_mva;
512 let q = initial
513 .and_then(|s| number(s, "q"))
514 .or_else(|| first_number(ts, "q_ub"))
515 .unwrap_or(0.0)
516 .abs()
517 * base_mva;
518 Load {
519 bus,
520 p,
521 q,
522 voltage_model: None,
523 in_service: initial_status_flag(obj, true),
524 uid,
525 extras: extras(
526 obj,
527 &[
528 "uid",
529 "bus",
530 "device_type",
531 "initial_status",
532 "startup_cost",
533 "shutdown_cost",
534 ],
535 ),
536 }
537}
538
539fn read_hvdc(network: &Map<String, Value>, base_mva: f64, buses: &Goc3BusMap) -> Result<Vec<Hvdc>> {
540 section(network, "dc_line")?
541 .into_iter()
542 .map(|item| {
543 let obj = item_object(item, "dc_line")?;
544 let initial = initial_status(obj);
545 let pdc = initial.and_then(|s| number(s, "pdc_fr")).unwrap_or(0.0) * base_mva;
546 Ok(Hvdc {
547 from: bus_ref(obj, "fr_bus", buses)?,
548 to: bus_ref(obj, "to_bus", buses)?,
549 in_service: initial_status_flag(obj, true),
550 pf: pdc,
551 pt: -pdc,
552 qf: initial.and_then(|s| number(s, "qdc_fr")).unwrap_or(0.0) * base_mva,
553 qt: initial.and_then(|s| number(s, "qdc_to")).unwrap_or(0.0) * base_mva,
554 vf: 1.0,
555 vt: 1.0,
556 pmin: -number(obj, "pdc_ub").unwrap_or(0.0) * base_mva,
557 pmax: number(obj, "pdc_ub").unwrap_or(0.0) * base_mva,
558 qminf: number(obj, "qdc_fr_lb").unwrap_or(0.0) * base_mva,
559 qmaxf: number(obj, "qdc_fr_ub").unwrap_or(0.0) * base_mva,
560 qmint: number(obj, "qdc_to_lb").unwrap_or(0.0) * base_mva,
561 qmaxt: number(obj, "qdc_to_ub").unwrap_or(0.0) * base_mva,
562 loss0: 0.0,
563 loss1: 0.0,
564 cost: None,
565 uid: item_uid(item, obj),
566 extras: extras(
567 obj,
568 &[
569 "uid",
570 "fr_bus",
571 "to_bus",
572 "pdc_ub",
573 "qdc_fr_lb",
574 "qdc_fr_ub",
575 "qdc_to_lb",
576 "qdc_to_ub",
577 "initial_status",
578 ],
579 ),
580 })
581 })
582 .collect()
583}
584
585fn assign_bus_types(
586 buses: &mut [Bus],
587 bus_pos: &HashMap<BusId, usize>,
588 generator_buses: &HashSet<BusId>,
589 reference_candidate: Option<(BusId, f64)>,
590 warnings: &mut Vec<String>,
591) {
592 for bus in generator_buses {
593 super::set_bus_kind(buses, bus_pos, *bus, BusType::Pv);
594 }
595 if let Some((bus, _)) = reference_candidate
596 && bus_pos.contains_key(&bus)
597 {
598 super::set_bus_kind(buses, bus_pos, bus, BusType::Ref);
599 warnings.push(format!(
600 "GO Challenge 3 has no explicit reference bus; selected bus {} from the largest producer pmax",
601 bus.0
602 ));
603 }
604}
605
606#[derive(Clone, Copy, Debug, PartialEq, Eq)]
608pub enum Goc3DeviceKind {
609 Generators,
610 Loads,
611}
612
613pub struct Goc3DeviceRecord<'a> {
616 pub kind: Goc3DeviceKind,
617 pub row: usize,
618 pub uid: Option<String>,
619 pub obj: &'a Map<String, Value>,
620}
621
622fn device_rows(network: &Map<String, Value>) -> Result<Vec<Goc3DeviceRecord<'_>>> {
628 let mut rows = Vec::new();
629 let mut generators = 0usize;
630 let mut loads = 0usize;
631 let mut seen_uids = std::collections::HashSet::new();
636 for item in section(network, "simple_dispatchable_device")? {
637 let obj = item_object(item, "simple_dispatchable_device")?;
638 let uid = item_uid(item, obj);
639 if let Some(uid) = &uid {
640 if !seen_uids.insert(uid.clone()) {
641 return Err(bad(format!(
642 "duplicate simple_dispatchable_device uid `{uid}`"
643 )));
644 }
645 }
646 let (table, row) = match string(obj, "device_type").unwrap_or("producer") {
647 "producer" => {
648 generators += 1;
649 (Goc3DeviceKind::Generators, generators - 1)
650 }
651 "consumer" => {
652 loads += 1;
653 (Goc3DeviceKind::Loads, loads - 1)
654 }
655 other => {
656 return Err(bad(format!(
657 "simple_dispatchable_device `{}` has unsupported `device_type` `{other}`",
658 uid.unwrap_or_else(|| "?".into())
659 )));
660 }
661 };
662 rows.push(Goc3DeviceRecord {
663 kind: table,
664 row,
665 uid,
666 obj,
667 });
668 }
669 Ok(rows)
670}
671
672fn cost_at(
677 obj: &Map<String, Value>,
678 ts: Option<&Value>,
679 index: usize,
680 base_mva: f64,
681) -> Option<GenCost> {
682 let periods = ts?.get("cost")?.as_array()?;
683 let curve = periods.get(index)?.as_array()?;
684 let mut coeffs = vec![0.0, 0.0];
685 let mut p = 0.0;
686 let mut y = 0.0;
687 for segment in curve {
688 let values = segment.as_array()?;
689 let marginal = values.first()?.as_f64()?;
690 let width = values.get(1)?.as_f64()?;
691 if !marginal.is_finite() || !width.is_finite() || width <= 0.0 {
692 continue;
693 }
694 p += width * base_mva;
695 y += marginal * width;
696 coeffs.push(p);
697 coeffs.push(y);
698 }
699 (coeffs.len() >= 4).then_some(GenCost {
700 model: 1,
701 startup: number(obj, "startup_cost").unwrap_or(0.0),
702 shutdown: number(obj, "shutdown_cost").unwrap_or(0.0),
703 ncost: coeffs.len() / 2,
704 coeffs,
705 })
706}
707
708fn device_time_series(time_series: Option<&Map<String, Value>>) -> Result<HashMap<String, &Value>> {
709 let Some(time_series) = time_series else {
710 return Ok(HashMap::new());
711 };
712 let mut out = HashMap::new();
713 for item in section(time_series, "simple_dispatchable_device")? {
714 if let Some(key) = item.key {
715 out.insert(key.to_owned(), item.value);
716 }
717 if let Some(obj) = item.value.as_object() {
718 if let Some(uid) = string(obj, "uid") {
719 out.insert(uid.to_owned(), item.value);
720 }
721 }
722 }
723 Ok(out)
724}
725
726fn warn_static_reduction(
727 root: &Map<String, Value>,
728 network: &Map<String, Value>,
729 warnings: &mut Vec<String>,
730) {
731 if root.get("time_series_input").is_some() {
732 warnings.push(
733 "time_series_input reduced to the first interval for static BalancedNetwork dispatch and limits"
734 .into(),
735 );
736 }
737 if root.get("reliability").is_some() {
738 warnings.push("reliability contingencies retained in source only".into());
739 }
740 for section in [
741 "active_zonal_reserve",
742 "reactive_zonal_reserve",
743 "violation_cost",
744 ] {
745 if network.get(section).is_some() {
746 warnings.push(format!("network.{section} retained in source only"));
747 }
748 }
749 if !section(network, "simple_dispatchable_device")
750 .unwrap_or_default()
751 .is_empty()
752 {
753 warnings.push(
754 "simple dispatchable device commitment, ramp, reserve, and multi-interval cost data retained in source only"
755 .into(),
756 );
757 }
758}
759
760#[derive(Clone, Copy)]
761struct SectionItem<'a> {
762 pub key: Option<&'a str>,
763 pub value: &'a Value,
764}
765
766fn section<'a>(parent: &'a Map<String, Value>, name: &'static str) -> Result<Vec<SectionItem<'a>>> {
767 let Some(value) = parent.get(name) else {
768 return Ok(Vec::new());
769 };
770 match value {
771 Value::Array(items) => Ok(items
772 .iter()
773 .map(|value| SectionItem { key: None, value })
774 .collect()),
775 Value::Object(map) => {
776 let mut items: Vec<_> = map
777 .iter()
778 .map(|(key, value)| SectionItem {
779 key: Some(key.as_str()),
780 value,
781 })
782 .collect();
783 items.sort_by(|a, b| compare_keys(a.key.unwrap_or(""), b.key.unwrap_or("")));
784 Ok(items)
785 }
786 other => Err(bad(format!(
787 "`network.{name}` is not an array or object, got {}",
788 kind(other)
789 ))),
790 }
791}
792
793fn item_object<'a>(
794 item: SectionItem<'a>,
795 section_name: &'static str,
796) -> Result<&'a Map<String, Value>> {
797 item.value.as_object().ok_or_else(|| {
798 bad(format!(
799 "`network.{section_name}` record is not an object, got {}",
800 kind(item.value)
801 ))
802 })
803}
804
805fn item_uid(item: SectionItem<'_>, obj: &Map<String, Value>) -> Option<String> {
806 string(obj, "uid")
807 .map(str::to_owned)
808 .or_else(|| item.key.map(str::to_owned))
809 .filter(|uid| !uid.is_empty())
810}
811
812fn compare_keys(a: &str, b: &str) -> Ordering {
817 match (a.parse::<u64>(), b.parse::<u64>()) {
818 (Ok(a_num), Ok(b_num)) => a_num.cmp(&b_num).then_with(|| a.cmp(b)),
819 (Ok(_), Err(_)) => Ordering::Less,
820 (Err(_), Ok(_)) => Ordering::Greater,
821 (Err(_), Err(_)) => a.cmp(b),
822 }
823}
824
825fn bus_ref(obj: &Map<String, Value>, key: &'static str, buses: &Goc3BusMap) -> Result<BusId> {
826 let uid = string(obj, key).ok_or_else(|| bad(format!("missing string `{key}`")))?;
827 buses.get(uid)
828}
829
830fn official_bus_suffix(uid: &str) -> Option<usize> {
831 let rest = uid.strip_prefix("bus_")?;
832 (!rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()))
833 .then(|| rest.parse::<usize>().ok())
834 .flatten()
835}
836
837fn bus_id_by_uid(items: &[SectionItem<'_>]) -> Result<HashMap<String, BusId>> {
843 let mut uids = Vec::with_capacity(items.len());
844 for item in items {
845 let obj = item
846 .value
847 .as_object()
848 .ok_or_else(|| bad("bus section item is not an object"))?;
849 let uid = item_uid(*item, obj).ok_or_else(|| bad("bus section item missing `uid`"))?;
850 uids.push(uid);
851 }
852 let suffixes: Vec<Option<usize>> = uids.iter().map(|uid| official_bus_suffix(uid)).collect();
853 let suffixes_unique = suffixes.iter().all(Option::is_some)
854 && suffixes
855 .iter()
856 .flatten()
857 .copied()
858 .collect::<HashSet<_>>()
859 .len()
860 == suffixes.len();
861 Ok(uids
862 .into_iter()
863 .zip(suffixes)
864 .enumerate()
865 .map(|(index, (uid, suffix))| {
866 let id = match suffix.and_then(|s| s.checked_add(1)) {
870 Some(id) if suffixes_unique => id,
871 _ => index + 1,
872 };
873 (uid, BusId(id))
874 })
875 .collect())
876}
877
878fn string<'a>(obj: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
879 obj.get(key).and_then(Value::as_str)
880}
881
882fn number(obj: &Map<String, Value>, key: &str) -> Option<f64> {
883 obj.get(key).and_then(Value::as_f64)
884}
885
886fn first_number(value: Option<&Value>, key: &str) -> Option<f64> {
887 value?.get(key)?.as_array()?.first().and_then(Value::as_f64)
888}
889
890fn initial_status(obj: &Map<String, Value>) -> Option<&Map<String, Value>> {
891 obj.get("initial_status").and_then(Value::as_object)
892}
893
894fn initial_status_flag(obj: &Map<String, Value>, default: bool) -> bool {
895 initial_status(obj)
896 .and_then(|status| number(status, "on_status"))
897 .map_or(default, |v| v != 0.0)
898}
899
900fn equal_bounds(obj: &Map<String, Value>, low: &str, high: &str) -> Option<f64> {
901 let lo = number(obj, low)?;
902 let hi = number(obj, high)?;
903 ((lo - hi).abs() <= f64::EPSILON).then_some(lo)
904}
905
906fn extras(obj: &Map<String, Value>, known: &[&str]) -> Extras {
907 obj.iter()
908 .filter(|(key, _)| !known.contains(&key.as_str()))
909 .map(|(key, value)| (key.clone(), value.clone()))
910 .collect()
911}
912
913fn push_once(warnings: &mut Vec<String>, warning: &str) {
914 if !warnings.iter().any(|w| w == warning) {
915 warnings.push(warning.to_owned());
916 }
917}
918
919fn kind(value: &Value) -> &'static str {
920 match value {
921 Value::Null => "null",
922 Value::Bool(_) => "bool",
923 Value::Number(_) => "number",
924 Value::String(_) => "string",
925 Value::Array(_) => "array",
926 Value::Object(_) => "object",
927 }
928}
929
930fn bad(message: impl Into<String>) -> Error {
931 Error::FormatRead {
932 format: FMT,
933 message: message.into(),
934 }
935}
936
937fn records<'a>(parent: &'a Map<String, Value>, name: &'static str) -> Result<Vec<Goc3Record<'a>>> {
938 section(parent, name).map(|items| {
939 items
940 .into_iter()
941 .map(|item| Goc3Record {
942 uid: item
943 .value
944 .as_object()
945 .and_then(|object| item_uid(item, object))
946 .or_else(|| item.key.map(str::to_owned)),
947 value: item.value,
948 })
949 .collect()
950 })
951}
952
953#[cfg(test)]
954mod tests {
955 use super::*;
956
957 #[test]
958 fn duplicate_device_uid_is_rejected() {
959 let network: Map<String, Value> = serde_json::from_str(
962 r#"{"simple_dispatchable_device":[
963 {"uid":"sd1","device_type":"producer"},
964 {"uid":"sd1","device_type":"consumer"}]}"#,
965 )
966 .unwrap();
967 let Err(err) = device_rows(&network) else {
968 panic!("duplicate uid must be rejected")
969 };
970 assert!(err.to_string().contains("duplicate"), "got: {err}");
971 }
972
973 #[test]
974 fn extreme_bus_suffix_does_not_overflow() {
975 let content = format!(
979 r#"{{"network":{{"bus":[{{"uid":"bus_{}","base_nom_volt":100}}]}}}}"#,
980 usize::MAX
981 );
982 let parsed = parse_goc3_json(&content).unwrap();
983 assert_eq!(parsed.network.buses.len(), 1);
984 assert_eq!(parsed.network.buses[0].id, BusId(1));
985 }
986}