1use std::collections::HashMap;
8use std::sync::OnceLock;
9
10use crate::entities::Bus;
11use crate::{
12 EnergyContract, EntityId, Hydro, Line, NonControllableSource, PumpingStation, Thermal,
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub struct BusLineConnection {
22 pub line_id: EntityId,
24 pub is_source: bool,
27}
28
29#[derive(Debug, Clone, PartialEq, Default)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35pub struct BusGenerators {
36 pub hydro_ids: Vec<EntityId>,
38 pub thermal_ids: Vec<EntityId>,
40 pub ncs_ids: Vec<EntityId>,
42}
43
44#[derive(Debug, Clone, PartialEq, Default)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50pub struct BusLoads {
51 pub contract_ids: Vec<EntityId>,
53 pub pumping_station_ids: Vec<EntityId>,
55}
56
57#[allow(clippy::struct_field_names)]
68#[derive(Debug, Clone, PartialEq)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70pub struct NetworkTopology {
71 bus_lines: HashMap<EntityId, Vec<BusLineConnection>>,
74
75 bus_generators: HashMap<EntityId, BusGenerators>,
77
78 bus_loads: HashMap<EntityId, BusLoads>,
80}
81
82static DEFAULT_BUS_GENERATORS: OnceLock<BusGenerators> = OnceLock::new();
84
85static DEFAULT_BUS_LOADS: OnceLock<BusLoads> = OnceLock::new();
87
88impl NetworkTopology {
89 #[must_use]
107 pub fn build(
108 buses: &[Bus],
109 lines: &[Line],
110 hydros: &[Hydro],
111 thermals: &[Thermal],
112 non_controllable_sources: &[NonControllableSource],
113 contracts: &[EnergyContract],
114 pumping_stations: &[PumpingStation],
115 ) -> Self {
116 let mut bus_lines: HashMap<EntityId, Vec<BusLineConnection>> = HashMap::new();
117 let mut bus_generators: HashMap<EntityId, BusGenerators> = HashMap::new();
118 let mut bus_loads: HashMap<EntityId, BusLoads> = HashMap::new();
119
120 let _ = buses;
122
123 for line in lines {
124 bus_lines
125 .entry(line.source_bus_id)
126 .or_default()
127 .push(BusLineConnection {
128 line_id: line.id,
129 is_source: true,
130 });
131 bus_lines
132 .entry(line.target_bus_id)
133 .or_default()
134 .push(BusLineConnection {
135 line_id: line.id,
136 is_source: false,
137 });
138 }
139 for connections in bus_lines.values_mut() {
140 connections.sort_by_key(|c| c.line_id.0);
141 }
142
143 for hydro in hydros {
144 bus_generators
145 .entry(hydro.bus_id)
146 .or_default()
147 .hydro_ids
148 .push(hydro.id);
149 }
150
151 for thermal in thermals {
152 bus_generators
153 .entry(thermal.bus_id)
154 .or_default()
155 .thermal_ids
156 .push(thermal.id);
157 }
158
159 for ncs in non_controllable_sources {
160 bus_generators
161 .entry(ncs.bus_id)
162 .or_default()
163 .ncs_ids
164 .push(ncs.id);
165 }
166
167 for generators in bus_generators.values_mut() {
168 generators.hydro_ids.sort_by_key(|id| id.0);
169 generators.thermal_ids.sort_by_key(|id| id.0);
170 generators.ncs_ids.sort_by_key(|id| id.0);
171 }
172
173 for contract in contracts {
174 bus_loads
175 .entry(contract.bus_id)
176 .or_default()
177 .contract_ids
178 .push(contract.id);
179 }
180
181 for station in pumping_stations {
182 bus_loads
183 .entry(station.bus_id)
184 .or_default()
185 .pumping_station_ids
186 .push(station.id);
187 }
188
189 for loads in bus_loads.values_mut() {
190 loads.contract_ids.sort_by_key(|id| id.0);
191 loads.pumping_station_ids.sort_by_key(|id| id.0);
192 }
193
194 Self {
195 bus_lines,
196 bus_generators,
197 bus_loads,
198 }
199 }
200
201 #[must_use]
205 pub fn bus_lines(&self, bus_id: EntityId) -> &[BusLineConnection] {
206 self.bus_lines.get(&bus_id).map_or(&[], Vec::as_slice)
207 }
208
209 #[must_use]
213 pub fn bus_generators(&self, bus_id: EntityId) -> &BusGenerators {
214 self.bus_generators
215 .get(&bus_id)
216 .unwrap_or_else(|| DEFAULT_BUS_GENERATORS.get_or_init(BusGenerators::default))
217 }
218
219 #[must_use]
223 pub fn bus_loads(&self, bus_id: EntityId) -> &BusLoads {
224 self.bus_loads
225 .get(&bus_id)
226 .unwrap_or_else(|| DEFAULT_BUS_LOADS.get_or_init(BusLoads::default))
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use crate::entities::{ContractType, HydroGenerationModel, HydroPenalties, ThermalCostSegment};
234
235 fn make_bus(id: i32) -> Bus {
236 Bus {
237 id: EntityId(id),
238 name: String::new(),
239 deficit_segments: vec![],
240 excess_cost: 0.0,
241 }
242 }
243
244 fn make_line(id: i32, source_bus_id: i32, target_bus_id: i32) -> Line {
245 Line {
246 id: EntityId(id),
247 name: String::new(),
248 source_bus_id: EntityId(source_bus_id),
249 target_bus_id: EntityId(target_bus_id),
250 entry_stage_id: None,
251 exit_stage_id: None,
252 direct_capacity_mw: 100.0,
253 reverse_capacity_mw: 100.0,
254 losses_percent: 0.0,
255 exchange_cost: 0.0,
256 }
257 }
258
259 fn make_hydro(id: i32, bus_id: i32) -> Hydro {
260 let zero_penalties = HydroPenalties {
261 spillage_cost: 0.0,
262 diversion_cost: 0.0,
263 fpha_turbined_cost: 0.0,
264 storage_violation_below_cost: 0.0,
265 filling_target_violation_cost: 0.0,
266 turbined_violation_below_cost: 0.0,
267 outflow_violation_below_cost: 0.0,
268 outflow_violation_above_cost: 0.0,
269 generation_violation_below_cost: 0.0,
270 evaporation_violation_cost: 0.0,
271 water_withdrawal_violation_cost: 0.0,
272 };
273 Hydro {
274 id: EntityId(id),
275 name: String::new(),
276 bus_id: EntityId(bus_id),
277 downstream_id: None,
278 entry_stage_id: None,
279 exit_stage_id: None,
280 min_storage_hm3: 0.0,
281 max_storage_hm3: 1.0,
282 min_outflow_m3s: 0.0,
283 max_outflow_m3s: None,
284 generation_model: HydroGenerationModel::ConstantProductivity {
285 productivity_mw_per_m3s: 1.0,
286 },
287 min_turbined_m3s: 0.0,
288 max_turbined_m3s: 1.0,
289 min_generation_mw: 0.0,
290 max_generation_mw: 1.0,
291 tailrace: None,
292 hydraulic_losses: None,
293 efficiency: None,
294 evaporation_coefficients_mm: None,
295 diversion: None,
296 filling: None,
297 penalties: zero_penalties,
298 }
299 }
300
301 fn make_thermal(id: i32, bus_id: i32) -> Thermal {
302 Thermal {
303 id: EntityId(id),
304 name: String::new(),
305 bus_id: EntityId(bus_id),
306 entry_stage_id: None,
307 exit_stage_id: None,
308 cost_segments: vec![ThermalCostSegment {
309 capacity_mw: 100.0,
310 cost_per_mwh: 50.0,
311 }],
312 min_generation_mw: 0.0,
313 max_generation_mw: 100.0,
314 gnl_config: None,
315 }
316 }
317
318 fn make_ncs(id: i32, bus_id: i32) -> NonControllableSource {
319 NonControllableSource {
320 id: EntityId(id),
321 name: String::new(),
322 bus_id: EntityId(bus_id),
323 entry_stage_id: None,
324 exit_stage_id: None,
325 max_generation_mw: 50.0,
326 curtailment_cost: 0.0,
327 }
328 }
329
330 fn make_contract(id: i32, bus_id: i32) -> EnergyContract {
331 EnergyContract {
332 id: EntityId(id),
333 name: String::new(),
334 bus_id: EntityId(bus_id),
335 contract_type: ContractType::Import,
336 entry_stage_id: None,
337 exit_stage_id: None,
338 price_per_mwh: 0.0,
339 min_mw: 0.0,
340 max_mw: 100.0,
341 }
342 }
343
344 fn make_pumping_station(id: i32, bus_id: i32) -> PumpingStation {
345 PumpingStation {
346 id: EntityId(id),
347 name: String::new(),
348 bus_id: EntityId(bus_id),
349 source_hydro_id: EntityId(0),
350 destination_hydro_id: EntityId(1),
351 entry_stage_id: None,
352 exit_stage_id: None,
353 consumption_mw_per_m3s: 0.5,
354 min_flow_m3s: 0.0,
355 max_flow_m3s: 10.0,
356 }
357 }
358
359 #[test]
360 fn test_empty_network() {
361 let topo = NetworkTopology::build(&[], &[], &[], &[], &[], &[], &[]);
362
363 assert_eq!(topo.bus_lines(EntityId(0)), &[]);
365 assert!(topo.bus_generators(EntityId(0)).hydro_ids.is_empty());
366 assert!(topo.bus_generators(EntityId(0)).thermal_ids.is_empty());
367 assert!(topo.bus_generators(EntityId(0)).ncs_ids.is_empty());
368 assert!(topo.bus_loads(EntityId(0)).contract_ids.is_empty());
369 assert!(topo.bus_loads(EntityId(0)).pumping_station_ids.is_empty());
370 }
371
372 #[test]
373 fn test_single_line() {
374 let buses = vec![make_bus(0), make_bus(1)];
376 let lines = vec![make_line(0, 0, 1)];
377 let topo = NetworkTopology::build(&buses, &lines, &[], &[], &[], &[], &[]);
378
379 let conns_0 = topo.bus_lines(EntityId(0));
381 assert_eq!(conns_0.len(), 1);
382 assert_eq!(conns_0[0].line_id, EntityId(0));
383 assert!(conns_0[0].is_source);
384
385 let conns_1 = topo.bus_lines(EntityId(1));
387 assert_eq!(conns_1.len(), 1);
388 assert_eq!(conns_1[0].line_id, EntityId(0));
389 assert!(!conns_1[0].is_source);
390 }
391
392 #[test]
393 fn test_multiple_lines_same_bus() {
394 let buses = vec![make_bus(0), make_bus(1), make_bus(2), make_bus(3)];
396 let lines = vec![make_line(0, 0, 1), make_line(1, 0, 2), make_line(2, 0, 3)];
397 let topo = NetworkTopology::build(&buses, &lines, &[], &[], &[], &[], &[]);
398
399 let conns = topo.bus_lines(EntityId(0));
400 assert_eq!(conns.len(), 3);
401 assert!(conns.iter().all(|c| c.is_source));
403 assert_eq!(conns[0].line_id, EntityId(0));
405 assert_eq!(conns[1].line_id, EntityId(1));
406 assert_eq!(conns[2].line_id, EntityId(2));
407 }
408
409 #[test]
410 fn test_generators_per_bus() {
411 let buses = vec![make_bus(0), make_bus(1)];
413 let hydros = vec![make_hydro(0, 0), make_hydro(1, 0)];
414 let thermals = vec![make_thermal(0, 0)];
415 let ncs = vec![make_ncs(0, 1)];
416 let topo = NetworkTopology::build(&buses, &[], &hydros, &thermals, &ncs, &[], &[]);
417
418 let gen0 = topo.bus_generators(EntityId(0));
419 assert_eq!(gen0.hydro_ids.len(), 2);
420 assert_eq!(gen0.thermal_ids.len(), 1);
421 assert!(gen0.ncs_ids.is_empty());
422
423 let gen1 = topo.bus_generators(EntityId(1));
424 assert!(gen1.hydro_ids.is_empty());
425 assert!(gen1.thermal_ids.is_empty());
426 assert_eq!(gen1.ncs_ids.len(), 1);
427 assert_eq!(gen1.ncs_ids[0], EntityId(0));
428 }
429
430 #[test]
431 fn test_loads_per_bus() {
432 let buses = vec![make_bus(0)];
434 let contracts = vec![make_contract(0, 0)];
435 let stations = vec![make_pumping_station(0, 0)];
436 let topo = NetworkTopology::build(&buses, &[], &[], &[], &[], &contracts, &stations);
437
438 let loads0 = topo.bus_loads(EntityId(0));
439 assert_eq!(loads0.contract_ids.len(), 1);
440 assert_eq!(loads0.contract_ids[0], EntityId(0));
441 assert_eq!(loads0.pumping_station_ids.len(), 1);
442 assert_eq!(loads0.pumping_station_ids[0], EntityId(0));
443 }
444
445 #[test]
446 fn test_bus_no_connections() {
447 let buses = vec![make_bus(0)];
449 let topo = NetworkTopology::build(&buses, &[], &[], &[], &[], &[], &[]);
450
451 assert_eq!(topo.bus_lines(EntityId(0)), &[]);
452 let generators = topo.bus_generators(EntityId(0));
453 assert!(generators.hydro_ids.is_empty());
454 assert!(generators.thermal_ids.is_empty());
455 assert!(generators.ncs_ids.is_empty());
456 let loads = topo.bus_loads(EntityId(0));
457 assert!(loads.contract_ids.is_empty());
458 assert!(loads.pumping_station_ids.is_empty());
459 }
460
461 #[test]
462 fn test_deterministic_ordering() {
463 let buses = vec![make_bus(0)];
465 let hydros = vec![make_hydro(5, 0), make_hydro(3, 0), make_hydro(1, 0)];
467 let thermals = vec![make_thermal(4, 0), make_thermal(2, 0)];
469 let contracts = vec![make_contract(10, 0), make_contract(7, 0)];
471 let topo = NetworkTopology::build(&buses, &[], &hydros, &thermals, &[], &contracts, &[]);
472
473 let generators = topo.bus_generators(EntityId(0));
474 assert_eq!(
476 generators.hydro_ids,
477 vec![EntityId(1), EntityId(3), EntityId(5)]
478 );
479 assert_eq!(generators.thermal_ids, vec![EntityId(2), EntityId(4)]);
481
482 let loads = topo.bus_loads(EntityId(0));
483 assert_eq!(loads.contract_ids, vec![EntityId(7), EntityId(10)]);
485 }
486
487 #[cfg(feature = "serde")]
488 #[test]
489 fn test_topology_serde_roundtrip_network() {
490 let buses = vec![make_bus(0), make_bus(1)];
492 let lines = vec![make_line(0, 0, 1)];
493 let hydros = vec![make_hydro(0, 0)];
494 let thermals = vec![make_thermal(0, 1)];
495 let topo = NetworkTopology::build(&buses, &lines, &hydros, &thermals, &[], &[], &[]);
496 let json = serde_json::to_string(&topo).unwrap();
497 let deserialized: NetworkTopology = serde_json::from_str(&json).unwrap();
498 assert_eq!(topo, deserialized);
499 }
500}