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 water_withdrawal_violation_pos_cost: 0.0,
273 water_withdrawal_violation_neg_cost: 0.0,
274 evaporation_violation_pos_cost: 0.0,
275 evaporation_violation_neg_cost: 0.0,
276 inflow_nonnegativity_cost: 1000.0,
277 };
278 Hydro {
279 id: EntityId(id),
280 name: String::new(),
281 bus_id: EntityId(bus_id),
282 downstream_id: None,
283 entry_stage_id: None,
284 exit_stage_id: None,
285 min_storage_hm3: 0.0,
286 max_storage_hm3: 1.0,
287 min_outflow_m3s: 0.0,
288 max_outflow_m3s: None,
289 generation_model: HydroGenerationModel::ConstantProductivity {
290 productivity_mw_per_m3s: 1.0,
291 },
292 min_turbined_m3s: 0.0,
293 max_turbined_m3s: 1.0,
294 min_generation_mw: 0.0,
295 max_generation_mw: 1.0,
296 tailrace: None,
297 hydraulic_losses: None,
298 efficiency: None,
299 evaporation_coefficients_mm: None,
300 evaporation_reference_volumes_hm3: None,
301 diversion: None,
302 filling: None,
303 penalties: zero_penalties,
304 }
305 }
306
307 fn make_thermal(id: i32, bus_id: i32) -> Thermal {
308 Thermal {
309 id: EntityId(id),
310 name: String::new(),
311 bus_id: EntityId(bus_id),
312 entry_stage_id: None,
313 exit_stage_id: None,
314 cost_segments: vec![ThermalCostSegment {
315 capacity_mw: 100.0,
316 cost_per_mwh: 50.0,
317 }],
318 min_generation_mw: 0.0,
319 max_generation_mw: 100.0,
320 gnl_config: None,
321 }
322 }
323
324 fn make_ncs(id: i32, bus_id: i32) -> NonControllableSource {
325 NonControllableSource {
326 id: EntityId(id),
327 name: String::new(),
328 bus_id: EntityId(bus_id),
329 entry_stage_id: None,
330 exit_stage_id: None,
331 max_generation_mw: 50.0,
332 curtailment_cost: 0.0,
333 }
334 }
335
336 fn make_contract(id: i32, bus_id: i32) -> EnergyContract {
337 EnergyContract {
338 id: EntityId(id),
339 name: String::new(),
340 bus_id: EntityId(bus_id),
341 contract_type: ContractType::Import,
342 entry_stage_id: None,
343 exit_stage_id: None,
344 price_per_mwh: 0.0,
345 min_mw: 0.0,
346 max_mw: 100.0,
347 }
348 }
349
350 fn make_pumping_station(id: i32, bus_id: i32) -> PumpingStation {
351 PumpingStation {
352 id: EntityId(id),
353 name: String::new(),
354 bus_id: EntityId(bus_id),
355 source_hydro_id: EntityId(0),
356 destination_hydro_id: EntityId(1),
357 entry_stage_id: None,
358 exit_stage_id: None,
359 consumption_mw_per_m3s: 0.5,
360 min_flow_m3s: 0.0,
361 max_flow_m3s: 10.0,
362 }
363 }
364
365 #[test]
366 fn test_empty_network() {
367 let topo = NetworkTopology::build(&[], &[], &[], &[], &[], &[], &[]);
368
369 assert_eq!(topo.bus_lines(EntityId(0)), &[]);
371 assert!(topo.bus_generators(EntityId(0)).hydro_ids.is_empty());
372 assert!(topo.bus_generators(EntityId(0)).thermal_ids.is_empty());
373 assert!(topo.bus_generators(EntityId(0)).ncs_ids.is_empty());
374 assert!(topo.bus_loads(EntityId(0)).contract_ids.is_empty());
375 assert!(topo.bus_loads(EntityId(0)).pumping_station_ids.is_empty());
376 }
377
378 #[test]
379 fn test_single_line() {
380 let buses = vec![make_bus(0), make_bus(1)];
382 let lines = vec![make_line(0, 0, 1)];
383 let topo = NetworkTopology::build(&buses, &lines, &[], &[], &[], &[], &[]);
384
385 let conns_0 = topo.bus_lines(EntityId(0));
387 assert_eq!(conns_0.len(), 1);
388 assert_eq!(conns_0[0].line_id, EntityId(0));
389 assert!(conns_0[0].is_source);
390
391 let conns_1 = topo.bus_lines(EntityId(1));
393 assert_eq!(conns_1.len(), 1);
394 assert_eq!(conns_1[0].line_id, EntityId(0));
395 assert!(!conns_1[0].is_source);
396 }
397
398 #[test]
399 fn test_multiple_lines_same_bus() {
400 let buses = vec![make_bus(0), make_bus(1), make_bus(2), make_bus(3)];
402 let lines = vec![make_line(0, 0, 1), make_line(1, 0, 2), make_line(2, 0, 3)];
403 let topo = NetworkTopology::build(&buses, &lines, &[], &[], &[], &[], &[]);
404
405 let conns = topo.bus_lines(EntityId(0));
406 assert_eq!(conns.len(), 3);
407 assert!(conns.iter().all(|c| c.is_source));
409 assert_eq!(conns[0].line_id, EntityId(0));
411 assert_eq!(conns[1].line_id, EntityId(1));
412 assert_eq!(conns[2].line_id, EntityId(2));
413 }
414
415 #[test]
416 fn test_generators_per_bus() {
417 let buses = vec![make_bus(0), make_bus(1)];
419 let hydros = vec![make_hydro(0, 0), make_hydro(1, 0)];
420 let thermals = vec![make_thermal(0, 0)];
421 let ncs = vec![make_ncs(0, 1)];
422 let topo = NetworkTopology::build(&buses, &[], &hydros, &thermals, &ncs, &[], &[]);
423
424 let gen0 = topo.bus_generators(EntityId(0));
425 assert_eq!(gen0.hydro_ids.len(), 2);
426 assert_eq!(gen0.thermal_ids.len(), 1);
427 assert!(gen0.ncs_ids.is_empty());
428
429 let gen1 = topo.bus_generators(EntityId(1));
430 assert!(gen1.hydro_ids.is_empty());
431 assert!(gen1.thermal_ids.is_empty());
432 assert_eq!(gen1.ncs_ids.len(), 1);
433 assert_eq!(gen1.ncs_ids[0], EntityId(0));
434 }
435
436 #[test]
437 fn test_loads_per_bus() {
438 let buses = vec![make_bus(0)];
440 let contracts = vec![make_contract(0, 0)];
441 let stations = vec![make_pumping_station(0, 0)];
442 let topo = NetworkTopology::build(&buses, &[], &[], &[], &[], &contracts, &stations);
443
444 let loads0 = topo.bus_loads(EntityId(0));
445 assert_eq!(loads0.contract_ids.len(), 1);
446 assert_eq!(loads0.contract_ids[0], EntityId(0));
447 assert_eq!(loads0.pumping_station_ids.len(), 1);
448 assert_eq!(loads0.pumping_station_ids[0], EntityId(0));
449 }
450
451 #[test]
452 fn test_bus_no_connections() {
453 let buses = vec![make_bus(0)];
455 let topo = NetworkTopology::build(&buses, &[], &[], &[], &[], &[], &[]);
456
457 assert_eq!(topo.bus_lines(EntityId(0)), &[]);
458 let generators = topo.bus_generators(EntityId(0));
459 assert!(generators.hydro_ids.is_empty());
460 assert!(generators.thermal_ids.is_empty());
461 assert!(generators.ncs_ids.is_empty());
462 let loads = topo.bus_loads(EntityId(0));
463 assert!(loads.contract_ids.is_empty());
464 assert!(loads.pumping_station_ids.is_empty());
465 }
466
467 #[test]
468 fn test_deterministic_ordering() {
469 let buses = vec![make_bus(0)];
471 let hydros = vec![make_hydro(5, 0), make_hydro(3, 0), make_hydro(1, 0)];
473 let thermals = vec![make_thermal(4, 0), make_thermal(2, 0)];
475 let contracts = vec![make_contract(10, 0), make_contract(7, 0)];
477 let topo = NetworkTopology::build(&buses, &[], &hydros, &thermals, &[], &contracts, &[]);
478
479 let generators = topo.bus_generators(EntityId(0));
480 assert_eq!(
482 generators.hydro_ids,
483 vec![EntityId(1), EntityId(3), EntityId(5)]
484 );
485 assert_eq!(generators.thermal_ids, vec![EntityId(2), EntityId(4)]);
487
488 let loads = topo.bus_loads(EntityId(0));
489 assert_eq!(loads.contract_ids, vec![EntityId(7), EntityId(10)]);
491 }
492
493 #[cfg(feature = "serde")]
494 #[test]
495 fn test_topology_serde_roundtrip_network() {
496 let buses = vec![make_bus(0), make_bus(1)];
498 let lines = vec![make_line(0, 0, 1)];
499 let hydros = vec![make_hydro(0, 0)];
500 let thermals = vec![make_thermal(0, 1)];
501 let topo = NetworkTopology::build(&buses, &lines, &hydros, &thermals, &[], &[], &[]);
502 let json = serde_json::to_string(&topo).unwrap();
503 let deserialized: NetworkTopology = serde_json::from_str(&json).unwrap();
504 assert_eq!(topo, deserialized);
505 }
506}