use std::collections::{HashMap, HashSet};
use crate::{
Bus, CascadeTopology, CorrelationModel, EnergyContract, EntityId, GenericConstraint, Hydro,
InflowModel, InitialConditions, Line, LoadModel, NcsModel, NetworkTopology,
NonControllableSource, PolicyGraph, PumpingStation, ResolvedBounds, ResolvedExchangeFactors,
ResolvedGenericConstraintBounds, ResolvedLoadFactors, ResolvedNcsBounds, ResolvedNcsFactors,
ResolvedPenalties, ScenarioSource, Stage, Thermal, ValidationError,
};
#[derive(Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct System {
buses: Vec<Bus>,
lines: Vec<Line>,
hydros: Vec<Hydro>,
thermals: Vec<Thermal>,
pumping_stations: Vec<PumpingStation>,
contracts: Vec<EnergyContract>,
non_controllable_sources: Vec<NonControllableSource>,
#[cfg_attr(feature = "serde", serde(skip))]
bus_index: HashMap<EntityId, usize>,
#[cfg_attr(feature = "serde", serde(skip))]
line_index: HashMap<EntityId, usize>,
#[cfg_attr(feature = "serde", serde(skip))]
hydro_index: HashMap<EntityId, usize>,
#[cfg_attr(feature = "serde", serde(skip))]
thermal_index: HashMap<EntityId, usize>,
#[cfg_attr(feature = "serde", serde(skip))]
pumping_station_index: HashMap<EntityId, usize>,
#[cfg_attr(feature = "serde", serde(skip))]
contract_index: HashMap<EntityId, usize>,
#[cfg_attr(feature = "serde", serde(skip))]
non_controllable_source_index: HashMap<EntityId, usize>,
cascade: CascadeTopology,
network: NetworkTopology,
stages: Vec<Stage>,
policy_graph: PolicyGraph,
#[cfg_attr(feature = "serde", serde(skip))]
stage_index: HashMap<i32, usize>,
penalties: ResolvedPenalties,
bounds: ResolvedBounds,
resolved_generic_bounds: ResolvedGenericConstraintBounds,
resolved_load_factors: ResolvedLoadFactors,
resolved_exchange_factors: ResolvedExchangeFactors,
resolved_ncs_bounds: ResolvedNcsBounds,
resolved_ncs_factors: ResolvedNcsFactors,
inflow_models: Vec<InflowModel>,
load_models: Vec<LoadModel>,
ncs_models: Vec<NcsModel>,
correlation: CorrelationModel,
initial_conditions: InitialConditions,
generic_constraints: Vec<GenericConstraint>,
scenario_source: ScenarioSource,
}
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
const fn check() {
assert_send_sync::<System>();
}
let _ = check;
};
impl System {
#[must_use]
pub fn buses(&self) -> &[Bus] {
&self.buses
}
#[must_use]
pub fn lines(&self) -> &[Line] {
&self.lines
}
#[must_use]
pub fn hydros(&self) -> &[Hydro] {
&self.hydros
}
#[must_use]
pub fn thermals(&self) -> &[Thermal] {
&self.thermals
}
#[must_use]
pub fn pumping_stations(&self) -> &[PumpingStation] {
&self.pumping_stations
}
#[must_use]
pub fn contracts(&self) -> &[EnergyContract] {
&self.contracts
}
#[must_use]
pub fn non_controllable_sources(&self) -> &[NonControllableSource] {
&self.non_controllable_sources
}
#[must_use]
pub fn n_buses(&self) -> usize {
self.buses.len()
}
#[must_use]
pub fn n_lines(&self) -> usize {
self.lines.len()
}
#[must_use]
pub fn n_hydros(&self) -> usize {
self.hydros.len()
}
#[must_use]
pub fn n_thermals(&self) -> usize {
self.thermals.len()
}
#[must_use]
pub fn n_pumping_stations(&self) -> usize {
self.pumping_stations.len()
}
#[must_use]
pub fn n_contracts(&self) -> usize {
self.contracts.len()
}
#[must_use]
pub fn n_non_controllable_sources(&self) -> usize {
self.non_controllable_sources.len()
}
#[must_use]
pub fn bus(&self, id: EntityId) -> Option<&Bus> {
self.bus_index.get(&id).map(|&i| &self.buses[i])
}
#[must_use]
pub fn line(&self, id: EntityId) -> Option<&Line> {
self.line_index.get(&id).map(|&i| &self.lines[i])
}
#[must_use]
pub fn hydro(&self, id: EntityId) -> Option<&Hydro> {
self.hydro_index.get(&id).map(|&i| &self.hydros[i])
}
#[must_use]
pub fn thermal(&self, id: EntityId) -> Option<&Thermal> {
self.thermal_index.get(&id).map(|&i| &self.thermals[i])
}
#[must_use]
pub fn pumping_station(&self, id: EntityId) -> Option<&PumpingStation> {
self.pumping_station_index
.get(&id)
.map(|&i| &self.pumping_stations[i])
}
#[must_use]
pub fn contract(&self, id: EntityId) -> Option<&EnergyContract> {
self.contract_index.get(&id).map(|&i| &self.contracts[i])
}
#[must_use]
pub fn non_controllable_source(&self, id: EntityId) -> Option<&NonControllableSource> {
self.non_controllable_source_index
.get(&id)
.map(|&i| &self.non_controllable_sources[i])
}
#[must_use]
pub fn cascade(&self) -> &CascadeTopology {
&self.cascade
}
#[must_use]
pub fn network(&self) -> &NetworkTopology {
&self.network
}
#[must_use]
pub fn stages(&self) -> &[Stage] {
&self.stages
}
#[must_use]
pub fn n_stages(&self) -> usize {
self.stages.len()
}
#[must_use]
pub fn stage(&self, id: i32) -> Option<&Stage> {
self.stage_index.get(&id).map(|&i| &self.stages[i])
}
#[must_use]
pub fn policy_graph(&self) -> &PolicyGraph {
&self.policy_graph
}
#[must_use]
pub fn penalties(&self) -> &ResolvedPenalties {
&self.penalties
}
#[must_use]
pub fn bounds(&self) -> &ResolvedBounds {
&self.bounds
}
#[must_use]
pub fn resolved_generic_bounds(&self) -> &ResolvedGenericConstraintBounds {
&self.resolved_generic_bounds
}
#[must_use]
pub fn resolved_load_factors(&self) -> &ResolvedLoadFactors {
&self.resolved_load_factors
}
#[must_use]
pub fn resolved_exchange_factors(&self) -> &ResolvedExchangeFactors {
&self.resolved_exchange_factors
}
#[must_use]
pub fn resolved_ncs_bounds(&self) -> &ResolvedNcsBounds {
&self.resolved_ncs_bounds
}
#[must_use]
pub fn resolved_ncs_factors(&self) -> &ResolvedNcsFactors {
&self.resolved_ncs_factors
}
#[must_use]
pub fn inflow_models(&self) -> &[InflowModel] {
&self.inflow_models
}
#[must_use]
pub fn load_models(&self) -> &[LoadModel] {
&self.load_models
}
#[must_use]
pub fn ncs_models(&self) -> &[NcsModel] {
&self.ncs_models
}
#[must_use]
pub fn correlation(&self) -> &CorrelationModel {
&self.correlation
}
#[must_use]
pub fn initial_conditions(&self) -> &InitialConditions {
&self.initial_conditions
}
#[must_use]
pub fn generic_constraints(&self) -> &[GenericConstraint] {
&self.generic_constraints
}
#[must_use]
pub fn scenario_source(&self) -> &ScenarioSource {
&self.scenario_source
}
#[must_use]
pub fn with_scenario_models(
mut self,
inflow_models: Vec<InflowModel>,
correlation: CorrelationModel,
) -> Self {
self.inflow_models = inflow_models;
self.correlation = correlation;
self
}
pub fn rebuild_indices(&mut self) {
self.bus_index = build_index(&self.buses);
self.line_index = build_index(&self.lines);
self.hydro_index = build_index(&self.hydros);
self.thermal_index = build_index(&self.thermals);
self.pumping_station_index = build_index(&self.pumping_stations);
self.contract_index = build_index(&self.contracts);
self.non_controllable_source_index = build_index(&self.non_controllable_sources);
self.stage_index = build_stage_index(&self.stages);
}
}
pub struct SystemBuilder {
buses: Vec<Bus>,
lines: Vec<Line>,
hydros: Vec<Hydro>,
thermals: Vec<Thermal>,
pumping_stations: Vec<PumpingStation>,
contracts: Vec<EnergyContract>,
non_controllable_sources: Vec<NonControllableSource>,
stages: Vec<Stage>,
policy_graph: PolicyGraph,
penalties: ResolvedPenalties,
bounds: ResolvedBounds,
resolved_generic_bounds: ResolvedGenericConstraintBounds,
resolved_load_factors: ResolvedLoadFactors,
resolved_exchange_factors: ResolvedExchangeFactors,
resolved_ncs_bounds: ResolvedNcsBounds,
resolved_ncs_factors: ResolvedNcsFactors,
inflow_models: Vec<InflowModel>,
load_models: Vec<LoadModel>,
ncs_models: Vec<NcsModel>,
correlation: CorrelationModel,
initial_conditions: InitialConditions,
generic_constraints: Vec<GenericConstraint>,
scenario_source: ScenarioSource,
}
impl Default for SystemBuilder {
fn default() -> Self {
Self::new()
}
}
impl SystemBuilder {
#[must_use]
pub fn new() -> Self {
Self {
buses: Vec::new(),
lines: Vec::new(),
hydros: Vec::new(),
thermals: Vec::new(),
pumping_stations: Vec::new(),
contracts: Vec::new(),
non_controllable_sources: Vec::new(),
stages: Vec::new(),
policy_graph: PolicyGraph::default(),
penalties: ResolvedPenalties::empty(),
bounds: ResolvedBounds::empty(),
resolved_generic_bounds: ResolvedGenericConstraintBounds::empty(),
resolved_load_factors: ResolvedLoadFactors::empty(),
resolved_exchange_factors: ResolvedExchangeFactors::empty(),
resolved_ncs_bounds: ResolvedNcsBounds::empty(),
resolved_ncs_factors: ResolvedNcsFactors::empty(),
inflow_models: Vec::new(),
load_models: Vec::new(),
ncs_models: Vec::new(),
correlation: CorrelationModel::default(),
initial_conditions: InitialConditions::default(),
generic_constraints: Vec::new(),
scenario_source: ScenarioSource::default(),
}
}
#[must_use]
pub fn buses(mut self, buses: Vec<Bus>) -> Self {
self.buses = buses;
self
}
#[must_use]
pub fn lines(mut self, lines: Vec<Line>) -> Self {
self.lines = lines;
self
}
#[must_use]
pub fn hydros(mut self, hydros: Vec<Hydro>) -> Self {
self.hydros = hydros;
self
}
#[must_use]
pub fn thermals(mut self, thermals: Vec<Thermal>) -> Self {
self.thermals = thermals;
self
}
#[must_use]
pub fn pumping_stations(mut self, stations: Vec<PumpingStation>) -> Self {
self.pumping_stations = stations;
self
}
#[must_use]
pub fn contracts(mut self, contracts: Vec<EnergyContract>) -> Self {
self.contracts = contracts;
self
}
#[must_use]
pub fn non_controllable_sources(mut self, sources: Vec<NonControllableSource>) -> Self {
self.non_controllable_sources = sources;
self
}
#[must_use]
pub fn stages(mut self, stages: Vec<Stage>) -> Self {
self.stages = stages;
self
}
#[must_use]
pub fn policy_graph(mut self, policy_graph: PolicyGraph) -> Self {
self.policy_graph = policy_graph;
self
}
#[must_use]
pub fn penalties(mut self, penalties: ResolvedPenalties) -> Self {
self.penalties = penalties;
self
}
#[must_use]
pub fn bounds(mut self, bounds: ResolvedBounds) -> Self {
self.bounds = bounds;
self
}
#[must_use]
pub fn resolved_generic_bounds(
mut self,
resolved_generic_bounds: ResolvedGenericConstraintBounds,
) -> Self {
self.resolved_generic_bounds = resolved_generic_bounds;
self
}
#[must_use]
pub fn resolved_load_factors(mut self, resolved_load_factors: ResolvedLoadFactors) -> Self {
self.resolved_load_factors = resolved_load_factors;
self
}
#[must_use]
pub fn resolved_exchange_factors(
mut self,
resolved_exchange_factors: ResolvedExchangeFactors,
) -> Self {
self.resolved_exchange_factors = resolved_exchange_factors;
self
}
#[must_use]
pub fn resolved_ncs_bounds(mut self, resolved_ncs_bounds: ResolvedNcsBounds) -> Self {
self.resolved_ncs_bounds = resolved_ncs_bounds;
self
}
#[must_use]
pub fn resolved_ncs_factors(mut self, resolved_ncs_factors: ResolvedNcsFactors) -> Self {
self.resolved_ncs_factors = resolved_ncs_factors;
self
}
#[must_use]
pub fn inflow_models(mut self, inflow_models: Vec<InflowModel>) -> Self {
self.inflow_models = inflow_models;
self
}
#[must_use]
pub fn load_models(mut self, load_models: Vec<LoadModel>) -> Self {
self.load_models = load_models;
self
}
#[must_use]
pub fn ncs_models(mut self, ncs_models: Vec<NcsModel>) -> Self {
self.ncs_models = ncs_models;
self
}
#[must_use]
pub fn correlation(mut self, correlation: CorrelationModel) -> Self {
self.correlation = correlation;
self
}
#[must_use]
pub fn initial_conditions(mut self, initial_conditions: InitialConditions) -> Self {
self.initial_conditions = initial_conditions;
self
}
#[must_use]
pub fn generic_constraints(mut self, generic_constraints: Vec<GenericConstraint>) -> Self {
self.generic_constraints = generic_constraints;
self
}
#[must_use]
pub fn scenario_source(mut self, scenario_source: ScenarioSource) -> Self {
self.scenario_source = scenario_source;
self
}
#[allow(clippy::too_many_lines)]
pub fn build(mut self) -> Result<System, Vec<ValidationError>> {
self.buses.sort_by_key(|e| e.id.0);
self.lines.sort_by_key(|e| e.id.0);
self.hydros.sort_by_key(|e| e.id.0);
self.thermals.sort_by_key(|e| e.id.0);
self.pumping_stations.sort_by_key(|e| e.id.0);
self.contracts.sort_by_key(|e| e.id.0);
self.non_controllable_sources.sort_by_key(|e| e.id.0);
self.stages.sort_by_key(|s| s.id);
self.generic_constraints.sort_by_key(|c| c.id.0);
let mut errors: Vec<ValidationError> = Vec::new();
check_duplicates(&self.buses, "Bus", &mut errors);
check_duplicates(&self.lines, "Line", &mut errors);
check_duplicates(&self.hydros, "Hydro", &mut errors);
check_duplicates(&self.thermals, "Thermal", &mut errors);
check_duplicates(&self.pumping_stations, "PumpingStation", &mut errors);
check_duplicates(&self.contracts, "EnergyContract", &mut errors);
check_duplicates(
&self.non_controllable_sources,
"NonControllableSource",
&mut errors,
);
if !errors.is_empty() {
return Err(errors);
}
let bus_index = build_index(&self.buses);
let line_index = build_index(&self.lines);
let hydro_index = build_index(&self.hydros);
let thermal_index = build_index(&self.thermals);
let pumping_station_index = build_index(&self.pumping_stations);
let contract_index = build_index(&self.contracts);
let non_controllable_source_index = build_index(&self.non_controllable_sources);
validate_cross_references(
&self.lines,
&self.hydros,
&self.thermals,
&self.pumping_stations,
&self.contracts,
&self.non_controllable_sources,
&bus_index,
&hydro_index,
&mut errors,
);
if !errors.is_empty() {
return Err(errors);
}
let cascade = CascadeTopology::build(&self.hydros);
if cascade.topological_order().len() < self.hydros.len() {
let in_topo: HashSet<EntityId> = cascade.topological_order().iter().copied().collect();
let mut cycle_ids: Vec<EntityId> = self
.hydros
.iter()
.map(|h| h.id)
.filter(|id| !in_topo.contains(id))
.collect();
cycle_ids.sort_by_key(|id| id.0);
errors.push(ValidationError::CascadeCycle { cycle_ids });
}
validate_filling_configs(&self.hydros, &mut errors);
if !errors.is_empty() {
return Err(errors);
}
let network = NetworkTopology::build(
&self.buses,
&self.lines,
&self.hydros,
&self.thermals,
&self.non_controllable_sources,
&self.contracts,
&self.pumping_stations,
);
let stage_index = build_stage_index(&self.stages);
Ok(System {
buses: self.buses,
lines: self.lines,
hydros: self.hydros,
thermals: self.thermals,
pumping_stations: self.pumping_stations,
contracts: self.contracts,
non_controllable_sources: self.non_controllable_sources,
bus_index,
line_index,
hydro_index,
thermal_index,
pumping_station_index,
contract_index,
non_controllable_source_index,
cascade,
network,
stages: self.stages,
policy_graph: self.policy_graph,
stage_index,
penalties: self.penalties,
bounds: self.bounds,
resolved_generic_bounds: self.resolved_generic_bounds,
resolved_load_factors: self.resolved_load_factors,
resolved_exchange_factors: self.resolved_exchange_factors,
resolved_ncs_bounds: self.resolved_ncs_bounds,
resolved_ncs_factors: self.resolved_ncs_factors,
inflow_models: self.inflow_models,
load_models: self.load_models,
ncs_models: self.ncs_models,
correlation: self.correlation,
initial_conditions: self.initial_conditions,
generic_constraints: self.generic_constraints,
scenario_source: self.scenario_source,
})
}
}
trait HasId {
fn entity_id(&self) -> EntityId;
}
impl HasId for Bus {
fn entity_id(&self) -> EntityId {
self.id
}
}
impl HasId for Line {
fn entity_id(&self) -> EntityId {
self.id
}
}
impl HasId for Hydro {
fn entity_id(&self) -> EntityId {
self.id
}
}
impl HasId for Thermal {
fn entity_id(&self) -> EntityId {
self.id
}
}
impl HasId for PumpingStation {
fn entity_id(&self) -> EntityId {
self.id
}
}
impl HasId for EnergyContract {
fn entity_id(&self) -> EntityId {
self.id
}
}
impl HasId for NonControllableSource {
fn entity_id(&self) -> EntityId {
self.id
}
}
fn build_index<T: HasId>(entities: &[T]) -> HashMap<EntityId, usize> {
let mut index = HashMap::with_capacity(entities.len());
for (i, entity) in entities.iter().enumerate() {
index.insert(entity.entity_id(), i);
}
index
}
fn build_stage_index(stages: &[Stage]) -> HashMap<i32, usize> {
let mut index = HashMap::with_capacity(stages.len());
for (i, stage) in stages.iter().enumerate() {
index.insert(stage.id, i);
}
index
}
fn check_duplicates<T: HasId>(
entities: &[T],
entity_type: &'static str,
errors: &mut Vec<ValidationError>,
) {
for window in entities.windows(2) {
if window[0].entity_id() == window[1].entity_id() {
errors.push(ValidationError::DuplicateId {
entity_type,
id: window[0].entity_id(),
});
}
}
}
#[allow(clippy::too_many_arguments)]
fn validate_cross_references(
lines: &[Line],
hydros: &[Hydro],
thermals: &[Thermal],
pumping_stations: &[PumpingStation],
contracts: &[EnergyContract],
non_controllable_sources: &[NonControllableSource],
bus_index: &HashMap<EntityId, usize>,
hydro_index: &HashMap<EntityId, usize>,
errors: &mut Vec<ValidationError>,
) {
validate_line_refs(lines, bus_index, errors);
validate_hydro_refs(hydros, bus_index, hydro_index, errors);
validate_thermal_refs(thermals, bus_index, errors);
validate_pumping_station_refs(pumping_stations, bus_index, hydro_index, errors);
validate_contract_refs(contracts, bus_index, errors);
validate_ncs_refs(non_controllable_sources, bus_index, errors);
}
fn validate_line_refs(
lines: &[Line],
bus_index: &HashMap<EntityId, usize>,
errors: &mut Vec<ValidationError>,
) {
for line in lines {
if !bus_index.contains_key(&line.source_bus_id) {
errors.push(ValidationError::InvalidReference {
source_entity_type: "Line",
source_id: line.id,
field_name: "source_bus_id",
referenced_id: line.source_bus_id,
expected_type: "Bus",
});
}
if !bus_index.contains_key(&line.target_bus_id) {
errors.push(ValidationError::InvalidReference {
source_entity_type: "Line",
source_id: line.id,
field_name: "target_bus_id",
referenced_id: line.target_bus_id,
expected_type: "Bus",
});
}
}
}
fn validate_hydro_refs(
hydros: &[Hydro],
bus_index: &HashMap<EntityId, usize>,
hydro_index: &HashMap<EntityId, usize>,
errors: &mut Vec<ValidationError>,
) {
for hydro in hydros {
if !bus_index.contains_key(&hydro.bus_id) {
errors.push(ValidationError::InvalidReference {
source_entity_type: "Hydro",
source_id: hydro.id,
field_name: "bus_id",
referenced_id: hydro.bus_id,
expected_type: "Bus",
});
}
if let Some(downstream_id) = hydro.downstream_id {
if !hydro_index.contains_key(&downstream_id) {
errors.push(ValidationError::InvalidReference {
source_entity_type: "Hydro",
source_id: hydro.id,
field_name: "downstream_id",
referenced_id: downstream_id,
expected_type: "Hydro",
});
}
}
if let Some(ref diversion) = hydro.diversion {
if !hydro_index.contains_key(&diversion.downstream_id) {
errors.push(ValidationError::InvalidReference {
source_entity_type: "Hydro",
source_id: hydro.id,
field_name: "diversion.downstream_id",
referenced_id: diversion.downstream_id,
expected_type: "Hydro",
});
}
}
}
}
fn validate_thermal_refs(
thermals: &[Thermal],
bus_index: &HashMap<EntityId, usize>,
errors: &mut Vec<ValidationError>,
) {
for thermal in thermals {
if !bus_index.contains_key(&thermal.bus_id) {
errors.push(ValidationError::InvalidReference {
source_entity_type: "Thermal",
source_id: thermal.id,
field_name: "bus_id",
referenced_id: thermal.bus_id,
expected_type: "Bus",
});
}
}
}
fn validate_pumping_station_refs(
pumping_stations: &[PumpingStation],
bus_index: &HashMap<EntityId, usize>,
hydro_index: &HashMap<EntityId, usize>,
errors: &mut Vec<ValidationError>,
) {
for ps in pumping_stations {
if !bus_index.contains_key(&ps.bus_id) {
errors.push(ValidationError::InvalidReference {
source_entity_type: "PumpingStation",
source_id: ps.id,
field_name: "bus_id",
referenced_id: ps.bus_id,
expected_type: "Bus",
});
}
if !hydro_index.contains_key(&ps.source_hydro_id) {
errors.push(ValidationError::InvalidReference {
source_entity_type: "PumpingStation",
source_id: ps.id,
field_name: "source_hydro_id",
referenced_id: ps.source_hydro_id,
expected_type: "Hydro",
});
}
if !hydro_index.contains_key(&ps.destination_hydro_id) {
errors.push(ValidationError::InvalidReference {
source_entity_type: "PumpingStation",
source_id: ps.id,
field_name: "destination_hydro_id",
referenced_id: ps.destination_hydro_id,
expected_type: "Hydro",
});
}
}
}
fn validate_contract_refs(
contracts: &[EnergyContract],
bus_index: &HashMap<EntityId, usize>,
errors: &mut Vec<ValidationError>,
) {
for contract in contracts {
if !bus_index.contains_key(&contract.bus_id) {
errors.push(ValidationError::InvalidReference {
source_entity_type: "EnergyContract",
source_id: contract.id,
field_name: "bus_id",
referenced_id: contract.bus_id,
expected_type: "Bus",
});
}
}
}
fn validate_ncs_refs(
non_controllable_sources: &[NonControllableSource],
bus_index: &HashMap<EntityId, usize>,
errors: &mut Vec<ValidationError>,
) {
for ncs in non_controllable_sources {
if !bus_index.contains_key(&ncs.bus_id) {
errors.push(ValidationError::InvalidReference {
source_entity_type: "NonControllableSource",
source_id: ncs.id,
field_name: "bus_id",
referenced_id: ncs.bus_id,
expected_type: "Bus",
});
}
}
}
fn validate_filling_configs(hydros: &[Hydro], errors: &mut Vec<ValidationError>) {
for hydro in hydros {
if let Some(filling) = &hydro.filling {
if filling.filling_inflow_m3s.is_nan() || filling.filling_inflow_m3s <= 0.0 {
errors.push(ValidationError::InvalidFillingConfig {
hydro_id: hydro.id,
reason: "filling_inflow_m3s must be positive".to_string(),
});
}
if hydro.entry_stage_id.is_none() {
errors.push(ValidationError::InvalidFillingConfig {
hydro_id: hydro.id,
reason: "filling requires entry_stage_id to be set".to_string(),
});
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entities::{ContractType, HydroGenerationModel, HydroPenalties, ThermalCostSegment};
fn make_bus(id: i32) -> Bus {
Bus {
id: EntityId(id),
name: format!("bus-{id}"),
deficit_segments: vec![],
excess_cost: 0.0,
}
}
fn make_line(id: i32, source_bus_id: i32, target_bus_id: i32) -> Line {
crate::Line {
id: EntityId(id),
name: format!("line-{id}"),
source_bus_id: EntityId(source_bus_id),
target_bus_id: EntityId(target_bus_id),
entry_stage_id: None,
exit_stage_id: None,
direct_capacity_mw: 100.0,
reverse_capacity_mw: 100.0,
losses_percent: 0.0,
exchange_cost: 0.0,
}
}
fn make_hydro_on_bus(id: i32, bus_id: i32) -> Hydro {
let zero_penalties = HydroPenalties {
spillage_cost: 0.0,
diversion_cost: 0.0,
fpha_turbined_cost: 0.0,
storage_violation_below_cost: 0.0,
filling_target_violation_cost: 0.0,
turbined_violation_below_cost: 0.0,
outflow_violation_below_cost: 0.0,
outflow_violation_above_cost: 0.0,
generation_violation_below_cost: 0.0,
evaporation_violation_cost: 0.0,
water_withdrawal_violation_cost: 0.0,
};
Hydro {
id: EntityId(id),
name: format!("hydro-{id}"),
bus_id: EntityId(bus_id),
downstream_id: None,
entry_stage_id: None,
exit_stage_id: None,
min_storage_hm3: 0.0,
max_storage_hm3: 1.0,
min_outflow_m3s: 0.0,
max_outflow_m3s: None,
generation_model: HydroGenerationModel::ConstantProductivity {
productivity_mw_per_m3s: 1.0,
},
min_turbined_m3s: 0.0,
max_turbined_m3s: 1.0,
min_generation_mw: 0.0,
max_generation_mw: 1.0,
tailrace: None,
hydraulic_losses: None,
efficiency: None,
evaporation_coefficients_mm: None,
evaporation_reference_volumes_hm3: None,
diversion: None,
filling: None,
penalties: zero_penalties,
}
}
fn make_hydro(id: i32) -> Hydro {
make_hydro_on_bus(id, 0)
}
fn make_thermal_on_bus(id: i32, bus_id: i32) -> Thermal {
Thermal {
id: EntityId(id),
name: format!("thermal-{id}"),
bus_id: EntityId(bus_id),
entry_stage_id: None,
exit_stage_id: None,
cost_segments: vec![ThermalCostSegment {
capacity_mw: 100.0,
cost_per_mwh: 50.0,
}],
min_generation_mw: 0.0,
max_generation_mw: 100.0,
gnl_config: None,
}
}
fn make_thermal(id: i32) -> Thermal {
make_thermal_on_bus(id, 0)
}
fn make_pumping_station_full(
id: i32,
bus_id: i32,
source_hydro_id: i32,
destination_hydro_id: i32,
) -> PumpingStation {
PumpingStation {
id: EntityId(id),
name: format!("ps-{id}"),
bus_id: EntityId(bus_id),
source_hydro_id: EntityId(source_hydro_id),
destination_hydro_id: EntityId(destination_hydro_id),
entry_stage_id: None,
exit_stage_id: None,
consumption_mw_per_m3s: 0.5,
min_flow_m3s: 0.0,
max_flow_m3s: 10.0,
}
}
fn make_pumping_station(id: i32) -> PumpingStation {
make_pumping_station_full(id, 0, 0, 1)
}
fn make_contract_on_bus(id: i32, bus_id: i32) -> EnergyContract {
EnergyContract {
id: EntityId(id),
name: format!("contract-{id}"),
bus_id: EntityId(bus_id),
contract_type: ContractType::Import,
entry_stage_id: None,
exit_stage_id: None,
price_per_mwh: 0.0,
min_mw: 0.0,
max_mw: 100.0,
}
}
fn make_contract(id: i32) -> EnergyContract {
make_contract_on_bus(id, 0)
}
fn make_ncs_on_bus(id: i32, bus_id: i32) -> NonControllableSource {
NonControllableSource {
id: EntityId(id),
name: format!("ncs-{id}"),
bus_id: EntityId(bus_id),
entry_stage_id: None,
exit_stage_id: None,
max_generation_mw: 50.0,
curtailment_cost: 0.0,
}
}
fn make_ncs(id: i32) -> NonControllableSource {
make_ncs_on_bus(id, 0)
}
#[test]
fn test_empty_system() {
let system = SystemBuilder::new().build().expect("empty system is valid");
assert_eq!(system.n_buses(), 0);
assert_eq!(system.n_lines(), 0);
assert_eq!(system.n_hydros(), 0);
assert_eq!(system.n_thermals(), 0);
assert_eq!(system.n_pumping_stations(), 0);
assert_eq!(system.n_contracts(), 0);
assert_eq!(system.n_non_controllable_sources(), 0);
assert!(system.buses().is_empty());
assert!(system.cascade().is_empty());
}
#[test]
fn test_canonical_ordering() {
let system = SystemBuilder::new()
.buses(vec![make_bus(2), make_bus(1), make_bus(0)])
.build()
.expect("valid system");
assert_eq!(system.buses()[0].id, EntityId(0));
assert_eq!(system.buses()[1].id, EntityId(1));
assert_eq!(system.buses()[2].id, EntityId(2));
}
#[test]
fn test_lookup_by_id() {
let system = SystemBuilder::new()
.buses(vec![make_bus(0)])
.hydros(vec![make_hydro(10), make_hydro(5), make_hydro(20)])
.build()
.expect("valid system");
assert_eq!(system.hydro(EntityId(5)).map(|h| h.id), Some(EntityId(5)));
assert_eq!(system.hydro(EntityId(10)).map(|h| h.id), Some(EntityId(10)));
assert_eq!(system.hydro(EntityId(20)).map(|h| h.id), Some(EntityId(20)));
}
#[test]
fn test_lookup_missing_id() {
let system = SystemBuilder::new()
.buses(vec![make_bus(0)])
.hydros(vec![make_hydro(1), make_hydro(2)])
.build()
.expect("valid system");
assert!(system.hydro(EntityId(999)).is_none());
}
#[test]
fn test_count_queries() {
let system = SystemBuilder::new()
.buses(vec![make_bus(0), make_bus(1)])
.lines(vec![make_line(0, 0, 1)])
.hydros(vec![make_hydro(0), make_hydro(1), make_hydro(2)])
.thermals(vec![make_thermal(0)])
.pumping_stations(vec![make_pumping_station(0)])
.contracts(vec![make_contract(0), make_contract(1)])
.non_controllable_sources(vec![make_ncs(0)])
.build()
.expect("valid system");
assert_eq!(system.n_buses(), 2);
assert_eq!(system.n_lines(), 1);
assert_eq!(system.n_hydros(), 3);
assert_eq!(system.n_thermals(), 1);
assert_eq!(system.n_pumping_stations(), 1);
assert_eq!(system.n_contracts(), 2);
assert_eq!(system.n_non_controllable_sources(), 1);
}
#[test]
fn test_slice_accessors() {
let system = SystemBuilder::new()
.buses(vec![make_bus(0), make_bus(1), make_bus(2)])
.build()
.expect("valid system");
let buses = system.buses();
assert_eq!(buses.len(), 3);
assert_eq!(buses[0].id, EntityId(0));
assert_eq!(buses[1].id, EntityId(1));
assert_eq!(buses[2].id, EntityId(2));
}
#[test]
fn test_duplicate_id_error() {
let result = SystemBuilder::new()
.buses(vec![make_bus(0), make_bus(0)])
.build();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(!errors.is_empty());
assert!(errors.iter().any(|e| matches!(
e,
ValidationError::DuplicateId {
entity_type: "Bus",
id: EntityId(0),
}
)));
}
#[test]
fn test_multiple_duplicate_errors() {
let result = SystemBuilder::new()
.buses(vec![make_bus(0), make_bus(0)])
.thermals(vec![make_thermal(5), make_thermal(5)])
.build();
assert!(result.is_err());
let errors = result.unwrap_err();
let has_bus_dup = errors.iter().any(|e| {
matches!(
e,
ValidationError::DuplicateId {
entity_type: "Bus",
..
}
)
});
let has_thermal_dup = errors.iter().any(|e| {
matches!(
e,
ValidationError::DuplicateId {
entity_type: "Thermal",
..
}
)
});
assert!(has_bus_dup, "expected Bus duplicate error");
assert!(has_thermal_dup, "expected Thermal duplicate error");
}
#[test]
fn test_send_sync() {
fn require_send_sync<T: Send + Sync>(_: T) {}
let system = SystemBuilder::new().build().expect("valid system");
require_send_sync(system);
}
#[test]
fn test_cascade_accessible() {
let mut h0 = make_hydro_on_bus(0, 0);
h0.downstream_id = Some(EntityId(1));
let mut h1 = make_hydro_on_bus(1, 0);
h1.downstream_id = Some(EntityId(2));
let h2 = make_hydro_on_bus(2, 0);
let system = SystemBuilder::new()
.buses(vec![make_bus(0)])
.hydros(vec![h0, h1, h2])
.build()
.expect("valid system");
let order = system.cascade().topological_order();
assert!(!order.is_empty(), "topological order must be non-empty");
let pos_0 = order
.iter()
.position(|&id| id == EntityId(0))
.expect("EntityId(0) must be in topological order");
let pos_2 = order
.iter()
.position(|&id| id == EntityId(2))
.expect("EntityId(2) must be in topological order");
assert!(pos_0 < pos_2, "EntityId(0) must precede EntityId(2)");
}
#[test]
fn test_network_accessible() {
let system = SystemBuilder::new()
.buses(vec![make_bus(0), make_bus(1)])
.lines(vec![make_line(0, 0, 1)])
.build()
.expect("valid system");
let connections = system.network().bus_lines(EntityId(0));
assert!(!connections.is_empty(), "bus 0 must have connections");
assert_eq!(connections[0].line_id, EntityId(0));
}
#[test]
fn test_all_entity_lookups() {
let system = SystemBuilder::new()
.buses(vec![make_bus(0), make_bus(1)])
.lines(vec![make_line(2, 0, 1)])
.hydros(vec![
make_hydro_on_bus(0, 0),
make_hydro_on_bus(1, 0),
make_hydro_on_bus(3, 0),
])
.thermals(vec![make_thermal(4)])
.pumping_stations(vec![make_pumping_station(5)])
.contracts(vec![make_contract(6)])
.non_controllable_sources(vec![make_ncs(7)])
.build()
.expect("valid system");
assert!(system.bus(EntityId(1)).is_some());
assert!(system.line(EntityId(2)).is_some());
assert!(system.hydro(EntityId(3)).is_some());
assert!(system.thermal(EntityId(4)).is_some());
assert!(system.pumping_station(EntityId(5)).is_some());
assert!(system.contract(EntityId(6)).is_some());
assert!(system.non_controllable_source(EntityId(7)).is_some());
assert!(system.bus(EntityId(999)).is_none());
assert!(system.line(EntityId(999)).is_none());
assert!(system.hydro(EntityId(999)).is_none());
assert!(system.thermal(EntityId(999)).is_none());
assert!(system.pumping_station(EntityId(999)).is_none());
assert!(system.contract(EntityId(999)).is_none());
assert!(system.non_controllable_source(EntityId(999)).is_none());
}
#[test]
fn test_default_builder() {
let system = SystemBuilder::default()
.build()
.expect("default builder produces valid empty system");
assert_eq!(system.n_buses(), 0);
}
#[test]
fn test_invalid_bus_reference_hydro() {
let hydro = make_hydro_on_bus(1, 99);
let result = SystemBuilder::new().hydros(vec![hydro]).build();
assert!(result.is_err(), "expected Err for missing bus reference");
let errors = result.unwrap_err();
assert!(
errors.iter().any(|e| matches!(
e,
ValidationError::InvalidReference {
source_entity_type: "Hydro",
source_id: EntityId(1),
field_name: "bus_id",
referenced_id: EntityId(99),
expected_type: "Bus",
}
)),
"expected InvalidReference for Hydro bus_id=99, got: {errors:?}"
);
}
#[test]
fn test_invalid_downstream_reference() {
let bus = make_bus(0);
let mut hydro = make_hydro(1);
hydro.downstream_id = Some(EntityId(50));
let result = SystemBuilder::new()
.buses(vec![bus])
.hydros(vec![hydro])
.build();
assert!(
result.is_err(),
"expected Err for missing downstream reference"
);
let errors = result.unwrap_err();
assert!(
errors.iter().any(|e| matches!(
e,
ValidationError::InvalidReference {
source_entity_type: "Hydro",
source_id: EntityId(1),
field_name: "downstream_id",
referenced_id: EntityId(50),
expected_type: "Hydro",
}
)),
"expected InvalidReference for Hydro downstream_id=50, got: {errors:?}"
);
}
#[test]
fn test_invalid_pumping_station_hydro_refs() {
let bus = make_bus(0);
let dest_hydro = make_hydro(1);
let ps = make_pumping_station_full(10, 0, 77, 1);
let result = SystemBuilder::new()
.buses(vec![bus])
.hydros(vec![dest_hydro])
.pumping_stations(vec![ps])
.build();
assert!(
result.is_err(),
"expected Err for missing source_hydro_id reference"
);
let errors = result.unwrap_err();
assert!(
errors.iter().any(|e| matches!(
e,
ValidationError::InvalidReference {
source_entity_type: "PumpingStation",
source_id: EntityId(10),
field_name: "source_hydro_id",
referenced_id: EntityId(77),
expected_type: "Hydro",
}
)),
"expected InvalidReference for PumpingStation source_hydro_id=77, got: {errors:?}"
);
}
#[test]
fn test_multiple_invalid_references_collected() {
let line = make_line(1, 99, 0);
let thermal = make_thermal_on_bus(2, 88);
let result = SystemBuilder::new()
.buses(vec![make_bus(0)])
.lines(vec![line])
.thermals(vec![thermal])
.build();
assert!(
result.is_err(),
"expected Err for multiple invalid references"
);
let errors = result.unwrap_err();
let has_line_error = errors.iter().any(|e| {
matches!(
e,
ValidationError::InvalidReference {
source_entity_type: "Line",
field_name: "source_bus_id",
referenced_id: EntityId(99),
..
}
)
});
let has_thermal_error = errors.iter().any(|e| {
matches!(
e,
ValidationError::InvalidReference {
source_entity_type: "Thermal",
field_name: "bus_id",
referenced_id: EntityId(88),
..
}
)
});
assert!(
has_line_error,
"expected Line source_bus_id=99 error, got: {errors:?}"
);
assert!(
has_thermal_error,
"expected Thermal bus_id=88 error, got: {errors:?}"
);
assert!(
errors.len() >= 2,
"expected at least 2 errors, got {}: {errors:?}",
errors.len()
);
}
#[test]
fn test_valid_cross_references_pass() {
let bus_0 = make_bus(0);
let bus_1 = make_bus(1);
let h0 = make_hydro_on_bus(0, 0);
let h1 = make_hydro_on_bus(1, 1);
let mut h2 = make_hydro_on_bus(2, 0);
h2.downstream_id = Some(EntityId(1));
let line = make_line(10, 0, 1);
let thermal = make_thermal_on_bus(20, 0);
let ps = make_pumping_station_full(30, 0, 0, 1);
let contract = make_contract_on_bus(40, 1);
let ncs = make_ncs_on_bus(50, 0);
let result = SystemBuilder::new()
.buses(vec![bus_0, bus_1])
.lines(vec![line])
.hydros(vec![h0, h1, h2])
.thermals(vec![thermal])
.pumping_stations(vec![ps])
.contracts(vec![contract])
.non_controllable_sources(vec![ncs])
.build();
assert!(
result.is_ok(),
"expected Ok for all valid cross-references, got: {:?}",
result.unwrap_err()
);
let system = result.unwrap_or_else(|_| unreachable!());
assert_eq!(system.n_buses(), 2);
assert_eq!(system.n_hydros(), 3);
assert_eq!(system.n_lines(), 1);
assert_eq!(system.n_thermals(), 1);
assert_eq!(system.n_pumping_stations(), 1);
assert_eq!(system.n_contracts(), 1);
assert_eq!(system.n_non_controllable_sources(), 1);
}
#[test]
fn test_cascade_cycle_detected() {
let bus = make_bus(0);
let mut h0 = make_hydro(0);
h0.downstream_id = Some(EntityId(1));
let mut h1 = make_hydro(1);
h1.downstream_id = Some(EntityId(2));
let mut h2 = make_hydro(2);
h2.downstream_id = Some(EntityId(0));
let result = SystemBuilder::new()
.buses(vec![bus])
.hydros(vec![h0, h1, h2])
.build();
assert!(result.is_err(), "expected Err for 3-node cycle");
let errors = result.unwrap_err();
let cycle_error = errors
.iter()
.find(|e| matches!(e, ValidationError::CascadeCycle { .. }));
assert!(
cycle_error.is_some(),
"expected CascadeCycle error, got: {errors:?}"
);
let ValidationError::CascadeCycle { cycle_ids } = cycle_error.unwrap() else {
unreachable!()
};
assert_eq!(
cycle_ids,
&[EntityId(0), EntityId(1), EntityId(2)],
"cycle_ids must be sorted ascending, got: {cycle_ids:?}"
);
}
#[test]
fn test_cascade_self_loop_detected() {
let bus = make_bus(0);
let mut h0 = make_hydro(0);
h0.downstream_id = Some(EntityId(0));
let result = SystemBuilder::new()
.buses(vec![bus])
.hydros(vec![h0])
.build();
assert!(result.is_err(), "expected Err for self-loop");
let errors = result.unwrap_err();
let has_cycle = errors
.iter()
.any(|e| matches!(e, ValidationError::CascadeCycle { cycle_ids } if cycle_ids.contains(&EntityId(0))));
assert!(
has_cycle,
"expected CascadeCycle containing EntityId(0), got: {errors:?}"
);
}
#[test]
fn test_valid_acyclic_cascade_passes() {
let bus = make_bus(0);
let mut h0 = make_hydro(0);
h0.downstream_id = Some(EntityId(1));
let mut h1 = make_hydro(1);
h1.downstream_id = Some(EntityId(2));
let h2 = make_hydro(2);
let result = SystemBuilder::new()
.buses(vec![bus])
.hydros(vec![h0, h1, h2])
.build();
assert!(
result.is_ok(),
"expected Ok for acyclic cascade, got: {:?}",
result.unwrap_err()
);
let system = result.unwrap_or_else(|_| unreachable!());
assert_eq!(
system.cascade().topological_order().len(),
system.n_hydros(),
"topological_order must contain all hydros"
);
}
#[test]
fn test_filling_without_entry_stage() {
use crate::entities::FillingConfig;
let bus = make_bus(0);
let mut hydro = make_hydro(1);
hydro.entry_stage_id = None;
hydro.filling = Some(FillingConfig {
start_stage_id: 10,
filling_inflow_m3s: 100.0,
});
let result = SystemBuilder::new()
.buses(vec![bus])
.hydros(vec![hydro])
.build();
assert!(
result.is_err(),
"expected Err for filling without entry_stage_id"
);
let errors = result.unwrap_err();
let has_error = errors.iter().any(|e| match e {
ValidationError::InvalidFillingConfig { hydro_id, reason } => {
*hydro_id == EntityId(1) && reason.contains("entry_stage_id")
}
_ => false,
});
assert!(
has_error,
"expected InvalidFillingConfig with entry_stage_id reason, got: {errors:?}"
);
}
#[test]
fn test_filling_negative_inflow() {
use crate::entities::FillingConfig;
let bus = make_bus(0);
let mut hydro = make_hydro(1);
hydro.entry_stage_id = Some(10);
hydro.filling = Some(FillingConfig {
start_stage_id: 10,
filling_inflow_m3s: -5.0,
});
let result = SystemBuilder::new()
.buses(vec![bus])
.hydros(vec![hydro])
.build();
assert!(
result.is_err(),
"expected Err for negative filling_inflow_m3s"
);
let errors = result.unwrap_err();
let has_error = errors.iter().any(|e| match e {
ValidationError::InvalidFillingConfig { hydro_id, reason } => {
*hydro_id == EntityId(1) && reason.contains("filling_inflow_m3s must be positive")
}
_ => false,
});
assert!(
has_error,
"expected InvalidFillingConfig with positive inflow reason, got: {errors:?}"
);
}
#[test]
fn test_valid_filling_config_passes() {
use crate::entities::FillingConfig;
let bus = make_bus(0);
let mut hydro = make_hydro(1);
hydro.entry_stage_id = Some(10);
hydro.filling = Some(FillingConfig {
start_stage_id: 10,
filling_inflow_m3s: 100.0,
});
let result = SystemBuilder::new()
.buses(vec![bus])
.hydros(vec![hydro])
.build();
assert!(
result.is_ok(),
"expected Ok for valid filling config, got: {:?}",
result.unwrap_err()
);
}
#[test]
fn test_cascade_cycle_and_invalid_filling_both_reported() {
use crate::entities::FillingConfig;
let bus = make_bus(0);
let mut h0 = make_hydro(0);
h0.downstream_id = Some(EntityId(0));
let mut h1 = make_hydro(1);
h1.entry_stage_id = None; h1.filling = Some(FillingConfig {
start_stage_id: 5,
filling_inflow_m3s: 50.0,
});
let result = SystemBuilder::new()
.buses(vec![bus])
.hydros(vec![h0, h1])
.build();
assert!(result.is_err(), "expected Err for cycle + invalid filling");
let errors = result.unwrap_err();
let has_cycle = errors
.iter()
.any(|e| matches!(e, ValidationError::CascadeCycle { .. }));
let has_filling = errors
.iter()
.any(|e| matches!(e, ValidationError::InvalidFillingConfig { .. }));
assert!(has_cycle, "expected CascadeCycle error, got: {errors:?}");
assert!(
has_filling,
"expected InvalidFillingConfig error, got: {errors:?}"
);
}
#[cfg(feature = "serde")]
#[test]
fn test_system_serde_roundtrip() {
let bus_a = make_bus(1);
let bus_b = make_bus(2);
let hydro = make_hydro_on_bus(10, 1);
let thermal = make_thermal_on_bus(20, 2);
let line = make_line(1, 1, 2);
let system = SystemBuilder::new()
.buses(vec![bus_a, bus_b])
.hydros(vec![hydro])
.thermals(vec![thermal])
.lines(vec![line])
.build()
.expect("valid system");
let json = serde_json::to_string(&system).unwrap();
let mut deserialized: System = serde_json::from_str(&json).unwrap();
deserialized.rebuild_indices();
assert_eq!(system.buses(), deserialized.buses());
assert_eq!(system.hydros(), deserialized.hydros());
assert_eq!(system.thermals(), deserialized.thermals());
assert_eq!(system.lines(), deserialized.lines());
assert_eq!(
deserialized.bus(EntityId(1)).map(|b| b.id),
Some(EntityId(1))
);
assert_eq!(
deserialized.hydro(EntityId(10)).map(|h| h.id),
Some(EntityId(10))
);
assert_eq!(
deserialized.thermal(EntityId(20)).map(|t| t.id),
Some(EntityId(20))
);
assert_eq!(
deserialized.line(EntityId(1)).map(|l| l.id),
Some(EntityId(1))
);
}
fn make_stage(id: i32) -> Stage {
use crate::temporal::{
Block, BlockMode, NoiseMethod, ScenarioSourceConfig, StageRiskConfig, StageStateConfig,
};
use chrono::NaiveDate;
Stage {
index: usize::try_from(id.max(0)).unwrap_or(0),
id,
start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
end_date: NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
season_id: Some(0),
blocks: vec![Block {
index: 0,
name: "SINGLE".to_string(),
duration_hours: 744.0,
}],
block_mode: BlockMode::Parallel,
state_config: StageStateConfig {
storage: true,
inflow_lags: false,
},
risk_config: StageRiskConfig::Expectation,
scenario_config: ScenarioSourceConfig {
branching_factor: 50,
noise_method: NoiseMethod::Saa,
},
}
}
#[test]
fn test_system_backward_compat() {
let system = SystemBuilder::new().build().expect("empty system is valid");
assert_eq!(system.n_buses(), 0);
assert_eq!(system.n_hydros(), 0);
assert_eq!(system.n_stages(), 0);
assert!(system.stages().is_empty());
assert!(system.initial_conditions().storage.is_empty());
assert!(system.generic_constraints().is_empty());
assert!(system.inflow_models().is_empty());
assert!(system.load_models().is_empty());
assert_eq!(system.penalties().n_stages(), 0);
assert_eq!(system.bounds().n_stages(), 0);
assert!(!system.resolved_generic_bounds().is_active(0, 0));
assert!(
system
.resolved_generic_bounds()
.bounds_for_stage(0, 0)
.is_empty()
);
}
#[test]
fn test_system_resolved_generic_bounds_accessor() {
use crate::resolved::ResolvedGenericConstraintBounds;
use std::collections::HashMap as StdHashMap;
let id_map: StdHashMap<i32, usize> = [(0, 0), (1, 1)].into_iter().collect();
let rows = vec![(0i32, 0i32, None::<i32>, 100.0f64)];
let table = ResolvedGenericConstraintBounds::new(&id_map, rows.into_iter());
let system = SystemBuilder::new()
.resolved_generic_bounds(table)
.build()
.expect("valid system");
assert!(system.resolved_generic_bounds().is_active(0, 0));
assert!(!system.resolved_generic_bounds().is_active(1, 0));
let slice = system.resolved_generic_bounds().bounds_for_stage(0, 0);
assert_eq!(slice.len(), 1);
assert_eq!(slice[0], (None, 100.0));
}
#[test]
fn test_system_with_stages() {
let s0 = make_stage(0);
let s1 = make_stage(1);
let system = SystemBuilder::new()
.stages(vec![s1.clone(), s0.clone()]) .build()
.expect("valid system");
assert_eq!(system.n_stages(), 2);
assert_eq!(system.stages()[0].id, 0);
assert_eq!(system.stages()[1].id, 1);
let found = system.stage(0).expect("stage 0 must be found");
assert_eq!(found.id, s0.id);
let found1 = system.stage(1).expect("stage 1 must be found");
assert_eq!(found1.id, s1.id);
assert!(system.stage(99).is_none());
}
#[test]
fn test_system_stage_lookup_by_id() {
let stages: Vec<Stage> = [0i32, 1, 2].iter().map(|&id| make_stage(id)).collect();
let system = SystemBuilder::new()
.stages(stages)
.build()
.expect("valid system");
assert_eq!(system.stage(1).map(|s| s.id), Some(1));
assert!(system.stage(99).is_none());
}
#[test]
fn test_system_with_initial_conditions() {
let ic = InitialConditions {
storage: vec![crate::HydroStorage {
hydro_id: EntityId(0),
value_hm3: 15_000.0,
}],
filling_storage: vec![],
past_inflows: vec![],
};
let system = SystemBuilder::new()
.initial_conditions(ic)
.build()
.expect("valid system");
assert_eq!(system.initial_conditions().storage.len(), 1);
assert_eq!(system.initial_conditions().storage[0].hydro_id, EntityId(0));
assert!((system.initial_conditions().storage[0].value_hm3 - 15_000.0).abs() < f64::EPSILON);
}
#[cfg(feature = "serde")]
#[test]
fn test_system_serde_roundtrip_with_stages() {
use crate::temporal::PolicyGraphType;
let stages = vec![make_stage(0), make_stage(1)];
let policy_graph = PolicyGraph {
graph_type: PolicyGraphType::FiniteHorizon,
annual_discount_rate: 0.0,
transitions: vec![],
season_map: None,
};
let system = SystemBuilder::new()
.stages(stages)
.policy_graph(policy_graph)
.build()
.expect("valid system");
let json = serde_json::to_string(&system).unwrap();
let mut deserialized: System = serde_json::from_str(&json).unwrap();
deserialized.rebuild_indices();
assert_eq!(system.n_stages(), deserialized.n_stages());
assert_eq!(system.stages()[0].id, deserialized.stages()[0].id);
assert_eq!(system.stages()[1].id, deserialized.stages()[1].id);
assert_eq!(deserialized.stage(0).map(|s| s.id), Some(0));
assert_eq!(deserialized.stage(1).map(|s| s.id), Some(1));
assert!(deserialized.stage(99).is_none());
assert_eq!(
deserialized.policy_graph().graph_type,
system.policy_graph().graph_type
);
}
}