# Copyright 2025 Thousand Brains Project
# Copyright 2024 Numenta Inc.
#
# Copyright may exist in Contributors' modifications
# and/or contributions to the work.
#
# Use of this source code is governed by the MIT
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
from __future__ import annotations
from habitat_sim import ActuationSpec, Agent
from typing_extensions import Protocol
from tbp.monty.frameworks.actions.actions import (
Action,
LookDown,
LookUp,
MoveForward,
MoveTangentially,
OrientHorizontal,
OrientVertical,
SetAgentPitch,
SetAgentPose,
SetSensorPitch,
SetSensorPose,
SetSensorRotation,
SetYaw,
TurnLeft,
TurnRight,
)
__all__ = [
"HabitatActuator",
"HabitatActuatorRequirements",
]
[docs]class HabitatActuatorRequirements(Protocol):
"""HabitatActuator requires these to be available when mixed in."""
[docs] def get_agent(self, agent_id: str) -> Agent: ...
[docs]class HabitatActuator(HabitatActuatorRequirements):
"""Habitat implementation of an Actuator.
HabitatActuator is responsible for executing actions in the Habitat simulation.
It is a separate class to encapsulate the actuation logic in one place. This
class is expected to be mixed into HabitatSim and expects
HabitatActuatorRequirements to be met.
Note:
Habitat does not expose an API for passing parameters to actions.
So each actuate method works around this limitation by artisanally setting
specific action parameters directly in Habitat sim.
"""
[docs] def action_name(self, action: Action) -> str:
"""Returns Monty's Habitat action naming convention.
The action name is prefixed by the agent ID.
"""
return f"{action.agent_id}.{action.name}"
[docs] def to_habitat(self, action: Action) -> tuple[Agent, ActuationSpec, str]:
"""Transition from the Monty to the Habitat sim domain.
Args:
action: Monty action to execute by the agent specified in the action.
Returns:
The Habitat agent to execute the action, the Habitat action parameters to
set prior to executing the action, and the Habitat action name to execute.
Raises:
InvalidActionName: If the action name is invalid.
NoActionParameters: If the action has no parameters.
"""
agent = self.get_agent(action.agent_id)
action_name = self.action_name(action)
action_space = agent.agent_config.action_space
if action_name not in action_space:
raise InvalidActionName(action_name)
# actuation is Habitat's name for action parameters when action is executed
action_params = action_space[action_name].actuation
if action_params is None:
raise NoActionParameters(action_name)
return agent, action_params, action_name
[docs] def actuate_look_down(self, action: LookDown) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = action.rotation_degrees
action_params.constraint = action.constraint_degrees
agent.act(action_name)
[docs] def actuate_look_up(self, action: LookUp) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = action.rotation_degrees
action_params.constraint = action.constraint_degrees
agent.act(action_name)
[docs] def actuate_move_forward(self, action: MoveForward) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = action.distance
agent.act(action_name)
[docs] def actuate_move_tangentially(self, action: MoveTangentially) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = action.distance
action_params.constraint = action.direction
agent.act(action_name)
[docs] def actuate_orient_horizontal(self, action: OrientHorizontal) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = action.rotation_degrees
action_params.constraint = [action.left_distance, action.forward_distance]
agent.act(action_name)
[docs] def actuate_orient_vertical(self, action: OrientVertical) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = action.rotation_degrees
action_params.constraint = [action.down_distance, action.forward_distance]
agent.act(action_name)
[docs] def actuate_set_agent_pitch(self, action: SetAgentPitch) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = action.pitch_degrees
agent.act(action_name)
[docs] def actuate_set_agent_pose(self, action: SetAgentPose) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = [action.location, action.rotation_quat]
agent.act(action_name)
[docs] def actuate_set_sensor_pitch(self, action: SetSensorPitch) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = action.pitch_degrees
agent.act(action_name)
[docs] def actuate_set_sensor_pose(self, action: SetSensorPose) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = [action.location, action.rotation_quat]
agent.act(action_name)
[docs] def actuate_set_sensor_rotation(self, action: SetSensorRotation) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = [action.rotation_quat]
agent.act(action_name)
[docs] def actuate_set_yaw(self, action: SetYaw) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = action.rotation_degrees
agent.act(action_name)
[docs] def actuate_turn_left(self, action: TurnLeft) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = action.rotation_degrees
agent.act(action_name)
[docs] def actuate_turn_right(self, action: TurnRight) -> None:
agent, action_params, action_name = self.to_habitat(action)
action_params.amount = action.rotation_degrees
agent.act(action_name)
class InvalidActionName(Exception):
"""Raised when an action name is invalid."""
def __init__(self, action_name: str):
super().__init__(f"Invalid action name: {action_name}")
class NoActionParameters(Exception):
"""Raised when an action has no parameters."""
def __init__(self, action_name: str):
super().__init__(f"No action parameters for action: {action_name}")