pub mod dogleg;
pub mod steihaug;
pub use dogleg::Dogleg;
pub use steihaug::Steihaug;
use std::marker::PhantomData;
use crate::core::inner::InitialState;
use crate::core::math::{Dot, MatVec, NegInPlace, NormSquared, Scalar, ScaleInPlace, ScaledAdd};
use crate::core::problem::{CostFunction, Gradient, Hessian, HessianProduct, Problem};
use crate::core::solver::Solver;
use crate::core::state::BasicState;
use crate::core::termination::TerminationReason;
pub(crate) struct Step<V, F> {
pub(crate) d: V,
pub(crate) predicted_reduction: F,
pub(crate) hit_boundary: bool,
}
pub(crate) trait Subproblem<V, M, F> {
fn solve(&self, gradient: &V, hessian: &M, radius: F) -> Step<V, F>;
}
pub(crate) trait SubproblemHvp<V, F> {
fn solve_hvp<E>(
&self,
gradient: &V,
radius: F,
bv: impl FnMut(&V) -> Result<V, E>,
) -> Result<Step<V, F>, E>;
}
pub(crate) fn model_decrease_from_bd<V, F>(g: &V, d: &V, bd: &V) -> F
where
F: Scalar,
V: Dot<F>,
{
let half = F::from_f64(0.5).unwrap();
-g.dot(d) - half * d.dot(bd)
}
pub(crate) fn model_decrease<V, M, F>(g: &V, b: &M, d: &V) -> F
where
F: Scalar,
V: Dot<F>,
M: MatVec<V>,
{
let bd = b.matvec(d);
model_decrease_from_bd(g, d, &bd)
}
pub(crate) fn tau_to_boundary<V, F>(z: &V, d: &V, radius: F) -> F
where
F: Scalar,
V: Dot<F>,
{
let dd = d.dot(d);
let zd = z.dot(d);
let zz = z.dot(z);
let rr = radius * radius;
let disc = zd * zd - dd * (zz - rr);
let disc = if disc < F::zero() { F::zero() } else { disc };
(-zd + disc.sqrt()) / dd
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CauchyPoint;
impl<V, M, F> Subproblem<V, M, F> for CauchyPoint
where
F: Scalar,
V: Clone + Dot<F> + NormSquared<F> + ScaleInPlace<F> + NegInPlace,
M: MatVec<V>,
{
fn solve(&self, g: &V, b: &M, radius: F) -> Step<V, F> {
let g_norm = g.norm_squared().sqrt();
if g_norm <= F::zero() {
let mut d = g.clone();
d.scale_in_place(F::zero());
return Step {
d,
predicted_reduction: F::zero(),
hit_boundary: false,
};
}
let bg = b.matvec(g);
let gbg = g.dot(&bg);
let tau = if gbg <= F::zero() {
F::one()
} else {
let t = g_norm * g_norm * g_norm / (radius * gbg);
if t < F::one() { t } else { F::one() }
};
let mut d = g.clone();
d.scale_in_place(-(tau * radius / g_norm));
let predicted_reduction = model_decrease(g, b, &d);
Step {
d,
predicted_reduction,
hit_boundary: tau >= F::one(),
}
}
}
impl<V, F> SubproblemHvp<V, F> for CauchyPoint
where
F: Scalar,
V: Clone + Dot<F> + NormSquared<F> + ScaleInPlace<F> + NegInPlace,
{
fn solve_hvp<E>(
&self,
g: &V,
radius: F,
mut bv: impl FnMut(&V) -> Result<V, E>,
) -> Result<Step<V, F>, E> {
let g_norm = g.norm_squared().sqrt();
if g_norm <= F::zero() {
let mut d = g.clone();
d.scale_in_place(F::zero());
return Ok(Step {
d,
predicted_reduction: F::zero(),
hit_boundary: false,
});
}
let bg = bv(g)?;
let gbg = g.dot(&bg);
let tau = if gbg <= F::zero() {
F::one()
} else {
let t = g_norm * g_norm * g_norm / (radius * gbg);
if t < F::one() { t } else { F::one() }
};
let c = -(tau * radius / g_norm);
let mut d = g.clone();
d.scale_in_place(c);
let mut bd = bg;
bd.scale_in_place(c);
let predicted_reduction = model_decrease_from_bd(g, &d, &bd);
Ok(Step {
d,
predicted_reduction,
hit_boundary: tau >= F::one(),
})
}
}
pub struct TrustRegion<Sub = Steihaug, F = f64, Mode = ExactHessian> {
subproblem: Sub,
radius: F,
initial_radius: F,
max_radius: F,
eta: F,
max_inner: u32,
mode: PhantomData<Mode>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ExactHessian;
#[derive(Debug, Clone, Copy, Default)]
pub struct MatrixFree;
impl Default for TrustRegion<Steihaug> {
fn default() -> Self {
Self::new()
}
}
impl TrustRegion<Steihaug> {
pub fn new() -> Self {
Self::with_subproblem(Steihaug::new())
}
}
impl TrustRegion<Steihaug, f64, MatrixFree> {
pub fn matrix_free() -> Self {
Self::matrix_free_with(Steihaug::new())
}
}
impl<Sub, F: Scalar> TrustRegion<Sub, F, MatrixFree> {
pub fn matrix_free_with(subproblem: Sub) -> Self {
Self {
subproblem,
radius: F::one(),
initial_radius: F::one(),
max_radius: F::from_f64(100.0).unwrap(),
eta: F::from_f64(0.125).unwrap(),
max_inner: 10,
mode: PhantomData,
}
}
}
impl<Sub, F: Scalar> TrustRegion<Sub, F> {
pub fn with_subproblem(subproblem: Sub) -> Self {
Self {
subproblem,
radius: F::one(),
initial_radius: F::one(),
max_radius: F::from_f64(100.0).unwrap(),
eta: F::from_f64(0.125).unwrap(),
max_inner: 10,
mode: PhantomData,
}
}
}
impl<Sub, F: Scalar, Mode> TrustRegion<Sub, F, Mode> {
pub fn with_radius(mut self, radius: F) -> Self {
assert!(radius > F::zero(), "initial radius must be > 0");
self.initial_radius = radius;
self.radius = radius;
self
}
pub fn with_max_radius(mut self, max_radius: F) -> Self {
assert!(max_radius > F::zero(), "max radius must be > 0");
self.max_radius = max_radius;
self
}
pub fn with_eta(mut self, eta: F) -> Self {
assert!(
eta >= F::zero() && eta < F::from_f64(0.25).unwrap(),
"eta must be in [0, 1/4)"
);
self.eta = eta;
self
}
pub fn with_max_inner_attempts(mut self, n: u32) -> Self {
assert!(n >= 1, "max inner attempts must be ≥ 1");
self.max_inner = n;
self
}
}
impl<Sub, V, F, Mode> InitialState<V> for TrustRegion<Sub, F, Mode>
where
F: Scalar,
V: Clone,
{
type State = BasicState<V, F>;
fn seed(&self, x: &V) -> Self::State {
BasicState::new(x.clone())
}
}
fn tr_init<P, V, F>(
radius: &mut F,
initial_radius: F,
problem: &mut Problem<P>,
mut state: BasicState<V, F>,
) -> Result<BasicState<V, F>, P::Error>
where
F: Scalar,
P: CostFunction<Param = V, Output = F> + Gradient<Gradient = V>,
{
*radius = initial_radius;
let (cost, grad) = problem.cost_and_gradient(&state.param)?;
state.cost = Some(cost);
state.gradient = Some(grad);
Ok(state)
}
#[allow(clippy::type_complexity)]
fn tr_next_iter<P, V, F>(
radius: &mut F,
max_radius: F,
eta: F,
max_inner: u32,
problem: &mut Problem<P>,
mut state: BasicState<V, F>,
mut attempt: impl FnMut(&mut Problem<P>, &V, &V, F) -> Result<Step<V, F>, P::Error>,
) -> Result<(BasicState<V, F>, Option<TerminationReason>), P::Error>
where
F: Scalar,
P: CostFunction<Param = V, Output = F> + Gradient<Gradient = V>,
V: Clone + ScaledAdd<F> + NormSquared<F>,
{
let g = state
.gradient
.take()
.expect("gradient not set: Solver::init must run before next_iter");
let cost_old = state
.cost
.expect("cost not set: Solver::init must run before next_iter");
let quarter = F::from_f64(0.25).unwrap();
let three_quarters = F::from_f64(0.75).unwrap();
let two = F::from_f64(2.0).unwrap();
for _ in 0..max_inner {
let step = attempt(problem, &state.param, &g, *radius)?;
if step.predicted_reduction <= F::zero() {
state.gradient = Some(g);
return Ok((state, Some(TerminationReason::SolverConverged)));
}
let mut trial = state.param.clone();
trial.scaled_add(F::one(), &step.d);
let cost_trial = problem.cost(&trial)?;
let rho = (cost_old - cost_trial) / step.predicted_reduction;
let step_norm = step.d.norm_squared().sqrt();
if rho < quarter || !rho.is_finite() {
*radius = quarter * step_norm;
} else if rho > three_quarters && step.hit_boundary {
let grown = two * *radius;
*radius = if grown < max_radius {
grown
} else {
max_radius
};
}
if rho > eta {
state.param = trial;
state.cost = Some(cost_trial);
let g_new = problem.gradient(&state.param)?;
state.gradient = Some(g_new);
return Ok((state, None));
}
}
state.gradient = Some(g);
Ok((state, None))
}
impl<P, Sub, V, M, F> Solver<P, BasicState<V, F>> for TrustRegion<Sub, F, ExactHessian>
where
F: Scalar,
P: CostFunction<Param = V, Output = F> + Gradient<Gradient = V> + Hessian<Hessian = M>,
V: Clone + ScaledAdd<F> + NormSquared<F>,
Sub: Subproblem<V, M, F>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
state: BasicState<V, F>,
) -> Result<BasicState<V, F>, Self::Error> {
tr_init(&mut self.radius, self.initial_radius, problem, state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
state: BasicState<V, F>,
) -> Result<(BasicState<V, F>, Option<TerminationReason>), Self::Error> {
let b = problem.hessian(&state.param)?;
let subproblem = &self.subproblem;
tr_next_iter(
&mut self.radius,
self.max_radius,
self.eta,
self.max_inner,
problem,
state,
|_, _, g, radius| Ok(subproblem.solve(g, &b, radius)),
)
}
}
impl<P, Sub, V, F> Solver<P, BasicState<V, F>> for TrustRegion<Sub, F, MatrixFree>
where
F: Scalar,
P: CostFunction<Param = V, Output = F> + Gradient<Gradient = V> + HessianProduct,
V: Clone + ScaledAdd<F> + NormSquared<F>,
Sub: SubproblemHvp<V, F>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
state: BasicState<V, F>,
) -> Result<BasicState<V, F>, Self::Error> {
tr_init(&mut self.radius, self.initial_radius, problem, state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
state: BasicState<V, F>,
) -> Result<(BasicState<V, F>, Option<TerminationReason>), Self::Error> {
let subproblem = &self.subproblem;
tr_next_iter(
&mut self.radius,
self.max_radius,
self.eta,
self.max_inner,
problem,
state,
|problem, x, g, radius| {
subproblem.solve_hvp(g, radius, |v| problem.hessian_product(x, v))
},
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{BasicState, Executor, GradientTolerance};
struct Quadratic;
impl CostFunction for Quadratic {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, Self::Error> {
Ok(0.5 * (x[0] * x[0] + 100.0 * x[1] * x[1]))
}
}
impl Gradient for Quadratic {
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
Ok(vec![x[0], 100.0 * x[1]])
}
}
impl Hessian for Quadratic {
type Hessian = crate::core::math::DenseMatrix<f64>;
fn hessian(&self, _x: &Vec<f64>) -> Result<Self::Hessian, Self::Error> {
Ok(crate::core::math::DenseMatrix::from_row_slice(
2,
2,
&[1.0, 0.0, 0.0, 100.0],
))
}
}
struct Rosenbrock;
impl CostFunction for Rosenbrock {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, Self::Error> {
Ok((1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0].powi(2)).powi(2))
}
}
impl Gradient for Rosenbrock {
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
Ok(vec![
-2.0 * (1.0 - x[0]) - 400.0 * x[0] * (x[1] - x[0].powi(2)),
200.0 * (x[1] - x[0].powi(2)),
])
}
}
impl Hessian for Rosenbrock {
type Hessian = crate::core::math::DenseMatrix<f64>;
fn hessian(&self, x: &Vec<f64>) -> Result<Self::Hessian, Self::Error> {
let h11 = 2.0 + 1200.0 * x[0] * x[0] - 400.0 * x[1];
let h12 = -400.0 * x[0];
Ok(crate::core::math::DenseMatrix::from_row_slice(
2,
2,
&[h11, h12, h12, 200.0],
))
}
}
#[test]
fn cauchy_point_minimizes_quadratic() {
let result = Executor::new(
Quadratic,
TrustRegion::with_subproblem(CauchyPoint),
BasicState::new(vec![5.0, 1.0]),
)
.max_iter(500)
.terminate_on(GradientTolerance(1e-8))
.run()
.unwrap();
assert!(result.cost() < 1e-8, "cost = {}", result.cost());
}
#[test]
fn steihaug_minimizes_quadratic() {
let result = Executor::new(
Quadratic,
TrustRegion::with_subproblem(Steihaug::new()),
BasicState::new(vec![5.0, 1.0]),
)
.max_iter(100)
.terminate_on(GradientTolerance(1e-10))
.run()
.unwrap();
assert!(result.cost() < 1e-16, "cost = {}", result.cost());
}
#[test]
fn dogleg_minimizes_quadratic() {
let result = Executor::new(
Quadratic,
TrustRegion::with_subproblem(Dogleg),
BasicState::new(vec![5.0, 1.0]),
)
.max_iter(100)
.terminate_on(GradientTolerance(1e-10))
.run()
.unwrap();
assert!(result.cost() < 1e-16, "cost = {}", result.cost());
}
#[test]
fn steihaug_minimizes_rosenbrock() {
let result = Executor::new(
Rosenbrock,
TrustRegion::new(),
BasicState::new(vec![-1.2, 1.0]),
)
.max_iter(200)
.terminate_on(GradientTolerance(1e-8))
.run()
.unwrap();
assert!(result.cost() < 1e-10, "cost = {}", result.cost());
}
#[test]
fn dogleg_minimizes_rosenbrock() {
let result = Executor::new(
Rosenbrock,
TrustRegion::with_subproblem(Dogleg),
BasicState::new(vec![-1.2, 1.0]),
)
.max_iter(500)
.terminate_on(GradientTolerance(1e-8))
.run()
.unwrap();
assert!(result.cost() < 1e-10, "cost = {}", result.cost());
}
struct QuadraticHvOnly;
impl CostFunction for QuadraticHvOnly {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, Self::Error> {
Ok(0.5 * (x[0] * x[0] + 100.0 * x[1] * x[1]))
}
}
impl Gradient for QuadraticHvOnly {
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
Ok(vec![x[0], 100.0 * x[1]])
}
}
impl HessianProduct for QuadraticHvOnly {
fn hessian_product(&self, _x: &Vec<f64>, v: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
Ok(vec![v[0], 100.0 * v[1]])
}
}
struct RosenbrockHvOnly;
impl CostFunction for RosenbrockHvOnly {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, Self::Error> {
Rosenbrock.cost(x)
}
}
impl Gradient for RosenbrockHvOnly {
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
Rosenbrock.gradient(x)
}
}
impl HessianProduct for RosenbrockHvOnly {
fn hessian_product(&self, x: &Vec<f64>, v: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
let h11 = 2.0 + 1200.0 * x[0] * x[0] - 400.0 * x[1];
let h12 = -400.0 * x[0];
Ok(vec![h11 * v[0] + h12 * v[1], h12 * v[0] + 200.0 * v[1]])
}
}
#[test]
fn matrix_free_steihaug_minimizes_quadratic() {
let result = Executor::new(
QuadraticHvOnly,
TrustRegion::matrix_free(),
BasicState::new(vec![5.0, 1.0]),
)
.max_iter(100)
.terminate_on(GradientTolerance(1e-10))
.run()
.unwrap();
assert!(result.cost() < 1e-16, "cost = {}", result.cost());
}
#[test]
fn matrix_free_steihaug_minimizes_rosenbrock() {
let result = Executor::new(
RosenbrockHvOnly,
TrustRegion::matrix_free(),
BasicState::new(vec![-1.2, 1.0]),
)
.max_iter(200)
.terminate_on(GradientTolerance(1e-8))
.run()
.unwrap();
assert!(result.cost() < 1e-10, "cost = {}", result.cost());
}
#[test]
fn matrix_free_cauchy_point_minimizes_quadratic() {
let result = Executor::new(
QuadraticHvOnly,
TrustRegion::matrix_free_with(CauchyPoint),
BasicState::new(vec![5.0, 1.0]),
)
.max_iter(500)
.terminate_on(GradientTolerance(1e-8))
.run()
.unwrap();
assert!(result.cost() < 1e-8, "cost = {}", result.cost());
}
#[test]
fn matrix_free_matches_exact_steihaug() {
let exact = Executor::new(
Quadratic,
TrustRegion::new(),
BasicState::new(vec![5.0, 1.0]),
)
.max_iter(100)
.terminate_on(GradientTolerance(1e-10))
.run()
.unwrap();
let free = Executor::new(
QuadraticHvOnly,
TrustRegion::matrix_free(),
BasicState::new(vec![5.0, 1.0]),
)
.max_iter(100)
.terminate_on(GradientTolerance(1e-10))
.run()
.unwrap();
assert_eq!(exact.state.iter, free.state.iter);
assert!((exact.cost() - free.cost()).abs() < 1e-15);
}
#[test]
fn matrix_free_counts_products_not_hessians() {
use crate::core::solver::Solver as _;
let mut problem = Problem::new(RosenbrockHvOnly);
let mut solver = TrustRegion::matrix_free();
let mut state = solver
.init(&mut problem, BasicState::new(vec![-1.2, 1.0]))
.unwrap();
for _ in 0..3 {
let (next, _) = solver.next_iter(&mut problem, state).unwrap();
state = next;
}
let counts = problem.counts();
assert!(counts.hessian_product_evals > 0);
assert_eq!(counts.hessian_evals, 0);
use crate::core::state::CountsMirror as _;
state.mirror(counts);
assert!(state.gradient_evals >= counts.hessian_product_evals);
}
}