Dynamics Models¶
Purpose¶
This specification defines the mathematical contract for LuPNT orbit dynamics,
with emphasis on the numerical Cartesian dynamics used by
NBodyDynamics. The goal is to make the state, epoch, unit, frame, and
force-model conventions explicit enough that propagation and filtering tests
can compare against the same equations.
The Cartesian rate assembly lives in
cpp/lupnt/dynamics/numerical_orbit_dynamics.cc (classes
NBodyDynamics, JToCartTwoBodyDynamics, CartesianTwoBodyDynamics);
the individual force-model accelerations live in
cpp/lupnt/environment/forces.cc / forces.h.
Unified Force-Model Configuration¶
Every scenario specifies its orbit force model with the same YAML force_model:
block, whether it is the shared truth model (world.force_model), an agent’s
dynamics:, or an estimator’s filter/truth dynamics. The block is a bodies:
list plus optional relativity and cannonball-SRP (CR / area / mass)
entries:
force_model:
bodies:
- MOON: {n: 20, m: 20} # spherical-harmonic gravity field to degree/order 20
- EARTH: {} # third body, point-mass (empty {} = no harmonics)
- SUN: {} # third body, point-mass
relativity: true # n-body post-Newtonian correction (see below)
CR: 1.0 # SRP: enables cannonball SRP with B_SRP = CR * area / mass
area: 0.002
mass: 1.0
Each bodies entry is BODY: {n: <degree>, m: <order>} for a
spherical-harmonic gravity field; an empty {} (or omitted n/m) selects
point-mass gravity. A body’s presence in the list is what enables its
contribution to the sum in Total N-Body Acceleration below.
The same block feeds two consumers so that world, agent, and filter dynamics stay consistent:
NBodyDynamics(Config&)(cpp/lupnt/dynamics/numerical_orbit_dynamics.cc) constructs a full dynamics object directly from the block — adding each body viaBody::CreateBody, setting the SRP/drag ballistic coefficients fromCR/CD,area,mass, and readingframe,units,autodiff, andrelativity.ParseForceModelSpec(const Config&)reads the same block into a lightweightForceModelSpec(moon_degree/moon_orderfrom theMOONentry,include_earth/include_sunfrom the presence of those bodies, plusrelativityand the SRP scalars) so per-example dynamics builders that need their own integrator tolerances or clock coupling can be fed from one consistent config surface. Bothrelativityanduse_relativityare accepted.
State, Epoch, Frame, and Unit Contract¶
The propagated Cartesian orbit state is
All components of \(r\), \(v\), and \(\dot{x}\) are expressed in
the configured integration frame frame_ and configured coherent
UnitSystem:
For NBodyDynamics, the integration variable \(t\) is a simulation time
offset. The ephemeris epoch used internally is
where \(t_\mathrm{LuPNT,0}\) is GetLupntEpoch(). Ephemeris and frame
queries are therefore evaluated at TDB seconds from the LuPNT epoch origin.
The model computes
Implemented by
cpp/lupnt/dynamics/numerical_orbit_dynamics.cc :: NBodyDynamics::ComputeRates.
The TDB epoch offset and the \(\dot{x} = [v;\,a]\) assembly are the first
and last lines of the routine:
VecX NBodyDynamics::ComputeRates(Real t, const State& rv) const {
Real t_tdb = t + GetLupntEpoch();
Vec3 r = rv.head(3);
Vec3 v = rv.tail(3);
Vec3 a = Vec3::Zero();
// ... sum force-model accelerations into a ...
Vec6 rv_dot;
rv_dot << v, a;
return rv_dot;
}
Unit Conversion¶
Historical LuPNT constants are SI-valued. When a model uses a non-SI
UnitSystem, dimensional values are converted through
where \(p\), \(q\), and \(s\) are the length, time, and mass powers of the quantity.
For example,
Planetary ephemerides are evaluated in the internal TDB-compatible coordinate scale and then converted to the requested unit system at the dynamics boundary.
Implemented by the unit-conversion helpers in the anonymous namespace of
cpp/lupnt/dynamics/numerical_orbit_dynamics.cc
(PositionFromSI / PositionToSI / VelocityToSI /
AccelerationFromSI / StateToSI), each a scaling by the UnitSystem
length/time powers:
Vec3 PositionFromSI(const Vec3& r, const UnitSystem& units) { return r / units.length; }
Vec3 VelocityToSI(const Vec3& v, const UnitSystem& units) {
return v * (units.length / units.time);
}
Vec3 AccelerationFromSI(const Vec3& a, const UnitSystem& units) {
return a * (units.time * units.time / units.length);
}
Point-Mass Gravity¶
For a perturbing body with gravitational parameter \(\mu_i\) and position \(s_i\) in the integration frame, LuPNT uses the relative point-mass acceleration
The second term removes the acceleration of the integration-frame origin when the frame origin is not the perturbing body. If \(s_i = 0\), only the central two-body term remains:
Implemented by
cpp/lupnt/environment/forces.cc :: AccelerationPointMass (called per
non-gravity-field body by NBodyDynamics::ComputeRates):
Vec3 AccelerationPointMass(const Vec3& r, const Vec3& s, Real GM) {
Vec3 d = r - s;
Vec3 a = Vec3::Zero();
if (s.norm() > EPS) a += s / pow(s.norm(), 3); // indirect (origin recoil) term
if (d.norm() > EPS) a += d / pow(d.norm(), 3); // direct attraction
a *= -GM;
return a;
}
Gravity-Field Acceleration¶
For a body with a configured spherical-harmonic gravity field, the spacecraft position is first rotated into the body-fixed frame:
The gravity field acceleration is computed in that fixed frame from unnormalized coefficients \(C_{nm}\) and \(S_{nm}\) up to configured degree and order:
The result is rotated back to the integration frame:
This path uses only the rotation component of the frame transform for accelerations.
Implemented by
cpp/lupnt/environment/forces.cc :: AccelarationGravityField (the
Montenbruck-Gill V_nm / W_nm harmonic recursion, templated on Real
for autodiff or double for fast propagation), driven by the gravity-field
branch of NBodyDynamics::ComputeRates:
// NBodyDynamics::ComputeRates -- rotate to body-fixed, evaluate field, rotate back
Vec3 r_bf = PositionFromSI(ConvertFrame(t_tdb, r_si, frame_, body.fixed_frame), units_);
a_bf = AccelarationGravityField<Real>(r_bf, grav.GM, grav.R, grav.CS, grav.n, grav.m);
auto [R_bf_to_frame, translation] = GetFrameRotationTranslation(t_tdb, body.fixed_frame, frame_);
a += R_bf_to_frame * a_bf;
J2 Cartesian Dynamics¶
The Cartesian J2 model augments two-body gravity with a zonal term evaluated about the body’s fixed spin axis, not about the integration-frame z-axis.
Let \(F\) be the integration frame and \(B\) be the body-fixed frame
configured on JToCartTwoBodyDynamics. These frames must share the same
origin. The spacecraft position is first rotated into the body-fixed frame:
The J2 acceleration is computed in the body-fixed frame:
where \([x,y,z]^T = r_B\) and \(r = \lVert r_B \rVert = \lVert r_F \rVert\).
The acceleration is then rotated back to the integration frame:
Setting the body-fixed frame equal to the integration frame yields the inertial-axis J2 convention, useful for reference comparisons that use that approximation.
Implemented by
cpp/lupnt/dynamics/numerical_orbit_dynamics.cc :: JToCartTwoBodyDynamics::ComputeRates:
Vec3 r_bf = R_frame_to_body_fixed * r;
Real aux1 = -3.0 / 2.0 * GM_ * J2_ * pow(R_body_, 2.0) / pow(r_norm, 5.0);
Real aux2 = 5.0 * pow(r_bf(2) / r_norm, 2.0);
a_J2_bf(0) = aux1 * (1.0 - aux2) * r_bf(0);
a_J2_bf(1) = aux1 * (1.0 - aux2) * r_bf(1);
a_J2_bf(2) = aux1 * (3.0 - aux2) * r_bf(2);
rv_dot.tail(3) += R_frame_to_body_fixed.transpose() * a_J2_bf;
The plain two-body term \(-\mu r/\lVert r\rVert^3\) is
cpp/lupnt/dynamics/numerical_orbit_dynamics.cc :: CartesianTwoBodyDynamics::ComputeRates.
Solar Radiation Pressure¶
Solar radiation pressure is enabled by SetUseSrp or by setting the SRP
ballistic coefficient
For a Sun vector \(r_\odot\) in the integration frame, the SRP acceleration is
The illumination factor \(\nu\) is the apparent-disk overlap shadow function:
with \(\nu = 0\) in umbra, \(\nu = 1\) in full sunlight, and partial values in penumbra.
Let
The spacecraft is fully illuminated when
It is in umbra when the occulting disk covers the solar disk:
Otherwise, \(\nu\) is one minus the overlap area of the two apparent disks divided by the solar disk area.
The cannonball acceleration is
cpp/lupnt/environment/forces.cc :: AccelerationSolarRadiation and the
shadow factor \(\nu\) is
cpp/lupnt/environment/forces.cc :: ShadowFunction (wrapped by
Illumination); NBodyDynamics::ComputeRates multiplies them together.
The two apparent-radius angles and the umbra / penumbra tests are:
// ShadowFunction: apparent radii of Sun (a) and occulting body (b), separation (c)
Real a = asin(ClampUnit(R_sun / rho_sun_norm));
Real b = asin(ClampUnit(R_body / r_norm));
Real c = acos(ClampUnit(-r.dot(rho_sun) / (r_norm * rho_sun_norm)));
if (c >= a + b) return 1.0; // fully illuminated
if (c <= abs(b - a)) { if (b >= a) return 0.0; } // umbra / annular
Note
ClampUnit returns a strictly-interior constant \(\pm(1-10^{-12})\)
at the domain edges of asin / acos so the autodiff derivative stays
finite across a grazing shadow boundary; this is what lets the SRP
state-transition matrix be formed analytically (see the ClampUnit /
Clamp01 comments in forces.cc).
Atmospheric Drag¶
Earth atmospheric drag uses a Harris-Priester density model. The ballistic coefficient is
In the true-of-date frame,
and the drag acceleration is
The implementation converts the propagated state to SI for the drag model and then converts the resulting acceleration back to the configured unit system.
Implemented by
cpp/lupnt/environment/forces.cc :: AccelerationDrag (with the density from
cpp/lupnt/environment/forces.cc :: DensityHarrisPriester), invoked for
Earth by NBodyDynamics::ComputeRates:
Vec3 v_rel = v_tod - omega.cross(r_tod); // Earth-relative velocity
Real v_abs = v_rel.norm();
Real dens = DensityHarrisPriester(mjd_tt, r_tod);
Vec3 a_tod = -0.5 * bcoeff_drag * dens * v_abs * v_rel * KM_M;
return T.transpose() * a_tod; // back to inertial
DensityHarrisPriester interpolates the tabulated min/max density profiles
exponentially in altitude and weights them by the diurnal-bulge factor
\(\cos^{n}(\psi/2)\) before returning kg/m^3.
Relativistic Orbit Correction¶
When relativity is enabled, NBodyDynamics adds the full n-body
point-mass relativistic perturbative acceleration of Moyer (2000), Eq. (4-26) –
the parameterized post-Newtonian (PPN) Einstein–Infeld–Hoffmann acceleration in
the Solar-System barycentric frame. The leading Newtonian point-mass term (the
1 in Moyer’s first brace) is removed so this quantity adds to the Newtonian
gravity already summed above.
Let \(i\) denote the spacecraft and \(j,k,l\) the configured massive bodies, with barycentric positions \(r\), velocities \(\dot r\), gravitational parameters \(\mu\), and pairwise distances \(r_{ij}=\lVert r_i-r_j\rVert\). With PPN parameters \(\beta\) and \(\gamma\) (both unity in general relativity) and speed of light \(c\),
The perturbing-body accelerations \(\ddot r_j\) are taken from the Newtonian n-body model, \(\ddot r_j=\sum_{k\neq j}\mu_k(r_k-r_j)/r_{jk}^3\); terms of order \(1/c^4\) are dropped, so this Newtonian value is sufficient (Moyer, p. 4-21).
Because the equation is written in barycentric coordinates, position
differences are frame-independent but the absolute velocities in the
\(1/c^2\) terms are not. NBodyDynamics therefore gathers
Solar-System-barycenter (BodyId::SSB) referenced states of the spacecraft and
of every configured body before evaluating it.
For a single perturbing body at rest, the model above reduces exactly to the one-body Schwarzschild isotropic form (Moyer Eq. (4-61)),
which is the regression check in
cpp/test/dynamics/test_relativity_nbody.cc. Frame-dragging (Lense–Thirring)
and geodesic-precession terms are not included in the current dynamics model.
Implemented by
cpp/lupnt/environment/forces.cc :: AccelerationRelativisticNBody and wired
into NBodyDynamics::ComputeRates and ComputeAccelerations through the
private RelativisticNBodyAcceleration helper, which assembles the
SSB-referenced body states.
Note
Reference: T. D. Moyer, Formulation for Observed and Computed Values of Deep Space Network Data Types for Navigation, JPL Publication 00-7 (DESCANSO Monograph 2), 2000, Eqs. (4-26) and (4-61).
Total N-Body Acceleration¶
The total acceleration assembled by NBodyDynamics is
with optional terms omitted when their corresponding switches are disabled or when no applicable body is configured.
Assembled by
cpp/lupnt/dynamics/numerical_orbit_dynamics.cc :: NBodyDynamics::ComputeRates
(the per-body loop adds gravity, SRP, and drag; the relativity terms are added
after the loop). For a per-term breakdown of the same sum, see
cpp/lupnt/dynamics/numerical_orbit_dynamics.cc :: NBodyDynamics::ComputeAccelerations,
which returns a map keyed "<BODY>_gravity", "srp", "drag",
"relativity", etc., whose values sum to the acceleration part of
ComputeRates.
State Transition Matrix Convention¶
When STM propagation is requested, the integrator propagates the variational system for the same state ordering:
For a Cartesian state,
The finite-dimensional contract is that rows and columns follow the order
Requested through
cpp/lupnt/dynamics/numerical_orbit_dynamics.cc :: NBodyDynamics::Propagate
(the MatXd* stm overload), which requires autodiff to be enabled:
State NBodyDynamics::Propagate(const State& x0, Real t0, Real tf, const State* u, MatXd* stm) {
LUPNT_CHECK(use_ad_, "Autodiff not enabled", "NBodyDynamics");
return NumericalDynamics::Propagate(x0, t0, tf, u, stm);
}
See Numerical Integration for how the sensitivity matrix is actually formed
(Integrator::Propagate(..., MatXd* J) via JacobianParallel).
Current Model Boundaries¶
The following are intentional boundaries of the current implementation:
NBodyDynamicsexpects its configured bodies to use the sameUnitSystemas the dynamics model.SetUnitsmust be called before adding bodies.Relativity is evaluated from the closest configured planet-like body, not from every gravitating body.
SRP uses a cannonball coefficient and spherical occulting bodies.
Drag is currently Earth-specific.
Force-model accelerations are expressed in the integration frame and configured unit system before being returned by
ComputeRates.