How to Add Custom Controller#

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

Note that the controller cannot be operated independently. It must be used with robot to enable robot to act in the environment.

To add a custom controller, you need to:

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

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

In this tutorial we take Differential Drive Controller as an example to show how to add a custom controller.

Create Config Class#

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

from typing import Optional

from internutopia.core.config.robot import ControllerCfg


class DifferentialDriveControllerCfg(ControllerCfg):

    type: Optional[str] = 'DifferentialDriveController'
    wheel_radius: float
    wheel_base: float

For differential drive controller, we specify the type and add new fields wheel_radius and wheel_base to specify the radius and base of the wheels.

Create Controller Class#

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

import numpy as np
from typing import List, Union

from internutopia.core.robot.articulation_action import ArticulationAction
from internutopia.core.robot.controller import BaseController
from internutopia.core.robot.robot import BaseRobot


@BaseController.register('DifferentialDriveController')
class DifferentialDriveController(BaseController):
    def __init__(self, config: DifferentialDriveControllerCfg, robot: BaseRobot, scene: IScene) -> None:
        """Initialize the controller with the given configuration and its owner robot.

        Args:
            config (DifferentialDriveControllerCfg): controller configuration.
            robot (BaseRobot): robot owning the controller.
            scene (IScene): scene interface.
        """

    def action_to_control(self, action: Union[np.ndarray, List]) -> ArticulationAction:
        """Convert input action (in 1d array format) to joint signals to apply.

        Args:
            action (Union[np.ndarray, List]): input control action.

        Returns:
            ArticulationAction: joint signals to apply
        """

The action_to_control method translates the input action into joint signals to apply in each step.

For complete list of controller methods, please refer to the Controller API documentation.

Please note that the registration of the controller class is done through the @BaseController.register decorator, and the registered name MUST match the value of type field within the corresponding controller config class (here is DifferentialDriveController).

Sometimes the calculation logic is defined in a method named forward to show the input parameters the controller accepts (which is common in our implementations), making it more human-readable. In this case, the action_to_control method itself only expands the parameters, and invokes forward method to calculate the joint signals.

An example of controller class implementation is shown as following:

from typing import List

import numpy as np

from internutopia.core.robot.articulation_action import ArticulationAction
from internutopia.core.robot.controller import BaseController
from internutopia.core.robot.robot import BaseRobot
from internutopia.core.scene.scene import IScene
from internutopia_extension.configs.controllers import DifferentialDriveControllerCfg


@BaseController.register('DifferentialDriveController')
class DifferentialDriveController(BaseController):
    def __init__(self, config: DifferentialDriveControllerCfg, robot: BaseRobot, scene: IScene) -> None:
        super().__init__(config=config, robot=robot, scene=scene)
        self._robot_scale = self.robot.get_robot_scale()[0]
        self._wheel_base = config.wheel_base * self._robot_scale
        self._wheel_radius = config.wheel_radius * self._robot_scale

    def forward(
        self,
        forward_speed: float = 0,
        rotation_speed: float = 0,
    ) -> ArticulationAction:
        left_wheel_vel = ((2 * forward_speed) - (rotation_speed * self._wheel_base)) / (2 * self._wheel_radius)
        right_wheel_vel = ((2 * forward_speed) + (rotation_speed * self._wheel_base)) / (2 * self._wheel_radius)
        # A controller has to return an ArticulationAction
        return ArticulationAction(joint_velocities=[left_wheel_vel, right_wheel_vel])

    def action_to_control(self, action: List | np.ndarray) -> ArticulationAction:
        """
        Args:
            action (List | np.ndarray): n-element 1d array containing:
              0. forward_speed (float)
              1. rotation_speed (float)
        """
        assert len(action) == 2, 'action must contain 2 elements'
        return self.forward(
            forward_speed=action[0],
            rotation_speed=action[1],
        )

You can check implementations of our controllers under internutopia_extension/controllers/, within which you can find:

  • Rule-based controllers: such as ik_controller, dd_controller.

  • Learning-based controllers: such as g1_move_by_speed_controller, h1_move_by_speed_controller.

  • Composite controllers: controllers chained with other controller, such as move_to_point_by_speed_controller, rotate_controller.