use crate::EntityId;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TailracePoint {
pub outflow_m3s: f64,
pub height_m: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DiversionChannel {
pub downstream_id: EntityId,
pub max_flow_m3s: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FillingConfig {
pub start_stage_id: i32,
pub filling_inflow_m3s: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HydroPenalties {
pub spillage_cost: f64,
pub diversion_cost: f64,
pub fpha_turbined_cost: f64,
pub storage_violation_below_cost: f64,
pub filling_target_violation_cost: f64,
pub turbined_violation_below_cost: f64,
pub outflow_violation_below_cost: f64,
pub outflow_violation_above_cost: f64,
pub generation_violation_below_cost: f64,
pub evaporation_violation_cost: f64,
pub water_withdrawal_violation_cost: f64,
pub water_withdrawal_violation_pos_cost: f64,
pub water_withdrawal_violation_neg_cost: f64,
pub evaporation_violation_pos_cost: f64,
pub evaporation_violation_neg_cost: f64,
pub inflow_nonnegativity_cost: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum HydroGenerationModel {
ConstantProductivity {
productivity_mw_per_m3s: f64,
},
LinearizedHead {
productivity_mw_per_m3s: f64,
},
Fpha,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TailraceModel {
Polynomial {
coefficients: Vec<f64>,
},
Piecewise {
points: Vec<TailracePoint>,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum HydraulicLossesModel {
Factor {
value: f64,
},
Constant {
value_m: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EfficiencyModel {
Constant {
value: f64,
},
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Hydro {
pub id: EntityId,
pub name: String,
pub bus_id: EntityId,
pub downstream_id: Option<EntityId>,
pub entry_stage_id: Option<i32>,
pub exit_stage_id: Option<i32>,
pub min_storage_hm3: f64,
pub max_storage_hm3: f64,
pub min_outflow_m3s: f64,
pub max_outflow_m3s: Option<f64>,
pub generation_model: HydroGenerationModel,
pub min_turbined_m3s: f64,
pub max_turbined_m3s: f64,
pub min_generation_mw: f64,
pub max_generation_mw: f64,
pub tailrace: Option<TailraceModel>,
pub hydraulic_losses: Option<HydraulicLossesModel>,
pub efficiency: Option<EfficiencyModel>,
pub evaporation_coefficients_mm: Option<[f64; 12]>,
pub evaporation_reference_volumes_hm3: Option<[f64; 12]>,
pub diversion: Option<DiversionChannel>,
pub filling: Option<FillingConfig>,
pub penalties: HydroPenalties,
}
#[cfg(test)]
mod tests {
use super::*;
fn penalties_all(v: f64) -> HydroPenalties {
HydroPenalties {
spillage_cost: v,
diversion_cost: v,
fpha_turbined_cost: v,
storage_violation_below_cost: v,
filling_target_violation_cost: v,
turbined_violation_below_cost: v,
outflow_violation_below_cost: v,
outflow_violation_above_cost: v,
generation_violation_below_cost: v,
evaporation_violation_cost: v,
water_withdrawal_violation_cost: v,
water_withdrawal_violation_pos_cost: v,
water_withdrawal_violation_neg_cost: v,
evaporation_violation_pos_cost: v,
evaporation_violation_neg_cost: v,
inflow_nonnegativity_cost: 1000.0,
}
}
fn minimal_hydro(model: HydroGenerationModel) -> Hydro {
Hydro {
id: EntityId::from(1),
name: String::from("Itaipu"),
bus_id: EntityId::from(10),
downstream_id: None,
entry_stage_id: None,
exit_stage_id: None,
min_storage_hm3: 100.0,
max_storage_hm3: 2000.0,
min_outflow_m3s: 500.0,
max_outflow_m3s: None,
generation_model: model,
min_turbined_m3s: 200.0,
max_turbined_m3s: 12_600.0,
min_generation_mw: 0.0,
max_generation_mw: 14_000.0,
tailrace: None,
hydraulic_losses: None,
efficiency: None,
evaporation_coefficients_mm: None,
evaporation_reference_volumes_hm3: None,
diversion: None,
filling: None,
penalties: penalties_all(0.0),
}
}
#[test]
fn test_hydro_constant_productivity() {
let hydro = minimal_hydro(HydroGenerationModel::ConstantProductivity {
productivity_mw_per_m3s: 0.8765,
});
let HydroGenerationModel::ConstantProductivity {
productivity_mw_per_m3s,
} = hydro.generation_model
else {
panic!("expected ConstantProductivity variant");
};
assert!((productivity_mw_per_m3s - 0.8765).abs() < f64::EPSILON);
}
#[test]
fn test_hydro_fpha() {
let hydro = minimal_hydro(HydroGenerationModel::Fpha);
assert_eq!(hydro.generation_model, HydroGenerationModel::Fpha);
}
#[test]
fn test_hydro_optional_fields_none() {
let hydro = minimal_hydro(HydroGenerationModel::ConstantProductivity {
productivity_mw_per_m3s: 1.0,
});
assert_eq!(hydro.downstream_id, None);
assert_eq!(hydro.entry_stage_id, None);
assert_eq!(hydro.exit_stage_id, None);
assert_eq!(hydro.max_outflow_m3s, None);
assert!(hydro.tailrace.is_none());
assert!(hydro.hydraulic_losses.is_none());
assert!(hydro.efficiency.is_none());
assert_eq!(hydro.evaporation_coefficients_mm, None);
assert_eq!(hydro.evaporation_reference_volumes_hm3, None);
assert!(hydro.diversion.is_none());
assert!(hydro.filling.is_none());
}
#[test]
fn test_hydro_optional_fields_some() {
let hydro = Hydro {
id: EntityId::from(2),
name: String::from("Tucuruí"),
bus_id: EntityId::from(20),
downstream_id: Some(EntityId::from(3)),
entry_stage_id: Some(1),
exit_stage_id: Some(600),
min_storage_hm3: 50.0,
max_storage_hm3: 45_000.0,
min_outflow_m3s: 1000.0,
max_outflow_m3s: Some(100_000.0),
generation_model: HydroGenerationModel::LinearizedHead {
productivity_mw_per_m3s: 0.75,
},
min_turbined_m3s: 500.0,
max_turbined_m3s: 22_500.0,
min_generation_mw: 0.0,
max_generation_mw: 8370.0,
tailrace: Some(TailraceModel::Polynomial {
coefficients: vec![5.0, 0.001],
}),
hydraulic_losses: Some(HydraulicLossesModel::Factor { value: 0.03 }),
efficiency: Some(EfficiencyModel::Constant { value: 0.93 }),
evaporation_coefficients_mm: Some([
80.0, 75.0, 70.0, 65.0, 60.0, 55.0, 60.0, 65.0, 70.0, 75.0, 80.0, 85.0,
]),
evaporation_reference_volumes_hm3: Some([
12_000.0, 11_500.0, 11_000.0, 10_500.0, 10_000.0, 9_500.0, 10_000.0, 10_500.0,
11_000.0, 11_500.0, 12_000.0, 12_500.0,
]),
diversion: Some(DiversionChannel {
downstream_id: EntityId::from(4),
max_flow_m3s: 200.0,
}),
filling: Some(FillingConfig {
start_stage_id: 48,
filling_inflow_m3s: 100.0,
}),
penalties: penalties_all(1.0),
};
assert_eq!(hydro.downstream_id, Some(EntityId::from(3)));
assert_eq!(hydro.entry_stage_id, Some(1));
assert_eq!(hydro.exit_stage_id, Some(600));
assert_eq!(hydro.max_outflow_m3s, Some(100_000.0));
assert!(hydro.tailrace.is_some());
assert!(hydro.hydraulic_losses.is_some());
assert!(hydro.efficiency.is_some());
assert!(hydro.evaporation_coefficients_mm.is_some());
assert_eq!(hydro.evaporation_coefficients_mm.map(|a| a.len()), Some(12));
assert!(hydro.evaporation_reference_volumes_hm3.is_some());
assert_eq!(
hydro.evaporation_reference_volumes_hm3.map(|a| a.len()),
Some(12)
);
assert!(hydro.diversion.is_some());
assert!(hydro.filling.is_some());
}
#[test]
fn test_tailrace_polynomial() {
let model = TailraceModel::Polynomial {
coefficients: vec![3.5, 0.0012, -0.000_001],
};
let TailraceModel::Polynomial { coefficients } = model else {
panic!("expected Polynomial variant");
};
assert_eq!(coefficients.len(), 3);
assert!((coefficients[0] - 3.5).abs() < f64::EPSILON);
assert!((coefficients[1] - 0.0012).abs() < f64::EPSILON);
assert!((coefficients[2] - -0.000_001_f64).abs() < f64::EPSILON);
}
#[test]
fn test_tailrace_piecewise() {
let model = TailraceModel::Piecewise {
points: vec![
TailracePoint {
outflow_m3s: 0.0,
height_m: 3.0,
},
TailracePoint {
outflow_m3s: 5000.0,
height_m: 4.5,
},
TailracePoint {
outflow_m3s: 15_000.0,
height_m: 6.2,
},
],
};
let TailraceModel::Piecewise { points } = model else {
panic!("expected Piecewise variant");
};
assert_eq!(points.len(), 3);
assert!((points[0].outflow_m3s - 0.0).abs() < f64::EPSILON);
assert!((points[1].height_m - 4.5).abs() < f64::EPSILON);
assert!((points[2].outflow_m3s - 15_000.0).abs() < f64::EPSILON);
}
#[test]
fn test_hydraulic_losses_factor() {
let model = HydraulicLossesModel::Factor { value: 0.03 };
let HydraulicLossesModel::Factor { value } = model else {
panic!("expected Factor variant");
};
assert!((value - 0.03).abs() < f64::EPSILON);
}
#[test]
fn test_filling_config() {
let config = FillingConfig {
start_stage_id: 48,
filling_inflow_m3s: 100.0,
};
assert_eq!(config.start_stage_id, 48);
assert!((config.filling_inflow_m3s - 100.0).abs() < f64::EPSILON);
}
#[test]
fn test_hydro_penalties_all_fields() {
let p = HydroPenalties {
spillage_cost: 1.0,
diversion_cost: 2.0,
fpha_turbined_cost: 3.0,
storage_violation_below_cost: 4.0,
filling_target_violation_cost: 5.0,
turbined_violation_below_cost: 6.0,
outflow_violation_below_cost: 7.0,
outflow_violation_above_cost: 8.0,
generation_violation_below_cost: 9.0,
evaporation_violation_cost: 10.0,
water_withdrawal_violation_cost: 11.0,
water_withdrawal_violation_pos_cost: 11.0,
water_withdrawal_violation_neg_cost: 11.0,
evaporation_violation_pos_cost: 10.0,
evaporation_violation_neg_cost: 10.0,
inflow_nonnegativity_cost: 1000.0,
};
assert!((p.spillage_cost - 1.0).abs() < f64::EPSILON);
assert!((p.diversion_cost - 2.0).abs() < f64::EPSILON);
assert!((p.fpha_turbined_cost - 3.0).abs() < f64::EPSILON);
assert!((p.storage_violation_below_cost - 4.0).abs() < f64::EPSILON);
assert!((p.filling_target_violation_cost - 5.0).abs() < f64::EPSILON);
assert!((p.turbined_violation_below_cost - 6.0).abs() < f64::EPSILON);
assert!((p.outflow_violation_below_cost - 7.0).abs() < f64::EPSILON);
assert!((p.outflow_violation_above_cost - 8.0).abs() < f64::EPSILON);
assert!((p.generation_violation_below_cost - 9.0).abs() < f64::EPSILON);
assert!((p.evaporation_violation_cost - 10.0).abs() < f64::EPSILON);
assert!((p.water_withdrawal_violation_cost - 11.0).abs() < f64::EPSILON);
}
#[test]
fn test_diversion_channel() {
let channel = DiversionChannel {
downstream_id: EntityId::from(7),
max_flow_m3s: 350.0,
};
assert_eq!(channel.downstream_id, EntityId::from(7));
assert!((channel.max_flow_m3s - 350.0).abs() < f64::EPSILON);
}
#[cfg(feature = "serde")]
#[test]
fn test_hydro_serde_roundtrip() {
let hydro = Hydro {
id: EntityId::from(2),
name: "Tucuruí".to_string(),
bus_id: EntityId::from(20),
downstream_id: Some(EntityId::from(3)),
entry_stage_id: Some(1),
exit_stage_id: Some(600),
min_storage_hm3: 50.0,
max_storage_hm3: 45_000.0,
min_outflow_m3s: 1000.0,
max_outflow_m3s: Some(100_000.0),
generation_model: HydroGenerationModel::ConstantProductivity {
productivity_mw_per_m3s: 0.8765,
},
min_turbined_m3s: 500.0,
max_turbined_m3s: 22_500.0,
min_generation_mw: 0.0,
max_generation_mw: 8370.0,
tailrace: Some(TailraceModel::Polynomial {
coefficients: vec![5.0, 0.001],
}),
hydraulic_losses: Some(HydraulicLossesModel::Factor { value: 0.03 }),
efficiency: Some(EfficiencyModel::Constant { value: 0.93 }),
evaporation_coefficients_mm: Some([
80.0, 75.0, 70.0, 65.0, 60.0, 55.0, 60.0, 65.0, 70.0, 75.0, 80.0, 85.0,
]),
evaporation_reference_volumes_hm3: Some([
12_000.0, 11_500.0, 11_000.0, 10_500.0, 10_000.0, 9_500.0, 10_000.0, 10_500.0,
11_000.0, 11_500.0, 12_000.0, 12_500.0,
]),
diversion: Some(DiversionChannel {
downstream_id: EntityId::from(4),
max_flow_m3s: 200.0,
}),
filling: Some(FillingConfig {
start_stage_id: 48,
filling_inflow_m3s: 100.0,
}),
penalties: HydroPenalties {
spillage_cost: 0.01,
diversion_cost: 0.02,
fpha_turbined_cost: 0.03,
storage_violation_below_cost: 1.0,
filling_target_violation_cost: 2.0,
turbined_violation_below_cost: 3.0,
outflow_violation_below_cost: 4.0,
outflow_violation_above_cost: 5.0,
generation_violation_below_cost: 6.0,
evaporation_violation_cost: 7.0,
water_withdrawal_violation_cost: 8.0,
water_withdrawal_violation_pos_cost: 8.0,
water_withdrawal_violation_neg_cost: 8.0,
evaporation_violation_pos_cost: 7.0,
evaporation_violation_neg_cost: 7.0,
inflow_nonnegativity_cost: 1000.0,
},
};
let json = serde_json::to_string(&hydro).unwrap();
let deserialized: Hydro = serde_json::from_str(&json).unwrap();
assert_eq!(hydro, deserialized);
}
#[test]
fn test_hydro_evaporation_reference_volumes() {
let volumes: [f64; 12] = [
12_000.0, 11_500.0, 11_000.0, 10_500.0, 10_000.0, 9_500.0, 10_000.0, 10_500.0,
11_000.0, 11_500.0, 12_000.0, 12_500.0,
];
let hydro = Hydro {
evaporation_reference_volumes_hm3: Some(volumes),
..minimal_hydro(HydroGenerationModel::ConstantProductivity {
productivity_mw_per_m3s: 1.0,
})
};
assert_eq!(hydro.evaporation_reference_volumes_hm3, Some(volumes));
assert_eq!(
hydro.evaporation_reference_volumes_hm3.map(|a| a.len()),
Some(12)
);
assert!(
(hydro.evaporation_reference_volumes_hm3.unwrap()[0] - 12_000.0).abs() < f64::EPSILON
);
assert!(
(hydro.evaporation_reference_volumes_hm3.unwrap()[5] - 9_500.0).abs() < f64::EPSILON
);
}
}