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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants