use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contingency {
pub branch_idx: usize,
pub name: String,
pub from_bus: usize,
pub to_bus: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContingencyViolation {
pub contingency: Contingency,
pub overloaded_branch: usize,
pub pre_loading_pu: f64,
pub post_loading_pu: f64,
pub post_flow_pu: f64,
pub limit_pu: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContingencyResult {
pub violations: Vec<ContingencyViolation>,
pub n_contingencies: usize,
pub n_binding: usize,
pub worst_contingency: Option<String>,
pub worst_loading_pu: f64,
pub n_screened_out: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContingencyConfig {
pub loading_limit_pu: f64,
pub lodf_screen_threshold: f64,
pub pre_loading_alert_pu: f64,
pub max_violations_per_contingency: usize,
}
impl Default for ContingencyConfig {
fn default() -> Self {
Self {
loading_limit_pu: 1.0,
lodf_screen_threshold: 0.05,
pre_loading_alert_pu: 0.0,
max_violations_per_contingency: 5,
}
}
}
pub fn run_n1_contingency(
base_flows_pu: &[f64],
limits_pu: &[f64],
lodf_matrix: &[Vec<f64>],
contingencies: &[Contingency],
config: &ContingencyConfig,
) -> ContingencyResult {
let n_branches = base_flows_pu.len();
let mut violations = Vec::new();
let mut n_binding = 0;
let mut worst_loading = 0.0_f64;
let mut worst_name: Option<String> = None;
let mut n_screened_out = 0;
for cont in contingencies {
let k = cont.branch_idx;
if k >= n_branches {
continue;
}
let max_lodf = (0..n_branches)
.filter(|&i| i != k)
.map(|i| {
lodf_matrix
.get(i)
.and_then(|row| row.get(k))
.map(|&v| v.abs())
.unwrap_or(0.0)
})
.fold(0.0_f64, f64::max);
if max_lodf < config.lodf_screen_threshold {
n_screened_out += 1;
continue;
}
let f_k = base_flows_pu[k];
let mut cont_violations = Vec::new();
let mut cont_binding = false;
for i in 0..n_branches {
if i == k {
continue;
}
let lodf_ik = lodf_matrix
.get(i)
.and_then(|row| row.get(k))
.copied()
.unwrap_or(0.0);
let delta_f = lodf_ik * f_k;
let post_flow = base_flows_pu[i] + delta_f;
let limit = if i < limits_pu.len() {
limits_pu[i]
} else {
1.0
};
let post_loading = post_flow.abs() / limit.max(1e-9);
let pre_loading = base_flows_pu[i].abs() / limit.max(1e-9);
if post_loading > config.loading_limit_pu {
cont_binding = true;
if worst_loading < post_loading {
worst_loading = post_loading;
worst_name = Some(cont.name.clone());
}
cont_violations.push(ContingencyViolation {
contingency: cont.clone(),
overloaded_branch: i,
pre_loading_pu: pre_loading,
post_loading_pu: post_loading,
post_flow_pu: post_flow,
limit_pu: limit,
});
if cont_violations.len() >= config.max_violations_per_contingency {
break;
}
}
}
if cont_binding {
n_binding += 1;
}
violations.extend(cont_violations);
}
ContingencyResult {
violations,
n_contingencies: contingencies.len(),
n_binding,
worst_contingency: worst_name,
worst_loading_pu: worst_loading,
n_screened_out,
}
}
pub fn enumerate_n1(n_branches: usize) -> Vec<Contingency> {
(0..n_branches)
.map(|i| Contingency {
branch_idx: i,
name: format!("L{}", i + 1),
from_bus: 0,
to_bus: 0,
})
.collect()
}
pub fn rank_contingencies(
base_flows_pu: &[f64],
limits_pu: &[f64],
lodf_matrix: &[Vec<f64>],
) -> Vec<(usize, f64)> {
let n = base_flows_pu.len();
let mut rankings: Vec<(usize, f64)> = (0..n)
.map(|k| {
let f_k = base_flows_pu[k];
let max_post = (0..n)
.filter(|&i| i != k)
.map(|i| {
let lodf = lodf_matrix
.get(i)
.and_then(|r| r.get(k))
.copied()
.unwrap_or(0.0);
let limit = if i < limits_pu.len() {
limits_pu[i]
} else {
1.0
};
(base_flows_pu[i] + lodf * f_k).abs() / limit.max(1e-9)
})
.fold(0.0_f64, f64::max);
(k, max_post)
})
.collect();
rankings.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
rankings
}
#[cfg(test)]
mod tests {
use super::*;
fn make_lodf_3x3() -> Vec<Vec<f64>> {
vec![
vec![0.0, 0.5, -0.5], vec![0.5, 0.0, 0.5], vec![-0.5, 0.5, 0.0], ]
}
#[test]
fn test_n1_no_violations_low_loading() {
let flows = vec![0.3, 0.2, 0.1];
let limits = vec![1.0, 1.0, 1.0];
let lodf = make_lodf_3x3();
let conts = enumerate_n1(3);
let config = ContingencyConfig::default();
let result = run_n1_contingency(&flows, &limits, &lodf, &conts, &config);
assert_eq!(result.violations.len(), 0, "No violations for low loading");
}
#[test]
fn test_n1_violation_detected() {
let flows = vec![0.9, 0.8, 0.1]; let limits = vec![1.0, 1.0, 1.0];
let lodf = make_lodf_3x3();
let conts = enumerate_n1(3);
let config = ContingencyConfig {
lodf_screen_threshold: 0.01,
..Default::default()
};
let result = run_n1_contingency(&flows, &limits, &lodf, &conts, &config);
assert!(
!result.violations.is_empty(),
"Should detect violations with high loading"
);
}
#[test]
fn test_n1_worst_contingency_identified() {
let flows = vec![0.9, 0.3, 0.3];
let limits = vec![1.0, 1.0, 1.0];
let lodf = make_lodf_3x3();
let conts = vec![
Contingency {
branch_idx: 0,
name: "L1".to_string(),
from_bus: 0,
to_bus: 1,
},
Contingency {
branch_idx: 1,
name: "L2".to_string(),
from_bus: 1,
to_bus: 2,
},
];
let config = ContingencyConfig {
lodf_screen_threshold: 0.01,
..Default::default()
};
let result = run_n1_contingency(&flows, &limits, &lodf, &conts, &config);
if let Some(wc) = result.worst_contingency {
assert!(!wc.is_empty(), "Worst contingency should have a name");
}
}
#[test]
fn test_lodf_screening_reduces_computation() {
let flows = vec![0.5, 0.5, 0.5];
let limits = vec![1.0, 1.0, 1.0];
let lodf = make_lodf_3x3();
let conts = enumerate_n1(3);
let config = ContingencyConfig {
lodf_screen_threshold: 0.9,
..Default::default()
};
let result = run_n1_contingency(&flows, &limits, &lodf, &conts, &config);
assert!(
result.n_screened_out > 0,
"Should screen some contingencies"
);
}
#[test]
fn test_enumerate_n1_count() {
let conts = enumerate_n1(20);
assert_eq!(conts.len(), 20);
for (i, c) in conts.iter().enumerate() {
assert_eq!(c.branch_idx, i);
}
}
#[test]
fn test_rank_contingencies_sorted_descending() {
let flows = vec![0.5, 0.8, 0.2];
let limits = vec![1.0, 1.0, 1.0];
let lodf = make_lodf_3x3();
let rankings = rank_contingencies(&flows, &limits, &lodf);
assert_eq!(rankings.len(), 3);
for w in rankings.windows(2) {
assert!(w[0].1 >= w[1].1, "Rankings should be sorted descending");
}
}
#[test]
fn test_n1_empty_contingencies() {
let flows = vec![0.5; 5];
let limits = vec![1.0; 5];
let lodf = vec![vec![0.0; 5]; 5];
let result = run_n1_contingency(&flows, &limits, &lodf, &[], &ContingencyConfig::default());
assert_eq!(result.n_contingencies, 0);
assert_eq!(result.violations.len(), 0);
}
#[test]
fn test_n1_violation_fields_valid() {
let flows = vec![0.9, 0.8, 0.1];
let limits = vec![1.0, 1.0, 1.0];
let lodf = make_lodf_3x3();
let conts = enumerate_n1(3);
let config = ContingencyConfig {
lodf_screen_threshold: 0.01,
..Default::default()
};
let result = run_n1_contingency(&flows, &limits, &lodf, &conts, &config);
for v in &result.violations {
assert!(
v.post_loading_pu > 1.0,
"Violation loading should exceed limit"
);
assert!(v.limit_pu > 0.0);
}
}
#[test]
fn test_n1_result_n_binding_le_n_contingencies() {
let flows = vec![0.5, 0.5, 0.5];
let limits = vec![1.0, 1.0, 1.0];
let lodf = make_lodf_3x3();
let conts = enumerate_n1(3);
let result = run_n1_contingency(
&flows,
&limits,
&lodf,
&conts,
&ContingencyConfig::default(),
);
assert!(result.n_binding <= result.n_contingencies);
}
}