Source code for modelly.viz.render.renderer

"""Space rendering module for Modelly visualizations.

This module provides functionality to render Modelly model spaces with different
backends, supporting various space types and visualization components.
"""

from __future__ import annotations

from collections.abc import Callable
from typing import TYPE_CHECKING, Literal

if TYPE_CHECKING:
    from modelly.viz.components import PropertyLayerStyle

import modelly
from modelly.viz.render.altair_canvas import redraw_altair_canvas
from modelly.viz.render.drawers.base import Network
from modelly.viz.render.legacy_args import (
    apply_deprecated_agent_args,
    apply_deprecated_layer_portrayal,
    apply_deprecated_render_args,
    apply_deprecated_structure_args,
)
from modelly.viz.render.portrayal import as_property_layer_callable
from modelly.viz.render.space_setup import (
    make_backend,
    make_space_drawer,
    map_coordinates,
)


[docs] class SpaceRenderer: """Renders Modelly spaces using different visualization backends. Supports multiple space types and backends for flexible visualization of agent-based models. """ def __init__( self, model: modelly.Model, backend: Literal["matplotlib", "altair"] | None = "matplotlib", ): """Initialize the space renderer. Args: model (modelly.Model): The Modelly model to render. backend (Literal["matplotlib", "altair"] | None): The visualization backend to use. """ self.space = getattr(model, "grid", getattr(model, "space", None)) self.space_drawer = self._get_space_drawer() self.space_mesh = None self.agent_mesh = None self.property_layer_mesh = None self.draw_agent_kwargs = {} self.draw_space_kwargs = {} self.agent_portrayal = None self.property_layer_portrayal = None self.post_process_func = None # Keep track of whether post-processing has been applied # to avoid multiple applications on the same axis. self._post_process_applied = False self.backend = backend self.backend_renderer = make_backend(backend, self.space_drawer) self.backend_renderer.initialize_canvas() def _get_space_drawer(self): """Get appropriate space drawer based on space type. Returns: Space drawer instance for the model's space type. Raises: ValueError: If the space type is not supported. """ return make_space_drawer(self.space) def _map_coordinates(self, arguments): """Map agent coordinates to appropriate space coordinates. Args: arguments (dict): Dictionary containing agent data with coordinates. Returns: dict: Dictionary with mapped coordinates appropriate for the space type. """ return map_coordinates(self.space, self.space_drawer, arguments)
[docs] def setup_structure(self, **kwargs) -> SpaceRenderer: """Setup the space structure without drawing. Args: **kwargs: Additional keyword arguments for the setup function. For ContinuousSpace, you may pass ``viz_dims=(i, j)`` to select which two dimensions are projected to x/y. Checkout respective `SpaceDrawer` class on details on how to pass ``**kwargs``. Returns: SpaceRenderer: The current instance for method chaining. """ self.draw_space_kwargs = kwargs self.space_mesh = None return self
[docs] def setup_agents(self, agent_portrayal: Callable, **kwargs) -> SpaceRenderer: """Setup agents on the space without drawing. Args: agent_portrayal (Callable): Function that takes an agent and returns AgentPortrayalStyle. **kwargs: Additional keyword arguments for the setup function. Checkout respective `SpaceDrawer` class on details on how to pass ``**kwargs``. Returns: SpaceRenderer: The current instance for method chaining. """ self.agent_portrayal = agent_portrayal self.draw_agent_kwargs = kwargs self.agent_mesh = None return self
[docs] def setup_property_layer( self, property_layer_portrayal: Callable | dict | PropertyLayerStyle ) -> SpaceRenderer: """Setup property layers on the space without drawing. Args: property_layer_portrayal (Callable | dict | PropertyLayerStyle): A PropertyLayerStyle, a function that produces a PropertyLayerStyle instance, or a dictionary specifying portrayal parameters. Returns: SpaceRenderer: The current instance for method chaining. """ self.property_layer_portrayal = property_layer_portrayal self.property_layer_mesh = None return self
[docs] def draw_structure(self, **kwargs): """Draw the space structure. Args: **kwargs: (Deprecated) Additional keyword arguments for drawing. Use setup_structure() instead. Returns: The visual representation of the space structure. """ apply_deprecated_structure_args(self, kwargs) # Network-specific: the space instance is replaced on every model reset. # If the drawer still references the old space its layout positions belong # to the previous (now stale) graph. Rebuild before drawing edges so that # the structure is always consistent with the current space. if ( isinstance(self.space, Network) and self.space_drawer.space is not self.space ): self.space_drawer = self._get_space_drawer() self.backend_renderer.space_drawer = self.space_drawer self.space_mesh = self.backend_renderer.draw_structure(**self.draw_space_kwargs) return self.space_mesh
[docs] def draw_agents(self, agent_portrayal=None, **kwargs): """Draw agents on the space. Args: agent_portrayal: (Deprecated) Function that takes an agent and returns AgentPortrayalStyle. Use setup_agents() instead. **kwargs: (Deprecated) Additional keyword arguments for drawing. Returns: The visual representation of the agents. """ apply_deprecated_agent_args(self, agent_portrayal, kwargs) # Prepare data for agent plotting arguments = self.backend_renderer.collect_agent_data( self.space, self.agent_portrayal, default_size=self.space_drawer.s_default ) arguments = self._map_coordinates(arguments) self.agent_mesh = self.backend_renderer.draw_agents( arguments, **self.draw_agent_kwargs ) return self.agent_mesh
[docs] def draw_property_layer(self, property_layer_portrayal=None): """Draw property layers on the space. Args: property_layer_portrayal: (Deprecated) A PropertyLayerStyle, a function that produces a PropertyLayerStyle instance, or a dictionary specifying portrayal parameters. Use setup_property_layer() instead. Returns: The visual representation of the property layers. Raises: Exception: If no property layers are found on the space. """ apply_deprecated_layer_portrayal(self, property_layer_portrayal) property_layers = self.space.property_layers self.property_layer_portrayal = as_property_layer_callable( self.property_layer_portrayal ) number_of_props = sum(1 for layer in property_layers if layer != "empty") if number_of_props < 1: raise Exception("No property layers were found on the space.") self.property_layer_mesh = self.backend_renderer.draw_property_layer( self.space, property_layers, self.property_layer_portrayal ) return self.property_layer_mesh
[docs] def render(self, agent_portrayal=None, property_layer_portrayal=None, **kwargs): """Render the complete space with structure, agents, and property layers. Args: agent_portrayal: (Deprecated) Function for agent portrayal. Use setup_agents() instead. property_layer_portrayal: (Deprecated) Function for property layer portrayal. Use setup_property_layer() instead. **kwargs: (Deprecated) Additional keyword arguments. """ if ( agent_portrayal is not None or property_layer_portrayal is not None or kwargs ): apply_deprecated_render_args( self, agent_portrayal, property_layer_portrayal, kwargs ) if self.space_mesh is None: self.draw_structure() if self.agent_mesh is None and self.agent_portrayal is not None: self.draw_agents() if ( self.property_layer_mesh is None and self.property_layer_portrayal is not None ): self.draw_property_layer() return self
@property def canvas(self): """Get the current canvas object. Returns: The backend-specific canvas object. """ if self.backend == "matplotlib": ax = self.backend_renderer.ax if ax is None: self.backend_renderer.initialize_canvas() return ax elif self.backend == "altair": return redraw_altair_canvas(self) @property def post_process(self): """Get the current post-processing function. Returns: Callable | None: The post-processing function, or None if not set. """ return self.post_process_func @post_process.setter def post_process(self, func: Callable | None): """Set the post-processing function. Args: func (Callable | None): Function to apply post-processing to the canvas. Should accept the canvas object as its first argument. """ self.post_process_func = func