use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use super::map::{BRANCH_DEVICE_TYPE, LINE_CIRCUIT, derive_bus_kinds};
use crate::network::{
Branch, Bus, BusId, BusType, Extras, Generator, Load, Network, Shunt, SourceFormat,
};
use crate::{Error, Result};
const FMT: &str = "PowerWorld .pwb";
const MVA_BASE: f64 = 100.0;
const RESYNC_WINDOW: usize = 1024;
type Probe<T> = std::result::Result<T, &'static str>;
pub fn parse_pwb(bytes: &[u8], name_hint: Option<&str>) -> Result<Network> {
let header_constant = expect_header(bytes)?;
reject_unsupported_vintage(bytes)?;
let gen_variants = match header_constant {
425 => GenVariants {
plain: true,
reg: false,
},
508 => GenVariants {
plain: true,
reg: true,
},
_ => GenVariants {
plain: false,
reg: true,
},
};
let bus_runs = RefCell::new(HashMap::new());
match search_table_chain(bytes, name_hint, gen_variants, &bus_runs, false) {
Some(net) => net,
None => search_table_chain(bytes, name_hint, gen_variants, &bus_runs, true).unwrap_or_else(
|| {
Err(Error::FormatRead {
format: FMT,
message: "no table chain matches the validated .pwb layouts \
(buses, loads, generators, shunts, branches in sequence)"
.into(),
})
},
),
}
}
#[derive(Clone, Copy)]
struct GenVariants {
plain: bool,
reg: bool,
}
fn search_table_chain(
bytes: &[u8],
name_hint: Option<&str>,
gen_variants: GenVariants,
bus_runs: &RefCell<BusRuns>,
wide_bus_glue: bool,
) -> Option<Result<Network>> {
for (buses, bus_end, last_bus_unk) in bus_table_candidates(bytes, bus_runs, wide_bus_glue) {
let Some(bus_ids) = BusIdSet::new(&buses) else {
continue; };
let load_runs = RefCell::new(HashMap::new());
let gen_runs = RefCell::new(HashMap::new());
let gen_reg_runs = RefCell::new(HashMap::new());
let shunt_runs = RefCell::new(HashMap::new());
let branch_runs = RefCell::new(HashMap::new());
let load_scan_end = resync_end(bytes, bus_end, last_bus_unk & 0x10 != 0);
for (loads, l_end) in device_table_candidates(
bytes,
bus_end..load_scan_end,
&bus_ids,
read_load_record,
&load_runs,
48,
) {
let gen_candidates = gen_variants
.plain
.then(|| {
device_table_candidates(
bytes,
l_end..l_end + RESYNC_WINDOW,
&bus_ids,
read_gen_record,
&gen_runs,
48,
)
})
.into_iter()
.flatten()
.chain(
gen_variants
.reg
.then(|| {
device_table_candidates(
bytes,
l_end..l_end + RESYNC_WINDOW,
&bus_ids,
read_gen_reg_record,
&gen_reg_runs,
96,
)
})
.into_iter()
.flatten(),
);
for (generators, g_end) in gen_candidates {
for (shunts, s_end) in device_table_candidates(
bytes,
g_end..g_end + RESYNC_WINDOW,
&bus_ids,
read_shunt_record,
&shunt_runs,
48,
) {
let Some(branches) = find_branch_table(bytes, s_end, &bus_ids, &branch_runs)
else {
continue;
};
let mut buses = buses.clone();
derive_bus_kinds(&mut buses, &generators);
let net = Network {
name: name_hint.unwrap_or("case").to_string(),
base_mva: MVA_BASE,
buses,
loads,
shunts,
branches,
generators,
storage: Vec::new(),
hvdc: Vec::new(),
source_format: SourceFormat::PowerWorldBinary,
source: None,
};
return Some(net.check_references(FMT).map(|()| net));
}
}
}
}
None
}
struct Cur<'a> {
b: &'a [u8],
pos: usize,
}
impl<'a> Cur<'a> {
fn take(&mut self, n: usize) -> Probe<&'a [u8]> {
if self.pos + n > self.b.len() {
return Err("truncated record");
}
let s = &self.b[self.pos..self.pos + n];
self.pos += n;
Ok(s)
}
fn u8(&mut self) -> Probe<u8> {
Ok(self.take(1)?[0])
}
fn u16(&mut self) -> Probe<u16> {
Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
}
fn u32(&mut self) -> Probe<u32> {
Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
}
fn f32(&mut self) -> Probe<f64> {
Ok(f64::from(f32::from_le_bytes(
self.take(4)?.try_into().unwrap(),
)))
}
fn f64(&mut self) -> Probe<f64> {
Ok(f64::from_le_bytes(self.take(8)?.try_into().unwrap()))
}
fn string(&mut self, max: usize) -> Probe<&'a [u8]> {
let n = self.u32()? as usize;
if n > max {
return Err("string length exceeds the field maximum");
}
let s = self.take(n)?;
if !printable(s) {
return Err("string has non printable bytes");
}
Ok(s)
}
fn short_string(&mut self, max: usize) -> Probe<&'a [u8]> {
let n = self.u8()? as usize;
if n > max {
return Err("device ID length exceeds the field maximum");
}
let s = self.take(n)?;
if !printable(s) {
return Err("device ID has non printable bytes");
}
Ok(s)
}
fn short_string_2(&mut self) -> Probe<&'a [u8]> {
let n = self.u8()? as usize;
if n == 0 || n > 2 {
return Err("fixed capacity ID length not 1 or 2");
}
let text = self.take(2)?;
if !printable(&text[..n]) {
return Err("fixed capacity ID has non printable bytes");
}
Ok(&text[..n])
}
}
const BLOB_WINDOW: usize = 4 << 20;
fn resync_end(b: &[u8], after: usize, prev_bit4: bool) -> usize {
if prev_bit4 {
(after + BLOB_WINDOW).min(b.len())
} else {
after + RESYNC_WINDOW
}
}
fn printable(s: &[u8]) -> bool {
s.iter().all(|&c| (0x20..0x7f).contains(&c))
}
fn expect_header(b: &[u8]) -> Result<u64> {
const DECODED: [u64; 6] = [425, 483, 508, 537, 550, 551];
let bad = || Error::FormatRead {
format: FMT,
message: "not a recognized PowerWorld binary case (header magic mismatch); \
only the validated .pwb layouts are read"
.into(),
};
if b.len() < 0x40 {
return Err(bad());
}
let word = |i: usize| u64::from_le_bytes(b[i * 8..i * 8 + 8].try_into().unwrap());
let (a, v, c) = (word(0), word(1), word(2));
if a != 15000 {
return Err(bad());
}
if c != 20 || !DECODED.contains(&v) {
return Err(unsupported_vintage(format!(
"header format words ({v}, {c}); the decoded eras are \
425/483/508/537/550/551 with 20"
)));
}
Ok(v)
}
fn reject_unsupported_vintage(b: &[u8]) -> Result<()> {
let scan = b.len().min(0x10000).saturating_sub(8);
let mut heads = 0usize;
let mut at = 0x20;
while at < scan {
let Ok((_, after)) = read_bus_head(b, at) else {
at += 1;
continue;
};
heads += 1;
if heads >= 2 {
return Ok(());
}
at = after;
}
Err(unsupported_vintage(
"no recognized bus record layout in the leading 64 KiB",
))
}
fn unsupported_vintage(detail: impl std::fmt::Display) -> Error {
Error::FormatRead {
format: FMT,
message: format!(
"unsupported PowerWorld .pwb vintage: {detail}; only the validated 425/483/508 \
era record layouts are decoded (see docs/powerworld.md)"
),
}
}
enum BusIdSet {
Bitmap(Vec<u64>),
Sparse(Vec<usize>),
}
impl BusIdSet {
fn new(buses: &[Bus]) -> Option<Self> {
let max = buses.iter().map(|b| b.id.0).max().unwrap_or(0);
let words = max / 64 + 1;
if words > (buses.len() * 4).max(1024) {
let mut ids: Vec<usize> = buses.iter().map(|b| b.id.0).collect();
ids.sort_unstable();
if ids.windows(2).any(|w| w[0] == w[1]) {
return None;
}
return Some(Self::Sparse(ids));
}
let mut bits = vec![0u64; words];
for bus in buses {
let (w, bit) = (bus.id.0 / 64, 1u64 << (bus.id.0 % 64));
if bits[w] & bit != 0 {
return None;
}
bits[w] |= bit;
}
Some(Self::Bitmap(bits))
}
#[inline]
fn contains(&self, id: usize) -> bool {
match self {
Self::Bitmap(words) => words
.get(id / 64)
.is_some_and(|w| w & (1 << (id % 64)) != 0),
Self::Sparse(ids) => ids.binary_search(&id).is_ok(),
}
}
}
struct Run<T> {
items: Vec<T>,
ends: Vec<usize>,
dead: bool,
}
impl<T: Clone> Run<T> {
fn start(item: T, end: usize) -> Self {
Run {
items: vec![item],
ends: vec![end],
dead: false,
}
}
fn prefix(
&mut self,
count: usize,
mut next: impl FnMut(usize, &T) -> Option<(T, usize)>,
) -> Option<(Vec<T>, usize)> {
if count == 0 {
return None; }
while !self.dead && self.items.len() < count {
let after = *self.ends.last().unwrap();
match next(after, self.items.last().unwrap()) {
Some((item, end)) => {
self.items.push(item);
self.ends.push(end);
}
None => self.dead = true,
}
}
(self.items.len() >= count).then(|| (self.items[..count].to_vec(), self.ends[count - 1]))
}
}
struct BusHead {
bus: Bus,
unk: u32,
}
fn known_bus_flags(unk: u32) -> bool {
unk & !0x171 == 0x06
}
fn bus_family(unk: u32) -> u32 {
unk & !0x151
}
type BusRuns = HashMap<usize, (Run<(Bus, u32)>, u32)>;
fn bus_table_candidates<'a>(
b: &'a [u8],
runs: &'a RefCell<BusRuns>,
wide_glue: bool,
) -> impl Iterator<Item = (Vec<Bus>, usize, u32)> + 'a {
let limit = b.len().saturating_sub(4).min(0x10000);
(0x20..limit).flat_map(move |at| {
let count = u32::from_le_bytes(b[at..at + 4].try_into().unwrap()) as usize;
let glues = if wide_glue {
(count != 0 && count < 256).then_some(49..=96)
} else {
let max_glue = if count >= 256 { 96 } else { 48 };
(count != 0 && count <= 2_000_000).then_some(0..=max_glue)
};
glues
.into_iter()
.flatten()
.filter_map(move |glue| bus_run(b, runs, at + 4 + glue, count))
.map(|(heads, end)| {
let last_unk = heads.last().map_or(0, |(_, unk)| *unk);
(
heads.into_iter().map(|(bus, _)| bus).collect(),
end,
last_unk,
)
})
})
}
fn bus_run(
b: &[u8],
runs: &RefCell<BusRuns>,
first: usize,
count: usize,
) -> Option<(Vec<(Bus, u32)>, usize)> {
let mut map = runs.borrow_mut();
let (run, family) = match map.entry(first) {
Entry::Occupied(e) => e.into_mut(),
Entry::Vacant(e) => {
let (head, end) = read_bus_head(b, first).ok()?;
let family = bus_family(head.unk);
e.insert((Run::start((head.bus, head.unk), end), family))
}
};
let family = *family;
run.prefix(count, |after, prev| {
(after..resync_end(b, after, prev.1 & 0x10 != 0)).find_map(|p| {
read_bus_head(b, p)
.ok()
.filter(|(h, _)| bus_family(h.unk) == family)
.map(|(h, end)| ((h.bus, h.unk), end))
})
})
}
fn read_bus_head(b: &[u8], at: usize) -> Probe<(BusHead, usize)> {
let mut c = Cur { b, pos: at };
let num = c.u32()? as usize;
if num == 0 || num > 99_999_999 {
return Err("implausible bus number");
}
let name_len = c.u32()? as usize;
if name_len == 0 {
return Err("empty bus name");
}
if name_len > 64 {
return Err("string length exceeds the field maximum");
}
let name = c.take(name_len)?;
let unk = c.u32()?;
if !known_bus_flags(unk) {
return Err("bus record flags not in the validated set");
}
if !printable(name) {
return Err("string has non printable bytes");
}
if unk & 1 == 0 {
let _extra = c.u16()?;
}
let kv = c.f32()?;
if !kv.is_finite() || !(0.0..=10_000.0).contains(&kv) {
return Err("implausible nominal kV");
}
let area = c.u32()? as usize;
let zone = c.u32()? as usize;
let ba = c.u32()?;
if area > 100_000_000 || zone > 100_000_000 || ba > 100_000_000 {
return Err("implausible area/zone/BA number");
}
let _label = c.string(64)?;
let vm = c.f64()?;
let va_rad = c.f64()?;
if !vm.is_finite() || !(0.0..=10.0).contains(&vm) || !va_rad.is_finite() || va_rad.abs() > 100.0
{
return Err("implausible voltage solution");
}
let bus = Bus {
id: BusId(num),
kind: BusType::Pq,
vm,
va: va_rad.to_degrees(),
base_kv: kv,
vmax: 1.1,
vmin: 0.9,
area,
zone,
name: Some(String::from_utf8_lossy(name).into_owned()),
extras: Extras::new(),
};
Ok((BusHead { bus, unk }, c.pos))
}
trait ReadRecord<T>: Fn(&[u8], usize, &BusIdSet) -> Probe<(T, usize)> + Copy {}
impl<T, F: Fn(&[u8], usize, &BusIdSet) -> Probe<(T, usize)> + Copy> ReadRecord<T> for F {}
fn read_device_head<T>(
b: &[u8],
at: usize,
bus_ids: &BusIdSet,
read: fn(&mut Cur, BusId, &[u8]) -> Probe<T>,
) -> Probe<(T, usize)> {
let mut c = Cur { b, pos: at };
let bus = c.u32()? as usize;
if !bus_ids.contains(bus) {
return Err("record references an unknown bus");
}
let id = c.short_string(8)?;
if id.is_empty() {
return Err("empty device ID");
}
let v = read(&mut c, BusId(bus), id)?;
Ok((v, c.pos))
}
fn read_load_record(b: &[u8], at: usize, bus_ids: &BusIdSet) -> Probe<(Load, usize)> {
read_device_head(b, at, bus_ids, read_load)
}
fn read_gen_record(b: &[u8], at: usize, bus_ids: &BusIdSet) -> Probe<(Generator, usize)> {
read_device_head(b, at, bus_ids, read_gen)
}
fn read_shunt_record(b: &[u8], at: usize, bus_ids: &BusIdSet) -> Probe<(Shunt, usize)> {
read_device_head(b, at, bus_ids, read_shunt)
}
fn device_table_candidates<'a, T: Clone + 'a>(
b: &'a [u8],
scan: std::ops::Range<usize>,
bus_ids: &'a BusIdSet,
read: impl ReadRecord<T> + 'a,
runs: &'a RefCell<HashMap<usize, Run<T>>>,
max_glue: usize,
) -> impl Iterator<Item = (Vec<T>, usize)> + 'a {
let limit = scan.end.min(b.len().saturating_sub(4));
(scan.start..limit).flat_map(move |at| {
let count = u32::from_le_bytes(b[at..at + 4].try_into().unwrap()) as usize;
let glues = (count != 0 && count <= 10_000_000).then_some(0..=max_glue);
glues
.into_iter()
.flatten()
.filter_map(move |glue| device_run(b, runs, at + 4 + glue, count, bus_ids, read))
})
}
fn device_run<T: Clone>(
b: &[u8],
runs: &RefCell<HashMap<usize, Run<T>>>,
first: usize,
count: usize,
bus_ids: &BusIdSet,
read: impl ReadRecord<T>,
) -> Option<(Vec<T>, usize)> {
let mut map = runs.borrow_mut();
let run = match map.entry(first) {
Entry::Occupied(e) => e.into_mut(),
Entry::Vacant(e) => {
let (item, end) = read(b, first, bus_ids).ok()?;
e.insert(Run::start(item, end))
}
};
run.prefix(count, |after, _| {
(after..after + RESYNC_WINDOW).find_map(|p| read(b, p, bus_ids).ok())
})
}
fn read_load(c: &mut Cur, bus: BusId, id: &[u8]) -> Probe<Load> {
let _flag = c.u8()?;
let p = c.f32()? * MVA_BASE;
let q = c.f32()? * MVA_BASE;
if !p.is_finite() || !q.is_finite() || p.abs() > 1.0e6 || q.abs() > 1.0e6 {
return Err("implausible load power");
}
let mut extras = Extras::new();
extras.insert(
"LoadID".into(),
serde_json::Value::String(String::from_utf8_lossy(id).into_owned()),
);
Ok(Load {
bus,
p,
q,
in_service: true,
extras,
})
}
fn read_gen(c: &mut Cur, bus: BusId, id: &[u8]) -> Probe<Generator> {
let record_start = c.pos - (4 + 1) - id.len(); let mut chosen = None;
for anchor in [9usize, 10, 11, 12] {
let mut probe = Cur {
b: c.b,
pos: record_start + anchor,
};
if let Ok(vals) = read_gen_f32_block(&mut probe) {
chosen = Some((vals, probe.pos));
break;
}
}
let Some((v, end)) = chosen else {
return Err("generator record does not match the validated layouts");
};
c.pos = end;
Ok(gen_from_block(bus, &v, true))
}
fn read_gen_f32_block(c: &mut Cur) -> Probe<[f64; 8]> {
let mut v = [0.0f64; 8];
for (i, slot) in v.iter_mut().enumerate() {
let x = c.f32()?;
if !x.is_finite() || x.abs() >= 1.0e6 {
return Err("generator record does not match the validated layouts");
}
if (i == 4 && !(0.5..=1.6).contains(&x)) || (i == 5 && !(0.1..=1.0e5).contains(&x)) {
return Err("generator record does not match the validated layouts");
}
*slot = x;
}
Ok(v)
}
fn gen_from_block(bus: BusId, v: &[f64; 8], in_service: bool) -> Generator {
Generator {
bus,
pg: v[0] * MVA_BASE,
qg: v[1] * MVA_BASE,
qmax: v[2] * MVA_BASE,
qmin: v[3] * MVA_BASE,
vg: v[4],
mbase: v[5],
pmax: v[6] * MVA_BASE,
pmin: v[7] * MVA_BASE,
in_service,
cost: None,
caps: Default::default(),
}
}
fn read_gen_reg_record(b: &[u8], at: usize, bus_ids: &BusIdSet) -> Probe<(Generator, usize)> {
let mut c = Cur { b, pos: at };
let bus = c.u32()? as usize;
if !bus_ids.contains(bus) {
return Err("record references an unknown bus");
}
let reg = c.u32()? as usize;
if !bus_ids.contains(reg) {
return Err("regulated bus is not a known bus");
}
let _id = c.short_string_2()?;
if c.u8()? != 1 {
return Err("generator record lead byte not 1");
}
let _ = c.u8()?; let pres = c.u8()?;
if pres & !0x23 != 0 {
return Err("generator presence byte not in the validated set");
}
if pres & 0x22 == 0x22 {
return Err("generator presence bits 1 and 5 together are unobserved");
}
for bit in [0x01, 0x20] {
if pres & bit != 0 {
let v = c.f32()?;
if !v.is_finite() || v.abs() > 1.0e6 {
return Err("implausible presence gated generator value");
}
}
}
if pres & 2 != 0 {
let _ = c.u8()?;
}
let v = read_gen_f32_block(&mut c)?;
if c.u8()? != 0 {
return Err("generator record separator byte not zero");
}
let status = c.u8()?;
if status & !0x01 != 0x08 {
return Err("generator status byte not in the validated set");
}
let rmpct = c.f32()?;
if !rmpct.is_finite() || !(0.0..=1000.0).contains(&rmpct) {
return Err("implausible remote regulation percentage");
}
Ok((gen_from_block(BusId(bus), &v, status & 1 == 1), c.pos))
}
fn read_shunt(c: &mut Cur, bus: BusId, id: &[u8]) -> Probe<Shunt> {
let record_start = c.pos - (4 + 1) - id.len(); let mut probe = Cur {
b: c.b,
pos: record_start + 24,
};
let b_mvar = probe.f32()? * MVA_BASE;
if !b_mvar.is_finite() || b_mvar.abs() > 1.0e6 {
return Err("implausible shunt MVAr");
}
c.pos = probe.pos;
let mut extras = Extras::new();
extras.insert(
"ShuntID".into(),
serde_json::Value::String(String::from_utf8_lossy(id).into_owned()),
);
Ok(Shunt {
bus,
g: 0.0,
b: b_mvar,
in_service: true,
extras,
})
}
fn known_branch_flags(flags: u16) -> bool {
flags & !0x93 == 0x006C
}
fn find_branch_table(
b: &[u8],
from: usize,
bus_ids: &BusIdSet,
runs: &RefCell<HashMap<usize, Run<(Branch, u16)>>>,
) -> Option<Vec<Branch>> {
let limit = (from + RESYNC_WINDOW * 2).min(b.len().saturating_sub(4));
for at in from..limit {
let count = u32::from_le_bytes(b[at..at + 4].try_into().unwrap()) as usize;
if count == 0 || count > 10_000_000 {
continue;
}
let Some(first) =
(at + 4..(at + 64).min(b.len())).find(|&p| read_branch_head(b, p, bus_ids).is_ok())
else {
continue;
};
if let Some((branches, after)) = branch_run(b, runs, first, count, bus_ids) {
let last_bit4 = branches.last().is_some_and(|(_, flags)| flags & 0x10 != 0);
let continues = (after..resync_end(b, after, last_bit4))
.any(|p| read_branch_head(b, p, bus_ids).is_ok());
if !continues {
return Some(branches.into_iter().map(|(br, _)| br).collect());
}
}
}
None
}
fn branch_run(
b: &[u8],
runs: &RefCell<HashMap<usize, Run<(Branch, u16)>>>,
first: usize,
count: usize,
bus_ids: &BusIdSet,
) -> Option<(Vec<(Branch, u16)>, usize)> {
let mut map = runs.borrow_mut();
let run = match map.entry(first) {
Entry::Occupied(e) => e.into_mut(),
Entry::Vacant(e) => {
let (br, end, flags) = read_branch_head(b, first, bus_ids).ok()?;
e.insert(Run::start((br, flags), end))
}
};
run.prefix(count, |after, prev| {
(after..resync_end(b, after, prev.1 & 0x10 != 0)).find_map(|p| {
read_branch_head(b, p, bus_ids)
.ok()
.map(|(br, end, flags)| ((br, flags), end))
})
})
}
#[allow(clippy::many_single_char_names)] fn read_branch_head(b: &[u8], at: usize, bus_ids: &BusIdSet) -> Probe<(Branch, usize, u16)> {
let mut c = Cur { b, pos: at };
let from = c.u32()? as usize;
let to = c.u32()? as usize;
if !bus_ids.contains(from) || !bus_ids.contains(to) || from == to {
return Err("branch references unknown buses");
}
let flags = c.u16()?;
if !known_branch_flags(flags) {
return Err("branch record flags not in the validated set");
}
let circuit = if flags & 1 == 0 {
Some(c.short_string_2()?)
} else {
None
};
let r = c.f32()?;
let x = c.f32()?;
let b_chg = c.f32()?;
for v in [r, x, b_chg] {
if !v.is_finite() || v.abs() > 1.0e4 {
return Err("implausible branch impedance");
}
}
let _g = c.f32()?;
let inline = if flags & 2 == 0 { 3 } else { 2 };
let mut rates = [0.0f64; 3];
for slot in rates.iter_mut().take(inline) {
let v = c.f32()?;
if !v.is_finite() || !(0.0..=1.0e6).contains(&v) {
return Err("implausible branch rating");
}
*slot = v * MVA_BASE;
}
let tag = c.u32()?;
if tag != 12 {
return Err("branch rating block tag not 12; unvalidated .pwb branch variant");
}
for _ in 0..11 {
let v = c.f32()?;
if !v.is_finite() || v.abs() > 1.0e6 {
return Err("implausible branch rating block value");
}
}
if c.u8()? != 0 {
return Err("branch record separator byte not zero");
}
let kind = c.u8()?;
let (device, tap) = match kind {
0x01 => ("Line", 0.0),
0x00 => {
let tap = c.f32()?;
if !tap.is_finite() || !(0.2..=5.0).contains(&tap) {
return Err("implausible transformer tap");
}
("Transformer", tap)
}
_ => {
return Err("branch kind marker not in the validated set");
}
};
let mut extras = Extras::new();
extras.insert(
LINE_CIRCUIT.into(),
serde_json::Value::String(circuit.map_or_else(
|| " 1".to_string(),
|s| String::from_utf8_lossy(s).into_owned(),
)),
);
extras.insert(
BRANCH_DEVICE_TYPE.into(),
serde_json::Value::String(device.into()),
);
let br = Branch {
from: BusId(from),
to: BusId(to),
r,
x,
b: b_chg,
rate_a: rates[0],
rate_b: rates[1],
rate_c: rates[2],
tap,
shift: 0.0,
in_service: true,
angmin: -360.0,
angmax: 360.0,
extras,
};
Ok((br, c.pos, flags))
}