use serde::{Deserialize, Serialize};
use tracing::warn;
use crate::network::model::Network;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum AngleReference {
#[default]
PreserveInitial,
Zero,
Distributed(DistributedAngleWeight),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum DistributedAngleWeight {
#[default]
LoadWeighted,
GenerationWeighted,
InertiaWeighted,
}
pub fn apply_angle_reference(
angles: &mut [f64],
network: &Network,
reference_bus_idx: usize,
reference_angle0_rad: f64,
mode: AngleReference,
) {
let bus_indices: Vec<usize> = (0..angles.len()).collect();
apply_angle_reference_subset(
angles,
network,
&bus_indices,
reference_bus_idx,
reference_angle0_rad,
mode,
);
}
pub fn apply_angle_reference_subset(
angles: &mut [f64],
network: &Network,
bus_indices: &[usize],
reference_bus_idx: usize,
reference_angle0_rad: f64,
mode: AngleReference,
) {
if bus_indices.is_empty() || reference_bus_idx >= angles.len() {
return;
}
let shift = match mode {
AngleReference::PreserveInitial => reference_angle0_rad - angles[reference_bus_idx],
AngleReference::Zero => -angles[reference_bus_idx],
AngleReference::Distributed(weight_mode) => {
match distributed_reference_angle(network, angles, bus_indices, weight_mode) {
Some(theta_ref) => -theta_ref,
None => {
warn!(
?weight_mode,
"distributed angle reference: all weights are zero, falling back to PreserveInitial"
);
reference_angle0_rad - angles[reference_bus_idx]
}
}
}
};
if shift.abs() <= 1e-12 {
return;
}
for &bus_idx in bus_indices {
if let Some(angle) = angles.get_mut(bus_idx) {
*angle += shift;
}
}
}
fn distributed_reference_angle(
network: &Network,
angles: &[f64],
bus_indices: &[usize],
weight_mode: DistributedAngleWeight,
) -> Option<f64> {
let mut in_subset = vec![false; network.buses.len()];
for &bus_idx in bus_indices {
if let Some(flag) = in_subset.get_mut(bus_idx) {
*flag = true;
}
}
let bus_map = network.bus_index_map();
let mut weighted_angle_sum = 0.0;
let mut total_weight = 0.0;
match weight_mode {
DistributedAngleWeight::LoadWeighted => {
for load in &network.loads {
if !load.in_service {
continue;
}
let Some(&bus_idx) = bus_map.get(&load.bus) else {
continue;
};
if !in_subset[bus_idx] {
continue;
}
let weight = load.active_power_demand_mw.abs();
if weight > 0.0 {
weighted_angle_sum += weight * angles[bus_idx];
total_weight += weight;
}
}
}
DistributedAngleWeight::GenerationWeighted => {
for generator in &network.generators {
if !generator.in_service {
continue;
}
let Some(&bus_idx) = bus_map.get(&generator.bus) else {
continue;
};
if !in_subset[bus_idx] {
continue;
}
let weight = generator.p.abs();
if weight > 0.0 {
weighted_angle_sum += weight * angles[bus_idx];
total_weight += weight;
}
}
}
DistributedAngleWeight::InertiaWeighted => {
for generator in &network.generators {
if !generator.in_service {
continue;
}
let Some(&bus_idx) = bus_map.get(&generator.bus) else {
continue;
};
if !in_subset[bus_idx] {
continue;
}
let weight = generator.h_inertia_s.unwrap_or(0.0) * generator.machine_base_mva;
if weight > 0.0 {
weighted_angle_sum += weight * angles[bus_idx];
total_weight += weight;
}
}
}
}
(total_weight > 1e-12).then_some(weighted_angle_sum / total_weight)
}
#[cfg(test)]
mod tests {
use super::{AngleReference, DistributedAngleWeight, apply_angle_reference_subset};
use crate::network::{Bus, BusType, Generator, Load, Network};
fn base_network() -> Network {
Network {
buses: vec![
Bus {
number: 1,
bus_type: BusType::Slack,
..Bus::default()
},
Bus {
number: 2,
bus_type: BusType::PQ,
..Bus::default()
},
Bus {
number: 3,
bus_type: BusType::PV,
..Bus::default()
},
],
loads: vec![
Load {
bus: 1,
active_power_demand_mw: 10.0,
..Load::default()
},
Load {
bus: 2,
active_power_demand_mw: 30.0,
..Load::default()
},
],
generators: vec![
Generator {
bus: 1,
p: 50.0,
..Generator::default()
},
Generator {
bus: 3,
p: 150.0,
h_inertia_s: Some(4.0),
machine_base_mva: 100.0,
..Generator::default()
},
],
..Network::default()
}
}
#[test]
fn distributed_load_reference_zeroes_subset_weighted_mean() {
let network = base_network();
let mut angles = vec![0.3, -0.1, 0.8];
apply_angle_reference_subset(
&mut angles,
&network,
&[0, 1],
0,
0.0,
AngleReference::Distributed(DistributedAngleWeight::LoadWeighted),
);
let weighted_mean = (10.0 * angles[0] + 30.0 * angles[1]) / 40.0;
assert!(weighted_mean.abs() < 1e-12);
assert!((angles[2] - 0.8).abs() < 1e-12);
}
#[test]
fn distributed_generation_reference_uses_generation_weights() {
let network = base_network();
let mut angles = vec![0.2, 0.1, -0.4];
apply_angle_reference_subset(
&mut angles,
&network,
&[0, 2],
0,
0.0,
AngleReference::Distributed(DistributedAngleWeight::GenerationWeighted),
);
let weighted_mean = (50.0 * angles[0] + 150.0 * angles[2]) / 200.0;
assert!(weighted_mean.abs() < 1e-12);
assert!((angles[1] - 0.1).abs() < 1e-12);
}
#[test]
fn distributed_inertia_reference_uses_h_times_mbase() {
let network = base_network();
let mut angles = vec![0.1, 0.0, 0.7];
apply_angle_reference_subset(
&mut angles,
&network,
&[0, 2],
0,
0.0,
AngleReference::Distributed(DistributedAngleWeight::InertiaWeighted),
);
let weighted_mean = (4.0 * 100.0 * angles[2]) / (4.0 * 100.0);
assert!(weighted_mean.abs() < 1e-12);
}
#[test]
fn zero_weight_distributed_reference_falls_back_to_preserve_initial() {
let network = Network {
buses: vec![
Bus {
number: 1,
bus_type: BusType::Slack,
..Bus::default()
},
Bus {
number: 2,
bus_type: BusType::PQ,
..Bus::default()
},
],
..Network::default()
};
let mut angles = vec![0.6, -0.2];
apply_angle_reference_subset(
&mut angles,
&network,
&[0, 1],
0,
0.25,
AngleReference::Distributed(DistributedAngleWeight::LoadWeighted),
);
assert!((angles[0] - 0.25).abs() < 1e-12);
}
}