Skip to main content

parlov_analysis/aggregation/
stop_rule.rs

1//! Early-stopping rule for endpoint-level aggregation.
2//!
3//! The stop rule evaluates the current accumulated evidence and remaining strategy potential to
4//! determine whether it is safe to halt dispatch before running every strategy. It fires early
5//! accept when the posterior cannot fall below the confirm threshold even in the worst case, and
6//! early reject when the posterior cannot rise above the likely threshold even in the best case.
7
8use parlov_core::StrategyMetaForStop;
9
10use super::evidence::EvidenceAccumulator;
11
12/// Decision returned by [`StopRule::evaluate`].
13#[derive(Debug, Clone, PartialEq)]
14pub enum StopDecision {
15    /// Continue dispatching strategies.
16    Continue,
17    /// Posterior is high enough that even worst-case remaining evidence cannot drop it below the
18    /// confirm threshold. Endpoint existence is confirmed.
19    EarlyAccept {
20        /// Posterior probability at the time of the decision.
21        posterior: f64,
22    },
23    /// Posterior is low enough that even best-case remaining evidence cannot raise it above the
24    /// likely threshold. Endpoint non-existence is confirmed.
25    EarlyReject {
26        /// Posterior probability at the time of the decision.
27        posterior: f64,
28    },
29}
30
31/// Logit of 0.80: the confirm threshold (≈ 1.3862944).
32const CONFIRM_THRESHOLD: f64 = 1.386_294_361_119_890_6;
33
34/// Logit of 0.60: the likely threshold (≈ 0.4054651).
35const LIKELY_THRESHOLD: f64 = 0.405_465_108_108_164_4;
36
37/// Evaluates evidence against early-stop thresholds.
38///
39/// `confirm_threshold` = logit(0.80); any accumulated log-odds that can no longer fall below this
40/// value triggers `EarlyAccept`. `likely_threshold` = logit(0.60); any accumulated log-odds that
41/// can no longer rise above this value triggers `EarlyReject`.
42pub struct StopRule {
43    confirm_threshold: f64,
44    likely_threshold: f64,
45}
46
47impl StopRule {
48    /// Confirm at logit(0.80), reject at logit(0.60).
49    #[must_use]
50    pub fn new() -> Self {
51        Self {
52            confirm_threshold: CONFIRM_THRESHOLD,
53            likely_threshold: LIKELY_THRESHOLD,
54        }
55    }
56
57    /// Evaluates whether to continue, accept early, or reject early.
58    ///
59    /// - If `log_odds - max_negative_remaining >= confirm_threshold` → `EarlyAccept`.
60    /// - Else if `log_odds + max_positive_remaining < likely_threshold` → `EarlyReject`.
61    /// - Otherwise → `Continue`.
62    #[must_use]
63    pub fn evaluate(
64        &self,
65        accumulator: &EvidenceAccumulator,
66        remaining: &[StrategyMetaForStop],
67    ) -> StopDecision {
68        let log_odds = accumulator.log_odds_current();
69        let max_neg = accumulator.max_negative_remaining(remaining);
70        let max_pos = accumulator.max_positive_remaining(remaining);
71
72        if log_odds - max_neg >= self.confirm_threshold {
73            return StopDecision::EarlyAccept {
74                posterior: accumulator.posterior_probability(),
75            };
76        }
77        if log_odds + max_pos < self.likely_threshold {
78            return StopDecision::EarlyReject {
79                posterior: accumulator.posterior_probability(),
80            };
81        }
82        StopDecision::Continue
83    }
84}
85
86impl Default for StopRule {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92#[cfg(test)]
93#[path = "stop_rule_tests.rs"]
94mod tests;