use super::{CsrPattern, ProblemSpec};
use sqp_craft::qp::{QpModel, QpSolution};
const MAX_PASSES: usize = 20;
const IMPROVEMENT: f64 = 0.9;
const LIMIT: i32 = 60;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Scaling {
#[default]
None,
Linear,
Jacobian,
}
impl Scaling {
pub fn as_str(self) -> &'static str {
match self {
Scaling::None => "none",
Scaling::Linear => "linear",
Scaling::Jacobian => "jacobian",
}
}
pub fn parse(name: &str) -> sqp_craft::Result<Self> {
match name {
"none" => Ok(Scaling::None),
"linear" => Ok(Scaling::Linear),
"jacobian" => Ok(Scaling::Jacobian),
_ => Err(sqp_craft::Error::InvalidInput(format!(
"scaling must be \"none\", \"linear\" or \"jacobian\", got {name:?}"
))),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct Scales {
pub variables: Vec<f64>,
pub rows: Vec<f64>,
}
fn power_of_two(v: f64) -> f64 {
if !(v.is_finite() && v > 0.0) {
return 1.0;
}
let exponent = (v.log2().round() as i32).clamp(-LIMIT, LIMIT);
2.0_f64.powi(exponent)
}
impl Scales {
pub fn identity(n: usize, m: usize) -> Self {
Self {
variables: vec![1.0; n],
rows: vec![1.0; m],
}
}
pub fn is_identity(&self) -> bool {
self.variables.iter().chain(&self.rows).all(|&v| v == 1.0)
}
pub fn geometric(pattern: &CsrPattern, values: &[f64], selected: &[bool]) -> Self {
let (m, n) = (pattern.rows, pattern.cols);
let mut rows = vec![1.0_f64; m];
let mut columns = vec![1.0_f64; n];
let entries: Vec<(usize, usize, f64)> = (0..m)
.filter(|&i| selected[i])
.flat_map(|i| {
(pattern.indptr[i]..pattern.indptr[i + 1])
.map(move |a| (i, pattern.indices[a], values[a].abs()))
})
.filter(|&(_, _, v)| v.is_finite() && v > 0.0)
.collect();
if entries.is_empty() {
return Self::identity(n, m);
}
let extremes = |rows: &[f64], columns: &[f64], by_row: bool, len: usize| {
let mut low = vec![f64::INFINITY; len];
let mut high = vec![0.0_f64; len];
for &(i, j, v) in &entries {
let scaled = rows[i] * v * columns[j];
let k = if by_row { i } else { j };
low[k] = low[k].min(scaled);
high[k] = high[k].max(scaled);
}
(low, high)
};
let ratio = |rows: &[f64], columns: &[f64]| {
let (low, high) = extremes(rows, columns, false, n);
low.iter()
.zip(&high)
.filter(|&(&l, _)| l.is_finite())
.fold(1.0_f64, |r, (&l, &h)| r.max(h / l))
};
let mut previous = ratio(&rows, &columns);
for _ in 0..MAX_PASSES {
let (low, high) = extremes(&rows, &columns, true, m);
for i in 0..m {
if low[i].is_finite() {
rows[i] /= (low[i] * high[i]).sqrt();
}
}
let (low, high) = extremes(&rows, &columns, false, n);
for j in 0..n {
if low[j].is_finite() {
columns[j] /= (low[j] * high[j]).sqrt();
}
}
let current = ratio(&rows, &columns);
if current > IMPROVEMENT * previous {
break;
}
previous = current;
}
Self {
variables: columns.into_iter().map(power_of_two).collect(),
rows: rows.into_iter().map(power_of_two).collect(),
}
}
pub fn scale_qp(&self, q: &mut QpModel, row_factors: &[f64]) {
let d = &self.variables;
for (g, &dj) in q.gradient.iter_mut().zip(d) {
*g *= dj;
}
let pattern = &q.matrix.pattern;
for (i, &factor) in row_factors.iter().enumerate().take(pattern.rows) {
for a in pattern.indptr[i]..pattern.indptr[i + 1] {
q.matrix.values[a] *= factor * d[pattern.indices[a]];
}
}
for (j, &dj) in d.iter().enumerate() {
for v in [&mut q.variables.lower[j], &mut q.variables.upper[j]] {
if v.is_finite() {
*v /= dj;
}
}
}
for (i, &factor) in row_factors.iter().enumerate().take(q.rows.len()) {
for v in [&mut q.rows.lower[i], &mut q.rows.upper[i]] {
if v.is_finite() {
*v *= factor;
}
}
}
}
pub fn unscale_solution(&self, s: &mut QpSolution, row_factors: &[f64]) {
for (p, &dj) in s.step.iter_mut().zip(&self.variables) {
*p *= dj;
}
for (l, &factor) in s.multipliers.constraints.iter_mut().zip(row_factors) {
*l *= factor;
}
for (z, &dj) in s.multipliers.variables.iter_mut().zip(&self.variables) {
*z /= dj;
}
}
pub fn to_scaled(&self, v: &[f64]) -> Vec<f64> {
v.iter()
.zip(&self.variables)
.map(|(&a, &d)| a / d)
.collect()
}
pub fn smallest_row(&self) -> f64 {
self.rows.iter().copied().reduce(f64::min).unwrap_or(1.0)
}
pub fn largest_row(&self) -> f64 {
self.rows.iter().copied().reduce(f64::max).unwrap_or(1.0)
}
pub fn of(mode: Scaling, spec: &ProblemSpec, values: &[f64]) -> Self {
match mode {
Scaling::None => Self::identity(spec.n(), spec.m()),
Scaling::Linear => Self::geometric(spec.jacobian(), values, spec.linear_rows()),
Scaling::Jacobian => Self::geometric(spec.jacobian(), values, &vec![true; spec.m()]),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn factors_are_powers_of_two_and_equalize_a_badly_scaled_matrix() {
let pattern = CsrPattern::dense(2, 2);
let values = [1e4, 2e4, 3e-3, 1e-3];
let s = Scales::geometric(&pattern, &values, &[true, true]);
for v in s.variables.iter().chain(&s.rows) {
assert_eq!(v.log2().fract(), 0.0, "{v}");
}
let scaled: Vec<f64> = (0..4)
.map(|k| values[k] * s.rows[k / 2] * s.variables[k % 2])
.collect();
let (low, high) = scaled
.iter()
.fold((f64::INFINITY, 0.0_f64), |(l, h), &v| (l.min(v), h.max(v)));
assert!(high / low < 16.0, "{scaled:?}");
}
#[test]
fn unselected_rows_and_empty_columns_keep_the_factor_one() {
let pattern = CsrPattern {
rows: 2,
cols: 3,
indptr: vec![0, 1, 3],
indices: vec![0, 1, 2],
};
let s = Scales::geometric(&pattern, &[1e6, 5.0, 7.0], &[true, false]);
assert_eq!(s.rows[1], 1.0);
assert_eq!(&s.variables[1..], &[1.0, 1.0]);
assert!((s.rows[0] * 1e6 * s.variables[0]).log2().abs() <= 1.0);
assert!(Scales::geometric(&pattern, &[1e6, 5.0, 7.0], &[false, false]).is_identity());
assert!(Scales::geometric(&pattern, &[0.0, 5.0, 7.0], &[true, false]).is_identity());
}
#[test]
fn scaling_names_round_trip() {
for mode in [Scaling::None, Scaling::Linear, Scaling::Jacobian] {
assert_eq!(Scaling::parse(mode.as_str()).unwrap(), mode);
}
assert!(Scaling::parse("auto").is_err());
assert_eq!(power_of_two(3.0), 4.0);
assert_eq!(power_of_two(f64::NAN), 1.0);
assert_eq!(power_of_two(1e300), 2.0_f64.powi(LIMIT));
}
}