use crate::problem::ConstraintType;
use crate::sparse::CscMatrix;
pub fn dual_sign_violation(ct: &[ConstraintType], y: &[f64], bounds: &[(f64, f64)], z: &[f64]) -> f64 {
let mut max_rel = 0.0_f64;
let m = ct.len().min(y.len());
#[allow(unreachable_patterns)]
for i in 0..m {
let viol = match ct[i] {
ConstraintType::Le => (-y[i]).max(0.0), ConstraintType::Ge => y[i].max(0.0), ConstraintType::Eq => 0.0, _ => 0.0,
};
if viol > 0.0 {
let rel = viol / (1.0 + y[i].abs());
if rel > max_rel { max_rel = rel; }
}
}
if z.is_empty() {
return max_rel;
}
let mut idx = 0_usize;
for &(lb, _) in bounds.iter() {
if lb.is_finite() && idx < z.len() {
let v = (-z[idx]).max(0.0); if v > 0.0 {
let rel = v / (1.0 + z[idx].abs());
if rel > max_rel { max_rel = rel; }
}
idx += 1;
}
}
for &(_, ub) in bounds.iter() {
if ub.is_finite() && idx < z.len() {
let v = (-z[idx]).max(0.0); if v > 0.0 {
let rel = v / (1.0 + z[idx].abs());
if rel > max_rel { max_rel = rel; }
}
idx += 1;
}
}
max_rel
}
pub fn bound_contrib(bounds: &[(f64, f64)], bd: &[f64]) -> Vec<f64> {
let n = bounds.len();
let mut contrib = vec![0.0_f64; n];
if bd.is_empty() {
return contrib;
}
let mut idx = 0_usize;
for (j, &(lb, _)) in bounds.iter().enumerate() {
if lb.is_finite() && idx < bd.len() {
contrib[j] -= bd[idx];
idx += 1;
}
}
for (j, &(_, ub)) in bounds.iter().enumerate() {
if ub.is_finite() && idx < bd.len() {
contrib[j] += bd[idx];
idx += 1;
}
}
contrib
}
pub fn comp_bound_products(bounds: &[(f64, f64)], x: &[f64], bd: &[f64]) -> Vec<f64> {
let mut out = Vec::with_capacity(bd.len());
if bd.is_empty() {
return out;
}
let mut idx = 0_usize;
for (j, &(lb, _)) in bounds.iter().enumerate() {
if lb.is_finite() && idx < bd.len() {
out.push((bd[idx] * (x[j] - lb)).abs());
idx += 1;
}
}
for (j, &(_, ub)) in bounds.iter().enumerate() {
if ub.is_finite() && idx < bd.len() {
out.push((bd[idx] * (ub - x[j])).abs());
idx += 1;
}
}
out
}
pub mod f64_impl {
use super::*;
pub fn qx(q: &CscMatrix, x: &[f64]) -> Vec<f64> {
q.mat_vec_mul(x).unwrap_or_else(|_| vec![0.0; q.nrows])
}
pub fn aty(a: &CscMatrix, y: &[f64], n: usize) -> Vec<f64> {
if a.nrows == 0 || y.is_empty() {
return vec![0.0; n];
}
a.transpose().mat_vec_mul(y).unwrap_or_else(|_| vec![0.0; n])
}
pub fn ax(a: &CscMatrix, x: &[f64]) -> Vec<f64> {
if a.nrows == 0 {
return Vec::new();
}
a.mat_vec_mul(x).unwrap_or_else(|_| Vec::new())
}
pub fn constraint_violations(ax: &[f64], b: &[f64], ct: &[ConstraintType]) -> Vec<f64> {
let m = ct.len();
let mut out = vec![0.0_f64; m];
#[allow(unreachable_patterns)] for (i, cti) in ct.iter().enumerate() {
if i >= ax.len() || i >= b.len() {
continue;
}
out[i] = match cti {
ConstraintType::Le => (ax[i] - b[i]).max(0.0),
ConstraintType::Ge => (b[i] - ax[i]).max(0.0),
ConstraintType::Eq => (ax[i] - b[i]).abs(),
_ => 0.0,
};
}
out
}
pub fn comp_ineq_products(
ax: &[f64], b: &[f64], ct: &[ConstraintType], y: &[f64],
) -> Vec<f64> {
let m = ct.len();
let mut out = vec![0.0_f64; m];
if ax.is_empty() || y.is_empty() {
return out;
}
#[allow(unreachable_patterns)] for (i, cti) in ct.iter().enumerate() {
let slack = match cti {
ConstraintType::Le => b[i] - ax[i],
ConstraintType::Ge => ax[i] - b[i],
ConstraintType::Eq => continue,
_ => continue,
};
out[i] = (y[i] * slack).abs();
}
out
}
}
pub mod dd_impl {
use super::*;
use twofloat::TwoFloat;
pub fn qx(q: &CscMatrix, x: &[f64]) -> Vec<TwoFloat> {
let n = q.nrows;
let zero = TwoFloat::from(0.0);
let mut out: Vec<TwoFloat> = vec![zero; n];
for col in 0..q.ncols {
let xv = x[col];
for k in q.col_ptr[col]..q.col_ptr[col + 1] {
let row = q.row_ind[k];
out[row] = out[row] + TwoFloat::new_mul(q.values[k], xv);
}
}
out
}
pub fn aty(a: &CscMatrix, y: &[f64], n: usize) -> Vec<TwoFloat> {
let zero = TwoFloat::from(0.0);
if a.nrows == 0 || y.is_empty() {
return vec![zero; n];
}
let mut out: Vec<TwoFloat> = vec![zero; n];
for col in 0..a.ncols {
for k in a.col_ptr[col]..a.col_ptr[col + 1] {
let row = a.row_ind[k];
out[col] = out[col] + TwoFloat::new_mul(a.values[k], y[row]);
}
}
out
}
pub fn ax(a: &CscMatrix, x: &[f64]) -> Vec<TwoFloat> {
if a.nrows == 0 {
return Vec::new();
}
let zero = TwoFloat::from(0.0);
let mut out: Vec<TwoFloat> = vec![zero; a.nrows];
for col in 0..a.ncols {
let xv = x[col];
for k in a.col_ptr[col]..a.col_ptr[col + 1] {
out[a.row_ind[k]] = out[a.row_ind[k]] + TwoFloat::new_mul(a.values[k], xv);
}
}
out
}
pub fn constraint_violations(
ax_dd: &[TwoFloat], b: &[f64], ct: &[ConstraintType],
) -> Vec<f64> {
let m = ct.len();
let mut out = vec![0.0_f64; m];
#[allow(unreachable_patterns)] for (i, cti) in ct.iter().enumerate() {
if i >= ax_dd.len() || i >= b.len() {
continue;
}
let raw = f64::from(ax_dd[i] - TwoFloat::from(b[i]));
out[i] = match cti {
ConstraintType::Le => raw.max(0.0),
ConstraintType::Ge => (-raw).max(0.0),
ConstraintType::Eq => raw.abs(),
_ => 0.0,
};
}
out
}
pub fn comp_ineq_products(
ax_dd: &[TwoFloat], b: &[f64], ct: &[ConstraintType], y: &[f64],
) -> Vec<f64> {
let m = ct.len();
let mut out = vec![0.0_f64; m];
if ax_dd.is_empty() || y.is_empty() {
return out;
}
#[allow(unreachable_patterns)] for (i, cti) in ct.iter().enumerate() {
let slack_dd = match cti {
ConstraintType::Le => TwoFloat::from(b[i]) - ax_dd[i],
ConstraintType::Ge => ax_dd[i] - TwoFloat::from(b[i]),
ConstraintType::Eq => continue,
_ => continue,
};
out[i] = (f64::from(slack_dd) * y[i]).abs();
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bound_contrib_empty_bd_yields_zero() {
let bounds = vec![(0.0, 1.0), (f64::NEG_INFINITY, f64::INFINITY)];
let c = bound_contrib(&bounds, &[]);
assert_eq!(c, vec![0.0, 0.0]);
}
#[test]
fn bound_contrib_lb_then_ub_layout() {
let bounds = vec![(0.0, 10.0), (f64::NEG_INFINITY, f64::INFINITY), (5.0, f64::INFINITY)];
let bd = vec![1.0, 2.0, 3.0];
let c = bound_contrib(&bounds, &bd);
assert_eq!(c, vec![-1.0 + 3.0, 0.0, -2.0]);
}
#[test]
fn comp_bound_products_lb_then_ub_layout() {
let bounds = vec![(0.0, 10.0), (5.0, f64::INFINITY)];
let x = vec![2.0, 7.0];
let bd = vec![1.5, 0.5, 4.0];
let p = comp_bound_products(&bounds, &x, &bd);
assert_eq!(
p,
vec![
(1.5_f64 * (2.0 - 0.0)).abs(),
(0.5_f64 * (7.0 - 5.0)).abs(),
(4.0_f64 * (10.0 - 2.0)).abs(),
]
);
}
#[test]
fn comp_bound_products_empty_bd() {
let bounds = vec![(0.0, 1.0)];
let x = vec![0.5];
let p = comp_bound_products(&bounds, &x, &[]);
assert!(p.is_empty());
}
#[test]
fn f64_constraint_violations_le_ge_eq() {
use ConstraintType::*;
let ax = vec![1.0, 2.0, 3.0];
let b = vec![0.5, 3.0, 3.0];
let ct = vec![Le, Ge, Eq];
let v = f64_impl::constraint_violations(&ax, &b, &ct);
assert_eq!(v, vec![0.5, 1.0, 0.0]);
}
#[test]
fn f64_constraint_violations_no_negative() {
use ConstraintType::*;
let ax = vec![1.0, 5.0];
let b = vec![2.0, 3.0];
let ct = vec![Le, Ge];
let v = f64_impl::constraint_violations(&ax, &b, &ct);
assert_eq!(v, vec![0.0, 0.0]);
}
#[test]
fn dd_constraint_violations_recovers_from_f64_cancellation() {
use twofloat::TwoFloat;
use ConstraintType::*;
let ax_dd = vec![TwoFloat::from(1.0_f64) + TwoFloat::new_mul(1.0e16, 1.0) - TwoFloat::new_mul(1.0e16, 1.0)];
let b = vec![0.0];
let ct = vec![Eq];
let v = dd_impl::constraint_violations(&ax_dd, &b, &ct);
assert!((v[0] - 1.0).abs() < 1e-12, "got {}", v[0]);
}
#[test]
fn f64_comp_ineq_products_skip_eq() {
use ConstraintType::*;
let ax = vec![2.0, 5.0, 3.0];
let b = vec![3.0, 2.0, 3.0];
let y = vec![1.0, 2.0, 7.0];
let ct = vec![Le, Ge, Eq];
let p = f64_impl::comp_ineq_products(&ax, &b, &ct, &y);
assert_eq!(p, vec![1.0, 6.0, 0.0]);
}
#[test]
fn dual_sign_le_y_negative_is_violation() {
use ConstraintType::*;
let ct = vec![Le];
let y = vec![-0.5_f64];
let bounds: Vec<(f64, f64)> = vec![];
let z: Vec<f64> = vec![];
let v = dual_sign_violation(&ct, &y, &bounds, &z);
assert!(v > 0.0, "Le with y=-0.5 should give violation > 0, got {}", v);
assert!((v - 0.5 / 1.5).abs() < 1e-12, "exact check: {}", v);
}
#[test]
fn dual_sign_le_y_positive_no_violation() {
use ConstraintType::*;
let ct = vec![Le, Le];
let y = vec![0.0_f64, 2.0];
let bounds: Vec<(f64, f64)> = vec![];
let z: Vec<f64> = vec![];
let v = dual_sign_violation(&ct, &y, &bounds, &z);
assert_eq!(v, 0.0, "Le with y>=0 must give 0");
}
#[test]
fn dual_sign_ge_y_positive_is_violation() {
use ConstraintType::*;
let ct = vec![Ge];
let y = vec![1.0_f64];
let bounds: Vec<(f64, f64)> = vec![];
let z: Vec<f64> = vec![];
let v = dual_sign_violation(&ct, &y, &bounds, &z);
assert!(v > 0.0, "Ge with y=1.0 should be violation > 0, got {}", v);
assert!((v - 1.0 / 2.0).abs() < 1e-12, "exact: {}", v);
}
#[test]
fn dual_sign_ge_y_negative_no_violation() {
use ConstraintType::*;
let ct = vec![Ge];
let y = vec![-3.0_f64];
let bounds: Vec<(f64, f64)> = vec![];
let z: Vec<f64> = vec![];
let v = dual_sign_violation(&ct, &y, &bounds, &z);
assert_eq!(v, 0.0, "Ge with y=-3 must give 0");
}
#[test]
fn dual_sign_eq_y_any_sign_no_violation() {
use ConstraintType::*;
for yi in [-100.0, -1.0, 0.0, 1.0, 100.0] {
let ct = vec![Eq];
let y = vec![yi];
let v = dual_sign_violation(&ct, &y, &vec![], &vec![]);
assert_eq!(v, 0.0, "Eq with y={yi} should give 0");
}
}
#[test]
fn dual_sign_mixed_constraints() {
use ConstraintType::*;
let ct = vec![Le, Ge, Eq];
let y = vec![0.5, 0.3, -10.0];
let v = dual_sign_violation(&ct, &y, &vec![], &vec![]);
let expected = 0.3 / 1.3;
assert!((v - expected).abs() < 1e-12, "got {v}, expected {expected}");
}
#[test]
fn dual_sign_z_lb_negative_is_violation() {
use ConstraintType::*;
let ct = vec![Le];
let y = vec![0.5_f64]; let bounds = vec![(0.0_f64, f64::INFINITY)]; let z = vec![-0.4_f64]; let v = dual_sign_violation(&ct, &y, &bounds, &z);
let expected = 0.4 / 1.4;
assert!((v - expected).abs() < 1e-12, "got {v}");
}
#[test]
fn dual_sign_z_ub_negative_is_violation() {
let ct: Vec<ConstraintType> = vec![];
let y: Vec<f64> = vec![];
let bounds = vec![(f64::NEG_INFINITY, 1.0_f64)]; let z = vec![-0.7_f64]; let v = dual_sign_violation(&ct, &y, &bounds, &z);
let expected = 0.7 / 1.7;
assert!((v - expected).abs() < 1e-12, "got {v}");
}
#[test]
fn dual_sign_z_ub_positive_no_violation() {
let ct: Vec<ConstraintType> = vec![];
let y: Vec<f64> = vec![];
let bounds = vec![(f64::NEG_INFINITY, 1.0_f64)]; let z = vec![0.7_f64]; let v = dual_sign_violation(&ct, &y, &bounds, &z);
assert_eq!(v, 0.0, "positive z_ub must not be a violation, got {v}");
}
#[test]
fn dual_sign_empty_z_no_violation() {
use ConstraintType::*;
let ct = vec![Le];
let y = vec![0.5_f64];
let bounds = vec![(0.0, f64::INFINITY)];
let v = dual_sign_violation(&ct, &y, &bounds, &[]);
assert_eq!(v, 0.0);
}
#[test]
fn dual_sign_scale_robust() {
use ConstraintType::*;
let ct = vec![Ge];
for &yi in &[1e-6_f64, 1.0, 1e3, 1e9] {
let v = dual_sign_violation(&ct, &[yi], &[], &[]);
assert!(v > 0.0 && v < 1.0,
"violation y={yi} must be in (0,1), got {v}");
if yi > 100.0 {
assert!(v > 0.99, "large yi={yi} should give v close to 1, got {v}");
}
}
for &yi in &[-1e-6_f64, -1.0, -1e9] {
let v = dual_sign_violation(&ct, &[yi], &[], &[]);
assert_eq!(v, 0.0, "no violation for yi={yi}");
}
}
#[test]
fn dual_sign_all_satisfied_returns_zero() {
use ConstraintType::*;
let ct = vec![Le, Ge, Eq, Le, Ge];
let y = vec![1.0, -1.0, 0.5, 0.0, -2.0];
let bounds = vec![
(0.0_f64, 1.0_f64), (f64::NEG_INFINITY, f64::INFINITY), ];
let z = vec![0.5_f64, 0.5]; let v = dual_sign_violation(&ct, &y, &bounds, &z);
assert_eq!(v, 0.0, "all satisfied should give 0");
}
#[test]
fn dual_sign_z_ub_observed_positive_at_active_ub() {
use crate::qp::{QpProblem, solve_qp};
use crate::sparse::CscMatrix;
let q = CscMatrix::from_triplets(&[0usize], &[0usize], &[2.0_f64], 1, 1).unwrap();
let a = CscMatrix::new(0, 1);
let prob = QpProblem::new(q, vec![-20.0], a, vec![], vec![(0.0, 5.0)], vec![]).unwrap();
let result = solve_qp(&prob);
assert!((result.solution[0] - 5.0).abs() < 1e-4,
"x should be ≈5, got {}", result.solution[0]);
assert!(result.bound_duals.len() >= 2,
"expected >=2 bound duals, got {}", result.bound_duals.len());
let z_ub = result.bound_duals[1];
assert!(z_ub > 1.0,
"z_ub should be ≈10 (active ub dual), got {z_ub}");
}
#[test]
fn dd_qx_aty_ax_match_f64_on_well_conditioned() {
use crate::sparse::CscMatrix;
let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0_f64, 3.0], 2, 2).unwrap();
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0_f64, 2.0], 1, 2).unwrap();
let x = vec![5.0_f64, 7.0];
let y = vec![4.0_f64];
let qx_f = f64_impl::qx(&q, &x);
let qx_d: Vec<f64> = dd_impl::qx(&q, &x).iter().map(|&v| f64::from(v)).collect();
assert_eq!(qx_f, qx_d);
let aty_f = f64_impl::aty(&a, &y, 2);
let aty_d: Vec<f64> = dd_impl::aty(&a, &y, 2).iter().map(|&v| f64::from(v)).collect();
assert_eq!(aty_f, aty_d);
let ax_f = f64_impl::ax(&a, &x);
let ax_d: Vec<f64> = dd_impl::ax(&a, &x).iter().map(|&v| f64::from(v)).collect();
assert_eq!(ax_f, ax_d);
}
}