Hebbian-Robotics / Hebbian-Robotics/hflow

[Bug]: _joint_motion_profile treats NaN velocity steps as compliant, causing duplicate-stamped streams to pass velocity gates and dead channels to report 0% idle

Closed
#546 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
269
Forks
150
Avg merge
8h 18m
Merged PRs (30d)
246

Description

### Version or commit

Current main

### Environment

Ubuntu 24.04 (WSL2), Python 3.12, x86_64

### Minimal reproduction

1. Create a stream of 100 joint position hops of 5 rad, where every hop occurs on a duplicate timestamp (duration <= 0).
2. Run `_joint_motion_profile` to compute `violation_pct`, `max_abs_velocity`, and `idle_fraction`.
3. Observe that despite 100 unmeasurable steps, the stream reports `violation_pct=0.0` and passes the gate.

### Expected behavior

Unmeasurable steps (duplicate timestamps or NaN positions) must be excluded from compliance percentages or cause the check to abstain/refuse. They must not be silently counted as "clean" measurements. A stream of identical motion with advancing timestamps should not yield a 100% difference in gate verdict compared to duplicate timestamps.

### Actual behavior

The code converts unmeasurable steps to NaN velocity (`safe_deltas_s = np.where(deltas_s > 0, deltas_s, np.nan)` at checks.py:103). However, both consumers compare this NaN:
- `speed > velocity_limit` is False (treated as non-violating).
- `speed < velocity_epsilon` is False (treated as not idle).

Executed proof:
[A: 100 hops of 5 rad, each on a duplicate stamp]
max_abs_velocity=0.0 violation_count=0 violation_pct=0.0 nonpositive_dt_count=100
gate(violation_pct<=5) -> GateDecided(verdict=True)

[B: IDENTICAL motion, advancing stamps]
max_abs_velocity=5000.0 violation_count=99 violation_pct=100.0
gate(violation_pct<=5) -> GateDecided(verdict=False)

[C: every sample NaN, advancing clock]
idle_fraction=0.0 -> stores silently ("zero idle" for a dead channel)

### Additional context

Root cause: `safe_deltas_s` marks steps unmeasurable with NaN, but no consumer treats NaN as "no data." In Python/NumPy, `>` and `<` against NaN both yield False, which the comparisons interpret as "clean" and "moving." This dilutes `np.mean(violation_mask)` and zeroes `np.nanmax` over surviving steps.

Why it matters: Curation gates that quarantine velocity spikes wave through batch-stamped stereo/multi-sensor streams. `idle_fraction < X` cuts reclassify teleoperating-but-jumpy demos as idle junk. The training corpus gets a two-way corruption: bad episodes kept, and good-looking numbers computed from steps that were never measured. The codebase already solves this correctly in `_trajectory_profile` (checks.py:1576-1584) and `estimate_fps_from_log_times` (video.py:86-90), but this profile missed the pattern.

Fix direction: Carry a measurable mask through `_JointMotionProfile` and compute `violation_pct`/`max_abs_velocity` over it only. Exclude unmeasurable steps from `idle_fraction`'s denominator. When the measurable share is below a floor (or the stream is all-NaN), emit `*_sample_count`/`nonpositive_dt_count` and no percentages, so an unmeasured stream cannot produce a pass-baiting number. Refuse NaN `max_abs_velocity` inside the check, not at the catalog.

Definition of done:
- Regression: streams A and B (identical positions, duplicate vs advancing stamps) must not disagree by 100.0 vs 0.0 `violation_pct`.
- Mutation proof 1: delete the measurable-mask from `violation_pct`'s denominator -> A test flips back to 0.0 -> suite fails.
- Mutation proof 2: feed an all-NaN channel -> `idle_fraction` must abstain instead of storing 0.0.
- Pin that `nonpositive_dt_count > 0` is visible in every percentage this profile feeds.

Contributor guide

Open the contributing guide

Research direction

Start in checks.py:103 and inspect _joint_motion_profile, then compare the measurable-data handling in _trajectory_profile at checks.py:1576-1584 and estimate_fps_from_log_times in video.py:86-90. Add regression coverage for duplicate timestamps, advancing timestamps, and an all-NaN channel; done means unmeasurable data cannot produce misleading percentages or an idle_fraction of 0.0, and nonpositive_dt_count remains visible.

Written by the indexing model from the issue text.

Assessment

Tech stack
numpy, python
Domain
data-engineering, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.