#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Minimize,
Maximize,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Objective {
pub name: String,
pub direction: Direction,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub label: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
pub unit: Option<String>,
}
impl Objective {
pub fn minimize(name: impl Into<String>) -> Self {
Self {
name: name.into(),
direction: Direction::Minimize,
label: None,
unit: None,
}
}
pub fn maximize(name: impl Into<String>) -> Self {
Self {
name: name.into(),
direction: Direction::Maximize,
label: None,
unit: None,
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
self.unit = Some(unit.into());
self
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjectiveSpace {
pub objectives: Vec<Objective>,
}
impl ObjectiveSpace {
pub fn new(objectives: Vec<Objective>) -> Self {
Self { objectives }
}
pub fn len(&self) -> usize {
self.objectives.len()
}
pub fn is_empty(&self) -> bool {
self.objectives.is_empty()
}
pub fn is_single_objective(&self) -> bool {
self.objectives.len() == 1
}
pub fn is_multi_objective(&self) -> bool {
self.objectives.len() >= 2
}
pub fn as_minimization(&self, values: &[f64]) -> Vec<f64> {
debug_assert_eq!(
values.len(),
self.objectives.len(),
"objective value count must match ObjectiveSpace length",
);
self.objectives
.iter()
.zip(values.iter())
.map(|(obj, &v)| match obj.direction {
Direction::Minimize => v,
Direction::Maximize => -v,
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minimize_constructor_sets_direction() {
let o = Objective::minimize("cost");
assert_eq!(o.name, "cost");
assert_eq!(o.direction, Direction::Minimize);
}
#[test]
fn maximize_constructor_sets_direction() {
let o = Objective::maximize("accuracy");
assert_eq!(o.name, "accuracy");
assert_eq!(o.direction, Direction::Maximize);
}
#[test]
fn label_and_unit_default_to_none_and_round_trip_through_builders() {
let o = Objective::minimize("price");
assert!(o.label.is_none());
assert!(o.unit.is_none());
let o = Objective::minimize("price")
.with_label("Price")
.with_unit("$k");
assert_eq!(o.label.as_deref(), Some("Price"));
assert_eq!(o.unit.as_deref(), Some("$k"));
assert_eq!(o.direction, Direction::Minimize);
assert_eq!(o.name, "price");
}
#[test]
fn as_minimization_negates_maximize_only() {
let space = ObjectiveSpace::new(vec![
Objective::minimize("cost"),
Objective::maximize("accuracy"),
]);
assert_eq!(space.as_minimization(&[10.0, 0.8]), vec![10.0, -0.8]);
}
#[test]
fn lengths_and_predicates() {
let single = ObjectiveSpace::new(vec![Objective::minimize("f")]);
assert!(single.is_single_objective());
assert!(!single.is_multi_objective());
assert!(!single.is_empty());
assert_eq!(single.len(), 1);
let multi = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
assert!(multi.is_multi_objective());
assert!(!multi.is_single_objective());
let empty = ObjectiveSpace::new(Vec::new());
assert!(empty.is_empty());
}
}