use std::collections::BTreeMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::geo::{DistGeoMeta, DistLocation};
pub type Extras = BTreeMap<String, serde_json::Value>;
pub type Mat = Vec<Vec<f64>>;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[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)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[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>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vpos_min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vpos_max: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vneg_max: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vzero_max: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vn_max: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location: Option<DistLocation>,
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,
vpos_min: None,
vpos_max: None,
vneg_max: None,
vzero_max: None,
vn_max: None,
location: None,
extras: Extras::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[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,
#[serde(default, with = "crate::nonfinite::upper_bounds")]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub i_max: Option<Vec<f64>>,
#[serde(default, with = "crate::nonfinite::upper_bounds")]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub s_max: Option<Vec<f64>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
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,
source: None,
extras: Extras::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[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,
#[serde(with = "crate::nonfinite::nan_scalar")]
#[cfg_attr(
feature = "schema",
schemars(schema_with = "crate::nonfinite::nullable_number")
)]
pub length: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub route: Option<Vec<DistLocation>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::nonfinite::upper_bounds"
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub i_max: Option<Vec<f64>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::nonfinite::upper_bounds"
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub s_max: Option<Vec<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,
route: None,
i_max: None,
s_max: None,
extras: Extras::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct DistCapacitor {
pub name: String,
pub bus: String,
pub terminal_map: Vec<String>,
pub configuration: Configuration,
#[serde(with = "crate::nonfinite::nan_scalar")]
#[cfg_attr(
feature = "schema",
schemars(schema_with = "crate::nonfinite::nullable_number")
)]
pub q_rated: f64,
#[serde(with = "crate::nonfinite::nan_scalar")]
#[cfg_attr(
feature = "schema",
schemars(schema_with = "crate::nonfinite::nullable_number")
)]
pub v_nom: f64,
pub extras: Extras,
}
impl DistCapacitor {
#[must_use]
pub fn new(
name: impl Into<String>,
bus: impl Into<String>,
terminal_map: Vec<String>,
configuration: Configuration,
q_rated: f64,
v_nom: f64,
) -> Self {
Self {
name: name.into(),
bus: bus.into(),
terminal_map,
configuration,
q_rated,
v_nom,
extras: Extras::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[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,
#[serde(default, with = "crate::nonfinite::upper_bounds")]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
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)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Configuration {
Wye,
Delta,
SinglePhase,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[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)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[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)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[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>,
#[serde(default, with = "crate::nonfinite::lower_bounds")]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub p_min: Option<Vec<f64>>,
#[serde(default, with = "crate::nonfinite::upper_bounds")]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub p_max: Option<Vec<f64>>,
#[serde(default, with = "crate::nonfinite::lower_bounds")]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub q_min: Option<Vec<f64>>,
#[serde(default, with = "crate::nonfinite::upper_bounds")]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub q_max: Option<Vec<f64>>,
pub cost: Option<f64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::nonfinite::upper_bounds"
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub s_max: Option<Vec<f64>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::nonfinite::upper_bounds"
)]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub i_max: Option<Vec<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,
s_max: None,
i_max: None,
extras: Extras::new(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum IbrTopology {
SinglePhase,
ThreeLeg,
FourLeg,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum IbrPrimeMover {
Pv,
Battery,
Generic,
Statcom,
Dstatcom,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum IbrVoltageAggregation {
PerPhase,
Average,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct DistIbr {
pub name: String,
pub bus: String,
pub terminal_map: Vec<String>,
pub topology: IbrTopology,
pub prime_mover: IbrPrimeMover,
#[serde(with = "crate::nonfinite::upper_limits")]
#[cfg_attr(feature = "schema", schemars(with = "Vec<Option<f64>>"))]
pub s_max: Vec<f64>,
#[serde(default, with = "crate::nonfinite::upper_bounds")]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub i_max: Option<Vec<f64>>,
pub p_avail: Option<f64>,
#[serde(default, with = "crate::nonfinite::lower_bounds")]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub p_min: Option<Vec<f64>>,
#[serde(default, with = "crate::nonfinite::upper_bounds")]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub p_max: Option<Vec<f64>>,
#[serde(default, with = "crate::nonfinite::lower_bounds")]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub q_min: Option<Vec<f64>>,
#[serde(default, with = "crate::nonfinite::upper_bounds")]
#[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
pub q_max: Option<Vec<f64>>,
pub control_profile: Option<String>,
pub voltage_aggregation: Option<IbrVoltageAggregation>,
pub extras: Extras,
}
impl DistIbr {
#[must_use]
pub fn new(
name: impl Into<String>,
bus: impl Into<String>,
terminal_map: Vec<String>,
topology: IbrTopology,
prime_mover: IbrPrimeMover,
s_max: Vec<f64>,
) -> Self {
Self {
name: name.into(),
bus: bus.into(),
terminal_map,
topology,
prime_mover,
s_max,
i_max: None,
p_avail: None,
p_min: None,
p_max: None,
q_min: None,
q_max: None,
control_profile: None,
voltage_aggregation: None,
extras: Extras::new(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum ControlVoltageReference {
PnPerPhase,
PpPerPhase,
PpAveraged,
PgAveraged,
PnAveraged,
PgPerPhase,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum ReactivePowerUnit {
VaFraction,
Var,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum ActivePowerUnit {
VaFraction,
W,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum ReactivePowerReference {
VarMax,
VarAvailable,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum ActivePowerReference {
PAvailable,
PMax,
SMax,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct PowerFactorControl {
pub pf: f64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct VoltVarControl {
pub voltage_reference: Option<ControlVoltageReference>,
pub breakpoints: Vec<f64>,
pub q_limits: Vec<f64>,
pub q_unit: Option<ReactivePowerUnit>,
pub q_ref: Option<ReactivePowerReference>,
pub p_min_for_q: Option<f64>,
pub p_min_for_q_max: Option<f64>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct VoltWattControl {
pub voltage_reference: Option<ControlVoltageReference>,
pub breakpoints: Vec<f64>,
pub p_limits: Vec<f64>,
pub p_unit: Option<ActivePowerUnit>,
pub p_ref: Option<ActivePowerReference>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct DistControlProfile {
pub name: String,
pub power_factor: Option<PowerFactorControl>,
pub volt_var: Option<VoltVarControl>,
pub volt_watt: Option<VoltWattControl>,
pub extras: Extras,
}
impl DistControlProfile {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
power_factor: None,
volt_var: None,
volt_watt: None,
extras: Extras::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[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)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DistWindingConn {
Wye,
Delta,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct DistWinding {
pub bus: String,
pub terminal_map: Vec<String>,
pub conn: DistWindingConn,
#[serde(with = "crate::nonfinite::nan_scalar")]
#[cfg_attr(
feature = "schema",
schemars(schema_with = "crate::nonfinite::nullable_number")
)]
pub v_ref: f64,
#[serde(with = "crate::nonfinite::nan_scalar")]
#[cfg_attr(
feature = "schema",
schemars(schema_with = "crate::nonfinite::nullable_number")
)]
pub s_rating: f64,
#[serde(with = "crate::nonfinite::nan_scalar")]
#[cfg_attr(
feature = "schema",
schemars(schema_with = "crate::nonfinite::nullable_number")
)]
pub r_pct: f64,
pub tap: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r_neutral: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub x_neutral: Option<f64>,
}
impl DistWinding {
#[must_use]
pub fn new(
bus: impl Into<String>,
terminal_map: Vec<String>,
conn: DistWindingConn,
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,
r_neutral: None,
x_neutral: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct DistTransformer {
pub name: String,
pub windings: Vec<DistWinding>,
pub xsc_pct: Vec<f64>,
pub phases: usize,
pub extras: Extras,
}
impl DistTransformer {
#[must_use]
pub fn new(
name: impl Into<String>,
windings: Vec<DistWinding>,
xsc_pct: Vec<f64>,
phases: usize,
) -> Self {
Self {
name: name.into(),
windings,
xsc_pct,
phases,
extras: Extras::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[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)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[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)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(remote = "Self")]
#[non_exhaustive]
pub struct MulticonductorNetwork {
pub name: Option<String>,
pub base_frequency: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub geo: Option<DistGeoMeta>,
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>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ibrs: Vec<DistIbr>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub control_profiles: Vec<DistControlProfile>,
pub shunts: Vec<DistShunt>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capacitors: Vec<DistCapacitor>,
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 parse_diagnostics: Vec<crate::diagnostics::StructuredDiagnostic>,
#[serde(skip)]
pub source: Option<Arc<String>>,
pub source_format: Option<DistSourceFormat>,
pub extras: Extras,
}
impl serde::Serialize for MulticonductorNetwork {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
MulticonductorNetwork::serialize(self, powerio_diag::nonfinite::NonFiniteSer(serializer))
}
}
impl<'de> serde::Deserialize<'de> for MulticonductorNetwork {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
MulticonductorNetwork::deserialize(powerio_diag::nonfinite::NonFiniteDe(deserializer))
}
}
impl MulticonductorNetwork {
pub(crate) fn note(
&mut self,
info: &crate::diagnostics::DiagnosticInfo,
message: impl Into<String>,
) {
let diagnostic = crate::diagnostics::StructuredDiagnostic::of(info, message);
self.warnings
.push(crate::diagnostics::render_line(&diagnostic));
self.parse_diagnostics.push(diagnostic);
}
pub(crate) fn record(&mut self, diagnostic: crate::diagnostics::StructuredDiagnostic) {
self.warnings
.push(crate::diagnostics::render_line(&diagnostic));
self.parse_diagnostics.push(diagnostic);
}
}
impl Default for MulticonductorNetwork {
fn default() -> Self {
MulticonductorNetwork {
name: None,
base_frequency: crate::dss::defaults::BASE_FREQUENCY,
geo: None,
buses: Vec::new(),
linecodes: Vec::new(),
lines: Vec::new(),
switches: Vec::new(),
transformers: Vec::new(),
loads: Vec::new(),
generators: Vec::new(),
ibrs: Vec::new(),
control_profiles: Vec::new(),
shunts: Vec::new(),
capacitors: Vec::new(),
sources: Vec::new(),
untyped: Vec::new(),
commands: Vec::new(),
options: Vec::new(),
defaulted: BTreeMap::new(),
warnings: Vec::new(),
parse_diagnostics: Vec::new(),
source: None,
source_format: None,
extras: Extras::new(),
}
}
}
impl MulticonductorNetwork {
#[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))
}
}
pub(crate) fn warn_defaulted_frequency(net: &mut MulticonductorNetwork, field: &str) {
let charging = net.linecodes.iter().any(|c| {
[&c.b_from, &c.b_to]
.iter()
.flat_map(|m| m.iter())
.flatten()
.any(|v| v.is_finite() && v.abs() > 0.0)
});
if charging {
net.note(
&crate::diagnostics::codes::READ_MULTICONDUCTOR_VALUE_DEFAULTED,
format!(
"document states no {field} and carries line susceptance; read at {} Hz",
net.base_frequency
),
);
}
}
pub(crate) fn warn_unresolved_references(net: &mut MulticonductorNetwork) {
use std::collections::BTreeSet;
let buses: BTreeSet<String> = net
.buses
.iter()
.map(|b| b.id.to_ascii_lowercase())
.collect();
let linecodes: BTreeSet<String> = net
.linecodes
.iter()
.map(|c| c.name.to_ascii_lowercase())
.collect();
let mut warnings = Vec::new();
{
let mut bus = |what: &str, field: &str, id: &str| {
if id.is_empty() {
warnings.push(format!("{what}: `{field}` reference is empty or missing"));
} else if !buses.contains(&id.to_ascii_lowercase()) {
warnings.push(format!("{what}: references undefined bus `{id}`"));
}
};
for l in &net.lines {
let what = format!("line {}", l.name);
bus(&what, "bus_from", &l.bus_from);
bus(&what, "bus_to", &l.bus_to);
}
for sw in &net.switches {
let what = format!("switch {}", sw.name);
bus(&what, "bus_from", &sw.bus_from);
bus(&what, "bus_to", &sw.bus_to);
}
for t in &net.transformers {
let what = format!("transformer {}", t.name);
for w in &t.windings {
bus(&what, "bus", &w.bus);
}
}
for (what, id) in std::iter::empty()
.chain(
net.loads
.iter()
.map(|x| (format!("load {}", x.name), &x.bus)),
)
.chain(
net.generators
.iter()
.map(|x| (format!("generator {}", x.name), &x.bus)),
)
.chain(
net.shunts
.iter()
.map(|x| (format!("shunt {}", x.name), &x.bus)),
)
.chain(
net.capacitors
.iter()
.map(|x| (format!("capacitor {}", x.name), &x.bus)),
)
.chain(net.ibrs.iter().map(|x| (format!("ibr {}", x.name), &x.bus)))
.chain(
net.sources
.iter()
.map(|x| (format!("voltage_source {}", x.name), &x.bus)),
)
{
bus(&what, "bus", id);
}
}
for l in &net.lines {
if l.linecode.is_empty() {
warnings.push(format!(
"line {}: `linecode` reference is empty or missing",
l.name
));
} else if !linecodes.contains(&l.linecode.to_ascii_lowercase()) {
warnings.push(format!(
"line {}: references undefined linecode `{}`",
l.name, l.linecode
));
}
}
for message in warnings {
net.note(
&crate::diagnostics::codes::VALIDATE_MULTICONDUCTOR_REFERENCE_UNDEFINED,
message,
);
}
}
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: DistWindingConn, terminal_map: &[String]) -> usize {
match conn {
DistWindingConn::Wye => terminal_map.len().saturating_sub(1).max(1),
DistWindingConn::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 winding_phase_pair(winding: &DistWinding) -> Option<(u8, u8)> {
let [first, second] = winding.terminal_map.as_slice() else {
return None;
};
let phase = |term: &String| term.parse::<u8>().ok().filter(|n| (1..=3).contains(n));
match (phase(first), phase(second)) {
(Some(lead), Some(lag)) if lead != lag => Some((lead, lag)),
_ => None,
}
}
pub(crate) fn open_delta_connection(a: (u8, u8), b: (u8, u8)) -> Option<(&'static str, bool)> {
match (a, b) {
((1, 2), (2, 3)) => Some(("ABBC", false)),
((2, 3), (1, 2)) => Some(("ABBC", true)),
((2, 3), (1, 3)) => Some(("BCAC", false)),
((1, 3), (2, 3)) => Some(("BCAC", true)),
((3, 1), (2, 1)) => Some(("CABA", false)),
((2, 1), (3, 1)) => Some(("CABA", true)),
_ => None,
}
}
pub(crate) fn open_delta_pairable(a: &DistTransformer, b: &DistTransformer) -> bool {
let ([a0, a1], [b0, b1]) = (a.windings.as_slice(), b.windings.as_slice()) else {
return false;
};
let eq = |x: f64, y: f64| x.to_bits() == y.to_bits();
eq(a0.v_ref, b0.v_ref)
&& eq(a1.v_ref, b1.v_ref)
&& eq(a0.s_rating, b0.s_rating)
&& eq(a1.s_rating, b1.s_rating)
&& eq(a0.r_pct, b0.r_pct)
&& eq(a1.r_pct, b1.r_pct)
&& a.xsc_pct.len() == b.xsc_pct.len()
&& a.xsc_pct.iter().zip(&b.xsc_pct).all(|(x, y)| eq(*x, *y))
&& ["%noloadloss", "%imag", "g_no_load", "b_no_load"]
.iter()
.all(|k| a.extras.get(*k) == b.extras.get(*k))
}
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)
}
#[deprecated(
since = "0.9.0",
note = "renamed to MulticonductorNetwork in 0.9.0; the alias goes away at 1.0.0"
)]
pub type DistNetwork = MulticonductorNetwork;
#[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());
}
}
#[cfg(test)]
mod deprecation_shim_tests {
#[test]
#[allow(deprecated)]
fn the_08_type_name_still_compiles() {
let net: crate::DistNetwork = crate::MulticonductorNetwork::named("shim");
assert_eq!(net.name.as_deref(), Some("shim"));
}
}