use crate::linear::DcOptions;
use crate::linear::btheta::dc_branches;
use crate::linear::sensitivity::DcSensitivity;
use crate::types::Transformer;
use super::crac::{Crac, ElementaryAction, InstantKind, RangeActionKind, State, UsageRule};
use super::evaluate::{CnecResult, FlowModel, Network, Resolution, evaluate_model};
use super::linear::{phase_shift_sensitivity, tap_table};
#[derive(Clone, Debug, Default)]
pub struct AutomatonResult {
pub network_actions: Vec<usize>,
pub range_actions: Vec<(usize, f64, Option<i32>)>,
pub open_branches: Vec<usize>,
pub transformers: Vec<Transformer>,
pub initial_margin_mw: f64,
pub final_margin_mw: f64,
pub batches: usize,
}
impl AutomatonResult {
pub fn fired(&self) -> usize {
self.network_actions.len() + self.range_actions.len()
}
}
const MAX_ROUNDS: usize = 20;
const MAX_SHIFTS: usize = 11;
const SENSI_UNDERESTIMATOR_STEP: f64 = 0.15;
const SENSI_UNDERESTIMATOR_MIN: f64 = 0.5;
const DEFAULT_SPEED: i64 = 0;
fn speed_of(speed: Option<i64>) -> i64 {
speed.unwrap_or(DEFAULT_SPEED)
}
pub fn simulate(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
contingency: usize,
open: &[usize],
transformers: &[Transformer],
model: FlowModel,
) -> Option<AutomatonResult> {
let instant = crac
.instants
.iter()
.position(|i| i.kind == InstantKind::Auto)?;
let state = State {
instant,
contingency: Some(contingency),
};
if !crac.flow_cnecs.iter().any(|c| c.state == state) {
return None;
}
let mut result = AutomatonResult {
open_branches: open.to_vec(),
transformers: transformers.to_vec(),
..Default::default()
};
result.initial_margin_mw = margin(
crac,
network,
resolution,
&state,
&result.open_branches,
&result.transformers,
model,
);
result.final_margin_mw = result.initial_margin_mw;
let anchors = super::linear::taps_now(
crac,
&Network {
transformers,
..*network
},
resolution,
);
let mut speeds: Vec<i64> = Vec::new();
for action in crac
.network_actions
.iter()
.filter(|a| covers(&a.usage_rules, &state))
{
speeds.push(speed_of(action.speed));
}
for action in crac
.range_actions
.iter()
.filter(|a| covers(&a.usage_rules, &state))
{
speeds.push(speed_of(action.speed));
}
speeds.sort_unstable();
speeds.dedup();
for speed in speeds {
result.batches += 1;
let snapshot = violated(
crac,
network,
resolution,
&state,
&result.open_branches,
&result.transformers,
model,
);
for (index, action) in crac.network_actions.iter().enumerate() {
if result.network_actions.contains(&index) {
continue;
}
if speed_of(action.speed) != speed {
continue;
}
if !triggered(&action.usage_rules, &state, &snapshot) {
continue;
}
if !apply_network_action(action, resolution, &mut result) {
continue;
}
result.network_actions.push(index);
}
let mut rounds = 0;
loop {
rounds += 1;
if rounds > MAX_ROUNDS {
break;
}
let violations = violated(
crac,
network,
resolution,
&state,
&result.open_branches,
&result.transformers,
model,
);
let mut moved = false;
for (index, action) in crac.range_actions.iter().enumerate() {
if result.range_actions.iter().any(|(i, _, _)| *i == index) {
continue;
}
if speed_of(action.speed) != speed {
continue;
}
if !triggered(&action.usage_rules, &state, &violations) {
continue;
}
let RangeActionKind::Pst {
element,
tap_to_angle,
initial_tap,
} = &action.kind
else {
continue;
};
let Some(branch) = resolution.branch(element) else {
continue;
};
let table = tap_table(
tap_to_angle,
network.tap_changers,
branch,
network.lines.len(),
);
let previous = anchors
.iter()
.find(|(i, _)| *i == index)
.map_or(*initial_tap, |(_, t)| *t);
let (low, high) = super::linear::tap_bounds(action, &table, *initial_tap, previous);
let table: Vec<(i32, f64)> = table
.into_iter()
.filter(|(_, a)| *a >= low - 1e-9 && *a <= high + 1e-9)
.collect();
let watched = watched_cnecs(&action.usage_rules, &state, &violations);
if watched.is_empty() {
continue;
}
let Some((tap, angle)) = shift_until_secure(
crac, network, resolution, &state, &result, branch, &table, &watched, model,
) else {
continue;
};
if let Some(i) = branch.checked_sub(network.lines.len()) {
apply_tap(network, &mut result.transformers, i, tap, angle);
}
result.range_actions.push((index, angle, Some(tap)));
moved = true;
break;
}
if !moved {
break;
}
}
}
result.final_margin_mw = margin(
crac,
network,
resolution,
&state,
&result.open_branches,
&result.transformers,
model,
);
Some(result)
}
fn covers(rules: &[UsageRule], state: &State) -> bool {
rules.iter().any(|r| r.covers(state))
}
fn triggered(rules: &[UsageRule], state: &State, violations: &[Violated]) -> bool {
let mut conditional = false;
for rule in rules.iter().filter(|r| r.covers(state)) {
match rule {
UsageRule::OnInstant { .. } | UsageRule::OnContingencyState { .. } => return true,
UsageRule::OnConstraint { cnec, .. } => {
conditional = true;
if violations.iter().any(|v| v.cnec == *cnec) {
return true;
}
}
UsageRule::OnFlowConstraintInCountry { country, .. } => {
conditional = true;
if violations
.iter()
.any(|v| v.countries.iter().any(|c| c.eq_ignore_ascii_case(country)))
{
return true;
}
}
}
}
let _ = conditional;
false
}
fn watched_cnecs(rules: &[UsageRule], state: &State, violations: &[Violated]) -> Vec<String> {
let mut named = Vec::new();
for rule in rules.iter().filter(|r| r.covers(state)) {
match rule {
UsageRule::OnConstraint { cnec, .. } => {
if violations.iter().any(|v| v.cnec == *cnec) {
named.push(cnec.clone());
}
}
UsageRule::OnFlowConstraintInCountry { country, .. } => {
named.extend(
violations
.iter()
.filter(|v| v.countries.iter().any(|c| c.eq_ignore_ascii_case(country)))
.map(|v| v.cnec.clone()),
);
}
_ => return violations.iter().map(|v| v.cnec.clone()).collect(),
}
}
named
}
fn apply_network_action(
action: &super::crac::NetworkAction,
resolution: &Resolution,
result: &mut AutomatonResult,
) -> bool {
let mut opened = Vec::new();
let mut closed = Vec::new();
for elementary in &action.elementary {
match elementary {
ElementaryAction::TerminalsConnection { element, connected } => {
let Some(branch) = resolution.branch(element) else {
return false;
};
if *connected {
closed.push(branch);
} else {
opened.push(branch);
}
}
ElementaryAction::Switch { element, open } => {
let Some(branch) = resolution.branch(element) else {
return false;
};
if *open {
opened.push(branch);
} else {
closed.push(branch);
}
}
_ => return false,
}
}
result.open_branches.extend(opened);
result.open_branches.retain(|b| !closed.contains(b));
result.open_branches.sort_unstable();
result.open_branches.dedup();
true
}
#[allow(clippy::too_many_arguments)]
fn shift_until_secure(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
state: &State,
result: &AutomatonResult,
branch: usize,
tap_to_angle: &[(i32, f64)],
watched: &[String],
model: FlowModel,
) -> Option<(i32, f64)> {
let start = current_tap(result, branch, network.lines.len(), tap_to_angle)?;
let angle_of = |tap: i32| {
tap_to_angle
.iter()
.find(|(t, _)| *t == tap)
.map(|(_, a)| *a)
};
let start_angle = angle_of(start)?;
let mut transformers = result.transformers.clone();
let (mut tap, mut angle) = (start, start_angle);
let mut direction = 0.0f64;
let mut hopeless: Vec<usize> = Vec::new();
let mut previous: Option<usize> = None;
let mut underestimator = 1.0f64;
for _ in 0..MAX_SHIFTS {
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 assessment = measure(crac, &view, resolution, &result.open_branches, model);
let perimeter = assessment.perimeters.iter().find(|p| p.state == *state)?;
let Some(worst) = perimeter
.cnecs
.iter()
.filter(|c| watched.iter().any(|v| *v == crac.flow_cnecs[c.cnec].id))
.filter(|c| !hopeless.contains(&c.cnec))
.filter(|c| c.is_violated())
.min_by(|a, b| overload(a, model).total_cmp(&overload(b, model)))
else {
break;
};
underestimator = if previous == Some(worst.cnec) {
(underestimator - SENSI_UNDERESTIMATOR_STEP).max(SENSI_UNDERESTIMATOR_MIN)
} else {
1.0
};
previous = Some(worst.cnec);
let options = DcOptions::default();
let branches = dc_branches(network.lines, &transformers, options);
let Some(sensitivity) = DcSensitivity::new(
network.buses,
&branches,
network.lines.len() + transformers.len(),
) else {
break;
};
let Some(column) = phase_shift_sensitivity(&sensitivity, &branches, branch) else {
break;
};
let sigma = -column.get(worst.branch).copied().unwrap_or(0.0)
* network.base_mva
* std::f64::consts::PI
/ 180.0
* underestimator;
if sigma.abs() < 1e-6 {
hopeless.push(worst.cnec);
previous = None;
continue;
}
let needed = worst.flow_mw.signum() * worst.margin_mw.min(0.0) / sigma;
let target = angle + needed;
let Some(next) = round_away(tap_to_angle, angle, target) else {
break;
};
let step = (next.1 - angle).signum() * f64::from(u8::from((next.1 - angle).abs() > 1e-9));
if step == 0.0 || (direction != 0.0 && step != direction) {
break;
}
direction = step;
if let Some(i) = branch.checked_sub(network.lines.len()) {
apply_tap(network, &mut transformers, i, next.0, next.1);
}
(tap, angle) = next;
}
if tap == start {
return None;
}
Some(smallest_securing(
crac,
network,
resolution,
state,
result,
branch,
tap_to_angle,
watched,
&hopeless,
model,
start,
tap,
))
}
#[allow(clippy::too_many_arguments)]
fn smallest_securing(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
state: &State,
result: &AutomatonResult,
branch: usize,
tap_to_angle: &[(i32, f64)],
watched: &[String],
hopeless: &[usize],
model: FlowModel,
start: i32,
reached: i32,
) -> (i32, f64) {
let angle_of = |tap: i32| {
tap_to_angle
.iter()
.find(|(t, _)| *t == tap)
.map(|(_, a)| *a)
};
let reached_angle = angle_of(reached).unwrap_or_default();
let Some(i) = branch.checked_sub(network.lines.len()) else {
return (reached, reached_angle);
};
let mut between: Vec<i32> = tap_to_angle
.iter()
.map(|(t, _)| *t)
.filter(|t| {
if reached > start {
*t > start && *t < reached
} else {
*t < start && *t > reached
}
})
.collect();
between.sort_by_key(|t| (t - start).abs());
for tap in between {
let Some(angle) = angle_of(tap) else { continue };
let mut transformers = result.transformers.clone();
apply_tap(network, &mut transformers, i, tap, angle);
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 secure = measure(crac, &view, resolution, &result.open_branches, model)
.perimeters
.iter()
.find(|p| p.state == *state)
.is_some_and(|p| {
p.cnecs
.iter()
.filter(|c| watched.iter().any(|v| *v == crac.flow_cnecs[c.cnec].id))
.filter(|c| !hopeless.contains(&c.cnec))
.all(|c| !c.is_violated())
});
if secure {
return (tap, angle);
}
}
(reached, reached_angle)
}
fn overload(cnec: &CnecResult, model: FlowModel) -> f64 {
match model {
FlowModel::Ac => cnec.margin_a,
FlowModel::Dc => cnec.margin_mw,
}
}
fn round_away(tap_to_angle: &[(i32, f64)], from: f64, target: f64) -> Option<(i32, f64)> {
let forward = target >= from;
let onward = tap_to_angle
.iter()
.filter(|(_, a)| if forward { *a >= from } else { *a <= from });
onward
.clone()
.filter(|(_, a)| if forward { *a >= target } else { *a <= target })
.min_by(|a, b| (a.1 - from).abs().total_cmp(&(b.1 - from).abs()))
.or_else(|| onward.max_by(|a, b| (a.1 - from).abs().total_cmp(&(b.1 - from).abs())))
.copied()
}
fn apply_tap(
network: &Network<'_>,
transformers: &mut [Transformer],
index: usize,
tap: i32,
angle: f64,
) {
let from_network = network
.tap_changers
.get(index)
.and_then(|c| c.as_ref())
.and_then(|c| c.at(tap));
if let Some(t) = transformers.get_mut(index) {
let ratio = t.tap.norm();
t.tap = match from_network {
Some(step) => num_complex::Complex::from_polar(ratio, step.arg()),
None => num_complex::Complex::from_polar(ratio, (-angle).to_radians()),
};
}
}
fn measure(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
open: &[usize],
model: FlowModel,
) -> super::evaluate::SecurityResult {
let ac = super::evaluate::ac_options(network);
evaluate_model(crac, network, resolution, open, model, &ac)
}
fn current_tap(
result: &AutomatonResult,
branch: usize,
lines: usize,
tap_to_angle: &[(i32, f64)],
) -> Option<i32> {
let live = branch
.checked_sub(lines)
.and_then(|i| result.transformers.get(i))
.map(|t| -t.tap.arg().to_degrees())
.unwrap_or(0.0);
tap_to_angle
.iter()
.min_by(|a, b| (a.1 - live).abs().total_cmp(&(b.1 - live).abs()))
.map(|(t, _)| *t)
}
fn violated(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
state: &State,
open: &[usize],
transformers: &[Transformer],
model: FlowModel,
) -> Vec<Violated> {
let view = Network {
generation: network.generation,
buses: network.buses,
lines: network.lines,
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 branches = crate::linear::btheta::dc_branches(
view.lines,
view.transformers,
crate::linear::DcOptions::default(),
);
let countries = super::usage::branch_countries(crac, &view, resolution, &branches);
measure(crac, &view, resolution, open, model)
.perimeters
.iter()
.filter(|p| p.state == *state)
.flat_map(|p| p.violations())
.map(|c| Violated {
cnec: crac.flow_cnecs[c.cnec].id.clone(),
countries: countries.get(&c.branch).cloned().unwrap_or_default(),
})
.collect()
}
#[derive(Clone, Debug)]
struct Violated {
cnec: String,
countries: Vec<String>,
}
fn margin(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
state: &State,
open: &[usize],
transformers: &[Transformer],
model: FlowModel,
) -> f64 {
let view = Network {
generation: network.generation,
buses: network.buses,
lines: network.lines,
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,
};
measure(crac, &view, resolution, open, model)
.perimeters
.iter()
.find(|p| p.state == *state)
.and_then(|p| p.min_margin())
.unwrap_or(f64::INFINITY)
}
#[cfg(test)]
mod country_tests {
use super::*;
use crate::rao::crac::UsageRule;
fn violated(cnec: &str, countries: &[&str]) -> Violated {
Violated {
cnec: cnec.into(),
countries: countries.iter().map(|c| (*c).to_string()).collect(),
}
}
fn in_country(country: &str) -> UsageRule {
UsageRule::OnFlowConstraintInCountry {
instant: 0,
country: country.into(),
contingency: None,
}
}
const AUTO: State = State {
instant: 0,
contingency: Some(0),
};
#[test]
fn a_country_scoped_automaton_ignores_an_overload_elsewhere() {
let rules = [in_country("BE")];
let elsewhere = [violated("fr-cnec", &["FR"])];
assert!(
!triggered(&rules, &AUTO, &elsewhere),
"an overload in France must not operate equipment scoped to Belgium"
);
let here = [violated("be-cnec", &["BE"])];
assert!(
triggered(&rules, &AUTO, &here),
"an overload in Belgium must"
);
}
#[test]
fn a_tie_line_counts_for_both_countries() {
let tie = [violated("be-fr", &["BE", "FR"])];
assert!(triggered(&[in_country("BE")], &AUTO, &tie));
assert!(triggered(&[in_country("FR")], &AUTO, &tie));
assert!(!triggered(&[in_country("DE")], &AUTO, &tie));
}
#[test]
fn what_a_country_scoped_rule_watches_is_scoped_too() {
let rules = [in_country("BE")];
let violations = [violated("be-cnec", &["BE"]), violated("fr-cnec", &["FR"])];
assert_eq!(
watched_cnecs(&rules, &AUTO, &violations),
vec!["be-cnec".to_string()]
);
}
}