use serde::{Deserialize, Serialize};
use tracing::debug;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OltcControl {
pub branch_index: usize,
pub bus_regulated: usize,
pub v_target: f64,
pub v_band: f64,
pub tap_min: f64,
pub tap_max: f64,
pub tap_step: f64,
}
impl OltcControl {
pub fn standard(branch_index: usize, bus_regulated: usize, v_target: f64) -> Self {
debug!(
branch_index,
bus_regulated, v_target, "creating standard OLTC control"
);
Self {
branch_index,
bus_regulated,
v_target,
v_band: 0.01, tap_min: 0.9,
tap_max: 1.1,
tap_step: 0.00625, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SwitchedShunt {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub id: String,
pub bus: u32,
pub bus_regulated: u32,
pub b_step: f64,
pub n_steps_cap: i32,
pub n_steps_react: i32,
pub v_target: f64,
pub v_band: f64,
pub n_active_steps: i32,
}
impl SwitchedShunt {
pub fn capacitor_only(bus: u32, b_total_cap_pu: f64, n_steps: i32, v_target: f64) -> Self {
let b_step = if n_steps > 0 {
b_total_cap_pu / n_steps as f64
} else {
b_total_cap_pu
};
Self {
id: String::new(),
bus,
bus_regulated: bus,
b_step,
n_steps_cap: n_steps,
n_steps_react: 0,
v_target,
v_band: 0.02,
n_active_steps: 0,
}
}
#[inline]
pub fn b_injected(&self) -> f64 {
let b = self.b_step * self.n_active_steps as f64;
debug!(
bus = self.bus,
n_active_steps = self.n_active_steps,
b_injected = b,
"switched shunt reactive injection"
);
b
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OltcSpec {
pub from_bus: u32,
pub to_bus: u32,
pub circuit: String,
pub regulated_bus: u32,
pub v_target: f64,
pub v_band: f64,
pub tap_min: f64,
pub tap_max: f64,
pub tap_step: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParSpec {
pub from_bus: u32,
pub to_bus: u32,
pub circuit: String,
pub monitored_from_bus: u32,
pub monitored_to_bus: u32,
pub monitored_circuit: String,
pub p_target_mw: f64,
pub p_band_mw: f64,
#[serde(alias = "ang_min_deg")]
pub angle_min_deg: f64,
#[serde(alias = "ang_max_deg")]
pub angle_max_deg: f64,
pub ang_step_deg: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParControl {
pub branch_index: usize,
pub monitored_branch_index: usize,
pub p_target_mw: f64,
pub p_band_mw: f64,
#[serde(alias = "ang_min_deg")]
pub angle_min_deg: f64,
#[serde(alias = "ang_max_deg")]
pub angle_max_deg: f64,
pub ang_step_deg: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SwitchedShuntOpf {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub id: String,
pub bus: u32,
pub b_min_pu: f64,
pub b_max_pu: f64,
pub b_init_pu: f64,
pub b_step_pu: f64,
}
impl SwitchedShuntOpf {
pub fn round_to_steps(&self, b_val: f64) -> f64 {
if self.b_step_pu <= 0.0 {
return b_val.clamp(self.b_min_pu, self.b_max_pu);
}
let clamped = b_val.clamp(self.b_min_pu, self.b_max_pu);
let n_steps = ((clamped - self.b_min_pu) / self.b_step_pu).round() as i64;
(self.b_min_pu + n_steps as f64 * self.b_step_pu).clamp(self.b_min_pu, self.b_max_pu)
}
}
pub fn round_tap(tap: f64, tap_min: f64, tap_max: f64, tap_step: f64) -> f64 {
if tap_step <= 0.0 {
return tap.clamp(tap_min, tap_max);
}
let clamped = tap.clamp(tap_min, tap_max);
let n = ((clamped - tap_min) / tap_step).round() as i64;
(tap_min + n as f64 * tap_step).clamp(tap_min, tap_max)
}
pub fn round_phase(shift_rad: f64, min_rad: f64, max_rad: f64, step_deg: f64) -> f64 {
if step_deg <= 0.0 {
return shift_rad.clamp(min_rad, max_rad);
}
let step_rad = step_deg.to_radians();
let clamped = shift_rad.clamp(min_rad, max_rad);
let n = ((clamped - min_rad) / step_rad).round() as i64;
(min_rad + n as f64 * step_rad).clamp(min_rad, max_rad)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_round_tap_exact_step() {
let step = 0.00625;
assert!((round_tap(0.953, 0.9, 1.1, step) - 0.95).abs() < 1e-12);
assert!((round_tap(0.95, 0.9, 1.1, step) - 0.95).abs() < 1e-12);
assert!((round_tap(0.9535, 0.9, 1.1, step) - 0.95625).abs() < 1e-12);
}
#[test]
fn test_round_tap_continuous() {
assert!((round_tap(0.953, 0.9, 1.1, 0.0) - 0.953).abs() < 1e-12);
assert!((round_tap(0.85, 0.9, 1.1, 0.0) - 0.9).abs() < 1e-12);
assert!((round_tap(1.15, 0.9, 1.1, 0.0) - 1.1).abs() < 1e-12);
}
#[test]
fn test_round_phase_exact_step() {
let min_rad = (-30.0_f64).to_radians();
let max_rad = (30.0_f64).to_radians();
let step_deg = 1.0;
let val = (5.5_f64).to_radians();
let rounded = round_phase(val, min_rad, max_rad, step_deg);
let expected = (6.0_f64).to_radians();
assert!((rounded - expected).abs() < 1e-10);
}
#[test]
fn test_round_phase_continuous() {
let min_rad = (-30.0_f64).to_radians();
let max_rad = (30.0_f64).to_radians();
let val = (5.5_f64).to_radians();
assert!((round_phase(val, min_rad, max_rad, 0.0) - val).abs() < 1e-12);
}
}