Skip to content

Fix compute_first_contact/air missing transitions as the sensor clock ages - #7574

Open
AntoineRichard wants to merge 3 commits into
developfrom
antoiner/fix-first-contact-tolerance
Open

Fix compute_first_contact/air missing transitions as the sensor clock ages#7574
AntoineRichard wants to merge 3 commits into
developfrom
antoiner/fix-first-contact-tolerance

Conversation

@AntoineRichard

@AntoineRichard AntoineRichard commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Description

ContactSensor.compute_first_contact(dt) / compute_first_air(dt) silently missed most touchdowns and lift-offs once the simulation had run for a few seconds. On a transition step the contact (resp. air) timer is exactly one sensor update interval, so the check degenerated to dt < dt + 1e-8. The sensor clock is a float32 accumulator whose rounding error reaches ~1e-6 after a few seconds of simulated time, roughly 100x the default tolerance, so the events vanished depending on the magnitude of the sim clock. The reporter's CPU reproducer shows 352/500 touchdowns and lift-offs missed at the default tolerance; it reproduces exactly on develop. All three backends (Newton, PhysX, OVPhysX) share the same logic, and the 2.x torch sensor had the same defect, so this is long-standing rather than a Newton regression.

Fix. abs_tol now defaults to None and is resolved at call time by a single helper in BaseContactSensor to half the sensor update interval, i.e. 0.5 * max(cfg.update_period, physics_dt). Valid timer values are integer multiples of that interval, so half an interval is the midpoint between "one update ago" and "two updates ago". This stays robust to float32 clock drift for hours of simulated time and works for both history_length == 0 (lazy, once-per-policy-step refresh) and history_length > 0 (every physics substep). The Warp kernels are unchanged. Passing an explicit abs_tol still works; callers who want the previous behaviour can pass abs_tol=1e-8.

Tests. Public-API regression tests in the Newton, PhysX and OVPhysX contact sensor suites: settle a body in contact, age the sensor clock to 2.5 / 10 / 30 s, lift and land the body, and poll compute_first_contact / compute_first_air with default arguments. These fail on the unfixed code (e.g. compute_first_air missed the lift-off at clock 2.833s: reported [], expected [0]) and pass with the fix. The Newton variant covers both history_length modes.

Out of scope, noted for follow-up.

  • The transition interval is counted in both the ending and the starting phase, so last_air_time / last_contact_time overestimate each phase by one update interval (also raised in the issue). That bias is present since 2.x and baked into tuned feet_air_time thresholds, so changing it would rescale rewards for trained velocity policies and deserves its own PR.
  • compute_first_* read the private timer buffers directly and therefore bypass the lazy refresh that the data property triggers when history_length == 0.
  • On cuda:0 with history_length == 0, the PhysX and OVPhysX sensors serve stale net forces when buffers refresh only on data access, independently of this change. That is why the PhysX/OVPhysX public-API tests cover only the substep cadence.

Fixes #7283

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Release backport

  • Backport this pull request to the active release branch after it merges into develop

Checklist

  • I have read and understood the contribution guidelines
  • I have run the pre-commit checks with ./isaaclab.sh --format
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • I have added a changelog fragment under source/<pkg>/changelog.d/ for every touched package (do not edit CHANGELOG.rst or bump extension.toml — CI handles that)
  • I have added my name to the CONTRIBUTORS.md or my name already exists there

compute_first_contact and compute_first_air compared the contact/air timer
against dt + 1e-8. On a transition step the timer is exactly one sensor
update interval, so the check degenerated to dt < dt + 1e-8. The sensor
clock is a float32 accumulator whose rounding error reaches ~1e-6 after a
few seconds of simulated time, roughly 100x the tolerance, so most
touchdowns and lift-offs were silently dropped.

Resolve abs_tol at call time instead: it now defaults to None, meaning half
the sensor update interval. Valid timers are integer multiples of that
interval, so half an interval is the midpoint between "one update ago" and
"two updates ago", which stays robust for hours of simulated time and works
for both the substep and the lazy refresh cadence. Callers that relied on
the old default can pass abs_tol=1e-8 explicitly.

Fixes #7283
Cover the tolerance contract at two levels.

Kernel-level (no simulation, CPU, one file per backend): drive each
backend's contact update kernel from an aged clock over an alternating
contact schedule and assert that a threshold of dt + half an update
interval flags every touchdown and lift-off and nothing else. These
exercise the threshold math the new default produces, for both the substep
and the lazy refresh cadence.

Public-API: drop or teleport a body and poll compute_first_contact and
compute_first_air with the default tolerance after ageing the sensor clock
to 2.5, 10 and 30 s, comparing against transitions observed from the
reported contact forces. These fail on the old 1e-8 default.

The PhysX and OvPhysx public-API cases cover only the substep cadence:
with zero history those backends serve stale contact forces on GPU when
buffers refresh solely on data access, independently of this change.
@AntoineRichard
AntoineRichard requested a review from a team September 4, 2026 14:53
@github-actions github-actions Bot added bug Something isn't working isaac-lab Related to Isaac Lab team labels Sep 4, 2026
@greptile-apps

greptile-apps Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR replaces the fixed contact-transition comparison tolerance with half the configured sensor update interval across Newton, PhysX, and OVPhysX, and adds aged-clock regression coverage.

  • Centralizes default tolerance resolution in BaseContactSensor.
  • Applies the resolved threshold to first-contact and first-air detection in all three backends.
  • Adds backend kernel and public-API regression tests plus changelog fragments.

