use cobre_core::Stage;
use cobre_solver::StageTemplate;
use cobre_stochastic::par::precompute::PrecomputedPar;
use super::M3S_TO_HM3;
#[must_use]
#[allow(clippy::cast_sign_loss)] pub(crate) fn compute_col_scale(num_cols: usize, col_starts: &[i32], values: &[f64]) -> Vec<f64> {
let mut scale = vec![1.0_f64; num_cols];
for j in 0..num_cols {
let start = col_starts[j] as usize;
let end = col_starts[j + 1] as usize;
if start == end {
continue;
}
let mut max_abs = 0.0_f64;
let mut min_abs = f64::INFINITY;
for &v in &values[start..end] {
let abs_val = v.abs();
if abs_val > 0.0 {
max_abs = max_abs.max(abs_val);
min_abs = min_abs.min(abs_val);
}
}
if max_abs > 0.0 && min_abs < f64::INFINITY {
let d = 1.0 / (max_abs * min_abs).sqrt();
scale[j] = d;
}
}
scale
}
pub(crate) fn apply_col_scale(template: &mut StageTemplate, col_scale: &[f64]) {
let num_cols = template.num_cols;
debug_assert_eq!(col_scale.len(), num_cols);
#[allow(clippy::needless_range_loop, clippy::cast_sign_loss)]
for j in 0..num_cols {
let start = template.col_starts[j] as usize;
let end = template.col_starts[j + 1] as usize;
let d = col_scale[j];
for v in &mut template.values[start..end] {
*v *= d;
}
}
for (obj, &d) in template.objective.iter_mut().zip(col_scale) {
*obj *= d;
}
for ((lo, hi), &d) in template
.col_lower
.iter_mut()
.zip(template.col_upper.iter_mut())
.zip(col_scale)
{
*lo /= d;
*hi /= d;
}
}
#[must_use]
#[allow(clippy::cast_sign_loss)] pub(crate) fn compute_row_scale(
num_rows: usize,
num_cols: usize,
col_starts: &[i32],
row_indices: &[i32],
values: &[f64],
) -> Vec<f64> {
let mut row_max = vec![0.0_f64; num_rows];
let mut row_min = vec![f64::INFINITY; num_rows];
#[allow(clippy::needless_range_loop)] for j in 0..num_cols {
let start = col_starts[j] as usize;
let end = col_starts[j + 1] as usize;
for k in start..end {
let row = row_indices[k] as usize;
let abs_val = values[k].abs();
if abs_val > 0.0 {
row_max[row] = row_max[row].max(abs_val);
row_min[row] = row_min[row].min(abs_val);
}
}
}
let mut scale = vec![1.0_f64; num_rows];
for (s, (&rmax, &rmin)) in scale.iter_mut().zip(row_max.iter().zip(row_min.iter())) {
if rmax > 0.0 && rmin < f64::INFINITY {
*s = 1.0 / (rmax * rmin).sqrt();
}
}
scale
}
pub(crate) fn apply_row_scale(template: &mut StageTemplate, row_scale: &[f64]) {
let num_rows = template.num_rows;
debug_assert_eq!(row_scale.len(), num_rows);
let num_cols = template.num_cols;
#[allow(clippy::needless_range_loop, clippy::cast_sign_loss)]
for j in 0..num_cols {
let start = template.col_starts[j] as usize;
let end = template.col_starts[j + 1] as usize;
for k in start..end {
let row = template.row_indices[k] as usize;
template.values[k] *= row_scale[row];
}
}
for ((lo, hi), &d) in template
.row_lower
.iter_mut()
.zip(template.row_upper.iter_mut())
.zip(row_scale)
{
*lo *= d;
*hi *= d;
}
}
pub(super) fn compute_noise_scale(
study_stages: &[&Stage],
n_hydros: usize,
par_lp: &PrecomputedPar,
) -> (Vec<f64>, Vec<f64>, Vec<Vec<f64>>) {
let n = study_stages.len();
let mut noise_scale = vec![0.0_f64; n * n_hydros];
let mut zeta_per_stage = Vec::with_capacity(n);
let mut block_hours_per_stage = Vec::with_capacity(n);
for (s_idx, stage) in study_stages.iter().enumerate() {
let total_hours: f64 = stage.blocks.iter().map(|b| b.duration_hours).sum();
let zeta_s = total_hours * M3S_TO_HM3;
zeta_per_stage.push(zeta_s);
block_hours_per_stage.push(stage.blocks.iter().map(|b| b.duration_hours).collect());
for h_idx in 0..n_hydros {
let sigma = if par_lp.n_stages() > 0 && par_lp.n_hydros() == n_hydros {
par_lp.sigma(s_idx, h_idx)
} else {
0.0
};
noise_scale[s_idx * n_hydros + h_idx] = zeta_s * sigma;
}
}
(noise_scale, zeta_per_stage, block_hours_per_stage)
}
#[cfg(test)]
#[allow(
clippy::doc_markdown,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
mod tests {
use cobre_solver::StageTemplate;
fn minimal_template(
num_rows: usize,
num_cols: usize,
col_starts: Vec<i32>,
row_indices: Vec<i32>,
values: Vec<f64>,
row_lower: Vec<f64>,
row_upper: Vec<f64>,
) -> StageTemplate {
let num_nz = values.len();
StageTemplate {
num_cols,
num_rows,
num_nz,
col_starts,
row_indices,
values,
col_lower: vec![0.0; num_cols],
col_upper: vec![f64::INFINITY; num_cols],
objective: vec![0.0; num_cols],
row_lower,
row_upper,
n_state: 0,
n_transfer: 0,
n_dual_relevant: 0,
n_hydro: 0,
max_par_order: 0,
col_scale: Vec::new(),
row_scale: Vec::new(),
}
}
#[test]
fn row_scale_identity_for_uniform_matrix() {
let col_starts = vec![0, 2, 4];
let row_indices = vec![0, 1, 0, 1];
let values = vec![1.0, 1.0, 1.0, 1.0];
let scale = super::compute_row_scale(2, 2, &col_starts, &row_indices, &values);
assert_eq!(scale.len(), 2);
assert!(
(scale[0] - 1.0).abs() < 1e-15,
"row 0 scale should be 1.0, got {}",
scale[0]
);
assert!(
(scale[1] - 1.0).abs() < 1e-15,
"row 1 scale should be 1.0, got {}",
scale[1]
);
}
#[test]
fn row_scale_geometric_mean() {
let col_starts = vec![0, 1, 3];
let row_indices = vec![0, 0, 1];
let values = vec![1.0, 100.0, 4.0];
let scale = super::compute_row_scale(2, 2, &col_starts, &row_indices, &values);
assert_eq!(scale.len(), 2);
let expected_row0 = 1.0_f64 / (1.0_f64 * 100.0_f64).sqrt(); let expected_row1 = 1.0_f64 / (4.0_f64 * 4.0_f64).sqrt(); assert!(
(scale[0] - expected_row0).abs() < 1e-14,
"row 0 scale: expected {expected_row0}, got {}",
scale[0]
);
assert!(
(scale[1] - expected_row1).abs() < 1e-14,
"row 1 scale: expected {expected_row1}, got {}",
scale[1]
);
}
#[test]
fn apply_row_scale_scales_values_and_bounds() {
let col_starts = vec![0_i32, 1, 3];
let row_indices = vec![0_i32, 0, 1];
let values = vec![1.0_f64, 100.0, 4.0];
let row_lower = vec![-5.0_f64, 7.0];
let row_upper = vec![f64::INFINITY, 7.0];
let mut tmpl =
minimal_template(2, 2, col_starts, row_indices, values, row_lower, row_upper);
let row_scale = vec![0.1_f64, 0.25];
super::apply_row_scale(&mut tmpl, &row_scale);
assert!((tmpl.values[0] - 0.1).abs() < 1e-15, "value[0] wrong");
assert!((tmpl.values[1] - 10.0).abs() < 1e-15, "value[1] wrong");
assert!((tmpl.values[2] - 1.0).abs() < 1e-15, "value[2] wrong");
assert!(
(tmpl.row_lower[0] - (-0.5)).abs() < 1e-15,
"row_lower[0] wrong"
);
assert!(
tmpl.row_upper[0].is_infinite() && tmpl.row_upper[0] > 0.0,
"row_upper[0] must remain +inf"
);
assert!(
(tmpl.row_lower[1] - 1.75).abs() < 1e-15,
"row_lower[1] wrong"
);
assert!(
(tmpl.row_upper[1] - 1.75).abs() < 1e-15,
"row_upper[1] wrong"
);
assert_eq!(tmpl.col_lower, vec![0.0; 2]);
assert!(tmpl.col_upper[0].is_infinite());
assert!(tmpl.col_upper[1].is_infinite());
assert_eq!(tmpl.objective, vec![0.0; 2]);
}
#[test]
fn row_scale_empty_row_gets_one() {
let col_starts = vec![0_i32, 1];
let row_indices = vec![1_i32];
let values = vec![8.0_f64];
let scale = super::compute_row_scale(3, 1, &col_starts, &row_indices, &values);
assert_eq!(scale.len(), 3);
assert!(
(scale[0] - 1.0).abs() < 1e-15,
"empty row 0 scale should be 1.0"
);
let expected = 1.0_f64 / 8.0;
assert!(
(scale[1] - expected).abs() < 1e-15,
"row 1 scale: expected {expected}, got {}",
scale[1]
);
assert!(
(scale[2] - 1.0).abs() < 1e-15,
"empty row 2 scale should be 1.0"
);
}
}