use std::ops::Range;
use cobre_solver::StageTemplate;
#[derive(Debug, Clone)]
pub struct StageIndexer {
pub storage: Range<usize>,
pub inflow_lags: Range<usize>,
pub storage_in: Range<usize>,
pub theta: usize,
pub n_state: usize,
pub storage_fixing: Range<usize>,
pub lag_fixing: Range<usize>,
pub hydro_count: usize,
pub max_par_order: usize,
pub turbine: Range<usize>,
pub spillage: Range<usize>,
pub thermal: Range<usize>,
pub line_fwd: Range<usize>,
pub line_rev: Range<usize>,
pub deficit: Range<usize>,
pub excess: Range<usize>,
pub n_blks: usize,
pub n_thermals: usize,
pub n_lines: usize,
pub n_buses: usize,
pub load_balance: Range<usize>,
pub inflow_slack: Range<usize>,
pub inflow_slack_rows: Range<usize>,
pub has_inflow_penalty: bool,
}
impl StageIndexer {
#[must_use]
pub fn new(hydro_count: usize, max_par_order: usize) -> Self {
let n = hydro_count;
let l = max_par_order;
let storage = 0..n;
let inflow_lags = n..n * (1 + l);
let storage_in = n * (1 + l)..n * (2 + l);
let theta = n * (2 + l);
let n_state = n * (1 + l);
let storage_fixing = 0..n;
let lag_fixing = n..n * (1 + l);
Self {
storage,
inflow_lags,
storage_in,
theta,
n_state,
storage_fixing,
lag_fixing,
hydro_count,
max_par_order,
turbine: 0..0,
spillage: 0..0,
thermal: 0..0,
line_fwd: 0..0,
line_rev: 0..0,
deficit: 0..0,
excess: 0..0,
n_blks: 0,
n_thermals: 0,
n_lines: 0,
n_buses: 0,
load_balance: 0..0,
inflow_slack: 0..0,
inflow_slack_rows: 0..0,
has_inflow_penalty: false,
}
}
#[must_use]
pub fn with_equipment(
hydro_count: usize,
max_par_order: usize,
n_thermals: usize,
n_lines: usize,
n_buses: usize,
n_blks: usize,
has_inflow_penalty: bool,
) -> Self {
let base = Self::new(hydro_count, max_par_order);
let decision_start = base.theta + 1;
let turbine_start = decision_start;
let spillage_start = turbine_start + hydro_count * n_blks;
let thermal_start = spillage_start + hydro_count * n_blks;
let line_fwd_start = thermal_start + n_thermals * n_blks;
let line_rev_start = line_fwd_start + n_lines * n_blks;
let deficit_start = line_rev_start + n_lines * n_blks;
let excess_start = deficit_start + n_buses * n_blks;
let excess_end = excess_start + n_buses * n_blks;
let (inflow_slack, active_penalty) = if has_inflow_penalty && hydro_count > 0 {
(excess_end..excess_end + hydro_count, true)
} else {
(0..0, false)
};
let load_balance_start = base.n_state + hydro_count;
let load_balance_end = load_balance_start + n_buses * n_blks;
Self {
turbine: turbine_start..spillage_start,
spillage: spillage_start..thermal_start,
thermal: thermal_start..line_fwd_start,
line_fwd: line_fwd_start..line_rev_start,
line_rev: line_rev_start..deficit_start,
deficit: deficit_start..excess_start,
excess: excess_start..excess_end,
n_blks,
n_thermals,
n_lines,
n_buses,
load_balance: load_balance_start..load_balance_end,
inflow_slack,
inflow_slack_rows: 0..0,
has_inflow_penalty: active_penalty,
..base
}
}
#[must_use]
pub fn from_stage_template(template: &StageTemplate) -> Self {
Self::new(template.n_hydro, template.max_par_order)
}
}
const _: () = {
fn assert_send_sync<T: Send + Sync>() {}
fn check() {
assert_send_sync::<StageIndexer>();
}
let _ = check;
};
#[cfg(test)]
mod tests {
use cobre_solver::StageTemplate;
use super::StageIndexer;
fn indexer_3_2() -> StageIndexer {
StageIndexer::new(3, 2)
}
#[test]
fn storage_range_3_2() {
assert_eq!(indexer_3_2().storage, 0..3);
}
#[test]
fn inflow_lags_range_3_2() {
assert_eq!(indexer_3_2().inflow_lags, 3..9);
}
#[test]
fn storage_in_range_3_2() {
assert_eq!(indexer_3_2().storage_in, 9..12);
}
#[test]
fn theta_index_3_2() {
assert_eq!(indexer_3_2().theta, 12);
}
#[test]
fn n_state_3_2() {
assert_eq!(indexer_3_2().n_state, 9);
}
#[test]
fn storage_fixing_range_3_2() {
assert_eq!(indexer_3_2().storage_fixing, 0..3);
}
#[test]
fn lag_fixing_range_3_2() {
assert_eq!(indexer_3_2().lag_fixing, 3..9);
}
#[test]
fn row_column_symmetry_3_2() {
let idx = indexer_3_2();
assert_eq!(idx.storage_fixing, idx.storage);
assert_eq!(idx.lag_fixing, idx.inflow_lags);
}
fn indexer_160_12() -> StageIndexer {
StageIndexer::new(160, 12)
}
#[test]
fn n_state_production_scale() {
assert_eq!(indexer_160_12().n_state, 2080);
}
#[test]
fn theta_production_scale() {
assert_eq!(indexer_160_12().theta, 2240);
}
#[test]
fn row_column_symmetry_production_scale() {
let idx = indexer_160_12();
assert_eq!(idx.storage_fixing, idx.storage);
assert_eq!(idx.lag_fixing, idx.inflow_lags);
}
#[test]
fn single_hydro_no_lags() {
let idx = StageIndexer::new(1, 0);
assert_eq!(idx.storage, 0..1);
assert_eq!(idx.inflow_lags, 1..1);
assert_eq!(idx.storage_in, 1..2);
assert_eq!(idx.theta, 2);
assert_eq!(idx.n_state, 1);
assert_eq!(idx.storage_fixing, 0..1);
assert_eq!(idx.lag_fixing, 1..1);
assert_eq!(idx.storage_fixing, idx.storage);
assert_eq!(idx.lag_fixing, idx.inflow_lags);
}
#[test]
fn degenerate_zero_hydros() {
let idx = StageIndexer::new(0, 0);
assert_eq!(idx.storage, 0..0);
assert_eq!(idx.inflow_lags, 0..0);
assert_eq!(idx.storage_in, 0..0);
assert_eq!(idx.theta, 0);
assert_eq!(idx.n_state, 0);
assert_eq!(idx.storage_fixing, 0..0);
assert_eq!(idx.lag_fixing, 0..0);
assert_eq!(idx.storage_fixing, idx.storage);
assert_eq!(idx.lag_fixing, idx.inflow_lags);
}
fn make_template(n_hydro: usize, max_par_order: usize) -> StageTemplate {
let n_state = n_hydro * (1 + max_par_order);
let n_transfer = n_hydro * max_par_order;
StageTemplate {
num_cols: 0,
num_rows: 0,
num_nz: 0,
col_starts: vec![0_i32],
row_indices: vec![],
values: vec![],
col_lower: vec![],
col_upper: vec![],
objective: vec![],
row_lower: vec![],
row_upper: vec![],
n_state,
n_transfer,
n_dual_relevant: n_state,
n_hydro,
max_par_order,
}
}
#[test]
fn from_stage_template_matches_new_3_2() {
let tmpl = make_template(3, 2);
let from_tmpl = StageIndexer::from_stage_template(&tmpl);
let from_new = StageIndexer::new(3, 2);
assert_eq!(from_tmpl.storage, from_new.storage);
assert_eq!(from_tmpl.inflow_lags, from_new.inflow_lags);
assert_eq!(from_tmpl.storage_in, from_new.storage_in);
assert_eq!(from_tmpl.theta, from_new.theta);
assert_eq!(from_tmpl.n_state, from_new.n_state);
assert_eq!(from_tmpl.storage_fixing, from_new.storage_fixing);
assert_eq!(from_tmpl.lag_fixing, from_new.lag_fixing);
assert_eq!(from_tmpl.hydro_count, from_new.hydro_count);
assert_eq!(from_tmpl.max_par_order, from_new.max_par_order);
}
#[test]
fn from_stage_template_matches_new_160_12() {
let tmpl = make_template(160, 12);
let from_tmpl = StageIndexer::from_stage_template(&tmpl);
let from_new = StageIndexer::new(160, 12);
assert_eq!(from_tmpl.n_state, from_new.n_state);
assert_eq!(from_tmpl.theta, from_new.theta);
assert_eq!(from_tmpl.hydro_count, from_new.hydro_count);
assert_eq!(from_tmpl.max_par_order, from_new.max_par_order);
}
#[test]
fn from_stage_template_matches_new_edge_cases() {
for (n, l) in [(0, 0), (1, 0), (1, 1)] {
let tmpl = make_template(n, l);
let from_tmpl = StageIndexer::from_stage_template(&tmpl);
let from_new = StageIndexer::new(n, l);
assert_eq!(from_tmpl.storage, from_new.storage, "N={n} L={l}");
assert_eq!(from_tmpl.inflow_lags, from_new.inflow_lags, "N={n} L={l}");
assert_eq!(from_tmpl.theta, from_new.theta, "N={n} L={l}");
assert_eq!(from_tmpl.n_state, from_new.n_state, "N={n} L={l}");
}
}
#[test]
fn clone_and_debug() {
let idx = indexer_3_2();
let cloned = idx.clone();
assert_eq!(cloned.theta, idx.theta);
assert_eq!(cloned.n_state, idx.n_state);
let debug_str = format!("{idx:?}");
assert!(debug_str.contains("StageIndexer"));
}
#[test]
fn new_equipment_ranges_are_empty() {
let idx = StageIndexer::new(3, 2);
assert!(idx.turbine.is_empty());
assert!(idx.spillage.is_empty());
assert!(idx.thermal.is_empty());
assert!(idx.line_fwd.is_empty());
assert!(idx.line_rev.is_empty());
assert!(idx.deficit.is_empty());
assert!(idx.excess.is_empty());
assert_eq!(idx.n_blks, 0);
assert_eq!(idx.n_thermals, 0);
assert_eq!(idx.n_lines, 0);
assert_eq!(idx.n_buses, 0);
}
#[test]
fn with_equipment_doctest_n1_l0_t2_l1_b2_k1() {
let idx = StageIndexer::with_equipment(1, 0, 2, 1, 2, 1, false);
assert_eq!(idx.storage, 0..1);
assert_eq!(idx.inflow_lags, 1..1);
assert_eq!(idx.storage_in, 1..2);
assert_eq!(idx.theta, 2);
assert_eq!(idx.n_state, 1);
assert_eq!(idx.turbine, 3..4);
assert_eq!(idx.spillage, 4..5);
assert_eq!(idx.thermal, 5..7);
assert_eq!(idx.line_fwd, 7..8);
assert_eq!(idx.line_rev, 8..9);
assert_eq!(idx.deficit, 9..11);
assert_eq!(idx.excess, 11..13);
assert_eq!(idx.n_blks, 1);
assert_eq!(idx.n_thermals, 2);
assert_eq!(idx.n_lines, 1);
assert_eq!(idx.n_buses, 2);
}
#[test]
fn with_equipment_n2_l1_t3_l2_b4_k2() {
let idx = StageIndexer::with_equipment(2, 1, 3, 2, 4, 2, false);
assert_eq!(idx.theta, 6);
assert_eq!(idx.n_state, 4);
assert_eq!(idx.turbine, 7..11);
assert_eq!(idx.spillage, 11..15);
assert_eq!(idx.thermal, 15..21);
assert_eq!(idx.line_fwd, 21..25);
assert_eq!(idx.line_rev, 25..29);
assert_eq!(idx.deficit, 29..37);
assert_eq!(idx.excess, 37..45);
}
#[test]
fn with_equipment_all_counts_zero_matches_new() {
let with_eq = StageIndexer::with_equipment(3, 2, 0, 0, 0, 0, false);
let base = StageIndexer::new(3, 2);
assert_eq!(with_eq.storage, base.storage);
assert_eq!(with_eq.inflow_lags, base.inflow_lags);
assert_eq!(with_eq.storage_in, base.storage_in);
assert_eq!(with_eq.theta, base.theta);
assert_eq!(with_eq.n_state, base.n_state);
assert!(with_eq.turbine.is_empty());
assert!(with_eq.spillage.is_empty());
assert!(with_eq.thermal.is_empty());
assert!(with_eq.line_fwd.is_empty());
assert!(with_eq.line_rev.is_empty());
assert!(with_eq.deficit.is_empty());
assert!(with_eq.excess.is_empty());
}
#[test]
fn with_equipment_ranges_are_contiguous() {
let idx = StageIndexer::with_equipment(2, 1, 3, 2, 4, 2, false);
assert_eq!(idx.turbine.start, idx.theta + 1);
assert_eq!(idx.spillage.start, idx.turbine.end);
assert_eq!(idx.thermal.start, idx.spillage.end);
assert_eq!(idx.line_fwd.start, idx.thermal.end);
assert_eq!(idx.line_rev.start, idx.line_fwd.end);
assert_eq!(idx.deficit.start, idx.line_rev.end);
assert_eq!(idx.excess.start, idx.deficit.end);
}
#[test]
fn with_equipment_column_index_formulas() {
let n_blks = 3_usize;
let idx = StageIndexer::with_equipment(2, 1, 1, 1, 1, n_blks, false);
assert_eq!(idx.turbine.start, idx.turbine.start);
assert_eq!(idx.turbine.start + n_blks + 2, idx.turbine.start + 5);
assert_eq!(idx.deficit.start + 1, idx.deficit.start + 1);
assert_eq!(idx.turbine.start + n_blks, idx.turbine.start + 3);
}
#[test]
fn with_equipment_inflow_penalty_appends_slack() {
let idx = StageIndexer::with_equipment(2, 1, 1, 1, 1, 1, true);
assert!(idx.has_inflow_penalty, "has_inflow_penalty must be true");
assert_eq!(
idx.inflow_slack.start, idx.excess.end,
"inflow_slack.start must equal excess.end (contiguous)"
);
assert_eq!(
idx.inflow_slack.len(),
idx.hydro_count,
"inflow_slack must contain exactly hydro_count columns"
);
assert_eq!(idx.inflow_slack, 16..18);
assert!(
idx.inflow_slack_rows.is_empty(),
"inflow_slack_rows must remain empty"
);
let no_penalty = StageIndexer::with_equipment(2, 1, 1, 1, 1, 1, false);
assert!(!no_penalty.has_inflow_penalty);
assert!(no_penalty.inflow_slack.is_empty());
}
}