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 evaporation_reference_volumes_hm3: None,
1126 diversion: None,
1127 filling: None,
1128 penalties: zero_penalties,
1129 }
1130 }
1131
1132 fn make_hydro(id: i32) -> Hydro {
1134 make_hydro_on_bus(id, 0)
1135 }
1136
1137 fn make_thermal_on_bus(id: i32, bus_id: i32) -> Thermal {
1138 Thermal {
1139 id: EntityId(id),
1140 name: format!("thermal-{id}"),
1141 bus_id: EntityId(bus_id),
1142 entry_stage_id: None,
1143 exit_stage_id: None,
1144 cost_segments: vec![ThermalCostSegment {
1145 capacity_mw: 100.0,
1146 cost_per_mwh: 50.0,
1147 }],
1148 min_generation_mw: 0.0,
1149 max_generation_mw: 100.0,
1150 gnl_config: None,
1151 }
1152 }
1153
1154 fn make_thermal(id: i32) -> Thermal {
1156 make_thermal_on_bus(id, 0)
1157 }
1158
1159 fn make_pumping_station_full(
1160 id: i32,
1161 bus_id: i32,
1162 source_hydro_id: i32,
1163 destination_hydro_id: i32,
1164 ) -> PumpingStation {
1165 PumpingStation {
1166 id: EntityId(id),
1167 name: format!("ps-{id}"),
1168 bus_id: EntityId(bus_id),
1169 source_hydro_id: EntityId(source_hydro_id),
1170 destination_hydro_id: EntityId(destination_hydro_id),
1171 entry_stage_id: None,
1172 exit_stage_id: None,
1173 consumption_mw_per_m3s: 0.5,
1174 min_flow_m3s: 0.0,
1175 max_flow_m3s: 10.0,
1176 }
1177 }
1178
1179 fn make_pumping_station(id: i32) -> PumpingStation {
1180 make_pumping_station_full(id, 0, 0, 1)
1181 }
1182
1183 fn make_contract_on_bus(id: i32, bus_id: i32) -> EnergyContract {
1184 EnergyContract {
1185 id: EntityId(id),
1186 name: format!("contract-{id}"),
1187 bus_id: EntityId(bus_id),
1188 contract_type: ContractType::Import,
1189 entry_stage_id: None,
1190 exit_stage_id: None,
1191 price_per_mwh: 0.0,
1192 min_mw: 0.0,
1193 max_mw: 100.0,
1194 }
1195 }
1196
1197 fn make_contract(id: i32) -> EnergyContract {
1198 make_contract_on_bus(id, 0)
1199 }
1200
1201 fn make_ncs_on_bus(id: i32, bus_id: i32) -> NonControllableSource {
1202 NonControllableSource {
1203 id: EntityId(id),
1204 name: format!("ncs-{id}"),
1205 bus_id: EntityId(bus_id),
1206 entry_stage_id: None,
1207 exit_stage_id: None,
1208 max_generation_mw: 50.0,
1209 curtailment_cost: 0.0,
1210 }
1211 }
1212
1213 fn make_ncs(id: i32) -> NonControllableSource {
1214 make_ncs_on_bus(id, 0)
1215 }
1216
1217 #[test]
1218 fn test_empty_system() {
1219 let system = SystemBuilder::new().build().expect("empty system is valid");
1220 assert_eq!(system.n_buses(), 0);
1221 assert_eq!(system.n_lines(), 0);
1222 assert_eq!(system.n_hydros(), 0);
1223 assert_eq!(system.n_thermals(), 0);
1224 assert_eq!(system.n_pumping_stations(), 0);
1225 assert_eq!(system.n_contracts(), 0);
1226 assert_eq!(system.n_non_controllable_sources(), 0);
1227 assert!(system.buses().is_empty());
1228 assert!(system.cascade().is_empty());
1229 }
1230
1231 #[test]
1232 fn test_canonical_ordering() {
1233 let system = SystemBuilder::new()
1235 .buses(vec![make_bus(2), make_bus(1), make_bus(0)])
1236 .build()
1237 .expect("valid system");
1238
1239 assert_eq!(system.buses()[0].id, EntityId(0));
1240 assert_eq!(system.buses()[1].id, EntityId(1));
1241 assert_eq!(system.buses()[2].id, EntityId(2));
1242 }
1243
1244 #[test]
1245 fn test_lookup_by_id() {
1246 let system = SystemBuilder::new()
1248 .buses(vec![make_bus(0)])
1249 .hydros(vec![make_hydro(10), make_hydro(5), make_hydro(20)])
1250 .build()
1251 .expect("valid system");
1252
1253 assert_eq!(system.hydro(EntityId(5)).map(|h| h.id), Some(EntityId(5)));
1254 assert_eq!(system.hydro(EntityId(10)).map(|h| h.id), Some(EntityId(10)));
1255 assert_eq!(system.hydro(EntityId(20)).map(|h| h.id), Some(EntityId(20)));
1256 }
1257
1258 #[test]
1259 fn test_lookup_missing_id() {
1260 let system = SystemBuilder::new()
1262 .buses(vec![make_bus(0)])
1263 .hydros(vec![make_hydro(1), make_hydro(2)])
1264 .build()
1265 .expect("valid system");
1266
1267 assert!(system.hydro(EntityId(999)).is_none());
1268 }
1269
1270 #[test]
1271 fn test_count_queries() {
1272 let system = SystemBuilder::new()
1273 .buses(vec![make_bus(0), make_bus(1)])
1274 .lines(vec![make_line(0, 0, 1)])
1275 .hydros(vec![make_hydro(0), make_hydro(1), make_hydro(2)])
1276 .thermals(vec![make_thermal(0)])
1277 .pumping_stations(vec![make_pumping_station(0)])
1278 .contracts(vec![make_contract(0), make_contract(1)])
1279 .non_controllable_sources(vec![make_ncs(0)])
1280 .build()
1281 .expect("valid system");
1282
1283 assert_eq!(system.n_buses(), 2);
1284 assert_eq!(system.n_lines(), 1);
1285 assert_eq!(system.n_hydros(), 3);
1286 assert_eq!(system.n_thermals(), 1);
1287 assert_eq!(system.n_pumping_stations(), 1);
1288 assert_eq!(system.n_contracts(), 2);
1289 assert_eq!(system.n_non_controllable_sources(), 1);
1290 }
1291
1292 #[test]
1293 fn test_slice_accessors() {
1294 let system = SystemBuilder::new()
1295 .buses(vec![make_bus(0), make_bus(1), make_bus(2)])
1296 .build()
1297 .expect("valid system");
1298
1299 let buses = system.buses();
1300 assert_eq!(buses.len(), 3);
1301 assert_eq!(buses[0].id, EntityId(0));
1302 assert_eq!(buses[1].id, EntityId(1));
1303 assert_eq!(buses[2].id, EntityId(2));
1304 }
1305
1306 #[test]
1307 fn test_duplicate_id_error() {
1308 let result = SystemBuilder::new()
1310 .buses(vec![make_bus(0), make_bus(0)])
1311 .build();
1312
1313 assert!(result.is_err());
1314 let errors = result.unwrap_err();
1315 assert!(!errors.is_empty());
1316 assert!(errors.iter().any(|e| matches!(
1317 e,
1318 ValidationError::DuplicateId {
1319 entity_type: "Bus",
1320 id: EntityId(0),
1321 }
1322 )));
1323 }
1324
1325 #[test]
1326 fn test_multiple_duplicate_errors() {
1327 let result = SystemBuilder::new()
1329 .buses(vec![make_bus(0), make_bus(0)])
1330 .thermals(vec![make_thermal(5), make_thermal(5)])
1331 .build();
1332
1333 assert!(result.is_err());
1334 let errors = result.unwrap_err();
1335
1336 let has_bus_dup = errors.iter().any(|e| {
1337 matches!(
1338 e,
1339 ValidationError::DuplicateId {
1340 entity_type: "Bus",
1341 ..
1342 }
1343 )
1344 });
1345 let has_thermal_dup = errors.iter().any(|e| {
1346 matches!(
1347 e,
1348 ValidationError::DuplicateId {
1349 entity_type: "Thermal",
1350 ..
1351 }
1352 )
1353 });
1354 assert!(has_bus_dup, "expected Bus duplicate error");
1355 assert!(has_thermal_dup, "expected Thermal duplicate error");
1356 }
1357
1358 #[test]
1359 fn test_send_sync() {
1360 fn require_send_sync<T: Send + Sync>(_: T) {}
1361 let system = SystemBuilder::new().build().expect("valid system");
1362 require_send_sync(system);
1363 }
1364
1365 #[test]
1366 fn test_cascade_accessible() {
1367 let mut h0 = make_hydro_on_bus(0, 0);
1369 h0.downstream_id = Some(EntityId(1));
1370 let mut h1 = make_hydro_on_bus(1, 0);
1371 h1.downstream_id = Some(EntityId(2));
1372 let h2 = make_hydro_on_bus(2, 0);
1373
1374 let system = SystemBuilder::new()
1375 .buses(vec![make_bus(0)])
1376 .hydros(vec![h0, h1, h2])
1377 .build()
1378 .expect("valid system");
1379
1380 let order = system.cascade().topological_order();
1381 assert!(!order.is_empty(), "topological order must be non-empty");
1382 let pos_0 = order
1383 .iter()
1384 .position(|&id| id == EntityId(0))
1385 .expect("EntityId(0) must be in topological order");
1386 let pos_2 = order
1387 .iter()
1388 .position(|&id| id == EntityId(2))
1389 .expect("EntityId(2) must be in topological order");
1390 assert!(pos_0 < pos_2, "EntityId(0) must precede EntityId(2)");
1391 }
1392
1393 #[test]
1394 fn test_network_accessible() {
1395 let system = SystemBuilder::new()
1396 .buses(vec![make_bus(0), make_bus(1)])
1397 .lines(vec![make_line(0, 0, 1)])
1398 .build()
1399 .expect("valid system");
1400
1401 let connections = system.network().bus_lines(EntityId(0));
1402 assert!(!connections.is_empty(), "bus 0 must have connections");
1403 assert_eq!(connections[0].line_id, EntityId(0));
1404 }
1405
1406 #[test]
1407 fn test_all_entity_lookups() {
1408 let system = SystemBuilder::new()
1413 .buses(vec![make_bus(0), make_bus(1)])
1414 .lines(vec![make_line(2, 0, 1)])
1415 .hydros(vec![
1416 make_hydro_on_bus(0, 0),
1417 make_hydro_on_bus(1, 0),
1418 make_hydro_on_bus(3, 0),
1419 ])
1420 .thermals(vec![make_thermal(4)])
1421 .pumping_stations(vec![make_pumping_station(5)])
1422 .contracts(vec![make_contract(6)])
1423 .non_controllable_sources(vec![make_ncs(7)])
1424 .build()
1425 .expect("valid system");
1426
1427 assert!(system.bus(EntityId(1)).is_some());
1428 assert!(system.line(EntityId(2)).is_some());
1429 assert!(system.hydro(EntityId(3)).is_some());
1430 assert!(system.thermal(EntityId(4)).is_some());
1431 assert!(system.pumping_station(EntityId(5)).is_some());
1432 assert!(system.contract(EntityId(6)).is_some());
1433 assert!(system.non_controllable_source(EntityId(7)).is_some());
1434
1435 assert!(system.bus(EntityId(999)).is_none());
1436 assert!(system.line(EntityId(999)).is_none());
1437 assert!(system.hydro(EntityId(999)).is_none());
1438 assert!(system.thermal(EntityId(999)).is_none());
1439 assert!(system.pumping_station(EntityId(999)).is_none());
1440 assert!(system.contract(EntityId(999)).is_none());
1441 assert!(system.non_controllable_source(EntityId(999)).is_none());
1442 }
1443
1444 #[test]
1445 fn test_default_builder() {
1446 let system = SystemBuilder::default()
1447 .build()
1448 .expect("default builder produces valid empty system");
1449 assert_eq!(system.n_buses(), 0);
1450 }
1451
1452 #[test]
1455 fn test_invalid_bus_reference_hydro() {
1456 let hydro = make_hydro_on_bus(1, 99);
1458 let result = SystemBuilder::new().hydros(vec![hydro]).build();
1459
1460 assert!(result.is_err(), "expected Err for missing bus reference");
1461 let errors = result.unwrap_err();
1462 assert!(
1463 errors.iter().any(|e| matches!(
1464 e,
1465 ValidationError::InvalidReference {
1466 source_entity_type: "Hydro",
1467 source_id: EntityId(1),
1468 field_name: "bus_id",
1469 referenced_id: EntityId(99),
1470 expected_type: "Bus",
1471 }
1472 )),
1473 "expected InvalidReference for Hydro bus_id=99, got: {errors:?}"
1474 );
1475 }
1476
1477 #[test]
1478 fn test_invalid_downstream_reference() {
1479 let bus = make_bus(0);
1481 let mut hydro = make_hydro(1);
1482 hydro.downstream_id = Some(EntityId(50));
1483
1484 let result = SystemBuilder::new()
1485 .buses(vec![bus])
1486 .hydros(vec![hydro])
1487 .build();
1488
1489 assert!(
1490 result.is_err(),
1491 "expected Err for missing downstream reference"
1492 );
1493 let errors = result.unwrap_err();
1494 assert!(
1495 errors.iter().any(|e| matches!(
1496 e,
1497 ValidationError::InvalidReference {
1498 source_entity_type: "Hydro",
1499 source_id: EntityId(1),
1500 field_name: "downstream_id",
1501 referenced_id: EntityId(50),
1502 expected_type: "Hydro",
1503 }
1504 )),
1505 "expected InvalidReference for Hydro downstream_id=50, got: {errors:?}"
1506 );
1507 }
1508
1509 #[test]
1510 fn test_invalid_pumping_station_hydro_refs() {
1511 let bus = make_bus(0);
1513 let dest_hydro = make_hydro(1);
1514 let ps = make_pumping_station_full(10, 0, 77, 1);
1515
1516 let result = SystemBuilder::new()
1517 .buses(vec![bus])
1518 .hydros(vec![dest_hydro])
1519 .pumping_stations(vec![ps])
1520 .build();
1521
1522 assert!(
1523 result.is_err(),
1524 "expected Err for missing source_hydro_id reference"
1525 );
1526 let errors = result.unwrap_err();
1527 assert!(
1528 errors.iter().any(|e| matches!(
1529 e,
1530 ValidationError::InvalidReference {
1531 source_entity_type: "PumpingStation",
1532 source_id: EntityId(10),
1533 field_name: "source_hydro_id",
1534 referenced_id: EntityId(77),
1535 expected_type: "Hydro",
1536 }
1537 )),
1538 "expected InvalidReference for PumpingStation source_hydro_id=77, got: {errors:?}"
1539 );
1540 }
1541
1542 #[test]
1543 fn test_multiple_invalid_references_collected() {
1544 let line = make_line(1, 99, 0);
1547 let thermal = make_thermal_on_bus(2, 88);
1548
1549 let result = SystemBuilder::new()
1550 .buses(vec![make_bus(0)])
1551 .lines(vec![line])
1552 .thermals(vec![thermal])
1553 .build();
1554
1555 assert!(
1556 result.is_err(),
1557 "expected Err for multiple invalid references"
1558 );
1559 let errors = result.unwrap_err();
1560
1561 let has_line_error = errors.iter().any(|e| {
1562 matches!(
1563 e,
1564 ValidationError::InvalidReference {
1565 source_entity_type: "Line",
1566 field_name: "source_bus_id",
1567 referenced_id: EntityId(99),
1568 ..
1569 }
1570 )
1571 });
1572 let has_thermal_error = errors.iter().any(|e| {
1573 matches!(
1574 e,
1575 ValidationError::InvalidReference {
1576 source_entity_type: "Thermal",
1577 field_name: "bus_id",
1578 referenced_id: EntityId(88),
1579 ..
1580 }
1581 )
1582 });
1583
1584 assert!(
1585 has_line_error,
1586 "expected Line source_bus_id=99 error, got: {errors:?}"
1587 );
1588 assert!(
1589 has_thermal_error,
1590 "expected Thermal bus_id=88 error, got: {errors:?}"
1591 );
1592 assert!(
1593 errors.len() >= 2,
1594 "expected at least 2 errors, got {}: {errors:?}",
1595 errors.len()
1596 );
1597 }
1598
1599 #[test]
1600 fn test_valid_cross_references_pass() {
1601 let bus_0 = make_bus(0);
1603 let bus_1 = make_bus(1);
1604 let h0 = make_hydro_on_bus(0, 0);
1605 let h1 = make_hydro_on_bus(1, 1);
1606 let mut h2 = make_hydro_on_bus(2, 0);
1607 h2.downstream_id = Some(EntityId(1));
1608 let line = make_line(10, 0, 1);
1609 let thermal = make_thermal_on_bus(20, 0);
1610 let ps = make_pumping_station_full(30, 0, 0, 1);
1611 let contract = make_contract_on_bus(40, 1);
1612 let ncs = make_ncs_on_bus(50, 0);
1613
1614 let result = SystemBuilder::new()
1615 .buses(vec![bus_0, bus_1])
1616 .lines(vec![line])
1617 .hydros(vec![h0, h1, h2])
1618 .thermals(vec![thermal])
1619 .pumping_stations(vec![ps])
1620 .contracts(vec![contract])
1621 .non_controllable_sources(vec![ncs])
1622 .build();
1623
1624 assert!(
1625 result.is_ok(),
1626 "expected Ok for all valid cross-references, got: {:?}",
1627 result.unwrap_err()
1628 );
1629 let system = result.unwrap_or_else(|_| unreachable!());
1630 assert_eq!(system.n_buses(), 2);
1631 assert_eq!(system.n_hydros(), 3);
1632 assert_eq!(system.n_lines(), 1);
1633 assert_eq!(system.n_thermals(), 1);
1634 assert_eq!(system.n_pumping_stations(), 1);
1635 assert_eq!(system.n_contracts(), 1);
1636 assert_eq!(system.n_non_controllable_sources(), 1);
1637 }
1638
1639 #[test]
1642 fn test_cascade_cycle_detected() {
1643 let bus = make_bus(0);
1646 let mut h0 = make_hydro(0);
1647 h0.downstream_id = Some(EntityId(1));
1648 let mut h1 = make_hydro(1);
1649 h1.downstream_id = Some(EntityId(2));
1650 let mut h2 = make_hydro(2);
1651 h2.downstream_id = Some(EntityId(0));
1652
1653 let result = SystemBuilder::new()
1654 .buses(vec![bus])
1655 .hydros(vec![h0, h1, h2])
1656 .build();
1657
1658 assert!(result.is_err(), "expected Err for 3-node cycle");
1659 let errors = result.unwrap_err();
1660 let cycle_error = errors
1661 .iter()
1662 .find(|e| matches!(e, ValidationError::CascadeCycle { .. }));
1663 assert!(
1664 cycle_error.is_some(),
1665 "expected CascadeCycle error, got: {errors:?}"
1666 );
1667 let ValidationError::CascadeCycle { cycle_ids } = cycle_error.unwrap() else {
1668 unreachable!()
1669 };
1670 assert_eq!(
1671 cycle_ids,
1672 &[EntityId(0), EntityId(1), EntityId(2)],
1673 "cycle_ids must be sorted ascending, got: {cycle_ids:?}"
1674 );
1675 }
1676
1677 #[test]
1678 fn test_cascade_self_loop_detected() {
1679 let bus = make_bus(0);
1681 let mut h0 = make_hydro(0);
1682 h0.downstream_id = Some(EntityId(0));
1683
1684 let result = SystemBuilder::new()
1685 .buses(vec![bus])
1686 .hydros(vec![h0])
1687 .build();
1688
1689 assert!(result.is_err(), "expected Err for self-loop");
1690 let errors = result.unwrap_err();
1691 let has_cycle = errors
1692 .iter()
1693 .any(|e| matches!(e, ValidationError::CascadeCycle { cycle_ids } if cycle_ids.contains(&EntityId(0))));
1694 assert!(
1695 has_cycle,
1696 "expected CascadeCycle containing EntityId(0), got: {errors:?}"
1697 );
1698 }
1699
1700 #[test]
1701 fn test_valid_acyclic_cascade_passes() {
1702 let bus = make_bus(0);
1705 let mut h0 = make_hydro(0);
1706 h0.downstream_id = Some(EntityId(1));
1707 let mut h1 = make_hydro(1);
1708 h1.downstream_id = Some(EntityId(2));
1709 let h2 = make_hydro(2);
1710
1711 let result = SystemBuilder::new()
1712 .buses(vec![bus])
1713 .hydros(vec![h0, h1, h2])
1714 .build();
1715
1716 assert!(
1717 result.is_ok(),
1718 "expected Ok for acyclic cascade, got: {:?}",
1719 result.unwrap_err()
1720 );
1721 let system = result.unwrap_or_else(|_| unreachable!());
1722 assert_eq!(
1723 system.cascade().topological_order().len(),
1724 system.n_hydros(),
1725 "topological_order must contain all hydros"
1726 );
1727 }
1728
1729 #[test]
1732 fn test_filling_without_entry_stage() {
1733 use crate::entities::FillingConfig;
1735 let bus = make_bus(0);
1736 let mut hydro = make_hydro(1);
1737 hydro.entry_stage_id = None;
1738 hydro.filling = Some(FillingConfig {
1739 start_stage_id: 10,
1740 filling_inflow_m3s: 100.0,
1741 });
1742
1743 let result = SystemBuilder::new()
1744 .buses(vec![bus])
1745 .hydros(vec![hydro])
1746 .build();
1747
1748 assert!(
1749 result.is_err(),
1750 "expected Err for filling without entry_stage_id"
1751 );
1752 let errors = result.unwrap_err();
1753 let has_error = errors.iter().any(|e| match e {
1754 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
1755 *hydro_id == EntityId(1) && reason.contains("entry_stage_id")
1756 }
1757 _ => false,
1758 });
1759 assert!(
1760 has_error,
1761 "expected InvalidFillingConfig with entry_stage_id reason, got: {errors:?}"
1762 );
1763 }
1764
1765 #[test]
1766 fn test_filling_negative_inflow() {
1767 use crate::entities::FillingConfig;
1769 let bus = make_bus(0);
1770 let mut hydro = make_hydro(1);
1771 hydro.entry_stage_id = Some(10);
1772 hydro.filling = Some(FillingConfig {
1773 start_stage_id: 10,
1774 filling_inflow_m3s: -5.0,
1775 });
1776
1777 let result = SystemBuilder::new()
1778 .buses(vec![bus])
1779 .hydros(vec![hydro])
1780 .build();
1781
1782 assert!(
1783 result.is_err(),
1784 "expected Err for negative filling_inflow_m3s"
1785 );
1786 let errors = result.unwrap_err();
1787 let has_error = errors.iter().any(|e| match e {
1788 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
1789 *hydro_id == EntityId(1) && reason.contains("filling_inflow_m3s must be positive")
1790 }
1791 _ => false,
1792 });
1793 assert!(
1794 has_error,
1795 "expected InvalidFillingConfig with positive inflow reason, got: {errors:?}"
1796 );
1797 }
1798
1799 #[test]
1800 fn test_valid_filling_config_passes() {
1801 use crate::entities::FillingConfig;
1803 let bus = make_bus(0);
1804 let mut hydro = make_hydro(1);
1805 hydro.entry_stage_id = Some(10);
1806 hydro.filling = Some(FillingConfig {
1807 start_stage_id: 10,
1808 filling_inflow_m3s: 100.0,
1809 });
1810
1811 let result = SystemBuilder::new()
1812 .buses(vec![bus])
1813 .hydros(vec![hydro])
1814 .build();
1815
1816 assert!(
1817 result.is_ok(),
1818 "expected Ok for valid filling config, got: {:?}",
1819 result.unwrap_err()
1820 );
1821 }
1822
1823 #[test]
1824 fn test_cascade_cycle_and_invalid_filling_both_reported() {
1825 use crate::entities::FillingConfig;
1828 let bus = make_bus(0);
1829
1830 let mut h0 = make_hydro(0);
1832 h0.downstream_id = Some(EntityId(0));
1833
1834 let mut h1 = make_hydro(1);
1836 h1.entry_stage_id = None; h1.filling = Some(FillingConfig {
1838 start_stage_id: 5,
1839 filling_inflow_m3s: 50.0,
1840 });
1841
1842 let result = SystemBuilder::new()
1843 .buses(vec![bus])
1844 .hydros(vec![h0, h1])
1845 .build();
1846
1847 assert!(result.is_err(), "expected Err for cycle + invalid filling");
1848 let errors = result.unwrap_err();
1849 let has_cycle = errors
1850 .iter()
1851 .any(|e| matches!(e, ValidationError::CascadeCycle { .. }));
1852 let has_filling = errors
1853 .iter()
1854 .any(|e| matches!(e, ValidationError::InvalidFillingConfig { .. }));
1855 assert!(has_cycle, "expected CascadeCycle error, got: {errors:?}");
1856 assert!(
1857 has_filling,
1858 "expected InvalidFillingConfig error, got: {errors:?}"
1859 );
1860 }
1861
1862 #[cfg(feature = "serde")]
1863 #[test]
1864 fn test_system_serde_roundtrip() {
1865 let bus_a = make_bus(1);
1867 let bus_b = make_bus(2);
1868 let hydro = make_hydro_on_bus(10, 1);
1869 let thermal = make_thermal_on_bus(20, 2);
1870 let line = make_line(1, 1, 2);
1871
1872 let system = SystemBuilder::new()
1873 .buses(vec![bus_a, bus_b])
1874 .hydros(vec![hydro])
1875 .thermals(vec![thermal])
1876 .lines(vec![line])
1877 .build()
1878 .expect("valid system");
1879
1880 let json = serde_json::to_string(&system).unwrap();
1881
1882 let mut deserialized: System = serde_json::from_str(&json).unwrap();
1884 deserialized.rebuild_indices();
1885
1886 assert_eq!(system.buses(), deserialized.buses());
1888 assert_eq!(system.hydros(), deserialized.hydros());
1889 assert_eq!(system.thermals(), deserialized.thermals());
1890 assert_eq!(system.lines(), deserialized.lines());
1891
1892 assert_eq!(
1894 deserialized.bus(EntityId(1)).map(|b| b.id),
1895 Some(EntityId(1))
1896 );
1897 assert_eq!(
1898 deserialized.hydro(EntityId(10)).map(|h| h.id),
1899 Some(EntityId(10))
1900 );
1901 assert_eq!(
1902 deserialized.thermal(EntityId(20)).map(|t| t.id),
1903 Some(EntityId(20))
1904 );
1905 assert_eq!(
1906 deserialized.line(EntityId(1)).map(|l| l.id),
1907 Some(EntityId(1))
1908 );
1909 }
1910
1911 fn make_stage(id: i32) -> Stage {
1914 use crate::temporal::{
1915 Block, BlockMode, NoiseMethod, ScenarioSourceConfig, StageRiskConfig, StageStateConfig,
1916 };
1917 use chrono::NaiveDate;
1918 Stage {
1919 index: usize::try_from(id.max(0)).unwrap_or(0),
1920 id,
1921 start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1922 end_date: NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
1923 season_id: Some(0),
1924 blocks: vec![Block {
1925 index: 0,
1926 name: "SINGLE".to_string(),
1927 duration_hours: 744.0,
1928 }],
1929 block_mode: BlockMode::Parallel,
1930 state_config: StageStateConfig {
1931 storage: true,
1932 inflow_lags: false,
1933 },
1934 risk_config: StageRiskConfig::Expectation,
1935 scenario_config: ScenarioSourceConfig {
1936 branching_factor: 50,
1937 noise_method: NoiseMethod::Saa,
1938 },
1939 }
1940 }
1941
1942 #[test]
1945 fn test_system_backward_compat() {
1946 let system = SystemBuilder::new().build().expect("empty system is valid");
1947 assert_eq!(system.n_buses(), 0);
1949 assert_eq!(system.n_hydros(), 0);
1950 assert_eq!(system.n_stages(), 0);
1952 assert!(system.stages().is_empty());
1953 assert!(system.initial_conditions().storage.is_empty());
1954 assert!(system.generic_constraints().is_empty());
1955 assert!(system.inflow_models().is_empty());
1956 assert!(system.load_models().is_empty());
1957 assert_eq!(system.penalties().n_stages(), 0);
1958 assert_eq!(system.bounds().n_stages(), 0);
1959 }
1960
1961 #[test]
1963 fn test_system_with_stages() {
1964 let s0 = make_stage(0);
1965 let s1 = make_stage(1);
1966
1967 let system = SystemBuilder::new()
1968 .stages(vec![s1.clone(), s0.clone()]) .build()
1970 .expect("valid system");
1971
1972 assert_eq!(system.n_stages(), 2);
1974 assert_eq!(system.stages()[0].id, 0);
1975 assert_eq!(system.stages()[1].id, 1);
1976
1977 let found = system.stage(0).expect("stage 0 must be found");
1979 assert_eq!(found.id, s0.id);
1980
1981 let found1 = system.stage(1).expect("stage 1 must be found");
1982 assert_eq!(found1.id, s1.id);
1983
1984 assert!(system.stage(99).is_none());
1986 }
1987
1988 #[test]
1990 fn test_system_stage_lookup_by_id() {
1991 let stages: Vec<Stage> = [0i32, 1, 2].iter().map(|&id| make_stage(id)).collect();
1992
1993 let system = SystemBuilder::new()
1994 .stages(stages)
1995 .build()
1996 .expect("valid system");
1997
1998 assert_eq!(system.stage(1).map(|s| s.id), Some(1));
1999 assert!(system.stage(99).is_none());
2000 }
2001
2002 #[test]
2004 fn test_system_with_initial_conditions() {
2005 let ic = InitialConditions {
2006 storage: vec![crate::HydroStorage {
2007 hydro_id: EntityId(0),
2008 value_hm3: 15_000.0,
2009 }],
2010 filling_storage: vec![],
2011 };
2012
2013 let system = SystemBuilder::new()
2014 .initial_conditions(ic)
2015 .build()
2016 .expect("valid system");
2017
2018 assert_eq!(system.initial_conditions().storage.len(), 1);
2019 assert_eq!(system.initial_conditions().storage[0].hydro_id, EntityId(0));
2020 assert!((system.initial_conditions().storage[0].value_hm3 - 15_000.0).abs() < f64::EPSILON);
2021 }
2022
2023 #[cfg(feature = "serde")]
2026 #[test]
2027 fn test_system_serde_roundtrip_with_stages() {
2028 use crate::temporal::PolicyGraphType;
2029
2030 let stages = vec![make_stage(0), make_stage(1)];
2031 let policy_graph = PolicyGraph {
2032 graph_type: PolicyGraphType::FiniteHorizon,
2033 annual_discount_rate: 0.0,
2034 transitions: vec![],
2035 season_map: None,
2036 };
2037
2038 let system = SystemBuilder::new()
2039 .stages(stages)
2040 .policy_graph(policy_graph)
2041 .build()
2042 .expect("valid system");
2043
2044 let json = serde_json::to_string(&system).unwrap();
2045 let mut deserialized: System = serde_json::from_str(&json).unwrap();
2046
2047 deserialized.rebuild_indices();
2049
2050 assert_eq!(system.n_stages(), deserialized.n_stages());
2052 assert_eq!(system.stages()[0].id, deserialized.stages()[0].id);
2053 assert_eq!(system.stages()[1].id, deserialized.stages()[1].id);
2054
2055 assert_eq!(deserialized.stage(0).map(|s| s.id), Some(0));
2057 assert_eq!(deserialized.stage(1).map(|s| s.id), Some(1));
2058 assert!(deserialized.stage(99).is_none());
2059
2060 assert_eq!(
2062 deserialized.policy_graph().graph_type,
2063 system.policy_graph().graph_type
2064 );
2065 }
2066}