use crate::opf::Solver;
use super::automaton::{AutomatonResult, simulate};
use super::crac::{Crac, InstantKind, RangeActionKind, State};
use super::evaluate::{
Network, PerimeterResult, Resolution, SecurityResult, evaluate_model, evaluate_with,
};
use super::linear::Setpoint;
use super::mnec::Baseline;
use super::search::{SearchOptions, SearchResult, objective_of, search, search_with_open};
#[derive(Clone, Debug)]
pub struct PerimeterPlan {
pub states: Vec<State>,
pub network_actions: Vec<usize>,
pub setpoints: Vec<Setpoint>,
pub initial_margin_mw: f64,
pub final_margin_mw: f64,
pub leaves: usize,
pub open_branches: Vec<usize>,
pub buses: Vec<crate::types::Bus>,
pub transformers: Vec<crate::types::Transformer>,
}
impl PerimeterPlan {
pub fn improvement(&self) -> f64 {
self.final_margin_mw - self.initial_margin_mw
}
pub fn is_secure(&self) -> bool {
self.final_margin_mw >= 0.0
}
}
#[derive(Clone, Debug)]
pub struct ScenarioPlan {
pub contingency: usize,
pub automatons: Option<AutomatonResult>,
pub perimeters: Vec<PerimeterPlan>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StepsExecuted {
#[default]
FirstPreventiveOnly,
FirstPreventiveFellBackToInitial,
SecondPreventiveImproved,
SecondPreventiveFellBackToFirst,
SecondPreventiveFellBackToInitial,
}
impl StepsExecuted {
pub fn as_str(self) -> &'static str {
match self {
Self::FirstPreventiveOnly => "The RAO only went through first preventive",
Self::FirstPreventiveFellBackToInitial => {
"First preventive fell back to initial situation"
}
Self::SecondPreventiveImproved => "Second preventive improved first preventive results",
Self::SecondPreventiveFellBackToFirst => {
"Second preventive fell back to first preventive results"
}
Self::SecondPreventiveFellBackToInitial => {
"Second preventive fell back to initial situation"
}
}
}
fn fell_back(self) -> Self {
match self {
Self::FirstPreventiveOnly => Self::FirstPreventiveFellBackToInitial,
_ => Self::SecondPreventiveFellBackToInitial,
}
}
}
#[derive(Clone, Debug)]
pub struct Plan {
pub preventive: PerimeterPlan,
pub scenarios: Vec<ScenarioPlan>,
pub pulled_forward: Vec<usize>,
pub initial_margin_mw: f64,
pub final_margin_mw: f64,
pub steps: StepsExecuted,
}
impl Plan {
pub fn is_secure(&self) -> bool {
self.final_margin_mw >= 0.0
}
pub fn improvement(&self) -> f64 {
self.final_margin_mw - self.initial_margin_mw
}
pub fn perimeters(&self) -> impl Iterator<Item = &PerimeterPlan> {
std::iter::once(&self.preventive)
.chain(self.scenarios.iter().flat_map(|s| s.perimeters.iter()))
}
pub fn spent(&self, crac: &Crac) -> (Vec<usize>, Vec<(usize, f64)>) {
(
activated_by(&self.preventive, &self.scenarios),
moved_by(crac, &self.preventive, &self.scenarios),
)
}
}
pub fn run(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
solver: &mut dyn Solver,
options: &SearchOptions,
) -> Plan {
let states = crac.states();
let actionable = |state: &State| -> bool {
crac.network_actions
.iter()
.any(|a| a.usage_rules.iter().any(|r| r.covers(state)))
|| crac
.range_actions
.iter()
.any(|a| a.usage_rules.iter().any(|r| r.covers(state)))
};
let mut preventive_states: Vec<State> = Vec::new();
let mut pulled_forward: Vec<usize> = Vec::new();
for state in &states {
let kind = crac.instants[state.instant].kind;
let include = match kind {
InstantKind::Preventive | InstantKind::Outage => true,
InstantKind::Auto => false,
InstantKind::Curative => !actionable(state),
};
if include {
preventive_states.push(state.clone());
if matches!(kind, InstantKind::Curative) {
pulled_forward.extend(
crac.flow_cnecs
.iter()
.enumerate()
.filter(|(_, c)| c.state == *state)
.map(|(i, _)| i),
);
}
}
}
if preventive_states.is_empty() {
preventive_states = states.clone();
}
let initial = worst_margin(crac, network, resolution, network.initially_open, &states);
let options = &with_mnec_baseline(crac, network, resolution, options);
let preventive = search(
crac,
network,
resolution,
&preventive_states,
solver,
options,
);
let mut plan_preventive = perimeter_plan(&preventive, preventive_states.clone());
let mut scenarios =
scenarios_after(crac, network, resolution, solver, options, &preventive, &[]);
let mut steps = StepsExecuted::FirstPreventiveOnly;
let all_states = crac.states();
let untouched = assess(crac, network, resolution, network.initially_open, options);
let before = objective_of(crac, &untouched, &all_states, &[], &[], &options.linear);
let judge = |p: &PerimeterPlan, s: &[ScenarioPlan]| {
objective_of(
crac,
&final_assessment(crac, network, resolution, p, s, options),
&all_states,
&activated_by(p, s),
&moved_by(crac, p, s),
&options.linear,
)
};
let first_objective = judge(&plan_preventive, &scenarios);
if let Some(second) = second_preventive(
crac,
network,
resolution,
solver,
options,
&preventive,
&scenarios,
first_objective < before - 1e-6,
) {
let after = scenarios_after(
crac,
network,
resolution,
solver,
options,
&second,
&second.held_setpoints,
);
steps = StepsExecuted::SecondPreventiveFellBackToFirst;
let second_plan = perimeter_plan(&second, preventive_states.clone());
if judge(&second_plan, &after) > first_objective + 1e-6 {
steps = StepsExecuted::SecondPreventiveImproved;
plan_preventive = second_plan;
scenarios = after;
}
}
let final_margin = final_assessment(
crac,
network,
resolution,
&plan_preventive,
&scenarios,
options,
)
.perimeters
.iter()
.filter_map(|p| p.min_optimized_margin(crac))
.fold(f64::INFINITY, f64::min);
plan_preventive.states = preventive_states;
pulled_forward.sort_unstable();
pulled_forward.dedup();
let plan = Plan {
preventive: plan_preventive,
scenarios,
pulled_forward,
initial_margin_mw: initial,
final_margin_mw: final_margin,
steps,
};
let after = judge(&plan.preventive, &plan.scenarios);
if after < before - 1e-6 {
return unoptimized(network, &plan, initial, steps.fell_back());
}
plan
}
fn unoptimized(network: &Network<'_>, plan: &Plan, initial: f64, steps: StepsExecuted) -> Plan {
let bare = |states: Vec<State>, leaves: usize| PerimeterPlan {
states,
network_actions: Vec::new(),
setpoints: Vec::new(),
initial_margin_mw: initial,
final_margin_mw: initial,
leaves,
open_branches: network.initially_open.to_vec(),
buses: network.buses.to_vec(),
transformers: network.transformers.to_vec(),
};
Plan {
preventive: bare(plan.preventive.states.clone(), plan.preventive.leaves),
scenarios: plan
.scenarios
.iter()
.map(|s| ScenarioPlan {
contingency: s.contingency,
automatons: None,
perimeters: s
.perimeters
.iter()
.map(|p| bare(p.states.clone(), p.leaves))
.collect(),
})
.collect(),
pulled_forward: plan.pulled_forward.clone(),
initial_margin_mw: initial,
final_margin_mw: initial,
steps,
}
}
fn curative_search(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
perimeter: &[State],
solver: &mut dyn Solver,
options: &SearchOptions,
already_open: &[usize],
) -> SearchResult {
search_with_open(
crac,
network,
resolution,
perimeter,
solver,
options,
already_open,
)
}
fn assess(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
open: &[usize],
options: &SearchOptions,
) -> SecurityResult {
let ac = super::evaluate::ac_options(network);
evaluate_model(
crac,
network,
resolution,
open,
options.linear.flow_model,
&ac,
)
}
fn final_assessment(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
preventive: &PerimeterPlan,
scenarios: &[ScenarioPlan],
options: &SearchOptions,
) -> SecurityResult {
let mut perimeters: Vec<PerimeterResult> = Vec::new();
let mut take = |result: SecurityResult, states: &[State]| {
for p in result.perimeters {
if states.contains(&p.state) && !perimeters.iter().any(|q| q.state == p.state) {
perimeters.push(p);
}
}
};
for scenario in scenarios {
for stage in scenario.perimeters.iter().rev() {
let net = Network {
buses: &stage.buses,
transformers: &stage.transformers,
..*network
};
take(
assess(crac, &net, resolution, &stage.open_branches, options),
&stage.states,
);
}
if let Some(automatons) = &scenario.automatons {
let net = Network {
buses: &preventive.buses,
transformers: &automatons.transformers,
..*network
};
let auto_states: Vec<State> = crac
.states()
.into_iter()
.filter(|s| s.contingency == Some(scenario.contingency))
.filter(|s| {
matches!(
crac.instants[s.instant].kind,
InstantKind::Auto | InstantKind::Curative
)
})
.collect();
take(
assess(crac, &net, resolution, &automatons.open_branches, options),
&auto_states,
);
}
}
let net = Network {
buses: &preventive.buses,
transformers: &preventive.transformers,
..*network
};
let rest = crac.states();
take(
assess(crac, &net, resolution, &preventive.open_branches, options),
&rest,
);
SecurityResult {
perimeters,
skipped: Vec::new(),
}
}
fn worst_margin(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
open: &[usize],
states: &[State],
) -> f64 {
evaluate_with(crac, network, resolution, open)
.perimeters
.iter()
.filter(|p| states.contains(&p.state))
.filter_map(|p| p.min_optimized_margin(crac))
.fold(f64::INFINITY, f64::min)
}
fn with_mnec_baseline(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
options: &SearchOptions,
) -> SearchOptions {
let mut options = options.clone();
if !options.linear.mnec.options.enabled
|| !crac.flow_cnecs.iter().any(|c| c.monitored)
|| !options.linear.mnec.baseline.is_empty()
{
return options;
}
options.linear.mnec.baseline =
Baseline::measure(crac, network, resolution, options.linear.flow_model);
options
}
#[allow(clippy::too_many_arguments)]
fn scenarios_after(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
solver: &mut dyn Solver,
options: &SearchOptions,
preventive: &SearchResult,
decided: &[Setpoint],
) -> Vec<ScenarioPlan> {
let states = crac.states();
let actionable = |state: &State| -> bool {
crac.network_actions
.iter()
.any(|a| a.usage_rules.iter().any(|r| r.covers(state)))
|| crac
.range_actions
.iter()
.any(|a| a.usage_rules.iter().any(|r| r.covers(state)))
};
let carried_open = preventive.open_branches.clone();
let carried_transformers = preventive.transformers.clone();
let branch_of = |action: usize| -> Option<usize> {
let RangeActionKind::Pst { element, .. } = &crac.range_actions[action].kind else {
return None;
};
resolution.branch(element)
};
let preventive_tap = |action: usize| -> Option<i32> {
let branch = branch_of(action)?;
preventive
.setpoints
.iter()
.filter(|p| branch_of(p.action) == Some(branch))
.find_map(|p| p.tap)
};
let activated: Vec<&Setpoint> = decided
.iter()
.filter(|s| s.moved())
.filter(|s| match (s.tap, preventive_tap(s.action)) {
(Some(here), Some(there)) => here != there,
_ => true,
})
.collect();
let decided_taps: Vec<(usize, i32, f64)> = activated
.iter()
.filter_map(|s| Some((branch_of(s.action)?, s.tap?, s.value)))
.collect();
let before_decided: Vec<(usize, f64)> = activated
.iter()
.filter_map(|s| {
let RangeActionKind::Pst {
tap_to_angle,
initial_tap,
..
} = &crac.range_actions[s.action].kind
else {
return None;
};
let branch = branch_of(s.action)?;
let tap = preventive
.setpoints
.iter()
.filter(|p| branch_of(p.action) == Some(branch))
.find_map(|p| p.tap)
.unwrap_or(*initial_tap);
let angle = tap_to_angle
.iter()
.find(|(t, _)| *t == tap)
.map(|(_, a)| *a)?;
Some((s.action, angle))
})
.collect();
let costly_objective = options.linear.objective_kind.costly().is_some();
let curative_target = if costly_objective {
options.stop_at_target
} else {
Some(options.stop_at_target.unwrap_or_else(|| {
let target = preventive.final_objective + options.curative_min_obj_improvement;
if options.enforce_curative_security {
target.max(0.0)
} else {
target
}
}))
};
let options = &SearchOptions {
stop_at_target: curative_target,
max_depth: options.curative_max_depth.unwrap_or(options.max_depth),
linear: super::linear::LinearOptions {
decided: decided.iter().map(|s| s.action).collect(),
..options.linear.clone()
},
..options.clone()
};
let mut scenarios = Vec::new();
for (contingency, _) in crac.contingencies.iter().enumerate() {
let mut curative_instants: Vec<usize> = states
.iter()
.filter(|s| s.contingency == Some(contingency))
.filter(|s| crac.instants[s.instant].kind == InstantKind::Curative)
.filter(|s| actionable(s))
.map(|s| s.instant)
.collect();
curative_instants.sort_unstable();
curative_instants.dedup();
let mut open = carried_open.clone();
let mut transformers = carried_transformers.clone();
let automatons = {
let view = Network {
generation: network.generation,
buses: network.buses,
lines: network.lines,
transformers: &transformers,
branch_ids: network.branch_ids,
bus_ids: network.bus_ids,
initially_open: network.initially_open,
bus_countries: network.bus_countries,
shunts: network.shunts,
tap_changers: network.tap_changers,
base_mva: network.base_mva,
};
simulate(
crac,
&view,
resolution,
contingency,
&open,
&transformers,
options.linear.flow_model,
)
};
if let Some(result) = &automatons {
open = result.open_branches.clone();
transformers = result.transformers.clone();
}
if !decided_taps.is_empty() {
super::search::apply_taps(
&mut transformers,
network.tap_changers,
network.lines.len(),
&decided_taps,
);
}
if curative_instants.is_empty() && automatons.is_none() {
continue;
}
let mut perimeters = Vec::new();
for instant in curative_instants {
let state = State {
instant,
contingency: Some(contingency),
};
let view = Network {
generation: network.generation,
buses: network.buses,
lines: network.lines,
transformers: &transformers,
branch_ids: network.branch_ids,
bus_ids: network.bus_ids,
initially_open: network.initially_open,
bus_countries: network.bus_countries,
shunts: network.shunts,
tap_changers: network.tap_changers,
base_mva: network.base_mva,
};
let mut result = curative_search(
crac,
&view,
resolution,
&[state.clone()],
solver,
options,
&open,
);
for setpoint in &activated {
let Some((_, initial)) = before_decided.iter().find(|(a, _)| *a == setpoint.action)
else {
continue;
};
result.setpoints.push(Setpoint {
initial: *initial,
..(*setpoint).clone()
});
}
let in_force = result.open_branches.clone();
perimeters.push(PerimeterPlan {
states: vec![state],
network_actions: result.network_actions.clone(),
setpoints: result.setpoints.clone(),
initial_margin_mw: result.initial_margin_mw,
final_margin_mw: result.final_margin_mw,
leaves: result.leaves,
open_branches: in_force.clone(),
buses: result.buses.clone(),
transformers: result.transformers.clone(),
});
open = in_force;
transformers = result.transformers;
}
scenarios.push(ScenarioPlan {
contingency,
automatons,
perimeters,
});
}
scenarios
}
fn activated_by(preventive: &PerimeterPlan, scenarios: &[ScenarioPlan]) -> Vec<usize> {
let mut all: Vec<usize> = preventive.network_actions.clone();
for scenario in scenarios {
if let Some(automatons) = &scenario.automatons {
all.extend(&automatons.network_actions);
}
for perimeter in &scenario.perimeters {
all.extend(&perimeter.network_actions);
}
}
all.sort_unstable();
all
}
fn moved_by(
crac: &Crac,
preventive: &PerimeterPlan,
scenarios: &[ScenarioPlan],
) -> Vec<(usize, f64)> {
let tap_at = |action: usize, angle: f64| -> Option<i32> {
let RangeActionKind::Pst { tap_to_angle, .. } = &crac.range_actions.get(action)?.kind
else {
return None;
};
tap_to_angle
.iter()
.min_by(|a, b| (a.1 - angle).abs().total_cmp(&(b.1 - angle).abs()))
.map(|(t, _)| *t)
};
let take = |all: &mut Vec<(usize, f64)>, plan: &PerimeterPlan| {
for s in plan.setpoints.iter().filter(|s| s.moved()) {
let distance = match (s.tap, tap_at(s.action, s.initial)) {
(Some(now), Some(was)) => f64::from(now - was),
_ => s.value - s.initial,
};
all.push((s.action, distance));
}
};
let mut all: Vec<(usize, f64)> = Vec::new();
take(&mut all, preventive);
for scenario in scenarios {
if let Some(automatons) = &scenario.automatons {
for &(action, value, tap) in &automatons.range_actions {
let distance = match (tap, crac.range_actions.get(action).map(|r| &r.kind)) {
(Some(now), Some(RangeActionKind::Pst { initial_tap, .. })) => {
f64::from(now - initial_tap)
}
_ => value,
};
all.push((action, distance));
}
}
for perimeter in &scenario.perimeters {
take(&mut all, perimeter);
}
}
all.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.total_cmp(&b.1)));
all
}
fn perimeter_plan(result: &SearchResult, states: Vec<State>) -> PerimeterPlan {
PerimeterPlan {
states,
network_actions: result.network_actions.clone(),
setpoints: result.setpoints.clone(),
initial_margin_mw: result.initial_margin_mw,
final_margin_mw: result.final_margin_mw,
leaves: result.leaves,
open_branches: result.open_branches.clone(),
buses: result.buses.clone(),
transformers: result.transformers.clone(),
}
}
#[allow(clippy::too_many_arguments)]
fn second_preventive(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
solver: &mut dyn Solver,
options: &SearchOptions,
preventive: &SearchResult,
scenarios: &[ScenarioPlan],
cost_increased: bool,
) -> Option<SearchResult> {
if !options
.second_preventive
.runs(options, preventive, scenarios, cost_increased)
{
return None;
}
let mut opened: Vec<usize> = Vec::new();
let mut closed: Vec<usize> = Vec::new();
for scenario in scenarios {
let after = scenario
.perimeters
.last()
.map(|p| &p.open_branches)
.or(scenario.automatons.as_ref().map(|a| &a.open_branches));
let Some(after) = after else { continue };
opened.extend(
after
.iter()
.filter(|b| !preventive.open_branches.contains(b))
.copied(),
);
closed.extend(
preventive
.open_branches
.iter()
.filter(|b| !after.contains(b))
.copied(),
);
}
closed.retain(|b| !opened.contains(b));
opened.sort_unstable();
opened.dedup();
closed.sort_unstable();
closed.dedup();
let held = super::evaluate::Held {
open: opened,
close: closed,
taps: Vec::new(),
states: crac
.states()
.into_iter()
.filter(|s| {
matches!(
crac.instants[s.instant].kind,
InstantKind::Auto | InstantKind::Curative
)
})
.collect(),
};
let all = crac.states();
let preventive_states: Vec<State> = all
.iter()
.filter(|s| crac.instants[s.instant].kind == InstantKind::Preventive)
.cloned()
.collect();
let mut linear = options.linear.clone();
linear.held = Some(held);
linear.a_r_s = true;
let options = SearchOptions {
linear,
available_at: Some(preventive_states),
predefined_combinations: if options.second_preventive.hint {
let hint: Vec<String> = preventive
.network_actions
.iter()
.map(|&a| crac.network_actions[a].id.clone())
.collect();
let mut combos = options.predefined_combinations.clone();
if hint.len() > 1 {
combos.push(hint);
}
combos
} else {
options.predefined_combinations.clone()
},
..options.clone()
};
let result = search_with_open(
crac,
network,
resolution,
&all,
solver,
&options,
network.initially_open,
);
Some(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rao::costly::Costly;
fn pst_crac() -> Crac {
Crac {
range_actions: vec![crate::rao::crac::RangeAction {
id: "pstBeFr4".into(),
name: None,
operator: None,
speed: None,
activation_cost: Some(20.0),
variation_cost: Some(crate::rao::crac::VariationCost {
up: 15.0,
down: 15.0,
}),
group: None,
kind: RangeActionKind::Pst {
element: "BBE2AA1 BBE3AA1 1".into(),
initial_tap: 0,
tap_to_angle: (-16..=16).map(|t| (t, f64::from(t) * 0.5)).collect(),
},
ranges: Vec::new(),
usage_rules: Vec::new(),
}],
..Crac::default()
}
}
fn plan(setpoints: Vec<Setpoint>) -> PerimeterPlan {
PerimeterPlan {
states: Vec::new(),
network_actions: Vec::new(),
setpoints,
initial_margin_mw: 0.0,
final_margin_mw: 0.0,
leaves: 0,
open_branches: Vec::new(),
buses: Vec::new(),
transformers: Vec::new(),
}
}
#[test]
fn a_shifter_movement_is_measured_in_taps() {
let crac = pst_crac();
let preventive = plan(vec![Setpoint {
action: 0,
value: -2.5,
initial: 0.0,
tap: Some(-5),
}]);
assert_eq!(moved_by(&crac, &preventive, &[]), vec![(0, -5.0)]);
}
#[test]
fn moving_one_shifter_in_two_states_is_billed_twice() {
let crac = pst_crac();
let preventive = plan(vec![Setpoint {
action: 0,
value: -2.5,
initial: 0.0,
tap: Some(-5),
}]);
let scenario = ScenarioPlan {
contingency: 0,
automatons: None,
perimeters: vec![plan(vec![Setpoint {
action: 0,
value: -3.0,
initial: -2.5,
tap: Some(-6),
}])],
};
let moved = moved_by(&crac, &preventive, std::slice::from_ref(&scenario));
assert_eq!(moved, vec![(0, -5.0), (0, -1.0)]);
let costly = Costly::default();
assert_eq!(
costly.activation(&crac, &[], &moved),
2.0 * 20.0 + 6.0 * 15.0
);
let straight = plan(vec![Setpoint {
action: 0,
value: -3.0,
initial: 0.0,
tap: Some(-6),
}]);
let once = moved_by(&crac, &straight, &[]);
assert_eq!(costly.activation(&crac, &[], &once), 20.0 + 6.0 * 15.0);
}
#[test]
fn a_shifter_left_where_it_stood_costs_nothing() {
let crac = pst_crac();
let still = plan(vec![Setpoint {
action: 0,
value: 0.0,
initial: 0.0,
tap: Some(0),
}]);
assert!(moved_by(&crac, &still, &[]).is_empty());
}
#[test]
fn an_automaton_movement_is_measured_from_the_crac_s_initial_tap() {
let crac = pst_crac();
let scenario = ScenarioPlan {
contingency: 0,
automatons: Some(AutomatonResult {
network_actions: Vec::new(),
range_actions: vec![(0, -1.5, Some(-3))],
..AutomatonResult::default()
}),
perimeters: Vec::new(),
};
let moved = moved_by(&crac, &plan(Vec::new()), std::slice::from_ref(&scenario));
assert_eq!(moved, vec![(0, -3.0)]);
}
}