hyperopt_pruners/lib.rs
1//! # hyperopt-pruners
2//!
3//! Early-stopping policies for `hyperopt-rs`. A [`Pruner`] inspects a trial's
4//! intermediate reports against the rest of the study and decides whether
5//! continuing is worth it. Pruners are queried from the user's objective via
6//! [`hyperopt_core::TrialContext::should_prune`].
7//!
8//! - [`NopPruner`] — never prunes (the default; keeps the API shape consistent).
9//! - [`MedianPruner`] — prune when a trial is worse than the median of others
10//! at the same step.
11//! - [`SuccessiveHalvingPruner`] — ASHA-style rung promotion (the advanced option).
12
13use hyperopt_core::{Direction, Pruner, StudyState, Trial};
14
15mod median;
16mod successive_halving;
17
18pub use median::MedianPruner;
19pub use successive_halving::SuccessiveHalvingPruner;
20
21/// A no-op pruner: [`should_prune`](Pruner::should_prune) always returns
22/// `false`. Use it when early-stopping isn't wanted but the objective still
23/// calls `should_prune()` so the same code runs with and without pruning.
24#[derive(Debug, Clone, Copy, Default)]
25pub struct NopPruner;
26
27impl NopPruner {
28 pub fn new() -> Self {
29 NopPruner
30 }
31}
32
33impl Pruner for NopPruner {
34 fn should_prune(&self, _study_state: &StudyState, _trial: &Trial) -> bool {
35 false
36 }
37}
38
39/// Returns `true` if `value` is *worse* than `reference` under `direction`
40/// (used by pruners to decide whether a trial is lagging).
41pub(crate) fn is_worse(direction: Direction, value: f64, reference: f64) -> bool {
42 match direction {
43 Direction::Minimize => value > reference,
44 Direction::Maximize => value < reference,
45 }
46}
47
48/// Median of a slice of finite values (average of the two middle elements for
49/// even lengths). Returns `None` for an empty slice.
50pub(crate) fn median(values: &[f64]) -> Option<f64> {
51 if values.is_empty() {
52 return None;
53 }
54 let mut v: Vec<f64> = values.iter().copied().filter(|x| x.is_finite()).collect();
55 if v.is_empty() {
56 return None;
57 }
58 v.sort_by(|a, b| a.total_cmp(b));
59 let n = v.len();
60 if n % 2 == 1 {
61 Some(v[n / 2])
62 } else {
63 Some((v[n / 2 - 1] + v[n / 2]) / 2.0)
64 }
65}