use crate::core::math::Scalar;
use crate::solver::powell::model::QuadraticModel;
impl<F: Scalar> QuadraticModel<F> {
pub(crate) fn shift_origin(&mut self) {
let n = self.n;
let m = self.m;
let rank = m - n - 1;
let half = F::from_f64(0.5).expect("0.5 representable");
let quarter = F::from_f64(0.25).expect("0.25 representable");
let s = self.xpt.row(self.kopt).to_vec();
let s_sq = dot(&s, &s);
if s_sq <= F::zero() {
return; }
let hs = self.hessian_matvec(&s);
for i in 0..n {
self.gq[i] = self.gq[i] + hs[i];
}
let mut v = vec![F::zero(); n];
for j in 0..m {
let gj = self.gamma[j];
let row = self.xpt.row(j);
for i in 0..n {
v[i] = v[i] + gj * (row[i] - half * s[i]);
}
}
for a in 0..n {
for b in 0..n {
let add = v[a] * s[b] + s[a] * v[b];
self.gamma_explicit
.set(a, b, self.gamma_explicit.get(a, b) + add);
}
}
let mut y: Vec<Vec<F>> = Vec::with_capacity(m);
for j in 0..m {
let row = self.xpt.row(j);
let wj: Vec<F> = (0..n).map(|i| row[i] - half * s[i]).collect();
let s_dot_w = dot(&s, &wj);
let yj: Vec<F> = (0..n)
.map(|i| s_dot_w * wj[i] + quarter * s_sq * s[i])
.collect();
y.push(yj);
}
let mut yz: Vec<Vec<F>> = Vec::with_capacity(rank);
for k in 0..rank {
let mut col = vec![F::zero(); n];
for j in 0..m {
let zjk = self.zmat.get(j, k);
for i in 0..n {
col[i] = col[i] + y[j][i] * zjk;
}
}
yz.push(col);
}
let mut ups_add = vec![vec![F::zero(); n]; n];
for a in 0..n {
for b in 0..n {
let mut yxt = F::zero(); let mut xyt = F::zero(); for i in 0..m {
yxt = yxt + y[i][a] * self.bmat_xi.get(b, i);
xyt = xyt + y[i][b] * self.bmat_xi.get(a, i);
}
let mut yomyt = F::zero();
for k in 0..rank {
yomyt = yomyt + self.zsign[k] * yz[k][a] * yz[k][b];
}
ups_add[a][b] = yxt + xyt + yomyt;
}
}
for a in 0..n {
for b in 0..n {
self.bmat_ups
.set(a, b, self.bmat_ups.get(a, b) + ups_add[a][b]);
}
}
for a in 0..n {
for i in 0..m {
let mut yom = F::zero();
for k in 0..rank {
yom = yom + self.zsign[k] * yz[k][a] * self.zmat.get(i, k);
}
self.bmat_xi.set(a, i, self.bmat_xi.get(a, i) + yom);
}
}
for j in 0..m {
for i in 0..n {
let val = self.xpt.get(j, i) - s[i];
self.xpt.set(j, i, val);
}
}
for i in 0..n {
self.x0[i] = self.x0[i] + s[i];
}
}
}
fn dot<F: Scalar>(a: &[F], b: &[F]) -> F {
a.iter().zip(b).map(|(x, y)| *x * *y).sum()
}
#[cfg(test)]
mod tests {
use crate::solver::powell::kkt::assert_h_matches_inverse;
use crate::solver::powell::model::QuadraticModel;
fn offset_quadratic(x: &[f64]) -> f64 {
(x[0] - 2.0).powi(2) + 3.0 * (x[1] + 1.5).powi(2) + 0.5 * x[0] * x[1]
}
#[test]
fn shift_preserves_kkt_identity() {
let mut model = QuadraticModel::initialize(vec![0.0, 0.0], 0.5, 5, &offset_quadratic);
assert_ne!(model.kopt(), usize::MAX);
assert!(model.xpt_row(model.kopt()).iter().any(|v| v.abs() > 0.0));
model.shift_origin();
assert_h_matches_inverse(&model, 1e-9);
assert!(model.xpt_row(model.kopt()).iter().all(|v| v.abs() < 1e-12));
}
#[test]
fn shift_preserves_the_quadratic() {
let mut model = QuadraticModel::initialize(vec![0.0, 0.0], 0.5, 5, &offset_quadratic);
let m = model.m();
let kopt = model.kopt();
let probes = [[0.3, -0.4], [-0.2, 0.1], [0.5, 0.5]];
let x0_old = model.x0().to_vec();
let before: Vec<f64> = probes
.iter()
.map(|p| {
let d: Vec<f64> = (0..2).map(|i| p[i] - x0_old[i]).collect();
model.eval_change(&d)
})
.collect();
let interp_before: Vec<f64> = (0..m)
.map(|j| {
model.eval_change(model.xpt_row(j).to_vec().as_slice())
- model.eval_change(model.xpt_row(kopt).to_vec().as_slice())
- (model.fval(j) - model.fopt())
})
.collect();
model.shift_origin();
let x0_new = model.x0().to_vec();
for (k, p) in probes.iter().enumerate() {
let d: Vec<f64> = (0..2).map(|i| p[i] - x0_new[i]).collect();
let after = model.eval_change(&d);
let q0 = {
let d0: Vec<f64> = (0..2).map(|i| probes[0][i] - x0_new[i]).collect();
model.eval_change(&d0)
};
let want = before[k] - before[0];
assert!(
(after - q0 - want).abs() < 1e-9,
"probe {k}: Δ {} vs {want}",
after - q0
);
}
for j in 0..m {
let res = model.eval_change(model.xpt_row(j).to_vec().as_slice())
- model.eval_change(model.xpt_row(kopt).to_vec().as_slice())
- (model.fval(j) - model.fopt());
assert!(
(res - interp_before[j]).abs() < 1e-9 && res.abs() < 1e-8,
"interp residual {j}: {res}"
);
}
}
#[test]
fn shift_after_updates_preserves_kkt() {
let f = |x: &[f64]| {
(1.0 - x[0]).powi(2)
+ 100.0 * (x[1] - x[0] * x[0]).powi(2)
+ (x[2] - 0.5).powi(2) * (x[2] - 0.5).powi(2)
};
let mut model = QuadraticModel::initialize(vec![-1.0, 1.0, 0.0], 0.4, 7, &f);
for step in 0..6 {
let kopt = model.kopt();
let xopt = model.xpt_row(kopt).to_vec();
let d = [0.15 * (step as f64 + 1.0) * 0.3, -0.1, 0.05];
let xnew_disp: Vec<f64> = (0..3).map(|i| xopt[i] + d[i]).collect();
let xabs: Vec<f64> = (0..3).map(|i| model.x0()[i] + xnew_disp[i]).collect();
let f_new = f(&xabs);
let ctx = model.prepare_update(&xnew_disp);
let t = (0..model.m())
.filter(|j| *j != kopt)
.max_by(|a, b| {
let da = model.update_params(*a, &ctx).sigma.abs();
let db = model.update_params(*b, &ctx).sigma.abs();
da.partial_cmp(&db).unwrap()
})
.unwrap();
let sc = model.update_params(t, &ctx);
if sc.sigma != 0.0 {
model.commit_update(t, &ctx, &sc, f_new);
}
}
assert_h_matches_inverse(&model, 1e-7);
model.shift_origin();
assert_h_matches_inverse(&model, 1e-7);
}
}