use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::geo::{DistGeoMeta, DistLocation};
pub type Extras = BTreeMap<String, serde_json::Value>;
pub type ConductorMatrix = 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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub v_min_phase: Option<Vec<f64>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub v_max_phase: Option<Vec<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 phase_indices(&self, conventions: Option<&serde_json::Value>) -> Vec<usize> {
let numeric_four = self.terminals.len() == 4
&& ["1", "2", "3", "4"]
.iter()
.all(|t| self.terminals.iter().any(|v| v == t));
self.terminals
.iter()
.enumerate()
.filter_map(|(index, terminal)| {
let nonphase = if let Some(roles) = conventions {
["neutral", "earth"].iter().any(|role| {
roles
.get(role)
.and_then(serde_json::Value::as_array)
.is_some_and(|names| {
names
.iter()
.any(|name| name.as_str() == Some(terminal.as_str()))
})
})
} else {
terminal.eq_ignore_ascii_case("n") || (numeric_four && terminal == "4")
};
(!nonphase).then_some(index)
})
.collect()
}
#[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,
v_min_phase: None,
v_max_phase: 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: ConductorMatrix,
pub x_series: ConductorMatrix,
pub g_from: ConductorMatrix,
pub b_from: ConductorMatrix,
pub g_to: ConductorMatrix,
pub b_to: ConductorMatrix,
#[serde(default)]
pub i_max: Option<Vec<f64>>,
#[serde(default)]
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: ConductorMatrix,
x_series: ConductorMatrix,
) -> 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,
pub length: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub route: Option<Vec<DistLocation>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub i_max: Option<Vec<f64>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
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,
pub q_rated: f64,
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)]
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)]
pub p_min: Option<Vec<f64>>,
#[serde(default)]
pub p_max: Option<Vec<f64>>,
#[serde(default)]
pub q_min: Option<Vec<f64>>,
#[serde(default)]
pub q_max: Option<Vec<f64>>,
pub cost: Option<Vec<f64>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub s_max: Option<Vec<f64>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
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,
pub s_max: Vec<f64>,
#[serde(default)]
pub i_max: Option<Vec<f64>>,
pub p_avail: Option<f64>,
#[serde(default)]
pub p_min: Option<Vec<f64>>,
#[serde(default)]
pub p_max: Option<Vec<f64>>,
#[serde(default)]
pub q_min: Option<Vec<f64>>,
#[serde(default)]
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: ConductorMatrix,
pub b: ConductorMatrix,
pub extras: Extras,
}
impl DistShunt {
#[must_use]
pub fn new(
name: impl Into<String>,
bus: impl Into<String>,
terminal_map: Vec<String>,
g: ConductorMatrix,
b: ConductorMatrix,
) -> 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,
pub v_ref: f64,
pub s_rating: f64,
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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub energy_cost_rate: Option<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,
energy_cost_rate: None,
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(Debug, Clone)]
pub struct MulticonductorNetwork {
tables: std::sync::Arc<MulticonductorNetworkTables>,
}
impl MulticonductorNetwork {
pub(crate) fn from_tables(tables: MulticonductorNetworkTables) -> Self {
Self {
tables: std::sync::Arc::new(tables),
}
}
pub(crate) fn tables_mut(&mut self) -> &mut MulticonductorNetworkTables {
std::sync::Arc::make_mut(&mut self.tables)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schema", schemars(rename = "MulticonductorNetwork"))]
#[serde(remote = "Self")]
pub(crate) struct MulticonductorNetworkTables {
pub name: Option<String>,
pub base_frequency: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub geo: Option<DistGeoMeta>,
pub buses: Vec<DistBus>,
#[serde(rename = "linecodes")]
pub line_codes: 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>,
#[serde(rename = "untyped")]
pub untyped_objects: Vec<UntypedObject>,
pub commands: Vec<(String, String)>,
pub options: Vec<(String, String)>,
#[serde(skip)]
pub defaulted: BTreeMap<String, Vec<&'static str>>,
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> {
MulticonductorNetworkTables::serialize(
&self.tables,
powerio_core::__implementation::nonfinite::NonFiniteSer(serializer),
)
}
}
impl<'de> serde::Deserialize<'de> for MulticonductorNetwork {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
MulticonductorNetworkTables::deserialize(
powerio_core::__implementation::nonfinite::NonFiniteDe(deserializer),
)
.map(MulticonductorNetwork::from_tables)
}
}
#[cfg(feature = "schema")]
impl schemars::JsonSchema for MulticonductorNetwork {
fn schema_name() -> std::borrow::Cow<'static, str> {
<MulticonductorNetworkTables as schemars::JsonSchema>::schema_name()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
<MulticonductorNetworkTables as schemars::JsonSchema>::schema_id()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
<MulticonductorNetworkTables as schemars::JsonSchema>::json_schema(generator)
}
}
macro_rules! dist_table_accessors {
($($field:ident, $field_mut:ident: $ty:ty;)+) => {
impl MulticonductorNetwork {
$(
#[must_use]
pub fn $field(&self) -> &$ty {
&self.tables.$field
}
#[must_use]
pub fn $field_mut(&mut self) -> &mut $ty {
&mut self.tables_mut().$field
}
)+
}
};
}
dist_table_accessors! {
name, name_mut: Option<String>;
geo, geo_mut: Option<DistGeoMeta>;
buses, buses_mut: Vec<DistBus>;
line_codes, line_codes_mut: Vec<DistLineCode>;
lines, lines_mut: Vec<DistLine>;
switches, switches_mut: Vec<DistSwitch>;
transformers, transformers_mut: Vec<DistTransformer>;
loads, loads_mut: Vec<DistLoad>;
generators, generators_mut: Vec<DistGenerator>;
ibrs, ibrs_mut: Vec<DistIbr>;
control_profiles, control_profiles_mut: Vec<DistControlProfile>;
shunts, shunts_mut: Vec<DistShunt>;
capacitors, capacitors_mut: Vec<DistCapacitor>;
sources, sources_mut: Vec<VoltageSource>;
untyped_objects, untyped_objects_mut: Vec<UntypedObject>;
commands, commands_mut: Vec<(String, String)>;
options, options_mut: Vec<(String, String)>;
defaulted, defaulted_mut: BTreeMap<String, Vec<&'static str>>;
source_format, source_format_mut: Option<DistSourceFormat>;
extras, extras_mut: Extras;
}
impl MulticonductorNetwork {
#[must_use]
pub fn base_frequency(&self) -> f64 {
self.tables.base_frequency
}
#[must_use]
pub fn base_frequency_mut(&mut self) -> &mut f64 {
&mut self.tables_mut().base_frequency
}
}
impl Default for MulticonductorNetwork {
fn default() -> Self {
MulticonductorNetwork::from_tables(MulticonductorNetworkTables::default())
}
}
impl Default for MulticonductorNetworkTables {
fn default() -> Self {
MulticonductorNetworkTables {
name: None,
base_frequency: crate::dss::defaults::BASE_FREQUENCY,
geo: None,
buses: Vec::new(),
line_codes: 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_objects: Vec::new(),
commands: Vec::new(),
options: Vec::new(),
defaulted: BTreeMap::new(),
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 {
MulticonductorNetwork::from_tables(MulticonductorNetworkTables {
name: Some(name.into()),
..MulticonductorNetworkTables::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.line_codes()
.iter()
.find(|c| c.name.eq_ignore_ascii_case(name))
}
}
pub(crate) fn warn_defaulted_frequency(
net: &MulticonductorNetwork,
field: &str,
diags: &mut crate::collect::Diagnostics,
) {
let charging = net.line_codes().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 {
diags.push(
&crate::diagnostics::codes::READ_MULTICONDUCTOR_VALUE_DEFAULTED,
format!(
"document states no {field} and carries line susceptance; read at {} Hz",
net.base_frequency()
),
);
}
}
#[must_use]
pub fn find_unresolved_references(net: &MulticonductorNetwork) -> Vec<String> {
let mut diags = crate::collect::Diagnostics::new();
warn_unresolved_references(net, &mut diags);
diags
.into_records()
.into_iter()
.map(|record| record.message().to_owned())
.collect()
}
pub(crate) fn warn_unresolved_references(
net: &MulticonductorNetwork,
diags: &mut crate::collect::Diagnostics,
) {
use std::collections::BTreeSet;
let buses: BTreeSet<String> = net
.buses()
.iter()
.map(|b| b.id.to_ascii_lowercase())
.collect();
let linecodes: BTreeSet<String> = net
.line_codes()
.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 {
diags.push(
&crate::diagnostics::codes::VALIDATE_MULTICONDUCTOR_REFERENCE_UNDEFINED,
message,
);
}
}
fn zero_mat(n: usize) -> ConductorMatrix {
vec![vec![0.0; n]; n]
}
fn matrix_extent(m: &ConductorMatrix) -> 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<ConductorMatrix> {
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)
}
pub(crate) fn winding_coil_voltage(winding: &DistWinding, phases: usize) -> f64 {
let divisor = if winding.conn == DistWindingConn::Wye && matches!(phases, 2 | 3) {
3f64.sqrt()
} else {
1.0
};
winding.v_ref * winding.tap / divisor
}
pub(crate) fn transformer_no_load_percentages(t: &DistTransformer) -> Option<(f64, f64)> {
let shunt = t.extras.get("no_load_shunt")?;
if shunt.get("winding")?.as_u64()? != 2 {
return None;
}
let g = shunt.get("g")?.as_f64()?;
let b = shunt.get("b")?.as_f64()?;
let voltage = winding_coil_voltage(t.windings.get(1)?, t.phases);
let rating = t.windings.first()?.s_rating;
if !voltage.is_finite()
|| voltage <= 0.0
|| !rating.is_finite()
|| rating <= 0.0
|| g < 0.0
|| b > 0.0
{
return None;
}
let scale = 100.0 * t.phases.max(1) as f64 * voltage.powi(2) / rating;
Some((g * scale, -b * scale))
}
#[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());
}
#[test]
fn multiconductor_json_uses_only_final_nonfinite_spellings() {
let mut network = MulticonductorNetwork::named("strict-ir");
let mut code = DistLineCode::new("lc", vec![vec![0.1]], vec![vec![0.2]]);
code.i_max = Some(vec![f64::INFINITY]);
network.line_codes_mut().push(code);
let text = serde_json::to_string(&network).unwrap();
assert!(text.contains(r#""i_max":["Infinity"]"#), "{text}");
let invalid = text.replace(r#""i_max":["Infinity"]"#, r#""i_max":[null]"#);
let error = serde_json::from_str::<MulticonductorNetwork>(&invalid).unwrap_err();
assert!(error.to_string().contains("cannot be null"), "{error}");
}
}