Example
A handful of small, focused examples rather than one big walkthrough — each one contrasts a different way of driving the engine. The last one is the important one: it's the case where run()/last() genuinely isn't enough and you need the iterator instead.
A single limit
The simplest shape: build, attach one limit, run().
Combined limits
Attach several limits — the engine stops on whichever trips first, so this run stops well before 10,000 generations if the score target is hit sooner.
import radiate as rd
# Stops on whichever trips first - almost always the score target here, well before
# the 10,000-generation ceiling.
combined_engine = (
rd.Engine.float(init_range=(0.0, 1.0))
.fitness(loop_fit)
.limit(
rd.Limit.generations(10_000),
rd.Limit.score(0.01),
)
)
combined_result = combined_engine.run()
An ad-hoc limit
Rust only: when none of the built-in Limit variants fit, until(closure) takes an arbitrary predicate over a GenerationView — still routed through run()/last() under the hood, so no Generation gets built until the predicate finally returns true.
// `until` takes an arbitrary predicate over a borrowed `GenerationView` - still
// routed through the cheap, Limit-driven `run()`/`.last()`, so no `Generation` is
// built until it trips.
let engine = build_engine();
let result = engine
.iter()
.until(|view: GenerationView<'_, FloatChromosome<f32>, f32>| {
view.index() >= 20 && view.score().as_f32() < 0.05
})
.last()
.unwrap();
When you actually need the iterator
Limits only answer "should I stop?" — they can't hand you the intermediate state itself. If you need to act on every generation as it happens (stream scores somewhere, update a live plot, react to a pause request), a Limit can't do that job no matter how it's composed — you need the real per-generation Generation, which means the iterator, not run().
import radiate as rd
# A Limit can only answer "should I stop?" - it can't hand you the intermediate state.
# Collecting the score history needs the real Generation at every step, which means
# iterating, not run().
iter_engine = (
rd.Engine.float(init_range=(0.0, 1.0)).fitness(loop_fit).limit(rd.Limit.generations(50))
)
score_history: list[list[float]] = []
for epoch in iter_engine:
score_history.append(epoch.score())
assert len(score_history) == 50
// A `Limit` can only answer "should I stop?" - it can't hand you the intermediate
// state. Collecting the score history needs the real `Generation` at every step,
// which means the iterator, not `run()`/`.last()`.
let engine = build_engine();
let mut score_history = Vec::new();
for epoch in engine.iter().take(50) {
score_history.push(epoch.score().as_f32());
}
assert_eq!(score_history.len(), 50);