Executive Summary

The AgentFarm perception system implements a sophisticated multi-agent observation framework that provides agents with local, agent-centric views of their environment. The system operates through a single, unified perception component that balances memory efficiency, computational performance, and neural network compatibility while enabling scalable multi-agent reinforcement learning simulations.

The perception system operates through a single observation path via the PerceptionComponent, providing consistent multi-channel perception across all agents.


Table of Contents

  1. System Overview
  2. Key Principles
  3. Core Components
  4. System Architecture
  5. Configuration
  6. Performance Characteristics
  7. Integration & Usage
  8. References & Technical Details

System Overview

Mission & Requirements

The perception system provides:

  • Real-time observation generation for hundreds to thousands of agents
  • Multi-channel environmental awareness with temporal persistence
  • Neural network compatibility with existing RL frameworks
  • Scalable memory usage that grows linearly with agent count
  • Efficient spatial queries for proximity-based perception

Core Innovation: Hybrid Architecture

The system combines:

  • Sparse internal storage for memory efficiency
  • Lazy dense construction for computational performance, with optional prebuilding of high-frequency channels (high_frequency_channels) for fast O(1) copies during dense builds
  • Channel-specific optimization strategies (including prebuilt high-frequency channels and vectorized sparse population)
  • Spatial indexing for efficient proximity queries

Key Principles

Local, Agent-Centric Perception

Local Perspective Fields: Each agent receives a small window (configurable) centered on their position, creating subjective, limited experiences rather than global knowledge.

Orientation Alignment: The agent’s facing direction is aligned to “up” in the perception grid based on each agent’s orientation (degrees, 0=north). World layers and entities are rotated accordingly for agent-centric perception. Default orientation=0 preserves legacy behavior.

Limited Field of View: Agents can only “see” within a certain radius, with areas beyond this masked as unknown, simulating partial observability.

Multi-Channel Observations

The system uses 13 default channels (indices 0-12) that can be extended:

Entity Channels (Sparse storage, INSTANT behavior):

  • SELF_HP (Index 0) - Agent’s current health at center position
  • ALLIES_HP (Index 1) - Visible allies’ health at their positions
  • ENEMIES_HP (Index 2) - Visible enemies’ health at their positions

Environmental Channels (Dense storage, INSTANT behavior):

  • RESOURCES (Index 3) - Resource distribution with bilinear interpolation
  • OBSTACLES (Index 4) - Obstacle/passability map
  • TERRAIN_COST (Index 5) - Movement cost terrain
  • VISIBILITY (Index 6) - Field-of-view visibility mask

Temporal Channels (Sparse with decay, DYNAMIC behavior):

  • KNOWN_EMPTY (Index 7) - Previously observed empty cells (decays over time)
  • DAMAGE_HEAT (Index 8) - Recent damage events (decays over time)
  • TRAILS (Index 9) - Agent movement trails (decays over time)
  • ALLY_SIGNAL (Index 10) - Communication signals (decays over time)

Navigation Channels:

  • GOAL (Index 11) - Target waypoint positions (INSTANT behavior)
  • LANDMARKS (Index 12) - Permanent reference points (PERSISTENT behavior)

Spatial Reasoning & Efficiency

Dual Spatial Indexing: The system uses KD-tree, Quadtree, and Spatial Hash indexing strategies for optimal performance across different query patterns:

  • KD-Tree Indexing: scipy.spatial.cKDTree for O(log n) radial and nearest-neighbor queries
  • Quadtree Indexing: Hierarchical partitioning for efficient rectangular range queries and dynamic updates
  • Spatial Hash Grid: Uniform grid buckets for fast neighbor queries and dynamic updates

Smart Index Selection: The system automatically chooses the most efficient indexing strategy based on query type and entity movement patterns.

Key Features:

  • Multi-level optimization: Dirty flag check (O(1)) → Count check (O(1)) → Hash verification (O(n))
  • Named index system: Configurable indices for different entity types (agents, resources, etc.)
  • Automatic filtering: Built-in support for filtering (e.g., only alive agents)
  • Position validation: Relaxed bounds checking with 1% margin for edge cases

