Skip to content

Limits


A Limit is a condition checked once after every generation to decide whether the engine should keep going. Multiple limits can be active at once (via Limit::Combined in Rust, or by passing several to .limit(...)/run(...) in Python) — in every case, the engine stops as soon as any one limit trips.

Limit Stops when Rust Python
Generation the generation counter reaches a target count Limit::Generation(n) / .until_generation(n) rd.Limit.generations(n)
Seconds cumulative engine time reaches a duration Limit::Seconds(Duration) / .until_seconds(secs) / .until_duration(dur) rd.Limit.seconds(secs)
Score the best score reaches or crosses a target (direction-aware per objective; for multi-objective, every component must pass) Limit::Score(Score) / .until_score(score) rd.Limit.score(target)
Convergence improvement over a sliding window drops to (or below) an epsilon Limit::Convergence(window, epsilon, _) / .until_convergence(window, epsilon) rd.Limit.convergence(window, threshold)
Expr a metric expression evaluates to true — see Expressions for the full recipe Limit::Expr(Expr) / .until_expr(expr) rd.Limit.expr(expr)
Combined any one of a set of sub-limits trips Limit::Combined(vec![...]) / .limit((a, b, c)) .limit(a, b, c)

Metric-based limits

A predicate-based limit keyed to a named metric also exists (Limit::Metric/until_metric in Rust, rd.Limit.metric(...) in Python), but is intentionally left out of this page for now pending a closer look at its stop/continue polarity.


Each limit on its own

import radiate as rd

score_limit = rd.Limit.score(0.01)
generations_limit = rd.Limit.generations(100)
seconds_limit = rd.Limit.seconds(60)
# window and threshold for convergence - how close the scores must be over the
# window to consider convergence
convergence_limit = rd.Limit.convergence(window=50, threshold=0.01)
// Run until a score target is reached
let engine = build_engine();
let target_score = 0.01;
let result = engine.iter().until_score(target_score).last().unwrap();
// Run until a time limit is reached
let engine = build_engine();
let time_limit = Duration::from_secs(60);
let result = engine.iter().until_duration(time_limit).last().unwrap();
// Run until the score stops improving by more than `epsilon` over a sliding `window`
let engine = build_engine();
let window = 50;
let epsilon = 0.01; // how close the scores must be over the window to consider convergence
let result = engine
    .iter()
    .limit(Limit::Convergence(
        window,
        epsilon,
        VecDeque::with_capacity(window),
    ))
    .last()
    .unwrap();

Combining limits

Combining several at once — the engine stops on whichever trips first:

import radiate as rd

combined_engine = (
    rd.Engine.float(init_range=(0.0, 1.0))
    .fitness(loop_fit)
    # The engine stops as soon as ANY one of these is reached
    .limit(
        rd.Limit.generations(5),
        rd.Limit.seconds(30),
        rd.Limit.score(0.01),
    )
)

combined_result = combined_engine.run()
// Combine several limits - the engine stops as soon as ANY one of them is reached
let engine = build_engine();
let result = engine
    .iter()
    .logging()
    .limit((
        Limit::Generation(100),
        Limit::Seconds(Duration::from_secs_f64(2.0)),
        Limit::Score(0.01.into()),
    ))
    .last()
    .unwrap();

Python requires at least one limit

Unlike Rust — where an unbounded .iter() or run(closure) is legal, since the closure or a break is itself the stop condition — Python's engine raises immediately if you call .run() or start iterating with no Limit attached anywhere. Always set at least one via .limit(...).