use crate::options::SolverOptions;
#[inline]
pub fn is_equality_constraint(g_l: f64, g_u: f64) -> bool {
g_l.is_finite() && g_u.is_finite() && (g_l - g_u).abs() < 1e-15
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConvergenceStatus {
NotConverged,
Converged,
Acceptable,
Diverging,
}
pub struct ConvergenceInfo {
pub primal_inf: f64,
pub dual_inf: f64,
pub dual_inf_unscaled: f64,
pub compl_inf: f64,
pub mu: f64,
pub objective: f64,
pub multiplier_sum: f64,
pub multiplier_count: usize,
pub bound_multiplier_sum: f64,
pub bound_multiplier_count: usize,
}
pub fn check_convergence(
info: &ConvergenceInfo,
options: &SolverOptions,
consecutive_acceptable: usize,
) -> ConvergenceStatus {
let s_max: f64 = 100.0;
let s_d = if info.multiplier_count > 0 {
s_max.max(info.multiplier_sum / info.multiplier_count as f64) / s_max
} else {
1.0
};
let s_c = if info.bound_multiplier_count > 0 {
s_max.max(info.bound_multiplier_sum / info.bound_multiplier_count as f64) / s_max
} else {
1.0
};
let primal_tol = options.tol;
let dual_tol = options.tol * s_d;
let compl_tol = options.tol * s_c;
let scaled_ok = info.primal_inf <= primal_tol
&& info.dual_inf <= dual_tol
&& info.compl_inf <= compl_tol;
let unscaled_ok = info.primal_inf <= options.constr_viol_tol
&& info.dual_inf_unscaled <= options.dual_inf_tol
&& info.compl_inf <= options.compl_inf_tol;
if scaled_ok && unscaled_ok {
return ConvergenceStatus::Converged;
}
const NEAR_TOL_ITERS: usize = 15;
const ACCEPTABLE_TOL: f64 = 1e-6;
const ACCEPTABLE_DUAL_INF_TOL: f64 = 1e10;
const ACCEPTABLE_CONSTR_VIOL_TOL: f64 = 1e-2;
const ACCEPTABLE_COMPL_INF_TOL: f64 = 1e-2;
let near_scaled_ok = info.primal_inf <= ACCEPTABLE_TOL
&& info.dual_inf <= ACCEPTABLE_TOL * s_d
&& info.compl_inf <= ACCEPTABLE_TOL * s_c;
let near_unscaled_ok = info.primal_inf <= ACCEPTABLE_CONSTR_VIOL_TOL
&& info.dual_inf_unscaled <= ACCEPTABLE_DUAL_INF_TOL
&& info.compl_inf <= ACCEPTABLE_COMPL_INF_TOL;
if near_scaled_ok && near_unscaled_ok
&& consecutive_acceptable >= NEAR_TOL_ITERS
{
return ConvergenceStatus::Acceptable;
}
if info.objective.abs() > 1e50 {
return ConvergenceStatus::Diverging;
}
ConvergenceStatus::NotConverged
}
pub fn primal_infeasibility(g: &[f64], g_l: &[f64], g_u: &[f64]) -> f64 {
let mut sum_viol = 0.0f64;
for i in 0..g.len() {
if g[i] < g_l[i] {
sum_viol += g_l[i] - g[i];
}
if g[i] > g_u[i] {
sum_viol += g[i] - g_u[i];
}
}
sum_viol
}
pub fn primal_infeasibility_max(g: &[f64], g_l: &[f64], g_u: &[f64]) -> f64 {
let mut max_viol = 0.0f64;
for i in 0..g.len() {
if g[i] < g_l[i] {
max_viol = max_viol.max(g_l[i] - g[i]);
}
if g[i] > g_u[i] {
max_viol = max_viol.max(g[i] - g_u[i]);
}
}
max_viol
}
#[allow(clippy::too_many_arguments)]
pub fn dual_infeasibility(
grad_f: &[f64],
jac_rows: &[usize],
jac_cols: &[usize],
jac_vals: &[f64],
lambda: &[f64],
z_l: &[f64],
z_u: &[f64],
n: usize,
) -> f64 {
let mut residual = vec![0.0; n];
residual[..n].copy_from_slice(&grad_f[..n]);
for (idx, (&row, &col)) in jac_rows.iter().zip(jac_cols.iter()).enumerate() {
residual[col] += jac_vals[idx] * lambda[row];
}
for i in 0..n {
residual[i] -= z_l[i];
residual[i] += z_u[i];
}
residual.iter().map(|r| r.abs()).fold(0.0f64, f64::max)
}
#[allow(clippy::too_many_arguments)]
pub fn dual_infeasibility_scaled(
grad_f: &[f64],
jac_rows: &[usize],
jac_cols: &[usize],
jac_vals: &[f64],
lambda: &[f64],
z_l: &[f64],
z_u: &[f64],
n: usize,
) -> f64 {
let mut residual = vec![0.0; n];
residual[..n].copy_from_slice(&grad_f[..n]);
for (idx, (&row, &col)) in jac_rows.iter().zip(jac_cols.iter()).enumerate() {
residual[col] += jac_vals[idx] * lambda[row];
}
for i in 0..n {
residual[i] -= z_l[i];
residual[i] += z_u[i];
}
residual
.iter()
.enumerate()
.map(|(i, r)| r.abs() / (1.0 + grad_f[i].abs()))
.fold(0.0f64, f64::max)
}
pub fn complementarity_error(
x: &[f64],
x_l: &[f64],
x_u: &[f64],
z_l: &[f64],
z_u: &[f64],
mu: f64,
) -> f64 {
let mut max_err = 0.0f64;
let n = x.len();
for i in 0..n {
if x_l[i].is_finite() {
let slack = x[i] - x_l[i];
max_err = max_err.max((slack * z_l[i] - mu).abs());
}
if x_u[i].is_finite() {
let slack = x_u[i] - x[i];
max_err = max_err.max((slack * z_u[i] - mu).abs());
}
}
max_err
}
#[allow(clippy::too_many_arguments)]
pub fn complementarity_error_full(
x: &[f64],
x_l: &[f64],
x_u: &[f64],
z_l: &[f64],
z_u: &[f64],
g: &[f64],
g_l: &[f64],
g_u: &[f64],
y: &[f64],
mu: f64,
) -> f64 {
let mut max_err = complementarity_error(x, x_l, x_u, z_l, z_u, mu);
let m = g.len();
for i in 0..m {
if is_equality_constraint(g_l[i], g_u[i]) {
continue;
}
if g_l[i].is_finite() {
let slack = g[i] - g_l[i];
let mult = y[i].max(0.0);
max_err = max_err.max((slack * mult - mu).abs());
}
if g_u[i].is_finite() {
let slack = g_u[i] - g[i];
let mult = (-y[i]).max(0.0);
max_err = max_err.max((slack * mult - mu).abs());
}
}
max_err
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_primal_infeasibility_feasible() {
let g = vec![1.5, 3.0];
let g_l = vec![1.0, 2.0];
let g_u = vec![2.0, 4.0];
assert_eq!(primal_infeasibility(&g, &g_l, &g_u), 0.0);
}
#[test]
fn test_primal_infeasibility_violated() {
let g = vec![0.5, 5.0];
let g_l = vec![1.0, 2.0];
let g_u = vec![2.0, 4.0];
assert_eq!(primal_infeasibility(&g, &g_l, &g_u), 1.5);
}
#[test]
fn test_convergence_optimal() {
let info = ConvergenceInfo {
primal_inf: 1e-10,
dual_inf: 1e-10,
dual_inf_unscaled: 1e-10,
compl_inf: 1e-10,
mu: 1e-11,
objective: 17.0,
multiplier_sum: 0.0,
multiplier_count: 0,
bound_multiplier_sum: 0.0,
bound_multiplier_count: 0,
};
let opts = SolverOptions::default();
assert_eq!(
check_convergence(&info, &opts, 0),
ConvergenceStatus::Converged
);
}
#[test]
fn test_convergence_not_converged() {
let info = ConvergenceInfo {
primal_inf: 1e-3,
dual_inf: 1e-3,
dual_inf_unscaled: 1e-3,
compl_inf: 1e-3,
mu: 0.01,
objective: 17.0,
multiplier_sum: 0.0,
multiplier_count: 0,
bound_multiplier_sum: 0.0,
bound_multiplier_count: 0,
};
let opts = SolverOptions::default();
assert_eq!(
check_convergence(&info, &opts, 0),
ConvergenceStatus::NotConverged
);
}
#[test]
fn test_convergence_diverging() {
let info = ConvergenceInfo {
primal_inf: 1e-3,
dual_inf: 1e-3,
dual_inf_unscaled: 1e-3,
compl_inf: 1e-3,
mu: 1e-11,
objective: 1e51,
multiplier_sum: 0.0,
multiplier_count: 0,
bound_multiplier_sum: 0.0,
bound_multiplier_count: 0,
};
let opts = SolverOptions::default();
assert_eq!(
check_convergence(&info, &opts, 0),
ConvergenceStatus::Diverging
);
}
#[test]
fn test_convergence_acceptable() {
let info = ConvergenceInfo {
primal_inf: 1e-7,
dual_inf: 1e-7,
dual_inf_unscaled: 1e-7,
compl_inf: 1e-7,
mu: 1e-8,
objective: 5.0,
multiplier_sum: 0.0,
multiplier_count: 0,
bound_multiplier_sum: 0.0,
bound_multiplier_count: 0,
};
let opts = SolverOptions::default();
assert_eq!(
check_convergence(&info, &opts, 15),
ConvergenceStatus::Acceptable
);
}
#[test]
fn test_convergence_acceptable_insufficient_count() {
let info = ConvergenceInfo {
primal_inf: 1e-7,
dual_inf: 1e-7,
dual_inf_unscaled: 1e-7,
compl_inf: 1e-7,
mu: 1e-8,
objective: 5.0,
multiplier_sum: 0.0,
multiplier_count: 0,
bound_multiplier_sum: 0.0,
bound_multiplier_count: 0,
};
let opts = SolverOptions::default();
assert_eq!(
check_convergence(&info, &opts, 14),
ConvergenceStatus::NotConverged
);
}
#[test]
fn test_convergence_dual_scaling() {
let info = ConvergenceInfo {
primal_inf: 1e-10,
dual_inf: 5e-5, dual_inf_unscaled: 5e-5,
compl_inf: 1e-10,
mu: 1e-11,
objective: 1.0,
multiplier_sum: 1e6, multiplier_count: 10,
bound_multiplier_sum: 1e6,
bound_multiplier_count: 10,
};
let opts = SolverOptions::default();
assert_eq!(
check_convergence(&info, &opts, 0),
ConvergenceStatus::NotConverged
);
let info2 = ConvergenceInfo {
dual_inf: 5e-6,
dual_inf_unscaled: 5e-6,
..info
};
assert_eq!(
check_convergence(&info2, &opts, 0),
ConvergenceStatus::Converged
);
}
#[test]
fn test_convergence_unscaled_gate_blocks_false_convergence() {
let info = ConvergenceInfo {
primal_inf: 1e-10,
dual_inf: 1e-10,
dual_inf_unscaled: 1.5, compl_inf: 1e-10,
mu: 1e-11,
objective: 1.0,
multiplier_sum: 0.0,
multiplier_count: 0,
bound_multiplier_sum: 0.0,
bound_multiplier_count: 0,
};
let opts = SolverOptions::default();
assert_eq!(
check_convergence(&info, &opts, 0),
ConvergenceStatus::NotConverged
);
let info2 = ConvergenceInfo {
dual_inf_unscaled: 0.5,
..info
};
assert_eq!(
check_convergence(&info2, &opts, 0),
ConvergenceStatus::Converged
);
}
#[test]
fn test_complementarity_error_no_bounds() {
let x = vec![1.0, 2.0];
let x_l = vec![f64::NEG_INFINITY; 2];
let x_u = vec![f64::INFINITY; 2];
let z_l = vec![0.0; 2];
let z_u = vec![0.0; 2];
let err = complementarity_error(&x, &x_l, &x_u, &z_l, &z_u, 0.0);
assert!((err).abs() < 1e-15);
}
#[test]
fn test_complementarity_error_at_optimality() {
let mu = 0.01;
let x = vec![1.1]; let x_l = vec![1.0];
let x_u = vec![f64::INFINITY];
let z_l = vec![mu / 0.1]; let z_u = vec![0.0];
let err = complementarity_error(&x, &x_l, &x_u, &z_l, &z_u, mu);
assert!(err < 1e-12, "At optimality, complementarity error should be ~0, got {}", err);
}
#[test]
fn test_complementarity_error_away_from_optimality() {
let mu = 0.01;
let x = vec![1.5]; let x_l = vec![1.0];
let x_u = vec![f64::INFINITY];
let z_l = vec![1.0]; let z_u = vec![0.0];
let err = complementarity_error(&x, &x_l, &x_u, &z_l, &z_u, mu);
assert!((err - 0.49).abs() < 1e-12);
}
#[test]
fn test_dual_infeasibility_stationarity() {
let n = 2;
let grad_f = vec![1.0, 2.0];
let jac_rows = vec![0, 0];
let jac_cols = vec![0, 1];
let jac_vals = vec![1.0, 1.0]; let lambda = vec![-0.5]; let z_l = vec![0.5, 1.5];
let z_u = vec![0.0, 0.0];
let di = dual_infeasibility(&grad_f, &jac_rows, &jac_cols, &jac_vals, &lambda, &z_l, &z_u, n);
assert!(di < 1e-12, "Exact stationarity should give 0, got {}", di);
let z_l2 = vec![0.0, 0.0];
let di2 = dual_infeasibility(&grad_f, &jac_rows, &jac_cols, &jac_vals, &lambda, &z_l2, &z_u, n);
assert!(di2 > 0.1, "Non-stationary should give positive dual_inf");
}
#[test]
fn test_dual_infeasibility_scaled_insensitive_to_gradient_magnitude() {
let n = 2;
let grad_f = vec![1e6, 1e6];
let jac_rows = vec![];
let jac_cols = vec![];
let jac_vals: Vec<f64> = vec![];
let lambda: Vec<f64> = vec![];
let z_l = vec![0.0, 0.0];
let z_u = vec![0.0, 0.0];
let di = dual_infeasibility(&grad_f, &jac_rows, &jac_cols, &jac_vals, &lambda, &z_l, &z_u, n);
assert!(di > 1e5, "Unscaled should be large: {}", di);
let di_s = dual_infeasibility_scaled(&grad_f, &jac_rows, &jac_cols, &jac_vals, &lambda, &z_l, &z_u, n);
assert!(di_s < 1.1, "Scaled should be ~1.0: {}", di_s);
}
#[test]
fn test_dual_infeasibility_scaled_stationarity() {
let n = 2;
let grad_f = vec![1.0, 2.0];
let jac_rows = vec![0, 0];
let jac_cols = vec![0, 1];
let jac_vals = vec![1.0, 1.0];
let lambda = vec![-0.5];
let z_l = vec![0.5, 1.5];
let z_u = vec![0.0, 0.0];
let di_s = dual_infeasibility_scaled(&grad_f, &jac_rows, &jac_cols, &jac_vals, &lambda, &z_l, &z_u, n);
assert!(di_s < 1e-12, "Scaled stationarity should give 0, got {}", di_s);
}
}