blr-active 0.1.0

Active learning orchestration for Bayesian Linear Regression with Automatic Relevance Determination
Documentation
//! Goal feasibility assessment for noise-aware precision targeting.
//!
//! Before collecting data, users should understand whether their precision goal
//! is realistic given the sensor's inherent noise floor. This module evaluates
//! the feasibility of a chosen [`PrecisionLevel`] relative to σ_noise.
//!
//! ## Feasibility Classification
//!
//! | Condition            | Classification | Interpretation                          |
//! |----------------------|----------------|-----------------------------------------|
//! | factor > 1.5×        | Achievable     | Safe margin above noise; go ahead       |
//! | 0.9× ≤ factor ≤ 1.5× | Marginal       | Risky; close to ceiling; may not meet   |
//! | factor < 0.9×        | Unachievable   | Below noise floor; impossible           |
//!
//! **Note:** For the current tier set {Low=5×, Moderate=3×, High=1.5×, Max=1×},
//! the `Max` tier (factor=1.0) falls in the `Marginal` range and `CalibrationSession`
//! will warn but still allow the user to proceed. No tier currently produces
//! `Unachievable` — that classification applies when a user supplies a target
//! below σ_noise (possible via custom targets in Phase 2).
//!
//! # Example
//!
//! ```rust
//! use blr_active::goal_assessment::{assess_goal_feasibility, GoalFeasibility};
//! use blr_active::precision_tiers::PrecisionLevel;
//!
//! // Moderate tier (3×) is comfortably achievable
//! let goal = assess_goal_feasibility(PrecisionLevel::Moderate, 0.008);
//! assert_eq!(goal.feasibility, GoalFeasibility::Achievable);
//! assert!(goal.message.contains("achievable"));
//!
//! // Max tier (1×) is marginal — at the noise ceiling
//! let goal = assess_goal_feasibility(PrecisionLevel::Max, 0.008);
//! assert_eq!(goal.feasibility, GoalFeasibility::Marginal);
//! ```
//!
//! # Logic Fix (Decision 2 from task_04.md)
//!
//! The original plan (plan_04.md) incorrectly used `factor > 1.0` for Achievable,
//! which made all four tiers Achievable. The corrected thresholds above use
//! `factor > 1.5` so that High (1.5×) and Max (1.0×) are classified as potentially
//! risky, matching the practical difficulty of reaching those precision levels.

use crate::precision_tiers::PrecisionLevel;
use serde::{Deserialize, Serialize};

// ─── GoalFeasibility ─────────────────────────────────────────────────────────

/// Feasibility classification for a chosen calibration goal.
///
/// See module-level documentation for threshold definitions.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GoalFeasibility {
    /// Goal is comfortably above the noise floor (factor > 1.5×). Recommended.
    Achievable,
    /// Goal is close to the noise floor (0.9× ≤ factor ≤ 1.5×).
    /// Calibration may converge but is risky; warn the user.
    Marginal,
    /// Goal is at or below the noise floor (factor < 0.9×).
    /// Physically impossible regardless of sample size.
    Unachievable,
}

// ─── CalibrationGoal ─────────────────────────────────────────────────────────

/// Result of assessing a user's chosen precision goal against the noise floor.
///
/// Used by `CalibrationSession::set_goal()` to decide whether to accept or
/// reject the user's tier selection.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalibrationGoal {
    /// The precision tier the user selected.
    pub precision_level: PrecisionLevel,
    /// Absolute target std in signal units (σ_noise × factor).
    pub target_std: f64,
    /// Estimated sensor noise floor σ_noise used for assessment.
    pub estimated_noise: f64,
    /// Feasibility verdict.
    pub feasibility: GoalFeasibility,
    /// User-facing explanation of the verdict.
    pub message: String,
}

// ─── assess_goal_feasibility ─────────────────────────────────────────────────