Query Types:

  • get_nearby(position, radius, index_names) - Radius-based entity queries across multiple indices
  • get_nearest(position, index_names) - Single nearest entity lookup per index
  • Coordinate transformation from world → local observation space

Channel Behaviors

INSTANT: Completely overwritten each simulation step with fresh data. Used for current state information that doesn’t need to persist between ticks.

DYNAMIC: Persist across ticks and decay over time using configurable gamma factors. Values gradually fade away, simulating the natural decay of transient information.

PERSISTENT: Remain unchanged until explicitly cleared. Used for long-term memory and permanent environment features that should accumulate over time.


Core Components

1. AgentObservation Class

Purpose: Central observation management with hybrid storage strategy

Key Features:

  • Hybrid sparse/dense tensor management
  • Lazy dense construction on-demand
  • Channel-specific storage optimization
  • Temporal decay with automatic cleanup
  • Backward compatibility with existing RL code

Memory Optimization:

  • Sparse storage: ~2-3 KB per agent average
  • Lazy dense: ~6-24 KB built when needed
  • ~60% memory reduction vs pure dense approach

2. Channel System

Purpose: Extensible multi-channel observation framework

Architecture:

  • Dynamic channel registration system
  • Handler-based processing with inheritance
  • Three behavioral types: INSTANT, DYNAMIC, PERSISTENT
  • Type-safe channel indexing and lookup

3. Spatial Index Integration

Purpose: Efficient proximity queries and spatial reasoning with smart change detection

Technical Implementation:

  • KD-tree based spatial indexing using scipy.spatial.cKDTree
  • Smart change detection with three-level optimization strategy:
    • Dirty flag check (O(1)) for no-change scenarios
    • Count-based check (O(1)) for structural changes
    • Hash-based verification (O(n)) for position changes
  • Named index system supporting configurable entity types with custom filters
  • Automatic position validation with relaxed bounds checking
  • Memory-efficient caching with position hash tracking

System Architecture

Single Observation Path

The perception system provides a single, unified path for observation generation:

AgentCore.step() 
  → AgentCore._create_observation()
    → PerceptionComponent.get_observation_tensor()
      → AgentObservation.perceive_world() [with full multi-channel system]
        → DecisionModule.decide_action()

Architecture Components

1. Unified Perception Component

  • All perception logic integrated into PerceptionComponent
  • Direct integration with AgentObservation system
  • Single source of truth for observation generation
  • Consistent error handling and logging

2. Single Observation Path

  • Single observation path through agent’s perception component
  • Consistent multi-channel observation generation
  • Improved maintainability and debugging

3. Enhanced Error Handling

  • Specific error logging instead of silent failures
  • Graceful fallback to simple perception grid
  • Better debugging and monitoring capabilities
  • Robust error recovery

4. Full Multi-Channel Integration

  • Complete AgentObservation system integration
  • All 13+ observation channels available
  • Sparse/dense hybrid storage
  • Bilinear interpolation support
  • Spatial indexing integration

System Benefits

  • Consistency: All agents use identical observation generation
  • Maintainability: Single location for perception logic
  • Performance: Efficient computation without redundancy
  • Debugging: Clear error handling and logging
  • Extensibility: Easy to add new observation channels
  • Reliability: Robust error handling and fallbacks

Configuration

Basic Configuration

from farm.config import SimulationConfig
from farm.core.observations import ObservationConfig

