use std::collections::HashMap;
use crate::batch::{BatchSolver, Scenario};
use crate::branch_flow::{Terminal, branch_params, bus_voltages, terminal_flow};
use crate::linear::DcOptions;
use crate::linear::btheta::{dc_branches, dc_power_flow};
use crate::linear::sensitivity::DcSensitivity;
use crate::network::ShuntAdm;
use crate::network::{build_ybus_with_outages, stamp_shunts};
use crate::outerloop::SlackDistribution;
use crate::ratings::current_to_power_pu;
use crate::solver::{IslandStatus, JacobianBackend};
use crate::types::{Bus, Line, Transformer};
use super::crac::{Crac, FlowCnec, Side, State, Threshold, Unit};
#[derive(Clone, Debug, Default)]
pub struct Resolution {
pub branch_of: HashMap<String, usize>,
pub bus_of: HashMap<String, usize>,
pub unresolved: Vec<String>,
}
impl Resolution {
pub fn new(crac: &Crac, branch_ids: &[String]) -> Self {
Self::with_buses(crac, branch_ids, &[])
}
pub fn with_buses(crac: &Crac, branch_ids: &[String], bus_ids: &[String]) -> Self {
Self::with_bus_aliases(crac, branch_ids, bus_ids, &[])
}
pub fn with_bus_aliases(
crac: &Crac,
branch_ids: &[String],
bus_ids: &[String],
aliases: &[(String, usize)],
) -> Self {
let mut exact: HashMap<&str, usize> = HashMap::with_capacity(branch_ids.len());
let mut trimmed: HashMap<&str, usize> = HashMap::with_capacity(branch_ids.len());
for (i, id) in branch_ids.iter().enumerate() {
exact.entry(id.as_str()).or_insert(i);
trimmed.entry(id.trim()).or_insert(i);
}
let mut bus_exact: HashMap<&str, usize> =
HashMap::with_capacity(bus_ids.len() + aliases.len());
let mut bus_trimmed: HashMap<&str, usize> =
HashMap::with_capacity(bus_ids.len() + aliases.len());
for (i, id) in bus_ids.iter().enumerate() {
bus_exact.entry(id.as_str()).or_insert(i);
bus_trimmed.entry(id.trim()).or_insert(i);
}
for (id, i) in aliases {
bus_exact.entry(id.as_str()).or_insert(*i);
bus_trimmed.entry(id.trim()).or_insert(*i);
}
let find_bus = |element: &str| -> Option<usize> {
if let Some(&i) = bus_exact
.get(element)
.or_else(|| bus_trimmed.get(element.trim()))
{
return Some(i);
}
for suffix in ["_generator", "_load"] {
if let Some(stem) = element.strip_suffix(suffix) {
if let Some(&i) = bus_exact.get(stem).or_else(|| bus_trimmed.get(stem.trim())) {
return Some(i);
}
}
}
None
};
let mut branch_of = HashMap::new();
let mut bus_of = HashMap::new();
let mut unresolved = Vec::new();
for element in crac.network_elements() {
if let Some(&i) = exact.get(element).or_else(|| trimmed.get(element.trim())) {
branch_of.insert(element.to_string(), i);
continue;
}
if let Some(i) = find_bus(element) {
bus_of.insert(element.to_string(), i);
continue;
}
unresolved.push(element.to_string());
}
Self {
branch_of,
bus_of,
unresolved,
}
}
pub fn branch(&self, element: &str) -> Option<usize> {
self.branch_of.get(element).copied()
}
pub fn bus(&self, element: &str) -> Option<usize> {
self.bus_of.get(element).copied()
}
pub fn is_complete(&self) -> bool {
self.unresolved.is_empty()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FlowModel {
#[default]
Dc,
Ac,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CnecResult {
pub cnec: usize,
pub branch: usize,
pub flow_mw: f64,
pub margin_mw: f64,
pub upper_mw: f64,
pub lower_mw: f64,
pub limit_mw: f64,
pub margin_a: f64,
pub conversion_v: f64,
pub current_a: f64,
pub side_two: (f64, f64),
}
impl CnecResult {
pub fn amperes_per_mw(&self) -> f64 {
if self.conversion_v > 0.0 {
1e6 / (3f64.sqrt() * self.conversion_v)
} else {
0.0
}
}
pub fn is_violated(&self) -> bool {
self.margin_mw < 0.0
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PerimeterResult {
pub state: State,
pub cnecs: Vec<CnecResult>,
pub severed: bool,
}
impl PerimeterResult {
pub fn min_margin(&self) -> Option<f64> {
self.cnecs.iter().map(|c| c.margin_mw).fold(None, |acc, m| {
Some(match acc {
Some(a) => f64::min(a, m),
None => m,
})
})
}
pub fn min_optimized_margin(&self, crac: &Crac) -> Option<f64> {
self.cnecs
.iter()
.filter(|c| crac.flow_cnecs[c.cnec].optimized)
.map(|c| c.margin_mw)
.fold(None, |acc, m| {
Some(match acc {
Some(a) => f64::min(a, m),
None => m,
})
})
}
pub fn violations(&self) -> impl Iterator<Item = &CnecResult> {
self.cnecs.iter().filter(|c| c.is_violated())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SecurityResult {
pub perimeters: Vec<PerimeterResult>,
pub skipped: Vec<usize>,
}
impl SecurityResult {
pub fn min_margin(&self) -> Option<f64> {
self.perimeters
.iter()
.filter_map(PerimeterResult::min_margin)
.fold(None, |acc, m| {
Some(match acc {
Some(a) => f64::min(a, m),
None => m,
})
})
}
pub fn is_secure(&self) -> bool {
self.min_margin().is_none_or(|m| m >= 0.0)
}
pub fn violations(&self) -> impl Iterator<Item = (&PerimeterResult, &CnecResult)> {
self.perimeters
.iter()
.flat_map(|p| p.violations().map(move |c| (p, c)))
}
}
pub struct Network<'a> {
pub buses: &'a [Bus],
pub lines: &'a [Line],
pub transformers: &'a [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 [ShuntAdm],
pub generation: &'a [f64],
pub tap_changers: &'a [Option<crate::types::TapChanger>],
pub base_mva: f64,
}
impl Network<'_> {
fn n_branches(&self) -> usize {
self.lines.len() + self.transformers.len()
}
fn branch_voltage(&self, branch: usize, dc: &[crate::linear::btheta::DcBranch]) -> Option<f64> {
let b = dc.iter().find(|b| b.index == branch)?;
Some(self.buses.get(b.from)?.u_rated)
}
fn branch_far_voltage(
&self,
branch: usize,
dc: &[crate::linear::btheta::DcBranch],
) -> Option<f64> {
let b = dc.iter().find(|b| b.index == branch)?;
Some(self.buses.get(b.to)?.u_rated)
}
}
struct Bounds {
upper: f64,
lower: f64,
u_upper: f64,
u_lower: f64,
upper_a: f64,
lower_a: f64,
}
impl Bounds {
fn unbounded(u_rated: f64) -> Self {
Self {
upper: f64::INFINITY,
lower: f64::NEG_INFINITY,
u_upper: u_rated,
u_lower: u_rated,
upper_a: f64::INFINITY,
lower_a: f64::NEG_INFINITY,
}
}
fn tighten(
&mut self,
lower: f64,
upper: f64,
voltage: f64,
expressed: f64,
frm: f64,
charge: f64,
) {
let per_amp = 3f64.sqrt() * expressed / 1e6;
let (upper_a, lower_a) = if per_amp > 0.0 {
((upper - frm) / per_amp, (lower + frm) / per_amp)
} else {
(f64::INFINITY, f64::NEG_INFINITY)
};
let upper = upper - frm - charge;
let lower = lower + frm + charge;
if upper < self.upper {
self.upper = upper;
self.u_upper = voltage;
self.upper_a = upper_a;
}
if lower > self.lower {
self.lower = lower;
self.u_lower = voltage;
self.lower_a = lower_a;
}
}
fn is_constraining(&self) -> bool {
self.upper.is_finite() || self.lower.is_finite()
}
fn margin(&self, flow_mw: f64) -> (f64, f64) {
let from_upper = self.upper - flow_mw;
let from_lower = flow_mw - self.lower;
if from_upper <= from_lower {
(from_upper, self.u_upper)
} else {
(from_lower, self.u_lower)
}
}
fn expressed(&self, flow_mw: f64) -> f64 {
let volts = |bound_mw: f64, bound_a: f64| {
if bound_a.is_finite() && bound_a.abs() > 0.0 {
bound_mw.abs() / bound_a.abs() * 1e6 / 3f64.sqrt()
} else {
0.0
}
};
if self.upper - flow_mw <= flow_mw - self.lower {
volts(self.upper, self.upper_a)
} else {
volts(self.lower, self.lower_a)
}
}
fn margin_amperes(&self, flow_mw: f64, flow_a: f64) -> f64 {
if self.upper - flow_mw <= flow_mw - self.lower {
self.upper_a - flow_a
} else {
flow_a - self.lower_a
}
}
fn magnitude(&self) -> f64 {
f64::min(self.upper.abs(), self.lower.abs())
}
}
fn threshold_bounds(
threshold: &Threshold,
cnec: &FlowCnec,
u_rated_v: f64,
s_base_va: f64,
) -> Option<(f64, f64, f64, f64)> {
if threshold.min.is_none() && threshold.max.is_none() {
return None;
}
let side = match threshold.side {
Side::Two => 1,
_ => 0,
};
let voltage = cnec
.nominal_v
.and_then(|v| v[side].or(v[0]))
.map(|kv| kv * 1000.0)
.filter(|v| *v > 0.0)
.unwrap_or(u_rated_v);
let scale = match threshold.unit {
Unit::Megawatt => 1.0,
Unit::Ampere => current_to_power_pu(1.0, voltage, s_base_va) * s_base_va / 1e6,
Unit::PercentImax => {
let i_max = cnec.i_max?[side].or(cnec.i_max?[0])?;
current_to_power_pu(i_max, voltage, s_base_va) * s_base_va / 1e6
}
Unit::Degree | Unit::Kilovolt => return None,
};
let lower = threshold.min.map_or(f64::NEG_INFINITY, |v| v * scale);
let upper = threshold.max.map_or(f64::INFINITY, |v| v * scale);
let expressed = match threshold.unit {
Unit::Megawatt if u_rated_v > 0.0 => u_rated_v,
_ => voltage,
};
Some((lower, upper, voltage, expressed))
}
pub fn evaluate(crac: &Crac, network: &Network<'_>, resolution: &Resolution) -> SecurityResult {
evaluate_with(crac, network, resolution, network.initially_open)
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Held {
pub open: Vec<usize>,
pub close: Vec<usize>,
pub taps: Vec<(usize, i32, f64)>,
pub states: Vec<State>,
}
impl Held {
pub fn transformers_of(&self, network: &Network<'_>) -> Vec<crate::types::Transformer> {
let mut out = network.transformers.to_vec();
for &(branch, tap, angle_deg) in &self.taps {
let Some(i) = branch.checked_sub(network.lines.len()) else {
continue;
};
let from_network = network
.tap_changers
.get(i)
.and_then(|c| c.as_ref())
.and_then(|c| c.at(tap));
let Some(slot) = out.get_mut(i) else { continue };
let ratio = slot.tap.norm();
slot.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()),
};
}
out
}
pub fn applied_to(&self, open: &[usize]) -> Vec<usize> {
let mut out: Vec<usize> = open
.iter()
.chain(self.open.iter())
.copied()
.filter(|b| !self.close.contains(b))
.collect();
out.sort_unstable();
out.dedup();
out
}
}
pub fn evaluate_split(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
open: &[usize],
held: Option<&Held>,
model: FlowModel,
ac: &AcOptions<'_>,
) -> SecurityResult {
let Some(held) = held.filter(|h| !h.states.is_empty()) else {
return evaluate_model(crac, network, resolution, open, model, ac);
};
let base = evaluate_model(crac, network, resolution, open, model, ac);
let transformers;
let stepped;
let other_network = if held.taps.is_empty() {
network
} else {
transformers = held.transformers_of(network);
stepped = Network {
transformers: &transformers,
..*network
};
&stepped
};
let other = evaluate_model(
crac,
other_network,
resolution,
&held.applied_to(open),
model,
ac,
);
let mut perimeters: Vec<PerimeterResult> = Vec::with_capacity(base.perimeters.len());
for perimeter in base.perimeters {
if held.states.contains(&perimeter.state)
&& let Some(from_held) = other.perimeters.iter().find(|p| p.state == perimeter.state)
{
perimeters.push(from_held.clone());
continue;
}
perimeters.push(perimeter);
}
SecurityResult {
perimeters,
skipped: base.skipped,
}
}
pub fn evaluate_model(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
open: &[usize],
model: FlowModel,
ac: &AcOptions<'_>,
) -> SecurityResult {
match model {
FlowModel::Dc => evaluate_with(crac, network, resolution, open),
FlowModel::Ac => evaluate_ac(crac, network, resolution, open, ac),
}
}
pub fn evaluate_with(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
open: &[usize],
) -> SecurityResult {
let s_base_va = network.base_mva * 1e6;
let options = DcOptions::default();
let dc = dc_branches(network.lines, network.transformers, options);
let mut buses = network.buses.to_vec();
let solution = dc_power_flow(&mut buses, network.lines, network.transformers, options);
let intact_flows = solution.branch_p;
let sensitivity = DcSensitivity::new(network.buses, &dc, network.n_branches());
let mut applied: Vec<usize> = open.to_vec();
applied.sort_unstable();
applied.dedup();
let (base_flows, base_severed) = if applied.is_empty() {
(intact_flows.clone(), false)
} else {
match sensitivity.as_ref().and_then(|s| {
(!s.is_breaking_set(&applied))
.then(|| s.multi_outage_flows(&intact_flows, &applied))
.flatten()
}) {
Some(f) => (f, false),
None => (outaged_flows(network, &applied, options), true),
}
};
let mut skipped = Vec::new();
let mut by_state: HashMap<State, Vec<usize>> = HashMap::new();
for (i, cnec) in crac.flow_cnecs.iter().enumerate() {
match resolution.branch(&cnec.network_element) {
Some(_) => by_state.entry(cnec.state.clone()).or_default().push(i),
None => skipped.push(i),
}
}
let mut perimeters = Vec::new();
for state in crac.states() {
let Some(indices) = by_state.get(&state) else {
continue;
};
let (flows, severed) = match state.contingency {
None => (base_flows.clone(), base_severed),
Some(c) => {
let mut outages: Vec<usize> = crac.contingencies[c]
.elements
.iter()
.filter_map(|e| resolution.branch(e))
.chain(applied.iter().copied())
.collect();
outages.sort_unstable();
outages.dedup();
if outages.is_empty() {
(base_flows.clone(), true)
} else {
match sensitivity.as_ref().and_then(|s| {
(!s.is_breaking_set(&outages))
.then(|| s.multi_outage_flows(&intact_flows, &outages))
.flatten()
}) {
Some(f) => (f, false),
None => (outaged_flows(network, &outages, options), true),
}
}
}
};
let mut cnecs = Vec::new();
for &i in indices {
let cnec = &crac.flow_cnecs[i];
let Some(branch) = resolution.branch(&cnec.network_element) else {
continue;
};
let Some(u_rated) = network.branch_voltage(branch, &dc) else {
continue;
};
let flow_mw = flows.get(branch).copied().unwrap_or(0.0) * network.base_mva;
let mut bound = Bounds::unbounded(u_rated);
for t in &cnec.thresholds {
let Some((lo, hi, v, expressed)) = threshold_bounds(t, cnec, u_rated, s_base_va)
else {
continue;
};
bound.tighten(lo, hi, v, expressed, cnec.reliability_margin, 0.0);
}
if !bound.is_constraining() {
continue;
}
let (margin_mw, u_bind) = bound.margin(flow_mw);
let current_a = to_amperes(flow_mw.abs(), bound.expressed(flow_mw));
let margin_a = bound.margin_amperes(flow_mw, current_a.copysign(flow_mw));
let u_far = network.branch_far_voltage(branch, &dc).unwrap_or(u_bind);
cnecs.push(CnecResult {
cnec: i,
branch,
flow_mw,
margin_mw,
upper_mw: bound.upper,
lower_mw: bound.lower,
limit_mw: bound.magnitude(),
conversion_v: u_bind,
margin_a,
current_a,
side_two: (flow_mw, to_amperes(flow_mw.abs(), u_far)),
});
}
perimeters.push(PerimeterResult {
state,
cnecs,
severed,
});
}
SecurityResult {
perimeters,
skipped,
}
}
pub fn ac_options<'a>(network: &Network<'a>) -> AcOptions<'a> {
AcOptions {
shunts: network.shunts,
distribute_slack: true,
slack_weights: network.generation,
..Default::default()
}
}
fn outaged_flows(network: &Network<'_>, outages: &[usize], options: DcOptions) -> Vec<f64> {
let mut lines = network.lines.to_vec();
let mut transformers = network.transformers.to_vec();
for &branch in outages {
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 buses = network.buses.to_vec();
dc_power_flow(&mut buses, &lines, &transformers, options).branch_p
}
#[derive(Clone, Copy, Debug)]
pub struct AcOptions<'a> {
pub shunts: &'a [ShuntAdm],
pub tol: f64,
pub max_iter: usize,
pub backend: JacobianBackend,
pub distribute_slack: bool,
pub slack_weights: &'a [f64],
}
impl Default for AcOptions<'_> {
fn default() -> Self {
Self {
shunts: &[],
tol: 1e-8,
max_iter: 30,
backend: JacobianBackend::default(),
distribute_slack: false,
slack_weights: &[],
}
}
}
pub fn evaluate_ac(
crac: &Crac,
network: &Network<'_>,
resolution: &Resolution,
open: &[usize],
ac: &AcOptions<'_>,
) -> SecurityResult {
let s_base_va = network.base_mva * 1e6;
let dc = dc_branches(network.lines, network.transformers, DcOptions::default());
let mut applied: Vec<usize> = open.to_vec();
applied.sort_unstable();
applied.dedup();
let mut skipped = Vec::new();
let mut by_state: HashMap<State, Vec<usize>> = HashMap::new();
for (i, cnec) in crac.flow_cnecs.iter().enumerate() {
match resolution.branch(&cnec.network_element) {
Some(_) => by_state.entry(cnec.state.clone()).or_default().push(i),
None => skipped.push(i),
}
}
let states: Vec<State> = crac
.states()
.into_iter()
.filter(|s| by_state.contains_key(s))
.collect();
let scenarios: Vec<Scenario> = states
.iter()
.map(|state| {
let mut outages = applied.clone();
if let Some(c) = state.contingency {
outages.extend(
crac.contingencies[c]
.elements
.iter()
.filter_map(|e| resolution.branch(e)),
);
}
outages.sort_unstable();
outages.dedup();
Scenario {
bus_overrides: Vec::new(),
branch_outages: outages,
}
})
.collect();
let reports = if ac.distribute_slack {
Ok(solve_distributing_slack(network, &scenarios, ac))
} else {
let solver = BatchSolver::new(ac.backend);
solver.solve_contingencies(
network.buses,
network.lines,
network.transformers,
ac.shunts,
&scenarios,
ac.tol,
ac.max_iter,
)
};
let Ok(reports) = reports else {
return SecurityResult {
perimeters: states
.into_iter()
.map(|state| PerimeterResult {
state,
cnecs: Vec::new(),
severed: true,
})
.collect(),
skipped,
};
};
let params = branch_params(network.lines, network.transformers);
let mut perimeters = Vec::new();
for ((state, scenario), report) in states.into_iter().zip(&scenarios).zip(&reports) {
let v = bus_voltages(&report.buses);
let severed = report
.islands
.iter()
.any(|i| i.status != IslandStatus::Converged);
let out: Vec<bool> = {
let mut out = vec![false; params.len()];
for &b in &scenario.branch_outages {
if let Some(slot) = out.get_mut(b) {
*slot = true;
}
}
out
};
let mut cnecs = Vec::new();
for &i in by_state.get(&state).into_iter().flatten() {
let cnec = &crac.flow_cnecs[i];
let Some(branch) = resolution.branch(&cnec.network_element) else {
continue;
};
let Some(u_rated) = network.branch_voltage(branch, &dc) else {
continue;
};
let Some(param) = params.get(branch) else {
continue;
};
let outaged = out.get(branch).copied().unwrap_or(false);
let ends = [Terminal::From, Terminal::To].map(|t| {
if outaged {
(0.0, 0.0, u_rated)
} else {
let (p, q) = terminal_flow(param, t, &v);
let bus = match t {
Terminal::From => param.from,
Terminal::To => param.to,
};
let u_actual = network
.buses
.get(bus)
.map(|b| b.u_rated * v.get(bus).map_or(1.0, |x| x.norm()))
.filter(|u| *u > 0.0)
.unwrap_or(u_rated);
(p * network.base_mva, q * network.base_mva, u_actual)
}
});
let flow_mw = ends[0].0;
let mut bound = Bounds::unbounded(u_rated);
for t in &cnec.thresholds {
let Some((lo, hi, u_written, expressed)) =
threshold_bounds(t, cnec, u_rated, s_base_va)
else {
continue;
};
let (p_mw, q_mvar, u_actual) = match t.side {
Side::Two => ends[1],
_ => ends[0],
};
let charge = match t.unit {
Unit::Ampere | Unit::PercentImax => {
let s_equiv = p_mw.hypot(q_mvar) * u_written / u_actual;
s_equiv - p_mw.abs()
}
Unit::Megawatt | Unit::Degree | Unit::Kilovolt => 0.0,
};
bound.tighten(
lo,
hi,
u_written,
expressed,
cnec.reliability_margin,
charge,
);
}
if !bound.is_constraining() {
continue;
}
let (p_mw, q_mvar, u_actual) = match cnec.thresholds.first().map(|t| t.side) {
Some(Side::Two) => ends[1],
_ => ends[0],
};
let current_a = if u_actual > 0.0 {
p_mw.hypot(q_mvar) * 1e6 / (3f64.sqrt() * u_actual)
} else {
0.0
};
let (margin_mw, u_bind) = bound.margin(flow_mw);
let margin_a = bound.margin_amperes(flow_mw, current_a.copysign(flow_mw));
let (p_two, q_two, u_two) = (-ends[1].0, -ends[1].1, ends[1].2);
cnecs.push(CnecResult {
cnec: i,
branch,
flow_mw,
margin_mw,
upper_mw: bound.upper,
lower_mw: bound.lower,
limit_mw: bound.magnitude(),
conversion_v: u_bind,
margin_a,
current_a,
side_two: (
p_two,
if u_two > 0.0 {
p_two.hypot(q_two) * 1e6 / (3f64.sqrt() * u_two)
} else {
0.0
},
),
});
}
perimeters.push(PerimeterResult {
state,
cnecs,
severed,
});
}
SecurityResult {
perimeters,
skipped,
}
}
fn solve_distributing_slack(
network: &Network<'_>,
scenarios: &[Scenario],
ac: &AcOptions<'_>,
) -> Vec<crate::PowerFlowReport> {
let n = network.buses.len();
let n_branches = network.n_branches();
let weights: Vec<f64> = if ac.slack_weights.len() == network.buses.len() {
network
.buses
.iter()
.zip(ac.slack_weights)
.map(|(b, w)| match b.bus_type {
crate::types::BusType::Slack | crate::types::BusType::PV => w.max(0.0),
crate::types::BusType::PQ => 0.0,
})
.collect()
} else {
network
.buses
.iter()
.map(|b| match b.bus_type {
crate::types::BusType::Slack | crate::types::BusType::PV => b.p_spec.max(0.0),
crate::types::BusType::PQ => 0.0,
})
.collect()
};
let distribution = if weights.iter().sum::<f64>() > 0.0 {
SlackDistribution::from_weights(weights)
} else {
SlackDistribution::uniform(network.buses)
};
scenarios
.iter()
.map(|scenario| {
let mut outaged = vec![false; n_branches];
for &b in &scenario.branch_outages {
if let Some(slot) = outaged.get_mut(b) {
*slot = true;
}
}
let mut ybus =
build_ybus_with_outages(n, network.lines, network.transformers, &outaged);
stamp_shunts(&mut ybus, ac.shunts);
let mut buses = network.buses.to_vec();
let mut ybus = ybus.finish();
let mut slack = crate::outerloop::DistributedSlack::new(distribution.clone());
let (islands, _) = {
let mut ctx = crate::outerloop::SolveContext::new(&mut buses, &mut ybus);
let mut list: Vec<&mut dyn crate::outerloop::OuterLoop> = vec![&mut slack];
crate::outerloop::solve_with_loops(
&mut ctx,
ac.tol,
ac.max_iter,
ac.backend,
&mut list,
distribution.max_outer_iter,
)
};
let converged = islands.iter().all(|i| i.status == IslandStatus::Converged);
crate::PowerFlowReport {
buses,
islands,
stats: crate::solver::SolveStats {
status: if converged {
crate::solver::SolveStatus::Converged
} else {
crate::solver::SolveStatus::MaxIterationsReached
},
mismatch_history: Vec::new(),
},
dc: None,
linear: None,
outer: None,
}
})
.collect()
}
fn to_amperes(power_mw: f64, voltage_v: f64) -> f64 {
if voltage_v > 0.0 {
power_mw * 1e6 / (3f64.sqrt() * voltage_v)
} else {
0.0
}
}