1use std::collections::{HashMap, HashSet};
12
13use crate::{
14 Bus, CascadeTopology, CorrelationModel, EnergyContract, EntityId, GenericConstraint, Hydro,
15 InflowModel, InitialConditions, Line, LoadModel, NcsModel, NetworkTopology,
16 NonControllableSource, PolicyGraph, PumpingStation, ResolvedBounds, ResolvedExchangeFactors,
17 ResolvedGenericConstraintBounds, ResolvedLoadFactors, ResolvedNcsBounds, ResolvedNcsFactors,
18 ResolvedPenalties, ScenarioSource, Stage, Thermal, ValidationError,
19};
20
21#[derive(Debug, PartialEq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub struct System {
53 buses: Vec<Bus>,
55 lines: Vec<Line>,
56 hydros: Vec<Hydro>,
57 thermals: Vec<Thermal>,
58 pumping_stations: Vec<PumpingStation>,
59 contracts: Vec<EnergyContract>,
60 non_controllable_sources: Vec<NonControllableSource>,
61
62 #[cfg_attr(feature = "serde", serde(skip))]
66 bus_index: HashMap<EntityId, usize>,
67 #[cfg_attr(feature = "serde", serde(skip))]
68 line_index: HashMap<EntityId, usize>,
69 #[cfg_attr(feature = "serde", serde(skip))]
70 hydro_index: HashMap<EntityId, usize>,
71 #[cfg_attr(feature = "serde", serde(skip))]
72 thermal_index: HashMap<EntityId, usize>,
73 #[cfg_attr(feature = "serde", serde(skip))]
74 pumping_station_index: HashMap<EntityId, usize>,
75 #[cfg_attr(feature = "serde", serde(skip))]
76 contract_index: HashMap<EntityId, usize>,
77 #[cfg_attr(feature = "serde", serde(skip))]
78 non_controllable_source_index: HashMap<EntityId, usize>,
79
80 cascade: CascadeTopology,
83 network: NetworkTopology,
85
86 stages: Vec<Stage>,
89 policy_graph: PolicyGraph,
91
92 #[cfg_attr(feature = "serde", serde(skip))]
96 stage_index: HashMap<i32, usize>,
97
98 penalties: ResolvedPenalties,
101 bounds: ResolvedBounds,
103 resolved_generic_bounds: ResolvedGenericConstraintBounds,
105 resolved_load_factors: ResolvedLoadFactors,
107 resolved_exchange_factors: ResolvedExchangeFactors,
109 resolved_ncs_bounds: ResolvedNcsBounds,
111 resolved_ncs_factors: ResolvedNcsFactors,
113
114 inflow_models: Vec<InflowModel>,
117 load_models: Vec<LoadModel>,
119 ncs_models: Vec<NcsModel>,
121 correlation: CorrelationModel,
123
124 initial_conditions: InitialConditions,
127 generic_constraints: Vec<GenericConstraint>,
129 scenario_source: ScenarioSource,
131}
132
133const _: () = {
135 const fn assert_send_sync<T: Send + Sync>() {}
136 const fn check() {
137 assert_send_sync::<System>();
138 }
139 let _ = check;
140};
141
142impl System {
143 #[must_use]
145 pub fn buses(&self) -> &[Bus] {
146 &self.buses
147 }
148
149 #[must_use]
151 pub fn lines(&self) -> &[Line] {
152 &self.lines
153 }
154
155 #[must_use]
157 pub fn hydros(&self) -> &[Hydro] {
158 &self.hydros
159 }
160
161 #[must_use]
163 pub fn thermals(&self) -> &[Thermal] {
164 &self.thermals
165 }
166
167 #[must_use]
169 pub fn pumping_stations(&self) -> &[PumpingStation] {
170 &self.pumping_stations
171 }
172
173 #[must_use]
175 pub fn contracts(&self) -> &[EnergyContract] {
176 &self.contracts
177 }
178
179 #[must_use]
181 pub fn non_controllable_sources(&self) -> &[NonControllableSource] {
182 &self.non_controllable_sources
183 }
184
185 #[must_use]
187 pub fn n_buses(&self) -> usize {
188 self.buses.len()
189 }
190
191 #[must_use]
193 pub fn n_lines(&self) -> usize {
194 self.lines.len()
195 }
196
197 #[must_use]
199 pub fn n_hydros(&self) -> usize {
200 self.hydros.len()
201 }
202
203 #[must_use]
205 pub fn n_thermals(&self) -> usize {
206 self.thermals.len()
207 }
208
209 #[must_use]
211 pub fn n_pumping_stations(&self) -> usize {
212 self.pumping_stations.len()
213 }
214
215 #[must_use]
217 pub fn n_contracts(&self) -> usize {
218 self.contracts.len()
219 }
220
221 #[must_use]
223 pub fn n_non_controllable_sources(&self) -> usize {
224 self.non_controllable_sources.len()
225 }
226
227 #[must_use]
229 pub fn bus(&self, id: EntityId) -> Option<&Bus> {
230 self.bus_index.get(&id).map(|&i| &self.buses[i])
231 }
232
233 #[must_use]
235 pub fn line(&self, id: EntityId) -> Option<&Line> {
236 self.line_index.get(&id).map(|&i| &self.lines[i])
237 }
238
239 #[must_use]
241 pub fn hydro(&self, id: EntityId) -> Option<&Hydro> {
242 self.hydro_index.get(&id).map(|&i| &self.hydros[i])
243 }
244
245 #[must_use]
247 pub fn thermal(&self, id: EntityId) -> Option<&Thermal> {
248 self.thermal_index.get(&id).map(|&i| &self.thermals[i])
249 }
250
251 #[must_use]
253 pub fn pumping_station(&self, id: EntityId) -> Option<&PumpingStation> {
254 self.pumping_station_index
255 .get(&id)
256 .map(|&i| &self.pumping_stations[i])
257 }
258
259 #[must_use]
261 pub fn contract(&self, id: EntityId) -> Option<&EnergyContract> {
262 self.contract_index.get(&id).map(|&i| &self.contracts[i])
263 }
264
265 #[must_use]
267 pub fn non_controllable_source(&self, id: EntityId) -> Option<&NonControllableSource> {
268 self.non_controllable_source_index
269 .get(&id)
270 .map(|&i| &self.non_controllable_sources[i])
271 }
272
273 #[must_use]
275 pub fn cascade(&self) -> &CascadeTopology {
276 &self.cascade
277 }
278
279 #[must_use]
281 pub fn network(&self) -> &NetworkTopology {
282 &self.network
283 }
284
285 #[must_use]
287 pub fn stages(&self) -> &[Stage] {
288 &self.stages
289 }
290
291 #[must_use]
293 pub fn n_stages(&self) -> usize {
294 self.stages.len()
295 }
296
297 #[must_use]
302 pub fn stage(&self, id: i32) -> Option<&Stage> {
303 self.stage_index.get(&id).map(|&i| &self.stages[i])
304 }
305
306 #[must_use]
308 pub fn policy_graph(&self) -> &PolicyGraph {
309 &self.policy_graph
310 }
311
312 #[must_use]
314 pub fn penalties(&self) -> &ResolvedPenalties {
315 &self.penalties
316 }
317
318 #[must_use]
320 pub fn bounds(&self) -> &ResolvedBounds {
321 &self.bounds
322 }
323
324 #[must_use]
326 pub fn resolved_generic_bounds(&self) -> &ResolvedGenericConstraintBounds {
327 &self.resolved_generic_bounds
328 }
329
330 #[must_use]
332 pub fn resolved_load_factors(&self) -> &ResolvedLoadFactors {
333 &self.resolved_load_factors
334 }
335
336 #[must_use]
338 pub fn resolved_exchange_factors(&self) -> &ResolvedExchangeFactors {
339 &self.resolved_exchange_factors
340 }
341
342 #[must_use]
344 pub fn resolved_ncs_bounds(&self) -> &ResolvedNcsBounds {
345 &self.resolved_ncs_bounds
346 }
347
348 #[must_use]
350 pub fn resolved_ncs_factors(&self) -> &ResolvedNcsFactors {
351 &self.resolved_ncs_factors
352 }
353
354 #[must_use]
356 pub fn inflow_models(&self) -> &[InflowModel] {
357 &self.inflow_models
358 }
359
360 #[must_use]
362 pub fn load_models(&self) -> &[LoadModel] {
363 &self.load_models
364 }
365
366 #[must_use]
368 pub fn ncs_models(&self) -> &[NcsModel] {
369 &self.ncs_models
370 }
371
372 #[must_use]
374 pub fn correlation(&self) -> &CorrelationModel {
375 &self.correlation
376 }
377
378 #[must_use]
380 pub fn initial_conditions(&self) -> &InitialConditions {
381 &self.initial_conditions
382 }
383
384 #[must_use]
386 pub fn generic_constraints(&self) -> &[GenericConstraint] {
387 &self.generic_constraints
388 }
389
390 #[must_use]
392 pub fn scenario_source(&self) -> &ScenarioSource {
393 &self.scenario_source
394 }
395
396 #[must_use]
423 pub fn with_scenario_models(
424 mut self,
425 inflow_models: Vec<InflowModel>,
426 correlation: CorrelationModel,
427 ) -> Self {
428 self.inflow_models = inflow_models;
429 self.correlation = correlation;
430 self
431 }
432
433 pub fn rebuild_indices(&mut self) {
466 self.bus_index = build_index(&self.buses);
467 self.line_index = build_index(&self.lines);
468 self.hydro_index = build_index(&self.hydros);
469 self.thermal_index = build_index(&self.thermals);
470 self.pumping_station_index = build_index(&self.pumping_stations);
471 self.contract_index = build_index(&self.contracts);
472 self.non_controllable_source_index = build_index(&self.non_controllable_sources);
473 self.stage_index = build_stage_index(&self.stages);
474 }
475}
476
477pub struct SystemBuilder {
501 buses: Vec<Bus>,
502 lines: Vec<Line>,
503 hydros: Vec<Hydro>,
504 thermals: Vec<Thermal>,
505 pumping_stations: Vec<PumpingStation>,
506 contracts: Vec<EnergyContract>,
507 non_controllable_sources: Vec<NonControllableSource>,
508 stages: Vec<Stage>,
510 policy_graph: PolicyGraph,
511 penalties: ResolvedPenalties,
512 bounds: ResolvedBounds,
513 resolved_generic_bounds: ResolvedGenericConstraintBounds,
514 resolved_load_factors: ResolvedLoadFactors,
515 resolved_exchange_factors: ResolvedExchangeFactors,
516 resolved_ncs_bounds: ResolvedNcsBounds,
517 resolved_ncs_factors: ResolvedNcsFactors,
518 inflow_models: Vec<InflowModel>,
519 load_models: Vec<LoadModel>,
520 ncs_models: Vec<NcsModel>,
521 correlation: CorrelationModel,
522 initial_conditions: InitialConditions,
523 generic_constraints: Vec<GenericConstraint>,
524 scenario_source: ScenarioSource,
525}
526
527impl Default for SystemBuilder {
528 fn default() -> Self {
529 Self::new()
530 }
531}
532
533impl SystemBuilder {
534 #[must_use]
539 pub fn new() -> Self {
540 Self {
541 buses: Vec::new(),
542 lines: Vec::new(),
543 hydros: Vec::new(),
544 thermals: Vec::new(),
545 pumping_stations: Vec::new(),
546 contracts: Vec::new(),
547 non_controllable_sources: Vec::new(),
548 stages: Vec::new(),
549 policy_graph: PolicyGraph::default(),
550 penalties: ResolvedPenalties::empty(),
551 bounds: ResolvedBounds::empty(),
552 resolved_generic_bounds: ResolvedGenericConstraintBounds::empty(),
553 resolved_load_factors: ResolvedLoadFactors::empty(),
554 resolved_exchange_factors: ResolvedExchangeFactors::empty(),
555 resolved_ncs_bounds: ResolvedNcsBounds::empty(),
556 resolved_ncs_factors: ResolvedNcsFactors::empty(),
557 inflow_models: Vec::new(),
558 load_models: Vec::new(),
559 ncs_models: Vec::new(),
560 correlation: CorrelationModel::default(),
561 initial_conditions: InitialConditions::default(),
562 generic_constraints: Vec::new(),
563 scenario_source: ScenarioSource::default(),
564 }
565 }
566
567 #[must_use]
569 pub fn buses(mut self, buses: Vec<Bus>) -> Self {
570 self.buses = buses;
571 self
572 }
573
574 #[must_use]
576 pub fn lines(mut self, lines: Vec<Line>) -> Self {
577 self.lines = lines;
578 self
579 }
580
581 #[must_use]
583 pub fn hydros(mut self, hydros: Vec<Hydro>) -> Self {
584 self.hydros = hydros;
585 self
586 }
587
588 #[must_use]
590 pub fn thermals(mut self, thermals: Vec<Thermal>) -> Self {
591 self.thermals = thermals;
592 self
593 }
594
595 #[must_use]
597 pub fn pumping_stations(mut self, stations: Vec<PumpingStation>) -> Self {
598 self.pumping_stations = stations;
599 self
600 }
601
602 #[must_use]
604 pub fn contracts(mut self, contracts: Vec<EnergyContract>) -> Self {
605 self.contracts = contracts;
606 self
607 }
608
609 #[must_use]
611 pub fn non_controllable_sources(mut self, sources: Vec<NonControllableSource>) -> Self {
612 self.non_controllable_sources = sources;
613 self
614 }
615
616 #[must_use]
620 pub fn stages(mut self, stages: Vec<Stage>) -> Self {
621 self.stages = stages;
622 self
623 }
624
625 #[must_use]
627 pub fn policy_graph(mut self, policy_graph: PolicyGraph) -> Self {
628 self.policy_graph = policy_graph;
629 self
630 }
631
632 #[must_use]
636 pub fn penalties(mut self, penalties: ResolvedPenalties) -> Self {
637 self.penalties = penalties;
638 self
639 }
640
641 #[must_use]
645 pub fn bounds(mut self, bounds: ResolvedBounds) -> Self {
646 self.bounds = bounds;
647 self
648 }
649
650 #[must_use]
655 pub fn resolved_generic_bounds(
656 mut self,
657 resolved_generic_bounds: ResolvedGenericConstraintBounds,
658 ) -> Self {
659 self.resolved_generic_bounds = resolved_generic_bounds;
660 self
661 }
662
663 #[must_use]
667 pub fn resolved_load_factors(mut self, resolved_load_factors: ResolvedLoadFactors) -> Self {
668 self.resolved_load_factors = resolved_load_factors;
669 self
670 }
671
672 #[must_use]
676 pub fn resolved_exchange_factors(
677 mut self,
678 resolved_exchange_factors: ResolvedExchangeFactors,
679 ) -> Self {
680 self.resolved_exchange_factors = resolved_exchange_factors;
681 self
682 }
683
684 #[must_use]
688 pub fn resolved_ncs_bounds(mut self, resolved_ncs_bounds: ResolvedNcsBounds) -> Self {
689 self.resolved_ncs_bounds = resolved_ncs_bounds;
690 self
691 }
692
693 #[must_use]
697 pub fn resolved_ncs_factors(mut self, resolved_ncs_factors: ResolvedNcsFactors) -> Self {
698 self.resolved_ncs_factors = resolved_ncs_factors;
699 self
700 }
701
702 #[must_use]
704 pub fn inflow_models(mut self, inflow_models: Vec<InflowModel>) -> Self {
705 self.inflow_models = inflow_models;
706 self
707 }
708
709 #[must_use]
711 pub fn load_models(mut self, load_models: Vec<LoadModel>) -> Self {
712 self.load_models = load_models;
713 self
714 }
715
716 #[must_use]
718 pub fn ncs_models(mut self, ncs_models: Vec<NcsModel>) -> Self {
719 self.ncs_models = ncs_models;
720 self
721 }
722
723 #[must_use]
725 pub fn correlation(mut self, correlation: CorrelationModel) -> Self {
726 self.correlation = correlation;
727 self
728 }
729
730 #[must_use]
732 pub fn initial_conditions(mut self, initial_conditions: InitialConditions) -> Self {
733 self.initial_conditions = initial_conditions;
734 self
735 }
736
737 #[must_use]
741 pub fn generic_constraints(mut self, generic_constraints: Vec<GenericConstraint>) -> Self {
742 self.generic_constraints = generic_constraints;
743 self
744 }
745
746 #[must_use]
748 pub fn scenario_source(mut self, scenario_source: ScenarioSource) -> Self {
749 self.scenario_source = scenario_source;
750 self
751 }
752
753 #[allow(clippy::too_many_lines)]
778 pub fn build(mut self) -> Result<System, Vec<ValidationError>> {
779 self.buses.sort_by_key(|e| e.id.0);
780 self.lines.sort_by_key(|e| e.id.0);
781 self.hydros.sort_by_key(|e| e.id.0);
782 self.thermals.sort_by_key(|e| e.id.0);
783 self.pumping_stations.sort_by_key(|e| e.id.0);
784 self.contracts.sort_by_key(|e| e.id.0);
785 self.non_controllable_sources.sort_by_key(|e| e.id.0);
786 self.stages.sort_by_key(|s| s.id);
787 self.generic_constraints.sort_by_key(|c| c.id.0);
788
789 let mut errors: Vec<ValidationError> = Vec::new();
790 check_duplicates(&self.buses, "Bus", &mut errors);
791 check_duplicates(&self.lines, "Line", &mut errors);
792 check_duplicates(&self.hydros, "Hydro", &mut errors);
793 check_duplicates(&self.thermals, "Thermal", &mut errors);
794 check_duplicates(&self.pumping_stations, "PumpingStation", &mut errors);
795 check_duplicates(&self.contracts, "EnergyContract", &mut errors);
796 check_duplicates(
797 &self.non_controllable_sources,
798 "NonControllableSource",
799 &mut errors,
800 );
801
802 if !errors.is_empty() {
803 return Err(errors);
804 }
805
806 let bus_index = build_index(&self.buses);
807 let line_index = build_index(&self.lines);
808 let hydro_index = build_index(&self.hydros);
809 let thermal_index = build_index(&self.thermals);
810 let pumping_station_index = build_index(&self.pumping_stations);
811 let contract_index = build_index(&self.contracts);
812 let non_controllable_source_index = build_index(&self.non_controllable_sources);
813
814 validate_cross_references(
815 &CrossRefEntities {
816 lines: &self.lines,
817 hydros: &self.hydros,
818 thermals: &self.thermals,
819 pumping_stations: &self.pumping_stations,
820 contracts: &self.contracts,
821 non_controllable_sources: &self.non_controllable_sources,
822 },
823 &bus_index,
824 &hydro_index,
825 &mut errors,
826 );
827
828 if !errors.is_empty() {
829 return Err(errors);
830 }
831
832 let cascade = CascadeTopology::build(&self.hydros);
833
834 if cascade.topological_order().len() < self.hydros.len() {
835 let in_topo: HashSet<EntityId> = cascade.topological_order().iter().copied().collect();
836 let mut cycle_ids: Vec<EntityId> = self
837 .hydros
838 .iter()
839 .map(|h| h.id)
840 .filter(|id| !in_topo.contains(id))
841 .collect();
842 cycle_ids.sort_by_key(|id| id.0);
843 errors.push(ValidationError::CascadeCycle { cycle_ids });
844 }
845
846 validate_filling_configs(&self.hydros, &mut errors);
847
848 if !errors.is_empty() {
849 return Err(errors);
850 }
851
852 let network = NetworkTopology::build(
853 &self.buses,
854 &self.lines,
855 &self.hydros,
856 &self.thermals,
857 &self.non_controllable_sources,
858 &self.contracts,
859 &self.pumping_stations,
860 );
861
862 let stage_index = build_stage_index(&self.stages);
863
864 Ok(System {
865 buses: self.buses,
866 lines: self.lines,
867 hydros: self.hydros,
868 thermals: self.thermals,
869 pumping_stations: self.pumping_stations,
870 contracts: self.contracts,
871 non_controllable_sources: self.non_controllable_sources,
872 bus_index,
873 line_index,
874 hydro_index,
875 thermal_index,
876 pumping_station_index,
877 contract_index,
878 non_controllable_source_index,
879 cascade,
880 network,
881 stages: self.stages,
882 policy_graph: self.policy_graph,
883 stage_index,
884 penalties: self.penalties,
885 bounds: self.bounds,
886 resolved_generic_bounds: self.resolved_generic_bounds,
887 resolved_load_factors: self.resolved_load_factors,
888 resolved_exchange_factors: self.resolved_exchange_factors,
889 resolved_ncs_bounds: self.resolved_ncs_bounds,
890 resolved_ncs_factors: self.resolved_ncs_factors,
891 inflow_models: self.inflow_models,
892 load_models: self.load_models,
893 ncs_models: self.ncs_models,
894 correlation: self.correlation,
895 initial_conditions: self.initial_conditions,
896 generic_constraints: self.generic_constraints,
897 scenario_source: self.scenario_source,
898 })
899 }
900}
901
902trait HasId {
903 fn entity_id(&self) -> EntityId;
904}
905
906impl HasId for Bus {
907 fn entity_id(&self) -> EntityId {
908 self.id
909 }
910}
911impl HasId for Line {
912 fn entity_id(&self) -> EntityId {
913 self.id
914 }
915}
916impl HasId for Hydro {
917 fn entity_id(&self) -> EntityId {
918 self.id
919 }
920}
921impl HasId for Thermal {
922 fn entity_id(&self) -> EntityId {
923 self.id
924 }
925}
926impl HasId for PumpingStation {
927 fn entity_id(&self) -> EntityId {
928 self.id
929 }
930}
931impl HasId for EnergyContract {
932 fn entity_id(&self) -> EntityId {
933 self.id
934 }
935}
936impl HasId for NonControllableSource {
937 fn entity_id(&self) -> EntityId {
938 self.id
939 }
940}
941
942fn build_index<T: HasId>(entities: &[T]) -> HashMap<EntityId, usize> {
943 let mut index = HashMap::with_capacity(entities.len());
944 for (i, entity) in entities.iter().enumerate() {
945 index.insert(entity.entity_id(), i);
946 }
947 index
948}
949
950fn build_stage_index(stages: &[Stage]) -> HashMap<i32, usize> {
954 let mut index = HashMap::with_capacity(stages.len());
955 for (i, stage) in stages.iter().enumerate() {
956 index.insert(stage.id, i);
957 }
958 index
959}
960
961fn check_duplicates<T: HasId>(
962 entities: &[T],
963 entity_type: &'static str,
964 errors: &mut Vec<ValidationError>,
965) {
966 for window in entities.windows(2) {
967 if window[0].entity_id() == window[1].entity_id() {
968 errors.push(ValidationError::DuplicateId {
969 entity_type,
970 id: window[0].entity_id(),
971 });
972 }
973 }
974}
975
976struct CrossRefEntities<'a> {
986 lines: &'a [Line],
987 hydros: &'a [Hydro],
988 thermals: &'a [Thermal],
989 pumping_stations: &'a [PumpingStation],
990 contracts: &'a [EnergyContract],
991 non_controllable_sources: &'a [NonControllableSource],
992}
993
994fn validate_cross_references(
995 entities: &CrossRefEntities<'_>,
996 bus_index: &HashMap<EntityId, usize>,
997 hydro_index: &HashMap<EntityId, usize>,
998 errors: &mut Vec<ValidationError>,
999) {
1000 validate_line_refs(entities.lines, bus_index, errors);
1001 validate_hydro_refs(entities.hydros, bus_index, hydro_index, errors);
1002 validate_thermal_refs(entities.thermals, bus_index, errors);
1003 validate_pumping_station_refs(entities.pumping_stations, bus_index, hydro_index, errors);
1004 validate_contract_refs(entities.contracts, bus_index, errors);
1005 validate_ncs_refs(entities.non_controllable_sources, bus_index, errors);
1006}
1007
1008fn validate_line_refs(
1009 lines: &[Line],
1010 bus_index: &HashMap<EntityId, usize>,
1011 errors: &mut Vec<ValidationError>,
1012) {
1013 for line in lines {
1014 if !bus_index.contains_key(&line.source_bus_id) {
1015 errors.push(ValidationError::InvalidReference {
1016 source_entity_type: "Line",
1017 source_id: line.id,
1018 field_name: "source_bus_id",
1019 referenced_id: line.source_bus_id,
1020 expected_type: "Bus",
1021 });
1022 }
1023 if !bus_index.contains_key(&line.target_bus_id) {
1024 errors.push(ValidationError::InvalidReference {
1025 source_entity_type: "Line",
1026 source_id: line.id,
1027 field_name: "target_bus_id",
1028 referenced_id: line.target_bus_id,
1029 expected_type: "Bus",
1030 });
1031 }
1032 }
1033}
1034
1035fn validate_hydro_refs(
1036 hydros: &[Hydro],
1037 bus_index: &HashMap<EntityId, usize>,
1038 hydro_index: &HashMap<EntityId, usize>,
1039 errors: &mut Vec<ValidationError>,
1040) {
1041 for hydro in hydros {
1042 if !bus_index.contains_key(&hydro.bus_id) {
1043 errors.push(ValidationError::InvalidReference {
1044 source_entity_type: "Hydro",
1045 source_id: hydro.id,
1046 field_name: "bus_id",
1047 referenced_id: hydro.bus_id,
1048 expected_type: "Bus",
1049 });
1050 }
1051 if let Some(downstream_id) = hydro.downstream_id {
1052 if !hydro_index.contains_key(&downstream_id) {
1053 errors.push(ValidationError::InvalidReference {
1054 source_entity_type: "Hydro",
1055 source_id: hydro.id,
1056 field_name: "downstream_id",
1057 referenced_id: downstream_id,
1058 expected_type: "Hydro",
1059 });
1060 }
1061 }
1062 if let Some(ref diversion) = hydro.diversion {
1063 if !hydro_index.contains_key(&diversion.downstream_id) {
1064 errors.push(ValidationError::InvalidReference {
1065 source_entity_type: "Hydro",
1066 source_id: hydro.id,
1067 field_name: "diversion.downstream_id",
1068 referenced_id: diversion.downstream_id,
1069 expected_type: "Hydro",
1070 });
1071 }
1072 }
1073 }
1074}
1075
1076fn validate_thermal_refs(
1077 thermals: &[Thermal],
1078 bus_index: &HashMap<EntityId, usize>,
1079 errors: &mut Vec<ValidationError>,
1080) {
1081 for thermal in thermals {
1082 if !bus_index.contains_key(&thermal.bus_id) {
1083 errors.push(ValidationError::InvalidReference {
1084 source_entity_type: "Thermal",
1085 source_id: thermal.id,
1086 field_name: "bus_id",
1087 referenced_id: thermal.bus_id,
1088 expected_type: "Bus",
1089 });
1090 }
1091 }
1092}
1093
1094fn validate_pumping_station_refs(
1095 pumping_stations: &[PumpingStation],
1096 bus_index: &HashMap<EntityId, usize>,
1097 hydro_index: &HashMap<EntityId, usize>,
1098 errors: &mut Vec<ValidationError>,
1099) {
1100 for ps in pumping_stations {
1101 if !bus_index.contains_key(&ps.bus_id) {
1102 errors.push(ValidationError::InvalidReference {
1103 source_entity_type: "PumpingStation",
1104 source_id: ps.id,
1105 field_name: "bus_id",
1106 referenced_id: ps.bus_id,
1107 expected_type: "Bus",
1108 });
1109 }
1110 if !hydro_index.contains_key(&ps.source_hydro_id) {
1111 errors.push(ValidationError::InvalidReference {
1112 source_entity_type: "PumpingStation",
1113 source_id: ps.id,
1114 field_name: "source_hydro_id",
1115 referenced_id: ps.source_hydro_id,
1116 expected_type: "Hydro",
1117 });
1118 }
1119 if !hydro_index.contains_key(&ps.destination_hydro_id) {
1120 errors.push(ValidationError::InvalidReference {
1121 source_entity_type: "PumpingStation",
1122 source_id: ps.id,
1123 field_name: "destination_hydro_id",
1124 referenced_id: ps.destination_hydro_id,
1125 expected_type: "Hydro",
1126 });
1127 }
1128 }
1129}
1130
1131fn validate_contract_refs(
1132 contracts: &[EnergyContract],
1133 bus_index: &HashMap<EntityId, usize>,
1134 errors: &mut Vec<ValidationError>,
1135) {
1136 for contract in contracts {
1137 if !bus_index.contains_key(&contract.bus_id) {
1138 errors.push(ValidationError::InvalidReference {
1139 source_entity_type: "EnergyContract",
1140 source_id: contract.id,
1141 field_name: "bus_id",
1142 referenced_id: contract.bus_id,
1143 expected_type: "Bus",
1144 });
1145 }
1146 }
1147}
1148
1149fn validate_ncs_refs(
1150 non_controllable_sources: &[NonControllableSource],
1151 bus_index: &HashMap<EntityId, usize>,
1152 errors: &mut Vec<ValidationError>,
1153) {
1154 for ncs in non_controllable_sources {
1155 if !bus_index.contains_key(&ncs.bus_id) {
1156 errors.push(ValidationError::InvalidReference {
1157 source_entity_type: "NonControllableSource",
1158 source_id: ncs.id,
1159 field_name: "bus_id",
1160 referenced_id: ncs.bus_id,
1161 expected_type: "Bus",
1162 });
1163 }
1164 }
1165}
1166
1167fn validate_filling_configs(hydros: &[Hydro], errors: &mut Vec<ValidationError>) {
1175 for hydro in hydros {
1176 if let Some(filling) = &hydro.filling {
1177 if filling.filling_inflow_m3s.is_nan() || filling.filling_inflow_m3s <= 0.0 {
1178 errors.push(ValidationError::InvalidFillingConfig {
1179 hydro_id: hydro.id,
1180 reason: "filling_inflow_m3s must be positive".to_string(),
1181 });
1182 }
1183 if hydro.entry_stage_id.is_none() {
1184 errors.push(ValidationError::InvalidFillingConfig {
1185 hydro_id: hydro.id,
1186 reason: "filling requires entry_stage_id to be set".to_string(),
1187 });
1188 }
1189 }
1190 }
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195 use super::*;
1196 use crate::entities::{ContractType, HydroGenerationModel, HydroPenalties, ThermalCostSegment};
1197
1198 fn make_bus(id: i32) -> Bus {
1199 Bus {
1200 id: EntityId(id),
1201 name: format!("bus-{id}"),
1202 deficit_segments: vec![],
1203 excess_cost: 0.0,
1204 }
1205 }
1206
1207 fn make_line(id: i32, source_bus_id: i32, target_bus_id: i32) -> Line {
1208 crate::Line {
1209 id: EntityId(id),
1210 name: format!("line-{id}"),
1211 source_bus_id: EntityId(source_bus_id),
1212 target_bus_id: EntityId(target_bus_id),
1213 entry_stage_id: None,
1214 exit_stage_id: None,
1215 direct_capacity_mw: 100.0,
1216 reverse_capacity_mw: 100.0,
1217 losses_percent: 0.0,
1218 exchange_cost: 0.0,
1219 }
1220 }
1221
1222 fn make_hydro_on_bus(id: i32, bus_id: i32) -> Hydro {
1223 let zero_penalties = HydroPenalties {
1224 spillage_cost: 0.0,
1225 diversion_cost: 0.0,
1226 fpha_turbined_cost: 0.0,
1227 storage_violation_below_cost: 0.0,
1228 filling_target_violation_cost: 0.0,
1229 turbined_violation_below_cost: 0.0,
1230 outflow_violation_below_cost: 0.0,
1231 outflow_violation_above_cost: 0.0,
1232 generation_violation_below_cost: 0.0,
1233 evaporation_violation_cost: 0.0,
1234 water_withdrawal_violation_cost: 0.0,
1235 water_withdrawal_violation_pos_cost: 0.0,
1236 water_withdrawal_violation_neg_cost: 0.0,
1237 evaporation_violation_pos_cost: 0.0,
1238 evaporation_violation_neg_cost: 0.0,
1239 inflow_nonnegativity_cost: 1000.0,
1240 };
1241 Hydro {
1242 id: EntityId(id),
1243 name: format!("hydro-{id}"),
1244 bus_id: EntityId(bus_id),
1245 downstream_id: None,
1246 entry_stage_id: None,
1247 exit_stage_id: None,
1248 min_storage_hm3: 0.0,
1249 max_storage_hm3: 1.0,
1250 min_outflow_m3s: 0.0,
1251 max_outflow_m3s: None,
1252 generation_model: HydroGenerationModel::ConstantProductivity {
1253 productivity_mw_per_m3s: 1.0,
1254 },
1255 min_turbined_m3s: 0.0,
1256 max_turbined_m3s: 1.0,
1257 min_generation_mw: 0.0,
1258 max_generation_mw: 1.0,
1259 tailrace: None,
1260 hydraulic_losses: None,
1261 efficiency: None,
1262 evaporation_coefficients_mm: None,
1263 evaporation_reference_volumes_hm3: None,
1264 diversion: None,
1265 filling: None,
1266 penalties: zero_penalties,
1267 }
1268 }
1269
1270 fn make_hydro(id: i32) -> Hydro {
1272 make_hydro_on_bus(id, 0)
1273 }
1274
1275 fn make_thermal_on_bus(id: i32, bus_id: i32) -> Thermal {
1276 Thermal {
1277 id: EntityId(id),
1278 name: format!("thermal-{id}"),
1279 bus_id: EntityId(bus_id),
1280 entry_stage_id: None,
1281 exit_stage_id: None,
1282 cost_segments: vec![ThermalCostSegment {
1283 capacity_mw: 100.0,
1284 cost_per_mwh: 50.0,
1285 }],
1286 min_generation_mw: 0.0,
1287 max_generation_mw: 100.0,
1288 gnl_config: None,
1289 }
1290 }
1291
1292 fn make_thermal(id: i32) -> Thermal {
1294 make_thermal_on_bus(id, 0)
1295 }
1296
1297 fn make_pumping_station_full(
1298 id: i32,
1299 bus_id: i32,
1300 source_hydro_id: i32,
1301 destination_hydro_id: i32,
1302 ) -> PumpingStation {
1303 PumpingStation {
1304 id: EntityId(id),
1305 name: format!("ps-{id}"),
1306 bus_id: EntityId(bus_id),
1307 source_hydro_id: EntityId(source_hydro_id),
1308 destination_hydro_id: EntityId(destination_hydro_id),
1309 entry_stage_id: None,
1310 exit_stage_id: None,
1311 consumption_mw_per_m3s: 0.5,
1312 min_flow_m3s: 0.0,
1313 max_flow_m3s: 10.0,
1314 }
1315 }
1316
1317 fn make_pumping_station(id: i32) -> PumpingStation {
1318 make_pumping_station_full(id, 0, 0, 1)
1319 }
1320
1321 fn make_contract_on_bus(id: i32, bus_id: i32) -> EnergyContract {
1322 EnergyContract {
1323 id: EntityId(id),
1324 name: format!("contract-{id}"),
1325 bus_id: EntityId(bus_id),
1326 contract_type: ContractType::Import,
1327 entry_stage_id: None,
1328 exit_stage_id: None,
1329 price_per_mwh: 0.0,
1330 min_mw: 0.0,
1331 max_mw: 100.0,
1332 }
1333 }
1334
1335 fn make_contract(id: i32) -> EnergyContract {
1336 make_contract_on_bus(id, 0)
1337 }
1338
1339 fn make_ncs_on_bus(id: i32, bus_id: i32) -> NonControllableSource {
1340 NonControllableSource {
1341 id: EntityId(id),
1342 name: format!("ncs-{id}"),
1343 bus_id: EntityId(bus_id),
1344 entry_stage_id: None,
1345 exit_stage_id: None,
1346 max_generation_mw: 50.0,
1347 curtailment_cost: 0.0,
1348 }
1349 }
1350
1351 fn make_ncs(id: i32) -> NonControllableSource {
1352 make_ncs_on_bus(id, 0)
1353 }
1354
1355 #[test]
1356 fn test_empty_system() {
1357 let system = SystemBuilder::new().build().expect("empty system is valid");
1358 assert_eq!(system.n_buses(), 0);
1359 assert_eq!(system.n_lines(), 0);
1360 assert_eq!(system.n_hydros(), 0);
1361 assert_eq!(system.n_thermals(), 0);
1362 assert_eq!(system.n_pumping_stations(), 0);
1363 assert_eq!(system.n_contracts(), 0);
1364 assert_eq!(system.n_non_controllable_sources(), 0);
1365 assert!(system.buses().is_empty());
1366 assert!(system.cascade().is_empty());
1367 }
1368
1369 #[test]
1370 fn test_canonical_ordering() {
1371 let system = SystemBuilder::new()
1373 .buses(vec![make_bus(2), make_bus(1), make_bus(0)])
1374 .build()
1375 .expect("valid system");
1376
1377 assert_eq!(system.buses()[0].id, EntityId(0));
1378 assert_eq!(system.buses()[1].id, EntityId(1));
1379 assert_eq!(system.buses()[2].id, EntityId(2));
1380 }
1381
1382 #[test]
1383 fn test_lookup_by_id() {
1384 let system = SystemBuilder::new()
1386 .buses(vec![make_bus(0)])
1387 .hydros(vec![make_hydro(10), make_hydro(5), make_hydro(20)])
1388 .build()
1389 .expect("valid system");
1390
1391 assert_eq!(system.hydro(EntityId(5)).map(|h| h.id), Some(EntityId(5)));
1392 assert_eq!(system.hydro(EntityId(10)).map(|h| h.id), Some(EntityId(10)));
1393 assert_eq!(system.hydro(EntityId(20)).map(|h| h.id), Some(EntityId(20)));
1394 }
1395
1396 #[test]
1397 fn test_lookup_missing_id() {
1398 let system = SystemBuilder::new()
1400 .buses(vec![make_bus(0)])
1401 .hydros(vec![make_hydro(1), make_hydro(2)])
1402 .build()
1403 .expect("valid system");
1404
1405 assert!(system.hydro(EntityId(999)).is_none());
1406 }
1407
1408 #[test]
1409 fn test_count_queries() {
1410 let system = SystemBuilder::new()
1411 .buses(vec![make_bus(0), make_bus(1)])
1412 .lines(vec![make_line(0, 0, 1)])
1413 .hydros(vec![make_hydro(0), make_hydro(1), make_hydro(2)])
1414 .thermals(vec![make_thermal(0)])
1415 .pumping_stations(vec![make_pumping_station(0)])
1416 .contracts(vec![make_contract(0), make_contract(1)])
1417 .non_controllable_sources(vec![make_ncs(0)])
1418 .build()
1419 .expect("valid system");
1420
1421 assert_eq!(system.n_buses(), 2);
1422 assert_eq!(system.n_lines(), 1);
1423 assert_eq!(system.n_hydros(), 3);
1424 assert_eq!(system.n_thermals(), 1);
1425 assert_eq!(system.n_pumping_stations(), 1);
1426 assert_eq!(system.n_contracts(), 2);
1427 assert_eq!(system.n_non_controllable_sources(), 1);
1428 }
1429
1430 #[test]
1431 fn test_slice_accessors() {
1432 let system = SystemBuilder::new()
1433 .buses(vec![make_bus(0), make_bus(1), make_bus(2)])
1434 .build()
1435 .expect("valid system");
1436
1437 let buses = system.buses();
1438 assert_eq!(buses.len(), 3);
1439 assert_eq!(buses[0].id, EntityId(0));
1440 assert_eq!(buses[1].id, EntityId(1));
1441 assert_eq!(buses[2].id, EntityId(2));
1442 }
1443
1444 #[test]
1445 fn test_duplicate_id_error() {
1446 let result = SystemBuilder::new()
1448 .buses(vec![make_bus(0), make_bus(0)])
1449 .build();
1450
1451 assert!(result.is_err());
1452 let errors = result.unwrap_err();
1453 assert!(!errors.is_empty());
1454 assert!(errors.iter().any(|e| matches!(
1455 e,
1456 ValidationError::DuplicateId {
1457 entity_type: "Bus",
1458 id: EntityId(0),
1459 }
1460 )));
1461 }
1462
1463 #[test]
1464 fn test_multiple_duplicate_errors() {
1465 let result = SystemBuilder::new()
1467 .buses(vec![make_bus(0), make_bus(0)])
1468 .thermals(vec![make_thermal(5), make_thermal(5)])
1469 .build();
1470
1471 assert!(result.is_err());
1472 let errors = result.unwrap_err();
1473
1474 let has_bus_dup = errors.iter().any(|e| {
1475 matches!(
1476 e,
1477 ValidationError::DuplicateId {
1478 entity_type: "Bus",
1479 ..
1480 }
1481 )
1482 });
1483 let has_thermal_dup = errors.iter().any(|e| {
1484 matches!(
1485 e,
1486 ValidationError::DuplicateId {
1487 entity_type: "Thermal",
1488 ..
1489 }
1490 )
1491 });
1492 assert!(has_bus_dup, "expected Bus duplicate error");
1493 assert!(has_thermal_dup, "expected Thermal duplicate error");
1494 }
1495
1496 #[test]
1497 fn test_send_sync() {
1498 fn require_send_sync<T: Send + Sync>(_: T) {}
1499 let system = SystemBuilder::new().build().expect("valid system");
1500 require_send_sync(system);
1501 }
1502
1503 #[test]
1504 fn test_cascade_accessible() {
1505 let mut h0 = make_hydro_on_bus(0, 0);
1507 h0.downstream_id = Some(EntityId(1));
1508 let mut h1 = make_hydro_on_bus(1, 0);
1509 h1.downstream_id = Some(EntityId(2));
1510 let h2 = make_hydro_on_bus(2, 0);
1511
1512 let system = SystemBuilder::new()
1513 .buses(vec![make_bus(0)])
1514 .hydros(vec![h0, h1, h2])
1515 .build()
1516 .expect("valid system");
1517
1518 let order = system.cascade().topological_order();
1519 assert!(!order.is_empty(), "topological order must be non-empty");
1520 let pos_0 = order
1521 .iter()
1522 .position(|&id| id == EntityId(0))
1523 .expect("EntityId(0) must be in topological order");
1524 let pos_2 = order
1525 .iter()
1526 .position(|&id| id == EntityId(2))
1527 .expect("EntityId(2) must be in topological order");
1528 assert!(pos_0 < pos_2, "EntityId(0) must precede EntityId(2)");
1529 }
1530
1531 #[test]
1532 fn test_network_accessible() {
1533 let system = SystemBuilder::new()
1534 .buses(vec![make_bus(0), make_bus(1)])
1535 .lines(vec![make_line(0, 0, 1)])
1536 .build()
1537 .expect("valid system");
1538
1539 let connections = system.network().bus_lines(EntityId(0));
1540 assert!(!connections.is_empty(), "bus 0 must have connections");
1541 assert_eq!(connections[0].line_id, EntityId(0));
1542 }
1543
1544 #[test]
1545 fn test_all_entity_lookups() {
1546 let system = SystemBuilder::new()
1551 .buses(vec![make_bus(0), make_bus(1)])
1552 .lines(vec![make_line(2, 0, 1)])
1553 .hydros(vec![
1554 make_hydro_on_bus(0, 0),
1555 make_hydro_on_bus(1, 0),
1556 make_hydro_on_bus(3, 0),
1557 ])
1558 .thermals(vec![make_thermal(4)])
1559 .pumping_stations(vec![make_pumping_station(5)])
1560 .contracts(vec![make_contract(6)])
1561 .non_controllable_sources(vec![make_ncs(7)])
1562 .build()
1563 .expect("valid system");
1564
1565 assert!(system.bus(EntityId(1)).is_some());
1566 assert!(system.line(EntityId(2)).is_some());
1567 assert!(system.hydro(EntityId(3)).is_some());
1568 assert!(system.thermal(EntityId(4)).is_some());
1569 assert!(system.pumping_station(EntityId(5)).is_some());
1570 assert!(system.contract(EntityId(6)).is_some());
1571 assert!(system.non_controllable_source(EntityId(7)).is_some());
1572
1573 assert!(system.bus(EntityId(999)).is_none());
1574 assert!(system.line(EntityId(999)).is_none());
1575 assert!(system.hydro(EntityId(999)).is_none());
1576 assert!(system.thermal(EntityId(999)).is_none());
1577 assert!(system.pumping_station(EntityId(999)).is_none());
1578 assert!(system.contract(EntityId(999)).is_none());
1579 assert!(system.non_controllable_source(EntityId(999)).is_none());
1580 }
1581
1582 #[test]
1583 fn test_default_builder() {
1584 let system = SystemBuilder::default()
1585 .build()
1586 .expect("default builder produces valid empty system");
1587 assert_eq!(system.n_buses(), 0);
1588 }
1589
1590 #[test]
1593 fn test_invalid_bus_reference_hydro() {
1594 let hydro = make_hydro_on_bus(1, 99);
1596 let result = SystemBuilder::new().hydros(vec![hydro]).build();
1597
1598 assert!(result.is_err(), "expected Err for missing bus reference");
1599 let errors = result.unwrap_err();
1600 assert!(
1601 errors.iter().any(|e| matches!(
1602 e,
1603 ValidationError::InvalidReference {
1604 source_entity_type: "Hydro",
1605 source_id: EntityId(1),
1606 field_name: "bus_id",
1607 referenced_id: EntityId(99),
1608 expected_type: "Bus",
1609 }
1610 )),
1611 "expected InvalidReference for Hydro bus_id=99, got: {errors:?}"
1612 );
1613 }
1614
1615 #[test]
1616 fn test_invalid_downstream_reference() {
1617 let bus = make_bus(0);
1619 let mut hydro = make_hydro(1);
1620 hydro.downstream_id = Some(EntityId(50));
1621
1622 let result = SystemBuilder::new()
1623 .buses(vec![bus])
1624 .hydros(vec![hydro])
1625 .build();
1626
1627 assert!(
1628 result.is_err(),
1629 "expected Err for missing downstream reference"
1630 );
1631 let errors = result.unwrap_err();
1632 assert!(
1633 errors.iter().any(|e| matches!(
1634 e,
1635 ValidationError::InvalidReference {
1636 source_entity_type: "Hydro",
1637 source_id: EntityId(1),
1638 field_name: "downstream_id",
1639 referenced_id: EntityId(50),
1640 expected_type: "Hydro",
1641 }
1642 )),
1643 "expected InvalidReference for Hydro downstream_id=50, got: {errors:?}"
1644 );
1645 }
1646
1647 #[test]
1648 fn test_invalid_pumping_station_hydro_refs() {
1649 let bus = make_bus(0);
1651 let dest_hydro = make_hydro(1);
1652 let ps = make_pumping_station_full(10, 0, 77, 1);
1653
1654 let result = SystemBuilder::new()
1655 .buses(vec![bus])
1656 .hydros(vec![dest_hydro])
1657 .pumping_stations(vec![ps])
1658 .build();
1659
1660 assert!(
1661 result.is_err(),
1662 "expected Err for missing source_hydro_id reference"
1663 );
1664 let errors = result.unwrap_err();
1665 assert!(
1666 errors.iter().any(|e| matches!(
1667 e,
1668 ValidationError::InvalidReference {
1669 source_entity_type: "PumpingStation",
1670 source_id: EntityId(10),
1671 field_name: "source_hydro_id",
1672 referenced_id: EntityId(77),
1673 expected_type: "Hydro",
1674 }
1675 )),
1676 "expected InvalidReference for PumpingStation source_hydro_id=77, got: {errors:?}"
1677 );
1678 }
1679
1680 #[test]
1681 fn test_multiple_invalid_references_collected() {
1682 let line = make_line(1, 99, 0);
1685 let thermal = make_thermal_on_bus(2, 88);
1686
1687 let result = SystemBuilder::new()
1688 .buses(vec![make_bus(0)])
1689 .lines(vec![line])
1690 .thermals(vec![thermal])
1691 .build();
1692
1693 assert!(
1694 result.is_err(),
1695 "expected Err for multiple invalid references"
1696 );
1697 let errors = result.unwrap_err();
1698
1699 let has_line_error = errors.iter().any(|e| {
1700 matches!(
1701 e,
1702 ValidationError::InvalidReference {
1703 source_entity_type: "Line",
1704 field_name: "source_bus_id",
1705 referenced_id: EntityId(99),
1706 ..
1707 }
1708 )
1709 });
1710 let has_thermal_error = errors.iter().any(|e| {
1711 matches!(
1712 e,
1713 ValidationError::InvalidReference {
1714 source_entity_type: "Thermal",
1715 field_name: "bus_id",
1716 referenced_id: EntityId(88),
1717 ..
1718 }
1719 )
1720 });
1721
1722 assert!(
1723 has_line_error,
1724 "expected Line source_bus_id=99 error, got: {errors:?}"
1725 );
1726 assert!(
1727 has_thermal_error,
1728 "expected Thermal bus_id=88 error, got: {errors:?}"
1729 );
1730 assert!(
1731 errors.len() >= 2,
1732 "expected at least 2 errors, got {}: {errors:?}",
1733 errors.len()
1734 );
1735 }
1736
1737 #[test]
1738 fn test_valid_cross_references_pass() {
1739 let bus_0 = make_bus(0);
1741 let bus_1 = make_bus(1);
1742 let h0 = make_hydro_on_bus(0, 0);
1743 let h1 = make_hydro_on_bus(1, 1);
1744 let mut h2 = make_hydro_on_bus(2, 0);
1745 h2.downstream_id = Some(EntityId(1));
1746 let line = make_line(10, 0, 1);
1747 let thermal = make_thermal_on_bus(20, 0);
1748 let ps = make_pumping_station_full(30, 0, 0, 1);
1749 let contract = make_contract_on_bus(40, 1);
1750 let ncs = make_ncs_on_bus(50, 0);
1751
1752 let result = SystemBuilder::new()
1753 .buses(vec![bus_0, bus_1])
1754 .lines(vec![line])
1755 .hydros(vec![h0, h1, h2])
1756 .thermals(vec![thermal])
1757 .pumping_stations(vec![ps])
1758 .contracts(vec![contract])
1759 .non_controllable_sources(vec![ncs])
1760 .build();
1761
1762 assert!(
1763 result.is_ok(),
1764 "expected Ok for all valid cross-references, got: {:?}",
1765 result.unwrap_err()
1766 );
1767 let system = result.unwrap_or_else(|_| unreachable!());
1768 assert_eq!(system.n_buses(), 2);
1769 assert_eq!(system.n_hydros(), 3);
1770 assert_eq!(system.n_lines(), 1);
1771 assert_eq!(system.n_thermals(), 1);
1772 assert_eq!(system.n_pumping_stations(), 1);
1773 assert_eq!(system.n_contracts(), 1);
1774 assert_eq!(system.n_non_controllable_sources(), 1);
1775 }
1776
1777 #[test]
1780 fn test_cascade_cycle_detected() {
1781 let bus = make_bus(0);
1784 let mut h0 = make_hydro(0);
1785 h0.downstream_id = Some(EntityId(1));
1786 let mut h1 = make_hydro(1);
1787 h1.downstream_id = Some(EntityId(2));
1788 let mut h2 = make_hydro(2);
1789 h2.downstream_id = Some(EntityId(0));
1790
1791 let result = SystemBuilder::new()
1792 .buses(vec![bus])
1793 .hydros(vec![h0, h1, h2])
1794 .build();
1795
1796 assert!(result.is_err(), "expected Err for 3-node cycle");
1797 let errors = result.unwrap_err();
1798 let cycle_error = errors
1799 .iter()
1800 .find(|e| matches!(e, ValidationError::CascadeCycle { .. }));
1801 assert!(
1802 cycle_error.is_some(),
1803 "expected CascadeCycle error, got: {errors:?}"
1804 );
1805 let ValidationError::CascadeCycle { cycle_ids } = cycle_error.unwrap() else {
1806 unreachable!()
1807 };
1808 assert_eq!(
1809 cycle_ids,
1810 &[EntityId(0), EntityId(1), EntityId(2)],
1811 "cycle_ids must be sorted ascending, got: {cycle_ids:?}"
1812 );
1813 }
1814
1815 #[test]
1816 fn test_cascade_self_loop_detected() {
1817 let bus = make_bus(0);
1819 let mut h0 = make_hydro(0);
1820 h0.downstream_id = Some(EntityId(0));
1821
1822 let result = SystemBuilder::new()
1823 .buses(vec![bus])
1824 .hydros(vec![h0])
1825 .build();
1826
1827 assert!(result.is_err(), "expected Err for self-loop");
1828 let errors = result.unwrap_err();
1829 let has_cycle = errors
1830 .iter()
1831 .any(|e| matches!(e, ValidationError::CascadeCycle { cycle_ids } if cycle_ids.contains(&EntityId(0))));
1832 assert!(
1833 has_cycle,
1834 "expected CascadeCycle containing EntityId(0), got: {errors:?}"
1835 );
1836 }
1837
1838 #[test]
1839 fn test_valid_acyclic_cascade_passes() {
1840 let bus = make_bus(0);
1843 let mut h0 = make_hydro(0);
1844 h0.downstream_id = Some(EntityId(1));
1845 let mut h1 = make_hydro(1);
1846 h1.downstream_id = Some(EntityId(2));
1847 let h2 = make_hydro(2);
1848
1849 let result = SystemBuilder::new()
1850 .buses(vec![bus])
1851 .hydros(vec![h0, h1, h2])
1852 .build();
1853
1854 assert!(
1855 result.is_ok(),
1856 "expected Ok for acyclic cascade, got: {:?}",
1857 result.unwrap_err()
1858 );
1859 let system = result.unwrap_or_else(|_| unreachable!());
1860 assert_eq!(
1861 system.cascade().topological_order().len(),
1862 system.n_hydros(),
1863 "topological_order must contain all hydros"
1864 );
1865 }
1866
1867 #[test]
1870 fn test_filling_without_entry_stage() {
1871 use crate::entities::FillingConfig;
1873 let bus = make_bus(0);
1874 let mut hydro = make_hydro(1);
1875 hydro.entry_stage_id = None;
1876 hydro.filling = Some(FillingConfig {
1877 start_stage_id: 10,
1878 filling_inflow_m3s: 100.0,
1879 });
1880
1881 let result = SystemBuilder::new()
1882 .buses(vec![bus])
1883 .hydros(vec![hydro])
1884 .build();
1885
1886 assert!(
1887 result.is_err(),
1888 "expected Err for filling without entry_stage_id"
1889 );
1890 let errors = result.unwrap_err();
1891 let has_error = errors.iter().any(|e| match e {
1892 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
1893 *hydro_id == EntityId(1) && reason.contains("entry_stage_id")
1894 }
1895 _ => false,
1896 });
1897 assert!(
1898 has_error,
1899 "expected InvalidFillingConfig with entry_stage_id reason, got: {errors:?}"
1900 );
1901 }
1902
1903 #[test]
1904 fn test_filling_negative_inflow() {
1905 use crate::entities::FillingConfig;
1907 let bus = make_bus(0);
1908 let mut hydro = make_hydro(1);
1909 hydro.entry_stage_id = Some(10);
1910 hydro.filling = Some(FillingConfig {
1911 start_stage_id: 10,
1912 filling_inflow_m3s: -5.0,
1913 });
1914
1915 let result = SystemBuilder::new()
1916 .buses(vec![bus])
1917 .hydros(vec![hydro])
1918 .build();
1919
1920 assert!(
1921 result.is_err(),
1922 "expected Err for negative filling_inflow_m3s"
1923 );
1924 let errors = result.unwrap_err();
1925 let has_error = errors.iter().any(|e| match e {
1926 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
1927 *hydro_id == EntityId(1) && reason.contains("filling_inflow_m3s must be positive")
1928 }
1929 _ => false,
1930 });
1931 assert!(
1932 has_error,
1933 "expected InvalidFillingConfig with positive inflow reason, got: {errors:?}"
1934 );
1935 }
1936
1937 #[test]
1938 fn test_valid_filling_config_passes() {
1939 use crate::entities::FillingConfig;
1941 let bus = make_bus(0);
1942 let mut hydro = make_hydro(1);
1943 hydro.entry_stage_id = Some(10);
1944 hydro.filling = Some(FillingConfig {
1945 start_stage_id: 10,
1946 filling_inflow_m3s: 100.0,
1947 });
1948
1949 let result = SystemBuilder::new()
1950 .buses(vec![bus])
1951 .hydros(vec![hydro])
1952 .build();
1953
1954 assert!(
1955 result.is_ok(),
1956 "expected Ok for valid filling config, got: {:?}",
1957 result.unwrap_err()
1958 );
1959 }
1960
1961 #[test]
1962 fn test_cascade_cycle_and_invalid_filling_both_reported() {
1963 use crate::entities::FillingConfig;
1966 let bus = make_bus(0);
1967
1968 let mut h0 = make_hydro(0);
1970 h0.downstream_id = Some(EntityId(0));
1971
1972 let mut h1 = make_hydro(1);
1974 h1.entry_stage_id = None; h1.filling = Some(FillingConfig {
1976 start_stage_id: 5,
1977 filling_inflow_m3s: 50.0,
1978 });
1979
1980 let result = SystemBuilder::new()
1981 .buses(vec![bus])
1982 .hydros(vec![h0, h1])
1983 .build();
1984
1985 assert!(result.is_err(), "expected Err for cycle + invalid filling");
1986 let errors = result.unwrap_err();
1987 let has_cycle = errors
1988 .iter()
1989 .any(|e| matches!(e, ValidationError::CascadeCycle { .. }));
1990 let has_filling = errors
1991 .iter()
1992 .any(|e| matches!(e, ValidationError::InvalidFillingConfig { .. }));
1993 assert!(has_cycle, "expected CascadeCycle error, got: {errors:?}");
1994 assert!(
1995 has_filling,
1996 "expected InvalidFillingConfig error, got: {errors:?}"
1997 );
1998 }
1999
2000 #[cfg(feature = "serde")]
2001 #[test]
2002 fn test_system_serde_roundtrip() {
2003 let bus_a = make_bus(1);
2005 let bus_b = make_bus(2);
2006 let hydro = make_hydro_on_bus(10, 1);
2007 let thermal = make_thermal_on_bus(20, 2);
2008 let line = make_line(1, 1, 2);
2009
2010 let system = SystemBuilder::new()
2011 .buses(vec![bus_a, bus_b])
2012 .hydros(vec![hydro])
2013 .thermals(vec![thermal])
2014 .lines(vec![line])
2015 .build()
2016 .expect("valid system");
2017
2018 let json = serde_json::to_string(&system).unwrap();
2019
2020 let mut deserialized: System = serde_json::from_str(&json).unwrap();
2022 deserialized.rebuild_indices();
2023
2024 assert_eq!(system.buses(), deserialized.buses());
2026 assert_eq!(system.hydros(), deserialized.hydros());
2027 assert_eq!(system.thermals(), deserialized.thermals());
2028 assert_eq!(system.lines(), deserialized.lines());
2029
2030 assert_eq!(
2032 deserialized.bus(EntityId(1)).map(|b| b.id),
2033 Some(EntityId(1))
2034 );
2035 assert_eq!(
2036 deserialized.hydro(EntityId(10)).map(|h| h.id),
2037 Some(EntityId(10))
2038 );
2039 assert_eq!(
2040 deserialized.thermal(EntityId(20)).map(|t| t.id),
2041 Some(EntityId(20))
2042 );
2043 assert_eq!(
2044 deserialized.line(EntityId(1)).map(|l| l.id),
2045 Some(EntityId(1))
2046 );
2047 }
2048
2049 fn make_stage(id: i32) -> Stage {
2052 use crate::temporal::{
2053 Block, BlockMode, NoiseMethod, ScenarioSourceConfig, StageRiskConfig, StageStateConfig,
2054 };
2055 use chrono::NaiveDate;
2056 Stage {
2057 index: usize::try_from(id.max(0)).unwrap_or(0),
2058 id,
2059 start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
2060 end_date: NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
2061 season_id: Some(0),
2062 blocks: vec![Block {
2063 index: 0,
2064 name: "SINGLE".to_string(),
2065 duration_hours: 744.0,
2066 }],
2067 block_mode: BlockMode::Parallel,
2068 state_config: StageStateConfig {
2069 storage: true,
2070 inflow_lags: false,
2071 },
2072 risk_config: StageRiskConfig::Expectation,
2073 scenario_config: ScenarioSourceConfig {
2074 branching_factor: 50,
2075 noise_method: NoiseMethod::Saa,
2076 },
2077 }
2078 }
2079
2080 #[test]
2083 fn test_system_backward_compat() {
2084 let system = SystemBuilder::new().build().expect("empty system is valid");
2085 assert_eq!(system.n_buses(), 0);
2087 assert_eq!(system.n_hydros(), 0);
2088 assert_eq!(system.n_stages(), 0);
2090 assert!(system.stages().is_empty());
2091 assert!(system.initial_conditions().storage.is_empty());
2092 assert!(system.generic_constraints().is_empty());
2093 assert!(system.inflow_models().is_empty());
2094 assert!(system.load_models().is_empty());
2095 assert_eq!(system.penalties().n_stages(), 0);
2096 assert_eq!(system.bounds().n_stages(), 0);
2097 assert!(!system.resolved_generic_bounds().is_active(0, 0));
2099 assert!(
2100 system
2101 .resolved_generic_bounds()
2102 .bounds_for_stage(0, 0)
2103 .is_empty()
2104 );
2105 }
2106
2107 #[test]
2109 fn test_system_resolved_generic_bounds_accessor() {
2110 use crate::resolved::ResolvedGenericConstraintBounds;
2111 use std::collections::HashMap as StdHashMap;
2112
2113 let id_map: StdHashMap<i32, usize> = [(0, 0), (1, 1)].into_iter().collect();
2114 let rows = vec![(0i32, 0i32, None::<i32>, 100.0f64)];
2115 let table = ResolvedGenericConstraintBounds::new(&id_map, rows.into_iter());
2116
2117 let system = SystemBuilder::new()
2118 .resolved_generic_bounds(table)
2119 .build()
2120 .expect("valid system");
2121
2122 assert!(system.resolved_generic_bounds().is_active(0, 0));
2123 assert!(!system.resolved_generic_bounds().is_active(1, 0));
2124 let slice = system.resolved_generic_bounds().bounds_for_stage(0, 0);
2125 assert_eq!(slice.len(), 1);
2126 assert_eq!(slice[0], (None, 100.0));
2127 }
2128
2129 #[test]
2131 fn test_system_with_stages() {
2132 let s0 = make_stage(0);
2133 let s1 = make_stage(1);
2134
2135 let system = SystemBuilder::new()
2136 .stages(vec![s1.clone(), s0.clone()]) .build()
2138 .expect("valid system");
2139
2140 assert_eq!(system.n_stages(), 2);
2142 assert_eq!(system.stages()[0].id, 0);
2143 assert_eq!(system.stages()[1].id, 1);
2144
2145 let found = system.stage(0).expect("stage 0 must be found");
2147 assert_eq!(found.id, s0.id);
2148
2149 let found1 = system.stage(1).expect("stage 1 must be found");
2150 assert_eq!(found1.id, s1.id);
2151
2152 assert!(system.stage(99).is_none());
2154 }
2155
2156 #[test]
2158 fn test_system_stage_lookup_by_id() {
2159 let stages: Vec<Stage> = [0i32, 1, 2].iter().map(|&id| make_stage(id)).collect();
2160
2161 let system = SystemBuilder::new()
2162 .stages(stages)
2163 .build()
2164 .expect("valid system");
2165
2166 assert_eq!(system.stage(1).map(|s| s.id), Some(1));
2167 assert!(system.stage(99).is_none());
2168 }
2169
2170 #[test]
2172 fn test_system_with_initial_conditions() {
2173 let ic = InitialConditions {
2174 storage: vec![crate::HydroStorage {
2175 hydro_id: EntityId(0),
2176 value_hm3: 15_000.0,
2177 }],
2178 filling_storage: vec![],
2179 past_inflows: vec![],
2180 };
2181
2182 let system = SystemBuilder::new()
2183 .initial_conditions(ic)
2184 .build()
2185 .expect("valid system");
2186
2187 assert_eq!(system.initial_conditions().storage.len(), 1);
2188 assert_eq!(system.initial_conditions().storage[0].hydro_id, EntityId(0));
2189 assert!((system.initial_conditions().storage[0].value_hm3 - 15_000.0).abs() < f64::EPSILON);
2190 }
2191
2192 #[cfg(feature = "serde")]
2195 #[test]
2196 fn test_system_serde_roundtrip_with_stages() {
2197 use crate::temporal::PolicyGraphType;
2198
2199 let stages = vec![make_stage(0), make_stage(1)];
2200 let policy_graph = PolicyGraph {
2201 graph_type: PolicyGraphType::FiniteHorizon,
2202 annual_discount_rate: 0.0,
2203 transitions: vec![],
2204 season_map: None,
2205 };
2206
2207 let system = SystemBuilder::new()
2208 .stages(stages)
2209 .policy_graph(policy_graph)
2210 .build()
2211 .expect("valid system");
2212
2213 let json = serde_json::to_string(&system).unwrap();
2214 let mut deserialized: System = serde_json::from_str(&json).unwrap();
2215
2216 deserialized.rebuild_indices();
2218
2219 assert_eq!(system.n_stages(), deserialized.n_stages());
2221 assert_eq!(system.stages()[0].id, deserialized.stages()[0].id);
2222 assert_eq!(system.stages()[1].id, deserialized.stages()[1].id);
2223
2224 assert_eq!(deserialized.stage(0).map(|s| s.id), Some(0));
2226 assert_eq!(deserialized.stage(1).map(|s| s.id), Some(1));
2227 assert!(deserialized.stage(99).is_none());
2228
2229 assert_eq!(
2231 deserialized.policy_graph().graph_type,
2232 system.policy_graph().graph_type
2233 );
2234 }
2235}