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 pub fn rebuild_indices(&mut self) {
380 self.bus_index = build_index(&self.buses);
381 self.line_index = build_index(&self.lines);
382 self.hydro_index = build_index(&self.hydros);
383 self.thermal_index = build_index(&self.thermals);
384 self.pumping_station_index = build_index(&self.pumping_stations);
385 self.contract_index = build_index(&self.contracts);
386 self.non_controllable_source_index = build_index(&self.non_controllable_sources);
387 self.stage_index = build_stage_index(&self.stages);
388 }
389}
390
391pub struct SystemBuilder {
415 buses: Vec<Bus>,
416 lines: Vec<Line>,
417 hydros: Vec<Hydro>,
418 thermals: Vec<Thermal>,
419 pumping_stations: Vec<PumpingStation>,
420 contracts: Vec<EnergyContract>,
421 non_controllable_sources: Vec<NonControllableSource>,
422 stages: Vec<Stage>,
424 policy_graph: PolicyGraph,
425 penalties: ResolvedPenalties,
426 bounds: ResolvedBounds,
427 inflow_models: Vec<InflowModel>,
428 load_models: Vec<LoadModel>,
429 correlation: CorrelationModel,
430 initial_conditions: InitialConditions,
431 generic_constraints: Vec<GenericConstraint>,
432 scenario_source: ScenarioSource,
433}
434
435impl Default for SystemBuilder {
436 fn default() -> Self {
437 Self::new()
438 }
439}
440
441impl SystemBuilder {
442 #[must_use]
447 pub fn new() -> Self {
448 Self {
449 buses: Vec::new(),
450 lines: Vec::new(),
451 hydros: Vec::new(),
452 thermals: Vec::new(),
453 pumping_stations: Vec::new(),
454 contracts: Vec::new(),
455 non_controllable_sources: Vec::new(),
456 stages: Vec::new(),
457 policy_graph: PolicyGraph::default(),
458 penalties: ResolvedPenalties::empty(),
459 bounds: ResolvedBounds::empty(),
460 inflow_models: Vec::new(),
461 load_models: Vec::new(),
462 correlation: CorrelationModel::default(),
463 initial_conditions: InitialConditions::default(),
464 generic_constraints: Vec::new(),
465 scenario_source: ScenarioSource::default(),
466 }
467 }
468
469 #[must_use]
471 pub fn buses(mut self, buses: Vec<Bus>) -> Self {
472 self.buses = buses;
473 self
474 }
475
476 #[must_use]
478 pub fn lines(mut self, lines: Vec<Line>) -> Self {
479 self.lines = lines;
480 self
481 }
482
483 #[must_use]
485 pub fn hydros(mut self, hydros: Vec<Hydro>) -> Self {
486 self.hydros = hydros;
487 self
488 }
489
490 #[must_use]
492 pub fn thermals(mut self, thermals: Vec<Thermal>) -> Self {
493 self.thermals = thermals;
494 self
495 }
496
497 #[must_use]
499 pub fn pumping_stations(mut self, stations: Vec<PumpingStation>) -> Self {
500 self.pumping_stations = stations;
501 self
502 }
503
504 #[must_use]
506 pub fn contracts(mut self, contracts: Vec<EnergyContract>) -> Self {
507 self.contracts = contracts;
508 self
509 }
510
511 #[must_use]
513 pub fn non_controllable_sources(mut self, sources: Vec<NonControllableSource>) -> Self {
514 self.non_controllable_sources = sources;
515 self
516 }
517
518 #[must_use]
522 pub fn stages(mut self, stages: Vec<Stage>) -> Self {
523 self.stages = stages;
524 self
525 }
526
527 #[must_use]
529 pub fn policy_graph(mut self, policy_graph: PolicyGraph) -> Self {
530 self.policy_graph = policy_graph;
531 self
532 }
533
534 #[must_use]
538 pub fn penalties(mut self, penalties: ResolvedPenalties) -> Self {
539 self.penalties = penalties;
540 self
541 }
542
543 #[must_use]
547 pub fn bounds(mut self, bounds: ResolvedBounds) -> Self {
548 self.bounds = bounds;
549 self
550 }
551
552 #[must_use]
554 pub fn inflow_models(mut self, inflow_models: Vec<InflowModel>) -> Self {
555 self.inflow_models = inflow_models;
556 self
557 }
558
559 #[must_use]
561 pub fn load_models(mut self, load_models: Vec<LoadModel>) -> Self {
562 self.load_models = load_models;
563 self
564 }
565
566 #[must_use]
568 pub fn correlation(mut self, correlation: CorrelationModel) -> Self {
569 self.correlation = correlation;
570 self
571 }
572
573 #[must_use]
575 pub fn initial_conditions(mut self, initial_conditions: InitialConditions) -> Self {
576 self.initial_conditions = initial_conditions;
577 self
578 }
579
580 #[must_use]
584 pub fn generic_constraints(mut self, generic_constraints: Vec<GenericConstraint>) -> Self {
585 self.generic_constraints = generic_constraints;
586 self
587 }
588
589 #[must_use]
591 pub fn scenario_source(mut self, scenario_source: ScenarioSource) -> Self {
592 self.scenario_source = scenario_source;
593 self
594 }
595
596 pub fn build(mut self) -> Result<System, Vec<ValidationError>> {
621 self.buses.sort_by_key(|e| e.id.0);
622 self.lines.sort_by_key(|e| e.id.0);
623 self.hydros.sort_by_key(|e| e.id.0);
624 self.thermals.sort_by_key(|e| e.id.0);
625 self.pumping_stations.sort_by_key(|e| e.id.0);
626 self.contracts.sort_by_key(|e| e.id.0);
627 self.non_controllable_sources.sort_by_key(|e| e.id.0);
628 self.stages.sort_by_key(|s| s.id);
629 self.generic_constraints.sort_by_key(|c| c.id.0);
630
631 let mut errors: Vec<ValidationError> = Vec::new();
632 check_duplicates(&self.buses, "Bus", &mut errors);
633 check_duplicates(&self.lines, "Line", &mut errors);
634 check_duplicates(&self.hydros, "Hydro", &mut errors);
635 check_duplicates(&self.thermals, "Thermal", &mut errors);
636 check_duplicates(&self.pumping_stations, "PumpingStation", &mut errors);
637 check_duplicates(&self.contracts, "EnergyContract", &mut errors);
638 check_duplicates(
639 &self.non_controllable_sources,
640 "NonControllableSource",
641 &mut errors,
642 );
643
644 if !errors.is_empty() {
645 return Err(errors);
646 }
647
648 let bus_index = build_index(&self.buses);
649 let line_index = build_index(&self.lines);
650 let hydro_index = build_index(&self.hydros);
651 let thermal_index = build_index(&self.thermals);
652 let pumping_station_index = build_index(&self.pumping_stations);
653 let contract_index = build_index(&self.contracts);
654 let non_controllable_source_index = build_index(&self.non_controllable_sources);
655
656 validate_cross_references(
657 &self.lines,
658 &self.hydros,
659 &self.thermals,
660 &self.pumping_stations,
661 &self.contracts,
662 &self.non_controllable_sources,
663 &bus_index,
664 &hydro_index,
665 &mut errors,
666 );
667
668 if !errors.is_empty() {
669 return Err(errors);
670 }
671
672 let cascade = CascadeTopology::build(&self.hydros);
673
674 if cascade.topological_order().len() < self.hydros.len() {
675 let in_topo: HashSet<EntityId> = cascade.topological_order().iter().copied().collect();
676 let mut cycle_ids: Vec<EntityId> = self
677 .hydros
678 .iter()
679 .map(|h| h.id)
680 .filter(|id| !in_topo.contains(id))
681 .collect();
682 cycle_ids.sort_by_key(|id| id.0);
683 errors.push(ValidationError::CascadeCycle { cycle_ids });
684 }
685
686 validate_filling_configs(&self.hydros, &mut errors);
687
688 if !errors.is_empty() {
689 return Err(errors);
690 }
691
692 let network = NetworkTopology::build(
693 &self.buses,
694 &self.lines,
695 &self.hydros,
696 &self.thermals,
697 &self.non_controllable_sources,
698 &self.contracts,
699 &self.pumping_stations,
700 );
701
702 let stage_index = build_stage_index(&self.stages);
703
704 Ok(System {
705 buses: self.buses,
706 lines: self.lines,
707 hydros: self.hydros,
708 thermals: self.thermals,
709 pumping_stations: self.pumping_stations,
710 contracts: self.contracts,
711 non_controllable_sources: self.non_controllable_sources,
712 bus_index,
713 line_index,
714 hydro_index,
715 thermal_index,
716 pumping_station_index,
717 contract_index,
718 non_controllable_source_index,
719 cascade,
720 network,
721 stages: self.stages,
722 policy_graph: self.policy_graph,
723 stage_index,
724 penalties: self.penalties,
725 bounds: self.bounds,
726 inflow_models: self.inflow_models,
727 load_models: self.load_models,
728 correlation: self.correlation,
729 initial_conditions: self.initial_conditions,
730 generic_constraints: self.generic_constraints,
731 scenario_source: self.scenario_source,
732 })
733 }
734}
735
736trait HasId {
737 fn entity_id(&self) -> EntityId;
738}
739
740impl HasId for Bus {
741 fn entity_id(&self) -> EntityId {
742 self.id
743 }
744}
745impl HasId for Line {
746 fn entity_id(&self) -> EntityId {
747 self.id
748 }
749}
750impl HasId for Hydro {
751 fn entity_id(&self) -> EntityId {
752 self.id
753 }
754}
755impl HasId for Thermal {
756 fn entity_id(&self) -> EntityId {
757 self.id
758 }
759}
760impl HasId for PumpingStation {
761 fn entity_id(&self) -> EntityId {
762 self.id
763 }
764}
765impl HasId for EnergyContract {
766 fn entity_id(&self) -> EntityId {
767 self.id
768 }
769}
770impl HasId for NonControllableSource {
771 fn entity_id(&self) -> EntityId {
772 self.id
773 }
774}
775
776fn build_index<T: HasId>(entities: &[T]) -> HashMap<EntityId, usize> {
777 let mut index = HashMap::with_capacity(entities.len());
778 for (i, entity) in entities.iter().enumerate() {
779 index.insert(entity.entity_id(), i);
780 }
781 index
782}
783
784fn build_stage_index(stages: &[Stage]) -> HashMap<i32, usize> {
788 let mut index = HashMap::with_capacity(stages.len());
789 for (i, stage) in stages.iter().enumerate() {
790 index.insert(stage.id, i);
791 }
792 index
793}
794
795fn check_duplicates<T: HasId>(
796 entities: &[T],
797 entity_type: &'static str,
798 errors: &mut Vec<ValidationError>,
799) {
800 for window in entities.windows(2) {
801 if window[0].entity_id() == window[1].entity_id() {
802 errors.push(ValidationError::DuplicateId {
803 entity_type,
804 id: window[0].entity_id(),
805 });
806 }
807 }
808}
809
810#[allow(clippy::too_many_arguments)]
819fn validate_cross_references(
820 lines: &[Line],
821 hydros: &[Hydro],
822 thermals: &[Thermal],
823 pumping_stations: &[PumpingStation],
824 contracts: &[EnergyContract],
825 non_controllable_sources: &[NonControllableSource],
826 bus_index: &HashMap<EntityId, usize>,
827 hydro_index: &HashMap<EntityId, usize>,
828 errors: &mut Vec<ValidationError>,
829) {
830 validate_line_refs(lines, bus_index, errors);
831 validate_hydro_refs(hydros, bus_index, hydro_index, errors);
832 validate_thermal_refs(thermals, bus_index, errors);
833 validate_pumping_station_refs(pumping_stations, bus_index, hydro_index, errors);
834 validate_contract_refs(contracts, bus_index, errors);
835 validate_ncs_refs(non_controllable_sources, bus_index, errors);
836}
837
838fn validate_line_refs(
839 lines: &[Line],
840 bus_index: &HashMap<EntityId, usize>,
841 errors: &mut Vec<ValidationError>,
842) {
843 for line in lines {
844 if !bus_index.contains_key(&line.source_bus_id) {
845 errors.push(ValidationError::InvalidReference {
846 source_entity_type: "Line",
847 source_id: line.id,
848 field_name: "source_bus_id",
849 referenced_id: line.source_bus_id,
850 expected_type: "Bus",
851 });
852 }
853 if !bus_index.contains_key(&line.target_bus_id) {
854 errors.push(ValidationError::InvalidReference {
855 source_entity_type: "Line",
856 source_id: line.id,
857 field_name: "target_bus_id",
858 referenced_id: line.target_bus_id,
859 expected_type: "Bus",
860 });
861 }
862 }
863}
864
865fn validate_hydro_refs(
866 hydros: &[Hydro],
867 bus_index: &HashMap<EntityId, usize>,
868 hydro_index: &HashMap<EntityId, usize>,
869 errors: &mut Vec<ValidationError>,
870) {
871 for hydro in hydros {
872 if !bus_index.contains_key(&hydro.bus_id) {
873 errors.push(ValidationError::InvalidReference {
874 source_entity_type: "Hydro",
875 source_id: hydro.id,
876 field_name: "bus_id",
877 referenced_id: hydro.bus_id,
878 expected_type: "Bus",
879 });
880 }
881 if let Some(downstream_id) = hydro.downstream_id {
882 if !hydro_index.contains_key(&downstream_id) {
883 errors.push(ValidationError::InvalidReference {
884 source_entity_type: "Hydro",
885 source_id: hydro.id,
886 field_name: "downstream_id",
887 referenced_id: downstream_id,
888 expected_type: "Hydro",
889 });
890 }
891 }
892 if let Some(ref diversion) = hydro.diversion {
893 if !hydro_index.contains_key(&diversion.downstream_id) {
894 errors.push(ValidationError::InvalidReference {
895 source_entity_type: "Hydro",
896 source_id: hydro.id,
897 field_name: "diversion.downstream_id",
898 referenced_id: diversion.downstream_id,
899 expected_type: "Hydro",
900 });
901 }
902 }
903 }
904}
905
906fn validate_thermal_refs(
907 thermals: &[Thermal],
908 bus_index: &HashMap<EntityId, usize>,
909 errors: &mut Vec<ValidationError>,
910) {
911 for thermal in thermals {
912 if !bus_index.contains_key(&thermal.bus_id) {
913 errors.push(ValidationError::InvalidReference {
914 source_entity_type: "Thermal",
915 source_id: thermal.id,
916 field_name: "bus_id",
917 referenced_id: thermal.bus_id,
918 expected_type: "Bus",
919 });
920 }
921 }
922}
923
924fn validate_pumping_station_refs(
925 pumping_stations: &[PumpingStation],
926 bus_index: &HashMap<EntityId, usize>,
927 hydro_index: &HashMap<EntityId, usize>,
928 errors: &mut Vec<ValidationError>,
929) {
930 for ps in pumping_stations {
931 if !bus_index.contains_key(&ps.bus_id) {
932 errors.push(ValidationError::InvalidReference {
933 source_entity_type: "PumpingStation",
934 source_id: ps.id,
935 field_name: "bus_id",
936 referenced_id: ps.bus_id,
937 expected_type: "Bus",
938 });
939 }
940 if !hydro_index.contains_key(&ps.source_hydro_id) {
941 errors.push(ValidationError::InvalidReference {
942 source_entity_type: "PumpingStation",
943 source_id: ps.id,
944 field_name: "source_hydro_id",
945 referenced_id: ps.source_hydro_id,
946 expected_type: "Hydro",
947 });
948 }
949 if !hydro_index.contains_key(&ps.destination_hydro_id) {
950 errors.push(ValidationError::InvalidReference {
951 source_entity_type: "PumpingStation",
952 source_id: ps.id,
953 field_name: "destination_hydro_id",
954 referenced_id: ps.destination_hydro_id,
955 expected_type: "Hydro",
956 });
957 }
958 }
959}
960
961fn validate_contract_refs(
962 contracts: &[EnergyContract],
963 bus_index: &HashMap<EntityId, usize>,
964 errors: &mut Vec<ValidationError>,
965) {
966 for contract in contracts {
967 if !bus_index.contains_key(&contract.bus_id) {
968 errors.push(ValidationError::InvalidReference {
969 source_entity_type: "EnergyContract",
970 source_id: contract.id,
971 field_name: "bus_id",
972 referenced_id: contract.bus_id,
973 expected_type: "Bus",
974 });
975 }
976 }
977}
978
979fn validate_ncs_refs(
980 non_controllable_sources: &[NonControllableSource],
981 bus_index: &HashMap<EntityId, usize>,
982 errors: &mut Vec<ValidationError>,
983) {
984 for ncs in non_controllable_sources {
985 if !bus_index.contains_key(&ncs.bus_id) {
986 errors.push(ValidationError::InvalidReference {
987 source_entity_type: "NonControllableSource",
988 source_id: ncs.id,
989 field_name: "bus_id",
990 referenced_id: ncs.bus_id,
991 expected_type: "Bus",
992 });
993 }
994 }
995}
996
997fn validate_filling_configs(hydros: &[Hydro], errors: &mut Vec<ValidationError>) {
1005 for hydro in hydros {
1006 if let Some(filling) = &hydro.filling {
1007 if filling.filling_inflow_m3s.is_nan() || filling.filling_inflow_m3s <= 0.0 {
1008 errors.push(ValidationError::InvalidFillingConfig {
1009 hydro_id: hydro.id,
1010 reason: "filling_inflow_m3s must be positive".to_string(),
1011 });
1012 }
1013 if hydro.entry_stage_id.is_none() {
1014 errors.push(ValidationError::InvalidFillingConfig {
1015 hydro_id: hydro.id,
1016 reason: "filling requires entry_stage_id to be set".to_string(),
1017 });
1018 }
1019 }
1020 }
1021}
1022
1023#[cfg(test)]
1024mod tests {
1025 use super::*;
1026 use crate::entities::{ContractType, HydroGenerationModel, HydroPenalties, ThermalCostSegment};
1027
1028 fn make_bus(id: i32) -> Bus {
1029 Bus {
1030 id: EntityId(id),
1031 name: format!("bus-{id}"),
1032 deficit_segments: vec![],
1033 excess_cost: 0.0,
1034 }
1035 }
1036
1037 fn make_line(id: i32, source_bus_id: i32, target_bus_id: i32) -> Line {
1038 crate::Line {
1039 id: EntityId(id),
1040 name: format!("line-{id}"),
1041 source_bus_id: EntityId(source_bus_id),
1042 target_bus_id: EntityId(target_bus_id),
1043 entry_stage_id: None,
1044 exit_stage_id: None,
1045 direct_capacity_mw: 100.0,
1046 reverse_capacity_mw: 100.0,
1047 losses_percent: 0.0,
1048 exchange_cost: 0.0,
1049 }
1050 }
1051
1052 fn make_hydro_on_bus(id: i32, bus_id: i32) -> Hydro {
1053 let zero_penalties = HydroPenalties {
1054 spillage_cost: 0.0,
1055 diversion_cost: 0.0,
1056 fpha_turbined_cost: 0.0,
1057 storage_violation_below_cost: 0.0,
1058 filling_target_violation_cost: 0.0,
1059 turbined_violation_below_cost: 0.0,
1060 outflow_violation_below_cost: 0.0,
1061 outflow_violation_above_cost: 0.0,
1062 generation_violation_below_cost: 0.0,
1063 evaporation_violation_cost: 0.0,
1064 water_withdrawal_violation_cost: 0.0,
1065 };
1066 Hydro {
1067 id: EntityId(id),
1068 name: format!("hydro-{id}"),
1069 bus_id: EntityId(bus_id),
1070 downstream_id: None,
1071 entry_stage_id: None,
1072 exit_stage_id: None,
1073 min_storage_hm3: 0.0,
1074 max_storage_hm3: 1.0,
1075 min_outflow_m3s: 0.0,
1076 max_outflow_m3s: None,
1077 generation_model: HydroGenerationModel::ConstantProductivity {
1078 productivity_mw_per_m3s: 1.0,
1079 },
1080 min_turbined_m3s: 0.0,
1081 max_turbined_m3s: 1.0,
1082 min_generation_mw: 0.0,
1083 max_generation_mw: 1.0,
1084 tailrace: None,
1085 hydraulic_losses: None,
1086 efficiency: None,
1087 evaporation_coefficients_mm: None,
1088 diversion: None,
1089 filling: None,
1090 penalties: zero_penalties,
1091 }
1092 }
1093
1094 fn make_hydro(id: i32) -> Hydro {
1096 make_hydro_on_bus(id, 0)
1097 }
1098
1099 fn make_thermal_on_bus(id: i32, bus_id: i32) -> Thermal {
1100 Thermal {
1101 id: EntityId(id),
1102 name: format!("thermal-{id}"),
1103 bus_id: EntityId(bus_id),
1104 entry_stage_id: None,
1105 exit_stage_id: None,
1106 cost_segments: vec![ThermalCostSegment {
1107 capacity_mw: 100.0,
1108 cost_per_mwh: 50.0,
1109 }],
1110 min_generation_mw: 0.0,
1111 max_generation_mw: 100.0,
1112 gnl_config: None,
1113 }
1114 }
1115
1116 fn make_thermal(id: i32) -> Thermal {
1118 make_thermal_on_bus(id, 0)
1119 }
1120
1121 fn make_pumping_station_full(
1122 id: i32,
1123 bus_id: i32,
1124 source_hydro_id: i32,
1125 destination_hydro_id: i32,
1126 ) -> PumpingStation {
1127 PumpingStation {
1128 id: EntityId(id),
1129 name: format!("ps-{id}"),
1130 bus_id: EntityId(bus_id),
1131 source_hydro_id: EntityId(source_hydro_id),
1132 destination_hydro_id: EntityId(destination_hydro_id),
1133 entry_stage_id: None,
1134 exit_stage_id: None,
1135 consumption_mw_per_m3s: 0.5,
1136 min_flow_m3s: 0.0,
1137 max_flow_m3s: 10.0,
1138 }
1139 }
1140
1141 fn make_pumping_station(id: i32) -> PumpingStation {
1142 make_pumping_station_full(id, 0, 0, 1)
1143 }
1144
1145 fn make_contract_on_bus(id: i32, bus_id: i32) -> EnergyContract {
1146 EnergyContract {
1147 id: EntityId(id),
1148 name: format!("contract-{id}"),
1149 bus_id: EntityId(bus_id),
1150 contract_type: ContractType::Import,
1151 entry_stage_id: None,
1152 exit_stage_id: None,
1153 price_per_mwh: 0.0,
1154 min_mw: 0.0,
1155 max_mw: 100.0,
1156 }
1157 }
1158
1159 fn make_contract(id: i32) -> EnergyContract {
1160 make_contract_on_bus(id, 0)
1161 }
1162
1163 fn make_ncs_on_bus(id: i32, bus_id: i32) -> NonControllableSource {
1164 NonControllableSource {
1165 id: EntityId(id),
1166 name: format!("ncs-{id}"),
1167 bus_id: EntityId(bus_id),
1168 entry_stage_id: None,
1169 exit_stage_id: None,
1170 max_generation_mw: 50.0,
1171 curtailment_cost: 0.0,
1172 }
1173 }
1174
1175 fn make_ncs(id: i32) -> NonControllableSource {
1176 make_ncs_on_bus(id, 0)
1177 }
1178
1179 #[test]
1180 fn test_empty_system() {
1181 let system = SystemBuilder::new().build().expect("empty system is valid");
1182 assert_eq!(system.n_buses(), 0);
1183 assert_eq!(system.n_lines(), 0);
1184 assert_eq!(system.n_hydros(), 0);
1185 assert_eq!(system.n_thermals(), 0);
1186 assert_eq!(system.n_pumping_stations(), 0);
1187 assert_eq!(system.n_contracts(), 0);
1188 assert_eq!(system.n_non_controllable_sources(), 0);
1189 assert!(system.buses().is_empty());
1190 assert!(system.cascade().is_empty());
1191 }
1192
1193 #[test]
1194 fn test_canonical_ordering() {
1195 let system = SystemBuilder::new()
1197 .buses(vec![make_bus(2), make_bus(1), make_bus(0)])
1198 .build()
1199 .expect("valid system");
1200
1201 assert_eq!(system.buses()[0].id, EntityId(0));
1202 assert_eq!(system.buses()[1].id, EntityId(1));
1203 assert_eq!(system.buses()[2].id, EntityId(2));
1204 }
1205
1206 #[test]
1207 fn test_lookup_by_id() {
1208 let system = SystemBuilder::new()
1210 .buses(vec![make_bus(0)])
1211 .hydros(vec![make_hydro(10), make_hydro(5), make_hydro(20)])
1212 .build()
1213 .expect("valid system");
1214
1215 assert_eq!(system.hydro(EntityId(5)).map(|h| h.id), Some(EntityId(5)));
1216 assert_eq!(system.hydro(EntityId(10)).map(|h| h.id), Some(EntityId(10)));
1217 assert_eq!(system.hydro(EntityId(20)).map(|h| h.id), Some(EntityId(20)));
1218 }
1219
1220 #[test]
1221 fn test_lookup_missing_id() {
1222 let system = SystemBuilder::new()
1224 .buses(vec![make_bus(0)])
1225 .hydros(vec![make_hydro(1), make_hydro(2)])
1226 .build()
1227 .expect("valid system");
1228
1229 assert!(system.hydro(EntityId(999)).is_none());
1230 }
1231
1232 #[test]
1233 fn test_count_queries() {
1234 let system = SystemBuilder::new()
1235 .buses(vec![make_bus(0), make_bus(1)])
1236 .lines(vec![make_line(0, 0, 1)])
1237 .hydros(vec![make_hydro(0), make_hydro(1), make_hydro(2)])
1238 .thermals(vec![make_thermal(0)])
1239 .pumping_stations(vec![make_pumping_station(0)])
1240 .contracts(vec![make_contract(0), make_contract(1)])
1241 .non_controllable_sources(vec![make_ncs(0)])
1242 .build()
1243 .expect("valid system");
1244
1245 assert_eq!(system.n_buses(), 2);
1246 assert_eq!(system.n_lines(), 1);
1247 assert_eq!(system.n_hydros(), 3);
1248 assert_eq!(system.n_thermals(), 1);
1249 assert_eq!(system.n_pumping_stations(), 1);
1250 assert_eq!(system.n_contracts(), 2);
1251 assert_eq!(system.n_non_controllable_sources(), 1);
1252 }
1253
1254 #[test]
1255 fn test_slice_accessors() {
1256 let system = SystemBuilder::new()
1257 .buses(vec![make_bus(0), make_bus(1), make_bus(2)])
1258 .build()
1259 .expect("valid system");
1260
1261 let buses = system.buses();
1262 assert_eq!(buses.len(), 3);
1263 assert_eq!(buses[0].id, EntityId(0));
1264 assert_eq!(buses[1].id, EntityId(1));
1265 assert_eq!(buses[2].id, EntityId(2));
1266 }
1267
1268 #[test]
1269 fn test_duplicate_id_error() {
1270 let result = SystemBuilder::new()
1272 .buses(vec![make_bus(0), make_bus(0)])
1273 .build();
1274
1275 assert!(result.is_err());
1276 let errors = result.unwrap_err();
1277 assert!(!errors.is_empty());
1278 assert!(errors.iter().any(|e| matches!(
1279 e,
1280 ValidationError::DuplicateId {
1281 entity_type: "Bus",
1282 id: EntityId(0),
1283 }
1284 )));
1285 }
1286
1287 #[test]
1288 fn test_multiple_duplicate_errors() {
1289 let result = SystemBuilder::new()
1291 .buses(vec![make_bus(0), make_bus(0)])
1292 .thermals(vec![make_thermal(5), make_thermal(5)])
1293 .build();
1294
1295 assert!(result.is_err());
1296 let errors = result.unwrap_err();
1297
1298 let has_bus_dup = errors.iter().any(|e| {
1299 matches!(
1300 e,
1301 ValidationError::DuplicateId {
1302 entity_type: "Bus",
1303 ..
1304 }
1305 )
1306 });
1307 let has_thermal_dup = errors.iter().any(|e| {
1308 matches!(
1309 e,
1310 ValidationError::DuplicateId {
1311 entity_type: "Thermal",
1312 ..
1313 }
1314 )
1315 });
1316 assert!(has_bus_dup, "expected Bus duplicate error");
1317 assert!(has_thermal_dup, "expected Thermal duplicate error");
1318 }
1319
1320 #[test]
1321 fn test_send_sync() {
1322 fn require_send_sync<T: Send + Sync>(_: T) {}
1323 let system = SystemBuilder::new().build().expect("valid system");
1324 require_send_sync(system);
1325 }
1326
1327 #[test]
1328 fn test_cascade_accessible() {
1329 let mut h0 = make_hydro_on_bus(0, 0);
1331 h0.downstream_id = Some(EntityId(1));
1332 let mut h1 = make_hydro_on_bus(1, 0);
1333 h1.downstream_id = Some(EntityId(2));
1334 let h2 = make_hydro_on_bus(2, 0);
1335
1336 let system = SystemBuilder::new()
1337 .buses(vec![make_bus(0)])
1338 .hydros(vec![h0, h1, h2])
1339 .build()
1340 .expect("valid system");
1341
1342 let order = system.cascade().topological_order();
1343 assert!(!order.is_empty(), "topological order must be non-empty");
1344 let pos_0 = order
1345 .iter()
1346 .position(|&id| id == EntityId(0))
1347 .expect("EntityId(0) must be in topological order");
1348 let pos_2 = order
1349 .iter()
1350 .position(|&id| id == EntityId(2))
1351 .expect("EntityId(2) must be in topological order");
1352 assert!(pos_0 < pos_2, "EntityId(0) must precede EntityId(2)");
1353 }
1354
1355 #[test]
1356 fn test_network_accessible() {
1357 let system = SystemBuilder::new()
1358 .buses(vec![make_bus(0), make_bus(1)])
1359 .lines(vec![make_line(0, 0, 1)])
1360 .build()
1361 .expect("valid system");
1362
1363 let connections = system.network().bus_lines(EntityId(0));
1364 assert!(!connections.is_empty(), "bus 0 must have connections");
1365 assert_eq!(connections[0].line_id, EntityId(0));
1366 }
1367
1368 #[test]
1369 fn test_all_entity_lookups() {
1370 let system = SystemBuilder::new()
1375 .buses(vec![make_bus(0), make_bus(1)])
1376 .lines(vec![make_line(2, 0, 1)])
1377 .hydros(vec![
1378 make_hydro_on_bus(0, 0),
1379 make_hydro_on_bus(1, 0),
1380 make_hydro_on_bus(3, 0),
1381 ])
1382 .thermals(vec![make_thermal(4)])
1383 .pumping_stations(vec![make_pumping_station(5)])
1384 .contracts(vec![make_contract(6)])
1385 .non_controllable_sources(vec![make_ncs(7)])
1386 .build()
1387 .expect("valid system");
1388
1389 assert!(system.bus(EntityId(1)).is_some());
1390 assert!(system.line(EntityId(2)).is_some());
1391 assert!(system.hydro(EntityId(3)).is_some());
1392 assert!(system.thermal(EntityId(4)).is_some());
1393 assert!(system.pumping_station(EntityId(5)).is_some());
1394 assert!(system.contract(EntityId(6)).is_some());
1395 assert!(system.non_controllable_source(EntityId(7)).is_some());
1396
1397 assert!(system.bus(EntityId(999)).is_none());
1398 assert!(system.line(EntityId(999)).is_none());
1399 assert!(system.hydro(EntityId(999)).is_none());
1400 assert!(system.thermal(EntityId(999)).is_none());
1401 assert!(system.pumping_station(EntityId(999)).is_none());
1402 assert!(system.contract(EntityId(999)).is_none());
1403 assert!(system.non_controllable_source(EntityId(999)).is_none());
1404 }
1405
1406 #[test]
1407 fn test_default_builder() {
1408 let system = SystemBuilder::default()
1409 .build()
1410 .expect("default builder produces valid empty system");
1411 assert_eq!(system.n_buses(), 0);
1412 }
1413
1414 #[test]
1417 fn test_invalid_bus_reference_hydro() {
1418 let hydro = make_hydro_on_bus(1, 99);
1420 let result = SystemBuilder::new().hydros(vec![hydro]).build();
1421
1422 assert!(result.is_err(), "expected Err for missing bus reference");
1423 let errors = result.unwrap_err();
1424 assert!(
1425 errors.iter().any(|e| matches!(
1426 e,
1427 ValidationError::InvalidReference {
1428 source_entity_type: "Hydro",
1429 source_id: EntityId(1),
1430 field_name: "bus_id",
1431 referenced_id: EntityId(99),
1432 expected_type: "Bus",
1433 }
1434 )),
1435 "expected InvalidReference for Hydro bus_id=99, got: {errors:?}"
1436 );
1437 }
1438
1439 #[test]
1440 fn test_invalid_downstream_reference() {
1441 let bus = make_bus(0);
1443 let mut hydro = make_hydro(1);
1444 hydro.downstream_id = Some(EntityId(50));
1445
1446 let result = SystemBuilder::new()
1447 .buses(vec![bus])
1448 .hydros(vec![hydro])
1449 .build();
1450
1451 assert!(
1452 result.is_err(),
1453 "expected Err for missing downstream reference"
1454 );
1455 let errors = result.unwrap_err();
1456 assert!(
1457 errors.iter().any(|e| matches!(
1458 e,
1459 ValidationError::InvalidReference {
1460 source_entity_type: "Hydro",
1461 source_id: EntityId(1),
1462 field_name: "downstream_id",
1463 referenced_id: EntityId(50),
1464 expected_type: "Hydro",
1465 }
1466 )),
1467 "expected InvalidReference for Hydro downstream_id=50, got: {errors:?}"
1468 );
1469 }
1470
1471 #[test]
1472 fn test_invalid_pumping_station_hydro_refs() {
1473 let bus = make_bus(0);
1475 let dest_hydro = make_hydro(1);
1476 let ps = make_pumping_station_full(10, 0, 77, 1);
1477
1478 let result = SystemBuilder::new()
1479 .buses(vec![bus])
1480 .hydros(vec![dest_hydro])
1481 .pumping_stations(vec![ps])
1482 .build();
1483
1484 assert!(
1485 result.is_err(),
1486 "expected Err for missing source_hydro_id reference"
1487 );
1488 let errors = result.unwrap_err();
1489 assert!(
1490 errors.iter().any(|e| matches!(
1491 e,
1492 ValidationError::InvalidReference {
1493 source_entity_type: "PumpingStation",
1494 source_id: EntityId(10),
1495 field_name: "source_hydro_id",
1496 referenced_id: EntityId(77),
1497 expected_type: "Hydro",
1498 }
1499 )),
1500 "expected InvalidReference for PumpingStation source_hydro_id=77, got: {errors:?}"
1501 );
1502 }
1503
1504 #[test]
1505 fn test_multiple_invalid_references_collected() {
1506 let line = make_line(1, 99, 0);
1509 let thermal = make_thermal_on_bus(2, 88);
1510
1511 let result = SystemBuilder::new()
1512 .buses(vec![make_bus(0)])
1513 .lines(vec![line])
1514 .thermals(vec![thermal])
1515 .build();
1516
1517 assert!(
1518 result.is_err(),
1519 "expected Err for multiple invalid references"
1520 );
1521 let errors = result.unwrap_err();
1522
1523 let has_line_error = errors.iter().any(|e| {
1524 matches!(
1525 e,
1526 ValidationError::InvalidReference {
1527 source_entity_type: "Line",
1528 field_name: "source_bus_id",
1529 referenced_id: EntityId(99),
1530 ..
1531 }
1532 )
1533 });
1534 let has_thermal_error = errors.iter().any(|e| {
1535 matches!(
1536 e,
1537 ValidationError::InvalidReference {
1538 source_entity_type: "Thermal",
1539 field_name: "bus_id",
1540 referenced_id: EntityId(88),
1541 ..
1542 }
1543 )
1544 });
1545
1546 assert!(
1547 has_line_error,
1548 "expected Line source_bus_id=99 error, got: {errors:?}"
1549 );
1550 assert!(
1551 has_thermal_error,
1552 "expected Thermal bus_id=88 error, got: {errors:?}"
1553 );
1554 assert!(
1555 errors.len() >= 2,
1556 "expected at least 2 errors, got {}: {errors:?}",
1557 errors.len()
1558 );
1559 }
1560
1561 #[test]
1562 fn test_valid_cross_references_pass() {
1563 let bus_0 = make_bus(0);
1565 let bus_1 = make_bus(1);
1566 let h0 = make_hydro_on_bus(0, 0);
1567 let h1 = make_hydro_on_bus(1, 1);
1568 let mut h2 = make_hydro_on_bus(2, 0);
1569 h2.downstream_id = Some(EntityId(1));
1570 let line = make_line(10, 0, 1);
1571 let thermal = make_thermal_on_bus(20, 0);
1572 let ps = make_pumping_station_full(30, 0, 0, 1);
1573 let contract = make_contract_on_bus(40, 1);
1574 let ncs = make_ncs_on_bus(50, 0);
1575
1576 let result = SystemBuilder::new()
1577 .buses(vec![bus_0, bus_1])
1578 .lines(vec![line])
1579 .hydros(vec![h0, h1, h2])
1580 .thermals(vec![thermal])
1581 .pumping_stations(vec![ps])
1582 .contracts(vec![contract])
1583 .non_controllable_sources(vec![ncs])
1584 .build();
1585
1586 assert!(
1587 result.is_ok(),
1588 "expected Ok for all valid cross-references, got: {:?}",
1589 result.unwrap_err()
1590 );
1591 let system = result.unwrap_or_else(|_| unreachable!());
1592 assert_eq!(system.n_buses(), 2);
1593 assert_eq!(system.n_hydros(), 3);
1594 assert_eq!(system.n_lines(), 1);
1595 assert_eq!(system.n_thermals(), 1);
1596 assert_eq!(system.n_pumping_stations(), 1);
1597 assert_eq!(system.n_contracts(), 1);
1598 assert_eq!(system.n_non_controllable_sources(), 1);
1599 }
1600
1601 #[test]
1604 fn test_cascade_cycle_detected() {
1605 let bus = make_bus(0);
1608 let mut h0 = make_hydro(0);
1609 h0.downstream_id = Some(EntityId(1));
1610 let mut h1 = make_hydro(1);
1611 h1.downstream_id = Some(EntityId(2));
1612 let mut h2 = make_hydro(2);
1613 h2.downstream_id = Some(EntityId(0));
1614
1615 let result = SystemBuilder::new()
1616 .buses(vec![bus])
1617 .hydros(vec![h0, h1, h2])
1618 .build();
1619
1620 assert!(result.is_err(), "expected Err for 3-node cycle");
1621 let errors = result.unwrap_err();
1622 let cycle_error = errors
1623 .iter()
1624 .find(|e| matches!(e, ValidationError::CascadeCycle { .. }));
1625 assert!(
1626 cycle_error.is_some(),
1627 "expected CascadeCycle error, got: {errors:?}"
1628 );
1629 let ValidationError::CascadeCycle { cycle_ids } = cycle_error.unwrap() else {
1630 unreachable!()
1631 };
1632 assert_eq!(
1633 cycle_ids,
1634 &[EntityId(0), EntityId(1), EntityId(2)],
1635 "cycle_ids must be sorted ascending, got: {cycle_ids:?}"
1636 );
1637 }
1638
1639 #[test]
1640 fn test_cascade_self_loop_detected() {
1641 let bus = make_bus(0);
1643 let mut h0 = make_hydro(0);
1644 h0.downstream_id = Some(EntityId(0));
1645
1646 let result = SystemBuilder::new()
1647 .buses(vec![bus])
1648 .hydros(vec![h0])
1649 .build();
1650
1651 assert!(result.is_err(), "expected Err for self-loop");
1652 let errors = result.unwrap_err();
1653 let has_cycle = errors
1654 .iter()
1655 .any(|e| matches!(e, ValidationError::CascadeCycle { cycle_ids } if cycle_ids.contains(&EntityId(0))));
1656 assert!(
1657 has_cycle,
1658 "expected CascadeCycle containing EntityId(0), got: {errors:?}"
1659 );
1660 }
1661
1662 #[test]
1663 fn test_valid_acyclic_cascade_passes() {
1664 let bus = make_bus(0);
1667 let mut h0 = make_hydro(0);
1668 h0.downstream_id = Some(EntityId(1));
1669 let mut h1 = make_hydro(1);
1670 h1.downstream_id = Some(EntityId(2));
1671 let h2 = make_hydro(2);
1672
1673 let result = SystemBuilder::new()
1674 .buses(vec![bus])
1675 .hydros(vec![h0, h1, h2])
1676 .build();
1677
1678 assert!(
1679 result.is_ok(),
1680 "expected Ok for acyclic cascade, got: {:?}",
1681 result.unwrap_err()
1682 );
1683 let system = result.unwrap_or_else(|_| unreachable!());
1684 assert_eq!(
1685 system.cascade().topological_order().len(),
1686 system.n_hydros(),
1687 "topological_order must contain all hydros"
1688 );
1689 }
1690
1691 #[test]
1694 fn test_filling_without_entry_stage() {
1695 use crate::entities::FillingConfig;
1697 let bus = make_bus(0);
1698 let mut hydro = make_hydro(1);
1699 hydro.entry_stage_id = None;
1700 hydro.filling = Some(FillingConfig {
1701 start_stage_id: 10,
1702 filling_inflow_m3s: 100.0,
1703 });
1704
1705 let result = SystemBuilder::new()
1706 .buses(vec![bus])
1707 .hydros(vec![hydro])
1708 .build();
1709
1710 assert!(
1711 result.is_err(),
1712 "expected Err for filling without entry_stage_id"
1713 );
1714 let errors = result.unwrap_err();
1715 let has_error = errors.iter().any(|e| match e {
1716 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
1717 *hydro_id == EntityId(1) && reason.contains("entry_stage_id")
1718 }
1719 _ => false,
1720 });
1721 assert!(
1722 has_error,
1723 "expected InvalidFillingConfig with entry_stage_id reason, got: {errors:?}"
1724 );
1725 }
1726
1727 #[test]
1728 fn test_filling_negative_inflow() {
1729 use crate::entities::FillingConfig;
1731 let bus = make_bus(0);
1732 let mut hydro = make_hydro(1);
1733 hydro.entry_stage_id = Some(10);
1734 hydro.filling = Some(FillingConfig {
1735 start_stage_id: 10,
1736 filling_inflow_m3s: -5.0,
1737 });
1738
1739 let result = SystemBuilder::new()
1740 .buses(vec![bus])
1741 .hydros(vec![hydro])
1742 .build();
1743
1744 assert!(
1745 result.is_err(),
1746 "expected Err for negative filling_inflow_m3s"
1747 );
1748 let errors = result.unwrap_err();
1749 let has_error = errors.iter().any(|e| match e {
1750 ValidationError::InvalidFillingConfig { hydro_id, reason } => {
1751 *hydro_id == EntityId(1) && reason.contains("filling_inflow_m3s must be positive")
1752 }
1753 _ => false,
1754 });
1755 assert!(
1756 has_error,
1757 "expected InvalidFillingConfig with positive inflow reason, got: {errors:?}"
1758 );
1759 }
1760
1761 #[test]
1762 fn test_valid_filling_config_passes() {
1763 use crate::entities::FillingConfig;
1765 let bus = make_bus(0);
1766 let mut hydro = make_hydro(1);
1767 hydro.entry_stage_id = Some(10);
1768 hydro.filling = Some(FillingConfig {
1769 start_stage_id: 10,
1770 filling_inflow_m3s: 100.0,
1771 });
1772
1773 let result = SystemBuilder::new()
1774 .buses(vec![bus])
1775 .hydros(vec![hydro])
1776 .build();
1777
1778 assert!(
1779 result.is_ok(),
1780 "expected Ok for valid filling config, got: {:?}",
1781 result.unwrap_err()
1782 );
1783 }
1784
1785 #[test]
1786 fn test_cascade_cycle_and_invalid_filling_both_reported() {
1787 use crate::entities::FillingConfig;
1790 let bus = make_bus(0);
1791
1792 let mut h0 = make_hydro(0);
1794 h0.downstream_id = Some(EntityId(0));
1795
1796 let mut h1 = make_hydro(1);
1798 h1.entry_stage_id = None; h1.filling = Some(FillingConfig {
1800 start_stage_id: 5,
1801 filling_inflow_m3s: 50.0,
1802 });
1803
1804 let result = SystemBuilder::new()
1805 .buses(vec![bus])
1806 .hydros(vec![h0, h1])
1807 .build();
1808
1809 assert!(result.is_err(), "expected Err for cycle + invalid filling");
1810 let errors = result.unwrap_err();
1811 let has_cycle = errors
1812 .iter()
1813 .any(|e| matches!(e, ValidationError::CascadeCycle { .. }));
1814 let has_filling = errors
1815 .iter()
1816 .any(|e| matches!(e, ValidationError::InvalidFillingConfig { .. }));
1817 assert!(has_cycle, "expected CascadeCycle error, got: {errors:?}");
1818 assert!(
1819 has_filling,
1820 "expected InvalidFillingConfig error, got: {errors:?}"
1821 );
1822 }
1823
1824 #[cfg(feature = "serde")]
1825 #[test]
1826 fn test_system_serde_roundtrip() {
1827 let bus_a = make_bus(1);
1829 let bus_b = make_bus(2);
1830 let hydro = make_hydro_on_bus(10, 1);
1831 let thermal = make_thermal_on_bus(20, 2);
1832 let line = make_line(1, 1, 2);
1833
1834 let system = SystemBuilder::new()
1835 .buses(vec![bus_a, bus_b])
1836 .hydros(vec![hydro])
1837 .thermals(vec![thermal])
1838 .lines(vec![line])
1839 .build()
1840 .expect("valid system");
1841
1842 let json = serde_json::to_string(&system).unwrap();
1843
1844 let mut deserialized: System = serde_json::from_str(&json).unwrap();
1846 deserialized.rebuild_indices();
1847
1848 assert_eq!(system.buses(), deserialized.buses());
1850 assert_eq!(system.hydros(), deserialized.hydros());
1851 assert_eq!(system.thermals(), deserialized.thermals());
1852 assert_eq!(system.lines(), deserialized.lines());
1853
1854 assert_eq!(
1856 deserialized.bus(EntityId(1)).map(|b| b.id),
1857 Some(EntityId(1))
1858 );
1859 assert_eq!(
1860 deserialized.hydro(EntityId(10)).map(|h| h.id),
1861 Some(EntityId(10))
1862 );
1863 assert_eq!(
1864 deserialized.thermal(EntityId(20)).map(|t| t.id),
1865 Some(EntityId(20))
1866 );
1867 assert_eq!(
1868 deserialized.line(EntityId(1)).map(|l| l.id),
1869 Some(EntityId(1))
1870 );
1871 }
1872
1873 fn make_stage(id: i32) -> Stage {
1876 use crate::temporal::{
1877 Block, BlockMode, NoiseMethod, ScenarioSourceConfig, StageRiskConfig, StageStateConfig,
1878 };
1879 use chrono::NaiveDate;
1880 Stage {
1881 index: usize::try_from(id.max(0)).unwrap_or(0),
1882 id,
1883 start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1884 end_date: NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
1885 season_id: Some(0),
1886 blocks: vec![Block {
1887 index: 0,
1888 name: "SINGLE".to_string(),
1889 duration_hours: 744.0,
1890 }],
1891 block_mode: BlockMode::Parallel,
1892 state_config: StageStateConfig {
1893 storage: true,
1894 inflow_lags: false,
1895 },
1896 risk_config: StageRiskConfig::Expectation,
1897 scenario_config: ScenarioSourceConfig {
1898 branching_factor: 50,
1899 noise_method: NoiseMethod::Saa,
1900 },
1901 }
1902 }
1903
1904 #[test]
1907 fn test_system_backward_compat() {
1908 let system = SystemBuilder::new().build().expect("empty system is valid");
1909 assert_eq!(system.n_buses(), 0);
1911 assert_eq!(system.n_hydros(), 0);
1912 assert_eq!(system.n_stages(), 0);
1914 assert!(system.stages().is_empty());
1915 assert!(system.initial_conditions().storage.is_empty());
1916 assert!(system.generic_constraints().is_empty());
1917 assert!(system.inflow_models().is_empty());
1918 assert!(system.load_models().is_empty());
1919 assert_eq!(system.penalties().n_stages(), 0);
1920 assert_eq!(system.bounds().n_stages(), 0);
1921 }
1922
1923 #[test]
1925 fn test_system_with_stages() {
1926 let s0 = make_stage(0);
1927 let s1 = make_stage(1);
1928
1929 let system = SystemBuilder::new()
1930 .stages(vec![s1.clone(), s0.clone()]) .build()
1932 .expect("valid system");
1933
1934 assert_eq!(system.n_stages(), 2);
1936 assert_eq!(system.stages()[0].id, 0);
1937 assert_eq!(system.stages()[1].id, 1);
1938
1939 let found = system.stage(0).expect("stage 0 must be found");
1941 assert_eq!(found.id, s0.id);
1942
1943 let found1 = system.stage(1).expect("stage 1 must be found");
1944 assert_eq!(found1.id, s1.id);
1945
1946 assert!(system.stage(99).is_none());
1948 }
1949
1950 #[test]
1952 fn test_system_stage_lookup_by_id() {
1953 let stages: Vec<Stage> = [0i32, 1, 2].iter().map(|&id| make_stage(id)).collect();
1954
1955 let system = SystemBuilder::new()
1956 .stages(stages)
1957 .build()
1958 .expect("valid system");
1959
1960 assert_eq!(system.stage(1).map(|s| s.id), Some(1));
1961 assert!(system.stage(99).is_none());
1962 }
1963
1964 #[test]
1966 fn test_system_with_initial_conditions() {
1967 let ic = InitialConditions {
1968 storage: vec![crate::HydroStorage {
1969 hydro_id: EntityId(0),
1970 value_hm3: 15_000.0,
1971 }],
1972 filling_storage: vec![],
1973 };
1974
1975 let system = SystemBuilder::new()
1976 .initial_conditions(ic)
1977 .build()
1978 .expect("valid system");
1979
1980 assert_eq!(system.initial_conditions().storage.len(), 1);
1981 assert_eq!(system.initial_conditions().storage[0].hydro_id, EntityId(0));
1982 assert!((system.initial_conditions().storage[0].value_hm3 - 15_000.0).abs() < f64::EPSILON);
1983 }
1984
1985 #[cfg(feature = "serde")]
1988 #[test]
1989 fn test_system_serde_roundtrip_with_stages() {
1990 use crate::temporal::PolicyGraphType;
1991
1992 let stages = vec![make_stage(0), make_stage(1)];
1993 let policy_graph = PolicyGraph {
1994 graph_type: PolicyGraphType::FiniteHorizon,
1995 annual_discount_rate: 0.0,
1996 transitions: vec![],
1997 season_map: None,
1998 };
1999
2000 let system = SystemBuilder::new()
2001 .stages(stages)
2002 .policy_graph(policy_graph)
2003 .build()
2004 .expect("valid system");
2005
2006 let json = serde_json::to_string(&system).unwrap();
2007 let mut deserialized: System = serde_json::from_str(&json).unwrap();
2008
2009 deserialized.rebuild_indices();
2011
2012 assert_eq!(system.n_stages(), deserialized.n_stages());
2014 assert_eq!(system.stages()[0].id, deserialized.stages()[0].id);
2015 assert_eq!(system.stages()[1].id, deserialized.stages()[1].id);
2016
2017 assert_eq!(deserialized.stage(0).map(|s| s.id), Some(0));
2019 assert_eq!(deserialized.stage(1).map(|s| s.id), Some(1));
2020 assert!(deserialized.stage(99).is_none());
2021
2022 assert_eq!(
2024 deserialized.policy_graph().graph_type,
2025 system.policy_graph().graph_type
2026 );
2027 }
2028}