1use 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#[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), }?;
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}