Feat(randomization): add articulation mass randomization functor#219
Feat(randomization): add articulation mass randomization functor#219
Conversation
Add `randomize_articulation_mass` event functor that randomizes articulation link masses with regex-based link selection via the `link_names` parameter. Also adds `set_mass`/`get_mass` methods to the Articulation class for high-level mass management. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an event functor to randomize per-link masses on articulations, along with a batched Articulation.get_mass() / Articulation.set_mass() API to support that randomization (plus docs/tests updates).
Changes:
- Add
randomize_articulation_massfunctor with regex-based link selection and absolute/relative modes. - Add batched mass getters/setters on
Articulationfor per-link mass management. - Extend event functor tests and documentation to cover the new mass randomization.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
tests/gym/envs/managers/test_event_functors.py |
Adds mock per-link mass support and tests for randomize_articulation_mass. |
embodichain/lab/sim/objects/articulation.py |
Introduces Articulation.set_mass() / Articulation.get_mass() batched APIs. |
embodichain/lab/sim/cfg.py |
Clarifies articulation link mass/density behavior in config docstring. |
embodichain/lab/gym/envs/managers/randomization/physics.py |
Adds randomize_articulation_mass and related imports. |
docs/source/overview/gym/event_functors.md |
Documents the new event functor. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| lower=mass_range[0], upper=mass_range[1], size=(num_instance, num_links) | ||
| ) | ||
|
|
||
| if relative: | ||
| # Get current mass from the articulation | ||
| current_masses = articulation.get_mass( | ||
| link_names=matched_link_names, env_ids=env_ids | ||
| ) |
There was a problem hiding this comment.
sample_uniform is called without a device, so sampled_masses will be on CPU. In relative=True mode you then add it to current_masses returned by articulation.get_mass() (allocated on articulation.device), which will raise a device mismatch when the sim runs on CUDA. Pass an explicit device (e.g., articulation.device/env.device) to sample_uniform, and ensure both tensors are on the same device before adding.
| lower=mass_range[0], upper=mass_range[1], size=(num_instance, num_links) | |
| ) | |
| if relative: | |
| # Get current mass from the articulation | |
| current_masses = articulation.get_mass( | |
| link_names=matched_link_names, env_ids=env_ids | |
| ) | |
| lower=mass_range[0], | |
| upper=mass_range[1], | |
| size=(num_instance, num_links), | |
| device=articulation.device, | |
| ) | |
| if relative: | |
| # Get current mass from the articulation | |
| current_masses = articulation.get_mass( | |
| link_names=matched_link_names, env_ids=env_ids | |
| ).to(articulation.device) |
| from embodichain.lab.sim.objects import Articulation, RigidObject, Robot | ||
| from embodichain.lab.gym.envs.managers.cfg import SceneEntityCfg | ||
| from embodichain.utils.math import sample_uniform | ||
| from embodichain.utils.string import resolve_matching_names | ||
| from embodichain.utils import logger |
There was a problem hiding this comment.
Robot is imported but unused in this module. Please remove it to avoid unused-import warnings and keep the imports minimal.
|
|
||
| for i, env_idx in enumerate(local_env_ids): | ||
| for j, name in enumerate(link_names): | ||
| self._entities[env_idx].set_mass(name, mass[i, j].item()) |
There was a problem hiding this comment.
set_mass() calls self._entities[env_idx].set_mass(name, ...), but the rest of the codebase interacts with masses through get_physical_body(...).get_mass() / RigidBody.set_mass(...). Unless the underlying articulation entity exposes set_mass, this will raise at runtime. Consider using self._entities[env_idx].get_physical_body(name).set_mass(...) for symmetry with get_mass() and with RigidObject.set_mass().
| self._entities[env_idx].set_mass(name, mass[i, j].item()) | |
| self._entities[env_idx].get_physical_body(name).set_mass( | |
| mass[i, j].item() | |
| ) |
| if len(local_env_ids) != len(mass): | ||
| logger.log_error( | ||
| f"Length of env_ids {len(local_env_ids)} does not match mass length {len(mass)}." | ||
| ) |
There was a problem hiding this comment.
set_mass() validates only the first dimension (env count) but not that mass is 2D and that mass.shape[1] == len(link_names). If callers pass a mismatched tensor, this will fail later with an IndexError (or silently set wrong values). Add an explicit shape check and raise via logger.log_error/ValueError before looping.
| if len(local_env_ids) != len(mass): | |
| logger.log_error( | |
| f"Length of env_ids {len(local_env_ids)} does not match mass length {len(mass)}." | |
| ) | |
| expected_shape = (len(local_env_ids), len(link_names)) | |
| if mass.ndim != 2 or mass.shape[0] != expected_shape[0] or mass.shape[1] != expected_shape[1]: | |
| msg = ( | |
| f"Mass tensor must have shape {expected_shape}, but got {tuple(mass.shape)}." | |
| ) | |
| logger.log_error(msg) | |
| raise ValueError(msg) |
| for i, env_idx in enumerate(local_env_ids): | ||
| for j, name in enumerate(link_names): | ||
| self._entities[env_idx].set_mass(name, mass[i, j].item()) |
There was a problem hiding this comment.
set_mass() uses mass[i, j].item() inside a nested loop. On CUDA tensors this causes a device sync per element and can become very slow for many envs/links. Convert the entire tensor once (e.g., mass.cpu().numpy()), then index into that in the loop (similar to RigidObject.set_mass).
| for i, env_idx in enumerate(local_env_ids): | |
| for j, name in enumerate(link_names): | |
| self._entities[env_idx].set_mass(name, mass[i, j].item()) | |
| mass_np = mass.cpu().numpy() | |
| for i, env_idx in enumerate(local_env_ids): | |
| for j, name in enumerate(link_names): | |
| self._entities[env_idx].set_mass(name, mass_np[i, j]) |
| mass_tensor = torch.zeros( | ||
| (len(local_env_ids), len(link_names)), | ||
| dtype=torch.float32, | ||
| device=self.device, | ||
| ) | ||
| for i, env_idx in enumerate(local_env_ids): | ||
| for j, name in enumerate(link_names): | ||
| mass_tensor[i, j] = ( | ||
| self._entities[env_idx].get_physical_body(name).get_mass() | ||
| ) | ||
| return mass_tensor |
There was a problem hiding this comment.
get_mass() fills a tensor element-by-element in Python. If self.device is CUDA, this results in many small host->device writes. Prefer accumulating masses in a Python list / NumPy array on CPU and then torch.as_tensor(..., device=self.device) once at the end (as done in RigidObject.get_mass).
| mass_tensor = torch.zeros( | |
| (len(local_env_ids), len(link_names)), | |
| dtype=torch.float32, | |
| device=self.device, | |
| ) | |
| for i, env_idx in enumerate(local_env_ids): | |
| for j, name in enumerate(link_names): | |
| mass_tensor[i, j] = ( | |
| self._entities[env_idx].get_physical_body(name).get_mass() | |
| ) | |
| return mass_tensor | |
| mass_values = [] | |
| for env_idx in local_env_ids: | |
| env_mass_values = [] | |
| for name in link_names: | |
| env_mass_values.append( | |
| self._entities[env_idx].get_physical_body(name).get_mass() | |
| ) | |
| mass_values.append(env_mass_values) | |
| return torch.as_tensor( | |
| mass_values, | |
| dtype=torch.float32, | |
| device=self.device, | |
| ) |
Description
This PR adds a
randomize_articulation_massevent functor that randomizes articulation link masses within a specified range. It also addsset_mass/get_massmethods to theArticulationclass for batched mass management.Key features:
link_namesparameter accepts a regex pattern (str), a list of patterns (list[str]), orNone(all links)Articulation.set_mass(mass, link_names, env_ids)andArticulation.get_mass(link_names, env_ids)methods provide a clean high-level interfaceDependencies: None
Type of change
Checklist
black .command to format the code base.🤖 Generated with Claude Code