use crate::error::OxiGridError;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub enum VvoDevice {
OltcTransformer {
bus: usize,
min_tap: f64,
max_tap: f64,
tap_step: f64,
current_tap: f64,
time_delay_s: f64,
},
CapacitorBank {
bus: usize,
step_size_mvar: f64,
n_steps: usize,
current_steps: usize,
switchable: bool,
},
StaticVarCompensator {
bus: usize,
q_min_mvar: f64,
q_max_mvar: f64,
current_q_mvar: f64,
droop_pct: f64,
},
PhotovoltaicInverter {
bus: usize,
p_mw: f64,
s_rated_mva: f64,
power_factor_min: f64,
current_q_mvar: f64,
can_absorb_q: bool,
},
BatteryEss {
bus: usize,
p_mw: f64,
q_max_mvar: f64,
q_min_mvar: f64,
current_q_mvar: f64,
},
}
impl VvoDevice {
pub fn bus(&self) -> usize {
match self {
Self::OltcTransformer { bus, .. } => *bus,
Self::CapacitorBank { bus, .. } => *bus,
Self::StaticVarCompensator { bus, .. } => *bus,
Self::PhotovoltaicInverter { bus, .. } => *bus,
Self::BatteryEss { bus, .. } => *bus,
}
}
pub fn current_q_mvar(&self) -> f64 {
match self {
Self::OltcTransformer { .. } => 0.0,
Self::CapacitorBank {
step_size_mvar,
current_steps,
..
} => step_size_mvar * (*current_steps as f64),
Self::StaticVarCompensator { current_q_mvar, .. } => *current_q_mvar,
Self::PhotovoltaicInverter { current_q_mvar, .. } => *current_q_mvar,
Self::BatteryEss { current_q_mvar, .. } => *current_q_mvar,
}
}
pub fn q_range(&self) -> (f64, f64) {
match self {
Self::OltcTransformer { .. } => (0.0, 0.0),
Self::CapacitorBank {
step_size_mvar,
n_steps,
..
} => (0.0, step_size_mvar * (*n_steps as f64)),
Self::StaticVarCompensator {
q_min_mvar,
q_max_mvar,
..
} => (*q_min_mvar, *q_max_mvar),
Self::PhotovoltaicInverter {
p_mw,
s_rated_mva,
power_factor_min,
can_absorb_q,
..
} => {
let q_max_apparent = (s_rated_mva.powi(2) - p_mw.powi(2).min(s_rated_mva.powi(2)))
.max(0.0)
.sqrt();
let pf = power_factor_min.clamp(1e-6, 1.0);
let q_max_pf = p_mw * (1.0 - pf * pf).max(0.0).sqrt() / pf;
let q_max = q_max_apparent.min(q_max_pf).max(0.0);
let q_min = if *can_absorb_q { -q_max } else { 0.0 };
(q_min, q_max)
}
Self::BatteryEss {
q_min_mvar,
q_max_mvar,
..
} => (*q_min_mvar, *q_max_mvar),
}
}
pub fn device_type_name(&self) -> &str {
match self {
Self::OltcTransformer { .. } => "OLTC",
Self::CapacitorBank { .. } => "CapacitorBank",
Self::StaticVarCompensator { .. } => "SVC",
Self::PhotovoltaicInverter { .. } => "PV_Inverter",
Self::BatteryEss { .. } => "BESS",
}
}
fn is_discrete(&self) -> bool {
matches!(
self,
Self::OltcTransformer { .. } | Self::CapacitorBank { .. }
)
}
fn is_continuous(&self) -> bool {
matches!(
self,
Self::StaticVarCompensator { .. }
| Self::PhotovoltaicInverter { .. }
| Self::BatteryEss { .. }
)
}
}
#[derive(Debug, Clone)]
pub struct VvoBus {
pub id: usize,
pub v_min_pu: f64,
pub v_max_pu: f64,
pub p_load_mw: f64,
pub q_load_mvar: f64,
pub is_substation: bool,
}
#[derive(Debug, Clone)]
pub struct VvoBranch {
pub from: usize,
pub to: usize,
pub r_ohm: f64,
pub x_ohm: f64,
pub rating_mva: f64,
}
#[derive(Debug, Clone)]
pub struct VvoObjective {
pub weight_losses: f64,
pub weight_voltage_deviation: f64,
pub weight_device_operations: f64,
pub v_ref_pu: f64,
}
impl Default for VvoObjective {
fn default() -> Self {
Self {
weight_losses: 1.0,
weight_voltage_deviation: 0.5,
weight_device_operations: 0.1,
v_ref_pu: 1.0,
}
}
}
#[derive(Debug, Clone)]
pub struct VvoConfig {
pub n_buses: usize,
pub base_mva: f64,
pub base_kv: f64,
pub objective: VvoObjective,
pub max_iterations: usize,
pub voltage_tolerance: f64,
pub n_discrete_trials: usize,
}
impl Default for VvoConfig {
fn default() -> Self {
Self {
n_buses: 0,
base_mva: 100.0,
base_kv: 11.0,
objective: VvoObjective::default(),
max_iterations: 50,
voltage_tolerance: 1e-4,
n_discrete_trials: 10,
}
}
}
#[derive(Debug, Clone)]
pub struct VvoResult {
pub converged: bool,
pub iterations: usize,
pub voltage_magnitudes: Vec<f64>,
pub voltage_angles: Vec<f64>,
pub device_setpoints: Vec<f64>,
pub total_losses_mw: f64,
pub voltage_deviation_pu: f64,
pub n_tap_operations: usize,
pub n_capacitor_switches: usize,
pub objective_value: f64,
pub voltage_violations: Vec<(usize, f64)>,
pub branch_loadings_pct: Vec<f64>,
}
pub struct VoltVarOptimizer {
pub buses: Vec<VvoBus>,
pub branches: Vec<VvoBranch>,
pub devices: Vec<VvoDevice>,
pub config: VvoConfig,
}
impl VoltVarOptimizer {
pub fn new(
buses: Vec<VvoBus>,
branches: Vec<VvoBranch>,
devices: Vec<VvoDevice>,
config: VvoConfig,
) -> Self {
Self {
buses,
branches,
devices,
config,
}
}
pub fn optimize(&mut self) -> Result<VvoResult, OxiGridError> {
let n_buses = self.config.n_buses;
if n_buses == 0 {
return Err(OxiGridError::InvalidNetwork(
"VVO network has zero buses".into(),
));
}
let mut q_setpoints: Vec<f64> = self.devices.iter().map(|d| d.current_q_mvar()).collect();
let mut q_injections = self.q_setpoints_to_injections(&q_setpoints);
let (mut v, _) = self.solve_bfs(&q_injections)?;
let mut converged = false;
let mut iterations = 0usize;
let mut total_tap_ops = 0usize;
let mut total_cap_switches = 0usize;
let step_size_initial = 0.05_f64;
for iter in 0..self.config.max_iterations {
iterations = iter + 1;
let v_prev: Vec<f64> = v.clone();
let step_size = step_size_initial / (1.0 + iter as f64 * 0.05);
self.optimize_continuous_devices(&mut q_injections, &v, step_size);
for (idx, dev) in self.devices.iter().enumerate() {
if dev.is_continuous() {
let bus = dev.bus();
if bus < q_injections.len() {
let (lo, hi) = dev.q_range();
q_setpoints[idx] = q_injections[bus].max(lo).min(hi);
}
}
}
q_injections = self.q_setpoints_to_injections(&q_setpoints);
let (v_c, _) = self.solve_bfs(&q_injections)?;
v = v_c;
let (q_disc, tap_ops, cap_sw) = self.optimize_discrete_devices(&q_injections, &v);
total_tap_ops += tap_ops;
total_cap_switches += cap_sw;
q_injections = q_disc;
for (idx, dev) in self.devices.iter().enumerate() {
if dev.is_discrete() {
q_setpoints[idx] = dev.current_q_mvar();
}
}
let (v_d, _) = self.solve_bfs(&q_injections)?;
v = v_d;
let max_dv = v
.iter()
.zip(v_prev.iter())
.map(|(vi, vp)| (*vi - *vp).abs())
.fold(0.0_f64, f64::max);
if max_dv < self.config.voltage_tolerance {
converged = true;
break;
}
}
for (idx, dev) in self.devices.iter().enumerate() {
q_setpoints[idx] = dev.current_q_mvar();
}
q_injections = self.q_setpoints_to_injections(&q_setpoints);
let (v_final, angles_final) = self.solve_bfs(&q_injections)?;
let losses = self.compute_losses(&v_final, &q_injections);
let obj = self.compute_objective(&v_final, losses, &q_setpoints, &q_setpoints);
let n_buses_f = v_final.len() as f64;
let sum_sq: f64 = v_final
.iter()
.map(|vi| (vi - self.config.objective.v_ref_pu).powi(2))
.sum();
let v_dev = (sum_sq / n_buses_f.max(1.0)).sqrt();
let violations = self.check_violations(&v_final);
let branch_loadings = self.compute_branch_loading(&v_final, &q_injections);
Ok(VvoResult {
converged,
iterations,
voltage_magnitudes: v_final.to_vec(),
voltage_angles: angles_final,
device_setpoints: q_setpoints,
total_losses_mw: losses,
voltage_deviation_pu: v_dev,
n_tap_operations: total_tap_ops,
n_capacitor_switches: total_cap_switches,
objective_value: obj,
voltage_violations: violations,
branch_loadings_pct: branch_loadings,
})
}
pub fn solve_bfs(&self, q_injections: &[f64]) -> Result<(Vec<f64>, Vec<f64>), OxiGridError> {
let n = self.config.n_buses;
if n == 0 {
return Err(OxiGridError::InvalidNetwork("BFS: zero-bus network".into()));
}
let root = self
.buses
.iter()
.find(|b| b.is_substation)
.ok_or_else(|| OxiGridError::InvalidNetwork("No substation bus found".into()))?
.id;
let mut adj: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n];
for (br_idx, br) in self.branches.iter().enumerate() {
if br.from < n && br.to < n {
adj[br.from].push((br.to, br_idx));
adj[br.to].push((br.from, br_idx));
}
}
let mut order: Vec<usize> = Vec::with_capacity(n);
let mut parent: Vec<Option<usize>> = vec![None; n];
let mut parent_branch: Vec<Option<usize>> = vec![None; n];
let mut visited = vec![false; n];
let mut queue = VecDeque::new();
queue.push_back(root);
visited[root] = true;
while let Some(node) = queue.pop_front() {
order.push(node);
for &(nb, br_idx) in &adj[node] {
if !visited[nb] {
visited[nb] = true;
parent[nb] = Some(node);
parent_branch[nb] = Some(br_idx);
queue.push_back(nb);
}
}
}
let z_base = self.config.base_kv * self.config.base_kv / self.config.base_mva;
let mut p_sub = vec![0.0_f64; n];
let mut q_sub = vec![0.0_f64; n];
for bus in &self.buses {
if bus.id < n {
p_sub[bus.id] = bus.p_load_mw / self.config.base_mva;
let q_dev = q_injections.get(bus.id).copied().unwrap_or(0.0);
q_sub[bus.id] = (bus.q_load_mvar - q_dev) / self.config.base_mva;
}
}
for &bus in order.iter().rev() {
if bus == root {
continue;
}
if let Some(par) = parent[bus] {
let p_b = p_sub[bus];
let q_b = q_sub[bus];
p_sub[par] += p_b;
q_sub[par] += q_b;
}
}
let mut v = vec![1.0_f64; n];
v[root] = 1.0;
for &bus in order.iter() {
if bus == root {
continue;
}
if let (Some(par), Some(br_idx)) = (parent[bus], parent_branch[bus]) {
let br = &self.branches[br_idx];
let r_pu = br.r_ohm / z_base;
let x_pu = br.x_ohm / z_base;
let p_pu = p_sub[bus];
let q_pu = q_sub[bus];
let v_from = v[par].max(0.5);
let delta_v1 = (r_pu * p_pu + x_pu * q_pu) / v_from;
let delta_v2 = (r_pu.powi(2) + x_pu.powi(2)) * (p_pu.powi(2) + q_pu.powi(2))
/ (2.0 * v_from.powi(2));
v[bus] = (v_from - delta_v1 + delta_v2).max(0.5);
}
}
let angles = vec![0.0_f64; n];
Ok((v, angles))
}
fn compute_losses(&self, v: &[f64], q_injections: &[f64]) -> f64 {
let n = self.config.n_buses;
let z_base = self.config.base_kv * self.config.base_kv / self.config.base_mva;
let root = self
.buses
.iter()
.find(|b| b.is_substation)
.map(|b| b.id)
.unwrap_or(0);
let (order, parent, parent_branch) = self.build_bfs_tree(root, n);
let (p_sub, q_sub) = self.build_subtree_flows(n, root, &order, &parent, q_injections);
let mut total_loss_pu = 0.0_f64;
for &bus in &order {
if bus == root {
continue;
}
if let (Some(par), Some(br_idx)) = (parent[bus], parent_branch[bus]) {
let br = &self.branches[br_idx];
let r_pu = br.r_ohm / z_base;
let v_from = v.get(par).copied().unwrap_or(1.0).max(0.1);
total_loss_pu += (p_sub[bus].powi(2) + q_sub[bus].powi(2)) / v_from.powi(2) * r_pu;
}
}
total_loss_pu * self.config.base_mva
}
fn compute_objective(
&self,
v: &[f64],
losses_mw: f64,
q_setpoints_prev: &[f64],
q_setpoints_new: &[f64],
) -> f64 {
let obj = &self.config.objective;
let loss_term = obj.weight_losses * losses_mw;
let vdev_term = v.iter().map(|vi| (vi - obj.v_ref_pu).powi(2)).sum::<f64>()
* obj.weight_voltage_deviation;
let n_ops = q_setpoints_prev
.iter()
.zip(q_setpoints_new.iter())
.filter(|(prev, new)| (*prev - *new).abs() > 1e-6)
.count() as f64;
let ops_term = obj.weight_device_operations * n_ops;
loss_term + vdev_term + ops_term
}
fn compute_q_gradient(&self, v: &[f64], q_injections: &[f64]) -> Vec<f64> {
let n = self.config.n_buses;
let z_base = self.config.base_kv * self.config.base_kv / self.config.base_mva;
let obj = &self.config.objective;
let root = self
.buses
.iter()
.find(|b| b.is_substation)
.map(|b| b.id)
.unwrap_or(0);
let mut upstream_r = vec![0.0_f64; n];
let mut upstream_x = vec![0.0_f64; n];
let mut adj: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n];
for (br_idx, br) in self.branches.iter().enumerate() {
if br.from < n && br.to < n {
adj[br.from].push((br.to, br_idx));
adj[br.to].push((br.from, br_idx));
}
}
let mut visited = vec![false; n];
let mut queue = VecDeque::new();
queue.push_back(root);
visited[root] = true;
while let Some(node) = queue.pop_front() {
for &(nb, br_idx) in &adj[node] {
if !visited[nb] {
visited[nb] = true;
upstream_r[nb] = self.branches[br_idx].r_ohm / z_base;
upstream_x[nb] = self.branches[br_idx].x_ohm / z_base;
queue.push_back(nb);
}
}
}
let mut grad = vec![0.0_f64; n];
for i in 0..n {
let vi = v.get(i).copied().unwrap_or(1.0).max(0.1);
let qi_pu = q_injections.get(i).copied().unwrap_or(0.0) / self.config.base_mva;
let loss_grad = -2.0 * qi_pu * upstream_r[i] / vi.powi(2);
let dv_dq = upstream_x[i] / vi;
let vdev_grad = 2.0 * obj.weight_voltage_deviation * (vi - obj.v_ref_pu) * dv_dq;
grad[i] = loss_grad + vdev_grad;
}
grad
}
#[allow(clippy::ptr_arg)]
fn optimize_continuous_devices(&self, q_injections: &mut Vec<f64>, v: &[f64], step_size: f64) {
let grad = self.compute_q_gradient(v, q_injections);
for dev in &self.devices {
if !dev.is_continuous() {
continue;
}
let bus = dev.bus();
if bus >= q_injections.len() {
continue;
}
let (q_min, q_max) = dev.q_range();
let new_q = (q_injections[bus] - step_size * grad[bus])
.max(q_min)
.min(q_max);
q_injections[bus] = new_q;
}
}
fn evaluate_discrete_combination(
&self,
tap_positions: &[usize],
cap_steps: &[usize],
q_continuous: &[f64],
) -> (f64, Vec<f64>) {
let n = self.config.n_buses;
let mut q_inj = vec![0.0_f64; n];
for (i, &q) in q_continuous.iter().enumerate().take(n) {
q_inj[i] = q;
}
let mut oltc_idx = 0usize;
let mut cap_idx = 0usize;
for dev in &self.devices {
match dev {
VvoDevice::OltcTransformer {
bus,
min_tap,
tap_step,
..
} if *bus < n && oltc_idx < tap_positions.len() => {
let tap_val = min_tap + tap_positions[oltc_idx] as f64 * tap_step;
let bus_q_load = self
.buses
.iter()
.find(|b| b.id == *bus)
.map(|b| b.q_load_mvar)
.unwrap_or(0.0);
q_inj[*bus] += (tap_val - 1.0) * bus_q_load.abs().max(1.0);
oltc_idx += 1;
}
VvoDevice::CapacitorBank {
bus,
step_size_mvar,
..
} if *bus < n && cap_idx < cap_steps.len() => {
q_inj[*bus] += cap_steps[cap_idx] as f64 * step_size_mvar;
cap_idx += 1;
}
_ => {}
}
}
match self.solve_bfs(&q_inj) {
Ok((v, _)) => {
let losses = self.compute_losses(&v, &q_inj);
let sp: Vec<f64> = self.devices.iter().map(|d| d.current_q_mvar()).collect();
let obj = self.compute_objective(&v, losses, &sp, &sp);
(obj, v)
}
Err(_) => (f64::MAX, vec![1.0; n]),
}
}
fn optimize_discrete_devices(
&mut self,
q_continuous: &[f64],
_v: &[f64],
) -> (Vec<f64>, usize, usize) {
let n = self.config.n_buses;
let mut n_tap_ops = 0usize;
let mut n_cap_sw = 0usize;
let mut oltc_taps: Vec<usize> = Vec::new();
let mut cap_steps_vec: Vec<usize> = Vec::new();
let mut oltc_dev_indices: Vec<usize> = Vec::new();
let mut cap_dev_indices: Vec<usize> = Vec::new();
for (idx, dev) in self.devices.iter().enumerate() {
match dev {
VvoDevice::OltcTransformer {
min_tap,
max_tap,
tap_step,
current_tap,
..
} => {
let n_taps = ((*max_tap - *min_tap) / tap_step.max(1e-9)).round() as usize + 1;
let init_pos =
((*current_tap - *min_tap) / tap_step.max(1e-9)).round() as usize;
oltc_taps.push(init_pos.min(n_taps.saturating_sub(1)));
oltc_dev_indices.push(idx);
}
VvoDevice::CapacitorBank { current_steps, .. } => {
cap_steps_vec.push(*current_steps);
cap_dev_indices.push(idx);
}
_ => {}
}
}
for (i, &dev_idx) in oltc_dev_indices.iter().enumerate() {
if let VvoDevice::OltcTransformer {
min_tap,
max_tap,
tap_step,
..
} = &self.devices[dev_idx]
{
let n_taps = ((*max_tap - *min_tap) / tap_step.max(1e-9)).round() as usize + 1;
let orig_pos = oltc_taps[i];
let mut best_obj = f64::MAX;
let mut best_pos = orig_pos;
for pos in 0..n_taps {
let mut trial_taps = oltc_taps.clone();
trial_taps[i] = pos;
let (obj, _) = self.evaluate_discrete_combination(
&trial_taps,
&cap_steps_vec,
q_continuous,
);
if obj < best_obj {
best_obj = obj;
best_pos = pos;
}
}
if best_pos != orig_pos {
oltc_taps[i] = best_pos;
n_tap_ops += 1;
let min_t = *min_tap;
let step_t = *tap_step;
if let VvoDevice::OltcTransformer { current_tap, .. } =
&mut self.devices[dev_idx]
{
*current_tap = min_t + best_pos as f64 * step_t;
}
}
}
}
for (i, &dev_idx) in cap_dev_indices.iter().enumerate() {
if let VvoDevice::CapacitorBank {
n_steps,
switchable,
..
} = &self.devices[dev_idx]
{
if !switchable {
continue;
}
let n_st = *n_steps;
let orig_steps = cap_steps_vec[i];
let mut best_obj = f64::MAX;
let mut best_steps = orig_steps;
for s in 0..=n_st {
let mut trial_caps = cap_steps_vec.clone();
trial_caps[i] = s;
let (obj, _) =
self.evaluate_discrete_combination(&oltc_taps, &trial_caps, q_continuous);
if obj < best_obj {
best_obj = obj;
best_steps = s;
}
}
if best_steps != orig_steps {
cap_steps_vec[i] = best_steps;
n_cap_sw += 1;
if let VvoDevice::CapacitorBank { current_steps, .. } =
&mut self.devices[dev_idx]
{
*current_steps = best_steps;
}
}
}
}
let mut q_inj = vec![0.0_f64; n];
for (i, &q) in q_continuous.iter().enumerate().take(n) {
q_inj[i] = q;
}
let mut oltc_idx = 0usize;
let mut cap_idx = 0usize;
for dev in &self.devices {
match dev {
VvoDevice::OltcTransformer {
bus,
min_tap,
tap_step,
..
} if *bus < n && oltc_idx < oltc_taps.len() => {
let tap_val = min_tap + oltc_taps[oltc_idx] as f64 * tap_step;
let bus_q_load = self
.buses
.iter()
.find(|b| b.id == *bus)
.map(|b| b.q_load_mvar)
.unwrap_or(0.0);
q_inj[*bus] += (tap_val - 1.0) * bus_q_load.abs().max(1.0);
oltc_idx += 1;
}
VvoDevice::CapacitorBank {
bus,
step_size_mvar,
..
} if *bus < n && cap_idx < cap_steps_vec.len() => {
q_inj[*bus] += cap_steps_vec[cap_idx] as f64 * step_size_mvar;
cap_idx += 1;
}
_ => {}
}
}
(q_inj, n_tap_ops, n_cap_sw)
}
pub fn q_setpoints_to_injections(&self, setpoints: &[f64]) -> Vec<f64> {
let n = self.config.n_buses;
let mut q_inj = vec![0.0_f64; n];
for (idx, dev) in self.devices.iter().enumerate() {
let bus = dev.bus();
if bus < n && idx < setpoints.len() {
q_inj[bus] += setpoints[idx];
}
}
q_inj
}
pub fn compute_total_q_injections(&self, setpoints: &[f64]) -> Vec<f64> {
self.q_setpoints_to_injections(setpoints)
}
pub fn check_violations(&self, v: &[f64]) -> Vec<(usize, f64)> {
let mut violations = Vec::new();
for bus in &self.buses {
if bus.id < v.len() {
let vi = v[bus.id];
if vi < bus.v_min_pu || vi > bus.v_max_pu {
violations.push((bus.id, vi));
}
}
}
violations
}
pub fn compute_branch_loading(&self, v: &[f64], q_injections: &[f64]) -> Vec<f64> {
let n = self.config.n_buses;
let root = self
.buses
.iter()
.find(|b| b.is_substation)
.map(|b| b.id)
.unwrap_or(0);
let (order, parent, parent_branch) = self.build_bfs_tree(root, n);
let (p_sub, q_sub) = self.build_subtree_flows(n, root, &order, &parent, q_injections);
let mut loadings = vec![0.0_f64; self.branches.len()];
for &bus in &order {
if bus == root {
continue;
}
if let (Some(par), Some(br_idx)) = (parent[bus], parent_branch[bus]) {
let br = &self.branches[br_idx];
let v_from = v.get(par).copied().unwrap_or(1.0).max(0.1);
let p_pu = p_sub[bus];
let q_pu = q_sub[bus];
let i_mag = (p_pu.powi(2) + q_pu.powi(2)).sqrt() / v_from;
let s_mva = i_mag * v_from * self.config.base_mva;
loadings[br_idx] = if br.rating_mva > 0.0 {
100.0 * s_mva / br.rating_mva
} else {
0.0
};
}
}
loadings
}
fn build_bfs_tree(
&self,
root: usize,
n: usize,
) -> (Vec<usize>, Vec<Option<usize>>, Vec<Option<usize>>) {
let mut adj: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n];
for (br_idx, br) in self.branches.iter().enumerate() {
if br.from < n && br.to < n {
adj[br.from].push((br.to, br_idx));
adj[br.to].push((br.from, br_idx));
}
}
let mut order = Vec::with_capacity(n);
let mut parent: Vec<Option<usize>> = vec![None; n];
let mut parent_branch: Vec<Option<usize>> = vec![None; n];
let mut visited = vec![false; n];
let mut queue = VecDeque::new();
queue.push_back(root);
visited[root] = true;
while let Some(node) = queue.pop_front() {
order.push(node);
for &(nb, br_idx) in &adj[node] {
if !visited[nb] {
visited[nb] = true;
parent[nb] = Some(node);
parent_branch[nb] = Some(br_idx);
queue.push_back(nb);
}
}
}
(order, parent, parent_branch)
}
fn build_subtree_flows(
&self,
n: usize,
root: usize,
order: &[usize],
parent: &[Option<usize>],
q_injections: &[f64],
) -> (Vec<f64>, Vec<f64>) {
let mut p_sub = vec![0.0_f64; n];
let mut q_sub = vec![0.0_f64; n];
for bus in &self.buses {
if bus.id < n {
p_sub[bus.id] = bus.p_load_mw / self.config.base_mva;
let q_dev = q_injections.get(bus.id).copied().unwrap_or(0.0);
q_sub[bus.id] = (bus.q_load_mvar - q_dev) / self.config.base_mva;
}
}
for &bus in order.iter().rev() {
if bus == root {
continue;
}
if let Some(par) = parent[bus] {
let p_b = p_sub[bus];
let q_b = q_sub[bus];
p_sub[par] += p_b;
q_sub[par] += q_b;
}
}
(p_sub, q_sub)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_2bus_system() -> VoltVarOptimizer {
let buses = vec![
VvoBus {
id: 0,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 0.0,
q_load_mvar: 0.0,
is_substation: true,
},
VvoBus {
id: 1,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 1.0,
q_load_mvar: 0.5,
is_substation: false,
},
];
let branches = vec![VvoBranch {
from: 0,
to: 1,
r_ohm: 0.5,
x_ohm: 0.3,
rating_mva: 5.0,
}];
let config = VvoConfig {
n_buses: 2,
base_mva: 10.0,
base_kv: 11.0,
objective: VvoObjective::default(),
max_iterations: 50,
voltage_tolerance: 1e-4,
n_discrete_trials: 5,
};
VoltVarOptimizer::new(buses, branches, vec![], config)
}
fn make_3bus_system() -> VoltVarOptimizer {
let buses = vec![
VvoBus {
id: 0,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 0.0,
q_load_mvar: 0.0,
is_substation: true,
},
VvoBus {
id: 1,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 1.0,
q_load_mvar: 0.5,
is_substation: false,
},
VvoBus {
id: 2,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 0.8,
q_load_mvar: 0.3,
is_substation: false,
},
];
let branches = vec![
VvoBranch {
from: 0,
to: 1,
r_ohm: 0.5,
x_ohm: 0.3,
rating_mva: 5.0,
},
VvoBranch {
from: 1,
to: 2,
r_ohm: 0.4,
x_ohm: 0.2,
rating_mva: 5.0,
},
];
let config = VvoConfig {
n_buses: 3,
base_mva: 10.0,
base_kv: 11.0,
objective: VvoObjective::default(),
max_iterations: 50,
voltage_tolerance: 1e-4,
n_discrete_trials: 5,
};
VoltVarOptimizer::new(buses, branches, vec![], config)
}
#[test]
fn test_vvo_device_oltc_creation() {
let dev = VvoDevice::OltcTransformer {
bus: 2,
min_tap: 0.9,
max_tap: 1.1,
tap_step: 0.00625,
current_tap: 1.0,
time_delay_s: 30.0,
};
assert_eq!(dev.bus(), 2);
assert_eq!(dev.device_type_name(), "OLTC");
let (q_min, q_max) = dev.q_range();
assert_eq!(q_min, 0.0);
assert_eq!(q_max, 0.0);
assert_eq!(dev.current_q_mvar(), 0.0);
}
#[test]
fn test_vvo_device_capacitor_creation() {
let dev = VvoDevice::CapacitorBank {
bus: 3,
step_size_mvar: 0.5,
n_steps: 4,
current_steps: 2,
switchable: true,
};
assert_eq!(dev.bus(), 3);
assert_eq!(dev.device_type_name(), "CapacitorBank");
assert!((dev.current_q_mvar() - 1.0).abs() < 1e-9);
}
#[test]
fn test_vvo_device_pv_inverter_creation() {
let dev = VvoDevice::PhotovoltaicInverter {
bus: 5,
p_mw: 0.8,
s_rated_mva: 1.0,
power_factor_min: 0.9,
current_q_mvar: 0.1,
can_absorb_q: true,
};
assert_eq!(dev.bus(), 5);
assert_eq!(dev.device_type_name(), "PV_Inverter");
assert!((dev.current_q_mvar() - 0.1).abs() < 1e-9);
}
#[test]
fn test_vvo_device_bus() {
let devices = [
VvoDevice::OltcTransformer {
bus: 1,
min_tap: 0.9,
max_tap: 1.1,
tap_step: 0.00625,
current_tap: 1.0,
time_delay_s: 30.0,
},
VvoDevice::CapacitorBank {
bus: 2,
step_size_mvar: 0.5,
n_steps: 4,
current_steps: 0,
switchable: true,
},
VvoDevice::StaticVarCompensator {
bus: 3,
q_min_mvar: -2.0,
q_max_mvar: 2.0,
current_q_mvar: 0.0,
droop_pct: 5.0,
},
VvoDevice::PhotovoltaicInverter {
bus: 4,
p_mw: 1.0,
s_rated_mva: 1.5,
power_factor_min: 0.9,
current_q_mvar: 0.0,
can_absorb_q: true,
},
VvoDevice::BatteryEss {
bus: 5,
p_mw: 0.5,
q_max_mvar: 1.0,
q_min_mvar: -1.0,
current_q_mvar: 0.0,
},
];
assert_eq!(devices[0].bus(), 1);
assert_eq!(devices[1].bus(), 2);
assert_eq!(devices[2].bus(), 3);
assert_eq!(devices[3].bus(), 4);
assert_eq!(devices[4].bus(), 5);
}
#[test]
fn test_vvo_device_q_range_capacitor() {
let dev = VvoDevice::CapacitorBank {
bus: 0,
step_size_mvar: 0.25,
n_steps: 8,
current_steps: 0,
switchable: true,
};
let (q_min, q_max) = dev.q_range();
assert!((q_min - 0.0).abs() < 1e-9);
assert!((q_max - 2.0).abs() < 1e-9);
}
#[test]
fn test_vvo_device_q_range_svc() {
let dev = VvoDevice::StaticVarCompensator {
bus: 0,
q_min_mvar: -3.0,
q_max_mvar: 3.0,
current_q_mvar: 0.0,
droop_pct: 4.0,
};
let (q_min, q_max) = dev.q_range();
assert!((q_min - (-3.0)).abs() < 1e-9);
assert!((q_max - 3.0).abs() < 1e-9);
}
#[test]
fn test_vvo_bus_creation() {
let bus = VvoBus {
id: 7,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 2.5,
q_load_mvar: 1.2,
is_substation: false,
};
assert_eq!(bus.id, 7);
assert!((bus.v_min_pu - 0.95).abs() < 1e-9);
assert!((bus.v_max_pu - 1.05).abs() < 1e-9);
assert!((bus.p_load_mw - 2.5).abs() < 1e-9);
assert!(!bus.is_substation);
}
#[test]
fn test_vvo_branch_creation() {
let br = VvoBranch {
from: 0,
to: 1,
r_ohm: 0.3,
x_ohm: 0.15,
rating_mva: 10.0,
};
assert_eq!(br.from, 0);
assert_eq!(br.to, 1);
assert!((br.r_ohm - 0.3).abs() < 1e-9);
assert!((br.x_ohm - 0.15).abs() < 1e-9);
assert!((br.rating_mva - 10.0).abs() < 1e-9);
}
#[test]
fn test_vvo_config_creation() {
let cfg = VvoConfig {
n_buses: 10,
base_mva: 100.0,
base_kv: 33.0,
objective: VvoObjective::default(),
max_iterations: 30,
voltage_tolerance: 1e-5,
n_discrete_trials: 8,
};
assert_eq!(cfg.n_buses, 10);
assert!((cfg.base_mva - 100.0).abs() < 1e-9);
assert!((cfg.base_kv - 33.0).abs() < 1e-9);
assert_eq!(cfg.max_iterations, 30);
assert_eq!(cfg.n_discrete_trials, 8);
}
#[test]
fn test_bfs_single_branch() {
let opt = make_2bus_system();
let q_inj = vec![0.0, 0.0];
let (v, angles) = opt.solve_bfs(&q_inj).expect("BFS should converge");
assert_eq!(v.len(), 2);
assert_eq!(angles.len(), 2);
assert!((v[0] - 1.0).abs() < 1e-9, "Substation V must be 1.0 pu");
assert!(
v[1] < 1.0,
"Load bus V={} should drop below 1.0 due to line losses",
v[1]
);
assert!(v[1] > 0.5, "V1={} should be physically reasonable", v[1]);
}
#[test]
fn test_bfs_two_branches() {
let opt = make_3bus_system();
let q_inj = vec![0.0, 0.0, 0.0];
let (v, _) = opt.solve_bfs(&q_inj).expect("BFS should converge");
assert_eq!(v.len(), 3);
assert!((v[0] - 1.0).abs() < 1e-9, "Substation V should be 1.0 pu");
assert!(v[1] < 1.0, "V1={} should drop below 1.0", v[1]);
assert!(
v[2] < v[1],
"V2={} should be lower than V1={} (further from source)",
v[2],
v[1]
);
}
#[test]
fn test_compute_losses_zero_load() {
let mut opt = make_2bus_system();
opt.buses[1].p_load_mw = 0.0;
opt.buses[1].q_load_mvar = 0.0;
let q_inj = vec![0.0, 0.0];
let (v, _) = opt.solve_bfs(&q_inj).expect("BFS");
let losses = opt.compute_losses(&v, &q_inj);
assert!(
losses.abs() < 1e-9,
"Zero load → zero losses, got {}",
losses
);
}
#[test]
fn test_compute_losses_nonzero() {
let opt = make_2bus_system();
let q_inj = vec![0.0, 0.0];
let (v, _) = opt.solve_bfs(&q_inj).expect("BFS");
let losses = opt.compute_losses(&v, &q_inj);
assert!(
losses > 0.0,
"Nonzero load should produce positive losses, got {}",
losses
);
}
#[test]
fn test_compute_objective_zero_deviation() {
let opt = make_2bus_system();
let v = vec![1.0, 1.0]; let losses_mw = 0.1_f64;
let sp: Vec<f64> = vec![];
let obj = opt.compute_objective(&v, losses_mw, &sp, &sp);
assert!((obj - 0.1).abs() < 1e-9, "Expected 0.1, got {}", obj);
}
#[test]
fn test_q_gradient_direction() {
let opt = make_2bus_system();
let q_inj = vec![0.0, 2.0];
let (v, _) = opt.solve_bfs(&q_inj).expect("BFS");
let grad = opt.compute_q_gradient(&v, &q_inj);
assert_eq!(grad.len(), 2);
assert!(
grad[1] <= 0.1,
"Gradient at bus 1 should be non-positive for positive Q injection, got {}",
grad[1]
);
}
#[test]
fn test_optimize_continuous_devices_reduces_losses() {
let buses = vec![
VvoBus {
id: 0,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 0.0,
q_load_mvar: 0.0,
is_substation: true,
},
VvoBus {
id: 1,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 1.0,
q_load_mvar: 1.0,
is_substation: false,
},
];
let branches = vec![VvoBranch {
from: 0,
to: 1,
r_ohm: 0.5,
x_ohm: 0.3,
rating_mva: 5.0,
}];
let devices = vec![VvoDevice::StaticVarCompensator {
bus: 1,
q_min_mvar: -2.0,
q_max_mvar: 2.0,
current_q_mvar: 0.0,
droop_pct: 5.0,
}];
let config = VvoConfig {
n_buses: 2,
base_mva: 10.0,
base_kv: 11.0,
objective: VvoObjective::default(),
max_iterations: 20,
voltage_tolerance: 1e-4,
n_discrete_trials: 5,
};
let opt = VoltVarOptimizer::new(buses, branches, devices, config);
let q_inj_before = vec![0.0, 0.0];
let (v_before, _) = opt.solve_bfs(&q_inj_before).expect("BFS");
let losses_before = opt.compute_losses(&v_before, &q_inj_before);
let mut q_inj_after = q_inj_before.clone();
opt.optimize_continuous_devices(&mut q_inj_after, &v_before, 0.05);
let (v_after, _) = opt.solve_bfs(&q_inj_after).expect("BFS after");
let losses_after = opt.compute_losses(&v_after, &q_inj_after);
assert!(
losses_after <= losses_before + 1e-6,
"Losses after continuous opt ({}) should not exceed initial ({})",
losses_after,
losses_before
);
}
#[test]
fn test_q_setpoints_to_injections() {
let buses = vec![
VvoBus {
id: 0,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 0.0,
q_load_mvar: 0.0,
is_substation: true,
},
VvoBus {
id: 1,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 1.0,
q_load_mvar: 0.5,
is_substation: false,
},
];
let devices = vec![VvoDevice::CapacitorBank {
bus: 1,
step_size_mvar: 0.5,
n_steps: 4,
current_steps: 0,
switchable: true,
}];
let config = VvoConfig {
n_buses: 2,
base_mva: 10.0,
base_kv: 11.0,
objective: VvoObjective::default(),
max_iterations: 50,
voltage_tolerance: 1e-4,
n_discrete_trials: 5,
};
let opt = VoltVarOptimizer::new(buses, vec![], devices, config);
let setpoints = vec![1.5_f64];
let injections = opt.q_setpoints_to_injections(&setpoints);
assert_eq!(injections.len(), 2);
assert!((injections[0] - 0.0).abs() < 1e-9);
assert!((injections[1] - 1.5).abs() < 1e-9);
}
#[test]
fn test_optimize_2bus_with_capacitor() {
let buses = vec![
VvoBus {
id: 0,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 0.0,
q_load_mvar: 0.0,
is_substation: true,
},
VvoBus {
id: 1,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 1.0,
q_load_mvar: 0.8,
is_substation: false,
},
];
let branches = vec![VvoBranch {
from: 0,
to: 1,
r_ohm: 0.5,
x_ohm: 0.3,
rating_mva: 5.0,
}];
let devices = vec![VvoDevice::CapacitorBank {
bus: 1,
step_size_mvar: 0.25,
n_steps: 4,
current_steps: 0,
switchable: true,
}];
let config = VvoConfig {
n_buses: 2,
base_mva: 10.0,
base_kv: 11.0,
objective: VvoObjective::default(),
max_iterations: 30,
voltage_tolerance: 1e-4,
n_discrete_trials: 5,
};
let baseline_opt = make_2bus_system();
let q0 = vec![0.0, 0.0];
let (v0, _) = baseline_opt.solve_bfs(&q0).expect("BFS");
let losses_baseline = baseline_opt.compute_losses(&v0, &q0);
let mut opt = VoltVarOptimizer::new(buses, branches, devices, config);
let result = opt.optimize().expect("VVO should not error");
assert!(
result.iterations > 0,
"Should perform at least one iteration"
);
assert!(
result.total_losses_mw <= losses_baseline + 1e-2,
"Capacitor-compensated losses ({}) should not significantly exceed baseline ({})",
result.total_losses_mw,
losses_baseline
);
assert_eq!(result.voltage_magnitudes.len(), 2);
assert_eq!(result.branch_loadings_pct.len(), 1);
}
#[test]
fn test_optimize_3bus_oltc() {
let buses = vec![
VvoBus {
id: 0,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 0.0,
q_load_mvar: 0.0,
is_substation: true,
},
VvoBus {
id: 1,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 1.0,
q_load_mvar: 0.5,
is_substation: false,
},
VvoBus {
id: 2,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 0.8,
q_load_mvar: 0.3,
is_substation: false,
},
];
let branches = vec![
VvoBranch {
from: 0,
to: 1,
r_ohm: 0.5,
x_ohm: 0.3,
rating_mva: 5.0,
},
VvoBranch {
from: 1,
to: 2,
r_ohm: 0.4,
x_ohm: 0.2,
rating_mva: 5.0,
},
];
let devices = vec![VvoDevice::OltcTransformer {
bus: 0,
min_tap: 0.9,
max_tap: 1.1,
tap_step: 0.00625,
current_tap: 1.0,
time_delay_s: 30.0,
}];
let config = VvoConfig {
n_buses: 3,
base_mva: 10.0,
base_kv: 11.0,
objective: VvoObjective::default(),
max_iterations: 20,
voltage_tolerance: 1e-4,
n_discrete_trials: 5,
};
let mut opt = VoltVarOptimizer::new(buses, branches, devices, config);
let result = opt.optimize().expect("OLTC VVO should not error");
assert_eq!(result.voltage_magnitudes.len(), 3);
assert!((result.voltage_magnitudes[0] - 1.0).abs() < 1e-9);
assert!(result.total_losses_mw >= 0.0);
}
#[test]
fn test_voltage_violation_detection() {
let opt = make_2bus_system();
let v = vec![1.0, 0.93];
let violations = opt.check_violations(&v);
assert!(
violations.iter().any(|(bus, _)| *bus == 1),
"Bus 1 at 0.93 pu should be detected as a voltage violation"
);
}
#[test]
fn test_bfs_error_on_zero_buses() {
let config = VvoConfig {
n_buses: 0,
..VvoConfig::default()
};
let opt = VoltVarOptimizer::new(vec![], vec![], vec![], config);
let result = opt.solve_bfs(&[]);
assert!(result.is_err(), "BFS on zero-bus network should return Err");
}
#[test]
fn test_optimize_error_on_zero_buses() {
let config = VvoConfig {
n_buses: 0,
..VvoConfig::default()
};
let mut opt = VoltVarOptimizer::new(vec![], vec![], vec![], config);
let result = opt.optimize();
assert!(
result.is_err(),
"optimize() on zero buses should return Err"
);
}
#[test]
fn test_check_violations_high_voltage() {
let opt = make_2bus_system();
let v = vec![1.08, 1.0]; let violations = opt.check_violations(&v);
assert!(
violations.iter().any(|(bus, _)| *bus == 0),
"Bus 0 at 1.08 pu should be flagged as high-voltage violation"
);
}
#[test]
fn test_compute_total_q_injections_equivalence() {
let opt = make_2bus_system();
let setpoints = vec![];
let inj1 = opt.q_setpoints_to_injections(&setpoints);
let inj2 = opt.compute_total_q_injections(&setpoints);
assert_eq!(inj1, inj2, "Both methods should return identical results");
}
#[test]
fn test_bfs_q_injection_raises_voltage() {
let opt = make_2bus_system();
let q_no_inj = vec![0.0, 0.0];
let (v_no_inj, _) = opt.solve_bfs(&q_no_inj).expect("BFS");
let q_with_inj = vec![0.0, 2.0];
let (v_with_inj, _) = opt.solve_bfs(&q_with_inj).expect("BFS");
assert!(
v_with_inj[1] > v_no_inj[1],
"Q injection should raise bus 1 voltage: {} vs {}",
v_with_inj[1],
v_no_inj[1]
);
}
#[test]
fn test_branch_loading_nonzero_for_loaded_feeder() {
let opt = make_2bus_system();
let q_inj = vec![0.0, 0.0];
let (v, _) = opt.solve_bfs(&q_inj).expect("BFS");
let loadings = opt.compute_branch_loading(&v, &q_inj);
assert_eq!(loadings.len(), 1);
assert!(
loadings[0] > 0.0,
"Loaded feeder should show non-zero branch loading"
);
}
#[test]
fn test_bess_q_range() {
let dev = VvoDevice::BatteryEss {
bus: 2,
p_mw: 0.5,
q_max_mvar: 1.5,
q_min_mvar: -1.5,
current_q_mvar: 0.3,
};
let (q_min, q_max) = dev.q_range();
assert!((q_min - (-1.5)).abs() < 1e-9);
assert!((q_max - 1.5).abs() < 1e-9);
assert!((dev.current_q_mvar() - 0.3).abs() < 1e-9);
}
#[test]
fn test_vvo_objective_default() {
let obj = VvoObjective::default();
assert!((obj.weight_losses - 1.0).abs() < 1e-9);
assert!((obj.weight_voltage_deviation - 0.5).abs() < 1e-9);
assert!((obj.weight_device_operations - 0.1).abs() < 1e-9);
assert!((obj.v_ref_pu - 1.0).abs() < 1e-9);
}
#[test]
fn test_optimize_3bus_with_svc() {
let buses = vec![
VvoBus {
id: 0,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 0.0,
q_load_mvar: 0.0,
is_substation: true,
},
VvoBus {
id: 1,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 2.0,
q_load_mvar: 1.5,
is_substation: false,
},
VvoBus {
id: 2,
v_min_pu: 0.95,
v_max_pu: 1.05,
p_load_mw: 1.5,
q_load_mvar: 1.0,
is_substation: false,
},
];
let branches = vec![
VvoBranch {
from: 0,
to: 1,
r_ohm: 1.0,
x_ohm: 0.5,
rating_mva: 10.0,
},
VvoBranch {
from: 1,
to: 2,
r_ohm: 0.8,
x_ohm: 0.4,
rating_mva: 10.0,
},
];
let devices = vec![
VvoDevice::StaticVarCompensator {
bus: 1,
q_min_mvar: -3.0,
q_max_mvar: 3.0,
current_q_mvar: 0.0,
droop_pct: 5.0,
},
VvoDevice::StaticVarCompensator {
bus: 2,
q_min_mvar: -2.0,
q_max_mvar: 2.0,
current_q_mvar: 0.0,
droop_pct: 5.0,
},
];
let config = VvoConfig {
n_buses: 3,
base_mva: 10.0,
base_kv: 11.0,
objective: VvoObjective::default(),
max_iterations: 30,
voltage_tolerance: 1e-4,
n_discrete_trials: 5,
};
let mut base_opt =
VoltVarOptimizer::new(buses.clone(), branches.clone(), vec![], config.clone());
let base_result = base_opt.optimize().expect("Baseline VVO");
let base_losses = base_result.total_losses_mw;
let mut svc_opt = VoltVarOptimizer::new(buses, branches, devices, config);
let svc_result = svc_opt.optimize().expect("SVC VVO");
assert!(
svc_result.total_losses_mw <= base_losses + 1e-3,
"SVC losses ({}) should not exceed baseline ({})",
svc_result.total_losses_mw,
base_losses
);
}
}