Skip to content

Improve contact sensor data buffer and support save data to lerobot dataset#197

Merged
yuecideng merged 8 commits intomainfrom
yueci/contact-sensor
Mar 24, 2026
Merged

Improve contact sensor data buffer and support save data to lerobot dataset#197
yuecideng merged 8 commits intomainfrom
yueci/contact-sensor

Conversation

@yuecideng
Copy link
Contributor

Description

This PR introduces a comprehensive contact sensor implementation and expands the
observation functors API with physics attributes retrieval.

Key Changes

Contact Sensor (embodichain/lab/sim/sensors/contact_sensor.py)

  • Implemented ContactSensor class that detects contacts between rigid bodies and
    articulation links
  • Added ContactSensorCfg and ArticulationContactFilterCfg configuration classes
  • Supports filtering contacts by rigid bodies and articulation links
  • Supports both CPU and GPU physics engines
  • Provides contact data including: position, normal, friction, impulse, distance,
    user_ids, and validity mask
  • Implements filter_by_user_ids() for post-filtering contact reports
  • Added set_contact_point_visibility() for visualizing contact points with
    PointCloud
  • Configurable max_contacts_per_env with truncation warning
  • Supports filter_need_both_actor option for filtering behavior

Base Sensor Configuration (embodichain/lab/sim/sensors/base_sensor.py)

  • Enhanced SensorCfg.from_dict() to support:
    • Nested configclass initialization
    • List of configclasses with proper type conversion
    • Forward reference evaluation for complex types

Observation Functors (embodichain/lab/gym/envs/managers/observations.py)

  • Added get_object_uid(): Get user IDs of objects
  • Added get_rigid_object_physics_attributes(): Class functor for retrieving mass,
    friction, damping, inertia, and body scale with caching
  • Added get_articulation_joint_drive(): Class functor for retrieving joint drive
    properties (stiffness, damping, max_effort, max_velocity, friction) with caching
  • Added get_object_body_scale(): Get body scale of rigid objects

Documentation

  • Updated docs/source/overview/gym/observation_functors.md with new functor
    examples
  • Updated docs/source/overview/sim/sim_sensor.md with contact sensor
    documentation

Tests

  • Added tests/sim/sensors/test_contact.py: Comprehensive contact sensor tests for
    both CPU and GPU physics
  • Added tests/gym/envs/managers/test_observation_functors.py: Unit tests for all
    new observation functors

Type of change

  • New feature (non-breaking change which adds functionality)

Checklist

  • I have run the black . command to format the code base.
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • Dependencies have been updated, if applicable.

@yuecideng yuecideng added the sensor Virtual sensor for computing observation from simulation label Mar 24, 2026
Copilot AI review requested due to automatic review settings March 24, 2026 03:07
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR expands the simulation/gym API surface by introducing a richer contact sensor data layout (per-environment buffers + validity mask), improving SensorCfg.from_dict() for nested/list configclasses, and adding a new observation functor for retrieving object user IDs.

Changes:

  • Updated contact sensor reporting to use per-env fixed-size buffers with an is_valid mask and added Warp kernel support for scattering contacts.
  • Enhanced SensorCfg.from_dict() to better handle nested configclasses and lists of configclasses via type-hints.
  • Added get_object_uid observation functor plus docs/tests updates for the new functionality.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/sim/sensors/test_contact.py Extends contact sensor tests to validate new per-env buffer shapes and is_valid, and adds a from_dict conversion test.
tests/gym/envs/managers/test_observation_functors.py Adds unit tests for the new get_object_uid functor (plus mock env support).
examples/sim/sensors/create_contact_sensor.py Updates example sim config (but currently introduces a headless flag bug).
embodichain/utils/warp/kernels.py Adds scatter_contact_data Warp kernel to scatter filtered contacts into per-env buffers.
embodichain/lab/sim/sensors/contact_sensor.py Implements per-env contact buffers (max_contacts_per_env) and is_valid mask, plus env filtering/visualization changes.
embodichain/lab/sim/sensors/base_sensor.py Improves SensorCfg.from_dict() to support nested/list configclasses using evaluated type hints.
embodichain/lab/gym/envs/managers/observations.py Adds get_object_uid observation functor.
docs/source/overview/sim/sim_sensor.md Documents the new contact sensor buffer shapes, is_valid, env filtering, and max_contacts_per_env.
docs/source/overview/gym/observation_functors.md Documents get_object_uid and adds an example usage snippet.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings March 24, 2026 05:04
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@yuecideng yuecideng changed the title Improve contact sensor data buffer Improve contact sensor data buffer and support save data to lerobot dataset Mar 24, 2026
Copilot AI review requested due to automatic review settings March 24, 2026 08:06
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

wp.from_torch(self._data_buffer["user_ids"]),
wp.from_torch(self._data_buffer["is_valid"]),
],
device="cuda:0" if device == "cuda" else device,
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

