Source code for genesis_forge.managers.command.velocity_command

from typing import Tuple, TypedDict

import os
import torch
import genesis as gs

from genesis_forge.genesis_env import GenesisEnv
from genesis_forge.utils import entity_lin_vel, transform_by_quat
from genesis_forge.gamepads import Gamepad

from .command_manager import CommandManager, CommandRangeValue


THIS_DIR = os.path.dirname(os.path.abspath(__file__))


class VelocityCommandRange(TypedDict):
    lin_vel_x: CommandRangeValue
    lin_vel_y: CommandRangeValue
    ang_vel_z: CommandRangeValue


class VelocityDebugVisualizerConfig(TypedDict):
    """Defines the configuration for the debug visualizer."""

    envs_idx: list[int]
    """The indices of the environments to visualize. If None, all environments will be visualized."""

    arrow_offset: float
    """The vertical offset of the debug arrows from the top of the robot"""

    arrow_radius: float
    """The radius of the shaft of the debug arrows"""

    arrow_max_length: float
    """The maximum length of the debug arrows"""

    commanded_color: Tuple[float, float, float, float]
    """The color of the commanded velocity arrow"""

    actual_color: Tuple[float, float, float, float]
    """The color of the actual robot velocity arrow"""


DEFAULT_VISUALIZER_CONFIG: VelocityDebugVisualizerConfig = {
    "envs_idx": None,
    "arrow_offset": 0.03,
    "arrow_radius": 0.02,
    "arrow_max_length": 0.15,
    "commanded_color": (0.0, 0.5, 0.0, 1.0),
    "actual_color": (0.0, 0.0, 0.5, 1.0),
}


