Skip to content

Strongly Typed Frames

Astrea supports a system of strongly typed frames which allows for compile-time checking of frame transformations, while also being reasonably simple to extend.

The Frame class is a compile-time interface that allows rules to be imposed on frame-supporting types, and frame transformations. A frame in Astrea is defined by an origin (typically a celestial body), and an axis. Currently, Astrea only supports pre-defined origins for static frames (that is, the center is inertially fixed), and a series of pre-defined axes. Future releases may allow for completely customized origins and axes.

// Astrea provides definitions for many commonly used frames
using ECI  = frames::earth::icrf;        // static
using ECEF = frames::earth::earth_fixed; // static (in code, not in time)
using RIC  = frames::dynamic::ric;       // dynamic

The CartesianVector class is a simple wrapper around a 3D vector, templated by the united-type and the frame the vector is defined in (or with respect to, depending). It also hosts several common vector operations, such as dot and cross products.

// Some length vector in ECI frame
CartesianVector<Length, ECI> rEci{ 1.0 * m, 2.0 * m, 3.0 * m };

auto rEciMag   = rEci.norm();
auto rEciUnit  = rEci.unit();
auto rEciDot   = rEci.dot(rEci);
auto rEciCross = rEci.cross(rEci);

std::cout << "rEci: " << rEci << std::endl;
std::cout << "rEciMag: " << rEciMag << std::endl;
std::cout << "rEciUnit: " << rEciUnit << std::endl;
std::cout << "rEciDot: " << rEciDot << std::endl;
std::cout << "rEciCross: " << rEciCross << std::endl;

// Outputs:
// rEci: [1 m, 2 m, 3 m]
// rEciMag: 3.74166 m
// rEciUnit: [0.267261, 0.534522, 0.801784]
// rEciDot: 14 m²
// rEciCross: [0 m², 0 m², 0 m²]

Conversions to/from a static (compile-time) frame, are handled by the in_frame method, templated to the frame we'd like to convert to. This frame looks for an acceptable specialization of the get_dcm method and uses the output direction cosine matrix to perform the vector transformation in either direction.

// Astrea provides many static frame conversions
CartesianVector<Length, ECEF> rEcefJ2000 = rEci.in_frame<ECEF>(J2000);
CartesianVector<Length, ECEF> rEcef = rEci.in_frame<ECEF>(J2000 + hours(12));

std::cout << std::endl << "Position in ECI: " << rEci << std::endl;
std::cout << "Position in ECEF @ J2000: " << rEcefJ2000 << std::endl;
std::cout << "Position in ECEF @ J2000 + 12 hours: " << rEcef << std::endl;

// Outputs:
// Position in ECI: [1 m, 2 m, 3 m]
// Position in ECEF @ J2000: [2.14832 m, -0.620261 m, 3 m]
// Position in ECEF @ J2000 + 12 hours: [-2.15358 m, 0.601759 m, 3 m]
Implicit frame switches are not allowed, but can be forced in special circumstances
// CartesianVector<Length, ECEF> rEcefImplicit = rEci; // Compiler will fail!
CartesianVector<Length, ECEF> rEcefForced = rEci.force_frame_conversion<ECEF>();
Users are also able to define their own frames and associated transformations.
// Frames do not necessarily need to be fully defined to be used
class MyFrame;
CartesianVector<Length, MyFrame> rCustom{ 1.0 * m, 2.0 * m, 3.0 * m };

// But the definition needs to be complete to use frame transformations
// CartesianVector<Length, ECI> rEcef = rCustom.in_frame<ECI>(J2000); // Compiler will fail!

For complex, time-dependent frames, such as those attached to a payload, or vehicle, the frames must be explicitly instantiated to call any vector transformations. They are not required to declare the vector type, however, transformation to/from dynamic frames are not allowed without an instance of the dynamic frame. Dynamic frames can either be attached to a FrameReference object (such as a spacecraft), or defined instantaneously at a specific state.

// RadiusVector<Frame_T> = CartesianVector<Distance, Frame_T>
RadiusVector<RIC> rRic = { 1.0 * km, 2.0 * km, 3.0 * km };

Spacecraft frameParent;
RIC dynamicRicFrame(&frameParent);                              // RIC frame attached to a spacecraft
RIC instantaneousRicFrame = RIC::instantaneous(posVec, velVec); // RIC frame defined at a specific time

// RadiusVector<ECI> rEciFromRic = rRic.in_frame<ECI>(J2000); // No RIC frame instance at compile time: compiler will fail!
RadiusVector<ECI> rotatedrRic   = instRicFrame.rotate_out_of_this_frame(rRic, date);       // DCM * r
RadiusVector<ECI> convertedrRic = instRicFrame.convert_from_this_frame(rRic, date);        // DCM * r + framePos
RadiusVector<RIC> rRic2         = instRicFrame.rotate_into_this_frame(rotatedrRic, date);  // DCM^T * r
RadiusVector<RIC> rRic3         = instRicFrame.convert_to_this_frame(convertedrRic, date); // DCM_T * (r - framePos)

std::cout << "RIC frame parent position: " << posECI << std::endl;
std::cout << "RIC frame parent velocity: " << velEci << std::endl;
std::cout << "Position in RIC: " << rRic << std::endl;
std::cout << "Position rotated into ECI: " << rotatedrRic << std::endl;
std::cout << "Position w.r.t ECI: " << convertedrRic << std::endl;
std::cout << "Rotated back into RIC: " << rRic2 << std::endl;
std::cout << "Transformed back w.r.t RIC: " << rRic3 << std::endl << std::endl;

// Outputs:
// RIC frame parent position:   [1 km, 0 km, 0 km]
// RIC frame parent velocity:   [0 km/s, 1 km/s, 0 km/s]
// Position in RIC:             [1 km, 2 km, 3 km]
// Position rotated into ECI:   [1 km, 2 km, 3 km]
// Position w.r.t ECI:          [2 km, 2 km, 3 km]
// Rotated back into RIC:       [1 km, 2 km, 3 km]
// Transformed back w.r.t RIC:  [1 km, 2 km, 3 km]