# Create observation configuration
obs_config = ObservationConfig(
    R=6,                          # Observation radius (cells visible in each direction)
    fov_radius=6,                 # Field-of-view radius for visibility mask (default: 6)
    gamma_trail=0.90,             # Decay rate for movement trails (default: 0.90)
    gamma_dmg=0.85,               # Decay rate for damage heat (default: 0.85)
    gamma_sig=0.92,               # Decay rate for ally signals (default: 0.92)
    gamma_known=0.98,             # Decay rate for known empty cells (default: 0.98)
    device="cpu",                 # Device for tensor operations (default: "cpu")
    dtype="float32",              # PyTorch dtype as string (default: "float32")
    initialization="zeros",       # Tensor initialization method (default: "zeros")
    high_frequency_channels=["RESOURCES", "VISIBILITY"]  # Optional optimization
)

# Attach to simulation configuration (sizes live on environment.*)
config = SimulationConfig.from_centralized_config(environment="development")
config.observation = obs_config
config.environment.width = 100
config.environment.height = 100

All 13 default channels are always included. Custom channels can be added through the dynamic channel registry system.

Advanced Configuration

# Advanced ObservationConfig with custom settings
obs_config = ObservationConfig(
    R=8,                          # Larger observation radius
    fov_radius=7,                 # Larger field-of-view
    device="cuda",                # Use GPU if available
    dtype="float16",              # Use half precision for memory efficiency
    initialization="random",      # Random initialization instead of zeros
    random_min=-0.1,             # Random initialization range
    random_max=0.1,
    # Custom gamma factors for different decay rates
    gamma_trail=0.95,            # Slower decay for movement trails
    gamma_dmg=0.90,              # Moderate decay for damage heat
    gamma_sig=0.85,              # Faster decay for communication signals
    gamma_known=0.98             # Very slow decay for known empty areas
)

# Alternative: Create with defaults and modify
obs_config = ObservationConfig()  # Use all defaults
obs_config.R = 10                # Larger observation radius
obs_config.device = "cuda"       # Switch to GPU
obs_config.gamma_trail = 0.95    # Custom trail decay rate

YAML Configuration

# config/default.yaml - Observation settings
observation:
  R: 6                           # Observation radius
  fov_radius: 6                  # Field-of-view radius
  gamma_trail: 0.90              # Trail decay rate
  gamma_dmg: 0.85                # Damage heat decay rate
  gamma_sig: 0.92                # Signal decay rate
  gamma_known: 0.98              # Known empty decay rate
  device: "cpu"                  # Device for tensor operations
  dtype: "float32"               # PyTorch dtype
  initialization: "zeros"        # Initialization method
  random_min: 0.0                # Random init minimum
  random_max: 1.0                # Random init maximum

# Load from centralized YAML (see docs/reference/config/)
from farm.config import SimulationConfig

config = SimulationConfig.from_centralized_config(environment="development")

Performance Characteristics

Memory Performance

Component Radius 5 Radius 8 Radius 10
Observation Tensor 6,292 bytes 15,044 bytes 23,508 bytes
Channel Overhead 256 bytes 512 bytes 768 bytes
Spatial Index 2,048 bytes 2,048 bytes 2,048 bytes
Total per Agent 8,636 bytes 17,604 bytes 26,324 bytes

Computational Performance

Operation Dense Complexity Sparse Complexity Notes
Storage O(1) O(1) Dictionary lookup
Retrieval O(1) O(1) Cached construction
Decay O(grid_size) O(active_elements) Sparse much faster
NN Processing O(grid_size) O(grid_size) Same dense tensor
Dense Build (hybrid) Prebuilt channels copied O(S²), other sparse channels vectorized

Scalability Analysis

Agent Count Dense Memory Sparse Memory Savings
100 agents 864 KB 1.7 MB 864 KB (50%)
1,000 agents 8.6 MB 17.6 MB 9 MB (51%)
10,000 agents 86.4 MB 176 MB 89.6 MB (51%)

Benchmarking and Metrics (Comprehensive Analysis)

We conducted comprehensive benchmarks across varying agent counts, observation radii, storage modes, and interpolation methods to measure perception throughput, memory efficiency, and computational performance.

Test Configurations