wp.launch(...) forces device="cuda:0" when str(self.device) is exactly "cuda", which can ignore the intended CUDA device (e.g., when a caller passes torch.device("cuda") but the current/default device is not 0). Consider passing device=str(self.device) directly (or mapping torch.device -> Warp device in a way that preserves any explicit index) to avoid hard-coding GPU 0.

Suggested change
device="cuda:0" if device == "cuda" else device,
device=device,

Copilot uses AI. Check for mistakes.
Comment on lines +506 to +511
# Convert env_ids to tensor if needed
env_ids_tensor = (
torch.tensor(env_ids, device=self.device)
if not isinstance(env_ids, torch.Tensor)
else env_ids
)
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

env_ids_tensor is only moved to self.device when env_ids is not already a torch.Tensor. If a caller passes a CPU tensor while the sensor is on CUDA, the subsequent indexing operations will fail due to device mismatch. Consider ensuring env_ids_tensor = env_ids.to(self.device) when env_ids is a tensor (and possibly normalizing non-tensor sequences to a tensor as well).

Suggested change
# Convert env_ids to tensor if needed
env_ids_tensor = (
torch.tensor(env_ids, device=self.device)
if not isinstance(env_ids, torch.Tensor)
else env_ids
)
# Convert env_ids to tensor on the correct device
if isinstance(env_ids, torch.Tensor):
env_ids_tensor = env_ids.to(self.device)
else:
env_ids_tensor = torch.tensor(env_ids, device=self.device)

Copilot uses AI. Check for mistakes.
Comment on lines +349 to 353
features[key] = {
"dtype": str(space.dtype),
"shape": space.shape,
"names": key,
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

Extra top-level observations are now stored in features using the raw observation-space key (e.g. features[key]), but nested Dict observations still use the observation.{key}.{sub_key} prefix (see _add_nested_features). This creates an inconsistent schema within the same dataset (some extra obs under observation.*, others not), which can break downstream consumers expecting a single convention. Consider either keeping the observation. prefix for top-level extras as well, or removing it for nested extras so the naming is consistent.

Copilot uses AI. Check for mistakes.
Comment on lines +393 to +409
"""Get feature names for an observation based on its functor config.

Note:
The `space` parameter is kept for API consistency but not used
directly, as the feature names are derived from the functor config
and entity properties.

For observations generated by `get_object_uid`, returns meaningful names:
- RigidObject: object UID names
- Articulation/Robot: link names

Args:
key: The observation space key.
space: The observation space.

Returns:
A list of feature names for the observation.
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The _modify_feature_names docstring mentions parameters like key/space and says it returns a list of names, but the function signature is (_modify_feature_names(self, features: dict[str, Any]) -> None) and it mutates features in place. Please update the docstring so that the documented args/returns match the implementation (or adjust the signature if the docstring is intended).

Suggested change
"""Get feature names for an observation based on its functor config.
Note:
The `space` parameter is kept for API consistency but not used
directly, as the feature names are derived from the functor config
and entity properties.
For observations generated by `get_object_uid`, returns meaningful names:
- RigidObject: object UID names
- Articulation/Robot: link names
Args:
key: The observation space key.
space: The observation space.
Returns:
A list of feature names for the observation.
"""Modify feature metadata in-place based on the functor configuration.
This function adjusts the feature definitions stored in ``features``:
- Ensures scalar features with empty shapes ``()`` are converted to ``(1,)``,
as required by LeRobot.
- For observations generated by :func:`get_object_uid`, assigns meaningful
``names`` values based on the corresponding asset:
- :class:`RigidObject`: the asset UID is used as the feature name.
- :class:`Articulation` / :class:`Robot`: link names are used as
the feature names.
Args:
features: A mapping from feature keys to metadata dictionaries. This
dictionary is modified in-place; no value is returned.

Copilot uses AI. Check for mistakes.
sensor_name
][
frame_name
].cpu() # Debug here to inspect contact sensor data
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The inline comment # Debug here to inspect contact sensor data looks like a leftover debugging note in production code. Consider removing it or converting it into a proper explanatory comment (e.g., describing why .cpu() is required) to avoid confusing future readers.

Suggested change
].cpu() # Debug here to inspect contact sensor data
].cpu() # Move contact sensor tensor to CPU for serialization

Copilot uses AI. Check for mistakes.
Comment on lines +134 to +137
Note:
If an environment has more contacts than max_contacts_per_env, excess contacts
are silently dropped. The num_contacts_per_env output will reflect the actual
number of contacts written (capped at max_contacts_per_env).
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

This kernel docstring states that contacts beyond max_contacts_per_env are "silently dropped", but the PR description calls out a "truncation warning". Either implement a warning/metric when truncation occurs (e.g., after the launch, detect any env where num_contacts_per_env == max_contacts_per_env and log once), or update the PR description/docs to match the current silent-drop behavior.

Copilot uses AI. Check for mistakes.
@yuecideng yuecideng merged commit f43ccb2 into main Mar 24, 2026
9 checks passed
@yuecideng yuecideng deleted the yueci/contact-sensor branch March 24, 2026 08:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dataset sensor Virtual sensor for computing observation from simulation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants