use kinavis_kernel::error::{KernelError, Result};
use kinavis_kernel::geodesy::{Ellipsoid, GeodeticPoint};
use kinavis_kernel::local::{LocalFrame, Ned, Vector3};
use kinavis_kernel::math;
use kinavis_kernel::position::Position;
use kinavis_kernel::time::{Instant, Utc};
use kinavis_kernel::units::{Distance, Speed};
use crate::attitude::{cross, Attitude, Quaternion};
use crate::imu::ImuSample;
pub const EARTH_RATE: f64 = 7.292_115e-5;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Strapdown {
frame: LocalFrame,
position: [f64; 3],
velocity: [f64; 3],
attitude: Quaternion,
gyro_bias: [f64; 3],
accel_bias: [f64; 3],
valid_at: Instant<Utc>,
}
impl Strapdown {
pub fn new(
at: Instant<Utc>,
origin: GeodeticPoint,
velocity: Vector3<Ned, Speed>,
attitude: Quaternion,
) -> Result<Self> {
let frame = LocalFrame::at(origin, &Ellipsoid::WGS84)?;
Ok(Self {
frame,
position: [0.0; 3],
velocity: [
velocity.north().metres_per_second(),
velocity.east().metres_per_second(),
velocity.down().metres_per_second(),
],
attitude,
gyro_bias: [0.0; 3],
accel_bias: [0.0; 3],
valid_at: at,
})
}
pub fn step(&mut self, sample: &ImuSample) -> Result<()> {
let dt = sample.seconds();
let (latitude, height) = self.latitude_and_height();
let (sin_lat, cos_lat) = (math::sin(latitude), math::cos(latitude));
let (radius_north, radius_east) = curvature_radii(sin_lat);
let earth_rate = [EARTH_RATE * cos_lat, 0.0, -EARTH_RATE * sin_lat];
let [vn, ve, _] = self.velocity;
let transport_rate = [
ve / (radius_east + height),
-vn / (radius_north + height),
-ve * sin_lat / (cos_lat.max(1e-9) * (radius_east + height)),
];
let frame_rate = add(earth_rate, transport_rate);
let measured = sample.angular_rate();
let corrected = sub(measured, self.gyro_bias);
let body_rate = sub(corrected, self.attitude.rotate_back(frame_rate));
let rotation = scale(body_rate, dt);
let midway = self.attitude.rotated_by_body(scale(body_rate, dt / 2.0));
let attitude = self.attitude.rotated_by_body(rotation);
let force = midway.rotate(sub(sample.specific_force(), self.accel_bias));
let gravity = [0.0, 0.0, gravity_down(sin_lat, height)];
let coriolis = cross(add(scale(earth_rate, 2.0), transport_rate), self.velocity);
let acceleration = sub(add(force, gravity), coriolis);
let velocity = add(self.velocity, scale(acceleration, dt));
let position = add(self.position, scale(add(self.velocity, velocity), dt / 2.0));
self.valid_at =
self.valid_at
.checked_add(sample.over())
.ok_or(KernelError::Indeterminate {
quantity: "a moment beyond the end of time",
})?;
self.attitude = attitude;
self.velocity = velocity;
self.position = position;
Ok(())
}
fn latitude_and_height(&self) -> (f64, f64) {
let origin = self.frame.origin();
let latitude = origin.position().latitude().radians();
let height = origin.height().value().metres() - self.position[2];
let (radius_north, _) = curvature_radii(math::sin(latitude));
(
latitude + self.position[0] / (radius_north + height),
height,
)
}
#[must_use]
pub const fn valid_at(&self) -> Instant<Utc> {
self.valid_at
}
#[must_use]
pub const fn frame(&self) -> &LocalFrame {
&self.frame
}
#[must_use]
pub fn displacement(&self) -> Vector3<Ned, Distance> {
let [n, e, d] = self.position;
Vector3::new(metres(n), metres(e), metres(d))
}
pub fn point(&self) -> Result<GeodeticPoint> {
self.frame.point_from_ned(self.displacement())
}
pub fn position(&self) -> Result<Position> {
Ok(self.point()?.position())
}
#[must_use]
pub fn velocity(&self) -> Vector3<Ned, Speed> {
let [n, e, d] = self.velocity;
Vector3::new(
metres_per_second(n),
metres_per_second(e),
metres_per_second(d),
)
}
#[must_use]
pub fn attitude(&self) -> Attitude {
self.attitude.to_euler()
}
#[must_use]
pub const fn quaternion(&self) -> &Quaternion {
&self.attitude
}
#[must_use]
pub const fn gyro_bias(&self) -> [f64; 3] {
self.gyro_bias
}
#[must_use]
pub const fn accel_bias(&self) -> [f64; 3] {
self.accel_bias
}
pub(crate) const fn raw(&self) -> ([f64; 3], [f64; 3], [f64; 3], [f64; 3]) {
(
self.position,
self.velocity,
self.gyro_bias,
self.accel_bias,
)
}
pub(crate) fn navigation_force(&self, sample: &ImuSample) -> [f64; 3] {
self.attitude
.rotate(sub(sample.specific_force(), self.accel_bias))
}
pub(crate) fn earth_rate(&self) -> [f64; 3] {
let (latitude, _) = self.latitude_and_height();
[
EARTH_RATE * math::cos(latitude),
0.0,
-EARTH_RATE * math::sin(latitude),
]
}
pub(crate) fn correct(
&mut self,
position: [f64; 3],
velocity: [f64; 3],
misalignment: [f64; 3],
gyro_bias: [f64; 3],
accel_bias: [f64; 3],
) {
self.position = add(self.position, position);
self.velocity = add(self.velocity, velocity);
self.attitude = self.attitude.corrected_by(misalignment);
self.gyro_bias = add(self.gyro_bias, gyro_bias);
self.accel_bias = add(self.accel_bias, accel_bias);
}
}
fn curvature_radii(sin_lat: f64) -> (f64, f64) {
let a = Ellipsoid::WGS84.semi_major_axis().metres();
let e2 = Ellipsoid::WGS84.first_eccentricity_squared();
let denominator = 1.0 - e2 * sin_lat * sin_lat;
let east = a / math::sqrt(denominator);
let north = east * (1.0 - e2) / denominator;
(north, east)
}
#[must_use]
pub fn gravity_down(sin_lat: f64, height: f64) -> f64 {
let sin2 = sin_lat * sin_lat;
let surface = 9.780_325_335_9 * (1.0 + 0.001_931_852_652_41 * sin2)
/ math::sqrt(1.0 - 0.006_694_379_990_14 * sin2);
surface - 3.086e-6 * height
}
fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn scale(a: [f64; 3], by: f64) -> [f64; 3] {
[a[0] * by, a[1] * by, a[2] * by]
}
fn metres(value: f64) -> Distance {
Distance::from_metres(value).unwrap_or(Distance::ZERO)
}
fn metres_per_second(value: f64) -> Speed {
Speed::from_metres_per_second(value).unwrap_or(Speed::ZERO)
}