use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SensorType {
Hall,
Capacitive,
Gas,
Rtd,
Thermistor,
Generic,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoiseEstimate {
pub point_estimate: f64,
pub lower_bound: f64,
pub upper_bound: f64,
pub confidence: String,
}
pub fn estimate_sensor_noise(beta: f64) -> Result<f64, String> {
if beta <= 0.0 {
return Err(format!(
"Model not fitted or diverged: beta = {beta:.6} (must be > 0)"
));
}
Ok(1.0 / beta.sqrt())
}
pub fn estimate_noise_with_confidence(
beta: f64,
n_samples: usize,
d_features: usize,
) -> NoiseEstimate {
let sigma = if beta > 0.0 {
1.0 / beta.sqrt()
} else {
f64::INFINITY
};
let dof = (n_samples.saturating_sub(d_features)).max(1) as f64;
let relative_uncertainty = 0.5 / dof.sqrt();
let lower = sigma * (1.0 - relative_uncertainty).max(0.0);
let upper = sigma * (1.0 + relative_uncertainty);
let confidence = if n_samples < 30 {
"preliminary".to_string()
} else if n_samples > 50 {
"stable".to_string()
} else {
"transitional".to_string()
};
NoiseEstimate {
point_estimate: sigma,
lower_bound: lower,
upper_bound: upper,
confidence,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_noise_extraction_known_beta() {
let sigma = estimate_sensor_noise(1.5625).unwrap();
assert!((sigma - 0.8).abs() < 1e-10, "expected 0.8, got {sigma}");
}
#[test]
fn test_noise_extraction_beta_one() {
let sigma = estimate_sensor_noise(1.0).unwrap();
assert!((sigma - 1.0).abs() < 1e-10, "β=1 → σ=1");
}
#[test]
fn test_noise_extraction_large_beta() {
let sigma = estimate_sensor_noise(10_000.0).unwrap();
assert!((sigma - 0.01).abs() < 1e-10, "β=10000 → σ=0.01");
}
#[test]
fn test_noise_extraction_small_beta() {
let sigma = estimate_sensor_noise(0.04).unwrap();
assert!((sigma - 5.0).abs() < 1e-10, "β=0.04 → σ=5");
}
#[test]
fn test_noise_extraction_negative_beta() {
let result = estimate_sensor_noise(-1.0);
assert!(result.is_err(), "negative beta must return Err");
}
#[test]
fn test_noise_extraction_zero_beta() {
let result = estimate_sensor_noise(0.0);
assert!(result.is_err(), "zero beta must return Err");
}
#[test]
fn test_noise_confidence_preliminary() {
let est = estimate_noise_with_confidence(1.5625, 10, 3);
assert_eq!(est.confidence, "preliminary", "n=10 → preliminary");
}
#[test]
fn test_noise_confidence_stable() {
let est = estimate_noise_with_confidence(1.5625, 100, 3);
assert_eq!(est.confidence, "stable", "n=100 → stable");
}
#[test]
fn test_noise_confidence_transitional() {
let est = estimate_noise_with_confidence(1.5625, 40, 3);
assert_eq!(est.confidence, "transitional", "n=40 → transitional");
}
#[test]
fn test_noise_confidence_bounds_ordering() {
let est = estimate_noise_with_confidence(1.5625, 100, 3);
assert!(
est.lower_bound < est.point_estimate,
"lower_bound must be < point_estimate"
);
assert!(
est.point_estimate < est.upper_bound,
"point_estimate must be < upper_bound"
);
}
#[test]
fn test_noise_confidence_point_matches_estimate() {
let est = estimate_noise_with_confidence(1.5625, 100, 3);
assert!(
(est.point_estimate - 0.8).abs() < 1e-10,
"point_estimate should equal estimate_sensor_noise output"
);
}
}