Skip to content

feat(server): accuracy sprint 001 — Kalman tracker, multi-node fusion, eigenvalue counting#341

Draft
taylorjdawson wants to merge 3 commits intoruvnet:mainfrom
taylorjdawson:feat/accuracy-sprint-001
Draft

feat(server): accuracy sprint 001 — Kalman tracker, multi-node fusion, eigenvalue counting#341
taylorjdawson wants to merge 3 commits intoruvnet:mainfrom
taylorjdawson:feat/accuracy-sprint-001

Conversation

@taylorjdawson
Copy link
Copy Markdown

Summary

Wire three existing signal-crate components into the live sensing path to replace heuristic person counting with physics-grounded approaches:

  • Kalman Tracker (tracker_bridge.rs): Stable person IDs via PoseTracker with greedy Mahalanobis assignment, proper lifecycle transitions (Tentative→Active→Lost→Terminated), and Kalman-smoothed keypoints — eliminates frame-to-frame jitter and ephemeral 0-based IDs
  • Multi-Node Fusion (multistatic_bridge.rs): MultistaticFuser replaces naive .sum() of per-node person counts with attention-weighted CSI fusion — two nodes seeing one person now reports 1 (not 2). Fallback uses max to avoid double-counting overlapping coverage
  • Eigenvalue Person Counting (field_model.rs upgrade): Full covariance matrix + ndarray-linalg eigendecomposition with Marcenko-Pastur noise threshold replaces diagonal variance approximation. estimate_occupancy() for runtime counting, calibration API (/calibration/start|stop|status)

Changes

New files

File Lines Purpose
sensing-server/src/tracker_bridge.rs 397 f64↔f32 bridge, greedy assignment, COCO-17 mapping
sensing-server/src/multistatic_bridge.rs 263 NodeState→MultiBandCsiFrame conversion, fusion fallback
sensing-server/src/field_bridge.rs 143 Occupancy fallback chain, calibration feeding, position parsing

Modified files

File +/- Changes
sensing-server/Cargo.toml +3 Add wifi-densepose-signal dependency
sensing-server/src/main.rs +279/-81 AppStateInner fields, 5 tracker call sites, 2 fusion sites, 4 eigenvalue sites, calibration API, CLI args
signal/Cargo.toml +1 Add ndarray-linalg dependency
signal/src/ruvsense/field_model.rs +495/-81 Covariance accumulator, SVD finalization, estimate_occupancy(), baseline_eigenvalue_count

Key design decisions

  • Global tracker (not per-node) for cross-node re-ID when person walks between nodes
  • ndarray-linalg over nalgebra — signal crate already uses ndarray, workspace dep exists
  • Bridge modules isolate all f64↔f32 conversions, keeping main.rs changes minimal
  • Graceful fallback everywhere: uncalibrated → heuristic, fusion failure → max-per-node, no tracks → raw detections

Test plan

  • cargo test -p wifi-densepose-signal --no-default-features — existing 49+ tests pass (16 tracker + 15 multistatic + 18+4 field_model)
  • cargo test -p wifi-densepose-sensing-server — existing server tests + new bridge tests pass
  • cargo test --workspace --no-default-features — full 1031+ test suite green
  • Manual: connect ESP32 node, verify person IDs are stable UUIDs in WebSocket JSON
  • Manual: connect 2+ nodes, verify single person counts as 1 (not 2)
  • Manual: POST /calibration/start, wait, POST /calibration/stop — verify baseline eigenvalue count reported

Refs: .swarm/plans/accuracy-sprint-001.md

🤖 Generated with claude-flow

taylorjdawson and others added 2 commits March 27, 2026 21:21
Complements ruvnet#326 (per-node state pipeline) with additional features:

- Dynamic adaptive classifier: discover activity classes from training
  data filenames instead of hardcoded array. Users add classes via
  filename convention (train_<class>_<desc>.jsonl), no code changes.
- Per-node UI cards: SensingTab shows individual node status with
  color-coded markers, RSSI, variance, and classification per node.
- Colored node markers in 3D gaussian splat view (8-color palette).
- Per-node RSSI history tracking in sensing service.
- XSS fix: UI uses createElement/textContent instead of innerHTML.
- RSSI sign fix: ensure dBm values are always negative.
- GET /api/v1/nodes endpoint for per-node health monitoring.
- node_features field in WebSocket SensingUpdate messages.
- Firmware watchdog fix: yield after every frame to prevent IDLE1 starvation.

Addresses ruvnet#237, ruvnet#276, ruvnet#282

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…, eigenvalue counting

Wire three existing signal-crate components into the live sensing path:

Step 1 — Kalman Tracker (tracker_bridge.rs):
- PoseTracker from wifi-densepose-signal wired into all 5 mutable
  derive_pose_from_sensing call sites
- Stable TrackId-based person IDs replace ephemeral 0-based indices
- Greedy Mahalanobis assignment with proper lifecycle transitions
  (Tentative → Active → Lost → Terminated)
- Kalman-smoothed keypoint positions reduce frame-to-frame jitter

Step 2 — Multi-Node Fusion (multistatic_bridge.rs):
- MultistaticFuser replaces naive .sum() aggregation at both ESP32 paths
- Attention-weighted CSI fusion across nodes with cosine-similarity weights
- Fallback uses max (not sum) to avoid double-counting overlapping coverage
- Node positions configurable via --node-positions CLI arg
- Single-node passthrough preserved (min_nodes=1)

Step 3 — Eigenvalue Person Counting (field_model.rs upgrade):
- Full covariance matrix accumulation (replaces diagonal variance approx)
- True eigendecomposition via ndarray-linalg Eigh (Marcenko-Pastur threshold)
- estimate_occupancy() for runtime eigenvalue-based counting
- Calibration API: POST /calibration/start|stop, GET /calibration/status
- Graceful fallback to score_to_person_count when uncalibrated

New files: tracker_bridge.rs, multistatic_bridge.rs, field_bridge.rs
Modified: sensing-server main.rs, Cargo.toml; signal field_model.rs, Cargo.toml

Refs: .swarm/plans/accuracy-sprint-001.md

Co-Authored-By: claude-flow <ruv@ruv.net>
Critical fixes:
- C1: FieldModel created with n_links=1 (single_link_config) so
  feed_calibration/extract_perturbation no longer get DimensionMismatch
- C2: variance_explained now uses centered covariance trace (E[x²]-E[x]²)
  matching mode_energies normalization
- C3: MP ratio uses total_obs = frames * links for consistent threshold
  between calibration and runtime
- C4: Noise estimator filters to positive eigenvalues only, preventing
  collapse to ~0 on rank-deficient matrices (p > n)
- C5: ESP32 paths gate total_persons on presence — empty room reports 0

High fixes:
- H1: Bounding box computed from observed keypoints only (confidence > 0),
  preventing collapse from centroid-filled unobserved slots
- H2: fuse_or_fallback returns Option<usize> instead of sentinel 0,
  eliminating type ambiguity between "fusion succeeded" and "zero people"
- H3: Monotonic epoch-relative timestamps replace wall-clock/Instant mixing,
  preventing spurious TimestampMismatch on NTP steps
- H5: ndarray-linalg gated behind "eigenvalue" feature flag (default=on),
  diagonal fallback used with --no-default-features

Moderate fixes:
- M1: calibration_start guards against replacing Fresh calibration
- M2: parse_node_positions logs warning for malformed entries

Co-Authored-By: claude-flow <ruv@ruv.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants