# ADR 017: Deterministic SGP4 Orbit Simulation

- HTML version: https://robbiepalmer.me/projects/autonomic-satellite-swarm/adrs/017-deterministic-sgp4-orbit-simulation
- Project: Autonomic Satellite Swarm (https://robbiepalmer.me/projects/autonomic-satellite-swarm.md)
- Status: Accepted
- Date: 2026-09-20
- Supersedes: autonomic-satellite-swarm:012-qrptracker-embedded-orbit-propagation

# Context

[ADR 013](/projects/autonomic-satellite-swarm/adrs/013-browser-simulation-with-cesiumjs-and-webassembly)
uses scripted positions to prove that the C++ scorer and Cesium receive the same satellite snapshot.
The shipped trace exposes only four display positions between 0 and 120 milliseconds. Cesium redraws
each node at those snapshots, but the path is not an orbit and the short controller run cannot show
a satellite travelling around Earth at its natural rate.

[PDR 002](/projects/autonomic-satellite-swarm/pdrs/002-explorable-evidence-bounded-browser-simulation)
calls for progressively stronger simulation models with visible evidence limits. This ADR chooses
the orbital model that replaces the browser's scripted motion.

The demonstration does not need to run in real time. Quiet orbital periods can play at 10 or 100
times their simulated rate. Negotiation and fault events can pause or play slowly enough for a
reader to follow them. This requires a presentation clock whose rate may change without changing the
recorded simulation time.

[ADR 012](/projects/autonomic-satellite-swarm/adrs/012-qrptracker-embedded-orbit-propagation)
proposed qrpTracker because its PLAN-13 implementation had run on the ATmega328 used by the Arduino
Uno. That constraint does not apply here. The simulation represents the environment around each
controller and already supplies `SatelliteSnapshot` updates. Putting propagation into the reference
firmware would add clock, orbital-data, memory, and timing concerns without evidence that the swarm
experiment needs each board to estimate its own orbit. ADR 012 is therefore rejected.

A two-line element set, or TLE, records a satellite's estimated orbit at a stated epoch. The
[SGP4 reference work published by CelesTrak](https://celestrak.org/publications/AIAA/2006-6753/)
provides C++ source, test cases, and results for propagating such element sets. SGP4 predicts
position and velocity. It does not calculate the maneuver needed to reach a mission objective, and
its output becomes less reliable as the elements age or the real orbit changes.

# Decision

Use an SGP4 implementation in the C++ simulation layer to produce deterministic satellite motion
for native and WebAssembly runs. Do not add the propagator to the portable coordination core or the
Uno and ESP32 firmware.

Give every scenario a checked-in TLE for each node and one fixed UTC epoch. UTC is the common time
reference for the calculation, not a local timezone or the computer's wall clock. Derive every
requested orbital instant from the scenario epoch and the trace's monotonic elapsed time. Do not
fetch TLEs, clock data, or other orbital inputs at runtime.

Keep the propagation result richer than `SatelliteSnapshot`. It must identify its coordinate frame
and contain an epoch, position, and velocity. A simulation adapter converts that result once into
the latitude, longitude, orbital radius, and direction used by the historical scorer. The browser
serialization exposes positions from the same result for Cesium. The scorer and renderer must not
calculate separate trajectories.

Extend the demonstration beyond mission assignment so its ephemeris covers at least one complete
orbit. Sample often enough for bounded interpolation error, then let
[Cesium's `SampledPositionProperty`](https://cesium.com/learn/cesiumjs/ref-doc/SampledPositionProperty.html)
interpolate visual positions between those samples. Cesium remains a renderer and does not run
SGP4 or update controller state.

Keep presentation time separate from simulation time. The player may accelerate quiet flight, slow
an interval containing negotiation or a fault, pause, or step between recorded events. Changing
playback speed must not alter controller inputs, propagated positions, event timestamps, candidate
scores, or the serialized native and WebAssembly evidence. Show the current simulation time and
playback multiplier so a reader can distinguish display speed from simulated orbital time.

Keep `HistoricalOrbitalScorer` as the default candidacy policy. Propagated position improves one of
its inputs but does not validate its maneuver-energy heuristic.

# Alternatives

## Run qrpTracker PLAN-13 on each reference board

This would test onboard dead reckoning from orbital elements and a device clock. The current goal is
to simulate the environment around the coordination algorithm, not prove that an Uno can navigate
without an external position source. The embedded experiment carries costs and failure modes that
do not help the browser demonstration.

## Continue with scripted paths

Longer scripted paths could show smooth travel with little new code. They would remain authored
coordinates rather than output from an orbital model. That is acceptable for testing arbitrary
movement, but it cannot support a claim that the displayed path comes from TLE propagation.

## Precompute and commit every position

An external tool could generate a fixed ephemeris for the C++ runner to replay. This keeps orbital
code out of the browser module, but it leaves native and WebAssembly runs unable to propagate a
different checked-in scenario without regenerating an opaque fixture. Runtime C++ propagation keeps
the scenario inputs small and makes the relationship between TLE, time, scorer, and display
testable.

## Propagate in TypeScript

A browser library could feed Cesium directly. The displayed position could then diverge from the
snapshot scored by C++, and the native runner could not produce the same evidence. Keep orbit
generation on the C++ side of the existing replay contract.

## Fetch current TLEs when the demo starts

Live elements would make the displayed satellites appear current, but they would make results
depend on a network service and changing data. The existing demonstration is self-hosted,
repeatable, and audited for external requests. Pinned scenario inputs fit those properties better.

# Acceptance evidence

* The imported CelesTrak C++ source is pinned to
  [`4efec61212ce03529bdf1037e0b86eb1ece2ea49`](https://github.com/CelesTrak/fundamentals-of-astrodynamics/tree/4efec61212ce03529bdf1037e0b86eb1ece2ea49/software/cpp/SGP4/SGP4).
  Its notice records AGPL-3.0, the upstream citation, normalized line endings, and a result-neutral
  guard against an overlapping satellite-number self-copy in upstream `twoline2rv`.
* The scenario contains three fixed TLE pairs with one common epoch. Construction checks their
  fixed-width ASCII layout, line numbers, checksums, catalog pairing, and upstream parser result.
  Tests reject corrupt checksums, mismatched catalogs, and truncated lines.
* Tests compare CelesTrak near-Earth vectors at epoch and six hours later and a deep-space vector
  3.6 days before its epoch. Every TEME position must be within one metre. A separate polar case
  crosses 80 degrees latitude and the longitude discontinuity.
* Each `PropagationResult` carries epoch, position, velocity, and explicit TEME and Earth-fixed
  frames. The documented Earth-fixed conversion rotates by Greenwich mean sidereal time, treats UTC
  as UT1, sets polar motion to zero, and includes Earth rotation in velocity.
* One Earth-fixed result supplies both the quantized `SatelliteSnapshot` used by the scorer and the
  Cartesian position and velocity serialized for Cesium. No TypeScript propagator exists.
* The fixed trace contains 147 frames from 0 through 8,040 seconds, exceeding the reference orbit's
  roughly 7,983-second period. It derives every instant from the TLE epoch and trace offset without
  reading the wall clock or network.
* Native, WebAssembly, and production-worker output match byte for byte. Browser positions are
  integral metres, velocities are integral millimetres per second, and scorer inputs are quantized
  to `0.0001` degree and one metre.
* Cesium linearly interpolates 60-second Earth-fixed samples. The measured midpoint error stays
  below the enforced 3,300-metre bound. Negotiation plays at `0.01x`, quiet flight at `100x`, and
  the controls expose pause, step, UTC simulation time, and multiplier. Reduced-motion mode starts
  paused.
* The debug native executable grew from 1,811,400 to 2,194,200 bytes. The browser loader grew from
  19,678 to 21,231 bytes, WebAssembly from 227,825 to 274,931 bytes, and the fixture from 10,795 to
  208,382 bytes. The production audit passed in Chromium 153.0.8010.12 without external requests or
  page errors; first-globe time was 3,050 milliseconds on desktop and 4,684 milliseconds at 4x
  mobile CPU throttling.
* The CMake dependency is private to the simulation target. All 93 host tests, the browser parity
  gate, and both Uno and ESP32 compile gates pass, leaving the portable core and firmware free of
  SGP4.

The detailed model, conversion, sampling, reproducibility rules, and cost measurements are recorded
in the repository's
[orbit-simulation note](https://github.com/Robbie-Palmer/hq/blob/main/cpp/autonomic-satellite-swarm/docs/orbit-simulation.md).

# Consequences

The demonstration can show each node completing an orbit while the real coordination code changes
state at recorded instants. A reader can accelerate routine travel and inspect negotiations without
mistaking presentation time for simulation time. Fixed orbital inputs keep the result offline and
repeatable.

The simulation gains TLE parsing, time conversion, coordinate conversion, interpolation, and a new
third-party source dependency. Reference-vector agreement and native-to-WebAssembly parity become
release requirements. More position samples also increase the worker response and rendering work.

The result remains a research visualization. SGP4 propagation does not validate the historical
score, represent propulsion, prove hardware behavior, or make the project flight software.

---

Markdown index of this site: https://robbiepalmer.me/llms.txt
