use core::f32::consts::FRAC_1_SQRT_2;
use core::fmt;
use core::marker::PhantomData;
use nalgebra::{UnitQuaternion, Vector3};
use crate::frames::{Body, Enu, Frame, Ned};
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct Quaternion {
pub w: f32,
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Quaternion {
pub const IDENTITY: Self = Self {
w: 1.0,
x: 0.0,
y: 0.0,
z: 0.0,
};
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Attitude(UnitQuaternion<f32>);
#[cfg(feature = "defmt")]
impl defmt::Format for Attitude {
fn format(&self, f: defmt::Formatter<'_>) {
let q = self.0.quaternion();
defmt::write!(
f,
"Attitude({=f32}, {=f32}, {=f32}, {=f32})",
q.w,
q.i,
q.j,
q.k
)
}
}
impl Attitude {
pub fn level() -> Self {
Self(UnitQuaternion::identity())
}
pub fn from_body_to_ned(q: Quaternion) -> Self {
Self(unit(q))
}
pub(crate) const fn from_quaternion(q: UnitQuaternion<f32>) -> Self {
Self(q)
}
pub fn from_ned_to_body(q: Quaternion) -> Self {
Self(unit(q).inverse())
}
pub fn from_flu_to_enu(q: Quaternion) -> Self {
let r_nav = ned_from_enu();
let r_body = frd_from_flu();
Self(r_nav * unit(q) * r_body.inverse())
}
pub fn from_flu_to_nwu(q: Quaternion) -> Self {
let r_nav = ned_from_nwu();
let r_body = frd_from_flu();
Self(r_nav * unit(q) * r_body.inverse())
}
pub fn body_to_ned(self) -> Quaternion {
components(self.0)
}
pub(crate) const fn quaternion(self) -> UnitQuaternion<f32> {
self.0
}
pub fn ned_to_body(self) -> Quaternion {
components(self.0.inverse())
}
pub fn flu_to_enu(self) -> Quaternion {
components(ned_from_enu().inverse() * self.0 * frd_from_flu())
}
pub fn flu_to_nwu(self) -> Quaternion {
components(ned_from_nwu().inverse() * self.0 * frd_from_flu())
}
pub fn euler_angles(self) -> (f32, f32, f32) {
self.0.euler_angles()
}
}
impl Default for Attitude {
fn default() -> Self {
Self::level()
}
}
pub(crate) fn unit(q: Quaternion) -> UnitQuaternion<f32> {
let largest = [q.w, q.x, q.y, q.z]
.iter()
.fold(0.0f32, |m, c| m.max(c.abs()));
UnitQuaternion::from_quaternion(nalgebra::Quaternion::new(
q.w / largest,
q.x / largest,
q.y / largest,
q.z / largest,
))
}
pub(crate) fn components(q: UnitQuaternion<f32>) -> Quaternion {
Quaternion {
w: q.w,
x: q.i,
y: q.j,
z: q.k,
}
}
fn ned_from_enu() -> UnitQuaternion<f32> {
UnitQuaternion::new_unchecked(nalgebra::Quaternion::new(
0.0,
FRAC_1_SQRT_2,
FRAC_1_SQRT_2,
0.0,
))
}
fn ned_from_nwu() -> UnitQuaternion<f32> {
half_turn_about_first_axis()
}
fn frd_from_flu() -> UnitQuaternion<f32> {
half_turn_about_first_axis()
}
fn half_turn_about_first_axis() -> UnitQuaternion<f32> {
UnitQuaternion::new_unchecked(nalgebra::Quaternion::new(0.0, 1.0, 0.0, 0.0))
}
macro_rules! scalar {
(
$(#[$meta:meta])*
$name:ident, unit = $unit:literal, new = $new:ident, get = $get:ident
) => {
$(#[$meta])*
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct $name(f32);
impl $name {
pub const ZERO: Self = Self(0.0);
#[doc = concat!("Construct from a value in ", $unit, ".")]
pub const fn $new(value: f32) -> Self {
Self(value)
}
#[doc = concat!("The value in ", $unit, ".")]
pub const fn $get(self) -> f32 {
self.0
}
}
};
}
scalar!(
Seconds,
unit = "seconds",
new = from_secs,
get = as_secs
);
impl Seconds {
pub(crate) fn is_usable_step(self) -> bool {
self.0 > 0.0 && !self.0.is_nan()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct Timestamp(u64);
impl Timestamp {
pub const ZERO: Self = Self(0);
pub const fn from_micros(micros: u64) -> Self {
Self(micros)
}
pub const fn as_micros(self) -> u64 {
self.0
}
pub fn from_secs_f64(seconds: f64) -> Self {
Self((seconds * 1e6 + 0.5) as u64)
}
pub fn after(self, interval: Seconds) -> Self {
let micros = f64::from(interval.as_secs()) * 1e6;
if micros >= 0.0 {
Self(self.0.saturating_add((micros + 0.5) as u64))
} else {
Self(self.0.saturating_sub((0.5 - micros) as u64))
}
}
pub fn before(self, interval: Seconds) -> Self {
self.after(Seconds::from_secs(-interval.as_secs()))
}
pub fn since(self, earlier: Self) -> Seconds {
let micros = self.0.wrapping_sub(earlier.0) as i64;
Seconds::from_secs(micros as f32 / 1_000_000.0)
}
}
scalar!(
Radians,
unit = "radians",
new = from_radians,
get = as_radians
);
impl Radians {
pub const fn from_degrees(degrees: f32) -> Self {
Self(degrees.to_radians())
}
}
scalar!(
Meters,
unit = "meters",
new = from_meters,
get = as_meters
);
scalar!(
MetersPerSecond,
unit = "meters per second",
new = from_m_per_s,
get = as_m_per_s
);
scalar!(
MetersPerSecond2,
unit = "meters per second squared",
new = from_m_per_s2,
get = as_m_per_s2
);
scalar!(
RadiansPerSecond,
unit = "radians per second",
new = from_rad_per_s,
get = as_rad_per_s
);
scalar!(
Altitude,
unit = "meters",
new = from_meters,
get = as_meters
);
macro_rules! noise {
(
$(#[$meta:meta])*
$name:ident, sigma = $sigma_unit:literal, variance = $variance_unit:literal
) => {
$(#[$meta])*
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct $name {
variance: f32,
}
impl $name {
#[doc = concat!("From a standard deviation in ", $sigma_unit, ".")]
pub const fn from_sigma(sigma: f32) -> Self {
Self::from_variance(sigma * sigma)
}
#[doc = concat!("From a variance in ", $variance_unit, ".")]
pub const fn from_variance(variance: f32) -> Self {
Self { variance }
}
#[doc = concat!("The variance, in ", $variance_unit, ".")]
pub const fn variance(self) -> f32 {
self.variance
}
pub(crate) fn is_finite(self) -> bool {
self.variance.is_finite()
}
pub(crate) fn is_positive(self) -> bool {
self.variance > 0.0
}
}
};
}
noise!(
AltitudeNoise,
sigma = "meters",
variance = "meters squared"
);
noise!(
HeadingNoise,
sigma = "radians",
variance = "radians squared"
);
macro_rules! vector_impls {
($name:ident, $field:ident, $unit:literal) => {
impl<F: Frame> Clone for $name<F> {
fn clone(&self) -> Self {
*self
}
}
impl<F: Frame> Copy for $name<F> {}
impl<F: Frame> PartialEq for $name<F> {
fn eq(&self, other: &Self) -> bool {
self.$field == other.$field
}
}
impl<F: Frame> fmt::Debug for $name<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let v = self.$field;
write!(
f,
"{}<{}>({}, {}, {}) {}",
stringify!($name),
F::NAME,
v.x,
v.y,
v.z,
$unit
)
}
}
#[cfg(feature = "defmt")]
impl<F: Frame> defmt::Format for $name<F> {
fn format(&self, f: defmt::Formatter<'_>) {
let v = self.$field;
defmt::write!(
f,
"{=str}<{=str}>({=f32}, {=f32}, {=f32}) {=str}",
stringify!($name),
F::NAME,
v.x,
v.y,
v.z,
$unit
)
}
}
};
}
macro_rules! framed {
(
$(#[$meta:meta])*
$name:ident, unit = $unit:literal
) => {
$(#[$meta])*
pub struct $name<F: Frame> {
value: Vector3<f32>,
frame: PhantomData<F>,
}
impl<F: Frame> $name<F> {
#[doc = concat!("From components on the axes of `F`, in ", $unit, ".")]
pub fn from_array(components: [f32; 3]) -> Self {
Self::from_vector(components.into())
}
pub(crate) const fn from_vector(value: Vector3<f32>) -> Self {
Self {
value,
frame: PhantomData,
}
}
pub fn zero() -> Self {
Self::from_vector(Vector3::zeros())
}
pub(crate) const fn vector(self) -> Vector3<f32> {
self.value
}
#[doc = concat!("The components on the axes of `F`, in ", $unit, ".")]
pub fn to_array(self) -> [f32; 3] {
self.value.into()
}
pub fn x(self) -> f32 {
self.value.x
}
pub fn y(self) -> f32 {
self.value.y
}
pub fn z(self) -> f32 {
self.value.z
}
pub(crate) fn is_finite(self) -> bool {
self.value.iter().all(|v| v.is_finite())
}
}
impl<F: Frame> Default for $name<F> {
fn default() -> Self {
Self::zero()
}
}
vector_impls!($name, value, $unit);
};
}
framed!(
Position,
unit = "meters"
);
framed!(
Velocity,
unit = "meters per second"
);
framed!(
Acceleration,
unit = "meters per second squared"
);
framed!(
AngularRate,
unit = "radians per second"
);
framed!(
MagField,
unit = "arbitrary units"
);
framed!(
DeltaAngle,
unit = "radians"
);
framed!(
DeltaVelocity,
unit = "meters per second"
);
macro_rules! navigation {
($name:ident) => {
impl $name<Ned> {
pub fn ned(north: f32, east: f32, down: f32) -> Self {
Self::from_vector(Vector3::new(north, east, down))
}
}
impl $name<Enu> {
pub fn enu(east: f32, north: f32, up: f32) -> Self {
Self::from_vector(Vector3::new(east, north, up))
}
pub fn to_ned(self) -> $name<Ned> {
let enu = self.value;
$name::ned(enu.y, enu.x, -enu.z)
}
}
impl $name<Ned> {
pub fn to_enu(self) -> $name<Enu> {
let ned = self.value;
$name::enu(ned.y, ned.x, -ned.z)
}
}
};
}
navigation!(Position);
navigation!(Velocity);
navigation!(Acceleration);
macro_rules! body {
($name:ident, $what:literal) => {
impl $name<Body> {
#[doc = concat!($what, " from forward, right, down components.")]
pub fn body(forward: f32, right: f32, down: f32) -> Self {
Self::from_vector(Vector3::new(forward, right, down))
}
#[doc = concat!($what, " from forward, left, up components, converted to")]
pub fn flu(forward: f32, left: f32, up: f32) -> Self {
Self::body(forward, -left, -up)
}
pub fn to_flu(self) -> [f32; 3] {
[self.value.x, -self.value.y, -self.value.z]
}
}
};
}
body!(Position, "Offset from the IMU");
body!(Acceleration, "Specific force");
body!(AngularRate, "Angular rate");
body!(MagField, "Field");
body!(DeltaAngle, "Rotation increment");
body!(DeltaVelocity, "Velocity increment");
impl AngularRate<Body> {
pub fn body_deg_per_s(forward: f32, right: f32, down: f32) -> Self {
Self::body(forward.to_radians(), right.to_radians(), down.to_radians())
}
}
macro_rules! noise3 {
(
$(#[$meta:meta])*
$name:ident, sigma = $sigma_unit:literal, variance = $variance_unit:literal
) => {
$(#[$meta])*
pub struct $name<F: Frame> {
variance: Vector3<f32>,
frame: PhantomData<F>,
}
impl<F: Frame> $name<F> {
#[doc = concat!("From per-axis standard deviations in ", $sigma_unit, ".")]
pub fn from_sigma(x: f32, y: f32, z: f32) -> Self {
Self::from_variance(x * x, y * y, z * z)
}
#[doc = concat!("From per-axis variances in ", $variance_unit, " — the diagonal")]
pub fn from_variance(x: f32, y: f32, z: f32) -> Self {
Self {
variance: Vector3::new(x, y, z),
frame: PhantomData,
}
}
#[doc = concat!("The per-axis variances, in ", $variance_unit, ".")]
pub fn variance(self) -> [f32; 3] {
self.variance.into()
}
pub(crate) fn is_finite(self) -> bool {
self.variance.iter().all(|v| v.is_finite())
}
pub(crate) fn is_positive(self) -> bool {
self.variance.iter().all(|v| *v > 0.0)
}
}
vector_impls!($name, variance, $variance_unit);
};
}
noise3!(
PositionNoise,
sigma = "meters",
variance = "meters squared"
);
noise3!(
VelocityNoise,
sigma = "meters per second",
variance = "meters squared per second squared"
);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SigmaBounds {
min: f32,
max: f32,
}
impl SigmaBounds {
pub const fn new(min: f32, max: f32) -> Self {
Self { min, max }
}
pub const fn at_least(min: f32) -> Self {
Self::new(min, f32::INFINITY)
}
fn apply(self, sigma: f32) -> f32 {
let floored = if sigma < self.min { self.min } else { sigma };
if floored > self.max {
self.max
} else {
floored
}
}
}
impl PositionNoise<Ned> {
pub fn horizontal_vertical(horizontal: f32, vertical: f32) -> Self {
Self::from_sigma(horizontal, horizontal, vertical)
}
pub fn clamped(
horizontal: f32,
vertical: f32,
horizontal_bounds: SigmaBounds,
vertical_bounds: SigmaBounds,
) -> Self {
let horizontal = horizontal_bounds.apply(horizontal);
Self::from_sigma(horizontal, horizontal, vertical_bounds.apply(vertical))
}
}
impl VelocityNoise<Ned> {
pub fn from_speed_accuracy(speed: f32) -> Self {
Self::from_sigma(speed, speed, speed)
}
pub fn horizontal_vertical(horizontal: f32, vertical: f32) -> Self {
Self::from_sigma(horizontal, horizontal, vertical)
}
pub fn clamped(
horizontal: f32,
vertical: f32,
horizontal_bounds: SigmaBounds,
vertical_bounds: SigmaBounds,
) -> Self {
let horizontal = horizontal_bounds.apply(horizontal);
Self::from_sigma(horizontal, horizontal, vertical_bounds.apply(vertical))
}
}
impl HeadingNoise {
pub fn clamped(sigma: f32, bounds: SigmaBounds) -> Self {
Self::from_sigma(bounds.apply(sigma))
}
}
#[cfg(test)]
mod tests {
use nalgebra::{Matrix3, Rotation3};
use super::*;
#[test]
fn a_short_interval_late_in_a_long_flight_keeps_its_microseconds() {
let late = Timestamp::from_secs_f64(7127.0);
let next = late.after(Seconds::from_secs(0.0025));
assert_eq!(next.as_micros() - late.as_micros(), 2500);
assert_eq!(next.since(late), Seconds::from_secs(0.0025));
assert_eq!(late.since(next), Seconds::from_secs(-0.0025));
assert_eq!(next.before(Seconds::from_secs(0.0025)), late);
let in_f32 = (7127.0f32 + 0.0025) - 7127.0;
assert!((in_f32 - 0.0025).abs() > 5e-5, "{in_f32}");
}
#[test]
fn a_time_before_the_epoch_or_not_a_number_is_the_epoch() {
assert_eq!(Timestamp::from_secs_f64(-1.0), Timestamp::ZERO);
assert_eq!(Timestamp::from_secs_f64(f64::NAN), Timestamp::ZERO);
assert_eq!(Timestamp::from_secs_f64(1.000_000_4).as_micros(), 1_000_000);
assert_eq!(Timestamp::from_secs_f64(1.000_000_6).as_micros(), 1_000_001);
assert_eq!(
Timestamp::ZERO.before(Seconds::from_secs(1.0)),
Timestamp::ZERO
);
let one = Timestamp::from_micros(1);
assert_eq!(one.after(Seconds::from_secs(f32::NAN)), one);
}
fn assert_same_rotation(left: Quaternion, right: UnitQuaternion<f32>) {
let left = unit(left);
assert!(
left.angle_to(&right) < 1.0e-6,
"{left} and {right} differ by {} rad",
left.angle_to(&right)
);
}
fn nose_east_rolled_right() -> [UnitQuaternion<f32>; 3] {
let ned_from_frd = Matrix3::from_columns(&[
Vector3::new(0.0, 1.0, 0.0), Vector3::new(0.0, 0.0, 1.0), Vector3::new(1.0, 0.0, 0.0), ]);
let enu_from_flu = Matrix3::from_columns(&[
Vector3::new(1.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 1.0), Vector3::new(0.0, -1.0, 0.0), ]);
let nwu_from_flu = Matrix3::from_columns(&[
Vector3::new(0.0, -1.0, 0.0), Vector3::new(0.0, 0.0, 1.0), Vector3::new(-1.0, 0.0, 0.0), ]);
[ned_from_frd, enu_from_flu, nwu_from_flu]
.map(|m| UnitQuaternion::from_rotation_matrix(&Rotation3::from_matrix_unchecked(m)))
}
#[test]
fn every_convention_maps_one_attitude_to_the_same_body_to_ned_quaternion() {
let [ned_from_frd, enu_from_flu, nwu_from_flu] = nose_east_rolled_right();
assert_same_rotation(
Attitude::from_body_to_ned(components(ned_from_frd)).body_to_ned(),
ned_from_frd,
);
assert_same_rotation(
Attitude::from_flu_to_enu(components(enu_from_flu)).body_to_ned(),
ned_from_frd,
);
assert_same_rotation(
Attitude::from_flu_to_nwu(components(nwu_from_flu)).body_to_ned(),
ned_from_frd,
);
}
#[test]
fn a_navigation_only_conversion_keeps_the_heading_and_turns_the_vehicle_over() {
let [ned_from_frd, enu_from_flu, _] = nose_east_rolled_right();
let half_applied = ned_from_enu() * enu_from_flu;
assert!(half_applied.angle_to(&ned_from_frd) > 1.0);
let level = UnitQuaternion::identity();
let (half_roll, _, half_yaw) = (ned_from_enu() * level).euler_angles();
let (roll, _, yaw) = Attitude::from_flu_to_enu(components(level)).euler_angles();
assert!((half_yaw - yaw).abs() < 1.0e-6);
assert!((half_roll.abs() - core::f32::consts::PI).abs() < 1.0e-6);
assert!(roll.abs() < 1.0e-6);
}
#[test]
fn an_identity_in_each_convention_is_the_attitude_that_convention_calls_level() {
let (roll, pitch, yaw) =
Attitude::from_flu_to_enu(components(UnitQuaternion::identity())).euler_angles();
assert!(roll.abs() < 1.0e-6 && pitch.abs() < 1.0e-6);
assert!((yaw - core::f32::consts::FRAC_PI_2).abs() < 1.0e-6);
assert_same_rotation(
Attitude::from_flu_to_nwu(components(UnitQuaternion::identity())).body_to_ned(),
unit(Attitude::level().body_to_ned()),
);
let rolled = UnitQuaternion::from_euler_angles(0.4, 0.0, 0.0);
assert_same_rotation(
Attitude::from_flu_to_nwu(components(rolled)).body_to_ned(),
rolled,
);
}
#[test]
fn every_convention_leaves_as_it_arrived() {
let [ned_from_frd, enu_from_flu, nwu_from_flu] = nose_east_rolled_right();
let attitude = Attitude::from_body_to_ned(components(ned_from_frd));
assert_same_rotation(attitude.flu_to_enu(), enu_from_flu);
assert_same_rotation(attitude.flu_to_nwu(), nwu_from_flu);
assert_same_rotation(attitude.ned_to_body(), ned_from_frd.inverse());
assert_same_rotation(
Attitude::from_flu_to_enu(attitude.flu_to_enu()).body_to_ned(),
ned_from_frd,
);
assert_same_rotation(
Attitude::from_flu_to_nwu(attitude.flu_to_nwu()).body_to_ned(),
ned_from_frd,
);
let half_applied = ned_from_enu().inverse() * ned_from_frd;
assert!(half_applied.angle_to(&enu_from_flu) > 1.0);
}
#[test]
fn a_stored_inverse_is_inverted_rather_than_wrapped() {
let [ned_from_frd, ..] = nose_east_rolled_right();
let frd_from_ned = ned_from_frd.inverse();
assert_same_rotation(
Attitude::from_ned_to_body(components(frd_from_ned)).body_to_ned(),
ned_from_frd,
);
assert!(frd_from_ned.angle_to(&ned_from_frd) > 1.0);
}
#[test]
fn enu_converts_to_ned_by_permutation() {
let ned = Position::enu(1.0, 2.0, 3.0).to_ned();
assert_eq!(ned, Position::ned(2.0, 1.0, -3.0));
let ned = Velocity::enu(-4.0, 5.0, -6.0).to_ned();
assert_eq!(ned, Velocity::ned(5.0, -4.0, 6.0));
}
#[test]
fn ned_converts_back_to_enu_by_the_same_permutation() {
let enu = Position::ned(2.0, 1.0, -3.0).to_enu();
assert_eq!(enu, Position::enu(1.0, 2.0, 3.0));
let velocity = Velocity::enu(-4.0, 5.0, -6.0);
assert_eq!(velocity.to_ned().to_enu(), velocity);
let acceleration = Acceleration::ned(0.1, -0.2, 0.3);
assert_eq!(acceleration.to_enu().to_ned(), acceleration);
}
#[test]
fn frd_converts_back_to_flu() {
assert_eq!(AngularRate::flu(0.1, 0.2, 0.3).to_flu(), [0.1, 0.2, 0.3]);
assert_eq!(Acceleration::body(0.0, 0.0, -9.8).to_flu(), [0.0, 0.0, 9.8]);
assert_eq!(MagField::flu(1.0, -2.0, 3.0).to_flu(), [1.0, -2.0, 3.0]);
}
#[test]
fn flu_converts_to_frd_by_negating_left_and_up() {
assert_eq!(
AngularRate::flu(0.1, 0.2, 0.3),
AngularRate::body(0.1, -0.2, -0.3)
);
assert_eq!(
Acceleration::flu(0.0, 0.0, 9.8),
Acceleration::body(0.0, 0.0, -9.8)
);
}
#[test]
fn degrees_are_converted_where_they_arrive() {
let rate = AngularRate::body_deg_per_s(180.0, 0.0, -90.0);
assert!((rate.x() - core::f32::consts::PI).abs() < 1e-6);
assert!((rate.z() + core::f32::consts::FRAC_PI_2).abs() < 1e-6);
assert!((Radians::from_degrees(-3.5).as_radians() + 0.061_086).abs() < 1e-6);
}
#[test]
fn sigma_and_variance_name_the_same_noise() {
assert_eq!(
PositionNoise::horizontal_vertical(1.5, 3.0),
PositionNoise::<Ned>::from_variance(2.25, 2.25, 9.0)
);
assert_eq!(
VelocityNoise::from_speed_accuracy(0.5).variance(),
[0.25; 3]
);
assert_eq!(
AltitudeNoise::from_sigma(2.0),
AltitudeNoise::from_variance(4.0)
);
}
fn within(min: f32, max: f32) -> SigmaBounds {
SigmaBounds::new(min, max)
}
#[test]
fn clamping_bounds_a_sigma_on_both_sides() {
assert_eq!(
PositionNoise::clamped(1.5, 3.0, within(0.5, 100.0), within(0.5, 100.0)),
PositionNoise::horizontal_vertical(1.5, 3.0)
);
assert_eq!(
PositionNoise::clamped(0.01, 250.0, within(0.5, 100.0), within(0.5, 100.0)),
PositionNoise::horizontal_vertical(0.5, 100.0)
);
assert_eq!(
VelocityNoise::clamped(0.02, 0.02, within(0.5, 50.0), within(0.5, 50.0)).variance(),
[0.25; 3]
);
assert_eq!(
VelocityNoise::clamped(0.4, 0.6, within(0.3, 50.0), within(0.3, 50.0)),
VelocityNoise::horizontal_vertical(0.4, 0.6)
);
assert_eq!(
PositionNoise::clamped(0.05, 0.05, within(0.5, 10.0), within(0.75, 10.0)),
PositionNoise::horizontal_vertical(0.5, 0.75)
);
assert_eq!(
VelocityNoise::clamped(0.1, 0.15, SigmaBounds::at_least(0.3), within(0.4, 0.42)),
VelocityNoise::horizontal_vertical(0.3, 0.4)
);
assert_eq!(
VelocityNoise::clamped(80.0, 80.0, within(0.3, 50.0), within(0.5, 60.0)),
VelocityNoise::horizontal_vertical(50.0, 60.0)
);
assert_eq!(
HeadingNoise::clamped(0.0, SigmaBounds::at_least(0.1)),
HeadingNoise::from_sigma(0.1)
);
assert_eq!(
HeadingNoise::clamped(2.0, within(0.1, 1.0)),
HeadingNoise::from_sigma(1.0)
);
assert!(!HeadingNoise::clamped(f32::NAN, within(0.1, 1.0)).is_finite());
}
#[test]
fn a_clamped_nan_stays_nan_rather_than_becoming_a_bound() {
let noise = PositionNoise::clamped(f32::NAN, 3.0, within(0.5, 100.0), within(0.5, 100.0));
assert!(noise.variance()[0].is_nan());
assert!(!noise.is_finite());
}
#[test]
fn bounds_the_wrong_way_round_saturate_rather_than_panicking() {
assert_eq!(
VelocityNoise::clamped(1.0, 1.0, within(10.0, 5.0), within(10.0, 5.0)).variance(),
[25.0; 3]
);
}
#[test]
fn a_dropped_vertical_axis_is_a_large_sigma_on_that_axis_alone() {
let noise = VelocityNoise::horizontal_vertical(0.3, 1000.0);
assert_eq!(noise.variance()[0], 0.09);
assert_eq!(noise.variance()[2], 1.0e6);
assert!(noise.is_positive());
}
#[test]
fn components_come_out_as_plain_numbers() {
let p = Position::ned(1.0, 2.0, 3.0);
assert_eq!(p.to_array(), [1.0, 2.0, 3.0]);
assert_eq!((p.x(), p.y(), p.z()), (1.0, 2.0, 3.0));
}
}