1use std::collections::{HashMap, HashSet};
12
13use crate::{
14 Bus, CascadeTopology, CorrelationModel, EnergyContract, EntityId, ExternalLoadRow,
15 ExternalNcsRow, ExternalScenarioRow, GenericConstraint, Hydro, InflowHistoryRow, InflowModel,
16 InitialConditions, Line, LoadModel, NcsModel, NetworkTopology, NonControllableSource,
17 PolicyGraph, PumpingStation, ResolvedBounds, ResolvedExchangeFactors,
18 ResolvedGenericConstraintBounds, ResolvedLoadFactors, ResolvedNcsBounds, ResolvedNcsFactors,
19 ResolvedPenalties, Stage, Thermal, ValidationError,
20};
21
22#[derive(Debug, PartialEq)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53pub struct System {
54 buses: Vec<Bus>,
56 lines: Vec<Line>,
57 hydros: Vec<Hydro>,
58 thermals: Vec<Thermal>,
59 pumping_stations: Vec<PumpingStation>,
60 contracts: Vec<EnergyContract>,
61 non_controllable_sources: Vec<NonControllableSource>,
62
63 #[cfg_attr(feature = "serde", serde(skip))]
67 bus_index: HashMap<EntityId, usize>,
68 #[cfg_attr(feature = "serde", serde(skip))]
69 line_index: HashMap<EntityId, usize>,
70 #[cfg_attr(feature = "serde", serde(skip))]
71 hydro_index: HashMap<EntityId, usize>,
72 #[cfg_attr(feature = "serde", serde(skip))]
73 thermal_index: HashMap<EntityId, usize>,
74 #[cfg_attr(feature = "serde", serde(skip))]
75 pumping_station_index: HashMap<EntityId, usize>,
76 #[cfg_attr(feature = "serde", serde(skip))]
77 contract_index: HashMap<EntityId, usize>,
78 #[cfg_attr(feature = "serde", serde(skip))]
79 non_controllable_source_index: HashMap<EntityId, usize>,
80
81 cascade: CascadeTopology,
84 network: NetworkTopology,
86
87 stages: Vec<Stage>,
90 policy_graph: PolicyGraph,
92
93 #[cfg_attr(feature = "serde", serde(skip))]
97 stage_index: HashMap<i32, usize>,
98
99 penalties: ResolvedPenalties,
102 bounds: ResolvedBounds,
104 resolved_generic_bounds: ResolvedGenericConstraintBounds,
106 resolved_load_factors: ResolvedLoadFactors,
108 resolved_exchange_factors: ResolvedExchangeFactors,
110 resolved_ncs_bounds: ResolvedNcsBounds,
112 resolved_ncs_factors: ResolvedNcsFactors,
114
115 inflow_models: Vec<InflowModel>,
118 load_models: Vec<LoadModel>,
120 ncs_models: Vec<NcsModel>,
122 correlation: CorrelationModel,
124
125 initial_conditions: InitialConditions,
128 generic_constraints: Vec<GenericConstraint>,
130
131 inflow_history: Vec<InflowHistoryRow>,
136 external_scenarios: Vec<ExternalScenarioRow>,
140 external_load_scenarios: Vec<ExternalLoadRow>,
144 external_ncs_scenarios: Vec<ExternalNcsRow>,
148}
149
150const _: () = {
152 const fn assert_send_sync<T: Send + Sync>() {}
153 const fn check() {
154 assert_send_sync::<System>();
155 }
156 let _ = check;
157};
158
159impl System {
160 #[must_use]
162 pub fn buses(&self) -> &[Bus] {
163 &self.buses
164 }
165
166 #[must_use]
168 pub fn lines(&self) -> &[Line] {
169 &self.lines
170 }
171
172 #[must_use]
174 pub fn hydros(&self) -> &[Hydro] {
175 &self.hydros
176 }
177
178 #[must_use]
180 pub fn thermals(&self) -> &[Thermal] {
181 &self.thermals
182 }
183
184 #[must_use]
186 pub fn pumping_stations(&self) -> &[PumpingStation] {
187 &self.pumping_stations
188 }
189
190 #[must_use]
192 pub fn contracts(&self) -> &[EnergyContract] {
193 &self.contracts
194 }
195
196 #[must_use]
198 pub fn non_controllable_sources(&self) -> &[NonControllableSource] {
199 &self.non_controllable_sources
200 }
201
202 #[must_use]
204 pub fn n_buses(&self) -> usize {
205 self.buses.len()
206 }
207
208 #[must_use]
210 pub fn n_lines(&self) -> usize {
211 self.lines.len()
212 }
213
214 #[must_use]
216 pub fn n_hydros(&self) -> usize {
217 self.hydros.len()
218 }
219
220 #[must_use]
222 pub fn n_thermals(&self) -> usize {
223 self.thermals.len()
224 }
225
226 #[must_use]
228 pub fn n_pumping_stations(&self) -> usize {
229 self.pumping_stations.len()
230 }
231
232 #[must_use]
234 pub fn n_contracts(&self) -> usize {
235 self.contracts.len()
236 }
237
238 #[must_use]
240 pub fn n_non_controllable_sources(&self) -> usize {
241 self.non_controllable_sources.len()
242 }
243
244 #[must_use]
246 pub fn bus(&self, id: EntityId) -> Option<&Bus> {
247 self.bus_index.get(&id).map(|&i| &self.buses[i])
248 }
249
250 #[must_use]
252 pub fn line(&self, id: EntityId) -> Option<&Line> {
253 self.line_index.get(&id).map(|&i| &self.lines[i])
254 }
255
256 #[must_use]
258 pub fn hydro(&self, id: EntityId) -> Option<&Hydro> {
259 self.hydro_index.get(&id).map(|&i| &self.hydros[i])
260 }
261
262 #[must_use]
264 pub fn thermal(&self, id: EntityId) -> Option<&Thermal> {
265 self.thermal_index.get(&id).map(|&i| &self.thermals[i])
266 }
267
268 #[must_use]
270 pub fn pumping_station(&self, id: EntityId) -> Option<&PumpingStation> {
271 self.pumping_station_index
272 .get(&id)
273 .map(|&i| &self.pumping_stations[i])
274 }
275
276 #[must_use]
278 pub fn contract(&self, id: EntityId) -> Option<&EnergyContract> {
279 self.contract_index.get(&id).map(|&i| &self.contracts[i])
280 }
281
282 #[must_use]
284 pub fn non_controllable_source(&self, id: EntityId) -> Option<&NonControllableSource> {
285 self.non_controllable_source_index
286 .get(&id)
287 .map(|&i| &self.non_controllable_sources[i])
288 }
289
290 #[must_use]
292 pub fn cascade(&self) -> &CascadeTopology {
293 &self.cascade
294 }
295
296 #[must_use]
298 pub fn network(&self) -> &NetworkTopology {
299 &self.network
300 }
301
302 #[must_use]
304 pub fn stages(&self) -> &[Stage] {
305 &self.stages
306 }
307
308 #[must_use]
310 pub fn n_stages(&self) -> usize {
311 self.stages.len()
312 }
313
314 #[must_use]
319 pub fn stage(&self, id: i32) -> Option<&Stage> {
320 self.stage_index.get(&id).map(|&i| &self.stages[i])
321 }
322
323 #[must_use]
325 pub fn policy_graph(&self) -> &PolicyGraph {
326 &self.policy_graph
327 }
328
329 #[must_use]
331 pub fn penalties(&self) -> &ResolvedPenalties {
332 &self.penalties
333 }
334
335 #[must_use]
337 pub fn bounds(&self) -> &ResolvedBounds {
338 &self.bounds
339 }
340
341 #[must_use]
343 pub fn resolved_generic_bounds(&self) -> &ResolvedGenericConstraintBounds {
344 &self.resolved_generic_bounds
345 }
346
347 #[must_use]
349 pub fn resolved_load_factors(&self) -> &ResolvedLoadFactors {
350 &self.resolved_load_factors
351 }
352
353 #[must_use]
355 pub fn resolved_exchange_factors(&self) -> &ResolvedExchangeFactors {
356 &self.resolved_exchange_factors
357 }
358
359 #[must_use]
361 pub fn resolved_ncs_bounds(&self) -> &ResolvedNcsBounds {
362 &self.resolved_ncs_bounds
363 }
364
365 #[must_use]
367 pub fn resolved_ncs_factors(&self) -> &ResolvedNcsFactors {
368 &self.resolved_ncs_factors
369 }
370
371 #[must_use]
373 pub fn inflow_models(&self) -> &[InflowModel] {
374 &self.inflow_models
375 }
376
377 #[must_use]
379 pub fn load_models(&self) -> &[LoadModel] {
380 &self.load_models
381 }
382
383 #[must_use]
385 pub fn ncs_models(&self) -> &[NcsModel] {
386 &self.ncs_models
387 }
388
389 #[must_use]
391 pub fn correlation(&self) -> &CorrelationModel {
392 &self.correlation
393 }
394
395 #[must_use]
397 pub fn initial_conditions(&self) -> &InitialConditions {
398 &self.initial_conditions
399 }
400
401 #[must_use]
403 pub fn generic_constraints(&self) -> &[GenericConstraint] {
404 &self.generic_constraints
405 }
406
407 #[must_use]
412 pub fn inflow_history(&self) -> &[InflowHistoryRow] {
413 &self.inflow_history
414 }
415
416 #[must_use]
420 pub fn external_scenarios(&self) -> &[ExternalScenarioRow] {
421 &self.external_scenarios
422 }
423
424 #[must_use]
428 pub fn external_load_scenarios(&self) -> &[ExternalLoadRow] {
429 &self.external_load_scenarios
430 }
431
432 #[must_use]
436 pub fn external_ncs_scenarios(&self) -> &[ExternalNcsRow] {
437 &self.external_ncs_scenarios
438 }
439
440 #[must_use]
468 pub fn with_scenario_models(
469 mut self,
470 inflow_models: Vec<InflowModel>,
471 correlation: CorrelationModel,
472 ) -> Self {
473 self.inflow_models = inflow_models;
474 self.correlation = correlation;
475 self
476 }
477
478 pub fn rebuild_indices(&mut self) {
511 self.bus_index = build_index(&self.buses);
512 self.line_index = build_index(&self.lines);
513 self.hydro_index = build_index(&self.hydros);
514 self.thermal_index = build_index(&self.thermals);
515 self.pumping_station_index = build_index(&self.pumping_stations);
516 self.contract_index = build_index(&self.contracts);
517 self.non_controllable_source_index = build_index(&self.non_controllable_sources);
518 self.stage_index = build_stage_index(&self.stages);
519 }
520}
521
522pub struct SystemBuilder {
546 buses: Vec<Bus>,
547 lines: Vec<Line>,
548 hydros: Vec<Hydro>,
549 thermals: Vec<Thermal>,
550 pumping_stations: Vec<PumpingStation>,
551 contracts: Vec<EnergyContract>,
552 non_controllable_sources: Vec<NonControllableSource>,
553 stages: Vec<Stage>,
554 policy_graph: PolicyGraph,
555 penalties: ResolvedPenalties,
556 bounds: ResolvedBounds,
557 resolved_generic_bounds: ResolvedGenericConstraintBounds,
558 resolved_load_factors: ResolvedLoadFactors,
559 resolved_exchange_factors: ResolvedExchangeFactors,
560 resolved_ncs_bounds: ResolvedNcsBounds,
561 resolved_ncs_factors: ResolvedNcsFactors,
562 inflow_models: Vec<InflowModel>,
563 load_models: Vec<LoadModel>,
564 ncs_models: Vec<NcsModel>,
565 correlation: CorrelationModel,
566 initial_conditions: InitialConditions,
567 generic_constraints: Vec<GenericConstraint>,
568 inflow_history: Vec<InflowHistoryRow>,
569 external_scenarios: Vec<ExternalScenarioRow>,
570 external_load_scenarios: Vec<ExternalLoadRow>,
571 external_ncs_scenarios: Vec<ExternalNcsRow>,
572}
573
574impl Default for SystemBuilder {
575 fn default() -> Self {
576 Self::new()
577 }
578}
579
580impl SystemBuilder {
581 #[must_use]
586 pub fn new() -> Self {
587 Self {
588 buses: Vec::new(),
589 lines: Vec::new(),
590 hydros: Vec::new(),
591 thermals: Vec::new(),
592 pumping_stations: Vec::new(),
593 contracts: Vec::new(),
594 non_controllable_sources: Vec::new(),
595 stages: Vec::new(),
596 policy_graph: PolicyGraph::default(),
597 penalties: ResolvedPenalties::empty(),
598 bounds: ResolvedBounds::empty(),
599 resolved_generic_bounds: ResolvedGenericConstraintBounds::empty(),
600 resolved_load_factors: ResolvedLoadFactors::empty(),
601 resolved_exchange_factors: ResolvedExchangeFactors::empty(),
602 resolved_ncs_bounds: ResolvedNcsBounds::empty(),
603 resolved_ncs_factors: ResolvedNcsFactors::empty(),
604 inflow_models: Vec::new(),
605 load_models: Vec::new(),
606 ncs_models: Vec::new(),
607 correlation: CorrelationModel::default(),
608 initial_conditions: InitialConditions::default(),
609 generic_constraints: Vec::new(),
610 inflow_history: Vec::new(),
611 external_scenarios: Vec::new(),
612 external_load_scenarios: Vec::new(),
613 external_ncs_scenarios: Vec::new(),
614 }
615 }
616
617 #[must_use]
619 pub fn buses(mut self, buses: Vec<Bus>) -> Self {
620 self.buses = buses;
621 self
622 }
623
624 #[must_use]
626 pub fn lines(mut self, lines: Vec<Line>) -> Self {
627 self.lines = lines;
628 self
629 }
630
631 #[must_use]
633 pub fn hydros(mut self, hydros: Vec<Hydro>) -> Self {
634 self.hydros = hydros;
635 self
636 }
637
638 #[must_use]
640 pub fn thermals(mut self, thermals: Vec<Thermal>) -> Self {
641 self.thermals = thermals;
642 self
643 }
644
645 #[must_use]
647 pub fn pumping_stations(mut self, stations: Vec<PumpingStation>) -> Self {
648 self.pumping_stations = stations;
649 self
650 }
651
652 #[must_use]
654 pub fn contracts(mut self, contracts: Vec<EnergyContract>) -> Self {
655 self.contracts = contracts;
656 self
657 }
658
659 #[must_use]
661 pub fn non_controllable_sources(mut self, sources: Vec<NonControllableSource>) -> Self {
662 self.non_controllable_sources = sources;
663 self
664 }
665
666 #[must_use]
670 pub fn stages(mut self, stages: Vec<Stage>) -> Self {
671 self.stages = stages;
672 self
673 }
674
675 #[must_use]
677 pub fn policy_graph(mut self, policy_graph: PolicyGraph) -> Self {
678 self.policy_graph = policy_graph;
679 self
680 }
681
682 #[must_use]
686 pub fn penalties(mut self, penalties: ResolvedPenalties) -> Self {
687 self.penalties = penalties;
688 self
689 }
690
691 #[must_use]
695 pub fn bounds(mut self, bounds: ResolvedBounds) -> Self {
696 self.bounds = bounds;
697 self
698 }
699
700 #[must_use]
705 pub fn resolved_generic_bounds(
706 mut self,
707 resolved_generic_bounds: ResolvedGenericConstraintBounds,
708 ) -> Self {
709 self.resolved_generic_bounds = resolved_generic_bounds;
710 self
711 }
712
713 #[must_use]
717 pub fn resolved_load_factors(mut self, resolved_load_factors: ResolvedLoadFactors) -> Self {
718 self.resolved_load_factors = resolved_load_factors;
719 self
720 }
721
722 #[must_use]
726 pub fn resolved_exchange_factors(
727 mut self,
728 resolved_exchange_factors: ResolvedExchangeFactors,
729 ) -> Self {
730 self.resolved_exchange_factors = resolved_exchange_factors;
731 self
732 }
733
734 #[must_use]
738 pub fn resolved_ncs_bounds(mut self, resolved_ncs_bounds: ResolvedNcsBounds) -> Self {
739 self.resolved_ncs_bounds = resolved_ncs_bounds;
740 self
741 }
742
743 #[must_use]
747 pub fn resolved_ncs_factors(mut self, resolved_ncs_factors: ResolvedNcsFactors) -> Self {
748 self.resolved_ncs_factors = resolved_ncs_factors;
749 self
750 }
751
752 #[must_use]
754 pub fn inflow_models(mut self, inflow_models: Vec<InflowModel>) -> Self {
755 self.inflow_models = inflow_models;
756 self
757 }
758
759 #[must_use]
761 pub fn load_models(mut self, load_models: Vec<LoadModel>) -> Self {
762 self.load_models = load_models;
763 self
764 }
765
766 #[must_use]
768 pub fn ncs_models(mut self, ncs_models: Vec<NcsModel>) -> Self {
769 self.ncs_models = ncs_models;
770 self
771 }
772
773 #[must_use]
775 pub fn correlation(mut self, correlation: CorrelationModel) -> Self {
776 self.correlation = correlation;
777 self
778 }
779
780 #[must_use]
782 pub fn initial_conditions(mut self, initial_conditions: InitialConditions) -> Self {
783 self.initial_conditions = initial_conditions;
784 self
785 }
786
787 #[must_use]
791 pub fn generic_constraints(mut self, generic_constraints: Vec<GenericConstraint>) -> Self {
792 self.generic_constraints = generic_constraints;
793 self
794 }
795
796 #[must_use]
802 pub fn inflow_history(mut self, rows: Vec<InflowHistoryRow>) -> Self {
803 self.inflow_history = rows;
804 self
805 }
806
807 #[must_use]
813 pub fn external_scenarios(mut self, rows: Vec<ExternalScenarioRow>) -> Self {
814 self.external_scenarios = rows;
815 self
816 }
817
818 #[must_use]
824 pub fn external_load_scenarios(mut self, rows: Vec<ExternalLoadRow>) -> Self {
825 self.external_load_scenarios = rows;
826 self
827 }
828
829 #[must_use]
835 pub fn external_ncs_scenarios(mut self, rows: Vec<ExternalNcsRow>) -> Self {
836 self.external_ncs_scenarios = rows;
837 self
838 }
839
840 #[allow(clippy::too_many_lines)]
865 pub fn build(mut self) -> Result<System, Vec<ValidationError>> {
866 self.buses.sort_by_key(|e| e.id.0);
867 self.lines.sort_by_key(|e| e.id.0);
868 self.hydros.sort_by_key(|e| e.id.0);
869 self.thermals.sort_by_key(|e| e.id.0);
870 self.pumping_stations.sort_by_key(|e| e.id.0);
871 self.contracts.sort_by_key(|e| e.id.0);
872 self.non_controllable_sources.sort_by_key(|e| e.id.0);
873 self.stages.sort_by_key(|s| s.id);
874 self.generic_constraints.sort_by_key(|c| c.id.0);
875
876 let mut errors: Vec<ValidationError> = Vec::new();
877 check_duplicates(&self.buses, "Bus", &mut errors);
878 check_duplicates(&self.lines, "Line", &mut errors);
879 check_duplicates(&self.hydros, "Hydro", &mut errors);
880 check_duplicates(&self.thermals, "Thermal", &mut errors);
881 check_duplicates(&self.pumping_stations, "PumpingStation", &mut errors);
882 check_duplicates(&self.contracts, "EnergyContract", &mut errors);
883 check_duplicates(
884 &self.non_controllable_sources,
885 "NonControllableSource",
886 &mut errors,
887 );
888
889 if !errors.is_empty() {
890 return Err(errors);
891 }
892
893 let bus_index = build_index(&self.buses);
894 let line_index = build_index(&self.lines);
895 let hydro_index = build_index(&self.hydros);
896 let thermal_index = build_index(&self.thermals);
897 let pumping_station_index = build_index(&self.pumping_stations);
898 let contract_index = build_index(&self.contracts);
899 let non_controllable_source_index = build_index(&self.non_controllable_sources);
900
901 validate_cross_references(
902 &CrossRefEntities {
903 lines: &self.lines,
904 hydros: &self.hydros,
905 thermals: &self.thermals,
906 pumping_stations: &self.pumping_stations,
907 contracts: &self.contracts,
908 non_controllable_sources: &self.non_controllable_sources,
909 },
910 &bus_index,
911 &hydro_index,
912 &mut errors,
913 );
914
915 if !errors.is_empty() {
916 return Err(errors);
917 }
918
919 let cascade = CascadeTopology::build(&self.hydros);
920
921 if cascade.topological_order().len() < self.hydros.len() {
922 let in_topo: HashSet<EntityId> = cascade.topological_order().iter().copied().collect();
923 let mut cycle_ids: Vec<EntityId> = self
924 .hydros
925 .iter()
926 .map(|h| h.id)
927 .filter(|id| !in_topo.contains(id))
928 .collect();
929 cycle_ids.sort_by_key(|id| id.0);
930 errors.push(ValidationError::CascadeCycle { cycle_ids });
931 }
932
933 validate_filling_configs(&self.hydros, &mut errors);
934
935 if !errors.is_empty() {
936 return Err(errors);
937 }
938
939 let network = NetworkTopology::build(
940 &self.buses,
941 &self.lines,
942 &self.hydros,
943 &self.thermals,
944 &self.non_controllable_sources,
945 &self.contracts,
946 &self.pumping_stations,
947 );
948
949 let stage_index = build_stage_index(&self.stages);
950
951 Ok(System {
952 buses: self.buses,
953 lines: self.lines,
954 hydros: self.hydros,
955 thermals: self.thermals,
956 pumping_stations: self.pumping_stations,
957 contracts: self.contracts,
958 non_controllable_sources: self.non_controllable_sources,
959 bus_index,
960 line_index,
961 hydro_index,
962 thermal_index,
963 pumping_station_index,
964 contract_index,
965 non_controllable_source_index,
966 cascade,
967 network,
968 stages: self.stages,
969 policy_graph: self.policy_graph,
970 stage_index,
971 penalties: self.penalties,
972 bounds: self.bounds,
973 resolved_generic_bounds: self.resolved_generic_bounds,
974 resolved_load_factors: self.resolved_load_factors,
975 resolved_exchange_factors: self.resolved_exchange_factors,
976 resolved_ncs_bounds: self.resolved_ncs_bounds,
977 resolved_ncs_factors: self.resolved_ncs_factors,
978 inflow_models: self.inflow_models,
979 load_models: self.load_models,
980 ncs_models: self.ncs_models,
981 correlation: self.correlation,
982 initial_conditions: self.initial_conditions,
983 generic_constraints: self.generic_constraints,
984 inflow_history: self.inflow_history,
985 external_scenarios: self.external_scenarios,
986 external_load_scenarios: self.external_load_scenarios,
987 external_ncs_scenarios: self.external_ncs_scenarios,
988 })
989 }
990}
991
992trait HasId {
993 fn entity_id(&self) -> EntityId;
994}
995
996impl HasId for Bus {
997 fn entity_id(&self) -> EntityId {
998 self.id
999 }
1000}
1001impl HasId for Line {
1002 fn entity_id(&self) -> EntityId {
1003 self.id
1004 }
1005}
1006impl HasId for Hydro {
1007 fn entity_id(&self) -> EntityId {
1008 self.id
1009 }
1010}
1011impl HasId for Thermal {
1012 fn entity_id(&self) -> EntityId {
1013 self.id
1014 }
1015}
1016impl HasId for PumpingStation {
1017 fn entity_id(&self) -> EntityId {
1018 self.id
1019 }
1020}
1021impl HasId for EnergyContract {
1022 fn entity_id(&self) -> EntityId {
1023 self.id
1024 }
1025}
1026impl HasId for NonControllableSource {
1027 fn entity_id(&self) -> EntityId {
1028 self.id
1029 }
1030}
1031
1032fn build_index<T: HasId>(entities: &[T]) -> HashMap<EntityId, usize> {
1033 let mut index = HashMap::with_capacity(entities.len());
1034 for (i, entity) in entities.iter().enumerate() {
1035 index.insert(entity.entity_id(), i);
1036 }
1037 index
1038}
1039
1040fn build_stage_index(stages: &[Stage]) -> HashMap<i32, usize> {
1044 let mut index = HashMap::with_capacity(stages.len());
1045 for (i, stage) in stages.iter().enumerate() {
1046 index.insert(stage.id, i);
1047 }
1048 index
1049}
1050
1051fn check_duplicates<T: HasId>(
1052 entities: &[T],
1053 entity_type: &'static str,
1054 errors: &mut Vec<ValidationError>,
1055) {
1056 for window in entities.windows(2) {
1057 if window[0].entity_id() == window[1].entity_id() {
1058 errors.push(ValidationError::DuplicateId {
1059 entity_type,
1060 id: window[0].entity_id(),
1061 });
1062 }
1063 }
1064}
1065
1066struct CrossRefEntities<'a> {
1076 lines: &'a [Line],
1077 hydros: &'a [Hydro],
1078 thermals: &'a [Thermal],
1079 pumping_stations: &'a [PumpingStation],
1080 contracts: &'a [EnergyContract],
1081 non_controllable_sources: &'a [NonControllableSource],
1082}
1083
1084fn validate_cross_references(
1085 entities: &CrossRefEntities<'_>,
1086 bus_index: &HashMap<EntityId, usize>,
1087 hydro_index: &HashMap<EntityId, usize>,
1088 errors: &mut Vec<ValidationError>,
1089) {
1090 validate_line_refs(entities.lines, bus_index, errors);
1091 validate_hydro_refs(entities.hydros, bus_index, hydro_index, errors);
1092 validate_thermal_refs(entities.thermals, bus_index, errors);
1093 validate_pumping_station_refs(entities.pumping_stations, bus_index, hydro_index, errors);
1094 validate_contract_refs(entities.contracts, bus_index, errors);
1095 validate_ncs_refs(entities.non_controllable_sources, bus_index, errors);
1096}
1097
1098fn validate_line_refs(
1099 lines: &[Line],
1100 bus_index: &HashMap<EntityId, usize>,
1101 errors: &mut Vec<ValidationError>,
1102) {
1103 for line in lines {
1104 if !bus_index.contains_key(&line.source_bus_id) {
1105 errors.push(ValidationError::InvalidReference {
1106 source_entity_type: "Line",
1107 source_id: line.id,
1108 field_name: "source_bus_id",
1109 referenced_id: line.source_bus_id,
1110 expected_type: "Bus",
1111 });
1112 }
1113 if !bus_index.contains_key(&line.target_bus_id) {
1114 errors.push(ValidationError::InvalidReference {
1115 source_entity_type: "Line",
1116 source_id: line.id,
1117 field_name: "target_bus_id",
1118 referenced_id: line.target_bus_id,
1119 expected_type: "Bus",
1120 });
1121 }
1122 }
1123}
1124
1125fn validate_hydro_refs(
1126 hydros: &[Hydro],
1127 bus_index: &HashMap<EntityId, usize>,
1128 hydro_index: &HashMap<EntityId, usize>,
1129 errors: &mut Vec<ValidationError>,
1130) {
1131 for hydro in hydros {
1132 if !bus_index.contains_key(&hydro.bus_id) {
1133 errors.push(ValidationError::InvalidReference {
1134 source_entity_type: "Hydro",
1135 source_id: hydro.id,
1136 field_name: "bus_id",
1137 referenced_id: hydro.bus_id,
1138 expected_type: "Bus",
1139 });
1140 }
1141 if let Some(downstream_id) = hydro.downstream_id {
1142 if !hydro_index.contains_key(&downstream_id) {
1143 errors.push(ValidationError::InvalidReference {
1144 source_entity_type: "Hydro",
1145 source_id: hydro.id,
1146 field_name: "downstream_id",
1147 referenced_id: downstream_id,
1148 expected_type: "Hydro",
1149 });
1150 }
1151 }
1152 if let Some(ref diversion) = hydro.diversion {
1153 if !hydro_index.contains_key(&diversion.downstream_id) {
1154 errors.push(ValidationError::InvalidReference {
1155 source_entity_type: "Hydro",
1156 source_id: hydro.id,
1157 field_name: "diversion.downstream_id",
1158 referenced_id: diversion.downstream_id,
1159 expected_type: "Hydro",
1160 });
1161 }
1162 }
1163 }
1164}
1165
1166fn validate_thermal_refs(
1167 thermals: &[Thermal],
1168 bus_index: &HashMap<EntityId, usize>,
1169 errors: &mut Vec<ValidationError>,
1170) {
1171 for thermal in thermals {
1172 if !bus_index.contains_key(&thermal.bus_id) {
1173 errors.push(ValidationError::InvalidReference {
1174 source_entity_type: "Thermal",
1175 source_id: thermal.id,
1176 field_name: "bus_id",
1177 referenced_id: thermal.bus_id,
1178 expected_type: "Bus",
1179 });
1180 }
1181 }
1182}
1183
1184fn validate_pumping_station_refs(
1185 pumping_stations: &[PumpingStation],
1186 bus_index: &HashMap<EntityId, usize>,
1187 hydro_index: &HashMap<EntityId, usize>,
1188 errors: &mut Vec<ValidationError>,
1189) {
1190 for ps in pumping_stations {
1191 if !bus_index.contains_key(&ps.bus_id) {
1192 errors.push(ValidationError::InvalidReference {
1193 source_entity_type: "PumpingStation",
1194 source_id: ps.id,
1195 field_name: "bus_id",
1196 referenced_id: ps.bus_id,
1197 expected_type: "Bus",
1198 });
1199 }
1200 if !hydro_index.contains_key(&ps.source_hydro_id) {
1201 errors.push(ValidationError::InvalidReference {
1202 source_entity_type: "PumpingStation",
1203 source_id: ps.id,
1204 field_name: "source_hydro_id",
1205 referenced_id: ps.source_hydro_id,
1206 expected_type: "Hydro",
1207 });
1208 }
1209 if !hydro_index.contains_key(&ps.destination_hydro_id) {
1210 errors.push(ValidationError::InvalidReference {
1211 source_entity_type: "PumpingStation",
1212 source_id: ps.id,
1213 field_name: "destination_hydro_id",
1214 referenced_id: ps.destination_hydro_id,
1215 expected_type: "Hydro",
1216 });
1217 }
1218 }
1219}
1220
1221fn validate_contract_refs(
1222 contracts: &[EnergyContract],
1223 bus_index: &HashMap<EntityId, usize>,
1224 errors: &mut Vec<ValidationError>,
1225) {
1226 for contract in contracts {
1227 if !bus_index.contains_key(&contract.bus_id) {
1228 errors.push(ValidationError::InvalidReference {
1229 source_entity_type: "EnergyContract",
1230 source_id: contract.id,
1231 field_name: "bus_id",
1232 referenced_id: contract.bus_id,
1233 expected_type: "Bus",
1234 });
1235 }
1236 }
1237}
1238
1239fn validate_ncs_refs(
1240 non_controllable_sources: &[NonControllableSource],
1241 bus_index: &HashMap<EntityId, usize>,
1242 errors: &mut Vec<ValidationError>,
1243) {
1244 for ncs in non_controllable_sources {
1245 if !bus_index.contains_key(&ncs.bus_id) {
1246 errors.push(ValidationError::InvalidReference {
1247 source_entity_type: "NonControllableSource",
1248 source_id: ncs.id,
1249 field_name: "bus_id",
1250 referenced_id: ncs.bus_id,
1251 expected_type: "Bus",
1252 });
1253 }
1254 }
1255}
1256
1257fn validate_filling_configs(hydros: &[Hydro], errors: &mut Vec<ValidationError>) {
1265 for hydro in hydros {
1266 if let Some(filling) = &hydro.filling {
1267 if filling.filling_inflow_m3s.is_nan() || filling.filling_inflow_m3s <= 0.0 {
1268 errors.push(ValidationError::InvalidFillingConfig {
1269 hydro_id: hydro.id,
1270 reason: "filling_inflow_m3s must be positive".to_string(),
1271 });
1272 }
1273 if hydro.entry_stage_id.is_none() {
1274 errors.push(ValidationError::InvalidFillingConfig {
1275 hydro_id: hydro.id,
1276 reason: "filling requires entry_stage_id to be set".to_string(),
1277 });
1278 }
1279 }
1280 }
1281}
1282
1283#[cfg(test)]
1284mod tests {
1285 use super::*;
1286 use crate::entities::{ContractType, HydroGenerationModel, HydroPenalties};
1287
1288 fn make_bus(id: i32) -> Bus {
1289 Bus {
1290 id: EntityId(id),
1291 name: format!("bus-{id}"),
1292 deficit_segments: vec![],
1293 excess_cost: 0.0,
1294 }
1295 }
1296
1297 fn make_line(id: i32, source_bus_id: i32, target_bus_id: i32) -> Line {
1298 crate::Line {
1299 id: EntityId(id),
1300 name: format!("line-{id}"),
1301 source_bus_id: EntityId(source_bus_id),
1302 target_bus_id: EntityId(target_bus_id),
1303 entry_stage_id: None,
1304 exit_stage_id: None,
1305 direct_capacity_mw: 100.0,
1306 reverse_capacity_mw: 100.0,
1307 losses_percent: 0.0,
1308 exchange_cost: 0.0,
1309 }
1310 }
1311
1312 fn make_hydro_on_bus(id: i32, bus_id: i32) -> Hydro {
1313 let zero_penalties = HydroPenalties {
1314 spillage_cost: 0.0,
1315 diversion_cost: 0.0,
1316 fpha_turbined_cost: 0.0,
1317 storage_violation_below_cost: 0.0,
1318 filling_target_violation_cost: 0.0,
1319 turbined_violation_below_cost: 0.0,
1320 outflow_violation_below_cost: 0.0,
1321 outflow_violation_above_cost: 0.0,
1322 generation_violation_below_cost: 0.0,
1323 evaporation_violation_cost: 0.0,
1324 water_withdrawal_violation_cost: 0.0,
1325 water_withdrawal_violation_pos_cost: 0.0,
1326 water_withdrawal_violation_neg_cost: 0.0,
1327 evaporation_violation_pos_cost: 0.0,
1328 evaporation_violation_neg_cost: 0.0,
1329 inflow_nonnegativity_cost: 1000.0,
1330 };
1331 Hydro {
1332 id: EntityId(id),
1333 name: format!("hydro-{id}"),
1334 bus_id: EntityId(bus_id),
1335 downstream_id: None,
1336 entry_stage_id: None,
1337 exit_stage_id: None,
1338 min_storage_hm3: 0.0,
1339 max_storage_hm3: 1.0,
1340 min_outflow_m3s: 0.0,
1341 max_outflow_m3s: None,
1342 generation_model: HydroGenerationModel::ConstantProductivity {
1343 productivity_mw_per_m3s: 1.0,
1344 },
1345 min_turbined_m3s: 0.0,
1346 max_turbined_m3s: 1.0,
1347 min_generation_mw: 0.0,
1348 max_generation_mw: 1.0,
1349 tailrace: None,
1350 hydraulic_losses: None,
1351 efficiency: None,
1352 evaporation_coefficients_mm: None,
1353 evaporation_reference_volumes_hm3: None,
1354 diversion: None,
1355 filling: None,
1356 penalties: zero_penalties,
1357 }
1358 }
1359
1360 fn make_hydro(id: i32) -> Hydro {
1362 make_hydro_on_bus(id, 0)
1363 }
1364
1365 fn make_thermal_on_bus(id: i32, bus_id: i32) -> Thermal {
1366 Thermal {
1367 id: EntityId(id),
1368 name: format!("thermal-{id}"),
1369 bus_id: EntityId(bus_id),
1370 entry_stage_id: None,
1371 exit_stage_id: None,
1372 cost_per_mwh: 50.0,
1373 min_generation_mw: 0.0,
1374 max_generation_mw: 100.0,
1375 gnl_config: None,
1376 }
1377 }
1378
1379 fn make_thermal(id: i32) -> Thermal {
1381 make_thermal_on_bus(id, 0)
1382 }
1383
1384 fn make_pumping_station_full(
1385 id: i32,
1386 bus_id: i32,
1387 source_hydro_id: i32,
1388 destination_hydro_id: i32,
1389 ) -> PumpingStation {
1390 PumpingStation {
1391 id: EntityId(id),
1392 name: format!("ps-{id}"),
1393 bus_id: EntityId(bus_id),
1394 source_hydro_id: EntityId(source_hydro_id),
1395 destination_hydro_id: EntityId(destination_hydro_id),
1396 entry_stage_id: None,
1397 exit_stage_id: None,
1398 consumption_mw_per_m3s: 0.5,
1399 min_flow_m3s: 0.0,
1400 max_flow_m3s: 10.0,
1401 }
1402 }
1403
1404 fn make_pumping_station(id: i32) -> PumpingStation {
1405 make_pumping_station_full(id, 0, 0, 1)
1406 }
1407
1408 fn make_contract_on_bus(id: i32, bus_id: i32) -> EnergyContract {
1409 EnergyContract {
1410 id: EntityId(id),
1411 name: format!("contract-{id}"),
1412 bus_id: EntityId(bus_id),
1413 contract_type: ContractType::Import,
1414 entry_stage_id: None,
1415 exit_stage_id: None,
1416 price_per_mwh: 0.0,
1417 min_mw: 0.0,
1418 max_mw: 100.0,
1419 }
1420 }
1421
1422 fn make_contract(id: i32) -> EnergyContract {
1423 make_contract_on_bus(id, 0)
1424 }
1425
1426 fn make_ncs_on_bus(id: i32, bus_id: i32) -> NonControllableSource {
1427 NonControllableSource {
1428 id: EntityId(id),
1429 name: format!("ncs-{id}"),
1430 bus_id: EntityId(bus_id),
1431 entry_stage_id: None,
1432 exit_stage_id: None,
1433 max_generation_mw: 50.0,
1434 curtailment_cost: 0.0,
1435 }
1436 }
1437
1438 fn make_ncs(id: i32) -> NonControllableSource {
1439 make_ncs_on_bus(id, 0)
1440 }
1441
1442 #[test]
1443 fn test_empty_system() {
1444 let system = SystemBuilder::new().build().expect("empty system is valid");
1445 assert_eq!(system.n_buses(), 0);
1446 assert_eq!(system.n_lines(), 0);
1447 assert_eq!(system.n_hydros(), 0);
1448 assert_eq!(system.n_thermals(), 0);
1449 assert_eq!(system.n_pumping_stations(), 0);
1450 assert_eq!(system.n_contracts(), 0);
1451 assert_eq!(system.n_non_controllable_sources(), 0);
1452 assert!(system.buses().is_empty());
1453 assert!(system.cascade().is_empty());
1454 }
1455
1456 #[test]
1457 fn test_canonical_ordering() {
1458 let system = SystemBuilder::new()
1460 .buses(vec![make_bus(2), make_bus(1), make_bus(0)])
1461 .build()
1462 .expect("valid system");
1463
1464 assert_eq!(system.buses()[0].id, EntityId(0));
1465 assert_eq!(system.buses()[1].id, EntityId(1));
1466 assert_eq!(system.buses()[2].id, EntityId(2));
1467 }
1468
1469 #[test]
1470 fn test_lookup_by_id() {
1471 let system = SystemBuilder::new()
1473 .buses(vec![make_bus(0)])
1474 .hydros(vec![make_hydro(10), make_hydro(5), make_hydro(20)])
1475 .build()
1476 .expect("valid system");
1477
1478 assert_eq!(system.hydro(EntityId(5)).map(|h| h.id), Some(EntityId(5)));
1479 assert_eq!(system.hydro(EntityId(10)).map(|h| h.id), Some(EntityId(10)));
1480 assert_eq!(system.hydro(EntityId(20)).map(|h| h.id), Some(EntityId(20)));
1481 }
1482
1483 #[test]
1484 fn test_lookup_missing_id() {
1485 let system = SystemBuilder::new()
1487 .buses(vec![make_bus(0)])
1488 .hydros(vec![make_hydro(1), make_hydro(2)])
1489 .build()
1490 .expect("valid system");
1491
1492 assert!(system.hydro(EntityId(999)).is_none());
1493 }
1494
1495 #[test]
1496 fn test_count_queries() {
1497 let system = SystemBuilder::new()
1498 .buses(vec![make_bus(0), make_bus(1)])
1499 .lines(vec![make_line(0, 0, 1)])
1500 .hydros(vec![make_hydro(0), make_hydro(1), make_hydro(2)])
1501 .thermals(vec![make_thermal(0)])
1502 .pumping_stations(vec![make_pumping_station(0)])
1503 .contracts(vec![make_contract(0), make_contract(1)])
1504 .non_controllable_sources(vec![make_ncs(0)])
1505 .build()
1506 .expect("valid system");
1507
1508 assert_eq!(system.n_buses(), 2);
1509 assert_eq!(system.n_lines(), 1);
1510 assert_eq!(system.n_hydros(), 3);
1511 assert_eq!(system.n_thermals(), 1);
1512 assert_eq!(system.n_pumping_stations(), 1);
1513 assert_eq!(system.n_contracts(), 2);
1514 assert_eq!(system.n_non_controllable_sources(), 1);
1515 }
1516
1517 #[test]
1518 fn test_slice_accessors() {
1519 let system = SystemBuilder::new()
1520 .buses(vec![make_bus(0), make_bus(1), make_bus(2)])
1521 .build()
1522 .expect("valid system");
1523
1524 let buses = system.buses();
1525 assert_eq!(buses.len(), 3);
1526 assert_eq!(buses[0].id, EntityId(0));
1527 assert_eq!(buses[1].id, EntityId(1));
1528 assert_eq!(buses[2].id, EntityId(2));
1529 }
1530
1531 #[test]
1532 fn test_duplicate_id_error() {
1533 let result = SystemBuilder::new()
1535 .buses(vec![make_bus(0), make_bus(0)])
1536 .build();
1537
1538 assert!(result.is_err());
1539 let errors = result.unwrap_err();
1540 assert!(!errors.is_empty());
1541 assert!(errors.iter().any(|e| matches!(
1542 e,
1543 ValidationError::DuplicateId {
1544 entity_type: "Bus",
1545 id: EntityId(0),
1546 }
1547 )));
1548 }
1549
1550 #[test]
1551 fn test_multiple_duplicate_errors() {
1552 let result = SystemBuilder::new()
1554 .buses(vec![make_bus(0), make_bus(0)])
1555 .thermals(vec![make_thermal(5), make_thermal(5)])
1556 .build();
1557
1558 assert!(result.is_err());
1559 let errors = result.unwrap_err();
1560
1561 let has_bus_dup = errors.iter().any(|e| {
1562 matches!(
1563 e,
1564 ValidationError::DuplicateId {
1565 entity_type: "Bus",
1566 ..
1567 }
1568 )
1569 });
1570 let has_thermal_dup = errors.iter().any(|e| {
1571 matches!(
1572 e,
1573 ValidationError::DuplicateId {
1574 entity_type: "Thermal",
1575 ..
1576 }
1577 )
1578 });
1579 assert!(has_bus_dup, "expected Bus duplicate error");
1580 assert!(has_thermal_dup, "expected Thermal duplicate error");
1581 }
1582
1583 #[test]
1584 fn test_send_sync() {
1585 fn require_send_sync<T: Send + Sync>(_: T) {}
1586 let system = SystemBuilder::new().build().expect("valid system");
1587 require_send_sync(system);
1588 }
1589
1590 #[test]
1591 fn test_cascade_accessible() {
1592 let mut h0 = make_hydro_on_bus(0, 0);
1594 h0.downstream_id = Some(EntityId(1));
1595 let mut h1 = make_hydro_on_bus(1, 0);
1596 h1.downstream_id = Some(EntityId(2));
1597 let h2 = make_hydro_on_bus(2, 0);
1598
1599 let system = SystemBuilder::new()
1600 .buses(vec![make_bus(0)])
1601 .hydros(vec![h0, h1, h2])
1602 .build()
1603 .expect("valid system");
1604
1605 let order = system.cascade().topological_order();
1606 assert!(!order.is_empty(), "topological order must be non-empty");
1607 let pos_0 = order
1608 .iter()
1609 .position(|&id| id == EntityId(0))
1610 .expect("EntityId(0) must be in topological order");
1611 let pos_2 = order
1612 .iter()
1613 .position(|&id| id == EntityId(2))
1614 .expect("EntityId(2) must be in topological order");
1615 assert!(pos_0 < pos_2, "EntityId(0) must precede EntityId(2)");
1616 }
1617
1618 #[test]
1619 fn test_network_accessible() {
1620 let system = SystemBuilder::new()
1621 .buses(vec![make_bus(0), make_bus(1)])
1622 .lines(vec![make_line(0, 0, 1)])
1623 .build()
1624 .expect("valid system");
1625
1626 let connections = system.network().bus_lines(EntityId(0));
1627 assert!(!connections.is_empty(), "bus 0 must have connections");
1628 assert_eq!(connections[0].line_id, EntityId(0));
1629 }
1630
1631 #[test]
1632 fn test_all_entity_lookups() {
1633 let system = SystemBuilder::new()
1638 .buses(vec![make_bus(0), make_bus(1)])
1639 .lines(vec![make_line(2, 0, 1)])
1640 .hydros(vec![
1641 make_hydro_on_bus(0, 0),
1642 make_hydro_on_bus(1, 0),
1643 make_hydro_on_bus(3, 0),
1644 ])
1645 .thermals(vec![make_thermal(4)])
1646 .pumping_stations(vec![make_pumping_station(5)])
1647 .contracts(vec![make_contract(6)])
1648 .non_controllable_sources(vec![make_ncs(7)])
1649 .build()
1650 .expect("valid system");
1651
1652 assert!(system.bus(EntityId(1)).is_some());
1653 assert!(system.line(EntityId(2)).is_some());
1654 assert!(system.hydro(EntityId(3)).is_some());
1655 assert!(system.thermal(EntityId(4)).is_some());
1656 assert!(system.pumping_station(EntityId(5)).is_some());
1657 assert!(system.contract(EntityId(6)).is_some());
1658 assert!(system.non_controllable_source(EntityId(7)).is_some());
1659
1660 assert!(system.bus(EntityId(999)).is_none());
1661 assert!(system.line(EntityId(999)).is_none());
1662 assert!(system.hydro(EntityId(999)).is_none());
1663 assert!(system.thermal(EntityId(999)).is_none());
1664 assert!(system.pumping_station(EntityId(999)).is_none());
1665 assert!(system.contract(EntityId(999)).is_none());
1666 assert!(system.non_controllable_source(EntityId(999)).is_none());
1667 }
1668
1669 #[test]
1670 fn test_default_builder() {
1671 let system = SystemBuilder::default()
1672 .build()
1673 .expect("default builder produces valid empty system");
1674 assert_eq!(system.n_buses(), 0);
1675 }
1676
1677 #[test]
1680 fn test_invalid_bus_reference_hydro() {
1681 let hydro = make_hydro_on_bus(1, 99);
1683 let result = SystemBuilder::new().hydros(vec![hydro]).build();
1684
1685 assert!(result.is_err(), "expected Err for missing bus reference");
1686 let errors = result.unwrap_err();
1687 assert!(
1688 errors.iter().any(|e| matches!(
1689 e,
1690 ValidationError::InvalidReference {
1691 source_entity_type: "Hydro",
1692 source_id: EntityId(1),
1693 field_name: "bus_id",
1694 referenced_id: EntityId(99),
1695 expected_type: "Bus",
1696 }
1697 )),
1698 "expected InvalidReference for Hydro bus_id=99, got: {errors:?}"
1699 );
1700 }
1701
1702 #[test]
1703 fn test_invalid_downstream_reference() {
1704 let bus = make_bus(0);
1706 let mut hydro = make_hydro(1);
1707 hydro.downstream_id = Some(EntityId(50));
1708
1709 let result = SystemBuilder::new()
1710 .buses(vec![bus])
1711 .hydros(vec![hydro])
1712 .build();
1713
1714 assert!(
1715 result.is_err(),
1716 "expected Err for missing downstream reference"
1717 );
1718 let errors = result.unwrap_err();
1719 assert!(
1720 errors.iter().any(|e| matches!(
1721 e,
1722 ValidationError::InvalidReference {
1723 source_entity_type: "Hydro",
1724 source_id: EntityId(1),
1725 field_name: "downstream_id",
1726 referenced_id: EntityId(50),
1727 expected_type: "Hydro",
1728 }
1729 )),
1730 "expected InvalidReference for Hydro downstream_id=50, got: {errors:?}"
1731 );
1732 }
1733
1734 #[test]
1735 fn test_invalid_pumping_station_hydro_refs() {
1736 let bus = make_bus(0);
1738 let dest_hydro = make_hydro(1);
1739 let ps = make_pumping_station_full(10, 0, 77, 1);
1740
1741 let result = SystemBuilder::new()
1742 .buses(vec![bus])
1743 .hydros(vec![dest_hydro])
1744 .pumping_stations(vec![ps])
1745 .build();
1746
1747 assert!(
1748 result.is_err(),
1749 "expected Err for missing source_hydro_id reference"
1750 );
1751 let errors = result.unwrap_err();
1752 assert!(
1753 errors.iter().any(|e| matches!(
1754 e,
1755 ValidationError::InvalidReference {
1756 source_entity_type: "PumpingStation",
1757 source_id: EntityId(10),
1758 field_name: "source_hydro_id",
1759 referenced_id: EntityId(77),
1760 expected_type: "Hydro",
1761 }
1762 )),
1763 "expected InvalidReference for PumpingStation source_hydro_id=77, got: {errors:?}"
1764 );
1765 }
1766
1767 #[test]
1768 fn test_multiple_invalid_references_collected() {
1769 let line = make_line(1, 99, 0);
1772 let thermal = make_thermal_on_bus(2, 88);
1773
1774 let result = SystemBuilder::new()
1775 .buses(vec![make_bus(0)])
1776 .lines(vec![line])
1777 .thermals(vec![thermal])
1778 .build();
1779
1780 assert!(
1781 result.is_err(),
1782 "expected Err for multiple invalid references"
1783 );
1784 let errors = result.unwrap_err();
1785
1786 let has_line_error = errors.iter().any(|e| {
1787 matches!(
1788 e,
1789 ValidationError::InvalidReference {
1790 source_entity_type: "Line",
1791 field_name: "source_bus_id",
1792 referenced_id: EntityId(99),
1793 ..
1794 }
1795 )
1796 });
1797 let has_thermal_error = errors.iter().any(|e| {
1798 matches!(
1799 e,
1800 ValidationError::InvalidReference {
1801 source_entity_type: "Thermal",
1802 field_name: "bus_id",
1803 referenced_id: EntityId(88),
1804 ..
1805 }
1806 )
1807 });
1808
1809 assert!(
1810 has_line_error,
1811 "expected Line source_bus_id=99 error, got: {errors:?}"
1812 );
1813 assert!(
1814 has_thermal_error,
1815 "expected Thermal bus_id=88 error, got: {errors:?}"
1816 );
1817 assert!(
1818 errors.len() >= 2,
1819 "expected at least 2 errors, got {}: {errors:?}",
1820 errors.len()
1821 );
1822 }
1823
1824 #[test]
1825 fn test_valid_cross_references_pass() {
1826 let bus_0 = make_bus(0);
1828 let bus_1 = make_bus(1);
1829 let h0 = make_hydro_on_bus(0, 0);
1830 let h1 = make_hydro_on_bus(1, 1);
1831 let mut h2 = make_hydro_on_bus(2, 0);
1832 h2.downstream_id = Some(EntityId(1));
1833 let line = make_line(10, 0, 1);
1834 let thermal = make_thermal_on_bus(20, 0);
1835 let ps = make_pumping_station_full(30, 0, 0, 1);
1836 let contract = make_contract_on_bus(40, 1);
1837 let ncs = make_ncs_on_bus(50, 0);
1838
1839 let result = SystemBuilder::new()
1840 .buses(vec![bus_0, bus_1])
1841 .lines(vec![line])
1842 .hydros(vec![h0, h1, h2])
1843 .thermals(vec![thermal])
1844 .pumping_stations(vec![ps])
1845 .contracts(vec![contract])
1846 .non_controllable_sources(vec![ncs])
1847 .build();
1848
1849 assert!(
1850 result.is_ok(),
1851 "expected Ok for all valid cross-references, got: {:?}",
1852 result.unwrap_err()
1853 );
1854 let system = result.unwrap_or_else(|_| unreachable!());
1855 assert_eq!(system.n_buses(), 2);
1856 assert_eq!(system.n_hydros(), 3);
1857 assert_eq!(system.n_lines(), 1);
1858 assert_eq!(system.n_thermals(), 1);
1859 assert_eq!(system.n_pumping_stations(), 1);
1860 assert_eq!(system.n_contracts(), 1);
1861 assert_eq!(system.n_non_controllable_sources(), 1);
1862 }
1863
1864 #[test]
1867 fn test_cascade_cycle_detected() {
1868 let bus = make_bus(0);
1871 let mut h0 = make_hydro(0);
1872 h0.downstream_id = Some(EntityId(1));
1873 let mut h1 = make_hydro(1);
1874 h1.downstream_id = Some(EntityId(2));
1875 let mut h2 = make_hydro(2);
1876 h2.downstream_id = Some(EntityId(0));
1877
1878 let result = SystemBuilder::new()
1879 .buses(vec![bus])
1880 .hydros(vec![h0, h1, h2])
1881 .build();
1882
1883 assert!(result.is_err(), "expected Err for 3-node cycle");
1884 let errors = result.unwrap_err();
1885 let cycle_error = errors
1886 .iter()
1887 .find(|e| matches!(e, ValidationError::CascadeCycle { .. }));
1888 assert!(
1889 cycle_error.is_some(),
1890 "expected CascadeCycle error, got: {errors:?}"
1891 );
1892 let ValidationError::CascadeCycle { cycle_ids } = cycle_error.unwrap() else {
1893 unreachable!()
1894 };
1895 assert_eq!(
1896 cycle_ids,
1897 &[EntityId(0), EntityId(1), EntityId(2)],
1898 "cycle_ids must be sorted ascending, got: {cycle_ids:?}"
1899 );
1900 }
1901
1902 #[test]
1903 fn test_cascade_self_loop_detected() {
1904 let bus = make_bus(0);
1906 let mut h0 = make_hydro(0);
1907 h0.downstream_id = Some(EntityId(0));
1908
1909 let result = SystemBuilder::new()
1910 .buses(vec![bus])
1911 .hydros(vec![h0])
1912 .build();
1913
1914 assert!(result.is_err(), "expected Err for self-loop");
1915 let errors = result.unwrap_err();
1916 let has_cycle = errors
1917 .iter()
1918 .any(|e| matches!(e, ValidationError::CascadeCycle { cycle_ids } if cycle_ids.contains(&EntityId(0))));
1919 assert!(
1920 has_cycle,
1921 "expected CascadeCycle containing EntityId(0), got: {errors:?}"
1922 );
1923 }
1924
1925 #[test]
1926 fn test_valid_acyclic_cascade_passes() {
1927 let bus = make_bus(0);
1930 let mut h0 = make_hydro(0);
1931 h0.downstream_id = Some(EntityId(1));
1932 let mut h1 = make_hydro(1);
1933 h1.downstream_id = Some(EntityId(2));
1934 let h2 = make_hydro(2);
1935
1936 let result = SystemBuilder::new()
1937 .buses(vec![bus])
1938 .hydros(vec![h0, h1, h2])
1939 .build();
1940
1941 assert!(
1942 result.is_ok(),
1943 "expected Ok for acyclic cascade, got: {:?}",
1944 result.unwrap_err()
1945 );
1946 let system = result.unwrap_or_else(|_| unreachable!());
1947 assert_eq!(
1948 system.cascade().topological_order().len(),
1949 system.n_hydros(),
1950 "topological_order must contain all hydros"
1951 );
1952 }
1953
1954 #[test]
1957 fn test_filling_without_entry_stage() {
1958 use crate::entities::FillingConfig;
1960 let bus = make_bus(0);
1961 let mut hydro = make_hydro(1);
1962 hydro.entry_stage_id = None;
1963 hydro.filling = Some(FillingConfig {
1964 start_stage_id: 10,
1965 filling_inflow_m3s: 100.0,
1966 });
1967
1968 let result = SystemBuilder::new()
1969 .buses(vec![bus])
1970 .hydros(vec![hydro])
1971 .build();
1972
1973 assert!(
1974 result.is_err(),
1975 "expected Err for filling without entry_stage_id"
1976 );
1977 let errors = result.unwrap_err();
1978 let has_error = errors.iter().any(|e| match e {
1979 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
1980 *hydro_id == EntityId(1) && reason.contains("entry_stage_id")
1981 }
1982 _ => false,
1983 });
1984 assert!(
1985 has_error,
1986 "expected InvalidFillingConfig with entry_stage_id reason, got: {errors:?}"
1987 );
1988 }
1989
1990 #[test]
1991 fn test_filling_negative_inflow() {
1992 use crate::entities::FillingConfig;
1994 let bus = make_bus(0);
1995 let mut hydro = make_hydro(1);
1996 hydro.entry_stage_id = Some(10);
1997 hydro.filling = Some(FillingConfig {
1998 start_stage_id: 10,
1999 filling_inflow_m3s: -5.0,
2000 });
2001
2002 let result = SystemBuilder::new()
2003 .buses(vec![bus])
2004 .hydros(vec![hydro])
2005 .build();
2006
2007 assert!(
2008 result.is_err(),
2009 "expected Err for negative filling_inflow_m3s"
2010 );
2011 let errors = result.unwrap_err();
2012 let has_error = errors.iter().any(|e| match e {
2013 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
2014 *hydro_id == EntityId(1) && reason.contains("filling_inflow_m3s must be positive")
2015 }
2016 _ => false,
2017 });
2018 assert!(
2019 has_error,
2020 "expected InvalidFillingConfig with positive inflow reason, got: {errors:?}"
2021 );
2022 }
2023
2024 #[test]
2025 fn test_valid_filling_config_passes() {
2026 use crate::entities::FillingConfig;
2028 let bus = make_bus(0);
2029 let mut hydro = make_hydro(1);
2030 hydro.entry_stage_id = Some(10);
2031 hydro.filling = Some(FillingConfig {
2032 start_stage_id: 10,
2033 filling_inflow_m3s: 100.0,
2034 });
2035
2036 let result = SystemBuilder::new()
2037 .buses(vec![bus])
2038 .hydros(vec![hydro])
2039 .build();
2040
2041 assert!(
2042 result.is_ok(),
2043 "expected Ok for valid filling config, got: {:?}",
2044 result.unwrap_err()
2045 );
2046 }
2047
2048 #[test]
2049 fn test_cascade_cycle_and_invalid_filling_both_reported() {
2050 use crate::entities::FillingConfig;
2053 let bus = make_bus(0);
2054
2055 let mut h0 = make_hydro(0);
2057 h0.downstream_id = Some(EntityId(0));
2058
2059 let mut h1 = make_hydro(1);
2061 h1.entry_stage_id = None; h1.filling = Some(FillingConfig {
2063 start_stage_id: 5,
2064 filling_inflow_m3s: 50.0,
2065 });
2066
2067 let result = SystemBuilder::new()
2068 .buses(vec![bus])
2069 .hydros(vec![h0, h1])
2070 .build();
2071
2072 assert!(result.is_err(), "expected Err for cycle + invalid filling");
2073 let errors = result.unwrap_err();
2074 let has_cycle = errors
2075 .iter()
2076 .any(|e| matches!(e, ValidationError::CascadeCycle { .. }));
2077 let has_filling = errors
2078 .iter()
2079 .any(|e| matches!(e, ValidationError::InvalidFillingConfig { .. }));
2080 assert!(has_cycle, "expected CascadeCycle error, got: {errors:?}");
2081 assert!(
2082 has_filling,
2083 "expected InvalidFillingConfig error, got: {errors:?}"
2084 );
2085 }
2086
2087 #[cfg(feature = "serde")]
2088 #[test]
2089 fn test_system_serde_roundtrip() {
2090 let bus_a = make_bus(1);
2092 let bus_b = make_bus(2);
2093 let hydro = make_hydro_on_bus(10, 1);
2094 let thermal = make_thermal_on_bus(20, 2);
2095 let line = make_line(1, 1, 2);
2096
2097 let system = SystemBuilder::new()
2098 .buses(vec![bus_a, bus_b])
2099 .hydros(vec![hydro])
2100 .thermals(vec![thermal])
2101 .lines(vec![line])
2102 .build()
2103 .expect("valid system");
2104
2105 let json = serde_json::to_string(&system).unwrap();
2106
2107 let mut deserialized: System = serde_json::from_str(&json).unwrap();
2109 deserialized.rebuild_indices();
2110
2111 assert_eq!(system.buses(), deserialized.buses());
2113 assert_eq!(system.hydros(), deserialized.hydros());
2114 assert_eq!(system.thermals(), deserialized.thermals());
2115 assert_eq!(system.lines(), deserialized.lines());
2116
2117 assert_eq!(
2119 deserialized.bus(EntityId(1)).map(|b| b.id),
2120 Some(EntityId(1))
2121 );
2122 assert_eq!(
2123 deserialized.hydro(EntityId(10)).map(|h| h.id),
2124 Some(EntityId(10))
2125 );
2126 assert_eq!(
2127 deserialized.thermal(EntityId(20)).map(|t| t.id),
2128 Some(EntityId(20))
2129 );
2130 assert_eq!(
2131 deserialized.line(EntityId(1)).map(|l| l.id),
2132 Some(EntityId(1))
2133 );
2134 }
2135
2136 fn make_stage(id: i32) -> Stage {
2139 use crate::temporal::{
2140 Block, BlockMode, NoiseMethod, ScenarioSourceConfig, StageRiskConfig, StageStateConfig,
2141 };
2142 use chrono::NaiveDate;
2143 Stage {
2144 index: usize::try_from(id.max(0)).unwrap_or(0),
2145 id,
2146 start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
2147 end_date: NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
2148 season_id: Some(0),
2149 blocks: vec![Block {
2150 index: 0,
2151 name: "SINGLE".to_string(),
2152 duration_hours: 744.0,
2153 }],
2154 block_mode: BlockMode::Parallel,
2155 state_config: StageStateConfig {
2156 storage: true,
2157 inflow_lags: false,
2158 },
2159 risk_config: StageRiskConfig::Expectation,
2160 scenario_config: ScenarioSourceConfig {
2161 branching_factor: 50,
2162 noise_method: NoiseMethod::Saa,
2163 },
2164 }
2165 }
2166
2167 #[test]
2170 fn test_system_backward_compat() {
2171 let system = SystemBuilder::new().build().expect("empty system is valid");
2172 assert_eq!(system.n_buses(), 0);
2174 assert_eq!(system.n_hydros(), 0);
2175 assert_eq!(system.n_stages(), 0);
2177 assert!(system.stages().is_empty());
2178 assert!(system.initial_conditions().storage.is_empty());
2179 assert!(system.generic_constraints().is_empty());
2180 assert!(system.inflow_models().is_empty());
2181 assert!(system.load_models().is_empty());
2182 assert_eq!(system.penalties().n_stages(), 0);
2183 assert_eq!(system.bounds().n_stages(), 0);
2184 assert!(!system.resolved_generic_bounds().is_active(0, 0));
2186 assert!(
2187 system
2188 .resolved_generic_bounds()
2189 .bounds_for_stage(0, 0)
2190 .is_empty()
2191 );
2192 }
2193
2194 #[test]
2196 fn test_system_resolved_generic_bounds_accessor() {
2197 use crate::resolved::ResolvedGenericConstraintBounds;
2198 use std::collections::HashMap as StdHashMap;
2199
2200 let id_map: StdHashMap<i32, usize> = [(0, 0), (1, 1)].into_iter().collect();
2201 let rows = vec![(0i32, 0i32, None::<i32>, 100.0f64)];
2202 let table = ResolvedGenericConstraintBounds::new(&id_map, rows.into_iter());
2203
2204 let system = SystemBuilder::new()
2205 .resolved_generic_bounds(table)
2206 .build()
2207 .expect("valid system");
2208
2209 assert!(system.resolved_generic_bounds().is_active(0, 0));
2210 assert!(!system.resolved_generic_bounds().is_active(1, 0));
2211 let slice = system.resolved_generic_bounds().bounds_for_stage(0, 0);
2212 assert_eq!(slice.len(), 1);
2213 assert_eq!(slice[0], (None, 100.0));
2214 }
2215
2216 #[test]
2218 fn test_system_with_stages() {
2219 let s0 = make_stage(0);
2220 let s1 = make_stage(1);
2221
2222 let system = SystemBuilder::new()
2223 .stages(vec![s1.clone(), s0.clone()]) .build()
2225 .expect("valid system");
2226
2227 assert_eq!(system.n_stages(), 2);
2229 assert_eq!(system.stages()[0].id, 0);
2230 assert_eq!(system.stages()[1].id, 1);
2231
2232 let found = system.stage(0).expect("stage 0 must be found");
2234 assert_eq!(found.id, s0.id);
2235
2236 let found1 = system.stage(1).expect("stage 1 must be found");
2237 assert_eq!(found1.id, s1.id);
2238
2239 assert!(system.stage(99).is_none());
2241 }
2242
2243 #[test]
2245 fn test_system_stage_lookup_by_id() {
2246 let stages: Vec<Stage> = [0i32, 1, 2].iter().map(|&id| make_stage(id)).collect();
2247
2248 let system = SystemBuilder::new()
2249 .stages(stages)
2250 .build()
2251 .expect("valid system");
2252
2253 assert_eq!(system.stage(1).map(|s| s.id), Some(1));
2254 assert!(system.stage(99).is_none());
2255 }
2256
2257 #[test]
2259 fn test_system_with_initial_conditions() {
2260 let ic = InitialConditions {
2261 storage: vec![crate::HydroStorage {
2262 hydro_id: EntityId(0),
2263 value_hm3: 15_000.0,
2264 }],
2265 filling_storage: vec![],
2266 past_inflows: vec![],
2267 recent_observations: vec![],
2268 };
2269
2270 let system = SystemBuilder::new()
2271 .initial_conditions(ic)
2272 .build()
2273 .expect("valid system");
2274
2275 assert_eq!(system.initial_conditions().storage.len(), 1);
2276 assert_eq!(system.initial_conditions().storage[0].hydro_id, EntityId(0));
2277 assert!((system.initial_conditions().storage[0].value_hm3 - 15_000.0).abs() < f64::EPSILON);
2278 }
2279
2280 #[cfg(feature = "serde")]
2283 #[test]
2284 fn test_system_serde_roundtrip_with_stages() {
2285 use crate::temporal::PolicyGraphType;
2286
2287 let stages = vec![make_stage(0), make_stage(1)];
2288 let policy_graph = PolicyGraph {
2289 graph_type: PolicyGraphType::FiniteHorizon,
2290 annual_discount_rate: 0.0,
2291 transitions: vec![],
2292 season_map: None,
2293 };
2294
2295 let system = SystemBuilder::new()
2296 .stages(stages)
2297 .policy_graph(policy_graph)
2298 .build()
2299 .expect("valid system");
2300
2301 let json = serde_json::to_string(&system).unwrap();
2302 let mut deserialized: System = serde_json::from_str(&json).unwrap();
2303
2304 deserialized.rebuild_indices();
2306
2307 assert_eq!(system.n_stages(), deserialized.n_stages());
2309 assert_eq!(system.stages()[0].id, deserialized.stages()[0].id);
2310 assert_eq!(system.stages()[1].id, deserialized.stages()[1].id);
2311
2312 assert_eq!(deserialized.stage(0).map(|s| s.id), Some(0));
2314 assert_eq!(deserialized.stage(1).map(|s| s.id), Some(1));
2315 assert!(deserialized.stage(99).is_none());
2316
2317 assert_eq!(
2319 deserialized.policy_graph().graph_type,
2320 system.policy_graph().graph_type
2321 );
2322 }
2323
2324 #[test]
2329 fn test_system_inflow_history_defaults_empty() {
2330 let system = SystemBuilder::new().build().expect("valid system");
2331 assert!(
2332 system.inflow_history().is_empty(),
2333 "inflow_history must default to empty"
2334 );
2335 }
2336
2337 #[test]
2340 fn test_system_inflow_history_stores_rows() {
2341 use crate::scenario::InflowHistoryRow;
2342 use chrono::NaiveDate;
2343
2344 let row1 = InflowHistoryRow {
2345 hydro_id: EntityId(1),
2346 date: NaiveDate::from_ymd_opt(2000, 1, 1).expect("valid date"),
2347 value_m3s: 500.0,
2348 };
2349 let row2 = InflowHistoryRow {
2350 hydro_id: EntityId(1),
2351 date: NaiveDate::from_ymd_opt(2000, 2, 1).expect("valid date"),
2352 value_m3s: 420.0,
2353 };
2354
2355 let system = SystemBuilder::new()
2356 .inflow_history(vec![row1.clone(), row2.clone()])
2357 .build()
2358 .expect("valid system");
2359
2360 assert_eq!(system.inflow_history().len(), 2);
2361 assert_eq!(system.inflow_history()[0], row1);
2362 assert_eq!(system.inflow_history()[1], row2);
2363 }
2364
2365 #[test]
2368 fn test_system_external_scenarios_defaults_empty() {
2369 let system = SystemBuilder::new().build().expect("valid system");
2370 assert!(
2371 system.external_scenarios().is_empty(),
2372 "external_scenarios must default to empty"
2373 );
2374 }
2375
2376 #[test]
2379 fn test_system_external_scenarios_stores_rows() {
2380 use crate::scenario::ExternalScenarioRow;
2381
2382 let row = ExternalScenarioRow {
2383 stage_id: 0,
2384 scenario_id: 2,
2385 hydro_id: EntityId(5),
2386 value_m3s: 320.5,
2387 };
2388
2389 let system = SystemBuilder::new()
2390 .external_scenarios(vec![row.clone()])
2391 .build()
2392 .expect("valid system");
2393
2394 assert_eq!(system.external_scenarios().len(), 1);
2395 assert_eq!(system.external_scenarios()[0], row);
2396 }
2397}