Skip to content

Conversation

@xyao-nv
Copy link
Collaborator

@xyao-nv xyao-nv commented Jan 6, 2026

Summary

Introduce turning knob into atomic task library, together with a stand mixer asset curated from LW open source repo.

Detailed description

  • Stand Mixer object is added into OV server downloaded from LW's simready assets (open source, non commercial use)
  • Introduce Turnable as affordance which has a linear mapping between revolute joint rotation angles and discrete value(e.g. temperature, power, time...)
  • Add turn knob task defined as turning from init level to a target level. Details regarding mapping continuous range to discrete levels can be referred in docstrings.
image
turn_knob_task-2026-01-05_16.26.32.mp4

TODO

  • Add task-specific metrics

@xyao-nv xyao-nv marked this pull request as ready for review January 6, 2026 00:36
Copy link
Collaborator

@alexmillane alexmillane left a comment

Choose a reason for hiding this comment

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

Looks great!

Just a few small comments. The only major thing is the (private) non-commercial asset. I think we should try to retieve that through the LightWheel sdk.

Comment on lines 71 to 72
self.min_level_angle = min_level_angle * math.pi / 180.0
self.max_level_angle = max_level_angle * math.pi / 180.0
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion to suffix with unit: self.min_level_angle_rad

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

"""

def __init__(
self, turnable_joint_name: str, min_level_angle: float, max_level_angle: float, num_levels: int, **kwargs
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggestion to suffix with units: min_level_angle_deg

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment on lines 129 to 131
# Active range: map level [0, num_levels-1] to angle [min_level_angle, max_level_angle]
step_size = (self.max_level_angle - self.min_level_angle) / (self.num_levels - 1)
theta = self.min_level_angle + step_size * target_level
Copy link
Collaborator

Choose a reason for hiding this comment

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

To me it might make more sense to set it in the middle of the level. With this, you could set it to some level, then read it back and get the level below due to a rounding change, or a small motion due to physX.

Perhaps there's a good reason to have it this way?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

great point of handling 0.5 oscillation. So I changed from round to floor in getter, and set it to mid point in setter.

def is_at_level(
self, env: ManagerBasedEnv, asset_cfg: SceneEntityCfg | None = None, target_level: int = -1
) -> torch.Tensor:
"""Check if the object is at the given level (in all the environments)."""
Copy link
Collaborator

Choose a reason for hiding this comment

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

Consider rewriting the docstring slightly. This description makes it sound like you'd get back True if the asset reaches the target level in all envs (simultaneously).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

asset_cfg = SceneEntityCfg(self.name)
asset_cfg = self._add_joint_name_to_scene_entity_cfg(asset_cfg)
current_level = self.get_turning_level(env, asset_cfg)
return torch.abs(current_level - target_level) <= 1e-6
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this be an integer comparison? (And if not, should we make the level and integer? We're rounding it anyway.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

@register_asset
class StandMixer(LibraryObject, Turnable):
"""
Encapsulates the pick-up object config for a pick-and-place environment.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Update the docstring

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment on lines 187 to 196
usd_path = (
"omniverse://isaac-dev.ov.nvidia.com/Isaac/IsaacLab/Arena/assets/object_library/StandMixer013/StandMixer013.usd"
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

If I'm not mistaken, this will fail in CI. Our CI only has access to public assets (which reflects what a public user has access to).

In your description, you say that this asset is from LightWheel. Could we retrieve this asset through the sdk, as we've done for the other LightWheel Assets?

If we plan on hosting ourselves (which I would be in favour of not doing), we need to name the asset lightwheel_stand_mixer (see attribution guide)

Copy link
Collaborator Author

@xyao-nv xyao-nv Jan 6, 2026

Choose a reason for hiding this comment

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

thx for the info! I was wondering why CI cannot fetch it. Lemme see if LW is hosted somehwere. If not, then i need to think of some ways.

Comment on lines +22 to +30
class TurnKnobTask(TaskBase):
def __init__(
self,
turnable_object: Turnable,
target_level: int,
reset_level: int = -1,
episode_length_s: float | None = None,
task_description: str | None = None,
):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Cool!

torch.tensor([joint_index]).to(env.device),
env_ids=env_ids.to(env.device) if env_ids is not None else None,
)
set_unnormalized_joint_position(env, asset_cfg, target_joint_position_unnormlized, env_ids)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for modularizing this file!

@xyao-nv xyao-nv enabled auto-merge (squash) January 8, 2026 05:58
@xyao-nv xyao-nv merged commit 63faf14 into main Jan 8, 2026
5 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

Development

Successfully merging this pull request may close these issues.

3 participants