Numerical Integration =================================================================== Purpose ------------------------------------------------------------------- This specification defines the mathematical contract for LuPNT's ODE integrators, which advance a state under ``dx/dt = f(t, x)`` and, when requested, propagate the associated sensitivity (state-transition) matrix. The goal is to make the step equations, embedded error estimates, adaptive step-size control, and termination semantics explicit enough that propagation and filtering tests can compare against the same equations. The concrete classes are ``Integrator`` and its subclasses ``RK4``, ``RK8``, ``IRKF`` / ``RKF45``, and ``PD45`` (``cpp/lupnt/numerics/integrator.h`` / ``integrator.cc``). ODE and State Contract ------------------------------------------------------------------- Every integrator advances a first-order system .. math:: \dot{x} = f(t, x), \qquad x \in \mathbb{R}^n, where the right-hand side has the LuPNT type .. math:: \texttt{ODE} : (t, x) \mapsto f(t, x) \in \mathbb{R}^n . Numerical dynamics models (e.g. ``NumericalOrbitDynamics``, ``ClockDynamics``, ``JointOrbitClockDynamics``) build an ``ODE`` from their force/derivative models and pass it to ``Integrator::Propagate`` / ``PropagateEx``. The integrator does not interpret the state contents; the unit, frame, and epoch conventions are those of the calling dynamics model (see :doc:`dynamics`). The independent variable :math:`t` is in seconds and the step size :math:`h` is signed by the propagation direction. The ``ODE`` typedef is declared in ``cpp/lupnt/numerics/integrator.h``: .. code-block:: cpp using ODE = std::function; Integrator Selection ------------------------------------------------------------------- ``IntegratorType`` selects the concrete subclass instantiated by a dynamics model's ``SetIntegrator``: .. math:: \texttt{IntegratorType} \in \{\ \texttt{RK4},\ \texttt{RK8},\ \texttt{RKF45},\ \texttt{PD45}\ \}. ``RK4`` and ``RK8`` are fixed-step methods; ``RKF45`` (an ``IRKF``) and ``PD45`` are embedded, adaptive-step methods. The default is ``IntegratorType::RK4``. Implemented by ``cpp/lupnt/dynamics/numerical_orbit_dynamics.cc :: NumericalDynamics::SetIntegrator`` (default ``default_integrator = IntegratorType::RK4`` in ``integrator.h``): .. code-block:: cpp void NumericalDynamics::SetIntegrator(IntegratorType integ) { if (integ == IntegratorType::RK4) integrator_ = MakeUnique(); else if (integ == IntegratorType::RK8) integrator_ = MakeUnique(); else if (integ == IntegratorType::RKF45) integrator_ = MakeUnique(); else if (integ == IntegratorType::PD45) integrator_ = MakeUnique(); } Fixed-Step Runge-Kutta Contract ------------------------------------------------------------------- A fixed-step explicit :math:`s`-stage Runge-Kutta method with Butcher coefficients :math:`(a_{ij}, b_i, c_i)` advances one step of size :math:`h` by .. math:: k_i = f\!\left(t + c_i h,\; x + \sum_{j0}, \qquad \texttt{abstol} > 0, \qquad \texttt{reltol} > 0 . It also holds an optional early-termination predicate :math:`\texttt{terminate\_if}(t, x) \to \{\text{true},\text{false}\}`. Defined in ``cpp/lupnt/numerics/integrator.h :: IntegratorParams`` (defaults ``max_iter = 20``, ``abstol = reltol = 1e-6``); validated by ``cpp/lupnt/numerics/integrator.cc :: IntegratorParams::CheckIntegratorParams``. Termination Semantics ------------------------------------------------------------------- ``PropagateEx`` returns an ``IntegratorResult`` :math:`(x, t, \texttt{reason}, \texttt{steps})`. The predicate is checked once before stepping and again after every accepted step; the run stops with .. math:: \texttt{reason} = \begin{cases} \texttt{UserCondition} & \text{if } \texttt{terminate\_if}(t,x)=\text{true},\\[2pt] \texttt{ReachedTf} & \text{when } t \text{ reaches } t_f . \end{cases} ``steps`` counts the accepted integration steps taken. Implemented by ``cpp/lupnt/numerics/integrator.cc :: Integrator::PropagateEx`` (predicate checked before the loop and after each ``Step``): .. code-block:: cpp if (params_.terminate_if && params_.terminate_if(t, x)) return {x, t, TerminationReason::UserCondition, steps}; // ... after each accepted Step(...) ++steps; re-check terminate_if ... return {x, t, TerminationReason::ReachedTf, steps}; State-Transition (Sensitivity) Matrix Propagation ------------------------------------------------------------------- The sensitivity-matrix overloads ``Propagate(..., MatXd* J)`` / ``PropagateEx(..., MatXd* J)`` compute .. math:: J = \frac{\partial x(t_f)}{\partial x(t_0)} by **column-parallel forward-mode automatic differentiation** (``JacobianParallel``): the re-propagation function is differentiated one input component at a time, with the columns distributed across cores by OpenMP (see :doc:`autodiff`). This is exact AD carried through the integration in ``Real`` arithmetic -- not a finite-difference approximation and not an augmented variational integration; the ordering of rows and columns of :math:`J` matches the state ordering of :math:`x`. When ``J`` is ``nullptr`` the Jacobian computation is skipped. Implemented by ``cpp/lupnt/numerics/integrator.cc :: Integrator::Propagate`` (the ``MatXd* J`` overload), which forwards a re-propagation closure to ``cpp/lupnt/numerics/math_utils.cc :: JacobianParallel``: .. code-block:: cpp // Integrator::Propagate(..., MatXd* J) auto func = [=, this](const State& x) { return Propagate(odefunc, t0, tf, x, dt); }; State xf = (J != nullptr) ? JacobianParallel(func, x0, *J) : func(x0); // JacobianParallel -- one autodiff column per state, OpenMP-parallel #pragma omp parallel for for (int i = 0; i < n; i++) jacobian(func, wrt(x0_tmp(i)), at(x0_tmp), xi, Ji); .. note:: The filter-facing state-transition matrix is obtained separately. ``FilterDynamicsFunction`` (see :doc:`filters`) returns the STM :math:`F = \partial x(t_f)/\partial x(t_0)` produced by the dynamics model's own linearization (autodiff via ``GetFilterDynamicsFunction``), which the EKF/SRIF/UDU filters consume directly. The conceptual variational contract :math:`\dot{\Phi} = A(t)\,\Phi`, :math:`A = \partial f/\partial x` is documented in :doc:`dynamics`. Model Boundaries ------------------------------------------------------------------- * The step size ``dt`` passed to ``Propagate`` must be positive; direction is inferred from :math:`t_f - t_0` in ``PropagateEx``. * ``RK8`` is a fixed-step method with no embedded error estimate; step control is available only through ``RKF45`` and ``PD45``. * The integrator's ``MatXd* J`` sensitivity is finite-difference based and therefore re-propagates the trajectory :math:`n+1` times.