Metadata-Version: 2.4
Name: gri-kalman
Version: 0.3.0
Summary: Per-target Kalman/IMM tracking: interacting multiple models, maneuver segmentation, RTS smoothing, EKF/UKF observable updates, and a motion-model bank
Project-URL: Homepage, https://geosolresearch.com
Project-URL: Repository, https://gitlab.com/geosol-foss/python/gri-kalman
Project-URL: Issues, https://gitlab.com/geosol-foss/python/gri-kalman/-/issues
Project-URL: Changelog, https://gitlab.com/geosol-foss/python/gri-kalman/-/releases
Author-email: GeoSol Research Inc <contact@geosolresearch.com>
License-Expression: MIT
License-File: LICENSE
Keywords: IMM,RTS,UKF,estimation,kalman,tracking
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: GIS
Requires-Python: >=3.12
Requires-Dist: gri-ell>=0.3.2
Requires-Dist: gri-obs>=0.3.0
Requires-Dist: gri-pos>=0.2.0
Requires-Dist: gri-utils>=0.3.3
Requires-Dist: numpy>=2.3.3
Requires-Dist: scipy>=1.16.2
Description-Content-Type: text/markdown

# gri-kalman

Per-target Kalman tracking for geolocation: interacting multiple model (IMM)
filters with a motion-model bank, maneuver segmentation, RTS smoothing, and
EKF/UKF measurement updates from either `Ell` position fixes or raw observables.

This package was factored out of `gri-convolve` so the per-target estimators can
be reused without the ellipsoid-convolution stack. It depends only on gri-ell,
gri-obs, gri-pos, gri-utils (plus numpy/scipy).

## Install

```bash
uv add gri-kalman
```

## Trackers

All implement the `Tracker` protocol (`update` / `update_observable` / `predict` /
`coast` / `smoothed_track` / `result` / `is_initialized`):

| Tracker | Description |
| --- | --- |
| `IMM` | Interacting multiple model filter over a motion-model bank |
| `SmartIMM` | IMM with outlier rejection on the measurement stream |
| `SegmentedIMM` | Maneuver-segmenting IMM (per-segment filters) |
| `SmartSegmentedIMM` | Segmenting + outlier-rejecting (the recommended default) |

Motion models: `ConstantVelocity`, `ConstantAcceleration`, `NearlyConstantSpeed`,
`CoordinatedTurn`, `Singer`, `AscendDescend`, `Static` (plus the
`LinearMotionModel` / `NonlinearMotionModel` / `MotionModel` base protocols).

### Local-level (ENU) awareness

The state is ECEF, but a maneuver's "horizontal" and "vertical" are defined
relative to *local up*, not the ECEF axes. Models whose dynamics or noise are
anisotropic in that sense take a `level_rotation` provider (an ECEF position ->
3x3 ECEF->ENU rotation):

- `CoordinatedTurn` turns in the local horizontal plane about local up (not the
  ECEF polar axis).
- `AscendDescend` puts its large maneuvering noise along local up.
- `ConstantVelocity` can split process noise / velocity prior into horizontal and
  a small vertical component, to model a level mover (boat, car, cruising aircraft).

Pass a *constant* rotation for a fixed local-level frame (rigorous over a bounded
area) or the position-dependent `wgs84_level_rotation` to follow Earth curvature
with no re-origining.

### Gauss-Markov reverting components

Several models hold a state that should revert toward zero absent evidence, all
via the same first-order Gauss-Markov (Ornstein-Uhlenbeck) mechanism — a
`(tau, sigma)` pair (time constant + steady-state spread), `gauss_markov_step`:

- `Static` reverts its (nuisance) velocity toward zero.
- `CoordinatedTurn` reverts its turn rate toward zero (straight), so a turn rate
  picked up from noise on a straight leg relaxes instead of persisting.
- `Singer` reverts its acceleration toward zero (the canonical named case).

The pull is gentle (long `tau`) — real maneuver evidence overrides it within a
scan or two, so it costs no responsiveness.

Smoothing: `rts_smooth`, `rts_smooth_segments`, `track_to_ells`. Result container:
`KalmanResult`.

## EKF vs UKF for observable updates

`update()` consumes an `Ell` (3D position + covariance) and is always an exact
linear update. `update_observable()` consumes a nonlinear observable (TDOA, FDOA,
AOA, Range, ...); choose the linearization with `update_method`:

- `"ekf"` (default): linearizes at the predicted mean via the observable's
  `jacobian()`. Cheap and accurate when the prior is tight relative to the
  geometry's nonlinearity.
- `"ukf"`: propagates sigma points through `predicted()` (no Jacobian). More
  robust (better-calibrated covariance) when the prior is broad and the
  observable is strongly nonlinear -- e.g. track initiation, long coast gaps,
  AOA, or satellite TDOA. Tune the spread with `ukf_alpha`.

The UKF's advantage is consistency, not necessarily smaller point error; switch
to it for robustness when the prior is broad, not expecting lower position error
in mildly nonlinear cases.

## Output: the unified tracking surface

`tracker.result` returns a `KalmanResult` that satisfies the `TrackingOutput`
protocol — the same surface the multi-target `gri-multitrack` engine reports, so
a consumer reads tracks, per-observation dispositions, and (on request) smoothed
trajectories the same way regardless of which tracker produced them. A
single-target tracker is the degenerate one-track case.

- `result.tracks` — a list of `TrackEstimate` (one element here; use
  `result.track` for it). Each carries `state` as an `EllVel` (position +
  velocity + full 6x6 covariance; an `EllAcc` when a constant-acceleration model
  contributes), `mode_probabilities`, `existence`, `confirmed`, `hits`, and a
  bound `predict`.
- `result.dispositions` — one `Disposition` per observation, in arrival order.
  Each has `index`, `used`, `verdict` (`assigned` / `rejected`), `track`,
  `confidence`. The **outlier / unused bucket** is simply:

  ```python
  outliers = [d for d in result.dispositions if not d.used]
  ```

  Only the gating trackers (`SmartIMM`, `SmartSegmentedIMM`) reject; a plain
  `IMM` has no gate, so everything is `assigned`.
- Prediction is a bound closure: `result.track.predict(dt_s)` returns a
  `PredictedState` (an `EllVel` at `time + dt_s`), propagated through the motion
  model. Ask for any horizon on demand — there is no fixed prediction list.
- Smoothing is **optional and on request** (it refines the *past* trajectory,
  not the present estimate, so it is never part of the live output):

  ```python
  smoothed = result.smoothed()          # {label: [(t, Ell), ...]}
  ```

  The live `tracks` are the filtered ("best given data so far") estimate;
  `smoothed()` returns the retrospective ("best given all data") trajectory.

```python
out = tracker.result
fix = out.track.state                  # EllVel: .ell (position), .vel_xyz, .vel_cov
v = out.track.state.vel_xyz            # current velocity
outliers = [d for d in out.dispositions if not d.used]
future = out.track.predict(30.0).state # extrapolate 30 s ahead
past = out.smoothed()                  # retrospective trajectory (opt-in)
```

## Notes

- State is ECEF; positions/covariances interchange with gri-ell `Ell` objects.
- Downstream consumers: the multi-target engine `gri-multitrack` orchestrates
  these trackers via the `Tracker` protocol; gri-convolve no longer ships them.

## License

MIT -- see [LICENSE](LICENSE).
