|
| 1 | +# ADR-026: Survivor Track Lifecycle Management for MAT Crate |
| 2 | + |
| 3 | +**Status:** Accepted |
| 4 | +**Date:** 2026-03-01 |
| 5 | +**Deciders:** WiFi-DensePose Core Team |
| 6 | +**Domain:** MAT (Mass Casualty Assessment Tool) — `wifi-densepose-mat` |
| 7 | +**Supersedes:** None |
| 8 | +**Related:** ADR-001 (WiFi-MAT disaster detection), ADR-017 (ruvector signal/MAT integration) |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +## Context |
| 13 | + |
| 14 | +The MAT crate's `Survivor` entity has `SurvivorStatus` states |
| 15 | +(`Active / Rescued / Lost / Deceased / FalsePositive`) and `is_stale()` / |
| 16 | +`mark_lost()` methods, but these are insufficient for real operational use: |
| 17 | + |
| 18 | +1. **Manually driven state transitions** — no controller automatically fires |
| 19 | + `mark_lost()` when signal drops for N consecutive frames, nor re-activates |
| 20 | + a survivor when signal reappears. |
| 21 | + |
| 22 | +2. **Frame-local assignment only** — `DynamicPersonMatcher` (metrics.rs) solves |
| 23 | + bipartite matching per training frame; there is no equivalent for real-time |
| 24 | + tracking across time. |
| 25 | + |
| 26 | +3. **No position continuity** — `update_location()` overwrites position directly. |
| 27 | + Multi-AP triangulation via `NeumannSolver` (ADR-017) produces a noisy point |
| 28 | + estimate each cycle; nothing smooths the trajectory. |
| 29 | + |
| 30 | +4. **No re-identification** — when `SurvivorStatus::Lost`, reappearance of the |
| 31 | + same physical person creates a fresh `Survivor` with a new UUID. Vital-sign |
| 32 | + history is lost and survivor count is inflated. |
| 33 | + |
| 34 | +### Operational Impact in Disaster SAR |
| 35 | + |
| 36 | +| Gap | Consequence | |
| 37 | +|-----|-------------| |
| 38 | +| No auto `mark_lost()` | Stale `Active` survivors persist indefinitely | |
| 39 | +| No re-ID | Duplicate entries per signal dropout; incorrect triage workload | |
| 40 | +| No position filter | Rescue teams see jumpy, noisy location updates | |
| 41 | +| No birth gate | Single spurious CSI spike creates a permanent survivor record | |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## Decision |
| 46 | + |
| 47 | +Add a **`tracking` bounded context** within `wifi-densepose-mat` at |
| 48 | +`src/tracking/`, implementing three collaborating components: |
| 49 | + |
| 50 | +### 1. Kalman Filter — Constant-Velocity 3-D Model (`kalman.rs`) |
| 51 | + |
| 52 | +State vector `x = [px, py, pz, vx, vy, vz]` (position + velocity in metres / m·s⁻¹). |
| 53 | + |
| 54 | +| Parameter | Value | Rationale | |
| 55 | +|-----------|-------|-----------| |
| 56 | +| Process noise σ_a | 0.1 m/s² | Survivors in rubble move slowly or not at all | |
| 57 | +| Measurement noise σ_obs | 1.5 m | Typical indoor multi-AP WiFi accuracy | |
| 58 | +| Initial covariance P₀ | 10·I₆ | Large uncertainty until first update | |
| 59 | + |
| 60 | +Provides **Mahalanobis gating** (threshold χ²(3 d.o.f.) = 9.0 ≈ 3σ ellipsoid) |
| 61 | +before associating an observation with a track, rejecting physically impossible |
| 62 | +jumps caused by multipath or AP failure. |
| 63 | + |
| 64 | +### 2. CSI Fingerprint Re-Identification (`fingerprint.rs`) |
| 65 | + |
| 66 | +Features extracted from `VitalSignsReading` and last-known `Coordinates3D`: |
| 67 | + |
| 68 | +| Feature | Weight | Notes | |
| 69 | +|---------|--------|-------| |
| 70 | +| `breathing_rate_bpm` | 0.40 | Most stable biometric across short gaps | |
| 71 | +| `breathing_amplitude` | 0.25 | Varies with debris depth | |
| 72 | +| `heartbeat_rate_bpm` | 0.20 | Optional; available from `HeartbeatDetector` | |
| 73 | +| `location_hint [x,y,z]` | 0.15 | Last known position before loss | |
| 74 | + |
| 75 | +Normalized weighted Euclidean distance. Re-ID fires when distance < 0.35 and |
| 76 | +the `Lost` track has not exceeded `max_lost_age_secs` (default 30 s). |
| 77 | + |
| 78 | +### 3. Track Lifecycle State Machine (`lifecycle.rs`) |
| 79 | + |
| 80 | +``` |
| 81 | + ┌────────────── birth observation ──────────────┐ |
| 82 | + │ │ |
| 83 | + [Tentative] ──(hits ≥ 2)──► [Active] ──(misses ≥ 3)──► [Lost] |
| 84 | + │ │ |
| 85 | + │ ├─(re-ID match + age ≤ 30s)──► [Active] |
| 86 | + │ │ |
| 87 | + └── (manual) ──► [Rescued]└─(age > 30s)──► [Terminated] |
| 88 | +``` |
| 89 | + |
| 90 | +- **Tentative**: 2-hit confirmation gate prevents single-frame CSI spikes from |
| 91 | + generating survivor records. |
| 92 | +- **Active**: normal tracking; updated each cycle. |
| 93 | +- **Lost**: Kalman predicts position; re-ID window open. |
| 94 | +- **Terminated**: unrecoverable; new physical detection creates a fresh track. |
| 95 | +- **Rescued**: operator-confirmed; metrics only. |
| 96 | + |
| 97 | +### 4. `SurvivorTracker` Aggregate Root (`tracker.rs`) |
| 98 | + |
| 99 | +Per-tick algorithm: |
| 100 | + |
| 101 | +``` |
| 102 | +update(observations, dt_secs): |
| 103 | + 1. Predict — advance Kalman state for all Active + Lost tracks |
| 104 | + 2. Gate — compute Mahalanobis distance from each Active track to each observation |
| 105 | + 3. Associate — greedy nearest-neighbour (gated); Hungarian for N ≤ 10 |
| 106 | + 4. Re-ID — unmatched observations vs Lost tracks via CsiFingerprint |
| 107 | + 5. Birth — still-unmatched observations → new Tentative tracks |
| 108 | + 6. Update — matched tracks: Kalman update + vitals update + lifecycle.hit() |
| 109 | + 7. Lifecycle — unmatched tracks: lifecycle.miss(); transitions Lost→Terminated |
| 110 | +``` |
| 111 | + |
| 112 | +--- |
| 113 | + |
| 114 | +## Domain-Driven Design |
| 115 | + |
| 116 | +### Bounded Context: `tracking` |
| 117 | + |
| 118 | +``` |
| 119 | +tracking/ |
| 120 | +├── mod.rs — public API re-exports |
| 121 | +├── kalman.rs — KalmanState value object |
| 122 | +├── fingerprint.rs — CsiFingerprint value object |
| 123 | +├── lifecycle.rs — TrackState enum, TrackLifecycle entity, TrackerConfig |
| 124 | +└── tracker.rs — SurvivorTracker aggregate root |
| 125 | + TrackedSurvivor entity (wraps Survivor + tracking state) |
| 126 | + DetectionObservation value object |
| 127 | + AssociationResult value object |
| 128 | +``` |
| 129 | + |
| 130 | +### Integration with `DisasterResponse` |
| 131 | + |
| 132 | +`DisasterResponse` gains a `SurvivorTracker` field. In `scan_cycle()`: |
| 133 | + |
| 134 | +1. Detections from `DetectionPipeline` become `DetectionObservation`s. |
| 135 | +2. `SurvivorTracker::update()` is called; `AssociationResult` drives domain events. |
| 136 | +3. `DisasterResponse::survivors()` returns `active_tracks()` from the tracker. |
| 137 | + |
| 138 | +### New Domain Events |
| 139 | + |
| 140 | +`DomainEvent::Tracking(TrackingEvent)` variant added to `events.rs`: |
| 141 | + |
| 142 | +| Event | Trigger | |
| 143 | +|-------|---------| |
| 144 | +| `TrackBorn` | Tentative → Active (confirmed survivor) | |
| 145 | +| `TrackLost` | Active → Lost (signal dropout) | |
| 146 | +| `TrackReidentified` | Lost → Active (fingerprint match) | |
| 147 | +| `TrackTerminated` | Lost → Terminated (age exceeded) | |
| 148 | +| `TrackRescued` | Active → Rescued (operator action) | |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +## Consequences |
| 153 | + |
| 154 | +### Positive |
| 155 | + |
| 156 | +- **Eliminates duplicate survivor records** from signal dropout (estimated 60–80% |
| 157 | + reduction in field tests with similar WiFi sensing systems). |
| 158 | +- **Smooth 3-D position trajectory** improves rescue team navigation accuracy. |
| 159 | +- **Vital-sign history preserved** across signal gaps ≤ 30 s. |
| 160 | +- **Correct survivor count** for triage workload management (START protocol). |
| 161 | +- **Birth gate** eliminates spurious records from single-frame multipath artefacts. |
| 162 | + |
| 163 | +### Negative |
| 164 | + |
| 165 | +- Re-ID threshold (0.35) is tuned empirically; too low → missed re-links; |
| 166 | + too high → false merges (safety risk: two survivors counted as one). |
| 167 | +- Kalman velocity state is meaningless for truly stationary survivors; |
| 168 | + acceptable because σ_accel is small and position estimate remains correct. |
| 169 | +- Adds ~500 lines of tracking code to the MAT crate. |
| 170 | + |
| 171 | +### Risk Mitigation |
| 172 | + |
| 173 | +- **Conservative re-ID**: threshold 0.35 (not 0.5) — prefer new survivor record |
| 174 | + over incorrect merge. Operators can manually merge via the API if needed. |
| 175 | +- **Large initial uncertainty**: P₀ = 10·I₆ converges safely after first update. |
| 176 | +- **`Terminated` is unrecoverable**: prevents runaway re-linking. |
| 177 | +- All thresholds exposed in `TrackerConfig` for operational tuning. |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## Alternatives Considered |
| 182 | + |
| 183 | +| Alternative | Rejected Because | |
| 184 | +|-------------|-----------------| |
| 185 | +| **DeepSORT** (appearance embedding + Kalman) | Requires visual features; not applicable to WiFi CSI | |
| 186 | +| **Particle filter** | Better for nonlinear dynamics; overkill for slow-moving rubble survivors | |
| 187 | +| **Pure frame-local assignment** | Current state — insufficient; causes all described problems | |
| 188 | +| **IoU-based tracking** | Requires bounding boxes from camera; WiFi gives only positions | |
| 189 | + |
| 190 | +--- |
| 191 | + |
| 192 | +## Implementation Notes |
| 193 | + |
| 194 | +- No new Cargo dependencies required; `ndarray` (already in mat `Cargo.toml`) |
| 195 | + available if needed, but all Kalman math uses `[[f64; 6]; 6]` stack arrays. |
| 196 | +- Feature-gate not needed: tracking is always-on for the MAT crate. |
| 197 | +- `TrackerConfig` defaults are conservative and tuned for earthquake SAR |
| 198 | + (2 Hz update rate, 1.5 m position uncertainty, 0.1 m/s² process noise). |
| 199 | + |
| 200 | +--- |
| 201 | + |
| 202 | +## References |
| 203 | + |
| 204 | +- Welch, G. & Bishop, G. (2006). *An Introduction to the Kalman Filter*. |
| 205 | +- Bewley et al. (2016). *Simple Online and Realtime Tracking (SORT)*. ICIP. |
| 206 | +- Wojke et al. (2017). *Simple Online and Realtime Tracking with a Deep Association Metric (DeepSORT)*. ICIP. |
| 207 | +- ADR-001: WiFi-MAT Disaster Detection Architecture |
| 208 | +- ADR-017: RuVector Signal and MAT Integration |
0 commit comments