use cobre_core::{CoefficientRef, ConstraintSense, Stage};
use crate::generic_constraints::resolve_variable_ref;
use crate::hydro_models::{EvaporationModel, ResolvedProductionModel};
use super::layout::{StageLayout, TemplateBuildCtx};
use super::{EVAPORATION_FLOW_SAFETY_MARGIN, M3S_TO_HM3};
struct ColumnBufs<'a> {
col_lower: &'a mut [f64],
col_upper: &'a mut [f64],
objective: &'a mut [f64],
}
pub(super) fn fill_stage_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let mut col_lower = vec![0.0_f64; layout.num_cols];
let mut col_upper = vec![f64::INFINITY; layout.num_cols];
let mut objective = vec![0.0_f64; layout.num_cols];
let total_stage_hours: f64 = stage.blocks.iter().map(|b| b.duration_hours).sum();
let b = &mut ColumnBufs {
col_lower: &mut col_lower,
col_upper: &mut col_upper,
objective: &mut objective,
};
fill_storage_columns(ctx, stage_idx, layout, b);
fill_ar_lag_columns(layout, b);
fill_anticipated_state_columns(layout, b);
fill_theta_column(layout, b);
fill_turbine_columns(ctx, stage, stage_idx, layout, b);
fill_spillage_columns(ctx, stage, stage_idx, layout, b);
fill_diversion_columns(ctx, stage, stage_idx, layout, b);
fill_thermal_columns(ctx, stage, stage_idx, layout, b);
fill_anticipated_decision_columns(ctx, stage_idx, layout, b);
fill_anticipated_state_out_columns(ctx, stage_idx, layout, b);
fill_anticipated_decision_objective(ctx, stage_idx, layout, b);
zero_anticipated_delivery_thermal_cost(ctx, stage_idx, layout, b);
fill_line_columns(ctx, stage, stage_idx, layout, b);
fill_deficit_and_excess_columns(ctx, stage, stage_idx, layout, b);
fill_inflow_slack_columns(ctx, stage_idx, layout, total_stage_hours, b);
fill_fpha_generation_columns(ctx, stage_idx, layout, b);
fill_evaporation_columns(ctx, stage_idx, layout, total_stage_hours, b);
fill_withdrawal_slack_columns(ctx, stage_idx, layout, total_stage_hours, b);
fill_operational_slack_columns(ctx, stage, stage_idx, layout, b);
fill_ncs_columns(ctx, stage, stage_idx, layout, b);
fill_z_inflow_columns(layout, b);
(col_lower, col_upper, objective)
}
fn fill_storage_columns(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
for h_idx in 0..layout.n_h {
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
bufs.col_lower[h_idx] = hb.min_storage_hm3;
bufs.col_upper[h_idx] = hb.max_storage_hm3;
bufs.col_lower[layout.col_storage_in_start + h_idx] = f64::NEG_INFINITY;
bufs.col_upper[layout.col_storage_in_start + h_idx] = f64::INFINITY;
}
}
fn fill_ar_lag_columns(layout: &StageLayout, bufs: &mut ColumnBufs<'_>) {
let n_lag_cols = layout.lag_order * layout.n_h;
for lag_col in layout.col_inflow_lags_start..layout.col_inflow_lags_start + n_lag_cols {
bufs.col_lower[lag_col] = f64::NEG_INFINITY;
bufs.col_upper[lag_col] = f64::INFINITY;
}
}
fn fill_anticipated_state_columns(layout: &StageLayout, bufs: &mut ColumnBufs<'_>) {
for slot in 0..layout.k_max {
for plant in 0..layout.n_anticipated {
let col = layout.col_anticipated_state_start + slot * layout.n_anticipated + plant;
bufs.col_lower[col] = f64::NEG_INFINITY;
bufs.col_upper[col] = f64::INFINITY;
}
}
}
fn fill_anticipated_state_out_columns(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
let n_stages = ctx.bounds.n_stages();
for local_idx in 0..ctx.n_anticipated {
let k_i = ctx.anticipated_lead_stages[local_idx];
let col = layout.col_anticipated_state_out_start + local_idx;
if stage_idx.saturating_add(k_i) < n_stages {
bufs.col_lower[col] = f64::NEG_INFINITY;
bufs.col_upper[col] = f64::INFINITY;
} else {
bufs.col_lower[col] = 0.0;
bufs.col_upper[col] = 0.0;
}
}
let active_count = (0..ctx.n_anticipated)
.filter(|&i| stage_idx.saturating_add(ctx.anticipated_lead_stages[i]) < n_stages)
.count();
debug_assert_eq!(
active_count, layout.n_anticipated_state_out_def_rows,
"active state_out column count must match def-row count at stage {stage_idx}"
);
}
fn fill_theta_column(layout: &StageLayout, bufs: &mut ColumnBufs<'_>) {
bufs.col_lower[layout.col_theta] = 0.0;
bufs.col_upper[layout.col_theta] = f64::INFINITY;
bufs.objective[layout.col_theta] = 1.0;
}
fn fill_turbine_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
for h_idx in 0..layout.n_h {
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
let model = ctx.production_models.model(h_idx, stage_idx);
let turb_upper = match model {
ResolvedProductionModel::ConstantProductivity { productivity }
if *productivity > 0.0 =>
{
hb.max_turbined_m3s.min(hb.max_generation_mw / productivity)
}
_ => hb.max_turbined_m3s,
};
for blk in 0..layout.n_blks {
let col = layout.col_turbine_start + h_idx * layout.n_blks + blk;
bufs.col_lower[col] = 0.0;
bufs.col_upper[col] = turb_upper;
let block_hours = stage.blocks[blk].duration_hours;
bufs.objective[col] = hp.turbined_cost * block_hours;
}
}
}
fn fill_spillage_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
for h_idx in 0..layout.n_h {
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
for blk in 0..layout.n_blks {
let col = layout.col_spillage_start + h_idx * layout.n_blks + blk;
bufs.col_upper[col] = f64::INFINITY;
let block_hours = stage.blocks[blk].duration_hours;
bufs.objective[col] = hp.spillage_cost * block_hours;
}
}
}
fn fill_diversion_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
for (h_idx, hydro) in ctx.hydros.iter().enumerate() {
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
let max_div = hydro.diversion.as_ref().map_or(0.0, |d| d.max_flow_m3s);
for blk in 0..layout.n_blks {
let col = layout.col_diversion_start + h_idx * layout.n_blks + blk;
bufs.col_lower[col] = 0.0;
bufs.col_upper[col] = max_div;
if max_div > 0.0 {
let block_hours = stage.blocks[blk].duration_hours;
bufs.objective[col] = hp.diversion_cost * block_hours;
}
}
}
}
fn fill_thermal_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
for (t_idx, _thermal) in ctx.thermals.iter().enumerate() {
let tb = ctx.bounds.thermal_bounds(t_idx, stage_idx);
let marginal_cost_per_mwh = tb.cost_per_mwh;
for blk in 0..layout.n_blks {
let col = layout.col_thermal_start + t_idx * layout.n_blks + blk;
bufs.col_lower[col] = tb.min_generation_mw;
bufs.col_upper[col] = tb.max_generation_mw;
let block_hours = stage.blocks[blk].duration_hours;
bufs.objective[col] = marginal_cost_per_mwh * block_hours;
}
}
}
fn fill_anticipated_decision_columns(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
let n_stages = ctx.bounds.n_stages();
for local_idx in 0..ctx.n_anticipated {
let k_i = ctx.anticipated_lead_stages[local_idx];
let thermal_idx = ctx.anticipated_thermal_indices[local_idx];
let col = layout.col_anticipated_decision_start + local_idx;
if stage_idx.saturating_add(k_i) < n_stages {
let delivery_stage = stage_idx + k_i;
let tb = ctx.bounds.thermal_bounds(thermal_idx, delivery_stage);
bufs.col_lower[col] = tb.min_generation_mw;
bufs.col_upper[col] = tb.max_generation_mw;
} else {
bufs.col_lower[col] = 0.0;
bufs.col_upper[col] = 0.0;
}
}
}
fn fill_anticipated_decision_objective(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
let n_stages = ctx.bounds.n_stages();
for local_idx in 0..ctx.n_anticipated {
let k_i = ctx.anticipated_lead_stages[local_idx];
let col = layout.col_anticipated_decision_start + local_idx;
if stage_idx.saturating_add(k_i) < n_stages {
let delivery_stage = stage_idx + k_i;
let thermal_idx = ctx.anticipated_thermal_indices[local_idx];
let tb = ctx.bounds.thermal_bounds(thermal_idx, delivery_stage);
let delivery_hours = ctx.total_hours_per_stage[delivery_stage];
let d_factor = ctx.cumulative_discount_factors[delivery_stage];
bufs.objective[col] = tb.cost_per_mwh * delivery_hours * d_factor;
}
}
}
fn zero_anticipated_delivery_thermal_cost(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
let n_blks = layout.n_blks;
let n_stages = ctx.bounds.n_stages();
for local_idx in 0..ctx.n_anticipated {
if !layout
.indexer
.is_anticipated_fishing_active(local_idx, stage_idx, n_stages)
{
continue;
}
let thermal_idx = ctx.anticipated_thermal_indices[local_idx];
for blk in 0..n_blks {
let col = layout.col_thermal_start + thermal_idx * n_blks + blk;
bufs.objective[col] = 0.0;
}
}
}
fn fill_line_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
for (l_idx, _line) in ctx.lines.iter().enumerate() {
let lb = ctx.bounds.line_bounds(l_idx, stage_idx);
let lp = ctx.penalties.line_penalties(l_idx, stage_idx);
for blk in 0..layout.n_blks {
let (df, rf) = ctx.resolved_exchange_factors.factors(l_idx, stage_idx, blk);
let col_fwd = layout.col_line_fwd_start + l_idx * layout.n_blks + blk;
let col_rev = layout.col_line_rev_start + l_idx * layout.n_blks + blk;
bufs.col_upper[col_fwd] = lb.direct_mw * df;
bufs.col_upper[col_rev] = lb.reverse_mw * rf;
let block_hours = stage.blocks[blk].duration_hours;
bufs.objective[col_fwd] = lp.exchange_cost * block_hours;
bufs.objective[col_rev] = lp.exchange_cost * block_hours;
}
}
}
fn fill_deficit_and_excess_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
for (b_idx, bus) in ctx.buses.iter().enumerate() {
let bp = ctx.penalties.bus_penalties(b_idx, stage_idx);
for (seg_idx, segment) in bus.deficit_segments.iter().enumerate() {
for blk in 0..layout.n_blks {
let col_def = layout.col_deficit_start
+ b_idx * layout.max_deficit_segments * layout.n_blks
+ seg_idx * layout.n_blks
+ blk;
let block_hours = stage.blocks[blk].duration_hours;
bufs.col_upper[col_def] = segment.depth_mw.unwrap_or(f64::INFINITY);
bufs.objective[col_def] = segment.cost_per_mwh * block_hours;
}
}
for blk in 0..layout.n_blks {
let col_exc = layout.col_excess_start + b_idx * layout.n_blks + blk;
let block_hours = stage.blocks[blk].duration_hours;
bufs.col_upper[col_exc] = f64::INFINITY;
bufs.objective[col_exc] = bp.excess_cost * block_hours;
}
}
}
fn fill_inflow_slack_columns(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
total_stage_hours: f64,
bufs: &mut ColumnBufs<'_>,
) {
if ctx.has_penalty {
for h_idx in 0..layout.n_h {
let col = layout.col_inflow_slack_start + h_idx;
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
bufs.objective[col] = hp.inflow_nonnegativity_cost * total_stage_hours;
}
}
}
fn fill_fpha_generation_columns(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
for (local_idx, &h_idx) in layout.fpha_hydro_indices.iter().enumerate() {
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
for blk in 0..layout.n_blks {
let col = layout.col_generation_start + local_idx * layout.n_blks + blk;
bufs.col_lower[col] = 0.0;
bufs.col_upper[col] = hb.max_generation_mw;
}
}
}
fn fill_evaporation_columns(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
total_stage_hours: f64,
bufs: &mut ColumnBufs<'_>,
) {
for (local_idx, &h_idx) in layout.evap_hydro_indices.iter().enumerate() {
let col_evaporation_flow = layout.col_evap_start + local_idx * 3;
let col_f_plus = layout.col_evap_start + local_idx * 3 + 1;
let col_f_minus = layout.col_evap_start + local_idx * 3 + 2;
match ctx.evaporation_models.model(h_idx) {
EvaporationModel::Linearized { coefficients, .. } => {
let coeff = &coefficients[stage_idx];
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
let q_max_abs = (coeff.intercept_m3s
+ coeff.volume_slope_m3s_per_hm3 * hb.max_storage_hm3)
.abs()
* EVAPORATION_FLOW_SAFETY_MARGIN;
bufs.col_lower[col_evaporation_flow] = -q_max_abs;
bufs.col_upper[col_evaporation_flow] = q_max_abs;
}
EvaporationModel::None => {
debug_assert!(
false,
"evap_hydro_indices contains hydro {h_idx} but model is None"
);
continue;
}
}
bufs.col_lower[col_f_plus] = 0.0;
bufs.col_upper[col_f_plus] = f64::INFINITY;
bufs.col_lower[col_f_minus] = 0.0;
bufs.col_upper[col_f_minus] = f64::INFINITY;
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
bufs.objective[col_f_plus] = hp.evaporation_violation_neg_cost * total_stage_hours;
bufs.objective[col_f_minus] = hp.evaporation_violation_pos_cost * total_stage_hours;
}
}
fn fill_withdrawal_slack_columns(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
total_stage_hours: f64,
bufs: &mut ColumnBufs<'_>,
) {
for h_idx in 0..layout.n_h {
let col = layout.col_withdrawal_neg_start + h_idx;
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
let t = hb.water_withdrawal_m3s;
bufs.col_upper[col] = if t > 0.0 {
t } else if t < 0.0 {
f64::INFINITY } else {
0.0 };
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
bufs.objective[col] = hp.water_withdrawal_violation_neg_cost * total_stage_hours;
}
for h_idx in 0..layout.n_h {
let col = layout.col_withdrawal_pos_start + h_idx;
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
let t = hb.water_withdrawal_m3s;
bufs.col_upper[col] = if t > 0.0 {
f64::INFINITY } else if t < 0.0 {
-t } else {
0.0 };
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
bufs.objective[col] = hp.water_withdrawal_violation_pos_cost * total_stage_hours;
}
}
fn fill_operational_slack_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
fill_outflow_below_columns(ctx, stage, stage_idx, layout, bufs);
fill_outflow_above_columns(ctx, stage, stage_idx, layout, bufs);
fill_turbine_below_columns(ctx, stage, stage_idx, layout, bufs);
fill_generation_below_columns(ctx, stage, stage_idx, layout, bufs);
}
fn fill_outflow_below_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
for h_idx in 0..layout.n_h {
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
for blk in 0..layout.n_blks {
let col = layout.col_outflow_below_start + h_idx * layout.n_blks + blk;
bufs.col_upper[col] = if hb.min_outflow_m3s > 0.0 {
f64::INFINITY
} else {
0.0
};
bufs.objective[col] =
hp.outflow_violation_below_cost * stage.blocks[blk].duration_hours;
}
}
}
fn fill_outflow_above_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
for h_idx in 0..layout.n_h {
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
for blk in 0..layout.n_blks {
let col = layout.col_outflow_above_start + h_idx * layout.n_blks + blk;
bufs.col_upper[col] = if hb.max_outflow_m3s.is_some() {
f64::INFINITY
} else {
0.0
};
bufs.objective[col] =
hp.outflow_violation_above_cost * stage.blocks[blk].duration_hours;
}
}
}
fn fill_turbine_below_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
for h_idx in 0..layout.n_h {
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
for blk in 0..layout.n_blks {
let col = layout.col_turbine_below_start + h_idx * layout.n_blks + blk;
bufs.col_upper[col] = if hb.min_turbined_m3s > 0.0 {
f64::INFINITY
} else {
0.0
};
bufs.objective[col] =
hp.turbined_violation_below_cost * stage.blocks[blk].duration_hours;
}
}
}
fn fill_generation_below_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
for h_idx in 0..layout.n_h {
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
for blk in 0..layout.n_blks {
let col = layout.col_generation_below_start + h_idx * layout.n_blks + blk;
bufs.col_upper[col] = if hb.min_generation_mw > 0.0 {
f64::INFINITY
} else {
0.0
};
bufs.objective[col] =
hp.generation_violation_below_cost * stage.blocks[blk].duration_hours;
}
}
}
fn fill_ncs_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
bufs: &mut ColumnBufs<'_>,
) {
for (ncs_local, &ncs_sys_idx) in layout.active_ncs_indices.iter().enumerate() {
let ncs = &ctx.non_controllable_sources[ncs_sys_idx];
let avail_gen = ctx
.resolved_ncs_bounds
.available_generation(ncs_sys_idx, stage_idx);
for blk in 0..layout.n_blks {
let col = layout.col_ncs_start + ncs_local * layout.n_blks + blk;
let factor = ctx.resolved_ncs_factors.factor(ncs_sys_idx, stage_idx, blk);
let upper = avail_gen * factor;
bufs.col_upper[col] = upper;
bufs.col_lower[col] = if ncs.allow_curtailment { 0.0 } else { upper };
let block_hours = stage.blocks[blk].duration_hours;
bufs.objective[col] = -ncs.curtailment_cost * block_hours;
}
}
}
fn fill_z_inflow_columns(layout: &StageLayout, bufs: &mut ColumnBufs<'_>) {
for h_idx in 0..layout.n_h {
let col = layout.col_z_inflow_start + h_idx;
bufs.col_lower[col] = f64::NEG_INFINITY;
bufs.col_upper[col] = f64::INFINITY;
}
}
pub(super) fn fill_stage_rows(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
) -> (Vec<f64>, Vec<f64>) {
let mut row_lower = vec![0.0_f64; layout.num_rows];
let mut row_upper = vec![0.0_f64; layout.num_rows];
for h_idx in 0..layout.n_h {
let row = layout.row_water_balance_start + h_idx;
let base = if ctx.par_lp.n_stages() > 0 && ctx.par_lp.n_hydros() == layout.n_h {
ctx.par_lp.deterministic_base(stage_idx, h_idx)
} else {
0.0
};
let withdrawal = ctx
.bounds
.hydro_bounds(h_idx, stage_idx)
.water_withdrawal_m3s;
let rhs = layout.zeta * (base - withdrawal);
row_lower[row] = rhs;
row_upper[row] = rhs;
}
for (b_idx, bus) in ctx.buses.iter().enumerate() {
let mean_mw = ctx
.load_models
.iter()
.find(|lm| lm.bus_id == bus.id && lm.stage_id == stage.id)
.map_or(0.0, |lm| lm.mean_mw);
for blk in 0..layout.n_blks {
let factor = ctx.resolved_load_factors.factor(b_idx, stage_idx, blk);
let row = layout.row_load_balance_start + b_idx * layout.n_blks + blk;
let rhs = mean_mw * factor;
row_lower[row] = rhs;
row_upper[row] = rhs;
}
}
let n_blks = layout.n_blks;
let mut fpha_block_start = layout.row_fpha_start;
for (local_idx, &h_idx) in layout.fpha_hydro_indices.iter().enumerate() {
if let ResolvedProductionModel::Fpha { planes, .. } =
ctx.production_models.model(h_idx, stage_idx)
{
let n_planes = planes.len();
debug_assert_eq!(
n_planes, layout.fpha_planes_per_hydro[local_idx],
"plane count mismatch for FPHA hydro {h_idx} at stage {stage_idx}"
);
for blk in 0..n_blks {
for (p_idx, plane) in planes.iter().enumerate() {
let row = fpha_block_start + blk * n_planes + p_idx;
row_lower[row] = f64::NEG_INFINITY;
row_upper[row] = plane.intercept;
}
}
fpha_block_start += n_blks * n_planes;
}
}
for (local_idx, &h_idx) in layout.evap_hydro_indices.iter().enumerate() {
if let EvaporationModel::Linearized { coefficients, .. } =
ctx.evaporation_models.model(h_idx)
{
debug_assert!(
stage_idx < coefficients.len(),
"stage index {stage_idx} out of bounds for evaporation coefficients (len = {})",
coefficients.len()
);
let intercept_m3s = coefficients[stage_idx].intercept_m3s;
let row = layout.row_evap_start + local_idx;
row_lower[row] = intercept_m3s;
row_upper[row] = intercept_m3s;
}
}
fill_operational_violation_rows(
ctx,
stage,
stage_idx,
layout,
&mut row_lower,
&mut row_upper,
);
fill_anticipated_fishing_rows(
ctx,
stage,
stage_idx,
layout,
&mut row_lower,
&mut row_upper,
);
fill_anticipated_state_out_def_rows(ctx, stage_idx, layout, &mut row_lower, &mut row_upper);
for h_idx in 0..layout.n_h {
let row = layout.row_z_inflow_start + h_idx;
let base = if ctx.par_lp.n_stages() > 0 && ctx.par_lp.n_hydros() == layout.n_h {
ctx.par_lp.deterministic_base(stage_idx, h_idx)
} else {
0.0
};
row_lower[row] = base;
row_upper[row] = base;
}
(row_lower, row_upper)
}
fn fill_operational_violation_rows(
ctx: &TemplateBuildCtx<'_>,
_stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
row_lower: &mut [f64],
row_upper: &mut [f64],
) {
for h_idx in 0..layout.n_h {
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
for blk in 0..layout.n_blks {
let row = layout.row_min_outflow_start + h_idx * layout.n_blks + blk;
row_lower[row] = hb.min_outflow_m3s;
row_upper[row] = f64::INFINITY;
}
}
for h_idx in 0..layout.n_h {
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
for blk in 0..layout.n_blks {
let row = layout.row_max_outflow_start + h_idx * layout.n_blks + blk;
row_lower[row] = f64::NEG_INFINITY;
row_upper[row] = hb.max_outflow_m3s.unwrap_or(f64::INFINITY);
}
}
for h_idx in 0..layout.n_h {
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
for blk in 0..layout.n_blks {
let row = layout.row_min_turbine_start + h_idx * layout.n_blks + blk;
row_lower[row] = hb.min_turbined_m3s;
row_upper[row] = f64::INFINITY;
}
}
for h_idx in 0..layout.n_h {
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
for blk in 0..layout.n_blks {
let row = layout.row_min_generation_start + h_idx * layout.n_blks + blk;
row_lower[row] = hb.min_generation_mw;
row_upper[row] = f64::INFINITY;
}
}
}
pub(super) fn fill_anticipated_fishing_rows(
ctx: &TemplateBuildCtx<'_>,
_stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
row_lower: &mut [f64],
row_upper: &mut [f64],
) {
let n_stages = ctx.bounds.n_stages();
for local_idx in 0..ctx.n_anticipated {
if !layout
.indexer
.is_anticipated_fishing_active(local_idx, stage_idx, n_stages)
{
continue;
}
let row = layout.row_anticipated_fishing_start + local_idx;
row_lower[row] = 0.0;
row_upper[row] = 0.0;
}
debug_assert_eq!(
ctx.n_anticipated, layout.n_anticipated_fishing_rows,
"fill_anticipated_fishing_rows: row count must equal n_anticipated"
);
}
pub(super) fn fill_anticipated_state_out_def_rows(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
row_lower: &mut [f64],
row_upper: &mut [f64],
) {
let n_stages = ctx.bounds.n_stages();
let mut active_pos: usize = 0;
for &k_i in &ctx.anticipated_lead_stages {
if stage_idx.saturating_add(k_i) >= n_stages {
continue;
}
let row = layout.row_anticipated_state_out_def_start + active_pos;
row_lower[row] = 0.0;
row_upper[row] = 0.0;
active_pos += 1;
}
debug_assert_eq!(
active_pos, layout.n_anticipated_state_out_def_rows,
"fill_anticipated_state_out_def_rows: active_pos mismatch at stage {stage_idx}"
);
}
pub(super) fn fill_anticipated_fishing_entries(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
col_entries: &mut [Vec<(usize, f64)>],
) {
let n_blks = layout.n_blks;
let n_stages = ctx.bounds.n_stages();
for local_idx in 0..ctx.n_anticipated {
if !layout
.indexer
.is_anticipated_fishing_active(local_idx, stage_idx, n_stages)
{
continue;
}
let row = layout.row_anticipated_fishing_start + local_idx;
let thermal_idx = ctx.anticipated_thermal_indices[local_idx];
let mut block_hours_total: f64 = 0.0;
for blk in 0..n_blks {
let col_gen = layout.col_thermal_start + thermal_idx * n_blks + blk;
let block_hours = stage.blocks[blk].duration_hours;
col_entries[col_gen].push((row, block_hours));
block_hours_total += block_hours;
}
let col_state = layout.col_anticipated_state_start + local_idx;
col_entries[col_state].push((row, -block_hours_total));
}
debug_assert_eq!(
ctx.n_anticipated, layout.n_anticipated_fishing_rows,
"fill_anticipated_fishing_entries: row count must equal n_anticipated"
);
}
pub(super) fn fill_anticipated_state_out_def_entries(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
col_entries: &mut [Vec<(usize, f64)>],
) {
let n_stages = ctx.bounds.n_stages();
let mut active_pos: usize = 0;
for local_idx in 0..ctx.n_anticipated {
let k_i = ctx.anticipated_lead_stages[local_idx];
if stage_idx.saturating_add(k_i) >= n_stages {
continue;
}
let row = layout.row_anticipated_state_out_def_start + active_pos;
let col_state_out = layout.col_anticipated_state_out_start + local_idx;
let col_decision = layout.col_anticipated_decision_start + local_idx;
col_entries[col_state_out].push((row, 1.0));
col_entries[col_decision].push((row, -1.0));
active_pos += 1;
}
debug_assert_eq!(
active_pos, layout.n_anticipated_state_out_def_rows,
"fill_anticipated_state_out_def_entries: active_pos mismatch at stage {stage_idx}"
);
}
pub(super) fn fill_state_and_water_entries(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
col_entries: &mut [Vec<(usize, f64)>],
) {
let n_h = layout.n_h;
let n_blks = layout.n_blks;
let lag_order = layout.lag_order;
let zeta = layout.zeta;
let row_water = layout.row_water_balance_start;
let col_storage_in_start = layout.col_storage_in_start;
let col_inflow_lags_start = layout.col_inflow_lags_start;
for h_idx in 0..n_h {
let hydro = &ctx.hydros[h_idx];
let row = row_water + h_idx;
col_entries[h_idx].push((row, 1.0));
col_entries[col_storage_in_start + h_idx].push((row, -1.0));
for blk in 0..n_blks {
let tau_h = stage.blocks[blk].duration_hours * M3S_TO_HM3;
let col_turbine = layout.col_turbine_start + h_idx * n_blks + blk;
col_entries[col_turbine].push((row, tau_h));
let col_spillage = layout.col_spillage_start + h_idx * n_blks + blk;
col_entries[col_spillage].push((row, tau_h));
let col_diversion = layout.col_diversion_start + h_idx * n_blks + blk;
col_entries[col_diversion].push((row, tau_h));
for &up_id in ctx.cascade.upstream(hydro.id) {
if let Some(&u_idx) = ctx.hydro_pos.get(&up_id) {
col_entries[layout.col_turbine_start + u_idx * n_blks + blk]
.push((row, -tau_h));
col_entries[layout.col_spillage_start + u_idx * n_blks + blk]
.push((row, -tau_h));
}
}
if let Some(sources) = ctx.diversion_upstream.get(&hydro.id) {
for &d_idx in sources {
let col_div = layout.col_diversion_start + d_idx * n_blks + blk;
col_entries[col_div].push((row, -tau_h));
}
}
}
if ctx.par_lp.n_stages() > 0 && ctx.par_lp.n_hydros() == n_h {
let psi = ctx.par_lp.psi_slice(stage_idx, h_idx);
for (lag, &psi_val) in psi.iter().enumerate() {
if psi_val != 0.0 && lag < lag_order {
let col = col_inflow_lags_start + lag * n_h + h_idx;
col_entries[col].push((row, -zeta * psi_val));
}
}
}
}
if ctx.has_penalty {
for h_idx in 0..n_h {
let col = layout.col_inflow_slack_start + h_idx;
let row = row_water + h_idx;
col_entries[col].push((row, -zeta));
}
}
for (local_idx, &h_idx) in layout.evap_hydro_indices.iter().enumerate() {
let col_evaporation_flow = layout.col_evap_start + local_idx * 3;
let row = row_water + h_idx;
col_entries[col_evaporation_flow].push((row, zeta));
}
for h_idx in 0..n_h {
let col = layout.col_withdrawal_neg_start + h_idx;
let row = row_water + h_idx;
col_entries[col].push((row, -zeta));
}
for h_idx in 0..n_h {
let col = layout.col_withdrawal_pos_start + h_idx;
let row = row_water + h_idx;
col_entries[col].push((row, zeta));
}
}
pub(super) fn fill_load_balance_entries(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
col_entries: &mut [Vec<(usize, f64)>],
) {
let n_blks = layout.n_blks;
let row_load = layout.row_load_balance_start;
let mut fpha_local: Vec<Option<usize>> = vec![None; ctx.n_hydros];
for (local_idx, &h_idx) in layout.fpha_hydro_indices.iter().enumerate() {
fpha_local[h_idx] = Some(local_idx);
}
for (h_idx, hydro) in ctx.hydros.iter().enumerate() {
if let Some(local_idx) = fpha_local[h_idx] {
debug_assert!(
matches!(
ctx.production_models.model(h_idx, stage_idx),
ResolvedProductionModel::Fpha { .. }
),
"FPHA local-index table inconsistent with production model for hydro {h_idx}"
);
if let Some(&b_idx) = ctx.bus_pos.get(&hydro.bus_id) {
for blk in 0..n_blks {
let row = row_load + b_idx * n_blks + blk;
let col = layout.col_generation_start + local_idx * n_blks + blk;
col_entries[col].push((row, 1.0));
}
}
} else {
let rho = match ctx.production_models.model(h_idx, stage_idx) {
ResolvedProductionModel::ConstantProductivity { productivity } => *productivity,
ResolvedProductionModel::Fpha { .. } => {
unreachable!(
"non-FPHA branch reached for FPHA resolved model at hydro {h_idx}"
);
}
};
if let Some(&b_idx) = ctx.bus_pos.get(&hydro.bus_id) {
for blk in 0..n_blks {
let row = row_load + b_idx * n_blks + blk;
let col = layout.col_turbine_start + h_idx * n_blks + blk;
col_entries[col].push((row, rho));
}
}
}
}
for (t_idx, thermal) in ctx.thermals.iter().enumerate() {
if let Some(&b_idx) = ctx.bus_pos.get(&thermal.bus_id) {
for blk in 0..n_blks {
let row = row_load + b_idx * n_blks + blk;
let col = layout.col_thermal_start + t_idx * n_blks + blk;
col_entries[col].push((row, 1.0));
}
}
}
for (l_idx, line) in ctx.lines.iter().enumerate() {
let src_idx = ctx.bus_pos.get(&line.source_bus_id).copied();
let tgt_idx = ctx.bus_pos.get(&line.target_bus_id).copied();
for blk in 0..n_blks {
let col_fwd = layout.col_line_fwd_start + l_idx * n_blks + blk;
let col_rev = layout.col_line_rev_start + l_idx * n_blks + blk;
if let Some(tgt) = tgt_idx {
let row = row_load + tgt * n_blks + blk;
col_entries[col_fwd].push((row, 1.0));
col_entries[col_rev].push((row, -1.0));
}
if let Some(src) = src_idx {
let row = row_load + src * n_blks + blk;
col_entries[col_fwd].push((row, -1.0));
col_entries[col_rev].push((row, 1.0));
}
}
}
for (b_idx, bus) in ctx.buses.iter().enumerate() {
for blk in 0..n_blks {
let row = row_load + b_idx * n_blks + blk;
for seg_idx in 0..bus.deficit_segments.len() {
let col_def = layout.col_deficit_start
+ b_idx * layout.max_deficit_segments * n_blks
+ seg_idx * n_blks
+ blk;
col_entries[col_def].push((row, 1.0));
}
let col_exc = layout.col_excess_start + b_idx * n_blks + blk;
col_entries[col_exc].push((row, -1.0));
}
}
}
pub(super) fn fill_fpha_entries(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
col_entries: &mut [Vec<(usize, f64)>],
) {
let n_blks = layout.n_blks;
let col_storage_in_start = layout.col_storage_in_start;
let mut fpha_block_start = layout.row_fpha_start;
for (local_idx, &h_idx) in layout.fpha_hydro_indices.iter().enumerate() {
let model = ctx.production_models.model(h_idx, stage_idx);
let planes = match model {
ResolvedProductionModel::Fpha { planes, .. } => planes,
ResolvedProductionModel::ConstantProductivity { .. } => {
debug_assert!(
false,
"fpha_hydro_indices contains hydro {h_idx} but model is ConstantProductivity"
);
continue;
}
};
let n_planes = planes.len();
for blk in 0..n_blks {
let col_v = h_idx; let col_v_in = col_storage_in_start + h_idx; let col_q = layout.col_turbine_start + h_idx * n_blks + blk;
let col_s = layout.col_spillage_start + h_idx * n_blks + blk;
let col_g = layout.col_generation_start + local_idx * n_blks + blk;
for (p_idx, plane) in planes.iter().enumerate() {
let row = fpha_block_start + blk * n_planes + p_idx;
col_entries[col_g].push((row, 1.0));
col_entries[col_v].push((row, -plane.gamma_v / 2.0));
col_entries[col_v_in].push((row, -plane.gamma_v / 2.0));
col_entries[col_q].push((row, -plane.gamma_q));
col_entries[col_s].push((row, -plane.gamma_s));
}
}
fpha_block_start += n_blks * n_planes;
}
}
pub(super) fn fill_evaporation_entries(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
col_entries: &mut [Vec<(usize, f64)>],
) {
let col_storage_in_start = layout.col_storage_in_start;
for (local_idx, &h_idx) in layout.evap_hydro_indices.iter().enumerate() {
let coeff = match ctx.evaporation_models.model(h_idx) {
EvaporationModel::Linearized { coefficients, .. } => {
debug_assert!(
stage_idx < coefficients.len(),
"evap_hydro_indices contains hydro {h_idx} but coefficients length {} \
is less than stage_idx {}",
coefficients.len(),
stage_idx
);
match coefficients.get(stage_idx) {
Some(c) => *c,
None => continue,
}
}
EvaporationModel::None => {
debug_assert!(
false,
"evap_hydro_indices contains hydro {h_idx} but model is None"
);
continue;
}
};
let col_evaporation_flow = layout.col_evap_start + local_idx * 3;
let col_f_plus = layout.col_evap_start + local_idx * 3 + 1;
let col_f_minus = layout.col_evap_start + local_idx * 3 + 2;
let col_v = h_idx;
let col_v_in = col_storage_in_start + h_idx;
let row = layout.row_evap_start + local_idx;
col_entries[col_evaporation_flow].push((row, 1.0));
col_entries[col_v].push((row, -coeff.volume_slope_m3s_per_hm3 / 2.0));
col_entries[col_v_in].push((row, -coeff.volume_slope_m3s_per_hm3 / 2.0));
col_entries[col_f_plus].push((row, 1.0));
col_entries[col_f_minus].push((row, -1.0));
}
}
pub(super) struct LpMatrixBuffers<'a> {
pub(super) col_entries: &'a mut [Vec<(usize, f64)>],
pub(super) _col_lower: &'a mut [f64],
pub(super) col_upper: &'a mut [f64],
pub(super) objective: &'a mut [f64],
pub(super) row_lower: &'a mut [f64],
pub(super) row_upper: &'a mut [f64],
}
pub(super) fn fill_generic_constraint_entries(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
buffers: &mut LpMatrixBuffers<'_>,
) {
let col_entries = &mut *buffers.col_entries;
let col_upper = &mut *buffers.col_upper;
let objective = &mut *buffers.objective;
let row_lower = &mut *buffers.row_lower;
let row_upper = &mut *buffers.row_upper;
if layout.n_generic_rows == 0 {
return;
}
let positions = crate::generic_constraints::EntityPositionMaps {
hydro: &ctx.hydro_pos,
thermal: &ctx.thermal_pos,
bus: &ctx.bus_pos,
line: &ctx.line_pos,
};
for (entry_idx, entry) in layout.generic_constraint_rows.iter().enumerate() {
let row = layout.row_generic_start + entry_idx;
let constraint = &ctx.generic_constraints[entry.constraint_idx];
let block_hours = if entry.is_stage_level {
stage.blocks.iter().map(|b| b.duration_hours).sum()
} else {
stage.blocks[entry.block_idx].duration_hours
};
match entry.sense {
ConstraintSense::LessEqual => {
row_lower[row] = f64::NEG_INFINITY;
row_upper[row] = entry.bound;
}
ConstraintSense::GreaterEqual => {
row_lower[row] = entry.bound;
row_upper[row] = f64::INFINITY;
}
ConstraintSense::Equal => {
row_lower[row] = entry.bound;
row_upper[row] = entry.bound;
}
}
for term in &constraint.expression.terms {
let pairs = resolve_variable_ref(
&term.variable,
entry.block_idx,
layout.n_blks,
stage_idx,
&layout.indexer,
ctx.production_models,
&positions,
);
for (col, multiplier) in pairs {
let coef = match term.coefficient {
CoefficientRef::Literal(v) => v,
CoefficientRef::Parameter(param_id) => {
ctx.resolved_parameters.get(param_id, stage_idx)
}
};
col_entries[col].push((row, coef * term.scale * multiplier));
}
}
if let Some(plus_col) = entry.slack_plus_col {
let penalty = constraint.slack.penalty.unwrap_or(0.0);
let obj_coeff = penalty * block_hours;
col_upper[plus_col] = f64::INFINITY;
objective[plus_col] = obj_coeff;
match entry.sense {
ConstraintSense::LessEqual => {
col_entries[plus_col].push((row, -1.0));
}
ConstraintSense::GreaterEqual => {
col_entries[plus_col].push((row, 1.0));
}
ConstraintSense::Equal => {
col_entries[plus_col].push((row, 1.0));
}
}
if let Some(minus_col) = entry.slack_minus_col {
col_upper[minus_col] = f64::INFINITY;
objective[minus_col] = obj_coeff;
col_entries[minus_col].push((row, -1.0));
}
}
}
}
pub(super) fn fill_ncs_load_balance_entries(
ctx: &TemplateBuildCtx<'_>,
layout: &StageLayout,
col_entries: &mut [Vec<(usize, f64)>],
) {
for (ncs_local, &ncs_sys_idx) in layout.active_ncs_indices.iter().enumerate() {
let ncs = &ctx.non_controllable_sources[ncs_sys_idx];
let Some(&bus_idx) = ctx.bus_pos.get(&ncs.bus_id) else {
continue;
};
for blk in 0..layout.n_blks {
let col = layout.col_ncs_start + ncs_local * layout.n_blks + blk;
let row = layout.row_load_balance_start + bus_idx * layout.n_blks + blk;
col_entries[col].push((row, 1.0));
}
}
}
pub(super) fn fill_z_inflow_entries(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
col_entries: &mut [Vec<(usize, f64)>],
) {
let n_h = layout.n_h;
let lag_order = layout.lag_order;
let col_inflow_lags_start = layout.col_inflow_lags_start;
for h_idx in 0..n_h {
let row = layout.row_z_inflow_start + h_idx;
let col_z = layout.col_z_inflow_start + h_idx;
col_entries[col_z].push((row, 1.0));
if ctx.par_lp.n_stages() > 0 && ctx.par_lp.n_hydros() == n_h {
let psi = ctx.par_lp.psi_slice(stage_idx, h_idx);
for (lag, &psi_val) in psi.iter().enumerate() {
if psi_val != 0.0 && lag < lag_order {
let col = col_inflow_lags_start + lag * n_h + h_idx;
col_entries[col].push((row, -psi_val));
}
}
}
}
}
pub(super) fn fill_operational_violation_entries(
ctx: &TemplateBuildCtx<'_>,
_stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
col_entries: &mut [Vec<(usize, f64)>],
) {
let n_blks = layout.n_blks;
let mut fpha_local: Vec<Option<usize>> = vec![None; ctx.n_hydros];
for (local_idx, &h_idx) in layout.fpha_hydro_indices.iter().enumerate() {
fpha_local[h_idx] = Some(local_idx);
}
for (h_idx, fpha_local_entry) in fpha_local.iter().enumerate() {
for blk in 0..n_blks {
let row = layout.row_min_outflow_start + h_idx * n_blks + blk;
let col_q = layout.col_turbine_start + h_idx * n_blks + blk;
col_entries[col_q].push((row, 1.0));
let col_s = layout.col_spillage_start + h_idx * n_blks + blk;
col_entries[col_s].push((row, 1.0));
let col_d = layout.col_diversion_start + h_idx * n_blks + blk;
col_entries[col_d].push((row, 1.0));
let col_slack = layout.col_outflow_below_start + h_idx * n_blks + blk;
col_entries[col_slack].push((row, 1.0));
}
for blk in 0..n_blks {
let row = layout.row_max_outflow_start + h_idx * n_blks + blk;
let col_q = layout.col_turbine_start + h_idx * n_blks + blk;
col_entries[col_q].push((row, 1.0));
let col_s = layout.col_spillage_start + h_idx * n_blks + blk;
col_entries[col_s].push((row, 1.0));
let col_d = layout.col_diversion_start + h_idx * n_blks + blk;
col_entries[col_d].push((row, 1.0));
let col_slack = layout.col_outflow_above_start + h_idx * n_blks + blk;
col_entries[col_slack].push((row, -1.0));
}
for blk in 0..n_blks {
let row = layout.row_min_turbine_start + h_idx * n_blks + blk;
let col_q = layout.col_turbine_start + h_idx * n_blks + blk;
col_entries[col_q].push((row, 1.0));
let col_slack = layout.col_turbine_below_start + h_idx * n_blks + blk;
col_entries[col_slack].push((row, 1.0));
}
if let Some(&local_fpha_idx) = fpha_local_entry.as_ref() {
for blk in 0..n_blks {
let row = layout.row_min_generation_start + h_idx * n_blks + blk;
let col_g = layout.col_generation_start + local_fpha_idx * n_blks + blk;
col_entries[col_g].push((row, 1.0));
let col_slack = layout.col_generation_below_start + h_idx * n_blks + blk;
col_entries[col_slack].push((row, 1.0));
}
} else {
let rho = match ctx.production_models.model(h_idx, stage_idx) {
ResolvedProductionModel::ConstantProductivity { productivity } => *productivity,
ResolvedProductionModel::Fpha { .. } => {
unreachable!(
"Fpha resolved model in ConstantProductivity LP path for hydro \
{h_idx}; validate production model assignment upstream"
);
}
};
for blk in 0..n_blks {
let row = layout.row_min_generation_start + h_idx * n_blks + blk;
let col_q = layout.col_turbine_start + h_idx * n_blks + blk;
col_entries[col_q].push((row, rho));
let col_slack = layout.col_generation_below_start + h_idx * n_blks + blk;
col_entries[col_slack].push((row, 1.0));
}
}
}
}
pub(super) fn build_stage_matrix_entries(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
) -> Vec<Vec<(usize, f64)>> {
let mut col_entries: Vec<Vec<(usize, f64)>> = vec![Vec::new(); layout.num_cols];
fill_state_and_water_entries(ctx, stage, stage_idx, layout, &mut col_entries);
fill_anticipated_state_out_def_entries(ctx, stage_idx, layout, &mut col_entries);
fill_load_balance_entries(ctx, stage_idx, layout, &mut col_entries);
fill_ncs_load_balance_entries(ctx, layout, &mut col_entries);
fill_fpha_entries(ctx, stage_idx, layout, &mut col_entries);
fill_evaporation_entries(ctx, stage_idx, layout, &mut col_entries);
fill_z_inflow_entries(ctx, stage_idx, layout, &mut col_entries);
fill_operational_violation_entries(ctx, stage, stage_idx, layout, &mut col_entries);
fill_anticipated_fishing_entries(ctx, stage, stage_idx, layout, &mut col_entries);
col_entries
}
pub(super) fn assemble_csc(col_entries: &[Vec<(usize, f64)>]) -> (Vec<i32>, Vec<i32>, Vec<f64>) {
let num_cols = col_entries.len();
let total_nz: usize = col_entries.iter().map(Vec::len).sum();
let mut col_starts = Vec::with_capacity(num_cols + 1);
let mut row_indices = Vec::with_capacity(total_nz);
let mut values = Vec::with_capacity(total_nz);
let mut offset: i32 = 0;
for entries in col_entries {
col_starts.push(offset);
for &(row, val) in entries {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
row_indices.push(row as i32);
values.push(val);
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
{
offset += entries.len() as i32;
}
}
col_starts.push(offset);
(col_starts, row_indices, values)
}
#[cfg(test)]
#[allow(
clippy::too_many_lines,
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::float_cmp
)]
mod parameter_resolution_tests {
use cobre_core::{
BoundsCountsSpec, BoundsDefaults, Bus, BusStagePenalties, ConstraintExpression,
ConstraintSense, ContractStageBounds, DeficitSegment, EntityId, GenericConstraint,
HydroStageBounds, HydroStagePenalties, LineStageBounds, LineStagePenalties,
NcsStagePenalties, ParameterKind, PenaltiesCountsSpec, PenaltiesDefaults,
PumpingStageBounds, ResolvedBounds, ResolvedGenericConstraintBounds, ResolvedPenalties,
ScalarParameter, SlackConfig, SystemBuilder, ThermalStageBounds,
};
use cobre_core::{LinearTerm, VariableRef};
use cobre_stochastic::normal::precompute::PrecomputedNormal;
use cobre_stochastic::par::precompute::PrecomputedPar;
use std::collections::HashMap;
use crate::energy_conversion::{EnergyConversionSet, build_hydro_energy_productivity_override};
use crate::hydro_models::PrepareHydroModelsResult;
use crate::inflow_method::InflowNonNegativityMethod;
use crate::resolved_parameters::build_resolved_parameters;
fn csc_entries_at(t: &cobre_solver::StageTemplate, col: usize, row: usize) -> Vec<f64> {
let start = t.col_starts[col] as usize;
let end = t.col_starts[col + 1] as usize;
t.row_indices[start..end]
.iter()
.zip(t.values[start..end].iter())
.filter_map(|(&r, &v)| if r as usize == row { Some(v) } else { None })
.collect()
}
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,
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
fn one_thermal_n_stages(
n_stages: usize,
thermal_entity_id: EntityId,
constraints: Vec<GenericConstraint>,
bounds: ResolvedGenericConstraintBounds,
) -> cobre_core::System {
use chrono::NaiveDate;
use cobre_core::entities::thermal::Thermal;
use cobre_core::scenario::LoadModel;
use cobre_core::temporal::{
Block, BlockMode, NoiseMethod, ScenarioSourceConfig, Stage, StageRiskConfig,
StageStateConfig,
};
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 thermal = Thermal {
id: thermal_entity_id,
name: "T1".to_string(),
bus_id: EntityId(1),
min_generation_mw: 0.0,
max_generation_mw: 100.0,
cost_per_mwh: 50.0,
anticipated_config: None,
entry_stage_id: None,
exit_stage_id: None,
};
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<_> = (0..n_stages)
.map(|i| LoadModel {
bus_id: EntityId(1),
stage_id: i as i32,
mean_mw: 100.0,
std_mw: 0.0,
})
.collect();
let resolved_bounds = ResolvedBounds::new(
&BoundsCountsSpec {
n_hydros: 0,
n_thermals: 1,
n_lines: 0,
n_pumping: 0,
n_contracts: 0,
n_stages,
k_max: 0,
},
&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(vec![thermal])
.stages(stages)
.load_models(load_models)
.bounds(resolved_bounds)
.penalties(penalties)
.generic_constraints(constraints)
.resolved_generic_bounds(bounds)
.build()
.expect("one_thermal_n_stages: valid system")
}
fn make_templates(
system: &cobre_core::System,
resolved_params: &crate::resolved_parameters::ResolvedParameters,
) -> Vec<cobre_solver::StageTemplate> {
let production = PrepareHydroModelsResult::default_from_system(system).production;
let evaporation = PrepareHydroModelsResult::default_from_system(system).evaporation;
crate::lp_builder::build_stage_templates(
system,
InflowNonNegativityMethod::None,
&PrecomputedPar::default(),
&PrecomputedNormal::default(),
&production,
&evaporation,
resolved_params,
)
.expect("make_templates: valid")
.templates
}
fn empty_resolved_params(n_stages: usize) -> crate::resolved_parameters::ResolvedParameters {
let stage_to_season: Vec<i32> = vec![0; n_stages];
let ec = EnergyConversionSet::new(vec![], vec![], 0, n_stages);
let override_table =
build_hydro_energy_productivity_override(&[]).expect("empty override table");
build_resolved_parameters(&[], &ec, &override_table, &[], &stage_to_season, n_stages)
.expect("empty_resolved_params: valid")
}
fn constant_param_resolved(
param_id: EntityId,
value: f64,
n_stages: usize,
) -> crate::resolved_parameters::ResolvedParameters {
let stage_to_season: Vec<i32> = vec![0; n_stages];
let ec = EnergyConversionSet::new(vec![], vec![], 0, n_stages);
let override_table =
build_hydro_energy_productivity_override(&[]).expect("empty override table");
let params = vec![ScalarParameter {
id: param_id,
name: format!("p{}", param_id.0),
kind: ParameterKind::Constant { value },
}];
build_resolved_parameters(
¶ms,
&ec,
&override_table,
&[],
&stage_to_season,
n_stages,
)
.expect("constant_param_resolved: valid")
}
fn per_stage_param_resolved(
param_id: EntityId,
values: Vec<f64>,
) -> crate::resolved_parameters::ResolvedParameters {
let n_stages = values.len();
let stage_to_season: Vec<i32> = vec![0; n_stages];
let ec = EnergyConversionSet::new(vec![], vec![], 0, n_stages);
let override_table =
build_hydro_energy_productivity_override(&[]).expect("empty override table");
let params = vec![ScalarParameter {
id: param_id,
name: format!("p{}", param_id.0),
kind: ParameterKind::PerStage { values },
}];
build_resolved_parameters(
¶ms,
&ec,
&override_table,
&[],
&stage_to_season,
n_stages,
)
.expect("per_stage_param_resolved: valid")
}
fn parameter_constraint(
constraint_id: EntityId,
param_id: EntityId,
scale: f64,
thermal_id: EntityId,
) -> GenericConstraint {
GenericConstraint {
id: constraint_id,
name: format!("gc_{}", constraint_id.0),
description: None,
expression: ConstraintExpression {
terms: vec![LinearTerm::parameter(
param_id,
scale,
VariableRef::ThermalGeneration {
thermal_id,
block_id: None,
},
)],
},
sense: ConstraintSense::LessEqual,
slack: SlackConfig {
enabled: false,
penalty: None,
},
}
}
fn literal_constraint(
constraint_id: EntityId,
coef: f64,
scale: f64,
thermal_id: EntityId,
) -> GenericConstraint {
GenericConstraint {
id: constraint_id,
name: format!("gc_lit_{}", constraint_id.0),
description: None,
expression: ConstraintExpression {
terms: vec![LinearTerm {
coefficient: cobre_core::CoefficientRef::Literal(coef),
scale,
variable: VariableRef::ThermalGeneration {
thermal_id,
block_id: None,
},
}],
},
sense: ConstraintSense::LessEqual,
slack: SlackConfig {
enabled: false,
penalty: None,
},
}
}
fn bounds_for_n_stages(
constraint_id: EntityId,
n_stages: usize,
) -> ResolvedGenericConstraintBounds {
let id_map: HashMap<i32, usize> = [(constraint_id.0, 0)].into_iter().collect();
let rows: Vec<(i32, i32, Option<i32>, f64)> = (0..n_stages)
.map(|s| (constraint_id.0, s as i32, None, 50.0_f64))
.collect();
ResolvedGenericConstraintBounds::new(&id_map, rows.into_iter())
}
const THERMAL_COL: usize = 1;
const GENERIC_ROW: usize = 1;
#[test]
fn parameter_coefficient_is_resolved_at_lp_build() {
let param_id = EntityId(7);
let thermal_id = EntityId(2);
let constraint_id = EntityId(10);
let scale = 2.0_f64;
let param_value = 3.0_f64;
let constraint = parameter_constraint(constraint_id, param_id, scale, thermal_id);
let generic_bounds = bounds_for_n_stages(constraint_id, 1);
let system = one_thermal_n_stages(1, thermal_id, vec![constraint], generic_bounds);
let resolved = constant_param_resolved(param_id, param_value, 1);
let templates = make_templates(&system, &resolved);
let t = &templates[0];
let entries = csc_entries_at(t, THERMAL_COL, GENERIC_ROW);
assert!(
!entries.is_empty(),
"no CSC entry at (col={THERMAL_COL}, row={GENERIC_ROW}) for parameter term"
);
let total: f64 = entries.iter().sum();
let expected = param_value * scale; assert_eq!(
total.to_bits(),
expected.to_bits(),
"expected {expected} (bit-exact), got {total}"
);
}
#[test]
fn parameter_coefficient_per_stage_changes_lp() {
let param_id = EntityId(7);
let thermal_id = EntityId(2);
let constraint_id = EntityId(10);
let scale = 2.0_f64;
let per_stage_values = vec![3.0_f64, 7.0, 11.0];
let n_stages = per_stage_values.len();
let constraint = parameter_constraint(constraint_id, param_id, scale, thermal_id);
let generic_bounds = bounds_for_n_stages(constraint_id, n_stages);
let system = one_thermal_n_stages(n_stages, thermal_id, vec![constraint], generic_bounds);
let resolved = per_stage_param_resolved(param_id, per_stage_values.clone());
let templates = make_templates(&system, &resolved);
for (stage_idx, ¶m_val) in per_stage_values.iter().enumerate() {
let t = &templates[stage_idx];
let entries = csc_entries_at(t, THERMAL_COL, GENERIC_ROW);
assert!(
!entries.is_empty(),
"no CSC entry at (col={THERMAL_COL}, row={GENERIC_ROW}) for stage {stage_idx}"
);
let total: f64 = entries.iter().sum();
let expected = param_val * scale; assert_eq!(
total.to_bits(),
expected.to_bits(),
"stage {stage_idx}: expected {expected} (bit-exact), got {total}"
);
}
}
#[test]
fn literal_coefficient_still_works_after_wiring() {
let thermal_id = EntityId(2);
let constraint_id = EntityId(10);
let coef = 5.0_f64;
let scale = 3.0_f64;
let constraint = literal_constraint(constraint_id, coef, scale, thermal_id);
let generic_bounds = bounds_for_n_stages(constraint_id, 1);
let system = one_thermal_n_stages(1, thermal_id, vec![constraint], generic_bounds);
let resolved = empty_resolved_params(1);
let templates = make_templates(&system, &resolved);
let t = &templates[0];
let entries = csc_entries_at(t, THERMAL_COL, GENERIC_ROW);
assert!(
!entries.is_empty(),
"no CSC entry at (col={THERMAL_COL}, row={GENERIC_ROW}) for literal term"
);
let total: f64 = entries.iter().sum();
let expected = coef * scale; assert_eq!(
total.to_bits(),
expected.to_bits(),
"expected {expected} (bit-exact), got {total}"
);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::float_cmp)]
mod zero_cost_tests {
use std::collections::HashMap;
use chrono::NaiveDate;
use cobre_core::{
Block, BlockMode, BoundsCountsSpec, BoundsDefaults, CascadeTopology, ContractStageBounds,
HydroStageBounds, LineStageBounds, NoiseMethod, PumpingStageBounds, ResolvedBounds,
ResolvedExchangeFactors, ResolvedGenericConstraintBounds, ResolvedLoadFactors,
ResolvedNcsBounds, ResolvedNcsFactors, ResolvedPenalties, ScenarioSourceConfig, Stage,
StageRiskConfig, StageStateConfig, ThermalStageBounds,
};
use cobre_stochastic::par::precompute::PrecomputedPar;
use crate::hydro_models::{EvaporationModelSet, ProductionModelSet};
use crate::resolved_parameters::ResolvedParameters;
use super::{
ColumnBufs, StageLayout, TemplateBuildCtx, build_stage_matrix_entries,
fill_anticipated_fishing_entries, fill_anticipated_fishing_rows,
fill_anticipated_state_out_columns, fill_anticipated_state_out_def_entries,
fill_anticipated_state_out_def_rows, zero_anticipated_delivery_thermal_cost,
};
fn two_block_stage(index: usize) -> Stage {
Stage {
index,
id: index 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: 372.0,
},
Block {
index: 1,
name: "BLK1".to_string(),
duration_hours: 372.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,
},
}
}
struct AntFixtures {
par_lp: PrecomputedPar,
cascade: CascadeTopology,
bounds: ResolvedBounds,
penalties: ResolvedPenalties,
resolved_generic_bounds: ResolvedGenericConstraintBounds,
resolved_load_factors: ResolvedLoadFactors,
resolved_exchange_factors: ResolvedExchangeFactors,
resolved_ncs_bounds: ResolvedNcsBounds,
resolved_ncs_factors: ResolvedNcsFactors,
resolved_parameters: ResolvedParameters,
production_models: ProductionModelSet,
evaporation_models: EvaporationModelSet,
}
impl AntFixtures {
fn bounds_with_n_stages(n_stages: usize) -> ResolvedBounds {
ResolvedBounds::new(
&BoundsCountsSpec {
n_hydros: 0,
n_thermals: 0,
n_lines: 0,
n_pumping: 0,
n_contracts: 0,
n_stages,
k_max: 0,
},
&BoundsDefaults {
hydro: HydroStageBounds {
min_storage_hm3: 0.0,
max_storage_hm3: 0.0,
min_turbined_m3s: 0.0,
max_turbined_m3s: 0.0,
min_outflow_m3s: 0.0,
max_outflow_m3s: None,
min_generation_mw: 0.0,
max_generation_mw: 0.0,
max_diversion_m3s: None,
filling_inflow_m3s: 0.0,
water_withdrawal_m3s: 0.0,
},
thermal: ThermalStageBounds {
min_generation_mw: 0.0,
max_generation_mw: 0.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,
},
},
)
}
fn new() -> Self {
Self {
par_lp: PrecomputedPar::default(),
cascade: CascadeTopology::build(&[]),
bounds: ResolvedBounds::empty(),
penalties: ResolvedPenalties::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(),
resolved_parameters: ResolvedParameters {
per_param: vec![],
id_to_slot: vec![],
},
production_models: ProductionModelSet::new(vec![], 0, 1),
evaporation_models: EvaporationModelSet::new(vec![]),
}
}
fn make_ctx(
&self,
n_anticipated: usize,
k_max: usize,
anticipated_lead_stages: Vec<usize>,
anticipated_thermal_indices: Vec<usize>,
n_thermals: usize,
) -> TemplateBuildCtx<'_> {
TemplateBuildCtx {
hydros: &[],
thermals: &[],
lines: &[],
buses: &[],
load_models: &[],
cascade: &self.cascade,
bounds: &self.bounds,
penalties: &self.penalties,
hydro_pos: HashMap::new(),
thermal_pos: HashMap::new(),
line_pos: HashMap::new(),
bus_pos: HashMap::new(),
par_lp: &self.par_lp,
production_models: &self.production_models,
evaporation_models: &self.evaporation_models,
generic_constraints: &[],
resolved_generic_bounds: &self.resolved_generic_bounds,
resolved_load_factors: &self.resolved_load_factors,
resolved_exchange_factors: &self.resolved_exchange_factors,
non_controllable_sources: &[],
resolved_ncs_bounds: &self.resolved_ncs_bounds,
resolved_ncs_factors: &self.resolved_ncs_factors,
resolved_parameters: &self.resolved_parameters,
diversion_upstream: HashMap::new(),
n_hydros: 0,
n_thermals,
n_lines: 0,
n_buses: 0,
max_par_order: 0,
n_anticipated,
k_max,
anticipated_lead_stages,
anticipated_thermal_indices,
has_penalty: false,
cumulative_discount_factors: vec![1.0],
total_hours_per_stage: vec![744.0],
}
}
}
#[test]
fn zero_anticipated_delivery_thermal_cost_zeroes_all_plants() {
let mut fixtures = AntFixtures::new();
fixtures.bounds = AntFixtures::bounds_with_n_stages(10);
let ctx = fixtures.make_ctx(
2, 5, vec![1, 5], vec![0, 1], 2, );
let stage = two_block_stage(2);
let layout = StageLayout::new(&ctx, &stage, 2);
let mut objective = vec![42.0_f64; layout.num_cols];
let mut col_lower = vec![0.0_f64; layout.num_cols];
let mut col_upper = vec![f64::INFINITY; layout.num_cols];
let mut bufs = ColumnBufs {
col_lower: &mut col_lower,
col_upper: &mut col_upper,
objective: &mut objective,
};
zero_anticipated_delivery_thermal_cost(&ctx, 2, &layout, &mut bufs);
let n_blks = layout.n_blks;
let thermal_idx_0 = ctx.anticipated_thermal_indices[0];
for blk in 0..n_blks {
let col = layout.col_thermal_start + thermal_idx_0 * n_blks + blk;
assert_eq!(
bufs.objective[col], 0.0,
"plant 0 must be zeroed at col {col}",
);
}
let thermal_idx_1 = ctx.anticipated_thermal_indices[1];
for blk in 0..n_blks {
let col = layout.col_thermal_start + thermal_idx_1 * n_blks + blk;
assert_eq!(
bufs.objective[col], 0.0,
"plant 1 must be zeroed at col {col}",
);
}
}
#[test]
fn fishing_rows_fill_all_plants() {
let mut fixtures = AntFixtures::new();
fixtures.bounds = AntFixtures::bounds_with_n_stages(10);
let ctx = fixtures.make_ctx(2, 5, vec![1, 5], vec![0, 1], 2);
let stage = two_block_stage(2);
let layout = StageLayout::new(&ctx, &stage, 2);
assert_eq!(
layout.n_anticipated_fishing_rows, 2,
"expected n_anticipated_fishing_rows == 2, got {}",
layout.n_anticipated_fishing_rows
);
let mut row_lower = vec![f64::NAN; layout.num_rows];
let mut row_upper = vec![f64::NAN; layout.num_rows];
fill_anticipated_fishing_rows(&ctx, &stage, 2, &layout, &mut row_lower, &mut row_upper);
for local_idx in 0..layout.n_anticipated_fishing_rows {
let row = layout.row_anticipated_fishing_start + local_idx;
assert_eq!(
row_lower[row], 0.0,
"row_lower[{row}] (local_idx={local_idx}) expected 0.0, got {}",
row_lower[row]
);
assert_eq!(
row_upper[row], 0.0,
"row_upper[{row}] (local_idx={local_idx}) expected 0.0, got {}",
row_upper[row]
);
}
}
#[test]
fn fishing_rows_always_active_stage_zero() {
let mut fixtures = AntFixtures::new();
fixtures.bounds = AntFixtures::bounds_with_n_stages(10);
let ctx = fixtures.make_ctx(2, 5, vec![1, 5], vec![0, 1], 2);
let stage = two_block_stage(0);
let layout = StageLayout::new(&ctx, &stage, 0);
assert_eq!(
layout.n_anticipated_fishing_rows, 2,
"expected n_anticipated_fishing_rows == 2 at stage 0, got {}",
layout.n_anticipated_fishing_rows
);
let mut row_lower = vec![f64::NAN; layout.num_rows];
let mut row_upper = vec![f64::NAN; layout.num_rows];
fill_anticipated_fishing_rows(&ctx, &stage, 0, &layout, &mut row_lower, &mut row_upper);
for local_idx in 0..layout.n_anticipated_fishing_rows {
let row = layout.row_anticipated_fishing_start + local_idx;
assert_eq!(
row_lower[row], 0.0,
"row_lower[{row}] (local_idx={local_idx}) expected 0.0, got {}",
row_lower[row]
);
assert_eq!(
row_upper[row], 0.0,
"row_upper[{row}] (local_idx={local_idx}) expected 0.0, got {}",
row_upper[row]
);
}
let mut col_entries: Vec<Vec<(usize, f64)>> = vec![Vec::new(); layout.num_cols];
fill_anticipated_fishing_entries(&ctx, &stage, 0, &layout, &mut col_entries);
let block_hours_total: f64 = stage.blocks.iter().map(|b| b.duration_hours).sum();
let expected_neg = -block_hours_total;
for local_idx in 0..layout.n_anticipated_fishing_rows {
let row = layout.row_anticipated_fishing_start + local_idx;
let col_state = layout.col_anticipated_state_start + local_idx;
let state_couplings: Vec<&(usize, f64)> = col_entries[col_state]
.iter()
.filter(|(r, _)| *r == row)
.collect();
assert_eq!(
state_couplings.len(),
1,
"anticipated_state col {col_state} must carry exactly 1 coupling \
at fishing row {row} (plant local_idx={local_idx})"
);
let (_, coeff) = state_couplings[0];
assert!(
(coeff - expected_neg).abs() < 1e-12,
"anticipated_state col {col_state} fishing-row coupling: \
expected {expected_neg}, got {coeff} (plant local_idx={local_idx})"
);
}
}
fn build_anticipated_ctx_n_stages_6() -> (AntFixtures, Stage) {
let mut fixtures = AntFixtures::new();
fixtures.bounds = ResolvedBounds::new(
&BoundsCountsSpec {
n_hydros: 0,
n_thermals: 0,
n_lines: 0,
n_pumping: 0,
n_contracts: 0,
n_stages: 6,
k_max: 3,
},
&BoundsDefaults {
hydro: HydroStageBounds {
min_storage_hm3: 0.0,
max_storage_hm3: 0.0,
min_turbined_m3s: 0.0,
max_turbined_m3s: 0.0,
min_outflow_m3s: 0.0,
max_outflow_m3s: None,
min_generation_mw: 0.0,
max_generation_mw: 0.0,
max_diversion_m3s: None,
filling_inflow_m3s: 0.0,
water_withdrawal_m3s: 0.0,
},
thermal: ThermalStageBounds {
min_generation_mw: 0.0,
max_generation_mw: 0.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 = two_block_stage(0);
(fixtures, stage)
}
#[test]
fn test_fill_anticipated_state_out_columns_active_and_inactive() {
let (fixtures, _) = build_anticipated_ctx_n_stages_6();
let ctx = fixtures.make_ctx(
2, 3, vec![2, 3], vec![0, 1], 0, );
let stage0 = two_block_stage(0);
let layout0 = StageLayout::new(&ctx, &stage0, 0);
let mut col_lower = vec![0.0_f64; layout0.num_cols];
let mut col_upper = vec![f64::INFINITY; layout0.num_cols];
let mut objective = vec![0.0_f64; layout0.num_cols];
let mut bufs = ColumnBufs {
col_lower: &mut col_lower,
col_upper: &mut col_upper,
objective: &mut objective,
};
fill_anticipated_state_out_columns(&ctx, 0, &layout0, &mut bufs);
for i in 0..2 {
let col = layout0.col_anticipated_state_out_start + i;
assert_eq!(
col_lower[col],
f64::NEG_INFINITY,
"stage 0, plant {i}: col_lower expected -INF, got {}",
col_lower[col]
);
assert_eq!(
col_upper[col],
f64::INFINITY,
"stage 0, plant {i}: col_upper expected +INF, got {}",
col_upper[col]
);
}
let stage5 = two_block_stage(5);
let layout5 = StageLayout::new(&ctx, &stage5, 5);
let mut col_lower5 = vec![0.0_f64; layout5.num_cols];
let mut col_upper5 = vec![f64::INFINITY; layout5.num_cols];
let mut objective5 = vec![0.0_f64; layout5.num_cols];
let mut bufs5 = ColumnBufs {
col_lower: &mut col_lower5,
col_upper: &mut col_upper5,
objective: &mut objective5,
};
fill_anticipated_state_out_columns(&ctx, 5, &layout5, &mut bufs5);
assert_eq!(
layout5.n_anticipated_state_out_def_rows, 0,
"stage 5 inactive: expected no def rows, got {}",
layout5.n_anticipated_state_out_def_rows,
);
for i in 0..2 {
let col = layout5.col_anticipated_state_out_start + i;
assert_eq!(
col_lower5[col], 0.0,
"stage 5, plant {i}: col_lower expected 0.0, got {}",
col_lower5[col]
);
assert_eq!(
col_upper5[col], 0.0,
"stage 5, plant {i}: col_upper expected 0.0, got {}",
col_upper5[col]
);
}
}
#[test]
fn test_fill_anticipated_state_out_def_rows_two_active_plants() {
let (fixtures, stage) = build_anticipated_ctx_n_stages_6();
let ctx = fixtures.make_ctx(2, 3, vec![2, 3], vec![0, 1], 0);
let layout = StageLayout::new(&ctx, &stage, 0);
assert_eq!(
layout.n_anticipated_state_out_def_rows, 2,
"expected n_anticipated_state_out_def_rows == 2, got {}",
layout.n_anticipated_state_out_def_rows
);
let mut row_lower = vec![f64::NEG_INFINITY; layout.num_rows];
let mut row_upper = vec![f64::INFINITY; layout.num_rows];
fill_anticipated_state_out_def_rows(&ctx, 0, &layout, &mut row_lower, &mut row_upper);
for k in 0..2 {
let row = layout.row_anticipated_state_out_def_start + k;
assert_eq!(
row_lower[row], 0.0,
"def row {k}: row_lower expected 0.0, got {}",
row_lower[row]
);
assert_eq!(
row_upper[row], 0.0,
"def row {k}: row_upper expected 0.0, got {}",
row_upper[row]
);
}
}
#[test]
fn test_fill_anticipated_state_out_def_entries_two_active_plants() {
let (fixtures, stage) = build_anticipated_ctx_n_stages_6();
let ctx = fixtures.make_ctx(2, 3, vec![2, 3], vec![0, 1], 0);
let layout = StageLayout::new(&ctx, &stage, 0);
let mut col_entries: Vec<Vec<(usize, f64)>> = vec![Vec::new(); layout.num_cols];
fill_anticipated_state_out_def_entries(&ctx, 0, &layout, &mut col_entries);
for k in 0..2 {
let row = layout.row_anticipated_state_out_def_start + k;
let col_state_out = layout.col_anticipated_state_out_start + k;
let col_decision = layout.col_anticipated_decision_start + k;
assert!(
col_entries[col_state_out]
.iter()
.any(|&(r, v)| r == row && (v - 1.0).abs() < 1e-15),
"plant {k}: expected (+1.0) entry at (col_state_out={col_state_out}, row={row}), \
got {:?}",
col_entries[col_state_out]
);
assert!(
col_entries[col_decision]
.iter()
.any(|&(r, v)| r == row && (v + 1.0).abs() < 1e-15),
"plant {k}: expected (-1.0) entry at (col_decision={col_decision}, row={row}), \
got {:?}",
col_entries[col_decision]
);
}
}
#[test]
fn state_fixing_diagonals_absent_from_csc() {
let (fixtures, stage) = build_anticipated_ctx_n_stages_6();
let ctx = fixtures.make_ctx(
2, 3, vec![2, 3], vec![0, 1], 0, );
let layout = StageLayout::new(&ctx, &stage, 0);
let col_entries = build_stage_matrix_entries(&ctx, &stage, 0, &layout);
let a = ctx.n_anticipated;
let k = ctx.k_max;
for slot in 0..k {
for plant in 0..a {
let col = layout.col_anticipated_state_start + slot * a + plant;
let diag_row = slot * a + plant;
let has_diag = col_entries[col]
.iter()
.any(|&(r, v)| r == diag_row && (v - 1.0).abs() < 1e-15);
assert!(
!has_diag,
"anticipated-state-fixing diagonal (row={diag_row}, val=1.0) must be absent \
from col {col} (slot={slot}, plant={plant})"
);
}
}
let n_h = ctx.n_hydros;
let lag_order = ctx.max_par_order;
for h in 0..n_h {
let col = layout.col_storage_in_start + h;
let has_diag = col_entries[col]
.iter()
.any(|&(r, v)| r == h && (v - 1.0).abs() < 1e-15);
assert!(
!has_diag,
"storage-fixing diagonal (row={h}, val=1.0) must be absent from col {col}"
);
}
for lag in 0..lag_order {
for h in 0..n_h {
let col = layout.col_inflow_lags_start + lag * n_h + h;
let diag_row = n_h + lag * n_h + h;
let has_diag = col_entries[col]
.iter()
.any(|&(r, v)| r == diag_row && (v - 1.0).abs() < 1e-15);
assert!(
!has_diag,
"lag-fixing diagonal (row={diag_row}, val=1.0) must be absent from col {col} \
(lag={lag}, h={h})"
);
}
}
}
}