Example 7: Ground-Station Orbit Determination¶
A lunar satellite is tracked by the three 70 m Deep Space Network antennas
(Goldstone, Canberra, Madrid) via two-way range and range-rate. The whole
scenario lives in configs/ground_station_odts.yaml and is run by the generic
agent-based engine: each antenna is a GroundStation agent running a
GroundStationTrackingApp (a sensor that generates its own visibility-gated,
noisy observations of the target), and a GroundStationManager agent runs the
GroundStationManagerApp that aggregates those observations and recovers the
orbit from a perturbed initial guess with an iterative batch (weighted
least-squares) filter plus a square-root information filter / smoother, whose
design matrix is built analytically from the autodiff state-transition matrix.
By default the truth target and the estimator share the one force model in the
world: block, so the driver just builds the Simulation from the YAML and
calls Run(). The manager app also accepts an optional filter_dynamics:
block (or per-filter batch_dynamics: / sequential_dynamics:) to run the
batch and/or sequential filter at a deliberately different fidelity than truth.
Each GroundStationTrackingApp can optionally add Earth signal-path delays
(troposphere, ionosphere, relativistic Shapiro) and a solid Earth tide station
displacement to its truth observables via the apply_* keys in the config;
they default off, and an estimator that models a pure geometric range sees them
as realistic tracking errors. The GroundStationManagerApp in turn chooses
how much to model back out: it removes the deterministic Shapiro and solid-tide
terms in full and calibrates a configurable fraction of the troposphere and
ionosphere, inflating the range noise for the uncancelled remainder
(model_shapiro, model_solid_earth_tide, troposphere_cancel_fraction,
ionosphere_cancel_fraction, residual_delay_noise_scale). The closed
forms, magnitudes, and the estimation-side model are given in
Measurement Models (Ground-Station Signal-Path and Station-Location
Corrections).
Mirrors Example 7: Ground-Station Orbit Determination for a Lunar Satellite.
// Example 7: Ground-Station Orbit Determination for a Lunar Satellite
// ------------------------------------------------------------------------------
// C++ counterpart of python/examples/ex7_groundstation_odts.ipynb.
//
// A lunar satellite in an Elliptical Lunar Frozen Orbit is tracked by the three
// 70 m Deep Space Network antennas (Goldstone, Canberra, Madrid) via two-way
// range and range-rate (Doppler). The scenario is assembled from first-class
// agents that share a `World` (the read-only physical environment):
//
// * a `Satellite` agent (`sat`) self-propagates the truth trajectory using the
// World's force model;
// * three `GroundStation` agents each run a `GroundStationTrackingApp` (a
// sensor) that generates its own visibility-gated range/range-rate
// observations and pushes them to the manager;
// * a `GroundStationManager` agent (`gs_manager`) runs the centralized
// `GroundStationManagerApp`, which aggregates all stations' observations and
// recovers the orbit with an analytic batch (weighted least-squares) filter
// plus a square-root information filter / smoother.
//
// The whole scenario lives in configs/ground_station_odts.yaml; the Simulation
// just holds the agents, the World, and an event queue, and Run() drains it.
#include <yaml-cpp/yaml.h>
#include <iomanip>
#include <iostream>
#include <string>
#include "lupnt/applications/ground_station/ground_station_manager_app.h"
#include "lupnt/lupnt.h"
using namespace lupnt;
int main(int argc, char** argv) {
std::string config_path = (argc > 1) ? argv[1] : std::string("configs/ground_station_odts.yaml");
YAML::Node config = YAML::LoadFile(config_path);
Simulation sim(config);
sim.Run();
// The manager agent's application owns the centralized OD solution.
Agent* mgr_agent = sim.GetAgent("gs_manager");
auto mgr = std::dynamic_pointer_cast<GroundStationManagerApp>(mgr_agent->GetApplication());
LUPNT_CHECK(mgr && mgr->HasSolved(), "GroundStationManagerApp did not solve",
"ex7_groundstation_odts");
std::cout << "Ground stations: ";
for (const std::string& n : mgr->StationNames()) std::cout << n << " ";
std::cout << "\n";
std::cout << "Aggregated measurements: " << mgr->NumMeasurements() << "\n";
std::cout << "Batch filter converged: " << (mgr->Converged() ? "yes" : "no") << " in "
<< mgr->NumIterations() << " iterations\n\n";
const Vec6d x0_true = mgr->X0True();
const Vec6d x0_est = mgr->X0Estimated();
const Vec3d dr = x0_est.head(3) - x0_true.head(3);
const Vec3d dv = x0_est.tail(3) - x0_true.tail(3);
std::cout << std::fixed << std::setprecision(3);
std::cout << "Initial-guess position error : "
<< (mgr->X0InitialGuess().head(3) - x0_true.head(3)).norm() << " m\n";
std::cout << "Estimated position error : " << dr.norm() << " m\n";
std::cout << "Estimated velocity error : " << dv.norm() * 1e3 << " mm/s\n";
const double sigma_pos = std::sqrt(mgr->Covariance().topLeftCorner(3, 3).trace());
std::cout << "Formal 1-sigma position (RSS): " << sigma_pos << " m\n";
return 0;
}
pixi run build-examples
./build-examples-pixi/ex7_groundstation_odts