use std::collections::HashMap;
use cobre_core::{EntityId, Stage, System};
use cobre_solver::StageTemplate;
use cobre_stochastic::normal::precompute::PrecomputedNormal;
use cobre_stochastic::par::precompute::PrecomputedPar;
use crate::error::SddpError;
use crate::hydro_models::{EvaporationModelSet, ProductionModelSet, ResolvedProductionModel};
use crate::inflow_method::InflowNonNegativityMethod;
use crate::resolved_parameters::ResolvedParameters;
use crate::setup::template_postprocess::{
compute_cumulative_discount_factors, compute_per_stage_discount_factors,
};
use super::layout::{StageLayout, TemplateBuildCtx};
use super::{COST_SCALE_FACTOR, GenericConstraintRowEntry, matrix, scaling};
#[derive(Debug, Clone)]
pub struct StageTemplates {
pub templates: Vec<StageTemplate>,
pub base_rows: Vec<usize>,
pub noise_scale: Vec<f64>,
pub zeta_per_stage: Vec<f64>,
pub block_hours_per_stage: Vec<Vec<f64>>,
pub n_hydros: usize,
pub load_balance_row_starts: Vec<usize>,
pub n_load_buses: usize,
pub load_bus_indices: Vec<usize>,
pub generic_constraint_row_entries: Vec<Vec<GenericConstraintRowEntry>>,
pub ncs_col_starts: Vec<usize>,
pub n_ncs_per_stage: Vec<usize>,
pub active_ncs_indices: Vec<Vec<usize>>,
pub diversion_upstream: HashMap<EntityId, Vec<usize>>,
pub hydro_productivities_per_stage: Vec<Vec<f64>>,
pub discount_factors: Vec<f64>,
pub cumulative_discount_factors: Vec<f64>,
}
#[allow(clippy::type_complexity)]
pub(super) fn build_single_stage_template(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
) -> (
StageTemplate,
usize,
usize,
Vec<GenericConstraintRowEntry>,
usize,
usize,
Vec<usize>,
) {
let layout = StageLayout::new(ctx, stage, stage_idx);
let stage_base_row = layout.row_water_balance_start;
let load_balance_row_start = layout.row_load_balance_start;
let (mut col_lower, mut col_upper, mut objective) =
matrix::fill_stage_columns(ctx, stage, stage_idx, &layout);
let (mut row_lower, mut row_upper) = matrix::fill_stage_rows(ctx, stage, stage_idx, &layout);
let mut col_entries = matrix::build_stage_matrix_entries(ctx, stage, stage_idx, &layout);
{
let mut buffers = matrix::LpMatrixBuffers {
col_entries: &mut col_entries,
_col_lower: &mut col_lower,
col_upper: &mut col_upper,
objective: &mut objective,
row_lower: &mut row_lower,
row_upper: &mut row_upper,
};
matrix::fill_generic_constraint_entries(ctx, stage, stage_idx, &layout, &mut buffers);
}
let theta_col = layout.col_theta;
for (i, coeff) in objective.iter_mut().enumerate() {
if i != theta_col {
*coeff /= COST_SCALE_FACTOR;
}
}
for entries in &mut col_entries {
entries.sort_unstable_by_key(|&(row, _)| row);
}
let (col_starts, row_indices, values) = matrix::assemble_csc(&col_entries);
let n_transfer = ctx.n_hydros * ctx.max_par_order;
let template = StageTemplate {
num_cols: layout.num_cols,
num_rows: layout.num_rows,
num_nz: col_entries.iter().map(Vec::len).sum(),
col_starts,
row_indices,
values,
col_lower,
col_upper,
objective,
row_lower,
row_upper,
n_state: layout.n_state,
n_transfer,
n_dual_relevant: layout.n_dual_relevant,
n_hydro: layout.n_h,
max_par_order: layout.lag_order,
col_scale: Vec::new(),
row_scale: Vec::new(),
};
(
template,
stage_base_row,
load_balance_row_start,
layout.generic_constraint_rows,
layout.col_ncs_start,
layout.n_ncs,
layout.active_ncs_indices,
)
}
fn collect_load_bus_indices(system: &System, bus_pos: &HashMap<EntityId, usize>) -> Vec<usize> {
let mut ids: Vec<EntityId> = system
.load_models()
.iter()
.filter(|m| m.std_mw > 0.0)
.map(|m| m.bus_id)
.collect();
ids.sort_unstable_by_key(|id| id.0);
ids.dedup();
ids.iter()
.filter_map(|id| bus_pos.get(id).copied())
.collect()
}
pub fn build_stage_templates(
system: &System,
inflow_method: InflowNonNegativityMethod,
par_lp: &PrecomputedPar,
normal_lp: &PrecomputedNormal,
production_models: &ProductionModelSet,
evaporation_models: &EvaporationModelSet,
resolved_parameters: &ResolvedParameters,
) -> Result<StageTemplates, SddpError> {
let study_stages: Vec<_> = system.stages().iter().filter(|s| s.id >= 0).collect();
let n_hydros = system.hydros().len();
if study_stages.is_empty() {
return Ok(StageTemplates {
templates: Vec::new(),
base_rows: Vec::new(),
noise_scale: Vec::new(),
zeta_per_stage: Vec::new(),
block_hours_per_stage: Vec::new(),
n_hydros,
load_balance_row_starts: Vec::new(),
n_load_buses: 0,
load_bus_indices: Vec::new(),
generic_constraint_row_entries: Vec::new(),
ncs_col_starts: Vec::new(),
n_ncs_per_stage: Vec::new(),
active_ncs_indices: Vec::new(),
diversion_upstream: HashMap::new(),
hydro_productivities_per_stage: Vec::new(),
discount_factors: Vec::new(),
cumulative_discount_factors: Vec::new(),
});
}
let (ctx, load_bus_indices, diversion_upstream_output) = build_template_build_ctx(
system,
inflow_method,
par_lp,
production_models,
evaporation_models,
resolved_parameters,
);
let n_load_buses = load_bus_indices.len();
debug_assert!(
normal_lp.n_entities() == 0 || normal_lp.n_entities() == n_load_buses,
"PrecomputedNormal has {} entities but system has {} stochastic load buses",
normal_lp.n_entities(),
n_load_buses
);
let n_study = study_stages.len();
let mut templates = Vec::with_capacity(n_study);
let mut base_rows = Vec::with_capacity(n_study);
let mut load_balance_row_starts = Vec::with_capacity(n_study);
let mut generic_constraint_row_entries = Vec::with_capacity(n_study);
let mut ncs_col_starts = Vec::with_capacity(n_study);
let mut n_ncs_per_stage = Vec::with_capacity(n_study);
let mut active_ncs_indices_per_stage = Vec::with_capacity(n_study);
for (stage_idx, stage) in study_stages.iter().enumerate() {
let (
template,
stage_base_row,
load_balance_row_start,
gc_entries,
ncs_col_start,
ncs_count,
ncs_active,
) = build_single_stage_template(&ctx, stage, stage_idx);
templates.push(template);
base_rows.push(stage_base_row);
load_balance_row_starts.push(load_balance_row_start);
generic_constraint_row_entries.push(gc_entries);
ncs_col_starts.push(ncs_col_start);
n_ncs_per_stage.push(ncs_count);
active_ncs_indices_per_stage.push(ncs_active);
}
Ok(assemble_stage_templates_output(
templates,
base_rows,
load_balance_row_starts,
generic_constraint_row_entries,
ncs_col_starts,
n_ncs_per_stage,
active_ncs_indices_per_stage,
load_bus_indices,
diversion_upstream_output,
&study_stages,
&ctx,
par_lp,
n_hydros,
n_load_buses,
n_study,
))
}
fn build_template_build_ctx<'a>(
system: &'a System,
inflow_method: InflowNonNegativityMethod,
par_lp: &'a PrecomputedPar,
production_models: &'a ProductionModelSet,
evaporation_models: &'a EvaporationModelSet,
resolved_parameters: &'a ResolvedParameters,
) -> (
TemplateBuildCtx<'a>,
Vec<usize>,
HashMap<EntityId, Vec<usize>>,
) {
let hydros = system.hydros();
let buses = system.buses();
let n_hydros = hydros.len();
let hydro_pos: HashMap<EntityId, usize> =
hydros.iter().enumerate().map(|(i, h)| (h.id, i)).collect();
let thermal_pos: HashMap<EntityId, usize> = system
.thermals()
.iter()
.enumerate()
.map(|(i, t)| (t.id, i))
.collect();
let line_pos: HashMap<EntityId, usize> = system
.lines()
.iter()
.enumerate()
.map(|(i, l)| (l.id, i))
.collect();
let bus_pos: HashMap<EntityId, usize> =
buses.iter().enumerate().map(|(i, b)| (b.id, i)).collect();
let load_bus_indices = collect_load_bus_indices(system, &bus_pos);
let max_par_order: usize = system
.inflow_models()
.iter()
.filter(|m| m.stage_id >= 0)
.map(|m| m.ar_coefficients.len())
.max()
.unwrap_or(0)
.max(par_lp.max_order());
let mut anticipated_thermal_indices: Vec<usize> = Vec::new();
let mut anticipated_lead_stages: Vec<usize> = Vec::new();
for (t_idx, thermal) in system.thermals().iter().enumerate() {
if let Some(cfg) = thermal.anticipated_config.as_ref() {
anticipated_thermal_indices.push(t_idx);
anticipated_lead_stages.push(cfg.lead_stages as usize);
}
}
let n_anticipated = anticipated_thermal_indices.len();
let k_max = anticipated_lead_stages.iter().copied().max().unwrap_or(0);
let mut diversion_upstream: HashMap<EntityId, Vec<usize>> = HashMap::new();
for (h_idx, hydro) in hydros.iter().enumerate() {
if let Some(ref div) = hydro.diversion {
diversion_upstream
.entry(div.downstream_id)
.or_default()
.push(h_idx);
}
}
let diversion_upstream_output = diversion_upstream.clone();
let study_stages: Vec<_> = system.stages().iter().filter(|s| s.id >= 0).collect();
let per_stage_discount =
compute_per_stage_discount_factors(&study_stages, system.policy_graph());
let cumulative_discount_factors = compute_cumulative_discount_factors(&per_stage_discount);
let total_hours_per_stage: Vec<f64> = study_stages
.iter()
.map(|s| s.blocks.iter().map(|b| b.duration_hours).sum())
.collect();
debug_assert_eq!(
cumulative_discount_factors.len(),
study_stages.len(),
"cumulative_discount_factors length must equal n_study_stages"
);
debug_assert_eq!(
total_hours_per_stage.len(),
study_stages.len(),
"total_hours_per_stage length must equal n_study_stages"
);
let ctx = TemplateBuildCtx {
hydros,
thermals: system.thermals(),
lines: system.lines(),
buses,
load_models: system.load_models(),
cascade: system.cascade(),
bounds: system.bounds(),
penalties: system.penalties(),
hydro_pos,
thermal_pos,
line_pos,
bus_pos,
par_lp,
production_models,
evaporation_models,
generic_constraints: system.generic_constraints(),
resolved_generic_bounds: system.resolved_generic_bounds(),
resolved_load_factors: system.resolved_load_factors(),
resolved_exchange_factors: system.resolved_exchange_factors(),
non_controllable_sources: system.non_controllable_sources(),
resolved_ncs_bounds: system.resolved_ncs_bounds(),
resolved_ncs_factors: system.resolved_ncs_factors(),
resolved_parameters,
diversion_upstream,
n_hydros,
n_thermals: system.thermals().len(),
n_lines: system.lines().len(),
n_buses: buses.len(),
max_par_order,
n_anticipated,
k_max,
anticipated_lead_stages,
anticipated_thermal_indices,
has_penalty: n_hydros > 0 && inflow_method.has_slack_columns(),
cumulative_discount_factors,
total_hours_per_stage,
};
(ctx, load_bus_indices, diversion_upstream_output)
}
#[allow(clippy::too_many_arguments)]
fn assemble_stage_templates_output(
templates: Vec<cobre_solver::StageTemplate>,
base_rows: Vec<usize>,
load_balance_row_starts: Vec<usize>,
generic_constraint_row_entries: Vec<Vec<GenericConstraintRowEntry>>,
ncs_col_starts: Vec<usize>,
n_ncs_per_stage: Vec<usize>,
active_ncs_indices_per_stage: Vec<Vec<usize>>,
load_bus_indices: Vec<usize>,
diversion_upstream_output: HashMap<EntityId, Vec<usize>>,
study_stages: &[&cobre_core::Stage],
ctx: &TemplateBuildCtx<'_>,
par_lp: &PrecomputedPar,
n_hydros: usize,
n_load_buses: usize,
n_study: usize,
) -> StageTemplates {
let (noise_scale, zeta_per_stage, block_hours_per_stage) =
scaling::compute_noise_scale(study_stages, n_hydros, par_lp);
let hydro_productivities_per_stage: Vec<Vec<f64>> = (0..n_study)
.map(|s| {
(0..n_hydros)
.map(|h| match ctx.production_models.model(h, s) {
ResolvedProductionModel::ConstantProductivity { productivity } => *productivity,
ResolvedProductionModel::Fpha { .. } => 0.0,
})
.collect()
})
.collect();
let discount_factors = vec![1.0; templates.len()];
StageTemplates {
templates,
base_rows,
noise_scale,
zeta_per_stage,
block_hours_per_stage,
n_hydros,
load_balance_row_starts,
n_load_buses,
load_bus_indices,
generic_constraint_row_entries,
ncs_col_starts,
n_ncs_per_stage,
active_ncs_indices: active_ncs_indices_per_stage,
diversion_upstream: diversion_upstream_output,
hydro_productivities_per_stage,
discount_factors,
cumulative_discount_factors: vec![1.0; n_study],
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::too_many_lines,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::needless_range_loop,
clippy::doc_markdown,
clippy::doc_overindented_list_items
)]
mod tests {
use chrono::NaiveDate;
use cobre_core::{
AnticipatedConfig, Block, BlockMode, BoundsCountsSpec, BoundsDefaults, Bus,
BusStagePenalties, ContractStageBounds, DeficitSegment, EntityId, HydroStageBounds,
HydroStagePenalties, LineStageBounds, LineStagePenalties, LoadModel, NcsStagePenalties,
NoiseMethod, PenaltiesCountsSpec, PenaltiesDefaults, PumpingStageBounds, ResolvedBounds,
ResolvedPenalties, ScenarioSourceConfig, Stage, StageRiskConfig, StageStateConfig,
SystemBuilder, Thermal, ThermalStageBounds,
};
use cobre_stochastic::par::precompute::PrecomputedPar;
use crate::hydro_models::PrepareHydroModelsResult;
use crate::inflow_method::InflowNonNegativityMethod;
use crate::resolved_parameters::ResolvedParameters;
fn default_hydro_bounds() -> HydroStageBounds {
HydroStageBounds {
min_storage_hm3: 0.0,
max_storage_hm3: 200.0,
min_turbined_m3s: 0.0,
max_turbined_m3s: 100.0,
min_outflow_m3s: 0.0,
max_outflow_m3s: None,
min_generation_mw: 0.0,
max_generation_mw: 250.0,
max_diversion_m3s: None,
filling_inflow_m3s: 0.0,
water_withdrawal_m3s: 0.0,
}
}
fn default_hydro_penalties() -> HydroStagePenalties {
HydroStagePenalties {
spillage_cost: 0.01,
diversion_cost: 0.0,
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,
water_withdrawal_violation_pos_cost: 0.0,
water_withdrawal_violation_neg_cost: 0.0,
evaporation_violation_pos_cost: 0.0,
evaporation_violation_neg_cost: 0.0,
inflow_nonnegativity_cost: 1000.0,
}
}
fn system_with_thermals(thermals: Vec<Thermal>) -> cobre_core::System {
let n_thermals = thermals.len();
let n_stages = 1_usize;
let bus = Bus {
id: EntityId(1),
name: "B1".to_string(),
deficit_segments: vec![DeficitSegment {
depth_mw: None,
cost_per_mwh: 500.0,
}],
excess_cost: 0.0,
};
let stages: Vec<Stage> = vec![Stage {
index: 0,
id: 0,
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: "BLK0".to_string(),
duration_hours: 744.0,
}],
block_mode: BlockMode::Parallel,
state_config: StageStateConfig {
storage: false,
inflow_lags: false,
},
risk_config: StageRiskConfig::Expectation,
scenario_config: ScenarioSourceConfig {
branching_factor: 1,
noise_method: NoiseMethod::Saa,
},
}];
let load_models = vec![LoadModel {
bus_id: EntityId(1),
stage_id: 0,
mean_mw: 100.0,
std_mw: 0.0,
}];
let k_max = thermals
.iter()
.filter_map(|t| t.anticipated_config.as_ref())
.map(|c| c.lead_stages as usize)
.max()
.unwrap_or(0);
let resolved_bounds = ResolvedBounds::new(
&BoundsCountsSpec {
n_hydros: 0,
n_thermals,
n_lines: 0,
n_pumping: 0,
n_contracts: 0,
n_stages,
k_max,
},
&BoundsDefaults {
hydro: default_hydro_bounds(),
thermal: ThermalStageBounds {
min_generation_mw: 0.0,
max_generation_mw: 100.0,
cost_per_mwh: 0.0,
},
line: LineStageBounds {
direct_mw: 0.0,
reverse_mw: 0.0,
},
pumping: PumpingStageBounds {
min_flow_m3s: 0.0,
max_flow_m3s: 0.0,
},
contract: ContractStageBounds {
min_mw: 0.0,
max_mw: 0.0,
price_per_mwh: 0.0,
},
},
);
let penalties = ResolvedPenalties::new(
&PenaltiesCountsSpec {
n_hydros: 0,
n_buses: 1,
n_lines: 0,
n_ncs: 0,
n_stages,
},
&PenaltiesDefaults {
hydro: default_hydro_penalties(),
bus: BusStagePenalties { excess_cost: 0.0 },
line: LineStagePenalties { exchange_cost: 0.0 },
ncs: NcsStagePenalties {
curtailment_cost: 0.0,
},
},
);
SystemBuilder::new()
.buses(vec![bus])
.thermals(thermals)
.stages(stages)
.load_models(load_models)
.bounds(resolved_bounds)
.penalties(penalties)
.build()
.expect("system_with_thermals: valid system")
}
fn empty_resolved_params() -> ResolvedParameters {
ResolvedParameters {
per_param: vec![],
id_to_slot: vec![],
}
}
#[test]
fn build_template_build_ctx_populates_anticipated_metadata() {
let thermals = vec![
Thermal {
id: EntityId(1),
name: "T_a".to_string(),
bus_id: EntityId(1),
entry_stage_id: None,
exit_stage_id: None,
cost_per_mwh: 10.0,
min_generation_mw: 0.0,
max_generation_mw: 100.0,
anticipated_config: Some(AnticipatedConfig { lead_stages: 2 }),
},
Thermal {
id: EntityId(2),
name: "T_b".to_string(),
bus_id: EntityId(1),
entry_stage_id: None,
exit_stage_id: None,
cost_per_mwh: 20.0,
min_generation_mw: 0.0,
max_generation_mw: 100.0,
anticipated_config: None,
},
Thermal {
id: EntityId(3),
name: "T_c".to_string(),
bus_id: EntityId(1),
entry_stage_id: None,
exit_stage_id: None,
cost_per_mwh: 30.0,
min_generation_mw: 0.0,
max_generation_mw: 100.0,
anticipated_config: Some(AnticipatedConfig { lead_stages: 3 }),
},
];
let system = system_with_thermals(thermals);
let hydro_result = PrepareHydroModelsResult::default_from_system(&system);
let par_lp = PrecomputedPar::default();
let resolved_params = empty_resolved_params();
let (ctx, _, _) = super::build_template_build_ctx(
&system,
InflowNonNegativityMethod::None,
&par_lp,
&hydro_result.production,
&hydro_result.evaporation,
&resolved_params,
);
assert_eq!(ctx.n_anticipated, 2, "n_anticipated");
assert_eq!(ctx.k_max, 3, "k_max");
assert_eq!(
ctx.anticipated_lead_stages,
vec![2, 3],
"anticipated_lead_stages"
);
assert_eq!(
ctx.anticipated_thermal_indices,
vec![0, 2],
"anticipated_thermal_indices"
);
}
#[test]
fn build_template_build_ctx_zero_anticipated_when_none() {
let thermals = vec![
Thermal {
id: EntityId(1),
name: "T1".to_string(),
bus_id: EntityId(1),
entry_stage_id: None,
exit_stage_id: None,
cost_per_mwh: 10.0,
min_generation_mw: 0.0,
max_generation_mw: 100.0,
anticipated_config: None,
},
Thermal {
id: EntityId(2),
name: "T2".to_string(),
bus_id: EntityId(1),
entry_stage_id: None,
exit_stage_id: None,
cost_per_mwh: 20.0,
min_generation_mw: 0.0,
max_generation_mw: 100.0,
anticipated_config: None,
},
];
let system = system_with_thermals(thermals);
let hydro_result = PrepareHydroModelsResult::default_from_system(&system);
let par_lp = PrecomputedPar::default();
let resolved_params = empty_resolved_params();
let (ctx, _, _) = super::build_template_build_ctx(
&system,
InflowNonNegativityMethod::None,
&par_lp,
&hydro_result.production,
&hydro_result.evaporation,
&resolved_params,
);
assert_eq!(ctx.n_anticipated, 0, "n_anticipated");
assert_eq!(ctx.k_max, 0, "k_max");
assert!(
ctx.anticipated_lead_stages.is_empty(),
"anticipated_lead_stages"
);
assert!(
ctx.anticipated_thermal_indices.is_empty(),
"anticipated_thermal_indices"
);
}
fn anticipated_invariance_system() -> cobre_core::System {
let thermals = vec![
Thermal {
id: EntityId(1),
name: "T_ant_k2".to_string(),
bus_id: EntityId(1),
entry_stage_id: None,
exit_stage_id: None,
cost_per_mwh: 50.0,
min_generation_mw: 0.0,
max_generation_mw: 120.0,
anticipated_config: Some(AnticipatedConfig { lead_stages: 2 }),
},
Thermal {
id: EntityId(2),
name: "T_ant_k3".to_string(),
bus_id: EntityId(1),
entry_stage_id: None,
exit_stage_id: None,
cost_per_mwh: 40.0,
min_generation_mw: 0.0,
max_generation_mw: 80.0,
anticipated_config: Some(AnticipatedConfig { lead_stages: 3 }),
},
Thermal {
id: EntityId(3),
name: "T_backup".to_string(),
bus_id: EntityId(1),
entry_stage_id: None,
exit_stage_id: None,
cost_per_mwh: 500.0,
min_generation_mw: 0.0,
max_generation_mw: 200.0,
anticipated_config: None,
},
];
let n_thermals = thermals.len();
let n_stages = 5_usize;
let k_max = 3_usize;
let bus = Bus {
id: EntityId(1),
name: "B1".to_string(),
deficit_segments: vec![DeficitSegment {
depth_mw: None,
cost_per_mwh: 1000.0,
}],
excess_cost: 0.0,
};
let stages: Vec<Stage> = (0..n_stages)
.map(|i| Stage {
index: i,
id: i as i32,
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: "BLK0".to_string(),
duration_hours: 744.0,
}],
block_mode: BlockMode::Parallel,
state_config: StageStateConfig {
storage: false,
inflow_lags: false,
},
risk_config: StageRiskConfig::Expectation,
scenario_config: ScenarioSourceConfig {
branching_factor: 1,
noise_method: NoiseMethod::Saa,
},
})
.collect();
let load_models: Vec<LoadModel> = (0..n_stages)
.map(|s| LoadModel {
bus_id: EntityId(1),
stage_id: s as i32,
mean_mw: 150.0,
std_mw: 0.0,
})
.collect();
let mut resolved_bounds = ResolvedBounds::new(
&BoundsCountsSpec {
n_hydros: 0,
n_thermals,
n_lines: 0,
n_pumping: 0,
n_contracts: 0,
n_stages,
k_max,
},
&BoundsDefaults {
hydro: default_hydro_bounds(),
thermal: ThermalStageBounds {
min_generation_mw: 0.0,
max_generation_mw: 100.0,
cost_per_mwh: 0.0,
},
line: LineStageBounds {
direct_mw: 0.0,
reverse_mw: 0.0,
},
pumping: PumpingStageBounds {
min_flow_m3s: 0.0,
max_flow_m3s: 0.0,
},
contract: ContractStageBounds {
min_mw: 0.0,
max_mw: 0.0,
price_per_mwh: 0.0,
},
},
);
let stage_axis_len = resolved_bounds.thermal_stage_axis_len();
for t_idx in 0..n_thermals {
for s_idx in 0..stage_axis_len {
let tb = resolved_bounds.thermal_bounds_mut(t_idx, s_idx);
match t_idx {
0 => {
tb.max_generation_mw = 120.0;
tb.cost_per_mwh = 50.0;
}
1 => {
tb.max_generation_mw = 80.0;
tb.cost_per_mwh = 40.0;
}
2 => {
tb.max_generation_mw = 200.0;
tb.cost_per_mwh = 500.0;
}
_ => unreachable!("only 3 thermals"),
}
}
}
let penalties = ResolvedPenalties::new(
&PenaltiesCountsSpec {
n_hydros: 0,
n_buses: 1,
n_lines: 0,
n_ncs: 0,
n_stages,
},
&PenaltiesDefaults {
hydro: default_hydro_penalties(),
bus: BusStagePenalties { excess_cost: 0.0 },
line: LineStagePenalties { exchange_cost: 0.0 },
ncs: NcsStagePenalties {
curtailment_cost: 0.0,
},
},
);
SystemBuilder::new()
.buses(vec![bus])
.thermals(thermals)
.stages(stages)
.load_models(load_models)
.bounds(resolved_bounds)
.penalties(penalties)
.build()
.expect("anticipated_invariance_system: valid system")
}
#[allow(clippy::too_many_arguments)]
fn assert_lp_equivalence_after_anticipated_swap(
tpl_a: &cobre_solver::StageTemplate,
tpl_b: &cobre_solver::StageTemplate,
dec_start_a: usize,
dec_start_b: usize,
state_start_a: usize,
state_start_b: usize,
state_out_start_a: usize,
state_out_start_b: usize,
n_ant: usize,
k_max: usize,
fish_start_a: usize,
fish_start_b: usize,
n_fish_rows: usize,
def_row_start_a: usize,
def_row_start_b: usize,
n_def_rows: usize,
stage_idx: usize,
) {
assert_eq!(
tpl_a.num_cols, tpl_b.num_cols,
"stage {stage_idx}: num_cols"
);
assert_eq!(
tpl_a.num_rows, tpl_b.num_rows,
"stage {stage_idx}: num_rows"
);
assert_eq!(tpl_a.num_nz, tpl_b.num_nz, "stage {stage_idx}: num_nz");
assert_eq!(n_ant, 2, "this helper requires n_ant == 2");
let mut col_perm: Vec<usize> = (0..tpl_a.num_cols).collect();
col_perm[dec_start_b] = dec_start_a + 1;
col_perm[dec_start_b + 1] = dec_start_a;
col_perm[state_out_start_b] = state_out_start_a + 1;
col_perm[state_out_start_b + 1] = state_out_start_a;
for s in 0..k_max {
col_perm[state_start_b + s * n_ant] = state_start_a + s * n_ant + 1;
col_perm[state_start_b + s * n_ant + 1] = state_start_a + s * n_ant;
}
let mut row_perm: Vec<usize> = (0..tpl_a.num_rows).collect();
if n_fish_rows == 2 {
row_perm[fish_start_b] = fish_start_a + 1;
row_perm[fish_start_b + 1] = fish_start_a;
}
if n_fish_rows == 1 {
row_perm[fish_start_b] = fish_start_a;
}
if n_def_rows == 2 {
row_perm[def_row_start_b] = def_row_start_a + 1;
row_perm[def_row_start_b + 1] = def_row_start_a;
}
if n_def_rows == 1 {
row_perm[def_row_start_b] = def_row_start_a;
}
for j in 0..tpl_a.num_cols {
let a = col_perm[j];
assert_eq!(
tpl_a.col_lower[a].to_bits(),
tpl_b.col_lower[j].to_bits(),
"stage {stage_idx}: col_lower mismatch at permuted col {j} <- {a}"
);
assert_eq!(
tpl_a.col_upper[a].to_bits(),
tpl_b.col_upper[j].to_bits(),
"stage {stage_idx}: col_upper mismatch at permuted col {j} <- {a}"
);
assert_eq!(
tpl_a.objective[a].to_bits(),
tpl_b.objective[j].to_bits(),
"stage {stage_idx}: objective mismatch at permuted col {j} <- {a}"
);
}
for i in 0..tpl_a.num_rows {
let ra = row_perm[i];
assert_eq!(
tpl_a.row_lower[ra].to_bits(),
tpl_b.row_lower[i].to_bits(),
"stage {stage_idx}: row_lower mismatch at permuted row {i} <- {ra}"
);
assert_eq!(
tpl_a.row_upper[ra].to_bits(),
tpl_b.row_upper[i].to_bits(),
"stage {stage_idx}: row_upper mismatch at permuted row {i} <- {ra}"
);
}
let dense_a = csc_to_dense(tpl_a);
let dense_b = csc_to_dense(tpl_b);
for i in 0..tpl_a.num_rows {
for j in 0..tpl_a.num_cols {
let va = dense_a[row_perm[i]][col_perm[j]];
let vb = dense_b[i][j];
assert_eq!(
va.to_bits(),
vb.to_bits(),
"stage {stage_idx}: coefficient mismatch at row {i} col {j} \
(permuted from row {} col {} in tpl_a)",
row_perm[i],
col_perm[j],
);
}
}
}
fn csc_to_dense(tpl: &cobre_solver::StageTemplate) -> Vec<Vec<f64>> {
let mut dense = vec![vec![0.0_f64; tpl.num_cols]; tpl.num_rows];
for j in 0..tpl.num_cols {
let start = tpl.col_starts[j] as usize;
let end = tpl.col_starts[j + 1] as usize;
for k in start..end {
let row = tpl.row_indices[k] as usize;
dense[row][j] = tpl.values[k];
}
}
dense
}
#[test]
fn lp_template_invariant_under_anticipated_index_permutation() {
let system = anticipated_invariance_system();
assert_eq!(system.thermals().len(), 3);
assert_eq!(system.thermals()[0].id.0, 1);
assert_eq!(system.thermals()[1].id.0, 2);
assert_eq!(system.thermals()[2].id.0, 3);
let hydro_result = PrepareHydroModelsResult::default_from_system(&system);
let par_lp = PrecomputedPar::default();
let resolved_params = ResolvedParameters {
per_param: vec![],
id_to_slot: vec![],
};
let (ctx_a, _, _) = super::build_template_build_ctx(
&system,
InflowNonNegativityMethod::None,
&par_lp,
&hydro_result.production,
&hydro_result.evaporation,
&resolved_params,
);
assert_eq!(ctx_a.n_anticipated, 2);
assert_eq!(ctx_a.k_max, 3);
assert_eq!(ctx_a.anticipated_thermal_indices, vec![0, 1]);
assert_eq!(ctx_a.anticipated_lead_stages, vec![2, 3]);
let ctx_b = super::super::layout::TemplateBuildCtx {
hydros: ctx_a.hydros,
thermals: ctx_a.thermals,
lines: ctx_a.lines,
buses: ctx_a.buses,
load_models: ctx_a.load_models,
cascade: ctx_a.cascade,
bounds: ctx_a.bounds,
penalties: ctx_a.penalties,
hydro_pos: ctx_a.hydro_pos.clone(),
thermal_pos: ctx_a.thermal_pos.clone(),
line_pos: ctx_a.line_pos.clone(),
bus_pos: ctx_a.bus_pos.clone(),
par_lp: ctx_a.par_lp,
production_models: ctx_a.production_models,
evaporation_models: ctx_a.evaporation_models,
generic_constraints: ctx_a.generic_constraints,
resolved_generic_bounds: ctx_a.resolved_generic_bounds,
resolved_load_factors: ctx_a.resolved_load_factors,
resolved_exchange_factors: ctx_a.resolved_exchange_factors,
non_controllable_sources: ctx_a.non_controllable_sources,
resolved_ncs_bounds: ctx_a.resolved_ncs_bounds,
resolved_ncs_factors: ctx_a.resolved_ncs_factors,
resolved_parameters: ctx_a.resolved_parameters,
diversion_upstream: ctx_a.diversion_upstream.clone(),
n_hydros: ctx_a.n_hydros,
n_thermals: ctx_a.n_thermals,
n_lines: ctx_a.n_lines,
n_buses: ctx_a.n_buses,
max_par_order: ctx_a.max_par_order,
n_anticipated: ctx_a.n_anticipated,
k_max: ctx_a.k_max,
anticipated_lead_stages: vec![
ctx_a.anticipated_lead_stages[1],
ctx_a.anticipated_lead_stages[0],
],
anticipated_thermal_indices: vec![
ctx_a.anticipated_thermal_indices[1],
ctx_a.anticipated_thermal_indices[0],
],
has_penalty: ctx_a.has_penalty,
cumulative_discount_factors: ctx_a.cumulative_discount_factors.clone(),
total_hours_per_stage: ctx_a.total_hours_per_stage.clone(),
};
assert_eq!(ctx_b.anticipated_thermal_indices, vec![1, 0]);
assert_eq!(ctx_b.anticipated_lead_stages, vec![3, 2]);
let study_stages: Vec<_> = system.stages().iter().filter(|s| s.id >= 0).collect();
for stage_idx in [0_usize, 2, 3] {
let stage = study_stages[stage_idx];
let (tpl_a, _, _, _, _, _, _) =
super::build_single_stage_template(&ctx_a, stage, stage_idx);
let (tpl_b, _, _, _, _, _, _) =
super::build_single_stage_template(&ctx_b, stage, stage_idx);
let layout_a = super::super::layout::StageLayout::new(&ctx_a, stage, stage_idx);
let layout_b = super::super::layout::StageLayout::new(&ctx_b, stage, stage_idx);
assert_eq!(
layout_a.col_anticipated_decision_start, layout_b.col_anticipated_decision_start,
"stage {stage_idx}: dec_start"
);
assert_eq!(
layout_a.col_anticipated_state_start, layout_b.col_anticipated_state_start,
"stage {stage_idx}: state_start"
);
assert_eq!(
layout_a.row_anticipated_fishing_start, layout_b.row_anticipated_fishing_start,
"stage {stage_idx}: fish_start"
);
assert_eq!(
layout_a.n_anticipated_fishing_rows, layout_b.n_anticipated_fishing_rows,
"stage {stage_idx}: n_fish_rows"
);
assert_lp_equivalence_after_anticipated_swap(
&tpl_a,
&tpl_b,
layout_a.col_anticipated_decision_start,
layout_b.col_anticipated_decision_start,
layout_a.col_anticipated_state_start,
layout_b.col_anticipated_state_start,
layout_a.col_anticipated_state_out_start,
layout_b.col_anticipated_state_out_start,
ctx_a.n_anticipated,
ctx_a.k_max,
layout_a.row_anticipated_fishing_start,
layout_b.row_anticipated_fishing_start,
layout_a.n_anticipated_fishing_rows,
layout_a.row_anticipated_state_out_def_start,
layout_b.row_anticipated_state_out_def_start,
layout_a.n_anticipated_state_out_def_rows,
stage_idx,
);
}
}
}