use std::sync::Arc;
use serde_json::{Map, Value};
use super::{Conversion, finish, jnum};
use crate::network::{
Branch, Bus, BusId, BusType, Extras, GenCost, Generator, Hvdc, Load, Network, Shunt,
SourceFormat,
};
use crate::{Error, Result};
const FMT: &str = "egret JSON";
#[must_use]
pub fn write_egret_json(net: &Network) -> Conversion {
let mut warnings = Vec::new();
let mut bus = Map::new();
for b in &net.buses {
bus.insert(b.id.to_string(), bus_obj(b));
}
let mut load = Map::new();
for (i, l) in net.loads.iter().enumerate() {
load.insert(format!("load_{}", i + 1), load_obj(l));
}
let mut shunt = Map::new();
for (i, s) in net.shunts.iter().enumerate() {
shunt.insert(format!("shunt_{}", i + 1), shunt_obj(s));
}
let mut branch = Map::new();
for (i, br) in net.branches.iter().enumerate() {
branch.insert((i + 1).to_string(), branch_obj(br));
}
let mut generator = Map::new();
for (i, g) in net.generators.iter().enumerate() {
generator.insert((i + 1).to_string(), gen_obj(g, &mut warnings));
}
if !net.hvdc.is_empty() {
warnings.push(format!(
"{} dcline(s) dropped: egret HVDC mapping not implemented",
net.hvdc.len()
));
}
if !net.transformers_3w.is_empty() {
warnings.push(format!(
"{} 3-winding transformer(s) dropped: the egret writer emits no 3-winding record",
net.transformers_3w.len()
));
}
if net
.buses
.iter()
.any(|b| b.evhi.is_some() || b.evlo.is_some())
{
warnings.push(
"emergency voltage band(s) (EVHI/EVLO) dropped: this writer carries one voltage band"
.into(),
);
}
if !net.storage.is_empty() {
warnings.push(format!(
"{} storage unit(s) dropped: egret storage mapping not implemented",
net.storage.len()
));
}
let mut elements = Map::new();
elements.insert("bus".into(), Value::Object(bus));
elements.insert("load".into(), Value::Object(load));
elements.insert("shunt".into(), Value::Object(shunt));
elements.insert("branch".into(), Value::Object(branch));
elements.insert("generator".into(), Value::Object(generator));
let mut system = Map::new();
system.insert("baseMVA".into(), jnum(net.base_mva));
match reference_bus(net) {
Some(r) => {
system.insert("reference_bus".into(), Value::String(r.id.to_string()));
system.insert("reference_bus_angle".into(), jnum(r.va));
}
None => warnings
.push("no single reference bus (BusType::Ref); system.reference_bus omitted".into()),
}
let mut root = Map::new();
root.insert("elements".into(), Value::Object(elements));
root.insert("system".into(), Value::Object(system));
finish(root, warnings)
}
fn reference_bus(net: &Network) -> Option<&Bus> {
let mut refs = net.buses.iter().filter(|b| b.kind == BusType::Ref);
let first = refs.next()?;
if refs.next().is_some() {
None } else {
Some(first)
}
}
fn bustype(kind: BusType) -> &'static str {
match kind {
BusType::Pq => "PQ",
BusType::Pv => "PV",
BusType::Ref => "ref",
BusType::Isolated => "isolated",
}
}
fn bus_obj(b: &Bus) -> Value {
let mut m = Map::new();
m.insert("base_kv".into(), jnum(b.base_kv));
m.insert(
"matpower_bustype".into(),
Value::String(bustype(b.kind).into()),
);
m.insert("vm".into(), jnum(b.vm));
m.insert("va".into(), jnum(b.va));
m.insert("v_min".into(), jnum(b.vmin));
m.insert("v_max".into(), jnum(b.vmax));
m.insert("area".into(), Value::String(b.area.to_string()));
m.insert("zone".into(), Value::String(b.zone.to_string()));
if let Some(name) = &b.name {
m.insert("name".into(), Value::String(name.clone()));
}
Value::Object(m)
}
fn load_obj(l: &Load) -> Value {
let mut m = Map::new();
m.insert("bus".into(), Value::String(l.bus.to_string()));
m.insert("p_load".into(), jnum(l.p));
m.insert("q_load".into(), jnum(l.q));
m.insert("in_service".into(), Value::Bool(l.in_service));
Value::Object(m)
}
fn shunt_obj(s: &Shunt) -> Value {
let mut m = Map::new();
m.insert("bus".into(), Value::String(s.bus.to_string()));
m.insert("shunt_type".into(), Value::String("fixed".into()));
m.insert("gs".into(), jnum(s.g));
m.insert("bs".into(), jnum(s.b));
Value::Object(m)
}
fn branch_obj(br: &Branch) -> Value {
let mut m = Map::new();
m.insert("from_bus".into(), Value::String(br.from.to_string()));
m.insert("to_bus".into(), Value::String(br.to.to_string()));
m.insert("resistance".into(), jnum(br.r));
m.insert("reactance".into(), jnum(br.x));
m.insert("charging_susceptance".into(), jnum(br.b));
m.insert("in_service".into(), Value::Bool(br.in_service));
m.insert("angle_diff_min".into(), jnum(br.angmin));
m.insert("angle_diff_max".into(), jnum(br.angmax));
if br.is_transformer() {
m.insert("branch_type".into(), Value::String("transformer".into()));
m.insert("transformer_tap_ratio".into(), jnum(br.effective_tap()));
m.insert("transformer_phase_shift".into(), jnum(br.shift));
} else {
m.insert("branch_type".into(), Value::String("line".into()));
}
if br.rate_a != 0.0 {
m.insert("rating_long_term".into(), jnum(br.rate_a));
}
if br.rate_b != 0.0 {
m.insert("rating_short_term".into(), jnum(br.rate_b));
}
if br.rate_c != 0.0 {
m.insert("rating_emergency".into(), jnum(br.rate_c));
}
Value::Object(m)
}
fn gen_obj(g: &Generator, warnings: &mut Vec<String>) -> Value {
let mut m = Map::new();
m.insert("bus".into(), Value::String(g.bus.to_string()));
m.insert("generator_type".into(), Value::String("thermal".into()));
m.insert("in_service".into(), Value::Bool(g.in_service));
m.insert("pg".into(), jnum(g.pg));
m.insert("qg".into(), jnum(g.qg));
m.insert("vg".into(), jnum(g.vg));
m.insert("mbase".into(), jnum(g.mbase));
m.insert("p_min".into(), jnum(g.pmin));
m.insert("p_max".into(), jnum(g.pmax));
m.insert("q_min".into(), jnum(g.qmin));
m.insert("q_max".into(), jnum(g.qmax));
if let Some(cost) = &g.cost {
if let Some(curve) = cost_curve(cost) {
m.insert("p_cost".into(), curve);
} else {
warnings.push(format!(
"generator at bus {} has a cost model egret's writer can't express; cost dropped",
g.bus
));
}
}
Value::Object(m)
}
fn cost_curve(cost: &GenCost) -> Option<Value> {
let mut curve = Map::new();
curve.insert("data_type".into(), Value::String("cost_curve".into()));
match cost.model {
2 => {
let mut values = Map::new();
let k = cost.coeffs.len();
for (i, &c) in cost.coeffs.iter().enumerate() {
values.insert((k - 1 - i).to_string(), jnum(c));
}
curve.insert("cost_curve_type".into(), Value::String("polynomial".into()));
curve.insert("values".into(), Value::Object(values));
Some(Value::Object(curve))
}
1 => {
let points: Vec<Value> = cost
.coeffs
.chunks_exact(2)
.map(|pt| Value::Array(vec![jnum(pt[0]), jnum(pt[1])]))
.collect();
curve.insert("cost_curve_type".into(), Value::String("piecewise".into()));
curve.insert("values".into(), Value::Array(points));
Some(Value::Object(curve))
}
_ => None,
}
}
pub fn parse_egret_json(content: &str) -> Result<Network> {
parse_egret_source(Arc::new(content.to_owned()), None)
}
pub(crate) fn parse_egret_source(source: Arc<String>, name_hint: Option<&str>) -> Result<Network> {
let content: &str = &source;
let root: Value = serde_json::from_str(content).map_err(|e| bad(e.to_string()))?;
let root = root
.as_object()
.ok_or_else(|| bad("top level is not a JSON object"))?;
let system = obj(root, "system").ok_or_else(|| bad("missing `system` object"))?;
if system.contains_key("time_keys") {
return Err(bad(
"egret unit commitment cases (system.time_keys) are not supported; expected a power flow ModelData",
));
}
let base_mva = system
.get("baseMVA")
.and_then(Value::as_f64)
.ok_or_else(|| bad("missing numeric system.baseMVA"))?;
let elements = obj(root, "elements").ok_or_else(|| bad("missing `elements` object"))?;
let name = root
.get("model_name")
.and_then(Value::as_str)
.or(name_hint)
.unwrap_or("case")
.to_string();
let mut buses = Vec::new();
if let Some(m) = obj(elements, "bus") {
for (k, v) in sorted_kv(m) {
buses.push(read_bus(k, v)?);
}
}
let mut loads = Vec::new();
if let Some(m) = obj(elements, "load") {
for v in sorted_vals(m) {
loads.push(read_load(v)?);
}
}
let mut shunts = Vec::new();
if let Some(m) = obj(elements, "shunt") {
for v in sorted_vals(m) {
shunts.push(read_shunt(v)?);
}
}
let mut branches = Vec::new();
if let Some(m) = obj(elements, "branch") {
for v in sorted_vals(m) {
branches.push(read_branch(v)?);
}
}
let mut generators = Vec::new();
if let Some(m) = obj(elements, "generator") {
for v in sorted_vals(m) {
generators.push(read_gen(v)?);
}
}
let mut hvdc = Vec::new();
if let Some(m) = obj(elements, "dc_branch") {
for v in sorted_vals(m) {
hvdc.push(read_dc_branch(v)?);
}
}
let net = Network {
name,
base_mva,
base_frequency: crate::network::DEFAULT_BASE_FREQUENCY,
buses,
loads,
shunts,
branches,
generators,
storage: Vec::new(),
hvdc,
transformers_3w: Vec::new(),
areas: Vec::new(),
solver: None,
source_format: SourceFormat::EgretJson,
source: Some(source),
};
net.check_references(FMT)?;
Ok(net)
}
fn bad(message: impl Into<String>) -> Error {
Error::FormatRead {
format: FMT,
message: message.into(),
}
}
fn obj<'a>(v: &'a Map<String, Value>, key: &str) -> Option<&'a Map<String, Value>> {
v.get(key).and_then(Value::as_object)
}
fn sorted_kv(map: &Map<String, Value>) -> Vec<(&String, &Value)> {
let mut items: Vec<(&String, &Value)> = map.iter().collect();
items.sort_by(|(a, _), (b, _)| num_key(a).cmp(&num_key(b)).then_with(|| a.cmp(b)));
items
}
fn sorted_vals(map: &Map<String, Value>) -> Vec<&Value> {
sorted_kv(map).into_iter().map(|(_, v)| v).collect()
}
fn num_key(k: &str) -> i64 {
let start = k.len() - k.bytes().rev().take_while(u8::is_ascii_digit).count();
k[start..].parse::<i64>().unwrap_or(i64::MAX)
}
fn id_from_f64(x: f64) -> Option<usize> {
(x >= 0.0 && x.fract() == 0.0 && x < usize::MAX as f64).then_some(x as usize)
}
fn parse_id(v: &Value) -> Option<usize> {
match v {
Value::String(s) => {
let s = s.trim();
s.parse::<usize>()
.ok()
.or_else(|| s.parse::<f64>().ok().and_then(id_from_f64))
}
Value::Number(n) => n
.as_u64()
.map(|x| x as usize)
.or_else(|| n.as_f64().and_then(id_from_f64)),
_ => None,
}
}
fn id_field(v: &Value, key: &str) -> Result<BusId> {
let raw = v
.get(key)
.ok_or_else(|| bad(format!("element missing `{key}`")))?;
parse_id(raw)
.map(BusId)
.ok_or_else(|| bad(format!("`{key}` is not a numeric bus id: {raw}")))
}
fn f(v: &Value, key: &str) -> Result<f64> {
f_or(v, key, 0.0)
}
fn f_or(v: &Value, key: &str, default: f64) -> Result<f64> {
match v.get(key) {
None | Some(Value::Null) => Ok(default),
Some(x) => x
.as_f64()
.ok_or_else(|| bad(format!("`{key}` is not a number: {x}"))),
}
}
fn usize_or(v: &Value, key: &str, default: usize) -> Result<usize> {
match v.get(key) {
None | Some(Value::Null) => Ok(default),
Some(x) => {
parse_id(x).ok_or_else(|| bad(format!("`{key}` is not a non-negative integer: {x}")))
}
}
}
fn flag(v: &Value, key: &str, default: bool) -> Result<bool> {
match v.get(key) {
None | Some(Value::Null) => Ok(default),
Some(Value::Bool(b)) => Ok(*b),
Some(x) => Err(bad(format!("`{key}` is not a boolean: {x}"))),
}
}
fn bustype_from_str(s: &str) -> BusType {
match s {
"PV" => BusType::Pv,
"ref" => BusType::Ref,
"isolated" => BusType::Isolated,
_ => BusType::Pq,
}
}
fn read_bus(key: &str, v: &Value) -> Result<Bus> {
let id = key
.trim()
.parse::<usize>()
.map_err(|_| bad(format!("bus key is not a numeric id: {key:?}")))?;
Ok(Bus {
id: BusId(id),
kind: bustype_from_str(
v.get("matpower_bustype")
.and_then(Value::as_str)
.unwrap_or("PQ"),
),
vm: f_or(v, "vm", 1.0)?,
va: f(v, "va")?,
base_kv: f(v, "base_kv")?,
vmax: f_or(v, "v_max", 1.1)?,
vmin: f_or(v, "v_min", 0.9)?,
evhi: None,
evlo: None,
area: usize_or(v, "area", 0)?,
zone: usize_or(v, "zone", 0)?,
name: v.get("name").and_then(Value::as_str).map(str::to_string),
extras: Extras::new(),
})
}
fn read_load(v: &Value) -> Result<Load> {
Ok(Load {
bus: id_field(v, "bus")?,
p: f(v, "p_load")?,
q: f(v, "q_load")?,
in_service: flag(v, "in_service", true)?,
extras: Extras::new(),
})
}
fn read_shunt(v: &Value) -> Result<Shunt> {
Ok(Shunt {
bus: id_field(v, "bus")?,
g: f(v, "gs")?,
b: f(v, "bs")?,
in_service: flag(v, "in_service", true)?,
control: None,
extras: Extras::new(),
})
}
fn read_branch(v: &Value) -> Result<Branch> {
let is_xf = v.get("branch_type").and_then(Value::as_str) == Some("transformer");
Ok(Branch {
from: id_field(v, "from_bus")?,
to: id_field(v, "to_bus")?,
r: f(v, "resistance")?,
x: f(v, "reactance")?,
b: f(v, "charging_susceptance")?,
rate_a: f(v, "rating_long_term")?,
rate_b: f(v, "rating_short_term")?,
rate_c: f(v, "rating_emergency")?,
tap: if is_xf {
f_or(v, "transformer_tap_ratio", 1.0)?
} else {
0.0
},
shift: f(v, "transformer_phase_shift")?,
in_service: flag(v, "in_service", true)?,
angmin: f_or(v, "angle_diff_min", -360.0)?,
angmax: f_or(v, "angle_diff_max", 360.0)?,
control: None,
extras: Extras::new(),
})
}
fn read_gen(v: &Value) -> Result<Generator> {
let startup = f_or(v, "startup_cost", 0.0)?;
let shutdown = f_or(v, "shutdown_cost", 0.0)?;
let cost = match v.get("p_cost") {
None | Some(Value::Null) => None,
Some(pc) => Some(read_cost(pc, startup, shutdown).ok_or_else(|| {
bad("`p_cost` is present but has an unrecognized or malformed cost_curve")
})?),
};
Ok(Generator {
bus: id_field(v, "bus")?,
pg: f(v, "pg")?,
qg: f(v, "qg")?,
pmax: f(v, "p_max")?,
pmin: f(v, "p_min")?,
qmax: f(v, "q_max")?,
qmin: f(v, "q_min")?,
vg: f_or(v, "vg", 1.0)?,
mbase: f_or(v, "mbase", 100.0)?,
in_service: flag(v, "in_service", true)?,
cost,
caps: Default::default(),
regulated_bus: None,
})
}
fn read_dc_branch(v: &Value) -> Result<Hvdc> {
Ok(Hvdc {
from: id_field(v, "from_bus")?,
to: id_field(v, "to_bus")?,
in_service: flag(v, "in_service", true)?,
pf: f(v, "pf")?,
pt: f(v, "pt")?,
qf: f(v, "qf")?,
qt: f(v, "qt")?,
vf: f_or(v, "vf", 1.0)?,
vt: f_or(v, "vt", 1.0)?,
pmin: f(v, "pmin")?,
pmax: f(v, "pmax")?,
qminf: f(v, "qminf")?,
qmaxf: f(v, "qmaxf")?,
qmint: f(v, "qmint")?,
qmaxt: f(v, "qmaxt")?,
loss0: f(v, "loss0")?,
loss1: f_or(v, "loss_factor", 0.0)?,
extras: Extras::new(),
})
}
fn read_cost(p_cost: &Value, startup: f64, shutdown: f64) -> Option<GenCost> {
let m = p_cost.as_object()?;
match m.get("cost_curve_type").and_then(Value::as_str)? {
"polynomial" => {
let values = m.get("values")?.as_object()?;
let pairs: Vec<(usize, f64)> = values
.iter()
.filter_map(|(k, c)| Some((k.parse().ok()?, c.as_f64()?)))
.collect();
let max_exp = pairs.iter().map(|(e, _)| *e).max()?;
let mut coeffs = vec![0.0; max_exp + 1]; for (e, c) in pairs {
coeffs[max_exp - e] = c;
}
let ncost = coeffs.len();
Some(GenCost {
model: 2,
startup,
shutdown,
ncost,
coeffs,
})
}
"piecewise" => {
let values = m.get("values")?.as_array()?;
let mut coeffs = Vec::with_capacity(values.len() * 2);
for pt in values {
let pair = pt.as_array()?;
coeffs.push(pair.first()?.as_f64()?);
coeffs.push(pair.get(1)?.as_f64()?);
}
Some(GenCost {
model: 1,
startup,
shutdown,
ncost: values.len(),
coeffs,
})
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network::BusType;
fn fixture(name: &str) -> String {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../tests/data/egret")
.join(name);
std::fs::read_to_string(path).unwrap()
}
#[test]
fn reads_buses_loads_branches_and_reference() {
let net = parse_egret_json(&fixture("case30.json")).unwrap();
assert!((net.base_mva - 100.0).abs() < 1e-9);
assert_eq!(net.buses.len(), 30);
assert_eq!(net.loads.len(), 20);
assert_eq!(net.shunts.len(), 2);
assert_eq!(net.branches.len(), 41);
assert_eq!(net.generators.len(), 6);
let refs = net.buses.iter().filter(|b| b.kind == BusType::Ref).count();
assert_eq!(refs, 1);
}
#[test]
fn inverts_transformer_and_polynomial_cost() {
let net = parse_egret_json(&fixture("case14.json")).unwrap();
assert!(net.branches.iter().any(Branch::is_transformer));
let cost = net
.generators
.iter()
.find_map(|g| g.cost.as_ref())
.expect("a generator cost");
assert_eq!(cost.model, 2);
assert_eq!(cost.coeffs.len(), cost.ncost);
}
#[test]
fn maps_dc_branch_to_hvdc() {
let net = parse_egret_json(&fixture("dcline3.json")).unwrap();
assert_eq!(net.hvdc.len(), 1);
let dc = &net.hvdc[0];
assert_eq!((dc.from, dc.to), (BusId(1), BusId(3)));
assert!((dc.loss1 - 0.1).abs() < 1e-12); }
#[test]
fn rejects_unit_commitment_time_series() {
let uc =
r#"{"elements":{"bus":{"1":{}}},"system":{"baseMVA":100.0,"time_keys":["1","2"]}}"#;
let err = parse_egret_json(uc).unwrap_err();
assert!(matches!(err, Error::FormatRead { .. }));
}
#[test]
fn rejects_present_but_malformed_numeric_field() {
let base = r#"{"elements":{"bus":{"1":{"matpower_bustype":"ref"},
"2":{"matpower_bustype":"PQ"}},"branch":{"1":{"from_bus":"1","to_bus":"2",
"reactance":REACT}}},"system":{"baseMVA":100.0,"reference_bus":"1"}}"#;
assert!(parse_egret_json(&base.replace("REACT", "0.1")).is_ok());
let err = parse_egret_json(&base.replace("REACT", "\"oops\"")).unwrap_err();
assert!(matches!(err, Error::FormatRead { .. }));
}
#[test]
fn piecewise_cost_round_trips() {
let cost = GenCost {
model: 1,
startup: 10.0,
shutdown: 5.0,
ncost: 3,
coeffs: vec![0.0, 0.0, 50.0, 1000.0, 100.0, 2500.0],
};
let curve = cost_curve(&cost).expect("model 1 maps to a piecewise curve");
let back = read_cost(&curve, 10.0, 5.0).expect("piecewise curve reads back");
assert_eq!(back.model, 1);
assert_eq!(back.ncost, 3);
assert_eq!(back.coeffs, cost.coeffs);
assert_eq!((back.startup, back.shutdown), (10.0, 5.0));
}
#[test]
fn dc_branch_reads_every_power_field() {
let v = serde_json::json!({
"from_bus": "1", "to_bus": "2", "in_service": true,
"pf": 10.0, "pt": -9.5, "qf": 1.5, "qt": -1.0,
"vf": 1.02, "vt": 0.99, "pmin": -50.0, "pmax": 60.0,
"qminf": -5.0, "qmaxf": 5.0, "qmint": -4.0, "qmaxt": 4.5,
"loss0": 0.2, "loss_factor": 0.03
});
let h = read_dc_branch(&v).unwrap();
assert_eq!((h.from, h.to), (BusId(1), BusId(2)));
assert_eq!((h.pf, h.pt, h.qf, h.qt), (10.0, -9.5, 1.5, -1.0));
assert_eq!((h.vf, h.vt), (1.02, 0.99));
assert_eq!((h.pmin, h.pmax), (-50.0, 60.0));
assert_eq!((h.qminf, h.qmaxf, h.qmint, h.qmaxt), (-5.0, 5.0, -4.0, 4.5));
assert_eq!((h.loss0, h.loss1), (0.2, 0.03));
}
#[test]
fn rejects_present_but_malformed_cost() {
let v = serde_json::json!({
"bus": "1", "pg": 0.0, "qg": 0.0,
"p_max": 1.0, "p_min": 0.0, "q_max": 1.0, "q_min": -1.0,
"p_cost": {"data_type": "cost_curve", "cost_curve_type": "bogus", "values": {}}
});
assert!(matches!(read_gen(&v), Err(Error::FormatRead { .. })));
}
}