Metadata-Version: 2.4
Name: gri-multitrack
Version: 0.3.0
Summary: Multi-target tracking over gri per-target estimators: ingest/routing, measurement-space scoring, GNN + MFA/MHT association, track lifecycle, LMB existence, and Poisson-binomial cardinality
Project-URL: Homepage, https://geosolresearch.com
Project-URL: Repository, https://gitlab.com/geosol-foss/python/gri-multitrack
Project-URL: Issues, https://gitlab.com/geosol-foss/python/gri-multitrack/-/issues
Project-URL: Changelog, https://gitlab.com/geosol-foss/python/gri-multitrack/-/releases
Author-email: GeoSol Research Inc <contact@geosolresearch.com>
License-Expression: MIT
License-File: LICENSE
Keywords: MFA,MHT,geolocation,lmb,multi-target,tracking
Classifier: Development Status :: 2 - Pre-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-convolve>=0.2.0
Requires-Dist: gri-ell>=0.3.2
Requires-Dist: gri-geosim>=0.4.1
Requires-Dist: gri-kalman>=0.2.1
Requires-Dist: gri-obs>=0.3.0
Requires-Dist: gri-pos>=0.2.0
Requires-Dist: gri-trajectory>=0.1.0
Requires-Dist: gri-utils>=0.4.0
Requires-Dist: numpy>=2.3.3
Requires-Dist: scipy>=1.16.2
Provides-Extra: crosscheck
Requires-Dist: stonesoup>=1.8; extra == 'crosscheck'
Description-Content-Type: text/markdown

# gri-multitrack

Multi-target geolocation tracking. gri-multitrack combines the gri per-target
Kalman-IMM (`gri-kalman`, fed by `gri-obs` observables) with a multi-target policy
layer: ingest/routing of the feed-in data tiers, measurement-space scoring,
per-scan association, track lifecycle, existence (Labeled Multi-Bernoulli), and
a Poisson-binomial count distribution.

The governing seam is **"gri scores, gri-multitrack assigns"**: gri-kalman owns
per-target estimation and the measurement-space likelihoods; gri-multitrack owns
everything multi-target. The multi-target layer is **DIY on numpy/scipy** --
gri-multitrack is Stone-Soup-free (see the "Architecture pivot" in `AGENTS.md`).
The primary tracker class is `MultiTracker` (the "Crucible" name now belongs to
the companion 3D app).

See `PLAN.md` for the live program plan (state, backlog, decisions) and
`AGENTS.md` for repo guidance. Scenario generation, replay harnesses, scoring,
and the replay viewer live in the sibling **`gri-tracksim`** repo, which
depends on this engine and serializes its outputs (the engine itself is
serialization-free).

## Status

v1 of the original build plan is **complete** (see `PLAN.md`): ingest ->
score -> associate -> gri IMM update -> lifecycle -> LMB output, on geos, raw
TDOAs, and presence events, plus the outlier stream (clutter floors,
per-observation dispositions, the extensible cure catalog), the stationary
convolve resolver, split/merge with lineage, and batch RTS retrospectives.

Implemented:

- Ingest + router for the feed-in tiers (geo `Ell`, observables, presence).
- Per-track adapter over any gri `Tracker` (default `SmartSegmentedIMM`; CV /
  CoordinatedTurn / Static bank).
