Custom Fitness Functions
This guide shows how to implement fitness functions for your optimization problems.
Basic Pattern
Implement the Fitness trait for your fitness function:
use fugue_evo::prelude::*;
struct MyFitness {
// Your fitness function parameters
}
impl Fitness<RealVector> for MyFitness {
type Value = f64;
fn evaluate(&self, genome: &RealVector) -> f64 {
// Your evaluation logic
// Return higher values for better solutions
let genes = genome.genes();
// ...compute fitness...
fitness_value
}
}
Minimization vs Maximization
Fugue-evo maximizes by default. For minimization problems, negate the objective:
impl Fitness<RealVector> for MinimizeProblem {
type Value = f64;
fn evaluate(&self, genome: &RealVector) -> f64 {
let objective = compute_objective(genome); // Value to minimize
-objective // Negate for maximization
}
}
Constrained Optimization
Penalty Method
Add penalties for constraint violations:
struct ConstrainedProblem {
penalty_weight: f64,
}
impl Fitness<RealVector> for ConstrainedProblem {
type Value = f64;
fn evaluate(&self, genome: &RealVector) -> f64 {
let genes = genome.genes();
// Objective to maximize
let objective = compute_objective(genes);
// Constraints (should be >= 0 for feasible solutions)
let g1 = genes[0] + genes[1] - 10.0; // x0 + x1 <= 10
let g2 = genes[0] * genes[1] - 5.0; // x0 * x1 >= 5
// Penalty for violations
let violation1 = g1.max(0.0); // Penalize if > 0
let violation2 = (-g2).max(0.0); // Penalize if < 0
let total_penalty = violation1.powi(2) + violation2.powi(2);
objective - self.penalty_weight * total_penalty
}
}
Choosing penalty weight:
- Too small: Constraints ignored
- Too large: Search biased toward feasibility over quality
- Typical: 100-10000 depending on objective scale
Death Penalty
Reject infeasible solutions entirely:
impl Fitness<RealVector> for StrictConstraints {
type Value = f64;
fn evaluate(&self, genome: &RealVector) -> f64 {
let genes = genome.genes();
// Check constraints
if !self.is_feasible(genes) {
return f64::NEG_INFINITY; // Or a very low value
}
compute_objective(genes)
}
fn is_feasible(&self, genes: &[f64]) -> bool {
genes[0] + genes[1] <= 10.0 && genes[0] * genes[1] >= 5.0
}
}
External Simulations
When fitness requires running external code:
struct SimulationFitness {
simulator_path: PathBuf,
}
impl Fitness<RealVector> for SimulationFitness {
type Value = f64;
fn evaluate(&self, genome: &RealVector) -> f64 {
// Write parameters to file
let params_file = write_params(genome.genes());
// Run external simulation
let output = std::process::Command::new(&self.simulator_path)
.arg(¶ms_file)
.output()
.expect("Failed to run simulator");
// Parse result
let result: f64 = parse_output(&output.stdout);
// Clean up
std::fs::remove_file(params_file).ok();
result
}
}
Data-Driven Fitness
When fitness is computed from data:
struct RegressionFitness {
x_data: Vec<f64>,
y_data: Vec<f64>,
}
impl RegressionFitness {
fn new(x: Vec<f64>, y: Vec<f64>) -> Self {
assert_eq!(x.len(), y.len());
Self { x_data: x, y_data: y }
}
}
impl Fitness<RealVector> for RegressionFitness {
type Value = f64;
fn evaluate(&self, genome: &RealVector) -> f64 {
let genes = genome.genes();
let a = genes[0];
let b = genes[1];
// Model: y = a*x + b
let mse: f64 = self.x_data.iter()
.zip(self.y_data.iter())
.map(|(x, y)| {
let predicted = a * x + b;
(y - predicted).powi(2)
})
.sum::<f64>() / self.x_data.len() as f64;
-mse // Minimize MSE
}
}
Multi-Objective Fitness
For NSGA-II, return multiple objectives:
struct MultiObjective;
impl Fitness<RealVector> for MultiObjective {
type Value = ParetoFitness;
fn evaluate(&self, genome: &RealVector) -> ParetoFitness {
let genes = genome.genes();
// Two conflicting objectives
let obj1 = genes.iter().sum::<f64>(); // Maximize sum
let obj2 = -genes.iter().product::<f64>(); // Maximize product
ParetoFitness::new(vec![obj1, obj2])
}
}
Caching Fitness
For expensive evaluations, cache results:
use std::collections::HashMap;
use std::sync::RwLock;
struct CachedFitness<F> {
inner: F,
cache: RwLock<HashMap<Vec<u64>, f64>>,
}
impl<F: Fitness<RealVector, Value = f64>> CachedFitness<F> {
fn cache_key(genome: &RealVector) -> Vec<u64> {
genome.genes().iter()
.map(|f| f.to_bits())
.collect()
}
}
impl<F: Fitness<RealVector, Value = f64>> Fitness<RealVector> for CachedFitness<F> {
type Value = f64;
fn evaluate(&self, genome: &RealVector) -> f64 {
let key = Self::cache_key(genome);
// Check cache
if let Ok(cache) = self.cache.read() {
if let Some(&cached) = cache.get(&key) {
return cached;
}
}
// Compute and cache
let result = self.inner.evaluate(genome);
if let Ok(mut cache) = self.cache.write() {
cache.insert(key, result);
}
result
}
}
Fitness for Different Genome Types
BitString
impl Fitness<BitString> for KnapsackFitness {
type Value = f64;
fn evaluate(&self, genome: &BitString) -> f64 {
let mut total_value = 0.0;
let mut total_weight = 0.0;
for (i, bit) in genome.bits().iter().enumerate() {
if *bit {
total_value += self.values[i];
total_weight += self.weights[i];
}
}
if total_weight > self.capacity {
0.0 // Infeasible
} else {
total_value
}
}
}
Permutation
impl Fitness<Permutation> for TSPFitness {
type Value = f64;
fn evaluate(&self, genome: &Permutation) -> f64 {
let order = genome.as_slice();
let mut total_distance = 0.0;
for i in 0..order.len() {
let from = order[i];
let to = order[(i + 1) % order.len()];
total_distance += self.distance_matrix[from][to];
}
-total_distance // Minimize distance
}
}
Testing Fitness Functions
Always test your fitness function:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_known_optimum() {
let fitness = MyFitness::new();
let optimum = RealVector::new(vec![0.0, 0.0, 0.0]);
assert!((fitness.evaluate(&optimum) - expected_value).abs() < 1e-6);
}
#[test]
fn test_fitness_ordering() {
let fitness = MyFitness::new();
let good = RealVector::new(vec![0.1, 0.1, 0.1]);
let bad = RealVector::new(vec![5.0, 5.0, 5.0]);
assert!(fitness.evaluate(&good) > fitness.evaluate(&bad));
}
#[test]
fn test_constraints() {
let fitness = ConstrainedProblem::new();
let feasible = RealVector::new(vec![3.0, 2.0]);
let infeasible = RealVector::new(vec![10.0, 10.0]);
assert!(fitness.evaluate(&feasible) > fitness.evaluate(&infeasible));
}
}
Next Steps
- Custom Genome Types - Create problem-specific representations
- Parallel Evolution - Speed up expensive fitness evaluations