use crate::core::math::{DenseMatrix, Scalar};
use crate::solver::powell::QuadraticModel;
pub(crate) struct BoundedInit<F = f64> {
pub(crate) model: QuadraticModel<F>,
pub(crate) sl: Vec<F>,
pub(crate) su: Vec<F>,
}
fn adjust_x0<F: Scalar>(x0: &mut [F], lower: &[F], upper: &[F], rho: F) {
let half = F::from_f64(0.5).expect("0.5 representable");
for i in 0..x0.len() {
let (a, b) = (lower[i], upper[i]);
let mut xi = x0[i].max(a).min(b);
if xi <= a + half * rho {
xi = a;
} else if xi < a + rho {
xi = a + rho;
}
if xi >= b - half * rho {
xi = b;
} else if xi > b - rho {
xi = b - rho;
}
x0[i] = xi;
}
}
pub(crate) fn xinbd<F: Scalar>(
x0: &[F],
step: &[F],
lower: &[F],
upper: &[F],
sl: &[F],
su: &[F],
) -> Vec<F> {
(0..x0.len())
.map(|i| {
let s = sl[i].max(su[i].min(step[i]));
if s <= sl[i] {
lower[i]
} else if s >= su[i] {
upper[i]
} else {
lower[i].max(upper[i].min(x0[i] + s))
}
})
.collect()
}
pub(crate) fn extra_pair(t: usize, n: usize) -> (usize, usize) {
let ip = 2 * n + 1 + t; let jdiv = (ip - n - 1) / n; let p1 = ip - n - jdiv * n; let mut q1 = p1 + jdiv;
if q1 > n {
q1 -= n;
}
(p1 - 1, q1 - 1)
}
impl<F: Scalar> QuadraticModel<F> {
pub(crate) fn try_initialize_bounded<E>(
mut x0: Vec<F>,
lower: &[F],
upper: &[F],
rho_beg: F,
m: usize,
eval: &mut impl FnMut(&[F]) -> Result<F, E>,
) -> Result<BoundedInit<F>, E> {
let n = x0.len();
assert!(n >= 1, "BOBYQA model needs n >= 1");
assert!(
m > 2 * n,
"BOBYQA model init currently requires m >= 2n+1 (got m={m}, n={n}); \
the n+2 <= m <= 2n partial-model case is not yet implemented"
);
assert!(
m <= (n + 1) * (n + 2) / 2,
"BOBYQA model requires m <= (n+1)(n+2)/2 (got m={m}, n={n})"
);
let two = F::from_f64(2.0).expect("2.0 representable");
let rho = rho_beg;
for i in 0..n {
assert!(
upper[i] >= lower[i] + two * rho,
"BOBYQA requires b_i >= a_i + 2*rho_beg for room to sample \
(coordinate {i}: a={:?}, b={:?}, rho_beg={:?})",
lower[i],
upper[i],
rho
);
}
adjust_x0(&mut x0, lower, upper, rho);
let sl: Vec<F> = (0..n).map(|i| lower[i] - x0[i]).collect();
let su: Vec<F> = (0..n).map(|i| upper[i] - x0[i]).collect();
let zero = F::zero();
let mut xpt = DenseMatrix::from_fn(m, n, |_, _| zero);
let mut fval = vec![zero; m];
for k in 0..n {
let alpha = if su[k] <= zero { -rho } else { rho };
xpt.set(k + 1, k, alpha);
let beta = if sl[k] >= zero {
(two * rho).min(su[k])
} else if su[k] <= zero {
(-two * rho).max(sl[k])
} else {
-rho
};
xpt.set(k + 1 + n, k, beta);
}
for k in 0..(2 * n + 1) {
let disp = xpt.row(k).to_vec();
fval[k] = eval(&xinbd(&x0, &disp, lower, upper, &sl, &su))?;
}
for k in 0..n {
let a = xpt.get(k + 1, k);
let b = xpt.get(k + 1 + n, k);
if a * b < zero && fval[k + 1 + n] < fval[k + 1] {
for c in 0..n {
let tmp = xpt.get(k + 1, c);
xpt.set(k + 1, c, xpt.get(k + 1 + n, c));
xpt.set(k + 1 + n, c, tmp);
}
fval.swap(k + 1, k + 1 + n);
}
}
let mut extras: Vec<(usize, usize, usize)> = Vec::new(); for t in 0..(m - 2 * n - 1) {
let ip = 2 * n + 1 + t;
let (p, q) = extra_pair(t, n);
let dp = xpt.get(p + 1, p);
let dq = xpt.get(q + 1, q);
xpt.set(ip, p, dp);
xpt.set(ip, q, dq);
let disp = xpt.row(ip).to_vec();
fval[ip] = eval(&xinbd(&x0, &disp, lower, upper, &sl, &su))?;
extras.push((t, p, q));
}
let fbase = fval[0];
let xa: Vec<F> = (0..n).map(|i| xpt.get(i + 1, i)).collect();
let xb: Vec<F> = (0..n).map(|i| xpt.get(i + 1 + n, i)).collect();
let mut gq = vec![zero; n];
let mut gamma_explicit = DenseMatrix::from_fn(n, n, |_, _| zero);
for i in 0..n {
let fa = fval[i + 1] - fbase;
let fb = fval[i + 1 + n] - fbase;
gq[i] = (fa / xa[i] * xb[i] - fb / xb[i] * xa[i]) / (xb[i] - xa[i]);
gamma_explicit.set(i, i, two * (fa / xa[i] - fb / xb[i]) / (xa[i] - xb[i]));
}
for &(t, p, q) in &extras {
let ip = 2 * n + 1 + t;
let val =
(fbase - fval[p + 1] - fval[q + 1] + fval[ip]) / (xpt.get(ip, p) * xpt.get(ip, q));
gamma_explicit.set(p, q, val);
gamma_explicit.set(q, p, val);
}
let gamma = vec![zero; m];
let npt_minus = m - n - 1;
let mut bmat_xi = DenseMatrix::from_fn(n, m, |_, _| zero);
let bmat_ups = DenseMatrix::from_fn(n, n, |_, _| zero);
let mut zmat = DenseMatrix::from_fn(m, npt_minus, |_, _| zero);
let zsign = vec![F::one(); npt_minus];
let half = F::from_f64(0.5).expect("0.5 representable");
let rhosq = rho * rho;
let sqrt2 = two.sqrt();
let sqrt_half = half.sqrt();
for i in 0..n {
bmat_xi.set(i, 0, -(xa[i] + xb[i]) / (xa[i] * xb[i]));
bmat_xi.set(i, i + n + 1, -half / xa[i]);
bmat_xi.set(i, i + 1, -bmat_xi.get(i, 0) - bmat_xi.get(i, i + n + 1));
zmat.set(0, i, sqrt2 / (xa[i] * xb[i]));
zmat.set(i + 1, i, -zmat.get(0, i) - sqrt_half / rhosq);
zmat.set(i + 1 + n, i, sqrt_half / rhosq);
}
for &(t, p, q) in &extras {
let kk = n + t; let ip = 2 * n + 1 + t;
zmat.set(0, kk, F::one() / rhosq);
zmat.set(ip, kk, F::one() / rhosq);
zmat.set(p + 1, kk, -F::one() / rhosq);
zmat.set(q + 1, kk, -F::one() / rhosq);
}
let mut kopt = 0;
for j in 1..m {
if fval[j] < fval[kopt] {
kopt = j;
}
}
let model = QuadraticModel {
n,
m,
x0,
xpt,
fval,
kopt,
gq,
gamma_explicit,
gamma,
bmat_xi,
bmat_ups,
zmat,
zsign,
};
Ok(BoundedInit { model, sl, su })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solver::powell::kkt::assert_h_matches_inverse;
fn init(
x0: Vec<f64>,
lower: &[f64],
upper: &[f64],
rho: f64,
m: usize,
f: impl Fn(&[f64]) -> f64,
) -> BoundedInit<f64> {
QuadraticModel::try_initialize_bounded::<core::convert::Infallible>(
x0,
lower,
upper,
rho,
m,
&mut |x| Ok(f(x)),
)
.expect("infallible objective")
}
#[test]
fn interior_start_recovers_quadratic_and_kkt() {
let g = [[4.0, 1.5], [1.5, 3.0]];
let b = [1.0, 2.0];
let f = |x: &[f64]| {
0.5 * (g[0][0] * x[0] * x[0] + 2.0 * g[0][1] * x[0] * x[1] + g[1][1] * x[1] * x[1])
+ b[0] * x[0]
+ b[1] * x[1]
};
let out = init(vec![0.0, 0.0], &[-10.0, -10.0], &[10.0, 10.0], 0.3, 6, f);
let model = &out.model;
assert!((model.gamma_explicit.get(0, 0) - g[0][0]).abs() < 1e-12);
assert!((model.gamma_explicit.get(1, 1) - g[1][1]).abs() < 1e-12);
assert!((model.gamma_explicit.get(0, 1) - g[0][1]).abs() < 1e-12);
assert!((model.gamma_explicit.get(1, 0) - g[0][1]).abs() < 1e-12);
assert!((model.gradient()[0] - b[0]).abs() < 1e-12);
assert!((model.gradient()[1] - b[1]).abs() < 1e-12);
assert_h_matches_inverse(model, 1e-10);
}
#[test]
fn lower_bound_start_is_exact_and_kkt() {
let f = |x: &[f64]| {
2.0 * x[0] * x[0] + 1.5 * x[0] * x[1] + 3.0 * x[1] * x[1] + x[0] - 2.0 * x[1] + 0.7
};
let out = init(vec![0.0, 0.5], &[0.0, -5.0], &[5.0, 5.0], 0.25, 6, f);
let model = &out.model;
assert!(out.sl[0] == 0.0, "coord 0 pinned to lower bound");
assert!(out.su[0] > 0.0);
let x0 = model.x0();
let f0 = f(x0);
for j in 0..model.m() {
let disp = model.xpt_row(j).to_vec();
let xj: Vec<f64> = x0.iter().zip(&disp).map(|(a, d)| a + d).collect();
assert!(
(model.eval_change(&disp) - (f(&xj) - f0)).abs() < 1e-9,
"interpolation at point {j}"
);
}
assert_h_matches_inverse(model, 1e-9);
}
#[test]
fn upper_bound_start_is_kkt() {
let f = |x: &[f64]| (x[0] - 3.0).powi(2) + 2.0 * (x[1] + 1.0).powi(2);
let out = init(vec![2.0, 0.0], &[-5.0, -5.0], &[2.0, 5.0], 0.5, 5, f);
assert!(out.su[0] == 0.0, "coord 0 pinned to upper bound");
assert!(out.sl[0] < 0.0);
assert_h_matches_inverse(&out.model, 1e-9);
}
#[test]
fn x0_half_rho_split() {
let f = |x: &[f64]| x[0] * x[0] + x[1] * x[1];
let rho = 0.5; let out = init(vec![0.2, 0.4], &[0.0, 0.0], &[10.0, 10.0], rho, 5, f);
assert_eq!(out.model.x0()[0], 0.0, "within ½ρ snaps to the bound");
assert_eq!(out.model.x0()[1], 0.5, "between ½ρ and ρ pushed ρ interior");
assert_eq!(out.sl[0], 0.0);
assert!(out.sl[1] < 0.0 && out.su[1] > 0.0);
assert_h_matches_inverse(&out.model, 1e-9);
}
#[test]
fn x0_adjusted_into_box() {
let f = |x: &[f64]| x[0] * x[0] + x[1] * x[1];
let out = init(vec![-9.0, 4.9], &[-2.0, -5.0], &[5.0, 5.0], 0.5, 5, f);
assert!(out.model.x0()[0] == -2.0, "x0_0 clipped to lower bound");
assert!(out.model.x0()[1] == 5.0, "x0_1 pushed onto upper bound");
assert_h_matches_inverse(&out.model, 1e-9);
}
}