use crate::core::math::{DenseMatrix, Scalar};
pub(crate) struct QuadraticModel<F = f64> {
pub(crate) n: usize,
pub(crate) m: usize,
pub(crate) x0: Vec<F>,
pub(crate) xpt: DenseMatrix<F>,
pub(crate) fval: Vec<F>,
pub(crate) kopt: usize,
pub(crate) gq: Vec<F>,
pub(crate) gamma_explicit: DenseMatrix<F>,
pub(crate) gamma: Vec<F>,
pub(crate) bmat_xi: DenseMatrix<F>,
pub(crate) bmat_ups: DenseMatrix<F>,
pub(crate) zmat: DenseMatrix<F>,
pub(crate) zsign: Vec<F>,
}
impl<F: Scalar> QuadraticModel<F> {
pub(crate) fn n(&self) -> usize {
self.n
}
pub(crate) fn m(&self) -> usize {
self.m
}
pub(crate) fn kopt(&self) -> usize {
self.kopt
}
pub(crate) fn xpt_row(&self, j: usize) -> &[F] {
self.xpt.row(j)
}
pub(crate) fn x0(&self) -> &[F] {
&self.x0
}
pub(crate) fn fval(&self, j: usize) -> F {
self.fval[j]
}
pub(crate) fn fopt(&self) -> F {
self.fval[self.kopt]
}
pub(crate) fn best_point(&self) -> Vec<F> {
let row = self.xpt.row(self.kopt);
(0..self.n).map(|i| self.x0[i] + row[i]).collect()
}
pub(crate) fn gradient(&self) -> &[F] {
&self.gq
}
pub(crate) fn hessian_matvec(&self, v: &[F]) -> Vec<F> {
assert_eq!(v.len(), self.n, "hessian_matvec: v must have length n");
let mut y = vec![F::zero(); self.n];
for i in 0..self.n {
let mut acc = F::zero();
for j in 0..self.n {
acc = acc + self.gamma_explicit.get(i, j) * v[j];
}
y[i] = acc;
}
for k in 0..self.m {
let row = self.xpt.row(k);
let dot = dot(row, v);
let coef = self.gamma[k] * dot;
for i in 0..self.n {
y[i] = y[i] + coef * row[i];
}
}
y
}
pub(crate) fn lagrange_hessian_matvec(&self, lambda: &[F], u: &[F]) -> Vec<F> {
assert_eq!(
lambda.len(),
self.m,
"lagrange_hessian_matvec: |λ| must be m"
);
assert_eq!(u.len(), self.n, "lagrange_hessian_matvec: |u| must be n");
let mut y = vec![F::zero(); self.n];
for k in 0..self.m {
let row = self.xpt.row(k);
let coef = lambda[k] * dot(row, u);
for i in 0..self.n {
y[i] = y[i] + coef * row[i];
}
}
y
}
pub(crate) fn eval_change(&self, d: &[F]) -> F {
assert_eq!(d.len(), self.n, "eval_change: d must have length n");
let hd = self.hessian_matvec(d);
let lin = dot(d, &self.gq);
let quad = dot(d, &hd);
let half = F::from_f64(0.5).expect("0.5 representable");
lin + half * quad
}
}
fn dot<F: Scalar>(a: &[F], b: &[F]) -> F {
a.iter().zip(b).map(|(x, y)| *x * *y).sum()
}
#[cfg(test)]
impl<F: Scalar> QuadraticModel<F> {
#[allow(clippy::too_many_arguments)]
pub(crate) fn from_parts(
n: usize,
m: usize,
x0: Vec<F>,
xpt: DenseMatrix<F>,
fval: Vec<F>,
kopt: usize,
gq: Vec<F>,
gamma_explicit: DenseMatrix<F>,
gamma: Vec<F>,
bmat_xi: DenseMatrix<F>,
bmat_ups: DenseMatrix<F>,
zmat: DenseMatrix<F>,
zsign: Vec<F>,
) -> Self {
Self {
n,
m,
x0,
xpt,
fval,
kopt,
gq,
gamma_explicit,
gamma,
bmat_xi,
bmat_ups,
zmat,
zsign,
}
}
pub(crate) fn dense_hessian(&self) -> DenseMatrix<F> {
let mut h = self.gamma_explicit.clone();
for k in 0..self.m {
let row = self.xpt.row(k).to_vec();
for i in 0..self.n {
for j in 0..self.n {
let add = self.gamma[k] * row[i] * row[j];
h.set(i, j, h.get(i, j) + add);
}
}
}
h
}
}
#[cfg(test)]
mod tests {
use super::*;
fn model_with(
n: usize,
m: usize,
gq: Vec<f64>,
gamma_explicit: DenseMatrix,
gamma: Vec<f64>,
xpt_rows: &[&[f64]],
) -> QuadraticModel {
let mut data = Vec::with_capacity(m * n);
for row in xpt_rows {
assert_eq!(row.len(), n);
data.extend_from_slice(row);
}
let xpt = DenseMatrix::from_row_slice(m, n, &data);
let npt_minus = m - n - 1;
QuadraticModel::from_parts(
n,
m,
vec![0.0; n],
xpt,
vec![0.0; m],
0,
gq,
gamma_explicit,
gamma,
DenseMatrix::from_fn(n, m, |_, _| 0.0),
DenseMatrix::from_fn(n, n, |_, _| 0.0),
DenseMatrix::from_fn(m, npt_minus, |_, _| 0.0),
vec![1.0; npt_minus],
)
}
#[test]
fn hessian_matvec_matches_dense_assembly() {
let n = 2;
let m = 5;
let gamma_explicit = DenseMatrix::from_row_slice(2, 2, &[3.0, 1.0, 1.0, 4.0]);
let gamma = vec![0.5, 0.0, -0.25, 0.0, 0.0];
let xpt_rows: &[&[f64]] = &[
&[1.0, 0.0],
&[0.0, 1.0],
&[1.0, 1.0],
&[-1.0, 0.0],
&[0.0, -1.0],
];
let model = model_with(n, m, vec![0.0, 0.0], gamma_explicit, gamma, xpt_rows);
let dense = model.dense_hessian();
for j in 0..n {
let mut e = vec![0.0; n];
e[j] = 1.0;
let col = model.hessian_matvec(&e);
for i in 0..n {
assert!(
(col[i] - dense.get(i, j)).abs() < 1e-12,
"∇²Q[{i},{j}]: matvec {} vs dense {}",
col[i],
dense.get(i, j)
);
}
}
}
#[test]
fn hessian_matvec_is_symmetric() {
let n = 3;
let m = 7;
let gamma_explicit =
DenseMatrix::from_row_slice(3, 3, &[2.0, -1.0, 0.5, -1.0, 3.0, 0.25, 0.5, 0.25, 1.0]);
let gamma = vec![0.3, -0.1, 0.0, 0.2, 0.0, 0.0, 0.15];
let xpt_rows: &[&[f64]] = &[
&[1.0, 0.0, 0.0],
&[0.0, 1.0, 0.0],
&[0.0, 0.0, 1.0],
&[1.0, -1.0, 0.0],
&[0.5, 0.5, 0.5],
&[-1.0, 0.0, 2.0],
&[0.0, 1.0, -1.0],
];
let model = model_with(n, m, vec![0.0; n], gamma_explicit, gamma, xpt_rows);
let h = model.dense_hessian();
for i in 0..n {
for j in 0..n {
assert!((h.get(i, j) - h.get(j, i)).abs() < 1e-12);
}
}
}
#[test]
fn eval_change_matches_quadratic_form() {
let n = 2;
let m = 5;
let b = vec![2.0, -3.0];
let gamma_explicit = DenseMatrix::from_row_slice(2, 2, &[3.0, 1.0, 1.0, 4.0]);
let gamma = vec![0.5, 0.0, -0.25, 0.0, 0.0];
let xpt_rows: &[&[f64]] = &[
&[1.0, 0.0],
&[0.0, 1.0],
&[1.0, 1.0],
&[-1.0, 0.0],
&[0.0, -1.0],
];
let model = model_with(n, m, b.clone(), gamma_explicit, gamma, xpt_rows);
let dense = model.dense_hessian();
for d in [vec![0.7, -1.2], vec![-2.0, 0.3], vec![1.0, 1.0]] {
let lin: f64 = b.iter().zip(&d).map(|(x, y)| x * y).sum();
let mut quad = 0.0;
for i in 0..n {
for j in 0..n {
quad += d[i] * dense.get(i, j) * d[j];
}
}
let expected = lin + 0.5 * quad;
let got = model.eval_change(&d);
assert!(
(got - expected).abs() < 1e-12,
"eval_change={got} expected={expected} for d={d:?}"
);
}
}
}