use cobre_core::entities::hydro::HydroGenerationModel;
use cobre_core::{ConstraintSense, Stage};
use crate::generic_constraints::resolve_variable_ref;
use crate::hydro_models::{EvaporationModel, ResolvedProductionModel};
use crate::indexer::StageIndexer;
use super::layout::{StageLayout, TemplateBuildCtx};
use super::{M3S_TO_HM3, Q_EV_SAFETY_MARGIN};
#[allow(clippy::too_many_lines)]
pub(super) fn fill_stage_columns(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let idx = StageIndexer::new(ctx.n_hydros, ctx.max_par_order);
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];
for (h_idx, _hydro) in ctx.hydros.iter().enumerate() {
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
col_lower[h_idx] = hb.min_storage_hm3;
col_upper[h_idx] = hb.max_storage_hm3;
col_lower[idx.storage_in.start + h_idx] = f64::NEG_INFINITY;
col_upper[idx.storage_in.start + h_idx] = f64::INFINITY;
}
for lag_col in idx.inflow_lags.clone() {
col_lower[lag_col] = f64::NEG_INFINITY;
col_upper[lag_col] = f64::INFINITY;
}
col_lower[idx.theta] = 0.0;
col_upper[idx.theta] = f64::INFINITY;
objective[idx.theta] = 1.0;
for (h_idx, _hydro) in ctx.hydros.iter().enumerate() {
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 is_fpha = matches!(model, ResolvedProductionModel::Fpha { .. });
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;
col_lower[col] = 0.0;
col_upper[col] = turb_upper;
if is_fpha {
let block_hours = stage.blocks[blk].duration_hours;
objective[col] = hp.fpha_turbined_cost * block_hours;
}
}
}
for (h_idx, _hydro) in ctx.hydros.iter().enumerate() {
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;
col_upper[col] = f64::INFINITY;
let block_hours = stage.blocks[blk].duration_hours;
objective[col] = hp.spillage_cost * block_hours;
}
}
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;
col_lower[col] = 0.0;
col_upper[col] = max_div;
if max_div > 0.0 {
let block_hours = stage.blocks[blk].duration_hours;
objective[col] = hp.diversion_cost * block_hours;
}
}
}
for (t_idx, thermal) in ctx.thermals.iter().enumerate() {
let tb = ctx.bounds.thermal_bounds(t_idx, stage_idx);
let marginal_cost_per_mwh = thermal
.cost_segments
.first()
.map_or(0.0, |seg| seg.cost_per_mwh);
for blk in 0..layout.n_blks {
let col = layout.col_thermal_start + t_idx * layout.n_blks + blk;
col_lower[col] = tb.min_generation_mw;
col_upper[col] = tb.max_generation_mw;
let block_hours = stage.blocks[blk].duration_hours;
objective[col] = marginal_cost_per_mwh * block_hours;
}
}
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;
col_upper[col_fwd] = lb.direct_mw * df;
col_upper[col_rev] = lb.reverse_mw * rf;
let block_hours = stage.blocks[blk].duration_hours;
objective[col_fwd] = lp.exchange_cost * block_hours;
objective[col_rev] = lp.exchange_cost * block_hours;
let _ = line;
}
}
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;
col_upper[col_def] = segment.depth_mw.unwrap_or(f64::INFINITY);
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;
col_upper[col_exc] = f64::INFINITY;
objective[col_exc] = bp.excess_cost * block_hours;
}
}
if ctx.has_penalty {
let total_stage_hours: f64 = stage.blocks.iter().map(|b| b.duration_hours).sum();
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);
objective[col] = hp.inflow_nonnegativity_cost * total_stage_hours;
}
}
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;
col_lower[col] = 0.0;
col_upper[col] = hb.max_generation_mw;
}
}
let total_stage_hours: f64 = stage.blocks.iter().map(|b| b.duration_hours).sum();
for (local_idx, &h_idx) in layout.evap_hydro_indices.iter().enumerate() {
let col_q_ev = 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;
col_lower[col_q_ev] = 0.0;
if let EvaporationModel::Linearized { coefficients, .. } =
ctx.evaporation_models.model(h_idx)
{
let coeff = &coefficients[stage_idx];
let hb = ctx.bounds.hydro_bounds(h_idx, stage_idx);
let q_ev_max = (coeff.k_evap0 + coeff.k_evap_v * hb.max_storage_hm3).max(0.0);
col_upper[col_q_ev] = q_ev_max * Q_EV_SAFETY_MARGIN;
} else {
col_upper[col_q_ev] = 0.0;
}
col_lower[col_f_plus] = 0.0;
col_upper[col_f_plus] = f64::INFINITY;
col_lower[col_f_minus] = 0.0;
col_upper[col_f_minus] = f64::INFINITY;
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
objective[col_f_plus] = hp.evaporation_violation_neg_cost * total_stage_hours;
objective[col_f_minus] = hp.evaporation_violation_pos_cost * total_stage_hours;
}
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);
if hb.water_withdrawal_m3s > 0.0 {
col_upper[col] = f64::INFINITY;
} else {
col_upper[col] = 0.0;
}
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
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);
if hb.water_withdrawal_m3s > 0.0 {
col_upper[col] = f64::INFINITY;
} else {
col_upper[col] = 0.0;
}
let hp = ctx.penalties.hydro_penalties(h_idx, stage_idx);
objective[col] = hp.water_withdrawal_violation_pos_cost * total_stage_hours;
}
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;
if hb.min_outflow_m3s > 0.0 {
col_upper[col] = f64::INFINITY;
} else {
col_upper[col] = 0.0;
}
let block_hours = stage.blocks[blk].duration_hours;
objective[col] = hp.outflow_violation_below_cost * block_hours;
}
}
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;
if hb.max_outflow_m3s.is_some() {
col_upper[col] = f64::INFINITY;
} else {
col_upper[col] = 0.0;
}
let block_hours = stage.blocks[blk].duration_hours;
objective[col] = hp.outflow_violation_above_cost * block_hours;
}
}
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;
if hb.min_turbined_m3s > 0.0 {
col_upper[col] = f64::INFINITY;
} else {
col_upper[col] = 0.0;
}
let block_hours = stage.blocks[blk].duration_hours;
objective[col] = hp.turbined_violation_below_cost * block_hours;
}
}
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;
if hb.min_generation_mw > 0.0 {
col_upper[col] = f64::INFINITY;
} else {
col_upper[col] = 0.0;
}
let block_hours = stage.blocks[blk].duration_hours;
objective[col] = hp.generation_violation_below_cost * block_hours;
}
}
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);
col_upper[col] = avail_gen * factor;
let block_hours = stage.blocks[blk].duration_hours;
objective[col] = -ncs.curtailment_cost * block_hours;
}
}
for h_idx in 0..layout.n_h {
let col = layout.col_z_inflow_start + h_idx;
col_lower[col] = f64::NEG_INFINITY;
col_upper[col] = f64::INFINITY;
}
(col_lower, col_upper, objective)
}
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;
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 = layout.row_fpha_start
+ local_idx * n_blks * n_planes
+ blk * n_planes
+ p_idx;
row_lower[row] = f64::NEG_INFINITY;
row_upper[row] = plane.intercept;
}
}
}
}
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 k_evap0 = coefficients[stage_idx].k_evap0;
let row = layout.row_evap_start + local_idx;
row_lower[row] = k_evap0;
row_upper[row] = k_evap0;
}
}
fill_operational_violation_rows(
ctx,
stage,
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_state_and_water_entries(
ctx: &TemplateBuildCtx<'_>,
stage: &Stage,
stage_idx: usize,
layout: &StageLayout,
col_entries: &mut [Vec<(usize, f64)>],
) {
let idx = StageIndexer::new(ctx.n_hydros, ctx.max_par_order);
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;
for h in 0..n_h {
let col = idx.storage_in.start + h;
col_entries[col].push((h, 1.0));
}
for lag in 0..lag_order {
for h in 0..n_h {
let col = idx.inflow_lags.start + lag * n_h + h;
let row = n_h + lag * n_h + h;
col_entries[col].push((row, 1.0));
}
}
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[idx.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 = idx.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_q_ev = layout.col_evap_start + local_idx * 3;
let row = row_water + h_idx;
col_entries[col_q_ev].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 idx = StageIndexer::new(ctx.n_hydros, ctx.max_par_order);
let n_blks = layout.n_blks;
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 = idx.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 =
layout.row_fpha_start + local_idx * n_blks * n_planes + 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));
}
}
}
}
pub(super) fn fill_evaporation_entries(
ctx: &TemplateBuildCtx<'_>,
stage_idx: usize,
layout: &StageLayout,
col_entries: &mut [Vec<(usize, f64)>],
) {
let idx = StageIndexer::new(ctx.n_hydros, ctx.max_par_order);
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_q_ev = 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 = idx.storage_in.start + h_idx;
let row = layout.row_evap_start + local_idx;
col_entries[col_q_ev].push((row, 1.0));
col_entries[col_v].push((row, -coeff.k_evap_v / 2.0));
col_entries[col_v_in].push((row, -coeff.k_evap_v / 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 indexer = crate::indexer::StageIndexer::with_equipment_and_evaporation(
&crate::indexer::EquipmentCounts {
hydro_count: ctx.n_hydros,
max_par_order: ctx.max_par_order,
n_thermals: ctx.n_thermals,
n_lines: ctx.n_lines,
n_buses: ctx.n_buses,
n_blks: layout.n_blks,
has_inflow_penalty: ctx.has_penalty,
max_deficit_segments: layout.max_deficit_segments,
},
&crate::indexer::FphaColumnLayout {
hydro_indices: layout.fpha_hydro_indices.clone(),
planes_per_hydro: layout.fpha_planes_per_hydro.clone(),
},
&crate::indexer::EvapConfig {
hydro_indices: layout.evap_hydro_indices.clone(),
},
);
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 = 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,
&indexer,
ctx.production_models,
&positions,
);
for (col, multiplier) in pairs {
col_entries[col].push((row, term.coefficient * 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 idx = StageIndexer::new(n_h, lag_order);
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 = idx.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.hydros[h_idx].generation_model {
HydroGenerationModel::ConstantProductivity {
productivity_mw_per_m3s,
}
| HydroGenerationModel::LinearizedHead {
productivity_mw_per_m3s,
} => *productivity_mw_per_m3s,
HydroGenerationModel::Fpha => {
if let ResolvedProductionModel::ConstantProductivity { productivity } =
ctx.production_models.model(h_idx, stage_idx)
{
*productivity
} else {
debug_assert!(
false,
"Fpha entity model with non-Fpha resolved model and no local index for hydro {h_idx}"
);
0.0
}
}
};
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_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);
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)
}