use std::collections::HashMap;
use std::path::Path;
use num_complex::Complex;
use quick_xml::Reader;
use quick_xml::events::Event as XmlEvent;
use crate::PowerFlowOptions;
use crate::network::{ShuntAdm, build_ybus, power_injections, stamp_shunts};
use crate::solver::SolveStatus;
use crate::types::{Bus, Line, Transformer};
use super::init::{DeviceSpec, SystemSpec, build};
use super::json::{AvrSpec, GovSpec, MachineSpec, UnitSpec};
use super::models::Limits;
use super::models::machine::{GenRoundParams, GenSalientParams};
#[derive(Clone, Debug, PartialEq)]
pub struct BlackBox {
pub id: String,
pub lib: String,
pub par_file: Option<String>,
pub par_id: Option<String>,
pub static_id: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct DydDocument {
pub models: Vec<BlackBox>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ParDocument {
pub sets: HashMap<String, HashMap<String, String>>,
}
impl ParDocument {
pub fn number(&self, set: &str, name: &str) -> Option<f64> {
self.sets.get(set)?.get(name)?.parse().ok()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Curve {
pub model: String,
pub variable: String,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DydError {
Io(String),
Xml(String),
NoParameterSet {
id: String,
},
UnknownParameterSet {
id: String,
par_id: String,
},
MissingParameter {
id: String,
set: String,
name: String,
},
UnknownStaticId {
id: String,
static_id: String,
},
Iidm(String),
PowerFlow(crate::solver::SolveStatus),
SharedBus {
bus: usize,
models: Vec<String>,
},
Build(String),
}
impl std::fmt::Display for DydError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DydError::Io(message) | DydError::Xml(message) => write!(f, "{message}"),
DydError::NoParameterSet { id } => write!(f, "{id} names no parId"),
DydError::UnknownParameterSet { id, par_id } => {
write!(
f,
"{id} names parameter set `{par_id}`, which the .par does not define"
)
}
DydError::MissingParameter { id, set, name } => {
write!(f, "{id}: parameter set `{set}` has no `{name}`")
}
DydError::UnknownStaticId { id, static_id } => {
write!(
f,
"{id} attaches to static element `{static_id}`, which has no bus"
)
}
DydError::Iidm(message) => write!(f, "{message}"),
DydError::PowerFlow(status) => {
write!(f, "the base-case power flow did not converge ({status:?})")
}
DydError::SharedBus { bus, models } => write!(
f,
"bus {bus} carries {} dynamic machines ({}); their share of its solved \
reactive power is not in the file, only the total is",
models.len(),
models.join(", ")
),
DydError::Build(message) => write!(f, "{message}"),
}
}
}
impl std::error::Error for DydError {}
#[derive(Clone, Debug, PartialEq)]
pub enum DydWarning {
UnsupportedLib { id: String, lib: String },
SaturationIgnored {
id: String,
md: f64,
mq: f64,
nd: f64,
nq: f64,
},
LimitsIgnored { id: String, detail: String },
}
impl std::fmt::Display for DydWarning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DydWarning::UnsupportedLib { id, lib } => {
write!(f, "{id}: `{lib}` is not implemented; skipped")
}
DydWarning::SaturationIgnored { id, md, mq, nd, nq } => write!(
f,
"{id}: exponential saturation (md = {md}, mq = {mq}, nd = {nd}, nq = {nq}) \
is not modelled and was dropped"
),
DydWarning::LimitsIgnored { id, detail } => {
write!(f, "{id}: {detail} is not modelled and was dropped")
}
}
}
}
fn attributes(e: &quick_xml::events::BytesStart<'_>) -> Result<HashMap<String, String>, DydError> {
let mut out = HashMap::new();
for attribute in e.attributes() {
let attribute = attribute.map_err(|e| DydError::Xml(e.to_string()))?;
let key = String::from_utf8_lossy(attribute.key.as_ref()).to_string();
let key = key.rsplit(':').next().unwrap_or(&key).to_string();
let raw = String::from_utf8_lossy(&attribute.value);
let value = quick_xml::escape::unescape(&raw)
.map_err(|e| DydError::Xml(e.to_string()))?
.into_owned();
out.insert(key, value);
}
Ok(out)
}
pub fn read_dyd(path: impl AsRef<Path>) -> Result<DydDocument, DydError> {
parse_dyd(&slurp(path)?)
}
pub fn read_par(path: impl AsRef<Path>) -> Result<ParDocument, DydError> {
parse_par(&slurp(path)?)
}
pub fn read_crv(path: impl AsRef<Path>) -> Result<Vec<Curve>, DydError> {
parse_crv(&slurp(path)?)
}
fn slurp(path: impl AsRef<Path>) -> Result<String, DydError> {
let path = path.as_ref();
std::fs::read_to_string(path).map_err(|e| DydError::Io(format!("{}: {e}", path.display())))
}
fn local_name(name: quick_xml::name::QName<'_>) -> String {
let raw = String::from_utf8_lossy(name.as_ref()).to_string();
raw.rsplit(':').next().unwrap_or(&raw).to_string()
}
pub fn parse_dyd(text: &str) -> Result<DydDocument, DydError> {
let mut reader = Reader::from_str(text);
let mut models = Vec::new();
loop {
match reader.read_event() {
Ok(XmlEvent::Empty(e)) | Ok(XmlEvent::Start(e)) => {
if local_name(e.name()) != "blackBoxModel" {
continue;
}
let a = attributes(&e)?;
models.push(BlackBox {
id: a.get("id").cloned().unwrap_or_default(),
lib: a.get("lib").cloned().unwrap_or_default(),
par_file: a.get("parFile").cloned(),
par_id: a.get("parId").cloned(),
static_id: a.get("staticId").cloned(),
});
}
Ok(XmlEvent::Eof) => break,
Ok(_) => {}
Err(e) => return Err(DydError::Xml(e.to_string())),
}
}
Ok(DydDocument { models })
}
pub fn parse_par(text: &str) -> Result<ParDocument, DydError> {
let mut reader = Reader::from_str(text);
let mut sets: HashMap<String, HashMap<String, String>> = HashMap::new();
let mut current: Option<String> = None;
loop {
match reader.read_event() {
Ok(XmlEvent::Start(e)) if local_name(e.name()) == "set" => {
current = attributes(&e)?.get("id").cloned();
if let Some(id) = ¤t {
sets.entry(id.clone()).or_default();
}
}
Ok(XmlEvent::End(e)) if local_name(e.name()) == "set" => current = None,
Ok(XmlEvent::Empty(e)) | Ok(XmlEvent::Start(e)) if local_name(e.name()) == "par" => {
let a = attributes(&e)?;
if let (Some(set), Some(name), Some(value)) =
(current.as_ref(), a.get("name"), a.get("value"))
{
sets.entry(set.clone())
.or_default()
.insert(name.clone(), value.clone());
}
}
Ok(XmlEvent::Eof) => break,
Ok(_) => {}
Err(e) => return Err(DydError::Xml(e.to_string())),
}
}
Ok(ParDocument { sets })
}
pub fn parse_crv(text: &str) -> Result<Vec<Curve>, DydError> {
let mut reader = Reader::from_str(text);
let mut curves = Vec::new();
loop {
match reader.read_event() {
Ok(XmlEvent::Empty(e)) | Ok(XmlEvent::Start(e)) if local_name(e.name()) == "curve" => {
let a = attributes(&e)?;
curves.push(Curve {
model: a.get("model").cloned().unwrap_or_default(),
variable: a.get("variable").cloned().unwrap_or_default(),
});
}
Ok(XmlEvent::Eof) => break,
Ok(_) => {}
Err(e) => return Err(DydError::Xml(e.to_string())),
}
}
Ok(curves)
}
fn is_four_windings(lib: &str) -> bool {
lib.starts_with("GeneratorSynchronousFourWindings")
}
fn is_three_windings(lib: &str) -> bool {
lib.starts_with("GeneratorSynchronousThreeWindings")
}
fn is_generator(lib: &str) -> bool {
lib.starts_with("GeneratorSynchronous")
}
fn has_proportional_regulations(lib: &str) -> bool {
lib.ends_with("ProportionalRegulations")
}
fn need(par: &ParDocument, id: &str, set: &str, name: &str) -> Result<f64, DydError> {
par.number(set, name)
.ok_or_else(|| DydError::MissingParameter {
id: id.to_string(),
set: set.to_string(),
name: name.to_string(),
})
}
pub fn to_units(
dyd: &DydDocument,
par: &ParDocument,
bus_of: &HashMap<String, usize>,
s_base: f64,
) -> Result<(Vec<UnitSpec>, Vec<DydWarning>), DydError> {
let mut warnings = Vec::new();
let mut units = Vec::new();
for model in &dyd.models {
if !is_generator(&model.lib) {
continue;
}
if !is_four_windings(&model.lib) && !is_three_windings(&model.lib) {
warnings.push(DydWarning::UnsupportedLib {
id: model.id.clone(),
lib: model.lib.clone(),
});
continue;
}
let set = model
.par_id
.clone()
.ok_or_else(|| DydError::NoParameterSet {
id: model.id.clone(),
})?;
if !par.sets.contains_key(&set) {
return Err(DydError::UnknownParameterSet {
id: model.id.clone(),
par_id: set,
});
}
let static_id = model.static_id.clone().unwrap_or_else(|| model.id.clone());
let &bus = bus_of
.get(&static_id)
.ok_or_else(|| DydError::UnknownStaticId {
id: model.id.clone(),
static_id: static_id.clone(),
})?;
let g = |name: &str| need(par, &model.id, &set, name);
let machine = if is_three_windings(&model.lib) {
MachineSpec::GenSalient(GenSalientParams {
h: g("generator_H")?,
d: g("generator_DPu")?,
ra: g("generator_RaPu")?,
xd: g("generator_XdPu")?,
xq: g("generator_XqPu")?,
xdp: g("generator_XpdPu")?,
xdpp: g("generator_XppdPu")?,
xqpp: g("generator_XppqPu")?,
xl: g("generator_XlPu")?,
td0p: g("generator_Tpd0")?,
td0pp: g("generator_Tppd0")?,
tq0pp: g("generator_Tppq0")?,
mbase: g("generator_SNom")?,
})
} else {
MachineSpec::GenRound(GenRoundParams {
h: g("generator_H")?,
d: g("generator_DPu")?,
ra: g("generator_RaPu")?,
xd: g("generator_XdPu")?,
xq: g("generator_XqPu")?,
xdp: g("generator_XpdPu")?,
xqp: g("generator_XpqPu")?,
xdpp: g("generator_XppdPu")?,
xqpp: g("generator_XppqPu")?,
xl: g("generator_XlPu")?,
td0p: g("generator_Tpd0")?,
tq0p: g("generator_Tpq0")?,
td0pp: g("generator_Tppd0")?,
tq0pp: g("generator_Tppq0")?,
mbase: g("generator_SNom")?,
})
};
let (md, mq, nd, nq) = (
par.number(&set, "generator_md").unwrap_or(0.0),
par.number(&set, "generator_mq").unwrap_or(0.0),
par.number(&set, "generator_nd").unwrap_or(0.0),
par.number(&set, "generator_nq").unwrap_or(0.0),
);
if md != 0.0 || mq != 0.0 {
warnings.push(DydWarning::SaturationIgnored {
id: model.id.clone(),
md,
mq,
nd,
nq,
});
}
let (mut avr, mut gov) = (None, None);
if has_proportional_regulations(&model.lib) {
avr = Some(AvrSpec::VrProportional {
k: g("voltageRegulator_Gain")?,
limits: Limits {
min: par.number(&set, "voltageRegulator_EfdMinPu"),
max: par.number(&set, "voltageRegulator_EfdMaxPu"),
},
});
let k_gover = g("governor_KGover")?;
let p_nom = g("governor_PNom")?;
gov = Some(GovSpec::GoverProportional {
k: k_gover * p_nom / s_base,
limits: Limits {
min: par.number(&set, "governor_PMin").map(|p| p / s_base),
max: par.number(&set, "governor_PMax").map(|p| p / s_base),
},
});
}
units.push(UnitSpec {
id: model.id.clone(),
bus,
p: None,
q: None,
machine,
avr,
gov,
pss: None,
});
}
Ok((units, warnings))
}
pub struct StaticNetwork<'a> {
pub buses: &'a [Bus],
pub lines: &'a [Line],
pub transformers: &'a [Transformer],
pub shunts: &'a [ShuntAdm],
pub base_mva: f64,
pub generators: HashMap<String, usize>,
pub loads: Vec<(usize, Complex<f64>)>,
}
pub fn case_from_network(
network: &StaticNetwork,
dyd: &DydDocument,
par: &ParDocument,
f_nom: f64,
) -> Result<(super::DynamicSystem, Vec<DydWarning>), DydError> {
let (units, mut warnings) = to_units(dyd, par, &network.generators, network.base_mva)?;
for model in dyd
.models
.iter()
.filter(|m| m.static_id.is_some() && !is_generator(&m.lib))
{
warnings.push(DydWarning::UnsupportedLib {
id: model.id.clone(),
lib: model.lib.clone(),
});
}
let report = crate::run_power_flow(
network.buses.to_vec(),
network.lines,
network.transformers,
network.shunts,
crate::TapData::none(),
PowerFlowOptions::default(),
);
if report.stats.status != SolveStatus::Converged {
return Err(DydError::PowerFlow(report.stats.status));
}
let buses = report.buses;
let mut ybus = build_ybus(buses.len(), network.lines, network.transformers);
stamp_shunts(&mut ybus, network.shunts);
let (p_calc, q_calc) = power_injections(&buses, &ybus.finish());
let mut machine_share: HashMap<usize, Complex<f64>> = HashMap::new();
for unit in &units {
machine_share
.entry(unit.bus)
.or_insert_with(|| Complex::new(p_calc[unit.bus], q_calc[unit.bus]));
}
for &(bus, s) in &network.loads {
if let Some(share) = machine_share.get_mut(&bus) {
*share -= s;
}
}
let mut per_bus: HashMap<usize, Vec<String>> = HashMap::new();
for unit in &units {
per_bus.entry(unit.bus).or_default().push(unit.id.clone());
}
for (&bus, models) in &per_bus {
if models.len() > 1 {
return Err(DydError::SharedBus {
bus,
models: models.clone(),
});
}
}
let mut devices = Vec::with_capacity(units.len());
for unit in units {
let model = unit
.clone()
.into_model(network.base_mva, f_nom)
.map_err(|e| DydError::Build(e.to_string()))?;
devices.push(DeviceSpec {
id: unit.id.clone(),
bus: unit.bus,
s: machine_share[&unit.bus],
model,
});
}
let system = build(SystemSpec {
buses: &buses,
lines: network.lines,
transformers: network.transformers,
shunts: network.shunts,
devices,
fixed_buses: Vec::new(),
})
.map_err(|e| DydError::Build(e.to_string()))?;
Ok((system, warnings))
}