pub mod diagnostics;
pub mod dogleg;
pub mod more_sorensen;
pub mod steihaug;
pub use dogleg::Dogleg;
pub use more_sorensen::MoreSorensen;
pub use steihaug::Steihaug;
use std::marker::PhantomData;
use crate::core::inner::InitialState;
use crate::core::math::{
Dot, MatVec, NegInPlace, NormSquared, Scalar, ScaleInPlace, ScaledAdd,
VectorLen,
};
use crate::core::problem::{
CostFunction, Gradient, Hessian, HessianProduct, Problem,
};
use crate::core::solver::Solver;
use crate::core::state::{FirstOrderState, State};
use crate::core::termination::Termination;
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)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
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(),
})
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TrustRegion<Sub = Steihaug, F: Scalar = f64, Mode = ExactHessian> {
subproblem: Sub,
radius: F,
initial_radius: F,
max_radius: F,
eta: F,
max_inner: u32,
rejected_step: bool,
#[cfg_attr(feature = "serde", serde(skip))]
trials: Option<Vec<diagnostics::Trial<F>>>,
mode: PhantomData<Mode>,
}
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ExactHessian;
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
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,
rejected_step: false,
trials: None,
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,
rejected_step: false,
trials: None,
mode: PhantomData,
}
}
}
impl<Sub, F: Scalar, Mode> TrustRegion<Sub, F, Mode> {
pub fn with_trial_diagnostics(mut self, enabled: bool) -> Self {
self.trials = enabled.then(Vec::new);
self
}
pub fn trial_observations(&self) -> &[diagnostics::Trial<F>] {
self.trials.as_deref().unwrap_or(&[])
}
pub fn radius(&self) -> F {
self.radius
}
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 = FirstOrderState<V, F>;
fn seed(&self, x: &V) -> Self::State {
FirstOrderState::new(x.clone())
}
}
fn tr_init<P, V, F>(
radius: &mut F,
initial_radius: F,
problem: &mut Problem<P>,
mut state: FirstOrderState<V, F>,
) -> Result<FirstOrderState<V, F>, P::Error>
where
F: Scalar,
V: Clone + VectorLen,
P: CostFunction<Param = V, Output = F> + Gradient<Gradient = V>,
{
state.reset();
*radius = initial_radius;
let (cost, grad) = problem.cost_and_gradient(state.param())?;
state
.set_evaluation(cost, grad)
.expect("gradient dimension differs from parameter");
Ok(state)
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
fn tr_next_iter<P, V, F>(
radius: &mut F,
max_radius: F,
eta: F,
max_inner: u32,
trials: &mut Option<Vec<diagnostics::Trial<F>>>,
problem: &mut Problem<P>,
mut state: FirstOrderState<V, F>,
mut attempt: impl FnMut(
&mut Problem<P>,
&V,
&V,
F,
) -> Result<Step<V, F>, P::Error>,
) -> Result<(FirstOrderState<V, F>, Option<Termination<F>>, bool), P::Error>
where
F: Scalar,
P: CostFunction<Param = V, Output = F> + Gradient<Gradient = V>,
V: Clone + ScaledAdd<F> + NormSquared<F> + VectorLen,
{
let (cost_old, g) = state
.take_evaluation()
.expect("trust region requires initialized progress");
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 {
if let Some(records) = trials.as_mut() {
records.push(diagnostics::Trial {
sequence: records.len() as u64 + 1,
phase: diagnostics::Phase::Subproblem,
radius_before: *radius,
radius_after: None,
gradient_norm_squared: g.norm_squared(),
step_norm_squared: None,
predicted_reduction: None,
trial_cost: None,
ratio: None,
hit_boundary: None,
eta,
counts_before: *problem.counts(),
counts_after: *problem.counts(),
});
}
let step = attempt(problem, state.param(), &g, *radius)?;
if let Some(record) = trials.as_mut().and_then(|r| r.last_mut()) {
record.step_norm_squared = Some(step.d.norm_squared());
record.predicted_reduction = Some(step.predicted_reduction);
record.hit_boundary = Some(step.hit_boundary);
record.counts_after = *problem.counts();
}
if !step.predicted_reduction.is_finite() {
if let Some(record) = trials.as_mut().and_then(|r| r.last_mut()) {
record.phase = diagnostics::Phase::InvalidModel;
}
state
.set_evaluation(cost_old, g)
.expect("gradient dimension differs from parameter");
return Ok((
state,
Some(Termination::numerical_failure(
"The trust-region subproblem returned a non-finite predicted reduction.",
)),
false,
));
}
if step.predicted_reduction <= F::zero() {
if let Some(record) = trials.as_mut().and_then(|r| r.last_mut()) {
record.phase = diagnostics::Phase::NoProgress;
}
let gradient_norm = g.norm_l2();
let reason = if gradient_norm == F::zero() {
Termination::custom(
"trust_region.zero_gradient",
"The Euclidean gradient norm is exactly zero.",
vec![crate::Measurement {
name: "gradient_norm".into(),
value: gradient_norm,
}],
)
} else {
Termination::numerical_stall(
"trust_region: no numerical progress",
)
};
state
.set_evaluation(cost_old, g)
.expect("gradient dimension differs from parameter");
return Ok((state, Some(reason), false));
}
let mut trial = state.param().clone();
trial.scaled_add(F::one(), &step.d);
if let Some(record) = trials.as_mut().and_then(|r| r.last_mut()) {
record.phase = diagnostics::Phase::TrialEvaluation;
}
let cost_trial = problem.cost(&trial)?;
if let Some(record) = trials.as_mut().and_then(|r| r.last_mut()) {
record.trial_cost = Some(cost_trial);
record.counts_after = *problem.counts();
}
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 let Some(record) = trials.as_mut().and_then(|r| r.last_mut()) {
record.ratio = Some(rho);
record.radius_after = Some(*radius);
record.phase = if rho > eta {
diagnostics::Phase::GradientRefresh
} else {
diagnostics::Phase::Rejected
};
}
if rho > eta {
let g_new = problem.gradient(&trial)?;
if let Some(record) = trials.as_mut().and_then(|r| r.last_mut()) {
record.phase = diagnostics::Phase::Accepted;
record.counts_after = *problem.counts();
}
state
.replace(trial, cost_trial, g_new)
.expect("gradient dimension differs from parameter");
return Ok((state, None, true));
}
}
state
.set_evaluation(cost_old, g)
.expect("gradient dimension differs from parameter");
Ok((state, None, false))
}
impl<P, Sub, V, M, F> Solver<P, FirstOrderState<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> + VectorLen,
Sub: Subproblem<V, M, F>,
{
type Error = P::Error;
fn should_check_iterate_change(&self) -> bool {
!self.rejected_step
}
fn init(
&mut self,
problem: &mut Problem<P>,
state: FirstOrderState<V, F>,
) -> Result<FirstOrderState<V, F>, Self::Error> {
self.rejected_step = false;
if let Some(records) = self.trials.as_mut() {
records.clear();
}
tr_init(&mut self.radius, self.initial_radius, problem, state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
state: FirstOrderState<V, F>,
) -> Result<crate::SolverStep<FirstOrderState<V, F>>, Self::Error> {
let b = problem.hessian(state.param())?;
let subproblem = &self.subproblem;
let (state, reason, accepted) = tr_next_iter(
&mut self.radius,
self.max_radius,
self.eta,
self.max_inner,
&mut self.trials,
problem,
state,
|_, _, g, radius| Ok(subproblem.solve(g, &b, radius)),
)?;
self.rejected_step = !accepted;
Ok(crate::SolverStep::from((state, reason)))
}
}
impl<P, Sub, V, F> Solver<P, FirstOrderState<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> + VectorLen,
Sub: SubproblemHvp<V, F>,
{
type Error = P::Error;
fn should_check_iterate_change(&self) -> bool {
!self.rejected_step
}
fn init(
&mut self,
problem: &mut Problem<P>,
state: FirstOrderState<V, F>,
) -> Result<FirstOrderState<V, F>, Self::Error> {
self.rejected_step = false;
if let Some(records) = self.trials.as_mut() {
records.clear();
}
tr_init(&mut self.radius, self.initial_radius, problem, state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
state: FirstOrderState<V, F>,
) -> Result<crate::SolverStep<FirstOrderState<V, F>>, Self::Error> {
let subproblem = &self.subproblem;
let (state, reason, accepted) = tr_next_iter(
&mut self.radius,
self.max_radius,
self.eta,
self.max_inner,
&mut self.trials,
problem,
state,
|problem, x, g, radius| {
subproblem
.solve_hvp(g, radius, |v| problem.hessian_product(x, v))
},
)?;
self.rejected_step = !accepted;
Ok(crate::SolverStep::from((state, reason)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Executor, FirstOrderState};
struct Quadratic;
impl crate::Constraints for Quadratic {
type Set = crate::NoConstraints;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_> {
crate::ConstraintSet::new()
}
}
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 crate::Constraints for Rosenbrock {
type Set = crate::NoConstraints;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_> {
crate::ConstraintSet::new()
}
}
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))
.with_absolute_gradient_tolerance(1e-8),
FirstOrderState::new(vec![5.0, 1.0]),
)
.max_iter(500)
.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()))
.with_absolute_gradient_tolerance(1e-10),
FirstOrderState::new(vec![5.0, 1.0]),
)
.max_iter(100)
.run()
.unwrap();
assert!(result.cost() < 1e-16, "cost = {}", result.cost());
}
#[test]
fn dogleg_minimizes_quadratic() {
let result = Executor::new(
Quadratic,
(TrustRegion::with_subproblem(Dogleg))
.with_absolute_gradient_tolerance(1e-10),
FirstOrderState::new(vec![5.0, 1.0]),
)
.max_iter(100)
.run()
.unwrap();
assert!(result.cost() < 1e-16, "cost = {}", result.cost());
}
struct NonFiniteGradient;
impl crate::Constraints for NonFiniteGradient {
type Set = crate::NoConstraints;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_> {
crate::ConstraintSet::new()
}
}
impl CostFunction for NonFiniteGradient {
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] + x[1] * x[1]))
}
}
impl Gradient for NonFiniteGradient {
type Gradient = Vec<f64>;
fn gradient(&self, _x: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
Ok(vec![f64::NAN, 1.0])
}
}
impl Hessian for NonFiniteGradient {
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, 1.0],
))
}
}
#[test]
fn nonfinite_gradient_is_reported_as_a_failure_not_convergence() {
let result = Executor::new(
NonFiniteGradient,
TrustRegion::with_subproblem(MoreSorensen::new()),
FirstOrderState::new(vec![3.0, 3.0]),
)
.max_iter(10)
.run()
.unwrap();
assert_eq!(result.report.code(), crate::TerminationCode::SolverFailed);
assert!(result.report.termination.is_failure());
}
#[test]
fn invalid_model_reduction_preserves_the_nonstationary_state() {
for (predicted_reduction, expected_reason) in [
(0.0, crate::TerminationCode::NumericalNoProgress),
(-1.0, crate::TerminationCode::NumericalNoProgress),
(f64::NAN, crate::TerminationCode::SolverFailed),
(f64::INFINITY, crate::TerminationCode::SolverFailed),
(f64::NEG_INFINITY, crate::TerminationCode::SolverFailed),
] {
let mut problem = Problem::new(Quadratic);
let mut radius = 1.0;
let state = tr_init(
&mut radius,
1.0,
&mut problem,
FirstOrderState::new(vec![1.0, 0.0]),
)
.unwrap();
let counts = *problem.counts();
let (state, reason, _) = tr_next_iter(
&mut radius,
100.0,
0.125,
10,
&mut None,
&mut problem,
state,
|_, _, _, _| {
Ok(Step {
d: vec![0.0, 0.0],
predicted_reduction,
hit_boundary: false,
})
},
)
.unwrap();
assert_eq!(
reason.as_ref().map(|stop| stop.code()),
Some(expected_reason)
);
assert_eq!(state.param(), &vec![1.0, 0.0]);
assert_eq!(state.current().unwrap().2, &vec![1.0, 0.0]);
assert_eq!(state.cost(), 0.5);
assert_eq!(*problem.counts(), counts);
}
}
#[test]
fn more_sorensen_minimizes_quadratic() {
let result = Executor::new(
Quadratic,
(TrustRegion::with_subproblem(MoreSorensen::new()))
.with_absolute_gradient_tolerance(1e-10),
FirstOrderState::new(vec![5.0, 1.0]),
)
.max_iter(100)
.run()
.unwrap();
assert!(result.cost() < 1e-16, "cost = {}", result.cost());
}
#[test]
fn steihaug_minimizes_rosenbrock() {
let result = Executor::new(
Rosenbrock,
(TrustRegion::new()).with_absolute_gradient_tolerance(1e-8),
FirstOrderState::new(vec![-1.2, 1.0]),
)
.max_iter(200)
.run()
.unwrap();
assert!(result.cost() < 1e-10, "cost = {}", result.cost());
}
#[test]
fn dogleg_minimizes_rosenbrock() {
let result = Executor::new(
Rosenbrock,
(TrustRegion::with_subproblem(Dogleg))
.with_absolute_gradient_tolerance(1e-8),
FirstOrderState::new(vec![-1.2, 1.0]),
)
.max_iter(500)
.run()
.unwrap();
assert!(result.cost() < 1e-10, "cost = {}", result.cost());
}
#[test]
fn more_sorensen_minimizes_rosenbrock() {
let result = Executor::new(
Rosenbrock,
(TrustRegion::with_subproblem(MoreSorensen::new()))
.with_absolute_gradient_tolerance(1e-8),
FirstOrderState::new(vec![-1.2, 1.0]),
)
.max_iter(200)
.run()
.unwrap();
assert!(result.cost() < 1e-10, "cost = {}", result.cost());
}
struct QuadraticHvOnly;
impl crate::Constraints for QuadraticHvOnly {
type Set = crate::NoConstraints;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_> {
crate::ConstraintSet::new()
}
}
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 crate::Constraints for RosenbrockHvOnly {
type Set = crate::NoConstraints;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_> {
crate::ConstraintSet::new()
}
}
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())
.with_absolute_gradient_tolerance(1e-10),
FirstOrderState::new(vec![5.0, 1.0]),
)
.max_iter(100)
.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()).with_absolute_gradient_tolerance(1e-8),
FirstOrderState::new(vec![-1.2, 1.0]),
)
.max_iter(200)
.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))
.with_absolute_gradient_tolerance(1e-8),
FirstOrderState::new(vec![5.0, 1.0]),
)
.max_iter(500)
.run()
.unwrap();
assert!(result.cost() < 1e-8, "cost = {}", result.cost());
}
#[test]
fn matrix_free_matches_exact_steihaug() {
let exact = Executor::new(
Quadratic,
(TrustRegion::new()).with_absolute_gradient_tolerance(1e-10),
FirstOrderState::new(vec![5.0, 1.0]),
)
.max_iter(100)
.run()
.unwrap();
let free = Executor::new(
QuadraticHvOnly,
(TrustRegion::matrix_free())
.with_absolute_gradient_tolerance(1e-10),
FirstOrderState::new(vec![5.0, 1.0]),
)
.max_iter(100)
.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, FirstOrderState::new(vec![-1.2, 1.0]))
.unwrap();
for _ in 0..3 {
let (next, _, _) =
solver.next_iter(&mut problem, state).unwrap().into_parts();
state = next;
}
let counts = problem.counts();
assert!(counts.hessian_product_evals > 0);
assert_eq!(counts.hessian_evals, 0);
use crate::core::state::{CountsMirror as _, GradientState as _};
state.mirror(counts);
assert_eq!(state.counts(), counts);
assert_eq!(state.gradient_evals(), counts.gradient_evals);
}
}