use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum InstantKind {
Preventive,
Outage,
Auto,
Curative,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Instant {
pub id: String,
pub kind: InstantKind,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Contingency {
pub id: String,
#[serde(default)]
pub name: Option<String>,
pub elements: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct State {
pub instant: usize,
pub contingency: Option<usize>,
}
impl State {
pub fn preventive(instant: usize) -> Self {
Self {
instant,
contingency: None,
}
}
pub fn is_preventive(&self) -> bool {
self.contingency.is_none()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Side {
One,
Two,
Both,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Unit {
Ampere,
Megawatt,
PercentImax,
Degree,
Kilovolt,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Threshold {
pub unit: Unit,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
pub side: Side,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FlowCnec {
pub id: String,
pub network_element: String,
pub state: State,
pub thresholds: Vec<Threshold>,
#[serde(default)]
pub reliability_margin: f64,
#[serde(default = "yes")]
pub optimized: bool,
#[serde(default)]
pub monitored: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operator: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub i_max: Option<[Option<f64>; 2]>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nominal_v: Option<[Option<f64>; 2]>,
}
fn yes() -> bool {
true
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ElementaryAction {
Switch { element: String, open: bool },
TerminalsConnection { element: String, connected: bool },
PstTapPosition { element: String, tap: i32 },
GeneratorSetpoint { element: String, p: f64 },
LoadSetpoint { element: String, p: f64 },
ShuntSection { element: String, section: i32 },
SwitchPair { open: String, close: String },
}
impl RangeActionKind {
pub fn angle_at(&self, tap: i32) -> Option<f64> {
match self {
RangeActionKind::Pst { tap_to_angle, .. } => tap_to_angle
.binary_search_by_key(&tap, |(t, _)| *t)
.ok()
.map(|i| tap_to_angle[i].1),
_ => None,
}
}
pub fn nearest_tap(&self, angle_deg: f64) -> Option<i32> {
match self {
RangeActionKind::Pst { tap_to_angle, .. } => tap_to_angle
.iter()
.min_by(|a, b| (a.1 - angle_deg).abs().total_cmp(&(b.1 - angle_deg).abs()))
.map(|(t, _)| *t),
_ => None,
}
}
}
impl ElementaryAction {
pub fn elements(&self) -> Vec<&str> {
match self {
ElementaryAction::Switch { element, .. }
| ElementaryAction::TerminalsConnection { element, .. }
| ElementaryAction::PstTapPosition { element, .. }
| ElementaryAction::GeneratorSetpoint { element, .. }
| ElementaryAction::LoadSetpoint { element, .. }
| ElementaryAction::ShuntSection { element, .. } => vec![element],
ElementaryAction::SwitchPair { open, close } => vec![open, close],
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum UsageRule {
OnInstant { instant: usize },
OnContingencyState { state: State },
OnConstraint { instant: usize, cnec: String },
OnFlowConstraintInCountry {
instant: usize,
country: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
contingency: Option<usize>,
},
}
impl UsageRule {
pub fn instant(&self) -> usize {
match self {
UsageRule::OnInstant { instant }
| UsageRule::OnConstraint { instant, .. }
| UsageRule::OnFlowConstraintInCountry { instant, .. } => *instant,
UsageRule::OnContingencyState { state } => state.instant,
}
}
pub fn covers(&self, state: &State) -> bool {
match self {
UsageRule::OnInstant { instant } => state.instant == *instant,
UsageRule::OnContingencyState { state: s } => s == state,
UsageRule::OnConstraint { instant, .. } => state.instant == *instant,
UsageRule::OnFlowConstraintInCountry {
instant,
contingency,
..
} => {
state.instant == *instant
&& contingency.is_none_or(|c| state.contingency == Some(c))
}
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct NetworkAction {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operator: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speed: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub activation_cost: Option<f64>,
pub elementary: Vec<ElementaryAction>,
pub usage_rules: Vec<UsageRule>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RangeKind {
Absolute,
RelativeToInitialNetwork,
RelativeToPreviousInstant,
RelativeToPreviousTimeStep,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Range {
pub kind: RangeKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RangeActionKind {
Pst {
element: String,
initial_tap: i32,
tap_to_angle: Vec<(i32, f64)>,
},
Injection { distribution: Vec<(String, f64)> },
Hvdc { element: String },
CounterTrade {
exporting: String,
importing: String,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RangeAction {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operator: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub speed: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub activation_cost: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub variation_cost: Option<VariationCost>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
pub kind: RangeActionKind,
pub ranges: Vec<Range>,
pub usage_rules: Vec<UsageRule>,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct VariationCost {
pub up: f64,
pub down: f64,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct RaUsageLimits {
pub instant: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_ra: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tso: Option<usize>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub max_topo_per_tso: HashMap<String, usize>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub max_pst_per_tso: HashMap<String, usize>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub max_ra_per_tso: HashMap<String, usize>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub max_elementary_actions_per_tso: HashMap<String, usize>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Crac {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub instants: Vec<Instant>,
pub contingencies: Vec<Contingency>,
pub flow_cnecs: Vec<FlowCnec>,
pub network_actions: Vec<NetworkAction>,
pub range_actions: Vec<RangeAction>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub usage_limits: Vec<RaUsageLimits>,
#[serde(default)]
pub angle_cnecs: usize,
#[serde(default)]
pub voltage_cnecs: usize,
}
impl Crac {
pub fn instant(&self, id: &str) -> Option<usize> {
self.instants.iter().position(|i| i.id == id)
}
pub fn contingency(&self, id: &str) -> Option<usize> {
self.contingencies.iter().position(|c| c.id == id)
}
pub fn preventive_instant(&self) -> Option<usize> {
self.instants
.iter()
.position(|i| i.kind == InstantKind::Preventive)
}
pub fn curative_instants(&self) -> Vec<usize> {
self.instants
.iter()
.enumerate()
.filter(|(_, i)| i.kind == InstantKind::Curative)
.map(|(i, _)| i)
.collect()
}
pub fn states(&self) -> Vec<State> {
let mut states: Vec<State> = Vec::new();
for cnec in &self.flow_cnecs {
if !states.contains(&cnec.state) {
states.push(cnec.state.clone());
}
}
states.sort_by_key(|s| (s.instant, s.contingency));
states
}
pub fn network_elements(&self) -> Vec<&str> {
let mut ids: Vec<&str> = Vec::new();
for c in &self.contingencies {
ids.extend(c.elements.iter().map(String::as_str));
}
for c in &self.flow_cnecs {
ids.push(&c.network_element);
}
for a in &self.network_actions {
ids.extend(a.elementary.iter().flat_map(ElementaryAction::elements));
}
for a in &self.range_actions {
match &a.kind {
RangeActionKind::Pst { element, .. } | RangeActionKind::Hvdc { element } => {
ids.push(element)
}
RangeActionKind::Injection { distribution } => {
ids.extend(distribution.iter().map(|(e, _)| e.as_str()))
}
RangeActionKind::CounterTrade { .. } => {}
}
}
ids.sort_unstable();
ids.dedup();
ids
}
pub fn network_actions_for(&self, state: &State) -> Vec<&NetworkAction> {
self.network_actions
.iter()
.filter(|a| a.usage_rules.iter().any(|r| r.covers(state)))
.collect()
}
pub fn range_actions_for(&self, state: &State) -> Vec<&RangeAction> {
self.range_actions
.iter()
.filter(|a| a.usage_rules.iter().any(|r| r.covers(state)))
.collect()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RaoDocument {
pub version: String,
#[serde(rename = "type")]
pub kind: String,
#[serde(flatten)]
pub crac: Crac,
}
pub const DOCUMENT_TYPE: &str = "rao_input";
pub const DOCUMENT_VERSION: &str = "1.0";
impl RaoDocument {
pub fn new(crac: Crac) -> Self {
Self {
version: DOCUMENT_VERSION.to_string(),
kind: DOCUMENT_TYPE.to_string(),
crac,
}
}
}
impl Crac {
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(&RaoDocument::new(self.clone()))
}
pub fn from_json(text: &str) -> Result<Self, serde_json::Error> {
let doc: RaoDocument = serde_json::from_str(text)?;
if doc.kind != DOCUMENT_TYPE {
return Err(serde::de::Error::custom(format!(
"expected a `{DOCUMENT_TYPE}` document, found `{}`",
doc.kind
)));
}
Ok(doc.crac)
}
}