blr-active 0.1.0

Active learning orchestration for Bayesian Linear Regression with Automatic Relevance Determination
Documentation
//! Quick-start example for `blr-active`.
//!
//! Demonstrates the full 4-phase calibration workflow:
//! 1. Simulate initial sensor measurements
//! 2. Characterise sensor noise with `NoiseCalibrationSession`
//! 3. Choose a precision tier
//! 4. Run the active learning loop until precision is met
//!
//! Run from the repository root:
//!
//! ```bash
//! cargo run --example quick_start -p blr-active
//! ```

use blr_active::{
    CalibrationSession, IterationOutcome, NoiseCalibrationSession, PrecisionLevel, SessionConfig,
};
use blr_core::noise_estimation::{NoiseEstimate, SensorType};

// ── Simulated sensor: y = 0.5·x + 0.1·x² + Gaussian noise (σ=0.02) ──────────

fn true_sensor(x: f64) -> f64 {
    0.5 * x + 0.1 * x * x
}

fn noisy_sensor(x: f64, noise_std: f64, rng_state: &mut u64) -> f64 {
    *rng_state = rng_state
        .wrapping_mul(6364136223846793005)
        .wrapping_add(1442695040888963407);
    let noise = noise_std * (*rng_state as i64 as f64) / (i64::MAX as f64);
    true_sensor(x) + noise
}

fn main() {
    let sigma_true = 0.02_f64;
    let mut rng: u64 = 0xcafe_dead_beef_1234;

    println!("=== blr-active Quick Start ===\n");

    // ── Phase 0: Noise characterisation ────────────────────────────────────
    //
    // Collect 15 quick measurements to estimate the sensor noise floor.
    // In production, this is done with `blr-core::fit` and then
    // `noise_estimation::estimate_noise_with_confidence`.
    //
    // Here we construct a NoiseEstimate directly with known values to keep
    // the example self-contained without a full initial fit.
    let noise_est = NoiseEstimate {
        point_estimate: sigma_true,
        lower_bound: sigma_true * 0.9,
        upper_bound: sigma_true * 1.1,
        confidence: "stable".to_string(),
    };

    println!("  Estimated noise std:  {:.4}", noise_est.point_estimate);
    println!("  Confidence:           {}", noise_est.confidence);

    let mut noise_session = NoiseCalibrationSession::new(SensorType::Hall, noise_est);

    println!("\n  Available precision tiers:");
    for tier in noise_session.precision_tiers.iter() {
        println!(
            "    {:?}  target std = {:.4}  (~{} samples est.)",
            tier.level, tier.absolute_tolerance, tier.estimated_samples
        );
    }

    noise_session
        .set_goal(PrecisionLevel::Moderate)
        .expect("Moderate should be feasible for this sensor");
    let al_config = noise_session
        .to_active_learning_config()
        .expect("should produce valid AL config after goal is set");
    println!(
        "\n  Selected: Moderate — target std = {:.4}",
        al_config.target_std
    );

    // ── Phase 1-3: Active learning loop ────────────────────────────────────
    println!("\nPhase 1–3: Active learning");

    // Feature function: polynomial [1, x, x²] for a smooth nonlinear sensor
    let feature_fn = |x: f64| vec![1.0_f64, x, x * x];
    let feature_dim = 3;

    let session_config = SessionConfig {
        target_precision: al_config.target_std,
        max_iterations: 40,
        top_k: 1,
        input_range: (0.0, 3.0),
        grid_resolution: 50,
        ..SessionConfig::default()
    };
    let mut session = CalibrationSession::new(session_config, feature_fn, feature_dim);

    // Seed with 8 initial measurements uniformly distributed
    println!("  Seeding with 8 initial measurements...");
    for i in 0..8 {
        let x = 0.0 + 3.0 * (i as f64) / 7.0;
        let y = noisy_sensor(x, sigma_true, &mut rng);
        session.add_measurement(x, y);
    }

    let mut final_std = f64::INFINITY;
    loop {
        match session.next_iteration() {
            IterationOutcome::RecommendNext(recs) => {
                let x = recs[0].input_value;
                let y = noisy_sensor(x, sigma_true, &mut rng);
                println!(
                    "  Iter {:2}  N={:3}  measure at x={:.3}  y={:.4}",
                    session.iteration,
                    session.sample_count(),
                    x,
                    y
                );
                session.add_measurement(x, y);
            }
            IterationOutcome::PrecisionMet => {
                if let Some(last) = session.precision_history.last() {
                    final_std = last.percentile_95_std;
                }
                println!("\n  ✓ Precision goal met!");
                break;
            }
            IterationOutcome::NoiseFloorHit(floor) => {
                println!("\n  ✗ Noise floor hit at {:.4} — stopping.", floor);
                break;
            }
            IterationOutcome::MaxIterationsReached => {
                println!("\n  ✗ Max iterations reached.");
                break;
            }
            IterationOutcome::FitError(e) => {
                println!("  Fit error (need more data first): {}", e);
            }
        }
    }

    // ── Phase 3: Summary ───────────────────────────────────────────────────
    println!("\nPhase 3: Summary");
    println!("  Total measurements:   {}", session.sample_count());
    println!("  Iterations:           {}", session.iteration);
    if final_std.is_finite() {
        println!(
            "  Final precision std:  {:.4}  (target: {:.4})",
            final_std, al_config.target_std
        );
    }

    println!("\nDone. See `cargo run --example multi_sensor_workflow` for multi-sensor demo.");
}