pub type Bound = (f64, f64);
pub trait Problem {
fn dim(&self) -> usize;
fn bounds(&self) -> &[Bound];
fn objective(&self, x: &[f64]) -> f64;
}
pub trait MultiProblem {
fn dim(&self) -> usize;
fn bounds(&self) -> &[Bound];
fn n_objectives(&self) -> usize;
fn objectives(&self, x: &[f64]) -> Vec<f64>;
}
pub fn multi_func<F>(bounds: Vec<Bound>, n_objectives: usize, f: F) -> MultiFunc<F>
where
F: Fn(&[f64]) -> Vec<f64>,
{
MultiFunc {
bounds,
n_objectives,
f,
}
}
pub struct MultiFunc<F> {
bounds: Vec<Bound>,
n_objectives: usize,
f: F,
}
impl<F> MultiProblem for MultiFunc<F>
where
F: Fn(&[f64]) -> Vec<f64>,
{
fn dim(&self) -> usize {
self.bounds.len()
}
fn bounds(&self) -> &[Bound] {
&self.bounds
}
fn n_objectives(&self) -> usize {
self.n_objectives
}
fn objectives(&self, x: &[f64]) -> Vec<f64> {
(self.f)(x)
}
}
pub fn validate(problem: &dyn Problem) -> Result<(), BoundsError> {
let b = problem.bounds();
if b.is_empty() || problem.dim() == 0 {
return Err(BoundsError::Empty);
}
if b.len() != problem.dim() {
return Err(BoundsError::DimMismatch {
dim: problem.dim(),
bounds: b.len(),
});
}
for (i, &(lo, hi)) in b.iter().enumerate() {
if lo.partial_cmp(&hi) != Some(std::cmp::Ordering::Less) {
return Err(BoundsError::NotOrdered { dim: i, lo, hi });
}
}
Ok(())
}
#[derive(Debug, Clone, PartialEq)]
pub enum BoundsError {
Empty,
DimMismatch { dim: usize, bounds: usize },
NotOrdered { dim: usize, lo: f64, hi: f64 },
}
impl std::fmt::Display for BoundsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BoundsError::Empty => write!(f, "problem must have at least one variable"),
BoundsError::DimMismatch { dim, bounds } => {
write!(f, "dim() = {dim} but bounds() has {bounds} entries")
}
BoundsError::NotOrdered { dim, lo, hi } => {
write!(
f,
"dimension {dim}: lower bound ({lo}) must be < upper bound ({hi})"
)
}
}
}
}
impl std::error::Error for BoundsError {}
pub fn func<F>(bounds: Vec<Bound>, f: F) -> Func<F>
where
F: Fn(&[f64]) -> f64,
{
Func { bounds, f }
}
pub struct Func<F> {
bounds: Vec<Bound>,
f: F,
}
impl<F> Problem for Func<F>
where
F: Fn(&[f64]) -> f64,
{
fn dim(&self) -> usize {
self.bounds.len()
}
fn bounds(&self) -> &[Bound] {
&self.bounds
}
fn objective(&self, x: &[f64]) -> f64 {
(self.f)(x)
}
}
pub struct Maximize<P>(pub P);
impl<P: Problem> Problem for Maximize<P> {
fn dim(&self) -> usize {
self.0.dim()
}
fn bounds(&self) -> &[Bound] {
self.0.bounds()
}
fn objective(&self, x: &[f64]) -> f64 {
let v = self.0.objective(x);
if v.is_finite() {
-v
} else {
v
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn func_adapter_works() {
let p = func(vec![(-1.0, 1.0), (-1.0, 1.0)], |x| x[0] + x[1]);
assert_eq!(p.dim(), 2);
assert_eq!(p.objective(&[0.3, 0.4]), 0.7);
}
#[test]
fn maximize_negates_finite_only() {
let p = Maximize(func(vec![(0.0, 1.0)], |x| x[0]));
assert_eq!(p.objective(&[0.6]), -0.6);
let bad = Maximize(func(vec![(0.0, 1.0)], |_| f64::NAN));
assert!(bad.objective(&[0.5]).is_nan());
}
#[test]
fn validate_catches_bad_bounds() {
assert_eq!(
validate(&func(vec![], |_| 0.0)).unwrap_err(),
BoundsError::Empty
);
assert!(matches!(
validate(&func(vec![(1.0, 0.0)], |_| 0.0)).unwrap_err(),
BoundsError::NotOrdered { dim: 0, .. }
));
assert!(matches!(
validate(&func(vec![(0.0, f64::NAN)], |_| 0.0)).unwrap_err(),
BoundsError::NotOrdered { .. }
));
assert!(validate(&func(vec![(0.0, 1.0), (-2.0, 2.0)], |_| 0.0)).is_ok());
}
}