Example 11: Lunar Lander Navigation¶
A lunar lander on powered descent to a south-pole site navigates with an
error-state INS MEKF that fuses a full IMU (Kalibr noise model), a nadir radar
altimeter (height above DEM terrain), crater-landmark bearings (terrain-relative
navigation against a synthetic crater map), and LunaNet/LANS pseudoranges, with
the IMU biases estimated online. The scenario is agent-based
(configs/lander_nav.yaml): the one Lander agent hosts two applications
— a guidance LanderGncApp that owns the powered-descent truth trajectory
(built-in smoothstep or an injected reference) and writes the lander’s truth state
each epoch, and a navigation LanderNavApp that reads that truth to synthesize
its measurements and drive the descent MEKF. The shared world: block provides
the DEM. The example ends with a sensor-ablation study — disabling craters,
altimeter, and LunaNet in turn — to show what each aiding source buys.
Mirrors Example 11: Lunar Lander Navigation with IMU, Altimeter, Crater Bearings, and LunaNet.
// Example 11: Lunar Lander Navigation -- IMU + Altimeter + Crater Bearings + LunaNet
// ------------------------------------------------------------------------------
// C++ counterpart of python/examples/ex11_lander_navigation.ipynb.
//
// A lunar lander on powered descent to a south-pole site navigates with an
// error-state INS MEKF that fuses a full IMU (Kalibr noise model), a nadir radar
// altimeter (height above DEM terrain), crater-landmark bearings (terrain-relative
// navigation against a synthetic crater map), and LunaNet/LANS pseudoranges, with
// the IMU biases estimated online. The scenario is built from a single YAML file:
// a thin `Lander` agent hosts a `LanderNavApp`; the shared environment (frame,
// gravity, DEM site) lives in the top-level `world:` block. `pnt.Simulation` runs
// the descent; results are read off the app.
//
// The final block re-runs with each aiding sensor disabled in turn to show what
// each one buys: craters -> horizontal, altimeter -> vertical, LunaNet -> absolute.
#include <iomanip>
#include <iostream>
#include <string>
#include "lupnt/lupnt.h"
using namespace lupnt;
static double FinalPosError(Config cfg) {
Simulation sim(cfg);
sim.Run();
auto* app = dynamic_cast<LanderNavApp*>(
sim.GetAgent("Lander")->GetApplicationByName("LanderNavApp").get());
const VecXd& e = app->pos_err_norm();
return e(e.size() - 1);
}
int main(int argc, char** argv) {
const std::string config_path = argc > 1 ? argv[1] : "configs/lander_nav.yaml";
Config cfg = YAML::LoadFile(config_path);
Simulation sim(cfg);
sim.Run();
auto* app = dynamic_cast<LanderNavApp*>(
sim.GetAgent("Lander")->GetApplicationByName("LanderNavApp").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 << " descent epochs\n";
std::cout << std::fixed << std::setprecision(2);
std::cout << "Touchdown altitude (truth) : " << app->alt_truth()(N - 1) << " m\n";
std::cout << "Final 3D position error : " << app->pos_err_norm()(N - 1) << " m\n";
std::cout << "Final 3D velocity error : " << app->vel_err_norm()(N - 1) << " m/s\n\n";
// --- Sensor ablation: disable one aiding source at a time ------------------
std::cout << "Sensor ablation (final 3D position error):\n";
std::cout << " all sensors : " << app->pos_err_norm()(N - 1) << " m\n";
{
Config c = YAML::LoadFile(config_path);
c["agents"]["Lander"]["application"]["enable_craters"] = false;
std::cout << " no craters : " << FinalPosError(c) << " m\n";
}
{
Config c = YAML::LoadFile(config_path);
c["agents"]["Lander"]["application"]["enable_altimeter"] = false;
std::cout << " no altimeter : " << FinalPosError(c) << " m\n";
}
{
Config c = YAML::LoadFile(config_path);
c["agents"]["Lander"]["application"]["enable_lunanet"] = false;
std::cout << " no LunaNet : " << FinalPosError(c) << " m\n";
}
return 0;
}
pixi run build-examples
./build-examples-pixi/ex11_lander_navigation