Add new actions

This example shows how to add new actions outside of the source code.

Agent actions are implemented with “control” functors. These functors are subclasses of the habitat_sim.agent.SceneNodeControl class and have a __call__ method with the following signature:

def __call__(self,
        scene_node: habitat_sim.SceneNode,
        actuation_spec: habitat_sim.ActuationSpec):
    pass

The scene_node is what the control function manipulates (or controls) and the actuation_spec contains any parameters needed by that control function. See src_python/habitat_sim/agent/controls/default_controls.py for more example controls.

Controls are registered using habitat_sim.registry.register_move_fn(). This function takes the functor to register and, optionally, the name to register it with, and whether or not the control effects the body or just the sensors. If no name is given, the functor is registered with its own name, converted to snake case.

This function can also be used as a decorator — the following will register the control functor MyNewControl with the name my_new_control:

import habitat_sim

@habitat_sim.registry.register_move_fn(body_action=True)
class MyNewControl(habitat_sim.SceneNodeControl):
    pass

We also define two types of actions, body actions and non body actions. Whether a control is a body or non body action is determined by whether it was registered with body_action=True or body_action=False. Body actions move the body of the agent (thereby also moving the sensors) while non-body actions move just the sensors.

The example is runnable via

$ python examples/tutorials/new_actions.py
import attr
import magnum as mn
import numpy as np
import quaternion  # noqa: F401

import habitat_sim
from habitat_sim.utils.common import quat_from_angle_axis, quat_rotate_vector

try:
    import pprint

    _pprint = pprint.pprint
except ImportError:
    _pprint = print


# This is wrapped in a such that it can be added to a unit test
def main():
    # We will define an action that moves the agent and turns it by some amount

    # First, define a class to keep the parameters of the control
    # @attr.s is just syntactic sugar for creating these data-classes
    @attr.s(auto_attribs=True, slots=True)
    class MoveAndSpinSpec:
        forward_amount: float
        spin_amount: float

    print(MoveAndSpinSpec(1.0, 45.0))

    # Register the control functor
    # This action will be an action that effects the body, so body_action=True
    @habitat_sim.registry.register_move_fn(body_action=True)
    class MoveForwardAndSpin(habitat_sim.SceneNodeControl):
        def __call__(
            self, scene_node: habitat_sim.SceneNode, actuation_spec: MoveAndSpinSpec
        ):
            forward_ax = (
                np.array(scene_node.absolute_transformation().rotation_scaling())
                @ habitat_sim.geo.FRONT
            )
            scene_node.translate_local(forward_ax * actuation_spec.forward_amount)

            # Rotate about the +y (up) axis
            rotation_ax = habitat_sim.geo.UP
            scene_node.rotate_local(mn.Deg(actuation_spec.spin_amount), rotation_ax)
            # Calling normalize is needed after rotating to deal with machine precision errors
            scene_node.rotation = scene_node.rotation.normalized()

    # We can also register the function with a custom name
    habitat_sim.registry.register_move_fn(
        MoveForwardAndSpin, name="my_custom_name", body_action=True
    )

    # We can also re-register this function such that it effects just the sensors
    habitat_sim.registry.register_move_fn(
        MoveForwardAndSpin, name="move_forward_and_spin_sensors", body_action=False
    )

    # Now we need to add this action to the agent's action space in the configuration!
    agent_config = habitat_sim.AgentConfiguration()
    _pprint(agent_config.action_space)

    # We can add the control function in a bunch of ways
    # Note that the name of the action does not need to match the name the control function
    # was registered under.

    # The habitat_sim.ActionSpec defines an action.  The first argument is the registered name
    # of the control spec, the second is the parameter spec
    agent_config.action_space["fwd_and_spin"] = habitat_sim.ActionSpec(
        "move_forward_and_spin", MoveAndSpinSpec(1.0, 45.0)
    )

    # Add the sensor version
    agent_config.action_space["fwd_and_spin_sensors"] = habitat_sim.ActionSpec(
        "move_forward_and_spin_sensors", MoveAndSpinSpec(1.0, 45.0)
    )

    # Add the same control with different parameters
    agent_config.action_space["fwd_and_spin_double"] = habitat_sim.ActionSpec(
        "move_forward_and_spin", MoveAndSpinSpec(2.0, 90.0)
    )

    # Use the custom name with an integer name for the action
    agent_config.action_space[100] = habitat_sim.ActionSpec(
        "my_custom_name", MoveAndSpinSpec(0.1, 1.0)
    )

    _pprint(agent_config.action_space)

    # Spin up the simulator
    backend_cfg = habitat_sim.SimulatorConfiguration()
    backend_cfg.scene_id = "data/scene_datasets/habitat-test-scenes/van-gogh-room.glb"
    sim = habitat_sim.Simulator(habitat_sim.Configuration(backend_cfg, [agent_config]))
    print(sim.get_agent(0).state)

    # Take the new actions!
    sim.step("fwd_and_spin")
    print(sim.get_agent(0).state)

    # Take the new actions!
    sim.step("fwd_and_spin_sensors")
    print(sim.get_agent(0).state)

    sim.step("fwd_and_spin_double")
    print(sim.get_agent(0).state)

    sim.step(100)
    print(sim.get_agent(0).state)

    sim.close()

    # Let's define a strafe action!
    @attr.s(auto_attribs=True, slots=True)
    class StrafeActuationSpec:
        forward_amount: float
        # Classic strafing is to move perpendicular (90 deg) to the forward direction
        strafe_angle: float = 90.0

    def _strafe_impl(
        scene_node: habitat_sim.SceneNode, forward_amount: float, strafe_angle: float
    ):
        forward_ax = (
            np.array(scene_node.absolute_transformation().rotation_scaling())
            @ habitat_sim.geo.FRONT
        )
        rotation = quat_from_angle_axis(np.deg2rad(strafe_angle), habitat_sim.geo.UP)
        move_ax = quat_rotate_vector(rotation, forward_ax)

        scene_node.translate_local(move_ax * forward_amount)

    @habitat_sim.registry.register_move_fn(body_action=True)
    class StrafeLeft(habitat_sim.SceneNodeControl):
        def __call__(
            self, scene_node: habitat_sim.SceneNode, actuation_spec: StrafeActuationSpec
        ):
            _strafe_impl(
                scene_node, actuation_spec.forward_amount, actuation_spec.strafe_angle
            )

    @habitat_sim.registry.register_move_fn(body_action=True)
    class StrafeRight(habitat_sim.SceneNodeControl):
        def __call__(
            self, scene_node: habitat_sim.SceneNode, actuation_spec: StrafeActuationSpec
        ):
            _strafe_impl(
                scene_node, actuation_spec.forward_amount, -actuation_spec.strafe_angle
            )

    agent_config = habitat_sim.AgentConfiguration()
    agent_config.action_space["strafe_left"] = habitat_sim.ActionSpec(
        "strafe_left", StrafeActuationSpec(0.25)
    )
    agent_config.action_space["strafe_right"] = habitat_sim.ActionSpec(
        "strafe_right", StrafeActuationSpec(0.25)
    )

    sim = habitat_sim.Simulator(habitat_sim.Configuration(backend_cfg, [agent_config]))
    print(sim.get_agent(0).state)

    sim.step("strafe_left")
    print(sim.get_agent(0).state)

    sim.step("strafe_right")
    print(sim.get_agent(0).state)


if __name__ == "__main__":
    main()