use std::collections::HashMap;
use surge_network::network::Network;
use surge_network::network::SwitchedShunt;
use thiserror::Error;
pub(crate) fn unquote(s: &str) -> String {
let s = s.trim();
if s.starts_with('\'') && s.ends_with('\'') && s.len() >= 2 {
s[1..s.len() - 1].trim().to_string()
} else {
s.to_string()
}
}
pub(crate) struct RawLoad {
pub bus: u32,
pub id: String,
pub status: i32,
pub owner: Option<u32>,
pub pl: f64,
pub ql: f64,
pub conforming: bool,
pub zip_p_impedance_frac: f64,
pub zip_p_current_frac: f64,
pub zip_p_power_frac: f64,
pub zip_q_impedance_frac: f64,
pub zip_q_current_frac: f64,
pub zip_q_power_frac: f64,
}
pub(crate) struct RawShunt {
pub bus: u32,
pub status: i32,
pub gl: f64,
pub bl: f64,
}
#[derive(Debug)]
pub(crate) struct RawSwitchedShunt {
pub bus: u32,
pub modsw: i32,
pub stat: i32,
pub vswhi: f64,
pub vswlo: f64,
pub swrem: u32,
pub binit: f64,
pub blocks: Vec<(i32, f64)>,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub(crate) enum ApplyError {
#[error("load references missing bus {bus}")]
MissingLoadBus { bus: u32 },
}
pub(crate) fn apply_loads(network: &mut Network, loads: &[RawLoad]) -> Result<(), ApplyError> {
use surge_network::network::Load;
let bus_map: HashMap<u32, usize> = network
.buses
.iter()
.enumerate()
.map(|(i, b)| (b.number, i))
.collect();
for load in loads {
if load.status != 1 {
continue;
}
if !bus_map.contains_key(&load.bus) {
return Err(ApplyError::MissingLoadBus { bus: load.bus });
}
network.loads.push(Load {
bus: load.bus,
active_power_demand_mw: load.pl,
reactive_power_demand_mvar: load.ql,
in_service: load.status == 1,
conforming: load.conforming,
id: load.id.clone(),
zip_p_impedance_frac: load.zip_p_impedance_frac,
zip_p_current_frac: load.zip_p_current_frac,
zip_p_power_frac: load.zip_p_power_frac,
zip_q_impedance_frac: load.zip_q_impedance_frac,
zip_q_current_frac: load.zip_q_current_frac,
zip_q_power_frac: load.zip_q_power_frac,
owners: load
.owner
.map(|owner| {
vec![surge_network::network::OwnershipEntry {
owner,
fraction: 1.0,
}]
})
.unwrap_or_default(),
..Load::new(0, 0.0, 0.0)
});
}
Ok(())
}
pub(crate) fn apply_shunts(network: &mut Network, shunts: &[RawShunt]) {
let bus_map: HashMap<u32, usize> = network
.buses
.iter()
.enumerate()
.map(|(i, b)| (b.number, i))
.collect();
for shunt in shunts {
if shunt.status != 1 {
continue;
}
if let Some(&idx) = bus_map.get(&shunt.bus) {
network.buses[idx].shunt_conductance_mw += shunt.gl;
network.buses[idx].shunt_susceptance_mvar += shunt.bl;
}
}
}
pub(crate) fn apply_switched_shunts(
network: &mut Network,
shunts: &[RawSwitchedShunt],
base_mva: f64,
) {
let bus_map: HashMap<u32, usize> = network
.buses
.iter()
.enumerate()
.map(|(i, b)| (b.number, i))
.collect();
let mut ordinal_by_bus: HashMap<u32, usize> = HashMap::new();
for shunt in shunts {
let Some(&bus_idx) = bus_map.get(&shunt.bus) else {
continue;
};
if shunt.stat != 1 || shunt.modsw == 0 {
if shunt.stat == 1 {
network.buses[bus_idx].shunt_susceptance_mvar += shunt.binit;
}
continue;
}
let v_target = (shunt.vswhi + shunt.vswlo) / 2.0;
let v_band = (shunt.vswhi - shunt.vswlo).abs().max(0.02);
let bus_regulated = if shunt.swrem != 0 {
if bus_map.contains_key(&shunt.swrem) {
shunt.swrem
} else {
shunt.bus
}
} else {
shunt.bus
};
let next_ordinal = ordinal_by_bus.entry(shunt.bus).or_insert(0);
let mut binit_remaining = shunt.binit;
for &(ni, bi) in &shunt.blocks {
if ni <= 0 || bi.abs() < 1e-9 {
continue;
}
let n_active_steps = if bi > 0.0 {
let active = (binit_remaining / bi).round() as i32;
active.clamp(0, ni)
} else {
let n_steps = (binit_remaining / bi).round() as i32;
(-n_steps).clamp(-ni, 0)
};
binit_remaining -= n_active_steps.unsigned_abs() as f64 * bi;
*next_ordinal += 1;
network.controls.switched_shunts.push(SwitchedShunt {
id: format!("switched_shunt_{}_{}", shunt.bus, *next_ordinal),
bus: shunt.bus,
bus_regulated,
b_step: bi.abs() / base_mva,
n_steps_cap: if bi > 0.0 { ni } else { 0 },
n_steps_react: if bi < 0.0 { ni } else { 0 },
v_target,
v_band,
n_active_steps,
});
}
if shunt.blocks.iter().all(|&(n, b)| n <= 0 || b.abs() < 1e-9) && shunt.binit.abs() > 1e-9 {
let bi = shunt.binit;
*next_ordinal += 1;
network.controls.switched_shunts.push(SwitchedShunt {
id: format!("switched_shunt_{}_{}", shunt.bus, *next_ordinal),
bus: shunt.bus,
bus_regulated,
b_step: bi.abs() / base_mva,
n_steps_cap: if bi > 0.0 { 1 } else { 0 },
n_steps_react: if bi < 0.0 { 1 } else { 0 },
v_target,
v_band,
n_active_steps: if bi > 0.0 { 1 } else { -1 },
});
}
}
}
pub(crate) fn sanitize_voltage_limits(network: &mut Network) {
for bus in &mut network.buses {
if bus.voltage_min_pu > 10.0 || bus.voltage_max_pu > 100.0 {
tracing::warn!(
"bus {}: vmin={:.2}, vmax={:.2} appear to be in kV, not p.u.; \
resetting to 0.9/1.1 p.u.",
bus.number,
bus.voltage_min_pu,
bus.voltage_max_pu
);
bus.voltage_min_pu = 0.9;
bus.voltage_max_pu = 1.1;
}
if bus.voltage_min_pu <= 0.0 {
bus.voltage_min_pu = 0.9;
}
if bus.voltage_max_pu <= 0.0 {
bus.voltage_max_pu = 1.1;
}
if bus.voltage_min_pu > bus.voltage_max_pu {
std::mem::swap(&mut bus.voltage_min_pu, &mut bus.voltage_max_pu);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use surge_network::Network;
use surge_network::network::{Bus, BusType};
fn one_bus_network(bus_num: u32) -> Network {
let mut net = Network::new("test");
net.base_mva = 100.0;
net.buses = vec![Bus::new(bus_num, BusType::Slack, 100.0)];
net
}
#[test]
fn fixed_shunt_baked_into_bus_bs() {
let mut net = one_bus_network(5);
let shunts = vec![RawSwitchedShunt {
bus: 5,
modsw: 0,
stat: 1,
vswhi: 1.1,
vswlo: 0.9,
swrem: 0,
binit: 50.0, blocks: vec![],
}];
apply_switched_shunts(&mut net, &shunts, 100.0);
assert!(
(net.buses[0].shunt_susceptance_mvar - 50.0).abs() < 1e-9,
"fixed shunt BINIT must be in bus.shunt_susceptance_mvar"
);
assert!(
net.controls.switched_shunts.is_empty(),
"no discrete SwitchedShunt objects for a fixed shunt"
);
}
#[test]
fn out_of_service_shunt_ignored() {
let mut net = one_bus_network(5);
let shunts = vec![RawSwitchedShunt {
bus: 5,
modsw: 1, stat: 0,
vswhi: 1.1,
vswlo: 0.9,
swrem: 0,
binit: 50.0,
blocks: vec![(1, 50.0)],
}];
apply_switched_shunts(&mut net, &shunts, 100.0);
assert!(
(net.buses[0].shunt_susceptance_mvar).abs() < 1e-9,
"OOS shunt must not affect bus.shunt_susceptance_mvar"
);
assert!(net.controls.switched_shunts.is_empty());
}
#[test]
fn controlled_shunt_creates_switched_shunt_not_in_bus_bs() {
let mut net = one_bus_network(5);
let shunts = vec![RawSwitchedShunt {
bus: 5,
modsw: 1,
stat: 1,
vswhi: 1.05,
vswlo: 0.95,
swrem: 0,
binit: 150.0,
blocks: vec![(4, 50.0)],
}];
apply_switched_shunts(&mut net, &shunts, 100.0);
assert!(
net.buses[0].shunt_susceptance_mvar.abs() < 1e-9,
"controlled shunt BINIT must not be baked into bus.shunt_susceptance_mvar"
);
assert_eq!(net.controls.switched_shunts.len(), 1);
let ss = &net.controls.switched_shunts[0];
assert_eq!(ss.bus, 5);
assert_eq!(ss.bus_regulated, 5);
assert_eq!(ss.n_steps_cap, 4);
assert_eq!(ss.n_steps_react, 0);
assert!(
(ss.b_step - 50.0 / 100.0).abs() < 1e-9,
"b_step = 50 Mvar / 100 MVA = 0.5 pu"
);
assert_eq!(ss.n_active_steps, 3);
assert!((ss.v_target - 1.0).abs() < 1e-9);
assert!((ss.v_band - 0.10).abs() < 1e-9);
}
#[test]
fn controlled_shunt_multi_block() {
let mut net = one_bus_network(1);
let shunts = vec![RawSwitchedShunt {
bus: 1,
modsw: 1,
stat: 1,
vswhi: 1.05,
vswlo: 0.95,
swrem: 0,
binit: 300.0,
blocks: vec![(2, 100.0), (4, 50.0)],
}];
apply_switched_shunts(&mut net, &shunts, 100.0);
assert!(net.buses[0].shunt_susceptance_mvar.abs() < 1e-9);
assert_eq!(net.controls.switched_shunts.len(), 2);
let b1 = &net.controls.switched_shunts[0];
assert_eq!(b1.n_steps_cap, 2);
assert!((b1.b_step - 1.0).abs() < 1e-9); assert_eq!(b1.n_active_steps, 2);
let b2 = &net.controls.switched_shunts[1];
assert_eq!(b2.n_steps_cap, 4);
assert!((b2.b_step - 0.5).abs() < 1e-9); assert_eq!(b2.n_active_steps, 2);
}
#[test]
fn controlled_shunt_no_blocks_uses_binit_as_single_step() {
let mut net = one_bus_network(3);
let shunts = vec![RawSwitchedShunt {
bus: 3,
modsw: 1,
stat: 1,
vswhi: 1.05,
vswlo: 0.95,
swrem: 0,
binit: 75.0,
blocks: vec![], }];
apply_switched_shunts(&mut net, &shunts, 100.0);
assert_eq!(net.controls.switched_shunts.len(), 1);
let ss = &net.controls.switched_shunts[0];
assert_eq!(ss.n_steps_cap, 1);
assert_eq!(ss.n_active_steps, 1);
assert!((ss.b_step - 0.75).abs() < 1e-9); }
#[test]
fn reactor_block_creates_react_steps() {
let mut net = one_bus_network(2);
let shunts = vec![RawSwitchedShunt {
bus: 2,
modsw: 1,
stat: 1,
vswhi: 1.05,
vswlo: 0.95,
swrem: 0,
binit: -200.0,
blocks: vec![(3, -100.0)],
}];
apply_switched_shunts(&mut net, &shunts, 100.0);
assert_eq!(net.controls.switched_shunts.len(), 1);
let ss = &net.controls.switched_shunts[0];
assert_eq!(ss.n_steps_cap, 0);
assert_eq!(ss.n_steps_react, 3);
assert!((ss.b_step - 1.0).abs() < 1e-9); assert_eq!(ss.n_active_steps, -2); }
#[test]
fn apply_loads_rejects_missing_bus_reference() {
let mut net = one_bus_network(1);
let loads = vec![RawLoad {
bus: 99,
id: String::new(),
status: 1,
owner: None,
pl: 10.0,
ql: 5.0,
conforming: true,
zip_p_impedance_frac: 0.0,
zip_p_current_frac: 0.0,
zip_p_power_frac: 1.0,
zip_q_impedance_frac: 0.0,
zip_q_current_frac: 0.0,
zip_q_power_frac: 1.0,
}];
let err = apply_loads(&mut net, &loads).expect_err("missing bus should be rejected");
assert_eq!(err, ApplyError::MissingLoadBus { bus: 99 });
assert!(net.loads.is_empty());
}
}