Skip to content

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().

import radiate as rd

engine = (
    rd.Engine.float(init_range=(0.0, 1.0)).fitness(loop_fit).limit(rd.Limit.generations(50))
)

result = engine.run()
let engine = build_engine();
let result = engine.iter().until_generation(50).last().unwrap();

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()
// Stops on whichever trips first - almost always the score target here, well
// before the 10,000-generation ceiling.
let engine = build_engine();
let result = engine
    .iter()
    .limit((Limit::Generation(10_000), Limit::Score(0.01.into())))
    .last()
    .unwrap();

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);