use std::collections::HashMap;
use super::crac::{Crac, RaUsageLimits};
const UNLIMITED: usize = usize::MAX;
fn cap(value: Option<usize>) -> usize {
match value {
Some(v) if v >= i32::MAX as usize => UNLIMITED,
Some(v) => v,
None => UNLIMITED,
}
}
fn spend(limit: usize, used: usize) -> usize {
if limit == UNLIMITED {
UNLIMITED
} else {
limit.saturating_sub(used)
}
}
fn caps(map: &HashMap<String, usize>) -> HashMap<String, usize> {
map.iter()
.map(|(k, &v)| (k.clone(), cap(Some(v))))
.collect()
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Limits {
pub max_ra: usize,
pub max_topo_per_tso: HashMap<String, usize>,
pub max_pst_per_tso: HashMap<String, usize>,
pub max_ra_per_tso: HashMap<String, usize>,
pub max_elementary_per_tso: HashMap<String, usize>,
}
impl Limits {
pub fn unlimited() -> Self {
Self {
max_ra: UNLIMITED,
..Default::default()
}
}
pub fn of_instant(crac: &Crac, instant: usize) -> Self {
crac.usage_limits
.iter()
.find(|l| l.instant == instant)
.map_or_else(Self::unlimited, Self::from_crac)
}
fn from_crac(limits: &RaUsageLimits) -> Self {
Self {
max_ra: cap(limits.max_ra),
max_topo_per_tso: caps(&limits.max_topo_per_tso),
max_pst_per_tso: caps(&limits.max_pst_per_tso),
max_ra_per_tso: caps(&limits.max_ra_per_tso),
max_elementary_per_tso: caps(&limits.max_elementary_actions_per_tso),
}
}
pub fn is_unlimited(&self) -> bool {
self.max_ra == UNLIMITED
&& self.max_topo_per_tso.is_empty()
&& self.max_pst_per_tso.is_empty()
&& self.max_ra_per_tso.is_empty()
&& self.max_elementary_per_tso.is_empty()
}
pub fn admits(&self, crac: &Crac, chosen: &[usize]) -> bool {
if chosen.len() > self.max_ra {
return false;
}
let tsos: Vec<&str> = operators(crac, chosen);
for (tso, &limit) in self.max_topo_per_tso.iter().chain(&self.max_ra_per_tso) {
if tsos.iter().filter(|t| **t == tso).count() > limit {
return false;
}
}
for (tso, &limit) in &self.max_elementary_per_tso {
let used: usize = chosen
.iter()
.filter(|&&i| crac.network_actions[i].operator.as_deref() == Some(tso.as_str()))
.map(|&i| crac.network_actions[i].elementary.len())
.sum();
if used > limit {
return false;
}
}
true
}
pub fn remaining(&self, crac: &Crac, chosen: &[usize]) -> Budget {
let tsos = operators(crac, chosen);
let spent = |tso: &str| tsos.iter().filter(|t| **t == tso).count();
Budget {
max_ra: spend(self.max_ra, chosen.len()),
max_pst_per_tso: self.max_pst_per_tso.clone(),
max_ra_per_tso: self
.max_ra_per_tso
.iter()
.map(|(tso, &limit)| (tso.clone(), spend(limit, spent(tso))))
.collect(),
}
}
}
fn operators<'a>(crac: &'a Crac, chosen: &[usize]) -> Vec<&'a str> {
chosen
.iter()
.filter_map(|&i| crac.network_actions[i].operator.as_deref())
.collect()
}
#[derive(Clone, Debug, PartialEq)]
pub struct Budget {
pub max_ra: usize,
pub max_pst_per_tso: HashMap<String, usize>,
pub max_ra_per_tso: HashMap<String, usize>,
}
impl Default for Budget {
fn default() -> Self {
Self {
max_ra: UNLIMITED,
max_pst_per_tso: HashMap::new(),
max_ra_per_tso: HashMap::new(),
}
}
}
impl Budget {
pub fn is_unlimited(&self) -> bool {
self.max_ra == UNLIMITED
&& self.max_pst_per_tso.is_empty()
&& self.max_ra_per_tso.is_empty()
}
pub fn admits(&self, crac: &Crac, moved: &[usize]) -> bool {
if moved.len() > self.max_ra {
return false;
}
let is = |i: usize, tso: &str| crac.range_actions[i].operator.as_deref() == Some(tso);
for (tso, &limit) in &self.max_ra_per_tso {
if moved.iter().filter(|&&i| is(i, tso)).count() > limit {
return false;
}
}
for (tso, &limit) in &self.max_pst_per_tso {
let psts = moved
.iter()
.filter(|&&i| is(i, tso))
.filter(|&&i| {
matches!(
crac.range_actions[i].kind,
super::crac::RangeActionKind::Pst { .. }
)
})
.count();
if psts > limit {
return false;
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rao::crac::{
ElementaryAction, NetworkAction, Range, RangeAction, RangeActionKind, RangeKind,
};
fn network_action(id: &str, tso: &str, elementary: usize) -> NetworkAction {
NetworkAction {
id: id.into(),
name: None,
operator: Some(tso.into()),
speed: None,
activation_cost: None,
elementary: (0..elementary)
.map(|i| ElementaryAction::Switch {
element: format!("{id}-{i}"),
open: true,
})
.collect(),
usage_rules: Vec::new(),
}
}
fn pst(id: &str, tso: &str) -> RangeAction {
RangeAction {
id: id.into(),
name: None,
operator: Some(tso.into()),
speed: None,
activation_cost: None,
variation_cost: None,
group: None,
kind: RangeActionKind::Pst {
element: id.into(),
initial_tap: 0,
tap_to_angle: Vec::new(),
},
ranges: vec![Range {
kind: RangeKind::Absolute,
min: None,
max: None,
}],
usage_rules: Vec::new(),
}
}
fn crac() -> Crac {
Crac {
network_actions: vec![
network_action("be-topo-1", "be", 1),
network_action("be-topo-2", "be", 3),
network_action("fr-topo", "fr", 1),
],
range_actions: vec![
pst("be-pst-1", "be"),
pst("be-pst-2", "be"),
pst("fr-pst", "fr"),
],
..Default::default()
}
}
fn limits() -> Limits {
Limits {
max_ra: 3,
max_topo_per_tso: [("be".into(), 1)].into_iter().collect(),
max_pst_per_tso: [("be".into(), 1)].into_iter().collect(),
max_ra_per_tso: [("be".into(), 2)].into_iter().collect(),
max_elementary_per_tso: HashMap::new(),
}
}
#[test]
fn a_per_tso_topology_cap_stops_the_second_action_from_that_tso() {
let crac = crac();
let limits = limits();
assert!(limits.admits(&crac, &[0]));
assert!(
!limits.admits(&crac, &[0, 1]),
"two BE topological actions, cap is one"
);
assert!(limits.admits(&crac, &[0, 2]));
}
#[test]
fn the_total_cap_counts_every_operator_together() {
let crac = crac();
let limits = Limits {
max_ra: 1,
..Limits::unlimited()
};
assert!(limits.admits(&crac, &[0]));
assert!(!limits.admits(&crac, &[0, 2]));
}
#[test]
fn a_network_action_spends_the_shared_budget_but_not_the_pst_one() {
let crac = crac();
let budget = limits().remaining(&crac, &[0]);
assert_eq!(budget.max_ra, 2, "one of the three is spent");
assert_eq!(budget.max_ra_per_tso["be"], 1, "BE spent one of its two");
assert_eq!(
budget.max_pst_per_tso["be"], 1,
"…but its shifter allowance is untouched"
);
assert!(budget.admits(&crac, &[0]));
assert!(!budget.admits(&crac, &[0, 1]));
assert!(budget.admits(&crac, &[0, 2]));
}
#[test]
fn the_elementary_cap_counts_switches_rather_than_actions() {
let crac = crac();
let limits = Limits {
max_elementary_per_tso: [("be".into(), 2)].into_iter().collect(),
..Limits::unlimited()
};
assert!(limits.admits(&crac, &[0]), "one switch, cap is two");
assert!(!limits.admits(&crac, &[1]), "one action but three switches");
}
#[test]
fn dropping_an_action_never_makes_a_set_inadmissible() {
let crac = crac();
let budget = limits().remaining(&crac, &[]);
let all: Vec<usize> = (0..crac.range_actions.len()).collect();
for mask in 0u32..(1 << all.len()) {
let subset: Vec<usize> = all
.iter()
.copied()
.filter(|&i| mask >> i & 1 == 1)
.collect();
if !budget.admits(&crac, &subset) {
continue;
}
for drop in &subset {
let smaller: Vec<usize> = subset.iter().copied().filter(|i| i != drop).collect();
assert!(
budget.admits(&crac, &smaller),
"{subset:?} is admissible but {smaller:?} is not"
);
}
}
}
#[test]
fn a_two_billion_limit_is_no_limit() {
let crac = crac();
let limits = Limits::from_crac(&RaUsageLimits {
instant: 0,
max_ra: Some(i32::MAX as usize),
max_tso: Some(i32::MAX as usize),
..Default::default()
});
assert!(limits.is_unlimited());
assert!(limits.admits(&crac, &[0, 1, 2]));
assert!(limits.remaining(&crac, &[0, 1, 2]).is_unlimited());
}
}