use crate::branch_flow::{Terminal, branch_params, bus_voltages, terminal_flow};
use crate::network::{effective_injection, power_injections};
use crate::types::{Bus, BusType};
use super::{Invalidates, OuterLoop, OuterLoopContext, OuterLoopStatus};
#[derive(Clone, Debug)]
pub struct AreaDefinition {
pub of_bus: Vec<Option<usize>>,
pub targets: Vec<f64>,
pub factors: Vec<f64>,
pub tolerance: f64,
pub max_outer_iter: usize,
}
impl AreaDefinition {
pub fn uniform(buses: &[Bus], of_bus: Vec<Option<usize>>, n_areas: usize) -> Self {
let factors = buses
.iter()
.map(|b| match b.bus_type {
BusType::Slack | BusType::PV => 1.0,
BusType::PQ => 0.0,
})
.collect();
Self {
of_bus,
targets: vec![0.0; n_areas],
factors,
tolerance: 1e-8,
max_outer_iter: 30,
}
}
pub fn by_generation(buses: &[Bus], of_bus: Vec<Option<usize>>, n_areas: usize) -> Self {
let factors = buses.iter().map(|b| b.p_spec.max(0.0)).collect();
Self {
of_bus,
targets: vec![0.0; n_areas],
factors,
tolerance: 1e-8,
max_outer_iter: 30,
}
}
fn n_areas(&self) -> usize {
self.targets.len()
}
}
#[derive(Clone, Debug, Default)]
pub struct AreaInterchangeReport {
pub interchange: Vec<f64>,
pub residual: Vec<f64>,
pub dependent: Option<usize>,
pub shift: Vec<f64>,
pub outer_iterations: usize,
pub converged: bool,
pub unbalanced: Vec<(usize, &'static str)>,
}
#[derive(Clone, Debug)]
pub struct AreaInterchange {
areas: AreaDefinition,
report: AreaInterchangeReport,
exhausted: bool,
}
impl AreaInterchange {
pub fn new(areas: AreaDefinition) -> Self {
Self {
areas,
report: AreaInterchangeReport::default(),
exhausted: false,
}
}
pub fn report(&self) -> &AreaInterchangeReport {
&self.report
}
pub fn into_report(self) -> AreaInterchangeReport {
self.report
}
pub fn measure(
buses: &[Bus],
lines: &[crate::types::Line],
transformers: &[crate::types::Transformer],
of_bus: &[Option<usize>],
n_areas: usize,
) -> Vec<f64> {
let params = branch_params(lines, transformers);
let v = bus_voltages(buses);
let mut interchange = vec![0.0; n_areas];
for bp in ¶ms {
let (a_from, a_to) = (of_bus[bp.from], of_bus[bp.to]);
if a_from == a_to {
continue;
}
if let Some(a) = a_from {
interchange[a] += terminal_flow(bp, Terminal::From, &v).0;
}
if let Some(a) = a_to {
interchange[a] += terminal_flow(bp, Terminal::To, &v).0;
}
}
interchange
}
}
impl OuterLoop for AreaInterchange {
fn name(&self) -> &'static str {
"AreaInterchange"
}
fn invalidates(&self) -> Invalidates {
Invalidates::Nothing
}
fn initialize(&mut self, ctx: &mut OuterLoopContext<'_, '_>) {
let n = ctx.net.buses.len();
assert_eq!(
self.areas.of_bus.len(),
n,
"area assignment must carry one entry per bus"
);
assert_eq!(
self.areas.factors.len(),
n,
"participation factors must carry one weight per bus"
);
self.report.shift = vec![0.0; n];
self.report.interchange = vec![0.0; self.areas.n_areas()];
}
fn check(&mut self, ctx: &mut OuterLoopContext<'_, '_>) -> OuterLoopStatus {
if self.exhausted {
return OuterLoopStatus::Stable;
}
if ctx.net.lines.is_empty() && ctx.net.transformers.is_empty() {
return OuterLoopStatus::Failed(
"area interchange needs the branch lists; build the SolveContext with \
`with_branches`"
.into(),
);
}
self.report.outer_iterations += 1;
let n_areas = self.areas.n_areas();
let interchange = Self::measure(
ctx.net.buses,
ctx.net.lines,
ctx.net.transformers,
&self.areas.of_bus,
n_areas,
);
let (p_calc, _) = power_injections(ctx.net.buses, ctx.net.ybus);
let mut slack_delta = vec![0.0; n_areas];
let mut homeless_slack = false;
let mut dependent: Option<usize> = None;
for island in ctx.islands {
let [slack] = island.slack_indices.as_slice() else {
continue;
};
let delta = p_calc[*slack] - effective_injection(&ctx.net.buses[*slack]).0;
match self.areas.of_bus[*slack] {
Some(a) => {
slack_delta[a] += delta;
dependent = dependent.or(Some(a));
}
None => homeless_slack = homeless_slack || delta.abs() > self.areas.tolerance,
}
}
let mut unbalanced = Vec::new();
if homeless_slack {
unbalanced.push((
usize::MAX,
"the slack is in no area; nothing drives it to its schedule",
));
}
let mut residual = vec![0.0; n_areas];
let mut worst = 0.0f64;
let mut updates: Vec<(usize, f64)> = Vec::new();
for a in 0..n_areas {
residual[a] = interchange[a] - self.areas.targets[a];
let driven = if dependent == Some(a) {
-slack_delta[a]
} else {
residual[a]
};
let total: f64 = (0..ctx.net.buses.len())
.filter(|&i| self.areas.of_bus[i] == Some(a))
.map(|i| self.areas.factors[i].max(0.0))
.sum();
if total <= 0.0 {
if driven.abs() > self.areas.tolerance {
unbalanced.push((a, "no participating generator in this area"));
}
continue;
}
worst = worst.max(driven.abs());
for i in 0..ctx.net.buses.len() {
if self.areas.of_bus[i] != Some(a) {
continue;
}
let weight = self.areas.factors[i].max(0.0);
if weight > 0.0 {
updates.push((i, -weight / total * driven));
}
}
}
self.report.dependent = dependent;
self.report.interchange = interchange;
self.report.residual = residual;
self.report.unbalanced = unbalanced;
if worst <= self.areas.tolerance {
self.report.converged = true;
return OuterLoopStatus::Stable;
}
for (i, amount) in updates {
ctx.net.buses[i].p_spec += amount;
self.report.shift[i] += amount;
}
if self.report.outer_iterations >= self.areas.max_outer_iter.max(1) {
self.exhausted = true;
}
OuterLoopStatus::Unstable
}
}