/// Assess whether a [`PrecisionLevel`] goal is achievable given σ_noise.
///
/// Computes `target_std = σ_noise × factor` and classifies feasibility using
/// the corrected thresholds (Decision 2, task_04.md):
/// - **Achievable** if `factor > 1.5`
/// - **Marginal**   if `0.9 ≤ factor ≤ 1.5`
/// - **Unachievable** if `factor < 0.9`
///
/// # Arguments
///
/// * `level`      — The user's chosen precision tier.
/// * `sigma_noise` — Estimated sensor noise std (σ = 1/√β).
///
/// # Example
///
/// ```rust
/// use blr_active::goal_assessment::{assess_goal_feasibility, GoalFeasibility};
/// use blr_active::precision_tiers::PrecisionLevel;
///
/// let goal = assess_goal_feasibility(PrecisionLevel::Moderate, 0.008);
/// assert_eq!(goal.feasibility, GoalFeasibility::Achievable);
/// assert!((goal.target_std - 0.024).abs() < 1e-10);
/// ```
pub fn assess_goal_feasibility(level: PrecisionLevel, sigma_noise: f64) -> CalibrationGoal {
    let factor = level.factor();
    let target_std = sigma_noise * factor;
    let estimated_samples = level.estimated_samples();

    let (feasibility, message) = if factor > 1.5 {
        (
            GoalFeasibility::Achievable,
            format!(
                "Goal ±{:.4} ({:.1}× noise) is achievable. \
                 Estimated ~{} samples needed.",
                target_std, factor, estimated_samples
            ),
        )
    } else if factor >= 0.9 {
        (
            GoalFeasibility::Marginal,
            format!(
                "Goal ±{:.4} ({:.1}× noise) is close to the sensor noise floor \{:.4}). Calibration may not converge. \
                 Estimated ~{} samples; consider a less demanding tier.",
                target_std, factor, sigma_noise, estimated_samples
            ),
        )
    } else {
        (
            GoalFeasibility::Unachievable,
            format!(
                "Goal ±{:.4} ({:.1}× noise) is below the sensor noise floor \{:.4}). This precision is physically impossible — \
                 choose a less demanding tier.",
                target_std, factor, sigma_noise
            ),
        )
    };

    CalibrationGoal {
        precision_level: level,
        target_std,
        estimated_noise: sigma_noise,
        feasibility,
        message,
    }
}

// ─── Tests ────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    const SIGMA: f64 = 0.008;

    #[test]
    fn test_feasibility_low_is_achievable() {
        let goal = assess_goal_feasibility(PrecisionLevel::Low, SIGMA);
        assert_eq!(
            goal.feasibility,
            GoalFeasibility::Achievable,
            "Low tier (5.0×) must be Achievable"
        );
    }

    #[test]
    fn test_feasibility_moderate_is_achievable() {
        let goal = assess_goal_feasibility(PrecisionLevel::Moderate, SIGMA);
        assert_eq!(
            goal.feasibility,
            GoalFeasibility::Achievable,
            "Moderate tier (3.0×) must be Achievable"
        );
    }

    #[test]
    fn test_feasibility_high_is_marginal() {
        // High tier factor = 1.5; boundary case (1.5 is NOT > 1.5)
        let goal = assess_goal_feasibility(PrecisionLevel::High, SIGMA);
        assert_eq!(
            goal.feasibility,
            GoalFeasibility::Marginal,
            "High tier (1.5×) must be Marginal (at the 1.5 boundary)"
        );
    }

    #[test]
    fn test_feasibility_max_is_marginal() {
        let goal = assess_goal_feasibility(PrecisionLevel::Max, SIGMA);
        assert_eq!(
            goal.feasibility,
            GoalFeasibility::Marginal,
            "Max tier (1.0×) must be Marginal (< 1.5×, ≥ 0.9×)"
        );
    }

    #[test]
    fn test_feasibility_target_std_correct() {
        let goal = assess_goal_feasibility(PrecisionLevel::Moderate, SIGMA);
        assert!(
            (goal.target_std - 0.024).abs() < 1e-10,
            "Moderate target = 3.0 × 0.008 = 0.024"
        );
    }

    #[test]
    fn test_feasibility_achievable_message_contains_keyword() {
        let goal = assess_goal_feasibility(PrecisionLevel::Low, SIGMA);
        assert!(
            goal.message.contains("achievable"),
            "Achievable message must contain 'achievable'"
        );
    }

    #[test]
    fn test_feasibility_marginal_message_contains_noise_floor() {
        let goal = assess_goal_feasibility(PrecisionLevel::Max, SIGMA);
        assert!(
            goal.message.contains("noise floor"),
            "Marginal message must mention 'noise floor'"
        );
    }

    #[test]
    fn test_feasibility_message_contains_sample_estimate() {
        let goal = assess_goal_feasibility(PrecisionLevel::Moderate, SIGMA);
        assert!(
            goal.message.contains("25"),
            "Message must include estimated sample count (~25 for Moderate)"
        );
    }

    #[test]
    fn test_feasibility_estimated_noise_stored() {
        let goal = assess_goal_feasibility(PrecisionLevel::Moderate, SIGMA);
        assert!(
            (goal.estimated_noise - SIGMA).abs() < 1e-15,
            "estimated_noise must equal sigma_noise input"
        );
    }
}