Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Time error projected kalman filter #1036

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 58 additions & 13 deletions crates/control/src/ball_filter.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use std::time::SystemTime;
use std::time::{Duration, SystemTime};

use color_eyre::Result;
use hardware::TimeInterface;
use nalgebra::{matrix, Matrix2, Matrix2x4, Matrix4, Matrix4x2};
use serde::{Deserialize, Serialize};

use context_attribute::context;
use coordinate_systems::{Ground, Pixel};
use filtering::kalman_filter::KalmanFilter;
use filtering::{
kalman_filter::KalmanFilter, time_error_projected_kalman_filter::TimeErrorProjectedKalmanFilter,
};
use framework::{AdditionalOutput, HistoricInput, MainOutput, PerceptionInput};
use geometry::circle::Circle;
use linear_algebra::Point2;
Expand All @@ -24,11 +27,14 @@ use types::{

#[derive(Deserialize, Serialize)]
pub struct BallFilter {
last_update: SystemTime,
hypotheses: Vec<Hypothesis>,
}

#[context]
pub struct CreationContext {}
pub struct CreationContext {
hardware_interface: HardwareInterface,
}

#[context]
pub struct CycleContext {
Expand Down Expand Up @@ -65,8 +71,9 @@ pub struct MainOutputs {
}

impl BallFilter {
pub fn new(_context: CreationContext) -> Result<Self> {
pub fn new(context: CreationContext<impl TimeInterface>) -> Result<Self> {
Ok(Self {
last_update: context.hardware_interface.get_now(),
hypotheses: Vec::new(),
})
}
Expand Down Expand Up @@ -97,6 +104,11 @@ impl BallFilter {
context: &CycleContext,
) -> Vec<Hypothesis> {
for (detection_time, balls) in measurements {
let cycle_time = detection_time
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let cycle_time = detection_time
let last_cycle_duration = detection_time

? What do you think?

.duration_since(self.last_update)
.expect("time ran backwards");
self.last_update = *detection_time;

let current_odometry_to_last_odometry = context
.current_odometry_to_last_odometry
.get(detection_time)
Expand All @@ -105,6 +117,7 @@ impl BallFilter {
context.ball_filter_configuration.velocity_decay_factor,
current_odometry_to_last_odometry.inverse(),
Matrix4::from_diagonal(&context.ball_filter_configuration.process_noise),
cycle_time,
);

let camera_matrices = context.historic_camera_matrices.get(detection_time);
Expand All @@ -126,6 +139,7 @@ impl BallFilter {
ball.position,
*detection_time,
context.ball_filter_configuration,
cycle_time,
);
}
}
Expand Down Expand Up @@ -259,9 +273,10 @@ impl BallFilter {
velocity_decay_factor: f32,
last_odometry_to_current_odometry: nalgebra::Isometry2<f32>,
process_noise: Matrix4<f32>,
cycle_time: Duration,
) {
let cycle_time = cycle_time.as_secs_f32();
for hypothesis in self.hypotheses.iter_mut() {
let cycle_time = 0.012;
let constant_velocity_prediction = matrix![
1.0, 0.0, cycle_time, 0.0;
0.0, 1.0, 0.0, cycle_time;
Expand All @@ -280,13 +295,15 @@ impl BallFilter {
let state_prediction = constant_velocity_prediction * state_rotation;
let control_input_model = Matrix4x2::identity();
let odometry_translation = last_odometry_to_current_odometry.translation.vector;
hypothesis.moving_state.predict(
KalmanFilter::predict(
&mut hypothesis.moving_state,
Comment on lines +298 to +299
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you'd rename the trait function as commented below, you can revert this explicit syntax back to the self.xxx

state_prediction,
control_input_model,
odometry_translation,
process_noise,
);
hypothesis.resting_state.predict(
KalmanFilter::predict(
&mut hypothesis.resting_state,
state_prediction,
control_input_model,
odometry_translation,
Expand All @@ -300,18 +317,42 @@ impl BallFilter {
detected_position: Point2<Ground>,
detection_time: SystemTime,
configuration: &BallFilterParameters,
cycle_time: Duration,
) {
hypothesis.moving_state.update(
let moving_state_derivative = nalgebra::vector![
hypothesis.moving_state.mean.z,
hypothesis.moving_state.mean.w,
(configuration.velocity_decay_factor - 1.) * hypothesis.moving_state.mean.z
/ cycle_time.as_secs_f32(),
(configuration.velocity_decay_factor - 1.) * hypothesis.moving_state.mean.w
/ cycle_time.as_secs_f32(),
];
TimeErrorProjectedKalmanFilter::update(
&mut hypothesis.moving_state,
Matrix2x4::identity(),
detected_position.inner.coords,
Matrix2::from_diagonal(&configuration.measurement_noise_moving)
* detected_position.coords().norm_squared(),
moving_state_derivative,
configuration.measurement_time_noise,
);
hypothesis.resting_state.update(

let resting_state_derivative = nalgebra::vector![
hypothesis.resting_state.mean.z,
hypothesis.resting_state.mean.w,
(configuration.velocity_decay_factor - 1.) * hypothesis.resting_state.mean.z
/ cycle_time.as_secs_f32(),
(configuration.velocity_decay_factor - 1.) * hypothesis.resting_state.mean.w
/ cycle_time.as_secs_f32(),
];
Comment on lines +340 to +347
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this repeated, if it is the same derivative as above?

TimeErrorProjectedKalmanFilter::update(
&mut hypothesis.resting_state,
Matrix2x4::identity(),
detected_position.inner.coords,
Matrix2::from_diagonal(&configuration.measurement_noise_resting)
Matrix2::from_diagonal(&configuration.measurement_noise_moving)
* detected_position.coords().norm_squared(),
resting_state_derivative,
configuration.measurement_time_noise,
);

if !hypothesis.is_resting(configuration) {
Expand All @@ -326,6 +367,7 @@ impl BallFilter {
detected_position: Point2<Ground>,
detection_time: SystemTime,
configuration: &BallFilterParameters,
cycle_time: Duration,
) {
let mut matching_hypotheses = self
.hypotheses
Expand All @@ -348,6 +390,7 @@ impl BallFilter {
detected_position,
detection_time,
configuration,
cycle_time,
)
});
}
Expand Down Expand Up @@ -406,7 +449,7 @@ impl BallFilter {
&& is_inside_field
});
let mut deduplicated_hypotheses = Vec::<Hypothesis>::new();
for hypothesis in retained_hypotheses {
for mut hypothesis in retained_hypotheses {
let hypothesis_in_merge_distance =
deduplicated_hypotheses
.iter_mut()
Expand All @@ -421,12 +464,14 @@ impl BallFilter {
match hypothesis_in_merge_distance {
Some(existing_hypothesis) => {
let update_state = hypothesis.selected_state(configuration);
existing_hypothesis.moving_state.update(
KalmanFilter::update(
&mut hypothesis.moving_state,
Matrix4::identity(),
update_state.mean,
update_state.covariance,
);
existing_hypothesis.resting_state.update(
KalmanFilter::update(
&mut existing_hypothesis.resting_state,
Matrix4::identity(),
update_state.mean,
update_state.covariance,
Expand Down
1 change: 1 addition & 0 deletions crates/filtering/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pub mod orientation_filtering;
pub mod pose_filter;
pub mod statistics;
pub mod tap_detector;
pub mod time_error_projected_kalman_filter;
66 changes: 66 additions & 0 deletions crates/filtering/src/time_error_projected_kalman_filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use nalgebra::{SMatrix, SVector};
use types::multivariate_normal_distribution::MultivariateNormalDistribution;

use crate::kalman_filter::KalmanFilter;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please link the blogpost somewhere here

pub trait TimeErrorProjectedKalmanFilter<const STATE_DIMENSION: usize> {
fn predict<const CONTROL_DIMENSION: usize>(
&mut self,
state_prediction: SMatrix<f32, STATE_DIMENSION, STATE_DIMENSION>,
control_input_model: SMatrix<f32, STATE_DIMENSION, CONTROL_DIMENSION>,
control: SVector<f32, CONTROL_DIMENSION>,
process_noise: SMatrix<f32, STATE_DIMENSION, STATE_DIMENSION>,
);
fn update<const MEASUREMENT_DIMENSION: usize>(
&mut self,
measurement_prediction: SMatrix<f32, MEASUREMENT_DIMENSION, STATE_DIMENSION>,
measurement: SVector<f32, MEASUREMENT_DIMENSION>,
measurement_noise: SMatrix<f32, MEASUREMENT_DIMENSION, MEASUREMENT_DIMENSION>,
state_derivative: SVector<f32, STATE_DIMENSION>,
measurement_time_noise: f32,
);
}
Comment on lines +6 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub trait TimeErrorProjectedKalmanFilter<const STATE_DIMENSION: usize> {
fn predict<const CONTROL_DIMENSION: usize>(
&mut self,
state_prediction: SMatrix<f32, STATE_DIMENSION, STATE_DIMENSION>,
control_input_model: SMatrix<f32, STATE_DIMENSION, CONTROL_DIMENSION>,
control: SVector<f32, CONTROL_DIMENSION>,
process_noise: SMatrix<f32, STATE_DIMENSION, STATE_DIMENSION>,
);
fn update<const MEASUREMENT_DIMENSION: usize>(
&mut self,
measurement_prediction: SMatrix<f32, MEASUREMENT_DIMENSION, STATE_DIMENSION>,
measurement: SVector<f32, MEASUREMENT_DIMENSION>,
measurement_noise: SMatrix<f32, MEASUREMENT_DIMENSION, MEASUREMENT_DIMENSION>,
state_derivative: SVector<f32, STATE_DIMENSION>,
measurement_time_noise: f32,
);
}
pub trait TimeErrorProjectedKalmanFilter<const STATE_DIMENSION: usize> {
fn update_with_timing_noise<const MEASUREMENT_DIMENSION: usize>(
&mut self,
measurement_prediction: SMatrix<f32, MEASUREMENT_DIMENSION, STATE_DIMENSION>,
measurement: SVector<f32, MEASUREMENT_DIMENSION>,
measurement_noise: SMatrix<f32, MEASUREMENT_DIMENSION, MEASUREMENT_DIMENSION>,
state_derivative: SVector<f32, STATE_DIMENSION>,
measurement_time_noise: f32,
);
}


impl<const STATE_DIMENSION: usize> TimeErrorProjectedKalmanFilter<STATE_DIMENSION>
for MultivariateNormalDistribution<STATE_DIMENSION>
{
fn predict<const CONTROL_DIMENSION: usize>(
&mut self,
state_prediction: SMatrix<f32, STATE_DIMENSION, STATE_DIMENSION>,
control_input_model: SMatrix<f32, STATE_DIMENSION, CONTROL_DIMENSION>,
control: SVector<f32, CONTROL_DIMENSION>,
process_noise: SMatrix<f32, STATE_DIMENSION, STATE_DIMENSION>,
) {
KalmanFilter::<STATE_DIMENSION>::predict(
self,
state_prediction,
control_input_model,
control,
process_noise,
)
}

fn update<const MEASUREMENT_DIMENSION: usize>(
&mut self,
measurement_prediction: SMatrix<f32, MEASUREMENT_DIMENSION, STATE_DIMENSION>,
measurement: SVector<f32, MEASUREMENT_DIMENSION>,
measurement_noise: SMatrix<f32, MEASUREMENT_DIMENSION, MEASUREMENT_DIMENSION>,
state_derivative: SVector<f32, STATE_DIMENSION>,
measurement_noise_variance: f32,
) {
let residual = measurement - measurement_prediction * self.mean;
let time_error_adjusted_covariance = self.covariance
+ measurement_noise_variance * state_derivative * state_derivative.transpose();
let residual_covariance = measurement_prediction
* time_error_adjusted_covariance
* measurement_prediction.transpose()
+ measurement_noise;
let kalman_gain = self.covariance
* measurement_prediction.transpose()
* residual_covariance
.try_inverse()
.expect("Residual covariance matrix is not invertible");
self.mean += kalman_gain * residual;
self.covariance -= kalman_gain * measurement_prediction * self.covariance;
Comment on lines +51 to +64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether we want to split up the Kalman update into smaller steps, to reuse the functionality between the normal and the time-invariant version #codeDuplication

}
}
1 change: 1 addition & 0 deletions crates/types/src/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ pub struct BallFilterParameters {
pub validity_discard_threshold: f32,
pub velocity_decay_factor: f32,
pub resting_ball_velocity_threshold: f32,
pub measurement_time_noise: f32,
}

#[derive(
Expand Down
1 change: 1 addition & 0 deletions etc/parameters/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@
"process_noise": [0.005, 0.005, 0.2, 0.2],
"measurement_noise_moving": [0.5, 2.0],
"measurement_noise_resting": [300.0, 500.0],
"measurement_time_noise": 40.0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did you tune this?

"initial_covariance": [0.5, 0.5, 0.5, 0.5],
"resting_ball_velocity_threshold": 0.25,
"visible_validity_exponential_decay_factor": 0.96,
Expand Down
2 changes: 1 addition & 1 deletion tools/twix/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "twix"
version = "0.5.0"
version = "0.5.1"
edition.workspace = true
license.workspace = true
homepage.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion tools/twix/src/twix_painter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ impl<Frame> TwixPainter<Frame> {
} else if b == 0.0 && a < c {
FRAC_PI_2
} else {
b.atan2(l1 - a)
(l1 - a).atan2(b)
};
self.ellipse(position, l1.sqrt(), l2.sqrt(), theta, stroke, fill_color)
}
Expand Down