use core::marker::PhantomData;
use super::spec::{Dimensionality, HasSpec, ProblemSpec, Properties, Reference};
use crate::{BoxConstraints, CostFunction, Gradient};
pub const STANDARD_LOWER: f64 = -10.0;
pub const STANDARD_UPPER: f64 = 10.0;
pub fn levy(x: &[f64]) -> f64 {
debug_assert!(!x.is_empty());
let pi = core::f64::consts::PI;
let n = x.len();
let w = |i: usize| 1.0 + (x[i] - 1.0) / 4.0;
let mut s = (pi * w(0)).sin().powi(2);
for i in 0..n - 1 {
let wi = w(i);
let t = (pi * wi + 1.0).sin();
s += (wi - 1.0).powi(2) * (1.0 + 10.0 * t * t);
}
let wl = w(n - 1);
let tl = (2.0 * pi * wl).sin();
s += (wl - 1.0).powi(2) * (1.0 + tl * tl);
s
}
pub fn levy_gradient(x: &[f64], out: &mut [f64]) {
debug_assert_eq!(x.len(), out.len());
debug_assert!(!x.is_empty());
let pi = core::f64::consts::PI;
let n = x.len();
let w = |i: usize| 1.0 + (x[i] - 1.0) / 4.0;
for g in out.iter_mut() {
*g = 0.0;
}
out[0] += 0.25 * pi * (2.0 * pi * w(0)).sin();
for (g, &xi) in out.iter_mut().zip(x.iter()).take(n - 1) {
let wi = 1.0 + (xi - 1.0) / 4.0;
let theta = pi * wi + 1.0;
let s = theta.sin();
let dm = 2.0 * (wi - 1.0) * (1.0 + 10.0 * s * s)
+ (wi - 1.0).powi(2) * 10.0 * pi * (2.0 * theta).sin();
*g += 0.25 * dm;
}
let wl = w(n - 1);
let phi = 2.0 * pi * wl;
let sl = phi.sin();
let dl = 2.0 * (wl - 1.0) * (1.0 + sl * sl) + (wl - 1.0).powi(2) * 2.0 * pi * (2.0 * phi).sin();
out[n - 1] += 0.25 * dl;
}
pub struct Levy<P = Vec<f64>>(PhantomData<fn() -> P>);
impl<P> Levy<P> {
pub const fn new() -> Self {
Self(PhantomData)
}
}
impl<P> Default for Levy<P> {
fn default() -> Self {
Self::new()
}
}
pub static LEVY_SPEC: ProblemSpec = ProblemSpec {
name: "Levy",
dim: Dimensionality::NDimensional { min: 1 },
properties: Properties {
smooth: true,
differentiable: true,
convex: false,
unimodal: false,
separable: false,
scalable: true,
},
references: &[Reference {
citation: "Laguna & Martí (2005)",
title: "Experimental testing of advanced scatter search designs for global optimization of multimodal functions",
source: "Journal of Global Optimization, 33(2), 235–255",
doi: Some("10.1007/s10898-004-1936-z"),
url: None,
}],
description: "Smooth but multimodal: an affine reparameterization wᵢ = 1 + \
(xᵢ−1)/4 places the global minimum at x = (1, …, 1), value 0, \
surrounded by sin² ripple minima. Standard search domain is \
[−10, 10]ⁿ. Differentiable everywhere (analytic gradient).",
};
impl<P> HasSpec for Levy<P> {
const SPEC: &'static ProblemSpec = &LEVY_SPEC;
}
impl CostFunction for Levy<Vec<f64>> {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, std::convert::Infallible> {
Ok(levy(x))
}
}
impl Gradient for Levy<Vec<f64>> {
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Result<Vec<f64>, std::convert::Infallible> {
let mut out = vec![0.0; x.len()];
levy_gradient(x, &mut out);
Ok(out)
}
}
#[cfg(feature = "nalgebra")]
mod nalgebra_impl {
use super::{levy, levy_gradient, Levy};
use crate::{CostFunction, Gradient};
use nalgebra::DVector;
impl CostFunction for Levy<DVector<f64>> {
type Param = DVector<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &DVector<f64>) -> Result<f64, std::convert::Infallible> {
Ok(levy(x.as_slice()))
}
}
impl Gradient for Levy<DVector<f64>> {
type Gradient = DVector<f64>;
fn gradient(&self, x: &DVector<f64>) -> Result<DVector<f64>, std::convert::Infallible> {
let mut out = DVector::zeros(x.len());
levy_gradient(x.as_slice(), out.as_mut_slice());
Ok(out)
}
}
}
#[cfg(feature = "ndarray")]
mod ndarray_impl {
use super::{levy, levy_gradient, Levy};
use crate::{CostFunction, Gradient};
use ndarray::Array1;
impl CostFunction for Levy<Array1<f64>> {
type Param = Array1<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Array1<f64>) -> Result<f64, std::convert::Infallible> {
Ok(levy(x.as_slice().expect("Array1 is contiguous")))
}
}
impl Gradient for Levy<Array1<f64>> {
type Gradient = Array1<f64>;
fn gradient(&self, x: &Array1<f64>) -> Result<Array1<f64>, std::convert::Infallible> {
let mut out = Array1::zeros(x.len());
levy_gradient(
x.as_slice().expect("Array1 is contiguous"),
out.as_slice_mut().expect("Array1 is contiguous"),
);
Ok(out)
}
}
}
#[cfg(feature = "faer")]
mod faer_impl {
use super::{levy, levy_gradient, Levy};
use crate::{CostFunction, Gradient};
use faer::Col;
impl CostFunction for Levy<Col<f64>> {
type Param = Col<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Col<f64>) -> Result<f64, std::convert::Infallible> {
let v: Vec<f64> = (0..x.nrows()).map(|i| x[i]).collect();
Ok(levy(&v))
}
}
impl Gradient for Levy<Col<f64>> {
type Gradient = Col<f64>;
fn gradient(&self, x: &Col<f64>) -> Result<Col<f64>, std::convert::Infallible> {
let v: Vec<f64> = (0..x.nrows()).map(|i| x[i]).collect();
let mut g = vec![0.0; v.len()];
levy_gradient(&v, &mut g);
Ok(Col::<f64>::from_fn(g.len(), |i| g[i]))
}
}
}
pub struct LevyBoxed<P> {
lower: P,
upper: P,
}
impl<P> LevyBoxed<P> {
pub fn new(lower: P, upper: P) -> Self {
Self { lower, upper }
}
}
impl<P> HasSpec for LevyBoxed<P> {
const SPEC: &'static ProblemSpec = &LEVY_SPEC;
}
impl LevyBoxed<Vec<f64>> {
pub fn with_standard_bounds(n: usize) -> Self {
Self {
lower: vec![STANDARD_LOWER; n],
upper: vec![STANDARD_UPPER; n],
}
}
}
impl CostFunction for LevyBoxed<Vec<f64>> {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, std::convert::Infallible> {
Ok(levy(x))
}
}
impl Gradient for LevyBoxed<Vec<f64>> {
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Result<Vec<f64>, std::convert::Infallible> {
let mut out = vec![0.0; x.len()];
levy_gradient(x, &mut out);
Ok(out)
}
}
impl BoxConstraints for LevyBoxed<Vec<f64>> {
fn lower(&self) -> &Vec<f64> {
&self.lower
}
fn upper(&self) -> &Vec<f64> {
&self.upper
}
}
#[cfg(feature = "nalgebra")]
mod nalgebra_boxed_impl {
use super::{levy, levy_gradient, LevyBoxed, STANDARD_LOWER, STANDARD_UPPER};
use crate::{BoxConstraints, CostFunction, Gradient};
use nalgebra::DVector;
impl LevyBoxed<DVector<f64>> {
pub fn with_standard_bounds(n: usize) -> Self {
Self {
lower: DVector::from_element(n, STANDARD_LOWER),
upper: DVector::from_element(n, STANDARD_UPPER),
}
}
}
impl CostFunction for LevyBoxed<DVector<f64>> {
type Param = DVector<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &DVector<f64>) -> Result<f64, std::convert::Infallible> {
Ok(levy(x.as_slice()))
}
}
impl Gradient for LevyBoxed<DVector<f64>> {
type Gradient = DVector<f64>;
fn gradient(&self, x: &DVector<f64>) -> Result<DVector<f64>, std::convert::Infallible> {
let mut out = DVector::zeros(x.len());
levy_gradient(x.as_slice(), out.as_mut_slice());
Ok(out)
}
}
impl BoxConstraints for LevyBoxed<DVector<f64>> {
fn lower(&self) -> &DVector<f64> {
&self.lower
}
fn upper(&self) -> &DVector<f64> {
&self.upper
}
}
}
#[cfg(feature = "ndarray")]
mod ndarray_boxed_impl {
use super::{levy, levy_gradient, LevyBoxed, STANDARD_LOWER, STANDARD_UPPER};
use crate::{BoxConstraints, CostFunction, Gradient};
use ndarray::Array1;
impl LevyBoxed<Array1<f64>> {
pub fn with_standard_bounds(n: usize) -> Self {
Self {
lower: Array1::from_elem(n, STANDARD_LOWER),
upper: Array1::from_elem(n, STANDARD_UPPER),
}
}
}
impl CostFunction for LevyBoxed<Array1<f64>> {
type Param = Array1<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Array1<f64>) -> Result<f64, std::convert::Infallible> {
Ok(levy(x.as_slice().expect("Array1 is contiguous")))
}
}
impl Gradient for LevyBoxed<Array1<f64>> {
type Gradient = Array1<f64>;
fn gradient(&self, x: &Array1<f64>) -> Result<Array1<f64>, std::convert::Infallible> {
let mut out = Array1::zeros(x.len());
levy_gradient(
x.as_slice().expect("Array1 is contiguous"),
out.as_slice_mut().expect("Array1 is contiguous"),
);
Ok(out)
}
}
impl BoxConstraints for LevyBoxed<Array1<f64>> {
fn lower(&self) -> &Array1<f64> {
&self.lower
}
fn upper(&self) -> &Array1<f64> {
&self.upper
}
}
}
#[cfg(feature = "faer")]
mod faer_boxed_impl {
use super::{levy, levy_gradient, LevyBoxed, STANDARD_LOWER, STANDARD_UPPER};
use crate::{BoxConstraints, CostFunction, Gradient};
use faer::Col;
impl LevyBoxed<Col<f64>> {
pub fn with_standard_bounds(n: usize) -> Self {
Self {
lower: Col::<f64>::from_fn(n, |_| STANDARD_LOWER),
upper: Col::<f64>::from_fn(n, |_| STANDARD_UPPER),
}
}
}
impl CostFunction for LevyBoxed<Col<f64>> {
type Param = Col<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Col<f64>) -> Result<f64, std::convert::Infallible> {
let v: Vec<f64> = (0..x.nrows()).map(|i| x[i]).collect();
Ok(levy(&v))
}
}
impl Gradient for LevyBoxed<Col<f64>> {
type Gradient = Col<f64>;
fn gradient(&self, x: &Col<f64>) -> Result<Col<f64>, std::convert::Infallible> {
let v: Vec<f64> = (0..x.nrows()).map(|i| x[i]).collect();
let mut g = vec![0.0; v.len()];
levy_gradient(&v, &mut g);
Ok(Col::<f64>::from_fn(g.len(), |i| g[i]))
}
}
impl BoxConstraints for LevyBoxed<Col<f64>> {
fn lower(&self) -> &Col<f64> {
&self.lower
}
fn upper(&self) -> &Col<f64> {
&self.upper
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minimum_is_zero_at_ones() {
assert!(levy(&[1.0]).abs() < 1e-12);
assert!(levy(&[1.0, 1.0]).abs() < 1e-12);
assert!(levy(&[1.0; 7]).abs() < 1e-12);
}
#[test]
fn known_value_at_fives() {
let expected = 2.0 + 10.0 * 1.0_f64.sin().powi(2);
assert!((levy(&[5.0, 5.0]) - expected).abs() < 1e-9);
}
#[test]
fn gradient_zero_at_minimum() {
let mut g = vec![0.0; 4];
levy_gradient(&[1.0; 4], &mut g);
for v in g {
assert!(v.abs() < 1e-12);
}
}
#[test]
fn gradient_matches_finite_difference() {
let x = [-1.2, 0.7, 2.3];
let mut g = vec![0.0; x.len()];
levy_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 = (levy(&xp) - levy(&xm)) / (2.0 * h);
assert!((g[i] - fd).abs() < 1e-5, "i={i}, g={}, fd={fd}", g[i]);
}
}
#[test]
fn spec_is_wired_up_via_has_spec_trait() {
let spec = <Levy<Vec<f64>> as HasSpec>::SPEC;
assert_eq!(spec.name, "Levy");
assert!(spec.properties.smooth);
assert!(spec.properties.differentiable);
assert!(spec.properties.scalable);
assert!(matches!(spec.dim, Dimensionality::NDimensional { min: 1 }));
assert!(!spec.references.is_empty());
}
#[test]
fn boxed_form_exposes_standard_bounds_and_shares_math() {
let p = LevyBoxed::<Vec<f64>>::with_standard_bounds(4);
let lo = <LevyBoxed<Vec<f64>> as BoxConstraints>::lower(&p);
let hi = <LevyBoxed<Vec<f64>> as BoxConstraints>::upper(&p);
assert!(lo.iter().all(|&v| v == STANDARD_LOWER));
assert!(hi.iter().all(|&v| v == STANDARD_UPPER));
let x = vec![0.3, -0.7, 1.2, 2.1];
assert!((p.cost(&x).unwrap() - levy(&x)).abs() < 1e-12);
let mut g = vec![0.0; x.len()];
levy_gradient(&x, &mut g);
assert_eq!(p.gradient(&x).unwrap(), g);
}
#[test]
fn boxed_form_reuses_levy_spec() {
let spec = <LevyBoxed<Vec<f64>> as HasSpec>::SPEC;
assert!(core::ptr::eq(spec, &LEVY_SPEC));
}
}