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 evaporation_reference_volumes_hm3: None,
296 diversion: None,
297 filling: None,
298 penalties: zero_penalties,
299 }
300 }
301
302 fn make_thermal(id: i32, bus_id: i32) -> Thermal {
303 Thermal {
304 id: EntityId(id),
305 name: String::new(),
306 bus_id: EntityId(bus_id),
307 entry_stage_id: None,
308 exit_stage_id: None,
309 cost_segments: vec![ThermalCostSegment {
310 capacity_mw: 100.0,
311 cost_per_mwh: 50.0,
312 }],
313 min_generation_mw: 0.0,
314 max_generation_mw: 100.0,
315 gnl_config: None,
316 }
317 }
318
319 fn make_ncs(id: i32, bus_id: i32) -> NonControllableSource {
320 NonControllableSource {
321 id: EntityId(id),
322 name: String::new(),
323 bus_id: EntityId(bus_id),
324 entry_stage_id: None,
325 exit_stage_id: None,
326 max_generation_mw: 50.0,
327 curtailment_cost: 0.0,
328 }
329 }
330
331 fn make_contract(id: i32, bus_id: i32) -> EnergyContract {
332 EnergyContract {
333 id: EntityId(id),
334 name: String::new(),
335 bus_id: EntityId(bus_id),
336 contract_type: ContractType::Import,
337 entry_stage_id: None,
338 exit_stage_id: None,
339 price_per_mwh: 0.0,
340 min_mw: 0.0,
341 max_mw: 100.0,
342 }
343 }
344
345 fn make_pumping_station(id: i32, bus_id: i32) -> PumpingStation {
346 PumpingStation {
347 id: EntityId(id),
348 name: String::new(),
349 bus_id: EntityId(bus_id),
350 source_hydro_id: EntityId(0),
351 destination_hydro_id: EntityId(1),
352 entry_stage_id: None,
353 exit_stage_id: None,
354 consumption_mw_per_m3s: 0.5,
355 min_flow_m3s: 0.0,
356 max_flow_m3s: 10.0,
357 }
358 }
359
360 #[test]
361 fn test_empty_network() {
362 let topo = NetworkTopology::build(&[], &[], &[], &[], &[], &[], &[]);
363
364 assert_eq!(topo.bus_lines(EntityId(0)), &[]);
366 assert!(topo.bus_generators(EntityId(0)).hydro_ids.is_empty());
367 assert!(topo.bus_generators(EntityId(0)).thermal_ids.is_empty());
368 assert!(topo.bus_generators(EntityId(0)).ncs_ids.is_empty());
369 assert!(topo.bus_loads(EntityId(0)).contract_ids.is_empty());
370 assert!(topo.bus_loads(EntityId(0)).pumping_station_ids.is_empty());
371 }
372
373 #[test]
374 fn test_single_line() {
375 let buses = vec![make_bus(0), make_bus(1)];
377 let lines = vec![make_line(0, 0, 1)];
378 let topo = NetworkTopology::build(&buses, &lines, &[], &[], &[], &[], &[]);
379
380 let conns_0 = topo.bus_lines(EntityId(0));
382 assert_eq!(conns_0.len(), 1);
383 assert_eq!(conns_0[0].line_id, EntityId(0));
384 assert!(conns_0[0].is_source);
385
386 let conns_1 = topo.bus_lines(EntityId(1));
388 assert_eq!(conns_1.len(), 1);
389 assert_eq!(conns_1[0].line_id, EntityId(0));
390 assert!(!conns_1[0].is_source);
391 }
392
393 #[test]
394 fn test_multiple_lines_same_bus() {
395 let buses = vec![make_bus(0), make_bus(1), make_bus(2), make_bus(3)];
397 let lines = vec![make_line(0, 0, 1), make_line(1, 0, 2), make_line(2, 0, 3)];
398 let topo = NetworkTopology::build(&buses, &lines, &[], &[], &[], &[], &[]);
399
400 let conns = topo.bus_lines(EntityId(0));
401 assert_eq!(conns.len(), 3);
402 assert!(conns.iter().all(|c| c.is_source));
404 assert_eq!(conns[0].line_id, EntityId(0));
406 assert_eq!(conns[1].line_id, EntityId(1));
407 assert_eq!(conns[2].line_id, EntityId(2));
408 }
409
410 #[test]
411 fn test_generators_per_bus() {
412 let buses = vec![make_bus(0), make_bus(1)];
414 let hydros = vec![make_hydro(0, 0), make_hydro(1, 0)];
415 let thermals = vec![make_thermal(0, 0)];
416 let ncs = vec![make_ncs(0, 1)];
417 let topo = NetworkTopology::build(&buses, &[], &hydros, &thermals, &ncs, &[], &[]);
418
419 let gen0 = topo.bus_generators(EntityId(0));
420 assert_eq!(gen0.hydro_ids.len(), 2);
421 assert_eq!(gen0.thermal_ids.len(), 1);
422 assert!(gen0.ncs_ids.is_empty());
423
424 let gen1 = topo.bus_generators(EntityId(1));
425 assert!(gen1.hydro_ids.is_empty());
426 assert!(gen1.thermal_ids.is_empty());
427 assert_eq!(gen1.ncs_ids.len(), 1);
428 assert_eq!(gen1.ncs_ids[0], EntityId(0));
429 }
430
431 #[test]
432 fn test_loads_per_bus() {
433 let buses = vec![make_bus(0)];
435 let contracts = vec![make_contract(0, 0)];
436 let stations = vec![make_pumping_station(0, 0)];
437 let topo = NetworkTopology::build(&buses, &[], &[], &[], &[], &contracts, &stations);
438
439 let loads0 = topo.bus_loads(EntityId(0));
440 assert_eq!(loads0.contract_ids.len(), 1);
441 assert_eq!(loads0.contract_ids[0], EntityId(0));
442 assert_eq!(loads0.pumping_station_ids.len(), 1);
443 assert_eq!(loads0.pumping_station_ids[0], EntityId(0));
444 }
445
446 #[test]
447 fn test_bus_no_connections() {
448 let buses = vec![make_bus(0)];
450 let topo = NetworkTopology::build(&buses, &[], &[], &[], &[], &[], &[]);
451
452 assert_eq!(topo.bus_lines(EntityId(0)), &[]);
453 let generators = topo.bus_generators(EntityId(0));
454 assert!(generators.hydro_ids.is_empty());
455 assert!(generators.thermal_ids.is_empty());
456 assert!(generators.ncs_ids.is_empty());
457 let loads = topo.bus_loads(EntityId(0));
458 assert!(loads.contract_ids.is_empty());
459 assert!(loads.pumping_station_ids.is_empty());
460 }
461
462 #[test]
463 fn test_deterministic_ordering() {
464 let buses = vec![make_bus(0)];
466 let hydros = vec![make_hydro(5, 0), make_hydro(3, 0), make_hydro(1, 0)];
468 let thermals = vec![make_thermal(4, 0), make_thermal(2, 0)];
470 let contracts = vec![make_contract(10, 0), make_contract(7, 0)];
472 let topo = NetworkTopology::build(&buses, &[], &hydros, &thermals, &[], &contracts, &[]);
473
474 let generators = topo.bus_generators(EntityId(0));
475 assert_eq!(
477 generators.hydro_ids,
478 vec![EntityId(1), EntityId(3), EntityId(5)]
479 );
480 assert_eq!(generators.thermal_ids, vec![EntityId(2), EntityId(4)]);
482
483 let loads = topo.bus_loads(EntityId(0));
484 assert_eq!(loads.contract_ids, vec![EntityId(7), EntityId(10)]);
486 }
487
488 #[cfg(feature = "serde")]
489 #[test]
490 fn test_topology_serde_roundtrip_network() {
491 let buses = vec![make_bus(0), make_bus(1)];
493 let lines = vec![make_line(0, 0, 1)];
494 let hydros = vec![make_hydro(0, 0)];
495 let thermals = vec![make_thermal(0, 1)];
496 let topo = NetworkTopology::build(&buses, &lines, &hydros, &thermals, &[], &[], &[]);
497 let json = serde_json::to_string(&topo).unwrap();
498 let deserialized: NetworkTopology = serde_json::from_str(&json).unwrap();
499 assert_eq!(topo, deserialized);
500 }
501}