orbitforge od ekf
Synopsis
orbitforge od ekf --obs <PATH> --stations <PATH> --initial <PATH> --epoch <RFC3339> [OPTIONS]Description
An extended Kalman filter processes observations one at a time, in order, maintaining a running state and covariance. Between measurements it propagates both forward, growing the uncertainty. At each measurement it applies a correction weighted by how much it trusts the prediction against the observation.
Unlike od bls, it never revisits an earlier
observation. That makes it suitable for tracking an object as data arrives, and
it means the final state reflects only information available up to that point.
Options
| Parameter | Type | Unit | Default | Required | Description |
|---|---|---|---|---|---|
--obs | path | n/a | — | Yes | Tracking CSV. |
--stations | path | n/a | — | Yes | Tracking-stations JSON. Must describe every station in the observation file. |
--initial | path | n/a | — | Yes | Keplerian-elements JSON, the initial state at the filter start epoch. |
--epoch | string | RFC 3339 UTC | — | Yes | Filter start epoch. |
--force | enum | n/a | j2 | No | Force model: `two-body`, `j2`, or `j4`. |
--snc | float | km^2/s^3 | 1e-12 | No | State noise compensation: acceleration power spectral density. See below. |
--sigma-pos-km | float | km | 10 | No | Initial position uncertainty, one sigma per axis. |
--sigma-vel-km-s | float | km/s | 0.01 | No | Initial velocity uncertainty, one sigma per axis. |
--json | path | n/a | — | No | Write the full filter history: state and covariance at every epoch. |
Worked example
The same 220 observations that od bls fits in batch:
orbitforge od ekf \
--obs obs.csv \
--stations stations.json \
--initial orbit.json \
--epoch 2026-01-01T00:00:00ZEKF processed 110 epochs: 220 accepted / 0 rejected.
Final position sigma [0.0362, 0.1861, 0.0256] km at 2026-01-02T00:00:00Z.110 epochs carrying 220 observations, because each visible epoch produced both a range and a range-rate measurement.
Comparing against batch least squares
Run on identical data, the two methods report very different uncertainty:
| Method | Position sigma, km per axis | At epoch |
|---|---|---|
od bls | 0.0006, 0.0008, 0.0009 | Start of arc |
od ekf | 0.0362, 0.1861, 0.0256 | End of arc |
The filter’s figures are one to two orders of magnitude larger, and that is correct rather than a defect. Three things drive it:
- Different epochs. Batch reports uncertainty at the solution epoch, constrained by observations both before and after it in the arc. The filter reports it at the final epoch, constrained only by what came before.
- Time since the last observation. The covariance grows during propagation. If the last pass ended well before the final epoch, the filter has been coasting.
- Process noise.
--sncdeliberately inflates the covariance to keep the filter responsive, which batch does not do at all.
The middle component, 0.1861 km, is several times larger than the other two. That is the in-track direction, and it is the expected signature: along-track uncertainty grows fastest under almost every error source, because an error in velocity magnitude integrates directly into along-track position.
If your filter reports its largest uncertainty in radial or cross-track instead, something is usually wrong with the geometry or the station model.
Process noise, and why it exists
--snc adds acceleration power spectral density to the propagation step,
inflating the covariance over time. Its purpose is to stop the filter becoming
overconfident.
Without process noise, a filter processing many consistent observations drives its covariance toward zero. It then effectively stops believing new measurements, because the prediction appears far more certain than any observation. That is called filter saturation or smugness, and its practical symptom is a filter that ignores a genuine maneuver or a real change in dynamics.
| Setting | Effect | Symptom when wrong |
|---|---|---|
| Too small | Covariance collapses; the filter stops responding to data | Residuals grow but the covariance stays tiny |
| Too large | Covariance stays inflated; the filter over-trusts each new measurement | Estimate is noisy and jumps between observations |
The default of 1e-12 km^2/s^3 is a starting point for a well-modeled orbit. Tune it upward when the dynamics are less well known, for example a drag-dominated low-altitude object or a spacecraft that maneuvers.
Initial uncertainty
--sigma-pos-km and --sigma-vel-km-s set how much the filter trusts the
initial guess. The defaults, 10 km and 0.01 km/s, are deliberately loose so a
rough initial orbit does not fight the data.
Setting them too tight is the classic way to make a filter reject good observations: the prediction looks so certain that every measurement appears to be an outlier.
Validating a filter
Because od simulate-tracking starts from
a known truth orbit, you can check something you never can operationally: whether
the reported covariance is honest.
Run many seeds, compare the actual error against the reported sigma, and confirm the errors fall within one sigma about 68 percent of the time. A filter that reports 0.03 km while actually erring by 0.3 km is worse than one that honestly reports 0.3 km, because downstream decisions trust the number.
See also
od blsfor the batch alternative.od simulate-trackingto generate input.
main (pre-release)