import matplotlib.pyplot as plt
import numpy as np
import pygame
from swarmsim.Renderers import Renderer
from swarmsim.Environments import Environment
[docs]
class BaseRenderer(Renderer):
"""
A base renderer class responsible for rendering agents in an environment using either
Matplotlib or Pygame.
This class provides a flexible rendering system for visualizing populations within
a simulation environment. It supports both **Matplotlib** (for static plots) and
**Pygame** (for interactive rendering).
Parameters
----------
populations : list
A list of population objects to render.
environment : object, optional
The environment instance in which the populations exist (default is None).
config_path : str, optional
Path to the YAML configuration file containing rendering settings (default is None).
Attributes
----------
populations : list
List of population objects to be rendered.
environment : object
The environment in which agents operate.
config : dict
Dictionary containing rendering configuration parameters.
agent_colors : str or list
Color(s) used to render the agents.
agent_shapes : str or list
Shape(s) used to represent agents (e.g., `"circle"`, `"diamond"`).
agent_sizes : float or list
Size of the agents in the rendering.
background_color : str
Background color of the rendering window.
render_mode : str
Rendering mode, either `"matplotlib"` or `"pygame"`.
render_dt : float
Time delay between frames in seconds.
fig, ax : matplotlib objects
Matplotlib figure and axis (if using Matplotlib mode).
window : pygame.Surface
Pygame window surface (if using Pygame mode).
clock : pygame.time.Clock
Pygame clock for controlling frame rate.
screen_size : tuple
Size of the Pygame window.
arena_size : tuple
Size of the simulation arena in the Pygame window.
Config requirements
-------------------
The YAML configuration file must contain a `renderer` section with the following parameters:
agent_colors : str or list, optional
Default color(s) for the agents (default is `"blue"`).
agent_shapes : str or list, optional
Shape(s) used to render agents (`"circle"` or `"diamond"`, default is `"circle"`).
agent_sizes : float or list, optional
Size of the agents (default is `1`).
background_color : str, optional
Background color of the rendering (default is `"white"`).
render_mode : str, optional
Rendering mode (`"matplotlib"` or `"pygame"`, default is `"matplotlib"`).
render_dt : float, optional
Time delay between frames in seconds (default is `0.05`).
Notes
-----
- The rendering mode must be either `"matplotlib"` (for static plots) or `"pygame"` (for interactive rendering).
- If using Matplotlib, a new figure is created at initialization.
- If using Pygame, a window is created, and agents are rendered as circles or diamonds.
Examples
--------
Example YAML configuration:
.. code-block:: yaml
renderer:
agent_colors: ["red", "green"]
agent_shapes: ["circle", "diamond"]
agent_size: 5
background_color: "black"
render_mode: "pygame"
render_dt: 0.1
This configuration will render agents using **Pygame**, with red and green agents appearing
as circles and diamonds, on a black background.
"""
[docs]
def __init__(self, populations: list, environment: Environment, config_path: str):
"""
Initializes the renderer with the selected visualization mode.
Parameters
----------
populations : list
A list of population objects to render.
environment : object, optional
The environment instance in which the populations exist (default is None).
config_path : str, optional
Path to the YAML configuration file (default is None).
"""
super().__init__(populations, environment, config_path)
# Load rendering settings from the config
self.agent_colors = self.config.get('agent_colors', ['blue'])
self.agent_shapes = self.config.get('agent_shapes', ['circle'])
self.agent_sizes = self.config.get('agent_sizes', [1])
self.background_color = self.config.get('background_color', 'white').lower()
self.render_mode = self.config.get('render_mode', 'matplotlib').lower()
# Pygame setup
self.window = None
self.clock = None
self.screen_size = (600, 600)
self.arena_size = (600, 600) # Smaller arena size
self.scale_factor = None
# Inside __init__ or called from it
if self.render_mode == "matplotlib":
self._setup_matplotlib()
def _setup_matplotlib(self):
"""Initialize Matplotlib rendering."""
arena_width, arena_height = self.environment.dimensions
self.fig, self.ax = plt.subplots(figsize=(8, 8))
self.ax.set_xlim(-arena_width / 2, arena_width / 2)
self.ax.set_ylim(-arena_height / 2, arena_height / 2)
self.ax.set_aspect('equal')
self.ax.set_facecolor(self.background_color)
# Set labels and static elements once
self.ax.set_xlabel('X Position')
self.ax.set_ylabel('Y Position')
self.ax.set_title('Population in the Environment')
self.ax.grid(True)
self.agent_scatters = []
for population, color, shape, size in zip(self.populations,
self.agent_colors,
self.agent_shapes,
self.agent_sizes):
marker = 'o' if shape == 'circle' else 'D'
marker_size = size * 30 # Matplotlib marker size
# Create empty scatter and cache it
scatter = self.ax.scatter([], [], c=color, label=population.id,
marker=marker, s=marker_size)
self.agent_scatters.append(scatter)
self.ax.legend(loc='upper right', framealpha=0.8)
plt.tight_layout()
[docs]
def render(self):
"""
Render the agents and environment using the selected renderer mode.
Raises
------
ValueError
If an unsupported rendering mode is specified.
"""
if self.activate:
if self.render_mode == "matplotlib":
return self.render_matplotlib()
elif self.render_mode == "pygame":
return self.render_pygame()
else:
raise ValueError("Unsupported renderer mode. Use 'matplotlib' or 'pygame'.")
[docs]
def render_matplotlib(self):
"""
Render the simulation using Matplotlib with efficient scatter plot updates.
This method provides high-performance rendering by updating existing scatter plot
data rather than recreating plots at each frame. It supports real-time visualization
of agent positions with customizable colors, shapes, and sizes.
Implementation Details
----------------------
1. **Dynamic Axis Updates**: Adjusts plot limits based on environment dimensions
2. **Pre-render Hook**: Calls custom pre-rendering logic for environment features
3. **Efficient Updates**: Updates scatter plot offsets without recreation
4. **Post-render Hook**: Adds custom visualization elements after agent rendering
5. **Frame Control**: Uses ``plt.pause()`` for animation timing
Performance Notes
-----------------
- Only updates position data, preserving color/shape/size settings
- Suitable for real-time visualization with hundreds of agents
- Memory efficient for long simulation runs
Notes
-----
- Background color and axis limits are updated each frame for dynamic environments
- Legend is created once during initialization to avoid redrawing overhead
- Grid and labels remain static for better visual stability
"""
# Update axis limits only if they change
self.ax.set_xlim(-self.environment.dimensions[0] / 2, self.environment.dimensions[0] / 2)
self.ax.set_ylim(-self.environment.dimensions[1] / 2, self.environment.dimensions[1] / 2)
self.ax.set_facecolor(self.background_color)
# Optional pre-hook
self.pre_render_hook_matplotlib()
# Update scatter plot data
for scatter, population in zip(self.agent_scatters, self.populations):
scatter.set_offsets(population.x)
# Optional post-hook
self.post_render_hook_matplotlib()
# Only this is needed for refresh
plt.pause(self.render_dt)
[docs]
def render_pygame(self):
"""
Renders agents and the environment using Pygame.
This method initializes a Pygame window (if not already created) and
renders the agent populations using circles or diamond shapes.
Notes
-----
- The function first fills the screen with the background color.
- Calls `pre_render_hook_pygame()` before rendering agents.
- Calls `post_render_hook_pygame()` after rendering.
- Uses `pygame.display.flip()` to update the screen.
- Uses `self.clock.tick(1 / self.render_dt)` to control rendering speed.
Returns
-------
np.ndarray or pygame.Surface
- If `render_mode == "rgb_array"`, returns a NumPy array representing the rendered frame.
- Otherwise, returns the Pygame window.
"""
# Initialize Pygame window if it hasn't been created
if self.window is None:
pygame.init()
self.window = pygame.display.set_mode(self.screen_size)
self.clock = pygame.time.Clock()
pygame.display.set_caption("Simulation Render")
# Convert agent colors to Pygame format
self.agent_colors = [
pygame.Color(color) if isinstance(color, str) else pygame.Color(*color)
for color in self.agent_colors
]
# Calculate scale factor for rendering
self.scale_factor = min(self.arena_size[0] / self.environment.dimensions[0],
self.arena_size[1] / self.environment.dimensions[1])
# Fill the screen with the background color
background_color = pygame.Color(self.background_color)
self.window.fill(background_color)
# Call the pre-render hook
self.pre_render_hook_pygame()
# Render agents
for population, color, shape, size in zip(self.populations,
self.agent_colors,
self.agent_shapes,
self.agent_sizes):
for position in population.x:
# Convert simulation coordinates to screen coordinates
x = int((position[0] + self.environment.dimensions[0] / 2) * self.scale_factor)
y = int((self.environment.dimensions[1] / 2 - position[1]) * self.scale_factor)
if shape == 'circle':
agent_radius = int(size / 2 * self.scale_factor)
pygame.draw.circle(self.window, color, (x, y), agent_radius)
elif shape == 'diamond':
agent_side = int(size * np.sqrt(2) / 2 * self.scale_factor)
pygame.draw.polygon(self.window, color, [
(x, y - agent_side), # Top
(x + agent_side, y), # Right
(x, y + agent_side), # Bottom
(x - agent_side, y), # Left
])
# Call the post-render hook
self.post_render_hook_pygame()
# Update the display
pygame.display.flip()
# Control rendering speed
self.clock.tick(1 / self.render_dt)
# Return frame as an array if requested
if self.render_mode != "rgb_array":
frame = pygame.surfarray.array3d(self.window)
frame = np.transpose(frame, (1, 0, 2)) # Convert to (height, width, channels)
return frame
return self.window
[docs]
def pre_render_hook_matplotlib(self):
"""
Customizable pre-rendering hook for Matplotlib visualization extensions.
This method is called before agent rendering, providing a hook for subclasses
to add custom visualization elements such as environment features, force fields,
trajectories, or background images.
Common Use Cases
----------------
- Environment boundary visualization
- Obstacle and barrier rendering
- Force field or potential visualization
- Background image overlay
- Dynamic environment feature updates
Performance Considerations
--------------------------
- Cache expensive computations outside this method when possible
- Use matplotlib's artist system for efficient updates
Notes
-----
- Called after axis setup but before agent scatter plot updates
- Has access to ``self.ax`` matplotlib axes object
- Should not modify agent scatter plots directly
"""
pass
[docs]
def post_render_hook_matplotlib(self):
"""
Customizable post-rendering hook for Matplotlib visualization.
This method is called after agent rendering, providing a hook for subclasses
to add overlay elements such as annotations, measurements, goal regions,
or real-time analysis visualizations.
Common Use Cases
----------------
- Goal region visualization
- Performance metric overlays
- Agent trajectory trails
- Inter-agent connection lines
- Real-time analysis annotations
- Success/failure indicators
- Measurement and scaling references
Notes
-----
- Called after agent positions are updated but before frame display
- Has full access to population states and environment information
- Should preserve existing plot elements unless intentionally modifying them
"""
pass
[docs]
def pre_render_hook_pygame(self):
"""
Customizable pre-rendering hook for Pygame visualization extensions.
This method is called after screen clearing but before agent rendering,
providing a hook for subclasses to add background elements, environment
features, or spatial information visualization.
Common Use Cases
----------------
- Environment boundary visualization
- Obstacle and terrain rendering
- Spatial field visualization (heat maps, gradients)
- Background texture or pattern overlay
- Grid system or coordinate references
- Dynamic environment state visualization
Notes
-----
- Called after ``self.window.fill()`` but before agent rendering
- Has access to ``self.window`` pygame surface
- Coordinate system uses pygame screen coordinates (origin top-left)
- Can access ``self.scale_factor`` for simulation-to-screen conversion
"""
pass
[docs]
def post_render_hook_pygame(self):
"""
Customizable post-rendering hook for Pygame visualization extensions.
This method is called after agent rendering but before display update,
providing a hook for subclasses to add overlay elements, UI components,
or analysis visualizations on top of the agent layer.
Common Use Cases
----------------
- Goal region and target visualization
- Agent connection networks
- Performance metric displays
- UI elements and controls
- Trajectory trails and paths
- Analysis overlay graphics
- Semi-transparent information layers
Notes
-----
- Overlays appear on top of agent visualization
- Can access real-time simulation state for dynamic visualizations
- Should handle coordinate system conversion from simulation to screen space
"""
pass
[docs]
def close(self):
"""
Closes the Pygame window if it is open.
This method ensures that Pygame is properly shut down, freeing
up any allocated resources.
Notes
-----
- If the window is not `None`, the function quits Pygame and resets the window to `None`.
"""
if self.window is not None:
pygame.display.quit()
pygame.quit()
self.window = None