use ndarray::Array1;
use std::sync::Arc;
use crate::error::{DEError, Result};
pub type CobylaConstraintFn = Arc<dyn Fn(&Array1<f64>) -> f64 + Send + Sync>;
#[derive(Clone)]
pub struct CobylaConstraint {
pub fun: CobylaConstraintFn,
}
#[derive(Debug, Clone, Copy)]
pub struct CobylaStopTols {
pub stopval: f64,
pub ftol_abs: f64,
pub ftol_rel: f64,
pub xtol_abs: f64,
pub xtol_rel: f64,
}
impl Default for CobylaStopTols {
fn default() -> Self {
Self {
stopval: 1e-4,
ftol_abs: 0.0,
ftol_rel: 1e-6,
xtol_abs: 0.0,
xtol_rel: 1e-4,
}
}
}
#[derive(Debug, Clone)]
pub enum CobylaRhoBegin {
All(f64),
PerDim(Vec<f64>),
}
impl Default for CobylaRhoBegin {
fn default() -> Self {
Self::All(0.5)
}
}
#[derive(Clone)]
pub struct CobylaConfig {
pub x0: Array1<f64>,
pub bounds: Vec<(f64, f64)>,
pub rho_begin: CobylaRhoBegin,
pub maxeval: usize,
pub stop_tol: CobylaStopTols,
}
impl Default for CobylaConfig {
fn default() -> Self {
Self {
x0: Array1::zeros(0),
bounds: Vec::new(),
rho_begin: CobylaRhoBegin::default(),
maxeval: 1000,
stop_tol: CobylaStopTols::default(),
}
}
}
#[derive(Clone)]
pub struct CobylaReport {
pub x: Array1<f64>,
pub fun: f64,
pub success: bool,
pub message: String,
pub nfev: usize,
}
impl std::fmt::Debug for CobylaReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CobylaReport")
.field("x_len", &self.x.len())
.field("fun", &self.fun)
.field("success", &self.success)
.field("message", &self.message)
.field("nfev", &self.nfev)
.finish()
}
}
pub fn cobyla<F>(
f: &F,
constraints: &[CobylaConstraint],
config: CobylaConfig,
) -> Result<CobylaReport>
where
F: Fn(&Array1<f64>) -> f64 + Sync,
{
let n = config.x0.len();
if n == 0 {
return Err(DEError::BoundsMismatch {
lower_len: 0,
upper_len: 0,
});
}
if config.bounds.len() != n {
return Err(DEError::BoundsMismatch {
lower_len: config.bounds.len(),
upper_len: n,
});
}
for (i, (lo, hi)) in config.bounds.iter().enumerate() {
if lo > hi {
return Err(DEError::InvalidBounds {
index: i,
lower: *lo,
upper: *hi,
});
}
}
use crate::cobyla_native as native;
let dx: Vec<f64> = match &config.rho_begin {
CobylaRhoBegin::All(r) => vec![*r; n],
CobylaRhoBegin::PerDim(v) => {
if v.len() != n {
return Err(DEError::BoundsMismatch {
lower_len: v.len(),
upper_len: n,
});
}
v.clone()
}
};
let stopval = if config.stop_tol.stopval > 0.0 {
config.stop_tol.stopval
} else {
f64::NEG_INFINITY
};
let stop = native::StopCriteria {
stopval,
ftol_abs: config.stop_tol.ftol_abs,
ftol_rel: config.stop_tol.ftol_rel,
xtol_abs: config.stop_tol.xtol_abs,
xtol_rel: config.stop_tol.xtol_rel,
maxeval: config.maxeval,
};
let f_native = |x: &[f64]| -> f64 {
let arr = Array1::from(x.to_vec());
f(&arr)
};
type SliceObj = Box<dyn Fn(&[f64]) -> f64>;
let cons_native: Vec<SliceObj> = constraints
.iter()
.map(|c| {
let g = c.fun.clone();
let b: SliceObj = Box::new(move |x: &[f64]| {
let arr = Array1::from(x.to_vec());
g(&arr)
});
b
})
.collect();
let mut x_buf: Vec<f64> = config.x0.to_vec();
let report = native::cobyla_native(
n,
f_native,
&cons_native,
&config.bounds,
&mut x_buf,
&dx,
&stop,
)?;
Ok(CobylaReport {
x: Array1::from(report.x),
fun: report.fun,
success: report.success,
message: report.message.to_string(),
nfev: report.nfev,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paraboloid_unconstrained() {
let f = |x: &Array1<f64>| (x[0] + 1.0).powi(2) + x[1].powi(2);
let cfg = CobylaConfig {
x0: Array1::from(vec![1.0, 1.0]),
bounds: vec![(-10.0, 10.0), (-10.0, 10.0)],
rho_begin: CobylaRhoBegin::All(0.5),
maxeval: 500,
stop_tol: CobylaStopTols {
stopval: 0.0,
..CobylaStopTols::default()
},
};
let report = cobyla(&f, &[], cfg).expect("cobyla failed");
assert!(report.fun < 1e-6, "fun = {} should be ~0", report.fun);
assert!((report.x[0] - (-1.0)).abs() < 1e-3);
assert!(report.x[1].abs() < 1e-3);
}
#[test]
fn paraboloid_with_inequality() {
let f = |x: &Array1<f64>| (x[0] + 1.0).powi(2) + x[1].powi(2);
let constraints = vec![CobylaConstraint {
fun: Arc::new(|x: &Array1<f64>| -x[0]),
}];
let cfg = CobylaConfig {
x0: Array1::from(vec![1.0, 1.0]),
bounds: vec![(-10.0, 10.0), (-10.0, 10.0)],
rho_begin: CobylaRhoBegin::All(0.5),
maxeval: 500,
..Default::default()
};
let report = cobyla(&f, &constraints, cfg).expect("cobyla failed");
assert!(
(report.fun - 1.0).abs() < 1e-3,
"fun = {} should be ~1",
report.fun
);
assert!(
report.x[0] >= -1e-3,
"x0 = {} should respect x0 >= 0",
report.x[0]
);
}
#[test]
fn rejects_bad_bounds() {
let f = |x: &Array1<f64>| x[0] * x[0];
let cfg = CobylaConfig {
x0: Array1::from(vec![0.0]),
bounds: vec![(1.0, -1.0)], rho_begin: CobylaRhoBegin::All(0.1),
maxeval: 100,
..Default::default()
};
let err = cobyla(&f, &[], cfg).unwrap_err();
matches!(err, DEError::InvalidBounds { .. });
}
}