Confidence Score: 4/5

The long-running transition-detection failure should be addressed before merging because simulations around 18 hours can again silently lose contact and air events.

The new half-interval tolerance fixes short aged-clock runs but remains below the quantized float32 timestamp increment at sufficiently large, realistic simulation times, causing all three backends to fail the same strict transition comparison.

Files Needing Attention: source/isaaclab/isaaclab/sensors/contact_sensor/base_contact_sensor.py and the aged-clock kernel tests

Important Files Changed

Filename Overview
source/isaaclab/isaaclab/sensors/contact_sensor/base_contact_sensor.py Introduces the shared cadence-based tolerance, but half an interval becomes insufficient once float32 timestamp spacing exceeds the resulting threshold margin.
source/isaaclab_newton/isaaclab_newton/sensors/contact_sensor/contact_sensor.py Adopts the shared tolerance consistently for Newton, inheriting its long-runtime precision limit.
source/isaaclab_ov/isaaclab_ov/sensors/contact_sensor/contact_sensor.py Adopts the shared tolerance consistently for OVPhysX, inheriting its long-runtime precision limit.
source/isaaclab_physx/isaaclab_physx/sensors/contact_sensor/contact_sensor.py Adopts the shared tolerance consistently for PhysX, inheriting its long-runtime precision limit.
source/isaaclab_newton/test/sensors/test_contact_sensor_kernels.py Covers clocks only through 30 seconds, leaving the float32 spacing boundary at long runtimes untested.
source/isaaclab_ov/test/sensors/test_contact_sensor_kernels.py Adds both refresh-cadence regression coverage but does not exercise large float32 clock magnitudes.
source/isaaclab_physx/test/sensors/test_contact_sensor_kernels.py Adds both refresh-cadence regression coverage but does not exercise large float32 clock magnitudes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Float32 sensor timestamp] --> B[Backend timer update]
  B --> C[Current contact or air timer]
  D[dt plus resolved tolerance] --> E[Transition comparison]
  C --> E
  E --> F[First contact or first air result]
Loading

Reviews (1): Last reviewed commit: "Add first-transition tolerance regressio..." | Re-trigger Greptile

if abs_tol is not None:
return abs_tol
# An update period of 0.0 means the sensor is updated on every physics step.
return 0.5 * max(self.cfg.update_period, self._sim_physics_dt)

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.

P1 Long-running transitions fail again

When a simulation with a 0.005-second physics step reaches 65536 seconds, the next float32 timestamp increment is 0.0078125 seconds while the new default threshold is only 0.0075 seconds. The strict timer comparison therefore silently drops touchdowns and lift-offs again across all three backends.

Knowledge Base Used: Simulation, rendering, and sensors

@isaaclab-review-bot isaaclab-review-bot Bot 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.

Isaac Lab Review Bot

The PR centralizes the default first-transition tolerance in BaseContactSensor and applies it consistently across Newton, PhysX, and OVPhysX while preserving explicit tolerance behavior. No candidate findings were supplied or supported for acceptance.

  • Design and architecture: The shared helper is an appropriate ownership point for backend-independent transition semantics. Each backend resolves the tolerance before launching its unchanged Warp transition kernel, avoiding duplicated policy logic in kernels.
  • API: The abstract base and all concrete overrides consistently change abs_tol from float = 1e-8 to float | None = None. Explicit float values retain their prior behavior, documentation includes [s] units, and changelog fragments describe the changed default and the abs_tol=1e-8 compatibility path.
  • Implementation: The threshold path was traced as dt + 0.5 * max(cfg.update_period, physics_dt) through all three backends and both refresh cadences represented by the tests. Public API regression tests exercise the new default against aged clocks. The duplicated kernel-level tests validate the numerical threshold rather than the helper itself, but the backend API tests cover helper integration, so this remains a non-blocking test-maintenance tradeoff.

No blocking issues. No inline issue met the actionable-evidence threshold; the assessment above records the review feedback.

Automated review; human maintainers own approval decisions.

@aidaodedjl

Copy link
Copy Markdown

Thanks for the quick turnaround and for the archaeology on the 2.x torch sensor — good to know this predates the Newton backend entirely.

The half-interval default is the right minimal fix, and the reasoning for rejecting option (2) is airtight. One pushback on the reason for rejecting option (1), though, since #7294 implements exactly that path: the policy-step-visibility concern is solvable, and that PR solves it. The latches are cleared by the reset kernels and — the part that matters for substep refresh — compute_first_contact() / compute_first_air() refresh outdated buffers before reading, so a lazy caller never sees a latch from a previous policy step, and an every-substep caller never sees a stale one. The flags never need to cross a boundary the kernel cannot see; the query pulls them across.

Not arguing to swap fixes — the tolerance change is smaller, and the calibrated-thresholds argument for keeping the last_*_time bias out of this PR is the correct call. The question is whether the latching design is still of interest as a follow-up: beyond not resting on a comparison margin, it makes compute_first_* event-accurate rather than timer-inferred, which matters if anyone ever queries with a dt smaller than the update interval, and it composes with per-transition event ages (useful once the last_*_time bias gets revisited, since the correction will need exactly that distinction). If the answer is "the tolerance fix is all we want", I will close #7294 against this one with no hard feelings — it did its job getting the issue confirmed.

The kernels are unchanged by the fix, so these tests passed with or without
it and only duplicated the threshold arithmetic already covered by the
public-API aged-clock tests in each backend's contact sensor suite.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants