use std::time::Instant;
use crate::error::{QpError, QpStatus};
use crate::factor::LinearSolver;
use crate::kkt::{
a_times_x, assemble_active_set_kkt, assemble_box_with_active, assemble_equality_plus_bounds,
h_times_x, is_all_equality_constraints, is_pure_box, is_pure_equality_no_bounds,
rhs_equality_only, KktTriplet,
};
use crate::options::{AntiCyclingChoice, QpOptions};
use crate::problem::{QpProblem, QpSolution, QpStats, QpWarmStart};
use crate::working_set::{BoundStatus, ConsStatus, WorkingSet};
use pounce_common::types::{NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF};
use pounce_common::{Index, Number};
use pounce_linsol::status::ESymSolverStatus;
use pounce_linsol::SparseSymLinearSolverInterface;
pub trait QpSolver {
fn solve(
&mut self,
qp: &QpProblem,
ws: Option<&QpWarmStart>,
opts: &QpOptions,
) -> Result<QpSolution, QpError>;
fn solve_parametric(
&mut self,
qp_prev: &QpProblem,
sol_prev: &QpSolution,
qp_new: &QpProblem,
opts: &QpOptions,
) -> Result<QpSolution, QpError>;
fn solve_with_working_set(
&mut self,
qp: &QpProblem,
working: &crate::working_set::WorkingSet,
opts: &QpOptions,
) -> Result<QpSolution, QpError>;
}
pub struct ParametricActiveSetSolver {
linsol: LinearSolver,
}
impl ParametricActiveSetSolver {
pub fn new(backend: Box<dyn SparseSymLinearSolverInterface>) -> Self {
Self {
linsol: LinearSolver::new(backend),
}
}
fn factorize_with_inertia_control(
&mut self,
mut kkt: KktTriplet,
rhs: &mut [Number],
expected_neg: i32,
n_h_rows: usize,
opts: &QpOptions,
) -> Result<Number, QpError> {
let rhs_snapshot = rhs.to_vec();
let mut rhs_local = rhs_snapshot.clone();
match self
.linsol
.factorize_and_solve(&kkt, &mut rhs_local, Some(expected_neg))
{
Ok(()) => {
rhs.copy_from_slice(&rhs_local);
return Ok(0.0);
}
Err(ref e) if e.is_recoverable_factorization_failure() => {}
Err(e) => return Err(e),
}
let mut current = 0.0;
let mut next = opts.inertia_shift_initial;
for _ in 0..opts.inertia_max_shifts {
kkt.add_h_diagonal_shift(n_h_rows, next - current);
current = next;
let mut rhs_local = rhs_snapshot.clone();
match self
.linsol
.factorize_and_solve(&kkt, &mut rhs_local, Some(expected_neg))
{
Ok(()) => {
rhs.copy_from_slice(&rhs_local);
return Ok(current);
}
Err(ref e) if e.is_recoverable_factorization_failure() => {
next *= opts.inertia_shift_factor;
}
Err(e) => return Err(e),
}
}
Err(QpError::LinearSolverFailure(format!(
"inertia control exhausted {} shifts (final δ = {:.3e}); reduced Hessian \
remains non-PD on null(A_W) — consider an `HessianInertia::Indefinite` \
problem with no PD reduced direction, or relax `inertia_shift_factor`",
opts.inertia_max_shifts, current
)))
}
fn factor_pinned_primal(
&mut self,
qp: &QpProblem,
active_cons: &[usize],
cons_targets: &[Number],
active_bounds: &[usize],
bound_targets: &[Number],
opts: &QpOptions,
) -> Result<Vec<Number>, QpError> {
let n = qp.n;
let k_c = active_cons.len();
let k_b = active_bounds.len();
let kkt = assemble_active_set_kkt(qp, active_cons, active_bounds);
let mut rhs = vec![0.0; n + k_c + k_b];
for (rhs_i, &g_i) in rhs[..n].iter_mut().zip(qp.g.iter()) {
*rhs_i = -g_i;
}
rhs[n..n + k_c].copy_from_slice(cons_targets);
rhs[n + k_c..n + k_c + k_b].copy_from_slice(bound_targets);
self.factorize_with_inertia_control(kkt, &mut rhs, (k_c + k_b) as i32, n, opts)?;
Ok(rhs[..n].to_vec())
}
fn solve_box_constrained(
&mut self,
qp: &QpProblem,
opts: &QpOptions,
) -> Result<QpSolution, QpError> {
let started = Instant::now();
let n = qp.n;
let mut x = vec![0.0; n];
for (xi, (&l, &u)) in x.iter_mut().zip(qp.xl.iter().zip(qp.xu.iter())) {
if l > NLP_LOWER_BOUND_INF && *xi < l {
*xi = l;
}
if u < NLP_UPPER_BOUND_INF && *xi > u {
*xi = u;
}
}
let mut working = WorkingSet::cold(n, 0);
for (i, (status, xi)) in working.bounds.iter_mut().zip(x.iter_mut()).enumerate() {
let l = qp.xl[i];
let u = qp.xu[i];
let l_finite = l > NLP_LOWER_BOUND_INF;
let u_finite = u < NLP_UPPER_BOUND_INF;
if l_finite && u_finite && (l - u).abs() <= opts.feas_tol {
*status = BoundStatus::Fixed;
*xi = l;
} else if l_finite && (*xi - l).abs() <= opts.feas_tol {
*status = BoundStatus::AtLower;
*xi = l;
} else if u_finite && (*xi - u).abs() <= opts.feas_tol {
*status = BoundStatus::AtUpper;
*xi = u;
}
}
let mut n_refactor: u32 = 0;
let mut n_changes: u32 = 0;
for _iter in 0..opts.max_iter {
let active: Vec<usize> = (0..n).filter(|&i| working.bounds[i].is_active()).collect();
let k = active.len();
let kkt = assemble_box_with_active(qp, &active);
let hx = h_times_x(qp.h, &x);
let mut rhs = vec![0.0; n + k];
for i in 0..n {
rhs[i] = -(hx[i] + qp.g[i]);
}
self.factorize_with_inertia_control(kkt, &mut rhs, k as i32, qp.n, opts)?;
n_refactor += 1;
let p_inf = rhs[..n].iter().map(|pi| pi.abs()).fold(0.0, f64::max);
if p_inf <= opts.opt_tol {
let mut worst: Option<(usize, Number)> = None;
for (j, &i) in active.iter().enumerate() {
let lam = rhs[n + j];
let viol = match working.bounds[i] {
BoundStatus::AtLower => lam, BoundStatus::AtUpper => -lam, BoundStatus::Fixed => 0.0, BoundStatus::Inactive => unreachable!(),
};
if viol > worst.map(|(_, v)| v).unwrap_or(opts.opt_tol) {
worst = Some((i, viol));
}
}
if let Some((i_drop, _)) = worst {
working.bounds[i_drop] = BoundStatus::Inactive;
n_changes += 1;
continue;
}
let mut lambda_x = vec![0.0; n];
for (j, &i) in active.iter().enumerate() {
lambda_x[i] = -rhs[n + j];
}
return Ok(QpSolution {
obj: quad_objective(qp, &x),
x,
lambda_g: Vec::new(),
lambda_x,
working,
status: QpStatus::Optimal,
stats: QpStats {
n_working_set_changes: n_changes,
n_refactor,
n_schur_updates: 0,
used_phase1: false,
time: started.elapsed(),
},
});
}
let p: Vec<Number> = rhs[..n].to_vec();
let mut alpha = 1.0_f64;
let mut blocker: Option<(usize, BoundStatus)> = None;
for i in 0..n {
if working.bounds[i].is_active() {
continue;
}
if p[i] < -opts.feas_tol && qp.xl[i] > NLP_LOWER_BOUND_INF {
let r = (x[i] - qp.xl[i]) / -p[i];
if r < alpha {
alpha = r;
blocker = Some((i, BoundStatus::AtLower));
}
}
if p[i] > opts.feas_tol && qp.xu[i] < NLP_UPPER_BOUND_INF {
let r = (qp.xu[i] - x[i]) / p[i];
if r < alpha {
alpha = r;
blocker = Some((i, BoundStatus::AtUpper));
}
}
}
if alpha < 0.0 {
alpha = 0.0;
}
for i in 0..n {
x[i] += alpha * p[i];
}
if let Some((i_block, status)) = blocker {
match status {
BoundStatus::AtLower => x[i_block] = qp.xl[i_block],
BoundStatus::AtUpper => x[i_block] = qp.xu[i_block],
_ => unreachable!(),
}
working.bounds[i_block] = status;
n_changes += 1;
}
}
Ok(QpSolution {
obj: quad_objective(qp, &x),
x,
lambda_g: Vec::new(),
lambda_x: vec![0.0; n],
working,
status: QpStatus::MaxIter,
stats: QpStats {
n_working_set_changes: n_changes,
n_refactor,
n_schur_updates: 0,
used_phase1: false,
time: started.elapsed(),
},
})
}
fn solve_equality_plus_bounds(
&mut self,
qp: &QpProblem,
opts: &QpOptions,
) -> Result<QpSolution, QpError> {
let started = Instant::now();
let n = qp.n;
let m = qp.m;
let kkt0 = KktTriplet::assemble_equality_only(qp);
let mut rhs0 = rhs_equality_only(qp);
self.factorize_with_inertia_control(kkt0, &mut rhs0, m as i32, qp.n, opts)?;
let mut n_refactor: u32 = 1;
let mut n_changes: u32 = 0;
let mut x: Vec<Number> = rhs0[..n].to_vec();
for (i, &xi) in x.iter().enumerate() {
let l = qp.xl[i];
let u = qp.xu[i];
if (l > NLP_LOWER_BOUND_INF && xi < l - opts.feas_tol)
|| (u < NLP_UPPER_BOUND_INF && xi > u + opts.feas_tol)
{
return self.solve_elastic(qp, opts);
}
}
let mut working = WorkingSet::cold(n, m);
for c in working.constraints.iter_mut() {
*c = ConsStatus::Equality;
}
for (i, (status, xi)) in working.bounds.iter_mut().zip(x.iter_mut()).enumerate() {
let l = qp.xl[i];
let u = qp.xu[i];
let l_finite = l > NLP_LOWER_BOUND_INF;
let u_finite = u < NLP_UPPER_BOUND_INF;
if l_finite && u_finite && (l - u).abs() <= opts.feas_tol {
*status = BoundStatus::Fixed;
*xi = l;
} else if l_finite && (*xi - l).abs() <= opts.feas_tol {
*status = BoundStatus::AtLower;
*xi = l;
} else if u_finite && (*xi - u).abs() <= opts.feas_tol {
*status = BoundStatus::AtUpper;
*xi = u;
}
}
for _iter in 0..opts.max_iter {
let active: Vec<usize> = (0..n).filter(|&i| working.bounds[i].is_active()).collect();
let k = active.len();
let kkt = assemble_equality_plus_bounds(qp, &active);
let hx = h_times_x(qp.h, &x);
let mut rhs = vec![0.0; n + m + k];
for (rhs_i, (hx_i, &g_i)) in rhs[..n].iter_mut().zip(hx.iter().zip(qp.g.iter())) {
*rhs_i = -(hx_i + g_i);
}
self.factorize_with_inertia_control(kkt, &mut rhs, (m + k) as i32, qp.n, opts)?;
n_refactor += 1;
let p_inf = rhs[..n].iter().map(|pi| pi.abs()).fold(0.0, f64::max);
if p_inf <= opts.opt_tol {
let mut worst: Option<(usize, Number)> = None;
for (j, &i) in active.iter().enumerate() {
let lam = rhs[n + m + j];
let viol = match working.bounds[i] {
BoundStatus::AtLower => lam,
BoundStatus::AtUpper => -lam,
BoundStatus::Fixed => 0.0,
BoundStatus::Inactive => unreachable!(),
};
if viol > worst.map(|(_, v)| v).unwrap_or(opts.opt_tol) {
worst = Some((i, viol));
}
}
if let Some((i_drop, _)) = worst {
working.bounds[i_drop] = BoundStatus::Inactive;
n_changes += 1;
continue;
}
let lambda_g: Vec<Number> = rhs[n..n + m].to_vec();
let mut lambda_x = vec![0.0; n];
for (j, &i) in active.iter().enumerate() {
lambda_x[i] = -rhs[n + m + j];
}
return Ok(QpSolution {
obj: quad_objective(qp, &x),
x,
lambda_g,
lambda_x,
working,
status: QpStatus::Optimal,
stats: QpStats {
n_working_set_changes: n_changes,
n_refactor,
n_schur_updates: 0,
used_phase1: false,
time: started.elapsed(),
},
});
}
let p: Vec<Number> = rhs[..n].to_vec();
let mut alpha = 1.0_f64;
let mut blocker: Option<(usize, BoundStatus)> = None;
for i in 0..n {
if working.bounds[i].is_active() {
continue;
}
if p[i] < -opts.feas_tol && qp.xl[i] > NLP_LOWER_BOUND_INF {
let r = (x[i] - qp.xl[i]) / -p[i];
if r < alpha {
alpha = r;
blocker = Some((i, BoundStatus::AtLower));
}
}
if p[i] > opts.feas_tol && qp.xu[i] < NLP_UPPER_BOUND_INF {
let r = (qp.xu[i] - x[i]) / p[i];
if r < alpha {
alpha = r;
blocker = Some((i, BoundStatus::AtUpper));
}
}
}
if alpha < 0.0 {
alpha = 0.0;
}
for (xi, &pi) in x.iter_mut().zip(p.iter()) {
*xi += alpha * pi;
}
if let Some((i_block, status)) = blocker {
match status {
BoundStatus::AtLower => x[i_block] = qp.xl[i_block],
BoundStatus::AtUpper => x[i_block] = qp.xu[i_block],
_ => unreachable!(),
}
working.bounds[i_block] = status;
n_changes += 1;
}
}
Ok(QpSolution {
obj: quad_objective(qp, &x),
x,
lambda_g: vec![0.0; m],
lambda_x: vec![0.0; n],
working,
status: QpStatus::MaxIter,
stats: QpStats {
n_working_set_changes: n_changes,
n_refactor,
n_schur_updates: 0,
used_phase1: false,
time: started.elapsed(),
},
})
}
fn solve_equality_only(
&mut self,
qp: &QpProblem,
opts: &QpOptions,
) -> Result<QpSolution, QpError> {
let started = Instant::now();
let kkt = KktTriplet::assemble_equality_only(qp);
let mut rhs = rhs_equality_only(qp);
let delta = self.factorize_with_inertia_control(kkt, &mut rhs, qp.m as i32, qp.n, opts)?;
let mut x = vec![0.0; qp.n];
x.copy_from_slice(&rhs[..qp.n]);
let mut lambda_g = vec![0.0; qp.m];
lambda_g.copy_from_slice(&rhs[qp.n..]);
if delta > 0.0 {
let x_norm = x.iter().map(|v| v * v).sum::<Number>().sqrt();
let feasible_ray = if x_norm > 0.0 {
let inv = 1.0 / x_norm;
let mut ad = vec![0.0; qp.m];
let mut a_scale: Number = 0.0;
let irows = qp.a.irows();
let jcols = qp.a.jcols();
let vals = qp.a.values();
for k in 0..irows.len() {
let i = (irows[k] - 1) as usize;
let j = (jcols[k] - 1) as usize;
a_scale = a_scale.max(vals[k].abs());
ad[i] += vals[k] * x[j] * inv;
}
let ad_inf = ad.iter().map(|v| v.abs()).fold(0.0, f64::max);
ad_inf <= 1e-6 * (1.0 + a_scale)
} else {
false
};
if feasible_ray && ray_is_unbounded_descent(qp.h, qp.g, &x, &x) {
return Ok(QpSolution {
x,
lambda_g,
lambda_x: vec![0.0; qp.n],
working: WorkingSet::cold(qp.n, qp.m),
obj: Number::NEG_INFINITY,
status: QpStatus::Unbounded,
stats: QpStats {
n_working_set_changes: 0,
n_refactor: 1,
n_schur_updates: 0,
used_phase1: false,
time: started.elapsed(),
},
});
}
}
let obj = quad_objective(qp, &x);
let mut working = WorkingSet::cold(qp.n, qp.m);
for c in working.constraints.iter_mut() {
*c = ConsStatus::Equality;
}
let _ = opts;
Ok(QpSolution {
x,
lambda_g,
lambda_x: vec![0.0; qp.n],
working,
obj,
status: QpStatus::Optimal,
stats: QpStats {
n_working_set_changes: 0,
n_refactor: 1,
n_schur_updates: 0,
used_phase1: false,
time: started.elapsed(),
},
})
}
fn solve_general(
&mut self,
qp: &QpProblem,
ws: Option<&QpWarmStart>,
opts: &QpOptions,
) -> Result<QpSolution, QpError> {
let started = Instant::now();
let n = qp.n;
let m = qp.m;
let mut n_refactor: u32 = 0;
let mut n_changes: u32 = 0;
let (mut x, mut working) = if let Some(w) = ws {
(w.x.clone(), w.working.clone())
} else {
match self.cold_general_initial(qp, opts, &mut n_refactor)? {
Some(p) => p,
None => return self.solve_elastic(qp, opts),
}
};
for (i, &status) in working.bounds.iter().enumerate() {
match status {
BoundStatus::AtLower | BoundStatus::Fixed => x[i] = qp.xl[i],
BoundStatus::AtUpper => x[i] = qp.xu[i],
BoundStatus::Inactive => {}
}
}
let mut expand_tol = opts.expand_tol_initial;
let mut tabu_cons = vec![false; m];
let mut tabu_bounds = vec![false; n];
let mut force_bland = false;
let mut best_obj = Number::INFINITY;
let mut stall_iters: u32 = 0;
const STALL_LIMIT: u32 = 50;
for _iter in 0..opts.max_iter {
let active_cons: Vec<usize> = (0..m)
.filter(|&i| working.constraints[i].is_active())
.collect();
let active_bounds: Vec<usize> =
(0..n).filter(|&i| working.bounds[i].is_active()).collect();
let k_c = active_cons.len();
let k_b = active_bounds.len();
let kkt = assemble_active_set_kkt(qp, &active_cons, &active_bounds);
let hx = h_times_x(qp.h, &x);
let mut rhs = vec![0.0; n + k_c + k_b];
for (rhs_i, (hx_i, &g_i)) in rhs[..n].iter_mut().zip(hx.iter().zip(qp.g.iter())) {
*rhs_i = -(hx_i + g_i);
}
let delta = match self.factorize_with_inertia_control(
kkt,
&mut rhs,
(k_c + k_b) as i32,
qp.n,
opts,
) {
Ok(d) => d,
Err(e) if e.is_recoverable_factorization_failure() => {
let (kc, kb) = independent_active_subset(
&mut self.linsol,
qp,
&active_cons,
&active_bounds,
);
if kc.len() == active_cons.len() && kb.len() == active_bounds.len() {
return Err(e);
}
let mut keep_c = vec![false; m];
for &i in &kc {
keep_c[i] = true;
}
let mut keep_b = vec![false; n];
for &i in &kb {
keep_b[i] = true;
}
for &i in &active_cons {
if !keep_c[i] {
working.constraints[i] = ConsStatus::Inactive;
tabu_cons[i] = true;
n_changes += 1;
}
}
for &i in &active_bounds {
if !keep_b[i] {
working.bounds[i] = BoundStatus::Inactive;
tabu_bounds[i] = true;
n_changes += 1;
}
}
continue;
}
Err(e) => return Err(e),
};
n_refactor += 1;
let p_inf = rhs[..n].iter().map(|pi| pi.abs()).fold(0.0, f64::max);
if p_inf <= opts.opt_tol {
let use_bland =
force_bland || matches!(opts.anti_cycling, AntiCyclingChoice::Bland);
let mut worst: Option<(DropTarget, Number)> = None;
let consider =
|worst: &mut Option<(DropTarget, Number)>, target: DropTarget, viol: Number| {
if viol <= opts.opt_tol {
return;
}
let take = match *worst {
None => true,
Some((prev_target, prev_viol)) => {
if use_bland {
let new_key = drop_target_key(target);
let prev_key = drop_target_key(prev_target);
new_key < prev_key
} else {
viol > prev_viol
}
}
};
if take {
*worst = Some((target, viol));
}
};
for (j, &i) in active_cons.iter().enumerate() {
let lam = rhs[n + j];
let viol = match working.constraints[i] {
ConsStatus::AtLower => lam,
ConsStatus::AtUpper => -lam,
ConsStatus::Equality => 0.0,
ConsStatus::Inactive => unreachable!(),
};
consider(&mut worst, DropTarget::Cons(i), viol);
}
for (j, &i) in active_bounds.iter().enumerate() {
let lam = rhs[n + k_c + j];
let viol = match working.bounds[i] {
BoundStatus::AtLower => lam,
BoundStatus::AtUpper => -lam,
BoundStatus::Fixed => 0.0,
BoundStatus::Inactive => unreachable!(),
};
consider(&mut worst, DropTarget::Bound(i), viol);
}
if let Some((target, _viol)) = worst {
match target {
DropTarget::Cons(i) => working.constraints[i] = ConsStatus::Inactive,
DropTarget::Bound(i) => working.bounds[i] = BoundStatus::Inactive,
}
n_changes += 1;
continue;
}
let mut lambda_g = vec![0.0; m];
for (j, &i) in active_cons.iter().enumerate() {
lambda_g[i] = rhs[n + j];
}
let mut lambda_x = vec![0.0; n];
for (j, &i) in active_bounds.iter().enumerate() {
lambda_x[i] = -rhs[n + k_c + j];
}
return Ok(QpSolution {
obj: quad_objective(qp, &x),
x,
lambda_g,
lambda_x,
working,
status: QpStatus::Optimal,
stats: QpStats {
n_working_set_changes: n_changes,
n_refactor,
n_schur_updates: 0,
used_phase1: false,
time: started.elapsed(),
},
});
}
let p: Vec<Number> = rhs[..n].to_vec();
let ap = a_times_x(qp.a, &p, m);
let ax = a_times_x(qp.a, &x, m);
let mut candidates: Vec<(BlockerTarget, f64, f64)> = Vec::new();
for i in 0..n {
if working.bounds[i].is_active() {
continue;
}
if tabu_bounds[i] && p[i].abs() <= TABU_DRIFT_REL * p_inf {
continue;
}
if p[i] < -opts.feas_tol && qp.xl[i] > NLP_LOWER_BOUND_INF {
let r = (x[i] - qp.xl[i]) / -p[i];
candidates.push((BlockerTarget::Bound(i, BoundStatus::AtLower), r, p[i].abs()));
}
if p[i] > opts.feas_tol && qp.xu[i] < NLP_UPPER_BOUND_INF {
let r = (qp.xu[i] - x[i]) / p[i];
candidates.push((BlockerTarget::Bound(i, BoundStatus::AtUpper), r, p[i].abs()));
}
}
for i in 0..m {
if working.constraints[i].is_active() {
continue;
}
if qp.bl[i] == qp.bu[i] {
continue;
}
if tabu_cons[i] && ap[i].abs() <= TABU_DRIFT_REL * p_inf {
continue;
}
if ap[i] < -opts.feas_tol && qp.bl[i] > NLP_LOWER_BOUND_INF {
let r = (ax[i] - qp.bl[i]) / -ap[i];
candidates.push((BlockerTarget::Cons(i, ConsStatus::AtLower), r, ap[i].abs()));
}
if ap[i] > opts.feas_tol && qp.bu[i] < NLP_UPPER_BOUND_INF {
let r = (qp.bu[i] - ax[i]) / ap[i];
candidates.push((BlockerTarget::Cons(i, ConsStatus::AtUpper), r, ap[i].abs()));
}
}
let (mut alpha, blocker) = select_blocker(&candidates, opts, expand_tol, force_bland);
if candidates.is_empty() && delta > 0.0 && ray_is_unbounded_descent(qp.h, qp.g, &x, &p)
{
return Ok(QpSolution {
obj: Number::NEG_INFINITY,
x,
lambda_g: vec![0.0; m],
lambda_x: vec![0.0; n],
working,
status: QpStatus::Unbounded,
stats: QpStats {
n_working_set_changes: n_changes,
n_refactor,
n_schur_updates: 0,
used_phase1: false,
time: started.elapsed(),
},
});
}
if alpha < 0.0 {
alpha = 0.0;
}
if alpha > opts.feas_tol {
tabu_cons.iter_mut().for_each(|t| *t = false);
tabu_bounds.iter_mut().for_each(|t| *t = false);
}
for (xi, &pi) in x.iter_mut().zip(p.iter()) {
*xi += alpha * pi;
}
if let Some(blk) = blocker {
match blk {
BlockerTarget::Bound(i, status) => {
match status {
BoundStatus::AtLower => x[i] = qp.xl[i],
BoundStatus::AtUpper => x[i] = qp.xu[i],
_ => unreachable!(),
}
working.bounds[i] = status;
}
BlockerTarget::Cons(i, status) => {
working.constraints[i] = status;
}
}
n_changes += 1;
}
if matches!(opts.anti_cycling, AntiCyclingChoice::Expand) && blocker.is_some() {
expand_tol += opts.expand_tol_growth;
}
if expand_tol > opts.expand_tol_max {
for (i, &status) in working.bounds.iter().enumerate() {
match status {
BoundStatus::AtLower | BoundStatus::Fixed => x[i] = qp.xl[i],
BoundStatus::AtUpper => x[i] = qp.xu[i],
BoundStatus::Inactive => {}
}
}
expand_tol = opts.expand_tol_initial;
}
if !force_bland {
let obj_now = quad_objective(qp, &x);
let improved = obj_now < best_obj - 1e-9 * best_obj.abs() - 1e-12;
if improved {
best_obj = obj_now;
stall_iters = 0;
} else {
stall_iters += 1;
if stall_iters >= STALL_LIMIT {
force_bland = true;
}
}
}
}
Ok(QpSolution {
obj: quad_objective(qp, &x),
x,
lambda_g: vec![0.0; m],
lambda_x: vec![0.0; n],
working,
status: QpStatus::MaxIter,
stats: QpStats {
n_working_set_changes: n_changes,
n_refactor,
n_schur_updates: 0,
used_phase1: false,
time: started.elapsed(),
},
})
}
fn cold_general_initial(
&mut self,
qp: &QpProblem,
opts: &QpOptions,
n_refactor: &mut u32,
) -> Result<Option<(Vec<Number>, WorkingSet)>, QpError> {
let n = qp.n;
let m = qp.m;
let eq_rows: Vec<usize> = (0..m).filter(|&i| qp.bl[i] == qp.bu[i]).collect();
let eq_targets: Vec<Number> = eq_rows.iter().map(|&r| qp.bl[r]).collect();
let (x, kept_eq): (Vec<Number>, Vec<usize>) =
match self.factor_pinned_primal(qp, &eq_rows, &eq_targets, &[], &[], opts) {
Ok(x) => (x, eq_rows.clone()),
Err(e) if e.is_recoverable_factorization_failure() => {
let (kept, _) = independent_active_subset(&mut self.linsol, qp, &eq_rows, &[]);
if kept.len() == eq_rows.len() {
return Err(e);
}
let kept_targets: Vec<Number> = kept.iter().map(|&r| qp.bl[r]).collect();
let x = self.factor_pinned_primal(qp, &kept, &kept_targets, &[], &[], opts)?;
(x, kept)
}
Err(e) => return Err(e),
};
*n_refactor += 1;
let ax = a_times_x(qp.a, &x, m);
for i in 0..m {
if qp.bl[i] == qp.bu[i] {
continue;
}
if qp.bl[i] > NLP_LOWER_BOUND_INF && ax[i] < qp.bl[i] - opts.feas_tol {
return Ok(None);
}
if qp.bu[i] < NLP_UPPER_BOUND_INF && ax[i] > qp.bu[i] + opts.feas_tol {
return Ok(None);
}
}
for (i, &xi) in x.iter().enumerate() {
if qp.xl[i] > NLP_LOWER_BOUND_INF && xi < qp.xl[i] - opts.feas_tol {
return Ok(None);
}
if qp.xu[i] < NLP_UPPER_BOUND_INF && xi > qp.xu[i] + opts.feas_tol {
return Ok(None);
}
}
let mut working = WorkingSet::cold(n, m);
let mut kept_eq_flag = vec![false; m];
for &r in &kept_eq {
kept_eq_flag[r] = true;
}
for (i, c) in working.constraints.iter_mut().enumerate() {
if qp.bl[i] == qp.bu[i] {
if kept_eq_flag[i] {
*c = ConsStatus::Equality;
}
} else if qp.bl[i] > NLP_LOWER_BOUND_INF && (ax[i] - qp.bl[i]).abs() <= opts.feas_tol {
*c = ConsStatus::AtLower;
} else if qp.bu[i] < NLP_UPPER_BOUND_INF && (ax[i] - qp.bu[i]).abs() <= opts.feas_tol {
*c = ConsStatus::AtUpper;
}
}
for (i, status) in working.bounds.iter_mut().enumerate() {
let l = qp.xl[i];
let u = qp.xu[i];
let l_finite = l > NLP_LOWER_BOUND_INF;
let u_finite = u < NLP_UPPER_BOUND_INF;
if l_finite && u_finite && (l - u).abs() <= opts.feas_tol {
*status = BoundStatus::Fixed;
} else if l_finite && (x[i] - l).abs() <= opts.feas_tol {
*status = BoundStatus::AtLower;
} else if u_finite && (x[i] - u).abs() <= opts.feas_tol {
*status = BoundStatus::AtUpper;
}
}
Ok(Some((x, working)))
}
fn solve_elastic(&mut self, qp: &QpProblem, opts: &QpOptions) -> Result<QpSolution, QpError> {
let started = Instant::now();
let n = qp.n;
let m = qp.m;
let reform = crate::elastic::ElasticReformulation::build(qp, opts.elastic_gamma);
let qp_aug = reform.as_qp();
let mut x_orig = vec![0.0; n];
for (xi, (&l, &u)) in x_orig.iter_mut().zip(qp.xl.iter().zip(qp.xu.iter())) {
if l > NLP_LOWER_BOUND_INF && *xi < l {
*xi = l;
}
if u < NLP_UPPER_BOUND_INF && *xi > u {
*xi = u;
}
}
let (x_aug, working_aug) = reform.initial_seed(qp, &x_orig, opts.feas_tol);
let ws = QpWarmStart {
x: x_aug,
lambda_g: vec![0.0; reform.m_aug],
lambda_x: vec![0.0; reform.n_aug],
working: working_aug,
};
let mut opts_p1 = opts.clone();
opts_p1.anti_cycling = AntiCyclingChoice::Bland;
let opts = &opts_p1;
let sol_aug = if opts.use_schur_updates {
self.solve_general_schur(&qp_aug, Some(&ws), opts)?
} else {
self.solve_general(&qp_aug, Some(&ws), opts)?
};
let x = sol_aug.x[..n].to_vec();
let lambda_g = sol_aug.lambda_g.clone();
let lambda_x = sol_aug.lambda_x[..n].to_vec();
let mut working = WorkingSet::cold(n, m);
working
.constraints
.copy_from_slice(&sol_aug.working.constraints);
working.bounds.copy_from_slice(&sol_aug.working.bounds[..n]);
let feasible = reform.is_feasible(&sol_aug.x, opts.feas_tol);
let status = if feasible {
QpStatus::Optimal
} else {
QpStatus::Infeasible
};
let obj = quad_objective(qp, &x);
Ok(QpSolution {
x,
lambda_g,
lambda_x,
working,
obj,
status,
stats: QpStats {
n_working_set_changes: sol_aug.stats.n_working_set_changes,
n_refactor: sol_aug.stats.n_refactor,
n_schur_updates: sol_aug.stats.n_schur_updates,
used_phase1: true,
time: started.elapsed(),
},
})
}
fn solve_general_schur(
&mut self,
qp: &QpProblem,
ws: Option<&QpWarmStart>,
opts: &QpOptions,
) -> Result<QpSolution, QpError> {
let started = Instant::now();
let n = qp.n;
let m = qp.m;
let m_total = m + n;
let mut n_refactor: u32 = 0;
let mut n_changes: u32 = 0;
let mut n_schur_updates: u32 = 0;
let (mut x, mut working) = if let Some(w) = ws {
(w.x.clone(), w.working.clone())
} else {
match self.cold_general_initial(qp, opts, &mut n_refactor)? {
Some(p) => p,
None => return self.solve_elastic(qp, opts),
}
};
for (i, &status) in working.bounds.iter().enumerate() {
match status {
BoundStatus::AtLower | BoundStatus::Fixed => x[i] = qp.xl[i],
BoundStatus::AtUpper => x[i] = qp.xu[i],
BoundStatus::Inactive => {}
}
}
let mut schur = crate::schur::SchurState::new(n, m);
let active_count = active_slot_count(&working);
schur.reset(&mut self.linsol, qp, &working, active_count as i32, opts)?;
n_refactor += 1;
let mut expand_tol = opts.expand_tol_initial;
for _iter in 0..opts.max_iter {
let hx = h_times_x(qp.h, &x);
let mut rhs = vec![0.0; n + m_total];
for (rhs_i, (hx_i, &g_i)) in rhs[..n].iter_mut().zip(hx.iter().zip(qp.g.iter())) {
*rhs_i = -(hx_i + g_i);
}
schur.solve(&mut self.linsol, &mut rhs)?;
let p: Vec<Number> = rhs[..n].to_vec();
let p_inf = p.iter().map(|pi| pi.abs()).fold(0.0, f64::max);
if p_inf <= opts.opt_tol {
let mut worst: Option<(DropTarget, Number)> = None;
for slot in 0..m_total {
if !crate::schur::SchurState::slot_active(&working, slot) {
continue;
}
let lam = rhs[n + slot];
let (target, viol) = if slot < m {
let v = match working.constraints[slot] {
ConsStatus::AtLower => lam,
ConsStatus::AtUpper => -lam,
ConsStatus::Equality => 0.0,
ConsStatus::Inactive => unreachable!(),
};
(DropTarget::Cons(slot), v)
} else {
let var = slot - m;
let v = match working.bounds[var] {
BoundStatus::AtLower => lam,
BoundStatus::AtUpper => -lam,
BoundStatus::Fixed => 0.0,
BoundStatus::Inactive => unreachable!(),
};
(DropTarget::Bound(var), v)
};
if viol > worst.map(|(_, w)| w).unwrap_or(opts.opt_tol) {
worst = Some((target, viol));
}
}
if let Some((target, _)) = worst {
let slot = match target {
DropTarget::Cons(i) => {
working.constraints[i] = ConsStatus::Inactive;
i
}
DropTarget::Bound(i) => {
working.bounds[i] = BoundStatus::Inactive;
m + i
}
};
schur.apply_change(&mut self.linsol, qp, slot, false)?;
n_changes += 1;
n_schur_updates += 1;
if schur.needs_reset(opts) {
let ac = active_slot_count(&working);
schur.reset(&mut self.linsol, qp, &working, ac as i32, opts)?;
n_refactor += 1;
}
continue;
}
let mut lambda_g = vec![0.0; m];
for s in 0..m {
if working.constraints[s].is_active() {
lambda_g[s] = rhs[n + s];
}
}
let mut lambda_x = vec![0.0; n];
for j in 0..n {
if working.bounds[j].is_active() {
lambda_x[j] = -rhs[n + m + j];
}
}
return Ok(QpSolution {
obj: quad_objective(qp, &x),
x,
lambda_g,
lambda_x,
working,
status: QpStatus::Optimal,
stats: QpStats {
n_working_set_changes: n_changes,
n_refactor,
n_schur_updates,
used_phase1: false,
time: started.elapsed(),
},
});
}
let ap = a_times_x(qp.a, &p, m);
let ax = a_times_x(qp.a, &x, m);
let mut candidates: Vec<(BlockerTarget, Number, Number)> = Vec::new();
for i in 0..n {
if working.bounds[i].is_active() {
continue;
}
if p[i] < -opts.feas_tol && qp.xl[i] > NLP_LOWER_BOUND_INF {
let r = (x[i] - qp.xl[i]) / -p[i];
candidates.push((BlockerTarget::Bound(i, BoundStatus::AtLower), r, p[i].abs()));
}
if p[i] > opts.feas_tol && qp.xu[i] < NLP_UPPER_BOUND_INF {
let r = (qp.xu[i] - x[i]) / p[i];
candidates.push((BlockerTarget::Bound(i, BoundStatus::AtUpper), r, p[i].abs()));
}
}
for i in 0..m {
if working.constraints[i].is_active() {
continue;
}
if qp.bl[i] == qp.bu[i] {
continue;
}
if ap[i] < -opts.feas_tol && qp.bl[i] > NLP_LOWER_BOUND_INF {
let r = (ax[i] - qp.bl[i]) / -ap[i];
candidates.push((BlockerTarget::Cons(i, ConsStatus::AtLower), r, ap[i].abs()));
}
if ap[i] > opts.feas_tol && qp.bu[i] < NLP_UPPER_BOUND_INF {
let r = (qp.bu[i] - ax[i]) / ap[i];
candidates.push((BlockerTarget::Cons(i, ConsStatus::AtUpper), r, ap[i].abs()));
}
}
let (mut alpha, blocker) = select_blocker(&candidates, opts, expand_tol, false);
if candidates.is_empty() && ray_is_unbounded_descent(qp.h, qp.g, &x, &p) {
return Ok(QpSolution {
obj: Number::NEG_INFINITY,
x,
lambda_g: vec![0.0; m],
lambda_x: vec![0.0; n],
working,
status: QpStatus::Unbounded,
stats: QpStats {
n_working_set_changes: n_changes,
n_refactor,
n_schur_updates,
used_phase1: false,
time: started.elapsed(),
},
});
}
if alpha < 0.0 {
alpha = 0.0;
}
for (xi, &pi) in x.iter_mut().zip(p.iter()) {
*xi += alpha * pi;
}
if let Some(blk) = blocker {
let slot = match blk {
BlockerTarget::Bound(i, status) => {
match status {
BoundStatus::AtLower => x[i] = qp.xl[i],
BoundStatus::AtUpper => x[i] = qp.xu[i],
_ => unreachable!(),
}
working.bounds[i] = status;
m + i
}
BlockerTarget::Cons(i, status) => {
working.constraints[i] = status;
i
}
};
schur.apply_change(&mut self.linsol, qp, slot, true)?;
n_changes += 1;
n_schur_updates += 1;
if schur.needs_reset(opts) {
let ac = active_slot_count(&working);
schur.reset(&mut self.linsol, qp, &working, ac as i32, opts)?;
n_refactor += 1;
}
}
if matches!(opts.anti_cycling, AntiCyclingChoice::Expand) && blocker.is_some() {
expand_tol += opts.expand_tol_growth;
}
if expand_tol > opts.expand_tol_max {
for (i, &status) in working.bounds.iter().enumerate() {
match status {
BoundStatus::AtLower | BoundStatus::Fixed => x[i] = qp.xl[i],
BoundStatus::AtUpper => x[i] = qp.xu[i],
BoundStatus::Inactive => {}
}
}
expand_tol = opts.expand_tol_initial;
}
}
Ok(QpSolution {
obj: quad_objective(qp, &x),
x,
lambda_g: vec![0.0; m],
lambda_x: vec![0.0; n],
working,
status: QpStatus::MaxIter,
stats: QpStats {
n_working_set_changes: n_changes,
n_refactor,
n_schur_updates,
used_phase1: false,
time: started.elapsed(),
},
})
}
}
fn active_slot_count(working: &WorkingSet) -> usize {
working.constraints.iter().filter(|s| s.is_active()).count()
+ working.bounds.iter().filter(|s| s.is_active()).count()
}
const RANK_REL_TOL: Number = 1e-9;
const TABU_DRIFT_REL: Number = 1e-7;
fn independent_active_subset(
linsol: &mut LinearSolver,
qp: &QpProblem,
active_cons: &[usize],
active_bounds: &[usize],
) -> (Vec<usize>, Vec<usize>) {
if linsol.provides_degeneracy_detection() {
if let Some(kept) = independent_active_subset_sparse(linsol, qp, active_cons, active_bounds)
{
return kept;
}
}
independent_active_subset_dense(qp, active_cons, active_bounds)
}
fn independent_active_subset_sparse(
linsol: &mut LinearSolver,
qp: &QpProblem,
active_cons: &[usize],
active_bounds: &[usize],
) -> Option<(Vec<usize>, Vec<usize>)> {
let n_cols = qp.n;
let n_c = active_cons.len();
let n_b = active_bounds.len();
let n_rows = n_c + n_b;
if n_rows == 0 {
return Some((Vec::new(), Vec::new()));
}
let mut j_row_of_con: Vec<Option<usize>> = vec![None; qp.m];
for (pos, &row) in active_cons.iter().enumerate() {
j_row_of_con[row] = Some(pos);
}
let mut irn: Vec<Index> = Vec::new();
let mut jcn: Vec<Index> = Vec::new();
let mut vals: Vec<Number> = Vec::new();
let a_irows = qp.a.irows();
let a_jcols = qp.a.jcols();
let a_vals = qp.a.values();
for k in 0..a_irows.len() {
let row = (a_irows[k] - 1) as usize;
if let Some(pos) = j_row_of_con[row] {
let col = (a_jcols[k] - 1) as usize;
irn.push((pos + 1) as Index);
jcn.push((col + 1) as Index);
vals.push(a_vals[k]);
}
}
for (b, &var) in active_bounds.iter().enumerate() {
irn.push((n_c + b + 1) as Index);
jcn.push((var + 1) as Index);
vals.push(1.0);
}
let mut c_deps: Vec<Index> = Vec::new();
let st = linsol.determine_dependent_rows(
n_rows as Index,
n_cols as Index,
&irn,
&jcn,
&vals,
&mut c_deps,
);
if st != ESymSolverStatus::Success {
return None;
}
let mut dropped = vec![false; n_rows];
for &d in &c_deps {
let d = d as usize;
if d < n_rows {
dropped[d] = true;
}
}
let mut keep_cons = Vec::with_capacity(n_c);
for (pos, &row) in active_cons.iter().enumerate() {
if !dropped[pos] {
keep_cons.push(row);
}
}
let mut keep_bounds = Vec::with_capacity(n_b);
for (b, &var) in active_bounds.iter().enumerate() {
if !dropped[n_c + b] {
keep_bounds.push(var);
}
}
Some((keep_cons, keep_bounds))
}
fn independent_active_subset_dense(
qp: &QpProblem,
active_cons: &[usize],
active_bounds: &[usize],
) -> (Vec<usize>, Vec<usize>) {
let n = qp.n;
let mut pos_of_row: Vec<Option<usize>> = vec![None; qp.m];
for (pos, &row) in active_cons.iter().enumerate() {
pos_of_row[row] = Some(pos);
}
let mut cons_normals = vec![vec![0.0; n]; active_cons.len()];
let a_irows = qp.a.irows();
let a_jcols = qp.a.jcols();
let a_vals = qp.a.values();
for k in 0..a_irows.len() {
let row = (a_irows[k] - 1) as usize;
if let Some(pos) = pos_of_row[row] {
let col = (a_jcols[k] - 1) as usize;
cons_normals[pos][col] += a_vals[k];
}
}
let mut basis: Vec<Vec<Number>> = Vec::new();
let mut keep_cons = Vec::new();
let mut keep_bounds = Vec::new();
for (pos, &row) in active_cons.iter().enumerate() {
let mut v = std::mem::take(&mut cons_normals[pos]);
if accept_if_independent(&mut v, &mut basis) {
keep_cons.push(row);
}
}
for &var in active_bounds {
let mut v = vec![0.0; n];
v[var] = 1.0;
if accept_if_independent(&mut v, &mut basis) {
keep_bounds.push(var);
}
}
(keep_cons, keep_bounds)
}
fn accept_if_independent(v: &mut [Number], basis: &mut Vec<Vec<Number>>) -> bool {
let orig = dot(v, v).sqrt();
if orig == 0.0 {
return false;
}
for _pass in 0..2 {
for q in basis.iter() {
let d = dot(q, v);
if d != 0.0 {
for (vi, &qi) in v.iter_mut().zip(q.iter()) {
*vi -= d * qi;
}
}
}
}
let r = dot(v, v).sqrt();
if r > RANK_REL_TOL * orig {
let inv = 1.0 / r;
basis.push(v.iter().map(|&vi| vi * inv).collect());
true
} else {
false
}
}
fn dot(a: &[Number], b: &[Number]) -> Number {
a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()
}
#[derive(Clone, Copy)]
enum DropTarget {
Cons(usize),
Bound(usize),
}
fn drop_target_key(t: DropTarget) -> (u8, usize) {
match t {
DropTarget::Cons(i) => (0, i),
DropTarget::Bound(i) => (1, i),
}
}
#[derive(Clone, Copy)]
enum BlockerTarget {
Cons(usize, ConsStatus),
Bound(usize, BoundStatus),
}
fn blocker_index_key(b: BlockerTarget) -> (u8, usize) {
match b {
BlockerTarget::Cons(i, _) => (0, i),
BlockerTarget::Bound(i, _) => (1, i),
}
}
fn select_blocker(
candidates: &[(BlockerTarget, f64, f64)],
opts: &QpOptions,
expand_tol: f64,
force_bland: bool,
) -> (f64, Option<BlockerTarget>) {
if candidates.is_empty() {
return (1.0, None);
}
let mut alpha_min = 1.0_f64;
let mut alpha_min_relaxed = 1.0_f64;
for &(_, r, ap_mag) in candidates {
if r < alpha_min {
alpha_min = r;
}
let r_relaxed = if ap_mag > 0.0 {
r + expand_tol / ap_mag
} else {
r
};
if r_relaxed < alpha_min_relaxed {
alpha_min_relaxed = r_relaxed;
}
}
if alpha_min >= 1.0 {
return (1.0, None);
}
let effective = if force_bland {
AntiCyclingChoice::Bland
} else {
opts.anti_cycling
};
match effective {
AntiCyclingChoice::None | AntiCyclingChoice::Bland => {
let mut best: Option<(BlockerTarget, f64)> = None;
for &(target, r, _) in candidates {
if r > alpha_min {
continue;
}
if best.is_none() {
best = Some((target, r));
}
}
let (target, r) = best.expect("non-empty candidates above");
(r, Some(target))
}
AntiCyclingChoice::Expand => {
let tol = opts.feas_tol * (1.0 + alpha_min_relaxed.abs());
let mut best: Option<(BlockerTarget, f64, f64)> = None;
for &(target, r, ap_mag) in candidates {
let r_relaxed = if ap_mag > 0.0 {
r + expand_tol / ap_mag
} else {
r
};
if r_relaxed > alpha_min_relaxed + tol {
continue;
}
let take = match best {
None => true,
Some((prev_target, _, prev_ap)) => {
if ap_mag > prev_ap {
true
} else if ap_mag == prev_ap {
blocker_index_key(target) < blocker_index_key(prev_target)
} else {
false
}
}
};
if take {
best = Some((target, r, ap_mag));
}
}
match best {
Some((target, r, _)) => {
let alpha = r.max(alpha_min_relaxed).min(1.0).max(0.0);
(alpha, Some(target))
}
None => {
let mut fb: Option<BlockerTarget> = None;
for &(target, r, _) in candidates {
if r <= alpha_min {
fb = Some(target);
break;
}
}
(alpha_min, fb)
}
}
}
}
}
impl QpSolver for ParametricActiveSetSolver {
fn solve(
&mut self,
qp: &QpProblem,
ws: Option<&QpWarmStart>,
opts: &QpOptions,
) -> Result<QpSolution, QpError> {
qp.validate()?;
if let Some(w) = ws {
w.working.validate_dims(qp.n, qp.m)?;
if w.x.len() != qp.n {
return Err(QpError::WarmStartDimensionMismatch(format!(
"ws.x.len() = {} but n = {}",
w.x.len(),
qp.n
)));
}
}
if let Some(w) = ws {
if !point_is_feasible(qp, &w.x, opts.feas_tol) {
return self.solve_elastic(qp, opts);
}
}
let has_general_inequality = !is_all_equality_constraints(qp);
if ws.is_some() || has_general_inequality {
let sol = if opts.use_schur_updates {
self.solve_general_schur(qp, ws, opts)?
} else {
self.solve_general(qp, ws, opts)?
};
if matches!(sol.status, QpStatus::Optimal)
&& !point_is_feasible(qp, &sol.x, opts.feas_tol)
{
return self.solve_elastic(qp, opts);
}
return Ok(sol);
}
if is_pure_equality_no_bounds(qp) {
return self.solve_equality_only(qp, opts);
}
if is_pure_box(qp) {
return self.solve_box_constrained(qp, opts);
}
self.solve_equality_plus_bounds(qp, opts)
}
fn solve_parametric(
&mut self,
_qp_prev: &QpProblem,
_sol_prev: &QpSolution,
qp_new: &QpProblem,
opts: &QpOptions,
) -> Result<QpSolution, QpError> {
self.solve(qp_new, None, opts)
}
fn solve_with_working_set(
&mut self,
qp: &QpProblem,
working: &crate::working_set::WorkingSet,
opts: &QpOptions,
) -> Result<QpSolution, QpError> {
qp.validate()?;
working.validate_dims(qp.n, qp.m)?;
let active_cons: Vec<usize> = (0..qp.m)
.filter(|&i| working.constraints[i].is_active())
.collect();
let active_bounds: Vec<usize> = (0..qp.n)
.filter(|&i| working.bounds[i].is_active())
.collect();
let cons_target = |i: usize| match working.constraints[i] {
ConsStatus::AtLower | ConsStatus::Equality => qp.bl[i],
ConsStatus::AtUpper => qp.bu[i],
ConsStatus::Inactive => unreachable!(),
};
let bound_target = |i: usize| match working.bounds[i] {
BoundStatus::AtLower | BoundStatus::Fixed => qp.xl[i],
BoundStatus::AtUpper => qp.xu[i],
BoundStatus::Inactive => unreachable!(),
};
let cons_targets: Vec<Number> = active_cons.iter().map(|&i| cons_target(i)).collect();
let bound_targets: Vec<Number> = active_bounds.iter().map(|&i| bound_target(i)).collect();
let (x_init, fwd_working) = match self.factor_pinned_primal(
qp,
&active_cons,
&cons_targets,
&active_bounds,
&bound_targets,
opts,
) {
Ok(x) => (x, working.clone()),
Err(e) if e.is_recoverable_factorization_failure() => {
let (kc, kb) =
independent_active_subset(&mut self.linsol, qp, &active_cons, &active_bounds);
if kc.len() == active_cons.len() && kb.len() == active_bounds.len() {
return Err(e);
}
let kc_targets: Vec<Number> = kc.iter().map(|&i| cons_target(i)).collect();
let kb_targets: Vec<Number> = kb.iter().map(|&i| bound_target(i)).collect();
let x = self.factor_pinned_primal(qp, &kc, &kc_targets, &kb, &kb_targets, opts)?;
let mut fwd = working.clone();
let mut keep_c = vec![false; qp.m];
for &i in &kc {
keep_c[i] = true;
}
let mut keep_b = vec![false; qp.n];
for &i in &kb {
keep_b[i] = true;
}
for i in 0..qp.m {
if working.constraints[i].is_active() && !keep_c[i] {
fwd.constraints[i] = ConsStatus::Inactive;
}
}
for i in 0..qp.n {
if working.bounds[i].is_active() && !keep_b[i] {
fwd.bounds[i] = BoundStatus::Inactive;
}
}
(x, fwd)
}
Err(e) => return Err(e),
};
let ws = QpWarmStart {
x: x_init,
lambda_g: vec![0.0; qp.m],
lambda_x: vec![0.0; qp.n],
working: fwd_working,
};
self.solve(qp, Some(&ws), opts)
}
}
fn point_is_feasible(qp: &QpProblem, x: &[Number], feas_tol: Number) -> bool {
let ax = a_times_x(qp.a, x, qp.m);
for i in 0..qp.m {
if qp.bl[i] > NLP_LOWER_BOUND_INF && ax[i] < qp.bl[i] - feas_tol {
return false;
}
if qp.bu[i] < NLP_UPPER_BOUND_INF && ax[i] > qp.bu[i] + feas_tol {
return false;
}
}
for (i, &xi) in x.iter().enumerate() {
if qp.xl[i] > NLP_LOWER_BOUND_INF && xi < qp.xl[i] - feas_tol {
return false;
}
if qp.xu[i] < NLP_UPPER_BOUND_INF && xi > qp.xu[i] + feas_tol {
return false;
}
}
true
}
fn ray_is_unbounded_descent(
h: &pounce_linalg::triplet::SymTMatrix,
g: &[Number],
x_cand: &[Number],
dir: &[Number],
) -> bool {
let norm = dir.iter().map(|v| v * v).sum::<Number>().sqrt();
if norm == 0.0 {
return false;
}
let inv = 1.0 / norm;
let n = dir.len();
let mut hd = vec![0.0; n];
let mut hx = vec![0.0; n];
let mut h_scale: Number = 0.0;
let irows = h.irows();
let jcols = h.jcols();
let vals = h.values();
for k in 0..irows.len() {
let i = (irows[k] - 1) as usize;
let j = (jcols[k] - 1) as usize;
let v = vals[k];
h_scale = h_scale.max(v.abs());
hd[i] += v * dir[j] * inv;
hx[i] += v * x_cand[j];
if i != j {
hd[j] += v * dir[i] * inv;
hx[j] += v * x_cand[i];
}
}
let hd_inf = hd.iter().fold(0.0_f64, |a, v| a.max(v.abs()));
let zero_curvature = if h_scale > 0.0 {
hd_inf <= 1e-10 * h_scale
} else {
true };
let slope: Number = g
.iter()
.zip(hx.iter())
.zip(dir.iter())
.map(|((&gi, &hxi), &di)| (gi + hxi) * di * inv)
.sum();
let g_norm = g.iter().map(|v| v * v).sum::<Number>().sqrt();
let descent = slope < -1e-6 * g_norm.max(1.0);
zero_curvature && descent
}
fn quad_objective(qp: &QpProblem, x: &[Number]) -> Number {
let mut quad = 0.0;
let irows = qp.h.irows();
let jcols = qp.h.jcols();
let vals = qp.h.values();
for k in 0..irows.len() {
let i = (irows[k] - 1) as usize;
let j = (jcols[k] - 1) as usize;
let v = vals[k];
if i == j {
quad += 0.5 * v * x[i] * x[i];
} else {
quad += v * x[i] * x[j]; }
}
let lin: Number = qp.g.iter().zip(x.iter()).map(|(&gi, &xi)| gi * xi).sum();
quad + lin
}
#[cfg(test)]
mod select_blocker_tests {
use super::{select_blocker, BlockerTarget};
use crate::options::{AntiCyclingChoice, QpOptions};
use crate::working_set::BoundStatus;
fn expand_opts(feas_tol: f64) -> QpOptions {
QpOptions {
feas_tol,
anti_cycling: AntiCyclingChoice::Expand,
..QpOptions::default()
}
}
#[test]
fn expand_tau_inflation_falls_back_to_strict_min_no_panic() {
let opts = expand_opts(1e-6);
let candidates = [(BlockerTarget::Bound(0, BoundStatus::AtLower), 0.5, 1e-9)];
let (alpha, blocker) = select_blocker(&candidates, &opts, 1e-3, false);
assert!(
matches!(blocker, Some(BlockerTarget::Bound(0, BoundStatus::AtLower))),
"expected the sole candidate as blocker, got {:?}",
blocker.map(|b| match b {
BlockerTarget::Bound(i, _) => ("bound", i),
BlockerTarget::Cons(i, _) => ("cons", i),
})
);
assert!(
(alpha - 0.5).abs() < 1e-12,
"expected α = 0.5 (strict min), got {alpha}"
);
}
#[test]
fn expand_fallback_selects_strict_minimum_among_inflated() {
let opts = expand_opts(1e-6);
let candidates = [
(BlockerTarget::Bound(0, BoundStatus::AtLower), 0.75, 1e-9),
(BlockerTarget::Bound(1, BoundStatus::AtUpper), 0.25, 1e-9),
];
let (alpha, blocker) = select_blocker(&candidates, &opts, 1e-3, false);
assert!(
matches!(blocker, Some(BlockerTarget::Bound(1, BoundStatus::AtUpper))),
"expected the strict-min candidate (index 1)"
);
assert!(
(alpha - 0.25).abs() < 1e-12,
"expected α = 0.25, got {alpha}"
);
}
#[test]
fn expand_normal_case_admits_in_pass_two() {
let opts = expand_opts(1e-6);
let candidates = [(BlockerTarget::Bound(0, BoundStatus::AtLower), 0.5, 1.0)];
let (alpha, blocker) = select_blocker(&candidates, &opts, 1e-9, false);
assert!(matches!(
blocker,
Some(BlockerTarget::Bound(0, BoundStatus::AtLower))
));
assert!(alpha >= 0.5 && alpha <= 1.0, "α in range, got {alpha}");
}
}