use std::ops::Range;
use cobre_solver::StageTemplate;
#[derive(Debug, Clone, Copy)]
pub struct EvaporationIndices {
pub q_ev_col: usize,
pub f_evap_plus_col: usize,
pub f_evap_minus_col: usize,
pub evap_row: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct FphaRowRange {
pub start: usize,
pub planes_per_block: usize,
}
#[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 diversion: Range<usize>,
pub thermal: Range<usize>,
pub line_fwd: Range<usize>,
pub line_rev: Range<usize>,
pub deficit: Range<usize>,
pub max_deficit_segments: usize,
pub excess: Range<usize>,
pub n_blks: usize,
pub n_thermals: usize,
pub n_lines: usize,
pub n_buses: usize,
pub water_balance: Range<usize>,
pub load_balance: Range<usize>,
pub inflow_slack: Range<usize>,
pub inflow_slack_rows: Range<usize>,
pub has_inflow_penalty: bool,
pub generation: Range<usize>,
pub n_fpha_hydros: usize,
pub fpha_hydro_indices: Vec<usize>,
pub fpha_rows: Vec<FphaRowRange>,
pub n_evap_hydros: usize,
pub evap_hydro_indices: Vec<usize>,
pub evap_indices: Vec<EvaporationIndices>,
pub withdrawal_slack: Range<usize>,
pub has_withdrawal: bool,
pub generic_constraint_rows: Range<usize>,
pub generic_constraint_slack: Range<usize>,
pub n_generic_constraints_active: usize,
pub ncs_generation: Range<usize>,
pub z_inflow: Range<usize>,
pub z_inflow_rows: Range<usize>,
pub z_inflow_row_start: usize,
pub nonzero_state_indices: Vec<usize>,
}
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 z_inflow_start = n * (1 + l);
let z_inflow = z_inflow_start..z_inflow_start + n;
let storage_in = n * (2 + l)..n * (3 + l);
let theta = n * (3 + l);
let n_state = n * (1 + l);
let storage_fixing = 0..n;
let lag_fixing = n..n * (1 + l);
let z_inflow_rows = z_inflow_start..z_inflow_start + n;
let z_inflow_row_start = z_inflow_start;
Self {
storage,
inflow_lags,
storage_in,
theta,
n_state,
storage_fixing,
lag_fixing,
hydro_count,
max_par_order,
turbine: 0..0,
spillage: 0..0,
diversion: 0..0,
thermal: 0..0,
line_fwd: 0..0,
line_rev: 0..0,
deficit: 0..0,
max_deficit_segments: 0,
excess: 0..0,
n_blks: 0,
n_thermals: 0,
n_lines: 0,
n_buses: 0,
water_balance: 0..0,
load_balance: 0..0,
inflow_slack: 0..0,
inflow_slack_rows: 0..0,
has_inflow_penalty: false,
generation: 0..0,
n_fpha_hydros: 0,
fpha_hydro_indices: Vec::new(),
fpha_rows: Vec::new(),
n_evap_hydros: 0,
evap_hydro_indices: Vec::new(),
evap_indices: Vec::new(),
withdrawal_slack: 0..0,
has_withdrawal: false,
generic_constraint_rows: 0..0,
generic_constraint_slack: 0..0,
n_generic_constraints_active: 0,
ncs_generation: 0..0,
z_inflow,
z_inflow_rows,
z_inflow_row_start,
nonzero_state_indices: Vec::new(),
}
}
#[must_use]
#[allow(clippy::too_many_arguments)]
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,
fpha_hydro_indices: Vec<usize>,
fpha_planes_per_hydro: &[usize],
) -> Self {
Self::with_equipment_and_evaporation(
hydro_count,
max_par_order,
n_thermals,
n_lines,
n_buses,
n_blks,
has_inflow_penalty,
fpha_hydro_indices,
fpha_planes_per_hydro,
vec![],
1,
)
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn with_equipment_and_evaporation(
hydro_count: usize,
max_par_order: usize,
n_thermals: usize,
n_lines: usize,
n_buses: usize,
n_blks: usize,
has_inflow_penalty: bool,
fpha_hydro_indices: Vec<usize>,
fpha_planes_per_hydro: &[usize],
evap_hydro_indices: Vec<usize>,
max_deficit_segments: usize,
) -> Self {
debug_assert_eq!(
fpha_hydro_indices.len(),
fpha_planes_per_hydro.len(),
"fpha_hydro_indices and fpha_planes_per_hydro must have equal length"
);
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 diversion_start = spillage_start + hydro_count * n_blks;
let thermal_start = diversion_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 * max_deficit_segments * 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 n_fpha_hydros = fpha_hydro_indices.len();
let generation_start = if active_penalty {
inflow_slack.end
} else {
excess_end
};
let generation_end = generation_start + n_fpha_hydros * n_blks;
let generation = if n_fpha_hydros > 0 {
generation_start..generation_end
} else {
0..0
};
let n_evap_hydros = evap_hydro_indices.len();
let evap_col_start = generation_end;
let mut evap_indices_vec: Vec<EvaporationIndices> = Vec::with_capacity(n_evap_hydros);
let water_balance_start = base.n_state + hydro_count;
let load_balance_start = water_balance_start + hydro_count;
let load_balance_end = load_balance_start + n_buses * n_blks;
let mut fpha_rows: Vec<FphaRowRange> = Vec::with_capacity(n_fpha_hydros);
let mut fpha_row_cursor = load_balance_end;
for &planes in fpha_planes_per_hydro {
fpha_rows.push(FphaRowRange {
start: fpha_row_cursor,
planes_per_block: planes,
});
fpha_row_cursor += planes * n_blks;
}
let evap_row_start = fpha_row_cursor;
for i in 0..n_evap_hydros {
evap_indices_vec.push(EvaporationIndices {
q_ev_col: evap_col_start + i * 3,
f_evap_plus_col: evap_col_start + i * 3 + 1,
f_evap_minus_col: evap_col_start + i * 3 + 2,
evap_row: evap_row_start + i,
});
}
let evap_col_end = evap_col_start + n_evap_hydros * 3;
let (withdrawal_slack, has_withdrawal) = if hydro_count > 0 {
(evap_col_end..evap_col_end + hydro_count, true)
} else {
(0..0, false)
};
Self {
turbine: turbine_start..spillage_start,
spillage: spillage_start..diversion_start,
diversion: diversion_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,
max_deficit_segments,
excess: excess_start..excess_end,
n_blks,
n_thermals,
n_lines,
n_buses,
water_balance: water_balance_start..water_balance_start + hydro_count,
load_balance: load_balance_start..load_balance_end,
inflow_slack,
inflow_slack_rows: 0..0,
has_inflow_penalty: active_penalty,
generation,
n_fpha_hydros,
fpha_hydro_indices,
fpha_rows,
n_evap_hydros,
evap_hydro_indices,
evap_indices: evap_indices_vec,
withdrawal_slack,
has_withdrawal,
..base
}
}
#[must_use]
pub fn evap_indices(&self, local_idx: usize) -> &EvaporationIndices {
debug_assert!(
local_idx < self.n_evap_hydros,
"evap local index {local_idx} out of bounds (n_evap_hydros = {})",
self.n_evap_hydros
);
&self.evap_indices[local_idx]
}
#[must_use]
pub fn from_stage_template(template: &StageTemplate) -> Self {
Self::new(template.n_hydro, template.max_par_order)
}
pub fn set_nonzero_mask(&mut self, ar_orders: &[usize]) {
debug_assert_eq!(
ar_orders.len(),
self.hydro_count,
"ar_orders length {} != hydro_count {}",
ar_orders.len(),
self.hydro_count
);
let mut mask = Vec::with_capacity(self.n_state);
for h in 0..self.hydro_count {
mask.push(h);
}
for lag in 0..self.max_par_order {
for (h, &order) in ar_orders.iter().enumerate() {
debug_assert!(
order <= self.max_par_order,
"ar_orders[{h}] = {order} exceeds max_par_order {}",
self.max_par_order
);
if lag < order {
mask.push(self.inflow_lags.start + lag * self.hydro_count + h);
}
}
}
debug_assert!(
mask.windows(2).all(|w| w[0] < w[1]),
"nonzero_state_indices must be sorted and unique"
);
self.nonzero_state_indices = mask;
}
}
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::{FphaRowRange, 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 z_inflow_range_3_2() {
assert_eq!(indexer_3_2().z_inflow, 9..12);
}
#[test]
fn storage_in_range_3_2() {
assert_eq!(indexer_3_2().storage_in, 12..15);
}
#[test]
fn theta_index_3_2() {
assert_eq!(indexer_3_2().theta, 15);
}
#[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, 2400);
}
#[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.z_inflow, 1..2);
assert_eq!(idx.storage_in, 2..3);
assert_eq!(idx.theta, 3);
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.z_inflow, 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,
col_scale: Vec::new(),
row_scale: Vec::new(),
}
}
#[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.diversion.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, vec![], &[]);
assert_eq!(idx.storage, 0..1);
assert_eq!(idx.inflow_lags, 1..1);
assert_eq!(idx.z_inflow, 1..2);
assert_eq!(idx.storage_in, 2..3);
assert_eq!(idx.theta, 3);
assert_eq!(idx.n_state, 1);
assert_eq!(idx.turbine, 4..5);
assert_eq!(idx.spillage, 5..6);
assert_eq!(idx.diversion, 6..7);
assert_eq!(idx.thermal, 7..9);
assert_eq!(idx.line_fwd, 9..10);
assert_eq!(idx.line_rev, 10..11);
assert_eq!(idx.deficit, 11..13);
assert_eq!(idx.excess, 13..15);
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, vec![], &[]);
assert_eq!(idx.theta, 8);
assert_eq!(idx.n_state, 4);
assert_eq!(idx.turbine, 9..13);
assert_eq!(idx.spillage, 13..17);
assert_eq!(idx.diversion, 17..21);
assert_eq!(idx.thermal, 21..27);
assert_eq!(idx.line_fwd, 27..31);
assert_eq!(idx.line_rev, 31..35);
assert_eq!(idx.deficit, 35..43);
assert_eq!(idx.excess, 43..51);
}
#[test]
fn with_equipment_all_counts_zero_matches_new() {
let with_eq = StageIndexer::with_equipment(3, 2, 0, 0, 0, 0, false, vec![], &[]);
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.diversion.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, vec![], &[]);
assert_eq!(idx.turbine.start, idx.theta + 1);
assert_eq!(idx.spillage.start, idx.turbine.end);
assert_eq!(idx.diversion.start, idx.spillage.end);
assert_eq!(idx.thermal.start, idx.diversion.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, vec![], &[]);
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, vec![], &[]);
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, 20..22);
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, vec![], &[]);
assert!(!no_penalty.has_inflow_penalty);
assert!(no_penalty.inflow_slack.is_empty());
}
#[test]
fn fpha_no_hydros_generation_is_empty() {
let idx = StageIndexer::with_equipment(4, 0, 0, 0, 1, 1, false, vec![], &[]);
assert!(
idx.generation.is_empty(),
"generation must be empty with no FPHA hydros"
);
assert_eq!(idx.n_fpha_hydros, 0);
assert!(idx.fpha_hydro_indices.is_empty());
assert!(idx.fpha_rows.is_empty());
}
#[test]
fn fpha_one_hydro_one_block_three_planes() {
let idx = StageIndexer::with_equipment(2, 0, 1, 0, 1, 1, false, vec![0], &[3]);
assert_eq!(idx.generation.len(), 1, "generation must span 1 column");
assert_eq!(idx.generation, 16..17);
assert_eq!(idx.n_fpha_hydros, 1);
assert_eq!(idx.fpha_hydro_indices, vec![0]);
assert_eq!(idx.fpha_rows.len(), 1);
assert_eq!(
idx.fpha_rows[0].start, idx.load_balance.end,
"fpha_rows[0].start must equal load_balance.end"
);
assert_eq!(idx.fpha_rows[0].planes_per_block, 3);
}
#[test]
fn fpha_two_hydros_two_blocks_different_planes() {
let idx = StageIndexer::with_equipment(4, 0, 0, 0, 1, 2, false, vec![1, 3], &[5, 4]);
assert_eq!(idx.generation.len(), 4, "generation must span 4 columns");
assert_eq!(idx.n_fpha_hydros, 2);
assert_eq!(idx.fpha_hydro_indices, vec![1, 3]);
assert_eq!(idx.fpha_rows.len(), 2);
assert_eq!(
idx.fpha_rows[0].start, idx.load_balance.end,
"fpha_rows[0].start must equal load_balance.end"
);
assert_eq!(idx.fpha_rows[0].planes_per_block, 5);
assert_eq!(
idx.fpha_rows[1].start,
idx.fpha_rows[0].start + 5 * 2,
"fpha_rows[1].start must follow fpha_rows[0]'s 10-row region"
);
assert_eq!(idx.fpha_rows[1].planes_per_block, 4);
}
#[test]
fn fpha_generation_contiguous_with_prior_region() {
let no_penalty = StageIndexer::with_equipment(2, 0, 0, 0, 1, 1, false, vec![0], &[2]);
assert_eq!(
no_penalty.generation.start, no_penalty.excess.end,
"generation.start must equal excess.end when no penalty"
);
let with_penalty = StageIndexer::with_equipment(2, 0, 0, 0, 1, 1, true, vec![0], &[2]);
assert_eq!(
with_penalty.generation.start, with_penalty.inflow_slack.end,
"generation.start must equal inflow_slack.end when penalty active"
);
}
#[test]
fn fpha_rows_contiguous_with_load_balance() {
let idx = StageIndexer::with_equipment(3, 1, 2, 0, 2, 3, false, vec![0, 2], &[4, 6]);
assert_eq!(
idx.fpha_rows[0].start, idx.load_balance.end,
"fpha_rows[0] must start at load_balance.end"
);
assert_eq!(
idx.fpha_rows[1].start,
idx.fpha_rows[0].start + 4 * 3,
"fpha_rows[1] must start after fpha_rows[0]'s rows"
);
assert_eq!(idx.fpha_rows[1].planes_per_block, 6);
}
#[test]
fn evap_no_hydros_indices_empty() {
let idx = StageIndexer::with_equipment(3, 0, 1, 0, 1, 1, false, vec![], &[]);
assert_eq!(idx.n_evap_hydros, 0);
assert!(idx.evap_hydro_indices.is_empty());
assert!(idx.evap_indices.is_empty());
}
#[test]
fn evap_one_hydro_column_row_positions() {
let idx = StageIndexer::with_equipment_and_evaporation(
2,
0,
0,
0,
1,
1,
false,
vec![],
&[],
vec![0],
1,
);
assert_eq!(idx.n_evap_hydros, 1);
assert_eq!(idx.evap_hydro_indices, vec![0]);
assert_eq!(idx.evap_indices.len(), 1);
let ei = idx.evap_indices(0);
assert_eq!(ei.q_ev_col, 15);
assert_eq!(ei.f_evap_plus_col, 16);
assert_eq!(ei.f_evap_minus_col, 17);
assert_eq!(ei.evap_row, 7);
}
#[test]
fn evap_two_hydros_with_fpha_contiguous() {
let idx = StageIndexer::with_equipment_and_evaporation(
4,
0,
0,
0,
1,
1,
false,
vec![0],
&[3],
vec![1, 2],
1,
);
assert_eq!(idx.n_evap_hydros, 2);
assert_eq!(idx.evap_hydro_indices, vec![1, 2]);
let ei0 = idx.evap_indices(0);
let ei1 = idx.evap_indices(1);
assert_eq!(ei0.q_ev_col, 28);
assert_eq!(ei0.f_evap_plus_col, 29);
assert_eq!(ei0.f_evap_minus_col, 30);
assert_eq!(ei1.q_ev_col, 31);
assert_eq!(ei1.f_evap_plus_col, 32);
assert_eq!(ei1.f_evap_minus_col, 33);
assert_eq!(ei0.evap_row, 16);
assert_eq!(ei1.evap_row, 17);
assert!(ei0.evap_row > idx.fpha_rows[0].start);
}
#[test]
fn new_evap_ranges_are_empty() {
let idx = StageIndexer::new(3, 2);
assert_eq!(idx.n_evap_hydros, 0);
assert!(idx.evap_hydro_indices.is_empty());
assert!(idx.evap_indices.is_empty());
}
#[test]
fn withdrawal_slack_with_equipment_and_evaporation_n3_evap1() {
let idx = StageIndexer::with_equipment_and_evaporation(
3,
0,
0,
0,
1,
1,
false,
vec![],
&[],
vec![0],
1,
);
assert!(idx.has_withdrawal);
let evap_col_end = idx.evap_indices(0).f_evap_minus_col + 1;
assert_eq!(
idx.withdrawal_slack.start, evap_col_end,
"withdrawal_slack.start must equal evap_col_end"
);
assert_eq!(
idx.withdrawal_slack.len(),
3,
"withdrawal_slack must contain exactly hydro_count columns"
);
assert_eq!(idx.withdrawal_slack, 24..27);
}
#[test]
fn withdrawal_slack_zero_hydros_is_empty() {
let idx = StageIndexer::with_equipment_and_evaporation(
0,
0,
0,
0,
1,
1,
false,
vec![],
&[],
vec![],
1,
);
assert!(!idx.has_withdrawal);
assert_eq!(idx.withdrawal_slack, 0..0);
}
#[test]
fn withdrawal_slack_from_new_is_empty() {
let idx = StageIndexer::new(3, 2);
assert!(!idx.has_withdrawal);
assert_eq!(idx.withdrawal_slack, 0..0);
}
#[test]
fn withdrawal_slack_length_equals_hydro_count() {
for n in [1_usize, 5] {
let idx = StageIndexer::with_equipment_and_evaporation(
n,
0,
0,
0,
1,
1,
false,
vec![],
&[],
vec![],
1,
);
assert!(idx.has_withdrawal, "has_withdrawal must be true for n={n}");
assert_eq!(
idx.withdrawal_slack.len(),
n,
"withdrawal_slack length must equal hydro_count for n={n}"
);
}
}
#[test]
fn withdrawal_slack_immediately_after_evap_columns() {
let idx = StageIndexer::with_equipment_and_evaporation(
2,
0,
0,
0,
1,
1,
false,
vec![],
&[],
vec![0],
1,
);
assert_eq!(
idx.withdrawal_slack.start, 18,
"withdrawal_slack must start at evap_col_end=18"
);
assert_eq!(
idx.withdrawal_slack.len(),
2,
"withdrawal_slack length must equal hydro_count=2"
);
assert_eq!(idx.withdrawal_slack, 18..20);
}
#[test]
fn evap_indices_debug_clone_copy() {
use super::EvaporationIndices;
let ei = EvaporationIndices {
q_ev_col: 10,
f_evap_plus_col: 11,
f_evap_minus_col: 12,
evap_row: 5,
};
let cloned = ei;
assert_eq!(cloned.q_ev_col, 10);
assert_eq!(cloned.evap_row, 5);
let debug_str = format!("{ei:?}");
assert!(debug_str.contains("EvaporationIndices"));
}
#[test]
fn fpha_row_range_debug_clone_copy() {
let r = FphaRowRange {
start: 42,
planes_per_block: 5,
};
let cloned = r;
assert_eq!(cloned.start, 42);
assert_eq!(cloned.planes_per_block, 5);
let debug_str = format!("{r:?}");
assert!(debug_str.contains("FphaRowRange"));
}
#[test]
fn new_fpha_ranges_are_empty() {
let idx = StageIndexer::new(3, 2);
assert!(idx.generation.is_empty());
assert_eq!(idx.n_fpha_hydros, 0);
assert!(idx.fpha_hydro_indices.is_empty());
assert!(idx.fpha_rows.is_empty());
}
#[test]
fn extended_adjacency_invariant_with_fpha() {
let idx = StageIndexer::with_equipment(2, 1, 1, 1, 1, 1, false, vec![0], &[3]);
assert_eq!(idx.turbine.start, idx.theta + 1);
assert_eq!(idx.spillage.start, idx.turbine.end);
assert_eq!(idx.diversion.start, idx.spillage.end);
assert_eq!(idx.thermal.start, idx.diversion.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);
assert_eq!(idx.generation.start, idx.excess.end);
assert_eq!(idx.generation.len(), 1);
}
#[test]
fn test_diversion_range_n3_l0_k2() {
let idx = StageIndexer::with_equipment(3, 0, 0, 0, 1, 2, false, vec![], &[]);
assert_eq!(idx.diversion.start, idx.spillage.end);
assert_eq!(idx.diversion.len(), 3 * 2);
assert_eq!(idx.thermal.start, idx.diversion.end);
}
#[test]
fn test_diversion_zero_hydros() {
let idx = StageIndexer::with_equipment(0, 0, 1, 0, 1, 1, false, vec![], &[]);
assert!(idx.diversion.is_empty());
}
#[test]
fn z_inflow_range_new_constructor() {
let idx = StageIndexer::new(3, 2);
assert_eq!(idx.z_inflow, 9..12);
assert_eq!(idx.z_inflow.len(), idx.hydro_count);
}
#[test]
fn z_inflow_range_zero_hydros() {
let idx = StageIndexer::new(0, 0);
assert!(idx.z_inflow.is_empty());
assert!(idx.z_inflow_rows.is_empty());
assert_eq!(idx.z_inflow_row_start, 0);
}
#[test]
fn z_inflow_row_fields() {
let idx = StageIndexer::new(5, 1);
assert_eq!(idx.z_inflow_rows, 10..15);
assert_eq!(idx.z_inflow_row_start, 10);
assert_eq!(idx.z_inflow.len(), 5);
}
#[test]
fn z_inflow_range_with_equipment() {
let idx = StageIndexer::with_equipment(2, 1, 1, 1, 1, 1, false, vec![], &[]);
assert_eq!(idx.z_inflow, 4..6);
assert_eq!(idx.z_inflow.len(), 2);
assert_eq!(idx.z_inflow_rows, 4..6);
assert_eq!(idx.z_inflow_row_start, 4);
}
#[test]
fn z_inflow_single_hydro_no_lags() {
let idx = StageIndexer::new(1, 0);
assert_eq!(idx.z_inflow, 1..2);
assert_eq!(idx.z_inflow.len(), 1);
}
#[test]
fn nonzero_mask_default_is_empty() {
let idx = StageIndexer::new(4, 6);
assert!(
idx.nonzero_state_indices.is_empty(),
"default mask must be empty (dense path)"
);
}
#[test]
fn nonzero_mask_mixed_ar_orders() {
let mut idx = StageIndexer::new(4, 6);
idx.set_nonzero_mask(&[0, 1, 3, 6]);
assert_eq!(
idx.nonzero_state_indices.len(),
14,
"mask length: 4 storage + 0 + 1 + 3 + 6 = 14"
);
assert_eq!(&idx.nonzero_state_indices[..4], &[0, 1, 2, 3]);
assert_eq!(
&idx.nonzero_state_indices[4..],
&[5, 6, 7, 10, 11, 14, 15, 19, 23, 27]
);
assert!(idx.nonzero_state_indices.windows(2).all(|w| w[0] < w[1]));
}
#[test]
fn nonzero_mask_zero_par_order() {
let mut idx = StageIndexer::new(3, 0);
idx.set_nonzero_mask(&[0, 0, 0]);
assert_eq!(idx.nonzero_state_indices.len(), 3);
assert_eq!(&idx.nonzero_state_indices, &[0, 1, 2]);
}
#[test]
fn nonzero_mask_all_full_order() {
let mut idx = StageIndexer::new(2, 3);
idx.set_nonzero_mask(&[3, 3]);
assert_eq!(idx.nonzero_state_indices.len(), 8);
assert_eq!(idx.nonzero_state_indices.len(), idx.n_state);
}
}