use serde::{Deserialize, Serialize};
use crate::network::{BranchRef, WeightedBranchRef};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "InterfaceSerde")]
pub struct Interface {
pub name: String,
pub members: Vec<WeightedBranchRef>,
pub limit_forward_mw: f64,
pub limit_reverse_mw: f64,
pub in_service: bool,
#[serde(default)]
pub limit_forward_mw_schedule: Vec<f64>,
#[serde(default)]
pub limit_reverse_mw_schedule: Vec<f64>,
}
#[derive(Debug, Deserialize)]
struct InterfaceSerde {
pub name: String,
#[serde(default)]
pub members: Vec<WeightedBranchRef>,
#[serde(default)]
pub branches: Vec<(u32, u32, String)>,
#[serde(default)]
pub coefficients: Vec<f64>,
pub limit_forward_mw: f64,
pub limit_reverse_mw: f64,
pub in_service: bool,
#[serde(default)]
pub limit_forward_mw_schedule: Vec<f64>,
#[serde(default)]
pub limit_reverse_mw_schedule: Vec<f64>,
}
impl TryFrom<InterfaceSerde> for Interface {
type Error = String;
fn try_from(value: InterfaceSerde) -> Result<Self, Self::Error> {
let members = if !value.members.is_empty() {
value.members
} else if value.branches.is_empty() && value.coefficients.is_empty() {
Vec::new()
} else {
if value.branches.len() != value.coefficients.len() {
return Err(format!(
"interface '{}' has {} legacy branches but {} coefficients",
value.name,
value.branches.len(),
value.coefficients.len()
));
}
value
.branches
.into_iter()
.zip(value.coefficients)
.map(|(branch, coefficient)| WeightedBranchRef {
branch: branch.into(),
coefficient,
})
.collect()
};
Ok(Self {
name: value.name,
members,
limit_forward_mw: value.limit_forward_mw,
limit_reverse_mw: value.limit_reverse_mw,
in_service: value.in_service,
limit_forward_mw_schedule: value.limit_forward_mw_schedule,
limit_reverse_mw_schedule: value.limit_reverse_mw_schedule,
})
}
}
impl Interface {
pub fn effective_limit_forward_mw(&self, t: usize) -> f64 {
self.limit_forward_mw_schedule
.get(t)
.copied()
.unwrap_or(self.limit_forward_mw)
}
pub fn effective_limit_reverse_mw(&self, t: usize) -> f64 {
self.limit_reverse_mw_schedule
.get(t)
.copied()
.unwrap_or(self.limit_reverse_mw)
}
}
pub const INACTIVE_FLOWGATE_LIMIT_MW: f64 = 1e30;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "FlowgateSerde")]
pub struct Flowgate {
pub name: String,
pub monitored: Vec<WeightedBranchRef>,
pub contingency_branch: Option<BranchRef>,
pub limit_mw: f64,
#[serde(default)]
pub limit_reverse_mw: f64,
pub in_service: bool,
#[serde(default)]
pub limit_mw_schedule: Vec<f64>,
#[serde(default)]
pub limit_reverse_mw_schedule: Vec<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hvdc_coefficients: Vec<(usize, f64)>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ptdf_per_bus: Vec<(u32, f64)>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hvdc_band_coefficients: Vec<(usize, usize, f64)>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit_mw_active_period: Option<u32>,
#[serde(default, skip_serializing_if = "FlowgateBreachSides::is_both")]
pub breach_sides: FlowgateBreachSides,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FlowgateBreachSides {
#[default]
Both,
Upper,
Lower,
}
impl FlowgateBreachSides {
pub fn is_both(&self) -> bool {
matches!(self, FlowgateBreachSides::Both)
}
pub fn allocates_upper_slack(&self) -> bool {
matches!(self, FlowgateBreachSides::Both | FlowgateBreachSides::Upper)
}
pub fn allocates_lower_slack(&self) -> bool {
matches!(self, FlowgateBreachSides::Both | FlowgateBreachSides::Lower)
}
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum BranchRefSerde {
Structured(BranchRef),
Legacy((u32, u32, String)),
}
impl From<BranchRefSerde> for BranchRef {
fn from(value: BranchRefSerde) -> Self {
match value {
BranchRefSerde::Structured(branch) => branch,
BranchRefSerde::Legacy(branch) => branch.into(),
}
}
}
#[derive(Debug, Deserialize)]
struct FlowgateSerde {
pub name: String,
#[serde(default)]
pub monitored: Vec<WeightedBranchRef>,
#[serde(default)]
pub monitored_branches: Vec<(u32, u32, String)>,
#[serde(default)]
pub monitored_coefficients: Vec<f64>,
pub contingency_branch: Option<BranchRefSerde>,
pub limit_mw: f64,
#[serde(default)]
pub limit_reverse_mw: f64,
pub in_service: bool,
#[serde(default)]
pub limit_mw_schedule: Vec<f64>,
#[serde(default)]
pub limit_reverse_mw_schedule: Vec<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hvdc_coefficients: Vec<(usize, f64)>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hvdc_band_coefficients: Vec<(usize, usize, f64)>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ptdf_per_bus: Vec<(u32, f64)>,
#[serde(default)]
pub limit_mw_active_period: Option<u32>,
#[serde(default)]
pub breach_sides: FlowgateBreachSides,
}
impl TryFrom<FlowgateSerde> for Flowgate {
type Error = String;
fn try_from(value: FlowgateSerde) -> Result<Self, Self::Error> {
let monitored = if !value.monitored.is_empty() {
value.monitored
} else if value.monitored_branches.is_empty() && value.monitored_coefficients.is_empty() {
Vec::new()
} else {
if value.monitored_branches.len() != value.monitored_coefficients.len() {
return Err(format!(
"flowgate '{}' has {} legacy monitored branches but {} coefficients",
value.name,
value.monitored_branches.len(),
value.monitored_coefficients.len()
));
}
value
.monitored_branches
.into_iter()
.zip(value.monitored_coefficients)
.map(|(branch, coefficient)| WeightedBranchRef {
branch: branch.into(),
coefficient,
})
.collect()
};
Ok(Self {
name: value.name,
monitored,
contingency_branch: value.contingency_branch.map(Into::into),
limit_mw: value.limit_mw,
limit_reverse_mw: value.limit_reverse_mw,
in_service: value.in_service,
limit_mw_schedule: value.limit_mw_schedule,
limit_reverse_mw_schedule: value.limit_reverse_mw_schedule,
hvdc_coefficients: value.hvdc_coefficients,
hvdc_band_coefficients: value.hvdc_band_coefficients,
ptdf_per_bus: value.ptdf_per_bus,
limit_mw_active_period: value.limit_mw_active_period,
breach_sides: value.breach_sides,
})
}
}
impl Flowgate {
pub fn effective_limit_mw(&self, t: usize) -> f64 {
if let Some(active) = self.limit_mw_active_period {
return if t == active as usize {
self.limit_mw
} else {
INACTIVE_FLOWGATE_LIMIT_MW
};
}
self.limit_mw_schedule
.get(t)
.copied()
.unwrap_or(self.limit_mw)
}
pub fn effective_limit_reverse_mw(&self, t: usize) -> f64 {
self.limit_reverse_mw_schedule
.get(t)
.copied()
.unwrap_or(self.limit_reverse_mw)
}
pub fn effective_reverse_or_forward(&self, t: usize) -> f64 {
let rev = self.effective_limit_reverse_mw(t);
if rev > 0.0 {
rev
} else {
self.effective_limit_mw(t)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperatingNomogram {
pub name: String,
pub index_flowgate: String,
pub constrained_flowgate: String,
pub points: Vec<(f64, f64)>,
pub in_service: bool,
}
impl OperatingNomogram {
pub fn evaluate(&self, index_flow_mw: f64) -> f64 {
if self.points.is_empty() {
return f64::INFINITY;
}
if index_flow_mw <= self.points[0].0 {
return self.points[0].1;
}
let last = self.points[self.points.len() - 1];
if index_flow_mw >= last.0 {
return last.1;
}
for w in self.points.windows(2) {
let (x0, y0) = w[0];
let (x1, y1) = w[1];
if index_flow_mw < x1 {
let t = (index_flow_mw - x0) / (x1 - x0);
return y0 + t * (y1 - y0);
}
}
last.1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nomogram_evaluate() {
let nom = OperatingNomogram {
name: "N1".into(),
index_flowgate: "FG_A".into(),
constrained_flowgate: "FG_B".into(),
points: vec![(-500.0, 1000.0), (0.0, 800.0), (500.0, 500.0)],
in_service: true,
};
assert!((nom.evaluate(-600.0) - 1000.0).abs() < 1e-9);
assert!((nom.evaluate(0.0) - 800.0).abs() < 1e-9);
assert!((nom.evaluate(250.0) - 650.0).abs() < 1e-9);
assert!((nom.evaluate(600.0) - 500.0).abs() < 1e-9);
}
#[test]
fn test_effective_limit_mw_schedule() {
let fg = Flowgate {
name: "FG".into(),
monitored: vec![],
contingency_branch: None,
limit_mw: 100.0,
limit_reverse_mw: 0.0,
in_service: true,
limit_mw_schedule: vec![90.0, 80.0, 70.0],
limit_reverse_mw_schedule: vec![],
hvdc_coefficients: vec![],
hvdc_band_coefficients: vec![],
ptdf_per_bus: vec![],
limit_mw_active_period: None,
breach_sides: FlowgateBreachSides::Both,
};
assert_eq!(fg.effective_limit_mw(0), 90.0);
assert_eq!(fg.effective_limit_mw(2), 70.0);
assert_eq!(fg.effective_limit_mw(5), 100.0);
assert_eq!(fg.effective_reverse_or_forward(0), 90.0);
}
}