1use serde::{Deserialize, Serialize};
5use tracing::warn;
6
7use crate::network::model::Network;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
16pub enum AngleReference {
17 #[default]
19 PreserveInitial,
20
21 Zero,
23
24 Distributed(DistributedAngleWeight),
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
30#[serde(rename_all = "snake_case")]
31pub enum DistributedAngleWeight {
32 #[default]
34 LoadWeighted,
35 GenerationWeighted,
37 InertiaWeighted,
39}
40
41pub fn apply_angle_reference(
43 angles: &mut [f64],
44 network: &Network,
45 reference_bus_idx: usize,
46 reference_angle0_rad: f64,
47 mode: AngleReference,
48) {
49 let bus_indices: Vec<usize> = (0..angles.len()).collect();
50 apply_angle_reference_subset(
51 angles,
52 network,
53 &bus_indices,
54 reference_bus_idx,
55 reference_angle0_rad,
56 mode,
57 );
58}
59
60pub fn apply_angle_reference_subset(
65 angles: &mut [f64],
66 network: &Network,
67 bus_indices: &[usize],
68 reference_bus_idx: usize,
69 reference_angle0_rad: f64,
70 mode: AngleReference,
71) {
72 if bus_indices.is_empty() || reference_bus_idx >= angles.len() {
73 return;
74 }
75
76 let shift = match mode {
77 AngleReference::PreserveInitial => reference_angle0_rad - angles[reference_bus_idx],
78 AngleReference::Zero => -angles[reference_bus_idx],
79 AngleReference::Distributed(weight_mode) => {
80 match distributed_reference_angle(network, angles, bus_indices, weight_mode) {
81 Some(theta_ref) => -theta_ref,
82 None => {
83 warn!(
84 ?weight_mode,
85 "distributed angle reference: all weights are zero, falling back to PreserveInitial"
86 );
87 reference_angle0_rad - angles[reference_bus_idx]
88 }
89 }
90 }
91 };
92
93 if shift.abs() <= 1e-12 {
94 return;
95 }
96 for &bus_idx in bus_indices {
97 if let Some(angle) = angles.get_mut(bus_idx) {
98 *angle += shift;
99 }
100 }
101}
102
103fn distributed_reference_angle(
104 network: &Network,
105 angles: &[f64],
106 bus_indices: &[usize],
107 weight_mode: DistributedAngleWeight,
108) -> Option<f64> {
109 let mut in_subset = vec![false; network.buses.len()];
110 for &bus_idx in bus_indices {
111 if let Some(flag) = in_subset.get_mut(bus_idx) {
112 *flag = true;
113 }
114 }
115
116 let bus_map = network.bus_index_map();
117 let mut weighted_angle_sum = 0.0;
118 let mut total_weight = 0.0;
119
120 match weight_mode {
121 DistributedAngleWeight::LoadWeighted => {
122 for load in &network.loads {
123 if !load.in_service {
124 continue;
125 }
126 let Some(&bus_idx) = bus_map.get(&load.bus) else {
127 continue;
128 };
129 if !in_subset[bus_idx] {
130 continue;
131 }
132 let weight = load.active_power_demand_mw.abs();
133 if weight > 0.0 {
134 weighted_angle_sum += weight * angles[bus_idx];
135 total_weight += weight;
136 }
137 }
138 }
139 DistributedAngleWeight::GenerationWeighted => {
140 for generator in &network.generators {
141 if !generator.in_service {
142 continue;
143 }
144 let Some(&bus_idx) = bus_map.get(&generator.bus) else {
145 continue;
146 };
147 if !in_subset[bus_idx] {
148 continue;
149 }
150 let weight = generator.p.abs();
151 if weight > 0.0 {
152 weighted_angle_sum += weight * angles[bus_idx];
153 total_weight += weight;
154 }
155 }
156 }
157 DistributedAngleWeight::InertiaWeighted => {
158 for generator in &network.generators {
159 if !generator.in_service {
160 continue;
161 }
162 let Some(&bus_idx) = bus_map.get(&generator.bus) else {
163 continue;
164 };
165 if !in_subset[bus_idx] {
166 continue;
167 }
168 let weight = generator.h_inertia_s.unwrap_or(0.0) * generator.machine_base_mva;
169 if weight > 0.0 {
170 weighted_angle_sum += weight * angles[bus_idx];
171 total_weight += weight;
172 }
173 }
174 }
175 }
176
177 (total_weight > 1e-12).then_some(weighted_angle_sum / total_weight)
178}
179
180#[cfg(test)]
181mod tests {
182 use super::{AngleReference, DistributedAngleWeight, apply_angle_reference_subset};
183 use crate::network::{Bus, BusType, Generator, Load, Network};
184
185 fn base_network() -> Network {
186 Network {
187 buses: vec![
188 Bus {
189 number: 1,
190 bus_type: BusType::Slack,
191 ..Bus::default()
192 },
193 Bus {
194 number: 2,
195 bus_type: BusType::PQ,
196 ..Bus::default()
197 },
198 Bus {
199 number: 3,
200 bus_type: BusType::PV,
201 ..Bus::default()
202 },
203 ],
204 loads: vec![
205 Load {
206 bus: 1,
207 active_power_demand_mw: 10.0,
208 ..Load::default()
209 },
210 Load {
211 bus: 2,
212 active_power_demand_mw: 30.0,
213 ..Load::default()
214 },
215 ],
216 generators: vec![
217 Generator {
218 bus: 1,
219 p: 50.0,
220 ..Generator::default()
221 },
222 Generator {
223 bus: 3,
224 p: 150.0,
225 h_inertia_s: Some(4.0),
226 machine_base_mva: 100.0,
227 ..Generator::default()
228 },
229 ],
230 ..Network::default()
231 }
232 }
233
234 #[test]
235 fn distributed_load_reference_zeroes_subset_weighted_mean() {
236 let network = base_network();
237 let mut angles = vec![0.3, -0.1, 0.8];
238 apply_angle_reference_subset(
239 &mut angles,
240 &network,
241 &[0, 1],
242 0,
243 0.0,
244 AngleReference::Distributed(DistributedAngleWeight::LoadWeighted),
245 );
246 let weighted_mean = (10.0 * angles[0] + 30.0 * angles[1]) / 40.0;
247 assert!(weighted_mean.abs() < 1e-12);
248 assert!((angles[2] - 0.8).abs() < 1e-12);
249 }
250
251 #[test]
252 fn distributed_generation_reference_uses_generation_weights() {
253 let network = base_network();
254 let mut angles = vec![0.2, 0.1, -0.4];
255 apply_angle_reference_subset(
256 &mut angles,
257 &network,
258 &[0, 2],
259 0,
260 0.0,
261 AngleReference::Distributed(DistributedAngleWeight::GenerationWeighted),
262 );
263 let weighted_mean = (50.0 * angles[0] + 150.0 * angles[2]) / 200.0;
264 assert!(weighted_mean.abs() < 1e-12);
265 assert!((angles[1] - 0.1).abs() < 1e-12);
266 }
267
268 #[test]
269 fn distributed_inertia_reference_uses_h_times_mbase() {
270 let network = base_network();
271 let mut angles = vec![0.1, 0.0, 0.7];
272 apply_angle_reference_subset(
273 &mut angles,
274 &network,
275 &[0, 2],
276 0,
277 0.0,
278 AngleReference::Distributed(DistributedAngleWeight::InertiaWeighted),
279 );
280 let weighted_mean = (4.0 * 100.0 * angles[2]) / (4.0 * 100.0);
281 assert!(weighted_mean.abs() < 1e-12);
282 }
283
284 #[test]
285 fn zero_weight_distributed_reference_falls_back_to_preserve_initial() {
286 let network = Network {
287 buses: vec![
288 Bus {
289 number: 1,
290 bus_type: BusType::Slack,
291 ..Bus::default()
292 },
293 Bus {
294 number: 2,
295 bus_type: BusType::PQ,
296 ..Bus::default()
297 },
298 ],
299 ..Network::default()
300 };
301 let mut angles = vec![0.6, -0.2];
302 apply_angle_reference_subset(
303 &mut angles,
304 &network,
305 &[0, 1],
306 0,
307 0.25,
308 AngleReference::Distributed(DistributedAngleWeight::LoadWeighted),
309 );
310 assert!((angles[0] - 0.25).abs() < 1e-12);
311 }
312}