fn sqrt_f32(x: f32) -> f32 {
if x <= 0.0 {
return 0.0;
}
let mut guess = x;
for _ in 0..6 {
guess = 0.5 * (guess + x / guess);
}
guess
}
#[derive(Debug, Clone)]
pub struct KalmanFilter1D {
pub x: f32,
pub p: f32,
pub q: f32,
pub r: f32,
}
impl KalmanFilter1D {
#[must_use]
pub const fn new(
initial_estimate: f32,
initial_error: f32,
process_noise: f32,
measurement_noise: f32,
) -> Self {
Self {
x: initial_estimate,
p: initial_error,
q: process_noise,
r: measurement_noise,
}
}
pub fn predict(&mut self) {
self.p += self.q;
}
pub fn update(&mut self, measurement: f32) {
let k = self.p / (self.p + self.r);
self.x += k * (measurement - self.x);
self.p *= 1.0 - k;
}
pub fn filter(&mut self, measurement: f32) -> f32 {
self.predict();
self.update(measurement);
self.x
}
#[must_use]
pub const fn estimate(&self) -> f32 {
self.x
}
#[must_use]
pub const fn error(&self) -> f32 {
self.p
}
}
#[derive(Debug, Clone)]
pub struct KalmanFilter2D {
pub position: f32,
pub velocity: f32,
p00: f32,
p01: f32,
p11: f32,
q: f32,
r: f32,
dt: f32,
}
impl KalmanFilter2D {
#[must_use]
pub const fn new(dt: f32, process_noise: f32, measurement_noise: f32) -> Self {
Self {
position: 0.0,
velocity: 0.0,
p00: 1.0,
p01: 0.0,
p11: 1.0,
q: process_noise,
r: measurement_noise,
dt,
}
}
pub fn predict(&mut self) {
self.position += self.velocity * self.dt;
let dt = self.dt;
#[allow(clippy::suboptimal_flops)]
let new_p00 = dt * (dt * self.p11 + self.p01 + self.p01) + self.p00 + self.q;
#[allow(clippy::suboptimal_flops)]
let new_p01 = dt * self.p11 + self.p01;
let new_p11 = self.p11 + self.q;
self.p00 = new_p00;
self.p01 = new_p01;
self.p11 = new_p11;
}
pub fn update(&mut self, measured_position: f32) {
let y = measured_position - self.position;
let s = self.p00 + self.r;
let s_inv = 1.0 / s;
let k0 = self.p00 * s_inv;
let k1 = self.p01 * s_inv;
self.position += k0 * y;
self.velocity += k1 * y;
let new_p00 = self.p00 - k0 * self.p00;
let new_p01 = self.p01 - k0 * self.p01;
let new_p11 = self.p11 - k1 * self.p01;
self.p00 = new_p00;
self.p01 = new_p01;
self.p11 = new_p11;
}
pub fn filter(&mut self, measured_position: f32) -> (f32, f32) {
self.predict();
self.update(measured_position);
(self.position, self.velocity)
}
}
#[derive(Debug, Clone)]
pub struct FusionConfig {
pub process_noise: f32,
pub default_measurement_noise: f32,
pub outlier_threshold: f32,
}
impl Default for FusionConfig {
fn default() -> Self {
Self {
process_noise: 0.01,
default_measurement_noise: 1.0,
outlier_threshold: 3.0,
}
}
}
#[derive(Debug, Clone)]
pub struct SensorInput {
pub value: f32,
pub noise: f32,
}
#[derive(Debug)]
pub struct FusedSensor {
filter: KalmanFilter1D,
config: FusionConfig,
fusion_count: u64,
rejected_count: u64,
}
impl FusedSensor {
#[must_use]
pub const fn new(config: FusionConfig) -> Self {
let filter = KalmanFilter1D::new(
0.0,
100.0,
config.process_noise,
config.default_measurement_noise,
);
Self {
filter,
config,
fusion_count: 0,
rejected_count: 0,
}
}
pub fn fuse(&mut self, inputs: &[SensorInput]) -> f32 {
if inputs.is_empty() {
return self.filter.estimate();
}
self.filter.predict();
let mut total_weight = 0.0f32;
let mut weighted_sum = 0.0f32;
let mut valid_count = 0u32;
for input in inputs {
let innovation = (input.value - self.filter.x).abs();
let std_dev = sqrt_f32(self.filter.p + input.noise);
if innovation > self.config.outlier_threshold * std_dev {
self.rejected_count += 1;
continue;
}
let weight = 1.0 / input.noise;
total_weight += weight;
weighted_sum += weight * input.value;
valid_count += 1;
}
if valid_count > 0 && total_weight > 0.0 {
let fused_measurement = weighted_sum / total_weight;
let fused_noise = 1.0 / total_weight;
self.filter.r = fused_noise;
self.filter.update(fused_measurement);
}
self.fusion_count += 1;
self.filter.estimate()
}
#[must_use]
pub const fn estimate(&self) -> f32 {
self.filter.estimate()
}
#[must_use]
pub const fn fusion_count(&self) -> u64 {
self.fusion_count
}
#[must_use]
pub const fn rejected_count(&self) -> u64 {
self.rejected_count
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kalman_1d_constant() {
let mut kf = KalmanFilter1D::new(0.0, 1.0, 0.01, 0.1);
for _ in 0..50 {
kf.filter(10.0);
}
assert!((kf.estimate() - 10.0).abs() < 0.5);
}
#[test]
fn kalman_1d_noisy() {
let mut kf = KalmanFilter1D::new(0.0, 1.0, 0.01, 1.0);
let measurements = [10.5, 9.8, 10.2, 10.1, 9.9, 10.3, 10.0];
for &m in &measurements {
kf.filter(m);
}
assert!((kf.estimate() - 10.0).abs() < 2.0);
}
#[test]
fn kalman_1d_error_decreases() {
let mut kf = KalmanFilter1D::new(0.0, 10.0, 0.01, 0.1);
let initial_error = kf.error();
for _ in 0..10 {
kf.filter(5.0);
}
assert!(kf.error() < initial_error);
}
#[test]
fn kalman_2d_constant_velocity() {
let mut kf = KalmanFilter2D::new(1.0, 0.01, 0.1);
for i in 0..20 {
kf.filter(i as f32);
}
assert!((kf.velocity - 1.0).abs() < 0.5);
}
#[test]
fn kalman_2d_predict_only() {
let mut kf = KalmanFilter2D::new(0.1, 0.01, 0.1);
kf.position = 10.0;
kf.velocity = 2.0;
kf.predict();
assert!((kf.position - 10.2).abs() < 0.01);
}
#[test]
fn fusion_config_default() {
let config = FusionConfig::default();
assert!((config.process_noise - 0.01).abs() < f32::EPSILON);
assert!((config.outlier_threshold - 3.0).abs() < f32::EPSILON);
}
#[test]
fn fused_sensor_single_input() {
let mut sensor = FusedSensor::new(FusionConfig::default());
let inputs = [SensorInput {
value: 25.0,
noise: 0.1,
}];
for _ in 0..20 {
sensor.fuse(&inputs);
}
assert!((sensor.estimate() - 25.0).abs() < 1.0);
}
#[test]
fn fused_sensor_multiple_inputs() {
let mut sensor = FusedSensor::new(FusionConfig::default());
let inputs = [
SensorInput {
value: 25.0,
noise: 0.1,
},
SensorInput {
value: 25.5,
noise: 0.5,
},
SensorInput {
value: 24.8,
noise: 0.2,
},
];
for _ in 0..30 {
sensor.fuse(&inputs);
}
assert!((sensor.estimate() - 25.0).abs() < 1.0);
}
#[test]
fn fused_sensor_outlier_rejection() {
let mut sensor = FusedSensor::new(FusionConfig {
outlier_threshold: 2.0,
..FusionConfig::default()
});
for _ in 0..20 {
sensor.fuse(&[SensorInput {
value: 10.0,
noise: 0.1,
}]);
}
sensor.fuse(&[SensorInput {
value: 1000.0,
noise: 0.1,
}]);
assert!(sensor.rejected_count() > 0);
assert!((sensor.estimate() - 10.0).abs() < 5.0);
}
#[test]
fn fused_sensor_empty_input() {
let mut sensor = FusedSensor::new(FusionConfig::default());
sensor.fuse(&[SensorInput {
value: 10.0,
noise: 0.1,
}]);
let est = sensor.fuse(&[]);
assert!(est.is_finite());
}
#[test]
fn fused_sensor_count() {
let mut sensor = FusedSensor::new(FusionConfig::default());
sensor.fuse(&[SensorInput {
value: 1.0,
noise: 0.1,
}]);
sensor.fuse(&[SensorInput {
value: 2.0,
noise: 0.1,
}]);
assert_eq!(sensor.fusion_count(), 2);
}
#[test]
fn kalman_1d_predict_only() {
let mut kf = KalmanFilter1D::new(5.0, 1.0, 0.1, 0.5);
let before = kf.error();
kf.predict();
assert!(kf.error() > before);
assert!((kf.estimate() - 5.0).abs() < f32::EPSILON);
}
}