sguaba/lib.rs
1//! This library provides hard-to-misuse rigid body transforms (aka "spatial math") for engineers
2//! with other things to worry about than linear algebra.
3//!
4//! First and foremost, the library provides [`Coordinate`] and [`Vector`] types for representing
5//! points and vectors in coordinate spaces respectively. They are all generic over a
6//! [`CoordinateSystem`] so that coordinates from one system cannot (easily) be incorrectly misused
7//! as though they were in a different one. The [`system!`] macro allows you to define additional
8//! coordinate systems with particular semantics (eg, [`NedLike`] or [`FrdLike`]) such that you can
9//! distinguish between coordinates in, say, `PlaneFrd` and `EmitterFrd`.
10//!
11//! To move between coordinate systems, you'll want to use the mathematical constructs from the
12//! [`math`] submodule like [rigid body transforms](math::RigidBodyTransform) and
13//! [rotations](math::Rotation).
14//!
15//! Now _technically_, those are all you need to express anything in coordinate system math,
16//! including poses and orientation. It turns out that everything is an isometry if you think hard
17//! enough about it. But if your brain is more used to thinking about orientation and poses (ie,
18//! position + orientation), you'll want to make use of the [`engineering`] module which has
19//! easier-to-grasp types like [`Pose`](engineering::Pose) and
20//! [`Orientation`](engineering::Orientation).
21//!
22//! # Primer on coordinate systems
23//!
24//! If you're new to working with coordinate systems and frames of reference, you may wonder what
25//! coordinate systems there are in the first place, and how they differ. There are a wide variety
26//! of ways to describe the locations of objects in space, all of which have their own slight
27//! peculiarities about representation and conversion. At the time of writing, the coordinate
28//! systems this crate supports are: [WGS84] (latitude and longitude), [ECEF] ("Earth-centered,
29//! Earth-fixed"), [NED] ("North, East, Down"), [FRD] ("Front, Right, Down"), and [ENU] ("East,
30//! North, Up").
31//!
32//! [WGS84] ([`Wgs84`]) and [ECEF] ([`Ecef`]) are both Earth-bound
33//! coordinate systems that describe points in space on or near Earth. They do this by describing
34//! positions relative to Earth's major and minor axes, often by making slightly simplifying
35//! assumptions about the Earth's shape. WGS84 does this by using latitude and longitude (degrees
36//! north/south of the equator and east-west of the prime meridian), while ECEF does it by placing
37//! a coordinate system at the center of the earth and locating [the X, Y, and Z axes][axes]
38//! towards specific points on the Earth's surface. One can convert between them [without too much
39//! trouble][trouble].
40//!
41//! [NED] ([`NedLike`]), [FRD] ([`FrdLike`]), and [ENU] ([`EnuLike`]) on the other hand are "local"
42//! coordinate systems that are
43//! descriptions of relative positions to the location of the observer. [NED] and [ENU] are
44//! Earth-bound in that they describe positions in terms of how far North, East, and Down (for NED)
45//! or East, North, and Up (for ENU) they are relative to the observer. [FRD], meanwhile, is a
46//! "body frame", and just describes positions relative to the observer's concept of Forward (eg,
47//! the direction pointing in the same direction as the nose of a plane), Right (eg, the direction
48//! 90° to the right when viewing along Forward), and Down (eg, down through the belly of the
49//! plane). Converting between [FRD] and [NED] or [ENU] usually requires knowing the orientation of
50//! the observer relative to North, East, and Down/Up, and converting between [NED]/[ENU] and
51//! [ECEF] (or [WGS84]) requires also knowing the position of the observer in Earth-bound
52//! coordinates.
53//!
54//! [WGS84]: https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84
55//! [ECEF]: https://en.wikipedia.org/wiki/Earth-centered,_Earth-fixed_coordinate_system
56//! [NED]: https://en.wikipedia.org/wiki/Local_tangent_plane_coordinates#Local_north,_east,_down_(NED)_coordinates
57//! [ENU]: https://en.wikipedia.org/wiki/Local_tangent_plane_coordinates#Local_east,_north,_up_(ENU)_coordinates
58//! [FRD]: https://en.wikipedia.org/wiki/Body_relative_direction
59//! [axes]: https://en.wikipedia.org/wiki/Axes_conventions
60//! [trouble]: https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#Coordinate_system_conversion
61//!
62//! # Use of unsafe
63//!
64//! Sguaba requires you to use `unsafe` in order to construct most transformations between
65//! coordinate systems (eg,
66//! [`RigidBodyTransform::ecef_to_ned_at`](math::RigidBodyTransform::ecef_to_ned_at) or
67//! [`Orientation::map_as_zero_in`](engineering::Orientation::map_as_zero_in)). This is because
68//! once one of these transforms have been constructed, they allow you to freely convert between
69//! the _types_ representing each coordinate system. Thus, if a transform is constructed with
70//! incorrect parameters, such as giving a coordinate to `ecef_to_ned_at` that does not correspond
71//! to the location of the origin of the `To` [`NedLike`] system, _type_ safety
72//! would be violated. This is a slight abuse of Rust's `unsafe` mechanism, which tends to focus on
73//! memory safety, but has proven to be valuable in highlighting areas where frame of reference
74//! bugs are most likely to manifest.
75//!
76//! # Temporal drift
77//!
78//! Sguaba does not attempt to provide time-variant type safety. For example, if you have a
79//! `Coordinate<PlaneNed>` for the position of a moving plane, there are really an infinite number
80//! of NED systems, one for each point along the plane's trajectory. Trivially, (0, 0, 0) in the
81//! plane's NED frame at time t = 0 is not at the same real-world location as (0, 0, 0) in the
82//! frame at time t = 1. But Sguaba does not let you express this in the type system as you'd end
83//! up with an infinite number of coordinate system types. Instead, the expectation is that your
84//! code ensures that it does not mix up frames of reference from different points in time. This is
85//! unfortunate, and a potential source of errors, but not one with an obvious remedy.
86//!
87//! Perhaps surprisingly, this is _also_ the case for non-local frames like ECEF and WGS84. Due to
88//! plate tectonics, a spot measured relative to a point on earth's _surface_ technically "moves"
89//! over time. Consider, for example, the Eiffel tower, which is located at
90//! 48.85851298170608°N, 2.2944746521697468°E (according to Google Maps at least). Since the
91//! Eurasian plate drifts over time, if you measured its location a millennium from now, it would
92//! technically be at a different place relative to, say, the Statue of Liberty. So, how does that
93//! affect its latitude and longitude? The answer is that "it depends". There isn't really just
94//! _one_ ECEF/WGS84, there are multiple, each defined through an [International Terrestrial
95//! Reference Frame (ITRF)][ITRF], and over time, WGS84 [is "updated"][WGSup] to use a newer ITRF.
96//! At the time of writing, WGS84 is "really" [G2296], which was released on 7 January 2024, which
97//! is in turn aligned to [ITRF2020]. Sguaba's [`Ecef`] and [`Wgs84`] types do _not_ attempt to capture
98//! this time-variance, just as the [`NedLike`], [`EnuLike`], and [`FrdLike`] systems do not
99//! attempt to capture that these systems also change over time as the observer's location changes.
100//!
101//! [ITRF]: https://en.wikipedia.org/wiki/International_Terrestrial_Reference_System_and_Frame
102//! [WGSup]: https://en.wikipedia.org/wiki/World_Geodetic_System#Updates_and_new_standards
103//! [G2296]: https://earth-info.nga.mil/php/download.php?file=WGS%2084(G2296).pdf
104//! [ITRF2020]: https://itrf.ign.fr/en/solutions/ITRF2020
105//!
106//! # Examples
107//!
108//! Assume that a pilot of a plane observes something out of their window at a given bearing and
109//! elevation angles (ie, measured in the plane's [FRD](systems::FrdLike)) and wants to know the
110//! location of that thing in terms of Earth-bound Latitude and Longitude coordinates (ie,
111//! [WGS84](systems::Wgs84)).
112//!
113//! ```
114//! # use sguaba::{system, Bearing, Coordinate, engineering::Orientation, systems::Wgs84};
115//! use uom::si::f64::{Angle, Length};
116//! use uom::si::{angle::degree, length::meter};
117//!
118//! // FRD and NED systems are "local" coordinate systems, meaning a given coordinate in the FRD of
119//! // one plane will have a completely different coordinate if it were to be expressed in the FRD
120//! // of another. so, to guard against accidentally getting them mixed up, we construct a new type
121//! // for this plane's FRD and NED:
122//!
123//! // the pilot observes things in FRD of the plane
124//! system!(struct PlaneFrd using FRD);
125//!
126//! // the pilot's instruments indicate the plane's orientation in NED
127//! system!(struct PlaneNed using NED);
128//!
129//! // what the pilot saw:
130//! let observation = Coordinate::<PlaneFrd>::from_bearing(
131//! Bearing::builder()
132//! // clockwise from forward
133//! .azimuth(Angle::new::<degree>(20.))
134//! // upwards from straight-ahead
135//! .elevation(Angle::new::<degree>(10.)).expect("elevation is in [-90, 90]")
136//! .build(),
137//! Length::new::<meter>(400.), // at this range
138//! );
139//!
140//! // where the plane was at the time (eg, from GPS):
141//! let wgs84 = Wgs84::builder()
142//! .latitude(Angle::new::<degree>(12.)).expect("latitude is in [-90, 90]")
143//! .longitude(Angle::new::<degree>(30.))
144//! .altitude(Length::new::<meter>(1000.))
145//! .build();
146//!
147//! // where the plane was facing at the time (eg, from instrument panel);
148//! // expressed in yaw, pitch, roll relative to North-East-Down:
149//! let orientation_in_ned = Orientation::<PlaneNed>::from_tait_bryan_angles(
150//! Angle::new::<degree>(8.), // yaw
151//! Angle::new::<degree>(45.), // pitch
152//! Angle::new::<degree>(0.), // roll
153//! );
154//! ```
155//!
156//! From there, there are two possible paths forward, one using an API that
157//! will appeal more to folks with an engineering background, and one that
158//! will appeal more to a math-oriented crowd. We'll explore each in turn.
159//!
160//! ## Using the engineering-focused API
161//!
162//! In the ["engineering-focused" API](engineering), we can directly talk about an object's
163//! orientation and its "pose" (ie, position + orientation) in the world. Using these, we can
164//! transform between different coordinate systems to go from `PlaneFrd` to `PlaneNed` to
165//! [`Ecef`] (cartesian world location) to [`Wgs84`]. Note that one
166//! must know the observer's body orientation relative to NED to go from FRD to NED, and the
167//! observer's ECEF position to go from NED to ECEF.
168//!
169//! ```
170//! # use sguaba::{system, Bearing, builder::{bearing::Components as BearingComponents, wgs84::Components as Wgs84Components}, Coordinate, engineering::Orientation, math::RigidBodyTransform, systems::Wgs84};
171//! # use uom::si::f64::{Angle, Length};
172//! # use uom::si::{angle::degree, length::meter};
173//! # system!(struct PlaneFrd using FRD);
174//! # system!(struct PlaneNed using NED);
175//! # let observation = Coordinate::<PlaneFrd>::from_bearing(
176//! # Bearing::build(BearingComponents {
177//! # azimuth: Angle::new::<degree>(20.),
178//! # elevation: Angle::new::<degree>(10.),
179//! # }).expect("elevation is in [-90, 90]"),
180//! # Length::new::<meter>(400.), // at this range
181//! # );
182//! # let wgs84 = Wgs84::build(Wgs84Components {
183//! # latitude: Angle::new::<degree>(12.),
184//! # longitude: Angle::new::<degree>(30.),
185//! # altitude: Length::new::<meter>(1000.)
186//! # }).expect("latitude is in [-90, 90]");
187//! # let orientation_in_ned = Orientation::<PlaneNed>::from_tait_bryan_angles(
188//! # Angle::new::<degree>(8.), // yaw
189//! # Angle::new::<degree>(45.), // pitch
190//! # Angle::new::<degree>(0.), // roll
191//! # );
192//! #
193//! // to convert between NED and ECEF, we need a transform between the two.
194//! // this transform depends on where on the globe you are, so it takes the WGS84 position:
195//! // SAFETY: we're claiming that `wgs84` is the location of `PlaneNed`'s origin.
196//! let ecef_to_plane_ned = unsafe { RigidBodyTransform::ecef_to_ned_at(&wgs84) };
197//!
198//! // to convert between FRD (which the observation was made in) and NED,
199//! // we just need the plane's orientation, which we have from the instruments!
200//! // SAFETY: we're claiming that the given NED orientation makes up the axes of `PlaneFrd`.
201//! let plane_ned_to_plane_frd = unsafe { orientation_in_ned.map_as_zero_in::<PlaneFrd>() };
202//!
203//! // these transformations can be chained to go from ECEF to NED.
204//! // this chaining would fail to compile if you got the arguments wrong!
205//! let ecef_to_plane_frd = ecef_to_plane_ned.and_then(plane_ned_to_plane_frd);
206//!
207//! // this transform lets you go from ECEF to FRD, but transforms work both ways,
208//! // so we can apply it in inverse to take our `Coordinate<PlaneFrd>` and produce
209//! // a `Coordinate<Ecef>`:
210//! let observation_in_ecef = ecef_to_plane_frd.inverse_transform(observation);
211//!
212//! // we can then turn that into WGS84 lat/lon/altitude!
213//! println!("{:?}", observation_in_ecef.to_wgs84());
214//! ```
215//!
216//! ## Using the math-focused API
217//!
218//! In the ["math-focused" API](math), everything is represented in terms of transforms between
219//! coordinate systems and the components of those transforms. For example:
220//!
221//! ```
222//! # use sguaba::{system, Bearing, builder::{bearing::Components as BearingComponents, wgs84::Components as Wgs84Components}, Coordinate, engineering::Orientation, math::RigidBodyTransform, systems::{Ecef, Wgs84}};
223//! # use uom::si::f64::{Angle, Length};
224//! # use uom::si::{angle::degree, length::meter};
225//! # system!(struct PlaneFrd using FRD);
226//! # system!(struct PlaneNed using NED);
227//! # let observation = Coordinate::<PlaneFrd>::from_bearing(
228//! # Bearing::build(BearingComponents {
229//! # azimuth: Angle::new::<degree>(20.),
230//! # elevation: Angle::new::<degree>(10.),
231//! # }).expect("elevation is in [-90, 90]"),
232//! # Length::new::<meter>(400.), // at this range
233//! # );
234//! # let wgs84 = Wgs84::build(Wgs84Components {
235//! # latitude: Angle::new::<degree>(12.),
236//! # longitude: Angle::new::<degree>(30.),
237//! # altitude: Length::new::<meter>(1000.)
238//! # }).expect("latitude is in [-90, 90]");
239//! # let orientation_in_ned = Orientation::<PlaneNed>::from_tait_bryan_angles(
240//! # Angle::new::<degree>(8.), // yaw
241//! # Angle::new::<degree>(45.), // pitch
242//! # Angle::new::<degree>(0.), // roll
243//! # );
244//! #
245//! // we need to find the ECEF<>NED transform for the plane's location
246//! // SAFETY: we're claiming that `wgs84` is the location of `PlaneNed`'s origin.
247//! let ecef_to_plane_ned = unsafe { RigidBodyTransform::ecef_to_ned_at(&wgs84) };
248//! // the plane's orientation in NED is really a rotation and translation in ECEF
249//! let pose_in_ecef = ecef_to_plane_ned * orientation_in_ned;
250//! // that rotation and translation is exactly equal to the FRD of the plane
251//! // we could also have just constructed this rotation directly instead of an `Orientation`
252//! // SAFETY: `PlaneNed` is the orientation of the plane's FRD body axes (ie, `PlaneFrd`).
253//! let ecef_to_frd = unsafe { pose_in_ecef.map_as_zero_in::<PlaneFrd>() };
254//! // and we can apply that transform to the original observation to get it in ECEF
255//! let observation_in_ecef: Coordinate<Ecef> = ecef_to_frd * observation;
256//! // which we can then turn into WGS84 lat/lon/altitude!
257//! println!("{:?}", observation_in_ecef.to_wgs84());
258//! ```
259
260use typenum::{P1, Z0};
261use uom::{
262 si::{f64::V, Quantity, ISQ, SI},
263 ConstZero, Kind,
264};
265
266// for easier linking from docs above
267#[cfg(doc)]
268use systems::{Ecef, EnuLike, FrdLike, NedLike, Wgs84};
269
270#[macro_use]
271mod coordinate_systems;
272
273mod coordinates;
274mod directions;
275mod geodetic;
276mod util;
277mod vectors;
278
279pub mod engineering;
280pub mod math;
281
282pub(crate) type Point3 = nalgebra::Point3<f64>;
283pub(crate) type Vector3 = nalgebra::Vector3<f64>;
284pub(crate) type Quaternion = nalgebra::Quaternion<f64>;
285pub(crate) type UnitQuaternion = nalgebra::Unit<Quaternion>;
286pub(crate) type Isometry3 = nalgebra::Isometry3<f64>;
287
288#[doc(hidden)]
289pub type AngleForBearingTrait = uom::si::f64::Angle;
290
291#[doc(hidden)]
292pub const ZERO_FOR_BEARING: AngleForBearingTrait = AngleForBearingTrait::ZERO;
293
294/// Type alias for a quantity whose unit is one-dimensional in length and has a zero- or
295/// negative-valued time dimension (ie, [`uom::si::f64::Length`], [`uom::si::f64::Velocity`], and
296/// [`uom::si::f64::Acceleration`]).
297///
298/// Only used internally.
299type LengthPossiblyPer<Time> = Quantity<ISQ<P1, Z0, Time, Z0, Z0, Z0, Z0, dyn Kind>, SI<V>, V>;
300
301// re-structure our impots slightly to better match user expectation
302/// Well-known coordinate systems and conventions.
303pub mod systems {
304 pub use super::coordinate_systems::{
305 BearingDefined, Ecef, EnuLike, EquivalentTo, FrdLike, NedLike, RightHandedXyzLike,
306 };
307 pub use super::coordinate_systems::{
308 EnuComponents, FrdComponents, HasComponents, NedComponents, XyzComponents,
309 };
310 pub use super::geodetic::Wgs84;
311}
312pub use coordinate_systems::CoordinateSystem;
313pub use coordinates::Coordinate;
314pub use directions::Bearing;
315pub mod builder {
316 /// Used to indicate that a partially-constructed value is missing a given component.
317 #[derive(Debug, Clone, Copy)]
318 pub struct Unset;
319
320 /// Used to indicate that a partially-constructed value has a given component set.
321 #[derive(Debug, Clone, Copy)]
322 pub struct Set;
323
324 pub mod vector {
325 pub use crate::vectors::Builder;
326 }
327 pub mod coordinate {
328 pub use crate::coordinates::Builder;
329 }
330 pub mod bearing {
331 pub use crate::directions::{
332 Builder, Components, HasAzimuth, HasElevation, MissingAzimuth, MissingElevation,
333 };
334 }
335 pub mod wgs84 {
336 pub use crate::geodetic::{
337 Builder, Components, HasAltitude, HasLatitude, HasLongitude, MissingAltitude,
338 MissingLatitude, MissingLongitude,
339 };
340 }
341 pub mod tait_bryan {
342 pub use crate::math::tait_bryan_builder::{
343 Complete, NeedsPitch, NeedsRoll, NeedsYaw, TaitBryanBuilder,
344 };
345 }
346}
347
348/// Convenience re-exports for working with different vector units
349pub mod vector {
350 pub use crate::Vector;
351
352 /// Type alias for a length vector (meters)
353 pub type LengthVector<In> = Vector<In, typenum::Z0>;
354
355 /// Type alias for a velocity vector (meters per second)
356 pub type VelocityVector<In> = Vector<In, typenum::N1>;
357
358 /// Type alias for an acceleration vector (meters per second squared)
359 pub type AccelerationVector<In> = Vector<In, typenum::N2>;
360}
361
362pub use vectors::Vector;