use crate::error::{QpError, QpStatus};
use crate::kkt::{a_times_x, assemble_active_set_kkt};
use crate::options::QpOptions;
use crate::problem::{QpProblem, QpSolution, QpStats};
use crate::solver::ParametricActiveSetSolver;
use crate::solver::QpSolver as _;
use crate::working_set::{BoundStatus, ConsStatus, WorkingSet};
use pounce_common::Number;
use pounce_common::types::{NLP_LOWER_BOUND_INF, NLP_UPPER_BOUND_INF};
use pounce_linalg::triplet::{GenTMatrix, GenTMatrixSpace, SymTMatrix, SymTMatrixSpace};
use std::time::Instant;
const RELAX_MARGIN: Number = 1.0;
const T_EPS: Number = 1e-12;
const T_TIE: Number = 1e-14;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Event {
AddRowLower(usize),
AddRowUpper(usize),
DropRow(usize),
DropBound(usize),
}
pub(crate) struct RatioTest {
pub(crate) t_next: Number,
t: Number,
pub(crate) winners: Vec<(Number, Event)>,
}
impl RatioTest {
pub(crate) fn new(t: Number) -> Self {
RatioTest {
t_next: 1.0,
t,
winners: Vec::new(),
}
}
pub(crate) fn restart(&mut self, t: Number) {
self.t_next = 1.0;
self.t = t;
self.winners.clear();
}
pub(crate) fn admit(&mut self, dt: Number, ev: Event) {
if dt < -T_EPS {
return;
}
let tc = (self.t + dt).max(self.t);
if tc > self.t_next + T_TIE {
return;
}
if tc < self.t_next - T_TIE {
self.winners.clear();
}
self.t_next = self.t_next.min(tc);
self.winners.push((tc, ev));
}
pub(crate) fn firing(&self) -> impl Iterator<Item = Event> + '_ {
let t_next = self.t_next;
self.winners
.iter()
.filter(move |&&(tc, _)| tc <= t_next + T_TIE)
.map(|&(_, ev)| ev)
}
}
fn path_regularization_delta(qp: &QpProblem<'_>) -> Option<Number> {
const DELTA_REL: Number = 1e-6;
let g_inf = qp.g.iter().fold(0.0_f64, |a, v| a.max(v.abs()));
if !(g_inf > 0.0) || !g_inf.is_finite() {
return None;
}
let mut widths: Vec<Number> = (0..qp.n)
.filter_map(|i| {
let (l, u) = (qp.xl[i], qp.xu[i]);
(l > NLP_LOWER_BOUND_INF && u < NLP_UPPER_BOUND_INF).then(|| (u - l).abs())
})
.filter(|w| w.is_finite() && *w > 0.0)
.collect();
let x_scale = if widths.is_empty() {
1.0
} else {
widths.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
widths[widths.len() / 2]
};
let delta = DELTA_REL * g_inf / x_scale.max(1e-12);
delta.is_finite().then_some(delta.clamp(1e-12, 1e12))
}
fn regularized_hessian(qp: &QpProblem<'_>, delta: Number) -> SymTMatrix {
let (irows, jcols, vals) = (qp.h.irows(), qp.h.jcols(), qp.h.values());
let mut ir: Vec<i32> = irows.to_vec();
let mut jc: Vec<i32> = jcols.to_vec();
let mut vl: Vec<Number> = vals.to_vec();
let mut has_diag = vec![false; qp.n];
for k in 0..ir.len() {
if ir[k] == jc[k] {
let idx = (ir[k] - 1) as usize;
if idx < qp.n {
vl[k] += delta;
has_diag[idx] = true;
}
}
}
for (i, seen) in has_diag.iter().enumerate() {
if !seen {
ir.push((i + 1) as i32);
jc.push((i + 1) as i32);
vl.push(delta);
}
}
let space = SymTMatrixSpace::new(qp.n as i32, ir, jc);
let mut h = SymTMatrix::new(space);
h.set_values(&vl);
h
}
const PATH_FEAS_TOL_REL: Number = 1e-6;
fn worst_path_violation(
qp: &QpProblem<'_>,
x: &[Number],
bl0: &[Number],
bu0: &[Number],
t: Number,
m: usize,
) -> Option<(usize, Number)> {
let ax = a_times_x(qp.a, x, m);
let mut worst: Option<(usize, Number)> = None;
for i in 0..m {
let blt = bl0[i] + t * bound_rate(bl0[i], qp.bl[i], true);
let but = bu0[i] + t * bound_rate(bu0[i], qp.bu[i], false);
let v = (blt - ax[i]).max(ax[i] - but);
if v > PATH_FEAS_TOL_REL * (1.0 + ax[i].abs()) && worst.is_none_or(|(_, prev)| v > prev) {
worst = Some((i, v));
}
}
worst
}
fn trace_summary(
exit: &str,
steps: u32,
t: Number,
n_changes: u32,
n_refactor: u32,
longest_stall: u32,
) {
eprintln!(
"[hom] summary exit={exit} steps={steps} t={t:.17} changes={n_changes} \
refactor={n_refactor} stall={longest_stall}"
);
}
fn bound_rate(relaxed: Number, target: Number, is_lower: bool) -> Number {
let infinite = if is_lower {
target <= NLP_LOWER_BOUND_INF
} else {
target >= NLP_UPPER_BOUND_INF
};
if infinite { 0.0 } else { target - relaxed }
}
impl ParametricActiveSetSolver {
pub(crate) fn solve_homotopy(
&mut self,
qp: &QpProblem<'_>,
warm: Option<(&QpProblem<'_>, &QpSolution)>,
opts: &QpOptions,
) -> Result<Option<QpSolution>, QpError> {
let started = Instant::now();
let n = qp.n;
let m = qp.m;
if m == 0 {
return Ok(None);
}
if let Some((prev, sol_prev)) = warm {
let mut x = sol_prev.x.clone();
x.resize(n, 0.0);
let mut working = sol_prev.working.clone();
working.constraints.resize(m, ConsStatus::Inactive);
working.bounds.resize(n, BoundStatus::Inactive);
let mut lambda_g = sol_prev.lambda_g.clone();
lambda_g.resize(m, 0.0);
let mut lambda_x = sol_prev.lambda_x.clone();
lambda_x.resize(n, 0.0);
let bl0 = prev.bl.to_vec();
let bu0 = prev.bu.to_vec();
let dg: Vec<Number> = (0..n).map(|j| qp.g[j] - prev.g[j]).collect();
return self.trace_path(
qp, qp.h, x, working, lambda_g, lambda_x, bl0, bu0, dg, opts, started,
);
}
let empty_a = GenTMatrix::new(GenTMatrixSpace::new(0, n as i32, Vec::new(), Vec::new()));
let trace = std::env::var("POUNCE_HOMOTOPY_DEBUG").is_ok();
macro_rules! box_qp {
($h:expr) => {
QpProblem {
n,
m: 0,
h: $h,
g: qp.g,
a: &empty_a,
bl: &[],
bu: &[],
xl: qp.xl,
xu: qp.xu,
hessian_inertia: qp.hessian_inertia,
}
};
}
let mut h_reg_holder: Option<SymTMatrix> = None;
let box_sol = {
let first = self.solve(&box_qp!(qp.h), None, opts);
if trace {
match &first {
Ok(s) => eprintln!("[hom] box relaxation: {:?} obj={:.6e}", s.status, s.obj),
Err(e) => eprintln!("[hom] box relaxation ERROR: {e}"),
}
}
match first {
Ok(s) if s.status == QpStatus::Optimal => s,
_ => {
let Some(delta) = path_regularization_delta(qp) else {
return Ok(None);
};
let h_reg = regularized_hessian(qp, delta);
let retry = self.solve(&box_qp!(&h_reg), None, opts);
if trace {
match &retry {
Ok(s) => eprintln!(
"[hom] box relaxation (delta={delta:.3e}): {:?} obj={:.6e}",
s.status, s.obj
),
Err(e) => eprintln!("[hom] box relaxation (regularized) ERROR: {e}"),
}
}
match retry {
Ok(s) if s.status == QpStatus::Optimal => {
h_reg_holder = Some(h_reg);
s
}
_ => return Ok(None),
}
}
}
};
let path_h: &SymTMatrix = h_reg_holder.as_ref().unwrap_or(qp.h);
let x = box_sol.x.clone();
let mut working = WorkingSet::cold(n, m);
for (i, st) in working.bounds.iter_mut().enumerate() {
*st = box_sol.working.bounds[i];
}
let ax0 = a_times_x(qp.a, &x, m);
let mut bl0 = vec![0.0; m];
let mut bu0 = vec![0.0; m];
for i in 0..m {
let scale = RELAX_MARGIN * (1.0 + ax0[i].abs());
bl0[i] = if qp.bl[i] <= NLP_LOWER_BOUND_INF {
qp.bl[i]
} else {
(ax0[i] - scale).min(qp.bl[i])
};
bu0[i] = if qp.bu[i] >= NLP_UPPER_BOUND_INF {
qp.bu[i]
} else {
(ax0[i] + scale).max(qp.bu[i])
};
}
let lambda_g = vec![0.0; m];
let mut lambda_x = box_sol.lambda_x.clone();
lambda_x.resize(n, 0.0);
let dg = vec![0.0; n];
self.trace_path(
qp, path_h, x, working, lambda_g, lambda_x, bl0, bu0, dg, opts, started,
)
}
#[allow(clippy::too_many_arguments)]
fn trace_path(
&mut self,
qp: &QpProblem<'_>,
path_h: &SymTMatrix,
mut x: Vec<Number>,
mut working: WorkingSet,
mut lambda_g: Vec<Number>,
mut lambda_x: Vec<Number>,
bl0: Vec<Number>,
bu0: Vec<Number>,
dg: Vec<Number>,
opts: &QpOptions,
started: Instant,
) -> Result<Option<QpSolution>, QpError> {
let n = qp.n;
let m = qp.m;
let trace = std::env::var("POUNCE_HOMOTOPY_DEBUG").is_ok();
let path_qp = QpProblem {
n,
m,
h: path_h,
g: qp.g,
a: qp.a,
bl: qp.bl,
bu: qp.bu,
xl: qp.xl,
xu: qp.xu,
hessian_inertia: qp.hessian_inertia,
};
let mut t: Number = 0.0;
let mut n_changes: u32 = 0;
let mut n_refactor: u32 = 0;
let mut rank_repairs: u32 = (n + m).min(1000) as u32;
let mut tabu_cons = vec![false; m];
let mut steps: u32 = 0;
let mut ratio = RatioTest::new(0.0);
let mut stalled: u32 = 0;
let mut longest_stall: u32 = 0;
for _step in 0..opts.max_iter {
if t >= 1.0 - T_EPS {
break;
}
steps += 1;
if trace && _step % 50 == 0 {
eprintln!("[hom] step={_step} t={t:.17} stall={stalled}");
}
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, k_b) = (active_cons.len(), active_bounds.len());
let kkt = assemble_active_set_kkt(&path_qp, &active_cons, &active_bounds);
let mut rhs = vec![0.0; n + k_c + k_b];
for (j, r) in rhs[..n].iter_mut().enumerate() {
*r = -dg[j];
}
for (slot, &i) in active_cons.iter().enumerate() {
let is_lower = matches!(working.constraints[i], ConsStatus::AtLower);
rhs[n + slot] = match working.constraints[i] {
ConsStatus::Equality => bound_rate(bu0[i], qp.bu[i], false),
_ => bound_rate(
if is_lower { bl0[i] } else { bu0[i] },
if is_lower { qp.bl[i] } else { qp.bu[i] },
is_lower,
),
};
}
match self.factorize_with_inertia_control(kkt, &mut rhs, (k_c + k_b) as i32, n, opts) {
Ok(_) => {}
Err(e) if e.is_recoverable_factorization_failure() && rank_repairs > 0 => {
let (kc, kb) = crate::solver::independent_active_subset(
&mut self.linsol,
&path_qp,
&active_cons,
&active_bounds,
);
if kc.len() == active_cons.len() && kb.len() == active_bounds.len() {
if trace {
eprintln!("[hom] KKT failure at t={t:.6e}, full rank already: {e}");
trace_summary("rank", steps, t, n_changes, n_refactor, longest_stall);
}
return Ok(None);
}
rank_repairs -= 1;
let mut keep_c = vec![false; m];
for &i in &kc {
keep_c[i] = true;
}
let mut keep_b = vec![false; n];
for &j in &kb {
keep_b[j] = true;
}
for &i in &active_cons {
if !keep_c[i] {
working.constraints[i] = ConsStatus::Inactive;
lambda_g[i] = 0.0;
tabu_cons[i] = true;
n_changes += 1;
}
}
for &j in &active_bounds {
if !keep_b[j] {
working.bounds[j] = BoundStatus::Inactive;
lambda_x[j] = 0.0;
n_changes += 1;
}
}
if trace {
eprintln!(
"[hom] rank repair at t={t:.6e}: {} -> {} cons, {} -> {} bounds",
active_cons.len(),
kc.len(),
active_bounds.len(),
kb.len()
);
}
continue;
}
Err(e) => {
if trace {
eprintln!("[hom] KKT factorization failed at t={t:.6e}: {e}");
trace_summary("kkt", steps, t, n_changes, n_refactor, longest_stall);
}
return Ok(None);
}
}
n_refactor += 1;
let dx: Vec<Number> = rhs[..n].to_vec();
let dlam_c: Vec<Number> = (0..k_c).map(|s| rhs[n + s]).collect();
let dlam_b: Vec<Number> = (0..k_b).map(|s| rhs[n + k_c + s]).collect();
let a_dx = a_times_x(qp.a, &dx, m);
let ax = a_times_x(qp.a, &x, m);
ratio.restart(t);
for i in 0..m {
if working.constraints[i].is_active() || tabu_cons[i] {
continue;
}
if qp.bu[i] < NLP_UPPER_BOUND_INF {
let gap = bu0[i] + t * (qp.bu[i] - bu0[i]) - ax[i];
let rate = a_dx[i] - bound_rate(bu0[i], qp.bu[i], false);
if rate > 0.0 {
ratio.admit(gap / rate, Event::AddRowUpper(i));
}
}
if qp.bl[i] > NLP_LOWER_BOUND_INF {
let gap = ax[i] - (bl0[i] + t * (qp.bl[i] - bl0[i]));
let rate = bound_rate(bl0[i], qp.bl[i], true) - a_dx[i];
if rate > 0.0 {
ratio.admit(gap / rate, Event::AddRowLower(i));
}
}
}
for (slot, &i) in active_cons.iter().enumerate() {
if matches!(working.constraints[i], ConsStatus::Equality) {
continue;
}
let lam = lambda_g[i];
let rate = dlam_c[slot];
let heading_to_zero = (lam > 0.0 && rate < 0.0) || (lam < 0.0 && rate > 0.0);
if heading_to_zero {
ratio.admit(-lam / rate, Event::DropRow(i));
}
}
for (slot, &j) in active_bounds.iter().enumerate() {
if matches!(working.bounds[j], BoundStatus::Fixed) {
continue;
}
let lam = lambda_x[j];
let rate = dlam_b[slot];
let heading_to_zero = (lam > 0.0 && rate < 0.0) || (lam < 0.0 && rate > 0.0);
if heading_to_zero {
ratio.admit(-lam / rate, Event::DropBound(j));
}
}
let dt = ratio.t_next - t;
if dt > T_EPS {
tabu_cons.iter_mut().for_each(|f| *f = false);
stalled = 0;
} else {
stalled += 1;
longest_stall = longest_stall.max(stalled);
}
for (xi, &d) in x.iter_mut().zip(dx.iter()) {
*xi += dt * d;
}
for (slot, &i) in active_cons.iter().enumerate() {
lambda_g[i] += dt * dlam_c[slot];
}
for (slot, &j) in active_bounds.iter().enumerate() {
lambda_x[j] += dt * dlam_b[slot];
}
t = ratio.t_next;
if trace && let Some((i, v)) = worst_path_violation(qp, &x, &bl0, &bu0, t, m) {
eprintln!("[hom] path infeasible at t={t:.9e}: row {i} by {v:.3e}");
}
if ratio.winners.is_empty() {
t = 1.0;
break;
}
for ev in ratio.firing() {
match ev {
Event::AddRowUpper(i) => {
working.constraints[i] = ConsStatus::AtUpper;
}
Event::AddRowLower(i) => {
working.constraints[i] = ConsStatus::AtLower;
}
Event::DropRow(i) => {
working.constraints[i] = ConsStatus::Inactive;
lambda_g[i] = 0.0;
}
Event::DropBound(j) => {
working.bounds[j] = BoundStatus::Inactive;
lambda_x[j] = 0.0;
}
}
n_changes += 1;
}
}
if t < 1.0 - T_EPS {
if trace {
eprintln!("[hom] path did NOT reach t=1 (stopped at {t:.6e}); falling back");
let exit = if steps >= opts.max_iter {
"budget"
} else {
"stalled"
};
trace_summary(exit, steps, t, n_changes, n_refactor, longest_stall);
}
return Ok(None);
}
if trace {
trace_summary("complete", steps, t, n_changes, n_refactor, longest_stall);
eprintln!(
"[hom] reached t=1 after {n_changes} working-set changes; handoff x has \
max target violation {:.3e}",
crate::solver::max_violation(qp, &x)
);
}
let mut sol =
<Self as crate::solver::QpSolver>::solve_with_working_set(self, qp, &working, opts)?;
sol.stats = QpStats {
n_working_set_changes: sol.stats.n_working_set_changes + n_changes,
n_refactor: sol.stats.n_refactor + n_refactor,
n_schur_updates: sol.stats.n_schur_updates,
used_phase1: false,
time: started.elapsed(),
};
Ok(Some(sol))
}
}