Skip to main content

radiate_engines/
limit.rs

1//! # Limit System
2//!
3//! This module provides a flexible and extensible limit system for controlling
4//! the execution of genetic algorithms and evolutionary computations through the
5//! EngineIterator. The `Limit` enum defines various types of
6//! termination conditions that can be applied individually or combined for
7//! complex control scenarios.
8//!
9//! The limit system supports multiple termination strategies:
10//! - **Generation Limits**: Stop after a fixed number of generations
11//! - **Time Limits**: Stop after a specified duration
12//! - **Score Thresholds**: Stop when fitness targets are reached
13//! - **Convergence Detection**: Stop when improvement rate falls below threshold
14//! - **Combined Limits**: Apply multiple limits simultaneously
15
16use crate::{
17    EvolutionContext, Generation, events::LimitTriggered, generation::GenerationView,
18    runtime::RuntimeLimit,
19};
20use radiate_core::{
21    AnyValue, Chromosome, Engine, Expr, Objective, Optimize, Score, error::RadiateResult,
22};
23use radiate_error::radiate_bail;
24use std::{collections::VecDeque, fmt::Debug, time::Duration};
25
26/// Defines various types of limits for controlling genetic algorithm execution.
27///
28/// The `Limit` enum provides a unified interface for specifying when and how
29/// evolutionary algorithms should terminate. Limits can be used individually
30/// or combined to create complex termination scenarios that balance multiple
31/// objectives like computation time, solution quality, and convergence.
32///
33/// # Limit Types
34///
35/// ## Generation Limits
36/// Stop execution after a fixed number of generations, useful for controlling
37/// computational budget and ensuring reproducible results.
38///
39/// ## Time Limits
40/// Stop execution after a specified duration, useful for real-time applications
41/// or when running on shared computing resources with time constraints.
42///
43/// ## Score Thresholds
44/// Stop execution when fitness targets are reached, useful for problems where
45/// you know the desired solution quality or have specific performance requirements.
46///
47/// ## Convergence Detection
48/// Stop execution when the improvement rate falls below a threshold, useful
49/// for detecting when the algorithm has converged to a local or global optimum.
50///
51/// ## Combined Limits
52/// Apply multiple limits simultaneously, stopping when any limit is reached.
53/// This provides flexible control for complex scenarios.
54///
55/// # Examples
56///
57/// ## Basic Usage
58///
59/// ```rust
60/// use radiate_engines::Limit;
61/// use radiate_core::Score;
62/// use std::time::Duration;
63///
64/// // Generation limit
65/// let gen_limit = Limit::Generation(1000);
66///
67/// // Time limit
68/// let time_limit = Limit::Seconds(Duration::from_secs(300));
69///
70/// // Score threshold
71/// let score_limit = Limit::Score(Score::from(0.95));
72/// ```
73///
74/// ## Combined Limits
75///
76/// ```rust
77/// use radiate_engines::Limit;
78/// use radiate_core::Score;
79/// use std::time::Duration;
80///
81/// // Combine multiple limits
82/// let combined = Limit::Combined(vec![
83///     Limit::Generation(1000),           // Max 1000 generations
84///     Limit::Seconds(Duration::from_secs(600)), // Max 10 minutes
85///     Limit::Score(Score::from(0.99)),   // Stop at .99 fitness
86/// ]);
87///
88/// // This will stop when ANY of the limits is reached
89/// ```
90///
91/// ## Automatic Conversion
92///
93/// ```rust
94/// use radiate_engines::Limit;
95/// use std::time::Duration;
96///
97/// // Automatic conversion from common types
98/// let gen_limit: Limit = 500.into();                          // Generation limit
99/// let time_limit: Limit = Duration::from_secs(120).into();    // Time limit
100/// let score_limit: Limit = 0.85f32.into();                    // Score limit
101/// let multi_score: Limit = vec![0.9, 0.8, 0.7].into();        // Multi-objective
102/// let conv_limit: Limit = (25, 0.01f32).into();               // Convergence
103/// ```
104#[derive(Clone)]
105pub enum Limit {
106    Generation(usize),
107    Seconds(Duration),
108    Score(Score),
109    Convergence(usize, f32, VecDeque<f32>),
110    Combined(Vec<Limit>),
111    Expr(Expr),
112    Fn,
113}
114
115pub(crate) enum LimitOutcome {
116    Proceed,
117    Stop,
118}
119
120impl<C, T, E> RuntimeLimit<E> for Limit
121where
122    E: Engine<Epoch = Generation<C, T>, Ctx = EvolutionContext<C, T>>,
123    C: Chromosome + Clone,
124    T: Clone + Send + Sync,
125{
126    fn proceed(&mut self, ctx: &E::Ctx) -> RadiateResult<bool> {
127        let outcome = match self {
128            Limit::Generation(gens) => check_generation_limit(ctx, *gens),
129            Limit::Seconds(secs) => check_time_limit(ctx, *secs),
130            Limit::Score(limit) => check_score_limit(ctx, limit),
131            Limit::Convergence(window, epsilon, history) => {
132                check_convergence_limit(ctx, *window, *epsilon, history)
133            }
134            Limit::Combined(limits) => {
135                let proceed = limits
136                    .iter_mut()
137                    .map(|limit| <Limit as RuntimeLimit<E>>::proceed(limit, ctx))
138                    .collect::<RadiateResult<Vec<bool>>>()
139                    .map(|proceed| proceed.iter().all(|&p| p));
140
141                match proceed {
142                    Ok(true) => Ok(LimitOutcome::Proceed),
143                    Ok(false) => Ok(LimitOutcome::Stop),
144                    Err(e) => Err(e),
145                }
146            }
147            Limit::Expr(expr) => check_expr_limit(ctx, expr),
148            Limit::Fn => return Ok(true), // Custom function limits are handled externally
149        }?;
150
151        match outcome {
152            LimitOutcome::Proceed => Ok(true),
153            LimitOutcome::Stop => {
154                ctx.event_stream()
155                    .publish(LimitTriggered(ctx.index, self.clone()));
156                Ok(false)
157            }
158        }
159    }
160}
161
162#[inline]
163fn check_generation_limit<C, T>(
164    ctx: &EvolutionContext<C, T>,
165    limit: usize,
166) -> RadiateResult<LimitOutcome>
167where
168    C: Chromosome,
169{
170    let proceed = ctx.index < limit;
171
172    Ok(if proceed {
173        LimitOutcome::Proceed
174    } else {
175        LimitOutcome::Stop
176    })
177}
178
179#[inline]
180fn check_time_limit<C, T>(
181    ctx: &EvolutionContext<C, T>,
182    limit: Duration,
183) -> RadiateResult<LimitOutcome>
184where
185    C: Chromosome,
186{
187    let total_time = ctx
188        .metrics
189        .time()
190        .and_then(|m| m.times().map(|t| t.sum()))
191        .unwrap_or_default();
192
193    let proceed = total_time < limit;
194
195    Ok(if proceed {
196        LimitOutcome::Proceed
197    } else {
198        LimitOutcome::Stop
199    })
200}
201
202#[inline]
203fn check_score_limit<C, T>(
204    ctx: &EvolutionContext<C, T>,
205    limit: &Score,
206) -> RadiateResult<LimitOutcome>
207where
208    C: Chromosome,
209{
210    let Some(score) = &ctx.score else {
211        return Ok(LimitOutcome::Proceed);
212    };
213
214    let proceed = match &ctx.objective {
215        Objective::Single(obj) => match obj {
216            Optimize::Minimize => score > limit,
217            Optimize::Maximize => score < limit,
218        },
219        Objective::Multi(objs) => {
220            let mut all_pass = true;
221            for (i, score) in score.iter().enumerate() {
222                let passed = match objs[i] {
223                    Optimize::Minimize => score > &limit[i],
224                    Optimize::Maximize => score < &limit[i],
225                };
226
227                if !passed {
228                    all_pass = false;
229                    break;
230                }
231            }
232
233            all_pass
234        }
235    };
236
237    let outcome = if proceed {
238        LimitOutcome::Proceed
239    } else {
240        LimitOutcome::Stop
241    };
242
243    Ok(outcome)
244}
245
246#[inline]
247fn check_convergence_limit<C, T>(
248    ctx: &EvolutionContext<C, T>,
249    window: usize,
250    epsilon: f32,
251    history: &mut VecDeque<f32>,
252) -> RadiateResult<LimitOutcome>
253where
254    C: Chromosome,
255{
256    let Some(current_score) = &ctx.score else {
257        return Ok(LimitOutcome::Proceed);
258    };
259
260    history.push_back(current_score.as_f32());
261    if history.len() > window {
262        history.pop_front();
263    }
264
265    if history.len() < window {
266        return Ok(LimitOutcome::Proceed);
267    }
268
269    let first = history.front().unwrap();
270    let last = history.back().unwrap();
271
272    let improved = match &ctx.objective {
273        Objective::Single(_) => last - first,
274        Objective::Multi(_) => {
275            let mut total_improvement = 0.0;
276            for (i, score) in history.iter().enumerate() {
277                let improvement = match &ctx.objective {
278                    Objective::Multi(objs) => match objs[i] {
279                        Optimize::Minimize => score - first,
280                        Optimize::Maximize => first - score,
281                    },
282                    _ => 0.0,
283                };
284                total_improvement += improvement;
285            }
286            total_improvement / history.len() as f32
287        }
288    };
289
290    let proceed = improved.abs() > epsilon;
291
292    Ok(if proceed {
293        LimitOutcome::Proceed
294    } else {
295        LimitOutcome::Stop
296    })
297}
298
299#[inline]
300fn check_expr_limit<C, T>(
301    ctx: &EvolutionContext<C, T>,
302    expr: &mut Expr,
303) -> RadiateResult<LimitOutcome>
304where
305    C: Chromosome,
306{
307    let metrics = &ctx.metrics;
308    let result = expr.evaluate(metrics)?;
309
310    if let AnyValue::Bool(b) = result {
311        let proceed = !b;
312        Ok(if proceed {
313            LimitOutcome::Proceed
314        } else {
315            LimitOutcome::Stop
316        })
317    } else {
318        radiate_bail!(Engine: format!(
319            "Expression did not evaluate to a boolean value: {:?}",
320            result
321        ))
322    }
323}
324
325impl From<usize> for Limit {
326    fn from(value: usize) -> Self {
327        Limit::Generation(value)
328    }
329}
330
331impl From<Duration> for Limit {
332    fn from(value: Duration) -> Self {
333        Limit::Seconds(value)
334    }
335}
336
337impl From<f32> for Limit {
338    fn from(value: f32) -> Self {
339        Limit::Score(Score::from(value))
340    }
341}
342
343impl From<Vec<f32>> for Limit {
344    fn from(value: Vec<f32>) -> Self {
345        Limit::Score(Score::from(value))
346    }
347}
348
349impl From<(usize, f32)> for Limit {
350    fn from((window, epsilon): (usize, f32)) -> Self {
351        Limit::Convergence(window, epsilon, VecDeque::with_capacity(window))
352    }
353}
354
355impl From<Expr> for Limit {
356    fn from(value: Expr) -> Self {
357        Limit::Expr(value)
358    }
359}
360
361impl From<Vec<Limit>> for Limit {
362    fn from(value: Vec<Limit>) -> Self {
363        Limit::Combined(value)
364    }
365}
366
367impl From<(Limit, Limit)> for Limit {
368    fn from(value: (Limit, Limit)) -> Self {
369        Limit::Combined(vec![value.0, value.1])
370    }
371}
372
373impl From<(Limit, Limit, Limit)> for Limit {
374    fn from(value: (Limit, Limit, Limit)) -> Self {
375        Limit::Combined(vec![value.0, value.1, value.2])
376    }
377}
378
379impl From<(Limit, Limit, Limit, Limit)> for Limit {
380    fn from(value: (Limit, Limit, Limit, Limit)) -> Self {
381        Limit::Combined(vec![value.0, value.1, value.2, value.3])
382    }
383}
384
385impl<const N: usize> From<[Limit; N]> for Limit {
386    fn from(value: [Limit; N]) -> Self {
387        Limit::Combined(value.into_iter().collect::<Vec<Limit>>())
388    }
389}
390
391impl Debug for Limit {
392    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393        match self {
394            Limit::Generation(gens) => write!(f, "Generation({gens})"),
395            Limit::Seconds(secs) => write!(f, "Seconds({secs:?})"),
396            Limit::Score(score) => write!(f, "Score({:?})", score),
397            Limit::Convergence(window, epsilon, _) => {
398                write!(f, "Convergence(window: {window}, epsilon: {epsilon})")
399            }
400            Limit::Combined(limits) => write!(f, "Combined({limits:?})"),
401            Limit::Expr(expr) => write!(f, "ExprLimit({expr:?})"),
402            Limit::Fn => write!(f, "CustomFnLimit"),
403        }
404    }
405}
406
407impl<C, T, E, F> RuntimeLimit<E> for F
408where
409    C: Chromosome,
410    E: Engine<Epoch = Generation<C, T>, Ctx = EvolutionContext<C, T>>,
411    F: Fn(GenerationView<C, T>) -> bool,
412{
413    fn proceed(&mut self, ctx: &E::Ctx) -> RadiateResult<bool> {
414        let view = GenerationView::new(ctx);
415        let proceed = !(self)(view);
416        if !proceed {
417            ctx.event_stream()
418                .publish(LimitTriggered(ctx.index, Limit::Fn));
419        }
420        Ok(proceed)
421    }
422}
423
424#[cfg(test)]
425mod tests {
426
427    #[test]
428    fn test_limit_conversions() {
429        use super::Limit;
430        use std::time::Duration;
431
432        let gen_limit: Limit = 100.into();
433        match gen_limit {
434            Limit::Generation(n) => assert_eq!(n, 100),
435            _ => panic!("Expected Generation limit"),
436        }
437
438        let time_limit: Limit = Duration::from_secs(60).into();
439        match time_limit {
440            Limit::Seconds(dur) => assert_eq!(dur, Duration::from_secs(60)),
441            _ => panic!("Expected Seconds limit"),
442        }
443
444        let score_limit: Limit = 95.5f32.into();
445        match score_limit {
446            Limit::Score(score) => assert_eq!(score.as_f32(), 95.5),
447            _ => panic!("Expected Score limit"),
448        }
449
450        let multi_score_limit: Limit = vec![90.0f32, 85.5f32, 78.0f32].into();
451        match multi_score_limit {
452            Limit::Score(score) => {
453                assert_eq!(score[0], 90.0);
454                assert_eq!(score[1], 85.5);
455                assert_eq!(score[2], 78.0);
456            }
457            _ => panic!("Expected Multi Score limit"),
458        }
459
460        let conv_limit: Limit = (10, 0.01f32).into();
461        match conv_limit {
462            Limit::Convergence(gens, thresh, _) => {
463                assert_eq!(gens, 10);
464                assert_eq!(thresh, 0.01);
465            }
466            _ => panic!("Expected Convergence limit"),
467        }
468
469        let generation_combined_limit: Limit = 100.into();
470        let duration_combined_limit: Limit = Duration::from_secs(30).into();
471        let combined_limit: Limit = vec![generation_combined_limit, duration_combined_limit].into();
472        match combined_limit {
473            Limit::Combined(limits) => assert_eq!(limits.len(), 2),
474            _ => panic!("Expected Combined limit"),
475        }
476    }
477}