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 &self.lines,
816 &self.hydros,
817 &self.thermals,
818 &self.pumping_stations,
819 &self.contracts,
820 &self.non_controllable_sources,
821 &bus_index,
822 &hydro_index,
823 &mut errors,
824 );
825
826 if !errors.is_empty() {
827 return Err(errors);
828 }
829
830 let cascade = CascadeTopology::build(&self.hydros);
831
832 if cascade.topological_order().len() < self.hydros.len() {
833 let in_topo: HashSet<EntityId> = cascade.topological_order().iter().copied().collect();
834 let mut cycle_ids: Vec<EntityId> = self
835 .hydros
836 .iter()
837 .map(|h| h.id)
838 .filter(|id| !in_topo.contains(id))
839 .collect();
840 cycle_ids.sort_by_key(|id| id.0);
841 errors.push(ValidationError::CascadeCycle { cycle_ids });
842 }
843
844 validate_filling_configs(&self.hydros, &mut errors);
845
846 if !errors.is_empty() {
847 return Err(errors);
848 }
849
850 let network = NetworkTopology::build(
851 &self.buses,
852 &self.lines,
853 &self.hydros,
854 &self.thermals,
855 &self.non_controllable_sources,
856 &self.contracts,
857 &self.pumping_stations,
858 );
859
860 let stage_index = build_stage_index(&self.stages);
861
862 Ok(System {
863 buses: self.buses,
864 lines: self.lines,
865 hydros: self.hydros,
866 thermals: self.thermals,
867 pumping_stations: self.pumping_stations,
868 contracts: self.contracts,
869 non_controllable_sources: self.non_controllable_sources,
870 bus_index,
871 line_index,
872 hydro_index,
873 thermal_index,
874 pumping_station_index,
875 contract_index,
876 non_controllable_source_index,
877 cascade,
878 network,
879 stages: self.stages,
880 policy_graph: self.policy_graph,
881 stage_index,
882 penalties: self.penalties,
883 bounds: self.bounds,
884 resolved_generic_bounds: self.resolved_generic_bounds,
885 resolved_load_factors: self.resolved_load_factors,
886 resolved_exchange_factors: self.resolved_exchange_factors,
887 resolved_ncs_bounds: self.resolved_ncs_bounds,
888 resolved_ncs_factors: self.resolved_ncs_factors,
889 inflow_models: self.inflow_models,
890 load_models: self.load_models,
891 ncs_models: self.ncs_models,
892 correlation: self.correlation,
893 initial_conditions: self.initial_conditions,
894 generic_constraints: self.generic_constraints,
895 scenario_source: self.scenario_source,
896 })
897 }
898}
899
900trait HasId {
901 fn entity_id(&self) -> EntityId;
902}
903
904impl HasId for Bus {
905 fn entity_id(&self) -> EntityId {
906 self.id
907 }
908}
909impl HasId for Line {
910 fn entity_id(&self) -> EntityId {
911 self.id
912 }
913}
914impl HasId for Hydro {
915 fn entity_id(&self) -> EntityId {
916 self.id
917 }
918}
919impl HasId for Thermal {
920 fn entity_id(&self) -> EntityId {
921 self.id
922 }
923}
924impl HasId for PumpingStation {
925 fn entity_id(&self) -> EntityId {
926 self.id
927 }
928}
929impl HasId for EnergyContract {
930 fn entity_id(&self) -> EntityId {
931 self.id
932 }
933}
934impl HasId for NonControllableSource {
935 fn entity_id(&self) -> EntityId {
936 self.id
937 }
938}
939
940fn build_index<T: HasId>(entities: &[T]) -> HashMap<EntityId, usize> {
941 let mut index = HashMap::with_capacity(entities.len());
942 for (i, entity) in entities.iter().enumerate() {
943 index.insert(entity.entity_id(), i);
944 }
945 index
946}
947
948fn build_stage_index(stages: &[Stage]) -> HashMap<i32, usize> {
952 let mut index = HashMap::with_capacity(stages.len());
953 for (i, stage) in stages.iter().enumerate() {
954 index.insert(stage.id, i);
955 }
956 index
957}
958
959fn check_duplicates<T: HasId>(
960 entities: &[T],
961 entity_type: &'static str,
962 errors: &mut Vec<ValidationError>,
963) {
964 for window in entities.windows(2) {
965 if window[0].entity_id() == window[1].entity_id() {
966 errors.push(ValidationError::DuplicateId {
967 entity_type,
968 id: window[0].entity_id(),
969 });
970 }
971 }
972}
973
974#[allow(clippy::too_many_arguments)]
983fn validate_cross_references(
984 lines: &[Line],
985 hydros: &[Hydro],
986 thermals: &[Thermal],
987 pumping_stations: &[PumpingStation],
988 contracts: &[EnergyContract],
989 non_controllable_sources: &[NonControllableSource],
990 bus_index: &HashMap<EntityId, usize>,
991 hydro_index: &HashMap<EntityId, usize>,
992 errors: &mut Vec<ValidationError>,
993) {
994 validate_line_refs(lines, bus_index, errors);
995 validate_hydro_refs(hydros, bus_index, hydro_index, errors);
996 validate_thermal_refs(thermals, bus_index, errors);
997 validate_pumping_station_refs(pumping_stations, bus_index, hydro_index, errors);
998 validate_contract_refs(contracts, bus_index, errors);
999 validate_ncs_refs(non_controllable_sources, bus_index, errors);
1000}
1001
1002fn validate_line_refs(
1003 lines: &[Line],
1004 bus_index: &HashMap<EntityId, usize>,
1005 errors: &mut Vec<ValidationError>,
1006) {
1007 for line in lines {
1008 if !bus_index.contains_key(&line.source_bus_id) {
1009 errors.push(ValidationError::InvalidReference {
1010 source_entity_type: "Line",
1011 source_id: line.id,
1012 field_name: "source_bus_id",
1013 referenced_id: line.source_bus_id,
1014 expected_type: "Bus",
1015 });
1016 }
1017 if !bus_index.contains_key(&line.target_bus_id) {
1018 errors.push(ValidationError::InvalidReference {
1019 source_entity_type: "Line",
1020 source_id: line.id,
1021 field_name: "target_bus_id",
1022 referenced_id: line.target_bus_id,
1023 expected_type: "Bus",
1024 });
1025 }
1026 }
1027}
1028
1029fn validate_hydro_refs(
1030 hydros: &[Hydro],
1031 bus_index: &HashMap<EntityId, usize>,
1032 hydro_index: &HashMap<EntityId, usize>,
1033 errors: &mut Vec<ValidationError>,
1034) {
1035 for hydro in hydros {
1036 if !bus_index.contains_key(&hydro.bus_id) {
1037 errors.push(ValidationError::InvalidReference {
1038 source_entity_type: "Hydro",
1039 source_id: hydro.id,
1040 field_name: "bus_id",
1041 referenced_id: hydro.bus_id,
1042 expected_type: "Bus",
1043 });
1044 }
1045 if let Some(downstream_id) = hydro.downstream_id {
1046 if !hydro_index.contains_key(&downstream_id) {
1047 errors.push(ValidationError::InvalidReference {
1048 source_entity_type: "Hydro",
1049 source_id: hydro.id,
1050 field_name: "downstream_id",
1051 referenced_id: downstream_id,
1052 expected_type: "Hydro",
1053 });
1054 }
1055 }
1056 if let Some(ref diversion) = hydro.diversion {
1057 if !hydro_index.contains_key(&diversion.downstream_id) {
1058 errors.push(ValidationError::InvalidReference {
1059 source_entity_type: "Hydro",
1060 source_id: hydro.id,
1061 field_name: "diversion.downstream_id",
1062 referenced_id: diversion.downstream_id,
1063 expected_type: "Hydro",
1064 });
1065 }
1066 }
1067 }
1068}
1069
1070fn validate_thermal_refs(
1071 thermals: &[Thermal],
1072 bus_index: &HashMap<EntityId, usize>,
1073 errors: &mut Vec<ValidationError>,
1074) {
1075 for thermal in thermals {
1076 if !bus_index.contains_key(&thermal.bus_id) {
1077 errors.push(ValidationError::InvalidReference {
1078 source_entity_type: "Thermal",
1079 source_id: thermal.id,
1080 field_name: "bus_id",
1081 referenced_id: thermal.bus_id,
1082 expected_type: "Bus",
1083 });
1084 }
1085 }
1086}
1087
1088fn validate_pumping_station_refs(
1089 pumping_stations: &[PumpingStation],
1090 bus_index: &HashMap<EntityId, usize>,
1091 hydro_index: &HashMap<EntityId, usize>,
1092 errors: &mut Vec<ValidationError>,
1093) {
1094 for ps in pumping_stations {
1095 if !bus_index.contains_key(&ps.bus_id) {
1096 errors.push(ValidationError::InvalidReference {
1097 source_entity_type: "PumpingStation",
1098 source_id: ps.id,
1099 field_name: "bus_id",
1100 referenced_id: ps.bus_id,
1101 expected_type: "Bus",
1102 });
1103 }
1104 if !hydro_index.contains_key(&ps.source_hydro_id) {
1105 errors.push(ValidationError::InvalidReference {
1106 source_entity_type: "PumpingStation",
1107 source_id: ps.id,
1108 field_name: "source_hydro_id",
1109 referenced_id: ps.source_hydro_id,
1110 expected_type: "Hydro",
1111 });
1112 }
1113 if !hydro_index.contains_key(&ps.destination_hydro_id) {
1114 errors.push(ValidationError::InvalidReference {
1115 source_entity_type: "PumpingStation",
1116 source_id: ps.id,
1117 field_name: "destination_hydro_id",
1118 referenced_id: ps.destination_hydro_id,
1119 expected_type: "Hydro",
1120 });
1121 }
1122 }
1123}
1124
1125fn validate_contract_refs(
1126 contracts: &[EnergyContract],
1127 bus_index: &HashMap<EntityId, usize>,
1128 errors: &mut Vec<ValidationError>,
1129) {
1130 for contract in contracts {
1131 if !bus_index.contains_key(&contract.bus_id) {
1132 errors.push(ValidationError::InvalidReference {
1133 source_entity_type: "EnergyContract",
1134 source_id: contract.id,
1135 field_name: "bus_id",
1136 referenced_id: contract.bus_id,
1137 expected_type: "Bus",
1138 });
1139 }
1140 }
1141}
1142
1143fn validate_ncs_refs(
1144 non_controllable_sources: &[NonControllableSource],
1145 bus_index: &HashMap<EntityId, usize>,
1146 errors: &mut Vec<ValidationError>,
1147) {
1148 for ncs in non_controllable_sources {
1149 if !bus_index.contains_key(&ncs.bus_id) {
1150 errors.push(ValidationError::InvalidReference {
1151 source_entity_type: "NonControllableSource",
1152 source_id: ncs.id,
1153 field_name: "bus_id",
1154 referenced_id: ncs.bus_id,
1155 expected_type: "Bus",
1156 });
1157 }
1158 }
1159}
1160
1161fn validate_filling_configs(hydros: &[Hydro], errors: &mut Vec<ValidationError>) {
1169 for hydro in hydros {
1170 if let Some(filling) = &hydro.filling {
1171 if filling.filling_inflow_m3s.is_nan() || filling.filling_inflow_m3s <= 0.0 {
1172 errors.push(ValidationError::InvalidFillingConfig {
1173 hydro_id: hydro.id,
1174 reason: "filling_inflow_m3s must be positive".to_string(),
1175 });
1176 }
1177 if hydro.entry_stage_id.is_none() {
1178 errors.push(ValidationError::InvalidFillingConfig {
1179 hydro_id: hydro.id,
1180 reason: "filling requires entry_stage_id to be set".to_string(),
1181 });
1182 }
1183 }
1184 }
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189 use super::*;
1190 use crate::entities::{ContractType, HydroGenerationModel, HydroPenalties, ThermalCostSegment};
1191
1192 fn make_bus(id: i32) -> Bus {
1193 Bus {
1194 id: EntityId(id),
1195 name: format!("bus-{id}"),
1196 deficit_segments: vec![],
1197 excess_cost: 0.0,
1198 }
1199 }
1200
1201 fn make_line(id: i32, source_bus_id: i32, target_bus_id: i32) -> Line {
1202 crate::Line {
1203 id: EntityId(id),
1204 name: format!("line-{id}"),
1205 source_bus_id: EntityId(source_bus_id),
1206 target_bus_id: EntityId(target_bus_id),
1207 entry_stage_id: None,
1208 exit_stage_id: None,
1209 direct_capacity_mw: 100.0,
1210 reverse_capacity_mw: 100.0,
1211 losses_percent: 0.0,
1212 exchange_cost: 0.0,
1213 }
1214 }
1215
1216 fn make_hydro_on_bus(id: i32, bus_id: i32) -> Hydro {
1217 let zero_penalties = HydroPenalties {
1218 spillage_cost: 0.0,
1219 diversion_cost: 0.0,
1220 fpha_turbined_cost: 0.0,
1221 storage_violation_below_cost: 0.0,
1222 filling_target_violation_cost: 0.0,
1223 turbined_violation_below_cost: 0.0,
1224 outflow_violation_below_cost: 0.0,
1225 outflow_violation_above_cost: 0.0,
1226 generation_violation_below_cost: 0.0,
1227 evaporation_violation_cost: 0.0,
1228 water_withdrawal_violation_cost: 0.0,
1229 };
1230 Hydro {
1231 id: EntityId(id),
1232 name: format!("hydro-{id}"),
1233 bus_id: EntityId(bus_id),
1234 downstream_id: None,
1235 entry_stage_id: None,
1236 exit_stage_id: None,
1237 min_storage_hm3: 0.0,
1238 max_storage_hm3: 1.0,
1239 min_outflow_m3s: 0.0,
1240 max_outflow_m3s: None,
1241 generation_model: HydroGenerationModel::ConstantProductivity {
1242 productivity_mw_per_m3s: 1.0,
1243 },
1244 min_turbined_m3s: 0.0,
1245 max_turbined_m3s: 1.0,
1246 min_generation_mw: 0.0,
1247 max_generation_mw: 1.0,
1248 tailrace: None,
1249 hydraulic_losses: None,
1250 efficiency: None,
1251 evaporation_coefficients_mm: None,
1252 evaporation_reference_volumes_hm3: None,
1253 diversion: None,
1254 filling: None,
1255 penalties: zero_penalties,
1256 }
1257 }
1258
1259 fn make_hydro(id: i32) -> Hydro {
1261 make_hydro_on_bus(id, 0)
1262 }
1263
1264 fn make_thermal_on_bus(id: i32, bus_id: i32) -> Thermal {
1265 Thermal {
1266 id: EntityId(id),
1267 name: format!("thermal-{id}"),
1268 bus_id: EntityId(bus_id),
1269 entry_stage_id: None,
1270 exit_stage_id: None,
1271 cost_segments: vec![ThermalCostSegment {
1272 capacity_mw: 100.0,
1273 cost_per_mwh: 50.0,
1274 }],
1275 min_generation_mw: 0.0,
1276 max_generation_mw: 100.0,
1277 gnl_config: None,
1278 }
1279 }
1280
1281 fn make_thermal(id: i32) -> Thermal {
1283 make_thermal_on_bus(id, 0)
1284 }
1285
1286 fn make_pumping_station_full(
1287 id: i32,
1288 bus_id: i32,
1289 source_hydro_id: i32,
1290 destination_hydro_id: i32,
1291 ) -> PumpingStation {
1292 PumpingStation {
1293 id: EntityId(id),
1294 name: format!("ps-{id}"),
1295 bus_id: EntityId(bus_id),
1296 source_hydro_id: EntityId(source_hydro_id),
1297 destination_hydro_id: EntityId(destination_hydro_id),
1298 entry_stage_id: None,
1299 exit_stage_id: None,
1300 consumption_mw_per_m3s: 0.5,
1301 min_flow_m3s: 0.0,
1302 max_flow_m3s: 10.0,
1303 }
1304 }
1305
1306 fn make_pumping_station(id: i32) -> PumpingStation {
1307 make_pumping_station_full(id, 0, 0, 1)
1308 }
1309
1310 fn make_contract_on_bus(id: i32, bus_id: i32) -> EnergyContract {
1311 EnergyContract {
1312 id: EntityId(id),
1313 name: format!("contract-{id}"),
1314 bus_id: EntityId(bus_id),
1315 contract_type: ContractType::Import,
1316 entry_stage_id: None,
1317 exit_stage_id: None,
1318 price_per_mwh: 0.0,
1319 min_mw: 0.0,
1320 max_mw: 100.0,
1321 }
1322 }
1323
1324 fn make_contract(id: i32) -> EnergyContract {
1325 make_contract_on_bus(id, 0)
1326 }
1327
1328 fn make_ncs_on_bus(id: i32, bus_id: i32) -> NonControllableSource {
1329 NonControllableSource {
1330 id: EntityId(id),
1331 name: format!("ncs-{id}"),
1332 bus_id: EntityId(bus_id),
1333 entry_stage_id: None,
1334 exit_stage_id: None,
1335 max_generation_mw: 50.0,
1336 curtailment_cost: 0.0,
1337 }
1338 }
1339
1340 fn make_ncs(id: i32) -> NonControllableSource {
1341 make_ncs_on_bus(id, 0)
1342 }
1343
1344 #[test]
1345 fn test_empty_system() {
1346 let system = SystemBuilder::new().build().expect("empty system is valid");
1347 assert_eq!(system.n_buses(), 0);
1348 assert_eq!(system.n_lines(), 0);
1349 assert_eq!(system.n_hydros(), 0);
1350 assert_eq!(system.n_thermals(), 0);
1351 assert_eq!(system.n_pumping_stations(), 0);
1352 assert_eq!(system.n_contracts(), 0);
1353 assert_eq!(system.n_non_controllable_sources(), 0);
1354 assert!(system.buses().is_empty());
1355 assert!(system.cascade().is_empty());
1356 }
1357
1358 #[test]
1359 fn test_canonical_ordering() {
1360 let system = SystemBuilder::new()
1362 .buses(vec![make_bus(2), make_bus(1), make_bus(0)])
1363 .build()
1364 .expect("valid system");
1365
1366 assert_eq!(system.buses()[0].id, EntityId(0));
1367 assert_eq!(system.buses()[1].id, EntityId(1));
1368 assert_eq!(system.buses()[2].id, EntityId(2));
1369 }
1370
1371 #[test]
1372 fn test_lookup_by_id() {
1373 let system = SystemBuilder::new()
1375 .buses(vec![make_bus(0)])
1376 .hydros(vec![make_hydro(10), make_hydro(5), make_hydro(20)])
1377 .build()
1378 .expect("valid system");
1379
1380 assert_eq!(system.hydro(EntityId(5)).map(|h| h.id), Some(EntityId(5)));
1381 assert_eq!(system.hydro(EntityId(10)).map(|h| h.id), Some(EntityId(10)));
1382 assert_eq!(system.hydro(EntityId(20)).map(|h| h.id), Some(EntityId(20)));
1383 }
1384
1385 #[test]
1386 fn test_lookup_missing_id() {
1387 let system = SystemBuilder::new()
1389 .buses(vec![make_bus(0)])
1390 .hydros(vec![make_hydro(1), make_hydro(2)])
1391 .build()
1392 .expect("valid system");
1393
1394 assert!(system.hydro(EntityId(999)).is_none());
1395 }
1396
1397 #[test]
1398 fn test_count_queries() {
1399 let system = SystemBuilder::new()
1400 .buses(vec![make_bus(0), make_bus(1)])
1401 .lines(vec![make_line(0, 0, 1)])
1402 .hydros(vec![make_hydro(0), make_hydro(1), make_hydro(2)])
1403 .thermals(vec![make_thermal(0)])
1404 .pumping_stations(vec![make_pumping_station(0)])
1405 .contracts(vec![make_contract(0), make_contract(1)])
1406 .non_controllable_sources(vec![make_ncs(0)])
1407 .build()
1408 .expect("valid system");
1409
1410 assert_eq!(system.n_buses(), 2);
1411 assert_eq!(system.n_lines(), 1);
1412 assert_eq!(system.n_hydros(), 3);
1413 assert_eq!(system.n_thermals(), 1);
1414 assert_eq!(system.n_pumping_stations(), 1);
1415 assert_eq!(system.n_contracts(), 2);
1416 assert_eq!(system.n_non_controllable_sources(), 1);
1417 }
1418
1419 #[test]
1420 fn test_slice_accessors() {
1421 let system = SystemBuilder::new()
1422 .buses(vec![make_bus(0), make_bus(1), make_bus(2)])
1423 .build()
1424 .expect("valid system");
1425
1426 let buses = system.buses();
1427 assert_eq!(buses.len(), 3);
1428 assert_eq!(buses[0].id, EntityId(0));
1429 assert_eq!(buses[1].id, EntityId(1));
1430 assert_eq!(buses[2].id, EntityId(2));
1431 }
1432
1433 #[test]
1434 fn test_duplicate_id_error() {
1435 let result = SystemBuilder::new()
1437 .buses(vec![make_bus(0), make_bus(0)])
1438 .build();
1439
1440 assert!(result.is_err());
1441 let errors = result.unwrap_err();
1442 assert!(!errors.is_empty());
1443 assert!(errors.iter().any(|e| matches!(
1444 e,
1445 ValidationError::DuplicateId {
1446 entity_type: "Bus",
1447 id: EntityId(0),
1448 }
1449 )));
1450 }
1451
1452 #[test]
1453 fn test_multiple_duplicate_errors() {
1454 let result = SystemBuilder::new()
1456 .buses(vec![make_bus(0), make_bus(0)])
1457 .thermals(vec![make_thermal(5), make_thermal(5)])
1458 .build();
1459
1460 assert!(result.is_err());
1461 let errors = result.unwrap_err();
1462
1463 let has_bus_dup = errors.iter().any(|e| {
1464 matches!(
1465 e,
1466 ValidationError::DuplicateId {
1467 entity_type: "Bus",
1468 ..
1469 }
1470 )
1471 });
1472 let has_thermal_dup = errors.iter().any(|e| {
1473 matches!(
1474 e,
1475 ValidationError::DuplicateId {
1476 entity_type: "Thermal",
1477 ..
1478 }
1479 )
1480 });
1481 assert!(has_bus_dup, "expected Bus duplicate error");
1482 assert!(has_thermal_dup, "expected Thermal duplicate error");
1483 }
1484
1485 #[test]
1486 fn test_send_sync() {
1487 fn require_send_sync<T: Send + Sync>(_: T) {}
1488 let system = SystemBuilder::new().build().expect("valid system");
1489 require_send_sync(system);
1490 }
1491
1492 #[test]
1493 fn test_cascade_accessible() {
1494 let mut h0 = make_hydro_on_bus(0, 0);
1496 h0.downstream_id = Some(EntityId(1));
1497 let mut h1 = make_hydro_on_bus(1, 0);
1498 h1.downstream_id = Some(EntityId(2));
1499 let h2 = make_hydro_on_bus(2, 0);
1500
1501 let system = SystemBuilder::new()
1502 .buses(vec![make_bus(0)])
1503 .hydros(vec![h0, h1, h2])
1504 .build()
1505 .expect("valid system");
1506
1507 let order = system.cascade().topological_order();
1508 assert!(!order.is_empty(), "topological order must be non-empty");
1509 let pos_0 = order
1510 .iter()
1511 .position(|&id| id == EntityId(0))
1512 .expect("EntityId(0) must be in topological order");
1513 let pos_2 = order
1514 .iter()
1515 .position(|&id| id == EntityId(2))
1516 .expect("EntityId(2) must be in topological order");
1517 assert!(pos_0 < pos_2, "EntityId(0) must precede EntityId(2)");
1518 }
1519
1520 #[test]
1521 fn test_network_accessible() {
1522 let system = SystemBuilder::new()
1523 .buses(vec![make_bus(0), make_bus(1)])
1524 .lines(vec![make_line(0, 0, 1)])
1525 .build()
1526 .expect("valid system");
1527
1528 let connections = system.network().bus_lines(EntityId(0));
1529 assert!(!connections.is_empty(), "bus 0 must have connections");
1530 assert_eq!(connections[0].line_id, EntityId(0));
1531 }
1532
1533 #[test]
1534 fn test_all_entity_lookups() {
1535 let system = SystemBuilder::new()
1540 .buses(vec![make_bus(0), make_bus(1)])
1541 .lines(vec![make_line(2, 0, 1)])
1542 .hydros(vec![
1543 make_hydro_on_bus(0, 0),
1544 make_hydro_on_bus(1, 0),
1545 make_hydro_on_bus(3, 0),
1546 ])
1547 .thermals(vec![make_thermal(4)])
1548 .pumping_stations(vec![make_pumping_station(5)])
1549 .contracts(vec![make_contract(6)])
1550 .non_controllable_sources(vec![make_ncs(7)])
1551 .build()
1552 .expect("valid system");
1553
1554 assert!(system.bus(EntityId(1)).is_some());
1555 assert!(system.line(EntityId(2)).is_some());
1556 assert!(system.hydro(EntityId(3)).is_some());
1557 assert!(system.thermal(EntityId(4)).is_some());
1558 assert!(system.pumping_station(EntityId(5)).is_some());
1559 assert!(system.contract(EntityId(6)).is_some());
1560 assert!(system.non_controllable_source(EntityId(7)).is_some());
1561
1562 assert!(system.bus(EntityId(999)).is_none());
1563 assert!(system.line(EntityId(999)).is_none());
1564 assert!(system.hydro(EntityId(999)).is_none());
1565 assert!(system.thermal(EntityId(999)).is_none());
1566 assert!(system.pumping_station(EntityId(999)).is_none());
1567 assert!(system.contract(EntityId(999)).is_none());
1568 assert!(system.non_controllable_source(EntityId(999)).is_none());
1569 }
1570
1571 #[test]
1572 fn test_default_builder() {
1573 let system = SystemBuilder::default()
1574 .build()
1575 .expect("default builder produces valid empty system");
1576 assert_eq!(system.n_buses(), 0);
1577 }
1578
1579 #[test]
1582 fn test_invalid_bus_reference_hydro() {
1583 let hydro = make_hydro_on_bus(1, 99);
1585 let result = SystemBuilder::new().hydros(vec![hydro]).build();
1586
1587 assert!(result.is_err(), "expected Err for missing bus reference");
1588 let errors = result.unwrap_err();
1589 assert!(
1590 errors.iter().any(|e| matches!(
1591 e,
1592 ValidationError::InvalidReference {
1593 source_entity_type: "Hydro",
1594 source_id: EntityId(1),
1595 field_name: "bus_id",
1596 referenced_id: EntityId(99),
1597 expected_type: "Bus",
1598 }
1599 )),
1600 "expected InvalidReference for Hydro bus_id=99, got: {errors:?}"
1601 );
1602 }
1603
1604 #[test]
1605 fn test_invalid_downstream_reference() {
1606 let bus = make_bus(0);
1608 let mut hydro = make_hydro(1);
1609 hydro.downstream_id = Some(EntityId(50));
1610
1611 let result = SystemBuilder::new()
1612 .buses(vec![bus])
1613 .hydros(vec![hydro])
1614 .build();
1615
1616 assert!(
1617 result.is_err(),
1618 "expected Err for missing downstream reference"
1619 );
1620 let errors = result.unwrap_err();
1621 assert!(
1622 errors.iter().any(|e| matches!(
1623 e,
1624 ValidationError::InvalidReference {
1625 source_entity_type: "Hydro",
1626 source_id: EntityId(1),
1627 field_name: "downstream_id",
1628 referenced_id: EntityId(50),
1629 expected_type: "Hydro",
1630 }
1631 )),
1632 "expected InvalidReference for Hydro downstream_id=50, got: {errors:?}"
1633 );
1634 }
1635
1636 #[test]
1637 fn test_invalid_pumping_station_hydro_refs() {
1638 let bus = make_bus(0);
1640 let dest_hydro = make_hydro(1);
1641 let ps = make_pumping_station_full(10, 0, 77, 1);
1642
1643 let result = SystemBuilder::new()
1644 .buses(vec![bus])
1645 .hydros(vec![dest_hydro])
1646 .pumping_stations(vec![ps])
1647 .build();
1648
1649 assert!(
1650 result.is_err(),
1651 "expected Err for missing source_hydro_id reference"
1652 );
1653 let errors = result.unwrap_err();
1654 assert!(
1655 errors.iter().any(|e| matches!(
1656 e,
1657 ValidationError::InvalidReference {
1658 source_entity_type: "PumpingStation",
1659 source_id: EntityId(10),
1660 field_name: "source_hydro_id",
1661 referenced_id: EntityId(77),
1662 expected_type: "Hydro",
1663 }
1664 )),
1665 "expected InvalidReference for PumpingStation source_hydro_id=77, got: {errors:?}"
1666 );
1667 }
1668
1669 #[test]
1670 fn test_multiple_invalid_references_collected() {
1671 let line = make_line(1, 99, 0);
1674 let thermal = make_thermal_on_bus(2, 88);
1675
1676 let result = SystemBuilder::new()
1677 .buses(vec![make_bus(0)])
1678 .lines(vec![line])
1679 .thermals(vec![thermal])
1680 .build();
1681
1682 assert!(
1683 result.is_err(),
1684 "expected Err for multiple invalid references"
1685 );
1686 let errors = result.unwrap_err();
1687
1688 let has_line_error = errors.iter().any(|e| {
1689 matches!(
1690 e,
1691 ValidationError::InvalidReference {
1692 source_entity_type: "Line",
1693 field_name: "source_bus_id",
1694 referenced_id: EntityId(99),
1695 ..
1696 }
1697 )
1698 });
1699 let has_thermal_error = errors.iter().any(|e| {
1700 matches!(
1701 e,
1702 ValidationError::InvalidReference {
1703 source_entity_type: "Thermal",
1704 field_name: "bus_id",
1705 referenced_id: EntityId(88),
1706 ..
1707 }
1708 )
1709 });
1710
1711 assert!(
1712 has_line_error,
1713 "expected Line source_bus_id=99 error, got: {errors:?}"
1714 );
1715 assert!(
1716 has_thermal_error,
1717 "expected Thermal bus_id=88 error, got: {errors:?}"
1718 );
1719 assert!(
1720 errors.len() >= 2,
1721 "expected at least 2 errors, got {}: {errors:?}",
1722 errors.len()
1723 );
1724 }
1725
1726 #[test]
1727 fn test_valid_cross_references_pass() {
1728 let bus_0 = make_bus(0);
1730 let bus_1 = make_bus(1);
1731 let h0 = make_hydro_on_bus(0, 0);
1732 let h1 = make_hydro_on_bus(1, 1);
1733 let mut h2 = make_hydro_on_bus(2, 0);
1734 h2.downstream_id = Some(EntityId(1));
1735 let line = make_line(10, 0, 1);
1736 let thermal = make_thermal_on_bus(20, 0);
1737 let ps = make_pumping_station_full(30, 0, 0, 1);
1738 let contract = make_contract_on_bus(40, 1);
1739 let ncs = make_ncs_on_bus(50, 0);
1740
1741 let result = SystemBuilder::new()
1742 .buses(vec![bus_0, bus_1])
1743 .lines(vec![line])
1744 .hydros(vec![h0, h1, h2])
1745 .thermals(vec![thermal])
1746 .pumping_stations(vec![ps])
1747 .contracts(vec![contract])
1748 .non_controllable_sources(vec![ncs])
1749 .build();
1750
1751 assert!(
1752 result.is_ok(),
1753 "expected Ok for all valid cross-references, got: {:?}",
1754 result.unwrap_err()
1755 );
1756 let system = result.unwrap_or_else(|_| unreachable!());
1757 assert_eq!(system.n_buses(), 2);
1758 assert_eq!(system.n_hydros(), 3);
1759 assert_eq!(system.n_lines(), 1);
1760 assert_eq!(system.n_thermals(), 1);
1761 assert_eq!(system.n_pumping_stations(), 1);
1762 assert_eq!(system.n_contracts(), 1);
1763 assert_eq!(system.n_non_controllable_sources(), 1);
1764 }
1765
1766 #[test]
1769 fn test_cascade_cycle_detected() {
1770 let bus = make_bus(0);
1773 let mut h0 = make_hydro(0);
1774 h0.downstream_id = Some(EntityId(1));
1775 let mut h1 = make_hydro(1);
1776 h1.downstream_id = Some(EntityId(2));
1777 let mut h2 = make_hydro(2);
1778 h2.downstream_id = Some(EntityId(0));
1779
1780 let result = SystemBuilder::new()
1781 .buses(vec![bus])
1782 .hydros(vec![h0, h1, h2])
1783 .build();
1784
1785 assert!(result.is_err(), "expected Err for 3-node cycle");
1786 let errors = result.unwrap_err();
1787 let cycle_error = errors
1788 .iter()
1789 .find(|e| matches!(e, ValidationError::CascadeCycle { .. }));
1790 assert!(
1791 cycle_error.is_some(),
1792 "expected CascadeCycle error, got: {errors:?}"
1793 );
1794 let ValidationError::CascadeCycle { cycle_ids } = cycle_error.unwrap() else {
1795 unreachable!()
1796 };
1797 assert_eq!(
1798 cycle_ids,
1799 &[EntityId(0), EntityId(1), EntityId(2)],
1800 "cycle_ids must be sorted ascending, got: {cycle_ids:?}"
1801 );
1802 }
1803
1804 #[test]
1805 fn test_cascade_self_loop_detected() {
1806 let bus = make_bus(0);
1808 let mut h0 = make_hydro(0);
1809 h0.downstream_id = Some(EntityId(0));
1810
1811 let result = SystemBuilder::new()
1812 .buses(vec![bus])
1813 .hydros(vec![h0])
1814 .build();
1815
1816 assert!(result.is_err(), "expected Err for self-loop");
1817 let errors = result.unwrap_err();
1818 let has_cycle = errors
1819 .iter()
1820 .any(|e| matches!(e, ValidationError::CascadeCycle { cycle_ids } if cycle_ids.contains(&EntityId(0))));
1821 assert!(
1822 has_cycle,
1823 "expected CascadeCycle containing EntityId(0), got: {errors:?}"
1824 );
1825 }
1826
1827 #[test]
1828 fn test_valid_acyclic_cascade_passes() {
1829 let bus = make_bus(0);
1832 let mut h0 = make_hydro(0);
1833 h0.downstream_id = Some(EntityId(1));
1834 let mut h1 = make_hydro(1);
1835 h1.downstream_id = Some(EntityId(2));
1836 let h2 = make_hydro(2);
1837
1838 let result = SystemBuilder::new()
1839 .buses(vec![bus])
1840 .hydros(vec![h0, h1, h2])
1841 .build();
1842
1843 assert!(
1844 result.is_ok(),
1845 "expected Ok for acyclic cascade, got: {:?}",
1846 result.unwrap_err()
1847 );
1848 let system = result.unwrap_or_else(|_| unreachable!());
1849 assert_eq!(
1850 system.cascade().topological_order().len(),
1851 system.n_hydros(),
1852 "topological_order must contain all hydros"
1853 );
1854 }
1855
1856 #[test]
1859 fn test_filling_without_entry_stage() {
1860 use crate::entities::FillingConfig;
1862 let bus = make_bus(0);
1863 let mut hydro = make_hydro(1);
1864 hydro.entry_stage_id = None;
1865 hydro.filling = Some(FillingConfig {
1866 start_stage_id: 10,
1867 filling_inflow_m3s: 100.0,
1868 });
1869
1870 let result = SystemBuilder::new()
1871 .buses(vec![bus])
1872 .hydros(vec![hydro])
1873 .build();
1874
1875 assert!(
1876 result.is_err(),
1877 "expected Err for filling without entry_stage_id"
1878 );
1879 let errors = result.unwrap_err();
1880 let has_error = errors.iter().any(|e| match e {
1881 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
1882 *hydro_id == EntityId(1) && reason.contains("entry_stage_id")
1883 }
1884 _ => false,
1885 });
1886 assert!(
1887 has_error,
1888 "expected InvalidFillingConfig with entry_stage_id reason, got: {errors:?}"
1889 );
1890 }
1891
1892 #[test]
1893 fn test_filling_negative_inflow() {
1894 use crate::entities::FillingConfig;
1896 let bus = make_bus(0);
1897 let mut hydro = make_hydro(1);
1898 hydro.entry_stage_id = Some(10);
1899 hydro.filling = Some(FillingConfig {
1900 start_stage_id: 10,
1901 filling_inflow_m3s: -5.0,
1902 });
1903
1904 let result = SystemBuilder::new()
1905 .buses(vec![bus])
1906 .hydros(vec![hydro])
1907 .build();
1908
1909 assert!(
1910 result.is_err(),
1911 "expected Err for negative filling_inflow_m3s"
1912 );
1913 let errors = result.unwrap_err();
1914 let has_error = errors.iter().any(|e| match e {
1915 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
1916 *hydro_id == EntityId(1) && reason.contains("filling_inflow_m3s must be positive")
1917 }
1918 _ => false,
1919 });
1920 assert!(
1921 has_error,
1922 "expected InvalidFillingConfig with positive inflow reason, got: {errors:?}"
1923 );
1924 }
1925
1926 #[test]
1927 fn test_valid_filling_config_passes() {
1928 use crate::entities::FillingConfig;
1930 let bus = make_bus(0);
1931 let mut hydro = make_hydro(1);
1932 hydro.entry_stage_id = Some(10);
1933 hydro.filling = Some(FillingConfig {
1934 start_stage_id: 10,
1935 filling_inflow_m3s: 100.0,
1936 });
1937
1938 let result = SystemBuilder::new()
1939 .buses(vec![bus])
1940 .hydros(vec![hydro])
1941 .build();
1942
1943 assert!(
1944 result.is_ok(),
1945 "expected Ok for valid filling config, got: {:?}",
1946 result.unwrap_err()
1947 );
1948 }
1949
1950 #[test]
1951 fn test_cascade_cycle_and_invalid_filling_both_reported() {
1952 use crate::entities::FillingConfig;
1955 let bus = make_bus(0);
1956
1957 let mut h0 = make_hydro(0);
1959 h0.downstream_id = Some(EntityId(0));
1960
1961 let mut h1 = make_hydro(1);
1963 h1.entry_stage_id = None; h1.filling = Some(FillingConfig {
1965 start_stage_id: 5,
1966 filling_inflow_m3s: 50.0,
1967 });
1968
1969 let result = SystemBuilder::new()
1970 .buses(vec![bus])
1971 .hydros(vec![h0, h1])
1972 .build();
1973
1974 assert!(result.is_err(), "expected Err for cycle + invalid filling");
1975 let errors = result.unwrap_err();
1976 let has_cycle = errors
1977 .iter()
1978 .any(|e| matches!(e, ValidationError::CascadeCycle { .. }));
1979 let has_filling = errors
1980 .iter()
1981 .any(|e| matches!(e, ValidationError::InvalidFillingConfig { .. }));
1982 assert!(has_cycle, "expected CascadeCycle error, got: {errors:?}");
1983 assert!(
1984 has_filling,
1985 "expected InvalidFillingConfig error, got: {errors:?}"
1986 );
1987 }
1988
1989 #[cfg(feature = "serde")]
1990 #[test]
1991 fn test_system_serde_roundtrip() {
1992 let bus_a = make_bus(1);
1994 let bus_b = make_bus(2);
1995 let hydro = make_hydro_on_bus(10, 1);
1996 let thermal = make_thermal_on_bus(20, 2);
1997 let line = make_line(1, 1, 2);
1998
1999 let system = SystemBuilder::new()
2000 .buses(vec![bus_a, bus_b])
2001 .hydros(vec![hydro])
2002 .thermals(vec![thermal])
2003 .lines(vec![line])
2004 .build()
2005 .expect("valid system");
2006
2007 let json = serde_json::to_string(&system).unwrap();
2008
2009 let mut deserialized: System = serde_json::from_str(&json).unwrap();
2011 deserialized.rebuild_indices();
2012
2013 assert_eq!(system.buses(), deserialized.buses());
2015 assert_eq!(system.hydros(), deserialized.hydros());
2016 assert_eq!(system.thermals(), deserialized.thermals());
2017 assert_eq!(system.lines(), deserialized.lines());
2018
2019 assert_eq!(
2021 deserialized.bus(EntityId(1)).map(|b| b.id),
2022 Some(EntityId(1))
2023 );
2024 assert_eq!(
2025 deserialized.hydro(EntityId(10)).map(|h| h.id),
2026 Some(EntityId(10))
2027 );
2028 assert_eq!(
2029 deserialized.thermal(EntityId(20)).map(|t| t.id),
2030 Some(EntityId(20))
2031 );
2032 assert_eq!(
2033 deserialized.line(EntityId(1)).map(|l| l.id),
2034 Some(EntityId(1))
2035 );
2036 }
2037
2038 fn make_stage(id: i32) -> Stage {
2041 use crate::temporal::{
2042 Block, BlockMode, NoiseMethod, ScenarioSourceConfig, StageRiskConfig, StageStateConfig,
2043 };
2044 use chrono::NaiveDate;
2045 Stage {
2046 index: usize::try_from(id.max(0)).unwrap_or(0),
2047 id,
2048 start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
2049 end_date: NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
2050 season_id: Some(0),
2051 blocks: vec![Block {
2052 index: 0,
2053 name: "SINGLE".to_string(),
2054 duration_hours: 744.0,
2055 }],
2056 block_mode: BlockMode::Parallel,
2057 state_config: StageStateConfig {
2058 storage: true,
2059 inflow_lags: false,
2060 },
2061 risk_config: StageRiskConfig::Expectation,
2062 scenario_config: ScenarioSourceConfig {
2063 branching_factor: 50,
2064 noise_method: NoiseMethod::Saa,
2065 },
2066 }
2067 }
2068
2069 #[test]
2072 fn test_system_backward_compat() {
2073 let system = SystemBuilder::new().build().expect("empty system is valid");
2074 assert_eq!(system.n_buses(), 0);
2076 assert_eq!(system.n_hydros(), 0);
2077 assert_eq!(system.n_stages(), 0);
2079 assert!(system.stages().is_empty());
2080 assert!(system.initial_conditions().storage.is_empty());
2081 assert!(system.generic_constraints().is_empty());
2082 assert!(system.inflow_models().is_empty());
2083 assert!(system.load_models().is_empty());
2084 assert_eq!(system.penalties().n_stages(), 0);
2085 assert_eq!(system.bounds().n_stages(), 0);
2086 assert!(!system.resolved_generic_bounds().is_active(0, 0));
2088 assert!(
2089 system
2090 .resolved_generic_bounds()
2091 .bounds_for_stage(0, 0)
2092 .is_empty()
2093 );
2094 }
2095
2096 #[test]
2098 fn test_system_resolved_generic_bounds_accessor() {
2099 use crate::resolved::ResolvedGenericConstraintBounds;
2100 use std::collections::HashMap as StdHashMap;
2101
2102 let id_map: StdHashMap<i32, usize> = [(0, 0), (1, 1)].into_iter().collect();
2103 let rows = vec![(0i32, 0i32, None::<i32>, 100.0f64)];
2104 let table = ResolvedGenericConstraintBounds::new(&id_map, rows.into_iter());
2105
2106 let system = SystemBuilder::new()
2107 .resolved_generic_bounds(table)
2108 .build()
2109 .expect("valid system");
2110
2111 assert!(system.resolved_generic_bounds().is_active(0, 0));
2112 assert!(!system.resolved_generic_bounds().is_active(1, 0));
2113 let slice = system.resolved_generic_bounds().bounds_for_stage(0, 0);
2114 assert_eq!(slice.len(), 1);
2115 assert_eq!(slice[0], (None, 100.0));
2116 }
2117
2118 #[test]
2120 fn test_system_with_stages() {
2121 let s0 = make_stage(0);
2122 let s1 = make_stage(1);
2123
2124 let system = SystemBuilder::new()
2125 .stages(vec![s1.clone(), s0.clone()]) .build()
2127 .expect("valid system");
2128
2129 assert_eq!(system.n_stages(), 2);
2131 assert_eq!(system.stages()[0].id, 0);
2132 assert_eq!(system.stages()[1].id, 1);
2133
2134 let found = system.stage(0).expect("stage 0 must be found");
2136 assert_eq!(found.id, s0.id);
2137
2138 let found1 = system.stage(1).expect("stage 1 must be found");
2139 assert_eq!(found1.id, s1.id);
2140
2141 assert!(system.stage(99).is_none());
2143 }
2144
2145 #[test]
2147 fn test_system_stage_lookup_by_id() {
2148 let stages: Vec<Stage> = [0i32, 1, 2].iter().map(|&id| make_stage(id)).collect();
2149
2150 let system = SystemBuilder::new()
2151 .stages(stages)
2152 .build()
2153 .expect("valid system");
2154
2155 assert_eq!(system.stage(1).map(|s| s.id), Some(1));
2156 assert!(system.stage(99).is_none());
2157 }
2158
2159 #[test]
2161 fn test_system_with_initial_conditions() {
2162 let ic = InitialConditions {
2163 storage: vec![crate::HydroStorage {
2164 hydro_id: EntityId(0),
2165 value_hm3: 15_000.0,
2166 }],
2167 filling_storage: vec![],
2168 past_inflows: vec![],
2169 };
2170
2171 let system = SystemBuilder::new()
2172 .initial_conditions(ic)
2173 .build()
2174 .expect("valid system");
2175
2176 assert_eq!(system.initial_conditions().storage.len(), 1);
2177 assert_eq!(system.initial_conditions().storage[0].hydro_id, EntityId(0));
2178 assert!((system.initial_conditions().storage[0].value_hm3 - 15_000.0).abs() < f64::EPSILON);
2179 }
2180
2181 #[cfg(feature = "serde")]
2184 #[test]
2185 fn test_system_serde_roundtrip_with_stages() {
2186 use crate::temporal::PolicyGraphType;
2187
2188 let stages = vec![make_stage(0), make_stage(1)];
2189 let policy_graph = PolicyGraph {
2190 graph_type: PolicyGraphType::FiniteHorizon,
2191 annual_discount_rate: 0.0,
2192 transitions: vec![],
2193 season_map: None,
2194 };
2195
2196 let system = SystemBuilder::new()
2197 .stages(stages)
2198 .policy_graph(policy_graph)
2199 .build()
2200 .expect("valid system");
2201
2202 let json = serde_json::to_string(&system).unwrap();
2203 let mut deserialized: System = serde_json::from_str(&json).unwrap();
2204
2205 deserialized.rebuild_indices();
2207
2208 assert_eq!(system.n_stages(), deserialized.n_stages());
2210 assert_eq!(system.stages()[0].id, deserialized.stages()[0].id);
2211 assert_eq!(system.stages()[1].id, deserialized.stages()[1].id);
2212
2213 assert_eq!(deserialized.stage(0).map(|s| s.id), Some(0));
2215 assert_eq!(deserialized.stage(1).map(|s| s.id), Some(1));
2216 assert!(deserialized.stage(99).is_none());
2217
2218 assert_eq!(
2220 deserialized.policy_graph().graph_type,
2221 system.policy_graph().graph_type
2222 );
2223 }
2224}