feat(lerobot): add lerobot eval support - #36
Conversation
|
|
||
|
|
||
| class LeRobotScholaVectorEnv(gym.vector.VectorWrapper): | ||
| """Adapt Schola's native vector environment to LeRobot's rollout contract.""" |
There was a problem hiding this comment.
Docs and typing are mismatched here, docs are more restrictive than typing allows (docs say Schola Env, init says any VectorEnv).
There was a problem hiding this comment.
Updated the documentation. Thanks
| self._validate_render_camera() | ||
|
|
||
| def _validate_render_camera(self) -> None: | ||
| pixels_space = self.single_observation_space.spaces.get("pixels") |
There was a problem hiding this comment.
Is it possible to make this something exposed on the CLI rather than hardcoded?
e.g. --camera-obs-key "pixels", then use that to get the value (or map to pixels if pixels is special in LeRobot)
There was a problem hiding this comment.
Currently, we don't support the rendering modes from gymnasium etc. and instead handle it ourselves by passing NullRHI etc. I would recommend continuing that here as a permanent fix should go in the core Schola modules.
If we absolutely need a render camera to do anything then let's revisit
There was a problem hiding this comment.
pixels is LeRobot's harcoded image-observation key (not Schola's observation key).ObservationAdapter uses observations.cameras to map Schola image keys to LeRobot's pixels/<camera> structure. line 84 for similar example
_validate_render_camera() just verifies that the already-converted LeRobot observation space is what LeRobot expects (pixels/<camera>)
render_camera selects one of the cameras in the observation space. The render method just returns the last frame to assemble MP4 videos (ex. to see how a robot is doing). It doesn't initialize a separate Unreal render.
| passthrough: dict[str, str] = field(default_factory=dict) | ||
| """LeRobot output name to an unchanged Schola observation key.""" | ||
|
|
||
| ignore: list[str] = field(default_factory=list) |
There was a problem hiding this comment.
When would we intentionally ignore observations from Schola? (instead of just omitting them at the Unreal level)
There was a problem hiding this comment.
Removed ignore. I agree that the use case is way too narrow and impractical
|
|
||
| SINGLE_IMAGE_NDIM = 3 | ||
| BATCHED_IMAGE_NDIM = 4 | ||
| CHW_CHANNEL_AXIS = -3 |
There was a problem hiding this comment.
Terminology flips between AXIS and DIM here. Prefer DIM for a single dimension, and DIMS or NDIMS plural when referring to multiple.
There was a problem hiding this comment.
Updated to use NDIMS instead
| "within [0, 1]" | ||
| ) | ||
|
|
||
| # Prefer channel-last when both edge dimensions look like channel counts. |
There was a problem hiding this comment.
Schola is on CHW (there is a bug in the camera that means it could look otherwise but my open PR resolves the issue by standardizing on this)
There was a problem hiding this comment.
updated to use CHW
| BATCH_AXIS = 0 | ||
|
|
||
|
|
||
| def _convert_image_space(space: gym.Space, name: str) -> tuple[Box, str]: |
There was a problem hiding this comment.
I suspect this function and some of the other helpers are reinventing the wheel, please check existing libraries (gymnasium, lerobot for similar pre-existing helpers. I know SB3 has a helper along these lines but don't bring it into this library just for the helper)
There was a problem hiding this comment.
Updated and used some Gym helpers
| "Schola manages vectorization through GymVectorEnv; " | ||
| "LeRobot async environment wrapping is not supported." | ||
| ) | ||
| if self.observations.is_empty(): |
There was a problem hiding this comment.
What specifically do we absolutely need, that can't be inferred, potentially by doing the same trick as rllib where we spin up an environment to get the metadata?
There was a problem hiding this comment.
Unreal Engine might expose joint_positions, joint_velocities, and gripper. If the policy was trained on one combined observation.state vector we wouldn't be able to know how to combine them and in what order.
observations identifies the grouping and order.
Ex: I got policy online and i need to ensure I pass it as joint_positions, joint_velocities, and gripper in that order. However, without observations, we don't know which order to specify how the state must be assembled.
| verbosity=self.verbosity, | ||
| ) | ||
|
|
||
| if schola_env.num_envs != n_envs: |
There was a problem hiding this comment.
Can we just warn and ignore the LeRobot num_envs? If we can't it might be best to implement this as a GymEnvironment and leave the vectorization up to lerobot.
Let me know what options we can't control, and we can discuss
There was a problem hiding this comment.
Added a warning. We can just use the actual env.num_envs exposed by Schola
| raise | ||
|
|
||
| try: | ||
| if bool(self.features) != bool(self.features_map): |
There was a problem hiding this comment.
Why do we need two separate maps to handle this? What is the ground truth for the keys in these maps?
There was a problem hiding this comment.
features is a PolicyFeature that specifies the type (state, visual, action, etc.) and the shape.
features_map renames the feature to the name LeRobot expects.
While we could find a way to combine them with one structure, it seems like the norm is to pass them both as two different maps. Example here.
Summary
Related issues
Type of change
Changes
Add support for LeRobot
Testing
Python (
Resources/python,Test)pip install --group test -e "./Resources/python[all]"python -m pytest Test --import-mode=importlib -n 0Unreal C++ (
Source)Checklist