use crate::sparse::CscMatrix;
pub struct RuizScaler {
pub d: Vec<f64>,
pub e: Vec<f64>,
pub c: f64,
}
impl RuizScaler {
pub const RUIZ_SWEEPS: usize = f64::MANTISSA_DIGITS as usize;
pub fn new(n: usize, m: usize) -> Self {
RuizScaler {
d: vec![1.0; n],
e: vec![1.0; m],
c: 1.0,
}
}
#[allow(clippy::needless_range_loop)]
pub fn compute(
&mut self,
q: &CscMatrix,
a: &CscMatrix,
q_vec: &[f64],
_l: &[f64],
_u: &[f64],
) {
self.compute_with_rhs(q, a, q_vec, &[]);
}
#[allow(clippy::needless_range_loop)]
pub fn compute_with_rhs(
&mut self,
q: &CscMatrix,
a: &CscMatrix,
q_vec: &[f64],
b: &[f64],
) {
let n = q.ncols;
let m = a.nrows;
const EPS: f64 = 1e-6;
for _iter in 0..RuizScaler::RUIZ_SWEEPS {
if m > 0 {
let mut row_norms = vec![0.0f64; m];
for col in 0..n {
for k in a.col_ptr[col]..a.col_ptr[col + 1] {
let i = a.row_ind[k];
let val = (self.e[i] * a.values[k] * self.d[col]).abs();
if val > row_norms[i] {
row_norms[i] = val;
}
}
}
for i in 0..m.min(b.len()) {
let b_val = (self.e[i] * b[i]).abs();
if b_val > row_norms[i] {
row_norms[i] = b_val;
}
}
for i in 0..m {
let norm = row_norms[i].max(EPS);
self.e[i] /= norm.sqrt();
}
}
let mut col_norms = vec![0.0f64; n];
for col in 0..n {
for k in q.col_ptr[col]..q.col_ptr[col + 1] {
let row = q.row_ind[k];
let val = (self.c * self.d[row] * q.values[k] * self.d[col]).abs();
if val > col_norms[col] {
col_norms[col] = val;
}
}
}
if m > 0 {
for col in 0..n {
for k in a.col_ptr[col]..a.col_ptr[col + 1] {
let row = a.row_ind[k];
let val = (self.e[row] * a.values[k] * self.d[col]).abs();
if val > col_norms[col] {
col_norms[col] = val;
}
}
}
}
for j in 0..n {
let norm = col_norms[j].max(EPS);
self.d[j] /= norm.sqrt();
}
let mut q_mat_inf = 0.0f64;
for col in 0..n {
for k in q.col_ptr[col]..q.col_ptr[col + 1] {
let row = q.row_ind[k];
let val = (self.c * self.d[row] * q.values[k] * self.d[col]).abs();
if val > q_mat_inf {
q_mat_inf = val;
}
}
}
let q_vec_inf = q_vec
.iter()
.enumerate()
.map(|(j, &v)| (self.c * self.d[j] * v).abs())
.fold(0.0f64, f64::max);
let denom = q_mat_inf.max(q_vec_inf).max(EPS);
self.c /= denom;
}
}
#[allow(clippy::type_complexity)]
pub fn scale_problem(
&self,
q: &CscMatrix,
a: &CscMatrix,
q_vec: &[f64],
b: &[f64],
bounds: &[(f64, f64)],
) -> (CscMatrix, CscMatrix, Vec<f64>, Vec<f64>, Vec<(f64, f64)>) {
let n = q.ncols;
let m = a.nrows;
let mut q_s = q.clone();
for col in 0..n {
for k in q.col_ptr[col]..q.col_ptr[col + 1] {
let row = q.row_ind[k];
q_s.values[k] = self.c * self.d[row] * q.values[k] * self.d[col];
}
}
let mut a_s = a.clone();
for col in 0..n {
for k in a.col_ptr[col]..a.col_ptr[col + 1] {
let row = a.row_ind[k];
a_s.values[k] = self.e[row] * a.values[k] * self.d[col];
}
}
let q_vec_s: Vec<f64> = q_vec
.iter()
.enumerate()
.map(|(j, &v)| self.c * self.d[j] * v)
.collect();
let b_s: Vec<f64> = if m > 0 {
b.iter()
.enumerate()
.map(|(i, &v)| self.e[i] * v)
.collect()
} else {
vec![]
};
let bounds_s: Vec<(f64, f64)> = bounds
.iter()
.enumerate()
.map(|(j, &(lb, ub))| (lb / self.d[j], ub / self.d[j]))
.collect();
(q_s, a_s, q_vec_s, b_s, bounds_s)
}
pub fn unscale_bound_duals(&self, bound_duals_s: &[f64], bounds: &[(f64, f64)]) -> Vec<f64> {
if bound_duals_s.is_empty() {
return vec![];
}
let mut result = Vec::with_capacity(bound_duals_s.len());
let mut idx = 0;
for (j, &(lb, _)) in bounds.iter().enumerate() {
if lb.is_finite() {
result.push(bound_duals_s[idx] / (self.c * self.d[j]));
idx += 1;
debug_assert!(idx <= bound_duals_s.len(), "bound_duals_s index out of bounds (lb)");
}
}
for (j, &(_, ub)) in bounds.iter().enumerate() {
if ub.is_finite() {
result.push(bound_duals_s[idx] / (self.c * self.d[j]));
idx += 1;
debug_assert!(idx <= bound_duals_s.len(), "bound_duals_s index out of bounds (ub)");
}
}
debug_assert_eq!(idx, bound_duals_s.len(), "unscale_bound_duals: idx != bound_duals_s.len()");
result
}
pub fn unscale_solution(&self, x_s: &[f64], y_s: &[f64]) -> (Vec<f64>, Vec<f64>) {
let x: Vec<f64> = x_s
.iter()
.enumerate()
.map(|(j, &v)| self.d[j] * v)
.collect();
let y: Vec<f64> = y_s
.iter()
.enumerate()
.map(|(i, &v)| self.e[i] * v / self.c)
.collect();
(x, y)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sparse::CscMatrix;
#[test]
fn test_ruiz_scaler_identity() {
let n = 3usize;
let m = 3usize;
let q = CscMatrix::from_triplets(
&[0, 1, 2],
&[0, 1, 2],
&[1.0, 1.0, 1.0],
n, n,
).unwrap();
let q_vec = vec![0.0; n];
let a = CscMatrix::from_triplets(
&[0, 1, 2],
&[0, 1, 2],
&[1.0, 1.0, 1.0],
m, n,
).unwrap();
let l = vec![0.0; n];
let u = vec![1.0; n];
let mut scaler = RuizScaler::new(n, m);
scaler.compute(&q, &a, &q_vec, &l, &u);
for j in 0..n {
assert!(
(scaler.d[j] - 1.0).abs() < 0.2,
"d[{}] = {:.6} (expected ~1.0)",
j, scaler.d[j]
);
}
for i in 0..m {
assert!(
(scaler.e[i] - 1.0).abs() < 0.2,
"e[{}] = {:.6} (expected ~1.0)",
i, scaler.e[i]
);
}
}
#[test]
fn test_ruiz_scaling_correctness() {
use crate::qp::QpProblem;
use crate::options::SolverOptions;
use crate::problem::SolveStatus;
let n = 5usize;
let m = 3usize;
let q_rows: Vec<usize> = (0..n).collect();
let q_cols: Vec<usize> = (0..n).collect();
let q_vals = vec![1.0, 100.0, 1.0, 100.0, 1.0];
let q = CscMatrix::from_triplets(&q_rows, &q_cols, &q_vals, n, n).unwrap();
let q_vec = vec![-1.0, -10.0, -1.0, -10.0, -1.0];
let a = CscMatrix::from_triplets(
&[0, 0, 1, 1, 2, 2],
&[0, 1, 2, 3, 0, 4],
&[1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
m, n,
).unwrap();
let b = vec![2.0, 2.0, 2.0];
let bounds = vec![(0.0f64, f64::INFINITY); n];
let problem = QpProblem::new_all_le(q, q_vec, a, b, bounds).unwrap();
let opts_no_scale = SolverOptions { use_ruiz_scaling: false, ..Default::default() };
let r_no_scale = crate::qp::solve_qp_with(&problem, &opts_no_scale);
let opts_scale = SolverOptions { use_ruiz_scaling: true, ..Default::default() };
let r_scale = crate::qp::solve_qp_with(&problem, &opts_scale);
assert!(
r_no_scale.status == SolveStatus::Optimal
|| r_no_scale.status == SolveStatus::Timeout
|| r_no_scale.status == SolveStatus::SuboptimalSolution,
"no_scale: {:?}", r_no_scale.status
);
assert!(
r_scale.status == SolveStatus::Optimal
|| r_scale.status == SolveStatus::Timeout
|| r_scale.status == SolveStatus::SuboptimalSolution,
"scale: {:?}", r_scale.status
);
if r_no_scale.status == SolveStatus::Optimal && r_scale.status == SolveStatus::Optimal {
for j in 0..n {
assert!(
(r_no_scale.solution[j] - r_scale.solution[j]).abs() < 0.1,
"x[{}]: no_scale={:.6}, scale={:.6}",
j, r_no_scale.solution[j], r_scale.solution[j]
);
}
assert!(
(r_no_scale.objective - r_scale.objective).abs() < 0.1,
"obj: no_scale={:.6}, scale={:.6}",
r_no_scale.objective, r_scale.objective
);
}
}
#[test]
fn test_ruiz_disabled() {
use crate::qp::QpProblem;
use crate::options::SolverOptions;
use crate::problem::SolveStatus;
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
let q_vec = vec![0.0, 0.0];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[-1.0, -1.0], 1, 2).unwrap();
let b = vec![-1.0];
let bounds = vec![(f64::NEG_INFINITY, f64::INFINITY); 2];
let problem = QpProblem::new_all_le(q, q_vec, a, b, bounds).unwrap();
let opts = SolverOptions { use_ruiz_scaling: false, ..Default::default() };
let result = crate::qp::solve_qp_with(&problem, &opts);
assert_eq!(result.status, SolveStatus::Optimal, "disabled: {:?}", result.status);
assert!((result.solution[0] - 0.5).abs() < 0.05, "x[0]={}", result.solution[0]);
assert!((result.solution[1] - 0.5).abs() < 0.05, "x[1]={}", result.solution[1]);
}
#[test]
fn scale_unscale_round_trip_identity() {
let n = 3usize;
let m = 2usize;
let q = CscMatrix::from_triplets(
&[0, 1, 2], &[0, 1, 2], &[2.0, 3.0, 4.0], n, n,
).unwrap();
let q_vec = vec![1.0, 2.0, 3.0];
let a = CscMatrix::from_triplets(
&[0, 0, 1, 1], &[0, 1, 1, 2], &[1.0, 2.0, 3.0, 4.0], m, n,
).unwrap();
let b = vec![5.0, 6.0];
let bounds = vec![(0.0, 10.0), (0.0, 10.0), (0.0, 10.0)];
let mut scaler = RuizScaler::new(n, m);
scaler.compute(&q, &a, &q_vec, &[0.0; 3], &[10.0; 3]);
let (_q_s, _a_s, _q_s_vec, _b_s, bounds_s) =
scaler.scale_problem(&q, &a, &q_vec, &b, &bounds);
let x_s: Vec<f64> = bounds_s.iter().map(|&(l, u)| 0.5 * (l + u)).collect();
let y_s = vec![0.7_f64, -0.3];
let (x_orig, y_orig) = scaler.unscale_solution(&x_s, &y_s);
for j in 0..n {
let expected = scaler.d[j] * x_s[j];
assert!((x_orig[j] - expected).abs() < 1e-12 * (1.0 + expected.abs()),
"x_orig[{}]={} expected {}", j, x_orig[j], expected);
}
for i in 0..m {
let expected = scaler.e[i] * y_s[i] / scaler.c;
assert!((y_orig[i] - expected).abs() < 1e-12 * (1.0 + expected.abs()),
"y_orig[{}]={} expected {}", i, y_orig[i], expected);
}
}
#[test]
fn dual_residual_unscale_factor_is_c_times_d() {
let n = 2usize;
let m = 1usize;
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[1.0, 1.0], n, n).unwrap();
let q_vec = vec![1.0_f64, 2.0];
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[3.0_f64, 4.0], m, n).unwrap();
let b = vec![5.0_f64];
let bounds = vec![(0.0_f64, 10.0); 2];
let mut scaler = RuizScaler::new(n, m);
scaler.compute(&q, &a, &q_vec, &[0.0; 2], &[10.0; 2]);
let (q_s, a_s, q_s_vec, _b_s, _bounds_s) =
scaler.scale_problem(&q, &a, &q_vec, &b, &bounds);
let x_s = vec![0.3_f64, 0.4];
let y_s = vec![0.5_f64];
let qx_s = q_s.mat_vec_mul(&x_s).unwrap();
let aty_s = a_s.transpose().mat_vec_mul(&y_s).unwrap();
let r_d_s: Vec<f64> = (0..n).map(|j| qx_s[j] + q_s_vec[j] + aty_s[j]).collect();
let (x, y) = scaler.unscale_solution(&x_s, &y_s);
let qx = q.mat_vec_mul(&x).unwrap();
let aty = a.transpose().mat_vec_mul(&y).unwrap();
let r_d: Vec<f64> = (0..n).map(|j| qx[j] + q_vec[j] + aty[j]).collect();
for j in 0..n {
let expected = r_d_s[j] / (scaler.c * scaler.d[j]);
assert!((r_d[j] - expected).abs() < 1e-10 * (1.0 + expected.abs()),
"r_d[{}]={} expected {} (= r_d_s[{}] / (c×d[{}]))", j, r_d[j], expected, j, j);
}
}
}