use std::collections::HashMap;
use crate::linear::btheta::DcBranch;
use super::crac::{Crac, State, UsageRule};
use super::evaluate::{Network, Resolution, SecurityResult};
use super::linear::ObjectiveUnit;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Constrained {
at_or_below_zero: HashMap<usize, Vec<String>>,
measured: bool,
}
impl Constrained {
pub fn unmeasured() -> Self {
Self::default()
}
pub fn is_measured(&self) -> bool {
self.measured
}
pub fn from_result(
result: &SecurityResult,
unit: ObjectiveUnit,
countries: &HashMap<usize, Vec<String>>,
) -> Self {
let mut at_or_below_zero = HashMap::new();
for perimeter in &result.perimeters {
for cnec in &perimeter.cnecs {
let margin = match unit {
ObjectiveUnit::Megawatt => cnec.margin_mw,
ObjectiveUnit::Ampere => cnec.margin_a,
};
if margin <= 0.0 {
at_or_below_zero.insert(
cnec.cnec,
countries.get(&cnec.branch).cloned().unwrap_or_default(),
);
}
}
}
Self {
at_or_below_zero,
measured: true,
}
}
pub fn activates(&self, rule: &UsageRule, state: &State, crac: &Crac) -> bool {
match rule {
UsageRule::OnInstant { .. } | UsageRule::OnContingencyState { .. } => {
rule.covers(state)
}
_ if !self.measured => rule.covers(state),
UsageRule::OnConstraint { instant, cnec } => {
let Some(index) = crac.flow_cnecs.iter().position(|c| c.id == *cnec) else {
return false;
};
let named = &crac.flow_cnecs[index];
let preventive =
crac.instants[*instant].kind == super::crac::InstantKind::Preventive;
if !preventive && named.state.contingency != state.contingency {
return false;
}
state.instant == *instant && self.at_or_below_zero.contains_key(&index)
}
UsageRule::OnFlowConstraintInCountry {
instant,
country,
contingency,
} => {
if contingency.is_some_and(|c| state.contingency != Some(c)) {
return false;
}
if state.instant != *instant {
return false;
}
self.at_or_below_zero.iter().any(|(&index, located)| {
let cnec = &crac.flow_cnecs[index];
cnec.state.instant >= *instant
&& contingency.is_none_or(|c| cnec.state.contingency == Some(c))
&& located.iter().any(|l| l.eq_ignore_ascii_case(country))
})
}
}
}
pub fn allows(&self, rules: &[UsageRule], perimeter: &[State], crac: &Crac) -> bool {
perimeter
.iter()
.any(|s| rules.iter().any(|r| self.activates(r, s, crac)))
}
}
pub fn branch_countries(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
dc: &[DcBranch],
) -> HashMap<usize, Vec<String>> {
let mut out: HashMap<usize, Vec<String>> = HashMap::new();
for cnec in &crac.flow_cnecs {
let Some(branch) = resolution.branch(&cnec.network_element) else {
continue;
};
out.entry(branch).or_insert_with(|| {
let Some(b) = dc.iter().find(|b| b.index == branch) else {
return Vec::new();
};
[b.from, b.to]
.iter()
.filter_map(|&bus| network.bus_countries.get(bus)?.clone())
.collect()
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rao::crac::{FlowCnec, Instant, InstantKind, Side, Threshold, Unit};
fn crac() -> Crac {
let cnec = |id: &str, contingency: Option<usize>, instant: usize| FlowCnec {
id: id.into(),
network_element: id.into(),
state: State {
instant,
contingency,
},
thresholds: vec![Threshold {
unit: Unit::Megawatt,
min: None,
max: Some(100.0),
side: Side::One,
}],
reliability_margin: 0.0,
optimized: true,
monitored: false,
operator: None,
i_max: None,
nominal_v: None,
};
Crac {
instants: vec![
Instant {
id: "preventive".into(),
kind: InstantKind::Preventive,
},
Instant {
id: "curative".into(),
kind: InstantKind::Curative,
},
],
flow_cnecs: vec![
cnec("tight", None, 0),
cnec("slack", None, 0),
cnec("cur", Some(0), 1),
],
..Default::default()
}
}
fn constrained() -> Constrained {
Constrained {
at_or_below_zero: [(0, vec!["fr".into()]), (2, vec!["be".into()])]
.into_iter()
.collect(),
measured: true,
}
}
const PREVENTIVE: State = State {
instant: 0,
contingency: None,
};
#[test]
fn a_conditional_rule_needs_its_cnec_to_be_constrained() {
let crac = crac();
let c = constrained();
let on = |cnec: &str| UsageRule::OnConstraint {
instant: 0,
cnec: cnec.into(),
};
assert!(c.activates(&on("tight"), &PREVENTIVE, &crac));
assert!(!c.activates(&on("slack"), &PREVENTIVE, &crac));
assert!(!c.activates(&on("nonesuch"), &PREVENTIVE, &crac));
}
#[test]
fn a_preventive_rule_reaches_across_contingencies() {
let crac = crac();
let c = constrained();
let rule = UsageRule::OnConstraint {
instant: 0,
cnec: "cur".into(),
};
assert!(c.activates(&rule, &PREVENTIVE, &crac));
}
#[test]
fn an_unconditional_rule_is_not_narrowed_by_a_conditional_one() {
let crac = crac();
let c = constrained();
let rules = vec![
UsageRule::OnInstant { instant: 0 },
UsageRule::OnConstraint {
instant: 0,
cnec: "slack".into(),
},
];
assert!(
c.allows(&rules, &[PREVENTIVE], &crac),
"the free rule alone should make this available"
);
assert!(
!c.allows(&rules[1..], &[PREVENTIVE], &crac),
"…and without it, the unconstrained CNEC authorizes nothing"
);
}
#[test]
fn the_country_rule_is_answered_by_any_overload_it_covers() {
let crac = crac();
let c = constrained();
let rule = |instant, country: &str, contingency| UsageRule::OnFlowConstraintInCountry {
instant,
country: country.into(),
contingency,
};
assert!(c.activates(&rule(0, "fr", None), &PREVENTIVE, &crac));
assert!(c.activates(&rule(0, "be", None), &PREVENTIVE, &crac));
assert!(!c.activates(&rule(0, "de", None), &PREVENTIVE, &crac));
assert!(!c.activates(&rule(0, "be", Some(0)), &PREVENTIVE, &crac));
let curative = State {
instant: 1,
contingency: Some(0),
};
assert!(c.activates(&rule(1, "be", Some(0)), &curative, &crac));
assert!(!c.activates(&rule(1, "fr", Some(0)), &curative, &crac));
}
#[test]
fn sitting_on_the_threshold_counts_as_constrained() {
use crate::rao::evaluate::{CnecResult, PerimeterResult, SecurityResult};
let cnec = |i: usize, margin: f64| CnecResult {
cnec: i,
branch: i,
flow_mw: 0.0,
margin_mw: margin,
upper_mw: 0.0,
lower_mw: 0.0,
limit_mw: 0.0,
margin_a: margin,
conversion_v: 0.0,
current_a: 0.0,
side_two: (0.0, 0.0),
};
let result = SecurityResult {
perimeters: vec![PerimeterResult {
state: PREVENTIVE,
cnecs: vec![cnec(0, 0.0), cnec(1, 1e-9)],
severed: false,
}],
skipped: Vec::new(),
};
let c = Constrained::from_result(&result, ObjectiveUnit::Megawatt, &HashMap::new());
assert!(
c.at_or_below_zero.contains_key(&0),
"exactly zero is constrained"
);
assert!(
!c.at_or_below_zero.contains_key(&1),
"a hair above zero is not"
);
}
#[test]
fn an_unmeasured_perimeter_keeps_the_old_permissive_reading() {
let crac = crac();
let c = Constrained::unmeasured();
let rule = UsageRule::OnConstraint {
instant: 0,
cnec: "slack".into(),
};
assert!(c.activates(&rule, &PREVENTIVE, &crac));
assert!(!c.is_measured());
}
}