Example 10: Surface Rover Navigation¶
A lunar-surface rover near the south pole fuses a full strapdown IMU
(accelerometer + gyroscope, Kalibr noise model), LCRNS/LANS pseudoranges from a
five-satellite relay constellation, and a LOLA-DEM altitude constraint, with the
IMU biases estimated online. The scenario is agent-based
(configs/surface_rover_nav.yaml): a thin Rover agent hosts a
SurfaceRoverNavApp (the strapdown-INS EKF), and a LunarNavConstellation
agent stands in for the five navigation satellites — one config entry expanding
into five self-propagating Spacecraft — which the rover queries for the relay
geometry instead of propagating the orbits itself. The shared world: block
provides the LOLA DEM for the site (downloaded from NASA PGDA if not cached). The
example re-runs with the DEM constraint disabled to show how the terrain height
ties down the weakly-observable vertical channel.
Mirrors Example 10: Surface Rover Navigation with IMU, LCRNS, and DEM Aiding.
// Example 10: Surface Rover Navigation -- IMU + LCRNS + DEM
// ------------------------------------------------------------------------------
// C++ counterpart of python/examples/ex10_surface_rover.ipynb.
//
// A lunar-surface rover near the south pole fuses a full strapdown IMU
// (accelerometer + gyroscope, Kalibr noise model), LCRNS/LANS pseudoranges from a
// five-satellite relay constellation, and a LOLA-DEM altitude constraint, with the
// IMU biases estimated online. The scenario is built from a single YAML file: a
// thin `Rover` agent hosts a `SurfaceRoverNavApp` (an error-state strapdown-INS
// EKF); the shared physical environment (frame, gravity, DEM site) lives in the
// top-level `world:` block. `pnt.Simulation` runs the event loop; the per-epoch
// truth/estimate/covariance series are read off the app.
//
// The last block re-runs the arc with the DEM constraint disabled to show how the
// terrain height ties down the weakly-observable vertical channel.
#include <cmath>
#include <iomanip>
#include <iostream>
#include <string>
#include "lupnt/lupnt.h"
using namespace lupnt;
static double FinalUpError(const SurfaceRoverNavApp& app) {
const MatXd& e = app.pos_err_enu();
return std::abs(e(e.rows() - 1, 2));
}
int main(int argc, char** argv) {
const std::string config_path = argc > 1 ? argv[1] : "configs/surface_rover_nav.yaml";
// --- Run with the DEM altitude constraint enabled --------------------------
Config cfg = YAML::LoadFile(config_path);
Simulation sim(cfg);
sim.Run();
auto* app = dynamic_cast<SurfaceRoverNavApp*>(sim.GetAgent("Rover")->GetApplication().get());
const int N = static_cast<int>(app->time_series().size());
std::cout << "Site: " << app->site_id() << " (" << app->site_name() << ")\n";
std::cout << "Ran " << N
<< " epochs, mean visible satellites: " << app->n_visible().cast<double>().mean()
<< "\n";
std::cout << std::fixed << std::setprecision(2);
std::cout << "Final 3D position error (DEM on) : " << app->pos_err_norm()(N - 1) << " m\n";
// --- Ablation: same arc, DEM constraint disabled ---------------------------
Config cfg_off = YAML::LoadFile(config_path);
cfg_off["agents"]["Rover"]["application"]["enable_dem_constraint"] = false;
Simulation sim_off(cfg_off);
sim_off.Run();
auto* app_off
= dynamic_cast<SurfaceRoverNavApp*>(sim_off.GetAgent("Rover")->GetApplication().get());
const int M = static_cast<int>(app_off->time_series().size());
std::cout << "Final 3D position error (DEM off): " << app_off->pos_err_norm()(M - 1) << " m\n";
std::cout << "Final Up-channel error (DEM on) : " << FinalUpError(*app) << " m\n";
std::cout << "Final Up-channel error (DEM off): " << FinalUpError(*app_off) << " m\n";
return 0;
}
pixi run build-examples
./build-examples-pixi/ex10_surface_rover