Example 1: Propagating an ELFO Orbit

Initialise a lunar Elliptical Frozen Orbit (ELFO) from classical orbital elements in the Moon-centred Orbital Plane (OP) frame, convert the state to the Moon-Centred Inertial (MCI) frame, propagate it for 30 days with full N-body dynamics and Solar Radiation Pressure, and inspect the long-term libration of the osculating orbital elements. The OP frame tracks the Earth’s instantaneous orbit plane about the Moon, the natural frame for the frozen-orbit parameters of Ely (2005).

Mirrors Example 1: Propagating an ELFO Orbit.

// Example 1: Propagating an ELFO Orbit
// ------------------------------------------------------------------------------
// C++ counterpart of python/examples/ex1_propagate_orbit.ipynb.
//
// Initialise a lunar Elliptical Frozen Orbit (ELFO) from classical orbital
// elements (COE) defined in the Moon-centred Orbital Plane (OP) frame, convert
// the state to the Moon-Centred Inertial (MCI) frame, propagate it for 30 days
// with full N-body dynamics + Solar Radiation Pressure (SRP), and inspect the
// long-term libration of the osculating orbital elements.
//
// The OP frame tracks the instantaneous orbital plane of the Earth about the
// Moon, which is the natural frame in which the canonical frozen-orbit
// parameters (a, e, i, omega) of Ely (2005) are defined.

#include <iomanip>
#include <iostream>

#include "lupnt/lupnt.h"

using namespace lupnt;

int main() {
  // --- Epoch (LuPNT propagates in TAI seconds from J2000) --------------------
  const Real t0 = GregorianToTime(2009, 7, 15, 1, 0, 0);
  std::cout << "Epoch: " << TimeToGregorianString(t0) << "\n";

  // --- Initial state: ELFO in classical orbital elements (OP frame) ----------
  // COE layout: [a, e, i, RAAN, omega, M]; angles in radians, a in metres.
  const Real a = 6541.4e3;      // [m]   semi-major axis
  const Real e = 0.6;           // [-]   eccentricity
  const Real i = 56.2 * RAD;    // [rad] inclination
  const Real raan = 0.0 * RAD;  // [rad] right ascension of the ascending node
  const Real w = 90.0 * RAD;    // [rad] argument of perigee
  const Real M0 = 0.0 * RAD;    // [rad] mean anomaly
  Vec6 coe0_op(a, e, i, raan, w, M0);

  const Real period = GetOrbitalPeriod(a, GM_MOON);
  std::cout << "Orbital period: " << period / SECS_HOUR << " hours\n";

  // --- Convert COE -> Cartesian (OP), then OP -> MCI at t0 -------------------
  const Vec6 rv0_op = ClassicalToCart(coe0_op, GM_MOON);
  const Vec6 rv0_mci = ConvertFrame(t0, rv0_op, Frame::MOON_OP, Frame::MOON_CI);
  std::cout << "r0 (MCI): " << (rv0_mci.head(3) / 1e3).transpose() << " km\n";

  // --- Force model -----------------------------------------------------------
  //   Moon    : 2x2 spherical harmonics (J2 + C22)
  //   Earth   : point-mass third body (dominant long-period perturbation)
  //   Sun     : point-mass third body + SRP source
  //   SRP     : cannon-ball, C_R = 1.8, A = 0.02 m^2, m = 10 kg
  NBodyDynamics dyn;
  dyn.SetFrame(Frame::MOON_CI);
  dyn.SetUnits(SI_UNITS);
  dyn.AddBody(Body::Moon(2, 2));
  dyn.AddBody(Body::Earth());
  dyn.AddBody(Body::Sun());
  dyn.SetSrpCoefficient(1.8, 0.02, 10.0);

  dyn.SetIntegrator(IntegratorType::RKF45);
  IntegratorParams params;
  params.reltol = 1e-10;
  params.abstol = 1e-6;  // [m] 1 um position floor
  dyn.SetIntegratorParams(params);

  // --- Propagate for 30 days -------------------------------------------------
  const Real sim_duration = 30.0 * SECS_DAY;
  const Real dt_out = 30.0 * SECS_MINUTE;
  const VecX tspan = Arange(Real(0.0), sim_duration + dt_out, dt_out);
  const VecX tfs = t0 + tspan.array();
  std::cout << "Propagating " << tspan.size() << " epochs over " << sim_duration / SECS_DAY
            << " days ...\n";
  const MatX6 rv_mci = dyn.Propagate(rv0_mci, tfs);

  // --- Convert the propagated states to OP and extract osculating COE --------
  const MatX6 rv_op = ConvertFrame(tfs, rv_mci, Frame::MOON_CI, Frame::MOON_OP);
  const MatX6 coe = CartToClassical(rv_op, GM_MOON);

  const int last = static_cast<int>(coe.rows()) - 1;
  std::cout << std::fixed << std::setprecision(4);
  std::cout << "\nOsculating COE libration over the propagation (OP frame):\n";
  std::cout << "  a    [km]  : " << coe(0, 0) / 1e3 << " -> " << coe(last, 0) / 1e3 << "\n";
  std::cout << "  e    [-]   : " << coe(0, 1) << " -> " << coe(last, 1) << "\n";
  std::cout << "  i    [deg] : " << coe(0, 2) * DEG << " -> " << coe(last, 2) * DEG << "\n";
  std::cout << "  omega[deg] : " << coe(0, 4) * DEG << " -> " << coe(last, 4) * DEG << "\n";

  return 0;
}
pixi run build-examples
./build-examples-pixi/ex1_propagate_orbit