use crate::ac_sensitivity::{AcSensitivity, Variable};
use crate::branch_flow::Terminal;
use super::{Invalidates, OuterLoop, OuterLoopContext, OuterLoopStatus};
pub const MIN_SENSITIVITY: f64 = 0.05;
pub const MAX_DIRECTION_CHANGES: usize = 3;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RegulationMode {
Voltage,
ActivePower { branch: usize, terminal: Terminal },
}
#[derive(Clone, Debug)]
pub struct TapRegulation {
pub transformer: usize,
pub controlled_bus: usize,
pub mode: RegulationMode,
pub target: f64,
pub deadband: f64,
pub enabled: bool,
pub id: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ControllerOutcome {
InDeadband,
AtLimit,
Closest,
Insensitive,
Hunting,
Unfinished,
}
#[derive(Clone, Debug)]
pub struct ControllerReport {
pub id: String,
pub transformer: usize,
pub controlled_bus: usize,
pub initial_position: i32,
pub final_position: i32,
pub steps_moved: usize,
pub direction_changes: usize,
pub outcome: ControllerOutcome,
}
#[derive(Clone, Debug, Default)]
pub struct TapControlReport {
pub controllers: Vec<ControllerReport>,
}
impl TapControlReport {
pub fn controller(&self, transformer: usize) -> Option<&ControllerReport> {
self.controllers
.iter()
.find(|c| c.transformer == transformer)
}
}
#[derive(Clone, Debug)]
struct ControllerState {
regulation_index: usize,
initial_position: i32,
steps_moved: usize,
direction_changes: usize,
last_direction: Option<bool>,
outcome: ControllerOutcome,
frozen: bool,
}
struct TapLoop {
max_tap_shift: i32,
states: Vec<ControllerState>,
initialized: bool,
}
impl TapLoop {
fn new(max_tap_shift: i32) -> Self {
Self {
max_tap_shift: max_tap_shift.max(1),
states: Vec::new(),
initialized: false,
}
}
fn initialize(&mut self, ctx: &mut OuterLoopContext<'_, '_>, mine: fn(&TapRegulation) -> bool) {
if self.initialized {
return;
}
self.initialized = true;
for (i, reg) in ctx.net.regulation.iter().enumerate() {
if !reg.enabled || !mine(reg) {
continue;
}
let Some(Some(tc)) = ctx.net.tap_changers.get(reg.transformer) else {
continue;
};
self.states.push(ControllerState {
regulation_index: i,
initial_position: tc.position,
steps_moved: 0,
direction_changes: 0,
last_direction: None,
outcome: ControllerOutcome::Unfinished,
frozen: false,
});
}
}
fn report(
&self,
regulation: &[TapRegulation],
tap_changers: &[Option<crate::types::TapChanger>],
) -> TapControlReport {
TapControlReport {
controllers: self
.states
.iter()
.map(|s| {
let reg = ®ulation[s.regulation_index];
ControllerReport {
id: reg.id.clone(),
transformer: reg.transformer,
controlled_bus: reg.controlled_bus,
initial_position: s.initial_position,
final_position: tap_changers[reg.transformer]
.as_ref()
.map(|t| t.position)
.unwrap_or(s.initial_position),
steps_moved: s.steps_moved,
direction_changes: s.direction_changes,
outcome: s.outcome,
}
})
.collect(),
}
}
fn move_to(
&mut self,
state_idx: usize,
wanted: i32,
saturated: bool,
ctx: &mut OuterLoopContext<'_, '_>,
) -> bool {
let reg_idx = self.states[state_idx].regulation_index;
let transformer = ctx.net.regulation[reg_idx].transformer;
let Some(Some(tc)) = ctx.net.tap_changers.get(transformer) else {
return false;
};
let current = tc.position;
let low = tc.low;
let high = tc.high();
let capped = wanted.clamp(current - self.max_tap_shift, current + self.max_tap_shift);
let target = capped.clamp(low, high);
if target == current {
self.states[state_idx].outcome = if saturated {
ControllerOutcome::AtLimit
} else {
ControllerOutcome::Closest
};
self.states[state_idx].frozen = true;
return false;
}
let up = target > current;
if let Some(previous) = self.states[state_idx].last_direction {
if previous != up {
self.states[state_idx].direction_changes += 1;
if self.states[state_idx].direction_changes >= MAX_DIRECTION_CHANGES {
self.states[state_idx].outcome = ControllerOutcome::Hunting;
self.states[state_idx].frozen = true;
return false;
}
}
}
let tc = ctx.net.tap_changers[transformer]
.as_mut()
.expect("checked above");
if !tc.set_position(&mut ctx.net.transformers[transformer], target) {
return false;
}
self.states[state_idx].last_direction = Some(up);
self.states[state_idx].steps_moved += (target - current).unsigned_abs() as usize;
true
}
}
pub struct TransformerVoltageControl {
inner: TapLoop,
}
impl Default for TransformerVoltageControl {
fn default() -> Self {
Self {
inner: TapLoop::new(3),
}
}
}
impl TransformerVoltageControl {
pub fn new() -> Self {
Self::default()
}
pub fn max_tap_shift(mut self, steps: i32) -> Self {
self.inner.max_tap_shift = steps.max(1);
self
}
pub fn report(
&self,
ctx_regulation: &[TapRegulation],
tap_changers: &[Option<crate::types::TapChanger>],
) -> TapControlReport {
self.inner.report(ctx_regulation, tap_changers)
}
}
fn is_voltage(reg: &TapRegulation) -> bool {
matches!(reg.mode, RegulationMode::Voltage)
}
impl OuterLoop for TransformerVoltageControl {
fn name(&self) -> &'static str {
"TransformerVoltageControl"
}
fn invalidates(&self) -> Invalidates {
Invalidates::Admittances
}
fn initialize(&mut self, ctx: &mut OuterLoopContext<'_, '_>) {
self.inner.initialize(ctx, is_voltage);
}
fn check(&mut self, ctx: &mut OuterLoopContext<'_, '_>) -> OuterLoopStatus {
if self.inner.states.is_empty() {
return OuterLoopStatus::Stable;
}
let mut by_bus: Vec<(usize, Vec<usize>)> = Vec::new();
for (s, state) in self.inner.states.iter().enumerate() {
if state.frozen {
continue;
}
let bus = ctx.net.regulation[state.regulation_index].controlled_bus;
match by_bus.iter_mut().find(|(b, _)| *b == bus) {
Some((_, v)) => v.push(s),
None => by_bus.push((bus, vec![s])),
}
}
if by_bus.is_empty() {
return OuterLoopStatus::Stable;
}
let n_lines = ctx.net.lines.len();
let Some(sensi) = AcSensitivity::new(
ctx.net.buses,
ctx.net.ybus,
ctx.net.lines,
ctx.net.transformers,
) else {
return OuterLoopStatus::Failed(
"tap voltage control: the Jacobian is singular at the operating point".into(),
);
};
let mut moved = false;
for (bus, controllers) in by_bus {
let v = ctx.net.buses[bus].voltage_mag;
let reg0 = &ctx.net.regulation[self.inner.states[controllers[0]].regulation_index];
let target = reg0.target;
let half_deadband = reg0.deadband.abs() / 2.0;
let mut remaining = target - v;
if remaining.abs() <= half_deadband {
for &s in &controllers {
self.inner.states[s].outcome = ControllerOutcome::InDeadband;
}
continue;
}
let mut ranked: Vec<(usize, f64)> = Vec::new();
for &s in &controllers {
let t = ctx.net.regulation[self.inner.states[s].regulation_index].transformer;
let sens = sensi
.state_response(Variable::TransformerRatio(n_lines + t))
.map(|r| r.d_vmag[bus])
.unwrap_or(0.0);
if sens.abs() < MIN_SENSITIVITY {
self.inner.states[s].outcome = ControllerOutcome::Insensitive;
self.inner.states[s].frozen = true;
continue;
}
ranked.push((s, sens));
}
ranked.sort_by(|a, b| b.1.abs().total_cmp(&a.1.abs()));
for (s, sens) in ranked {
if remaining.abs() <= half_deadband {
self.inner.states[s].outcome = ControllerOutcome::InDeadband;
continue;
}
let t = ctx.net.regulation[self.inner.states[s].regulation_index].transformer;
let Some(Some(tc)) = ctx.net.tap_changers.get(t) else {
continue;
};
let Some(current_ratio) = tc.ratio(tc.position) else {
continue;
};
let wanted_ratio = current_ratio + remaining / sens;
let Some(wanted) = tc.nearest_to_ratio(wanted_ratio) else {
continue;
};
let (lo, hi) = tc
.steps
.iter()
.map(|t| t.norm())
.fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), r| {
(a.min(r), b.max(r))
});
let saturated = wanted_ratio < lo || wanted_ratio > hi;
let before = tc.position;
if self.inner.move_to(s, wanted, saturated, ctx) {
moved = true;
let tc = ctx.net.tap_changers[t].as_ref().expect("moved it");
let achieved =
(tc.ratio(tc.position).unwrap_or(current_ratio) - current_ratio) * sens;
remaining -= achieved;
let _ = before;
}
}
}
if moved {
OuterLoopStatus::Unstable
} else {
OuterLoopStatus::Stable
}
}
}
pub struct PhaseControl {
inner: TapLoop,
}
impl Default for PhaseControl {
fn default() -> Self {
Self {
inner: TapLoop::new(3),
}
}
}
impl PhaseControl {
pub fn new() -> Self {
Self::default()
}
pub fn max_tap_shift(mut self, steps: i32) -> Self {
self.inner.max_tap_shift = steps.max(1);
self
}
pub fn report(
&self,
regulation: &[TapRegulation],
tap_changers: &[Option<crate::types::TapChanger>],
) -> TapControlReport {
self.inner.report(regulation, tap_changers)
}
}
fn is_active_power(reg: &TapRegulation) -> bool {
matches!(reg.mode, RegulationMode::ActivePower { .. })
}
impl OuterLoop for PhaseControl {
fn name(&self) -> &'static str {
"PhaseControl"
}
fn invalidates(&self) -> Invalidates {
Invalidates::Admittances
}
fn initialize(&mut self, ctx: &mut OuterLoopContext<'_, '_>) {
self.inner.initialize(ctx, is_active_power);
}
fn check(&mut self, ctx: &mut OuterLoopContext<'_, '_>) -> OuterLoopStatus {
if self.inner.states.is_empty() {
return OuterLoopStatus::Stable;
}
let mut groups: Vec<((usize, Terminal), Vec<usize>)> = Vec::new();
for s in 0..self.inner.states.len() {
if self.inner.states[s].frozen {
continue;
}
let reg = &ctx.net.regulation[self.inner.states[s].regulation_index];
let RegulationMode::ActivePower { branch, terminal } = reg.mode else {
continue;
};
match groups.iter_mut().find(|(k, _)| *k == (branch, terminal)) {
Some((_, v)) => v.push(s),
None => groups.push(((branch, terminal), vec![s])),
}
}
if groups.is_empty() {
return OuterLoopStatus::Stable;
}
let n_lines = ctx.net.lines.len();
let Some(sensi) = AcSensitivity::new(
ctx.net.buses,
ctx.net.ybus,
ctx.net.lines,
ctx.net.transformers,
) else {
return OuterLoopStatus::Failed(
"tap phase control: the Jacobian is singular at the operating point".into(),
);
};
let params = crate::branch_flow::branch_params(ctx.net.lines, ctx.net.transformers);
let v = crate::branch_flow::bus_voltages(ctx.net.buses);
let mut moved = false;
for ((branch, terminal), controllers) in groups {
let Some(bp) = params.get(branch) else {
continue;
};
let (p, _) = crate::branch_flow::terminal_flow(bp, terminal, &v);
let reg0 = &ctx.net.regulation[self.inner.states[controllers[0]].regulation_index];
let target = reg0.target;
let half_deadband = reg0.deadband.abs() / 2.0;
let mut remaining = target - p;
if remaining.abs() <= half_deadband {
for &s in &controllers {
self.inner.states[s].outcome = ControllerOutcome::InDeadband;
}
continue;
}
let mut ranked: Vec<(usize, f64)> = Vec::new();
for &s in &controllers {
let t = ctx.net.regulation[self.inner.states[s].regulation_index].transformer;
let sens = sensi
.branch_response(Variable::PhaseShift(n_lines + t), terminal)
.and_then(|r| r.get(branch).map(|(dp, _)| *dp))
.unwrap_or(0.0);
if sens.abs() < MIN_SENSITIVITY {
self.inner.states[s].outcome = ControllerOutcome::Insensitive;
self.inner.states[s].frozen = true;
continue;
}
ranked.push((s, sens));
}
ranked.sort_by(|a, b| b.1.abs().total_cmp(&a.1.abs()));
for (s, sens) in ranked {
if remaining.abs() <= half_deadband {
self.inner.states[s].outcome = ControllerOutcome::InDeadband;
continue;
}
let t = ctx.net.regulation[self.inner.states[s].regulation_index].transformer;
let Some(Some(tc)) = ctx.net.tap_changers.get(t) else {
continue;
};
let Some(current_angle) = tc.angle_deg(tc.position) else {
continue;
};
let wanted_angle = current_angle + (remaining / sens).to_degrees();
let Some(wanted) = tc.nearest_to_angle(wanted_angle) else {
continue;
};
let (lo, hi) = tc
.steps
.iter()
.map(|t| t.arg().to_degrees())
.fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), r| {
(a.min(r), b.max(r))
});
let saturated = wanted_angle < lo || wanted_angle > hi;
if self.inner.move_to(s, wanted, saturated, ctx) {
moved = true;
let tc = ctx.net.tap_changers[t].as_ref().expect("moved it");
let achieved = (tc.angle_deg(tc.position).unwrap_or(current_angle)
- current_angle)
.to_radians()
* sens;
remaining -= achieved;
}
}
}
if moved {
OuterLoopStatus::Unstable
} else {
OuterLoopStatus::Stable
}
}
}