1use std::collections::{HashMap, HashSet};
12
13use crate::{
14 Bus, CascadeTopology, CorrelationModel, EnergyContract, EntityId, GenericConstraint, Hydro,
15 InflowModel, InitialConditions, Line, LoadModel, NetworkTopology, NonControllableSource,
16 PolicyGraph, PumpingStation, ResolvedBounds, ResolvedPenalties, ScenarioSource, Stage, Thermal,
17 ValidationError,
18};
19
20#[derive(Debug, PartialEq)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
51pub struct System {
52 buses: Vec<Bus>,
54 lines: Vec<Line>,
55 hydros: Vec<Hydro>,
56 thermals: Vec<Thermal>,
57 pumping_stations: Vec<PumpingStation>,
58 contracts: Vec<EnergyContract>,
59 non_controllable_sources: Vec<NonControllableSource>,
60
61 #[cfg_attr(feature = "serde", serde(skip))]
65 bus_index: HashMap<EntityId, usize>,
66 #[cfg_attr(feature = "serde", serde(skip))]
67 line_index: HashMap<EntityId, usize>,
68 #[cfg_attr(feature = "serde", serde(skip))]
69 hydro_index: HashMap<EntityId, usize>,
70 #[cfg_attr(feature = "serde", serde(skip))]
71 thermal_index: HashMap<EntityId, usize>,
72 #[cfg_attr(feature = "serde", serde(skip))]
73 pumping_station_index: HashMap<EntityId, usize>,
74 #[cfg_attr(feature = "serde", serde(skip))]
75 contract_index: HashMap<EntityId, usize>,
76 #[cfg_attr(feature = "serde", serde(skip))]
77 non_controllable_source_index: HashMap<EntityId, usize>,
78
79 cascade: CascadeTopology,
82 network: NetworkTopology,
84
85 stages: Vec<Stage>,
88 policy_graph: PolicyGraph,
90
91 #[cfg_attr(feature = "serde", serde(skip))]
95 stage_index: HashMap<i32, usize>,
96
97 penalties: ResolvedPenalties,
100 bounds: ResolvedBounds,
102
103 inflow_models: Vec<InflowModel>,
106 load_models: Vec<LoadModel>,
108 correlation: CorrelationModel,
110
111 initial_conditions: InitialConditions,
114 generic_constraints: Vec<GenericConstraint>,
116 scenario_source: ScenarioSource,
118}
119
120const _: () = {
122 const fn assert_send_sync<T: Send + Sync>() {}
123 const fn check() {
124 assert_send_sync::<System>();
125 }
126 let _ = check;
127};
128
129impl System {
130 #[must_use]
132 pub fn buses(&self) -> &[Bus] {
133 &self.buses
134 }
135
136 #[must_use]
138 pub fn lines(&self) -> &[Line] {
139 &self.lines
140 }
141
142 #[must_use]
144 pub fn hydros(&self) -> &[Hydro] {
145 &self.hydros
146 }
147
148 #[must_use]
150 pub fn thermals(&self) -> &[Thermal] {
151 &self.thermals
152 }
153
154 #[must_use]
156 pub fn pumping_stations(&self) -> &[PumpingStation] {
157 &self.pumping_stations
158 }
159
160 #[must_use]
162 pub fn contracts(&self) -> &[EnergyContract] {
163 &self.contracts
164 }
165
166 #[must_use]
168 pub fn non_controllable_sources(&self) -> &[NonControllableSource] {
169 &self.non_controllable_sources
170 }
171
172 #[must_use]
174 pub fn n_buses(&self) -> usize {
175 self.buses.len()
176 }
177
178 #[must_use]
180 pub fn n_lines(&self) -> usize {
181 self.lines.len()
182 }
183
184 #[must_use]
186 pub fn n_hydros(&self) -> usize {
187 self.hydros.len()
188 }
189
190 #[must_use]
192 pub fn n_thermals(&self) -> usize {
193 self.thermals.len()
194 }
195
196 #[must_use]
198 pub fn n_pumping_stations(&self) -> usize {
199 self.pumping_stations.len()
200 }
201
202 #[must_use]
204 pub fn n_contracts(&self) -> usize {
205 self.contracts.len()
206 }
207
208 #[must_use]
210 pub fn n_non_controllable_sources(&self) -> usize {
211 self.non_controllable_sources.len()
212 }
213
214 #[must_use]
216 pub fn bus(&self, id: EntityId) -> Option<&Bus> {
217 self.bus_index.get(&id).map(|&i| &self.buses[i])
218 }
219
220 #[must_use]
222 pub fn line(&self, id: EntityId) -> Option<&Line> {
223 self.line_index.get(&id).map(|&i| &self.lines[i])
224 }
225
226 #[must_use]
228 pub fn hydro(&self, id: EntityId) -> Option<&Hydro> {
229 self.hydro_index.get(&id).map(|&i| &self.hydros[i])
230 }
231
232 #[must_use]
234 pub fn thermal(&self, id: EntityId) -> Option<&Thermal> {
235 self.thermal_index.get(&id).map(|&i| &self.thermals[i])
236 }
237
238 #[must_use]
240 pub fn pumping_station(&self, id: EntityId) -> Option<&PumpingStation> {
241 self.pumping_station_index
242 .get(&id)
243 .map(|&i| &self.pumping_stations[i])
244 }
245
246 #[must_use]
248 pub fn contract(&self, id: EntityId) -> Option<&EnergyContract> {
249 self.contract_index.get(&id).map(|&i| &self.contracts[i])
250 }
251
252 #[must_use]
254 pub fn non_controllable_source(&self, id: EntityId) -> Option<&NonControllableSource> {
255 self.non_controllable_source_index
256 .get(&id)
257 .map(|&i| &self.non_controllable_sources[i])
258 }
259
260 #[must_use]
262 pub fn cascade(&self) -> &CascadeTopology {
263 &self.cascade
264 }
265
266 #[must_use]
268 pub fn network(&self) -> &NetworkTopology {
269 &self.network
270 }
271
272 #[must_use]
274 pub fn stages(&self) -> &[Stage] {
275 &self.stages
276 }
277
278 #[must_use]
280 pub fn n_stages(&self) -> usize {
281 self.stages.len()
282 }
283
284 #[must_use]
289 pub fn stage(&self, id: i32) -> Option<&Stage> {
290 self.stage_index.get(&id).map(|&i| &self.stages[i])
291 }
292
293 #[must_use]
295 pub fn policy_graph(&self) -> &PolicyGraph {
296 &self.policy_graph
297 }
298
299 #[must_use]
301 pub fn penalties(&self) -> &ResolvedPenalties {
302 &self.penalties
303 }
304
305 #[must_use]
307 pub fn bounds(&self) -> &ResolvedBounds {
308 &self.bounds
309 }
310
311 #[must_use]
313 pub fn inflow_models(&self) -> &[InflowModel] {
314 &self.inflow_models
315 }
316
317 #[must_use]
319 pub fn load_models(&self) -> &[LoadModel] {
320 &self.load_models
321 }
322
323 #[must_use]
325 pub fn correlation(&self) -> &CorrelationModel {
326 &self.correlation
327 }
328
329 #[must_use]
331 pub fn initial_conditions(&self) -> &InitialConditions {
332 &self.initial_conditions
333 }
334
335 #[must_use]
337 pub fn generic_constraints(&self) -> &[GenericConstraint] {
338 &self.generic_constraints
339 }
340
341 #[must_use]
343 pub fn scenario_source(&self) -> &ScenarioSource {
344 &self.scenario_source
345 }
346
347 #[must_use]
374 pub fn with_scenario_models(
375 mut self,
376 inflow_models: Vec<InflowModel>,
377 correlation: CorrelationModel,
378 ) -> Self {
379 self.inflow_models = inflow_models;
380 self.correlation = correlation;
381 self
382 }
383
384 pub fn rebuild_indices(&mut self) {
417 self.bus_index = build_index(&self.buses);
418 self.line_index = build_index(&self.lines);
419 self.hydro_index = build_index(&self.hydros);
420 self.thermal_index = build_index(&self.thermals);
421 self.pumping_station_index = build_index(&self.pumping_stations);
422 self.contract_index = build_index(&self.contracts);
423 self.non_controllable_source_index = build_index(&self.non_controllable_sources);
424 self.stage_index = build_stage_index(&self.stages);
425 }
426}
427
428pub struct SystemBuilder {
452 buses: Vec<Bus>,
453 lines: Vec<Line>,
454 hydros: Vec<Hydro>,
455 thermals: Vec<Thermal>,
456 pumping_stations: Vec<PumpingStation>,
457 contracts: Vec<EnergyContract>,
458 non_controllable_sources: Vec<NonControllableSource>,
459 stages: Vec<Stage>,
461 policy_graph: PolicyGraph,
462 penalties: ResolvedPenalties,
463 bounds: ResolvedBounds,
464 inflow_models: Vec<InflowModel>,
465 load_models: Vec<LoadModel>,
466 correlation: CorrelationModel,
467 initial_conditions: InitialConditions,
468 generic_constraints: Vec<GenericConstraint>,
469 scenario_source: ScenarioSource,
470}
471
472impl Default for SystemBuilder {
473 fn default() -> Self {
474 Self::new()
475 }
476}
477
478impl SystemBuilder {
479 #[must_use]
484 pub fn new() -> Self {
485 Self {
486 buses: Vec::new(),
487 lines: Vec::new(),
488 hydros: Vec::new(),
489 thermals: Vec::new(),
490 pumping_stations: Vec::new(),
491 contracts: Vec::new(),
492 non_controllable_sources: Vec::new(),
493 stages: Vec::new(),
494 policy_graph: PolicyGraph::default(),
495 penalties: ResolvedPenalties::empty(),
496 bounds: ResolvedBounds::empty(),
497 inflow_models: Vec::new(),
498 load_models: Vec::new(),
499 correlation: CorrelationModel::default(),
500 initial_conditions: InitialConditions::default(),
501 generic_constraints: Vec::new(),
502 scenario_source: ScenarioSource::default(),
503 }
504 }
505
506 #[must_use]
508 pub fn buses(mut self, buses: Vec<Bus>) -> Self {
509 self.buses = buses;
510 self
511 }
512
513 #[must_use]
515 pub fn lines(mut self, lines: Vec<Line>) -> Self {
516 self.lines = lines;
517 self
518 }
519
520 #[must_use]
522 pub fn hydros(mut self, hydros: Vec<Hydro>) -> Self {
523 self.hydros = hydros;
524 self
525 }
526
527 #[must_use]
529 pub fn thermals(mut self, thermals: Vec<Thermal>) -> Self {
530 self.thermals = thermals;
531 self
532 }
533
534 #[must_use]
536 pub fn pumping_stations(mut self, stations: Vec<PumpingStation>) -> Self {
537 self.pumping_stations = stations;
538 self
539 }
540
541 #[must_use]
543 pub fn contracts(mut self, contracts: Vec<EnergyContract>) -> Self {
544 self.contracts = contracts;
545 self
546 }
547
548 #[must_use]
550 pub fn non_controllable_sources(mut self, sources: Vec<NonControllableSource>) -> Self {
551 self.non_controllable_sources = sources;
552 self
553 }
554
555 #[must_use]
559 pub fn stages(mut self, stages: Vec<Stage>) -> Self {
560 self.stages = stages;
561 self
562 }
563
564 #[must_use]
566 pub fn policy_graph(mut self, policy_graph: PolicyGraph) -> Self {
567 self.policy_graph = policy_graph;
568 self
569 }
570
571 #[must_use]
575 pub fn penalties(mut self, penalties: ResolvedPenalties) -> Self {
576 self.penalties = penalties;
577 self
578 }
579
580 #[must_use]
584 pub fn bounds(mut self, bounds: ResolvedBounds) -> Self {
585 self.bounds = bounds;
586 self
587 }
588
589 #[must_use]
591 pub fn inflow_models(mut self, inflow_models: Vec<InflowModel>) -> Self {
592 self.inflow_models = inflow_models;
593 self
594 }
595
596 #[must_use]
598 pub fn load_models(mut self, load_models: Vec<LoadModel>) -> Self {
599 self.load_models = load_models;
600 self
601 }
602
603 #[must_use]
605 pub fn correlation(mut self, correlation: CorrelationModel) -> Self {
606 self.correlation = correlation;
607 self
608 }
609
610 #[must_use]
612 pub fn initial_conditions(mut self, initial_conditions: InitialConditions) -> Self {
613 self.initial_conditions = initial_conditions;
614 self
615 }
616
617 #[must_use]
621 pub fn generic_constraints(mut self, generic_constraints: Vec<GenericConstraint>) -> Self {
622 self.generic_constraints = generic_constraints;
623 self
624 }
625
626 #[must_use]
628 pub fn scenario_source(mut self, scenario_source: ScenarioSource) -> Self {
629 self.scenario_source = scenario_source;
630 self
631 }
632
633 pub fn build(mut self) -> Result<System, Vec<ValidationError>> {
658 self.buses.sort_by_key(|e| e.id.0);
659 self.lines.sort_by_key(|e| e.id.0);
660 self.hydros.sort_by_key(|e| e.id.0);
661 self.thermals.sort_by_key(|e| e.id.0);
662 self.pumping_stations.sort_by_key(|e| e.id.0);
663 self.contracts.sort_by_key(|e| e.id.0);
664 self.non_controllable_sources.sort_by_key(|e| e.id.0);
665 self.stages.sort_by_key(|s| s.id);
666 self.generic_constraints.sort_by_key(|c| c.id.0);
667
668 let mut errors: Vec<ValidationError> = Vec::new();
669 check_duplicates(&self.buses, "Bus", &mut errors);
670 check_duplicates(&self.lines, "Line", &mut errors);
671 check_duplicates(&self.hydros, "Hydro", &mut errors);
672 check_duplicates(&self.thermals, "Thermal", &mut errors);
673 check_duplicates(&self.pumping_stations, "PumpingStation", &mut errors);
674 check_duplicates(&self.contracts, "EnergyContract", &mut errors);
675 check_duplicates(
676 &self.non_controllable_sources,
677 "NonControllableSource",
678 &mut errors,
679 );
680
681 if !errors.is_empty() {
682 return Err(errors);
683 }
684
685 let bus_index = build_index(&self.buses);
686 let line_index = build_index(&self.lines);
687 let hydro_index = build_index(&self.hydros);
688 let thermal_index = build_index(&self.thermals);
689 let pumping_station_index = build_index(&self.pumping_stations);
690 let contract_index = build_index(&self.contracts);
691 let non_controllable_source_index = build_index(&self.non_controllable_sources);
692
693 validate_cross_references(
694 &self.lines,
695 &self.hydros,
696 &self.thermals,
697 &self.pumping_stations,
698 &self.contracts,
699 &self.non_controllable_sources,
700 &bus_index,
701 &hydro_index,
702 &mut errors,
703 );
704
705 if !errors.is_empty() {
706 return Err(errors);
707 }
708
709 let cascade = CascadeTopology::build(&self.hydros);
710
711 if cascade.topological_order().len() < self.hydros.len() {
712 let in_topo: HashSet<EntityId> = cascade.topological_order().iter().copied().collect();
713 let mut cycle_ids: Vec<EntityId> = self
714 .hydros
715 .iter()
716 .map(|h| h.id)
717 .filter(|id| !in_topo.contains(id))
718 .collect();
719 cycle_ids.sort_by_key(|id| id.0);
720 errors.push(ValidationError::CascadeCycle { cycle_ids });
721 }
722
723 validate_filling_configs(&self.hydros, &mut errors);
724
725 if !errors.is_empty() {
726 return Err(errors);
727 }
728
729 let network = NetworkTopology::build(
730 &self.buses,
731 &self.lines,
732 &self.hydros,
733 &self.thermals,
734 &self.non_controllable_sources,
735 &self.contracts,
736 &self.pumping_stations,
737 );
738
739 let stage_index = build_stage_index(&self.stages);
740
741 Ok(System {
742 buses: self.buses,
743 lines: self.lines,
744 hydros: self.hydros,
745 thermals: self.thermals,
746 pumping_stations: self.pumping_stations,
747 contracts: self.contracts,
748 non_controllable_sources: self.non_controllable_sources,
749 bus_index,
750 line_index,
751 hydro_index,
752 thermal_index,
753 pumping_station_index,
754 contract_index,
755 non_controllable_source_index,
756 cascade,
757 network,
758 stages: self.stages,
759 policy_graph: self.policy_graph,
760 stage_index,
761 penalties: self.penalties,
762 bounds: self.bounds,
763 inflow_models: self.inflow_models,
764 load_models: self.load_models,
765 correlation: self.correlation,
766 initial_conditions: self.initial_conditions,
767 generic_constraints: self.generic_constraints,
768 scenario_source: self.scenario_source,
769 })
770 }
771}
772
773trait HasId {
774 fn entity_id(&self) -> EntityId;
775}
776
777impl HasId for Bus {
778 fn entity_id(&self) -> EntityId {
779 self.id
780 }
781}
782impl HasId for Line {
783 fn entity_id(&self) -> EntityId {
784 self.id
785 }
786}
787impl HasId for Hydro {
788 fn entity_id(&self) -> EntityId {
789 self.id
790 }
791}
792impl HasId for Thermal {
793 fn entity_id(&self) -> EntityId {
794 self.id
795 }
796}
797impl HasId for PumpingStation {
798 fn entity_id(&self) -> EntityId {
799 self.id
800 }
801}
802impl HasId for EnergyContract {
803 fn entity_id(&self) -> EntityId {
804 self.id
805 }
806}
807impl HasId for NonControllableSource {
808 fn entity_id(&self) -> EntityId {
809 self.id
810 }
811}
812
813fn build_index<T: HasId>(entities: &[T]) -> HashMap<EntityId, usize> {
814 let mut index = HashMap::with_capacity(entities.len());
815 for (i, entity) in entities.iter().enumerate() {
816 index.insert(entity.entity_id(), i);
817 }
818 index
819}
820
821fn build_stage_index(stages: &[Stage]) -> HashMap<i32, usize> {
825 let mut index = HashMap::with_capacity(stages.len());
826 for (i, stage) in stages.iter().enumerate() {
827 index.insert(stage.id, i);
828 }
829 index
830}
831
832fn check_duplicates<T: HasId>(
833 entities: &[T],
834 entity_type: &'static str,
835 errors: &mut Vec<ValidationError>,
836) {
837 for window in entities.windows(2) {
838 if window[0].entity_id() == window[1].entity_id() {
839 errors.push(ValidationError::DuplicateId {
840 entity_type,
841 id: window[0].entity_id(),
842 });
843 }
844 }
845}
846
847#[allow(clippy::too_many_arguments)]
856fn validate_cross_references(
857 lines: &[Line],
858 hydros: &[Hydro],
859 thermals: &[Thermal],
860 pumping_stations: &[PumpingStation],
861 contracts: &[EnergyContract],
862 non_controllable_sources: &[NonControllableSource],
863 bus_index: &HashMap<EntityId, usize>,
864 hydro_index: &HashMap<EntityId, usize>,
865 errors: &mut Vec<ValidationError>,
866) {
867 validate_line_refs(lines, bus_index, errors);
868 validate_hydro_refs(hydros, bus_index, hydro_index, errors);
869 validate_thermal_refs(thermals, bus_index, errors);
870 validate_pumping_station_refs(pumping_stations, bus_index, hydro_index, errors);
871 validate_contract_refs(contracts, bus_index, errors);
872 validate_ncs_refs(non_controllable_sources, bus_index, errors);
873}
874
875fn validate_line_refs(
876 lines: &[Line],
877 bus_index: &HashMap<EntityId, usize>,
878 errors: &mut Vec<ValidationError>,
879) {
880 for line in lines {
881 if !bus_index.contains_key(&line.source_bus_id) {
882 errors.push(ValidationError::InvalidReference {
883 source_entity_type: "Line",
884 source_id: line.id,
885 field_name: "source_bus_id",
886 referenced_id: line.source_bus_id,
887 expected_type: "Bus",
888 });
889 }
890 if !bus_index.contains_key(&line.target_bus_id) {
891 errors.push(ValidationError::InvalidReference {
892 source_entity_type: "Line",
893 source_id: line.id,
894 field_name: "target_bus_id",
895 referenced_id: line.target_bus_id,
896 expected_type: "Bus",
897 });
898 }
899 }
900}
901
902fn validate_hydro_refs(
903 hydros: &[Hydro],
904 bus_index: &HashMap<EntityId, usize>,
905 hydro_index: &HashMap<EntityId, usize>,
906 errors: &mut Vec<ValidationError>,
907) {
908 for hydro in hydros {
909 if !bus_index.contains_key(&hydro.bus_id) {
910 errors.push(ValidationError::InvalidReference {
911 source_entity_type: "Hydro",
912 source_id: hydro.id,
913 field_name: "bus_id",
914 referenced_id: hydro.bus_id,
915 expected_type: "Bus",
916 });
917 }
918 if let Some(downstream_id) = hydro.downstream_id {
919 if !hydro_index.contains_key(&downstream_id) {
920 errors.push(ValidationError::InvalidReference {
921 source_entity_type: "Hydro",
922 source_id: hydro.id,
923 field_name: "downstream_id",
924 referenced_id: downstream_id,
925 expected_type: "Hydro",
926 });
927 }
928 }
929 if let Some(ref diversion) = hydro.diversion {
930 if !hydro_index.contains_key(&diversion.downstream_id) {
931 errors.push(ValidationError::InvalidReference {
932 source_entity_type: "Hydro",
933 source_id: hydro.id,
934 field_name: "diversion.downstream_id",
935 referenced_id: diversion.downstream_id,
936 expected_type: "Hydro",
937 });
938 }
939 }
940 }
941}
942
943fn validate_thermal_refs(
944 thermals: &[Thermal],
945 bus_index: &HashMap<EntityId, usize>,
946 errors: &mut Vec<ValidationError>,
947) {
948 for thermal in thermals {
949 if !bus_index.contains_key(&thermal.bus_id) {
950 errors.push(ValidationError::InvalidReference {
951 source_entity_type: "Thermal",
952 source_id: thermal.id,
953 field_name: "bus_id",
954 referenced_id: thermal.bus_id,
955 expected_type: "Bus",
956 });
957 }
958 }
959}
960
961fn validate_pumping_station_refs(
962 pumping_stations: &[PumpingStation],
963 bus_index: &HashMap<EntityId, usize>,
964 hydro_index: &HashMap<EntityId, usize>,
965 errors: &mut Vec<ValidationError>,
966) {
967 for ps in pumping_stations {
968 if !bus_index.contains_key(&ps.bus_id) {
969 errors.push(ValidationError::InvalidReference {
970 source_entity_type: "PumpingStation",
971 source_id: ps.id,
972 field_name: "bus_id",
973 referenced_id: ps.bus_id,
974 expected_type: "Bus",
975 });
976 }
977 if !hydro_index.contains_key(&ps.source_hydro_id) {
978 errors.push(ValidationError::InvalidReference {
979 source_entity_type: "PumpingStation",
980 source_id: ps.id,
981 field_name: "source_hydro_id",
982 referenced_id: ps.source_hydro_id,
983 expected_type: "Hydro",
984 });
985 }
986 if !hydro_index.contains_key(&ps.destination_hydro_id) {
987 errors.push(ValidationError::InvalidReference {
988 source_entity_type: "PumpingStation",
989 source_id: ps.id,
990 field_name: "destination_hydro_id",
991 referenced_id: ps.destination_hydro_id,
992 expected_type: "Hydro",
993 });
994 }
995 }
996}
997
998fn validate_contract_refs(
999 contracts: &[EnergyContract],
1000 bus_index: &HashMap<EntityId, usize>,
1001 errors: &mut Vec<ValidationError>,
1002) {
1003 for contract in contracts {
1004 if !bus_index.contains_key(&contract.bus_id) {
1005 errors.push(ValidationError::InvalidReference {
1006 source_entity_type: "EnergyContract",
1007 source_id: contract.id,
1008 field_name: "bus_id",
1009 referenced_id: contract.bus_id,
1010 expected_type: "Bus",
1011 });
1012 }
1013 }
1014}
1015
1016fn validate_ncs_refs(
1017 non_controllable_sources: &[NonControllableSource],
1018 bus_index: &HashMap<EntityId, usize>,
1019 errors: &mut Vec<ValidationError>,
1020) {
1021 for ncs in non_controllable_sources {
1022 if !bus_index.contains_key(&ncs.bus_id) {
1023 errors.push(ValidationError::InvalidReference {
1024 source_entity_type: "NonControllableSource",
1025 source_id: ncs.id,
1026 field_name: "bus_id",
1027 referenced_id: ncs.bus_id,
1028 expected_type: "Bus",
1029 });
1030 }
1031 }
1032}
1033
1034fn validate_filling_configs(hydros: &[Hydro], errors: &mut Vec<ValidationError>) {
1042 for hydro in hydros {
1043 if let Some(filling) = &hydro.filling {
1044 if filling.filling_inflow_m3s.is_nan() || filling.filling_inflow_m3s <= 0.0 {
1045 errors.push(ValidationError::InvalidFillingConfig {
1046 hydro_id: hydro.id,
1047 reason: "filling_inflow_m3s must be positive".to_string(),
1048 });
1049 }
1050 if hydro.entry_stage_id.is_none() {
1051 errors.push(ValidationError::InvalidFillingConfig {
1052 hydro_id: hydro.id,
1053 reason: "filling requires entry_stage_id to be set".to_string(),
1054 });
1055 }
1056 }
1057 }
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062 use super::*;
1063 use crate::entities::{ContractType, HydroGenerationModel, HydroPenalties, ThermalCostSegment};
1064
1065 fn make_bus(id: i32) -> Bus {
1066 Bus {
1067 id: EntityId(id),
1068 name: format!("bus-{id}"),
1069 deficit_segments: vec![],
1070 excess_cost: 0.0,
1071 }
1072 }
1073
1074 fn make_line(id: i32, source_bus_id: i32, target_bus_id: i32) -> Line {
1075 crate::Line {
1076 id: EntityId(id),
1077 name: format!("line-{id}"),
1078 source_bus_id: EntityId(source_bus_id),
1079 target_bus_id: EntityId(target_bus_id),
1080 entry_stage_id: None,
1081 exit_stage_id: None,
1082 direct_capacity_mw: 100.0,
1083 reverse_capacity_mw: 100.0,
1084 losses_percent: 0.0,
1085 exchange_cost: 0.0,
1086 }
1087 }
1088
1089 fn make_hydro_on_bus(id: i32, bus_id: i32) -> Hydro {
1090 let zero_penalties = HydroPenalties {
1091 spillage_cost: 0.0,
1092 diversion_cost: 0.0,
1093 fpha_turbined_cost: 0.0,
1094 storage_violation_below_cost: 0.0,
1095 filling_target_violation_cost: 0.0,
1096 turbined_violation_below_cost: 0.0,
1097 outflow_violation_below_cost: 0.0,
1098 outflow_violation_above_cost: 0.0,
1099 generation_violation_below_cost: 0.0,
1100 evaporation_violation_cost: 0.0,
1101 water_withdrawal_violation_cost: 0.0,
1102 };
1103 Hydro {
1104 id: EntityId(id),
1105 name: format!("hydro-{id}"),
1106 bus_id: EntityId(bus_id),
1107 downstream_id: None,
1108 entry_stage_id: None,
1109 exit_stage_id: None,
1110 min_storage_hm3: 0.0,
1111 max_storage_hm3: 1.0,
1112 min_outflow_m3s: 0.0,
1113 max_outflow_m3s: None,
1114 generation_model: HydroGenerationModel::ConstantProductivity {
1115 productivity_mw_per_m3s: 1.0,
1116 },
1117 min_turbined_m3s: 0.0,
1118 max_turbined_m3s: 1.0,
1119 min_generation_mw: 0.0,
1120 max_generation_mw: 1.0,
1121 tailrace: None,
1122 hydraulic_losses: None,
1123 efficiency: None,
1124 evaporation_coefficients_mm: None,
1125 diversion: None,
1126 filling: None,
1127 penalties: zero_penalties,
1128 }
1129 }
1130
1131 fn make_hydro(id: i32) -> Hydro {
1133 make_hydro_on_bus(id, 0)
1134 }
1135
1136 fn make_thermal_on_bus(id: i32, bus_id: i32) -> Thermal {
1137 Thermal {
1138 id: EntityId(id),
1139 name: format!("thermal-{id}"),
1140 bus_id: EntityId(bus_id),
1141 entry_stage_id: None,
1142 exit_stage_id: None,
1143 cost_segments: vec![ThermalCostSegment {
1144 capacity_mw: 100.0,
1145 cost_per_mwh: 50.0,
1146 }],
1147 min_generation_mw: 0.0,
1148 max_generation_mw: 100.0,
1149 gnl_config: None,
1150 }
1151 }
1152
1153 fn make_thermal(id: i32) -> Thermal {
1155 make_thermal_on_bus(id, 0)
1156 }
1157
1158 fn make_pumping_station_full(
1159 id: i32,
1160 bus_id: i32,
1161 source_hydro_id: i32,
1162 destination_hydro_id: i32,
1163 ) -> PumpingStation {
1164 PumpingStation {
1165 id: EntityId(id),
1166 name: format!("ps-{id}"),
1167 bus_id: EntityId(bus_id),
1168 source_hydro_id: EntityId(source_hydro_id),
1169 destination_hydro_id: EntityId(destination_hydro_id),
1170 entry_stage_id: None,
1171 exit_stage_id: None,
1172 consumption_mw_per_m3s: 0.5,
1173 min_flow_m3s: 0.0,
1174 max_flow_m3s: 10.0,
1175 }
1176 }
1177
1178 fn make_pumping_station(id: i32) -> PumpingStation {
1179 make_pumping_station_full(id, 0, 0, 1)
1180 }
1181
1182 fn make_contract_on_bus(id: i32, bus_id: i32) -> EnergyContract {
1183 EnergyContract {
1184 id: EntityId(id),
1185 name: format!("contract-{id}"),
1186 bus_id: EntityId(bus_id),
1187 contract_type: ContractType::Import,
1188 entry_stage_id: None,
1189 exit_stage_id: None,
1190 price_per_mwh: 0.0,
1191 min_mw: 0.0,
1192 max_mw: 100.0,
1193 }
1194 }
1195
1196 fn make_contract(id: i32) -> EnergyContract {
1197 make_contract_on_bus(id, 0)
1198 }
1199
1200 fn make_ncs_on_bus(id: i32, bus_id: i32) -> NonControllableSource {
1201 NonControllableSource {
1202 id: EntityId(id),
1203 name: format!("ncs-{id}"),
1204 bus_id: EntityId(bus_id),
1205 entry_stage_id: None,
1206 exit_stage_id: None,
1207 max_generation_mw: 50.0,
1208 curtailment_cost: 0.0,
1209 }
1210 }
1211
1212 fn make_ncs(id: i32) -> NonControllableSource {
1213 make_ncs_on_bus(id, 0)
1214 }
1215
1216 #[test]
1217 fn test_empty_system() {
1218 let system = SystemBuilder::new().build().expect("empty system is valid");
1219 assert_eq!(system.n_buses(), 0);
1220 assert_eq!(system.n_lines(), 0);
1221 assert_eq!(system.n_hydros(), 0);
1222 assert_eq!(system.n_thermals(), 0);
1223 assert_eq!(system.n_pumping_stations(), 0);
1224 assert_eq!(system.n_contracts(), 0);
1225 assert_eq!(system.n_non_controllable_sources(), 0);
1226 assert!(system.buses().is_empty());
1227 assert!(system.cascade().is_empty());
1228 }
1229
1230 #[test]
1231 fn test_canonical_ordering() {
1232 let system = SystemBuilder::new()
1234 .buses(vec![make_bus(2), make_bus(1), make_bus(0)])
1235 .build()
1236 .expect("valid system");
1237
1238 assert_eq!(system.buses()[0].id, EntityId(0));
1239 assert_eq!(system.buses()[1].id, EntityId(1));
1240 assert_eq!(system.buses()[2].id, EntityId(2));
1241 }
1242
1243 #[test]
1244 fn test_lookup_by_id() {
1245 let system = SystemBuilder::new()
1247 .buses(vec![make_bus(0)])
1248 .hydros(vec![make_hydro(10), make_hydro(5), make_hydro(20)])
1249 .build()
1250 .expect("valid system");
1251
1252 assert_eq!(system.hydro(EntityId(5)).map(|h| h.id), Some(EntityId(5)));
1253 assert_eq!(system.hydro(EntityId(10)).map(|h| h.id), Some(EntityId(10)));
1254 assert_eq!(system.hydro(EntityId(20)).map(|h| h.id), Some(EntityId(20)));
1255 }
1256
1257 #[test]
1258 fn test_lookup_missing_id() {
1259 let system = SystemBuilder::new()
1261 .buses(vec![make_bus(0)])
1262 .hydros(vec![make_hydro(1), make_hydro(2)])
1263 .build()
1264 .expect("valid system");
1265
1266 assert!(system.hydro(EntityId(999)).is_none());
1267 }
1268
1269 #[test]
1270 fn test_count_queries() {
1271 let system = SystemBuilder::new()
1272 .buses(vec![make_bus(0), make_bus(1)])
1273 .lines(vec![make_line(0, 0, 1)])
1274 .hydros(vec![make_hydro(0), make_hydro(1), make_hydro(2)])
1275 .thermals(vec![make_thermal(0)])
1276 .pumping_stations(vec![make_pumping_station(0)])
1277 .contracts(vec![make_contract(0), make_contract(1)])
1278 .non_controllable_sources(vec![make_ncs(0)])
1279 .build()
1280 .expect("valid system");
1281
1282 assert_eq!(system.n_buses(), 2);
1283 assert_eq!(system.n_lines(), 1);
1284 assert_eq!(system.n_hydros(), 3);
1285 assert_eq!(system.n_thermals(), 1);
1286 assert_eq!(system.n_pumping_stations(), 1);
1287 assert_eq!(system.n_contracts(), 2);
1288 assert_eq!(system.n_non_controllable_sources(), 1);
1289 }
1290
1291 #[test]
1292 fn test_slice_accessors() {
1293 let system = SystemBuilder::new()
1294 .buses(vec![make_bus(0), make_bus(1), make_bus(2)])
1295 .build()
1296 .expect("valid system");
1297
1298 let buses = system.buses();
1299 assert_eq!(buses.len(), 3);
1300 assert_eq!(buses[0].id, EntityId(0));
1301 assert_eq!(buses[1].id, EntityId(1));
1302 assert_eq!(buses[2].id, EntityId(2));
1303 }
1304
1305 #[test]
1306 fn test_duplicate_id_error() {
1307 let result = SystemBuilder::new()
1309 .buses(vec![make_bus(0), make_bus(0)])
1310 .build();
1311
1312 assert!(result.is_err());
1313 let errors = result.unwrap_err();
1314 assert!(!errors.is_empty());
1315 assert!(errors.iter().any(|e| matches!(
1316 e,
1317 ValidationError::DuplicateId {
1318 entity_type: "Bus",
1319 id: EntityId(0),
1320 }
1321 )));
1322 }
1323
1324 #[test]
1325 fn test_multiple_duplicate_errors() {
1326 let result = SystemBuilder::new()
1328 .buses(vec![make_bus(0), make_bus(0)])
1329 .thermals(vec![make_thermal(5), make_thermal(5)])
1330 .build();
1331
1332 assert!(result.is_err());
1333 let errors = result.unwrap_err();
1334
1335 let has_bus_dup = errors.iter().any(|e| {
1336 matches!(
1337 e,
1338 ValidationError::DuplicateId {
1339 entity_type: "Bus",
1340 ..
1341 }
1342 )
1343 });
1344 let has_thermal_dup = errors.iter().any(|e| {
1345 matches!(
1346 e,
1347 ValidationError::DuplicateId {
1348 entity_type: "Thermal",
1349 ..
1350 }
1351 )
1352 });
1353 assert!(has_bus_dup, "expected Bus duplicate error");
1354 assert!(has_thermal_dup, "expected Thermal duplicate error");
1355 }
1356
1357 #[test]
1358 fn test_send_sync() {
1359 fn require_send_sync<T: Send + Sync>(_: T) {}
1360 let system = SystemBuilder::new().build().expect("valid system");
1361 require_send_sync(system);
1362 }
1363
1364 #[test]
1365 fn test_cascade_accessible() {
1366 let mut h0 = make_hydro_on_bus(0, 0);
1368 h0.downstream_id = Some(EntityId(1));
1369 let mut h1 = make_hydro_on_bus(1, 0);
1370 h1.downstream_id = Some(EntityId(2));
1371 let h2 = make_hydro_on_bus(2, 0);
1372
1373 let system = SystemBuilder::new()
1374 .buses(vec![make_bus(0)])
1375 .hydros(vec![h0, h1, h2])
1376 .build()
1377 .expect("valid system");
1378
1379 let order = system.cascade().topological_order();
1380 assert!(!order.is_empty(), "topological order must be non-empty");
1381 let pos_0 = order
1382 .iter()
1383 .position(|&id| id == EntityId(0))
1384 .expect("EntityId(0) must be in topological order");
1385 let pos_2 = order
1386 .iter()
1387 .position(|&id| id == EntityId(2))
1388 .expect("EntityId(2) must be in topological order");
1389 assert!(pos_0 < pos_2, "EntityId(0) must precede EntityId(2)");
1390 }
1391
1392 #[test]
1393 fn test_network_accessible() {
1394 let system = SystemBuilder::new()
1395 .buses(vec![make_bus(0), make_bus(1)])
1396 .lines(vec![make_line(0, 0, 1)])
1397 .build()
1398 .expect("valid system");
1399
1400 let connections = system.network().bus_lines(EntityId(0));
1401 assert!(!connections.is_empty(), "bus 0 must have connections");
1402 assert_eq!(connections[0].line_id, EntityId(0));
1403 }
1404
1405 #[test]
1406 fn test_all_entity_lookups() {
1407 let system = SystemBuilder::new()
1412 .buses(vec![make_bus(0), make_bus(1)])
1413 .lines(vec![make_line(2, 0, 1)])
1414 .hydros(vec![
1415 make_hydro_on_bus(0, 0),
1416 make_hydro_on_bus(1, 0),
1417 make_hydro_on_bus(3, 0),
1418 ])
1419 .thermals(vec![make_thermal(4)])
1420 .pumping_stations(vec![make_pumping_station(5)])
1421 .contracts(vec![make_contract(6)])
1422 .non_controllable_sources(vec![make_ncs(7)])
1423 .build()
1424 .expect("valid system");
1425
1426 assert!(system.bus(EntityId(1)).is_some());
1427 assert!(system.line(EntityId(2)).is_some());
1428 assert!(system.hydro(EntityId(3)).is_some());
1429 assert!(system.thermal(EntityId(4)).is_some());
1430 assert!(system.pumping_station(EntityId(5)).is_some());
1431 assert!(system.contract(EntityId(6)).is_some());
1432 assert!(system.non_controllable_source(EntityId(7)).is_some());
1433
1434 assert!(system.bus(EntityId(999)).is_none());
1435 assert!(system.line(EntityId(999)).is_none());
1436 assert!(system.hydro(EntityId(999)).is_none());
1437 assert!(system.thermal(EntityId(999)).is_none());
1438 assert!(system.pumping_station(EntityId(999)).is_none());
1439 assert!(system.contract(EntityId(999)).is_none());
1440 assert!(system.non_controllable_source(EntityId(999)).is_none());
1441 }
1442
1443 #[test]
1444 fn test_default_builder() {
1445 let system = SystemBuilder::default()
1446 .build()
1447 .expect("default builder produces valid empty system");
1448 assert_eq!(system.n_buses(), 0);
1449 }
1450
1451 #[test]
1454 fn test_invalid_bus_reference_hydro() {
1455 let hydro = make_hydro_on_bus(1, 99);
1457 let result = SystemBuilder::new().hydros(vec![hydro]).build();
1458
1459 assert!(result.is_err(), "expected Err for missing bus reference");
1460 let errors = result.unwrap_err();
1461 assert!(
1462 errors.iter().any(|e| matches!(
1463 e,
1464 ValidationError::InvalidReference {
1465 source_entity_type: "Hydro",
1466 source_id: EntityId(1),
1467 field_name: "bus_id",
1468 referenced_id: EntityId(99),
1469 expected_type: "Bus",
1470 }
1471 )),
1472 "expected InvalidReference for Hydro bus_id=99, got: {errors:?}"
1473 );
1474 }
1475
1476 #[test]
1477 fn test_invalid_downstream_reference() {
1478 let bus = make_bus(0);
1480 let mut hydro = make_hydro(1);
1481 hydro.downstream_id = Some(EntityId(50));
1482
1483 let result = SystemBuilder::new()
1484 .buses(vec![bus])
1485 .hydros(vec![hydro])
1486 .build();
1487
1488 assert!(
1489 result.is_err(),
1490 "expected Err for missing downstream reference"
1491 );
1492 let errors = result.unwrap_err();
1493 assert!(
1494 errors.iter().any(|e| matches!(
1495 e,
1496 ValidationError::InvalidReference {
1497 source_entity_type: "Hydro",
1498 source_id: EntityId(1),
1499 field_name: "downstream_id",
1500 referenced_id: EntityId(50),
1501 expected_type: "Hydro",
1502 }
1503 )),
1504 "expected InvalidReference for Hydro downstream_id=50, got: {errors:?}"
1505 );
1506 }
1507
1508 #[test]
1509 fn test_invalid_pumping_station_hydro_refs() {
1510 let bus = make_bus(0);
1512 let dest_hydro = make_hydro(1);
1513 let ps = make_pumping_station_full(10, 0, 77, 1);
1514
1515 let result = SystemBuilder::new()
1516 .buses(vec![bus])
1517 .hydros(vec![dest_hydro])
1518 .pumping_stations(vec![ps])
1519 .build();
1520
1521 assert!(
1522 result.is_err(),
1523 "expected Err for missing source_hydro_id reference"
1524 );
1525 let errors = result.unwrap_err();
1526 assert!(
1527 errors.iter().any(|e| matches!(
1528 e,
1529 ValidationError::InvalidReference {
1530 source_entity_type: "PumpingStation",
1531 source_id: EntityId(10),
1532 field_name: "source_hydro_id",
1533 referenced_id: EntityId(77),
1534 expected_type: "Hydro",
1535 }
1536 )),
1537 "expected InvalidReference for PumpingStation source_hydro_id=77, got: {errors:?}"
1538 );
1539 }
1540
1541 #[test]
1542 fn test_multiple_invalid_references_collected() {
1543 let line = make_line(1, 99, 0);
1546 let thermal = make_thermal_on_bus(2, 88);
1547
1548 let result = SystemBuilder::new()
1549 .buses(vec![make_bus(0)])
1550 .lines(vec![line])
1551 .thermals(vec![thermal])
1552 .build();
1553
1554 assert!(
1555 result.is_err(),
1556 "expected Err for multiple invalid references"
1557 );
1558 let errors = result.unwrap_err();
1559
1560 let has_line_error = errors.iter().any(|e| {
1561 matches!(
1562 e,
1563 ValidationError::InvalidReference {
1564 source_entity_type: "Line",
1565 field_name: "source_bus_id",
1566 referenced_id: EntityId(99),
1567 ..
1568 }
1569 )
1570 });
1571 let has_thermal_error = errors.iter().any(|e| {
1572 matches!(
1573 e,
1574 ValidationError::InvalidReference {
1575 source_entity_type: "Thermal",
1576 field_name: "bus_id",
1577 referenced_id: EntityId(88),
1578 ..
1579 }
1580 )
1581 });
1582
1583 assert!(
1584 has_line_error,
1585 "expected Line source_bus_id=99 error, got: {errors:?}"
1586 );
1587 assert!(
1588 has_thermal_error,
1589 "expected Thermal bus_id=88 error, got: {errors:?}"
1590 );
1591 assert!(
1592 errors.len() >= 2,
1593 "expected at least 2 errors, got {}: {errors:?}",
1594 errors.len()
1595 );
1596 }
1597
1598 #[test]
1599 fn test_valid_cross_references_pass() {
1600 let bus_0 = make_bus(0);
1602 let bus_1 = make_bus(1);
1603 let h0 = make_hydro_on_bus(0, 0);
1604 let h1 = make_hydro_on_bus(1, 1);
1605 let mut h2 = make_hydro_on_bus(2, 0);
1606 h2.downstream_id = Some(EntityId(1));
1607 let line = make_line(10, 0, 1);
1608 let thermal = make_thermal_on_bus(20, 0);
1609 let ps = make_pumping_station_full(30, 0, 0, 1);
1610 let contract = make_contract_on_bus(40, 1);
1611 let ncs = make_ncs_on_bus(50, 0);
1612
1613 let result = SystemBuilder::new()
1614 .buses(vec![bus_0, bus_1])
1615 .lines(vec![line])
1616 .hydros(vec![h0, h1, h2])
1617 .thermals(vec![thermal])
1618 .pumping_stations(vec![ps])
1619 .contracts(vec![contract])
1620 .non_controllable_sources(vec![ncs])
1621 .build();
1622
1623 assert!(
1624 result.is_ok(),
1625 "expected Ok for all valid cross-references, got: {:?}",
1626 result.unwrap_err()
1627 );
1628 let system = result.unwrap_or_else(|_| unreachable!());
1629 assert_eq!(system.n_buses(), 2);
1630 assert_eq!(system.n_hydros(), 3);
1631 assert_eq!(system.n_lines(), 1);
1632 assert_eq!(system.n_thermals(), 1);
1633 assert_eq!(system.n_pumping_stations(), 1);
1634 assert_eq!(system.n_contracts(), 1);
1635 assert_eq!(system.n_non_controllable_sources(), 1);
1636 }
1637
1638 #[test]
1641 fn test_cascade_cycle_detected() {
1642 let bus = make_bus(0);
1645 let mut h0 = make_hydro(0);
1646 h0.downstream_id = Some(EntityId(1));
1647 let mut h1 = make_hydro(1);
1648 h1.downstream_id = Some(EntityId(2));
1649 let mut h2 = make_hydro(2);
1650 h2.downstream_id = Some(EntityId(0));
1651
1652 let result = SystemBuilder::new()
1653 .buses(vec![bus])
1654 .hydros(vec![h0, h1, h2])
1655 .build();
1656
1657 assert!(result.is_err(), "expected Err for 3-node cycle");
1658 let errors = result.unwrap_err();
1659 let cycle_error = errors
1660 .iter()
1661 .find(|e| matches!(e, ValidationError::CascadeCycle { .. }));
1662 assert!(
1663 cycle_error.is_some(),
1664 "expected CascadeCycle error, got: {errors:?}"
1665 );
1666 let ValidationError::CascadeCycle { cycle_ids } = cycle_error.unwrap() else {
1667 unreachable!()
1668 };
1669 assert_eq!(
1670 cycle_ids,
1671 &[EntityId(0), EntityId(1), EntityId(2)],
1672 "cycle_ids must be sorted ascending, got: {cycle_ids:?}"
1673 );
1674 }
1675
1676 #[test]
1677 fn test_cascade_self_loop_detected() {
1678 let bus = make_bus(0);
1680 let mut h0 = make_hydro(0);
1681 h0.downstream_id = Some(EntityId(0));
1682
1683 let result = SystemBuilder::new()
1684 .buses(vec![bus])
1685 .hydros(vec![h0])
1686 .build();
1687
1688 assert!(result.is_err(), "expected Err for self-loop");
1689 let errors = result.unwrap_err();
1690 let has_cycle = errors
1691 .iter()
1692 .any(|e| matches!(e, ValidationError::CascadeCycle { cycle_ids } if cycle_ids.contains(&EntityId(0))));
1693 assert!(
1694 has_cycle,
1695 "expected CascadeCycle containing EntityId(0), got: {errors:?}"
1696 );
1697 }
1698
1699 #[test]
1700 fn test_valid_acyclic_cascade_passes() {
1701 let bus = make_bus(0);
1704 let mut h0 = make_hydro(0);
1705 h0.downstream_id = Some(EntityId(1));
1706 let mut h1 = make_hydro(1);
1707 h1.downstream_id = Some(EntityId(2));
1708 let h2 = make_hydro(2);
1709
1710 let result = SystemBuilder::new()
1711 .buses(vec![bus])
1712 .hydros(vec![h0, h1, h2])
1713 .build();
1714
1715 assert!(
1716 result.is_ok(),
1717 "expected Ok for acyclic cascade, got: {:?}",
1718 result.unwrap_err()
1719 );
1720 let system = result.unwrap_or_else(|_| unreachable!());
1721 assert_eq!(
1722 system.cascade().topological_order().len(),
1723 system.n_hydros(),
1724 "topological_order must contain all hydros"
1725 );
1726 }
1727
1728 #[test]
1731 fn test_filling_without_entry_stage() {
1732 use crate::entities::FillingConfig;
1734 let bus = make_bus(0);
1735 let mut hydro = make_hydro(1);
1736 hydro.entry_stage_id = None;
1737 hydro.filling = Some(FillingConfig {
1738 start_stage_id: 10,
1739 filling_inflow_m3s: 100.0,
1740 });
1741
1742 let result = SystemBuilder::new()
1743 .buses(vec![bus])
1744 .hydros(vec![hydro])
1745 .build();
1746
1747 assert!(
1748 result.is_err(),
1749 "expected Err for filling without entry_stage_id"
1750 );
1751 let errors = result.unwrap_err();
1752 let has_error = errors.iter().any(|e| match e {
1753 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
1754 *hydro_id == EntityId(1) && reason.contains("entry_stage_id")
1755 }
1756 _ => false,
1757 });
1758 assert!(
1759 has_error,
1760 "expected InvalidFillingConfig with entry_stage_id reason, got: {errors:?}"
1761 );
1762 }
1763
1764 #[test]
1765 fn test_filling_negative_inflow() {
1766 use crate::entities::FillingConfig;
1768 let bus = make_bus(0);
1769 let mut hydro = make_hydro(1);
1770 hydro.entry_stage_id = Some(10);
1771 hydro.filling = Some(FillingConfig {
1772 start_stage_id: 10,
1773 filling_inflow_m3s: -5.0,
1774 });
1775
1776 let result = SystemBuilder::new()
1777 .buses(vec![bus])
1778 .hydros(vec![hydro])
1779 .build();
1780
1781 assert!(
1782 result.is_err(),
1783 "expected Err for negative filling_inflow_m3s"
1784 );
1785 let errors = result.unwrap_err();
1786 let has_error = errors.iter().any(|e| match e {
1787 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
1788 *hydro_id == EntityId(1) && reason.contains("filling_inflow_m3s must be positive")
1789 }
1790 _ => false,
1791 });
1792 assert!(
1793 has_error,
1794 "expected InvalidFillingConfig with positive inflow reason, got: {errors:?}"
1795 );
1796 }
1797
1798 #[test]
1799 fn test_valid_filling_config_passes() {
1800 use crate::entities::FillingConfig;
1802 let bus = make_bus(0);
1803 let mut hydro = make_hydro(1);
1804 hydro.entry_stage_id = Some(10);
1805 hydro.filling = Some(FillingConfig {
1806 start_stage_id: 10,
1807 filling_inflow_m3s: 100.0,
1808 });
1809
1810 let result = SystemBuilder::new()
1811 .buses(vec![bus])
1812 .hydros(vec![hydro])
1813 .build();
1814
1815 assert!(
1816 result.is_ok(),
1817 "expected Ok for valid filling config, got: {:?}",
1818 result.unwrap_err()
1819 );
1820 }
1821
1822 #[test]
1823 fn test_cascade_cycle_and_invalid_filling_both_reported() {
1824 use crate::entities::FillingConfig;
1827 let bus = make_bus(0);
1828
1829 let mut h0 = make_hydro(0);
1831 h0.downstream_id = Some(EntityId(0));
1832
1833 let mut h1 = make_hydro(1);
1835 h1.entry_stage_id = None; h1.filling = Some(FillingConfig {
1837 start_stage_id: 5,
1838 filling_inflow_m3s: 50.0,
1839 });
1840
1841 let result = SystemBuilder::new()
1842 .buses(vec![bus])
1843 .hydros(vec![h0, h1])
1844 .build();
1845
1846 assert!(result.is_err(), "expected Err for cycle + invalid filling");
1847 let errors = result.unwrap_err();
1848 let has_cycle = errors
1849 .iter()
1850 .any(|e| matches!(e, ValidationError::CascadeCycle { .. }));
1851 let has_filling = errors
1852 .iter()
1853 .any(|e| matches!(e, ValidationError::InvalidFillingConfig { .. }));
1854 assert!(has_cycle, "expected CascadeCycle error, got: {errors:?}");
1855 assert!(
1856 has_filling,
1857 "expected InvalidFillingConfig error, got: {errors:?}"
1858 );
1859 }
1860
1861 #[cfg(feature = "serde")]
1862 #[test]
1863 fn test_system_serde_roundtrip() {
1864 let bus_a = make_bus(1);
1866 let bus_b = make_bus(2);
1867 let hydro = make_hydro_on_bus(10, 1);
1868 let thermal = make_thermal_on_bus(20, 2);
1869 let line = make_line(1, 1, 2);
1870
1871 let system = SystemBuilder::new()
1872 .buses(vec![bus_a, bus_b])
1873 .hydros(vec![hydro])
1874 .thermals(vec![thermal])
1875 .lines(vec![line])
1876 .build()
1877 .expect("valid system");
1878
1879 let json = serde_json::to_string(&system).unwrap();
1880
1881 let mut deserialized: System = serde_json::from_str(&json).unwrap();
1883 deserialized.rebuild_indices();
1884
1885 assert_eq!(system.buses(), deserialized.buses());
1887 assert_eq!(system.hydros(), deserialized.hydros());
1888 assert_eq!(system.thermals(), deserialized.thermals());
1889 assert_eq!(system.lines(), deserialized.lines());
1890
1891 assert_eq!(
1893 deserialized.bus(EntityId(1)).map(|b| b.id),
1894 Some(EntityId(1))
1895 );
1896 assert_eq!(
1897 deserialized.hydro(EntityId(10)).map(|h| h.id),
1898 Some(EntityId(10))
1899 );
1900 assert_eq!(
1901 deserialized.thermal(EntityId(20)).map(|t| t.id),
1902 Some(EntityId(20))
1903 );
1904 assert_eq!(
1905 deserialized.line(EntityId(1)).map(|l| l.id),
1906 Some(EntityId(1))
1907 );
1908 }
1909
1910 fn make_stage(id: i32) -> Stage {
1913 use crate::temporal::{
1914 Block, BlockMode, NoiseMethod, ScenarioSourceConfig, StageRiskConfig, StageStateConfig,
1915 };
1916 use chrono::NaiveDate;
1917 Stage {
1918 index: usize::try_from(id.max(0)).unwrap_or(0),
1919 id,
1920 start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1921 end_date: NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
1922 season_id: Some(0),
1923 blocks: vec![Block {
1924 index: 0,
1925 name: "SINGLE".to_string(),
1926 duration_hours: 744.0,
1927 }],
1928 block_mode: BlockMode::Parallel,
1929 state_config: StageStateConfig {
1930 storage: true,
1931 inflow_lags: false,
1932 },
1933 risk_config: StageRiskConfig::Expectation,
1934 scenario_config: ScenarioSourceConfig {
1935 branching_factor: 50,
1936 noise_method: NoiseMethod::Saa,
1937 },
1938 }
1939 }
1940
1941 #[test]
1944 fn test_system_backward_compat() {
1945 let system = SystemBuilder::new().build().expect("empty system is valid");
1946 assert_eq!(system.n_buses(), 0);
1948 assert_eq!(system.n_hydros(), 0);
1949 assert_eq!(system.n_stages(), 0);
1951 assert!(system.stages().is_empty());
1952 assert!(system.initial_conditions().storage.is_empty());
1953 assert!(system.generic_constraints().is_empty());
1954 assert!(system.inflow_models().is_empty());
1955 assert!(system.load_models().is_empty());
1956 assert_eq!(system.penalties().n_stages(), 0);
1957 assert_eq!(system.bounds().n_stages(), 0);
1958 }
1959
1960 #[test]
1962 fn test_system_with_stages() {
1963 let s0 = make_stage(0);
1964 let s1 = make_stage(1);
1965
1966 let system = SystemBuilder::new()
1967 .stages(vec![s1.clone(), s0.clone()]) .build()
1969 .expect("valid system");
1970
1971 assert_eq!(system.n_stages(), 2);
1973 assert_eq!(system.stages()[0].id, 0);
1974 assert_eq!(system.stages()[1].id, 1);
1975
1976 let found = system.stage(0).expect("stage 0 must be found");
1978 assert_eq!(found.id, s0.id);
1979
1980 let found1 = system.stage(1).expect("stage 1 must be found");
1981 assert_eq!(found1.id, s1.id);
1982
1983 assert!(system.stage(99).is_none());
1985 }
1986
1987 #[test]
1989 fn test_system_stage_lookup_by_id() {
1990 let stages: Vec<Stage> = [0i32, 1, 2].iter().map(|&id| make_stage(id)).collect();
1991
1992 let system = SystemBuilder::new()
1993 .stages(stages)
1994 .build()
1995 .expect("valid system");
1996
1997 assert_eq!(system.stage(1).map(|s| s.id), Some(1));
1998 assert!(system.stage(99).is_none());
1999 }
2000
2001 #[test]
2003 fn test_system_with_initial_conditions() {
2004 let ic = InitialConditions {
2005 storage: vec![crate::HydroStorage {
2006 hydro_id: EntityId(0),
2007 value_hm3: 15_000.0,
2008 }],
2009 filling_storage: vec![],
2010 };
2011
2012 let system = SystemBuilder::new()
2013 .initial_conditions(ic)
2014 .build()
2015 .expect("valid system");
2016
2017 assert_eq!(system.initial_conditions().storage.len(), 1);
2018 assert_eq!(system.initial_conditions().storage[0].hydro_id, EntityId(0));
2019 assert!((system.initial_conditions().storage[0].value_hm3 - 15_000.0).abs() < f64::EPSILON);
2020 }
2021
2022 #[cfg(feature = "serde")]
2025 #[test]
2026 fn test_system_serde_roundtrip_with_stages() {
2027 use crate::temporal::PolicyGraphType;
2028
2029 let stages = vec![make_stage(0), make_stage(1)];
2030 let policy_graph = PolicyGraph {
2031 graph_type: PolicyGraphType::FiniteHorizon,
2032 annual_discount_rate: 0.0,
2033 transitions: vec![],
2034 season_map: None,
2035 };
2036
2037 let system = SystemBuilder::new()
2038 .stages(stages)
2039 .policy_graph(policy_graph)
2040 .build()
2041 .expect("valid system");
2042
2043 let json = serde_json::to_string(&system).unwrap();
2044 let mut deserialized: System = serde_json::from_str(&json).unwrap();
2045
2046 deserialized.rebuild_indices();
2048
2049 assert_eq!(system.n_stages(), deserialized.n_stages());
2051 assert_eq!(system.stages()[0].id, deserialized.stages()[0].id);
2052 assert_eq!(system.stages()[1].id, deserialized.stages()[1].id);
2053
2054 assert_eq!(deserialized.stage(0).map(|s| s.id), Some(0));
2056 assert_eq!(deserialized.stage(1).map(|s| s.id), Some(1));
2057 assert!(deserialized.stage(99).is_none());
2058
2059 assert_eq!(
2061 deserialized.policy_graph().graph_type,
2062 system.policy_graph().graph_type
2063 );
2064 }
2065}