parlov-analysis 0.7.0

Analysis engine trait and signal detection for parlov.
Documentation
//! Early-stopping rule for endpoint-level aggregation.
//!
//! The stop rule evaluates the current accumulated evidence and remaining strategy potential to
//! determine whether it is safe to halt dispatch before running every strategy. It fires early
//! accept when the posterior cannot fall below the confirm threshold even in the worst case, and
//! early reject when the posterior cannot rise above the likely threshold even in the best case.

use parlov_core::StrategyMetaForStop;

use super::evidence::EvidenceAccumulator;

/// Decision returned by [`StopRule::evaluate`].
#[derive(Debug, Clone, PartialEq)]
pub enum StopDecision {
    /// Continue dispatching strategies.
    Continue,
    /// Posterior is high enough that even worst-case remaining evidence cannot drop it below the
    /// confirm threshold. Endpoint existence is confirmed.
    EarlyAccept {
        /// Posterior probability at the time of the decision.
        posterior: f64,
    },
    /// Posterior is low enough that even best-case remaining evidence cannot raise it above the
    /// likely threshold. Endpoint non-existence is confirmed.
    EarlyReject {
        /// Posterior probability at the time of the decision.
        posterior: f64,
    },
}

/// Logit of 0.80: the confirm threshold (≈ 1.3862944).
const CONFIRM_THRESHOLD: f64 = 1.386_294_361_119_890_6;

/// Logit of 0.60: the likely threshold (≈ 0.4054651).
const LIKELY_THRESHOLD: f64 = 0.405_465_108_108_164_4;

/// Evaluates evidence against early-stop thresholds.
///
/// `confirm_threshold` = logit(0.80); any accumulated log-odds that can no longer fall below this
/// value triggers `EarlyAccept`. `likely_threshold` = logit(0.60); any accumulated log-odds that
/// can no longer rise above this value triggers `EarlyReject`.
pub struct StopRule {
    confirm_threshold: f64,
    likely_threshold: f64,
}

impl StopRule {
    /// Confirm at logit(0.80), reject at logit(0.60).
    #[must_use]
    pub fn new() -> Self {
        Self {
            confirm_threshold: CONFIRM_THRESHOLD,
            likely_threshold: LIKELY_THRESHOLD,
        }
    }

    /// Evaluates whether to continue, accept early, or reject early.
    ///
    /// - If `log_odds - max_negative_remaining >= confirm_threshold` → `EarlyAccept`.
    /// - Else if `log_odds + max_positive_remaining < likely_threshold` → `EarlyReject`.
    /// - Otherwise → `Continue`.
    #[must_use]
    pub fn evaluate(
        &self,
        accumulator: &EvidenceAccumulator,
        remaining: &[StrategyMetaForStop],
    ) -> StopDecision {
        let log_odds = accumulator.log_odds_current();
        let max_neg = accumulator.max_negative_remaining(remaining);
        let max_pos = accumulator.max_positive_remaining(remaining);

        if log_odds - max_neg >= self.confirm_threshold {
            return StopDecision::EarlyAccept {
                posterior: accumulator.posterior_probability(),
            };
        }
        if log_odds + max_pos < self.likely_threshold {
            return StopDecision::EarlyReject {
                posterior: accumulator.posterior_probability(),
            };
        }
        StopDecision::Continue
    }
}

impl Default for StopRule {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
#[path = "stop_rule_tests.rs"]
mod tests;