use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::Float;
use std::fmt::Debug;
use crate::error::{OptimError, Result};
pub trait MetaObjective<T: Float + Debug + Send + Sync + 'static> {
fn dimension(&self) -> usize;
fn initial_parameters(&self) -> Array1<T>;
fn loss_and_gradient(&self, params: &Array1<T>) -> Result<(T, Array1<T>)>;
fn name(&self) -> &str {
"meta-objective"
}
}
#[derive(Debug, Clone)]
pub struct QuadraticObjective<T: Float + Debug + Send + Sync + 'static> {
curvature: Array1<T>,
optimum: Array1<T>,
start: Array1<T>,
label: String,
}
impl<T: Float + Debug + Send + Sync + 'static> QuadraticObjective<T> {
pub fn from_curvature(curvature: Vec<f64>, start_value: f64) -> Result<Self> {
if curvature.is_empty() {
return Err(OptimError::InvalidConfig(
"a quadratic objective needs at least one coordinate".to_string(),
));
}
if !start_value.is_finite() {
return Err(OptimError::InvalidConfig(
"start_value must be finite".to_string(),
));
}
for value in &curvature {
if !(value.is_finite() && *value > 0.0) {
return Err(OptimError::InvalidConfig(format!(
"curvature entries must be finite and positive, got {value}"
)));
}
}
let dimension = curvature.len();
let cast =
|v: f64| -> T { scirs2_core::numeric::NumCast::from(v).unwrap_or_else(|| T::zero()) };
Ok(Self {
curvature: Array1::from_iter(curvature.into_iter().map(cast)),
optimum: Array1::zeros(dimension),
start: Array1::from_elem(dimension, cast(start_value)),
label: "quadratic".to_string(),
})
}
pub fn isotropic(dimension: usize, curvature: f64, start_value: f64) -> Result<Self> {
Self::from_curvature(vec![curvature; dimension], start_value)
}
pub fn with_optimum(mut self, optimum: Vec<f64>) -> Result<Self> {
if optimum.len() != self.curvature.len() {
return Err(OptimError::InvalidConfig(format!(
"optimum has {} coordinates but the objective has {}",
optimum.len(),
self.curvature.len()
)));
}
if optimum.iter().any(|v| !v.is_finite()) {
return Err(OptimError::InvalidConfig(
"optimum entries must be finite".to_string(),
));
}
self.optimum = Array1::from_iter(
optimum
.into_iter()
.map(|v| scirs2_core::numeric::NumCast::from(v).unwrap_or_else(|| T::zero())),
);
Ok(self)
}
pub fn labelled(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
pub fn condition_number(&self) -> f64 {
let mut low = f64::INFINITY;
let mut high = 0.0_f64;
for value in self.curvature.iter() {
let v = value.to_f64().unwrap_or(0.0);
low = low.min(v);
high = high.max(v);
}
if low > 0.0 && low.is_finite() {
high / low
} else {
f64::INFINITY
}
}
}
impl<T: Float + Debug + Send + Sync + 'static> MetaObjective<T> for QuadraticObjective<T> {
fn dimension(&self) -> usize {
self.curvature.len()
}
fn initial_parameters(&self) -> Array1<T> {
self.start.clone()
}
fn loss_and_gradient(&self, params: &Array1<T>) -> Result<(T, Array1<T>)> {
if params.len() != self.curvature.len() {
return Err(OptimError::InvalidConfig(format!(
"quadratic objective has {} coordinates but received {}",
self.curvature.len(),
params.len()
)));
}
let half = scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::one());
let mut loss = T::zero();
let mut gradient = Array1::zeros(params.len());
for i in 0..params.len() {
let offset = params[i] - self.optimum[i];
let scaled = self.curvature[i] * offset;
loss = loss + half * scaled * offset;
gradient[i] = scaled;
}
Ok((loss, gradient))
}
fn name(&self) -> &str {
&self.label
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quadratic_loss_and_gradient_match_the_closed_form() {
let task = QuadraticObjective::<f64>::from_curvature(vec![2.0, 8.0], 0.0)
.expect("task")
.with_optimum(vec![1.0, -0.5])
.expect("optimum")
.labelled("test-bowl");
assert_eq!(task.dimension(), 2);
assert_eq!(task.name(), "test-bowl");
assert!((task.condition_number() - 4.0).abs() < 1e-12);
let point = Array1::from_vec(vec![3.0, 0.5]);
let (loss, gradient) = task.loss_and_gradient(&point).expect("evaluate");
assert!((loss - 8.0).abs() < 1e-12, "loss {loss}");
assert!((gradient[0] - 4.0).abs() < 1e-12);
assert!((gradient[1] - 8.0).abs() < 1e-12);
let (loss, gradient) = task
.loss_and_gradient(&Array1::from_vec(vec![1.0, -0.5]))
.expect("evaluate");
assert_eq!(loss, 0.0);
assert!(gradient.iter().all(|g| *g == 0.0));
}
#[test]
fn quadratic_gradient_matches_finite_differences() {
let task =
QuadraticObjective::<f64>::from_curvature(vec![0.7, 3.1, 11.0], 0.0).expect("task");
let point = Array1::from_vec(vec![0.4, -1.2, 2.5]);
let (_, analytic) = task.loss_and_gradient(&point).expect("analytic");
let h = 1e-6;
for i in 0..point.len() {
let mut plus = point.clone();
let mut minus = point.clone();
plus[i] += h;
minus[i] -= h;
let (loss_plus, _) = task.loss_and_gradient(&plus).expect("plus");
let (loss_minus, _) = task.loss_and_gradient(&minus).expect("minus");
let numeric = (loss_plus - loss_minus) / (2.0 * h);
assert!(
(analytic[i] - numeric).abs() < 1e-6,
"coordinate {i}: analytic {} vs numeric {numeric}",
analytic[i]
);
}
}
#[test]
fn invalid_objectives_are_rejected() {
assert!(QuadraticObjective::<f64>::from_curvature(vec![], 0.0).is_err());
assert!(QuadraticObjective::<f64>::from_curvature(vec![0.0], 0.0).is_err());
assert!(QuadraticObjective::<f64>::from_curvature(vec![-1.0], 0.0).is_err());
assert!(QuadraticObjective::<f64>::from_curvature(vec![f64::NAN], 0.0).is_err());
assert!(QuadraticObjective::<f64>::from_curvature(vec![1.0], f64::INFINITY).is_err());
assert!(QuadraticObjective::<f64>::isotropic(3, 1.0, 0.5)
.expect("task")
.with_optimum(vec![0.0, 0.0])
.is_err());
let task = QuadraticObjective::<f64>::isotropic(2, 1.0, 0.5).expect("task");
assert!(task
.loss_and_gradient(&Array1::from_vec(vec![1.0]))
.is_err());
}
}