sguaba

repository·main·Indexed 19 days ago

https://github.com/helsing-ai/sguaba

A Rust library providing type-safe rigid body transforms and spatial math. It uses generic coordinate systems to prevent the accidental misuse of coordinates from different spaces, supporting base systems such as WGS84, ECEF, NED, FRD, and ENU. The library includes a `system!` macro for defining custom coordinate systems and provides both engineering-focused and math-focused APIs for performing coordinate conversions and pose calculations.

Tokens
11.9K
Snippets
37
Records
49
Agent score
66%

What's inside sguaba

  1. Core concepts of Coordinate and Vector types

    main

    The library uses Coordinate and Vector types to represent points and vectors. To prevent accidental misuse of coordinates from different spaces, these types are generic over a CoordinateSystem.

    You can define custom coordinate systems with specific semantics using the system! macro. This allows you to distinguish between different instances of the same base system (e.g., distinguishing between PlaneFrd and EmitterFrd even if both are based on the FRD system).

    Supported base coordinate systems include:

    • Wgs84: Latitude, longitude, and altitude.
    • Ecef: Earth-centered, Earth-fixed Cartesian coordinates.
    • Ned: North, East, Down (local tangent plane).
    • Frd: Front, Right, Down (body frame).
    • Enu: East, North, Up (local tangent plane).
    // Define custom coordinate systems using the system! macro
    system!(struct PlaneFrd using FRD);
    system!(struct PlaneNed using NED);
    
    // Coordinates are now tied to these specific systems
    let coord = Coordinate::<PlaneFrd>::from_bearing(...);
  2. Use the math-focused API for transforms

    main

    The math submodule treats everything as a transform (isometry). You can perform spatial math by multiplying transforms and components together.

    Key patterns:

    • Pose calculation: A pose in a global frame can be calculated by multiplying a transform (e.g., ECEF to NED) by a local orientation.
    • Coordinate transformation: Apply a transform directly to a Coordinate using the * operator.

    Note on Safety: Constructing transforms from orientations via map_as_zero_in is unsafe and requires verifying that the orientation correctly represents the target frame's axes.

    // Calculate a pose in ECEF by multiplying the ECEF->NED transform by the NED orientation
    let ecef_to_plane_ned = unsafe { RigidBodyTransform::ecef_to_ned_at(&wgs84) };
    let pose_in_ecef = ecef_to_plane_ned * orientation_in_ned;
    
    // Create a transform from that pose to a specific body frame
    let ecef_to_frd = unsafe { pose_in_ecef.map_as_zero_in::<PlaneFrd>() };
    
    // Apply the transform to a coordinate
    let observation_in_ecef: Coordinate<Ecef> = ecef_to_frd * observation;
  3. Use the engineering-focused API for transforms

    main

    The engineering module provides high-level types like Pose (position + orientation) and Orientation to make spatial math more intuitive.

    To transform between systems using this API:

    1. Create a RigidBodyTransform between systems (e.g., ecef_to_ned_at).
    2. Map an orientation to a new coordinate system using .map_as_zero_in::<T>().
    3. Chain transforms using .and_then().
    4. Apply or invert transforms using .inverse_transform() or the * operator.

    Note on Safety: Many transform construction methods (like ecef_to_ned_at or map_as_zero_in) are marked unsafe because they require the developer to guarantee that the provided orientation or position correctly defines the axes/origin of the target coordinate system.

    // 1. Create a transform between ECEF and NED at a specific WGS84 location
    let ecef_to_plane_ned = unsafe { RigidBodyTransform::ecef_to_ned_at(&wgs84) };
    
    // 2. Map an orientation to a body frame (FRD)
    let plane_ned_to_plane_frd = unsafe { orientation_in_ned.map_as_zero_in::<PlaneFrd>() };
    
    // 3. Chain transforms
    let ecef_to_plane_frd = ecef_to_plane_ned.and_then(plane_ned_to_plane_frd);
    
    // 4. Transform a coordinate
    let observation_in_ecef = ecef_to_plane_frd.inverse_transform(observation);
  4. Define custom coordinate systems with system!

    main

    Use the system! macro to create new types that represent specific coordinate systems. This provides type safety by ensuring you don't mix up coordinates from different frames that happen to share the same underlying mathematical structure.

    Syntax: system!(struct <TypeName> using <BaseSystem>);

    system!(struct MyCustomSystem using NED);
  5. How Orientation works in engineering applications

    main

    In the engineering module, Orientation<In> represents an object's rotation in a coordinate system In, independent of its position.

    Every object is assumed to have "body axes" that define its orientation. By convention, the positive X body axis is "forward" (the direction of movement). The other two axes depend on the coordinate system convention:

    • FLU (Forward-Left-Up): Positive Y is Left, Positive Z is Up (common in ENU).
    • FRD (Forward-Right-Down): Positive Y is Right, Positive Z is Down (common in NED).

    An object's orientation is defined as the rotation required to align the reference system's axes with the object's body axes.

  6. Represent an object's position and orientation with Pose

    main

    A Pose<System> represents an object's state (both position and orientation) within a specific coordinate system.

    Key operations:

    • Creating a Pose: Use Pose::new(position, orientation) where position is a Coordinate and orientation is an Orientation.
    • Transforming Poses: You can transform a Pose from one system to another using RigidBodyTransform::inverse_transform(pose).
    • Extracting components: Use .position() to get the coordinate and .orientation() to get the rotation.
    • Calculating relative poses: To find the pose of object B relative to object A, you can invert A's world pose and transform B's world pose into it.
    // Creating a pose in a specific system (e.g., NED)
    let pose_in_ned = Pose::<PlaneNed>::new(
        Coordinate::from_nalgebra_point(position),
        orientation_in_ned,
    );
    
    // Finding the pose of plane B relative to plane A
    let plane_a_frd_to_ecef = unsafe { pose_plane_a_in_world.map_as_zero_in::<PlaneFrd>() }.inverse();
    let pose_plane_b_for_plane_a = plane_a_frd_to_ecef.inverse_transform(pose_plane_b_in_world);
  7. Transform between coordinate systems using RigidBodyTransform

    main

    You can perform spatial transformations between different coordinate systems using RigidBodyTransform. This allows you to convert coordinates (like a point in one frame) from one system to another.

    Common patterns include:

    • Chaining transformations: Use the and_then method or the * operator to combine multiple RigidBodyTransform instances.
    • Inverse transformations: Use .inverse() to get the transform in the opposite direction, or .inverse_transform(coord) to convert a coordinate from the target system back to the source system.
    • Mapping to a local frame: Use .map_as_zero_in::<TargetSystem>() to create a transformation that treats a specific orientation or pose as the origin/zero in a new coordinate system.
    // Example: Chaining transformations
    let ecef_to_ned = unsafe { RigidBodyTransform::<Ecef, PlaneNed>::ecef_to_ned_at(&wgs84) };
    let ned_to_frd = Rotation::tait_bryan_builder()
        .yaw(d(0.))
        .pitch(d(45.))
        .roll(d(0.))
        .build();
    
    let ecef_to_frd = ecef_to_ned * ned_to_frd;
    
    // Convert a coordinate from ECEF to FRD
    let observation_in_frd = ecef_to_frd.inverse_transform(observation_in_ecef);
  8. How coordinate systems and type safety work in sguaba

    main

    Sguaba uses Rust's type system to prevent the accidental misuse of coordinates from different frames of reference.

    • Coordinate<S>: Represents a point in coordinate system S.
    • Vector<S, D>: Represents a vector in coordinate system S with dimension D (e.g., length, velocity, acceleration).
    • CoordinateSystem: A trait defining the semantics of a frame. You can use built-in systems like Ecef, Wgs84, NedLike, FrdLike, or EnuLike.
    • system! macro: Allows you to define custom coordinate systems with specific semantics to distinguish between different instances of the same base type (e.g., distinguishing PlaneFrd from EmitterFrd).

    Because transforming between systems (e.g., ecef_to_ned_at) requires parameters that might be incorrect, these operations are marked as unsafe. Using unsafe here signals that the developer is responsible for ensuring the transformation parameters (like the origin location) correctly correspond to the target coordinate system.

  9. How Pose and Orientation work together

    main

    A Pose<In> describes both an object's position (Coordinate<In>) and its orientation (Orientation<In>) within a coordinate system In.

    Key relationships:

    • Construction: Use Pose::new(position, orientation) to combine a coordinate and an orientation.
    • Extraction: Use .position() to get the Coordinate<In> and .orientation() to get the Orientation<In>.
    • Interpolation: Use .lerp(rhs, t) to linearly interpolate between two poses (interpolating position and orientation separately).
    use sguaba::{system, engineering::{Orientation, Pose}, Coordinate};
    use uom::si::f64::{Angle, Length};
    
    system!(struct PlaneNed using NED);
    
    let pos = Coordinate::<PlaneNed>::origin();
    let ori = Orientation::<PlaneNed>::aligned();
    let pose = Pose::<PlaneNed>::new(pos, ori);
  10. How coordinate systems and conventions work

    main

    In sguaba, a Coordinate System is a type used to mark Coordinate and Vector instances. It is a zero-sized type that defines how the system behaves via its Convention.

    Key Abstractions

    • CoordinateSystem: The core trait. The Convention associated type determines if the system is NedLike, FrdLike, EnuLike, or RightHandedXyzLike. This convention provides semantic accessors (e.g., .north(), .east(), .down()) on coordinates and vectors.
    • HasComponents<Time>: Links a convention to a specific component struct (like NedComponents). The Time parameter (using typenum) allows the system to represent different physical dimensions (e.g., Z0 for length, N1 for velocity).
    • EquivalentTo<Other>: An unsafe trait used to indicate that two different coordinate system types are mathematically identical (the transform is the identity function). Implementing this allows using the .cast() method to convert between types without transformation logic.
  11. Define custom coordinate systems with the `system!` macro

    main

    Use the system! macro to define new coordinate systems. These are zero-sized types used to tag Coordinate and Vector types with their specific frame of reference. The macro automatically implements the CoordinateSystem trait and provides appropriate component structures.

    Supported conventions:

    • NED: North-East-Down (Right-handed, earth-bounded)
    • FRD: Front-Right-Down (Right-handed, observer-bounded)
    • ENU: East-North-Up (Right-handed, earth-bounded)
    • right-handed XYZ: Generic right-handed Cartesian system

    You can include attributes (like #[derive(Hash)]) and visibility modifiers directly in the macro call.

    // Define a NED-like system for a sensor
    system!(pub struct SensorNed using NED);
    
    // Define an ENU-like system for an observer
    system!(pub struct ObserverEnu using ENU);
    
    // Define an FRD-like system for an aircraft
    system!(pub struct AircraftFrd using FRD);
    
    // Define a generic XYZ system
    system!(pub struct LocalFrame using right-handed XYZ);
    
    // Define a system with extra derives
    system! {
        #[derive(Hash, PartialEq)]
        pub struct CustomSystem using NED
    }
  12. Construct WGS84 coordinates using the Builder pattern

    main

    To avoid argument order confusion, use Wgs84::builder() or Wgs84::build(Components { ... }). The builder pattern ensures that all required components (latitude, longitude, and altitude) are provided before the object can be built.

    Note: Latitude must be in the range [-90°, 90°] % 360°. If the provided latitude is outside this range, the builder will return None during the .latitude() step.

    // Using the builder pattern
    let location = Wgs84::builder()
        .latitude(deg(35.3619))?
        .longitude(deg(138.7280))
        .altitude(m(2294.0))
        .build();