How to Add Custom Robot#

This tutorial guides you on how to add a custom robot.

To add a custom robot, you need to:

  • Create a config class for robot config, inheriting from the internutopia.core.config.robot.RobotCfg.

  • Create a class for robot, inheriting from the internutopia.core.robot.BaseRobot.

In this tutorial we take Unitree G1 as an example to show how to add a custom robot.

Prepare USD of robot#

We use USD file to represent a robot. If you have the URDF/MJCF file of the robot, you can refer to the links below to convert it to a USD file:

  • Import URDF: https://docs.omniverse.nvidia.com/isaacsim/latest/advanced_tutorials/tutorial_advanced_import_urdf.html

  • Import MJCF: https://docs.omniverse.nvidia.com/isaacsim/latest/advanced_tutorials/tutorial_advanced_import_mjcf.html

Create Config Class#

Here’s an example of a config class for a robot:

from typing import Optional

from internutopia.core.config import RobotCfg

class G1RobotCfg(RobotCfg):
    name: Optional[str] = 'g1'
    type: Optional[str] = 'G1Robot'
    prim_path: Optional[str] = '/g1'
    usd_path: Optional[str] = gm.ASSET_PATH + '/robots/g1/g1_29dof_color.usd'
  • name: name of the robot, must be unique in one episode

  • type: type of the robot, must be unique and same with the type of corresponding robot class

  • prim_path: prim path of the robot, relative to the env root path

  • usd_path: USD file path, absolute path is preferred to run your code from any working directory

More fields can be added if more attributes are configurable.

Generally, when creating a new config class for robot, reasonable default values for required fields should be specified to avoid validating error, and robot specific config fields can be added when necessary.

Create Robot Class#

In the simplest scenario, the following methods are required to be implemented in your robot class:

from internutopia.core.robot.robot import BaseRobot
from internutopia.core.scene.scene import IScene


@BaseRobot.register('G1Robot')  # Register this robot to internutopia
class G1Robot(BaseRobot):
    def __init__(self, config: G1RobotCfg, scene: IScene):
        """Initialize the robot with the given config.

        Args:
            config (G1RobotCfg): config for the robot, should be a instance of corresponding config class.
            scene (IScene): current scene.
        """

    def post_reset(self):
        """Set up things after the env resets."""

    def apply_action(self, action: dict):
        """Apply actions of controllers to robot.

        Args:
            action (dict): action dict.
              key: controller name.
              value: corresponding action array.
        """

    def get_obs(self) -> OrderedDict:
        """Get observation of robot, including controllers, sensors, and world pose. OrderedDict is used to ensure the order of observations.
        """

The apply_action method are used to apply the provided actions, and get_obs to obtain the robot’s current observations in each step.

For complete list of robot methods, please refer to the Robot API documentation.

Please note that the registration of the robot class is done through the @BaseRobot.register decorator, and the registered name should match the value of type field within the corresponding robot config class (here is G1Robot).

IArticulation is an interface class to deal with any articulated object in InternUtopia. Robot is one kind of articulated object, so here we use it to control the robot.

For users who want to get state of a certain rigid link of robot, we provide the self._rigid_body_map containing all rigid links of the robot with prim path as key. All the links within are of IRigidBody type which is an interface class to deal with rigid bodies.

An example of g1 robot class implementation is shown as following:

import numpy as np

from internutopia.core.config.robot import RobotUserConfig as Config
from internutopia.core.robot.articulation import IArticulation
from internutopia.core.robot.robot import BaseRobot
from internutopia.core.scene.scene import IScene
from internutopia.core.util import log

from internutopia.core.robot.robot import BaseRobot
from internutopia.core.scene.scene import IScene


@BaseRobot.register('G1Robot')  # Register this robot to internutopia
class G1Robot(BaseRobot):

        def __init__(self, config: G1RobotCfg, scene: IScene):  # Use the config class for this robot
        super().__init__(config, scene)
        self._start_position = np.array(config.position) if config.position is not None else None
        self._start_orientation = np.array(config.orientation) if config.orientation is not None else None

        log.debug(f'G1 {config.name}: position    : ' + str(self._start_position))
        log.debug(f'G1 {config.name}: orientation : ' + str(self._start_orientation))

        usd_path = config.usd_path

        log.debug(f'G1 {config.name}: usd_path         : ' + str(usd_path))
        log.debug(f'G1 {config.name}: config.prim_path : ' + str(config.prim_path))

        self._robot_scale = np.array([1.0, 1.0, 1.0])
        if config.scale is not None:
            self._robot_scale = np.array(config.scale)
        # Create articulation handler
        self.articulation = IArticulation.create(
            prim_path=config.prim_path,
            name=config.name,
            position=self._start_position,
            orientation=self._start_orientation,
            usd_path=usd_path,
            scale=self._robot_scale,
        )

        # More initialization here...

    def post_reset(self):
        super().post_reset()
        self._robot_base = self._rigid_body_map[self.config.prim_path + '/base']

    def apply_action(self, action: dict):
        """
        Args:
            action (dict): inputs for controllers.
        """
        for controller_name, controller_action in action.items():
            if controller_name not in self.controllers:
                log.warning(f'unknown controller {controller_name} in action')
                continue
            controller = self.controllers[controller_name]
            control = controller.action_to_control(controller_action)
            self.articulation.apply_action(control)

    def get_obs(self) -> OrderedDict:
        position, orientation = self._robot_base.get_pose()

        # custom
        obs = {
            'position': position,
            'orientation': orientation,
            'joint_positions': self.articulation.get_joint_positions(),
            'joint_velocities': self.articulation.get_joint_velocities(),
            'controllers': {},
            'sensors': {},
        }

        # common
        for c_obs_name, controller_obs in self.controllers.items():
            obs[c_obs_name] = controller_obs.get_obs()
        for sensor_name, sensor_obs in self.sensors.items():
            obs[sensor_name] = sensor_obs.get_data()
        return self._make_ordered(obs)

You can check the implementations of our robots under internutopia_extension/robots/g1.py.