use serde::{Deserialize, Serialize};
use tracing::instrument;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FitnessGoal {
Minimize { threshold: f64 },
Maximize { threshold: f64 },
}
#[derive(Debug, thiserror::Error)]
#[error("fitness goal threshold must be finite (not NaN or infinite), got {0}")]
pub struct ThresholdOutOfRange(f64);
impl FitnessGoal {
pub fn minimize(threshold: f64) -> Result<Self, ThresholdOutOfRange> {
let threshold = Self::validate(threshold)?;
Ok(Self::Minimize { threshold })
}
pub fn maximize(threshold: f64) -> Result<Self, ThresholdOutOfRange> {
let threshold = Self::validate(threshold)?;
Ok(Self::Maximize { threshold })
}
#[instrument(level = "debug", skip(self), fields(goal = ?self, fitness = fitness))]
pub(crate) fn is_reached(&self, fitness: f64) -> bool {
match self {
FitnessGoal::Minimize { threshold } => fitness <= *threshold,
FitnessGoal::Maximize { threshold } => fitness >= *threshold,
}
}
pub(crate) fn is_better(&self, candidate_fitness: f64, current_best_fitness: f64) -> bool {
match self {
FitnessGoal::Maximize { .. } => candidate_fitness > current_best_fitness,
FitnessGoal::Minimize { .. } => candidate_fitness < current_best_fitness,
}
}
pub(crate) fn best_fitness<'a>(
&'a self,
min_fitness: &'a Option<f64>,
max_fitness: &'a Option<f64>,
) -> &'a Option<f64> {
match self {
FitnessGoal::Maximize { .. } => max_fitness,
FitnessGoal::Minimize { .. } => min_fitness,
}
}
#[instrument(level = "debug", skip(self), fields(goal = ?self))]
pub(crate) fn calculate_progress(&self, _fitness: Option<f64>) -> f64 {
0.0
}
fn validate(threshold: f64) -> Result<f64, ThresholdOutOfRange> {
if !threshold.is_finite() {
return Err(ThresholdOutOfRange(threshold));
}
Ok(threshold)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_invalid_thresholds() {
assert!(FitnessGoal::minimize(f64::NAN).is_err());
assert!(FitnessGoal::minimize(f64::INFINITY).is_err());
assert!(FitnessGoal::minimize(f64::NEG_INFINITY).is_err());
assert!(FitnessGoal::maximize(f64::NAN).is_err());
assert!(FitnessGoal::maximize(f64::INFINITY).is_err());
assert!(FitnessGoal::maximize(f64::NEG_INFINITY).is_err());
assert!(FitnessGoal::minimize(-100.0).is_ok());
assert!(FitnessGoal::minimize(1000.0).is_ok());
assert!(FitnessGoal::maximize(-50.0).is_ok());
assert!(FitnessGoal::maximize(500.0).is_ok());
}
#[test]
fn test_is_reached_minimize() {
let goal = FitnessGoal::minimize(0.5).unwrap();
assert!(goal.is_reached(0.3)); assert!(goal.is_reached(0.5)); assert!(!goal.is_reached(0.7)); }
#[test]
fn test_is_reached_maximize() {
let goal = FitnessGoal::maximize(0.5).unwrap();
assert!(!goal.is_reached(0.3)); assert!(goal.is_reached(0.5)); assert!(goal.is_reached(0.7)); }
#[test]
fn test_boundary_values() {
let min_goal = FitnessGoal::minimize(0.0).unwrap();
let max_goal = FitnessGoal::maximize(100.0).unwrap();
assert!(min_goal.is_reached(0.0));
assert!(min_goal.is_reached(-0.1)); assert!(!min_goal.is_reached(0.1));
assert!(max_goal.is_reached(100.0));
assert!(max_goal.is_reached(150.0)); assert!(!max_goal.is_reached(99.9)); }
#[test]
fn test_progress_calculation_maximize() {
let goal = FitnessGoal::maximize(0.8).unwrap();
assert_eq!(goal.calculate_progress(None), 0.0);
assert_eq!(goal.calculate_progress(Some(0.0)), 0.0);
assert_eq!(goal.calculate_progress(Some(0.4)), 0.0);
assert_eq!(goal.calculate_progress(Some(0.8)), 0.0);
assert_eq!(goal.calculate_progress(Some(1.0)), 0.0);
assert_eq!(goal.calculate_progress(Some(-0.1)), 0.0);
}
#[test]
fn test_progress_calculation_minimize() {
let goal = FitnessGoal::minimize(0.2).unwrap();
assert_eq!(goal.calculate_progress(None), 0.0);
assert_eq!(goal.calculate_progress(Some(1.0)), 0.0);
assert_eq!(goal.calculate_progress(Some(0.6)), 0.0);
assert_eq!(goal.calculate_progress(Some(0.2)), 0.0);
assert_eq!(goal.calculate_progress(Some(0.1)), 0.0);
assert_eq!(goal.calculate_progress(Some(1.1)), 0.0);
}
#[test]
fn test_progress_calculation_edge_cases() {
let min_goal_edge = FitnessGoal::minimize(1.0).unwrap();
assert_eq!(min_goal_edge.calculate_progress(Some(0.5)), 0.0);
assert_eq!(min_goal_edge.calculate_progress(Some(1.0)), 0.0);
let max_goal_edge = FitnessGoal::maximize(0.001).unwrap();
assert_eq!(max_goal_edge.calculate_progress(Some(0.0005)), 0.0);
}
}