use super::compact::{bmv, BmvError};
pub(crate) mod iwhere {
pub(crate) const FREE_NOT_MOVED: i8 = -3;
pub(crate) const ALWAYS_FREE: i8 = -1;
pub(crate) const FREE_MOVED: i8 = 0;
pub(crate) const AT_LOWER: i8 = 1;
pub(crate) const AT_UPPER: i8 = 2;
pub(crate) const ALWAYS_FIXED: i8 = 3;
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub(crate) struct CauchyResult {
pub(crate) nseg: usize,
pub(crate) bounded: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CauchyError {
SingularMiddleMatrix,
}
impl From<BmvError> for CauchyError {
fn from(_: BmvError) -> Self {
Self::SingularMiddleMatrix
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn cauchy(
x: &[f64],
l: &[f64],
u: &[f64],
g: &[f64],
ws_cols: &[&[f64]],
wy_cols: &[&[f64]],
sy: &[f64],
wt: &[f64],
m: usize,
theta: f64,
sbgnrm: f64,
xcp: &mut [f64],
d: &mut [f64],
t: &mut [f64],
iwhere: &mut [i8],
iorder: &mut [usize],
p_buf: &mut [f64],
c_buf: &mut [f64],
wbp_buf: &mut [f64],
v_buf: &mut [f64],
) -> Result<CauchyResult, CauchyError> {
let n = x.len();
let col = ws_cols.len();
debug_assert_eq!(col, wy_cols.len());
debug_assert!(col <= m);
debug_assert_eq!(l.len(), n);
debug_assert_eq!(u.len(), n);
debug_assert_eq!(g.len(), n);
debug_assert_eq!(xcp.len(), n);
debug_assert_eq!(d.len(), n);
debug_assert_eq!(t.len(), n);
debug_assert_eq!(iwhere.len(), n);
debug_assert_eq!(iorder.len(), n);
debug_assert!(p_buf.len() >= 2 * m && c_buf.len() >= 2 * m);
debug_assert!(wbp_buf.len() >= 2 * m && v_buf.len() >= 2 * m);
if sbgnrm <= 0.0 {
xcp.copy_from_slice(x);
return Ok(CauchyResult {
nseg: 0,
bounded: true,
});
}
let col2 = 2 * col;
let mut bnded = true;
let mut nfree = n;
let mut nbreak: usize = 0;
let mut ibkmin: usize = 0;
let mut bkmin: f64 = 0.0;
let mut f1: f64 = 0.0;
for slot in p_buf.iter_mut().take(col2) {
*slot = 0.0;
}
for i in 0..n {
let neggi = -g[i];
if iwhere[i] != iwhere::ALWAYS_FIXED && iwhere[i] != iwhere::ALWAYS_FREE {
let lower_finite = l[i].is_finite();
let upper_finite = u[i].is_finite();
let tl = if lower_finite { x[i] - l[i] } else { 0.0 };
let tu = if upper_finite { u[i] - x[i] } else { 0.0 };
let xlower = lower_finite && tl <= 0.0;
let xupper = upper_finite && tu <= 0.0;
iwhere[i] = iwhere::FREE_MOVED;
if xlower {
if neggi <= 0.0 {
iwhere[i] = iwhere::AT_LOWER;
}
} else if xupper {
if neggi >= 0.0 {
iwhere[i] = iwhere::AT_UPPER;
}
} else if neggi.abs() <= 0.0 {
iwhere[i] = iwhere::FREE_NOT_MOVED;
}
}
if iwhere[i] != iwhere::FREE_MOVED && iwhere[i] != iwhere::ALWAYS_FREE {
d[i] = 0.0;
} else {
d[i] = neggi;
f1 -= neggi * neggi;
for j in 0..col {
p_buf[j] += wy_cols[j][i] * neggi;
p_buf[col + j] += ws_cols[j][i] * neggi;
}
let lower_finite = l[i].is_finite();
let upper_finite = u[i].is_finite();
if lower_finite && neggi < 0.0 {
let tnew = (x[i] - l[i]) / -neggi;
iorder[nbreak] = i;
t[nbreak] = tnew;
if nbreak == 0 || tnew < bkmin {
bkmin = tnew;
ibkmin = nbreak;
}
nbreak += 1;
} else if upper_finite && neggi > 0.0 {
let tnew = (u[i] - x[i]) / neggi;
iorder[nbreak] = i;
t[nbreak] = tnew;
if nbreak == 0 || tnew < bkmin {
bkmin = tnew;
ibkmin = nbreak;
}
nbreak += 1;
} else {
nfree -= 1;
iorder[nfree] = i;
if neggi.abs() > 0.0 {
bnded = false;
}
}
}
}
if theta != 1.0 {
for slot in p_buf.iter_mut().skip(col).take(col) {
*slot *= theta;
}
}
xcp.copy_from_slice(x);
if nbreak == 0 && nfree == n {
return Ok(CauchyResult {
nseg: 0,
bounded: true,
});
}
for slot in c_buf.iter_mut().take(col2) {
*slot = 0.0;
}
let mut f2 = -theta * f1;
let f2_org = f2;
if col > 0 {
bmv(sy, wt, col, m, p_buf, v_buf)?;
for j in 0..col2 {
f2 -= v_buf[j] * p_buf[j];
}
}
let mut dtm = -f1 / f2;
let mut tsum = 0.0;
let mut nseg: usize = 1;
if nbreak == 0 {
return Ok(finalize_with_free_move(
d, xcp, dtm, tsum, c_buf, p_buf, col2, bnded, nseg,
));
}
let mut nleft = nbreak;
let mut iter: usize = 1;
let mut tj: f64 = 0.0;
let mut all_pinned_early = false;
loop {
let tj0 = tj;
let ibp;
if iter == 1 {
tj = bkmin;
ibp = iorder[ibkmin];
} else {
if iter == 2 && ibkmin != nbreak - 1 {
t[ibkmin] = t[nbreak - 1];
iorder[ibkmin] = iorder[nbreak - 1];
}
hpsolb(nleft, t, iorder, iter == 2);
tj = t[nleft - 1];
ibp = iorder[nleft - 1];
}
let dt = tj - tj0;
if dtm < dt {
break;
}
tsum += dt;
nleft -= 1;
iter += 1;
let dibp = d[ibp];
d[ibp] = 0.0;
let zibp;
if dibp > 0.0 {
zibp = u[ibp] - x[ibp];
xcp[ibp] = u[ibp];
iwhere[ibp] = iwhere::AT_UPPER;
} else {
zibp = l[ibp] - x[ibp];
xcp[ibp] = l[ibp];
iwhere[ibp] = iwhere::AT_LOWER;
}
if nleft == 0 && nbreak == n {
dtm = dt;
all_pinned_early = true;
break;
}
nseg += 1;
let dibp2 = dibp * dibp;
f1 = f1 + dt * f2 + dibp2 - theta * dibp * zibp;
f2 -= theta * dibp2;
if col > 0 {
for j in 0..col2 {
c_buf[j] += dt * p_buf[j];
}
for j in 0..col {
wbp_buf[j] = wy_cols[j][ibp];
wbp_buf[col + j] = theta * ws_cols[j][ibp];
}
bmv(sy, wt, col, m, wbp_buf, v_buf)?;
let mut wmc = 0.0;
let mut wmp = 0.0;
let mut wmw = 0.0;
for j in 0..col2 {
wmc += c_buf[j] * v_buf[j];
wmp += p_buf[j] * v_buf[j];
wmw += wbp_buf[j] * v_buf[j];
}
for j in 0..col2 {
p_buf[j] -= dibp * wbp_buf[j];
}
f1 += dibp * wmc;
f2 += 2.0 * dibp * wmp - dibp2 * wmw;
}
let floor = f64::EPSILON * f2_org;
if f2 < floor {
f2 = floor;
}
if nleft > 0 {
dtm = -f1 / f2;
} else if bnded {
f1 = 0.0;
let _ = f1; dtm = 0.0;
all_pinned_early = true;
break;
} else {
dtm = -f1 / f2;
break;
}
}
if all_pinned_early {
if dtm <= 0.0 {
dtm = 0.0;
}
for j in 0..col2 {
c_buf[j] += dtm * p_buf[j];
}
return Ok(CauchyResult {
nseg,
bounded: bnded,
});
}
Ok(finalize_with_free_move(
d, xcp, dtm, tsum, c_buf, p_buf, col2, bnded, nseg,
))
}
#[allow(clippy::too_many_arguments)]
fn finalize_with_free_move(
d: &[f64],
xcp: &mut [f64],
mut dtm: f64,
tsum: f64,
c_buf: &mut [f64],
p_buf: &[f64],
col2: usize,
bnded: bool,
nseg: usize,
) -> CauchyResult {
if dtm <= 0.0 {
dtm = 0.0;
}
let total = tsum + dtm;
for i in 0..xcp.len() {
xcp[i] += total * d[i];
}
for j in 0..col2 {
c_buf[j] += dtm * p_buf[j];
}
CauchyResult {
nseg,
bounded: bnded,
}
}
fn hpsolb(nleft: usize, t: &mut [f64], iorder: &mut [usize], first: bool) {
if first {
for k in 1..nleft {
let ddum = t[k];
let indxin = iorder[k];
let mut i = k;
while i > 0 {
let parent = (i - 1) / 2;
if ddum < t[parent] {
t[i] = t[parent];
iorder[i] = iorder[parent];
i = parent;
} else {
break;
}
}
t[i] = ddum;
iorder[i] = indxin;
}
}
if nleft <= 1 {
return;
}
let out = t[0];
let out_i = iorder[0];
let ddum = t[nleft - 1];
let ddum_i = iorder[nleft - 1];
let active = nleft - 1;
let mut i: usize = 0;
loop {
let left = 2 * i + 1;
if left >= active {
break;
}
let right = left + 1;
let child = if right < active && t[right] < t[left] {
right
} else {
left
};
if t[child] < ddum {
t[i] = t[child];
iorder[i] = iorder[child];
i = child;
} else {
break;
}
}
t[i] = ddum;
iorder[i] = ddum_i;
t[nleft - 1] = out;
iorder[nleft - 1] = out_i;
}
#[cfg(test)]
mod tests {
use super::*;
struct Buffers {
xcp: Vec<f64>,
d: Vec<f64>,
t: Vec<f64>,
iwhere: Vec<i8>,
iorder: Vec<usize>,
p: Vec<f64>,
c: Vec<f64>,
wbp: Vec<f64>,
v: Vec<f64>,
}
impl Buffers {
fn new(n: usize, m: usize) -> Self {
Self {
xcp: vec![0.0; n],
d: vec![0.0; n],
t: vec![0.0; n],
iwhere: vec![iwhere::FREE_MOVED; n],
iorder: vec![0; n],
p: vec![0.0; 2 * m],
c: vec![0.0; 2 * m],
wbp: vec![0.0; 2 * m],
v: vec![0.0; 2 * m],
}
}
}
#[test]
fn zero_projected_gradient_returns_x() {
let x = vec![0.5, 1.5];
let l = vec![0.0, 0.0];
let u = vec![2.0, 2.0];
let g = vec![0.0, 0.0];
let ws_cols: Vec<&[f64]> = Vec::new();
let wy_cols: Vec<&[f64]> = Vec::new();
let sy = Vec::<f64>::new();
let wt = Vec::<f64>::new();
let mut b = Buffers::new(2, 1);
let res = cauchy(
&x,
&l,
&u,
&g,
&ws_cols,
&wy_cols,
&sy,
&wt,
1,
1.0,
0.0,
&mut b.xcp,
&mut b.d,
&mut b.t,
&mut b.iwhere,
&mut b.iorder,
&mut b.p,
&mut b.c,
&mut b.wbp,
&mut b.v,
)
.unwrap();
assert_eq!(b.xcp, x);
assert_eq!(res.nseg, 0);
}
#[test]
fn col_zero_unconstrained_step_is_steepest_descent() {
let x = vec![3.0, 4.0];
let l = vec![f64::NEG_INFINITY; 2];
let u = vec![f64::INFINITY; 2];
let g = vec![1.0, 2.0];
let ws_cols: Vec<&[f64]> = Vec::new();
let wy_cols: Vec<&[f64]> = Vec::new();
let sy = Vec::<f64>::new();
let wt = Vec::<f64>::new();
let mut b = Buffers::new(2, 1);
for slot in b.iwhere.iter_mut() {
*slot = iwhere::ALWAYS_FREE;
}
let sbgnrm = g.iter().cloned().map(f64::abs).fold(0.0, f64::max);
let res = cauchy(
&x,
&l,
&u,
&g,
&ws_cols,
&wy_cols,
&sy,
&wt,
1,
1.0,
sbgnrm,
&mut b.xcp,
&mut b.d,
&mut b.t,
&mut b.iwhere,
&mut b.iorder,
&mut b.p,
&mut b.c,
&mut b.wbp,
&mut b.v,
)
.unwrap();
assert!((b.xcp[0] - 2.0).abs() < 1e-12);
assert!((b.xcp[1] - 2.0).abs() < 1e-12);
assert_eq!(res.nseg, 1);
}
#[test]
fn col_zero_with_active_upper_bound_pins_at_breakpoint() {
let x = vec![0.5];
let l = vec![f64::NEG_INFINITY];
let u = vec![1.0];
let g = vec![-1.0];
let ws_cols: Vec<&[f64]> = Vec::new();
let wy_cols: Vec<&[f64]> = Vec::new();
let sy = Vec::<f64>::new();
let wt = Vec::<f64>::new();
let mut b = Buffers::new(1, 1);
let sbgnrm = 1.0;
let res = cauchy(
&x,
&l,
&u,
&g,
&ws_cols,
&wy_cols,
&sy,
&wt,
1,
1.0,
sbgnrm,
&mut b.xcp,
&mut b.d,
&mut b.t,
&mut b.iwhere,
&mut b.iorder,
&mut b.p,
&mut b.c,
&mut b.wbp,
&mut b.v,
)
.unwrap();
assert!((b.xcp[0] - 1.0).abs() < 1e-12);
assert_eq!(b.iwhere[0], iwhere::AT_UPPER);
assert_eq!(res.nseg, 1);
}
#[test]
fn already_at_lower_with_outward_gradient_pins_immediately() {
let x = vec![0.0];
let l = vec![0.0];
let u = vec![1.0];
let g = vec![1.0];
let ws_cols: Vec<&[f64]> = Vec::new();
let wy_cols: Vec<&[f64]> = Vec::new();
let sy = Vec::<f64>::new();
let wt = Vec::<f64>::new();
let mut b = Buffers::new(1, 1);
let sbgnrm = 1.0;
let res = cauchy(
&x,
&l,
&u,
&g,
&ws_cols,
&wy_cols,
&sy,
&wt,
1,
1.0,
sbgnrm,
&mut b.xcp,
&mut b.d,
&mut b.t,
&mut b.iwhere,
&mut b.iorder,
&mut b.p,
&mut b.c,
&mut b.wbp,
&mut b.v,
)
.unwrap();
assert_eq!(b.xcp, x);
assert_eq!(b.iwhere[0], iwhere::AT_LOWER);
assert_eq!(res.nseg, 0);
}
#[test]
fn col_one_unconstrained_uses_compact_quadratic() {
let x = vec![5.0];
let l = vec![f64::NEG_INFINITY];
let u = vec![f64::INFINITY];
let g = vec![1.0];
let s_hist = [1.0_f64];
let y_hist = [2.0_f64];
let ws_cols: Vec<&[f64]> = vec![&s_hist];
let wy_cols: Vec<&[f64]> = vec![&y_hist];
let m = 1;
let mut sy = vec![0.0; m * m];
let mut ss = vec![0.0; m * m];
sy[0] = 2.0; ss[0] = 1.0; let theta = 4.0 / 2.0;
let mut wt = vec![0.0; m * m];
super::super::compact::formt(theta, &sy, &ss, 1, m, &mut wt).unwrap();
let mut b = Buffers::new(1, m);
for slot in b.iwhere.iter_mut() {
*slot = iwhere::ALWAYS_FREE;
}
let res = cauchy(
&x,
&l,
&u,
&g,
&ws_cols,
&wy_cols,
&sy,
&wt,
m,
theta,
1.0,
&mut b.xcp,
&mut b.d,
&mut b.t,
&mut b.iwhere,
&mut b.iorder,
&mut b.p,
&mut b.c,
&mut b.wbp,
&mut b.v,
)
.unwrap();
assert!(b.xcp[0] < x[0], "expected xcp < x for g > 0");
assert_eq!(res.nseg, 1);
}
#[test]
fn col_one_with_breakpoint_pins_at_bound() {
let x = vec![0.5];
let l = vec![f64::NEG_INFINITY];
let u = vec![1.0];
let g = vec![-1.0];
let s_hist = [1.0_f64];
let y_hist = [1.0_f64];
let ws_cols: Vec<&[f64]> = vec![&s_hist];
let wy_cols: Vec<&[f64]> = vec![&y_hist];
let m = 1;
let mut sy = vec![0.0; m * m];
let mut ss = vec![0.0; m * m];
sy[0] = 1.0;
ss[0] = 1.0;
let theta = 1.0_f64;
let mut wt = vec![0.0; m * m];
super::super::compact::formt(theta, &sy, &ss, 1, m, &mut wt).unwrap();
let mut b = Buffers::new(1, m);
let res = cauchy(
&x,
&l,
&u,
&g,
&ws_cols,
&wy_cols,
&sy,
&wt,
m,
theta,
1.0,
&mut b.xcp,
&mut b.d,
&mut b.t,
&mut b.iwhere,
&mut b.iorder,
&mut b.p,
&mut b.c,
&mut b.wbp,
&mut b.v,
)
.unwrap();
assert!((b.xcp[0] - 1.0).abs() < 1e-12);
assert_eq!(b.iwhere[0], iwhere::AT_UPPER);
assert_eq!(res.nseg, 1);
}
#[test]
fn hpsolb_extracts_smallest_and_leaves_heap() {
let mut t = vec![3.0, 1.0, 4.0, 1.5, 9.0, 2.6];
let mut iorder = vec![10usize, 11, 12, 13, 14, 15];
let mut nleft = t.len();
let mut popped = Vec::new();
let mut first = true;
while nleft > 0 {
hpsolb(nleft, &mut t, &mut iorder, first);
first = false;
popped.push((t[nleft - 1], iorder[nleft - 1]));
nleft -= 1;
}
let mut expected = vec![
(3.0, 10),
(1.0, 11),
(4.0, 12),
(1.5, 13),
(9.0, 14),
(2.6, 15),
];
expected.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
assert_eq!(popped, expected);
}
}