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 Branch, Bus, BusId, BusType, Extras, GenCost, Generator, Hvdc, Load, LoadVoltageModel, Network,
21 Shunt, SourceFormat,
22};
23use crate::{Error, Result};
24
25const FMT: &str = "egret JSON";
26
27#[must_use]
28pub fn write_egret_json(net: &Network) -> 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: &Network, 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: &Network) -> 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(
219 "charging_susceptance".into(),
220 jnum(br.legacy_total_charging_b()),
221 );
222 m.insert("in_service".into(), Value::Bool(br.in_service));
223 m.insert("angle_diff_min".into(), jnum(br.angmin));
224 m.insert("angle_diff_max".into(), jnum(br.angmax));
225 if br.is_transformer() {
226 m.insert("branch_type".into(), Value::String("transformer".into()));
227 m.insert("transformer_tap_ratio".into(), jnum(br.effective_tap()));
228 m.insert("transformer_phase_shift".into(), jnum(br.shift));
229 } else {
230 m.insert("branch_type".into(), Value::String("line".into()));
231 }
232 if br.rate_a != 0.0 {
234 m.insert("rating_long_term".into(), jnum(br.rate_a));
235 }
236 if br.rate_b != 0.0 {
237 m.insert("rating_short_term".into(), jnum(br.rate_b));
238 }
239 if br.rate_c != 0.0 {
240 m.insert("rating_emergency".into(), jnum(br.rate_c));
241 }
242 Value::Object(m)
243}
244
245fn gen_obj(g: &Generator, warnings: &mut Vec<String>) -> Value {
246 let mut m = Map::new();
247 m.insert("bus".into(), Value::String(g.bus.to_string()));
248 m.insert("generator_type".into(), Value::String("thermal".into()));
249 m.insert("in_service".into(), Value::Bool(g.in_service));
250 m.insert("pg".into(), jnum(g.pg));
251 m.insert("qg".into(), jnum(g.qg));
252 m.insert("vg".into(), jnum(g.vg));
253 m.insert("mbase".into(), jnum(g.mbase));
254 m.insert("p_min".into(), jnum(g.pmin));
255 m.insert("p_max".into(), jnum(g.pmax));
256 m.insert("q_min".into(), jnum(g.qmin));
257 m.insert("q_max".into(), jnum(g.qmax));
258 if let Some(cost) = &g.cost {
259 if let Some(curve) = cost_curve(cost) {
260 m.insert("p_cost".into(), curve);
261 } else {
262 warnings.push(format!(
263 "generator at bus {} has a cost model egret's writer can't express; cost dropped",
264 g.bus
265 ));
266 }
267 }
268 Value::Object(m)
269}
270
271fn cost_curve(cost: &GenCost) -> Option<Value> {
274 let mut curve = Map::new();
275 curve.insert("data_type".into(), Value::String("cost_curve".into()));
276 match cost.model {
277 2 => {
278 let mut values = Map::new();
281 let k = cost.coeffs.len();
282 for (i, &c) in cost.coeffs.iter().enumerate() {
283 values.insert((k - 1 - i).to_string(), jnum(c));
284 }
285 curve.insert("cost_curve_type".into(), Value::String("polynomial".into()));
286 curve.insert("values".into(), Value::Object(values));
287 Some(Value::Object(curve))
288 }
289 1 => {
290 let points: Vec<Value> = cost
291 .coeffs
292 .chunks_exact(2)
293 .map(|pt| Value::Array(vec![jnum(pt[0]), jnum(pt[1])]))
294 .collect();
295 curve.insert("cost_curve_type".into(), Value::String("piecewise".into()));
296 curve.insert("values".into(), Value::Array(points));
297 Some(Value::Object(curve))
298 }
299 _ => None,
300 }
301}
302
303pub fn parse_egret_json(content: &str) -> Result<Network> {
310 parse_egret_source(Arc::new(content.to_owned()), None)
311}
312
313pub(crate) fn parse_egret_source(source: Arc<String>, name_hint: Option<&str>) -> Result<Network> {
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 = Network {
380 name,
381 base_mva,
382 base_frequency: crate::network::DEFAULT_BASE_FREQUENCY,
383 buses,
384 loads,
385 shunts,
386 branches,
387 switches: Vec::new(),
388 generators,
389 storage: Vec::new(),
390 hvdc,
391 transformers_3w: Vec::new(),
392 areas: Vec::new(),
393 solver: None,
394 source_format: SourceFormat::EgretJson,
395 source: Some(source),
396 };
397 net.check_references(FMT)?;
398 Ok(net)
399}
400
401fn bad(message: impl Into<String>) -> Error {
402 Error::FormatRead {
403 format: FMT,
404 message: message.into(),
405 }
406}
407
408fn obj<'a>(v: &'a Map<String, Value>, key: &str) -> Option<&'a Map<String, Value>> {
409 v.get(key).and_then(Value::as_object)
410}
411
412fn sorted_kv(map: &Map<String, Value>) -> Vec<(&String, &Value)> {
417 let mut items: Vec<(&String, &Value)> = map.iter().collect();
418 items.sort_by(|(a, _), (b, _)| num_key(a).cmp(&num_key(b)).then_with(|| a.cmp(b)));
419 items
420}
421
422fn sorted_vals(map: &Map<String, Value>) -> Vec<&Value> {
423 sorted_kv(map).into_iter().map(|(_, v)| v).collect()
424}
425
426fn num_key(k: &str) -> i64 {
429 let start = k.len() - k.bytes().rev().take_while(u8::is_ascii_digit).count();
430 k[start..].parse::<i64>().unwrap_or(i64::MAX)
431}
432
433fn id_from_f64(x: f64) -> Option<usize> {
437 (x >= 0.0 && x.fract() == 0.0 && x < usize::MAX as f64).then_some(x as usize)
440}
441
442fn parse_id(v: &Value) -> Option<usize> {
446 match v {
447 Value::String(s) => {
448 let s = s.trim();
449 s.parse::<usize>()
450 .ok()
451 .or_else(|| s.parse::<f64>().ok().and_then(id_from_f64))
452 }
453 Value::Number(n) => n
454 .as_u64()
455 .map(|x| x as usize)
456 .or_else(|| n.as_f64().and_then(id_from_f64)),
457 _ => None,
458 }
459}
460
461fn id_field(v: &Value, key: &str) -> Result<BusId> {
462 let raw = v
463 .get(key)
464 .ok_or_else(|| bad(format!("element missing `{key}`")))?;
465 parse_id(raw)
466 .map(BusId)
467 .ok_or_else(|| bad(format!("`{key}` is not a numeric bus id: {raw}")))
468}
469
470fn f(v: &Value, key: &str) -> Result<f64> {
475 f_or(v, key, 0.0)
476}
477fn f_or(v: &Value, key: &str, default: f64) -> Result<f64> {
479 match v.get(key) {
480 None | Some(Value::Null) => Ok(default),
481 Some(x) => x
482 .as_f64()
483 .ok_or_else(|| bad(format!("`{key}` is not a number: {x}"))),
484 }
485}
486fn usize_or(v: &Value, key: &str, default: usize) -> Result<usize> {
490 match v.get(key) {
491 None | Some(Value::Null) => Ok(default),
492 Some(x) => {
493 parse_id(x).ok_or_else(|| bad(format!("`{key}` is not a non-negative integer: {x}")))
494 }
495 }
496}
497fn flag(v: &Value, key: &str, default: bool) -> Result<bool> {
499 match v.get(key) {
500 None | Some(Value::Null) => Ok(default),
501 Some(Value::Bool(b)) => Ok(*b),
502 Some(x) => Err(bad(format!("`{key}` is not a boolean: {x}"))),
503 }
504}
505
506fn bustype_from_str(s: &str) -> BusType {
507 match s {
508 "PV" => BusType::Pv,
509 "ref" => BusType::Ref,
510 "isolated" => BusType::Isolated,
511 _ => BusType::Pq,
512 }
513}
514
515fn read_bus(key: &str, v: &Value) -> Result<Bus> {
516 let id = key
517 .trim()
518 .parse::<usize>()
519 .map_err(|_| bad(format!("bus key is not a numeric id: {key:?}")))?;
520 Ok(Bus {
521 id: BusId(id),
522 kind: bustype_from_str(
523 v.get("matpower_bustype")
524 .and_then(Value::as_str)
525 .unwrap_or("PQ"),
526 ),
527 vm: f_or(v, "vm", 1.0)?,
528 va: f(v, "va")?,
529 base_kv: f(v, "base_kv")?,
530 vmax: f_or(v, "v_max", 1.1)?,
531 vmin: f_or(v, "v_min", 0.9)?,
532 evhi: None,
533 evlo: None,
534 area: usize_or(v, "area", 0)?,
535 zone: usize_or(v, "zone", 0)?,
536 name: v.get("name").and_then(Value::as_str).map(str::to_string),
537 uid: None,
538 extras: Extras::new(),
539 })
540}
541
542fn read_load(v: &Value) -> Result<Load> {
543 Ok(Load {
544 bus: id_field(v, "bus")?,
545 p: f(v, "p_load")?,
546 q: f(v, "q_load")?,
547 voltage_model: None,
548 in_service: flag(v, "in_service", true)?,
549 uid: None,
550 extras: Extras::new(),
551 })
552}
553
554fn read_shunt(v: &Value) -> Result<Shunt> {
555 Ok(Shunt {
556 bus: id_field(v, "bus")?,
557 g: f(v, "gs")?,
558 b: f(v, "bs")?,
559 in_service: flag(v, "in_service", true)?,
560 control: None,
561 uid: None,
562 extras: Extras::new(),
563 })
564}
565
566fn read_branch(v: &Value) -> Result<Branch> {
567 let is_xf = v.get("branch_type").and_then(Value::as_str) == Some("transformer");
568 Ok(Branch {
569 from: id_field(v, "from_bus")?,
570 to: id_field(v, "to_bus")?,
571 r: f(v, "resistance")?,
572 x: f(v, "reactance")?,
573 b: f(v, "charging_susceptance")?,
574 charging: None,
575 rate_a: f(v, "rating_long_term")?,
576 rate_b: f(v, "rating_short_term")?,
577 rate_c: f(v, "rating_emergency")?,
578 rating_sets: Vec::new(),
579 current_ratings: None,
580 tap: if is_xf {
581 f_or(v, "transformer_tap_ratio", 1.0)?
582 } else {
583 0.0
584 },
585 shift: f(v, "transformer_phase_shift")?,
586 in_service: flag(v, "in_service", true)?,
587 angmin: f_or(v, "angle_diff_min", -360.0)?,
588 angmax: f_or(v, "angle_diff_max", 360.0)?,
589 control: None,
590 solution: None,
591 uid: None,
592 extras: Extras::new(),
593 })
594}
595
596fn read_gen(v: &Value) -> Result<Generator> {
597 let startup = f_or(v, "startup_cost", 0.0)?;
598 let shutdown = f_or(v, "shutdown_cost", 0.0)?;
599 let cost = match v.get("p_cost") {
603 None | Some(Value::Null) => None,
604 Some(pc) => Some(read_cost(pc, startup, shutdown).ok_or_else(|| {
605 bad("`p_cost` is present but has an unrecognized or malformed cost_curve")
606 })?),
607 };
608 Ok(Generator {
609 bus: id_field(v, "bus")?,
610 pg: f(v, "pg")?,
611 qg: f(v, "qg")?,
612 pmax: f(v, "p_max")?,
613 pmin: f(v, "p_min")?,
614 qmax: f(v, "q_max")?,
615 qmin: f(v, "q_min")?,
616 vg: f_or(v, "vg", 1.0)?,
617 mbase: f_or(v, "mbase", 100.0)?,
618 in_service: flag(v, "in_service", true)?,
619 cost,
620 caps: Default::default(),
621 regulated_bus: None,
622 uid: None,
623 })
624}
625
626fn read_dc_branch(v: &Value) -> Result<Hvdc> {
627 Ok(Hvdc {
628 from: id_field(v, "from_bus")?,
629 to: id_field(v, "to_bus")?,
630 in_service: flag(v, "in_service", true)?,
631 pf: f(v, "pf")?,
632 pt: f(v, "pt")?,
633 qf: f(v, "qf")?,
634 qt: f(v, "qt")?,
635 vf: f_or(v, "vf", 1.0)?,
636 vt: f_or(v, "vt", 1.0)?,
637 pmin: f(v, "pmin")?,
638 pmax: f(v, "pmax")?,
639 qminf: f(v, "qminf")?,
640 qmaxf: f(v, "qmaxf")?,
641 qmint: f(v, "qmint")?,
642 qmaxt: f(v, "qmaxt")?,
643 loss0: f(v, "loss0")?,
644 loss1: f_or(v, "loss_factor", 0.0)?,
645 cost: None,
646 uid: None,
647 extras: Extras::new(),
648 })
649}
650
651fn read_cost(p_cost: &Value, startup: f64, shutdown: f64) -> Option<GenCost> {
655 let m = p_cost.as_object()?;
656 match m.get("cost_curve_type").and_then(Value::as_str)? {
657 "polynomial" => {
658 let values = m.get("values")?.as_object()?;
659 let pairs: Vec<(usize, f64)> = values
660 .iter()
661 .filter_map(|(k, c)| Some((k.parse().ok()?, c.as_f64()?)))
662 .collect();
663 let max_exp = pairs.iter().map(|(e, _)| *e).max()?;
664 let mut coeffs = vec![0.0; max_exp + 1]; for (e, c) in pairs {
666 coeffs[max_exp - e] = c;
667 }
668 let ncost = coeffs.len();
669 Some(GenCost {
670 model: 2,
671 startup,
672 shutdown,
673 ncost,
674 coeffs,
675 })
676 }
677 "piecewise" => {
678 let values = m.get("values")?.as_array()?;
679 let mut coeffs = Vec::with_capacity(values.len() * 2);
680 for pt in values {
681 let pair = pt.as_array()?;
682 coeffs.push(pair.first()?.as_f64()?);
683 coeffs.push(pair.get(1)?.as_f64()?);
684 }
685 Some(GenCost {
686 model: 1,
687 startup,
688 shutdown,
689 ncost: values.len(),
690 coeffs,
691 })
692 }
693 _ => None,
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700 use crate::network::BusType;
701
702 fn fixture(name: &str) -> String {
703 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
704 .join("../tests/data/egret")
705 .join(name);
706 std::fs::read_to_string(path).unwrap()
707 }
708
709 #[test]
710 fn reads_buses_loads_branches_and_reference() {
711 let net = parse_egret_json(&fixture("case30.json")).unwrap();
712 assert!((net.base_mva - 100.0).abs() < 1e-9);
713 assert_eq!(net.buses.len(), 30);
714 assert_eq!(net.loads.len(), 20);
715 assert_eq!(net.shunts.len(), 2);
716 assert_eq!(net.branches.len(), 41);
717 assert_eq!(net.generators.len(), 6);
718 let refs = net.buses.iter().filter(|b| b.kind == BusType::Ref).count();
720 assert_eq!(refs, 1);
721 }
722
723 #[test]
724 fn inverts_transformer_and_polynomial_cost() {
725 let net = parse_egret_json(&fixture("case14.json")).unwrap();
726 assert!(net.branches.iter().any(Branch::is_transformer));
728 let cost = net
730 .generators
731 .iter()
732 .find_map(|g| g.cost.as_ref())
733 .expect("a generator cost");
734 assert_eq!(cost.model, 2);
735 assert_eq!(cost.coeffs.len(), cost.ncost);
736 }
737
738 #[test]
739 fn maps_dc_branch_to_hvdc() {
740 let net = parse_egret_json(&fixture("dcline3.json")).unwrap();
741 assert_eq!(net.hvdc.len(), 1);
742 let dc = &net.hvdc[0];
743 assert_eq!((dc.from, dc.to), (BusId(1), BusId(3)));
744 assert!((dc.loss1 - 0.1).abs() < 1e-12); }
746
747 #[test]
748 fn rejects_unit_commitment_time_series() {
749 let uc =
750 r#"{"elements":{"bus":{"1":{}}},"system":{"baseMVA":100.0,"time_keys":["1","2"]}}"#;
751 let err = parse_egret_json(uc).unwrap_err();
752 assert!(matches!(err, Error::FormatRead { .. }));
753 }
754
755 #[test]
756 fn rejects_present_but_malformed_numeric_field() {
757 let base = r#"{"elements":{"bus":{"1":{"matpower_bustype":"ref"},
761 "2":{"matpower_bustype":"PQ"}},"branch":{"1":{"from_bus":"1","to_bus":"2",
762 "reactance":REACT}}},"system":{"baseMVA":100.0,"reference_bus":"1"}}"#;
763 assert!(parse_egret_json(&base.replace("REACT", "0.1")).is_ok());
764 let err = parse_egret_json(&base.replace("REACT", "\"oops\"")).unwrap_err();
765 assert!(matches!(err, Error::FormatRead { .. }));
766 }
767
768 #[test]
769 fn piecewise_cost_round_trips() {
770 let cost = GenCost {
775 model: 1,
776 startup: 10.0,
777 shutdown: 5.0,
778 ncost: 3,
779 coeffs: vec![0.0, 0.0, 50.0, 1000.0, 100.0, 2500.0],
780 };
781 let curve = cost_curve(&cost).expect("model 1 maps to a piecewise curve");
782 let back = read_cost(&curve, 10.0, 5.0).expect("piecewise curve reads back");
783 assert_eq!(back.model, 1);
784 assert_eq!(back.ncost, 3);
785 assert_eq!(back.coeffs, cost.coeffs);
786 assert_eq!((back.startup, back.shutdown), (10.0, 5.0));
787 }
788
789 #[test]
790 fn dc_branch_reads_every_power_field() {
791 let v = serde_json::json!({
795 "from_bus": "1", "to_bus": "2", "in_service": true,
796 "pf": 10.0, "pt": -9.5, "qf": 1.5, "qt": -1.0,
797 "vf": 1.02, "vt": 0.99, "pmin": -50.0, "pmax": 60.0,
798 "qminf": -5.0, "qmaxf": 5.0, "qmint": -4.0, "qmaxt": 4.5,
799 "loss0": 0.2, "loss_factor": 0.03
800 });
801 let h = read_dc_branch(&v).unwrap();
802 assert_eq!((h.from, h.to), (BusId(1), BusId(2)));
803 assert_eq!((h.pf, h.pt, h.qf, h.qt), (10.0, -9.5, 1.5, -1.0));
804 assert_eq!((h.vf, h.vt), (1.02, 0.99));
805 assert_eq!((h.pmin, h.pmax), (-50.0, 60.0));
806 assert_eq!((h.qminf, h.qmaxf, h.qmint, h.qmaxt), (-5.0, 5.0, -4.0, 4.5));
807 assert_eq!((h.loss0, h.loss1), (0.2, 0.03));
808 }
809
810 #[test]
811 fn rejects_present_but_malformed_cost() {
812 let v = serde_json::json!({
815 "bus": "1", "pg": 0.0, "qg": 0.0,
816 "p_max": 1.0, "p_min": 0.0, "q_max": 1.0, "q_min": -1.0,
817 "p_cost": {"data_type": "cost_curve", "cost_curve_type": "bogus", "values": {}}
818 });
819 assert!(matches!(read_gen(&v), Err(Error::FormatRead { .. })));
820 }
821}