use crate::error::{OxiGridError, Result};
use crate::network::reduction::{build_b_bus, lodf_matrix, ptdf_matrix};
use crate::network::topology::PowerNetwork;
use crate::optimize::opf::dc_opf::{solve_dc_opf, DcOpfResult, GenCost};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContingencyViolation {
pub outage_branch: usize,
pub monitored_branch: usize,
pub post_flow_mw: f64,
pub limit_mw: f64,
pub lodf: f64,
}
impl ContingencyViolation {
pub fn loading_fraction(&self) -> f64 {
self.post_flow_mw.abs() / self.limit_mw
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScopfResult {
pub base_case: DcOpfResult,
pub violations: Vec<ContingencyViolation>,
pub is_n1_secure: bool,
pub contingencies_screened: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScopfConfig {
pub emergency_rating_fraction: f64,
pub flow_threshold_mw: f64,
}
impl Default for ScopfConfig {
fn default() -> Self {
Self {
emergency_rating_fraction: 1.0,
flow_threshold_mw: 0.1,
}
}
}
pub fn run_scopf(
network: &PowerNetwork,
gen_costs: &[GenCost],
config: &ScopfConfig,
) -> Result<ScopfResult> {
let n_branch = network.branch_count();
if n_branch == 0 {
return Err(OxiGridError::InvalidNetwork(
"No branches in network".into(),
));
}
let base_case = solve_dc_opf(network, gen_costs)?;
let base_flows = &base_case.branch_flows_mw;
let slack_idx = network.slack_bus_index()?;
let branch_from: Vec<usize> = network
.branches
.iter()
.filter(|b| b.status)
.map(|b| network.bus_index(b.from_bus).unwrap_or(0))
.collect();
let branch_to: Vec<usize> = network
.branches
.iter()
.filter(|b| b.status)
.map(|b| network.bus_index(b.to_bus).unwrap_or(0))
.collect();
let branch_x: Vec<f64> = network
.branches
.iter()
.filter(|b| b.status)
.map(|b| b.x)
.collect();
let b_bus = build_b_bus(network.bus_count(), &branch_from, &branch_to, &branch_x);
let ptdf = ptdf_matrix(&b_bus, &branch_from, &branch_to, &branch_x, slack_idx)?;
let lodf = lodf_matrix(&ptdf, &branch_from, &branch_to);
let mut violations = Vec::new();
let m = n_branch;
for k in 0..m {
let base_flow_k = if k < base_flows.len() {
base_flows[k]
} else {
0.0
};
if base_flow_k.abs() < config.flow_threshold_mw {
continue;
}
for l in 0..m {
if l == k {
continue;
} let base_flow_l = if l < base_flows.len() {
base_flows[l]
} else {
0.0
};
let lodf_lk = if l < lodf.len() && k < lodf[l].len() {
lodf[l][k]
} else {
0.0
};
let post_flow = base_flow_l + lodf_lk * base_flow_k;
let rate_a = network.branches[l].rate_a;
if rate_a < 1e-6 {
continue;
} let limit_mw = rate_a * config.emergency_rating_fraction;
if post_flow.abs() > limit_mw {
violations.push(ContingencyViolation {
outage_branch: k,
monitored_branch: l,
post_flow_mw: post_flow,
limit_mw,
lodf: lodf_lk,
});
}
}
}
let contingencies_screened = m;
let is_n1_secure = violations.is_empty();
Ok(ScopfResult {
base_case,
violations,
is_n1_secure,
contingencies_screened,
})
}
pub fn screen_contingencies(
network: &PowerNetwork,
base_case: &DcOpfResult,
contingency_branches: &[usize],
config: &ScopfConfig,
) -> Result<Vec<ContingencyViolation>> {
let slack_idx = network.slack_bus_index()?;
let branch_from: Vec<usize> = network
.branches
.iter()
.filter(|b| b.status)
.map(|b| network.bus_index(b.from_bus).unwrap_or(0))
.collect();
let branch_to: Vec<usize> = network
.branches
.iter()
.filter(|b| b.status)
.map(|b| network.bus_index(b.to_bus).unwrap_or(0))
.collect();
let branch_x: Vec<f64> = network
.branches
.iter()
.filter(|b| b.status)
.map(|b| b.x)
.collect();
let b_bus = build_b_bus(network.bus_count(), &branch_from, &branch_to, &branch_x);
let ptdf = ptdf_matrix(&b_bus, &branch_from, &branch_to, &branch_x, slack_idx)?;
let lodf = lodf_matrix(&ptdf, &branch_from, &branch_to);
let m = network.branch_count();
let base_flows = &base_case.branch_flows_mw;
let mut violations = Vec::new();
for &k in contingency_branches {
if k >= m {
continue;
}
let base_flow_k = if k < base_flows.len() {
base_flows[k]
} else {
0.0
};
for l in 0..m {
if l == k {
continue;
}
let base_flow_l = if l < base_flows.len() {
base_flows[l]
} else {
0.0
};
let lodf_lk = if l < lodf.len() && k < lodf[l].len() {
lodf[l][k]
} else {
0.0
};
let post_flow = base_flow_l + lodf_lk * base_flow_k;
let rate_a = network.branches[l].rate_a;
if rate_a < 1e-6 {
continue;
}
let limit_mw = rate_a * config.emergency_rating_fraction;
if post_flow.abs() > limit_mw {
violations.push(ContingencyViolation {
outage_branch: k,
monitored_branch: l,
post_flow_mw: post_flow,
limit_mw,
lodf: lodf_lk,
});
}
}
}
Ok(violations)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::network::PowerNetwork;
fn ieee14_network() -> PowerNetwork {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/ieee14.m");
PowerNetwork::from_matpower(path).expect("parse ieee14")
}
fn ieee14_costs(network: &PowerNetwork) -> Vec<GenCost> {
network
.generators
.iter()
.map(|g| GenCost::quadratic(0.0, 20.0, 0.05, g.pmin.max(0.0), g.pmax.max(10.0)))
.collect()
}
#[test]
fn test_scopf_base_case_solves() {
let net = ieee14_network();
let costs = ieee14_costs(&net);
let config = ScopfConfig::default();
let result = run_scopf(&net, &costs, &config);
assert!(result.is_ok(), "SCOPF should succeed: {:?}", result.err());
let r = result.unwrap();
assert!(r.base_case.total_cost > 0.0, "Base cost should be positive");
}
#[test]
fn test_scopf_screens_all_contingencies() {
let net = ieee14_network();
let costs = ieee14_costs(&net);
let config = ScopfConfig::default();
let r = run_scopf(&net, &costs, &config).unwrap();
assert!(r.contingencies_screened > 0);
assert!(r.contingencies_screened <= net.branch_count());
}
#[test]
fn test_scopf_violation_loading_fraction() {
let net = ieee14_network();
let costs = ieee14_costs(&net);
let config = ScopfConfig {
emergency_rating_fraction: 1.0,
flow_threshold_mw: 0.1,
};
let r = run_scopf(&net, &costs, &config).unwrap();
for v in &r.violations {
assert!(
v.loading_fraction() > 1.0,
"Violation should have loading > 1.0, got {:.3}",
v.loading_fraction()
);
}
}
#[test]
fn test_scopf_no_violations_with_zero_rating() {
let mut net = ieee14_network();
for b in &mut net.branches {
b.rate_a = 0.0;
}
let costs = ieee14_costs(&net);
let r = run_scopf(&net, &costs, &ScopfConfig::default()).unwrap();
assert!(r.is_n1_secure, "No ratings → no violations expected");
}
#[test]
fn test_screen_contingencies_subset() {
let net = ieee14_network();
let costs = ieee14_costs(&net);
let base = solve_dc_opf(&net, &costs).unwrap();
let viols = screen_contingencies(&net, &base, &[0, 1, 2], &ScopfConfig::default()).unwrap();
let _ = viols;
}
#[test]
fn test_scopf_is_n1_secure_flag_consistent() {
let net = ieee14_network();
let costs = ieee14_costs(&net);
let r = run_scopf(&net, &costs, &ScopfConfig::default()).unwrap();
assert_eq!(r.is_n1_secure, r.violations.is_empty());
}
}