Example 6: GNSS Sidelobe ODTS (EKF)

Sidelobe pseudorange + Doppler + TDCP orbit determination and time synchronization (ODTS) with a stochastic-cloning UDU EKF, including a GCPM / IRI plasmaspheric-delay ray-trace stage. It estimates position, velocity, clock bias / drift, and an SRP coefficient from the weak Earth-GNSS sidelobe signals a lunar receiver can track.

Mirrors Example 6: GNSS Sidelobe Pseudorange + Doppler + TDCP ODTS with an EKF.

The C++ program builds a Simulation from configs/lunar_gnss_odts.yaml and runs it: a physical receiver Spacecraft hosts the LunarGnssOdtsApp (the release-facing ODTS engine under cpp/lupnt/applications/lunar_gnss_odts), whose scheduled step runs the whole Monte-Carlo EKF. The program then reads the per-seed final / RMS position, velocity, and clock errors off the app.

Note

A full run first needs the SP3 precise-ephemeris products and the staged link + plasma-delay precompute produced by python/examples/ex6_precompute.py (Stage 1 link geometry, Stage 2 GCPM/IRI plasma delays). As with the other GNSS examples the program guards missing data and exits cleanly if it is absent. See SP3 Downloads for the Earthdata / SP3 setup.

// Example 6: Lunar GNSS Sidelobe Orbit Determination & Timing (ODTS)
// ------------------------------------------------------------------------------
// C++ counterpart of python/examples/ex6_gnss_odts.ipynb.
//
// A lunar-orbiting receiver navigates from Earth-GNSS sidelobe signals: sidelobe
// pseudorange plus time-differenced carrier phase (TDCP) drive an EKF that
// estimates the receiver orbit, clock (bias + drift), and SRP coefficient, with
// a GCPM/IRI plasmaspheric-delay stage on the truth signal. As with the ex7/ex8
// tutorials, the whole scenario is assembled from a single YAML file: a physical
// `receiver` Spacecraft agent hosts a `LunarGnssOdtsApp` whose scheduled step
// runs the entire Monte-Carlo ODTS body (bit-identical to the former
// `LunarGnssODTSSimulation`). `Simulation::Run()` drives it; the per-seed
// summaries (final / RMS position, velocity, and clock errors) are read off the
// app.
//
// Data note: a real run needs the SP3 precise ephemerides
// (data/LuPNT_data/ephemeris/gnsslibpy/sp3) and, ideally, the Stage 1 link +
// Stage 2 plasma-delay precompute caches (see python/examples/ex6_precompute.py
// and docs/pages/sp3_download). Absent the data the program prints an
// informative note and exits cleanly, so it always compiles and runs; with the
// data present, run it as `./ex6_gnss_odts` from the repository root.

#include <yaml-cpp/yaml.h>

#include <iomanip>
#include <iostream>
#include <string>

#include "lupnt/applications/lunar_gnss_odts/lunar_gnss_odts_app.h"
#include "lupnt/lupnt.h"

using namespace lupnt;

int main(int argc, char** argv) {
  const std::string config_path = argc > 1 ? argv[1] : std::string("configs/lunar_gnss_odts.yaml");

  try {
    Config cfg = YAML::LoadFile(config_path);
    Simulation sim(cfg);
    std::cout << "Running the lunar GNSS ODTS EKF (LunarGnssOdtsApp) ...\n";
    sim.Run();

    // The receiver Spacecraft hosts the ODTS coordinator application.
    auto app
        = std::dynamic_pointer_cast<LunarGnssOdtsApp>(sim.GetAgent("receiver")->GetApplication());
    LUPNT_CHECK(app, "receiver agent has no LunarGnssOdtsApp", "ex6_gnss_odts");

    const std::vector<LunarGnssODTSSummary>& summaries = app->GetSummaries();
    if (summaries.empty()) {
      std::cout << "Run completed but produced no summaries.\n";
      return 0;
    }

    std::cout << std::fixed << std::setprecision(3);
    std::cout << "\nMonte-Carlo seeds: " << summaries.size() << "\n";
    const LunarGnssODTSSummary& s = summaries.front();
    std::cout << "Epochs processed         : " << s.num_epochs << "\n";
    std::cout << "Final position error     : " << std::setw(9) << s.final_position_error_m
              << " m\n";
    std::cout << "Final velocity error     : " << std::setw(9) << s.final_velocity_error_mps * 1e3
              << " mm/s\n";
    std::cout << "Final clock bias error   : " << std::setw(9) << s.final_clock_bias_error_m * 1e2
              << " cm\n";
    std::cout << "Final clock drift error  : " << std::setw(9)
              << s.final_clock_drift_error_mps * 1e3 << " mm/s\n";
    std::cout << "RMS position error       : " << std::setw(9) << s.rms_position_error_m << " m\n";
    std::cout << "RMS velocity error       : " << std::setw(9) << s.rms_velocity_error_mps * 1e3
              << " mm/s\n";

    if (summaries.size() > 1) {
      double pos = 0.0, rms = 0.0;
      for (const LunarGnssODTSSummary& e : summaries) {
        pos += e.final_position_error_m;
        rms += e.rms_position_error_m;
      }
      const double n = static_cast<double>(summaries.size());
      std::cout << "\nSeed-averaged final position error: " << pos / n << " m\n";
      std::cout << "Seed-averaged RMS   position error: " << rms / n << " m\n";
    }

  } catch (const std::exception& e) {
    std::cout << "\n[skipped] ODTS inputs unavailable: " << e.what() << "\n"
              << "This example needs the SP3 precise ephemerides under\n"
              << "data/LuPNT_data/ephemeris/gnsslibpy/sp3 and (ideally) the Stage 1/2\n"
              << "precompute caches. Build them with python/examples/ex6_precompute.py;\n"
              << "see docs/pages/sp3_download. The program compiled and ran; it skipped\n"
              << "the ODTS run because the inputs are absent.\n";
    return 0;
  }

  return 0;
}
pixi run python python/examples/ex6_precompute.py   # Stage 1 links + Stage 2 plasma delays
pixi run build-examples
./build-examples-pixi/ex6_gnss_odts