use std::collections::HashMap;
use cobre_core::{
Bus, CascadeTopology, ConstraintSense, EntityId, GenericConstraint, Hydro, Line, LoadModel,
NonControllableSource, ResolvedBounds, ResolvedExchangeFactors,
ResolvedGenericConstraintBounds, ResolvedLoadFactors, ResolvedNcsBounds, ResolvedNcsFactors,
ResolvedPenalties, Stage, Thermal,
};
use cobre_stochastic::par::precompute::PrecomputedPar;
use crate::hydro_models::{
EvaporationModel, EvaporationModelSet, ProductionModelSet, ResolvedProductionModel,
};
use crate::indexer::StageIndexer;
use super::{GenericConstraintRowEntry, M3S_TO_HM3};
pub(crate) struct TemplateBuildCtx<'a> {
pub(crate) hydros: &'a [Hydro],
pub(crate) thermals: &'a [Thermal],
pub(crate) lines: &'a [Line],
pub(crate) buses: &'a [Bus],
pub(crate) load_models: &'a [LoadModel],
pub(crate) cascade: &'a CascadeTopology,
pub(crate) bounds: &'a ResolvedBounds,
pub(crate) penalties: &'a ResolvedPenalties,
pub(crate) hydro_pos: HashMap<EntityId, usize>,
pub(crate) thermal_pos: HashMap<EntityId, usize>,
pub(crate) line_pos: HashMap<EntityId, usize>,
pub(crate) bus_pos: HashMap<EntityId, usize>,
pub(crate) par_lp: &'a PrecomputedPar,
pub(crate) production_models: &'a ProductionModelSet,
pub(crate) evaporation_models: &'a EvaporationModelSet,
pub(crate) generic_constraints: &'a [GenericConstraint],
pub(crate) resolved_generic_bounds: &'a ResolvedGenericConstraintBounds,
pub(crate) resolved_load_factors: &'a ResolvedLoadFactors,
pub(crate) resolved_exchange_factors: &'a ResolvedExchangeFactors,
pub(crate) non_controllable_sources: &'a [NonControllableSource],
pub(crate) resolved_ncs_bounds: &'a ResolvedNcsBounds,
pub(crate) resolved_ncs_factors: &'a ResolvedNcsFactors,
pub(crate) resolved_parameters: &'a crate::resolved_parameters::ResolvedParameters,
pub(crate) diversion_upstream: HashMap<EntityId, Vec<usize>>,
pub(crate) n_hydros: usize,
pub(crate) n_thermals: usize,
pub(crate) n_lines: usize,
pub(crate) n_buses: usize,
pub(crate) max_par_order: usize,
pub(crate) has_penalty: bool,
}
pub(crate) struct StageLayout {
pub(crate) n_blks: usize,
pub(crate) n_h: usize,
pub(crate) lag_order: usize,
pub(crate) col_turbine_start: usize,
pub(crate) col_spillage_start: usize,
pub(crate) col_diversion_start: usize,
pub(crate) col_thermal_start: usize,
pub(crate) col_line_fwd_start: usize,
pub(crate) col_line_rev_start: usize,
pub(crate) col_deficit_start: usize,
pub(crate) max_deficit_segments: usize,
pub(crate) col_excess_start: usize,
pub(crate) col_inflow_slack_start: usize,
pub(crate) col_generation_start: usize,
pub(crate) row_water_balance_start: usize,
pub(crate) row_load_balance_start: usize,
pub(crate) row_fpha_start: usize,
pub(crate) row_evap_start: usize,
pub(crate) col_evap_start: usize,
pub(crate) col_withdrawal_neg_start: usize,
pub(crate) col_withdrawal_pos_start: usize,
pub(crate) col_outflow_below_start: usize,
pub(crate) col_outflow_above_start: usize,
pub(crate) col_turbine_below_start: usize,
pub(crate) col_generation_below_start: usize,
pub(crate) col_ncs_start: usize,
pub(crate) n_ncs: usize,
pub(crate) active_ncs_indices: Vec<usize>,
pub(crate) num_cols: usize,
pub(crate) row_min_outflow_start: usize,
pub(crate) row_max_outflow_start: usize,
pub(crate) row_min_turbine_start: usize,
pub(crate) row_min_generation_start: usize,
pub(crate) row_generic_start: usize,
pub(crate) num_rows: usize,
pub(crate) n_generic_rows: usize,
pub(crate) row_z_inflow_start: usize,
pub(crate) col_z_inflow_start: usize,
pub(crate) n_state: usize,
pub(crate) n_dual_relevant: usize,
pub(crate) zeta: f64,
pub(crate) fpha_hydro_indices: Vec<usize>,
pub(crate) fpha_planes_per_hydro: Vec<usize>,
pub(crate) evap_hydro_indices: Vec<usize>,
pub(crate) generic_constraint_rows: Vec<GenericConstraintRowEntry>,
}
struct DecisionColumnOffsets {
col_turbine_start: usize,
col_spillage_start: usize,
col_diversion_start: usize,
col_thermal_start: usize,
col_line_fwd_start: usize,
col_line_rev_start: usize,
col_deficit_start: usize,
col_excess_start: usize,
col_inflow_slack_start: usize,
max_deficit_segments: usize,
n_slack_cols: usize,
}
struct FphaColumnOffsets {
col_generation_start: usize,
col_generation_end: usize,
}
struct EvapColumnOffsets {
col_evap_start: usize,
n_evap_cols: usize,
}
struct OperationalSlackColumnOffsets {
col_withdrawal_neg_start: usize,
col_withdrawal_pos_start: usize,
col_outflow_below_start: usize,
col_outflow_above_start: usize,
col_turbine_below_start: usize,
col_generation_below_start: usize,
operational_slack_end: usize,
}
struct GenericConstraintLayout {
n_generic_rows: usize,
n_generic_slack_cols: usize,
generic_constraint_rows: Vec<GenericConstraintRowEntry>,
}
fn compute_decision_column_offsets(
ctx: &TemplateBuildCtx<'_>,
n_blks: usize,
decision_start: usize,
) -> DecisionColumnOffsets {
let col_turbine_start = decision_start;
let col_spillage_start = col_turbine_start + ctx.n_hydros * n_blks;
let col_diversion_start = col_spillage_start + ctx.n_hydros * n_blks;
let col_thermal_start = col_diversion_start + ctx.n_hydros * n_blks;
let col_line_fwd_start = col_thermal_start + ctx.n_thermals * n_blks;
let col_line_rev_start = col_line_fwd_start + ctx.n_lines * n_blks;
let max_deficit_segments = ctx
.buses
.iter()
.map(|b| b.deficit_segments.len())
.max()
.unwrap_or(0);
let col_deficit_start = col_line_rev_start + ctx.n_lines * n_blks;
let col_excess_start = col_deficit_start + ctx.n_buses * max_deficit_segments * n_blks;
let col_inflow_slack_start = col_excess_start + ctx.n_buses * n_blks;
let n_slack_cols = if ctx.has_penalty { ctx.n_hydros } else { 0 };
DecisionColumnOffsets {
col_turbine_start,
col_spillage_start,
col_diversion_start,
col_thermal_start,
col_line_fwd_start,
col_line_rev_start,
col_deficit_start,
col_excess_start,
col_inflow_slack_start,
max_deficit_segments,
n_slack_cols,
}
}
fn identify_and_layout_fpha_columns(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
n_blks: usize,
col_inflow_slack_start: usize,
n_slack_cols: usize,
) -> (FphaColumnOffsets, Vec<usize>, Vec<usize>) {
let mut fpha_hydro_indices: Vec<usize> = Vec::new();
let mut fpha_planes_per_hydro: Vec<usize> = Vec::new();
for h_idx in 0..ctx.n_hydros {
if let ResolvedProductionModel::Fpha { planes, .. } =
ctx.production_models.model(h_idx, stage_idx)
{
fpha_hydro_indices.push(h_idx);
fpha_planes_per_hydro.push(planes.len());
}
}
let n_fpha_hydros = fpha_hydro_indices.len();
let col_generation_start = col_inflow_slack_start + n_slack_cols;
let col_generation_end = col_generation_start + n_fpha_hydros * n_blks;
(
FphaColumnOffsets {
col_generation_start,
col_generation_end,
},
fpha_hydro_indices,
fpha_planes_per_hydro,
)
}
fn identify_and_layout_evap_columns(
ctx: &TemplateBuildCtx<'_>,
col_generation_end: usize,
) -> (EvapColumnOffsets, Vec<usize>) {
let mut evap_hydro_indices: Vec<usize> = Vec::new();
for h_idx in 0..ctx.n_hydros {
if matches!(
ctx.evaporation_models.model(h_idx),
EvaporationModel::Linearized { .. }
) {
evap_hydro_indices.push(h_idx);
}
}
let n_evap_hydros = evap_hydro_indices.len();
let col_evap_start = col_generation_end;
let n_evap_cols = n_evap_hydros * 3;
(
EvapColumnOffsets {
col_evap_start,
n_evap_cols,
},
evap_hydro_indices,
)
}
fn compute_operational_slack_column_offsets(
ctx: &TemplateBuildCtx<'_>,
n_blks: usize,
withdrawal_slack_start: usize,
) -> OperationalSlackColumnOffsets {
let col_withdrawal_neg_start = withdrawal_slack_start;
let col_withdrawal_pos_start = col_withdrawal_neg_start + ctx.n_hydros;
let n_op_slack = ctx.n_hydros * n_blks;
let col_outflow_below_start = col_withdrawal_pos_start + ctx.n_hydros;
let col_outflow_above_start = col_outflow_below_start + n_op_slack;
let col_turbine_below_start = col_outflow_above_start + n_op_slack;
let col_generation_below_start = col_turbine_below_start + n_op_slack;
let operational_slack_end = col_generation_below_start + n_op_slack;
OperationalSlackColumnOffsets {
col_withdrawal_neg_start,
col_withdrawal_pos_start,
col_outflow_below_start,
col_outflow_above_start,
col_turbine_below_start,
col_generation_below_start,
operational_slack_end,
}
}
fn identify_active_ncs(ctx: &TemplateBuildCtx<'_>, stage: &Stage) -> Vec<usize> {
ctx.non_controllable_sources
.iter()
.enumerate()
.filter_map(|(i, ncs)| {
let ok = ncs.entry_stage_id.is_none_or(|e| e <= stage.id)
&& ncs.exit_stage_id.is_none_or(|e| stage.id < e);
ok.then_some(i)
})
.collect()
}
fn enumerate_generic_constraint_rows(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
n_blks: usize,
row_generic_start: usize,
col_generic_slack_start: usize,
) -> GenericConstraintLayout {
let mut n_generic_rows: usize = 0;
let mut n_generic_slack_cols: usize = 0;
let mut generic_constraint_rows: Vec<GenericConstraintRowEntry> = Vec::new();
for (constraint_idx, constraint) in ctx.generic_constraints.iter().enumerate() {
if !ctx
.resolved_generic_bounds
.is_active(constraint_idx, stage.id)
{
continue;
}
let bound_entries = ctx
.resolved_generic_bounds
.bounds_for_stage(constraint_idx, stage.id);
for &(block_id, bound) in bound_entries {
match block_id {
None => {
for block_idx in 0..n_blks {
let row_offset = row_generic_start + n_generic_rows;
let (slack_plus_col, slack_minus_col) = if constraint.slack.enabled {
let plus_col = col_generic_slack_start + n_generic_slack_cols;
n_generic_slack_cols += 1;
let minus_col = if constraint.sense == ConstraintSense::Equal {
let mc = col_generic_slack_start + n_generic_slack_cols;
n_generic_slack_cols += 1;
Some(mc)
} else {
None
};
(Some(plus_col), minus_col)
} else {
(None, None)
};
let _ = row_offset; n_generic_rows += 1;
generic_constraint_rows.push(GenericConstraintRowEntry {
constraint_idx,
entity_id: constraint.id.0,
block_idx,
bound,
sense: constraint.sense,
slack_enabled: constraint.slack.enabled,
slack_penalty: constraint.slack.penalty.unwrap_or(0.0),
slack_plus_col,
slack_minus_col,
});
}
}
Some(blk_id) => {
#[allow(clippy::cast_sign_loss)]
let block_idx = blk_id as usize;
let (slack_plus_col, slack_minus_col) = if constraint.slack.enabled {
let plus_col = col_generic_slack_start + n_generic_slack_cols;
n_generic_slack_cols += 1;
let minus_col = if constraint.sense == ConstraintSense::Equal {
let mc = col_generic_slack_start + n_generic_slack_cols;
n_generic_slack_cols += 1;
Some(mc)
} else {
None
};
(Some(plus_col), minus_col)
} else {
(None, None)
};
n_generic_rows += 1;
generic_constraint_rows.push(GenericConstraintRowEntry {
constraint_idx,
entity_id: constraint.id.0,
block_idx,
bound,
sense: constraint.sense,
slack_enabled: constraint.slack.enabled,
slack_penalty: constraint.slack.penalty.unwrap_or(0.0),
slack_plus_col,
slack_minus_col,
});
}
}
}
}
GenericConstraintLayout {
n_generic_rows,
n_generic_slack_cols,
generic_constraint_rows,
}
}
impl StageLayout {
pub(crate) fn new(ctx: &TemplateBuildCtx<'_>, stage: &Stage, stage_idx: usize) -> Self {
let n_blks = stage.blocks.len();
let idx = StageIndexer::new(ctx.n_hydros, ctx.max_par_order);
let decision_start = idx.theta + 1;
let dec = compute_decision_column_offsets(ctx, n_blks, decision_start);
let (fpha_cols, fpha_hydro_indices, fpha_planes_per_hydro) =
identify_and_layout_fpha_columns(
ctx,
stage_idx,
n_blks,
dec.col_inflow_slack_start,
dec.n_slack_cols,
);
let (evap_cols, evap_hydro_indices) =
identify_and_layout_evap_columns(ctx, fpha_cols.col_generation_end);
let op_slack = compute_operational_slack_column_offsets(
ctx,
n_blks,
evap_cols.col_evap_start + evap_cols.n_evap_cols,
);
let active_ncs_indices = identify_active_ncs(ctx, stage);
let n_active_ncs = active_ncs_indices.len();
let col_ncs_start = op_slack.operational_slack_end;
let col_ncs_end = col_ncs_start + n_active_ncs * n_blks;
let n_state = idx.n_state;
let n_dual_relevant = n_state;
let row_water_balance_start = n_dual_relevant + ctx.n_hydros;
let row_load_balance_start = row_water_balance_start + ctx.n_hydros;
let row_fpha_start = row_load_balance_start + ctx.n_buses * n_blks;
let n_fpha_rows: usize = fpha_planes_per_hydro.iter().map(|&p| p * n_blks).sum();
let row_evap_start = row_fpha_start + n_fpha_rows;
let n_op_rows = ctx.n_hydros * n_blks;
let row_min_outflow_start = row_evap_start + evap_hydro_indices.len();
let row_max_outflow_start = row_min_outflow_start + n_op_rows;
let row_min_turbine_start = row_max_outflow_start + n_op_rows;
let row_min_generation_start = row_min_turbine_start + n_op_rows;
let row_generic_start = row_min_generation_start + n_op_rows;
let col_generic_slack_start = col_ncs_end;
let generic = enumerate_generic_constraint_rows(
ctx,
stage,
n_blks,
row_generic_start,
col_generic_slack_start,
);
let col_z_inflow_start = idx.z_inflow.start;
let row_z_inflow_start = idx.z_inflow_row_start;
let num_cols = col_generic_slack_start + generic.n_generic_slack_cols;
let num_rows = row_generic_start + generic.n_generic_rows;
let zeta = stage.blocks.iter().map(|b| b.duration_hours).sum::<f64>() * M3S_TO_HM3;
Self {
n_blks,
n_h: ctx.n_hydros,
lag_order: ctx.max_par_order,
col_turbine_start: dec.col_turbine_start,
col_spillage_start: dec.col_spillage_start,
col_diversion_start: dec.col_diversion_start,
col_thermal_start: dec.col_thermal_start,
col_line_fwd_start: dec.col_line_fwd_start,
col_line_rev_start: dec.col_line_rev_start,
col_deficit_start: dec.col_deficit_start,
max_deficit_segments: dec.max_deficit_segments,
col_excess_start: dec.col_excess_start,
col_inflow_slack_start: dec.col_inflow_slack_start,
col_generation_start: fpha_cols.col_generation_start,
col_evap_start: evap_cols.col_evap_start,
col_withdrawal_neg_start: op_slack.col_withdrawal_neg_start,
col_withdrawal_pos_start: op_slack.col_withdrawal_pos_start,
col_outflow_below_start: op_slack.col_outflow_below_start,
col_outflow_above_start: op_slack.col_outflow_above_start,
col_turbine_below_start: op_slack.col_turbine_below_start,
col_generation_below_start: op_slack.col_generation_below_start,
col_ncs_start,
n_ncs: n_active_ncs,
active_ncs_indices,
num_cols,
row_water_balance_start,
row_load_balance_start,
row_fpha_start,
row_evap_start,
row_min_outflow_start,
row_max_outflow_start,
row_min_turbine_start,
row_min_generation_start,
row_generic_start,
num_rows,
n_generic_rows: generic.n_generic_rows,
row_z_inflow_start,
col_z_inflow_start,
n_state,
n_dual_relevant,
zeta,
fpha_hydro_indices,
fpha_planes_per_hydro,
evap_hydro_indices,
generic_constraint_rows: generic.generic_constraint_rows,
}
}
}