Skip to content

Problem API

For certain optimization problems, it is useful to have a more structured way to define a problem. For instance, it may be useful to hold stateful information within a fitness function, store data in a more unified way, or evaluate a Genotype<C> directly without decoding. The problem interface provides a way to do just that. Under the hood of the GeneticEngine, the builder constructs a problem object that holds the codec and fitness function. Because of this, when using the problem API, we don't need a codec or a fitness function - the problem will take care of that for us.

See the image evolution example for a more detailed example of using the problem API.

The Problem interface is not available in python because it isn't needed.

// Define a problem struct that holds stateful information
struct MyFloatProblem {
    num_genes: usize,
    value_range: Range<f32>,
}

impl Problem<FloatChromosome<f32>, Vec<f32>> for MyFloatProblem {
    fn encode(&self) -> Genotype<FloatChromosome<f32>> {
        Genotype::from(FloatChromosome::from((
            self.num_genes,
            self.value_range.clone(),
        )))
    }

    fn decode(&self, genotype: &Genotype<FloatChromosome<f32>>) -> Vec<f32> {
        genotype
            .iter()
            .flat_map(|chromosome| chromosome.iter())
            .map(|gene| *gene.allele())
            .collect()
    }

    fn eval(&self, genotype: &Genotype<FloatChromosome<f32>>) -> Result<Score, RadiateError> {
        // Evaluate the genotype directly without decoding
        Ok(my_fitness_fn(genotype))
    }
}

// `Problem<C, T>` requires `Send + Sync`; this struct satisfies them automatically.
// You'd only write a manual `unsafe impl` if it held non-thread-safe state.

// Create an engine with the problem
let mut engine = GeneticEngine::builder()
    .problem(MyFloatProblem {
        num_genes: 10,
        value_range: 0.0..1.0,
    })
    .build();

// Run the engine
let result = engine.run(|epoch| epoch.index() >= 100);