use crate::types::{Bus, BusType, RegulatingMachine};
use super::{Invalidates, OuterLoop, OuterLoopContext, OuterLoopStatus};
const MIN_RESPONSE: f64 = 1e-4;
const MAX_STEP: f64 = 0.1;
#[derive(Clone, Debug, PartialEq)]
pub struct RemoteControlReport {
pub machine: String,
pub controller_bus: usize,
pub controlled_bus: usize,
pub target: f64,
pub reached: f64,
pub setpoint: f64,
pub moves: usize,
pub outcome: RemoteOutcome,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RemoteOutcome {
Held,
Insensitive,
Unfinished,
AtReactiveLimit,
}
struct Controller {
machine: String,
controller_bus: usize,
controlled_bus: usize,
target: f64,
moves: usize,
previous: Option<(f64, f64)>,
gain: f64,
outcome: RemoteOutcome,
}
pub struct RemoteVoltageControl {
controllers: Vec<Controller>,
machines: Vec<RegulatingMachine>,
deadband: f64,
retyped: Vec<(usize, BusType, BusType)>,
contested: Vec<String>,
}
impl RemoteVoltageControl {
pub fn new(machines: &[RegulatingMachine]) -> Self {
let controllers = machines
.iter()
.filter(|m| m.at_bus != m.controls_bus)
.map(|m| Controller {
machine: m.id.clone(),
controller_bus: m.at_bus,
controlled_bus: m.controls_bus,
target: m.target_pu,
moves: 0,
previous: None,
gain: 1.0,
outcome: RemoteOutcome::Unfinished,
})
.collect();
Self {
controllers,
machines: machines.to_vec(),
deadband: 2e-4,
retyped: Vec::new(),
contested: Vec::new(),
}
}
pub fn deadband(mut self, width: f64) -> Self {
self.deadband = width.abs().max(1e-9);
self
}
pub fn is_empty(&self) -> bool {
self.controllers.is_empty()
}
pub fn retyped(&self) -> &[(usize, BusType, BusType)] {
&self.retyped
}
pub fn contested(&self) -> &[String] {
&self.contested
}
pub fn reports(&self) -> Vec<RemoteControlReport> {
self.controllers
.iter()
.map(|c| RemoteControlReport {
machine: c.machine.clone(),
controller_bus: c.controller_bus,
controlled_bus: c.controlled_bus,
target: c.target,
reached: f64::NAN,
setpoint: f64::NAN,
moves: c.moves,
outcome: c.outcome,
})
.collect()
}
pub fn report_against(&self, buses: &[Bus]) -> Vec<RemoteControlReport> {
self.controllers
.iter()
.map(|c| RemoteControlReport {
machine: c.machine.clone(),
controller_bus: c.controller_bus,
controlled_bus: c.controlled_bus,
target: c.target,
reached: buses[c.controlled_bus].voltage_mag,
setpoint: buses[c.controller_bus].voltage_mag,
moves: c.moves,
outcome: c.outcome,
})
.collect()
}
}
impl OuterLoop for RemoteVoltageControl {
fn name(&self) -> &'static str {
"RemoteVoltageControl"
}
fn invalidates(&self) -> Invalidates {
Invalidates::Nothing
}
fn initialize(&mut self, ctx: &mut OuterLoopContext<'_, '_>) {
let buses = &mut *ctx.net.buses;
let mut keep: Vec<bool> = vec![true; self.controllers.len()];
for (i, c) in self.controllers.iter().enumerate() {
let held_locally = self.controllers.iter().any(|o| {
o.controller_bus == c.controlled_bus && o.controlled_bus == c.controlled_bus
});
if held_locally {
keep[i] = false;
self.contested.push(c.machine.clone());
}
}
let mut idx = 0;
self.controllers.retain(|_| {
idx += 1;
keep[idx - 1]
});
let capability = |bus: usize, machines: &[RegulatingMachine]| {
let here: Vec<&RegulatingMachine> =
machines.iter().filter(|m| m.at_bus == bus).collect();
if here.is_empty() {
return None;
}
Some((
here.iter().map(|m| m.q_min).sum::<f64>(),
here.iter().map(|m| m.q_max).sum::<f64>(),
))
};
for c in &self.controllers {
let was = buses[c.controlled_bus].bus_type;
if was == BusType::PV {
buses[c.controlled_bus].bus_type = BusType::PQ;
let (lo, hi) = capability(c.controlled_bus, &self.machines)
.unwrap_or((f64::NEG_INFINITY, f64::INFINITY));
buses[c.controlled_bus].q_min = lo;
buses[c.controlled_bus].q_max = hi;
self.retyped.push((c.controlled_bus, was, BusType::PQ));
}
}
for c in &self.controllers {
let was = buses[c.controller_bus].bus_type;
if was == BusType::PQ {
buses[c.controller_bus].bus_type = BusType::PV;
self.retyped.push((c.controller_bus, was, BusType::PV));
}
if was == BusType::Slack {
continue;
}
if let Some((lo, hi)) = capability(c.controller_bus, &self.machines) {
buses[c.controller_bus].q_min = lo;
buses[c.controller_bus].q_max = hi;
}
buses[c.controller_bus].voltage_mag = c.target;
}
}
fn check(&mut self, ctx: &mut OuterLoopContext<'_, '_>) -> OuterLoopStatus {
let half = self.deadband / 2.0;
let mut moved = false;
for c in &mut self.controllers {
if ctx.net.buses[c.controller_bus].bus_type != BusType::PV {
c.outcome = RemoteOutcome::AtReactiveLimit;
continue;
}
let reached = ctx.net.buses[c.controlled_bus].voltage_mag;
let error = c.target - reached;
if error.abs() <= half {
c.outcome = RemoteOutcome::Held;
continue;
}
let setpoint = ctx.net.buses[c.controller_bus].voltage_mag;
if let Some((prev_set, prev_reached)) = c.previous {
let d_set = setpoint - prev_set;
let d_reached = reached - prev_reached;
if d_set.abs() > 1e-12 {
let response = d_reached / d_set;
if response.abs() < MIN_RESPONSE {
c.outcome = RemoteOutcome::Insensitive;
continue;
}
c.gain = 1.0 / response;
}
}
let step = (c.gain * error).clamp(-MAX_STEP, MAX_STEP);
c.previous = Some((setpoint, reached));
ctx.net.buses[c.controller_bus].voltage_mag = setpoint + step;
c.moves += 1;
c.outcome = RemoteOutcome::Unfinished;
moved = true;
}
if moved {
OuterLoopStatus::Unstable
} else {
OuterLoopStatus::Stable
}
}
}