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;