#[derive(Debug, Clone)]
pub struct NoiseFloorDiagnostic {
pub sample_counts: Vec<usize>,
pub max_stds: Vec<f64>,
pub predicted_noise_floor: f64,
pub confidence: f64,
pub likely_at_floor: bool,
}
#[derive(Debug, Clone)]
pub struct NoiseFloorConfig {
pub improvement_threshold: f64,
pub window_size: usize,
}
impl Default for NoiseFloorConfig {
fn default() -> Self {
Self {
improvement_threshold: 0.01,
window_size: 3,
}
}
}
pub fn detect_noise_floor(
iteration_history: &[(usize, f64)],
config: &NoiseFloorConfig,
) -> NoiseFloorDiagnostic {
let sample_counts: Vec<usize> = iteration_history.iter().map(|&(n, _)| n).collect();
let max_stds: Vec<f64> = iteration_history.iter().map(|&(_, s)| s).collect();
if iteration_history.len() < config.window_size + 1 {
return NoiseFloorDiagnostic {
sample_counts,
max_stds,
predicted_noise_floor: iteration_history.last().map(|&(_, s)| s).unwrap_or(0.0),
confidence: 0.0,
likely_at_floor: false,
};
}
let recent = &iteration_history[iteration_history.len() - config.window_size..];
let relative_changes: Vec<f64> = recent
.windows(2)
.map(|w| {
let prev = w[0].1;
let curr = w[1].1;
if prev.abs() < 1e-15 {
0.0 } else {
(prev - curr).abs() / prev.abs()
}
})
.collect();
let n_plateau_steps = relative_changes
.iter()
.filter(|&&c| c < config.improvement_threshold)
.count();
let likely_at_floor = n_plateau_steps == relative_changes.len();
let required = config.window_size.saturating_sub(1).max(1);
let confidence = (n_plateau_steps as f64 / required as f64).min(1.0);
let predicted_noise_floor = iteration_history.last().map(|&(_, s)| s).unwrap_or(0.0);
NoiseFloorDiagnostic {
sample_counts,
max_stds,
predicted_noise_floor,
confidence,
likely_at_floor,
}
}
pub fn detect_noise_floor_default(iteration_history: &[(usize, f64)]) -> NoiseFloorDiagnostic {
detect_noise_floor(iteration_history, &NoiseFloorConfig::default())
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> NoiseFloorConfig {
NoiseFloorConfig::default()
}
#[test]
fn test_noise_floor_synthetic_plateau() {
let history: Vec<(usize, f64)> = vec![
(5, 0.100),
(10, 0.060),
(15, 0.040),
(20, 0.025),
(25, 0.0201),
(30, 0.0200),
(35, 0.0200), (40, 0.01999), ];
let diag = detect_noise_floor(&history, &cfg());
assert!(diag.likely_at_floor, "should detect plateau");
assert!((diag.predicted_noise_floor - 0.01999).abs() < 1e-4);
}
#[test]
fn test_noise_floor_early_data() {
let history = vec![(5, 0.1), (10, 0.08)];
let diag = detect_noise_floor(&history, &cfg());
assert!(!diag.likely_at_floor);
assert_eq!(diag.confidence, 0.0);
}
#[test]
fn test_noise_floor_threshold_tuning() {
let history = vec![
(5, 0.2),
(10, 0.1),
(15, 0.0502), (20, 0.0501), (25, 0.05009), (30, 0.05008), ];
let diag = detect_noise_floor(&history, &cfg());
assert!(
diag.likely_at_floor,
"3 consecutive plateau steps should trigger floor"
);
}
#[test]
fn test_false_positive_prevention() {
let history: Vec<(usize, f64)> = (0..10_usize)
.map(|i| {
let n = (i + 1) * 5;
let s = 0.1 * 0.98_f64.powi(i as i32);
(n, s)
})
.collect();
let diag = detect_noise_floor(&history, &cfg());
assert!(
!diag.likely_at_floor,
"steady 2% improvement should not be floor"
);
}
#[test]
fn test_all_zeros_history() {
let history = vec![(5, 0.0), (10, 0.0), (15, 0.0), (20, 0.0)];
let diag = detect_noise_floor(&history, &cfg());
assert!(diag.likely_at_floor);
}
#[test]
fn test_monotonic_decrease_not_floor() {
let history = vec![(10, 0.1), (20, 0.05), (30, 0.025), (40, 0.0125)];
let diag = detect_noise_floor(&history, &cfg());
assert!(
!diag.likely_at_floor,
"50% improvement steps should not trigger floor"
);
}
#[test]
fn test_custom_config() {
let custom_cfg = NoiseFloorConfig {
improvement_threshold: 0.05,
window_size: 2,
};
let history = vec![
(10, 0.1),
(20, 0.096), (30, 0.094), ];
let diag = detect_noise_floor(&history, &custom_cfg);
assert!(
diag.likely_at_floor,
"both steps < 5% → floor with custom config"
);
}
}