use crate::linear::DcOptions;
use crate::linear::btheta::{DcBranch, dc_branches, dc_power_flow};
use crate::linear::sensitivity::DcSensitivity;
use crate::opf::{LinearProgram, OpfError, OptStatus, Solver};
use crate::types::Transformer;
use super::crac::{Crac, RangeActionKind, State};
use super::evaluate::{FlowModel, Network, Resolution};
use super::limits::Budget;
use super::mnec::{Mnec, NO_CNEC_MARGIN};
use super::usage::Constrained;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ObjectiveUnit {
#[default]
Megawatt,
Ampere,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum ObjectiveKind {
#[default]
MaxMargin,
MinCost(super::costly::Costly),
}
impl ObjectiveKind {
pub fn costly(&self) -> Option<&super::costly::Costly> {
match self {
ObjectiveKind::MaxMargin => None,
ObjectiveKind::MinCost(c) => Some(c),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TapModel {
#[default]
Continuous,
Discrete,
}
#[derive(Clone, Debug)]
pub struct LinearOptions {
pub max_iterations: usize,
pub pst_penalty: f64,
pub injection_penalty: f64,
pub sensitivity_threshold: f64,
pub tap_model: TapModel,
pub objective_unit: ObjectiveUnit,
pub objective_kind: ObjectiveKind,
pub flow_model: FlowModel,
pub mnec: Mnec,
pub available: Constrained,
pub limits: Budget,
pub available_at: Option<Vec<State>>,
pub held: Option<super::evaluate::Held>,
pub decided: Vec<usize>,
pub activated: Vec<usize>,
pub a_r_s: bool,
pub previous_taps: Vec<(usize, i32)>,
}
impl Default for LinearOptions {
fn default() -> Self {
Self {
max_iterations: 10,
pst_penalty: 0.01,
injection_penalty: 0.001,
sensitivity_threshold: 1e-6,
tap_model: TapModel::Continuous,
objective_unit: ObjectiveUnit::Megawatt,
objective_kind: ObjectiveKind::MaxMargin,
flow_model: FlowModel::Dc,
mnec: Mnec::default(),
available: Constrained::unmeasured(),
limits: Budget::default(),
available_at: None,
held: None,
decided: Vec::new(),
activated: Vec::new(),
a_r_s: false,
previous_taps: Vec::new(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Setpoint {
pub action: usize,
pub value: f64,
pub initial: f64,
pub tap: Option<i32>,
}
impl Setpoint {
pub fn moved(&self) -> bool {
(self.value - self.initial).abs() > 1e-9
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LinearStatus {
Improved,
NoImprovement,
Failed,
}
#[derive(Clone, Debug)]
pub struct LinearResult {
pub status: LinearStatus,
pub setpoints: Vec<Setpoint>,
pub held_setpoints: Vec<Setpoint>,
pub initial_margin_mw: f64,
pub final_margin_mw: f64,
pub initial_objective: f64,
pub final_objective: f64,
pub iterations: usize,
}
impl LinearResult {
pub fn improvement(&self) -> f64 {
self.final_margin_mw - self.initial_margin_mw
}
}
fn evaluate_in(
crac: &Crac,
view: &Network<'_>,
resolution: &Resolution,
model: FlowModel,
held: Option<&super::evaluate::Held>,
) -> super::evaluate::SecurityResult {
let ac = super::evaluate::ac_options(view);
super::evaluate::evaluate_split(
crac,
view,
resolution,
view.initially_open,
held,
model,
&ac,
)
}
pub fn tap_table(
declared: &[(i32, f64)],
tap_changers: &[Option<crate::types::TapChanger>],
branch: usize,
lines: usize,
) -> Vec<(i32, f64)> {
if let Some(changer) = branch
.checked_sub(lines)
.and_then(|i| tap_changers.get(i)?.as_ref())
{
let table: Vec<(i32, f64)> = (changer.low..=changer.high())
.filter_map(|tap| changer.angle_deg(tap).map(|a| (tap, CRAC_ANGLE_SIGN * a)))
.collect();
if !table.is_empty() {
return table;
}
}
let mut table = declared.to_vec();
table.sort_by_key(|(t, _)| *t);
table
}
pub fn phase_shift_sensitivity(
sensitivity: &DcSensitivity,
branches: &[DcBranch],
branch: usize,
) -> Option<Vec<f64>> {
let target = branches.iter().find(|b| b.index == branch)?;
let mut injections = vec![0.0; sensitivity.n_buses()];
*injections.get_mut(target.from)? += target.b;
*injections.get_mut(target.to)? -= target.b;
let (_, mut flows) = sensitivity.response(&injections)?;
if let Some(direct) = flows.get_mut(branch) {
*direct -= target.b;
}
Some(flows)
}
struct Control {
action: usize,
reported: bool,
sensitivity: Vec<f64>,
current: f64,
start: f64,
start_tap: Option<i32>,
variation: (f64, f64),
chained: Option<ChainedWindow>,
activation: Option<f64>,
lower: f64,
upper: f64,
penalty: f64,
pst: Option<PstControl>,
injection: Option<InjectionControl>,
}
struct InjectionControl {
distribution: Vec<(usize, f64)>,
applied: f64,
key_sum: f64,
}
const CRAC_ANGLE_SIGN: f64 = -1.0;
struct PstControl {
branch: usize,
tap_to_angle: Vec<(i32, f64)>,
tap: i32,
}
impl PstControl {
fn nearest_tap(&self, angle: f64) -> i32 {
self.tap_to_angle
.iter()
.min_by(|a, b| (a.1 - angle).abs().total_cmp(&(b.1 - angle).abs()))
.map(|(t, _)| *t)
.unwrap_or(self.tap)
}
fn bracketing_taps(&self, angle: f64) -> Vec<i32> {
let mut below: Option<(i32, f64)> = None;
let mut above: Option<(i32, f64)> = None;
for &(tap, a) in &self.tap_to_angle {
if a <= angle && below.is_none_or(|(_, b)| a > b) {
below = Some((tap, a));
}
if a >= angle && above.is_none_or(|(_, b)| a < b) {
above = Some((tap, a));
}
}
let mut taps: Vec<i32> = below.into_iter().chain(above).map(|(t, _)| t).collect();
taps.dedup();
if taps.is_empty() {
taps.push(self.nearest_tap(angle));
}
taps
}
fn angle_at(&self, tap: i32) -> Option<f64> {
self.tap_to_angle
.binary_search_by_key(&tap, |(t, _)| *t)
.ok()
.map(|i| self.tap_to_angle[i].1)
}
}
pub fn optimize(
crac: &Crac,
network: &mut NetworkMut<'_>,
resolution: &Resolution,
perimeter: &[State],
solver: &mut dyn Solver,
options: &LinearOptions,
) -> LinearResult {
let before = (network.buses.clone(), network.transformers.clone());
let unconstrained =
optimize_within(crac, network, resolution, perimeter, solver, options, None);
let moved = moved_actions(&unconstrained);
if options.limits.is_unlimited() || options.limits.admits(crac, &moved) {
return unconstrained;
}
let usable = usable_range_actions(crac, perimeter, options);
let mut best: Option<(LinearResult, Vec<crate::types::Bus>, Vec<Transformer>)> = None;
for subset in admissible_subsets(crac, &usable, &options.limits) {
network.buses.clone_from(&before.0);
network.transformers.clone_from(&before.1);
let result = optimize_within(
crac,
network,
resolution,
perimeter,
solver,
options,
Some(&subset),
);
if best
.as_ref()
.is_none_or(|(b, ..)| result.final_objective > b.final_objective)
{
best = Some((result, network.buses.clone(), network.transformers.clone()));
}
}
let (result, buses, transformers) = best.expect("doing nothing fits every budget");
*network.buses = buses;
*network.transformers = transformers;
result
}
fn moved_actions(result: &LinearResult) -> Vec<usize> {
result
.setpoints
.iter()
.filter(|s| s.moved())
.map(|s| s.action)
.collect()
}
fn admissible_subsets(crac: &Crac, usable: &[usize], budget: &Budget) -> Vec<Vec<usize>> {
const CEILING: usize = 8;
if usable.len() > CEILING {
return vec![Vec::new()];
}
let admissible: Vec<Vec<usize>> = (0u32..(1 << usable.len()))
.map(|mask| {
usable
.iter()
.enumerate()
.filter(|(k, _)| mask >> k & 1 == 1)
.map(|(_, &i)| i)
.collect::<Vec<usize>>()
})
.filter(|subset| budget.admits(crac, subset))
.collect();
let mut out: Vec<Vec<usize>> = admissible
.iter()
.filter(|subset| {
!usable.iter().any(|extra| {
if subset.contains(extra) {
return false;
}
let mut bigger = (*subset).clone();
bigger.push(*extra);
budget.admits(crac, &bigger)
})
})
.cloned()
.collect();
out.sort_by_key(|s| s.len());
out
}
fn usable_range_actions(crac: &Crac, perimeter: &[State], options: &LinearOptions) -> Vec<usize> {
crac.range_actions
.iter()
.enumerate()
.filter(|(_, a)| {
let at = options.available_at.as_deref().unwrap_or(perimeter);
options.available.allows(&a.usage_rules, at, crac)
})
.map(|(i, _)| i)
.collect()
}
fn optimize_within(
crac: &Crac,
network: &mut NetworkMut<'_>,
resolution: &Resolution,
perimeter: &[State],
solver: &mut dyn Solver,
options: &LinearOptions,
allowed: Option<&[usize]>,
) -> LinearResult {
let dc_options = DcOptions::default();
let cnec_indices = perimeter_cnecs(crac, resolution, perimeter, |c| c.optimized);
let mnec_indices = if options.mnec.active() {
perimeter_cnecs(crac, resolution, perimeter, |c| c.monitored)
} else {
Vec::new()
};
let lp_indices: Vec<usize> = cnec_indices
.iter()
.chain(mnec_indices.iter())
.copied()
.collect();
let mut best = objective(crac, network, resolution, perimeter, &[], options);
let initial_objective = best;
let initial_margin = margin(
crac,
network,
resolution,
perimeter,
ObjectiveUnit::Megawatt,
&[],
options,
);
let mut setpoints: Vec<Setpoint> = Vec::new();
let mut held_setpoints: Vec<Setpoint> = Vec::new();
let mut iterations = 0;
if lp_indices.is_empty() {
return LinearResult {
status: LinearStatus::NoImprovement,
setpoints,
held_setpoints,
initial_margin_mw: initial_margin,
final_margin_mw: initial_margin,
initial_objective,
final_objective: best,
iterations,
};
}
let mut controls = match build_controls(
crac,
network,
resolution,
perimeter,
&lp_indices,
dc_options,
options,
allowed,
) {
Some(c) if !c.is_empty() => c,
_ => {
return LinearResult {
status: LinearStatus::NoImprovement,
setpoints,
held_setpoints,
initial_margin_mw: initial_margin,
final_margin_mw: initial_margin,
initial_objective,
final_objective: best,
iterations,
};
}
};
let starting: Vec<f64> = controls.iter().map(|c| c.current).collect();
let starting_taps: Vec<Option<i32>> = controls
.iter()
.map(|c| c.pst.as_ref().map(|p| p.tap))
.collect();
let mut binaries = options.objective_kind.costly().is_some()
&& controls
.iter()
.any(|c| c.activation.is_some_and(|k| k > 0.0));
for _ in 0..options.max_iterations {
iterations += 1;
let (flows, limits) = perimeter_flows(
crac,
network,
resolution,
perimeter,
&lp_indices,
&controls,
options,
);
let build = |binaries| {
build_program(
cnec_indices.len(),
&mnec_indices,
&flows,
&limits,
&controls,
options,
binaries,
)
};
let solved = match solver.solve(&build(binaries)) {
Err(OpfError::IntegralityUnsupported { .. }) if binaries => {
binaries = false;
solver.solve(&build(false))
}
other => other,
};
let Ok(solution) = solved else {
break;
};
if solution.status != OptStatus::Optimal {
break;
}
let previous: Vec<(f64, Option<i32>)> = controls
.iter()
.map(|c| (c.current, c.pst.as_ref().map(|p| p.tap)))
.collect();
let mut proposed: Vec<(f64, Option<i32>)> = controls
.iter()
.enumerate()
.map(|(k, control)| {
let value = solution.primal[setpoint_column(k)];
match &control.pst {
Some(pst) => {
let tap = pst.nearest_tap(value);
(pst.angle_at(tap).unwrap_or(value), Some(tap))
}
None => (value, None),
}
})
.collect();
apply(crac, network, &mut controls, &proposed);
let mut best_here = objective(crac, network, resolution, perimeter, &controls, options);
let coupled = options.objective_kind.costly().is_some();
for k in 0..controls.len() {
let Some(pst) = controls[k].pst.as_ref() else {
continue;
};
let target = solution.primal[setpoint_column(k)];
let candidates: Vec<i32> = pst
.bracketing_taps(target)
.into_iter()
.filter(|t| Some(*t) != proposed[k].1)
.collect();
for tap in candidates {
let Some(angle) = controls[k].pst.as_ref().and_then(|p| p.angle_at(tap)) else {
continue;
};
let mut trial = proposed.clone();
trial[k] = (angle, Some(tap));
if coupled {
for j in 0..trial.len() {
if anchor_of(&controls, j) == Some(k) && proposed[j].1 == proposed[k].1 {
trial[j] = (angle, Some(tap));
}
}
}
if !chained_taps_hold(&controls, &trial) {
continue;
}
apply(crac, network, &mut controls, &trial);
let score = objective(crac, network, resolution, perimeter, &controls, options);
if score > best_here + 1e-9 {
best_here = score;
proposed = trial;
} else {
apply(crac, network, &mut controls, &proposed);
}
}
}
for k in 0..controls.len() {
if controls[k].pst.is_none() {
continue;
}
let (rest_angle, rest_tap) = match previous_instant_of(&controls, k) {
Some(p) => (
controls[k]
.pst
.as_ref()
.and_then(|c| proposed[p].1.and_then(|t| c.angle_at(t)))
.unwrap_or(proposed[p].0),
proposed[p].1,
),
None => (starting[k], starting_taps[k]),
};
if proposed[k].1 == rest_tap {
continue;
}
let mut trial = proposed.clone();
trial[k] = (rest_angle, rest_tap);
if coupled {
for j in 0..trial.len() {
if anchor_of(&controls, j) == Some(k) && proposed[j].1 == proposed[k].1 {
trial[j] = (rest_angle, rest_tap);
}
}
}
if !chained_taps_hold(&controls, &trial) {
continue;
}
apply(crac, network, &mut controls, &trial);
let score = objective(crac, network, resolution, perimeter, &controls, options);
if score >= best_here - 1e-9 {
best_here = best_here.max(score);
proposed = trial;
} else {
apply(crac, network, &mut controls, &proposed);
}
}
let moved = proposed
.iter()
.zip(&previous)
.any(|((value, _), (was, _))| (value - was).abs() > 1e-9);
if !moved {
apply(crac, network, &mut controls, &previous);
break;
}
apply(crac, network, &mut controls, &proposed);
let score = objective(crac, network, resolution, perimeter, &controls, options);
if score > best + 1e-9 {
best = score;
setpoints = controls
.iter()
.enumerate()
.filter(|(_, c)| c.reported)
.map(|(k, c)| Setpoint {
action: c.action,
value: c.current,
initial: starting[k],
tap: c.pst.as_ref().map(|p| p.tap),
})
.collect();
held_setpoints = controls
.iter()
.enumerate()
.filter(|(_, c)| !c.reported)
.map(|(k, c)| Setpoint {
action: c.action,
value: c.current,
initial: starting[k],
tap: c.pst.as_ref().map(|p| p.tap),
})
.collect();
if let Some(mut rebuilt) = build_controls(
crac,
network,
resolution,
perimeter,
&lp_indices,
dc_options,
options,
allowed,
) {
if rebuilt.len() == controls.len() {
for (new, old) in rebuilt.iter_mut().zip(&controls) {
new.start = old.start;
new.start_tap = old.start_tap;
}
controls = rebuilt;
}
}
} else {
apply(crac, network, &mut controls, &previous);
break;
}
}
if setpoints.is_empty() {
let restore: Vec<(f64, Option<i32>)> = starting
.iter()
.copied()
.zip(starting_taps.iter().copied())
.collect();
apply(crac, network, &mut controls, &restore);
}
LinearResult {
status: if setpoints.iter().any(Setpoint::moved) {
LinearStatus::Improved
} else if iterations == 0 {
LinearStatus::Failed
} else {
LinearStatus::NoImprovement
},
setpoints,
held_setpoints,
initial_margin_mw: initial_margin,
final_margin_mw: margin(
crac,
network,
resolution,
perimeter,
ObjectiveUnit::Megawatt,
&controls,
options,
),
initial_objective,
final_objective: best,
iterations,
}
}
pub struct NetworkMut<'a> {
pub buses: &'a mut Vec<crate::types::Bus>,
pub lines: &'a [crate::types::Line],
pub transformers: &'a mut Vec<Transformer>,
pub branch_ids: &'a [String],
pub bus_ids: &'a [String],
pub initially_open: &'a [usize],
pub bus_countries: &'a [Option<String>],
pub shunts: &'a [crate::network::ShuntAdm],
pub generation: &'a [f64],
pub tap_changers: &'a [Option<crate::types::TapChanger>],
pub base_mva: f64,
}
impl NetworkMut<'_> {
fn view(&self) -> Network<'_> {
Network {
generation: self.generation,
buses: self.buses,
lines: self.lines,
transformers: self.transformers,
branch_ids: self.branch_ids,
bus_ids: self.bus_ids,
initially_open: self.initially_open,
bus_countries: self.bus_countries,
shunts: self.shunts,
tap_changers: self.tap_changers,
base_mva: self.base_mva,
}
}
}
fn setpoint_column(k: usize) -> usize {
3 * k
}
fn up_column(k: usize) -> usize {
3 * k + 1
}
fn down_column(k: usize) -> usize {
3 * k + 2
}
fn margin_column(n_controls: usize) -> usize {
3 * n_controls
}
fn build_program(
optimized: usize,
mnecs: &[usize],
flows: &[f64],
limits: &[(f64, f64, f64)],
controls: &[Control],
options: &LinearOptions,
binaries: bool,
) -> LinearProgram {
let n = controls.len();
let mm = margin_column(n);
let costly = options.objective_kind.costly();
let activation: Vec<Option<f64>> = controls
.iter()
.map(|c| {
binaries
.then_some(c.activation)
.flatten()
.filter(|k| *k > 0.0)
})
.collect();
let n_binary = activation.iter().filter(|a| a.is_some()).count();
let base = 3 * n + usize::from(costly.is_none());
let violation_of = |slot: usize| base + slot;
let overload_of = |slot: usize| base + mnecs.len() + slot;
let extra = if costly.is_some() { optimized } else { 0 };
let binary_base = base + mnecs.len() + extra;
let mut binary_of: Vec<Option<usize>> = Vec::with_capacity(n);
let mut next = binary_base;
for a in &activation {
binary_of.push(a.is_some().then(|| {
let column = next;
next += 1;
column
}));
}
let mut lp = LinearProgram::new(binary_base + n_binary);
for (k, control) in controls.iter().enumerate() {
lp.col_lower[setpoint_column(k)] = control.lower;
lp.col_upper[setpoint_column(k)] = control.upper;
for (column, price) in [
(up_column(k), control.variation.0),
(down_column(k), control.variation.1),
] {
lp.col_lower[column] = 0.0;
lp.col_upper[column] = f64::INFINITY;
lp.col_cost[column] = if costly.is_some() {
price
} else {
control.penalty
};
}
let previous = costly.is_some().then(|| anchor_of(controls, k)).flatten();
let mut row = vec![
(setpoint_column(k), 1.0),
(up_column(k), -1.0),
(down_column(k), 1.0),
];
let anchor = match previous {
Some(p) => {
row.push((setpoint_column(p), -1.0));
0.0
}
None if costly.is_some() => control.start,
None => control.current,
};
lp.add_row(&row, anchor, anchor);
if let (Some(window), Some(p)) = (control.chained, previous_instant_of(controls, k))
&& let Some(pst) = controls[p].pst.as_ref()
{
let (lo, hi) = chained_window(pst, window);
lp.add_row(
&[(setpoint_column(k), 1.0), (setpoint_column(p), -1.0)],
lo,
hi,
);
}
if let (Some(column), Some(cost)) = (binary_of[k], activation[k]) {
lp.set_binary(column);
lp.col_cost[column] = cost;
let big_m = control.upper - control.lower;
lp.add_row(
&[(up_column(k), 1.0), (down_column(k), 1.0), (column, -big_m)],
f64::NEG_INFINITY,
0.0,
);
}
}
let balance: Vec<(usize, f64)> = controls
.iter()
.enumerate()
.filter_map(|(k, c)| {
let sum = c.injection.as_ref()?.key_sum;
(sum.abs() > 1e-12).then_some((k, sum))
})
.collect();
if !balance.is_empty() {
let mut row: Vec<(usize, f64)> = Vec::with_capacity(balance.len() * 2);
for &(k, sum) in &balance {
row.push((up_column(k), sum));
row.push((down_column(k), -sum));
}
lp.add_row(&row, 0.0, 0.0);
}
if costly.is_none() {
lp.col_lower[mm] = f64::NEG_INFINITY;
lp.col_upper[mm] = f64::INFINITY;
lp.col_cost[mm] = -1.0;
}
if let Some(costly) = costly {
for slot in 0..optimized {
let column = overload_of(slot);
lp.col_lower[column] = 0.0;
lp.col_upper[column] = f64::INFINITY;
lp.col_cost[column] = costly.options.violation_penalty;
}
}
for position in 0..optimized {
let reference = flows[position];
let (lower, upper, amperes_per_mw) = limits[position];
let scale = match options.objective_unit {
ObjectiveUnit::Megawatt => 1.0,
ObjectiveUnit::Ampere => amperes_per_mw,
};
if scale == 0.0 {
continue;
}
let terms: Vec<(usize, f64)> = controls
.iter()
.enumerate()
.filter_map(|(k, c)| {
let s = c.sensitivity.get(position).copied().unwrap_or(0.0);
(s.abs() >= options.sensitivity_threshold).then_some((setpoint_column(k), s))
})
.collect();
let constant: f64 = reference
- controls
.iter()
.enumerate()
.map(|(k, c)| {
let s = c.sensitivity.get(position).copied().unwrap_or(0.0);
if s.abs() >= options.sensitivity_threshold {
let _ = k;
s * c.current
} else {
0.0
}
})
.sum::<f64>();
for is_upper in [true, false] {
let limit = if is_upper { upper } else { lower };
if !limit.is_finite() {
continue;
}
let sign = if is_upper { 1.0 } else { -1.0 };
let bound = scale
* if is_upper {
limit - constant
} else {
constant - limit
};
match costly {
Some(c) => {
let mut coefficients: Vec<(usize, f64)> = Vec::with_capacity(terms.len() + 1);
coefficients.push((overload_of(position), 1.0));
for &(column, s) in &terms {
coefficients.push((column, -sign * s * scale));
}
lp.add_row(
&coefficients,
c.options.violation_threshold - bound,
f64::INFINITY,
);
}
None => {
let mut coefficients: Vec<(usize, f64)> = Vec::with_capacity(terms.len() + 1);
coefficients.push((mm, 1.0));
for &(column, s) in &terms {
coefficients.push((column, sign * s * scale));
}
lp.add_row(&coefficients, f64::NEG_INFINITY, bound);
}
}
}
}
for slot in 0..mnecs.len() {
lp.col_lower[violation_of(slot)] = 0.0;
lp.col_upper[violation_of(slot)] = f64::INFINITY;
}
for (slot, &cnec) in mnecs.iter().enumerate() {
let position = optimized + slot;
let violation = violation_of(slot);
let (lower, upper, amperes_per_mw) = limits[position];
let Some(initial) = options.mnec.baseline.get(cnec) else {
continue;
};
let to_mw = match options.objective_unit {
ObjectiveUnit::Megawatt => 1.0,
ObjectiveUnit::Ampere if amperes_per_mw > 0.0 => 1.0 / amperes_per_mw,
ObjectiveUnit::Ampere => continue,
};
let decrease = options.mnec.options.acceptable_margin_decrease * to_mw;
let adjustment = options.mnec.options.constraint_adjustment_coefficient * to_mw;
lp.col_cost[violation] = options.mnec.options.violation_cost / to_mw;
let terms: Vec<(usize, f64)> = controls
.iter()
.enumerate()
.filter_map(|(k, c)| {
let s = c.sensitivity.get(position).copied().unwrap_or(0.0);
(s.abs() >= options.sensitivity_threshold).then_some((k, s))
})
.collect();
let constant: f64 = flows[position]
- terms
.iter()
.map(|&(k, s)| s * controls[k].current)
.sum::<f64>();
if upper.is_finite() {
let bound = f64::max(upper, initial.flow_mw + decrease) - adjustment;
let mut coefficients: Vec<(usize, f64)> = Vec::with_capacity(terms.len() + 1);
coefficients.extend(terms.iter().map(|&(k, s)| (setpoint_column(k), s)));
coefficients.push((violation, -1.0));
lp.add_row(&coefficients, f64::NEG_INFINITY, bound - constant);
}
if lower.is_finite() {
let bound = f64::min(lower, initial.flow_mw - decrease) + adjustment;
let mut coefficients: Vec<(usize, f64)> = Vec::with_capacity(terms.len() + 1);
coefficients.extend(terms.iter().map(|&(k, s)| (setpoint_column(k), -s)));
coefficients.push((violation, -1.0));
lp.add_row(&coefficients, f64::NEG_INFINITY, constant - bound);
}
}
lp
}
fn perimeter_cnecs(
crac: &Crac,
resolution: &Resolution,
perimeter: &[State],
wanted: impl Fn(&super::crac::FlowCnec) -> bool,
) -> Vec<usize> {
crac.flow_cnecs
.iter()
.enumerate()
.filter(|(_, c)| perimeter.contains(&c.state) && wanted(c))
.filter(|(_, c)| resolution.branch(&c.network_element).is_some())
.map(|(i, _)| i)
.collect()
}
fn injection_origin(
buses: &[crate::types::Bus],
base_mva: f64,
keys: &[(usize, f64)],
) -> Option<f64> {
let mut agreed: Option<f64> = None;
for &(bus, key) in keys {
if key.abs() < 1e-9 {
continue;
}
let target = buses.get(bus)?.p_spec * base_mva;
let setpoint = target / key;
match agreed {
None => agreed = Some(setpoint),
Some(first) if (setpoint - first).abs() <= 1e-6 * first.abs().max(1.0) => {}
Some(_) => return None,
}
}
agreed
}
pub fn injection_setpoint(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
action: usize,
) -> Option<f64> {
let RangeActionKind::Injection { distribution } = &crac.range_actions.get(action)?.kind else {
return None;
};
let keys: Vec<(usize, f64)> = distribution
.iter()
.filter_map(|(element, key)| Some((resolution.bus(element)?, *key)))
.collect();
injection_origin(network.buses, network.base_mva, &keys)
}
#[allow(clippy::too_many_arguments)]
fn build_controls(
crac: &Crac,
network: &NetworkMut<'_>,
resolution: &Resolution,
perimeter: &[State],
cnecs: &[usize],
dc_options: DcOptions,
options: &LinearOptions,
allowed: Option<&[usize]>,
) -> Option<Vec<Control>> {
let points = SensitivityPoints::build(
network,
crac,
resolution,
cnecs,
dc_options,
options.held.as_ref(),
)?;
let scoped = |sensitivity: Vec<f64>, governs: Option<&[State]>| -> Vec<f64> {
let Some(governs) = governs else {
return sensitivity;
};
sensitivity
.into_iter()
.enumerate()
.map(|(position, value)| {
let state = cnecs.get(position).map(|&i| &crac.flow_cnecs[i].state);
if state.is_some_and(|s| governs.contains(s)) {
value
} else {
0.0
}
})
.collect()
};
let split = options.held.as_ref().filter(|h| !h.states.is_empty());
let at = options.available_at.as_deref().unwrap_or(perimeter);
let mut wanted: Vec<(usize, Option<&[State]>)> = Vec::new();
for (index, action) in crac.range_actions.iter().enumerate() {
if allowed.is_some_and(|a| !a.contains(&index)) {
continue;
}
if options.decided.contains(&index) {
continue;
}
if options.available.allows(&action.usage_rules, at, crac) {
wanted.push((index, None));
}
if let Some(held) = split
&& options.a_r_s
&& options
.available
.allows(&action.usage_rules, &held.states, crac)
{
wanted.push((index, Some(&held.states)));
}
}
let preventive_branches: Vec<usize> = wanted
.iter()
.filter(|(_, governs)| governs.is_none())
.filter_map(|(index, _)| match &crac.range_actions[*index].kind {
RangeActionKind::Pst { element, .. } => resolution.branch(element),
_ => None,
})
.collect();
let mut controls = Vec::new();
for (index, governs) in wanted {
let action = &crac.range_actions[index];
match &action.kind {
RangeActionKind::Pst {
element,
initial_tap,
tap_to_angle,
} => {
let Some(branch) = resolution.branch(element) else {
continue;
};
let table = tap_table(
tap_to_angle,
network.tap_changers,
branch,
network.lines.len(),
);
if table.is_empty() {
continue;
}
let Some(column) = points.phase_shift(branch) else {
continue;
};
let scale = CRAC_ANGLE_SIGN * network.base_mva * std::f64::consts::PI / 180.0;
let sensitivity_mw: Vec<f64> = column.iter().map(|s| s * scale).collect();
let live_angle = live_crac_angle(network, branch).unwrap_or_else(|| {
table
.iter()
.find(|(t, _)| t == initial_tap)
.map(|(_, a)| *a)
.unwrap_or(0.0)
});
let tap = tap_nearest(&table, live_angle).unwrap_or(*initial_tap);
let current = table
.iter()
.find(|(t, _)| *t == tap)
.map(|(_, a)| *a)
.unwrap_or(live_angle);
let previous_tap = options
.previous_taps
.iter()
.find(|(i, _)| *i == index)
.map_or(*initial_tap, |(_, t)| *t);
let chain = governs.is_some() && preventive_branches.contains(&branch);
let ((lower, upper), chained) =
tap_bounds_split(action, &table, *initial_tap, previous_tap, chain);
if !starts_inside_its_range(current, lower, upper) {
continue;
}
let per_tap = |cost: f64| -> f64 {
let (lo, hi) = (table.first(), table.last());
match (lo, hi) {
(Some((t0, a0)), Some((t1, a1))) if t1 > t0 => {
let degrees = (a1 - a0).abs() / f64::from(t1 - t0);
if degrees > 1e-12 { cost / degrees } else { 0.0 }
}
_ => 0.0,
}
};
let variation = action
.variation_cost
.map_or((0.0, 0.0), |c| (per_tap(c.up), per_tap(c.down)));
controls.push(Control {
action: index,
reported: governs.is_none(),
sensitivity: scoped(sensitivity_mw, governs),
current,
start: current,
start_tap: Some(tap),
variation,
chained,
activation: action.activation_cost,
lower,
upper,
penalty: options.pst_penalty,
pst: Some(PstControl {
branch,
tap_to_angle: table,
tap,
}),
injection: None,
});
}
RangeActionKind::Injection { distribution } => {
let mut injections = vec![0.0; network.buses.len()];
let mut keys: Vec<(usize, f64)> = Vec::new();
let mut usable = false;
for (element, key) in distribution {
let Some(bus) = resolution.bus(element) else {
continue;
};
if bus >= injections.len() {
continue;
}
injections[bus] += key / network.base_mva;
keys.push((bus, *key));
usable = true;
}
if !usable {
continue;
}
let Some(column) = points.injection(&injections) else {
continue;
};
let sensitivity_mw: Vec<f64> =
column.iter().map(|s| s * network.base_mva).collect();
let (lower, upper) = standard_bounds(action);
let origin = injection_origin(network.buses, network.base_mva, &keys);
let start = origin.unwrap_or(0.0);
if !starts_inside_its_range(start, lower, upper) {
continue;
}
controls.push(Control {
action: index,
reported: governs.is_none(),
sensitivity: scoped(sensitivity_mw, governs),
current: start,
start,
start_tap: None,
variation: action.variation_cost.map_or((0.0, 0.0), |c| (c.up, c.down)),
chained: None,
activation: action.activation_cost,
lower,
upper,
penalty: options.injection_penalty,
pst: None,
injection: Some(InjectionControl {
key_sum: keys.iter().map(|(_, k)| *k).sum(),
distribution: keys,
applied: start,
}),
});
}
RangeActionKind::Hvdc { .. } | RangeActionKind::CounterTrade { .. } => {}
}
}
let superseded: Vec<usize> = controls
.iter()
.filter(|c| !c.reported)
.filter_map(|c| c.pst.as_ref().map(|p| p.branch))
.collect();
if let Some(held) = split
&& !superseded.is_empty()
{
for control in controls.iter_mut().filter(|c| c.reported) {
let Some(branch) = control.pst.as_ref().map(|p| p.branch) else {
continue;
};
if !superseded.contains(&branch) {
continue;
}
for (position, value) in control.sensitivity.iter_mut().enumerate() {
let governed = cnecs
.get(position)
.is_some_and(|&i| held.states.contains(&crac.flow_cnecs[i].state));
if governed {
*value = 0.0;
}
}
}
}
Some(controls)
}
fn live_crac_angle(network: &NetworkMut<'_>, branch: usize) -> Option<f64> {
let i = branch.checked_sub(network.lines.len())?;
Some(CRAC_ANGLE_SIGN * network.transformers.get(i)?.tap.arg().to_degrees())
}
fn tap_nearest(table: &[(i32, f64)], angle: f64) -> Option<i32> {
table
.iter()
.min_by(|a, b| (a.1 - angle).abs().total_cmp(&(b.1 - angle).abs()))
.map(|(t, _)| *t)
}
pub fn taps_now(crac: &Crac, network: &Network<'_>, resolution: &Resolution) -> Vec<(usize, i32)> {
crac.range_actions
.iter()
.enumerate()
.filter_map(|(index, action)| {
let RangeActionKind::Pst {
element,
initial_tap,
tap_to_angle,
} = &action.kind
else {
return None;
};
let branch = resolution.branch(element)?;
let table = tap_table(
tap_to_angle,
network.tap_changers,
branch,
network.lines.len(),
);
if table.is_empty() {
return None;
}
let i = branch.checked_sub(network.lines.len())?;
let angle = CRAC_ANGLE_SIGN * network.transformers.get(i)?.tap.arg().to_degrees();
Some((index, tap_nearest(&table, angle).unwrap_or(*initial_tap)))
})
.collect()
}
pub(super) fn tap_bounds(
action: &super::crac::RangeAction,
table: &[(i32, f64)],
initial_tap: i32,
previous_tap: i32,
) -> (f64, f64) {
tap_bounds_split(action, table, initial_tap, previous_tap, false).0
}
pub(super) type ChainedWindow = (Option<i32>, Option<i32>);
pub(super) fn tap_bounds_split(
action: &super::crac::RangeAction,
table: &[(i32, f64)],
initial_tap: i32,
previous_tap: i32,
chain: bool,
) -> ((f64, f64), Option<ChainedWindow>) {
let (mut low, mut high) = (i32::MIN, i32::MAX);
let (mut chained_min, mut chained_max) = (None::<i32>, None::<i32>);
let mut chained_any = false;
for range in &action.ranges {
if chain && range.kind == super::crac::RangeKind::RelativeToPreviousInstant {
chained_any = true;
if let Some(m) = range.min {
let m = m as i32;
chained_min = Some(chained_min.map_or(m, |c: i32| c.max(m)));
}
if let Some(m) = range.max {
let m = m as i32;
chained_max = Some(chained_max.map_or(m, |c: i32| c.min(m)));
}
continue;
}
let anchor = match range.kind {
super::crac::RangeKind::RelativeToInitialNetwork => initial_tap,
super::crac::RangeKind::RelativeToPreviousInstant => previous_tap,
super::crac::RangeKind::Absolute
| super::crac::RangeKind::RelativeToPreviousTimeStep => 0,
};
let (min, max) = (
range.min.map(|m| anchor + m as i32),
range.max.map(|m| anchor + m as i32),
);
if let Some(m) = min {
low = low.max(m);
}
if let Some(m) = max {
high = high.min(m);
}
}
let angles: Vec<f64> = table
.iter()
.filter(|(t, _)| *t >= low && *t <= high)
.map(|(_, a)| *a)
.collect();
if angles.is_empty() {
let all: Vec<f64> = table.iter().map(|(_, a)| *a).collect();
return (
(
all.iter().copied().fold(f64::INFINITY, f64::min),
all.iter().copied().fold(f64::NEG_INFINITY, f64::max),
),
chained_any.then_some((chained_min, chained_max)),
);
}
(
(
angles.iter().copied().fold(f64::INFINITY, f64::min),
angles.iter().copied().fold(f64::NEG_INFINITY, f64::max),
),
chained_any.then_some((chained_min, chained_max)),
)
}
struct SensitivityPoints {
points: Vec<SensitivityPoint>,
per_cnec: Vec<usize>,
branches: Vec<usize>,
}
type SensitivityPoint = ((Option<usize>, bool), Vec<DcBranch>, DcSensitivity);
impl SensitivityPoints {
fn build(
network: &NetworkMut<'_>,
crac: &Crac,
resolution: &Resolution,
cnecs: &[usize],
dc_options: DcOptions,
held: Option<&super::evaluate::Held>,
) -> Option<Self> {
let n_branches = network.lines.len() + network.transformers.len();
let held = held.filter(|h| !h.states.is_empty());
let is_held = |cnec: usize| -> bool {
held.is_some_and(|h| h.states.contains(&crac.flow_cnecs[cnec].state))
};
let mut wanted: Vec<(Option<usize>, bool)> = cnecs
.iter()
.map(|&i| (crac.flow_cnecs[i].state.contingency, is_held(i)))
.collect();
wanted.sort_unstable();
wanted.dedup();
let mut points = Vec::with_capacity(wanted.len());
for (contingency, held_here) in wanted {
let mut lines = network.lines.to_vec();
let mut transformers = network.transformers.clone();
let cut = |branch: usize,
lines: &mut Vec<crate::types::Line>,
transformers: &mut Vec<Transformer>| {
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;
}
};
if let Some(held) = held {
let open = if held_here {
held.applied_to(network.initially_open)
} else {
network.initially_open.to_vec()
};
for &branch in &open {
cut(branch, &mut lines, &mut transformers);
}
}
if let Some(c) = contingency {
for element in &crac.contingencies[c].elements {
let Some(branch) = resolution.branch(element) else {
continue;
};
cut(branch, &mut lines, &mut transformers);
}
}
let branches = dc_branches(&lines, &transformers, dc_options);
let Some(sensitivity) = DcSensitivity::new(network.buses, &branches, n_branches) else {
continue;
};
points.push(((contingency, held_here), branches, sensitivity));
}
if points.is_empty() {
return None;
}
let per_cnec = cnecs
.iter()
.map(|&i| {
let want = (crac.flow_cnecs[i].state.contingency, is_held(i));
points
.iter()
.position(|(key, _, _)| *key == want)
.or_else(|| points.iter().position(|((_, h), _, _)| *h == want.1))
.unwrap_or(0)
})
.collect();
let branches = cnecs
.iter()
.map(|&i| {
resolution
.branch(&crac.flow_cnecs[i].network_element)
.unwrap()
})
.collect();
Some(Self {
points,
per_cnec,
branches,
})
}
fn phase_shift(&self, branch: usize) -> Option<Vec<f64>> {
let columns: Vec<Option<Vec<f64>>> = self
.points
.iter()
.map(|(_, branches, s)| phase_shift_sensitivity(s, branches, branch))
.collect();
columns
.iter()
.any(Option::is_some)
.then(|| self.gather(&columns))
}
fn injection(&self, injections: &[f64]) -> Option<Vec<f64>> {
let columns: Vec<Option<Vec<f64>>> = self
.points
.iter()
.map(|(_, _, s)| s.response(injections).map(|(_, column)| column))
.collect();
columns
.iter()
.any(Option::is_some)
.then(|| self.gather(&columns))
}
fn gather(&self, columns: &[Option<Vec<f64>>]) -> Vec<f64> {
self.per_cnec
.iter()
.zip(&self.branches)
.map(|(&point, &branch)| {
columns
.get(point)
.and_then(|c| c.as_ref())
.and_then(|c| c.get(branch))
.copied()
.unwrap_or(0.0)
})
.collect()
}
}
fn starts_inside_its_range(current: f64, lower: f64, upper: f64) -> bool {
current >= lower - 1e-6 && current <= upper + 1e-6
}
fn standard_bounds(action: &super::crac::RangeAction) -> (f64, f64) {
let mut low = f64::NEG_INFINITY;
let mut high = f64::INFINITY;
for range in &action.ranges {
if let Some(m) = range.min {
low = low.max(m);
}
if let Some(m) = range.max {
high = high.min(m);
}
}
if low > high {
return (0.0, 0.0);
}
(low, high)
}
fn apply(
_crac: &Crac,
network: &mut NetworkMut<'_>,
controls: &mut [Control],
proposed: &[(f64, Option<i32>)],
) {
let base_mva = network.base_mva;
for (control, &(value, tap)) in controls.iter_mut().zip(proposed) {
control.current = value;
let reported = control.reported;
if let Some(injection) = control.injection.as_mut() {
let delta = value - injection.applied;
if delta != 0.0 {
for &(bus, key) in &injection.distribution {
if let Some(b) = network.buses.get_mut(bus) {
b.p_spec += key * delta / base_mva;
}
}
injection.applied = value;
}
}
if let Some(pst) = control.pst.as_mut() {
if let Some(tap) = tap {
pst.tap = tap;
}
if !reported {
continue;
}
if let Some(i) = pst.branch.checked_sub(network.lines.len()) {
let from_network = network
.tap_changers
.get(i)
.and_then(|c| c.as_ref())
.and_then(|c| tap.and_then(|t| c.at(t)));
if let Some(transformer) = network.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,
(CRAC_ANGLE_SIGN * value).to_radians(),
),
};
}
}
}
}
}
fn margin(
crac: &Crac,
network: &NetworkMut<'_>,
resolution: &Resolution,
perimeter: &[State],
unit: ObjectiveUnit,
controls: &[Control],
options: &LinearOptions,
) -> f64 {
let view = network.view();
let held = held_now(options, controls);
let result = evaluate_in(crac, &view, resolution, options.flow_model, held.as_ref());
margin_of(crac, &result, perimeter, unit)
}
fn anchor_of(controls: &[Control], k: usize) -> Option<usize> {
let control = &controls[k];
if control.reported {
return None;
}
controls[..k]
.iter()
.position(|c| c.action == control.action && c.reported)
}
fn chained_taps_hold(controls: &[Control], proposed: &[(f64, Option<i32>)]) -> bool {
for (k, control) in controls.iter().enumerate() {
let Some(window) = control.chained else {
continue;
};
let Some(p) = previous_instant_of(controls, k) else {
continue;
};
let (Some(here), Some(there)) = (proposed[k].1, proposed[p].1) else {
continue;
};
let delta = here - there;
if window.0.is_some_and(|m| delta < m) || window.1.is_some_and(|m| delta > m) {
return false;
}
}
true
}
fn previous_instant_of(controls: &[Control], k: usize) -> Option<usize> {
let control = &controls[k];
if control.reported || control.chained.is_none() {
return None;
}
let branch = control.pst.as_ref()?.branch;
controls
.iter()
.position(|c| c.reported && c.pst.as_ref().is_some_and(|p| p.branch == branch))
}
fn chained_window(anchor: &PstControl, window: ChainedWindow) -> (f64, f64) {
let base = anchor.angle_at(anchor.tap).unwrap_or(0.0);
let low = window.0.map_or(i32::MIN, |m| anchor.tap.saturating_add(m));
let high = window.1.map_or(i32::MAX, |m| anchor.tap.saturating_add(m));
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
for &(t, a) in &anchor.tap_to_angle {
if t >= low && t <= high {
lo = lo.min(a - base);
hi = hi.max(a - base);
}
}
if lo > hi { (0.0, 0.0) } else { (lo, hi) }
}
fn moved_range_actions(controls: &[Control]) -> Vec<(usize, f64)> {
controls
.iter()
.enumerate()
.filter_map(|(k, c)| {
let (from, from_tap) = match anchor_of(controls, k) {
Some(p) => (controls[p].current, controls[p].pst.as_ref().map(|x| x.tap)),
None => (c.start, c.start_tap),
};
if (c.current - from).abs() <= 1e-9 {
return None;
}
let distance = match (&c.pst, from_tap) {
(Some(pst), Some(start)) => f64::from(pst.tap - start),
_ => c.current - from,
};
Some((c.action, distance))
})
.collect()
}
fn held_now(options: &LinearOptions, controls: &[Control]) -> Option<super::evaluate::Held> {
let held = options.held.as_ref()?;
if controls.iter().all(|c| c.reported) {
return Some(held.clone());
}
let mut out = held.clone();
for control in controls.iter().filter(|c| !c.reported) {
if let Some(pst) = &control.pst {
out.taps.push((pst.branch, pst.tap, control.current));
}
}
Some(out)
}
fn margin_of(
crac: &Crac,
result: &super::evaluate::SecurityResult,
perimeter: &[State],
unit: ObjectiveUnit,
) -> f64 {
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 {
ObjectiveUnit::Megawatt => c.margin_mw,
ObjectiveUnit::Ampere => c.margin_a,
})
.fold(f64::INFINITY, f64::min)
}
fn objective(
crac: &Crac,
network: &NetworkMut<'_>,
resolution: &Resolution,
perimeter: &[State],
controls: &[Control],
options: &LinearOptions,
) -> f64 {
let view = network.view();
let held = held_now(options, controls);
let result = evaluate_in(crac, &view, resolution, options.flow_model, held.as_ref());
let unit = options.objective_unit;
let score = match options.objective_kind.costly() {
Some(costly) => -costly.cost(
crac,
&result,
perimeter,
unit,
&options.activated,
&moved_range_actions(controls),
),
None => {
let margin = margin_of(crac, &result, perimeter, unit);
if margin.is_finite() {
margin
} else {
NO_CNEC_MARGIN
}
}
};
score - options.mnec.cost(crac, &result, perimeter, unit)
}
fn perimeter_flows(
crac: &Crac,
network: &NetworkMut<'_>,
resolution: &Resolution,
perimeter: &[State],
cnecs: &[usize],
controls: &[Control],
options: &LinearOptions,
) -> (Vec<f64>, Vec<(f64, f64, f64)>) {
let view = network.view();
let held = held_now(options, controls);
let result = evaluate_in(crac, &view, resolution, options.flow_model, held.as_ref());
cnecs
.iter()
.map(|&i| {
result
.perimeters
.iter()
.filter(|p| perimeter.contains(&p.state))
.find_map(|p| p.cnecs.iter().find(|c| c.cnec == i))
.map(|c| (c.flow_mw, (c.lower_mw, c.upper_mw, c.amperes_per_mw())))
.unwrap_or((0.0, (f64::NEG_INFINITY, f64::INFINITY, 0.0)))
})
.unzip()
}
pub fn base_flows(network: &Network<'_>) -> Vec<f64> {
let mut buses = network.buses.to_vec();
dc_power_flow(
&mut buses,
network.lines,
network.transformers,
DcOptions::default(),
)
.branch_p
}
#[cfg(test)]
mod chained_tests {
use super::*;
fn pst(tap: i32) -> PstControl {
PstControl {
branch: 0,
tap_to_angle: (-8..=8)
.map(|t| (t, -(f64::from(t) * 0.4 + f64::from(t * t) * 0.01)))
.collect(),
tap,
}
}
#[test]
fn a_chained_window_is_read_off_the_tap_table() {
let anchor = pst(3);
let (lo, hi) = chained_window(&anchor, (Some(-2), Some(2)));
let at = |t: i32| -(f64::from(t) * 0.4 + f64::from(t * t) * 0.01);
assert!((lo - (at(5) - at(3))).abs() < 1e-12, "lo was {lo}");
assert!((hi - (at(1) - at(3))).abs() < 1e-12, "hi was {hi}");
assert!(
(lo.abs() - hi.abs()).abs() > 1e-6,
"this table is nonuniform, so the window must not come out symmetric: {lo} .. {hi}"
);
}
#[test]
fn a_one_sided_chained_window_stays_one_sided() {
let anchor = pst(0);
let (lo, hi) = chained_window(&anchor, (None, Some(1)));
let at = |t: i32| -(f64::from(t) * 0.4 + f64::from(t * t) * 0.01);
assert!((lo - (at(1) - at(0))).abs() < 1e-12, "lo was {lo}");
assert!(
(hi - (at(-8) - at(0))).abs() < 1e-12,
"the open side should reach the table's end, was {hi}"
);
}
#[test]
fn an_unsatisfiable_chained_window_pins_rather_than_inverts() {
let anchor = pst(8);
let (lo, hi) = chained_window(&anchor, (Some(4), Some(6)));
assert_eq!((lo, hi), (0.0, 0.0));
}
#[test]
fn the_rounding_filter_rejects_a_pair_outside_the_window() {
let mut anchor = control();
anchor.reported = true;
anchor.pst = Some(pst(0));
let mut chained = control();
chained.reported = false;
chained.chained = Some((Some(-2), Some(2)));
chained.pst = Some(pst(0));
let controls = vec![anchor, chained];
assert!(chained_taps_hold(
&controls,
&[(0.0, Some(1)), (0.0, Some(-1))]
));
assert!(!chained_taps_hold(
&controls,
&[(0.0, Some(1)), (0.0, Some(-2))]
));
}
fn control() -> Control {
Control {
action: 0,
reported: true,
sensitivity: Vec::new(),
current: 0.0,
start: 0.0,
start_tap: Some(0),
variation: (0.0, 0.0),
chained: None,
activation: None,
lower: -10.0,
upper: 10.0,
penalty: 0.0,
pst: None,
injection: None,
}
}
}