#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct SimulationCostResult {
pub stage_id: u32,
pub block_id: Option<u32>,
pub total_cost: f64,
pub immediate_cost: f64,
pub future_cost: f64,
pub discount_factor: f64,
pub thermal_cost: f64,
pub anticipated_thermal_cost: f64,
pub contract_cost: f64,
pub deficit_cost: f64,
pub excess_cost: f64,
pub storage_violation_cost: f64,
pub filling_target_cost: f64,
pub hydro_violation_cost: f64,
pub outflow_violation_below_cost: f64,
pub outflow_violation_above_cost: f64,
pub turbined_violation_cost: f64,
pub generation_violation_cost: f64,
pub evaporation_violation_cost: f64,
pub withdrawal_violation_cost: f64,
pub inflow_penalty_cost: f64,
pub generic_violation_cost: f64,
pub spillage_cost: f64,
pub turbined_cost: f64,
pub curtailment_cost: f64,
pub exchange_cost: f64,
pub pumping_cost: f64,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct SimulationHydroResult {
pub stage_id: u32,
pub block_id: Option<u32>,
pub hydro_id: i32,
pub turbined_m3s: f64,
pub spillage_m3s: f64,
pub evaporation_m3s: Option<f64>,
pub diverted_inflow_m3s: Option<f64>,
pub diverted_outflow_m3s: Option<f64>,
pub incremental_inflow_m3s: f64,
pub inflow_m3s: f64,
pub storage_initial_hm3: f64,
pub storage_final_hm3: f64,
pub generation_mw: f64,
pub equivalent_productivity_mw_per_m3s: f64,
pub accumulated_productivity_mw_per_m3s: f64,
pub incremental_inflow_energy_mw: f64,
pub stored_energy_initial_mwh: f64,
pub stored_energy_final_mwh: f64,
pub spillage_cost: f64,
pub water_value_per_hm3: f64,
pub storage_binding_code: i8,
pub operative_state_code: i8,
pub turbined_slack_m3s: f64,
pub outflow_slack_below_m3s: f64,
pub outflow_slack_above_m3s: f64,
pub generation_slack_mw: f64,
pub storage_violation_below_hm3: f64,
pub filling_target_violation_hm3: f64,
pub evaporation_violation_pos_m3s: f64,
pub evaporation_violation_neg_m3s: f64,
pub inflow_nonnegativity_slack_m3s: f64,
pub water_withdrawal_violation_pos_m3s: f64,
pub water_withdrawal_violation_neg_m3s: f64,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct SimulationThermalResult {
pub stage_id: u32,
pub block_id: Option<u32>,
pub thermal_id: i32,
pub generation_mw: f64,
pub generation_cost: f64,
pub is_anticipated: bool,
pub anticipated_committed_mw: Option<f64>,
pub anticipated_decision_mw: Option<f64>,
pub operative_state_code: i8,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct SimulationExchangeResult {
pub stage_id: u32,
pub block_id: Option<u32>,
pub line_id: i32,
pub direct_flow_mw: f64,
pub reverse_flow_mw: f64,
pub exchange_cost: f64,
pub operative_state_code: i8,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct SimulationBusResult {
pub stage_id: u32,
pub block_id: Option<u32>,
pub bus_id: i32,
pub load_mw: f64,
pub deficit_mw: f64,
pub excess_mw: f64,
pub spot_price: f64,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct SimulationPumpingResult {
pub stage_id: u32,
pub block_id: Option<u32>,
pub pumping_station_id: i32,
pub pumped_flow_m3s: f64,
pub power_consumption_mw: f64,
pub pumping_cost: f64,
pub operative_state_code: i8,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct SimulationContractResult {
pub stage_id: u32,
pub block_id: Option<u32>,
pub contract_id: i32,
pub power_mw: f64,
pub price_per_mwh: f64,
pub total_cost: f64,
pub operative_state_code: i8,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct SimulationNonControllableResult {
pub stage_id: u32,
pub block_id: Option<u32>,
pub non_controllable_id: i32,
pub generation_mw: f64,
pub available_mw: f64,
pub curtailment_mw: f64,
pub curtailment_cost: f64,
pub operative_state_code: i8,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct SimulationInflowLagResult {
pub stage_id: u32,
pub hydro_id: i32,
pub lag_index: u32,
pub inflow_m3s: f64,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct SimulationGenericViolationResult {
pub stage_id: u32,
pub block_id: Option<u32>,
pub constraint_id: i32,
pub slack_value: f64,
pub slack_cost: f64,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct SimulationStageResult {
pub stage_id: u32,
pub costs: Vec<SimulationCostResult>,
pub hydros: Vec<SimulationHydroResult>,
pub thermals: Vec<SimulationThermalResult>,
pub exchanges: Vec<SimulationExchangeResult>,
pub buses: Vec<SimulationBusResult>,
pub pumping_stations: Vec<SimulationPumpingResult>,
pub contracts: Vec<SimulationContractResult>,
pub non_controllables: Vec<SimulationNonControllableResult>,
pub inflow_lags: Vec<SimulationInflowLagResult>,
pub generic_violations: Vec<SimulationGenericViolationResult>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ScenarioCategoryCosts {
pub resource_cost: f64,
pub recourse_cost: f64,
pub violation_cost: f64,
pub regularization_cost: f64,
pub imputed_cost: f64,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct SimulationScenarioResult {
pub scenario_id: u32,
pub total_cost: f64,
pub per_category_costs: ScenarioCategoryCosts,
pub stages: Vec<SimulationStageResult>,
}
#[derive(Debug)]
pub struct SimulationSummary {
pub mean_cost: f64,
pub std_cost: f64,
pub min_cost: f64,
pub max_cost: f64,
pub cvar: f64,
pub cvar_alpha: f64,
pub category_stats: Vec<CategoryCostStats>,
pub deficit_frequency: f64,
pub total_deficit_mwh: f64,
pub total_spillage_mwh: f64,
pub n_scenarios: u32,
}
#[derive(Debug)]
pub struct CategoryCostStats {
pub category: String,
pub mean: f64,
pub max: f64,
pub frequency: f64,
}
const _: fn() = || {
fn assert_send<T: Send>() {}
assert_send::<SimulationScenarioResult>();
};
#[cfg(test)]
mod tests {
use super::{
CategoryCostStats, ScenarioCategoryCosts, SimulationBusResult, SimulationContractResult,
SimulationCostResult, SimulationExchangeResult, SimulationGenericViolationResult,
SimulationHydroResult, SimulationInflowLagResult, SimulationNonControllableResult,
SimulationPumpingResult, SimulationScenarioResult, SimulationStageResult,
SimulationSummary, SimulationThermalResult,
};
#[test]
fn cost_result_construction_all_fields() {
let r = SimulationCostResult {
stage_id: 0,
block_id: Some(0),
total_cost: 1000.0,
immediate_cost: 800.0,
future_cost: 200.0,
discount_factor: 0.95,
thermal_cost: 500.0,
anticipated_thermal_cost: 90.0,
contract_cost: 100.0,
deficit_cost: 50.0,
excess_cost: 10.0,
storage_violation_cost: 20.0,
filling_target_cost: 30.0,
hydro_violation_cost: 5.0,
outflow_violation_below_cost: 0.0,
outflow_violation_above_cost: 0.0,
turbined_violation_cost: 0.0,
generation_violation_cost: 0.0,
evaporation_violation_cost: 0.0,
withdrawal_violation_cost: 0.0,
inflow_penalty_cost: 3.0,
generic_violation_cost: 2.0,
spillage_cost: 1.0,
turbined_cost: 4.0,
curtailment_cost: 7.0,
exchange_cost: 8.0,
pumping_cost: 60.0,
};
assert_eq!(r.stage_id, 0);
assert_eq!(r.block_id, Some(0));
assert_eq!(r.total_cost, 1000.0);
assert_eq!(r.immediate_cost, 800.0);
assert_eq!(r.future_cost, 200.0);
assert_eq!(r.discount_factor, 0.95);
assert_eq!(r.thermal_cost, 500.0);
assert_eq!(r.anticipated_thermal_cost, 90.0);
assert_eq!(r.contract_cost, 100.0);
assert_eq!(r.deficit_cost, 50.0);
assert_eq!(r.excess_cost, 10.0);
assert_eq!(r.storage_violation_cost, 20.0);
assert_eq!(r.filling_target_cost, 30.0);
assert_eq!(r.hydro_violation_cost, 5.0);
assert_eq!(r.inflow_penalty_cost, 3.0);
assert_eq!(r.generic_violation_cost, 2.0);
assert_eq!(r.spillage_cost, 1.0);
assert_eq!(r.turbined_cost, 4.0);
assert_eq!(r.curtailment_cost, 7.0);
assert_eq!(r.exchange_cost, 8.0);
assert_eq!(r.pumping_cost, 60.0);
}
#[test]
fn hydro_result_optional_fields() {
let r = SimulationHydroResult {
stage_id: 1,
block_id: Some(0),
hydro_id: 5,
turbined_m3s: 100.0,
spillage_m3s: 0.0,
evaporation_m3s: None,
diverted_inflow_m3s: None,
diverted_outflow_m3s: None,
incremental_inflow_m3s: 200.0,
inflow_m3s: 200.0,
storage_initial_hm3: 500.0,
storage_final_hm3: 480.0,
generation_mw: 50.0,
equivalent_productivity_mw_per_m3s: 0.5,
accumulated_productivity_mw_per_m3s: 0.5,
incremental_inflow_energy_mw: 100.0,
stored_energy_initial_mwh: 250.0,
stored_energy_final_mwh: 240.0,
spillage_cost: 0.0,
water_value_per_hm3: 10.0,
storage_binding_code: 0,
operative_state_code: 1,
turbined_slack_m3s: 0.0,
outflow_slack_below_m3s: 0.0,
outflow_slack_above_m3s: 0.0,
generation_slack_mw: 0.0,
storage_violation_below_hm3: 0.0,
filling_target_violation_hm3: 0.0,
evaporation_violation_pos_m3s: 0.0,
evaporation_violation_neg_m3s: 0.0,
inflow_nonnegativity_slack_m3s: 0.0,
water_withdrawal_violation_pos_m3s: 0.0,
water_withdrawal_violation_neg_m3s: 0.0,
};
assert_eq!(r.hydro_id, 5);
assert_eq!(r.turbined_m3s, 100.0);
assert_eq!(r.evaporation_m3s, None);
assert_eq!(r.diverted_inflow_m3s, None);
assert_eq!(r.diverted_outflow_m3s, None);
assert_eq!(r.equivalent_productivity_mw_per_m3s, 0.5);
assert_eq!(r.accumulated_productivity_mw_per_m3s, 0.5);
assert_eq!(r.incremental_inflow_energy_mw, 100.0);
assert_eq!(r.stored_energy_initial_mwh, 250.0);
assert_eq!(r.stored_energy_final_mwh, 240.0);
}
#[test]
fn simulation_hydro_result_serde_round_trip() {
let original = SimulationHydroResult {
stage_id: 3,
block_id: Some(1),
hydro_id: 7,
turbined_m3s: 120.0,
spillage_m3s: 5.0,
evaporation_m3s: Some(0.3),
diverted_inflow_m3s: Some(10.0),
diverted_outflow_m3s: Some(8.0),
incremental_inflow_m3s: 300.0,
inflow_m3s: 310.0,
storage_initial_hm3: 600.0,
storage_final_hm3: 590.0,
generation_mw: 65.0,
equivalent_productivity_mw_per_m3s: 1.0,
accumulated_productivity_mw_per_m3s: 2.0,
incremental_inflow_energy_mw: 3.0,
stored_energy_initial_mwh: 4.0,
stored_energy_final_mwh: 5.0,
spillage_cost: 0.1,
water_value_per_hm3: 15.0,
storage_binding_code: 1,
operative_state_code: 1,
turbined_slack_m3s: 0.0,
outflow_slack_below_m3s: 0.0,
outflow_slack_above_m3s: 0.0,
generation_slack_mw: 0.0,
storage_violation_below_hm3: 0.0,
filling_target_violation_hm3: 0.0,
evaporation_violation_pos_m3s: 0.0,
evaporation_violation_neg_m3s: 0.0,
inflow_nonnegativity_slack_m3s: 0.0,
water_withdrawal_violation_pos_m3s: 0.0,
water_withdrawal_violation_neg_m3s: 0.0,
};
let json = serde_json::to_string(&original).expect("serialize");
let decoded: SimulationHydroResult = serde_json::from_str(&json).expect("deserialize");
assert_eq!(decoded.stage_id, original.stage_id);
assert_eq!(decoded.block_id, original.block_id);
assert_eq!(decoded.hydro_id, original.hydro_id);
assert_eq!(
decoded.equivalent_productivity_mw_per_m3s,
original.equivalent_productivity_mw_per_m3s
);
assert_eq!(
decoded.accumulated_productivity_mw_per_m3s,
original.accumulated_productivity_mw_per_m3s
);
assert_eq!(
decoded.incremental_inflow_energy_mw,
original.incremental_inflow_energy_mw
);
assert_eq!(
decoded.stored_energy_initial_mwh,
original.stored_energy_initial_mwh
);
assert_eq!(
decoded.stored_energy_final_mwh,
original.stored_energy_final_mwh
);
assert_eq!(
decoded.equivalent_productivity_mw_per_m3s.to_bits(),
original.equivalent_productivity_mw_per_m3s.to_bits()
);
assert_eq!(
decoded.accumulated_productivity_mw_per_m3s.to_bits(),
original.accumulated_productivity_mw_per_m3s.to_bits()
);
assert_eq!(
decoded.incremental_inflow_energy_mw.to_bits(),
original.incremental_inflow_energy_mw.to_bits()
);
assert_eq!(
decoded.stored_energy_initial_mwh.to_bits(),
original.stored_energy_initial_mwh.to_bits()
);
assert_eq!(
decoded.stored_energy_final_mwh.to_bits(),
original.stored_energy_final_mwh.to_bits()
);
}
#[test]
fn thermal_result_anticipated_fields_nullable() {
let anticipated = SimulationThermalResult {
stage_id: 2,
block_id: Some(1),
thermal_id: 10,
generation_mw: 200.0,
generation_cost: 5000.0,
is_anticipated: true,
anticipated_committed_mw: Some(250.0),
anticipated_decision_mw: Some(200.0),
operative_state_code: 1,
};
assert!(anticipated.is_anticipated);
assert_eq!(anticipated.anticipated_committed_mw, Some(250.0));
assert_eq!(anticipated.anticipated_decision_mw, Some(200.0));
let non_anticipated = SimulationThermalResult {
stage_id: 2,
block_id: Some(1),
thermal_id: 11,
generation_mw: 100.0,
generation_cost: 3000.0,
is_anticipated: false,
anticipated_committed_mw: None,
anticipated_decision_mw: None,
operative_state_code: 1,
};
assert!(!non_anticipated.is_anticipated);
assert_eq!(non_anticipated.anticipated_committed_mw, None);
assert_eq!(non_anticipated.anticipated_decision_mw, None);
}
#[test]
fn exchange_result_construction() {
let r = SimulationExchangeResult {
stage_id: 0,
block_id: Some(0),
line_id: 3,
direct_flow_mw: 150.0,
reverse_flow_mw: 0.0,
exchange_cost: 10.0,
operative_state_code: 1,
};
assert_eq!(r.stage_id, 0);
assert_eq!(r.block_id, Some(0));
assert_eq!(r.line_id, 3);
assert_eq!(r.direct_flow_mw, 150.0);
assert_eq!(r.reverse_flow_mw, 0.0);
assert_eq!(r.exchange_cost, 10.0);
assert_eq!(r.operative_state_code, 1);
}
#[test]
fn bus_result_construction() {
let r = SimulationBusResult {
stage_id: 0,
block_id: Some(0),
bus_id: 1,
load_mw: 300.0,
deficit_mw: 0.0,
excess_mw: 0.0,
spot_price: 120.0,
};
assert_eq!(r.stage_id, 0);
assert_eq!(r.block_id, Some(0));
assert_eq!(r.bus_id, 1);
assert_eq!(r.load_mw, 300.0);
assert_eq!(r.deficit_mw, 0.0);
assert_eq!(r.excess_mw, 0.0);
assert_eq!(r.spot_price, 120.0);
}
#[test]
fn pumping_result_construction() {
let r = SimulationPumpingResult {
stage_id: 0,
block_id: Some(0),
pumping_station_id: 2,
pumped_flow_m3s: 50.0,
power_consumption_mw: 25.0,
pumping_cost: 500.0,
operative_state_code: 1,
};
assert_eq!(r.stage_id, 0);
assert_eq!(r.block_id, Some(0));
assert_eq!(r.pumping_station_id, 2);
assert_eq!(r.pumped_flow_m3s, 50.0);
assert_eq!(r.power_consumption_mw, 25.0);
assert_eq!(r.pumping_cost, 500.0);
assert_eq!(r.operative_state_code, 1);
}
#[test]
fn contract_result_construction() {
let r = SimulationContractResult {
stage_id: 0,
block_id: Some(0),
contract_id: 7,
power_mw: 80.0,
price_per_mwh: 200.0,
total_cost: 16000.0,
operative_state_code: 1,
};
assert_eq!(r.stage_id, 0);
assert_eq!(r.block_id, Some(0));
assert_eq!(r.contract_id, 7);
assert_eq!(r.power_mw, 80.0);
assert_eq!(r.price_per_mwh, 200.0);
assert_eq!(r.total_cost, 16000.0);
assert_eq!(r.operative_state_code, 1);
}
#[test]
fn non_controllable_result_construction() {
let r = SimulationNonControllableResult {
stage_id: 0,
block_id: Some(0),
non_controllable_id: 4,
generation_mw: 60.0,
available_mw: 80.0,
curtailment_mw: 20.0,
curtailment_cost: 200.0,
operative_state_code: 1,
};
assert_eq!(r.stage_id, 0);
assert_eq!(r.block_id, Some(0));
assert_eq!(r.non_controllable_id, 4);
assert_eq!(r.generation_mw, 60.0);
assert_eq!(r.available_mw, 80.0);
assert_eq!(r.curtailment_mw, 20.0);
assert_eq!(r.curtailment_cost, 200.0);
assert_eq!(r.operative_state_code, 1);
}
#[test]
fn inflow_lag_result_construction() {
let r = SimulationInflowLagResult {
stage_id: 5,
hydro_id: 2,
lag_index: 1,
inflow_m3s: 350.0,
};
assert_eq!(r.stage_id, 5);
assert_eq!(r.hydro_id, 2);
assert_eq!(r.lag_index, 1);
assert_eq!(r.inflow_m3s, 350.0);
}
#[test]
fn generic_violation_result_construction() {
let r = SimulationGenericViolationResult {
stage_id: 3,
block_id: Some(2),
constraint_id: 15,
slack_value: 5.0,
slack_cost: 50.0,
};
assert_eq!(r.stage_id, 3);
assert_eq!(r.block_id, Some(2));
assert_eq!(r.constraint_id, 15);
assert_eq!(r.slack_value, 5.0);
assert_eq!(r.slack_cost, 50.0);
}
#[test]
fn stage_result_empty_optional_vecs() {
let stage = SimulationStageResult {
stage_id: 0,
costs: vec![],
hydros: vec![],
thermals: vec![],
exchanges: vec![],
buses: vec![],
pumping_stations: vec![],
contracts: vec![],
non_controllables: vec![],
inflow_lags: vec![],
generic_violations: vec![],
};
assert!(stage.pumping_stations.is_empty());
assert!(stage.contracts.is_empty());
assert!(stage.non_controllables.is_empty());
assert!(stage.inflow_lags.is_empty());
assert!(stage.generic_violations.is_empty());
}
#[test]
fn scenario_result_is_send() {
fn assert_send<T: Send>() {}
assert_send::<SimulationScenarioResult>();
}
#[test]
fn scenario_result_with_multiple_stages() {
let stages: Vec<SimulationStageResult> = (0..12)
.map(|i| SimulationStageResult {
stage_id: i,
costs: vec![],
hydros: vec![],
thermals: vec![],
exchanges: vec![],
buses: vec![],
pumping_stations: vec![],
contracts: vec![],
non_controllables: vec![],
inflow_lags: vec![],
generic_violations: vec![],
})
.collect();
let result = SimulationScenarioResult {
scenario_id: 42,
total_cost: 1_000_000.0,
per_category_costs: ScenarioCategoryCosts {
resource_cost: 600_000.0,
recourse_cost: 100_000.0,
violation_cost: 50_000.0,
regularization_cost: 200_000.0,
imputed_cost: 50_000.0,
},
stages,
};
assert_eq!(result.scenario_id, 42);
assert_eq!(result.stages.len(), 12);
}
#[test]
fn category_costs_construction() {
let c = ScenarioCategoryCosts {
resource_cost: 1.0,
recourse_cost: 2.0,
violation_cost: 3.0,
regularization_cost: 4.0,
imputed_cost: 5.0,
};
assert_eq!(c.resource_cost, 1.0);
assert_eq!(c.recourse_cost, 2.0);
assert_eq!(c.violation_cost, 3.0);
assert_eq!(c.regularization_cost, 4.0);
assert_eq!(c.imputed_cost, 5.0);
}
#[test]
fn category_cost_stats_construction() {
let stats = CategoryCostStats {
category: "recourse".to_string(),
mean: 500.0,
max: 2000.0,
frequency: 0.15,
};
assert_eq!(stats.category, "recourse");
assert_eq!(stats.mean, 500.0);
assert_eq!(stats.max, 2000.0);
assert_eq!(stats.frequency, 0.15);
}
#[test]
fn simulation_summary_construction() {
let category_stats: Vec<CategoryCostStats> = (0_i32..5)
.map(|i| CategoryCostStats {
category: format!("cat_{i}"),
mean: f64::from(i) * 100.0,
max: f64::from(i) * 500.0,
frequency: 0.1 * f64::from(i),
})
.collect();
let summary = SimulationSummary {
mean_cost: 1_500_000.0,
std_cost: 200_000.0,
min_cost: 900_000.0,
max_cost: 2_100_000.0,
cvar: 1_900_000.0,
cvar_alpha: 0.95,
category_stats,
deficit_frequency: 0.08,
total_deficit_mwh: 12_500.0,
total_spillage_mwh: 3_200.0,
n_scenarios: 2000,
};
assert_eq!(summary.mean_cost, 1_500_000.0);
assert_eq!(summary.std_cost, 200_000.0);
assert_eq!(summary.min_cost, 900_000.0);
assert_eq!(summary.max_cost, 2_100_000.0);
assert_eq!(summary.cvar, 1_900_000.0);
assert_eq!(summary.cvar_alpha, 0.95);
assert_eq!(summary.category_stats.len(), 5);
assert_eq!(summary.deficit_frequency, 0.08);
assert_eq!(summary.total_deficit_mwh, 12_500.0);
assert_eq!(summary.total_spillage_mwh, 3_200.0);
assert_eq!(summary.n_scenarios, 2000);
}
}