use std::collections::HashMap;
use std::path::Path;
use num_complex::Complex;
use serde::{Deserialize, Serialize};
use crate::json::NetworkData;
use crate::network::{build_ybus, power_injections};
use crate::solver::SolveStatus;
use crate::types::Bus;
use super::DynamicSystem;
use super::events::{Event, EventKind, Relay};
use super::init::{BuildError, DeviceSpec, SystemSpec, build};
use super::models::avr::{Sexs, SexsParams, VrProportional};
use super::models::gov::{GoverProportional, Tgov1, Tgov1Params};
use super::models::load::ZipLoad;
use super::models::machine::{
GenCls, GenClsParams, GenRound, GenRoundParams, GenSalient, GenSalientParams, GenTransient,
GenTransientParams, Machine,
};
use super::models::pss::{Stab1, Stab1Params};
use super::models::{Control, DynamicModel, GeneratingUnit, InitError, Limits};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DynamicsDocument {
#[serde(flatten)]
pub network: NetworkData,
pub dynamics: DynamicsData,
}
fn default_s_base() -> f64 {
100.0
}
fn default_f_nom() -> f64 {
50.0
}
fn default_cutoff() -> f64 {
0.5
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DynamicsData {
#[serde(default = "default_s_base")]
pub s_base: f64,
#[serde(default = "default_f_nom")]
pub f_nom: f64,
#[serde(default)]
pub speed_voltages: bool,
#[serde(default)]
pub units: Vec<UnitSpec>,
#[serde(default)]
pub loads: Vec<LoadSpec>,
#[serde(default)]
pub fixed_buses: Vec<usize>,
#[serde(default)]
pub events: Vec<EventSpec>,
#[serde(default)]
pub relays: Vec<super::events::Relay>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct UnitSpec {
pub id: String,
pub bus: usize,
#[serde(default)]
pub p: Option<f64>,
#[serde(default)]
pub q: Option<f64>,
pub machine: MachineSpec,
#[serde(default)]
pub avr: Option<AvrSpec>,
#[serde(default)]
pub gov: Option<GovSpec>,
#[serde(default)]
pub pss: Option<PssSpec>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LoadSpec {
pub id: String,
pub bus: usize,
#[serde(default)]
pub p: Option<f64>,
#[serde(default)]
pub q: Option<f64>,
pub zip: [f64; 3],
#[serde(default = "default_cutoff")]
pub cutoff: f64,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(tag = "model", rename_all = "snake_case")]
pub enum MachineSpec {
GenCls(GenClsParams),
GenTransient(GenTransientParams),
GenRound(GenRoundParams),
GenSalient(GenSalientParams),
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(tag = "model", rename_all = "snake_case")]
pub enum AvrSpec {
Sexs(SexsParams),
VrProportional {
k: f64,
#[serde(default)]
limits: Limits,
},
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(tag = "model", rename_all = "snake_case")]
pub enum GovSpec {
Tgov1(Tgov1Params),
GoverProportional {
k: f64,
#[serde(default)]
limits: Limits,
},
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(tag = "model", rename_all = "snake_case")]
pub enum PssSpec {
Stab1(Stab1Params),
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum EventSpec {
BusFault {
t: f64,
bus: usize,
#[serde(default)]
y: Option<[f64; 2]>,
},
ClearFault {
t: f64,
bus: usize,
},
BranchTrip {
t: f64,
branch: usize,
},
BranchClose {
t: f64,
branch: usize,
},
UnitTrip {
t: f64,
unit: usize,
},
UnitClose {
t: f64,
unit: usize,
},
LoadStep {
t: f64,
bus: usize,
dp: f64,
#[serde(default)]
dq: f64,
},
}
impl EventSpec {
pub fn time(&self) -> f64 {
match *self {
EventSpec::BusFault { t, .. }
| EventSpec::ClearFault { t, .. }
| EventSpec::BranchTrip { t, .. }
| EventSpec::BranchClose { t, .. }
| EventSpec::UnitTrip { t, .. }
| EventSpec::UnitClose { t, .. }
| EventSpec::LoadStep { t, .. } => t,
}
}
}
impl From<EventSpec> for Event {
fn from(spec: EventSpec) -> Self {
let t = spec.time();
let kind = match spec {
EventSpec::BusFault { bus, y, .. } => match y {
Some([g, b]) => EventKind::BusFault {
bus,
y: Complex::new(g, b),
},
None => return Event::bolted_fault(t, bus),
},
EventSpec::ClearFault { bus, .. } => EventKind::ClearFault { bus },
EventSpec::BranchTrip { branch, .. } => EventKind::BranchTrip { branch },
EventSpec::BranchClose { branch, .. } => EventKind::BranchClose { branch },
EventSpec::UnitTrip { unit, .. } => EventKind::UnitTrip { unit },
EventSpec::UnitClose { unit, .. } => EventKind::UnitClose { unit },
EventSpec::LoadStep { bus, dp, dq, .. } => EventKind::LoadStep {
bus,
ds: Complex::new(dp, dq),
},
};
Event::new(t, kind)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum DynamicsError {
Io(String),
Parse(String),
PowerFlow(SolveStatus),
BusOutOfRange {
id: String,
bus: usize,
n_bus: usize,
},
AmbiguousSplit {
bus: usize,
devices: Vec<String>,
},
Model {
id: String,
source: InitError,
},
Build(BuildError),
}
impl std::fmt::Display for DynamicsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DynamicsError::Io(message) => write!(f, "{message}"),
DynamicsError::Parse(message) => write!(f, "{message}"),
DynamicsError::PowerFlow(status) => {
write!(f, "the base-case power flow did not converge ({status:?})")
}
DynamicsError::BusOutOfRange { id, bus, n_bus } => {
write!(f, "{id} is at bus {bus}, but the network has {n_bus} buses")
}
DynamicsError::AmbiguousSplit { bus, devices } => write!(
f,
"bus {bus} has {} devices with no stated terminal power ({}); \
state p and q on all but one",
devices.len(),
devices.join(", ")
),
DynamicsError::Model { id, source } => write!(f, "{id}: {source}"),
DynamicsError::Build(source) => write!(f, "{source}"),
}
}
}
impl std::error::Error for DynamicsError {}
impl From<BuildError> for DynamicsError {
fn from(source: BuildError) -> Self {
DynamicsError::Build(source)
}
}
pub fn read(path: impl AsRef<Path>) -> Result<DynamicsDocument, DynamicsError> {
let path = path.as_ref();
let text = std::fs::read_to_string(path)
.map_err(|e| DynamicsError::Io(format!("{}: {e}", path.display())))?;
parse(&text)
}
pub fn parse(text: &str) -> Result<DynamicsDocument, DynamicsError> {
serde_json::from_str(text).map_err(|e| DynamicsError::Parse(e.to_string()))
}
impl MachineSpec {
fn build(
self,
s_base: f64,
f_nom: f64,
speed_voltages: bool,
) -> Result<Box<dyn Machine>, InitError> {
Ok(match self {
MachineSpec::GenCls(p) => {
Box::new(GenCls::new(p, s_base, f_nom)?.with_speed_voltages(speed_voltages))
}
MachineSpec::GenTransient(p) => {
Box::new(GenTransient::new(p, s_base, f_nom)?.with_speed_voltages(speed_voltages))
}
MachineSpec::GenRound(p) => {
Box::new(GenRound::new(p, s_base, f_nom)?.with_speed_voltages(speed_voltages))
}
MachineSpec::GenSalient(p) => {
Box::new(GenSalient::new(p, s_base, f_nom)?.with_speed_voltages(speed_voltages))
}
})
}
}
impl AvrSpec {
fn build(self) -> Result<Box<dyn Control>, InitError> {
match self {
AvrSpec::Sexs(p) => Ok(Box::new(Sexs::new(p)?)),
AvrSpec::VrProportional { k, limits } => {
Ok(Box::new(VrProportional::limited(k, limits)?))
}
}
}
}
impl GovSpec {
fn build(self) -> Result<Box<dyn Control>, InitError> {
match self {
GovSpec::Tgov1(p) => Ok(Box::new(Tgov1::new(p)?)),
GovSpec::GoverProportional { k, limits } => {
Ok(Box::new(GoverProportional::limited(k, limits)?))
}
}
}
}
impl PssSpec {
fn build(self) -> Result<Box<dyn Control>, InitError> {
match self {
PssSpec::Stab1(p) => Ok(Box::new(Stab1::new(p)?)),
}
}
}
struct Placed {
id: String,
bus: usize,
stated: Option<Complex<f64>>,
}
impl UnitSpec {
pub fn into_model(self, s_base: f64, f_nom: f64) -> Result<Box<dyn DynamicModel>, InitError> {
Ok(Box::new(GeneratingUnit::new(
self.machine.build(s_base, f_nom, false)?,
self.avr.map(|a| a.build()).transpose()?,
self.gov.map(|g| g.build()).transpose()?,
self.pss.map(|p| p.build()).transpose()?,
)))
}
}
impl DynamicsDocument {
pub fn build(&self) -> Result<(DynamicSystem, Vec<Event>), DynamicsError> {
self.build_with_relays()
.map(|(system, events, _)| (system, events))
}
pub fn build_with_relays(
&self,
) -> Result<(DynamicSystem, Vec<Event>, Vec<Relay>), DynamicsError> {
let report = crate::run_power_flow_analysis(self.network.clone());
if report.stats.status != SolveStatus::Converged {
return Err(DynamicsError::PowerFlow(report.stats.status));
}
let buses = report.buses;
let n_bus = buses.len();
let placed: Vec<Placed> = self
.dynamics
.units
.iter()
.map(|u| Placed {
id: u.id.clone(),
bus: u.bus,
stated: complex_or_none(u.p, u.q),
})
.chain(self.dynamics.loads.iter().map(|l| Placed {
id: l.id.clone(),
bus: l.bus,
stated: complex_or_none(l.p, l.q),
}))
.collect();
for device in &placed {
if device.bus >= n_bus {
return Err(DynamicsError::BusOutOfRange {
id: device.id.clone(),
bus: device.bus,
n_bus,
});
}
}
let resolved = resolve_split(&placed, &buses, &self.network)?;
let (s_base, f_nom) = (self.dynamics.s_base, self.dynamics.f_nom);
let mut devices = Vec::with_capacity(placed.len());
for (i, spec) in self.dynamics.units.iter().enumerate() {
let model = GeneratingUnit::new(
spec.machine
.build(s_base, f_nom, self.dynamics.speed_voltages)
.map_err(|source| DynamicsError::Model {
id: spec.id.clone(),
source,
})?,
option_build(spec.avr, |a| a.build(), &spec.id)?,
option_build(spec.gov, |g| g.build(), &spec.id)?,
option_build(spec.pss, |p| p.build(), &spec.id)?,
);
devices.push(DeviceSpec {
id: spec.id.clone(),
bus: spec.bus,
s: resolved[i],
model: Box::new(model),
});
}
for (j, spec) in self.dynamics.loads.iter().enumerate() {
let load = ZipLoad::new(spec.zip[0], spec.zip[1], spec.zip[2], spec.cutoff).map_err(
|source| DynamicsError::Model {
id: spec.id.clone(),
source,
},
)?;
devices.push(DeviceSpec {
id: spec.id.clone(),
bus: spec.bus,
s: resolved[self.dynamics.units.len() + j],
model: Box::new(load) as Box<dyn DynamicModel>,
});
}
let system = build(SystemSpec {
buses: &buses,
lines: &self.network.lines,
transformers: &[],
shunts: &[],
devices,
fixed_buses: self.dynamics.fixed_buses.clone(),
})?;
let events = self
.dynamics
.events
.iter()
.copied()
.map(Event::from)
.collect();
Ok((system, events, self.dynamics.relays.clone()))
}
}
fn complex_or_none(p: Option<f64>, q: Option<f64>) -> Option<Complex<f64>> {
match (p, q) {
(None, None) => None,
(p, q) => Some(Complex::new(p.unwrap_or(0.0), q.unwrap_or(0.0))),
}
}
fn option_build<T: Copy, F>(
spec: Option<T>,
make: F,
id: &str,
) -> Result<Option<Box<dyn Control>>, DynamicsError>
where
F: Fn(T) -> Result<Box<dyn Control>, InitError>,
{
match spec {
None => Ok(None),
Some(s) => make(s).map(Some).map_err(|source| DynamicsError::Model {
id: id.to_string(),
source,
}),
}
}
fn resolve_split(
placed: &[Placed],
buses: &[Bus],
network: &NetworkData,
) -> Result<Vec<Complex<f64>>, DynamicsError> {
let ybus = build_ybus(buses.len(), &network.lines, &[]).finish();
let (p_calc, q_calc) = power_injections(buses, &ybus);
let mut remaining: HashMap<usize, Complex<f64>> = HashMap::new();
let mut unstated: HashMap<usize, Vec<usize>> = HashMap::new();
for (k, device) in placed.iter().enumerate() {
let entry = remaining
.entry(device.bus)
.or_insert_with(|| Complex::new(p_calc[device.bus], q_calc[device.bus]));
match device.stated {
Some(s) => *entry -= s,
None => unstated.entry(device.bus).or_default().push(k),
}
}
for (&bus, indices) in &unstated {
if indices.len() > 1 {
return Err(DynamicsError::AmbiguousSplit {
bus,
devices: indices.iter().map(|&k| placed[k].id.clone()).collect(),
});
}
}
Ok(placed
.iter()
.enumerate()
.map(|(k, device)| match device.stated {
Some(s) => s,
None => {
let _ = k;
remaining[&device.bus]
}
})
.collect())
}