Scale Testing:

  • Agent counts: 100, 1,000, 10,000
  • Observation radii: 5, 8, 10
  • Storage modes: hybrid (sparse + lazy dense), dense
  • Interpolation: bilinear, nearest-neighbor
  • Steps per run: 5-20
  • Device: CPU (GPU support available)

Performance Results

Memory Efficiency:

  • Dense bytes per agent (R=5): 6,292 bytes
  • Dense bytes per agent (R=8): 15,028 bytes
  • Dense bytes per agent (R=10): 23,492 bytes
  • Sparse bytes per agent: ~15% of dense (943-3,524 bytes)
  • Memory reduction: 85.0% across all configurations

Throughput Analysis:

  • 100 agents, R=5: 0.03-0.06ms step time
  • 1,000 agents, R=5: 0.31-0.39ms step time
  • 1,000 agents, R=8: 0.36-0.62ms step time
  • Scaling: Linear with agent count, quadratic with radius

Interpolation Performance:

  • Nearest-neighbor: Consistently 2x faster than bilinear
  • Bilinear: Higher computational cost but preserves continuous positions
  • GFLOPS range: 0.008-0.221 (scales with radius and agent count)

Storage Mode Comparison:

  • Hybrid vs Dense: Minimal performance difference
  • Cache behavior: Hybrid provides 85% memory reduction with negligible overhead
  • Rebuild cost: Dense reconstruction adds <1ms per agent per step

Memory Estimation Formulas

Dense Memory:

dense_bytes_per_agent = channels × (2R+1)² × sizeof(dtype)

Sparse Memory:

sparse_bytes_per_agent ≈ num_active_points × (sizeof(value) + sizeof(y) + sizeof(x))

Memory Reduction:

reduction_percent = (1 - sparse_bytes / dense_bytes) × 100

Computational Analysis

GFLOPS Estimation:

gflops = (total_cells × operations_per_cell × steps) / 1e9 / total_time

Per-Agent Update Time:

update_time_ms = (total_time / (steps × agents)) × 1000

Cache Hit Rate:

hit_rate = hits / (hits + misses)

Key Performance Insights

  1. Memory Efficiency: Consistent 85% reduction with sparse storage across all scales
  2. Interpolation Choice: Nearest-neighbor provides 2x speedup for applications that can tolerate discrete positions
  3. Storage Mode: Hybrid storage provides memory benefits with minimal computational overhead
  4. Scaling: System scales linearly with agent count, making it suitable for large-scale simulations
  5. Radius Impact: Memory and computation scale quadratically with observation radius

Hardware Comparisons

CPU Performance (Intel/AMD):

  • 100 agents: <1ms per step
  • 1,000 agents: <1ms per step
  • 10,000 agents: <10ms per step

GPU Acceleration (RTX 3090):

  • Set ObservationConfig.device="cuda" for GPU acceleration
  • Expected 5-10x speedup for large agent counts
  • Memory bandwidth becomes limiting factor for very large simulations

Baseline Comparisons

vs Pure Dense Approaches:

  • 85% memory reduction with hybrid storage
  • Minimal computational overhead
  • Better cache locality for sparse environments

vs Unity ML-Agents:

  • Comparable throughput for similar agent counts
  • Superior memory efficiency for sparse observations
  • More flexible channel system

Integration & Usage

Basic Usage

from farm.config import SimulationConfig
from farm.core.environment import Environment
from farm.core.observations import ObservationConfig

# Create observation configuration
obs_config = ObservationConfig(
    R=6,                    # Observation radius
    fov_radius=6,          # Field of view radius (default: 6)
    gamma_trail=0.90,      # Trail decay rate
    gamma_dmg=0.85,        # Damage heat decay rate
    gamma_sig=0.92,        # Signal decay rate
    gamma_known=0.98       # Known empty decay rate
)

config = SimulationConfig.from_centralized_config(environment="development")
config.observation = obs_config
config.environment.width = 100
config.environment.height = 100

env = Environment(
    width=config.environment.width,
    height=config.environment.height,
    resource_distribution="uniform",
    config=config,
)

