# Copyright 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, Collection, Protocol, Sequence, runtime_checkable
import numpy as np
import quaternion as qt
from pydantic import BeforeValidator, ConfigDict, validate_call
from typing_extensions import Annotated
from tbp.monty.cmp import AttentionRegion, Goal, Message
from tbp.monty.context import RuntimeContext
from tbp.monty.frameworks.models.motor_system_state import AgentState, SensorState
from tbp.monty.frameworks.sensors import SensorID
from tbp.monty.memento import Memento, Snapshotable
from tbp.monty.observations import SensorObservation
[docs]@dataclass
class Payload:
observation: SensorObservation
percept: Message | None
goals: list[Goal]
region: AttentionRegion | None
telemetry: dict[str, Any]
[docs]@dataclass
class TransformContext:
rng: np.random.RandomState
agent_state: AgentState
sensor_state: SensorState
motor_only_step: bool = False
suppress_runtime_errors: bool = False
[docs]@runtime_checkable
class RuntimeSensorModule(Protocol):
"""Monty runtime interface to a Sensor Module."""
[docs] def update_state(self, agent: AgentState) -> None:
"""Update the proprioceptive state for this Sensor Module.
Args:
agent: The proprioceptive state of this sensor module's Agent.
"""
...
[docs] def step(
self,
ctx: RuntimeContext,
observation: SensorObservation,
motor_only_step: bool = False,
) -> Message | None:
"""Execute a time-step for the Sensor Module.
Args:
ctx: The runtime context.
observation: Sensor observation.
motor_only_step: Whether the current step is a motor-only step.
Returns:
An optional percept with features and morphological features.
"""
...
[docs] def propose_goals(self) -> Collection[Goal]:
"""Return the goals proposed by this Sensor Module.
Returns:
A collection of proposed Goals.
"""
...
[docs] def propose_region(self) -> AttentionRegion:
"""Return the region proposed by this Sensor Module.
Returns:
An AttentionRegion.
"""
...
[docs]class SensorModule(RuntimeSensorModule, Snapshotable):
"""A sensor module implemented as a pipeline of configurable transforms.
This sensor module does not implement a specific observation processing itself.
Instead, it is a generic mechanism for processing a ``SensorObservation`` through
a configurable sequence of transforms to produce sensor module outputs.
"""
_agent_state: AgentState
_goals: list[Goal]
_region: AttentionRegion | None
_sensor_id: SensorID
_sensor_module_id: str
_sensor_state: SensorState
_telemetry: dict[str, Any]
_transforms: Sequence[Transform]
[docs] @validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def __init__(
self,
sensor_module_id: str,
sensor_id: SensorID,
transforms: Annotated[Sequence[Transform], BeforeValidator(tuple)],
) -> None:
"""Initialize the sensor module.
Args:
sensor_module_id: The unique identifier for this sensor module.
sensor_id: The unique identifier for the sensor associated with this module.
transforms: A sequence of transforms to process the ``Payload`` containing
the sensor observation and produce sensor module outputs.
"""
self._goals = []
self._region = None
self._sensor_module_id = sensor_module_id
self._sensor_id = sensor_id
self._telemetry = {}
self._transforms = transforms
@property
def sensor_module_id(self) -> str:
return self._sensor_module_id
[docs] def load_state_dict(self, memento: Memento) -> None:
pass
[docs] def propose_goals(self) -> Collection[Goal]:
return self._goals
[docs] def propose_region(self) -> AttentionRegion:
return self._region if self._region is not None else AttentionRegion.empty()
[docs] def state_dict(self) -> Memento:
return self._telemetry
[docs] def step(
self,
ctx: RuntimeContext,
observation: SensorObservation,
motor_only_step: bool = False,
) -> Message | None:
transform_ctx = TransformContext(
rng=ctx.rng,
agent_state=self._agent_state,
sensor_state=self._sensor_state,
motor_only_step=motor_only_step,
suppress_runtime_errors=ctx.suppress_runtime_errors,
)
payload = Payload(
observation=observation,
percept=None,
goals=[],
region=None,
telemetry=self._telemetry,
)
for transform in self._transforms:
payload = transform(transform_ctx, payload)
self._goals = payload.goals
self._region = payload.region
self._telemetry = payload.telemetry
return payload.percept
[docs] def update_state(self, agent: AgentState) -> None:
self._agent_state = agent
sensor = agent.sensors[self._sensor_id]
self._sensor_state = SensorState(
position=agent.position
+ qt.rotate_vectors(agent.rotation, sensor.position),
rotation=agent.rotation * sensor.rotation,
)