use crate::schur_data::IndexSchurData;
use pounce_common::types::{Index, Number};
use pounce_linalg::Vector;
use pounce_linalg::expansion_matrix::ExpansionMatrix;
use std::rc::Rc;
pub fn expand_bounds(
n_x: usize,
px_l: &Rc<dyn pounce_linalg::Matrix>,
px_u: &Rc<dyn pounce_linalg::Matrix>,
x_l: &dyn Vector,
x_u: &dyn Vector,
) -> (Vec<Number>, Vec<Number>) {
let mut lo = vec![Number::NEG_INFINITY; n_x];
let mut hi = vec![Number::INFINITY; n_x];
for (pm, src, dst) in [(px_l, x_l, &mut lo), (px_u, x_u, &mut hi)] {
let Some(em) = pm.as_any().downcast_ref::<ExpansionMatrix>() else {
continue;
};
let vals = compressed_values(src);
for (ci, &full_pos) in em.expanded_pos_indices().iter().enumerate() {
let i = full_pos as usize;
if let (true, Some(&v)) = (i < n_x, vals.get(ci)) {
dst[i] = v;
}
}
}
(lo, hi)
}
pub fn bound_violations(
x_curr: &[Number],
dx: &[Number],
lo: &[Number],
hi: &[Number],
eps: Number,
skip: &[usize],
) -> Vec<(usize, Number, Number)> {
let mut out: Vec<(usize, Number, Number)> = Vec::new();
for i in 0..x_curr.len().min(dx.len()) {
if skip.contains(&i) {
continue;
}
let trial = x_curr[i] + dx[i];
let (bound, over) = if trial < lo[i] {
(lo[i], lo[i] - trial)
} else if trial > hi[i] {
(hi[i], trial - hi[i])
} else {
continue;
};
if over > eps {
out.push((i, bound, over));
}
}
out.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
out
}
pub fn worst_violation(
x_curr: &[Number],
dx: &[Number],
lo: &[Number],
hi: &[Number],
eps: Number,
skip: &[usize],
) -> Option<(usize, Number)> {
bound_violations(x_curr, dx, lo, hi, eps, skip)
.first()
.map(|&(i, bound, _)| (i, bound))
}
fn compressed_values(v: &dyn Vector) -> Vec<Number> {
use pounce_linalg::dense_vector::DenseVector;
match v.as_any().downcast_ref::<DenseVector>() {
Some(dv) => dv.expanded_values(),
None => Vec::new(),
}
}
#[doc(hidden)]
pub fn _index_to_usize(i: Index) -> usize {
i as usize
}
#[cfg(test)]
mod tests {
use super::*;
use pounce_linalg::Vector;
use pounce_linalg::dense_vector::{DenseVector, DenseVectorSpace};
use pounce_linalg::expansion_matrix::{ExpansionMatrix, ExpansionMatrixSpace};
fn make_dv(values: &[Number]) -> DenseVector {
let space = DenseVectorSpace::new(values.len() as Index);
let mut dv = DenseVector::new(space);
dv.values_mut().copy_from_slice(values);
dv
}
fn make_homogeneous_dv(dim: Index, scalar: Number) -> DenseVector {
let space = DenseVectorSpace::new(dim);
let mut dv = DenseVector::new(space);
dv.set(scalar);
assert!(dv.is_homogeneous());
dv
}
fn expansion(n: Index, positions: &[Index]) -> Rc<dyn pounce_linalg::Matrix> {
let space = ExpansionMatrixSpace::new(n, positions.len() as Index, positions, 0);
Rc::new(ExpansionMatrix::new(space)) as Rc<dyn pounce_linalg::Matrix>
}
#[test]
fn expand_bounds_puts_infinity_where_a_bound_is_absent() {
let (lo, hi) = expand_bounds(
3,
&expansion(3, &[1]),
&expansion(3, &[2]),
&make_dv(&[-2.0]),
&make_dv(&[7.0]),
);
assert_eq!(lo, vec![Number::NEG_INFINITY, -2.0, Number::NEG_INFINITY]);
assert_eq!(hi, vec![Number::INFINITY, Number::INFINITY, 7.0]);
}
#[test]
fn expand_bounds_materializes_a_homogeneous_vector() {
let (lo, _) = expand_bounds(
2,
&expansion(2, &[0, 1]),
&expansion(2, &[]),
&make_homogeneous_dv(2, 0.0),
&make_dv(&[]),
);
assert_eq!(lo, vec![0.0, 0.0]);
}
#[test]
fn worst_violation_takes_the_largest_overshoot_not_the_first() {
let x = [0.5, 0.5, 0.5];
let dx = [-0.6, -2.0, -0.7];
let lo = [0.0, 0.0, 0.0];
let hi = [10.0, 10.0, 10.0];
let (i, bound) = worst_violation(&x, &dx, &lo, &hi, 1e-9, &[]).unwrap();
assert_eq!(i, 1);
assert_eq!(bound, 0.0);
}
#[test]
fn worst_violation_skips_what_is_already_pinned() {
let x = [0.5, 0.5];
let dx = [-0.6, -2.0];
let lo = [0.0, 0.0];
let hi = [10.0, 10.0];
let (i, _) = worst_violation(&x, &dx, &lo, &hi, 1e-9, &[1]).unwrap();
assert_eq!(i, 0, "the worst one is pinned, so the next is taken");
}
#[test]
fn worst_violation_reports_an_upper_bound_too() {
let x = [0.5];
let dx = [3.0];
let (i, bound) = worst_violation(&x, &dx, &[0.0], &[1.0], 1e-9, &[]).unwrap();
assert_eq!((i, bound), (0, 1.0));
}
#[test]
fn worst_violation_is_none_inside_the_bounds_and_within_eps() {
let x = [0.5];
assert!(worst_violation(&x, &[0.1], &[0.0], &[1.0], 1e-9, &[]).is_none());
assert!(worst_violation(&x, &[0.5 + 1e-12], &[0.0], &[1.0], 1e-9, &[]).is_none());
}
#[test]
fn bound_violations_returns_every_crossing_worst_first() {
let x = [0.5, 0.5, 0.5, 0.5];
let dx = [-0.6, -2.0, -0.7, 0.1];
let lo = [0.0; 4];
let hi = [10.0; 4];
let v = bound_violations(&x, &dx, &lo, &hi, 1e-9, &[]);
assert_eq!(
v.iter().map(|&(i, _, _)| i).collect::<Vec<_>>(),
vec![1, 2, 0],
"the whole list, ordered by overshoot",
);
assert_eq!(v[0].1, 0.0, "and each carries the bound it left");
}
#[test]
fn bound_violations_leaves_out_what_is_already_pinned() {
let x = [0.5, 0.5];
let dx = [-0.6, -2.0];
let v = bound_violations(&x, &dx, &[0.0, 0.0], &[10.0, 10.0], 1e-9, &[1]);
assert_eq!(v.len(), 1);
assert_eq!(v[0].0, 0, "the pinned coordinate is not offered again");
}
fn lever_backsolver(lever: Number) -> crate::backsolver::DenseLuBacksolver {
crate::backsolver::DenseLuBacksolver::from_dense(2, &[1.0, 0.0, lever, 1.0])
.expect("nonsingular")
}
#[test]
fn a_refinement_that_ends_further_out_returns_the_unrefined_step() {
let bs = lever_backsolver(1000.0);
let dx_plain = [-0.1, 0.0];
let (dx, rows, stop) = refine_step_onto_bounds(
&bs,
&dx_plain,
&[0.0, 0.0],
&[0.0, -1e-3],
&[Number::INFINITY, 1e-3],
&[],
&[0.0, 0.0],
1e-9,
1e-9,
1,
)
.expect("refinement");
assert_eq!(stop, RefineStop::WorseThanPlain);
assert!(rows.is_empty(), "nothing is reported as constrained");
assert_eq!(dx, dx_plain.to_vec(), "the unrefined step comes back");
}
#[test]
fn a_pass_whose_correction_is_out_of_scale_is_refused() {
let bs = lever_backsolver(1e10);
let dx_plain = [-0.1, 0.0];
let (dx, rows, stop) = refine_step_onto_bounds(
&bs,
&dx_plain,
&[0.0, 0.0],
&[0.0, Number::NEG_INFINITY],
&[Number::INFINITY, Number::INFINITY],
&[],
&[0.0, 0.0],
1e-9,
1e-9,
8,
)
.expect("refinement");
assert_eq!(stop, RefineStop::DegreesOfFreedom);
assert!(
rows.is_empty(),
"the pass was refused, so nothing is pinned"
);
assert_eq!(dx, dx_plain.to_vec());
}
#[derive(Clone)]
struct ScriptedRelease {
base: crate::backsolver::DenseLuBacksolver,
rows: Vec<crate::backsolver::BoundRow>,
steps: std::collections::BTreeMap<usize, Vec<Number>>,
calls: Rc<std::cell::Cell<usize>>,
}
impl crate::backsolver::SensBacksolver for ScriptedRelease {
fn dim(&self) -> usize {
self.base.dim()
}
fn solve(&self, rhs: &[Number], lhs: &mut [Number]) -> bool {
self.base.solve(rhs, lhs)
}
fn bound_rows(&self) -> Option<&[crate::backsolver::BoundRow]> {
Some(&self.rows)
}
fn supports_release(&self) -> bool {
true
}
fn solve_released(&self, _released: &[usize], rhs: &[Number], lhs: &mut [Number]) -> bool {
self.base.solve(rhs, lhs)
}
fn solve_released_step(
&self,
released: &[usize],
_rhs: &[Number],
lhs: &mut [Number],
) -> bool {
self.calls.set(self.calls.get() + 1);
match self.steps.get(&released.len()) {
Some(s) => {
lhs.copy_from_slice(s);
true
}
None => false,
}
}
}
fn lever_matrix(n: usize, lever: Number) -> Vec<Number> {
let mut a = vec![0.0; n * n];
for i in 0..n {
a[i * n + i] = 1.0;
}
a[n] = lever;
a
}
#[test]
fn a_release_batch_that_makes_the_step_worse_backs_off_to_one() {
let calls = Rc::new(std::cell::Cell::new(0));
let bs = ScriptedRelease {
base: crate::backsolver::DenseLuBacksolver::from_dense(4, &lever_matrix(4, 0.0))
.expect("nonsingular"),
rows: vec![
crate::backsolver::BoundRow {
row: 2,
var_row: 0,
lower: true,
},
crate::backsolver::BoundRow {
row: 3,
var_row: 1,
lower: true,
},
],
steps: [
(2usize, vec![-5.0, 0.0, 0.0, 0.0]),
(1usize, vec![0.0, 0.0, 0.0, 0.0]),
]
.into_iter()
.collect(),
calls: Rc::clone(&calls),
};
let mults = [
BoundMultiplier { row: 2, base: 1.0 },
BoundMultiplier { row: 3, base: 1.0 },
];
let (dx, rows, stop) = refine_step_onto_bounds(
&bs,
&[0.0, 0.0, -2.0, -1.5],
&[0.0, 0.0],
&[0.0, 0.0],
&[Number::INFINITY, Number::INFINITY],
&mults,
&[0.0; 4],
1e-9,
1e-9,
8,
)
.expect("refinement");
assert_eq!(rows, vec![2], "the most negative one, alone");
assert_eq!(stop, RefineStop::Settled);
assert_eq!(dx, vec![0.0, 0.0, -1.0, 0.0], "and its multiplier is zero");
}
#[test]
fn a_wide_primal_margin_does_not_stop_a_release() {
let make = || ScriptedRelease {
base: crate::backsolver::DenseLuBacksolver::from_dense(4, &lever_matrix(4, 0.0))
.expect("nonsingular"),
rows: vec![
crate::backsolver::BoundRow {
row: 2,
var_row: 0,
lower: true,
},
crate::backsolver::BoundRow {
row: 3,
var_row: 1,
lower: true,
},
],
steps: [
(2usize, vec![-5.0, 0.0, 0.0, 0.0]),
(1usize, vec![0.0, 0.0, 0.0, 0.0]),
]
.into_iter()
.collect(),
calls: Rc::new(std::cell::Cell::new(0)),
};
let mults = [
BoundMultiplier { row: 2, base: 1.0 },
BoundMultiplier { row: 3, base: 1.0 },
];
let (_, rows, stop) = refine_step_onto_bounds(
&make(),
&[0.0, 0.0, -2.0, -1.5],
&[0.0, 0.0],
&[0.0, 0.0],
&[Number::INFINITY, Number::INFINITY],
&mults,
&[0.0; 4],
10.0,
1e-9,
8,
)
.expect("refinement");
assert_eq!(rows, vec![2, 3], "the release reads its own threshold");
assert_eq!(stop, RefineStop::Settled);
let (_, rows, _) = refine_step_onto_bounds(
&make(),
&[0.0, 0.0, -2.0, -1.5],
&[0.0, 0.0],
&[0.0, 0.0],
&[Number::INFINITY, Number::INFINITY],
&mults,
&[0.0; 4],
1e-9,
10.0,
8,
)
.expect("refinement");
assert!(rows.is_empty(), "nothing is negative past ten");
}
#[test]
fn a_release_the_factorization_refuses_is_not_asked_for_twice() {
let calls = Rc::new(std::cell::Cell::new(0));
let bs = ScriptedRelease {
base: crate::backsolver::DenseLuBacksolver::from_dense(4, &lever_matrix(4, 1.0))
.expect("nonsingular"),
rows: vec![crate::backsolver::BoundRow {
row: 2,
var_row: 0,
lower: true,
}],
steps: std::collections::BTreeMap::new(),
calls: Rc::clone(&calls),
};
let mults = [BoundMultiplier { row: 2, base: 1.0 }];
let (_dx, rows, stop) = refine_step_onto_bounds(
&bs,
&[-1.0, 0.0, -2.0, 0.0],
&[0.0, 0.0],
&[0.0, -0.5],
&[Number::INFINITY, Number::INFINITY],
&mults,
&[0.0; 4],
1e-9,
1e-9,
8,
)
.expect("refinement");
assert_eq!(calls.get(), 1, "asked for once, then barred");
assert_eq!(
stop,
RefineStop::DegreesOfFreedom,
"a bound that cannot leave the active set is not the pass limit",
);
assert_eq!(rows, vec![0, 1], "and the pins it could place still stand");
}
}
pub struct BoundMultiplier {
pub row: usize,
pub base: Number,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RefineStop {
Settled,
IterationLimit,
DegreesOfFreedom,
WorseThanPlain,
}
impl RefineStop {
pub fn as_str(self) -> &'static str {
match self {
RefineStop::Settled => "settled",
RefineStop::IterationLimit => "iteration_limit",
RefineStop::DegreesOfFreedom => "degrees_of_freedom",
RefineStop::WorseThanPlain => "worse_than_plain",
}
}
}
const CORRECTION_SCALE_LIMIT: Number = 1e4;
const WORSE_THAN_PLAIN_FACTOR: Number = 10.0;
pub fn release_floor(bound_relax_factor: Number) -> Number {
bound_relax_factor.abs().max(1e-9)
}
pub fn refine_step_onto_bounds<B>(
backsolver: &B,
dx_plain: &[Number],
x_curr: &[Number],
lo: &[Number],
hi: &[Number],
multipliers: &[BoundMultiplier],
rhs_plain: &[Number],
eps: Number,
release_eps: Number,
max_iter: usize,
) -> Result<(Vec<Number>, Vec<usize>, RefineStop), String>
where
B: crate::backsolver::SensBacksolver + Clone,
{
use crate::sens_app::{SensApplication, SensOptions};
let n_full = dx_plain.len();
let mut dx = dx_plain.to_vec();
let multipliers: Vec<BoundMultiplier> = match backsolver.natural_units_factor() {
None => multipliers
.iter()
.map(|m| BoundMultiplier {
row: m.row,
base: m.base,
})
.collect(),
Some(f) => multipliers
.iter()
.map(|m| BoundMultiplier {
row: m.row,
base: m.base * f[m.row],
})
.collect(),
};
let multipliers = &multipliers[..];
let bound_rows = backsolver.bound_rows();
let can_release = backsolver.supports_release() && rhs_plain.len() == n_full;
let worst_over = |d: &[Number]| {
bound_violations(x_curr, d, lo, hi, eps, &[])
.first()
.map_or(0.0, |&(_, _, over)| over)
};
let releasable = |dx: &[Number], released: &[usize], refused: &[usize]| -> Vec<usize> {
if !can_release {
return Vec::new();
}
let mut v: Vec<(usize, Number)> = multipliers
.iter()
.filter(|m| !released.contains(&m.row) && !refused.contains(&m.row))
.filter(|m| bound_rows.is_some_and(|br| br.iter().any(|b| b.row == m.row)))
.map(|m| (m.row, m.base + dx[m.row]))
.filter(|&(_, v)| v < -release_eps)
.collect();
v.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
v.into_iter().map(|(r, _)| r).collect()
};
let solve_pins = |pins: &[(usize, Number)],
released: &[usize],
dx_base: &[Number]|
-> Result<Option<Vec<Number>>, String> {
if pins.is_empty() {
return Ok(Some(dx_base.to_vec()));
}
let rows: Vec<Index> = pins.iter().map(|&(r, _)| r as Index).collect();
let rhs: Vec<Number> = pins
.iter()
.map(|&(r, bound)| (x_curr[r] + dx_base[r]) - bound)
.collect();
let signs = vec![1; rows.len()];
let mk = |r: Vec<Index>| {
IndexSchurData::from_parts(r, signs.clone()).map_err(|e| format!("{e:?}"))
};
let opts = SensOptions {
run_sens: true,
..SensOptions::default()
};
let view = ReleasedView {
base: backsolver.clone(),
rows: released.to_vec(),
pinned: Vec::new(),
};
let mut pin_app = SensApplication::new(mk(rows.clone())?, view, opts);
let mut du = vec![0.0; rows.len()];
let mut corr = vec![0.0; n_full];
if !pin_app.run_sens_step(&mk(rows)?, &rhs, &mut du, &mut corr) {
return Ok(None);
}
let achieved = pins
.iter()
.zip(rhs.iter())
.all(|(&(r, _), &want)| (corr[r] + want).abs() <= 1e-3 * want.abs().max(1.0));
if !achieved {
return Ok(None);
}
let inf = |v: &[Number]| v.iter().fold(0.0_f64, |a, b| a.max(b.abs()));
let scale = inf(dx_base).max(inf(&rhs)).max(1.0);
if inf(&corr) > CORRECTION_SCALE_LIMIT * scale {
return Ok(None);
}
Ok(Some(
dx_base
.iter()
.zip(corr.iter())
.map(|(b, c)| b + c)
.collect(),
))
};
let apply_releases = |released: &[usize], set: &[usize]| -> Option<(Vec<usize>, Vec<Number>)> {
let mut trial = released.to_vec();
trial.extend_from_slice(set);
let mut base = vec![0.0; n_full];
if !backsolver.solve_released_step(&trial, rhs_plain, &mut base) {
return None;
}
for &r in &trial {
if let Some(m) = multipliers.iter().find(|m| m.row == r) {
base[r] = -m.base;
}
}
Some((trial, base))
};
let mut pins: Vec<(usize, Number)> = Vec::new();
let mut released: Vec<usize> = Vec::new();
let mut dx_base = dx_plain.to_vec();
let mut refused_releases: Vec<usize> = Vec::new();
let mut stop = RefineStop::IterationLimit;
for _ in 0..max_iter {
let taken: Vec<usize> = pins.iter().map(|&(r, _)| r).collect();
let fresh_pins = bound_violations(x_curr, &dx, lo, hi, eps, &taken);
let fresh_releases = releasable(&dx, &released, &refused_releases);
if fresh_pins.is_empty() && fresh_releases.is_empty() {
stop = if releasable(&dx, &released, &[]).is_empty() {
RefineStop::Settled
} else {
RefineStop::DegreesOfFreedom
};
break;
}
if !fresh_releases.is_empty() {
let before = worst_over(&dx);
let mut sets: Vec<&[usize]> = vec![&fresh_releases[..]];
if fresh_releases.len() > 1 {
sets.push(&fresh_releases[..1]);
}
let mut taken: Option<(Vec<usize>, Vec<Number>, Vec<Number>)> = None;
for (k, set) in sets.iter().enumerate() {
let Some((trial, base)) = apply_releases(&released, set) else {
continue;
};
let Some(step) = solve_pins(&pins, &trial, &base)? else {
continue;
};
let alone = k + 1 == sets.len();
if alone || worst_over(&step) <= before.max(eps) {
taken = Some((trial, base, step));
break;
}
}
match taken {
Some((trial, base, step)) => {
released = trial;
dx_base = base;
dx = step;
}
None => {
refused_releases.extend_from_slice(&fresh_releases);
if fresh_pins.is_empty() {
stop = RefineStop::DegreesOfFreedom;
break;
}
}
}
if fresh_pins.is_empty() {
continue;
}
}
let keep_pins = pins.clone();
let keep_dx = dx.clone();
pins.extend(fresh_pins.iter().map(|&(i, bound, _)| (i, bound)));
let mut next = solve_pins(&pins, &released, &dx_base)?;
if next.is_none() && fresh_pins.len() > 1 {
pins.truncate(keep_pins.len());
pins.push((fresh_pins[0].0, fresh_pins[0].1));
next = solve_pins(&pins, &released, &dx_base)?;
}
match next {
Some(step) => dx = step,
None => {
pins = keep_pins;
dx = keep_dx;
stop = RefineStop::DegreesOfFreedom;
break;
}
}
}
if stop == RefineStop::IterationLimit {
let taken: Vec<usize> = pins.iter().map(|&(r, _)| r).collect();
let pins_left = !bound_violations(x_curr, &dx, lo, hi, eps, &taken).is_empty();
let rel_left = releasable(&dx, &released, &[]);
if !pins_left && rel_left.is_empty() {
stop = RefineStop::Settled;
} else if !pins_left && rel_left.iter().all(|r| refused_releases.contains(r)) {
stop = RefineStop::DegreesOfFreedom;
}
}
let plain_worst = worst_over(dx_plain);
if worst_over(&dx) > WORSE_THAN_PLAIN_FACTOR * plain_worst.max(eps) {
return Ok((dx_plain.to_vec(), Vec::new(), RefineStop::WorseThanPlain));
}
let mut out = released.clone();
out.extend(pins.into_iter().map(|(r, _)| r));
Ok((dx, out, stop))
}
#[derive(Clone)]
struct ReleasedView<B: crate::backsolver::SensBacksolver + Clone> {
base: B,
rows: Vec<usize>,
pinned: Vec<usize>,
}
impl<B: crate::backsolver::SensBacksolver + Clone> crate::backsolver::SensBacksolver
for ReleasedView<B>
{
fn dim(&self) -> usize {
self.base.dim()
}
fn solve(&self, rhs: &[Number], lhs: &mut [Number]) -> bool {
if !self.pinned.is_empty() {
return self
.base
.solve_released_pinned(&self.rows, &self.pinned, rhs, lhs);
}
if self.rows.is_empty() {
return self.base.solve(rhs, lhs);
}
self.base.solve_released(&self.rows, rhs, lhs)
}
fn natural_units_factor(&self) -> Option<&[Number]> {
self.base.natural_units_factor()
}
fn bound_rows(&self) -> Option<&[crate::backsolver::BoundRow]> {
self.base.bound_rows()
}
fn supports_release(&self) -> bool {
self.base.supports_release()
}
fn solve_released(&self, released: &[usize], rhs: &[Number], lhs: &mut [Number]) -> bool {
self.base.solve_released(released, rhs, lhs)
}
fn solve_released_step(&self, released: &[usize], rhs: &[Number], lhs: &mut [Number]) -> bool {
self.base.solve_released_step(released, rhs, lhs)
}
}
const NO_BOUND_LO: Number = -1e19;
const NO_BOUND_HI: Number = 1e19;
const PATH_MIN_SEGMENT: Number = 1e-12;
fn path_box_repair_budget(base_active_rows: usize) -> usize {
base_active_rows
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PathSegment {
pub at: Number,
pub var_row: usize,
pub lower: bool,
pub pinned: bool,
}
#[derive(Clone, Copy, Debug)]
struct PathHold {
row: usize,
lower: bool,
mult: Number,
}
#[allow(clippy::too_many_arguments)]
pub fn step_along_path<B>(
backsolver: &B,
rhs_plain: &[Number],
x_curr: &[Number],
lo: &[Number],
hi: &[Number],
multipliers: &[BoundMultiplier],
max_iter: usize,
forced_active: &[usize],
initial_holds: &[(usize, bool)],
weak_rows: &[usize],
eps: Number,
) -> Result<(Vec<Number>, Vec<PathSegment>), String>
where
B: crate::backsolver::SensBacksolver + Clone,
{
let n_full = backsolver.dim();
let n_p = x_curr.len().min(lo.len()).min(hi.len());
if rhs_plain.len() != n_full {
return Err("step_along_path: rhs length is not the KKT dimension".into());
}
let mult_nat: Vec<BoundMultiplier> = match backsolver.natural_units_factor() {
None => multipliers
.iter()
.map(|m| BoundMultiplier {
row: m.row,
base: m.base,
})
.collect(),
Some(f) => multipliers
.iter()
.map(|m| BoundMultiplier {
row: m.row,
base: m.base * f[m.row],
})
.collect(),
};
let bound_rows: Option<Vec<crate::backsolver::BoundRow>> =
backsolver.bound_rows().map(|b| b.to_vec());
let can_release = backsolver.supports_release();
let mut base_active_row: Vec<[Option<usize>; 2]> = vec![[None, None]; n_p];
if let Some(rows) = bound_rows.as_ref() {
for br in rows {
if br.var_row >= n_p {
continue;
}
let slack_base = if br.lower {
x_curr[br.var_row] - lo[br.var_row]
} else {
hi[br.var_row] - x_curr[br.var_row]
};
if !slack_base.is_finite() {
continue;
}
if forced_active.contains(&br.row)
|| mult_nat
.iter()
.any(|m| m.row == br.row && m.base > slack_base)
{
let side = if br.lower { 0 } else { 1 };
base_active_row[br.var_row][side] = Some(br.row);
}
}
}
let base_active_rows: Vec<usize> = base_active_row
.iter()
.flatten()
.filter_map(|slot| *slot)
.collect();
let setup = WalkSetup {
rhs_plain,
x_curr,
lo,
hi,
n_p,
n_full,
max_iter,
mult_nat,
bound_rows,
can_release,
base_active_row,
base_active_rows,
initial_holds,
};
let mut watch: Vec<usize> = weak_rows.to_vec();
let budget = path_box_repair_budget(setup.base_active_rows.len());
let mut best = walk_once(backsolver, &setup, &watch)?;
for _ in 0..budget {
let mut grew = false;
for (i, _, _) in bound_violations(
setup.x_curr,
&best.0[..setup.n_p],
setup.lo,
setup.hi,
eps,
&[],
) {
let side = usize::from(setup.x_curr[i] + best.0[i] > setup.hi[i]);
if let Some(r) = setup.base_active_row[i][side]
&& !watch.contains(&r)
{
watch.push(r);
grew = true;
}
}
if !grew {
break;
}
best = walk_once(backsolver, &setup, &watch).map_err(|e| {
format!(
"step_along_path: the walk left the box and the repair failed \
(watching {watch:?}): {e}"
)
})?;
}
if best.1.len() >= max_iter {
return Ok(best);
}
let left = bound_violations(
setup.x_curr,
&best.0[..setup.n_p],
setup.lo,
setup.hi,
eps,
&[],
);
if let Some((i, bnd, past)) = left.first() {
return Err(format!(
"step_along_path: the walk ended outside primal row {i}'s bound \
{bnd} by {past:e} and the repair could not reach it \
(watched {} rows over at most {budget} passes, \
{} segments of a {max_iter} cap). Rows below the `x` block's \
length are variables; at or above it they are constraint \
slacks, so read the row against `block_dims()`.",
watch.len(),
best.1.len()
));
}
Ok(best)
}
struct WalkSetup<'a> {
rhs_plain: &'a [Number],
x_curr: &'a [Number],
lo: &'a [Number],
hi: &'a [Number],
n_p: usize,
n_full: usize,
max_iter: usize,
mult_nat: Vec<BoundMultiplier>,
bound_rows: Option<Vec<crate::backsolver::BoundRow>>,
can_release: bool,
base_active_row: Vec<[Option<usize>; 2]>,
base_active_rows: Vec<usize>,
initial_holds: &'a [(usize, bool)],
}
fn walk_once<B>(
backsolver: &B,
su: &WalkSetup<'_>,
weak_rows: &[usize],
) -> Result<(Vec<Number>, Vec<PathSegment>), String>
where
B: crate::backsolver::SensBacksolver + Clone,
{
let rhs_plain = su.rhs_plain;
let x_curr = su.x_curr;
let lo = su.lo;
let hi = su.hi;
let n_p = su.n_p;
let n_full = su.n_full;
let max_iter = su.max_iter;
let mult_nat = &su.mult_nat;
let bound_rows = &su.bound_rows;
let can_release = su.can_release;
let base_active_row = &su.base_active_row;
let base_active_rows = &su.base_active_rows;
let initial_holds = su.initial_holds;
let mut acc = vec![0.0; n_full];
let mut t = 0.0_f64;
let mut holds: Vec<PathHold> = initial_holds
.iter()
.map(|&(row, lower)| PathHold {
row,
lower,
mult: 0.0,
})
.collect();
let mut released: Vec<usize> = initial_holds
.iter()
.filter_map(|&(var_row, lower)| {
bound_rows.as_ref().and_then(|rows| {
rows.iter()
.find(|b| b.var_row == var_row && b.lower == lower)
.map(|b| b.row)
})
})
.collect();
let mut segments: Vec<PathSegment> = Vec::new();
let mut changed_here: Vec<usize> = Vec::new();
let mut last_beta = 1.0_f64;
#[derive(Clone, Copy, PartialEq)]
enum Event {
ReachLower,
ReachUpper,
ReleaseBase,
DropHold,
}
for _ in 0..max_iter {
if last_beta > PATH_MIN_SEGMENT {
changed_here.clear();
}
let held: Vec<usize> = holds.iter().map(|h| h.row).collect();
let (d, du) = path_direction(backsolver, rhs_plain, &released, &held)?;
let remaining = 1.0 - t;
if remaining <= 0.0 {
break;
}
let mut best: Option<(Number, usize, Event)> = None;
let mut offer = |beta: Number, row: usize, ev: Event| {
if !beta.is_finite() || beta < 0.0 || beta > remaining {
return;
}
match best {
Some((b, _, _)) if b <= beta => {}
_ => best = Some((beta, row, ev)),
}
};
for i in 0..n_p {
if holds.iter().any(|h| h.row == i) || changed_here.contains(&i) {
continue;
}
let factor_holds = |lower_side: bool| -> bool {
let side = if lower_side { 0 } else { 1 };
base_active_row[i][side]
.is_some_and(|r| !released.contains(&r) && !weak_rows.contains(&r))
};
let v = x_curr[i] + acc[i];
if d[i] < 0.0 && lo[i] > NO_BOUND_LO && !factor_holds(true) {
offer((lo[i] - v) / d[i], i, Event::ReachLower);
}
if d[i] > 0.0 && hi[i] < NO_BOUND_HI && !factor_holds(false) {
offer((hi[i] - v) / d[i], i, Event::ReachUpper);
}
}
if can_release {
for m in mult_nat {
if released.contains(&m.row)
|| changed_here.contains(&m.row)
|| !base_active_rows.contains(&m.row)
{
continue;
}
let z_curr = m.base + acc[m.row];
if d[m.row] < 0.0 {
offer(-z_curr / d[m.row], m.row, Event::ReleaseBase);
}
}
}
for (k, h) in holds.iter().enumerate() {
if changed_here.contains(&h.row) {
continue;
}
let rate = du[k];
if h.mult * rate < 0.0 {
offer(-h.mult / rate, h.row, Event::DropHold);
}
}
let Some((beta, row, ev)) = best else {
for (a, dv) in acc.iter_mut().zip(d.iter()) {
*a += remaining * dv;
}
t = 1.0;
break;
};
for (a, dv) in acc.iter_mut().zip(d.iter()) {
*a += beta * dv;
}
for (k, h) in holds.iter_mut().enumerate() {
h.mult += beta * du[k];
}
last_beta = beta;
t += beta;
changed_here.push(row);
let (var_row, lower) = match ev {
Event::ReachLower | Event::ReachUpper => {
let lower = ev == Event::ReachLower;
let reached_row = bound_rows.as_ref().and_then(|rows| {
rows.iter()
.find(|b| b.var_row == row && b.lower == lower)
.map(|b| b.row)
});
if can_release
&& let Some(r) = reached_row
&& weak_rows.contains(&r)
&& !released.contains(&r)
{
released.push(r);
changed_here.push(r);
}
holds.push(PathHold {
row,
lower,
mult: 0.0,
});
(row, lower)
}
Event::ReleaseBase => {
let Some(br) = bound_rows
.as_ref()
.and_then(|rows| rows.iter().find(|b| b.row == row))
else {
return Err("step_along_path: released a row with no bound metadata".into());
};
changed_here.push(br.var_row);
released.push(row);
(br.var_row, br.lower)
}
Event::DropHold => {
let Some(h) = holds.iter().find(|h| h.row == row).copied() else {
return Err("step_along_path: dropped a hold that does not exist".into());
};
holds.retain(|h| h.row != row);
(row, h.lower)
}
};
segments.push(PathSegment {
at: t,
var_row,
lower,
pinned: matches!(ev, Event::ReachLower | Event::ReachUpper),
});
}
if t < 1.0 {
let held: Vec<usize> = holds.iter().map(|h| h.row).collect();
let (d, _) = path_direction(backsolver, rhs_plain, &released, &held)?;
for (a, dv) in acc.iter_mut().zip(d.iter()) {
*a += (1.0 - t) * dv;
}
}
Ok((acc, segments))
}
#[derive(Clone, Copy, Debug)]
pub struct WeakBound {
pub row: usize,
pub var_row: usize,
pub lower: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PathOperator {
Preferred,
Plain,
Regularized,
}
const PIN_TAKE_RTOL: Number = 1e-11;
fn pin_residual(d: &[Number], pinned: &[usize], want: &[Number]) -> Number {
let after = pinned
.iter()
.filter_map(|&i| d.get(i))
.fold(0.0, |a: Number, v| a.max(v.abs()));
if after == 0.0 {
return 0.0;
}
let before = want.iter().fold(0.0, |a: Number, v| a.max(v.abs()));
after / before.max(after)
}
pub fn path_direction<B>(
backsolver: &B,
rhs_plain: &[Number],
released: &[usize],
pinned: &[usize],
) -> Result<(Vec<Number>, Vec<Number>), String>
where
B: crate::backsolver::SensBacksolver + Clone,
{
let plain = path_direction_on(backsolver, rhs_plain, released, pinned, false);
if matches!(&plain, Ok((_, _, res)) if *res <= PIN_TAKE_RTOL) {
return plain.map(|(d, du, _)| (d, du));
}
let reg = path_direction_on(backsolver, rhs_plain, released, pinned, true);
match (plain, reg) {
(Ok(p), Ok(r)) => Ok(if r.2 < p.2 { (r.0, r.1) } else { (p.0, p.1) }),
(Ok(p), Err(_)) => Ok((p.0, p.1)),
(Err(_), Ok(r)) => Ok((r.0, r.1)),
(Err(e), Err(_)) => Err(e),
}
}
fn path_direction_on<B>(
backsolver: &B,
rhs_plain: &[Number],
released: &[usize],
pinned: &[usize],
regularized: bool,
) -> Result<(Vec<Number>, Vec<Number>, Number), String>
where
B: crate::backsolver::SensBacksolver + Clone,
{
use crate::backsolver::SensBacksolver;
use crate::sens_app::{SensApplication, SensOptions};
let n_full = backsolver.dim();
let view = ReleasedView {
base: backsolver.clone(),
rows: released.to_vec(),
pinned: if regularized {
pinned.to_vec()
} else {
Vec::new()
},
};
let mut d = vec![0.0; n_full];
if !view.solve(rhs_plain, &mut d) {
return Err("step_along_path: back-solve failed".into());
}
if pinned.is_empty() {
return Ok((d, Vec::new(), 0.0));
}
let rows: Vec<Index> = pinned.iter().map(|&r| r as Index).collect();
let signs = vec![1; rows.len()];
let mk =
|r: Vec<Index>| IndexSchurData::from_parts(r, signs.clone()).map_err(|e| format!("{e:?}"));
let opts = SensOptions {
run_sens: true,
..SensOptions::default()
};
let mut app = SensApplication::new(mk(rows.clone())?, view, opts);
let rhs: Vec<Number> = pinned.iter().map(|&i| d[i]).collect();
let mut du = vec![0.0; rows.len()];
let mut corr = vec![0.0; n_full];
if !app.run_sens_step(&mk(rows)?, &rhs, &mut du, &mut corr) {
return Err(format!(
"step_along_path: augmented solve failed (holds {pinned:?}, released {released:?})"
));
}
for (k, v) in d.iter_mut().enumerate() {
*v += corr[k];
}
let res = pin_residual(&d, pinned, &rhs);
Ok((d, du, res))
}
pub fn path_direction_with<B>(
backsolver: &B,
rhs_plain: &[Number],
released: &[usize],
pinned: &[usize],
operator: PathOperator,
) -> Result<(Vec<Number>, Vec<Number>), String>
where
B: crate::backsolver::SensBacksolver + Clone,
{
match operator {
PathOperator::Preferred => path_direction(backsolver, rhs_plain, released, pinned),
PathOperator::Plain => path_direction_on(backsolver, rhs_plain, released, pinned, false)
.map(|(d, du, _)| (d, du)),
PathOperator::Regularized => {
path_direction_on(backsolver, rhs_plain, released, pinned, true)
.map(|(d, du, _)| (d, du))
}
}
}