Skip to main content

radiate_engines/
runtime.rs

1use crate::{
2    Engine, EvolutionContext, Generation, Handler, Limit,
3    events::{EngineLogger, Event, GenerationSnapshot, HealthMonitor, LoggingHandler},
4};
5use crate::{generation::GenerationView, init_logging};
6use radiate_core::Expr;
7use radiate_core::error::{RadiateResult, Result};
8use radiate_core::{Chromosome, EngineState, Score};
9use std::collections::VecDeque;
10use std::time::Duration;
11
12pub trait RuntimeLimit<E: Engine> {
13    fn proceed(&mut self, context: &E::Ctx) -> RadiateResult<bool>;
14}
15
16pub struct EngineRuntime<E: Engine> {
17    engine: E,
18    limits: Vec<Box<dyn RuntimeLimit<E>>>,
19}
20
21impl<E: Engine> EngineRuntime<E> {
22    pub fn new(engine: E) -> Self {
23        Self {
24            engine,
25            limits: Vec::new(),
26        }
27    }
28
29    #[inline]
30    pub fn run(mut self) -> Result<E::Epoch> {
31        loop {
32            if matches!(self.engine.state(), EngineState::Stopped) {
33                return Ok(self.engine.epoch());
34            }
35
36            self.step()?;
37        }
38    }
39
40    #[inline]
41    fn step(&mut self) -> Result<()> {
42        if matches!(self.engine.state(), EngineState::Stopped) {
43            return Ok(());
44        }
45
46        self.engine.step()?;
47
48        let ctx = self.engine.context();
49
50        for limit in self.limits.iter_mut() {
51            if !limit.proceed(ctx)? {
52                self.engine.stop();
53                return Ok(());
54            }
55        }
56
57        Ok(())
58    }
59
60    fn add_limit<L>(&mut self, limit: L)
61    where
62        L: RuntimeLimit<E> + 'static,
63    {
64        let boxed: Box<dyn RuntimeLimit<E>> = Box::new(limit);
65        self.limits.push(boxed);
66    }
67}
68
69/// General iter fns for the `EngineRuntime` struct, allowing for a more ergonomic
70/// and fluent interface when configuring the runtime.
71impl<C, T, E> EngineRuntime<E>
72where
73    E: Engine<Epoch = Generation<C, T>, Ctx = EvolutionContext<C, T>>,
74    C: Chromosome + Clone + 'static,
75    T: Clone + Send + Sync + 'static,
76{
77    pub fn chain_if(self, condition: bool, action_fn: impl FnOnce(Self) -> Self) -> Self {
78        if condition { action_fn(self) } else { self }
79    }
80
81    pub fn last(self) -> Result<E::Epoch> {
82        self.run()
83    }
84
85    pub fn every<F>(self, interval: usize, mut action_fn: F) -> Self
86    where
87        F: FnMut(GenerationView<C, T>) + Send + Sync + 'static,
88    {
89        assert!(interval > 0, "every interval must be greater than zero");
90        let guarded_interval = interval.max(1);
91
92        self.engine
93            .context()
94            .event_stream()
95            .subscribe(move |ctx: &GenerationSnapshot<C, T>| {
96                let inner = &ctx.generation;
97                action_fn(GenerationView::from(inner.as_ref()));
98            })
99            .schedule(Expr::every(guarded_interval))
100            .unwrap();
101        self
102    }
103
104    pub fn throttle<F>(self, duration: Duration, mut action_fn: F) -> Self
105    where
106        F: FnMut(GenerationView<C, T>) + Send + Sync + 'static,
107    {
108        self.engine
109            .context()
110            .event_stream()
111            .subscribe(move |ctx: &GenerationSnapshot<C, T>| {
112                let inner = &ctx.generation;
113                action_fn(GenerationView::from(inner.as_ref()));
114            })
115            .schedule(Expr::throttle(duration))
116            .unwrap();
117        self
118    }
119
120    pub fn subscribe<EV: Event>(self, handler: impl Handler<EV>) -> Self {
121        self.engine.context().event_stream().subscribe(handler);
122        self
123    }
124}
125
126/// Limit configuration methods for the `EngineRuntime` struct, allowing users to specify various
127/// stopping conditions for the evolutionary process.
128impl<C, T, E> EngineRuntime<E>
129where
130    E: Engine<Epoch = Generation<C, T>, Ctx = EvolutionContext<C, T>>,
131    C: Chromosome + Clone + 'static,
132    T: Clone + Send + Sync + 'static,
133{
134    pub fn until_score(mut self, score: impl Into<Score>) -> EngineRuntime<E> {
135        self.add_limit(Limit::Score(score.into()));
136        self
137    }
138
139    pub fn until_generation(mut self, generations: usize) -> EngineRuntime<E> {
140        self.add_limit(Limit::Generation(generations));
141        self
142    }
143
144    pub fn until_seconds(mut self, seconds: f64) -> EngineRuntime<E> {
145        self.add_limit(Limit::Seconds(Duration::from_secs_f64(seconds)));
146        self
147    }
148
149    pub fn until_duration(mut self, duration: impl Into<std::time::Duration>) -> EngineRuntime<E> {
150        self.add_limit(Limit::Seconds(duration.into()));
151        self
152    }
153
154    pub fn until_convergence(mut self, window: usize, epsilon: f32) -> EngineRuntime<E> {
155        self.add_limit(Limit::Convergence(
156            window,
157            epsilon,
158            VecDeque::with_capacity(window),
159        ));
160        self
161    }
162
163    pub fn until_expr(mut self, expr: impl Into<Expr>) -> EngineRuntime<E> {
164        self.add_limit(Limit::Expr(expr.into()));
165        self
166    }
167
168    pub fn until<F>(mut self, limit: F) -> EngineRuntime<E>
169    where
170        C: 'static,
171        F: Fn(GenerationView<C, T>) -> bool + 'static,
172    {
173        self.add_limit(limit);
174        self
175    }
176
177    pub fn limit(self, limit: impl Into<Limit>) -> EngineRuntime<E> {
178        let limit = limit.into();
179        match limit {
180            Limit::Generation(gens) => self.until_generation(gens),
181            Limit::Seconds(secs) => self.until_duration(secs),
182            Limit::Score(score) => self.until_score(score),
183            Limit::Convergence(window, epsilon, _) => self.until_convergence(window, epsilon),
184            Limit::Expr(expr) => self.until_expr(expr),
185            Limit::Combined(lims) => lims
186                .into_iter()
187                .fold(self, |runtime, limit| runtime.limit(limit)),
188            Limit::Fn => self,
189        }
190    }
191
192    pub fn take(self, count: usize) -> EngineRuntime<E> {
193        self.until_generation(count)
194    }
195
196    pub fn take_while<F>(self, predicate: F) -> EngineRuntime<E>
197    where
198        C: 'static,
199        F: Fn(GenerationView<C, T>) -> bool + 'static,
200    {
201        self.until(move |view: GenerationView<C, T>| -> bool { !predicate(view) })
202    }
203}
204
205/// Action based configuration methods for the `EngineRuntime` struct, allowing users to specify various
206/// actions to be executed during the evolutionary process.
207impl<C, T, E> EngineRuntime<E>
208where
209    E: Engine<Epoch = Generation<C, T>, Ctx = EvolutionContext<C, T>>,
210    C: Chromosome + Clone + 'static,
211    T: Clone + Send + Sync + 'static,
212{
213    pub fn logging(self) -> EngineRuntime<E> {
214        init_logging();
215        let stream = self.engine.context().event_stream();
216
217        stream.attatch(EngineLogger::<T>::new()).unwrap();
218        stream.attatch(HealthMonitor::<T>::default()).unwrap();
219        stream.subscribe(LoggingHandler);
220
221        self
222    }
223}
224
225impl<E> Iterator for EngineRuntime<E>
226where
227    E: Engine + 'static,
228{
229    type Item = E::Epoch;
230
231    fn next(&mut self) -> Option<Self::Item> {
232        if matches!(self.engine.state(), EngineState::Stopped) {
233            return None;
234        }
235
236        self.step().ok()?;
237        Some(self.engine.epoch())
238    }
239}