1use std::sync::Arc;
17
18use serde_json::{Map, Value};
19
20use super::{Conversion, finish, jnum, warn_extra_branch_rating_sets};
21use crate::network::{
22 BalancedNetwork, Branch, BranchCharging, BranchCurrentRatings, BranchSolution, Bus, BusId,
23 BusType, GEN_EXTRA_KEYS, GenCost, Generator, Hvdc, Load, LoadVoltageModel, Shunt, SourceFormat,
24 Storage, Switch,
25};
26use crate::normalize::{self, GEN_PU_KEYS};
27use crate::{Error, Result};
28
29#[must_use]
30#[expect(clippy::too_many_lines)]
31pub fn write_powermodels_json(net: &BalancedNetwork) -> Conversion {
32 let mut warnings = Vec::new();
33
34 let base = net.base_mva;
37 let p = 1.0 / base;
38 let a = normalize::DEG_TO_RAD;
39
40 let mut bus = Map::new();
41 for b in &net.buses {
42 bus.insert(b.id.to_string(), bus_obj(b, a));
43 }
44
45 let mut branch = Map::new();
46 for (i, br) in net.branches.iter().enumerate() {
47 let idx = i + 1;
48 branch.insert(idx.to_string(), branch_obj(br, idx, p, a));
49 }
50
51 let mut gen_map = Map::new();
52 for (i, g) in net.generators.iter().enumerate() {
53 let idx = i + 1;
54 gen_map.insert(idx.to_string(), gen_obj(g, idx, p, base));
55 }
56
57 let mut load = Map::new();
58 for (i, l) in net.loads.iter().enumerate() {
59 let idx = i + 1;
60 load.insert(idx.to_string(), load_obj(l, idx, p));
61 }
62 let mut shunt = Map::new();
63 for (i, s) in net.shunts.iter().enumerate() {
64 let idx = i + 1;
65 shunt.insert(idx.to_string(), shunt_obj(s, idx, p));
66 }
67
68 let mut dcline = Map::new();
69 for (i, dc) in net.hvdc.iter().enumerate() {
70 let idx = i + 1;
71 dcline.insert(idx.to_string(), dcline_obj(dc, idx, p));
72 }
73 let mut storage = Map::new();
74 for (i, st) in net.storage.iter().enumerate() {
75 let idx = i + 1;
76 storage.insert(idx.to_string(), storage_obj(st, idx, p));
77 }
78 let mut switch = Map::new();
79 for (i, sw) in net.switches.iter().enumerate() {
80 let idx = i + 1;
81 switch.insert(idx.to_string(), switch_obj(sw, idx, p));
82 }
83 if !dcline.is_empty() {
84 warnings.push(format!(
85 "{} dcline(s) mapped with warnings to the PowerModels dcline schema",
86 dcline.len()
87 ));
88 }
89 if !storage.is_empty() {
90 warnings.push(format!(
91 "{} storage unit(s) mapped with warnings to the PowerModels storage schema",
92 storage.len()
93 ));
94 }
95 if !net.transformers_3w.is_empty() {
96 warnings.push(format!(
97 "{} 3-winding transformer(s) dropped: the PowerModels JSON writer emits no 3-winding record",
98 net.transformers_3w.len()
99 ));
100 }
101 let voltage_loads = net
102 .loads
103 .iter()
104 .filter(|l| {
105 l.voltage_model
106 .as_ref()
107 .is_some_and(LoadVoltageModel::has_non_matpower_fields)
108 })
109 .count();
110 if voltage_loads > 0 {
111 warnings.push(format!(
112 "{voltage_loads} voltage dependent load model(s) dropped: PowerModels load records carry static pd/qd only"
113 ));
114 }
115 warn_extra_branch_rating_sets("PowerModels JSON", net, &mut warnings);
116 if net
117 .buses
118 .iter()
119 .any(|b| b.evhi.is_some() || b.evlo.is_some())
120 {
121 warnings.push(
122 "emergency voltage band(s) (EVHI/EVLO) dropped: this writer carries one voltage band"
123 .into(),
124 );
125 }
126
127 let mut root = Map::new();
128 root.insert("name".into(), Value::String(net.name.clone()));
129 root.insert("baseMVA".into(), jnum(net.base_mva));
130 root.insert("per_unit".into(), Value::Bool(true));
131 root.insert("source_type".into(), Value::String("matpower".into()));
132 root.insert("source_version".into(), Value::String("2".into()));
133 root.insert("bus".into(), Value::Object(bus));
134 root.insert("branch".into(), Value::Object(branch));
135 root.insert("gen".into(), Value::Object(gen_map));
136 root.insert("load".into(), Value::Object(load));
137 root.insert("shunt".into(), Value::Object(shunt));
138 root.insert("dcline".into(), Value::Object(dcline));
139 root.insert("storage".into(), Value::Object(storage));
140 root.insert("switch".into(), Value::Object(switch));
141
142 finish(root, warnings)
143}
144
145fn source_id(kind: &str, idx: usize) -> Value {
147 Value::Array(vec![Value::String(kind.into()), Value::from(idx as u64)])
148}
149
150fn status_int(in_service: bool) -> Value {
151 Value::from(u64::from(in_service))
152}
153
154fn bus_obj(b: &Bus, a: f64) -> Value {
155 let mut m = Map::new();
156 m.insert("bus_i".into(), Value::from(b.id.0 as u64));
157 m.insert("index".into(), Value::from(b.id.0 as u64));
158 m.insert("bus_type".into(), Value::from(u64::from(b.kind as u8)));
159 m.insert("vm".into(), jnum(b.vm));
160 m.insert("va".into(), jnum(b.va * a));
161 m.insert("vmax".into(), jnum(b.vmax));
162 m.insert("vmin".into(), jnum(b.vmin));
163 m.insert("base_kv".into(), jnum(b.base_kv));
164 m.insert("area".into(), Value::from(b.area as u64));
165 m.insert("zone".into(), Value::from(b.zone as u64));
166 if let Some(name) = &b.name {
167 m.insert("name".into(), Value::String(name.clone()));
168 }
169 m.insert("source_id".into(), source_id("bus", b.id.0));
170 Value::Object(m)
171}
172
173fn branch_obj(br: &Branch, idx: usize, p: f64, a: f64) -> Value {
174 let mut m = Map::new();
175 m.insert("index".into(), Value::from(idx as u64));
176 m.insert("f_bus".into(), Value::from(br.from.0 as u64));
177 m.insert("t_bus".into(), Value::from(br.to.0 as u64));
178 m.insert("br_r".into(), jnum(br.r));
179 m.insert("br_x".into(), jnum(br.x));
180 let charging = br.terminal_charging();
181 m.insert("b_fr".into(), jnum(charging.b_fr));
182 m.insert("b_to".into(), jnum(charging.b_to));
183 m.insert("g_fr".into(), jnum(charging.g_fr));
184 m.insert("g_to".into(), jnum(charging.g_to));
185 m.insert("tap".into(), jnum(br.effective_tap()));
186 m.insert("shift".into(), jnum(br.shift * a));
187 m.insert("br_status".into(), status_int(br.in_service));
188 m.insert("angmin".into(), jnum(br.angmin * a));
189 m.insert("angmax".into(), jnum(br.angmax * a));
190 m.insert("transformer".into(), Value::Bool(br.tap != 0.0));
193 if br.rate_a != 0.0 {
195 m.insert("rate_a".into(), jnum(br.rate_a * p));
196 }
197 if br.rate_b != 0.0 {
198 m.insert("rate_b".into(), jnum(br.rate_b * p));
199 }
200 if br.rate_c != 0.0 {
201 m.insert("rate_c".into(), jnum(br.rate_c * p));
202 }
203 if let Some(current) = br.current_ratings {
204 if current.c_rating_a != 0.0 {
205 m.insert("c_rating_a".into(), jnum(current.c_rating_a));
206 }
207 if current.c_rating_b != 0.0 {
208 m.insert("c_rating_b".into(), jnum(current.c_rating_b));
209 }
210 if current.c_rating_c != 0.0 {
211 m.insert("c_rating_c".into(), jnum(current.c_rating_c));
212 }
213 }
214 if let Some(solution) = br.solution {
215 m.insert("pf".into(), jnum(solution.pf * p));
216 m.insert("qf".into(), jnum(solution.qf * p));
217 m.insert("pt".into(), jnum(solution.pt * p));
218 m.insert("qt".into(), jnum(solution.qt * p));
219 }
220 m.insert("source_id".into(), source_id("branch", idx));
221 Value::Object(m)
222}
223
224fn gen_obj(g: &Generator, idx: usize, p: f64, base: f64) -> Value {
225 let mut m = Map::new();
226 m.insert("index".into(), Value::from(idx as u64));
227 m.insert("gen_bus".into(), Value::from(g.bus.0 as u64));
228 m.insert("pg".into(), jnum(g.pg * p));
229 m.insert("qg".into(), jnum(g.qg * p));
230 m.insert("qmax".into(), jnum(g.qmax * p));
231 m.insert("qmin".into(), jnum(g.qmin * p));
232 m.insert("vg".into(), jnum(g.vg));
233 m.insert("mbase".into(), jnum(g.mbase));
234 m.insert("gen_status".into(), status_int(g.in_service));
235 m.insert("pmax".into(), jnum(g.pmax * p));
236 m.insert("pmin".into(), jnum(g.pmin * p));
237 for (i, key) in GEN_EXTRA_KEYS.iter().enumerate() {
240 if let Some(v) = g.caps[i] {
241 let scaled = if GEN_PU_KEYS.contains(key) {
242 jnum(v * p)
243 } else {
244 jnum(v)
245 };
246 m.insert((*key).into(), scaled);
247 }
248 }
249 if let Some(cost) = &g.cost {
250 let coeffs: Vec<Value> = normalize::cost_to_pu(cost, base)
251 .into_iter()
252 .map(jnum)
253 .collect();
254 let ncost = if cost.model == 1 {
259 coeffs.len() / 2
260 } else {
261 coeffs.len()
262 };
263 m.insert("model".into(), Value::from(u64::from(cost.model)));
264 m.insert("ncost".into(), Value::from(ncost as u64));
265 m.insert("startup".into(), jnum(cost.startup));
266 m.insert("shutdown".into(), jnum(cost.shutdown));
267 m.insert("cost".into(), Value::Array(coeffs));
268 }
269 m.insert("source_id".into(), source_id("gen", idx));
270 Value::Object(m)
271}
272
273fn load_obj(l: &Load, idx: usize, p: f64) -> Value {
274 let mut m = Map::new();
275 m.insert("index".into(), Value::from(idx as u64));
276 m.insert("load_bus".into(), Value::from(l.bus.0 as u64));
277 m.insert("pd".into(), jnum(l.p * p));
278 m.insert("qd".into(), jnum(l.q * p));
279 m.insert("status".into(), status_int(l.in_service));
280 m.insert("source_id".into(), source_id("bus", l.bus.0));
281 Value::Object(m)
282}
283
284fn shunt_obj(s: &Shunt, idx: usize, p: f64) -> Value {
285 let mut m = Map::new();
286 m.insert("index".into(), Value::from(idx as u64));
287 m.insert("shunt_bus".into(), Value::from(s.bus.0 as u64));
288 m.insert("gs".into(), jnum(s.g * p));
289 m.insert("bs".into(), jnum(s.b * p));
290 m.insert("status".into(), status_int(s.in_service));
291 m.insert("source_id".into(), source_id("bus", s.bus.0));
292 Value::Object(m)
293}
294
295fn dcline_obj(dc: &Hvdc, idx: usize, p: f64) -> Value {
296 let mut m = Map::new();
297 m.insert("index".into(), Value::from(idx as u64));
298 m.insert("f_bus".into(), Value::from(dc.from.0 as u64));
299 m.insert("t_bus".into(), Value::from(dc.to.0 as u64));
300 m.insert("br_status".into(), status_int(dc.in_service));
301 m.insert("pf".into(), jnum(dc.pf * p));
302 m.insert("pt".into(), jnum(-dc.pt * p));
304 m.insert("qf".into(), jnum(-dc.qf * p));
305 m.insert("qt".into(), jnum(-dc.qt * p));
306 m.insert("vf".into(), jnum(dc.vf));
307 m.insert("vt".into(), jnum(dc.vt));
308 let (pminf, pmaxf, pmint, pmaxt) = dcline_p_bounds(dc.pmin, dc.pmax, dc.loss0, dc.loss1);
313 m.insert("pminf".into(), jnum(pminf * p));
314 m.insert("pmaxf".into(), jnum(pmaxf * p));
315 m.insert("pmint".into(), jnum(pmint * p));
316 m.insert("pmaxt".into(), jnum(pmaxt * p));
317 m.insert("mp_pmin".into(), jnum(dc.pmin));
319 m.insert("mp_pmax".into(), jnum(dc.pmax));
320 m.insert("qminf".into(), jnum(dc.qminf * p));
321 m.insert("qmaxf".into(), jnum(dc.qmaxf * p));
322 m.insert("qmint".into(), jnum(dc.qmint * p));
323 m.insert("qmaxt".into(), jnum(dc.qmaxt * p));
324 m.insert("loss0".into(), jnum(dc.loss0 * p));
325 m.insert("loss1".into(), jnum(dc.loss1));
326 if let Some(cost) = &dc.cost {
327 let coeffs: Vec<Value> = normalize::cost_to_pu(cost, 1.0 / p)
328 .into_iter()
329 .map(jnum)
330 .collect();
331 let ncost = if cost.model == 1 {
332 coeffs.len() / 2
333 } else {
334 coeffs.len()
335 };
336 m.insert("model".into(), Value::from(u64::from(cost.model)));
337 m.insert("ncost".into(), Value::from(ncost as u64));
338 m.insert("startup".into(), jnum(cost.startup));
339 m.insert("shutdown".into(), jnum(cost.shutdown));
340 m.insert("cost".into(), Value::Array(coeffs));
341 }
342 m.insert("source_id".into(), source_id("dcline", idx));
343 Value::Object(m)
344}
345
346fn dcline_p_bounds(pmin: f64, pmax: f64, loss0: f64, loss1: f64) -> (f64, f64, f64, f64) {
350 let l = 1.0 - loss1;
351 if pmin >= 0.0 && pmax >= 0.0 {
352 (pmin, pmax, loss0 - pmax * l, loss0 - pmin * l)
353 } else if pmin >= 0.0 {
354 (pmin, (-pmax + loss0) / l, pmax, loss0 - pmin * l)
355 } else if pmax >= 0.0 {
356 ((pmin + loss0) / l, pmax, loss0 - pmax * l, -pmin)
357 } else {
358 ((pmin + loss0) / l, (-pmax + loss0) / l, pmax, -pmin)
359 }
360}
361
362fn storage_obj(st: &Storage, idx: usize, p: f64) -> Value {
363 let mut m = Map::new();
364 m.insert("index".into(), Value::from(idx as u64));
365 m.insert("storage_bus".into(), Value::from(st.bus.0 as u64));
366 m.insert("ps".into(), jnum(st.ps));
369 m.insert("qs".into(), jnum(st.qs));
370 m.insert("energy".into(), jnum(st.energy * p));
371 m.insert("energy_rating".into(), jnum(st.energy_rating * p));
372 m.insert("charge_rating".into(), jnum(st.charge_rating * p));
373 m.insert("discharge_rating".into(), jnum(st.discharge_rating * p));
374 m.insert("charge_efficiency".into(), jnum(st.charge_efficiency));
375 m.insert("discharge_efficiency".into(), jnum(st.discharge_efficiency));
376 m.insert("thermal_rating".into(), jnum(st.thermal_rating * p));
377 if let Some(current_rating) = st.current_rating {
378 m.insert("current_rating".into(), jnum(current_rating));
379 }
380 m.insert("qmin".into(), jnum(st.qmin * p));
381 m.insert("qmax".into(), jnum(st.qmax * p));
382 m.insert("r".into(), jnum(st.r));
383 m.insert("x".into(), jnum(st.x));
384 m.insert("p_loss".into(), jnum(st.p_loss * p));
385 m.insert("q_loss".into(), jnum(st.q_loss * p));
386 m.insert("status".into(), status_int(st.in_service));
387 m.insert("source_id".into(), source_id("storage", idx));
388 Value::Object(m)
389}
390
391fn switch_obj(sw: &Switch, idx: usize, p: f64) -> Value {
392 let mut m = Map::new();
393 m.insert("index".into(), Value::from(idx as u64));
394 m.insert("f_bus".into(), Value::from(sw.from.0 as u64));
395 m.insert("t_bus".into(), Value::from(sw.to.0 as u64));
396 m.insert("state".into(), status_int(sw.closed));
397 if let Some(rating) = sw.thermal_rating {
398 m.insert("thermal_rating".into(), jnum(rating * p));
399 }
400 if let Some(rating) = sw.current_rating {
401 m.insert("current_rating".into(), jnum(rating));
402 }
403 if let Some(pf) = sw.pf {
404 m.insert("pf".into(), jnum(pf * p));
405 }
406 if let Some(qf) = sw.qf {
407 m.insert("qf".into(), jnum(qf * p));
408 }
409 if let Some(pt) = sw.pt {
410 m.insert("pt".into(), jnum(pt * p));
411 }
412 if let Some(qt) = sw.qt {
413 m.insert("qt".into(), jnum(qt * p));
414 }
415 m.insert("source_id".into(), source_id("switch", idx));
416 Value::Object(m)
417}
418
419const FMT: &str = "PowerModels JSON";
422
423pub fn parse_powermodels_json(content: &str) -> Result<BalancedNetwork> {
431 let mut warnings = Vec::new();
432 parse_powermodels_json_source(Arc::new(content.to_owned()), None, &mut warnings)
433}
434
435pub(crate) fn parse_powermodels_json_source(
439 source: Arc<String>,
440 name_hint: Option<&str>,
441 warnings: &mut Vec<String>,
442) -> Result<BalancedNetwork> {
443 let content: &str = &source;
444 let root: Value = serde_json::from_str(content).map_err(|e| Error::FormatRead {
445 format: FMT,
446 message: e.to_string(),
447 })?;
448 let root = root.as_object().ok_or_else(|| Error::FormatRead {
449 format: FMT,
450 message: "top level is not a JSON object".into(),
451 })?;
452
453 let base_mva = root
457 .get("baseMVA")
458 .and_then(Value::as_f64)
459 .filter(|b| b.is_finite() && *b > 0.0)
460 .ok_or_else(|| Error::FormatRead {
461 format: FMT,
462 message: "missing, nonpositive, or non-finite numeric `baseMVA`".into(),
463 })?;
464 let per_unit = root
465 .get("per_unit")
466 .and_then(Value::as_bool)
467 .unwrap_or(false);
468 if root
469 .get("multinetwork")
470 .and_then(Value::as_bool)
471 .unwrap_or(false)
472 {
473 warnings.push("multinetwork=true: only the top-level single snapshot was read".into());
474 }
475 let pscale = if per_unit { base_mva } else { 1.0 };
476 let ascale = if per_unit { normalize::RAD_TO_DEG } else { 1.0 };
477 let name = root
478 .get("name")
479 .and_then(Value::as_str)
480 .or(name_hint)
481 .unwrap_or("case")
482 .to_string();
483
484 let net = BalancedNetwork {
485 name,
486 base_mva,
487 base_frequency: crate::network::DEFAULT_BASE_FREQUENCY,
488 geo: None,
489 buses: sorted(root, "bus", "index")
490 .iter()
491 .map(|v| read_bus(v, ascale))
492 .collect::<Result<Vec<_>>>()?,
493 loads: sorted(root, "load", "index")
494 .iter()
495 .map(|v| read_load(v, pscale))
496 .collect(),
497 shunts: sorted(root, "shunt", "index")
498 .iter()
499 .map(|v| read_shunt(v, pscale))
500 .collect(),
501 branches: read_branches(root, pscale, ascale, warnings),
502 switches: sorted(root, "switch", "index")
503 .iter()
504 .map(|v| read_switch(v, pscale))
505 .collect(),
506 generators: sorted(root, "gen", "index")
507 .iter()
508 .map(|v| read_gen(v, pscale, base_mva, per_unit))
509 .collect(),
510 storage: sorted(root, "storage", "index")
511 .iter()
512 .map(|v| read_storage(v, pscale))
513 .collect(),
514 hvdc: sorted(root, "dcline", "index")
515 .iter()
516 .map(|v| read_hvdc(v, pscale, base_mva, per_unit))
517 .collect(),
518 transformers_3w: Vec::new(),
519 areas: Vec::new(),
520 solver: None,
521 source_format: SourceFormat::PowerModelsJson,
522 source: Some(source),
523 };
524 net.check_references(FMT)?;
525 Ok(net)
526}
527
528fn sorted<'a>(root: &'a Map<String, Value>, section: &str, idx_key: &str) -> Vec<&'a Value> {
531 sorted_keyed(root, section, idx_key)
532 .into_iter()
533 .map(|(_, v)| v)
534 .collect()
535}
536
537fn sorted_keyed<'a>(
541 root: &'a Map<String, Value>,
542 section: &str,
543 idx_key: &str,
544) -> Vec<(&'a str, &'a Value)> {
545 let Some(obj) = root.get(section).and_then(Value::as_object) else {
546 return Vec::new();
547 };
548 let mut items: Vec<(&str, &Value)> = obj.iter().map(|(k, v)| (k.as_str(), v)).collect();
549 items.sort_by_key(|(_, v)| v.get(idx_key).and_then(Value::as_i64).unwrap_or(0));
550 items
551}
552
553fn f(v: &Value, key: &str) -> f64 {
554 v.get(key).and_then(Value::as_f64).unwrap_or(0.0)
555}
556fn f_or(v: &Value, key: &str, default: f64) -> f64 {
557 v.get(key).and_then(Value::as_f64).unwrap_or(default)
558}
559fn uid(v: &Value, key: &str) -> usize {
560 v.get(key).and_then(Value::as_u64).unwrap_or(0) as usize
561}
562fn flag(v: &Value, key: &str) -> bool {
567 match v.get(key) {
568 Some(Value::Bool(b)) => *b,
569 Some(value) => value.as_f64() != Some(0.0),
570 None => true,
571 }
572}
573
574fn bustype(code: i64) -> BusType {
575 match code {
576 2 => BusType::Pv,
577 3 => BusType::Ref,
578 4 => BusType::Isolated,
579 _ => BusType::Pq,
580 }
581}
582
583fn extras_excluding(v: &Value, known: &[&str]) -> crate::network::Extras {
586 v.as_object().map_or_else(Default::default, |obj| {
587 obj.iter()
588 .filter(|(k, _)| !known.contains(&k.as_str()))
589 .map(|(k, val)| (k.clone(), val.clone()))
590 .collect()
591 })
592}
593
594fn read_bus(v: &Value, ascale: f64) -> Result<Bus> {
595 let id = v
596 .get("bus_i")
597 .or_else(|| v.get("index"))
598 .and_then(Value::as_u64)
599 .ok_or_else(|| Error::FormatRead {
600 format: FMT,
601 message: "bus record missing integer `bus_i`".into(),
602 })? as usize;
603 Ok(Bus {
604 id: BusId(id),
605 kind: bustype(v.get("bus_type").and_then(Value::as_i64).unwrap_or(1)),
606 vm: f_or(v, "vm", 1.0),
607 va: f(v, "va") * ascale,
608 base_kv: f(v, "base_kv"),
609 vmax: f(v, "vmax"),
610 vmin: f(v, "vmin"),
611 evhi: None,
612 evlo: None,
613 area: uid(v, "area"),
614 zone: uid(v, "zone"),
615 name: v.get("name").and_then(Value::as_str).map(str::to_string),
616 uid: None,
617 location: None,
618 extras: extras_excluding(
619 v,
620 &[
621 "bus_i",
622 "index",
623 "bus_type",
624 "vm",
625 "va",
626 "vmax",
627 "vmin",
628 "base_kv",
629 "area",
630 "zone",
631 "name",
632 "source_id",
633 ],
634 ),
635 })
636}
637
638fn read_load(v: &Value, pscale: f64) -> Load {
639 Load {
640 bus: BusId(uid(v, "load_bus")),
641 p: f(v, "pd") * pscale,
642 q: f(v, "qd") * pscale,
643 voltage_model: None,
644 in_service: flag(v, "status"),
645 uid: None,
646 extras: extras_excluding(v, &["load_bus", "pd", "qd", "status", "index", "source_id"]),
647 }
648}
649
650fn read_shunt(v: &Value, pscale: f64) -> Shunt {
651 Shunt {
652 bus: BusId(uid(v, "shunt_bus")),
653 g: f(v, "gs") * pscale,
654 b: f(v, "bs") * pscale,
655 in_service: flag(v, "status"),
656 control: None,
657 uid: None,
658 extras: extras_excluding(
659 v,
660 &["shunt_bus", "gs", "bs", "status", "index", "source_id"],
661 ),
662 }
663}
664
665fn read_branches(
670 root: &Map<String, Value>,
671 pscale: f64,
672 ascale: f64,
673 warnings: &mut Vec<String>,
674) -> Vec<Branch> {
675 const NAMED: usize = 3;
676 let mut discarded: Vec<String> = Vec::new();
677 let branches = sorted_keyed(root, "branch", "index")
678 .iter()
679 .map(|(key, v)| read_branch(v, pscale, ascale, key, &mut discarded))
680 .collect();
681 if !discarded.is_empty() {
682 let head = discarded
683 .iter()
684 .take(NAMED)
685 .cloned()
686 .collect::<Vec<_>>()
687 .join(", ");
688 let rest = discarded.len().saturating_sub(NAMED);
689 let tail = if rest > 0 {
690 format!(" and {rest} more")
691 } else {
692 String::new()
693 };
694 warnings.push(format!(
695 "{} branch(es) carry an off-nominal `tap` without `transformer: true`, \
696 so the tap is discarded and the branch reads as a line: {head}{tail}",
697 discarded.len()
698 ));
699 }
700 branches
701}
702
703#[allow(clippy::float_cmp)]
706fn read_branch(
707 v: &Value,
708 pscale: f64,
709 ascale: f64,
710 key: &str,
711 discarded: &mut Vec<String>,
712) -> Branch {
713 let transformer = v
717 .get("transformer")
718 .and_then(Value::as_bool)
719 .unwrap_or(false);
720 let tap = if transformer {
721 f_or(v, "tap", 1.0)
722 } else {
723 if let Some(raw) = v.get("tap").and_then(Value::as_f64) {
727 if raw != 0.0 && raw != 1.0 {
728 discarded.push(format!(
729 "`{key}` ({} -> {}) tap {raw}",
730 uid(v, "f_bus"),
731 uid(v, "t_bus"),
732 ));
733 }
734 }
735 0.0
736 };
737 Branch {
738 from: BusId(uid(v, "f_bus")),
739 to: BusId(uid(v, "t_bus")),
740 r: f(v, "br_r"),
741 x: f(v, "br_x"),
742 b: f(v, "b_fr") + f(v, "b_to"),
743 charging: Some(BranchCharging {
744 g_fr: f(v, "g_fr"),
745 b_fr: f(v, "b_fr"),
746 g_to: f(v, "g_to"),
747 b_to: f(v, "b_to"),
748 }),
749 rate_a: f(v, "rate_a") * pscale,
750 rate_b: f(v, "rate_b") * pscale,
751 rate_c: f(v, "rate_c") * pscale,
752 rating_sets: Vec::new(),
753 current_ratings: has_any(v, &["c_rating_a", "c_rating_b", "c_rating_c"]).then_some(
754 BranchCurrentRatings {
755 c_rating_a: f(v, "c_rating_a"),
756 c_rating_b: f(v, "c_rating_b"),
757 c_rating_c: f(v, "c_rating_c"),
758 },
759 ),
760 tap,
761 shift: f(v, "shift") * ascale,
762 in_service: flag(v, "br_status"),
763 angmin: f(v, "angmin") * ascale,
764 angmax: f(v, "angmax") * ascale,
765 control: None,
766 solution: has_any(v, &["pf", "qf", "pt", "qt"]).then_some(BranchSolution {
767 pf: f(v, "pf") * pscale,
768 qf: f(v, "qf") * pscale,
769 pt: f(v, "pt") * pscale,
770 qt: f(v, "qt") * pscale,
771 }),
772 uid: None,
773 route: None,
774 extras: extras_excluding(
775 v,
776 &[
777 "f_bus",
778 "t_bus",
779 "br_r",
780 "br_x",
781 "b_fr",
782 "b_to",
783 "g_fr",
784 "g_to",
785 "tap",
786 "shift",
787 "br_status",
788 "angmin",
789 "angmax",
790 "transformer",
791 "rate_a",
792 "rate_b",
793 "rate_c",
794 "c_rating_a",
795 "c_rating_b",
796 "c_rating_c",
797 "pf",
798 "qf",
799 "pt",
800 "qt",
801 "index",
802 "source_id",
803 ],
804 ),
805 }
806}
807
808fn has_any(v: &Value, keys: &[&str]) -> bool {
809 keys.iter().any(|key| v.get(*key).is_some())
810}
811
812fn read_switch(v: &Value, pscale: f64) -> Switch {
813 let closed = if v.get("state").is_some() {
814 flag(v, "state")
815 } else {
816 flag(v, "status")
817 };
818 Switch {
819 from: BusId(uid(v, "f_bus")),
820 to: BusId(uid(v, "t_bus")),
821 closed,
822 thermal_rating: v
823 .get("thermal_rating")
824 .and_then(Value::as_f64)
825 .map(|x| x * pscale),
826 current_rating: v.get("current_rating").and_then(Value::as_f64),
827 pf: v.get("pf").and_then(Value::as_f64).map(|x| x * pscale),
828 qf: v.get("qf").and_then(Value::as_f64).map(|x| x * pscale),
829 pt: v.get("pt").and_then(Value::as_f64).map(|x| x * pscale),
830 qt: v.get("qt").and_then(Value::as_f64).map(|x| x * pscale),
831 uid: None,
832 extras: extras_excluding(
833 v,
834 &[
835 "f_bus",
836 "t_bus",
837 "state",
838 "status",
839 "thermal_rating",
840 "current_rating",
841 "pf",
842 "qf",
843 "pt",
844 "qt",
845 "index",
846 "source_id",
847 ],
848 ),
849 }
850}
851
852fn read_gen(v: &Value, pscale: f64, base_mva: f64, per_unit: bool) -> Generator {
853 let mut caps: crate::network::GenCaps = [None; GEN_EXTRA_KEYS.len()];
854 for (i, key) in GEN_EXTRA_KEYS.iter().enumerate() {
855 if let Some(val) = v.get(*key).and_then(Value::as_f64) {
856 caps[i] = Some(if GEN_PU_KEYS.contains(key) {
858 val * pscale
859 } else {
860 val
861 });
862 }
863 }
864 let cost = v.get("model").map(|_| read_cost(v, base_mva, per_unit));
865 Generator {
866 bus: BusId(uid(v, "gen_bus")),
867 pg: f(v, "pg") * pscale,
868 qg: f(v, "qg") * pscale,
869 pmax: f_or(v, "pmax", f64::INFINITY) * pscale,
872 pmin: f_or(v, "pmin", f64::NEG_INFINITY) * pscale,
873 qmax: f_or(v, "qmax", f64::INFINITY) * pscale,
874 qmin: f_or(v, "qmin", f64::NEG_INFINITY) * pscale,
875 vg: f_or(v, "vg", 1.0),
876 mbase: f_or(v, "mbase", base_mva),
877 in_service: flag(v, "gen_status"),
878 cost,
879 caps,
880 regulated_bus: None,
881 uid: None,
882 }
883}
884
885fn read_cost(v: &Value, base_mva: f64, per_unit: bool) -> GenCost {
886 let mut coeffs_raw: Vec<f64> = v
889 .get("cost")
890 .and_then(Value::as_array)
891 .map(|a| a.iter().map(|c| c.as_f64().unwrap_or(f64::NAN)).collect())
892 .unwrap_or_default();
893 let model = v
897 .get("model")
898 .and_then(Value::as_u64)
899 .map_or(2, |m| u8::try_from(m).unwrap_or(u8::MAX));
900 let declared_ncost = v
908 .get("ncost")
909 .and_then(Value::as_u64)
910 .and_then(|n| usize::try_from(n).ok());
911 if let Some(n) = declared_ncost {
912 let keep = if model == 1 { n.saturating_mul(2) } else { n };
913 if keep < coeffs_raw.len() {
914 coeffs_raw.truncate(keep);
915 }
916 }
917 let k = coeffs_raw.len();
918 let coeffs = if per_unit {
922 normalize::cost_from_pu(&coeffs_raw, model, base_mva)
923 } else {
924 coeffs_raw
925 };
926 let default_ncost = if model == 1 { k / 2 } else { k };
929 GenCost {
930 model,
931 startup: f(v, "startup"),
932 shutdown: f(v, "shutdown"),
933 ncost: declared_ncost.map_or(default_ncost, |n| n.min(default_ncost)),
936 coeffs,
937 }
938}
939
940fn read_hvdc(v: &Value, pscale: f64, base_mva: f64, per_unit: bool) -> Hvdc {
941 let pmin = v
944 .get("mp_pmin")
945 .and_then(Value::as_f64)
946 .unwrap_or_else(|| f(v, "pminf") * pscale);
947 let pmax = v
948 .get("mp_pmax")
949 .and_then(Value::as_f64)
950 .unwrap_or_else(|| f(v, "pmaxf") * pscale);
951 Hvdc {
952 from: BusId(uid(v, "f_bus")),
953 to: BusId(uid(v, "t_bus")),
954 in_service: flag(v, "br_status"),
955 pf: f(v, "pf") * pscale,
956 pt: -f(v, "pt") * pscale,
958 qf: -f(v, "qf") * pscale,
959 qt: -f(v, "qt") * pscale,
960 vf: f_or(v, "vf", 1.0),
961 vt: f_or(v, "vt", 1.0),
962 pmin,
963 pmax,
964 qminf: f_or(v, "qminf", f64::NEG_INFINITY) * pscale,
966 qmaxf: f_or(v, "qmaxf", f64::INFINITY) * pscale,
967 qmint: f_or(v, "qmint", f64::NEG_INFINITY) * pscale,
968 qmaxt: f_or(v, "qmaxt", f64::INFINITY) * pscale,
969 loss0: f(v, "loss0") * pscale,
970 loss1: f(v, "loss1"),
971 cost: v.get("model").map(|_| read_cost(v, base_mva, per_unit)),
972 uid: None,
973 extras: extras_excluding(
974 v,
975 &[
976 "f_bus",
977 "t_bus",
978 "br_status",
979 "pf",
980 "pt",
981 "qf",
982 "qt",
983 "vf",
984 "vt",
985 "pmin",
986 "pmax",
987 "mp_pmin",
988 "mp_pmax",
989 "pminf",
990 "pmaxf",
991 "pmint",
992 "pmaxt",
993 "qminf",
994 "qmaxf",
995 "qmint",
996 "qmaxt",
997 "loss0",
998 "loss1",
999 "model",
1000 "ncost",
1001 "startup",
1002 "shutdown",
1003 "cost",
1004 "index",
1005 "source_id",
1006 ],
1007 ),
1008 }
1009}
1010
1011fn read_storage(v: &Value, pscale: f64) -> Storage {
1012 Storage {
1013 bus: BusId(uid(v, "storage_bus")),
1014 ps: f(v, "ps"),
1015 qs: f(v, "qs"),
1016 energy: f(v, "energy") * pscale,
1017 energy_rating: f(v, "energy_rating") * pscale,
1018 charge_rating: f(v, "charge_rating") * pscale,
1019 discharge_rating: f(v, "discharge_rating") * pscale,
1020 charge_efficiency: f_or(v, "charge_efficiency", 1.0),
1021 discharge_efficiency: f_or(v, "discharge_efficiency", 1.0),
1022 thermal_rating: f(v, "thermal_rating") * pscale,
1023 current_rating: v.get("current_rating").and_then(Value::as_f64),
1024 qmin: f_or(v, "qmin", f64::NEG_INFINITY) * pscale,
1026 qmax: f_or(v, "qmax", f64::INFINITY) * pscale,
1027 r: f(v, "r"),
1028 x: f(v, "x"),
1029 p_loss: f(v, "p_loss") * pscale,
1030 q_loss: f(v, "q_loss") * pscale,
1031 in_service: flag(v, "status"),
1032 uid: None,
1033 extras: extras_excluding(
1034 v,
1035 &[
1036 "storage_bus",
1037 "ps",
1038 "qs",
1039 "energy",
1040 "energy_rating",
1041 "charge_rating",
1042 "discharge_rating",
1043 "charge_efficiency",
1044 "discharge_efficiency",
1045 "thermal_rating",
1046 "current_rating",
1047 "qmin",
1048 "qmax",
1049 "r",
1050 "x",
1051 "p_loss",
1052 "q_loss",
1053 "status",
1054 "index",
1055 "source_id",
1056 ],
1057 ),
1058 }
1059}
1060
1061#[cfg(test)]
1062mod tests {
1063 use super::*;
1064
1065 fn approx(a: f64, b: f64) -> bool {
1066 (a - b).abs() <= 1e-9 * a.abs().max(b.abs()).max(1.0)
1067 }
1068
1069 #[test]
1070 fn boolean_status_fields_read_out_of_service() {
1071 let doc = r#"{"baseMVA":100.0,"per_unit":true,
1072 "bus":{"1":{"bus_i":1,"bus_type":3,"vm":1.0,"va":0.0,"base_kv":345.0},
1073 "2":{"bus_i":2,"bus_type":1,"vm":1.0,"va":0.0,"base_kv":345.0}},
1074 "branch":{"1":{"f_bus":1,"t_bus":2,"br_r":0.01,"br_x":0.1,"br_status":false}},
1075 "gen":{"1":{"gen_bus":1,"pg":0.5,"gen_status":false}}}"#;
1076 let net = parse_powermodels_json(doc).unwrap();
1077 assert!(
1078 !net.branches[0].in_service,
1079 "br_status: false must read out of service"
1080 );
1081 assert!(
1082 !net.generators[0].in_service,
1083 "gen_status: false must read out of service"
1084 );
1085 }
1086
1087 #[test]
1088 #[allow(clippy::float_cmp)] fn nonunit_tap_without_transformer_flag_warns_and_stays_a_line() {
1090 let doc = r#"{"baseMVA":100.0,"per_unit":true,
1091 "bus":{"1":{"bus_i":1,"bus_type":3,"vm":1.0,"va":0.0,"base_kv":345.0},
1092 "2":{"bus_i":2,"bus_type":1,"vm":1.0,"va":0.0,"base_kv":345.0}},
1093 "branch":{"1":{"index":1,"f_bus":1,"t_bus":2,"br_r":0.01,"br_x":0.1,"tap":1.05},
1094 "2":{"index":2,"f_bus":1,"t_bus":2,"br_r":0.01,"br_x":0.1,"tap":1.0},
1095 "3":{"index":3,"f_bus":1,"t_bus":2,"br_r":0.01,"br_x":0.1,
1096 "tap":1.05,"transformer":true}}}"#;
1097 let mut warnings = Vec::new();
1098 let net =
1099 parse_powermodels_json_source(Arc::new(doc.to_owned()), None, &mut warnings).unwrap();
1100 assert_eq!(net.branches[0].tap, 0.0);
1103 assert_eq!(net.branches[1].tap, 0.0);
1104 assert_eq!(net.branches[2].tap, 1.05);
1105 assert_eq!(warnings.len(), 1, "{warnings:?}");
1108 assert!(
1109 warnings[0].contains("1 branch(es)") && warnings[0].contains("`1` (1 -> 2) tap 1.05"),
1110 "{warnings:?}"
1111 );
1112 }
1113
1114 #[test]
1115 fn nonpositive_base_mva_is_rejected() {
1116 for base in ["0.0", "-100.0", "1e999"] {
1117 let doc = format!(
1118 r#"{{"baseMVA":{base},"bus":{{"1":{{"bus_i":1,"bus_type":3,"vm":1.0,"va":0.0,"base_kv":345.0}}}}}}"#
1119 );
1120 assert!(
1121 parse_powermodels_json(&doc).is_err(),
1122 "baseMVA {base} must be rejected"
1123 );
1124 }
1125 }
1126
1127 #[test]
1128 fn padded_cost_rows_trim_to_ncost_before_unscaling() {
1129 let v: Value = serde_json::json!({
1134 "gen_bus": 1, "model": 2, "ncost": 3,
1135 "cost": [1.0, 1.0, 1.0, 0.0]
1136 });
1137 let cost = read_cost(&v, 100.0, true);
1138 assert_eq!(cost.ncost, 3);
1139 assert_eq!(cost.coeffs.len(), 3);
1140 assert!(approx(cost.coeffs[0], 1e-4));
1141 assert!(approx(cost.coeffs[1], 1e-2));
1142 assert!(approx(cost.coeffs[2], 1.0));
1143 }
1144
1145 #[test]
1146 fn out_of_range_cost_model_does_not_wrap_into_rescaling() {
1147 let v: Value = serde_json::json!({
1151 "gen_bus": 1, "model": 257,
1152 "cost": [10.0, 5.0]
1153 });
1154 let cost = read_cost(&v, 100.0, true);
1155 assert_eq!(cost.coeffs, vec![10.0, 5.0]);
1156 }
1157
1158 #[test]
1159 fn gen_pu_keys_subset_of_extra_keys() {
1160 for k in GEN_PU_KEYS {
1164 assert!(
1165 GEN_EXTRA_KEYS.contains(&k),
1166 "{k} is not a GEN_EXTRA_KEYS column"
1167 );
1168 }
1169 }
1170
1171 #[test]
1172 fn dcline_p_bounds_four_quadrants() {
1173 let q1 = dcline_p_bounds(2.0, 10.0, 1.0, 0.1);
1176 assert!(
1177 approx(q1.0, 2.0) && approx(q1.1, 10.0) && approx(q1.2, -8.0) && approx(q1.3, -0.8)
1178 );
1179
1180 let q2 = dcline_p_bounds(2.0, -5.0, 1.0, 0.1);
1181 assert!(
1182 approx(q2.0, 2.0)
1183 && approx(q2.1, 6.0 / 0.9)
1184 && approx(q2.2, -5.0)
1185 && approx(q2.3, -0.8)
1186 );
1187
1188 let q3 = dcline_p_bounds(-3.0, 10.0, 1.0, 0.1);
1189 assert!(
1190 approx(q3.0, -2.0 / 0.9)
1191 && approx(q3.1, 10.0)
1192 && approx(q3.2, -8.0)
1193 && approx(q3.3, 3.0)
1194 );
1195
1196 let q4 = dcline_p_bounds(-3.0, -5.0, 1.0, 0.1);
1197 assert!(
1198 approx(q4.0, -2.0 / 0.9)
1199 && approx(q4.1, 6.0 / 0.9)
1200 && approx(q4.2, -5.0)
1201 && approx(q4.3, 3.0)
1202 );
1203 }
1204}