Skip to content

fix(checks): fail closed on unmeasurable joint-motion steps (#546) - #549

Merged
kstonekuan merged 2 commits into
Hebbian-Robotics:mainfrom
Sagar-024:fix/546-joint-motion-nan
Sep 16, 2026
Merged

kstonekuan merged 2 commits into
Hebbian-Robotics:mainfrom
Sagar-024:fix/546-joint-motion-nan

Conversation

@Sagar-024

Copy link
Copy Markdown
Contributor

Fixes #546

Summary

As detailed in the issue, _joint_motion_profile was treating NaN velocity steps (from duplicate timestamps or NaN positions) as compliant because NaN > limit and NaN < epsilon both evaluate to False. This PR carries an explicit measurable mask through the profile and computes all compliance metrics over measurable data only, ensuring unmeasurable steps are excluded from percentages and dead/NaN streams abstain rather than emitting pass-baiting numbers.

Changes

  • src/hflow/checks.py:
    • Added a measurable mask to _JointMotionProfile (strictly positive dt AND finite position jumps).
    • joint_discontinuity: violation_pct divides by measurable count only; violation_count only counts measurable steps; max_abs_velocity is a finite np.max (NaN refused inside the check).
    • idle_fraction: Unmeasurable steps are excluded from both numerator and denominator. Removed the dead if total_span_s else 0.0 fail-open.
    • Zero-measurable streams now refuse: emit *_sample_count/*_measurable_step_count/nonpositive_dt_count and withhold percentage keys.
  • Tests: Added tests/test_checks_nan_failopen.py with 5 core proofs:
    1. Stream A vs Stream B: Identical position hops. Stream A (duplicate stamps) abstains; Stream B (advancing stamps) reports violation_pct=100.0.
    2. All-NaN Channel: idle_fraction abstains instead of storing 0.0.
    3. Denominator Pin: 1/3 measurable steps, not 1/5, proving unmeasurable steps are excluded.
    4. Count Visibility: nonpositive_dt_count is visible wherever a percentage is emitted.
    5. Finite-Value Sweep: All emitted measurements are finite.

Mutation Proof

  • Denominator mask removed: Dilution test fails (1 failed, 69 passed).
  • Idle abstain reverted: Dead channel stores 0.0 and fails (3 failed, 1 passed).
  • Restored: All tests pass.

Gates

  • Full suite: 2032 passed, 8 skipped, zero failures.
  • ruff check, ruff format, ty (repo-wide): Clean.

_joint_motion_profile yields NaN velocity where a step duplicates its
timestamp or a position is NaN. Both consumers compared that NaN with a
plain operator -- NaN comparisons are False -- so unmeasurable steps
were counted as compliant and not-idle while their duration stayed in
the percentage denominators: a stream whose every step duplicates its
timestamp reported violation_pct 0.0 (the duplicate stamp hiding the
jump), an all-NaN channel reported idle_fraction 0.0, and joint
discontinuity's max_abs_velocity computed np.nanmax over nothing and
refused at the catalog append instead of inside the check (Hebbian-Robotics#546).

The profile now carries an explicit measurable mask -- strictly positive
duration AND finite positions on every joint -- and both consumers
compute over measurable steps only: violation_pct divides by the
measurable count, idle_fraction excludes unmeasurable time from the
denominator, max_abs_velocity is a finite np.max. A stream with nothing
measurable reports its counts (velocity_sample_count,
velocity_measurable_step_count, nonpositive_dt_count; the idle-side
prefixed equivalents) and withholds the percentage keys instead of
emitting 0.0 or NaN. Counts are per-check prefixed per the module's
no-shared-keys rule.

Tests: duplicate-stamp refusal (red) versus advancing-stamp 100.0
(green) on identical position hops; all-NaN abstention from both
checks; a denominator-dilution stream where the honest percentage is
1/3, not 1/5; and a finite-value sweep over every measurement these
paths emit. Mutation-verified: dropping the mask from the percentage
denominator turns the dilution test red; reverting the idle abstain
turns three of the four red.

@kstonekuan kstonekuan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is right and each half of measurable is held: dropping deltas_s > 0 or finite_jump fails different cases.

One thing to fix. np.nanmax at checks.py:124 warns on an all-NaN row, so the suite gains four RuntimeWarning: All-NaN slice encountered that main does not have, and a user processing a duplicate-stamped stream gets numpy noise in exactly the case this PR exists to handle.

You already compute the mask that says which rows those are.

np.nanmax on every row warns once per all-NaN slice, so a duplicate-stamped
or NaN-position stream -- exactly what Hebbian-Robotics#546 is about -- got RuntimeWarning
noise from a check whose job is to measure it quietly. The measurable mask
already names the all-NaN rows: fill the max from those rows with np.max,
leave the rest NaN, and np.nanmax is gone (Hebbian-Robotics#549 review).
@Sagar-024

Copy link
Copy Markdown
Contributor Author

Good catch. Replaced np.nanmax with a pre-allocated np.full array and a masked np.max assignment (per_step_max_speed[measurable] = np.max(...)) so unmeasurable rows never reach the reduction.

Verified the suite with -W error::RuntimeWarning - zero All-NaN slice encountered warnings now, matching the baseline on main. (Note: a pre-existing ResourceWarning from ffprobe teardown remains on both trees, but all RuntimeWarnings are resolved).

Pushed 9c38a1d.

@kstonekuan kstonekuan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The masked assignment is the right shape, and the comment explaining why it replaced np.nanmax will save the next person rediscovering it.

Verified against main merged in: -W error::RuntimeWarning runs clean at 2166 passed, so the four warnings are gone rather than filtered. All six guards hold under mutation. Dropping finite_jump reddens 2, dropping deltas_s > 0 reddens 3, widening either percentage denominator back to all steps reddens 1 each, and removing either nothing-measurable early return reddens 2 and 1.

Taking the advanced ones is exactly where the leverage is. Thanks.

@kstonekuan
kstonekuan merged commit f2e3cf0 into Hebbian-Robotics:main Sep 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants