Skip to main content

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}