use core::f32::consts::PI;
use nalgebra::{
ComplexField, Matrix2, Matrix3, Matrix3x2, RealField, UnitQuaternion, Vector2, Vector3,
};
use crate::frames::Body;
use crate::init::level_from_accel;
use crate::math::{exp_quat, wrap_pi};
use crate::observation::heading::heading_of;
use crate::propagate::ImuSample;
use crate::units::{Acceleration, Timestamp};
pub(crate) const MODELS: usize = 5;
const TILT_GAIN: f32 = 0.2;
const BIAS_GAIN: f32 = 0.04;
const BIAS_LIMIT: f32 = 0.05;
const BIAS_RATE_MAX: f32 = 0.174_532_93;
const ACCEL_FILTER_RATIO: f32 = 10.0;
const ACCELERATION_GAP: f32 = 2.5;
const ACCEL_FILTER_TAU: f32 = 1.0 / (ACCEL_FILTER_RATIO * TILT_GAIN);
const SIGMA_MAX: f32 = 0.5;
const GYRO_NOISE: f32 = 0.01;
const ACCEL_NOISE: f32 = 0.2;
const VARIANCE_MIN: f32 = 1.0e-6;
const WEIGHT_MIN: f32 = 1.0e-5;
const NIS_MAX: f32 = 25.0;
const SIGMA_MIN: f32 = 0.3;
const RESTART_SETTLING: f32 = 10.0;
#[derive(Clone, Copy, Debug, PartialEq)]
struct Model {
attitude: UnitQuaternion<f32>,
gyro_bias: Vector3<f32>,
velocity: Vector2<f32>,
covariance: Matrix3<f32>,
weight: f32,
}
impl Default for Model {
fn default() -> Self {
Self {
attitude: UnitQuaternion::identity(),
gyro_bias: Vector3::zeros(),
velocity: Vector2::zeros(),
covariance: Matrix3::zeros(),
weight: 1.0 / MODELS as f32,
}
}
}
impl Model {
fn yaw(&self) -> f32 {
heading_of(self.attitude)
}
fn turn(&mut self, angle: f32) {
self.attitude = exp_quat(Vector3::z() * angle) * self.attitude;
self.attitude.renormalize();
}
}
fn turned(v: Vector2<f32>, angle: f32) -> Vector2<f32> {
let (sin, cos) = (ComplexField::sin(angle), ComplexField::cos(angle));
Vector2::new(cos * v.x - sin * v.y, sin * v.x + cos * v.y)
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct YawEstimator {
models: [Model; MODELS],
accel: Vector3<f32>,
acceleration: Vector2<f32>,
reference: Vector3<f32>,
last: Option<(Vector2<f32>, Timestamp)>,
leveled: bool,
fusing: bool,
active: f32,
restarted: bool,
yaw: f32,
variance: f32,
}
impl Default for YawEstimator {
fn default() -> Self {
Self {
models: [Model::default(); MODELS],
accel: Vector3::zeros(),
acceleration: Vector2::zeros(),
reference: Vector3::z(),
last: None,
leveled: false,
fusing: false,
active: 0.0,
restarted: false,
yaw: 0.0,
variance: f32::INFINITY,
}
}
}
impl YawEstimator {
pub(crate) fn restart(&mut self) {
self.clear();
self.restarted = true;
}
pub(crate) fn clear(&mut self) {
for model in &mut self.models {
*model = Model::default();
}
self.accel = Vector3::zeros();
self.acceleration = Vector2::zeros();
self.reference = Vector3::z();
self.last = None;
self.leveled = false;
self.fusing = false;
self.active = 0.0;
self.restarted = false;
self.yaw = 0.0;
self.variance = f32::INFINITY;
}
pub(crate) fn yaw(&self) -> Option<(f32, f32)> {
self.fusing.then_some((self.yaw, self.variance))
}
pub(crate) fn is_settled(&self) -> bool {
!self.restarted || self.active >= RESTART_SETTLING
}
pub(crate) fn is_leveled(&self) -> bool {
self.leveled
}
pub(crate) fn variance_of(north: f32, east: f32) -> Option<f32> {
let variance = north.max(east);
(variance < SIGMA_MAX * SIGMA_MAX).then_some(variance.max(SIGMA_MIN * SIGMA_MIN))
}
pub(crate) fn predict(&mut self, imu: ImuSample, gravity: f32, gyro_bias: Vector3<f32>) {
let dt_v = imu.velocity_interval.as_secs();
let dt_a = imu.angle_interval.as_secs();
let delta_velocity = imu.delta_velocity.vector();
let delta_angle = imu.delta_angle.vector();
let accel = delta_velocity / dt_v;
let measured_rate = delta_angle / dt_a;
let coefficient = (ACCEL_FILTER_RATIO * dt_v * TILT_GAIN).min(1.0);
self.accel = self.accel * (1.0 - coefficient) + accel * coefficient;
if !self.leveled {
let near_one_g = |f: &Vector3<f32>| (0.9 * gravity..1.1 * gravity).contains(&f.norm());
if !(near_one_g(&accel) && near_one_g(&self.accel)) {
return;
}
let level = level(accel);
for model in &mut self.models {
model.attitude = level;
}
self.leveled = true;
}
let force = self.accel.norm();
let reference = if self.fusing {
self.reference
} else {
Vector3::z()
};
let stepped = if self.fusing { MODELS } else { 1 };
if !self.fusing
&& let Some(first) = self.models.first_mut()
{
first.gyro_bias = gyro_bias;
}
let attenuation = 1.0 - (2.0 * (force - gravity).abs() / gravity).min(1.0);
let gain = TILT_GAIN * attenuation * attenuation;
for model in self.models.iter_mut().take(stepped) {
let expected = model.attitude.inverse() * reference;
let correction = if gain > 0.0 {
expected.cross(&self.accel) * (gain / force)
} else {
Vector3::zeros()
};
let rate = measured_rate - model.gyro_bias;
if rate.norm_squared() < BIAS_RATE_MAX * BIAS_RATE_MAX {
model.gyro_bias -= correction * (BIAS_GAIN * dt_a);
model.gyro_bias = model
.gyro_bias
.map(|bias| bias.clamp(-BIAS_LIMIT, BIAS_LIMIT));
}
let corrected = delta_angle + (correction - model.gyro_bias) * dt_a;
model.attitude *= exp_quat(corrected);
model.attitude.renormalize();
if !self.fusing {
continue;
}
let moved = (model.attitude * delta_velocity).xy();
let f = Vector3::new(-moved.y, moved.x, 0.0);
let p = &model.covariance;
let p3 = Vector3::new(p.m13, p.m23, p.m33);
let q_v = ACCEL_NOISE * ACCEL_NOISE * dt_v;
let q_psi = GYRO_NOISE * GYRO_NOISE * dt_a;
model.covariance += f * p3.transpose()
+ p3 * f.transpose()
+ f * f.transpose() * p3.z
+ Matrix3::from_diagonal(&Vector3::new(q_v, q_v, q_psi));
condition(&mut model.covariance);
model.velocity += moved;
}
if self.fusing {
self.active += dt_v;
} else {
let [first, rest @ ..] = &mut self.models;
for model in rest {
model.attitude = first.attitude;
model.gyro_bias = first.gyro_bias;
}
}
}
pub(crate) fn fuse_velocity(
&mut self,
taken: Timestamp,
velocity: Vector2<f32>,
variance: f32,
carried: Vector2<f32>,
heading: f32,
gravity: f32,
) {
if !self.leveled {
return;
}
self.measure_acceleration(taken, velocity, gravity);
if !self.fusing {
self.spread(velocity, variance, carried, heading);
self.fusing = velocity.norm_squared() > variance;
self.active = 0.0;
return;
}
let mut densities = [0.0; MODELS];
let mut yaws = [0.0; MODELS];
let mut conditioned = true;
let each = self.models.iter_mut().zip(&mut densities).zip(&mut yaws);
for ((model, density), yaw) in each {
*yaw = model.yaw();
let z = velocity + turned(carried, wrap_pi(*yaw - heading));
let s = model.covariance.fixed_view::<2, 2>(0, 0) + Matrix2::identity() * variance;
let determinant = s.m11 * s.m22 - s.m12 * s.m21;
let invertible = determinant > f32::MIN_POSITIVE;
if !invertible {
conditioned = false;
continue;
}
let s_inverse = Matrix2::new(s.m22, -s.m12, -s.m21, s.m11) / determinant;
let mut innovation = model.velocity - z;
let mut nis = (innovation.transpose() * s_inverse * innovation).x;
if nis > NIS_MAX {
innovation *= ComplexField::sqrt(NIS_MAX / nis);
nis = NIS_MAX;
}
let k: Matrix3x2<f32> = model.covariance.fixed_view::<3, 2>(0, 0) * s_inverse;
let correction = -k * innovation;
model.velocity += correction.xy();
model.turn(correction.z);
*yaw = wrap_pi(*yaw + correction.z);
model.covariance -= k * s * k.transpose();
condition(&mut model.covariance);
*density = ComplexField::exp(-0.5 * nis) / (2.0 * PI * ComplexField::sqrt(determinant));
}
if conditioned {
let mut total = 0.0;
for (model, density) in self.models.iter_mut().zip(densities) {
model.weight = (model.weight * density).max(WEIGHT_MIN);
total += model.weight;
}
for model in &mut self.models {
model.weight /= total;
}
}
self.compose(yaws);
}
fn measure_acceleration(&mut self, taken: Timestamp, velocity: Vector2<f32>, gravity: f32) {
let interval = self
.last
.map(|(_, last)| taken.since(last).as_secs())
.filter(|interval| *interval > 0.0 && *interval <= ACCELERATION_GAP);
match (self.last, interval) {
(Some((last, _)), Some(interval)) => {
let measured = (velocity - last) / interval;
let tau = ACCEL_FILTER_TAU - 0.5 * interval;
let coefficient = if tau > 0.0 {
-ComplexField::exp_m1(-interval / tau)
} else {
1.0
};
self.acceleration += (measured - self.acceleration) * coefficient;
}
_ => self.acceleration = Vector2::zeros(),
}
self.reference =
Vector3::new(-self.acceleration.x, -self.acceleration.y, gravity).normalize();
self.last = Some((velocity, taken));
}
fn spread(
&mut self,
velocity: Vector2<f32>,
variance: f32,
carried: Vector2<f32>,
heading: f32,
) {
let increment = 2.0 * PI / MODELS as f32;
let sigma_yaw = 0.5 * increment;
for (index, model) in self.models.iter_mut().enumerate() {
let yaw = -PI + (index as f32 + 0.5) * increment;
model.turn(wrap_pi(yaw - model.yaw()));
model.velocity = velocity + turned(carried, wrap_pi(yaw - heading));
model.covariance =
Matrix3::from_diagonal(&Vector3::new(variance, variance, sigma_yaw * sigma_yaw));
model.weight = 1.0 / MODELS as f32;
}
self.yaw = 0.0;
self.variance = f32::INFINITY;
}
fn compose(&mut self, yaws: [f32; MODELS]) {
let mut direction = Vector2::zeros();
for (model, yaw) in self.models.iter().zip(yaws) {
direction +=
Vector2::new(ComplexField::cos(yaw), ComplexField::sin(yaw)) * model.weight;
}
self.yaw = RealField::atan2(direction.y, direction.x);
self.variance = self
.models
.iter()
.zip(yaws)
.map(|(model, yaw)| {
let delta = wrap_pi(yaw - self.yaw);
model.weight * (model.covariance.m33 + delta * delta)
})
.sum();
if !(self.variance > 0.0 && self.variance.is_finite()) {
self.restart();
}
}
}
fn level(specific_force: Vector3<f32>) -> UnitQuaternion<f32> {
let (roll, pitch) = level_from_accel(Acceleration::<Body>::from_vector(specific_force));
UnitQuaternion::from_euler_angles(roll.as_radians(), pitch.as_radians(), 0.0)
}
fn condition(p: &mut Matrix3<f32>) {
*p = (*p + p.transpose()) * 0.5;
p.m11 = p.m11.max(VARIANCE_MIN);
p.m22 = p.m22.max(VARIANCE_MIN);
p.m33 = p.m33.max(VARIANCE_MIN);
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::config::GRAVITY;
use crate::frames::Body;
use crate::units::{Acceleration, AngularRate, Seconds};
const DT: f32 = 0.01;
struct Flight {
estimator: YawEstimator,
attitude: UnitQuaternion<f32>,
velocity: Vector3<f32>,
step: u64,
ripple: f32,
gyro_bias: Vector3<f32>,
every: u32,
}
impl Flight {
fn heading(yaw: f32) -> Self {
Self {
estimator: YawEstimator::default(),
attitude: UnitQuaternion::from_euler_angles(0.0, 0.0, yaw),
velocity: Vector3::zeros(),
step: 0,
ripple: 0.05,
gyro_bias: Vector3::zeros(),
every: 20,
}
}
fn step(&mut self, acceleration: Vector3<f32>, rate: Vector3<f32>) {
let force = self.attitude.inverse() * (acceleration - Vector3::z() * GRAVITY);
self.step += 1;
let imu = ImuSample::from_rates(
Timestamp::from_micros(10_000 * self.step),
AngularRate::<Body>::from_vector(rate + self.gyro_bias),
Acceleration::<Body>::from_vector(force),
Seconds::from_secs(DT),
);
self.estimator.predict(imu, GRAVITY, Vector3::zeros());
self.attitude *= exp_quat(rate * DT);
self.velocity += acceleration * DT;
}
fn fix(&mut self) {
let phase = self.step as f32 * 0.37;
let error = Vector2::new(ComplexField::sin(phase), ComplexField::cos(1.7 * phase));
self.estimator.fuse_velocity(
Timestamp::from_micros(10_000 * self.step),
self.velocity.xy() + error * self.ripple,
0.09,
Vector2::zeros(),
0.0,
GRAVITY,
);
}
fn fly(&mut self, seconds: f32, acceleration: impl Fn(f32) -> Vector3<f32>) {
for k in 0..(seconds / DT) as u32 {
self.step(acceleration(k as f32 * DT), Vector3::zeros());
if k % self.every == self.every - 1 {
self.fix();
}
}
}
fn airborne(yaw: f32) -> Self {
let mut flight = Self::heading(yaw);
flight.fly(2.0, |_| Vector3::zeros());
flight
}
}
pub(crate) fn legs(t: f32) -> Vector3<f32> {
let leg = (t / 4.0) as u32;
let sign = if t - leg as f32 * 4.0 < 2.0 {
1.0
} else {
-1.0
};
let direction = leg as f32 * core::f32::consts::FRAC_PI_2;
Vector3::new(
ComplexField::cos(direction),
ComplexField::sin(direction),
0.0,
) * (2.0 * sign)
}
pub(crate) fn circling(t: f32) -> Vector3<f32> {
Vector3::new(
2.0 * ComplexField::cos(0.8 * t),
2.0 * ComplexField::sin(0.8 * t),
0.0,
)
}
#[test]
fn a_maneuver_finds_the_yaw_wherever_it_started() {
for yaw in [0.3_f32, -1.9, 2.6, 3.1, -0.628] {
let mut flight = Flight::airborne(yaw);
flight.fly(10.0, legs);
let (estimate, variance) = flight.estimator.yaw().unwrap();
assert!(
wrap_pi(estimate - yaw).abs() < 0.05,
"yaw {yaw}: estimated {estimate}"
);
assert!(variance < 0.262 * 0.262, "yaw {yaw}: variance {variance}");
}
}
#[test]
fn a_hover_never_claims_a_yaw() {
let mut flight = Flight::airborne(1.0);
flight.velocity = Vector3::new(1.0, 0.0, 0.0);
flight.fly(20.0, |_| Vector3::zeros());
let (_, variance) = flight.estimator.yaw().unwrap();
assert!(variance > 1.0, "variance {variance}");
}
#[test]
fn nothing_is_fused_below_the_velocitys_own_accuracy() {
let mut flight = Flight::airborne(1.0);
assert_eq!(flight.estimator.yaw(), None);
flight.velocity = Vector3::new(0.05, 0.0, 0.0);
flight.ripple = 0.0;
flight.fix();
assert_eq!(flight.estimator.yaw(), None);
flight.velocity = Vector3::new(0.5, 0.0, 0.0);
flight.fix();
assert!(flight.estimator.yaw().is_some());
}
#[test]
fn the_weights_stay_a_distribution() {
let mut flight = Flight::airborne(-2.2);
flight.fly(6.0, legs);
let total: f32 = flight.estimator.models.iter().map(|m| m.weight).sum();
assert!((total - 1.0).abs() < 1.0e-5);
assert!(flight.estimator.models.iter().all(|m| m.weight > 0.0));
}
#[test]
fn the_accelerometer_levels_a_tilted_hypothesis() {
let mut flight = Flight::airborne(0.4);
for model in &mut flight.estimator.models {
model.attitude *= UnitQuaternion::from_euler_angles(0.2, -0.15, 0.0);
}
flight.fly(30.0, |_| Vector3::zeros());
for model in &flight.estimator.models {
let down = model.attitude.inverse() * Vector3::z();
assert!(down.xy().norm() < 0.01, "down {down}");
}
}
#[test]
fn a_gyroscope_bias_is_learned_and_the_tilt_stays_level() {
let mut flight = Flight::airborne(0.4);
flight.velocity = Vector3::new(1.0, 0.0, 0.0);
flight.gyro_bias = Vector3::new(0.02, 0.0, 0.0);
flight.fly(150.0, |_| Vector3::zeros());
for model in &flight.estimator.models {
assert!(
(model.gyro_bias.x - 0.02).abs() < 0.004,
"{}",
model.gyro_bias
);
let down = model.attitude.inverse() * Vector3::z();
assert!(down.xy().norm() < 0.02, "down {down}");
}
}
#[test]
fn a_velocity_spike_moves_each_hypothesis_five_sigma_and_no_more() {
let mut flight = Flight::airborne(0.4);
flight.fly(6.0, legs);
let before = flight.estimator.models.map(|model| model.velocity);
let spike = flight.velocity.xy() + Vector2::new(100.0, 0.0);
let now = Timestamp::from_micros(10_000 * flight.step);
flight
.estimator
.fuse_velocity(now, spike, 0.09, Vector2::zeros(), 0.0, GRAVITY);
for (model, before) in flight.estimator.models.iter().zip(before) {
let moved = (model.velocity - before).norm();
assert!(moved < 3.0, "moved {moved}");
}
}
#[test]
fn unobserved_the_variances_grow_at_the_densities() {
let mut flight = Flight::airborne(0.4);
flight.velocity = Vector3::new(1.0, 0.0, 0.0);
flight.fix();
let before = flight.estimator.models[0].covariance;
for _ in 0..200 {
flight.step(Vector3::zeros(), Vector3::zeros());
}
let grown = flight.estimator.models[0].covariance - before;
let (yaw, velocity) = (
GYRO_NOISE * GYRO_NOISE * 2.0,
ACCEL_NOISE * ACCEL_NOISE * 2.0,
);
assert!((grown.m33 / yaw - 1.0).abs() < 0.03, "{}", grown.m33);
assert!((grown.m11 / velocity - 1.0).abs() < 0.01, "{}", grown.m11);
assert!((grown.m22 / velocity - 1.0).abs() < 0.01, "{}", grown.m22);
}
#[test]
fn a_bank_begun_again_is_settled_after_ten_seconds_of_fusion() {
let mut flight = Flight::airborne(0.4);
assert!(flight.estimator.is_settled());
flight.estimator.restart();
flight.fly(2.0, |_| Vector3::zeros());
flight.fly(9.0, legs);
assert!(flight.estimator.yaw().is_some() && !flight.estimator.is_settled());
flight.fly(2.0, legs);
assert!(flight.estimator.is_settled());
}
#[test]
fn a_turn_carries_every_hypothesis_with_it() {
let mut flight = Flight::airborne(0.5);
flight.fly(12.0, legs);
for k in 0..300 {
flight.step(legs(k as f32 * DT), Vector3::z() * (PI / 6.0));
if k % 20 == 19 {
flight.fix();
}
}
let (estimate, _) = flight.estimator.yaw().unwrap();
assert!(
wrap_pi(estimate - (0.5 + PI / 2.0)).abs() < 0.1,
"{estimate}"
);
}
#[test]
fn an_old_velocity_is_carried_to_now_in_each_hypothesis_own_axes() {
let fly = |carry: bool| {
let (yaw, wrong) = (0.9_f32, 2.0_f32);
let mut flight = Flight::airborne(yaw);
let mut past = std::collections::VecDeque::new();
for k in 0..1500 {
flight.step(circling(k as f32 * DT), Vector3::zeros());
past.push_back(flight.velocity.xy());
if past.len() > 21 {
past.pop_front();
}
if k % 20 == 19 && past.len() == 21 {
let then = past[0];
let moved = turned(flight.velocity.xy() - then, wrong);
let carried = if carry { moved } else { Vector2::zeros() };
let taken = Timestamp::from_micros(10_000 * (flight.step - 20));
flight.estimator.fuse_velocity(
taken,
then,
0.09,
carried,
wrap_pi(yaw + wrong),
GRAVITY,
);
}
}
let (estimate, _) = flight.estimator.yaw().unwrap();
wrap_pi(estimate - yaw)
};
assert!(fly(true).abs() < 0.03, "carried {}", fly(true));
assert!(fly(false) < -0.12, "not carried {}", fly(false));
}
#[test]
fn a_steady_circle_is_not_read_as_a_tilt() {
let mut flight = Flight::airborne(0.3);
flight.fly(20.0, circling);
let (estimate, variance) = flight.estimator.yaw().unwrap();
let error = wrap_pi(estimate - 0.3);
assert!(error.abs() < 0.03, "error {error}");
assert!(variance < 0.1 * 0.1, "variance {variance}");
}
#[test]
fn a_velocitys_sigma_is_read_between_the_floor_and_the_ceiling() {
let read = YawEstimator::variance_of;
assert_eq!(read(1.0e-6, 1.0e-6), Some(0.09));
assert_eq!(read(0.16, 0.04), Some(0.16));
assert_eq!(read(0.04, 0.2499), Some(0.2499));
assert_eq!(read(0.25, 0.01), None);
assert_eq!(read(0.01, f32::NAN), Some(0.09));
}
#[test]
fn the_shear_of_48_is_the_dense_product_it_stands_for() {
let mut flight = Flight::airborne(0.4);
flight.fly(3.0, legs);
let before = flight.estimator.models[0];
let acceleration = Vector3::new(1.5, -0.7, 0.0);
flight.step(acceleration, Vector3::zeros());
let after = flight.estimator.models[0];
let moved = after.velocity - before.velocity;
#[rustfmt::skip]
let f = Matrix3::new(
1.0, 0.0, -moved.y,
0.0, 1.0, moved.x,
0.0, 0.0, 1.0,
);
let q = Matrix3::from_diagonal(&Vector3::new(
ACCEL_NOISE * ACCEL_NOISE * DT,
ACCEL_NOISE * ACCEL_NOISE * DT,
GYRO_NOISE * GYRO_NOISE * DT,
));
let dense = f * before.covariance * f.transpose() + q;
assert!(moved.norm() > 0.005);
assert!(
(after.covariance - dense).abs().max() < 1.0e-7,
"{}",
after.covariance - dense
);
}
#[test]
fn the_measured_acceleration_is_the_velocities_slope_and_a_gap_forgets_it() {
let mut flight = Flight::airborne(0.4);
flight.ripple = 0.0;
flight.fly(2.5, |_| Vector3::new(2.0, 0.0, 0.0));
let measured = flight.estimator.acceleration;
assert!(
(measured - Vector2::new(2.0, 0.0)).norm() < 0.05,
"{measured}"
);
for _ in 0..300 {
flight.step(Vector3::zeros(), Vector3::zeros());
}
flight.fix();
assert_eq!(flight.estimator.acceleration, Vector2::zeros());
}
#[test]
fn before_fusion_tilt_is_read_against_gravity_alone() {
let mut flight = Flight::airborne(1.3);
flight.estimator.acceleration = Vector2::new(3.0, 0.0);
for _ in 0..3000 {
flight.step(Vector3::zeros(), Vector3::zeros());
}
assert!(flight.estimator.yaw().is_none());
for model in &flight.estimator.models {
let down = model.attitude.inverse() * Vector3::z();
assert!(down.xy().norm() < 0.01, "down {down}");
}
}
#[test]
fn a_slow_receiver_still_measures_the_circle() {
for (every, bound) in [(10, 0.02), (20, 0.02), (50, 0.02), (100, 0.09), (101, 0.09)] {
let mut flight = Flight::airborne(0.3);
flight.every = every;
flight.fly(30.0, circling);
let (estimate, _) = flight.estimator.yaw().unwrap();
let error = wrap_pi(estimate - 0.3);
assert!(error.abs() < bound, "every {every}: error {error}");
}
}
#[test]
fn the_composite_is_of_the_headings_the_update_left() {
let mut flight = Flight::airborne(-1.1);
flight.fly(2.2, legs);
let (yaw, _) = flight.estimator.yaw().unwrap();
let mut direction = Vector2::zeros();
for model in &flight.estimator.models {
let psi = model.yaw();
direction += Vector2::new(psi.cos(), psi.sin()) * model.weight;
}
let expected = direction.y.atan2(direction.x);
assert!(wrap_pi(yaw - expected).abs() < 1.0e-5, "{yaw} {expected}");
}
#[test]
fn a_bank_that_stops_producing_numbers_starts_over() {
let mut flight = Flight::airborne(0.0);
flight.fly(3.0, circling);
flight.estimator.models[2].covariance.m33 = f32::NAN;
let yaws = flight.estimator.models.each_ref().map(Model::yaw);
flight.estimator.compose(yaws);
assert!(flight.estimator.yaw().is_none() && !flight.estimator.is_settled());
}
}