Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shift from dict for agent_potrayal to an AgentPortrayalStyle class #2436

Open
quaquel opened this issue Oct 30, 2024 · 1 comment
Open

Shift from dict for agent_potrayal to an AgentPortrayalStyle class #2436

quaquel opened this issue Oct 30, 2024 · 1 comment
Labels
feature Release notes label visualisation

Comments

@quaquel
Copy link
Member

quaquel commented Oct 30, 2024

Currently, agent_portrayal should return a dict. Valid fields are color, marker, size, and zorder. In #2430 and in a comment on #2389, it has been suggested to shift to an AgentPortrayaStyle class instead. The benefit of this is that is much easier to write your own agent_portrayal function. It also ensures that only valid fields are used. It might make it easier to write agent_portrayal functions that are indifferent about the plotting backend (i.e., altair or matplotlib). Last, it becomes trivial for all draw_x functions in mesa.visualization.compoments.matplotlib to be modifies so agent_portrayal becomes an optional keyword argument.

A while back @wang-boyu also made a remark about this in #1441.

@quaquel quaquel added feature Release notes label visualisation labels Oct 30, 2024
@quaquel quaquel changed the title Shift from dict for agent_potrayal to an AgentPortrayaStyle class Shift from dict for agent_potrayal to an AgentPortrayalStyle class Oct 30, 2024
@Sahil-Chhoker
Copy link
Contributor

@quaquel, I personally believe that using a dedicated class for agent portrayal is a great idea. I've implemented a simple version based on my vision.

@dataclass
class AgentPortrayalStyle:

    color: str | tuple = "tab:blue"
    marker: str = "o"
    size: int = 50
    zorder: int = 1
    valid_marker: list ["o", "s", "^", ...]

    def __post_init__(self):
        """Validate the attributes after initialization."""
        if self.color is not None:
            # Validate color
            try:
                mcolors.to_rgb(self.color)
            except ValueError:
                raise ValueError(f"Invalid color specification: {self.color}")

        if self.marker is not None:
            # Validate Markers
            if self.marker not in VALID_MARKERS:
                raise ValueError(f"Invalid marker '{self.marker}'.)
        
        if self.size is not None and not isinstance(self.size, Number):
            raise ValueError(f"Size must be a number, got {type(self.size)}")
        
        if self.zorder is not None and not isinstance(self.zorder, int):
            raise ValueError(f"Zorder must be an integer, got {type(self.zorder)}")

    def to_dict(self) -> dict:
        """Convert the style to a dictionary."""
        return {k: v for k, v in self.__dict__.items() if v is not None}

Example usage can be something like:

def portrayal(agent):
    if agent is None:
        return None
    
    style = AgentPortrayalStyle(
        color="tab:red" if agent.energy > 50 else "tab:blue",
        size=agent.size * 10,
        marker="^" if agent.is_predator else "o",
        zorder=2 if agent.is_predator else 1
    )
    return style.to_dict() # convert to dict

This approach can also remain consistent with the current API. I would love your feedback on this, as well as any suggestions for additional features or improvements that could be implemented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Release notes label visualisation
Projects
None yet
Development

No branches or pull requests

2 participants