Source code for tbp.monty.frameworks.models.motor_system
# Copyright 2025-2026 Thousand Brains Project
#
# 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 dataclasses import dataclass
from typing import Any, Literal, Protocol, Sequence
import numpy as np
from tbp.monty.cmp import Goal, Message
from tbp.monty.context import RuntimeContext
from tbp.monty.experiment.motor_system import ExperimentMotorSystem
from tbp.monty.frameworks.actions.actions import Action
from tbp.monty.frameworks.agents import AgentID
from tbp.monty.frameworks.models.abstract_monty_classes import Observations
from tbp.monty.frameworks.models.motor_policy_selectors import MotorPolicySelector
from tbp.monty.frameworks.models.motor_system_state import (
MotorSystemState,
ProprioceptiveState,
)
from tbp.monty.memento import Memento
__all__ = [
"MotorSystem",
"RuntimeMotorSystem",
]
@dataclass
class SurfacePolicyActionDetailsTelemetry:
pc_heading: list[Literal["min", "max", "no", "jump"] | None]
avoidance_heading: list[bool | None]
z_defined_pc: list[tuple[np.ndarray, tuple[np.ndarray, np.ndarray]] | None]
[docs]class RuntimeMotorSystem(Protocol):
"""Monty runtime interface to a Motor System."""
def __call__(
self,
ctx: RuntimeContext,
observations: Observations,
proprioceptive_state: ProprioceptiveState,
percept: Message,
goals: Sequence[Goal],
) -> list[Action]:
"""Defines the structure for __call__.
Delegates to the motor policy.
Args:
ctx: The runtime context.
observations: The observations from the environment.
proprioceptive_state: The proprioceptive state from the environment.
percept: The percept from (currently) the first sensor module.
goals: The goals to consider.
Returns:
The actions to take.
"""
...
[docs]class MotorSystem(RuntimeMotorSystem, ExperimentMotorSystem):
"""The basic motor system implementation."""
_policy_selector: MotorPolicySelector
_action_sequence: list[tuple[list[Action], dict[AgentID, Any] | None]]
[docs] def __init__(self, policy_selector: MotorPolicySelector) -> None:
"""Initialize the motor system with a motor policy.
Args:
policy_selector: The motor policy selector to use.
"""
self._policy_selector = policy_selector
# TODO: When the motor system is encapsulated within Monty, then motor_only_step
# attribute should be moved to Monty itself instead.
self._motor_only_step = False
# TODO: Passing self to policy selector is a hack. What we should be
# doing is using more sophisticated actions for surface agents instead.
# We only do this so that SurfacePolicy and its descendants can set
# motor_only_step to True.
# Undoing this hack should probably happen when motor_only_step is moved
# to Monty itself.
self._policy_selector.fixme_provide_motor_system(self)
# TODO: make this part of `__init__()` after `reset()` is removed.
self._init_MotorSystem()
@property
def motor_only_step(self) -> bool:
"""When `True`, suppress Learning Module processing."""
return self._motor_only_step
@motor_only_step.setter
def motor_only_step(self, value: bool) -> None:
self._motor_only_step = value
@property
def action_sequence(self) -> list[tuple[list[Action], dict[AgentID, Any] | None]]:
return self._action_sequence
def _init_MotorSystem(self) -> None: # noqa: N802
# For each step, we store the actions produced by the policy and the current
# motor system state as a (actions, state) tuple.
self._action_sequence = []
# TODO: Get rid of this once we have another path for telemetry.
self._telemetry_surface_action_details = SurfacePolicyActionDetailsTelemetry(
pc_heading=[],
avoidance_heading=[],
z_defined_pc=[],
)
[docs] def reset(self) -> None:
self._init_MotorSystem()
self._policy_selector.reset()
[docs] def state_dict(self) -> Memento:
return self._policy_selector.state_dict()
def __call__(
self,
ctx: RuntimeContext,
observations: Observations,
proprioceptive_state: ProprioceptiveState,
percept: Message,
goals: Sequence[Goal],
) -> list[Action]:
motor_system_state = MotorSystemState(proprioceptive_state)
policy_result = self._policy_selector(
ctx, observations, motor_system_state, percept, goals
)
self.motor_only_step = policy_result.motor_only_step
self._action_sequence.append((policy_result.actions, motor_system_state))
telemetry = policy_result.telemetry
if telemetry is not None:
self._telemetry_surface_action_details.pc_heading.append(
telemetry.pc_heading
)
self._telemetry_surface_action_details.avoidance_heading.append(
telemetry.avoidance_heading
)
self._telemetry_surface_action_details.z_defined_pc.append(
telemetry.z_defined_pc
)
return policy_result.actions