use std::collections::BTreeMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
pub type Extras = BTreeMap<String, serde_json::Value>;
pub type Mat = Vec<Vec<f64>>;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum DistSourceFormat {
Dss,
BmopfJson,
PmdJson,
}
impl DistSourceFormat {
pub fn name(self) -> &'static str {
match self {
DistSourceFormat::Dss => "dss",
DistSourceFormat::PmdJson => "pmd-json",
DistSourceFormat::BmopfJson => "bmopf-json",
}
}
}
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DistBus {
pub id: String,
pub terminals: Vec<String>,
pub grounded: Vec<String>,
pub v_min: Option<f64>,
pub v_max: Option<f64>,
pub vpn_min: Option<Vec<f64>>,
pub vpn_max: Option<Vec<f64>>,
pub vpp_min: Option<Vec<f64>>,
pub vpp_max: Option<Vec<f64>>,
pub vsym_min: Option<Vec<f64>>,
pub vsym_max: Option<Vec<f64>>,
pub extras: Extras,
}
impl DistBus {
#[must_use]
pub fn new(id: impl Into<String>, terminals: Vec<String>) -> Self {
Self {
id: id.into(),
terminals,
grounded: Vec::new(),
v_min: None,
v_max: None,
vpn_min: None,
vpn_max: None,
vpp_min: None,
vpp_max: None,
vsym_min: None,
vsym_max: None,
extras: Extras::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DistLineCode {
pub name: String,
pub n_conductors: usize,
pub r_series: Mat,
pub x_series: Mat,
pub g_from: Mat,
pub b_from: Mat,
pub g_to: Mat,
pub b_to: Mat,
pub i_max: Option<Vec<f64>>,
pub s_max: Option<Vec<f64>>,
pub extras: Extras,
}
impl DistLineCode {
#[must_use]
pub fn new(name: impl Into<String>, r_series: Mat, x_series: Mat) -> Self {
let n_conductors = matrix_extent(&r_series).max(matrix_extent(&x_series));
Self {
name: name.into(),
n_conductors,
r_series,
x_series,
g_from: zero_mat(n_conductors),
b_from: zero_mat(n_conductors),
g_to: zero_mat(n_conductors),
b_to: zero_mat(n_conductors),
i_max: None,
s_max: None,
extras: Extras::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DistLine {
pub name: String,
pub bus_from: String,
pub bus_to: String,
pub terminal_map_from: Vec<String>,
pub terminal_map_to: Vec<String>,
pub linecode: String,
pub length: f64,
pub extras: Extras,
}
impl DistLine {
#[must_use]
pub fn new(
name: impl Into<String>,
bus_from: impl Into<String>,
bus_to: impl Into<String>,
terminal_map_from: Vec<String>,
terminal_map_to: Vec<String>,
linecode: impl Into<String>,
length: f64,
) -> Self {
Self {
name: name.into(),
bus_from: bus_from.into(),
bus_to: bus_to.into(),
terminal_map_from,
terminal_map_to,
linecode: linecode.into(),
length,
extras: Extras::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DistSwitch {
pub name: String,
pub bus_from: String,
pub bus_to: String,
pub terminal_map_from: Vec<String>,
pub terminal_map_to: Vec<String>,
pub open: bool,
pub i_max: Option<Vec<f64>>,
pub extras: Extras,
}
impl DistSwitch {
#[must_use]
pub fn new(
name: impl Into<String>,
bus_from: impl Into<String>,
bus_to: impl Into<String>,
terminal_map_from: Vec<String>,
terminal_map_to: Vec<String>,
open: bool,
) -> Self {
Self {
name: name.into(),
bus_from: bus_from.into(),
bus_to: bus_to.into(),
terminal_map_from,
terminal_map_to,
open,
i_max: None,
extras: Extras::new(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Configuration {
Wye,
Delta,
SinglePhase,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DistLoad {
pub name: String,
pub bus: String,
pub terminal_map: Vec<String>,
pub configuration: Configuration,
pub p_nom: Vec<f64>,
pub q_nom: Vec<f64>,
pub voltage_model: DistLoadVoltageModel,
pub extras: Extras,
}
impl DistLoad {
#[must_use]
pub fn new(
name: impl Into<String>,
bus: impl Into<String>,
terminal_map: Vec<String>,
configuration: Configuration,
p_nom: Vec<f64>,
q_nom: Vec<f64>,
) -> Self {
Self {
name: name.into(),
bus: bus.into(),
terminal_map,
configuration,
p_nom,
q_nom,
voltage_model: DistLoadVoltageModel::default(),
extras: Extras::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "model", rename_all = "snake_case")]
#[non_exhaustive]
pub enum DistLoadVoltageModel {
ConstantPower { v_nom: Vec<f64> },
ConstantCurrent { v_nom: Vec<f64> },
ConstantImpedance { v_nom: Vec<f64> },
Zip {
v_nom: Vec<f64>,
alpha_z: Vec<f64>,
alpha_i: Vec<f64>,
alpha_p: Vec<f64>,
beta_z: Vec<f64>,
beta_i: Vec<f64>,
beta_p: Vec<f64>,
},
Exponential {
v_nom: Vec<f64>,
gamma_p: Vec<f64>,
gamma_q: Vec<f64>,
},
}
impl Default for DistLoadVoltageModel {
fn default() -> Self {
Self::ConstantPower { v_nom: Vec::new() }
}
}
impl DistLoadVoltageModel {
#[must_use]
pub fn v_nom(&self) -> &[f64] {
match self {
Self::ConstantPower { v_nom }
| Self::ConstantCurrent { v_nom }
| Self::ConstantImpedance { v_nom }
| Self::Zip { v_nom, .. }
| Self::Exponential { v_nom, .. } => v_nom,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DistGenerator {
pub name: String,
pub bus: String,
pub terminal_map: Vec<String>,
pub configuration: Configuration,
pub p_nom: Vec<f64>,
pub q_nom: Vec<f64>,
pub p_min: Option<Vec<f64>>,
pub p_max: Option<Vec<f64>>,
pub q_min: Option<Vec<f64>>,
pub q_max: Option<Vec<f64>>,
pub cost: Option<f64>,
pub extras: Extras,
}
impl DistGenerator {
#[must_use]
pub fn new(
name: impl Into<String>,
bus: impl Into<String>,
terminal_map: Vec<String>,
configuration: Configuration,
p_nom: Vec<f64>,
q_nom: Vec<f64>,
) -> Self {
Self {
name: name.into(),
bus: bus.into(),
terminal_map,
configuration,
p_nom,
q_nom,
p_min: None,
p_max: None,
q_min: None,
q_max: None,
cost: None,
extras: Extras::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DistShunt {
pub name: String,
pub bus: String,
pub terminal_map: Vec<String>,
pub g: Mat,
pub b: Mat,
pub extras: Extras,
}
impl DistShunt {
#[must_use]
pub fn new(
name: impl Into<String>,
bus: impl Into<String>,
terminal_map: Vec<String>,
g: Mat,
b: Mat,
) -> Self {
Self {
name: name.into(),
bus: bus.into(),
terminal_map,
g,
b,
extras: Extras::new(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum WindingConn {
Wye,
Delta,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Winding {
pub bus: String,
pub terminal_map: Vec<String>,
pub conn: WindingConn,
pub v_ref: f64,
pub s_rating: f64,
pub r_pct: f64,
pub tap: f64,
}
impl Winding {
#[must_use]
pub fn new(
bus: impl Into<String>,
terminal_map: Vec<String>,
conn: WindingConn,
v_ref: f64,
s_rating: f64,
) -> Self {
Self {
bus: bus.into(),
terminal_map,
conn,
v_ref,
s_rating,
r_pct: 0.0,
tap: 1.0,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DistTransformer {
pub name: String,
pub windings: Vec<Winding>,
pub xsc_pct: Vec<f64>,
pub phases: usize,
pub extras: Extras,
}
impl DistTransformer {
#[must_use]
pub fn new(
name: impl Into<String>,
windings: Vec<Winding>,
xsc_pct: Vec<f64>,
phases: usize,
) -> Self {
Self {
name: name.into(),
windings,
xsc_pct,
phases,
extras: Extras::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct VoltageSource {
pub name: String,
pub bus: String,
pub terminal_map: Vec<String>,
pub v_magnitude: Vec<f64>,
pub v_angle: Vec<f64>,
pub extras: Extras,
}
impl VoltageSource {
#[must_use]
pub fn new(
name: impl Into<String>,
bus: impl Into<String>,
terminal_map: Vec<String>,
v_magnitude: Vec<f64>,
v_angle: Vec<f64>,
) -> Self {
Self {
name: name.into(),
bus: bus.into(),
terminal_map,
v_magnitude,
v_angle,
extras: Extras::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UntypedObject {
pub class: String,
pub name: String,
pub props: Vec<(Option<String>, String)>,
}
impl UntypedObject {
#[must_use]
pub fn new(
class: impl Into<String>,
name: impl Into<String>,
props: Vec<(Option<String>, String)>,
) -> Self {
Self {
class: class.into(),
name: name.into(),
props,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DistNetwork {
pub name: Option<String>,
pub base_frequency: f64,
pub buses: Vec<DistBus>,
pub linecodes: Vec<DistLineCode>,
pub lines: Vec<DistLine>,
pub switches: Vec<DistSwitch>,
pub transformers: Vec<DistTransformer>,
pub loads: Vec<DistLoad>,
pub generators: Vec<DistGenerator>,
pub shunts: Vec<DistShunt>,
pub sources: Vec<VoltageSource>,
pub untyped: Vec<UntypedObject>,
pub commands: Vec<(String, String)>,
pub options: Vec<(String, String)>,
#[serde(skip)]
pub defaulted: BTreeMap<String, Vec<&'static str>>,
pub warnings: Vec<String>,
#[serde(skip)]
pub source: Option<Arc<String>>,
pub source_format: Option<DistSourceFormat>,
pub extras: Extras,
}
pub type MulticonductorNetwork = DistNetwork;
impl Default for DistNetwork {
fn default() -> Self {
DistNetwork {
name: None,
base_frequency: crate::dss::defaults::BASE_FREQUENCY,
buses: Vec::new(),
linecodes: Vec::new(),
lines: Vec::new(),
switches: Vec::new(),
transformers: Vec::new(),
loads: Vec::new(),
generators: Vec::new(),
shunts: Vec::new(),
sources: Vec::new(),
untyped: Vec::new(),
commands: Vec::new(),
options: Vec::new(),
defaulted: BTreeMap::new(),
warnings: Vec::new(),
source: None,
source_format: None,
extras: Extras::new(),
}
}
}
impl DistNetwork {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn named(name: impl Into<String>) -> Self {
Self {
name: Some(name.into()),
..Self::default()
}
}
pub fn bus(&self, id: &str) -> Option<&DistBus> {
self.buses.iter().find(|b| b.id.eq_ignore_ascii_case(id))
}
pub fn linecode(&self, name: &str) -> Option<&DistLineCode> {
self.linecodes
.iter()
.find(|c| c.name.eq_ignore_ascii_case(name))
}
}
fn zero_mat(n: usize) -> Mat {
vec![vec![0.0; n]; n]
}
fn matrix_extent(m: &Mat) -> usize {
m.iter().map(Vec::len).fold(m.len(), usize::max)
}
pub(crate) fn n_winding_phase_count(conn: WindingConn, terminal_map: &[String]) -> usize {
match conn {
WindingConn::Wye => terminal_map.len().saturating_sub(1).max(1),
WindingConn::Delta => {
if terminal_map.len() == 2 {
1
} else {
terminal_map.len().max(1)
}
}
}
}
pub(crate) fn n_winding_impedance_base(phases: usize, v_nom: f64, s: f64) -> Option<f64> {
let phases = phases as f64;
(phases > 0.0 && v_nom.is_finite() && v_nom > 0.0 && s.is_finite() && s > 0.0)
.then_some(phases * v_nom * v_nom / s)
}
pub(crate) fn pair_keys(n: usize) -> Vec<(usize, usize)> {
let mut pairs = Vec::new();
for i in 0..n {
for j in i + 1..n {
pairs.push((i, j));
}
}
pairs
}
pub(crate) fn square_from_rows(rows: &[Vec<f64>], n: usize) -> Option<Mat> {
let mut m = vec![vec![0.0; n]; n];
if rows.len() != n {
return None;
}
let lower = rows.iter().enumerate().all(|(i, r)| r.len() == i + 1);
let full = rows.iter().all(|r| r.len() == n);
if lower {
for (i, row) in rows.iter().enumerate() {
for (j, &v) in row.iter().enumerate() {
m[i][j] = v;
m[j][i] = v;
}
}
} else if full {
for (i, row) in rows.iter().enumerate() {
m[i].clone_from_slice(&row[..n]);
}
} else {
return None;
}
Some(m)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(clippy::float_cmp)]
fn lower_triangle_completes_symmetrically() {
let rows = vec![vec![1.0], vec![0.5, 2.0], vec![0.3, 0.4, 3.0]];
let m = square_from_rows(&rows, 3).unwrap();
assert_eq!(m[0][1], 0.5);
assert_eq!(m[1][0], 0.5);
assert_eq!(m[2][2], 3.0);
assert_eq!(m[0][2], 0.3);
}
#[test]
#[allow(clippy::float_cmp)]
fn full_rows_pass_through() {
let rows = vec![vec![1.0, 9.0], vec![8.0, 2.0]];
let m = square_from_rows(&rows, 2).unwrap();
assert_eq!(m[0][1], 9.0);
assert_eq!(m[1][0], 8.0);
}
#[test]
fn wrong_shape_is_rejected() {
assert!(square_from_rows(&[vec![1.0], vec![2.0]], 2).is_none());
assert!(square_from_rows(&[vec![1.0, 2.0]], 2).is_none());
}
}