Generations
Each time the engine advances, it produces a result describing that point in the evolutionary process — its generation number, the population, the best solution found, metrics, and more. Radiate has two types for this, and the difference between them is entirely about cost:
Generationis an owned snapshot — safe to hold onto, pass around, or send across threads, at the cost of cloning the ecosystem to build it.GenerationViewis a borrowed view over the exact same information — free to construct, but tied to the lifetime of the engine call that produced it.
Terminology
Epoch is the associated type name on the Engine trait — it's generic, so any type implementing Engine could in principle use a different type for it. Generation is the concrete type GeneticEngine uses. GenerationView is the borrowed alternative described below. The docs use "epoch" informally to mean "whatever came back from advancing the engine," and Generation when the concrete type matters.
Generation
This is the primary, owned type — the default epoch for the engine, and the only one exposed to Python. It contains:
- The generation number
Ecosysteminformation (population, species, etc.)- Score, which is the fitness of the best individual in the generation
- Value, which is the decoded value of the best individual
- Performance metrics (e.g., time taken)
- The Objective (max or min). The fitness objective being optimized, used for comparison and decision making during the evolutionary process.
Single-Objective
import radiate as rd
# Create an engine. Float Scalar engine (one chromosome, with one gene)
single_obj_engine: rd.Engine[float, float] = (
rd.Engine.float(init_range=(0.0, 1.0))
.fitness(single_fit)
.limit(rd.Limit.generations(100))
# ... other parameters ...
)
# Run the engine for 100 generations
single_result: rd.Generation[float, float] = single_obj_engine.run()
# Get the best individual's decoded value
value: float = single_result.value()
# Get the score (fitness) of the best individual or epoch score
score: list[float] = single_result.score() # note that this is a list.
# In this scenario, the engine is configured for single-objective optimization,
# so the list will contain a single value.
# Get the population of the engine's ecosystem
population: rd.Population[float] = single_result.population() # Population object
# Get the index of the epoch (number of generations)
index: int = single_result.index() # int
# Get the metrics of the engine
metrics: rd.MetricSet = single_result.metrics() # MetricSet object
# Get the objective of the engine
objective: list[str] | str = (
single_result.objective()
) # list[str] | str (list[str] if multi-objective) - "min" or "max"
// Create an engine of type:
// `GeneticEngine<FloatChromosome<f32>, f32>`
//
// Where the `epoch` is `Generation<FloatChromosome<f32>, f32>`
let engine = GeneticEngine::builder()
.codec(FloatCodec::scalar(0.0..1.0))
.fitness_fn(|genotype: f32| my_fitness_fn(genotype)) // Return a single fitness score
// ... other parameters ...
.build();
// Run the engine for 100 generations - the result will be a `Generation<FloatChromosome<f32>, f32>`
let result = engine.iter().take(100).last().unwrap();
// Get the best individual's decoded value:
let best_value: &f32 = result.value();
// Get the score (fitness) of the best individual (or epoch score):
let best_score: &Score = result.score();
// Get the index of the epoch (number of generations):
let index: usize = result.index();
// Get the ecosystem level information:
let ecosystem: &Ecosystem<FloatChromosome<f32>> = result.ecosystem();
let population: &Population<FloatChromosome<f32>> = ecosystem.population();
let species: Option<&Vec<Species<FloatChromosome<f32>>>> = ecosystem.species();
// Get performance metrics:
let metrics: &MetricSet = result.metrics();
// Get evolution duration (also available in metrics):
let time: Duration = result.time();
// Get the objective of the engine
let objective: &Objective = result.objective();
Multi-Objective
When the engine is configured for multi-objective optimization, the Generation will have a ParetoFront attached to it. The only difference between the single-objective and multi-objective case is the availability of the ParetoFront and the shape of the score — a list of values, one per objective, instead of a single value.
import radiate as rd
# Create an engine
multi_obj_engine: rd.Engine[float, list[np.ndarray]] = (
rd.Engine.float(shape=[2, 2, 2], init_range=(0.0, 1.0), use_numpy=True)
.fitness(multi_fit) # Multi-objective fitness function
.objective(rd.MIN, rd.MAX) # Specify multi-objective optimization
.limit(rd.Limit.generations(100))
# ... other parameters ...
)
# Run the engine for 100 generations
multi_result: rd.Generation[float, list[np.ndarray]] = multi_obj_engine.run()
# Everything in the multi-objective epoch is the same as the single-objective epoch, except for the value.
# The function call to `front()` will return a `ParetoFront` object while `value()` will return None.:
front: rd.Front[float] = multi_result.front() # ParetoFront object
# This is of type `Front` with `FrontValue` members.
value_at_index_0: rd.FrontValue[float] = front[0] # FrontValue object
all_values: list[rd.FrontValue[float]] = front.values() # list[FrontValue]
# Get the members of the Pareto front:
score: list[float] = all_values[0].score() # list[float] - multi-objective score
genotype: rd.Genotype[float] = all_values[0].genotype() # Genotype object
// Create an engine of type:
// `GeneticEngine<FloatChromosome<f32>, f32>`
//
// Where the `epoch` is `Generation<FloatChromosome<f32>, f32>`
let engine = GeneticEngine::builder()
.codec(FloatCodec::scalar(0.0..1.0))
.multi_objective(vec![Optimize::Minimize, Optimize::Maximize]) // Specify multi-objective optimization
// Return a multi-objective fitness score (one value per objective)
.fitness_fn(|genotype: f32| vec![my_fitness_fn(genotype), my_fitness_fn(genotype)])
// ... other parameters ...
.build();
// Run the engine for 100 generations
let result = engine.iter().take(100).last().unwrap();
// Everything in this generation is the same as the single-objective epoch, except that
// the call to `front()` will return the Pareto `Front`:
// This will be of type `Front<Phenotype<FloatChromosome<f32>>>`
let front: &Front<Phenotype<FloatChromosome<f32>>> = result.front().unwrap();
// Get the members of the Pareto front:
let individuals: &[Arc<Phenotype<FloatChromosome<f32>>>] = front.values();
GenerationView
Rust only
GenerationView has no Python binding — Python always gets an owned Generation at the end of the engine run.
GenerationView<'a, C, T> borrows straight through to the engine's live context instead of cloning it — every accessor (score(), value(), population(), metrics(), …) mirrors Generation's, just returning borrowed data instead of owned. Its only entry point is the argument to EngineRuntime::until(closure), which is how Radiate lets you write an ad-hoc stopping condition without paying to construct a Generation on every generation just to check it:
// `until` takes a closure over a borrowed `GenerationView` - no `Generation` clone
// happens on any generation except (implicitly) the last one, when `.last()` needs
// to hand back an owned result.
let engine = build_engine();
let result = engine
.iter()
.until(|view: GenerationView<FloatChromosome<f32>, f32>| view.score().as_f32() <= 0.01)
.last()
.unwrap();
If you find yourself wanting a GenerationView outside of until(...) — say, to inspect state mid-loop without the clone cost — that's a sign you may want the raw Engine trait's step()/context() directly instead (see Runtime).