use crate::opf::Solver;
use super::crac::{Crac, ElementaryAction, NetworkAction, State};
use super::evaluate::{Network, Resolution};
use super::limits::Limits;
use super::linear::{LinearOptions, NetworkMut, Setpoint, optimize};
use super::usage::Constrained;
#[derive(Clone, Debug)]
pub struct SearchOptions {
pub max_depth: usize,
pub curative_max_depth: Option<usize>,
pub second_preventive: SecondPreventive,
pub available_at: Option<Vec<State>>,
pub predefined_combinations: Vec<Vec<String>>,
pub absolute_min_impact: f64,
pub relative_min_impact: f64,
pub linear: LinearOptions,
pub curative_min_obj_improvement: f64,
pub enforce_curative_security: bool,
pub skip_far_actions: Option<usize>,
pub stop_at_target: Option<f64>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SecondPreventive {
pub condition: SecondPreventiveCondition,
pub hint: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SecondPreventiveCondition {
#[default]
Disabled,
CostIncrease,
PossibleCurativeImprovement,
}
impl SecondPreventive {
pub fn runs(
&self,
options: &SearchOptions,
preventive: &SearchResult,
scenarios: &[super::castor::ScenarioPlan],
cost_increased: bool,
) -> bool {
match self.condition {
SecondPreventiveCondition::Disabled => false,
SecondPreventiveCondition::PossibleCurativeImprovement => {
let target = options
.stop_at_target
.unwrap_or(preventive.final_objective + options.curative_min_obj_improvement);
scenarios
.iter()
.flat_map(|s| s.perimeters.iter())
.any(|p| p.final_margin_mw < target - 1e-6)
}
SecondPreventiveCondition::CostIncrease => cost_increased,
}
}
}
impl Default for SearchOptions {
fn default() -> Self {
Self {
max_depth: 2,
curative_max_depth: None,
second_preventive: SecondPreventive::default(),
available_at: None,
predefined_combinations: Vec::new(),
absolute_min_impact: 0.0,
relative_min_impact: 0.0,
linear: LinearOptions::default(),
curative_min_obj_improvement: 0.0,
enforce_curative_security: false,
skip_far_actions: None,
stop_at_target: None,
}
}
}
#[derive(Clone, Debug)]
pub struct SearchResult {
pub network_actions: Vec<usize>,
pub setpoints: Vec<Setpoint>,
pub held_setpoints: Vec<Setpoint>,
pub initial_margin_mw: f64,
pub final_margin_mw: f64,
pub final_objective: f64,
pub leaves: usize,
pub depth: usize,
pub open_branches: Vec<usize>,
pub buses: Vec<crate::types::Bus>,
pub transformers: Vec<crate::types::Transformer>,
}
impl SearchResult {
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
}
}
struct Effect {
open: Vec<usize>,
close: Vec<usize>,
taps: Vec<(usize, i32, f64)>,
}
fn effect_of(
action: &NetworkAction,
crac: &Crac,
resolution: &Resolution,
tap_changers: &[Option<crate::types::TapChanger>],
lines: usize,
) -> Option<Effect> {
let mut effect = Effect {
open: Vec::new(),
close: Vec::new(),
taps: Vec::new(),
};
for elementary in &action.elementary {
match elementary {
ElementaryAction::TerminalsConnection { element, connected } => {
let branch = resolution.branch(element)?;
if *connected {
effect.close.push(branch);
} else {
effect.open.push(branch);
}
}
ElementaryAction::Switch { element, open } => {
let branch = resolution.branch(element)?;
if *open {
effect.open.push(branch);
} else {
effect.close.push(branch);
}
}
ElementaryAction::PstTapPosition { element, tap } => {
let branch = resolution.branch(element)?;
if branch < lines {
return None;
}
let declared: Vec<(i32, f64)> = crac
.range_actions
.iter()
.find_map(|r| match &r.kind {
super::crac::RangeActionKind::Pst {
element: e,
tap_to_angle,
..
} if e == element => Some(tap_to_angle.clone()),
_ => None,
})
.unwrap_or_default();
let table = super::linear::tap_table(&declared, tap_changers, branch, lines);
let angle = table.iter().find(|(t, _)| t == tap).map(|(_, a)| *a)?;
effect.taps.push((branch, *tap, angle));
}
ElementaryAction::GeneratorSetpoint { .. }
| ElementaryAction::LoadSetpoint { .. }
| ElementaryAction::ShuntSection { .. }
| ElementaryAction::SwitchPair { .. } => return None,
}
}
(!effect.open.is_empty() || !effect.close.is_empty() || !effect.taps.is_empty())
.then_some(effect)
}
struct Candidate {
actions: Vec<usize>,
effect: Effect,
key: String,
}
fn compatible(a: &NetworkAction, b: &NetworkAction) -> bool {
let mine: Vec<&str> = a
.elementary
.iter()
.flat_map(ElementaryAction::elements)
.collect();
!b.elementary
.iter()
.flat_map(ElementaryAction::elements)
.any(|e| mine.contains(&e))
}
pub fn search(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
perimeter: &[State],
solver: &mut dyn Solver,
options: &SearchOptions,
) -> SearchResult {
search_with_open(
crac,
network,
resolution,
perimeter,
solver,
options,
network.initially_open,
)
}
pub fn search_with_open(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
perimeter: &[State],
solver: &mut dyn Solver,
options: &SearchOptions,
already_open: &[usize],
) -> SearchResult {
let base_open: Vec<usize> = already_open.to_vec();
let starting = measure_with(
crac,
network,
resolution,
&base_open,
options.linear.flow_model,
options.linear.held.as_ref(),
);
let branches = crate::linear::btheta::dc_branches(
network.lines,
network.transformers,
crate::linear::DcOptions::default(),
);
let constrained = Constrained::from_result(
&starting,
options.linear.objective_unit,
&super::usage::branch_countries(crac, network, resolution, &branches),
);
let mut linear = options.linear.clone();
linear.available = constrained.clone();
linear.available_at = options.available_at.clone();
linear.previous_taps = super::linear::taps_now(crac, network, resolution);
linear.activated = Vec::new();
let options = &SearchOptions {
linear,
..options.clone()
};
let available_at = options.available_at.as_deref().unwrap_or(perimeter);
let offered = |index: usize| -> Option<Effect> {
let action = crac.network_actions.get(index)?;
constrained
.allows(&action.usage_rules, available_at, crac)
.then(|| {
effect_of(
action,
crac,
resolution,
network.tap_changers,
network.lines.len(),
)
})
.flatten()
};
let mut available: Vec<Candidate> = (0..crac.network_actions.len())
.filter_map(|index| {
Some(Candidate {
actions: vec![index],
effect: offered(index)?,
key: crac.network_actions[index].id.clone(),
})
})
.collect();
for combination in &options.predefined_combinations {
let members: Option<Vec<usize>> = combination
.iter()
.map(|id| crac.network_actions.iter().position(|a| a.id == *id))
.collect();
let Some(members) = members else { continue };
if members.len() < 2 {
continue;
}
let pairwise_ok = members.iter().enumerate().all(|(i, &a)| {
members[i + 1..]
.iter()
.all(|&b| compatible(&crac.network_actions[a], &crac.network_actions[b]))
});
if !pairwise_ok {
continue;
}
let mut effect = Effect {
open: Vec::new(),
close: Vec::new(),
taps: Vec::new(),
};
let mut expressible = true;
for &index in &members {
match offered(index) {
Some(part) => {
effect.open.extend(part.open);
effect.close.extend(part.close);
effect.taps.extend(part.taps);
}
None => expressible = false,
}
}
if !expressible {
continue;
}
let key = members
.iter()
.map(|&i| crac.network_actions[i].id.as_str())
.collect::<Vec<_>>()
.join("+");
available.push(Candidate {
actions: members,
effect,
key,
});
}
let available = match options.skip_far_actions {
Some(max) => near_most_limiting(
crac,
network,
resolution,
perimeter,
&base_open,
available,
max,
options.linear.flow_model,
options.linear.held.as_ref(),
),
None => available,
};
let limits = available_at
.iter()
.map(|s| s.instant)
.min()
.map_or_else(Limits::unlimited, |i| Limits::of_instant(crac, i));
let budgeted = |chosen: &[usize]| LinearOptions {
limits: limits.remaining(crac, chosen),
activated: chosen.to_vec(),
..options.linear.clone()
};
let initial = worst_of(crac, &starting, perimeter);
let untouched = objective_of(crac, &starting, perimeter, &[], &[], &options.linear);
let reached = |objective: f64| options.stop_at_target.is_some_and(|t| objective >= t);
if reached(untouched) {
return SearchResult {
network_actions: Vec::new(),
setpoints: Vec::new(),
held_setpoints: Vec::new(),
initial_margin_mw: initial,
final_margin_mw: initial,
final_objective: untouched,
leaves: 0,
depth: 0,
open_branches: base_open,
buses: network.buses.to_vec(),
transformers: network.transformers.to_vec(),
};
}
let root = leaf(
crac,
network,
resolution,
perimeter,
solver,
&budgeted(&[]),
Applied {
open: &base_open,
taps: &[],
},
);
let mut chosen: Vec<usize> = Vec::new();
let mut best = root;
let mut leaves = 0usize;
let mut depth = 0usize;
for _ in 0..options.max_depth {
if reached(best.objective) {
break;
}
let mut open_here: Vec<usize> = base_open.clone();
let mut taps_here: Vec<(usize, i32, f64)> = Vec::new();
for &index in &chosen {
if let Some(effect) = effect_of_chosen(&available, index) {
open_here.extend(&effect.open);
open_here.retain(|b| !effect.close.contains(b));
taps_here.extend(&effect.taps);
}
}
let mut winner: Option<(&Candidate, Evaluated)> = None;
for offer in &available {
if offer.actions.iter().any(|i| chosen.contains(i)) {
continue;
}
if offer.actions.iter().any(|&index| {
chosen
.iter()
.any(|&c| !compatible(&crac.network_actions[c], &crac.network_actions[index]))
}) {
continue;
}
let mut with_candidate = chosen.clone();
with_candidate.extend(&offer.actions);
if !limits.admits(crac, &with_candidate) {
continue;
}
let mut open = open_here.clone();
open.extend(&offer.effect.open);
open.retain(|b| !offer.effect.close.contains(b));
let mut taps = taps_here.clone();
taps.extend(&offer.effect.taps);
let candidate = leaf(
crac,
network,
resolution,
perimeter,
solver,
&budgeted(&with_candidate),
Applied {
open: &open,
taps: &taps,
},
);
leaves += 1;
let better = match &winner {
None => true,
Some((previous, evaluated)) => {
candidate.objective > evaluated.objective + 1e-12
|| ((candidate.objective - evaluated.objective).abs() <= 1e-12
&& offer.key < previous.key)
}
};
if better {
winner = Some((offer, candidate));
}
}
let Some((offer, candidate)) = winner else {
break;
};
let enough = improved_enough(best.objective, candidate.objective, options)
|| (candidate.objective > best.objective && reached(candidate.objective));
if !enough {
break;
}
chosen.extend(&offer.actions);
best = candidate;
depth += 1;
}
let mut open_branches: Vec<usize> = base_open.clone();
for &index in &chosen {
if let Some(effect) = effect_of_chosen(&available, index) {
open_branches.extend(&effect.open);
open_branches.retain(|b| !effect.close.contains(b));
}
}
open_branches.sort_unstable();
open_branches.dedup();
SearchResult {
network_actions: chosen,
setpoints: best.setpoints,
held_setpoints: best.held_setpoints,
initial_margin_mw: initial,
final_margin_mw: best.margin_mw,
final_objective: best.objective,
leaves,
depth,
open_branches,
transformers: best.transformers,
buses: best.buses,
}
}
fn effect_of_chosen(available: &[Candidate], index: usize) -> Option<&Effect> {
available
.iter()
.find(|c| c.actions == [index])
.or_else(|| available.iter().find(|c| c.actions.contains(&index)))
.map(|c| &c.effect)
}
fn improved_enough(current: f64, candidate: f64, options: &SearchOptions) -> bool {
if candidate <= current + options.absolute_min_impact {
return false;
}
if options.linear.objective_kind.costly().is_some() {
return true;
}
if options.relative_min_impact > 0.0 {
let scale = current.abs().max(1.0);
if candidate - current < options.relative_min_impact * scale {
return false;
}
}
true
}
fn leaf(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
perimeter: &[State],
solver: &mut dyn Solver,
options: &LinearOptions,
applied: Applied<'_>,
) -> Evaluated {
let mut transformers = network.transformers.to_vec();
apply_taps(
&mut transformers,
network.tap_changers,
network.lines.len(),
applied.taps,
);
let mut buses = network.buses.to_vec();
let mut mutable = NetworkMut {
generation: network.generation,
buses: &mut buses,
lines: network.lines,
transformers: &mut transformers,
branch_ids: network.branch_ids,
bus_ids: network.bus_ids,
initially_open: applied.open,
bus_countries: network.bus_countries,
shunts: network.shunts,
tap_changers: network.tap_changers,
base_mva: network.base_mva,
};
let (objective, margin_mw, setpoints, held_setpoints) = optimize_with_open(
crac,
&mut mutable,
resolution,
perimeter,
solver,
options,
applied.open,
);
Evaluated {
objective,
margin_mw,
setpoints,
held_setpoints,
transformers,
buses,
}
}
struct Evaluated {
objective: f64,
margin_mw: f64,
setpoints: Vec<Setpoint>,
held_setpoints: Vec<Setpoint>,
transformers: Vec<crate::types::Transformer>,
buses: Vec<crate::types::Bus>,
}
#[derive(Clone, Copy)]
struct Applied<'a> {
open: &'a [usize],
taps: &'a [(usize, i32, f64)],
}
pub(super) fn apply_taps(
transformers: &mut [crate::types::Transformer],
tap_changers: &[Option<crate::types::TapChanger>],
lines: usize,
taps: &[(usize, i32, f64)],
) {
for &(branch, tap, angle_deg) in taps {
let Some(i) = branch.checked_sub(lines) else {
continue;
};
let from_network = tap_changers
.get(i)
.and_then(|c| c.as_ref())
.and_then(|c| c.at(tap));
if let Some(transformer) = transformers.get_mut(i) {
let ratio = transformer.tap.norm();
transformer.tap = match from_network {
Some(step) => num_complex::Complex::from_polar(ratio, step.arg()),
None => num_complex::Complex::from_polar(ratio, angle_deg.to_radians()),
};
}
}
}
fn optimize_with_open(
crac: &Crac,
network: &mut NetworkMut<'_>,
resolution: &Resolution,
perimeter: &[State],
solver: &mut dyn Solver,
options: &LinearOptions,
open: &[usize],
) -> (f64, f64, Vec<Setpoint>, Vec<Setpoint>) {
if open.is_empty() {
let result = optimize(crac, network, resolution, perimeter, solver, options);
return (
result.final_objective,
result.final_margin_mw,
result.setpoints,
result.held_setpoints,
);
}
let split = options.held.as_ref().is_some_and(|h| !h.states.is_empty());
let mut lines = network.lines.to_vec();
let mut transformers = network.transformers.clone();
for &branch in if split { &[][..] } else { open } {
if branch < lines.len() {
lines[branch].r = crate::topology::reduction::OPEN_BRANCH_Z;
lines[branch].x = crate::topology::reduction::OPEN_BRANCH_Z;
lines[branch].b_shunt = 0.0;
lines[branch].g_shunt = 0.0;
} else if let Some(t) = transformers.get_mut(branch - lines.len()) {
t.from_status = 0;
t.to_status = 0;
}
}
let mut inner = NetworkMut {
buses: network.buses,
generation: network.generation,
lines: &lines,
transformers: &mut transformers,
branch_ids: network.branch_ids,
bus_ids: network.bus_ids,
initially_open: open,
bus_countries: network.bus_countries,
shunts: network.shunts,
tap_changers: network.tap_changers,
base_mva: network.base_mva,
};
let result = optimize(crac, &mut inner, resolution, perimeter, solver, options);
for (a, b) in network.transformers.iter_mut().zip(transformers.iter()) {
a.tap = b.tap;
}
(
result.final_objective,
result.final_margin_mw,
result.setpoints,
result.held_setpoints,
)
}
fn measure_with(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
open: &[usize],
model: super::evaluate::FlowModel,
held: Option<&super::evaluate::Held>,
) -> super::evaluate::SecurityResult {
let ac = super::evaluate::ac_options(network);
super::evaluate::evaluate_split(crac, network, resolution, open, held, model, &ac)
}
pub(super) fn objective_of(
crac: &Crac,
result: &super::evaluate::SecurityResult,
perimeter: &[State],
activated: &[usize],
moved: &[(usize, f64)],
options: &LinearOptions,
) -> f64 {
let unit = options.objective_unit;
let score = match options.objective_kind.costly() {
Some(costly) => -costly.cost(crac, result, perimeter, unit, activated, moved),
None => {
let margin = result
.perimeters
.iter()
.filter(|p| perimeter.contains(&p.state))
.flat_map(|p| p.cnecs.iter())
.filter(|c| crac.flow_cnecs[c.cnec].optimized)
.map(|c| match unit {
super::linear::ObjectiveUnit::Megawatt => c.margin_mw,
super::linear::ObjectiveUnit::Ampere => c.margin_a,
})
.fold(f64::INFINITY, f64::min);
if margin.is_finite() {
margin
} else {
super::mnec::NO_CNEC_MARGIN
}
}
};
score - options.mnec.cost(crac, result, perimeter, unit)
}
fn worst_of(crac: &Crac, result: &super::evaluate::SecurityResult, perimeter: &[State]) -> f64 {
result
.perimeters
.iter()
.filter(|p| perimeter.contains(&p.state))
.filter_map(|p| p.min_optimized_margin(crac))
.fold(f64::INFINITY, f64::min)
}
fn branch_countries(
network: &Network<'_>,
branch: usize,
dc: &[crate::linear::btheta::DcBranch],
) -> Vec<String> {
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()
}
fn country_boundaries(
network: &Network<'_>,
dc: &[crate::linear::btheta::DcBranch],
) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = Vec::new();
for b in dc {
let (Some(Some(x)), Some(Some(y))) = (
network.bus_countries.get(b.from),
network.bus_countries.get(b.to),
) else {
continue;
};
if x == y {
continue;
}
let pair = if x < y {
(x.clone(), y.clone())
} else {
(y.clone(), x.clone())
};
if !out.contains(&pair) {
out.push(pair);
}
}
out
}
fn within_boundaries(
a: &str,
b: &str,
boundaries: &[(String, String)],
max_boundaries: usize,
) -> bool {
if a == b {
return true;
}
let mut seen: Vec<&str> = vec![a];
let mut frontier: Vec<&str> = vec![a];
for _ in 0..max_boundaries {
let mut next: Vec<&str> = Vec::new();
for country in &frontier {
for (x, y) in boundaries {
let other = if x == country {
y.as_str()
} else if y == country {
x.as_str()
} else {
continue;
};
if other == b {
return true;
}
if !seen.contains(&other) {
seen.push(other);
next.push(other);
}
}
}
if next.is_empty() {
break;
}
frontier = next;
}
false
}
fn near_most_limiting(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
perimeter: &[State],
open: &[usize],
available: Vec<Candidate>,
max_boundaries: usize,
model: super::evaluate::FlowModel,
held: Option<&super::evaluate::Held>,
) -> Vec<Candidate> {
if network.bus_countries.is_empty() {
return available;
}
let dc = crate::linear::btheta::dc_branches(
network.lines,
network.transformers,
crate::linear::DcOptions::default(),
);
let evaluated = measure_with(crac, network, resolution, open, model, held);
let worst = evaluated
.perimeters
.iter()
.filter(|p| perimeter.contains(&p.state))
.flat_map(|p| p.cnecs.iter())
.min_by(|a, b| a.margin_mw.total_cmp(&b.margin_mw));
let Some(worst) = worst else { return available };
let here = branch_countries(network, worst.branch, &dc);
if here.is_empty() {
return available;
}
let boundaries = country_boundaries(network, &dc);
available
.into_iter()
.filter(|candidate| {
let effect = &candidate.effect;
let mut touched: Vec<String> = Vec::new();
for &branch in effect.open.iter().chain(&effect.close) {
touched.extend(branch_countries(network, branch, &dc));
}
for &(branch, _, _) in &effect.taps {
touched.extend(branch_countries(network, branch, &dc));
}
if touched.is_empty() {
return true;
}
touched.iter().any(|t| {
here.iter()
.any(|h| within_boundaries(h, t, &boundaries, max_boundaries))
})
})
.collect()
}