use blr_active::active_learning::orchestration::{
CalibrationSession, IterationOutcome, SessionConfig,
};
fn linear_feature(x: f64) -> Vec<f64> {
vec![1.0, x]
}
fn poly2_feature(x: f64) -> Vec<f64> {
vec![1.0, x, x * x]
}
fn make_session(target: f64, max_iter: usize) -> CalibrationSession {
let config = SessionConfig {
target_precision: target,
max_iterations: max_iter,
top_k: 3,
grid_resolution: 50,
input_range: (0.0, 10.0),
..Default::default()
};
CalibrationSession::new(config, linear_feature, 2)
}
#[test]
fn test_no_samples_returns_fit_error() {
let mut session = make_session(0.01, 10);
let outcome = session.next_iteration();
assert!(matches!(outcome, IterationOutcome::FitError(_)));
}
#[test]
fn test_max_iterations_terminates() {
let mut session = make_session(0.0001, 2); session.add_measurement(0.0, 0.0);
session.add_measurement(5.0, 10.0);
session.add_measurement(10.0, 20.0);
session.next_iteration();
session.add_measurement(2.5, 5.0);
session.next_iteration();
session.add_measurement(7.5, 15.0);
let outcome = session.next_iteration();
assert!(matches!(outcome, IterationOutcome::MaxIterationsReached));
}
#[test]
fn test_recommend_next_contains_valid_locations() {
let mut session = make_session(0.001, 20); session.add_measurement(0.0, 1.0);
session.add_measurement(10.0, 21.0);
let outcome = session.next_iteration();
match outcome {
IterationOutcome::RecommendNext(recs) => {
assert!(!recs.is_empty(), "should have recommendations");
for r in &recs {
assert!(r.input_value.is_finite());
assert!(r.expected_std >= 0.0);
assert!(r.rank >= 1);
}
}
IterationOutcome::PrecisionMet => {
}
other => panic!("unexpected outcome: {:?}", other),
}
}
#[test]
fn test_iteration_count_increments() {
let mut session = make_session(0.001, 20);
session.add_measurement(0.0, 0.0);
session.add_measurement(5.0, 10.0);
session.add_measurement(10.0, 20.0);
assert_eq!(session.iteration, 0);
session.next_iteration();
assert_eq!(session.iteration, 1);
session.add_measurement(2.5, 5.0);
session.next_iteration();
assert_eq!(session.iteration, 2);
}
#[test]
fn test_precision_history_recorded() {
let mut session = make_session(0.5, 20); session.add_measurement(0.0, 1.0);
session.add_measurement(5.0, 11.0);
session.add_measurement(10.0, 21.0);
session.next_iteration();
assert_eq!(session.precision_history.len(), 1);
let record = &session.precision_history[0];
assert_eq!(record.iteration, 0);
assert_eq!(record.sample_count, 3);
assert!(record.max_posterior_std.is_finite());
assert!(record.mean_posterior_std.is_finite());
assert!(record.percentile_95_std.is_finite());
}
#[test]
fn test_calibration_loop_can_reach_precision_goal() {
let mut session = make_session(0.5, 30);
let seed_xs = [0.0_f64, 5.0, 10.0];
for x in &seed_xs {
session.add_measurement(*x, 2.0 * x + 1.0);
}
let mut reached = false;
for _ in 0..30 {
let outcome = session.next_iteration();
match outcome {
IterationOutcome::PrecisionMet => {
reached = true;
break;
}
IterationOutcome::RecommendNext(recs) => {
for r in &recs {
session.add_measurement(r.input_value, 2.0 * r.input_value + 1.0);
}
}
IterationOutcome::NoiseFloorHit(_) => break,
IterationOutcome::MaxIterationsReached => break,
IterationOutcome::FitError(_) => break,
}
}
assert!(reached, "calibration loop should reach precision goal");
}
#[test]
fn test_history_export_json_structure() {
let mut session = make_session(0.01, 10);
session.add_measurement(0.0, 1.0);
session.add_measurement(5.0, 11.0);
session.add_measurement(8.0, 17.0);
let _ = session.next_iteration();
let json = session.export_history_json();
assert!(json.contains("\"iteration_count\""));
assert!(json.contains("\"sample_count\""));
assert!(json.contains("\"target_precision\""));
assert!(json.contains("\"precision_history\""));
assert!(json.contains("\"samples\""));
assert!(json.contains("\"p95_std\""));
assert!(json.contains("\"goal_met\""));
assert!(json.contains("\"raw_input\""));
assert!(json.contains("\"measured_output\""));
}
#[test]
fn test_history_export_after_multiple_iterations() {
let mut session = make_session(0.5, 10);
session.add_measurements(&[0.0, 5.0, 10.0], &[1.0, 11.0, 21.0]);
session.next_iteration();
session.add_measurements(&[2.5, 7.5], &[6.0, 16.0]);
session.next_iteration();
let json = session.export_history_json();
assert!(json.contains("\"iteration_count\":2"));
}
#[test]
fn test_add_measurements_batch() {
let mut session = make_session(0.01, 5);
session.add_measurements(&[1.0, 2.0, 3.0], &[3.0, 5.0, 7.0]);
assert_eq!(session.sample_count(), 3);
}
#[test]
fn test_noise_floor_hit_when_variance_plateaus() {
let config = SessionConfig {
target_precision: 0.0001, max_iterations: 30,
top_k: 3,
grid_resolution: 30,
input_range: (0.0, 1.0),
noise_floor_config: blr_active::active_learning::noise_floor::NoiseFloorConfig {
improvement_threshold: 0.5, window_size: 2,
},
..Default::default()
};
let mut session = CalibrationSession::new(config, poly2_feature, 3);
for i in 0..20 {
let x = i as f64 / 20.0;
session.add_measurement(x, x * x + 0.01 * ((i as f64 * 1.23).sin()));
}
let mut floor_hit = false;
for _ in 0..30 {
let outcome = session.next_iteration();
match outcome {
IterationOutcome::NoiseFloorHit(_) => {
floor_hit = true;
break;
}
IterationOutcome::RecommendNext(recs) => {
for r in &recs {
session.add_measurement(r.input_value, r.input_value * r.input_value);
}
}
IterationOutcome::PrecisionMet => break,
IterationOutcome::MaxIterationsReached => break,
IterationOutcome::FitError(_) => break,
}
}
assert!(
floor_hit,
"aggressive noise floor config should trigger floor hit"
);
}