- Measurement-space scoring seam (Gaussian innovation + chi-squared gate).
- GNN scaffold associator (local scipy Hungarian) for bring-up.
- **MFA tracker** (`MfaTracker`): a local hypothesis-oriented MHT that defers at
  ambiguous crossings and resolves via accumulated kinematic likelihood -- the v1
  associator (loose-coupled; not Stone Soup's MFA). Standalone engine mirroring
  `MultiTracker`.
- Track lifecycle: birth from geos, M-of-N confirm, patient deletion.
- Existence r_i + Poisson-binomial count distribution.
- Presence ("is it on") -> `coast(t)` + existence bump.
- LMB output: labeled tracks + count distribution + top-level per-observation
  `dispositions` + the per-scan `association` diagnostic (gates / marginals /
  hypotheses), associator-agnostic. See [Output](#output).

Also implemented since the skeleton:

- Per-kind clutter likelihood floors and per-observation DISPOSITIONS
  (assigned / birthed / clutter / cured); the user-extensible `Cure` Protocol.
- The stationary resolver (convolve as the live estimator of a locked track;
  the cluster answer as `resolved`), split/merge with `TrackEvent` lineage,
  and `smoothed_tracks()` batch retrospectives.
- Serialization, GOSPA/OSPA metrics, and the replay viewer live in
  `gri-tracksim` (the engine stays serialization-free).

### Notable design choices

- **The per-track estimator is any gri-kalman `Tracker`; the default is
  `SmartSegmentedIMM`.** gri-kalman exposes one uniform interface
  (`update(ell, t)` / `update_observable` / `predict` / `coast` /
  `smoothed_track` / `result` / `is_initialized`) across `IMM`, `SmartIMM`,
  `SegmentedIMM`, and `SmartSegmentedIMM`. gri-multitrack defaults to the maneuver-segmenting,
  outlier-rejecting `SmartSegmentedIMM` the design calls for; pass
  `tracker_factory=make_imm` (or any `Tracker` factory) to swap it. The choice
  is isolated to `gri_multitrack/track.py`.
- **Stone-Soup-free; multi-target is DIY on numpy/scipy.** Both associators are
  local (GNN over scipy `linear_sum_assignment`; MFA a local hypothesis-oriented
  MHT). Stone Soup's MFA is filter-coupled and would cost the gri IMM, and is
  heavy (~48 MB of deps + `ortools`); see the `CLAUDE.md` "Architecture pivot".
  If LAP speed ever matters, add `lapsolver`/`lap` (tiny) -- not `ortools`. Stone
  Soup remains only as an optional dev-time GOSPA/OSPA cross-check.

## Install

Uses `uv` with editable path dependencies on the sibling gri repos (in
`../../foss/`).

```bash
uv sync                     # core (Stone-Soup-free)
uv sync --extra crosscheck  # optional: Stone Soup, for a dev-time GOSPA/OSPA check only
```

## Run

```bash
uv run python examples/two_target_demo.py   # end-to-end demo
uv run pytest                                # tests
uv run ruff check gri_multitrack test              # lint
uv run ty check                              # type check
```

## Quick use

```python
from gri_multitrack import MultiTracker

tracker = MultiTracker()
# each item is (payload, time_s); payload is an Ell, a gri-obs observable,
# or a PresenceObs.
outputs = tracker.process([(ell0, 0.0), (tdoa1, 1.0), (presence, 2.0)])

final = outputs[-1]
for t in final.tracks:
    print(t.label, t.existence, t.is_stationary, t.mode_probabilities)
print(final.count_distribution)  # Poisson-binomial P(N=k)
```

## Output

A `TrackerOutput` per scan, in the same unified surface a single-target
`gri-kalman` tracker reports (a single-target tracker is the degenerate
one-track case), so the two are read interchangeably.

- `output.tracks` — `LabeledTrack` records (a gri-kalman `TrackEstimate` plus
  the stationary fields). Each carries `state` as an `EllVel` (position +
  velocity + 6x6 covariance; `.ell` for the position-only `Ell`),
  `mode_probabilities`, `existence` (r_i), `confirmed`, `hits`, `parent`
  (split lineage), `is_stationary` / `stationary_locked` / `resolved` (the
  convolver's cluster answer), and a bound `predict`.
- **Where are the unused / outlier observations?** Top-level
  `output.dispositions`, one `Disposition` per observation. Each has `index`,
  `used`, `verdict`, `track`, `confidence`. The outlier bucket is:

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

  Verdicts: `assigned` (absorbed by a track), `birthed` (seeded a new track),
  `clutter` (explained better as clutter — `used=False`), `cured` (repaired by a
  `Cure` then absorbed; `d.cure` names it, `d.variant` is the repaired
  measurement). The committal GNN reports `confidence=1.0`; the MFA reports the
  world-agreement mass with `provisional=True`.
- `output.count_distribution` / `expected_count` / `most_likely_count` — the
  Poisson-binomial cardinality over the existences.
- `output.events` — this scan's split / merge `TrackEvent`s.
- `output.association` — the per-scan diagnostic (gates / marginals /
  hypotheses); `None` when not computed.
- `output.worlds` — the MFA's surviving global hypotheses with weights (the
  MHT-only confidence surface); empty for the committal GNN.
- Prediction: `track.predict(dt_s)` returns a `PredictedState` at any horizon
  (a locked-stationary track predicts its convolved fix). Smoothing is
  **opt-in**: `tracker.smoothed_tracks()` returns the per-label RTS
  retrospective (best given *all* data, refining the past); the live `tracks`
  are the filtered best-given-data-so-far.

## Layout

- `gri_multitrack/ingest.py` -- feed-in types, routing, scan grouping.
- `gri_multitrack/track.py` -- per-track adapter over the gri IMM.
- `gri_multitrack/scoring.py` -- measurement-space likelihood + gate ("gri scores").
- `gri_multitrack/association.py` -- GNN scaffold + `Associator` protocol.
- `gri_multitrack/lifecycle.py` -- birth / confirm / delete / existence.
- `gri_multitrack/cardinality.py` -- Poisson-binomial count distribution.
- `gri_multitrack/output.py` -- Labeled Multi-Bernoulli output records.
- `gri_multitrack/tracker.py` -- the `MultiTracker` orchestrator.
