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 Branch, BranchCharging, BranchCurrentRatings, BranchSolution, Bus, BusId, BusType,
23 GEN_EXTRA_KEYS, GenCost, Generator, Hvdc, Load, LoadVoltageModel, Network, 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: &Network) -> 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<Network> {
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<Network> {
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 =
454 root.get("baseMVA")
455 .and_then(Value::as_f64)
456 .ok_or_else(|| Error::FormatRead {
457 format: FMT,
458 message: "missing numeric `baseMVA`".into(),
459 })?;
460 let per_unit = root
461 .get("per_unit")
462 .and_then(Value::as_bool)
463 .unwrap_or(false);
464 if root
465 .get("multinetwork")
466 .and_then(Value::as_bool)
467 .unwrap_or(false)
468 {
469 warnings.push("multinetwork=true: only the top-level single snapshot was read".into());
470 }
471 let pscale = if per_unit { base_mva } else { 1.0 };
472 let ascale = if per_unit { normalize::RAD_TO_DEG } else { 1.0 };
473 let name = root
474 .get("name")
475 .and_then(Value::as_str)
476 .or(name_hint)
477 .unwrap_or("case")
478 .to_string();
479
480 let net = Network {
481 name,
482 base_mva,
483 base_frequency: crate::network::DEFAULT_BASE_FREQUENCY,
484 buses: sorted(root, "bus", "index")
485 .iter()
486 .map(|v| read_bus(v, ascale))
487 .collect::<Result<Vec<_>>>()?,
488 loads: sorted(root, "load", "index")
489 .iter()
490 .map(|v| read_load(v, pscale))
491 .collect(),
492 shunts: sorted(root, "shunt", "index")
493 .iter()
494 .map(|v| read_shunt(v, pscale))
495 .collect(),
496 branches: sorted(root, "branch", "index")
497 .iter()
498 .map(|v| read_branch(v, pscale, ascale))
499 .collect(),
500 switches: sorted(root, "switch", "index")
501 .iter()
502 .map(|v| read_switch(v, pscale))
503 .collect(),
504 generators: sorted(root, "gen", "index")
505 .iter()
506 .map(|v| read_gen(v, pscale, base_mva, per_unit))
507 .collect(),
508 storage: sorted(root, "storage", "index")
509 .iter()
510 .map(|v| read_storage(v, pscale))
511 .collect(),
512 hvdc: sorted(root, "dcline", "index")
513 .iter()
514 .map(|v| read_hvdc(v, pscale, base_mva, per_unit))
515 .collect(),
516 transformers_3w: Vec::new(),
517 areas: Vec::new(),
518 solver: None,
519 source_format: SourceFormat::PowerModelsJson,
520 source: Some(source),
521 };
522 net.check_references(FMT)?;
523 Ok(net)
524}
525
526fn sorted<'a>(root: &'a Map<String, Value>, section: &str, idx_key: &str) -> Vec<&'a Value> {
529 let Some(obj) = root.get(section).and_then(Value::as_object) else {
530 return Vec::new();
531 };
532 let mut items: Vec<&Value> = obj.values().collect();
533 items.sort_by_key(|v| v.get(idx_key).and_then(Value::as_i64).unwrap_or(0));
534 items
535}
536
537fn f(v: &Value, key: &str) -> f64 {
538 v.get(key).and_then(Value::as_f64).unwrap_or(0.0)
539}
540fn f_or(v: &Value, key: &str, default: f64) -> f64 {
541 v.get(key).and_then(Value::as_f64).unwrap_or(default)
542}
543fn uid(v: &Value, key: &str) -> usize {
544 v.get(key).and_then(Value::as_u64).unwrap_or(0) as usize
545}
546fn flag(v: &Value, key: &str) -> bool {
548 v.get(key).and_then(Value::as_f64) != Some(0.0)
549}
550
551fn bustype(code: i64) -> BusType {
552 match code {
553 2 => BusType::Pv,
554 3 => BusType::Ref,
555 4 => BusType::Isolated,
556 _ => BusType::Pq,
557 }
558}
559
560fn extras_excluding(v: &Value, known: &[&str]) -> crate::network::Extras {
563 v.as_object().map_or_else(Default::default, |obj| {
564 obj.iter()
565 .filter(|(k, _)| !known.contains(&k.as_str()))
566 .map(|(k, val)| (k.clone(), val.clone()))
567 .collect()
568 })
569}
570
571fn read_bus(v: &Value, ascale: f64) -> Result<Bus> {
572 let id = v
573 .get("bus_i")
574 .or_else(|| v.get("index"))
575 .and_then(Value::as_u64)
576 .ok_or_else(|| Error::FormatRead {
577 format: FMT,
578 message: "bus record missing integer `bus_i`".into(),
579 })? as usize;
580 Ok(Bus {
581 id: BusId(id),
582 kind: bustype(v.get("bus_type").and_then(Value::as_i64).unwrap_or(1)),
583 vm: f_or(v, "vm", 1.0),
584 va: f(v, "va") * ascale,
585 base_kv: f(v, "base_kv"),
586 vmax: f(v, "vmax"),
587 vmin: f(v, "vmin"),
588 evhi: None,
589 evlo: None,
590 area: uid(v, "area"),
591 zone: uid(v, "zone"),
592 name: v.get("name").and_then(Value::as_str).map(str::to_string),
593 uid: None,
594 extras: extras_excluding(
595 v,
596 &[
597 "bus_i",
598 "index",
599 "bus_type",
600 "vm",
601 "va",
602 "vmax",
603 "vmin",
604 "base_kv",
605 "area",
606 "zone",
607 "name",
608 "source_id",
609 ],
610 ),
611 })
612}
613
614fn read_load(v: &Value, pscale: f64) -> Load {
615 Load {
616 bus: BusId(uid(v, "load_bus")),
617 p: f(v, "pd") * pscale,
618 q: f(v, "qd") * pscale,
619 voltage_model: None,
620 in_service: flag(v, "status"),
621 uid: None,
622 extras: extras_excluding(v, &["load_bus", "pd", "qd", "status", "index", "source_id"]),
623 }
624}
625
626fn read_shunt(v: &Value, pscale: f64) -> Shunt {
627 Shunt {
628 bus: BusId(uid(v, "shunt_bus")),
629 g: f(v, "gs") * pscale,
630 b: f(v, "bs") * pscale,
631 in_service: flag(v, "status"),
632 control: None,
633 uid: None,
634 extras: extras_excluding(
635 v,
636 &["shunt_bus", "gs", "bs", "status", "index", "source_id"],
637 ),
638 }
639}
640
641fn read_branch(v: &Value, pscale: f64, ascale: f64) -> Branch {
642 let transformer = v
646 .get("transformer")
647 .and_then(Value::as_bool)
648 .unwrap_or(false);
649 let tap = if transformer {
650 f_or(v, "tap", 1.0)
651 } else {
652 0.0
653 };
654 Branch {
655 from: BusId(uid(v, "f_bus")),
656 to: BusId(uid(v, "t_bus")),
657 r: f(v, "br_r"),
658 x: f(v, "br_x"),
659 b: f(v, "b_fr") + f(v, "b_to"),
660 charging: Some(BranchCharging {
661 g_fr: f(v, "g_fr"),
662 b_fr: f(v, "b_fr"),
663 g_to: f(v, "g_to"),
664 b_to: f(v, "b_to"),
665 }),
666 rate_a: f(v, "rate_a") * pscale,
667 rate_b: f(v, "rate_b") * pscale,
668 rate_c: f(v, "rate_c") * pscale,
669 rating_sets: Vec::new(),
670 current_ratings: has_any(v, &["c_rating_a", "c_rating_b", "c_rating_c"]).then_some(
671 BranchCurrentRatings {
672 c_rating_a: f(v, "c_rating_a"),
673 c_rating_b: f(v, "c_rating_b"),
674 c_rating_c: f(v, "c_rating_c"),
675 },
676 ),
677 tap,
678 shift: f(v, "shift") * ascale,
679 in_service: flag(v, "br_status"),
680 angmin: f(v, "angmin") * ascale,
681 angmax: f(v, "angmax") * ascale,
682 control: None,
683 solution: has_any(v, &["pf", "qf", "pt", "qt"]).then_some(BranchSolution {
684 pf: f(v, "pf") * pscale,
685 qf: f(v, "qf") * pscale,
686 pt: f(v, "pt") * pscale,
687 qt: f(v, "qt") * pscale,
688 }),
689 uid: None,
690 extras: extras_excluding(
691 v,
692 &[
693 "f_bus",
694 "t_bus",
695 "br_r",
696 "br_x",
697 "b_fr",
698 "b_to",
699 "g_fr",
700 "g_to",
701 "tap",
702 "shift",
703 "br_status",
704 "angmin",
705 "angmax",
706 "transformer",
707 "rate_a",
708 "rate_b",
709 "rate_c",
710 "c_rating_a",
711 "c_rating_b",
712 "c_rating_c",
713 "pf",
714 "qf",
715 "pt",
716 "qt",
717 "index",
718 "source_id",
719 ],
720 ),
721 }
722}
723
724fn has_any(v: &Value, keys: &[&str]) -> bool {
725 keys.iter().any(|key| v.get(*key).is_some())
726}
727
728fn read_switch(v: &Value, pscale: f64) -> Switch {
729 let closed = if v.get("state").is_some() {
730 flag(v, "state")
731 } else {
732 flag(v, "status")
733 };
734 Switch {
735 from: BusId(uid(v, "f_bus")),
736 to: BusId(uid(v, "t_bus")),
737 closed,
738 thermal_rating: v
739 .get("thermal_rating")
740 .and_then(Value::as_f64)
741 .map(|x| x * pscale),
742 current_rating: v.get("current_rating").and_then(Value::as_f64),
743 pf: v.get("pf").and_then(Value::as_f64).map(|x| x * pscale),
744 qf: v.get("qf").and_then(Value::as_f64).map(|x| x * pscale),
745 pt: v.get("pt").and_then(Value::as_f64).map(|x| x * pscale),
746 qt: v.get("qt").and_then(Value::as_f64).map(|x| x * pscale),
747 uid: None,
748 extras: extras_excluding(
749 v,
750 &[
751 "f_bus",
752 "t_bus",
753 "state",
754 "status",
755 "thermal_rating",
756 "current_rating",
757 "pf",
758 "qf",
759 "pt",
760 "qt",
761 "index",
762 "source_id",
763 ],
764 ),
765 }
766}
767
768fn read_gen(v: &Value, pscale: f64, base_mva: f64, per_unit: bool) -> Generator {
769 let mut caps: crate::network::GenCaps = [None; GEN_EXTRA_KEYS.len()];
770 for (i, key) in GEN_EXTRA_KEYS.iter().enumerate() {
771 if let Some(val) = v.get(*key).and_then(Value::as_f64) {
772 caps[i] = Some(if GEN_PU_KEYS.contains(key) {
774 val * pscale
775 } else {
776 val
777 });
778 }
779 }
780 let cost = v.get("model").map(|_| read_cost(v, base_mva, per_unit));
781 Generator {
782 bus: BusId(uid(v, "gen_bus")),
783 pg: f(v, "pg") * pscale,
784 qg: f(v, "qg") * pscale,
785 pmax: f_or(v, "pmax", f64::INFINITY) * pscale,
788 pmin: f_or(v, "pmin", f64::NEG_INFINITY) * pscale,
789 qmax: f_or(v, "qmax", f64::INFINITY) * pscale,
790 qmin: f_or(v, "qmin", f64::NEG_INFINITY) * pscale,
791 vg: f_or(v, "vg", 1.0),
792 mbase: f_or(v, "mbase", base_mva),
793 in_service: flag(v, "gen_status"),
794 cost,
795 caps,
796 regulated_bus: None,
797 uid: None,
798 }
799}
800
801fn read_cost(v: &Value, base_mva: f64, per_unit: bool) -> GenCost {
802 let coeffs_raw: Vec<f64> = v
805 .get("cost")
806 .and_then(Value::as_array)
807 .map(|a| a.iter().map(|c| c.as_f64().unwrap_or(f64::NAN)).collect())
808 .unwrap_or_default();
809 let model = v.get("model").and_then(Value::as_u64).unwrap_or(2) as u8;
810 let k = coeffs_raw.len();
811 let coeffs = if per_unit {
815 normalize::cost_from_pu(&coeffs_raw, model, base_mva)
816 } else {
817 coeffs_raw
818 };
819 let default_ncost = if model == 1 { k / 2 } else { k };
822 GenCost {
823 model,
824 startup: f(v, "startup"),
825 shutdown: f(v, "shutdown"),
826 ncost: v
827 .get("ncost")
828 .and_then(Value::as_u64)
829 .map_or(default_ncost, |n| n as usize),
830 coeffs,
831 }
832}
833
834fn read_hvdc(v: &Value, pscale: f64, base_mva: f64, per_unit: bool) -> Hvdc {
835 let pmin = v
838 .get("mp_pmin")
839 .and_then(Value::as_f64)
840 .unwrap_or_else(|| f(v, "pminf") * pscale);
841 let pmax = v
842 .get("mp_pmax")
843 .and_then(Value::as_f64)
844 .unwrap_or_else(|| f(v, "pmaxf") * pscale);
845 Hvdc {
846 from: BusId(uid(v, "f_bus")),
847 to: BusId(uid(v, "t_bus")),
848 in_service: flag(v, "br_status"),
849 pf: f(v, "pf") * pscale,
850 pt: -f(v, "pt") * pscale,
852 qf: -f(v, "qf") * pscale,
853 qt: -f(v, "qt") * pscale,
854 vf: f_or(v, "vf", 1.0),
855 vt: f_or(v, "vt", 1.0),
856 pmin,
857 pmax,
858 qminf: f_or(v, "qminf", f64::NEG_INFINITY) * pscale,
860 qmaxf: f_or(v, "qmaxf", f64::INFINITY) * pscale,
861 qmint: f_or(v, "qmint", f64::NEG_INFINITY) * pscale,
862 qmaxt: f_or(v, "qmaxt", f64::INFINITY) * pscale,
863 loss0: f(v, "loss0") * pscale,
864 loss1: f(v, "loss1"),
865 cost: v.get("model").map(|_| read_cost(v, base_mva, per_unit)),
866 uid: None,
867 extras: extras_excluding(
868 v,
869 &[
870 "f_bus",
871 "t_bus",
872 "br_status",
873 "pf",
874 "pt",
875 "qf",
876 "qt",
877 "vf",
878 "vt",
879 "pmin",
880 "pmax",
881 "mp_pmin",
882 "mp_pmax",
883 "pminf",
884 "pmaxf",
885 "pmint",
886 "pmaxt",
887 "qminf",
888 "qmaxf",
889 "qmint",
890 "qmaxt",
891 "loss0",
892 "loss1",
893 "model",
894 "ncost",
895 "startup",
896 "shutdown",
897 "cost",
898 "index",
899 "source_id",
900 ],
901 ),
902 }
903}
904
905fn read_storage(v: &Value, pscale: f64) -> Storage {
906 Storage {
907 bus: BusId(uid(v, "storage_bus")),
908 ps: f(v, "ps"),
909 qs: f(v, "qs"),
910 energy: f(v, "energy") * pscale,
911 energy_rating: f(v, "energy_rating") * pscale,
912 charge_rating: f(v, "charge_rating") * pscale,
913 discharge_rating: f(v, "discharge_rating") * pscale,
914 charge_efficiency: f_or(v, "charge_efficiency", 1.0),
915 discharge_efficiency: f_or(v, "discharge_efficiency", 1.0),
916 thermal_rating: f(v, "thermal_rating") * pscale,
917 current_rating: v.get("current_rating").and_then(Value::as_f64),
918 qmin: f_or(v, "qmin", f64::NEG_INFINITY) * pscale,
920 qmax: f_or(v, "qmax", f64::INFINITY) * pscale,
921 r: f(v, "r"),
922 x: f(v, "x"),
923 p_loss: f(v, "p_loss") * pscale,
924 q_loss: f(v, "q_loss") * pscale,
925 in_service: flag(v, "status"),
926 uid: None,
927 extras: extras_excluding(
928 v,
929 &[
930 "storage_bus",
931 "ps",
932 "qs",
933 "energy",
934 "energy_rating",
935 "charge_rating",
936 "discharge_rating",
937 "charge_efficiency",
938 "discharge_efficiency",
939 "thermal_rating",
940 "current_rating",
941 "qmin",
942 "qmax",
943 "r",
944 "x",
945 "p_loss",
946 "q_loss",
947 "status",
948 "index",
949 "source_id",
950 ],
951 ),
952 }
953}
954
955#[cfg(test)]
956mod tests {
957 use super::*;
958
959 fn approx(a: f64, b: f64) -> bool {
960 (a - b).abs() <= 1e-9 * a.abs().max(b.abs()).max(1.0)
961 }
962
963 #[test]
964 fn gen_pu_keys_subset_of_extra_keys() {
965 for k in GEN_PU_KEYS {
969 assert!(
970 GEN_EXTRA_KEYS.contains(&k),
971 "{k} is not a GEN_EXTRA_KEYS column"
972 );
973 }
974 }
975
976 #[test]
977 fn dcline_p_bounds_four_quadrants() {
978 let q1 = dcline_p_bounds(2.0, 10.0, 1.0, 0.1);
981 assert!(
982 approx(q1.0, 2.0) && approx(q1.1, 10.0) && approx(q1.2, -8.0) && approx(q1.3, -0.8)
983 );
984
985 let q2 = dcline_p_bounds(2.0, -5.0, 1.0, 0.1);
986 assert!(
987 approx(q2.0, 2.0)
988 && approx(q2.1, 6.0 / 0.9)
989 && approx(q2.2, -5.0)
990 && approx(q2.3, -0.8)
991 );
992
993 let q3 = dcline_p_bounds(-3.0, 10.0, 1.0, 0.1);
994 assert!(
995 approx(q3.0, -2.0 / 0.9)
996 && approx(q3.1, 10.0)
997 && approx(q3.2, -8.0)
998 && approx(q3.3, 3.0)
999 );
1000
1001 let q4 = dcline_p_bounds(-3.0, -5.0, 1.0, 0.1);
1002 assert!(
1003 approx(q4.0, -2.0 / 0.9)
1004 && approx(q4.1, 6.0 / 0.9)
1005 && approx(q4.2, -5.0)
1006 && approx(q4.3, 3.0)
1007 );
1008 }
1009}