Engine Runtime
The engine can be driven two ways: as a plain iterator, or through run()/.last() against attached Limits. Prefer limits whenever you only care about the final result — they're checked against the engine's live state directly, with no per-generation Generation snapshot built along the way. Reach for the iterator when you actually need to observe or act on every generation as it happens — see the Example page for a case where that's genuinely necessary.
The engine is directly iterable (for epoch in engine), and next(engine) works too — both build a fresh Generation on every call.
import radiate as rd
engine = (
rd.Engine.float(init_range=(0.0, 1.0))
.fitness(loop_fit)
.limit(rd.Limit.generations(5))
)
# The engine is itself an iterator over `Generation` epochs
for epoch in engine:
print(f"Generation {epoch.index()}: Score = {epoch.score()}")
Or drive it one epoch at a time:
import radiate as rd
engine = (
rd.Engine.float(init_range=(0.0, 1.0))
.fitness(loop_fit)
.limit(rd.Limit.generations(5))
)
# Or drive it manually with the builtin `next()` - useful for interleaving other work
# between generations. Every call builds a fresh `Generation` snapshot; prefer `run()`
# (below) when you don't need to inspect each generation as it happens.
while True:
epoch = next(engine)
if epoch.index() >= 5:
break
print(f"Generation {epoch.index()}: Score = {epoch.score()}")
engine.run() takes the Limit-driven path instead. Python requires at least one Limit to be attached either way — there's no closure-based stop condition here.
.iter() hands back a runtime that implements the standard Iterator trait, so a for loop or an explicit .next() works directly:
// `iter()` consumes the engine and hands back an `EngineRuntime`, which imple¬ments
// the standard `Iterator` trait - so a plain for loop works directly. `take(n)` here
// is `EngineRuntime`'s own method (an alias for `until_generation(n)`), not
// `std::iter::Iterator::take` - it appends a `Limit::Generation(n)` to the runtime's
// existing limits and returns the same runtime, rather than wrapping it in a
// `std::iter::Take<Self>`, but reads the same in a for loop either way.
let engine = build_engine();
for epoch in engine.iter().take(100) {
println!(
"Generation {}: Score = {}",
epoch.index(),
epoch.score().as_f32()
);
}
That same runtime also has its own run()/.last(), driven by attached Limits instead of by iterating. A closure passed straight to the engine's own run(closure), without going through .iter() first, works too, but re-evaluates against a fresh Generation on every generation:
// This closure needs a `&Generation` to decide whether to stop, so `run()` here
// builds one on every single generation (via `Engine::next()` = step() + epoch()) -
// the same cost as iterating, even though no iterator is involved.
let mut engine = build_engine();
let result =
engine.run(|generation: &Generation<FloatChromosome<f32>, f32>| generation.index() >= 100);
// `take(100)` attaches a `Limit::Generation(100)` instead; `.last()` here is
// `EngineRuntime`'s own inherent method (not `Iterator::last()`), so it never calls
// `Iterator::next()` at all - `epoch()` only runs once, after the limit trips.
let engine = build_engine();
let result = engine.iter().take(100).last().unwrap();
A closure over a borrowed GenerationView via .until(closure) lets you write an ad-hoc stop condition without needing a full Generation — see Generations.
Always attach a stopping condition
The engine's iterator is a streaming, effectively infinite iterator — it produces epochs until a limit trips, a break, or a return. Always attach one (or a method like take/until/last in Rust) unless you genuinely want to run indefinitely.
Re-running the engine
Rust: a closure-based run(closure) call borrows the engine rather than consuming it, so calling it again on the same engine continues from wherever it left off. .iter(), by contrast, consumes the engine — once you've built a runtime from it, that engine value is gone.
Python: Engine is a reusable builder, not a live engine. Every for loop, every next() call after StopIteration, and every .run() call constructs a brand-new engine from the builder's queued inputs. Calling .run() twice runs two independent evolutionary processes from a fresh population — it does not resume the first one.
Combinators & Actions
Beyond the built-in stopping conditions, Radiate lets you compose several of them together and attach side effects like progress logging or periodic checkpointing.
Limits and side effects are both configured on the engine builder rather than chained onto an iterator — see Limits for combining stop conditions, and Convenience run() below for log/checkpoint options.
The runtime adds chainable methods for composing stop conditions and side effects: until_score, until_generation, until_seconds/until_duration, until_convergence, until_expr, until(closure), and the generic limit(...) — see Limits for what each one checks. logging()/log_every(n) print per-generation progress; checkpoint(interval, path)/checkpoint_with(...) persist engine state periodically.
// `logging()` (or `log_every(n)` to throttle it) prints progress to the console
// every generation; `checkpoint(interval, path)` persists engine state periodically.
let engine = build_engine();
let result = engine.iter().logging().until_seconds(10.0).last().unwrap();
let engine = build_engine();
let checkpoint_path = "checkpoint.json";
let result = engine
.iter()
.checkpoint(10, checkpoint_path)
.take(100)
.last()
.unwrap();
take() isn't std::iter::Iterator::take
take(n) is an alias for until_generation(n) — it appends a generation-count stop condition and hands back the same runtime, rather than wrapping it in a std::iter::Take<Self>. It reads the same in a for loop, but don't expect standard-library semantics if you go looking for them.
Convenience run()
For the common case — build an engine, run it to completion, get the final epoch — run() wraps the condition-driven loop without needing an explicit for/while:
import radiate as rd
engine = (
rd.Engine.float(init_range=(0.0, 1.0))
.fitness(loop_fit)
.limit(rd.Limit.generations(5))
)
# `run()` is the convenience wrapper: it drives the same Limit-driven loop internally
# and just hands back the final epoch.
convenience_result: rd.Generation[float, float] = engine.run()
run() also accepts log, ui, and checkpoint options:
import radiate as rd
engine = (
rd.Engine.float(init_range=(0.0, 1.0))
.fitness(my_fitness_fn)
.limit(rd.Limit.generations(100))
)
# `run()` also accepts logging, a terminal UI, and checkpointing
result = engine.run(
log=True,
ui=True, # Enable terminal UI - if enabled, log is ignored
checkpoint=(
10,
"checkpoint",
"pkl",
),
# checkpoint every 10 generations to the folder "checkpoint" in pickle
# format - can be loaded with the .load_checkpoint() method on the engine
)
Control Interface
The engine provides a control interface for pausing, resuming, and stopping the evolutionary process from outside the run loop — for example, pausing or stepping through generations from another thread or in response to user input.
Not currently implemented.
let mut engine = GeneticEngine::builder()
.minimizing()
.codec(IntCodec::vector(5, 0..100))
.fitness_fn(|geno: Vec<i32>| geno.iter().sum::<i32>())
.build();
let control = engine.control();
let handle = thread::spawn(move || {
// Run the engine for 1 second
let result = engine.iter().until_seconds(1_f64).last().unwrap();
// because we are running for only a second and are pausing the engine,
// the engine's internal time tracking should be very close to 1 second even
// though we paused it for +500ms
assert_eq!((result.seconds() - 1_f64).abs().round(), 0.0);
});
thread::sleep(Duration::from_millis(100));
control.set_paused(true);
// Ensure the engine is paused for at least 500ms
thread::sleep(Duration::from_millis(500));
control.set_paused(false);
handle.join().unwrap();