Source code for tbp.monty.frameworks.models.salience.sensor_module

# 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

import numpy as np
import numpy.typing as npt
import quaternion as qt

from tbp.monty.cmp import AttentionRegion, Goal
from tbp.monty.context import RuntimeContext
from tbp.monty.frameworks.models.abstract_monty_classes import (
    SensorModule,
)
from tbp.monty.frameworks.models.motor_system_state import AgentState, SensorState
from tbp.monty.frameworks.models.salience.on_object_observation import (
    on_object_observation,
)
from tbp.monty.frameworks.models.salience.return_inhibitor import ReturnInhibitor
from tbp.monty.frameworks.models.salience.segmentation.strategy import (
    SegmentationStrategy,
)
from tbp.monty.frameworks.models.salience.strategies import (
    SalienceStrategy,
    Uniform,
)
from tbp.monty.frameworks.models.salience.telemetry import (
    NoopSalienceSMTelemetry,
    SalienceSMTelemetry,
)
from tbp.monty.frameworks.sensors import SensorID
from tbp.monty.memento import Memento
from tbp.monty.observations import SensorObservation

__all__ = ["SalienceSM"]


[docs] class SalienceSM(SensorModule): """A wide field-of-view sensor module that proposes locations to attend to. Implements inhibition-of-return, and can be configured with different strategies for ranking target locations. It can also be configured with a segmentation strategy to propose ``AttentionRegion``s. """ _sensor_module_id: str _salience_strategy: SalienceStrategy _return_inhibitor: ReturnInhibitor _snapshot_telemetry: SalienceSMTelemetry _goals: list[Goal] _segmentation_strategy: SegmentationStrategy | None _region: AttentionRegion
[docs] def __init__( self, sensor_module_id: str, salience_strategy: SalienceStrategy | None = None, return_inhibitor: ReturnInhibitor | None = None, snapshot_telemetry: SalienceSMTelemetry | None = None, segmentation_strategy: SegmentationStrategy | None = None, ) -> None: self._sensor_module_id = sensor_module_id self._salience_strategy = ( Uniform() if salience_strategy is None else salience_strategy ) self._return_inhibitor = ( ReturnInhibitor() if return_inhibitor is None else return_inhibitor ) self._snapshot_telemetry = ( NoopSalienceSMTelemetry() if snapshot_telemetry is None else snapshot_telemetry ) self._goals = [] self._segmentation_strategy = segmentation_strategy self._region = AttentionRegion.empty()
@property def sensor_module_id(self) -> str: return self._sensor_module_id
[docs] def state_dict(self) -> Memento: return self._snapshot_telemetry.state_dict()
[docs] def update_state(self, agent: AgentState) -> None: """Update information about the sensor's location and rotation.""" sensor = agent.sensors[SensorID(self.sensor_module_id)] self.state = SensorState( position=agent.position + qt.rotate_vectors(agent.rotation, sensor.position), rotation=agent.rotation * sensor.rotation, )
[docs] def step( self, ctx: RuntimeContext, observation: SensorObservation, motor_only_step: bool = False, ) -> None: """Generate goal for the current step. If `motor_only_step` is True, this method will return without using the salience strategy, stepping the return inhibitor, or modifying `self._goals` in any way. Args: ctx: The runtime context. observation: Sensor observation. motor_only_step: Whether the current step is a motor-only step. """ if motor_only_step: return salience_map = self._salience_strategy( ctx=ctx, rgba=observation["rgba"], depth=observation["depth"] ) on_object = on_object_observation(observation, salience_map) ior_weights = self._return_inhibitor( on_object.center_location, on_object.locations ) salience = self._weight_salience(ctx, on_object.salience, ior_weights) self._goals = [ Goal( location=on_object.locations[i], morphological_features=None, non_morphological_features=None, confidence=salience[i], # SalienceSM goals are intended for the motor system pass_message=False, sender_id=self._sensor_module_id, sender_type="SM", process_features_in_lm=False, goal_tolerances=None, ) for i in range(len(on_object.locations)) ] self._region = self._segment_region( ctx=ctx, rgb=observation["rgba"][:, :, :3], on_object_map=on_object.on_object_map, location_map=on_object.location_map, ) self._snapshot_telemetry.raw_observation( observation, self.state.rotation, self.state.position ) self._snapshot_telemetry.salience_map(salience_map) self._snapshot_telemetry.goals(self._goals)
def _segment_region( self, ctx: RuntimeContext, rgb: npt.NDArray[np.uint8], on_object_map: npt.NDArray[np.bool_], location_map: npt.NDArray[np.float64], ) -> AttentionRegion: """Segment the surface under fixation into a region proposal. The region is the set of on-object locations inside the segmented surface, where each location is associated with an attention weight. Args: ctx: The runtime context. rgb: The RGB image from the sensor. on_object_map: The on-object view of the observation as a boolean mask. location_map: The corresponding 3D locations for each pixel in the observation. Returns: The attention region. The region is empty if there is no segmentation strategy. """ if self._segmentation_strategy is None: return AttentionRegion.empty() segmentation_map = self._segmentation_strategy(ctx=ctx, rgb=rgb) region_locations_on_object = self.region_locations_on_object( segmentation_map, on_object_map, location_map ) region = AttentionRegion.uniform( region_locations_on_object, AttentionRegion.MAX_WEIGHT ) self._snapshot_telemetry.segmentation_map(segmentation_map) self._snapshot_telemetry.attention_region(region) return region
[docs] @staticmethod def region_locations_on_object( segmentation_map: npt.NDArray[np.uint8], on_object_map: npt.NDArray[np.bool_], location_map: npt.NDArray[np.float64], ) -> npt.NDArray[np.float64]: """Return the 3D locations of the segmented region that are on the object. Args: segmentation_map: The segmentation map of the image. on_object_map: The on-object view of the observation as a boolean mask. location_map: The corresponding 3D locations for each pixel in the observation. Returns: The 3D locations of the segmented region that are on the object. """ region_on_object_map = segmentation_map.astype(bool) & on_object_map return location_map[region_on_object_map]
def _weight_salience( self, ctx: RuntimeContext, salience: np.ndarray, ior_weights: np.ndarray, ) -> np.ndarray: weighted_salience = self._decay_salience(salience, ior_weights) weighted_salience = self._randomize_salience(ctx, weighted_salience) return self._normalize_salience(weighted_salience) def _decay_salience( self, salience: np.ndarray, ior_weights: np.ndarray ) -> np.ndarray: decay_factor = 0.75 return salience - decay_factor * ior_weights def _randomize_salience( self, ctx: RuntimeContext, weighted_salience: np.ndarray ) -> np.ndarray: randomness_factor = 0.05 weighted_salience += ctx.rng.normal( loc=0, scale=randomness_factor, size=weighted_salience.shape[0] ) return weighted_salience def _normalize_salience(self, weighted_salience: np.ndarray) -> np.ndarray: if weighted_salience.size == 0: return weighted_salience min_ = weighted_salience.min() max_ = weighted_salience.max() scale = max_ - min_ if np.isclose(scale, 0): return np.clip(weighted_salience, 0, 1) return (weighted_salience - min_) / scale
[docs] def reset(self) -> None: self._goals.clear() self._return_inhibitor.reset() self._snapshot_telemetry.reset()
[docs] def propose_goals(self) -> list[Goal]: return self._goals
[docs] def propose_region(self) -> AttentionRegion: return self._region