# Environment automatically includes spatial indexing and observation system

Direct AgentObservation Usage

from farm.core.observations import AgentObservation, ObservationConfig

# Create observation configuration
config = ObservationConfig(R=6, fov_radius=5)

# Create agent observation instance
agent_obs = AgentObservation(config)

# Update observation with world state
agent_obs.perceive_world(
    world_layers={
        "RESOURCES": resource_grid,
        "OBSTACLES": obstacle_grid,
        "TERRAIN_COST": terrain_grid
    },
    agent_world_pos=(50, 50),
    self_hp01=0.8,
    allies=[(48, 50, 0.9), (52, 50, 0.7)],
    enemies=[(45, 45, 0.6)],
    goal_world_pos=(60, 60),
    spatial_index=env.spatial_index,
    agent_object=agent
)

# Get observation tensor for neural network input
observation_tensor = agent_obs.tensor()  # Shape: (13, 13, 13)

Channel Registration

from farm.core.channels import ChannelHandler, ChannelBehavior, register_channel

class CustomChannel(ChannelHandler):
    def __init__(self):
        super().__init__("CUSTOM_CHANNEL", ChannelBehavior.DYNAMIC, gamma=0.95)

    def process(self, observation, channel_idx, config, agent_world_pos, **kwargs):
        # Implement your channel processing logic
        pass

# Register the channel
channel_idx = register_channel(CustomChannel())

Spatial Queries

# Get nearby entities using spatial index
nearby_entities = env.spatial_index.get_nearby(
    position=agent.position, 
    radius=5.0, 
    index_names=["agents", "resources"]
)

# Extract specific entity types
nearby_agents = nearby_entities.get("agents", [])
nearby_resources = nearby_entities.get("resources", [])

# Get nearest entities across multiple indices
nearest_entities = env.spatial_index.get_nearest(
    position=agent.position, 
    index_names=["agents", "resources"]
)
nearest_agent = nearest_entities.get("agents")
nearest_resource = nearest_entities.get("resources")

# Process nearby entities (agents are pre-filtered to alive only)
for agent_obj in nearby_agents:
    # Calculate distance for observation processing
    distance = ((agent.position[0] - agent_obj.position[0])**2 + 
               (agent.position[1] - agent_obj.position[1])**2)**0.5
    
    # Update observation channels based on proximity
    if distance <= observation_radius:
        # Process agent for observation update
        pass

# Register custom indices for specialized queries
env.spatial_index.register_index(
    name="enemies",
    data_reference=env.agents,
    position_getter=lambda a: a.position,
    filter_func=lambda a: a.team != agent.team and a.alive
)

# Query custom indices
nearby_enemies = env.spatial_index.get_nearby(
    position=agent.position,
    radius=10.0,
    index_names=["enemies"]
)

References & Technical Details

📖 Detailed Technical Documentation

For in-depth technical information, refer to:

Design & Architecture:

Performance & Optimization:

Integration & Usage:

🔧 Implementation Files

Key implementation modules:

  • farm/core/observations.py - Core observation system
  • farm/core/channels.py - Channel system implementation
  • farm/core/spatial_index.py - Spatial indexing implementation
  • farm/core/environment.py - Environment integration

⚙️ Configuration Examples

See Configuration Guide for:

  • Complete configuration options
  • Performance tuning recommendations
  • Environment-specific settings

📊 Performance Analysis

For detailed performance characteristics:

  • Memory usage patterns
  • Computational bottlenecks
  • Scaling recommendations
  • Optimization strategies

Conclusion

The AgentFarm perception system provides a sophisticated foundation for realistic agent perception in multi-agent simulations. By combining efficient spatial indexing with a flexible channel system and hybrid storage strategies, it enables scalable, memory-efficient observation generation that supports both simple scripted agents and complex reinforcement learning systems.

The system’s design emphasizes local, subjective experiences over global knowledge, creating more realistic and adaptable agent behavior while maintaining the computational efficiency required for large-scale simulations.