[docs] class VelocityCommandManager(CommandManager): """ Generates a velocity command from uniform distribution. The command comprises of a linear velocity in x and y direction and an angular velocity around the z-axis. IMPORTANT: The velocity commands are interpreted as robot-relative coordinates: - X-axis: Forward/backward relative to robot's current orientation - Y-axis: Left/right relative to robot's current orientation - Z-axis: Yaw rotation around robot's vertical axis :::{admonition} Debug Visualization If you set `debug_visualizer` to True, arrows will be rendered above your robot showing the commanded velocity vs the actual velocity. Arrow meanings: - GREEN: Commanded velocity (robot-relative, transformed to world coordinates for visualization) When joystick is "forward", this arrow points in the robot's forward direction - BLUE: Actual robot velocity in world coordinates Args: env: The environment to control range: The ranges of linear & angular velocities standing_probability: The probability of all velocities being zero for an environment (0.0 = never, 1.0 = always) resample_time_sec: The time interval between changing the command debug_visualizer: Enable the debug arrow visualization debug_visualizer_cfg: The configuration for the debug visualizer Example:: class MyEnv(GenesisEnv): def config(self): # Create a velocity command manager self.command_manager = VelocityCommandManager( self, visualize=True, range = { "lin_vel_x_range": (-1.0, 1.0), "lin_vel_y_range": (-1.0, 1.0), "ang_vel_z_range": (-0.5, 0.5), } ) RewardManager( self, logging_enabled=True, cfg={ "tracking_lin_vel": { "weight": 1.0, "fn": rewards.command_tracking_lin_vel, "params": { "vel_cmd_manager": self.velocity_command, }, }, "tracking_ang_vel": { "weight": 1.0, "fn": rewards.command_tracking_ang_vel, "params": { "vel_cmd_manager": self.velocity_command, }, }, # ... other rewards ... }, ) # Observations ObservationManager( self, cfg={ "velocity_cmd": {"fn": self.velocity_command.observation}, # ... other observations ... }, ) """ def __init__( self, env: GenesisEnv, range: VelocityCommandRange, resample_time_sec: float = 5.0, standing_probability: float = 0.0, debug_visualizer: bool = False, debug_visualizer_cfg: VelocityDebugVisualizerConfig = DEFAULT_VISUALIZER_CONFIG, ): super().__init__(env, range=range, resample_time_sec=resample_time_sec) self._arrow_nodes: list = [] self.standing_probability = standing_probability self.debug_visualizer = debug_visualizer self.visualizer_cfg = {**DEFAULT_VISUALIZER_CONFIG, **debug_visualizer_cfg} self._is_standing_env = torch.zeros( env.num_envs, dtype=torch.bool, device=gs.device ) """ Properties """ """ Operations """
[docs] def step(self): """Render the command arrows""" if not self.enabled: return super().step() self._render_arrows()
[docs] def use_gamepad( self, gamepad: Gamepad, lin_vel_y_axis: int = 0, lin_vel_x_axis: int = 1, ang_vel_z_axis: int = 2, ): """ Use a connected gamepad to control the command. Args: gamepad: The gamepad to use. lin_vel_x_axis: Map this gamepad axis index to the linear velocity in the x-direction. lin_vel_y_axis: Map this gamepad axis index to the linear velocity in the y-direction. ang_vel_z_axis: Map this gamepad axis index to the angular velocity in the z-direction. """ super().use_gamepad( gamepad, range_axis={ "lin_vel_x": lin_vel_x_axis, "lin_vel_y": lin_vel_y_axis, "ang_vel_z": ang_vel_z_axis, }, )
""" Implementation """ def _resample_command(self, env_ids: list[int]): """ Overwrites commands for environments that should be standing still. """ super().resample_command(env_ids) if not self.enabled: return num = torch.empty(len(env_ids), device=gs.device) self._is_standing_env[env_ids] = ( num.uniform_(0.0, 1.0) <= self.standing_probability ) standing_envs_idx = self._is_standing_env.nonzero(as_tuple=False).flatten() self._command[standing_envs_idx, :] = 0.0 def _render_arrows(self): """ Render the command arrows showing velocity commands and actual robot velocities. The commanded velocity arrow (green) shows the robot-relative velocity command transformed to world coordinates for visualization. The blue arrow is the robot's actual velocity. """ if not self.debug_visualizer: return # Remove existing arrows for arrow in self._arrow_nodes: self.env.scene.clear_debug_object(arrow) self._arrow_nodes = [] # Scale the arrow size based on the maximum target velocity range scale_factor = self.visualizer_cfg["arrow_max_length"] / max( *self._range["lin_vel_x"], *self._range["lin_vel_y"], *self._range["ang_vel_z"], ) # Calculate the center of the robot aabb = self.env.robot.get_AABB() min_aabb = aabb[:, 0] # [min_x, min_y, min_z] max_aabb = aabb[:, 1] # [max_x, max_y, max_z] robot_x = (min_aabb[:, 0] + max_aabb[:, 0]) / 2 robot_y = (min_aabb[:, 1] + max_aabb[:, 1]) / 2 robot_z = max_aabb[:, 2] # Set the arrow position over the center of the robot arrow_pos = torch.zeros(self.env.num_envs, 3, device=gs.device) arrow_pos[:, 0] = robot_x arrow_pos[:, 1] = robot_y arrow_pos[:, 2] = robot_z + self.visualizer_cfg["arrow_offset"] arrow_pos += torch.from_numpy(self.env.scene.envs_offset).to(gs.device) # Get robot orientation (quaternion) for coordinate transformation robot_quat = self.env.robot.get_quat() # Transform robot-relative velocity commands to world coordinates for visualization target_velocity = self._resolve_xy_velocity_to_world_frame( self.command[:, :2], robot_quat, scale_factor ) # Actual robot velocity (already in world coordinates) actual_vec = self.env.robot.get_vel().clone() actual_vec[:, 2] = 0.0 actual_vec[:, :] *= scale_factor debug_envs = ( self.visualizer_cfg["envs_idx"] if self.visualizer_cfg["envs_idx"] is not None else range(self.env.num_envs) ) for i in debug_envs: # Target arrow (robot-relative command transformed to world coordinates for visualization) self._draw_arrow( pos=arrow_pos[i], vec=target_velocity[i], color=self.visualizer_cfg["commanded_color"], ) # Actual arrow self._draw_arrow( pos=arrow_pos[i], vec=actual_vec[i], color=self.visualizer_cfg["actual_color"], ) def _resolve_xy_velocity_to_world_frame( self, xy_velocity: torch.Tensor, robot_quat: torch.Tensor, scale_factor: float ) -> torch.Tensor: """ Converts robot-relative XY velocity commands to world coordinates for visualization. This method follows the IsaacLab pattern: 1. Takes robot-relative velocity commands (base frame) 2. Transforms them to world coordinates using the robot's current orientation 3. Scales them for visualization Args: xy_velocity: Robot-relative velocity commands in base frame, shape (num_envs, 2) robot_quat: Robot's current orientation quaternion, shape (num_envs, 4) scale_factor: Scaling factor for visualization Returns: World-frame velocity vectors scaled for visualization, shape (num_envs, 3) """ # Create 3D velocity tensor with Z component zeroed for 2D visualization vec_3d = torch.zeros(xy_velocity.shape[0], 3, device=xy_velocity.device) vec_3d[:, :2] = xy_velocity # Transform from robot-relative (base) frame to world frame using quaternion # This is the inverse of what robot_lin_vel does vec_world = transform_by_quat(vec_3d, robot_quat) # Scale the transformed world velocity vector vec_world[:, :] *= scale_factor return vec_world def _draw_arrow( self, pos: torch.Tensor, vec: torch.Tensor, color: list[float], ): # If velocity is zero, don't draw the arrow if torch.all(vec == 0.0): return try: node = self.env.scene.draw_debug_arrow( pos=pos.cpu().numpy(), vec=vec.cpu().numpy(), color=color, radius=self.visualizer_cfg["arrow_radius"], ) if node: self._arrow_nodes.append(node) except Exception as e: print(f"Error adding debug visualizing in VelocityCommandManager: {e}")