use core::marker::PhantomData;
use super::spec::{Dimensionality, HasSpec, ProblemSpec, Properties, Reference};
use crate::{CostFunction, Gradient, Residual};
pub static ROSENBROCK_SPEC: ProblemSpec = ProblemSpec {
name: "Rosenbrock",
dim: Dimensionality::NDimensional { min: 2 },
properties: Properties {
smooth: true,
differentiable: true,
convex: false,
unimodal: false,
separable: false,
scalable: true,
},
references: &[Reference {
citation: "Rosenbrock (1960)",
title: "An automatic method for finding the greatest or least value of a function",
source: "The Computer Journal, 3(3), 175–184",
doi: Some("10.1093/comjnl/3.3.175"),
url: None,
}],
description: "Banana-shaped curved valley with global minimum at \
x = (1, …, 1), value 0. Standard hard test for first- and \
second-order methods due to the narrow, ill-conditioned valley.",
};
impl<P> HasSpec for Rosenbrock<P> {
const SPEC: &'static ProblemSpec = &ROSENBROCK_SPEC;
}
pub fn rosenbrock(x: &[f64]) -> f64 {
let mut s = 0.0;
for i in 0..x.len().saturating_sub(1) {
let a = x[i + 1] - x[i] * x[i];
let b = 1.0 - x[i];
s += 100.0 * a * a + b * b;
}
s
}
pub fn rosenbrock_gradient(x: &[f64], out: &mut [f64]) {
debug_assert_eq!(x.len(), out.len());
for v in out.iter_mut() {
*v = 0.0;
}
for i in 0..x.len().saturating_sub(1) {
let a = x[i + 1] - x[i] * x[i];
out[i] += -400.0 * x[i] * a - 2.0 * (1.0 - x[i]);
out[i + 1] += 200.0 * a;
}
}
pub struct Rosenbrock<P = Vec<f64>>(PhantomData<fn() -> P>);
impl<P> Rosenbrock<P> {
pub const fn new() -> Self {
Self(PhantomData)
}
}
impl<P> Default for Rosenbrock<P> {
fn default() -> Self {
Self::new()
}
}
impl CostFunction for Rosenbrock<Vec<f64>> {
type Param = Vec<f64>;
type Output = f64;
fn cost(&self, x: &Vec<f64>) -> f64 {
rosenbrock(x)
}
}
impl Gradient for Rosenbrock<Vec<f64>> {
type Param = Vec<f64>;
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Vec<f64> {
let mut out = vec![0.0; x.len()];
rosenbrock_gradient(x, &mut out);
out
}
}
#[cfg(feature = "nalgebra")]
mod nalgebra_impl {
use super::{rosenbrock, rosenbrock_gradient, Rosenbrock};
use crate::{CostFunction, Gradient};
use nalgebra::DVector;
impl CostFunction for Rosenbrock<DVector<f64>> {
type Param = DVector<f64>;
type Output = f64;
fn cost(&self, x: &DVector<f64>) -> f64 {
rosenbrock(x.as_slice())
}
}
impl Gradient for Rosenbrock<DVector<f64>> {
type Param = DVector<f64>;
type Gradient = DVector<f64>;
fn gradient(&self, x: &DVector<f64>) -> DVector<f64> {
let mut out = DVector::zeros(x.len());
rosenbrock_gradient(x.as_slice(), out.as_mut_slice());
out
}
}
}
#[cfg(feature = "ndarray")]
mod ndarray_impl {
use super::{rosenbrock, rosenbrock_gradient, Rosenbrock};
use crate::{CostFunction, Gradient};
use ndarray::Array1;
impl CostFunction for Rosenbrock<Array1<f64>> {
type Param = Array1<f64>;
type Output = f64;
fn cost(&self, x: &Array1<f64>) -> f64 {
rosenbrock(x.as_slice().expect("Array1 is contiguous"))
}
}
impl Gradient for Rosenbrock<Array1<f64>> {
type Param = Array1<f64>;
type Gradient = Array1<f64>;
fn gradient(&self, x: &Array1<f64>) -> Array1<f64> {
let mut out = Array1::zeros(x.len());
rosenbrock_gradient(
x.as_slice().expect("Array1 is contiguous"),
out.as_slice_mut().expect("Array1 is contiguous"),
);
out
}
}
}
#[cfg(feature = "faer")]
mod faer_impl {
use super::Rosenbrock;
use crate::{CostFunction, Gradient};
use faer::Col;
impl CostFunction for Rosenbrock<Col<f64>> {
type Param = Col<f64>;
type Output = f64;
fn cost(&self, x: &Col<f64>) -> f64 {
let n = x.nrows();
let mut s = 0.0;
for i in 0..n.saturating_sub(1) {
let a = x[i + 1] - x[i] * x[i];
let b = 1.0 - x[i];
s += 100.0 * a * a + b * b;
}
s
}
}
impl Gradient for Rosenbrock<Col<f64>> {
type Param = Col<f64>;
type Gradient = Col<f64>;
fn gradient(&self, x: &Col<f64>) -> Col<f64> {
let n = x.nrows();
let mut out = Col::<f64>::zeros(n);
for i in 0..n.saturating_sub(1) {
let a = x[i + 1] - x[i] * x[i];
out[i] += -400.0 * x[i] * a - 2.0 * (1.0 - x[i]);
out[i + 1] += 200.0 * a;
}
out
}
}
}
pub fn rosenbrock_residuals(x: &[f64], out: &mut [f64]) {
debug_assert_eq!(x.len(), 2);
debug_assert_eq!(out.len(), 2);
out[0] = 10.0 * (x[1] - x[0] * x[0]);
out[1] = 1.0 - x[0];
}
pub fn rosenbrock_residuals_jacobian(x: &[f64], out: &mut [f64]) {
debug_assert_eq!(x.len(), 2);
debug_assert_eq!(out.len(), 4);
out[0] = -20.0 * x[0];
out[1] = 10.0;
out[2] = -1.0;
out[3] = 0.0;
}
pub struct RosenbrockResiduals<P = Vec<f64>>(PhantomData<fn() -> P>);
impl<P> RosenbrockResiduals<P> {
pub const fn new() -> Self {
Self(PhantomData)
}
}
impl<P> Default for RosenbrockResiduals<P> {
fn default() -> Self {
Self::new()
}
}
impl<P> HasSpec for RosenbrockResiduals<P> {
const SPEC: &'static ProblemSpec = &ROSENBROCK_SPEC;
}
impl CostFunction for RosenbrockResiduals<Vec<f64>> {
type Param = Vec<f64>;
type Output = f64;
fn cost(&self, x: &Vec<f64>) -> f64 {
rosenbrock(x)
}
}
impl Residual for RosenbrockResiduals<Vec<f64>> {
type Param = Vec<f64>;
type Output = Vec<f64>;
fn residual(&self, x: &Vec<f64>) -> Vec<f64> {
let mut out = vec![0.0; 2];
rosenbrock_residuals(x, &mut out);
out
}
}
#[cfg(feature = "nalgebra")]
mod nalgebra_residuals_impl {
use super::{
rosenbrock, rosenbrock_residuals, rosenbrock_residuals_jacobian, RosenbrockResiduals,
};
use crate::{CostFunction, Jacobian, Residual};
use nalgebra::{DMatrix, DVector};
impl CostFunction for RosenbrockResiduals<DVector<f64>> {
type Param = DVector<f64>;
type Output = f64;
fn cost(&self, x: &DVector<f64>) -> f64 {
rosenbrock(x.as_slice())
}
}
impl Residual for RosenbrockResiduals<DVector<f64>> {
type Param = DVector<f64>;
type Output = DVector<f64>;
fn residual(&self, x: &DVector<f64>) -> DVector<f64> {
let mut out = DVector::zeros(2);
rosenbrock_residuals(x.as_slice(), out.as_mut_slice());
out
}
}
impl Jacobian for RosenbrockResiduals<DVector<f64>> {
type Param = DVector<f64>;
type Output = DMatrix<f64>;
fn jacobian(&self, x: &DVector<f64>) -> DMatrix<f64> {
let mut buf = [0.0_f64; 4];
rosenbrock_residuals_jacobian(x.as_slice(), &mut buf);
DMatrix::from_row_slice(2, 2, &buf)
}
}
}
#[cfg(feature = "ndarray")]
mod ndarray_residuals_impl {
use super::{rosenbrock, rosenbrock_residuals, RosenbrockResiduals};
use crate::{CostFunction, Residual};
use ndarray::Array1;
impl CostFunction for RosenbrockResiduals<Array1<f64>> {
type Param = Array1<f64>;
type Output = f64;
fn cost(&self, x: &Array1<f64>) -> f64 {
rosenbrock(x.as_slice().expect("Array1 is contiguous"))
}
}
impl Residual for RosenbrockResiduals<Array1<f64>> {
type Param = Array1<f64>;
type Output = Array1<f64>;
fn residual(&self, x: &Array1<f64>) -> Array1<f64> {
let mut out = Array1::zeros(2);
rosenbrock_residuals(
x.as_slice().expect("Array1 is contiguous"),
out.as_slice_mut().expect("Array1 is contiguous"),
);
out
}
}
}
#[cfg(feature = "faer")]
mod faer_residuals_impl {
use super::{rosenbrock_residuals_jacobian, RosenbrockResiduals};
use crate::{CostFunction, Jacobian, Residual};
use faer::{Col, Mat};
impl CostFunction for RosenbrockResiduals<Col<f64>> {
type Param = Col<f64>;
type Output = f64;
fn cost(&self, x: &Col<f64>) -> f64 {
let a = x[1] - x[0] * x[0];
let b = 1.0 - x[0];
100.0 * a * a + b * b
}
}
impl Residual for RosenbrockResiduals<Col<f64>> {
type Param = Col<f64>;
type Output = Col<f64>;
fn residual(&self, x: &Col<f64>) -> Col<f64> {
let mut out = Col::<f64>::zeros(2);
out[0] = 10.0 * (x[1] - x[0] * x[0]);
out[1] = 1.0 - x[0];
out
}
}
impl Jacobian for RosenbrockResiduals<Col<f64>> {
type Param = Col<f64>;
type Output = Mat<f64>;
fn jacobian(&self, x: &Col<f64>) -> Mat<f64> {
let xs = [x[0], x[1]];
let mut buf = [0.0_f64; 4];
rosenbrock_residuals_jacobian(&xs, &mut buf);
Mat::from_fn(2, 2, |i, j| buf[i * 2 + j])
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rosenbrock_minimum_is_zero_at_ones() {
assert_eq!(rosenbrock(&[1.0, 1.0]), 0.0);
assert_eq!(rosenbrock(&[1.0, 1.0, 1.0, 1.0]), 0.0);
}
#[test]
fn rosenbrock_known_value_at_classical_start() {
assert!((rosenbrock(&[-1.2, 1.0]) - 24.2).abs() < 1e-12);
}
#[test]
fn gradient_zero_at_minimum() {
let mut g = vec![0.0; 4];
rosenbrock_gradient(&[1.0, 1.0, 1.0, 1.0], &mut g);
for v in g {
assert!(v.abs() < 1e-12);
}
}
#[test]
fn spec_is_wired_up_via_has_spec_trait() {
let spec = <Rosenbrock<Vec<f64>> as HasSpec>::SPEC;
assert_eq!(spec.name, "Rosenbrock");
assert!(spec.properties.smooth);
assert!(spec.properties.scalable);
assert!(matches!(spec.dim, Dimensionality::NDimensional { min: 2 }));
assert!(!spec.references.is_empty());
}
#[test]
fn gradient_matches_finite_difference() {
let x = [-1.2, 1.0, 0.7, 0.4];
let mut g = vec![0.0; x.len()];
rosenbrock_gradient(&x, &mut g);
let h = 1e-6;
for i in 0..x.len() {
let mut xp = x;
let mut xm = x;
xp[i] += h;
xm[i] -= h;
let fd = (rosenbrock(&xp) - rosenbrock(&xm)) / (2.0 * h);
assert!((g[i] - fd).abs() < 1e-5, "i={i}, g={}, fd={fd}", g[i]);
}
}
#[test]
fn residuals_are_zero_at_optimum() {
let mut r = vec![0.0; 2];
rosenbrock_residuals(&[1.0, 1.0], &mut r);
assert!(r[0].abs() < 1e-12);
assert!(r[1].abs() < 1e-12);
}
#[test]
fn residuals_match_cost_at_n2() {
for x in [[-1.2, 1.0], [0.5, 0.25], [2.0, 4.0], [0.0, 0.0]] {
let mut r = vec![0.0; 2];
rosenbrock_residuals(&x, &mut r);
let sum_sq = r[0] * r[0] + r[1] * r[1];
let c = rosenbrock(&x);
assert!(
(c - sum_sq).abs() < 1e-12,
"x={x:?}, c={c}, sum_sq={sum_sq}"
);
}
}
#[test]
fn residual_jacobian_matches_finite_difference() {
let x = [-1.2, 1.0];
let mut j = vec![0.0; 4];
rosenbrock_residuals_jacobian(&x, &mut j);
let h = 1e-6;
for i in 0..2 {
for k in 0..2 {
let mut xp = x;
let mut xm = x;
xp[k] += h;
xm[k] -= h;
let mut rp = vec![0.0; 2];
let mut rm = vec![0.0; 2];
rosenbrock_residuals(&xp, &mut rp);
rosenbrock_residuals(&xm, &mut rm);
let fd = (rp[i] - rm[i]) / (2.0 * h);
assert!(
(j[i * 2 + k] - fd).abs() < 1e-5,
"i={i}, k={k}, j={}, fd={fd}",
j[i * 2 + k]
);
}
}
}
#[test]
fn residual_wrapper_reuses_rosenbrock_spec() {
let spec = <RosenbrockResiduals<Vec<f64>> as HasSpec>::SPEC;
assert!(core::ptr::eq(spec, &ROSENBROCK_SPEC));
}
#[test]
fn residual_trait_returns_expected_vector() {
let p: RosenbrockResiduals = RosenbrockResiduals::default();
let r = p.residual(&vec![1.0, 1.0]);
assert_eq!(r.len(), 2);
for v in r {
assert!(v.abs() < 1e-12);
}
}
#[cfg(feature = "nalgebra")]
mod nalgebra_jacobian_tests {
use super::super::RosenbrockResiduals;
use crate::{GramMatrix, Jacobian, LinearSolveSpd, MatTransposeVec, Residual};
use nalgebra::{DMatrix, DVector};
#[test]
fn jacobian_at_minimum_matches_documented_layout() {
let p: RosenbrockResiduals<DVector<f64>> = RosenbrockResiduals::new();
let x = DVector::from_vec(vec![1.0, 1.0]);
let j: DMatrix<f64> = p.jacobian(&x);
assert_eq!(j.shape(), (2, 2));
assert!((j[(0, 0)] + 20.0).abs() < 1e-12);
assert!((j[(0, 1)] - 10.0).abs() < 1e-12);
assert!((j[(1, 0)] + 1.0).abs() < 1e-12);
assert!(j[(1, 1)].abs() < 1e-12);
}
#[test]
fn gauss_newton_step_at_classical_start_is_well_defined() {
let p: RosenbrockResiduals<DVector<f64>> = RosenbrockResiduals::new();
let x = DVector::from_vec(vec![-1.2, 1.0]);
let j = p.jacobian(&x);
let r = p.residual(&x);
let g = j.gram();
let rhs = j.mat_transpose_vec(&r);
let delta = g.solve_spd(&rhs).expect("JᵀJ at (-1.2, 1.0) is SPD");
assert_eq!(delta.len(), 2);
assert!((delta[0] + 2.2).abs() < 1e-9, "delta[0] = {}", delta[0]);
assert!((delta[1] - 4.84).abs() < 1e-9, "delta[1] = {}", delta[1]);
}
}
#[cfg(feature = "faer")]
mod faer_jacobian_tests {
use super::super::RosenbrockResiduals;
use crate::{Jacobian, Residual};
use faer::{Col, Mat};
#[test]
fn jacobian_at_minimum_matches_documented_layout() {
let p: RosenbrockResiduals<Col<f64>> = RosenbrockResiduals::new();
let x = Col::<f64>::from_fn(2, |i| [1.0, 1.0][i]);
let j: Mat<f64> = p.jacobian(&x);
assert_eq!((j.nrows(), j.ncols()), (2, 2));
assert!((j[(0, 0)] + 20.0).abs() < 1e-12);
assert!((j[(0, 1)] - 10.0).abs() < 1e-12);
assert!((j[(1, 0)] + 1.0).abs() < 1e-12);
assert!(j[(1, 1)].abs() < 1e-12);
}
#[test]
fn jacobian_agrees_with_residual_via_finite_difference() {
let p: RosenbrockResiduals<Col<f64>> = RosenbrockResiduals::new();
let x = Col::<f64>::from_fn(2, |i| [-1.2, 1.0][i]);
let j = p.jacobian(&x);
let h = 1e-6;
for k in 0..2 {
let mut xp = x.clone();
let mut xm = x.clone();
xp[k] += h;
xm[k] -= h;
let rp = p.residual(&xp);
let rm = p.residual(&xm);
for i in 0..2 {
let fd = (rp[i] - rm[i]) / (2.0 * h);
assert!(
(j[(i, k)] - fd).abs() < 1e-5,
"i={i} k={k} j={} fd={fd}",
j[(i, k)]
);
}
}
}
}
}