use std::collections::HashMap;
use super::crac::{Crac, State};
use super::evaluate::{FlowModel, Network, Resolution, SecurityResult, evaluate_model};
use super::linear::ObjectiveUnit;
pub const NO_CNEC_MARGIN: f64 = 1e9;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MnecOptions {
pub enabled: bool,
pub acceptable_margin_decrease: f64,
pub violation_cost: f64,
pub constraint_adjustment_coefficient: f64,
}
impl Default for MnecOptions {
fn default() -> Self {
Self {
enabled: true,
acceptable_margin_decrease: 50.0,
violation_cost: 10.0,
constraint_adjustment_coefficient: 0.0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Initial {
pub flow_mw: f64,
pub margin_mw: f64,
pub margin_a: f64,
}
impl Initial {
fn margin(&self, unit: ObjectiveUnit) -> f64 {
match unit {
ObjectiveUnit::Megawatt => self.margin_mw,
ObjectiveUnit::Ampere => self.margin_a,
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Baseline {
initial: HashMap<usize, Initial>,
}
impl Baseline {
pub fn from_result(result: &SecurityResult) -> Self {
let mut initial = HashMap::new();
for perimeter in &result.perimeters {
for cnec in &perimeter.cnecs {
initial.insert(
cnec.cnec,
Initial {
flow_mw: cnec.flow_mw,
margin_mw: cnec.margin_mw,
margin_a: cnec.margin_a,
},
);
}
}
Self { initial }
}
pub fn measure(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
model: FlowModel,
) -> Self {
let ac = super::evaluate::ac_options(network);
let result = evaluate_model(
crac,
network,
resolution,
network.initially_open,
model,
&ac,
);
Self::from_result(&result)
}
pub fn get(&self, cnec: usize) -> Option<Initial> {
self.initial.get(&cnec).copied()
}
pub fn is_empty(&self) -> bool {
self.initial.is_empty()
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Mnec {
pub options: MnecOptions,
pub baseline: Baseline,
}
impl Mnec {
pub fn active(&self) -> bool {
self.options.enabled && !self.baseline.is_empty()
}
pub fn floor(&self, initial: Initial, unit: ObjectiveUnit) -> f64 {
f64::min(
0.0,
initial.margin(unit) - self.options.acceptable_margin_decrease,
)
}
pub fn violation(&self, initial: Initial, margin: f64, unit: ObjectiveUnit) -> f64 {
(self.floor(initial, unit) - margin).max(0.0)
}
pub fn cost(
&self,
crac: &Crac,
result: &SecurityResult,
perimeter: &[State],
unit: ObjectiveUnit,
) -> f64 {
if !self.active() {
return 0.0;
}
let mut total = 0.0;
for p in result
.perimeters
.iter()
.filter(|p| perimeter.contains(&p.state))
{
for cnec in &p.cnecs {
if !crac.flow_cnecs[cnec.cnec].monitored {
continue;
}
let Some(initial) = self.baseline.get(cnec.cnec) else {
continue;
};
let margin = match unit {
ObjectiveUnit::Megawatt => cnec.margin_mw,
ObjectiveUnit::Ampere => cnec.margin_a,
};
total += self.options.violation_cost * self.violation(initial, margin, unit);
}
}
total
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rule(decrease: f64) -> Mnec {
Mnec {
options: MnecOptions {
acceptable_margin_decrease: decrease,
..Default::default()
},
baseline: Baseline {
initial: [(
0,
Initial {
flow_mw: 0.0,
margin_mw: 0.0,
margin_a: 0.0,
},
)]
.into_iter()
.collect(),
},
}
}
fn started_at(margin: f64) -> Initial {
Initial {
flow_mw: 0.0,
margin_mw: margin,
margin_a: margin,
}
}
#[test]
fn the_floor_is_zero_only_for_an_mnec_with_room_to_spare() {
let mnec = rule(50.0);
let mw = ObjectiveUnit::Megawatt;
assert_eq!(mnec.floor(started_at(200.0), mw), 0.0);
assert_eq!(mnec.floor(started_at(50.0), mw), 0.0);
assert!((mnec.floor(started_at(42.4), mw) - -7.6).abs() < 1e-9);
assert!((mnec.floor(started_at(-33.3), mw) - -83.3).abs() < 1e-9);
}
#[test]
fn a_healthy_mnec_may_not_spend_its_whole_margin_plus_fifty() {
let mnec = rule(50.0);
let mw = ObjectiveUnit::Megawatt;
let initial = started_at(200.0);
assert_eq!(mnec.violation(initial, 150.0, mw), 0.0);
assert_eq!(mnec.violation(initial, 0.0, mw), 0.0);
assert_eq!(mnec.violation(initial, -30.0, mw), 30.0);
}
#[test]
fn the_rule_is_read_in_whichever_unit_the_objective_uses() {
let mnec = rule(50.0);
let initial = Initial {
flow_mw: 0.0,
margin_mw: 40.0,
margin_a: 60.0,
};
assert_eq!(mnec.floor(initial, ObjectiveUnit::Megawatt), -10.0);
assert_eq!(mnec.floor(initial, ObjectiveUnit::Ampere), 0.0);
}
#[test]
fn an_unmeasured_baseline_leaves_mnecs_alone() {
let mnec = Mnec::default();
assert!(!mnec.active(), "the default baseline is empty");
assert!(
MnecOptions::default().enabled,
"…which is not the same as being switched off"
);
}
}