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 };
1236 Hydro {
1237 id: EntityId(id),
1238 name: format!("hydro-{id}"),
1239 bus_id: EntityId(bus_id),
1240 downstream_id: None,
1241 entry_stage_id: None,
1242 exit_stage_id: None,
1243 min_storage_hm3: 0.0,
1244 max_storage_hm3: 1.0,
1245 min_outflow_m3s: 0.0,
1246 max_outflow_m3s: None,
1247 generation_model: HydroGenerationModel::ConstantProductivity {
1248 productivity_mw_per_m3s: 1.0,
1249 },
1250 min_turbined_m3s: 0.0,
1251 max_turbined_m3s: 1.0,
1252 min_generation_mw: 0.0,
1253 max_generation_mw: 1.0,
1254 tailrace: None,
1255 hydraulic_losses: None,
1256 efficiency: None,
1257 evaporation_coefficients_mm: None,
1258 evaporation_reference_volumes_hm3: None,
1259 diversion: None,
1260 filling: None,
1261 penalties: zero_penalties,
1262 }
1263 }
1264
1265 fn make_hydro(id: i32) -> Hydro {
1267 make_hydro_on_bus(id, 0)
1268 }
1269
1270 fn make_thermal_on_bus(id: i32, bus_id: i32) -> Thermal {
1271 Thermal {
1272 id: EntityId(id),
1273 name: format!("thermal-{id}"),
1274 bus_id: EntityId(bus_id),
1275 entry_stage_id: None,
1276 exit_stage_id: None,
1277 cost_segments: vec![ThermalCostSegment {
1278 capacity_mw: 100.0,
1279 cost_per_mwh: 50.0,
1280 }],
1281 min_generation_mw: 0.0,
1282 max_generation_mw: 100.0,
1283 gnl_config: None,
1284 }
1285 }
1286
1287 fn make_thermal(id: i32) -> Thermal {
1289 make_thermal_on_bus(id, 0)
1290 }
1291
1292 fn make_pumping_station_full(
1293 id: i32,
1294 bus_id: i32,
1295 source_hydro_id: i32,
1296 destination_hydro_id: i32,
1297 ) -> PumpingStation {
1298 PumpingStation {
1299 id: EntityId(id),
1300 name: format!("ps-{id}"),
1301 bus_id: EntityId(bus_id),
1302 source_hydro_id: EntityId(source_hydro_id),
1303 destination_hydro_id: EntityId(destination_hydro_id),
1304 entry_stage_id: None,
1305 exit_stage_id: None,
1306 consumption_mw_per_m3s: 0.5,
1307 min_flow_m3s: 0.0,
1308 max_flow_m3s: 10.0,
1309 }
1310 }
1311
1312 fn make_pumping_station(id: i32) -> PumpingStation {
1313 make_pumping_station_full(id, 0, 0, 1)
1314 }
1315
1316 fn make_contract_on_bus(id: i32, bus_id: i32) -> EnergyContract {
1317 EnergyContract {
1318 id: EntityId(id),
1319 name: format!("contract-{id}"),
1320 bus_id: EntityId(bus_id),
1321 contract_type: ContractType::Import,
1322 entry_stage_id: None,
1323 exit_stage_id: None,
1324 price_per_mwh: 0.0,
1325 min_mw: 0.0,
1326 max_mw: 100.0,
1327 }
1328 }
1329
1330 fn make_contract(id: i32) -> EnergyContract {
1331 make_contract_on_bus(id, 0)
1332 }
1333
1334 fn make_ncs_on_bus(id: i32, bus_id: i32) -> NonControllableSource {
1335 NonControllableSource {
1336 id: EntityId(id),
1337 name: format!("ncs-{id}"),
1338 bus_id: EntityId(bus_id),
1339 entry_stage_id: None,
1340 exit_stage_id: None,
1341 max_generation_mw: 50.0,
1342 curtailment_cost: 0.0,
1343 }
1344 }
1345
1346 fn make_ncs(id: i32) -> NonControllableSource {
1347 make_ncs_on_bus(id, 0)
1348 }
1349
1350 #[test]
1351 fn test_empty_system() {
1352 let system = SystemBuilder::new().build().expect("empty system is valid");
1353 assert_eq!(system.n_buses(), 0);
1354 assert_eq!(system.n_lines(), 0);
1355 assert_eq!(system.n_hydros(), 0);
1356 assert_eq!(system.n_thermals(), 0);
1357 assert_eq!(system.n_pumping_stations(), 0);
1358 assert_eq!(system.n_contracts(), 0);
1359 assert_eq!(system.n_non_controllable_sources(), 0);
1360 assert!(system.buses().is_empty());
1361 assert!(system.cascade().is_empty());
1362 }
1363
1364 #[test]
1365 fn test_canonical_ordering() {
1366 let system = SystemBuilder::new()
1368 .buses(vec![make_bus(2), make_bus(1), make_bus(0)])
1369 .build()
1370 .expect("valid system");
1371
1372 assert_eq!(system.buses()[0].id, EntityId(0));
1373 assert_eq!(system.buses()[1].id, EntityId(1));
1374 assert_eq!(system.buses()[2].id, EntityId(2));
1375 }
1376
1377 #[test]
1378 fn test_lookup_by_id() {
1379 let system = SystemBuilder::new()
1381 .buses(vec![make_bus(0)])
1382 .hydros(vec![make_hydro(10), make_hydro(5), make_hydro(20)])
1383 .build()
1384 .expect("valid system");
1385
1386 assert_eq!(system.hydro(EntityId(5)).map(|h| h.id), Some(EntityId(5)));
1387 assert_eq!(system.hydro(EntityId(10)).map(|h| h.id), Some(EntityId(10)));
1388 assert_eq!(system.hydro(EntityId(20)).map(|h| h.id), Some(EntityId(20)));
1389 }
1390
1391 #[test]
1392 fn test_lookup_missing_id() {
1393 let system = SystemBuilder::new()
1395 .buses(vec![make_bus(0)])
1396 .hydros(vec![make_hydro(1), make_hydro(2)])
1397 .build()
1398 .expect("valid system");
1399
1400 assert!(system.hydro(EntityId(999)).is_none());
1401 }
1402
1403 #[test]
1404 fn test_count_queries() {
1405 let system = SystemBuilder::new()
1406 .buses(vec![make_bus(0), make_bus(1)])
1407 .lines(vec![make_line(0, 0, 1)])
1408 .hydros(vec![make_hydro(0), make_hydro(1), make_hydro(2)])
1409 .thermals(vec![make_thermal(0)])
1410 .pumping_stations(vec![make_pumping_station(0)])
1411 .contracts(vec![make_contract(0), make_contract(1)])
1412 .non_controllable_sources(vec![make_ncs(0)])
1413 .build()
1414 .expect("valid system");
1415
1416 assert_eq!(system.n_buses(), 2);
1417 assert_eq!(system.n_lines(), 1);
1418 assert_eq!(system.n_hydros(), 3);
1419 assert_eq!(system.n_thermals(), 1);
1420 assert_eq!(system.n_pumping_stations(), 1);
1421 assert_eq!(system.n_contracts(), 2);
1422 assert_eq!(system.n_non_controllable_sources(), 1);
1423 }
1424
1425 #[test]
1426 fn test_slice_accessors() {
1427 let system = SystemBuilder::new()
1428 .buses(vec![make_bus(0), make_bus(1), make_bus(2)])
1429 .build()
1430 .expect("valid system");
1431
1432 let buses = system.buses();
1433 assert_eq!(buses.len(), 3);
1434 assert_eq!(buses[0].id, EntityId(0));
1435 assert_eq!(buses[1].id, EntityId(1));
1436 assert_eq!(buses[2].id, EntityId(2));
1437 }
1438
1439 #[test]
1440 fn test_duplicate_id_error() {
1441 let result = SystemBuilder::new()
1443 .buses(vec![make_bus(0), make_bus(0)])
1444 .build();
1445
1446 assert!(result.is_err());
1447 let errors = result.unwrap_err();
1448 assert!(!errors.is_empty());
1449 assert!(errors.iter().any(|e| matches!(
1450 e,
1451 ValidationError::DuplicateId {
1452 entity_type: "Bus",
1453 id: EntityId(0),
1454 }
1455 )));
1456 }
1457
1458 #[test]
1459 fn test_multiple_duplicate_errors() {
1460 let result = SystemBuilder::new()
1462 .buses(vec![make_bus(0), make_bus(0)])
1463 .thermals(vec![make_thermal(5), make_thermal(5)])
1464 .build();
1465
1466 assert!(result.is_err());
1467 let errors = result.unwrap_err();
1468
1469 let has_bus_dup = errors.iter().any(|e| {
1470 matches!(
1471 e,
1472 ValidationError::DuplicateId {
1473 entity_type: "Bus",
1474 ..
1475 }
1476 )
1477 });
1478 let has_thermal_dup = errors.iter().any(|e| {
1479 matches!(
1480 e,
1481 ValidationError::DuplicateId {
1482 entity_type: "Thermal",
1483 ..
1484 }
1485 )
1486 });
1487 assert!(has_bus_dup, "expected Bus duplicate error");
1488 assert!(has_thermal_dup, "expected Thermal duplicate error");
1489 }
1490
1491 #[test]
1492 fn test_send_sync() {
1493 fn require_send_sync<T: Send + Sync>(_: T) {}
1494 let system = SystemBuilder::new().build().expect("valid system");
1495 require_send_sync(system);
1496 }
1497
1498 #[test]
1499 fn test_cascade_accessible() {
1500 let mut h0 = make_hydro_on_bus(0, 0);
1502 h0.downstream_id = Some(EntityId(1));
1503 let mut h1 = make_hydro_on_bus(1, 0);
1504 h1.downstream_id = Some(EntityId(2));
1505 let h2 = make_hydro_on_bus(2, 0);
1506
1507 let system = SystemBuilder::new()
1508 .buses(vec![make_bus(0)])
1509 .hydros(vec![h0, h1, h2])
1510 .build()
1511 .expect("valid system");
1512
1513 let order = system.cascade().topological_order();
1514 assert!(!order.is_empty(), "topological order must be non-empty");
1515 let pos_0 = order
1516 .iter()
1517 .position(|&id| id == EntityId(0))
1518 .expect("EntityId(0) must be in topological order");
1519 let pos_2 = order
1520 .iter()
1521 .position(|&id| id == EntityId(2))
1522 .expect("EntityId(2) must be in topological order");
1523 assert!(pos_0 < pos_2, "EntityId(0) must precede EntityId(2)");
1524 }
1525
1526 #[test]
1527 fn test_network_accessible() {
1528 let system = SystemBuilder::new()
1529 .buses(vec![make_bus(0), make_bus(1)])
1530 .lines(vec![make_line(0, 0, 1)])
1531 .build()
1532 .expect("valid system");
1533
1534 let connections = system.network().bus_lines(EntityId(0));
1535 assert!(!connections.is_empty(), "bus 0 must have connections");
1536 assert_eq!(connections[0].line_id, EntityId(0));
1537 }
1538
1539 #[test]
1540 fn test_all_entity_lookups() {
1541 let system = SystemBuilder::new()
1546 .buses(vec![make_bus(0), make_bus(1)])
1547 .lines(vec![make_line(2, 0, 1)])
1548 .hydros(vec![
1549 make_hydro_on_bus(0, 0),
1550 make_hydro_on_bus(1, 0),
1551 make_hydro_on_bus(3, 0),
1552 ])
1553 .thermals(vec![make_thermal(4)])
1554 .pumping_stations(vec![make_pumping_station(5)])
1555 .contracts(vec![make_contract(6)])
1556 .non_controllable_sources(vec![make_ncs(7)])
1557 .build()
1558 .expect("valid system");
1559
1560 assert!(system.bus(EntityId(1)).is_some());
1561 assert!(system.line(EntityId(2)).is_some());
1562 assert!(system.hydro(EntityId(3)).is_some());
1563 assert!(system.thermal(EntityId(4)).is_some());
1564 assert!(system.pumping_station(EntityId(5)).is_some());
1565 assert!(system.contract(EntityId(6)).is_some());
1566 assert!(system.non_controllable_source(EntityId(7)).is_some());
1567
1568 assert!(system.bus(EntityId(999)).is_none());
1569 assert!(system.line(EntityId(999)).is_none());
1570 assert!(system.hydro(EntityId(999)).is_none());
1571 assert!(system.thermal(EntityId(999)).is_none());
1572 assert!(system.pumping_station(EntityId(999)).is_none());
1573 assert!(system.contract(EntityId(999)).is_none());
1574 assert!(system.non_controllable_source(EntityId(999)).is_none());
1575 }
1576
1577 #[test]
1578 fn test_default_builder() {
1579 let system = SystemBuilder::default()
1580 .build()
1581 .expect("default builder produces valid empty system");
1582 assert_eq!(system.n_buses(), 0);
1583 }
1584
1585 #[test]
1588 fn test_invalid_bus_reference_hydro() {
1589 let hydro = make_hydro_on_bus(1, 99);
1591 let result = SystemBuilder::new().hydros(vec![hydro]).build();
1592
1593 assert!(result.is_err(), "expected Err for missing bus reference");
1594 let errors = result.unwrap_err();
1595 assert!(
1596 errors.iter().any(|e| matches!(
1597 e,
1598 ValidationError::InvalidReference {
1599 source_entity_type: "Hydro",
1600 source_id: EntityId(1),
1601 field_name: "bus_id",
1602 referenced_id: EntityId(99),
1603 expected_type: "Bus",
1604 }
1605 )),
1606 "expected InvalidReference for Hydro bus_id=99, got: {errors:?}"
1607 );
1608 }
1609
1610 #[test]
1611 fn test_invalid_downstream_reference() {
1612 let bus = make_bus(0);
1614 let mut hydro = make_hydro(1);
1615 hydro.downstream_id = Some(EntityId(50));
1616
1617 let result = SystemBuilder::new()
1618 .buses(vec![bus])
1619 .hydros(vec![hydro])
1620 .build();
1621
1622 assert!(
1623 result.is_err(),
1624 "expected Err for missing downstream reference"
1625 );
1626 let errors = result.unwrap_err();
1627 assert!(
1628 errors.iter().any(|e| matches!(
1629 e,
1630 ValidationError::InvalidReference {
1631 source_entity_type: "Hydro",
1632 source_id: EntityId(1),
1633 field_name: "downstream_id",
1634 referenced_id: EntityId(50),
1635 expected_type: "Hydro",
1636 }
1637 )),
1638 "expected InvalidReference for Hydro downstream_id=50, got: {errors:?}"
1639 );
1640 }
1641
1642 #[test]
1643 fn test_invalid_pumping_station_hydro_refs() {
1644 let bus = make_bus(0);
1646 let dest_hydro = make_hydro(1);
1647 let ps = make_pumping_station_full(10, 0, 77, 1);
1648
1649 let result = SystemBuilder::new()
1650 .buses(vec![bus])
1651 .hydros(vec![dest_hydro])
1652 .pumping_stations(vec![ps])
1653 .build();
1654
1655 assert!(
1656 result.is_err(),
1657 "expected Err for missing source_hydro_id reference"
1658 );
1659 let errors = result.unwrap_err();
1660 assert!(
1661 errors.iter().any(|e| matches!(
1662 e,
1663 ValidationError::InvalidReference {
1664 source_entity_type: "PumpingStation",
1665 source_id: EntityId(10),
1666 field_name: "source_hydro_id",
1667 referenced_id: EntityId(77),
1668 expected_type: "Hydro",
1669 }
1670 )),
1671 "expected InvalidReference for PumpingStation source_hydro_id=77, got: {errors:?}"
1672 );
1673 }
1674
1675 #[test]
1676 fn test_multiple_invalid_references_collected() {
1677 let line = make_line(1, 99, 0);
1680 let thermal = make_thermal_on_bus(2, 88);
1681
1682 let result = SystemBuilder::new()
1683 .buses(vec![make_bus(0)])
1684 .lines(vec![line])
1685 .thermals(vec![thermal])
1686 .build();
1687
1688 assert!(
1689 result.is_err(),
1690 "expected Err for multiple invalid references"
1691 );
1692 let errors = result.unwrap_err();
1693
1694 let has_line_error = errors.iter().any(|e| {
1695 matches!(
1696 e,
1697 ValidationError::InvalidReference {
1698 source_entity_type: "Line",
1699 field_name: "source_bus_id",
1700 referenced_id: EntityId(99),
1701 ..
1702 }
1703 )
1704 });
1705 let has_thermal_error = errors.iter().any(|e| {
1706 matches!(
1707 e,
1708 ValidationError::InvalidReference {
1709 source_entity_type: "Thermal",
1710 field_name: "bus_id",
1711 referenced_id: EntityId(88),
1712 ..
1713 }
1714 )
1715 });
1716
1717 assert!(
1718 has_line_error,
1719 "expected Line source_bus_id=99 error, got: {errors:?}"
1720 );
1721 assert!(
1722 has_thermal_error,
1723 "expected Thermal bus_id=88 error, got: {errors:?}"
1724 );
1725 assert!(
1726 errors.len() >= 2,
1727 "expected at least 2 errors, got {}: {errors:?}",
1728 errors.len()
1729 );
1730 }
1731
1732 #[test]
1733 fn test_valid_cross_references_pass() {
1734 let bus_0 = make_bus(0);
1736 let bus_1 = make_bus(1);
1737 let h0 = make_hydro_on_bus(0, 0);
1738 let h1 = make_hydro_on_bus(1, 1);
1739 let mut h2 = make_hydro_on_bus(2, 0);
1740 h2.downstream_id = Some(EntityId(1));
1741 let line = make_line(10, 0, 1);
1742 let thermal = make_thermal_on_bus(20, 0);
1743 let ps = make_pumping_station_full(30, 0, 0, 1);
1744 let contract = make_contract_on_bus(40, 1);
1745 let ncs = make_ncs_on_bus(50, 0);
1746
1747 let result = SystemBuilder::new()
1748 .buses(vec![bus_0, bus_1])
1749 .lines(vec![line])
1750 .hydros(vec![h0, h1, h2])
1751 .thermals(vec![thermal])
1752 .pumping_stations(vec![ps])
1753 .contracts(vec![contract])
1754 .non_controllable_sources(vec![ncs])
1755 .build();
1756
1757 assert!(
1758 result.is_ok(),
1759 "expected Ok for all valid cross-references, got: {:?}",
1760 result.unwrap_err()
1761 );
1762 let system = result.unwrap_or_else(|_| unreachable!());
1763 assert_eq!(system.n_buses(), 2);
1764 assert_eq!(system.n_hydros(), 3);
1765 assert_eq!(system.n_lines(), 1);
1766 assert_eq!(system.n_thermals(), 1);
1767 assert_eq!(system.n_pumping_stations(), 1);
1768 assert_eq!(system.n_contracts(), 1);
1769 assert_eq!(system.n_non_controllable_sources(), 1);
1770 }
1771
1772 #[test]
1775 fn test_cascade_cycle_detected() {
1776 let bus = make_bus(0);
1779 let mut h0 = make_hydro(0);
1780 h0.downstream_id = Some(EntityId(1));
1781 let mut h1 = make_hydro(1);
1782 h1.downstream_id = Some(EntityId(2));
1783 let mut h2 = make_hydro(2);
1784 h2.downstream_id = Some(EntityId(0));
1785
1786 let result = SystemBuilder::new()
1787 .buses(vec![bus])
1788 .hydros(vec![h0, h1, h2])
1789 .build();
1790
1791 assert!(result.is_err(), "expected Err for 3-node cycle");
1792 let errors = result.unwrap_err();
1793 let cycle_error = errors
1794 .iter()
1795 .find(|e| matches!(e, ValidationError::CascadeCycle { .. }));
1796 assert!(
1797 cycle_error.is_some(),
1798 "expected CascadeCycle error, got: {errors:?}"
1799 );
1800 let ValidationError::CascadeCycle { cycle_ids } = cycle_error.unwrap() else {
1801 unreachable!()
1802 };
1803 assert_eq!(
1804 cycle_ids,
1805 &[EntityId(0), EntityId(1), EntityId(2)],
1806 "cycle_ids must be sorted ascending, got: {cycle_ids:?}"
1807 );
1808 }
1809
1810 #[test]
1811 fn test_cascade_self_loop_detected() {
1812 let bus = make_bus(0);
1814 let mut h0 = make_hydro(0);
1815 h0.downstream_id = Some(EntityId(0));
1816
1817 let result = SystemBuilder::new()
1818 .buses(vec![bus])
1819 .hydros(vec![h0])
1820 .build();
1821
1822 assert!(result.is_err(), "expected Err for self-loop");
1823 let errors = result.unwrap_err();
1824 let has_cycle = errors
1825 .iter()
1826 .any(|e| matches!(e, ValidationError::CascadeCycle { cycle_ids } if cycle_ids.contains(&EntityId(0))));
1827 assert!(
1828 has_cycle,
1829 "expected CascadeCycle containing EntityId(0), got: {errors:?}"
1830 );
1831 }
1832
1833 #[test]
1834 fn test_valid_acyclic_cascade_passes() {
1835 let bus = make_bus(0);
1838 let mut h0 = make_hydro(0);
1839 h0.downstream_id = Some(EntityId(1));
1840 let mut h1 = make_hydro(1);
1841 h1.downstream_id = Some(EntityId(2));
1842 let h2 = make_hydro(2);
1843
1844 let result = SystemBuilder::new()
1845 .buses(vec![bus])
1846 .hydros(vec![h0, h1, h2])
1847 .build();
1848
1849 assert!(
1850 result.is_ok(),
1851 "expected Ok for acyclic cascade, got: {:?}",
1852 result.unwrap_err()
1853 );
1854 let system = result.unwrap_or_else(|_| unreachable!());
1855 assert_eq!(
1856 system.cascade().topological_order().len(),
1857 system.n_hydros(),
1858 "topological_order must contain all hydros"
1859 );
1860 }
1861
1862 #[test]
1865 fn test_filling_without_entry_stage() {
1866 use crate::entities::FillingConfig;
1868 let bus = make_bus(0);
1869 let mut hydro = make_hydro(1);
1870 hydro.entry_stage_id = None;
1871 hydro.filling = Some(FillingConfig {
1872 start_stage_id: 10,
1873 filling_inflow_m3s: 100.0,
1874 });
1875
1876 let result = SystemBuilder::new()
1877 .buses(vec![bus])
1878 .hydros(vec![hydro])
1879 .build();
1880
1881 assert!(
1882 result.is_err(),
1883 "expected Err for filling without entry_stage_id"
1884 );
1885 let errors = result.unwrap_err();
1886 let has_error = errors.iter().any(|e| match e {
1887 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
1888 *hydro_id == EntityId(1) && reason.contains("entry_stage_id")
1889 }
1890 _ => false,
1891 });
1892 assert!(
1893 has_error,
1894 "expected InvalidFillingConfig with entry_stage_id reason, got: {errors:?}"
1895 );
1896 }
1897
1898 #[test]
1899 fn test_filling_negative_inflow() {
1900 use crate::entities::FillingConfig;
1902 let bus = make_bus(0);
1903 let mut hydro = make_hydro(1);
1904 hydro.entry_stage_id = Some(10);
1905 hydro.filling = Some(FillingConfig {
1906 start_stage_id: 10,
1907 filling_inflow_m3s: -5.0,
1908 });
1909
1910 let result = SystemBuilder::new()
1911 .buses(vec![bus])
1912 .hydros(vec![hydro])
1913 .build();
1914
1915 assert!(
1916 result.is_err(),
1917 "expected Err for negative filling_inflow_m3s"
1918 );
1919 let errors = result.unwrap_err();
1920 let has_error = errors.iter().any(|e| match e {
1921 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
1922 *hydro_id == EntityId(1) && reason.contains("filling_inflow_m3s must be positive")
1923 }
1924 _ => false,
1925 });
1926 assert!(
1927 has_error,
1928 "expected InvalidFillingConfig with positive inflow reason, got: {errors:?}"
1929 );
1930 }
1931
1932 #[test]
1933 fn test_valid_filling_config_passes() {
1934 use crate::entities::FillingConfig;
1936 let bus = make_bus(0);
1937 let mut hydro = make_hydro(1);
1938 hydro.entry_stage_id = Some(10);
1939 hydro.filling = Some(FillingConfig {
1940 start_stage_id: 10,
1941 filling_inflow_m3s: 100.0,
1942 });
1943
1944 let result = SystemBuilder::new()
1945 .buses(vec![bus])
1946 .hydros(vec![hydro])
1947 .build();
1948
1949 assert!(
1950 result.is_ok(),
1951 "expected Ok for valid filling config, got: {:?}",
1952 result.unwrap_err()
1953 );
1954 }
1955
1956 #[test]
1957 fn test_cascade_cycle_and_invalid_filling_both_reported() {
1958 use crate::entities::FillingConfig;
1961 let bus = make_bus(0);
1962
1963 let mut h0 = make_hydro(0);
1965 h0.downstream_id = Some(EntityId(0));
1966
1967 let mut h1 = make_hydro(1);
1969 h1.entry_stage_id = None; h1.filling = Some(FillingConfig {
1971 start_stage_id: 5,
1972 filling_inflow_m3s: 50.0,
1973 });
1974
1975 let result = SystemBuilder::new()
1976 .buses(vec![bus])
1977 .hydros(vec![h0, h1])
1978 .build();
1979
1980 assert!(result.is_err(), "expected Err for cycle + invalid filling");
1981 let errors = result.unwrap_err();
1982 let has_cycle = errors
1983 .iter()
1984 .any(|e| matches!(e, ValidationError::CascadeCycle { .. }));
1985 let has_filling = errors
1986 .iter()
1987 .any(|e| matches!(e, ValidationError::InvalidFillingConfig { .. }));
1988 assert!(has_cycle, "expected CascadeCycle error, got: {errors:?}");
1989 assert!(
1990 has_filling,
1991 "expected InvalidFillingConfig error, got: {errors:?}"
1992 );
1993 }
1994
1995 #[cfg(feature = "serde")]
1996 #[test]
1997 fn test_system_serde_roundtrip() {
1998 let bus_a = make_bus(1);
2000 let bus_b = make_bus(2);
2001 let hydro = make_hydro_on_bus(10, 1);
2002 let thermal = make_thermal_on_bus(20, 2);
2003 let line = make_line(1, 1, 2);
2004
2005 let system = SystemBuilder::new()
2006 .buses(vec![bus_a, bus_b])
2007 .hydros(vec![hydro])
2008 .thermals(vec![thermal])
2009 .lines(vec![line])
2010 .build()
2011 .expect("valid system");
2012
2013 let json = serde_json::to_string(&system).unwrap();
2014
2015 let mut deserialized: System = serde_json::from_str(&json).unwrap();
2017 deserialized.rebuild_indices();
2018
2019 assert_eq!(system.buses(), deserialized.buses());
2021 assert_eq!(system.hydros(), deserialized.hydros());
2022 assert_eq!(system.thermals(), deserialized.thermals());
2023 assert_eq!(system.lines(), deserialized.lines());
2024
2025 assert_eq!(
2027 deserialized.bus(EntityId(1)).map(|b| b.id),
2028 Some(EntityId(1))
2029 );
2030 assert_eq!(
2031 deserialized.hydro(EntityId(10)).map(|h| h.id),
2032 Some(EntityId(10))
2033 );
2034 assert_eq!(
2035 deserialized.thermal(EntityId(20)).map(|t| t.id),
2036 Some(EntityId(20))
2037 );
2038 assert_eq!(
2039 deserialized.line(EntityId(1)).map(|l| l.id),
2040 Some(EntityId(1))
2041 );
2042 }
2043
2044 fn make_stage(id: i32) -> Stage {
2047 use crate::temporal::{
2048 Block, BlockMode, NoiseMethod, ScenarioSourceConfig, StageRiskConfig, StageStateConfig,
2049 };
2050 use chrono::NaiveDate;
2051 Stage {
2052 index: usize::try_from(id.max(0)).unwrap_or(0),
2053 id,
2054 start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
2055 end_date: NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
2056 season_id: Some(0),
2057 blocks: vec![Block {
2058 index: 0,
2059 name: "SINGLE".to_string(),
2060 duration_hours: 744.0,
2061 }],
2062 block_mode: BlockMode::Parallel,
2063 state_config: StageStateConfig {
2064 storage: true,
2065 inflow_lags: false,
2066 },
2067 risk_config: StageRiskConfig::Expectation,
2068 scenario_config: ScenarioSourceConfig {
2069 branching_factor: 50,
2070 noise_method: NoiseMethod::Saa,
2071 },
2072 }
2073 }
2074
2075 #[test]
2078 fn test_system_backward_compat() {
2079 let system = SystemBuilder::new().build().expect("empty system is valid");
2080 assert_eq!(system.n_buses(), 0);
2082 assert_eq!(system.n_hydros(), 0);
2083 assert_eq!(system.n_stages(), 0);
2085 assert!(system.stages().is_empty());
2086 assert!(system.initial_conditions().storage.is_empty());
2087 assert!(system.generic_constraints().is_empty());
2088 assert!(system.inflow_models().is_empty());
2089 assert!(system.load_models().is_empty());
2090 assert_eq!(system.penalties().n_stages(), 0);
2091 assert_eq!(system.bounds().n_stages(), 0);
2092 assert!(!system.resolved_generic_bounds().is_active(0, 0));
2094 assert!(
2095 system
2096 .resolved_generic_bounds()
2097 .bounds_for_stage(0, 0)
2098 .is_empty()
2099 );
2100 }
2101
2102 #[test]
2104 fn test_system_resolved_generic_bounds_accessor() {
2105 use crate::resolved::ResolvedGenericConstraintBounds;
2106 use std::collections::HashMap as StdHashMap;
2107
2108 let id_map: StdHashMap<i32, usize> = [(0, 0), (1, 1)].into_iter().collect();
2109 let rows = vec![(0i32, 0i32, None::<i32>, 100.0f64)];
2110 let table = ResolvedGenericConstraintBounds::new(&id_map, rows.into_iter());
2111
2112 let system = SystemBuilder::new()
2113 .resolved_generic_bounds(table)
2114 .build()
2115 .expect("valid system");
2116
2117 assert!(system.resolved_generic_bounds().is_active(0, 0));
2118 assert!(!system.resolved_generic_bounds().is_active(1, 0));
2119 let slice = system.resolved_generic_bounds().bounds_for_stage(0, 0);
2120 assert_eq!(slice.len(), 1);
2121 assert_eq!(slice[0], (None, 100.0));
2122 }
2123
2124 #[test]
2126 fn test_system_with_stages() {
2127 let s0 = make_stage(0);
2128 let s1 = make_stage(1);
2129
2130 let system = SystemBuilder::new()
2131 .stages(vec![s1.clone(), s0.clone()]) .build()
2133 .expect("valid system");
2134
2135 assert_eq!(system.n_stages(), 2);
2137 assert_eq!(system.stages()[0].id, 0);
2138 assert_eq!(system.stages()[1].id, 1);
2139
2140 let found = system.stage(0).expect("stage 0 must be found");
2142 assert_eq!(found.id, s0.id);
2143
2144 let found1 = system.stage(1).expect("stage 1 must be found");
2145 assert_eq!(found1.id, s1.id);
2146
2147 assert!(system.stage(99).is_none());
2149 }
2150
2151 #[test]
2153 fn test_system_stage_lookup_by_id() {
2154 let stages: Vec<Stage> = [0i32, 1, 2].iter().map(|&id| make_stage(id)).collect();
2155
2156 let system = SystemBuilder::new()
2157 .stages(stages)
2158 .build()
2159 .expect("valid system");
2160
2161 assert_eq!(system.stage(1).map(|s| s.id), Some(1));
2162 assert!(system.stage(99).is_none());
2163 }
2164
2165 #[test]
2167 fn test_system_with_initial_conditions() {
2168 let ic = InitialConditions {
2169 storage: vec![crate::HydroStorage {
2170 hydro_id: EntityId(0),
2171 value_hm3: 15_000.0,
2172 }],
2173 filling_storage: vec![],
2174 past_inflows: vec![],
2175 };
2176
2177 let system = SystemBuilder::new()
2178 .initial_conditions(ic)
2179 .build()
2180 .expect("valid system");
2181
2182 assert_eq!(system.initial_conditions().storage.len(), 1);
2183 assert_eq!(system.initial_conditions().storage[0].hydro_id, EntityId(0));
2184 assert!((system.initial_conditions().storage[0].value_hm3 - 15_000.0).abs() < f64::EPSILON);
2185 }
2186
2187 #[cfg(feature = "serde")]
2190 #[test]
2191 fn test_system_serde_roundtrip_with_stages() {
2192 use crate::temporal::PolicyGraphType;
2193
2194 let stages = vec![make_stage(0), make_stage(1)];
2195 let policy_graph = PolicyGraph {
2196 graph_type: PolicyGraphType::FiniteHorizon,
2197 annual_discount_rate: 0.0,
2198 transitions: vec![],
2199 season_map: None,
2200 };
2201
2202 let system = SystemBuilder::new()
2203 .stages(stages)
2204 .policy_graph(policy_graph)
2205 .build()
2206 .expect("valid system");
2207
2208 let json = serde_json::to_string(&system).unwrap();
2209 let mut deserialized: System = serde_json::from_str(&json).unwrap();
2210
2211 deserialized.rebuild_indices();
2213
2214 assert_eq!(system.n_stages(), deserialized.n_stages());
2216 assert_eq!(system.stages()[0].id, deserialized.stages()[0].id);
2217 assert_eq!(system.stages()[1].id, deserialized.stages()[1].id);
2218
2219 assert_eq!(deserialized.stage(0).map(|s| s.id), Some(0));
2221 assert_eq!(deserialized.stage(1).map(|s| s.id), Some(1));
2222 assert!(deserialized.stage(99).is_none());
2223
2224 assert_eq!(
2226 deserialized.policy_graph().graph_type,
2227 system.policy_graph().graph_type
2228 );
2229 }
2230}