1use std::sync::Arc;
15
16use serde_json::{Map, Value};
17
18use super::{Conversion, finish, jnum, warn_extra_branch_rating_sets};
19use crate::network::{
20 BalancedNetwork, Branch, Bus, BusId, BusType, Extras, GenCost, Generator, Hvdc, Load,
21 LoadVoltageModel, Shunt, SourceFormat,
22};
23use crate::{Error, Result};
24
25const FMT: &str = "egret JSON";
26
27#[must_use]
28pub fn write_egret_json(net: &BalancedNetwork) -> Conversion {
29 let mut warnings = Vec::new();
30
31 let mut bus = Map::new();
32 for b in &net.buses {
33 bus.insert(b.id.to_string(), bus_obj(b));
34 }
35
36 let mut load = Map::new();
39 for (i, l) in net.loads.iter().enumerate() {
40 load.insert(format!("load_{}", i + 1), load_obj(l));
41 }
42 let mut shunt = Map::new();
43 for (i, s) in net.shunts.iter().enumerate() {
44 shunt.insert(format!("shunt_{}", i + 1), shunt_obj(s));
45 }
46
47 let mut branch = Map::new();
48 for (i, br) in net.branches.iter().enumerate() {
49 branch.insert((i + 1).to_string(), branch_obj(br));
50 }
51
52 let mut generator = Map::new();
53 for (i, g) in net.generators.iter().enumerate() {
54 generator.insert((i + 1).to_string(), gen_obj(g, &mut warnings));
55 }
56
57 warn_egret_writer_losses(net, &mut warnings);
58
59 let mut elements = Map::new();
60 elements.insert("bus".into(), Value::Object(bus));
61 elements.insert("load".into(), Value::Object(load));
62 elements.insert("shunt".into(), Value::Object(shunt));
63 elements.insert("branch".into(), Value::Object(branch));
64 elements.insert("generator".into(), Value::Object(generator));
65
66 let mut system = Map::new();
67 system.insert("baseMVA".into(), jnum(net.base_mva));
68 match reference_bus(net) {
69 Some(r) => {
70 system.insert("reference_bus".into(), Value::String(r.id.to_string()));
71 system.insert("reference_bus_angle".into(), jnum(r.va));
72 }
73 None => warnings
74 .push("no single reference bus (BusType::Ref); system.reference_bus omitted".into()),
75 }
76
77 let mut root = Map::new();
78 root.insert("elements".into(), Value::Object(elements));
79 root.insert("system".into(), Value::Object(system));
80
81 finish(root, warnings)
82}
83
84fn warn_egret_writer_losses(net: &BalancedNetwork, warnings: &mut Vec<String>) {
85 if !net.hvdc.is_empty() {
86 warnings.push(format!(
87 "{} dcline(s) dropped: egret HVDC mapping not implemented",
88 net.hvdc.len()
89 ));
90 }
91 if !net.transformers_3w.is_empty() {
92 warnings.push(format!(
93 "{} 3-winding transformer(s) dropped: the egret writer emits no 3-winding record",
94 net.transformers_3w.len()
95 ));
96 }
97 if net
98 .buses
99 .iter()
100 .any(|b| b.evhi.is_some() || b.evlo.is_some())
101 {
102 warnings.push(
103 "emergency voltage band(s) (EVHI/EVLO) dropped: this writer carries one voltage band"
104 .into(),
105 );
106 }
107 if !net.storage.is_empty() {
108 warnings.push(format!(
109 "{} storage unit(s) dropped: egret storage mapping not implemented",
110 net.storage.len()
111 ));
112 }
113 let voltage_loads = net
114 .loads
115 .iter()
116 .filter(|l| {
117 l.voltage_model
118 .as_ref()
119 .is_some_and(LoadVoltageModel::has_non_matpower_fields)
120 })
121 .count();
122 if voltage_loads > 0 {
123 warnings.push(format!(
124 "{voltage_loads} voltage dependent load model(s) dropped: egret load records carry static p_load/q_load only"
125 ));
126 }
127 let terminal_charging = net
128 .branches
129 .iter()
130 .filter(|b| b.has_non_matpower_charging())
131 .count();
132 if terminal_charging > 0 {
133 warnings.push(format!(
134 "{terminal_charging} branch terminal admittance record(s) collapsed to total susceptance: egret branches cannot carry conductance or asymmetric terminal charging"
135 ));
136 }
137 let current_ratings = net
138 .branches
139 .iter()
140 .filter(|b| b.current_ratings.is_some())
141 .count();
142 if current_ratings > 0 {
143 warnings.push(format!(
144 "{current_ratings} branch current rating record(s) dropped: egret branch records carry MVA ratings only"
145 ));
146 }
147 warn_extra_branch_rating_sets("egret JSON", net, warnings);
148 let branch_solutions = net.branches.iter().filter(|b| b.solution.is_some()).count();
149 if branch_solutions > 0 {
150 warnings.push(format!(
151 "{branch_solutions} branch solution value set(s) dropped: egret branch result fields are not written"
152 ));
153 }
154}
155
156fn reference_bus(net: &BalancedNetwork) -> Option<&Bus> {
157 let mut refs = net.buses.iter().filter(|b| b.kind == BusType::Ref);
158 let first = refs.next()?;
159 if refs.next().is_some() {
160 None } else {
162 Some(first)
163 }
164}
165
166fn bustype(kind: BusType) -> &'static str {
167 match kind {
168 BusType::Pq => "PQ",
169 BusType::Pv => "PV",
170 BusType::Ref => "ref",
171 BusType::Isolated => "isolated",
172 }
173}
174
175fn bus_obj(b: &Bus) -> Value {
176 let mut m = Map::new();
177 m.insert("base_kv".into(), jnum(b.base_kv));
178 m.insert(
179 "matpower_bustype".into(),
180 Value::String(bustype(b.kind).into()),
181 );
182 m.insert("vm".into(), jnum(b.vm));
183 m.insert("va".into(), jnum(b.va));
184 m.insert("v_min".into(), jnum(b.vmin));
185 m.insert("v_max".into(), jnum(b.vmax));
186 m.insert("area".into(), Value::String(b.area.to_string()));
187 m.insert("zone".into(), Value::String(b.zone.to_string()));
188 if let Some(name) = &b.name {
189 m.insert("name".into(), Value::String(name.clone()));
190 }
191 Value::Object(m)
192}
193
194fn load_obj(l: &Load) -> Value {
195 let mut m = Map::new();
196 m.insert("bus".into(), Value::String(l.bus.to_string()));
197 m.insert("p_load".into(), jnum(l.p));
198 m.insert("q_load".into(), jnum(l.q));
199 m.insert("in_service".into(), Value::Bool(l.in_service));
200 Value::Object(m)
201}
202
203fn shunt_obj(s: &Shunt) -> Value {
204 let mut m = Map::new();
205 m.insert("bus".into(), Value::String(s.bus.to_string()));
206 m.insert("shunt_type".into(), Value::String("fixed".into()));
207 m.insert("gs".into(), jnum(s.g));
208 m.insert("bs".into(), jnum(s.b));
209 Value::Object(m)
210}
211
212fn branch_obj(br: &Branch) -> Value {
213 let mut m = Map::new();
214 m.insert("from_bus".into(), Value::String(br.from.to_string()));
215 m.insert("to_bus".into(), Value::String(br.to.to_string()));
216 m.insert("resistance".into(), jnum(br.r));
217 m.insert("reactance".into(), jnum(br.x));
218 m.insert("charging_susceptance".into(), jnum(br.total_charging_b()));
219 m.insert("in_service".into(), Value::Bool(br.in_service));
220 m.insert("angle_diff_min".into(), jnum(br.angmin));
221 m.insert("angle_diff_max".into(), jnum(br.angmax));
222 if br.is_transformer() {
223 m.insert("branch_type".into(), Value::String("transformer".into()));
224 m.insert("transformer_tap_ratio".into(), jnum(br.effective_tap()));
225 m.insert("transformer_phase_shift".into(), jnum(br.shift));
226 } else {
227 m.insert("branch_type".into(), Value::String("line".into()));
228 }
229 if br.rate_a != 0.0 {
231 m.insert("rating_long_term".into(), jnum(br.rate_a));
232 }
233 if br.rate_b != 0.0 {
234 m.insert("rating_short_term".into(), jnum(br.rate_b));
235 }
236 if br.rate_c != 0.0 {
237 m.insert("rating_emergency".into(), jnum(br.rate_c));
238 }
239 Value::Object(m)
240}
241
242fn gen_obj(g: &Generator, warnings: &mut Vec<String>) -> Value {
243 let mut m = Map::new();
244 m.insert("bus".into(), Value::String(g.bus.to_string()));
245 m.insert("generator_type".into(), Value::String("thermal".into()));
246 m.insert("in_service".into(), Value::Bool(g.in_service));
247 m.insert("pg".into(), jnum(g.pg));
248 m.insert("qg".into(), jnum(g.qg));
249 m.insert("vg".into(), jnum(g.vg));
250 m.insert("mbase".into(), jnum(g.mbase));
251 m.insert("p_min".into(), jnum(g.pmin));
252 m.insert("p_max".into(), jnum(g.pmax));
253 m.insert("q_min".into(), jnum(g.qmin));
254 m.insert("q_max".into(), jnum(g.qmax));
255 if let Some(cost) = &g.cost {
256 if let Some(curve) = cost_curve(cost) {
257 m.insert("p_cost".into(), curve);
258 } else {
259 warnings.push(format!(
260 "generator at bus {} has a cost model egret's writer can't express; cost dropped",
261 g.bus
262 ));
263 }
264 }
265 Value::Object(m)
266}
267
268fn cost_curve(cost: &GenCost) -> Option<Value> {
271 let mut curve = Map::new();
272 curve.insert("data_type".into(), Value::String("cost_curve".into()));
273 match cost.model {
274 2 => {
275 let mut values = Map::new();
278 let k = cost.coeffs.len();
279 for (i, &c) in cost.coeffs.iter().enumerate() {
280 values.insert((k - 1 - i).to_string(), jnum(c));
281 }
282 curve.insert("cost_curve_type".into(), Value::String("polynomial".into()));
283 curve.insert("values".into(), Value::Object(values));
284 Some(Value::Object(curve))
285 }
286 1 => {
287 let points: Vec<Value> = cost
288 .coeffs
289 .chunks_exact(2)
290 .map(|pt| Value::Array(vec![jnum(pt[0]), jnum(pt[1])]))
291 .collect();
292 curve.insert("cost_curve_type".into(), Value::String("piecewise".into()));
293 curve.insert("values".into(), Value::Array(points));
294 Some(Value::Object(curve))
295 }
296 _ => None,
297 }
298}
299
300pub fn parse_egret_json(content: &str) -> Result<BalancedNetwork> {
307 parse_egret_source(Arc::new(content.to_owned()), None)
308}
309
310pub(crate) fn parse_egret_source(
315 source: Arc<String>,
316 name_hint: Option<&str>,
317) -> Result<BalancedNetwork> {
318 let content: &str = &source;
319 let root: Value = serde_json::from_str(content).map_err(|e| bad(e.to_string()))?;
320 let root = root
321 .as_object()
322 .ok_or_else(|| bad("top level is not a JSON object"))?;
323
324 let system = obj(root, "system").ok_or_else(|| bad("missing `system` object"))?;
325 if system.contains_key("time_keys") {
326 return Err(bad(
327 "egret unit commitment cases (system.time_keys) are not supported; expected a power flow ModelData",
328 ));
329 }
330 let base_mva = system
331 .get("baseMVA")
332 .and_then(Value::as_f64)
333 .ok_or_else(|| bad("missing numeric system.baseMVA"))?;
334 let elements = obj(root, "elements").ok_or_else(|| bad("missing `elements` object"))?;
335 let name = root
336 .get("model_name")
337 .and_then(Value::as_str)
338 .or(name_hint)
339 .unwrap_or("case")
340 .to_string();
341
342 let mut buses = Vec::new();
343 if let Some(m) = obj(elements, "bus") {
344 for (k, v) in sorted_kv(m) {
345 buses.push(read_bus(k, v)?);
346 }
347 }
348 let mut loads = Vec::new();
349 if let Some(m) = obj(elements, "load") {
350 for v in sorted_vals(m) {
351 loads.push(read_load(v)?);
352 }
353 }
354 let mut shunts = Vec::new();
355 if let Some(m) = obj(elements, "shunt") {
356 for v in sorted_vals(m) {
357 shunts.push(read_shunt(v)?);
358 }
359 }
360 let mut branches = Vec::new();
361 if let Some(m) = obj(elements, "branch") {
362 for v in sorted_vals(m) {
363 branches.push(read_branch(v)?);
364 }
365 }
366 let mut generators = Vec::new();
367 if let Some(m) = obj(elements, "generator") {
368 for v in sorted_vals(m) {
369 generators.push(read_gen(v)?);
370 }
371 }
372 let mut hvdc = Vec::new();
373 if let Some(m) = obj(elements, "dc_branch") {
374 for v in sorted_vals(m) {
375 hvdc.push(read_dc_branch(v)?);
376 }
377 }
378
379 let net = BalancedNetwork {
380 name,
381 base_mva,
382 base_frequency: crate::network::DEFAULT_BASE_FREQUENCY,
383 geo: None,
384 buses,
385 loads,
386 shunts,
387 branches,
388 switches: Vec::new(),
389 generators,
390 storage: Vec::new(),
391 hvdc,
392 transformers_3w: Vec::new(),
393 areas: Vec::new(),
394 solver: None,
395 source_format: SourceFormat::EgretJson,
396 source: Some(source),
397 };
398 net.check_references(FMT)?;
399 Ok(net)
400}
401
402fn bad(message: impl Into<String>) -> Error {
403 Error::FormatRead {
404 format: FMT,
405 message: message.into(),
406 }
407}
408
409fn obj<'a>(v: &'a Map<String, Value>, key: &str) -> Option<&'a Map<String, Value>> {
410 v.get(key).and_then(Value::as_object)
411}
412
413fn sorted_kv(map: &Map<String, Value>) -> Vec<(&String, &Value)> {
418 let mut items: Vec<(&String, &Value)> = map.iter().collect();
419 items.sort_by(|(a, _), (b, _)| num_key(a).cmp(&num_key(b)).then_with(|| a.cmp(b)));
420 items
421}
422
423fn sorted_vals(map: &Map<String, Value>) -> Vec<&Value> {
424 sorted_kv(map).into_iter().map(|(_, v)| v).collect()
425}
426
427fn num_key(k: &str) -> i64 {
430 let start = k.len() - k.bytes().rev().take_while(u8::is_ascii_digit).count();
431 k[start..].parse::<i64>().unwrap_or(i64::MAX)
432}
433
434fn id_from_f64(x: f64) -> Option<usize> {
438 (x >= 0.0 && x.fract() == 0.0 && x < usize::MAX as f64).then_some(x as usize)
441}
442
443fn parse_id(v: &Value) -> Option<usize> {
447 match v {
448 Value::String(s) => {
449 let s = s.trim();
450 s.parse::<usize>()
451 .ok()
452 .or_else(|| s.parse::<f64>().ok().and_then(id_from_f64))
453 }
454 Value::Number(n) => n
455 .as_u64()
456 .map(|x| x as usize)
457 .or_else(|| n.as_f64().and_then(id_from_f64)),
458 _ => None,
459 }
460}
461
462fn id_field(v: &Value, key: &str) -> Result<BusId> {
463 let raw = v
464 .get(key)
465 .ok_or_else(|| bad(format!("element missing `{key}`")))?;
466 parse_id(raw)
467 .map(BusId)
468 .ok_or_else(|| bad(format!("`{key}` is not a numeric bus id: {raw}")))
469}
470
471fn f(v: &Value, key: &str) -> Result<f64> {
476 f_or(v, key, 0.0)
477}
478fn f_or(v: &Value, key: &str, default: f64) -> Result<f64> {
480 match v.get(key) {
481 None | Some(Value::Null) => Ok(default),
482 Some(x) => x
483 .as_f64()
484 .ok_or_else(|| bad(format!("`{key}` is not a number: {x}"))),
485 }
486}
487fn usize_or(v: &Value, key: &str, default: usize) -> Result<usize> {
491 match v.get(key) {
492 None | Some(Value::Null) => Ok(default),
493 Some(x) => {
494 parse_id(x).ok_or_else(|| bad(format!("`{key}` is not a non-negative integer: {x}")))
495 }
496 }
497}
498fn flag(v: &Value, key: &str, default: bool) -> Result<bool> {
500 match v.get(key) {
501 None | Some(Value::Null) => Ok(default),
502 Some(Value::Bool(b)) => Ok(*b),
503 Some(x) => Err(bad(format!("`{key}` is not a boolean: {x}"))),
504 }
505}
506
507fn bustype_from_str(s: &str) -> BusType {
508 match s {
509 "PV" => BusType::Pv,
510 "ref" => BusType::Ref,
511 "isolated" => BusType::Isolated,
512 _ => BusType::Pq,
513 }
514}
515
516fn extras_excluding(v: &Value, known: &[&str]) -> Extras {
522 v.as_object().map_or_else(Default::default, |obj| {
523 obj.iter()
524 .filter(|(k, _)| !known.contains(&k.as_str()))
525 .map(|(k, val)| (k.clone(), val.clone()))
526 .collect()
527 })
528}
529
530fn read_bus(key: &str, v: &Value) -> Result<Bus> {
531 let id = key
532 .trim()
533 .parse::<usize>()
534 .map_err(|_| bad(format!("bus key is not a numeric id: {key:?}")))?;
535 Ok(Bus {
536 id: BusId(id),
537 kind: bustype_from_str(
538 v.get("matpower_bustype")
539 .and_then(Value::as_str)
540 .unwrap_or("PQ"),
541 ),
542 vm: f_or(v, "vm", 1.0)?,
543 va: f(v, "va")?,
544 base_kv: f(v, "base_kv")?,
545 vmax: f_or(v, "v_max", 1.1)?,
546 vmin: f_or(v, "v_min", 0.9)?,
547 evhi: None,
548 evlo: None,
549 area: usize_or(v, "area", 0)?,
550 zone: usize_or(v, "zone", 0)?,
551 name: v.get("name").and_then(Value::as_str).map(str::to_string),
552 uid: None,
553 location: None,
554 extras: extras_excluding(
555 v,
556 &[
557 "matpower_bustype",
558 "vm",
559 "va",
560 "base_kv",
561 "v_max",
562 "v_min",
563 "area",
564 "zone",
565 "name",
566 ],
567 ),
568 })
569}
570
571fn read_load(v: &Value) -> Result<Load> {
572 Ok(Load {
573 bus: id_field(v, "bus")?,
574 p: f(v, "p_load")?,
575 q: f(v, "q_load")?,
576 voltage_model: None,
577 in_service: flag(v, "in_service", true)?,
578 uid: None,
579 extras: extras_excluding(v, &["bus", "p_load", "q_load", "in_service"]),
580 })
581}
582
583fn read_shunt(v: &Value) -> Result<Shunt> {
584 Ok(Shunt {
585 bus: id_field(v, "bus")?,
586 g: f(v, "gs")?,
587 b: f(v, "bs")?,
588 in_service: flag(v, "in_service", true)?,
589 control: None,
590 uid: None,
591 extras: extras_excluding(v, &["bus", "gs", "bs", "in_service", "shunt_type"]),
592 })
593}
594
595fn read_branch(v: &Value) -> Result<Branch> {
596 let is_xf = v.get("branch_type").and_then(Value::as_str) == Some("transformer");
597 Ok(Branch {
598 from: id_field(v, "from_bus")?,
599 to: id_field(v, "to_bus")?,
600 r: f(v, "resistance")?,
601 x: f(v, "reactance")?,
602 b: f(v, "charging_susceptance")?,
603 charging: None,
604 rate_a: f(v, "rating_long_term")?,
605 rate_b: f(v, "rating_short_term")?,
606 rate_c: f(v, "rating_emergency")?,
607 rating_sets: Vec::new(),
608 current_ratings: None,
609 tap: if is_xf {
610 f_or(v, "transformer_tap_ratio", 1.0)?
611 } else {
612 0.0
613 },
614 shift: f(v, "transformer_phase_shift")?,
615 in_service: flag(v, "in_service", true)?,
616 angmin: f_or(v, "angle_diff_min", -360.0)?,
617 angmax: f_or(v, "angle_diff_max", 360.0)?,
618 control: None,
619 solution: None,
620 uid: None,
621 route: None,
622 extras: extras_excluding(
623 v,
624 &[
625 "from_bus",
626 "to_bus",
627 "resistance",
628 "reactance",
629 "charging_susceptance",
630 "rating_long_term",
631 "rating_short_term",
632 "rating_emergency",
633 "branch_type",
634 "transformer_tap_ratio",
635 "transformer_phase_shift",
636 "in_service",
637 "angle_diff_min",
638 "angle_diff_max",
639 ],
640 ),
641 })
642}
643
644fn read_gen(v: &Value) -> Result<Generator> {
645 let startup = f_or(v, "startup_cost", 0.0)?;
646 let shutdown = f_or(v, "shutdown_cost", 0.0)?;
647 let cost = match v.get("p_cost") {
651 None | Some(Value::Null) => None,
652 Some(pc) => Some(read_cost(pc, startup, shutdown).ok_or_else(|| {
653 bad("`p_cost` is present but has an unrecognized or malformed cost_curve")
654 })?),
655 };
656 Ok(Generator {
657 bus: id_field(v, "bus")?,
658 pg: f(v, "pg")?,
659 qg: f(v, "qg")?,
660 pmax: f(v, "p_max")?,
661 pmin: f(v, "p_min")?,
662 qmax: f(v, "q_max")?,
663 qmin: f(v, "q_min")?,
664 vg: f_or(v, "vg", 1.0)?,
665 mbase: f_or(v, "mbase", 100.0)?,
666 in_service: flag(v, "in_service", true)?,
667 cost,
668 caps: Default::default(),
669 regulated_bus: None,
670 uid: None,
671 })
672}
673
674fn read_dc_branch(v: &Value) -> Result<Hvdc> {
675 Ok(Hvdc {
676 from: id_field(v, "from_bus")?,
677 to: id_field(v, "to_bus")?,
678 in_service: flag(v, "in_service", true)?,
679 pf: f(v, "pf")?,
680 pt: f(v, "pt")?,
681 qf: f(v, "qf")?,
682 qt: f(v, "qt")?,
683 vf: f_or(v, "vf", 1.0)?,
684 vt: f_or(v, "vt", 1.0)?,
685 pmin: f(v, "pmin")?,
686 pmax: f(v, "pmax")?,
687 qminf: f(v, "qminf")?,
688 qmaxf: f(v, "qmaxf")?,
689 qmint: f(v, "qmint")?,
690 qmaxt: f(v, "qmaxt")?,
691 loss0: f(v, "loss0")?,
692 loss1: f_or(v, "loss_factor", 0.0)?,
693 cost: None,
694 uid: None,
695 extras: extras_excluding(
696 v,
697 &[
698 "from_bus",
699 "to_bus",
700 "in_service",
701 "pf",
702 "pt",
703 "qf",
704 "qt",
705 "vf",
706 "vt",
707 "pmin",
708 "pmax",
709 "qminf",
710 "qmaxf",
711 "qmint",
712 "qmaxt",
713 "loss0",
714 "loss_factor",
715 ],
716 ),
717 })
718}
719
720fn read_cost(p_cost: &Value, startup: f64, shutdown: f64) -> Option<GenCost> {
724 let m = p_cost.as_object()?;
725 match m.get("cost_curve_type").and_then(Value::as_str)? {
726 "polynomial" => {
727 const MAX_COST_EXPONENT: usize = 64;
732 let values = m.get("values")?.as_object()?;
733 let pairs: Vec<(usize, f64)> = values
734 .iter()
735 .filter_map(|(k, c)| Some((k.parse().ok()?, c.as_f64()?)))
736 .filter(|(e, _)| *e <= MAX_COST_EXPONENT)
737 .collect();
738 let max_exp = pairs.iter().map(|(e, _)| *e).max()?;
739 let mut coeffs = vec![0.0; max_exp + 1]; for (e, c) in pairs {
741 coeffs[max_exp - e] = c;
742 }
743 let ncost = coeffs.len();
744 Some(GenCost {
745 model: 2,
746 startup,
747 shutdown,
748 ncost,
749 coeffs,
750 })
751 }
752 "piecewise" => {
753 let values = m.get("values")?.as_array()?;
754 let mut coeffs = Vec::with_capacity(values.len() * 2);
755 for pt in values {
756 let pair = pt.as_array()?;
757 coeffs.push(pair.first()?.as_f64()?);
758 coeffs.push(pair.get(1)?.as_f64()?);
759 }
760 Some(GenCost {
761 model: 1,
762 startup,
763 shutdown,
764 ncost: values.len(),
765 coeffs,
766 })
767 }
768 _ => None,
769 }
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775 use crate::network::BusType;
776
777 #[test]
778 fn oversized_cost_exponent_is_dropped_not_allocated() {
779 let all_oversized: Value = serde_json::json!({
784 "cost_curve_type": "polynomial",
785 "values": {"100000000000": 5.0, "18446744073709551615": 1.0}
786 });
787 assert!(read_cost(&all_oversized, 0.0, 0.0).is_none());
788
789 let mixed: Value = serde_json::json!({
790 "cost_curve_type": "polynomial",
791 "values": {"2": 3.0, "100000000000": 1.0}
792 });
793 let cost = read_cost(&mixed, 0.0, 0.0).unwrap();
794 assert_eq!(cost.coeffs, vec![3.0, 0.0, 0.0]);
795 }
796
797 fn fixture(name: &str) -> String {
798 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
799 .join("../tests/data/egret")
800 .join(name);
801 std::fs::read_to_string(path).unwrap()
802 }
803
804 #[test]
805 fn reads_buses_loads_branches_and_reference() {
806 let net = parse_egret_json(&fixture("case30.json")).unwrap();
807 assert!((net.base_mva - 100.0).abs() < 1e-9);
808 assert_eq!(net.buses.len(), 30);
809 assert_eq!(net.loads.len(), 20);
810 assert_eq!(net.shunts.len(), 2);
811 assert_eq!(net.branches.len(), 41);
812 assert_eq!(net.generators.len(), 6);
813 let refs = net.buses.iter().filter(|b| b.kind == BusType::Ref).count();
815 assert_eq!(refs, 1);
816 }
817
818 #[test]
819 fn inverts_transformer_and_polynomial_cost() {
820 let net = parse_egret_json(&fixture("case14.json")).unwrap();
821 assert!(net.branches.iter().any(Branch::is_transformer));
823 let cost = net
825 .generators
826 .iter()
827 .find_map(|g| g.cost.as_ref())
828 .expect("a generator cost");
829 assert_eq!(cost.model, 2);
830 assert_eq!(cost.coeffs.len(), cost.ncost);
831 }
832
833 #[test]
834 fn maps_dc_branch_to_hvdc() {
835 let net = parse_egret_json(&fixture("dcline3.json")).unwrap();
836 assert_eq!(net.hvdc.len(), 1);
837 let dc = &net.hvdc[0];
838 assert_eq!((dc.from, dc.to), (BusId(1), BusId(3)));
839 assert!((dc.loss1 - 0.1).abs() < 1e-12); }
841
842 #[test]
843 fn rejects_unit_commitment_time_series() {
844 let uc =
845 r#"{"elements":{"bus":{"1":{}}},"system":{"baseMVA":100.0,"time_keys":["1","2"]}}"#;
846 let err = parse_egret_json(uc).unwrap_err();
847 assert!(matches!(err, Error::FormatRead { .. }));
848 }
849
850 #[test]
851 fn rejects_present_but_malformed_numeric_field() {
852 let base = r#"{"elements":{"bus":{"1":{"matpower_bustype":"ref"},
856 "2":{"matpower_bustype":"PQ"}},"branch":{"1":{"from_bus":"1","to_bus":"2",
857 "reactance":REACT}}},"system":{"baseMVA":100.0,"reference_bus":"1"}}"#;
858 assert!(parse_egret_json(&base.replace("REACT", "0.1")).is_ok());
859 let err = parse_egret_json(&base.replace("REACT", "\"oops\"")).unwrap_err();
860 assert!(matches!(err, Error::FormatRead { .. }));
861 }
862
863 #[test]
864 fn piecewise_cost_round_trips() {
865 let cost = GenCost {
870 model: 1,
871 startup: 10.0,
872 shutdown: 5.0,
873 ncost: 3,
874 coeffs: vec![0.0, 0.0, 50.0, 1000.0, 100.0, 2500.0],
875 };
876 let curve = cost_curve(&cost).expect("model 1 maps to a piecewise curve");
877 let back = read_cost(&curve, 10.0, 5.0).expect("piecewise curve reads back");
878 assert_eq!(back.model, 1);
879 assert_eq!(back.ncost, 3);
880 assert_eq!(back.coeffs, cost.coeffs);
881 assert_eq!((back.startup, back.shutdown), (10.0, 5.0));
882 }
883
884 #[test]
885 fn dc_branch_reads_every_power_field() {
886 let v = serde_json::json!({
890 "from_bus": "1", "to_bus": "2", "in_service": true,
891 "pf": 10.0, "pt": -9.5, "qf": 1.5, "qt": -1.0,
892 "vf": 1.02, "vt": 0.99, "pmin": -50.0, "pmax": 60.0,
893 "qminf": -5.0, "qmaxf": 5.0, "qmint": -4.0, "qmaxt": 4.5,
894 "loss0": 0.2, "loss_factor": 0.03
895 });
896 let h = read_dc_branch(&v).unwrap();
897 assert_eq!((h.from, h.to), (BusId(1), BusId(2)));
898 assert_eq!((h.pf, h.pt, h.qf, h.qt), (10.0, -9.5, 1.5, -1.0));
899 assert_eq!((h.vf, h.vt), (1.02, 0.99));
900 assert_eq!((h.pmin, h.pmax), (-50.0, 60.0));
901 assert_eq!((h.qminf, h.qmaxf, h.qmint, h.qmaxt), (-5.0, 5.0, -4.0, 4.5));
902 assert_eq!((h.loss0, h.loss1), (0.2, 0.03));
903 }
904
905 #[test]
906 fn unrecognized_element_fields_are_preserved_as_extras() {
907 let doc = r#"{"elements":{
911 "bus":{"1":{"matpower_bustype":"ref","vm":1.0,"vendor_ext":42},
912 "2":{"matpower_bustype":"PQ"}},
913 "load":{"load_1":{"bus":"1","p_load":1.0,"q_load":0.5,"owner":"co-op"}},
914 "shunt":{"shunt_1":{"bus":"1","gs":0.0,"bs":5.0,"shunt_type":"fixed"}},
915 "branch":{"1":{"from_bus":"1","to_bus":"2","reactance":0.1,"pf":12.5}},
916 "dc_branch":{"1":{"from_bus":"1","to_bus":"2","rating_long_term":30.0}}},
917 "system":{"baseMVA":100.0,"reference_bus":"1"}}"#;
918 let net = parse_egret_json(doc).unwrap();
919 assert_eq!(
920 net.buses[0].extras.get("vendor_ext"),
921 Some(&Value::from(42))
922 );
923 assert!(!net.buses[0].extras.contains_key("vm"));
924 assert_eq!(
925 net.loads[0].extras.get("owner"),
926 Some(&Value::String("co-op".into()))
927 );
928 assert!(
929 net.shunts[0].extras.is_empty(),
930 "{:?}",
931 net.shunts[0].extras
932 );
933 assert_eq!(net.branches[0].extras.get("pf"), Some(&Value::from(12.5)));
934 assert_eq!(
935 net.hvdc[0].extras.get("rating_long_term"),
936 Some(&Value::from(30.0))
937 );
938 }
939
940 #[test]
941 fn rejects_present_but_malformed_cost() {
942 let v = serde_json::json!({
945 "bus": "1", "pg": 0.0, "qg": 0.0,
946 "p_max": 1.0, "p_min": 0.0, "q_max": 1.0, "q_min": -1.0,
947 "p_cost": {"data_type": "cost_curve", "cost_curve_type": "bogus", "values": {}}
948 });
949 assert!(matches!(read_gen(&v), Err(Error::FormatRead { .. })));
950 }
951}