mesa icon indicating copy to clipboard operation
mesa copied to clipboard

SpaceRenderer question

Open quaquel opened this issue 1 month ago • 7 comments

#2803 introduced the SpaceRenderer class. I am now using this for the first time, and I am a bit confused about how to use it. Following e.g., the Boltzmann example, I should do

def agent_portrayal(agent)
	return AgentPortrayalStyle(color=agent.wealth)

renderer = SpaceRenderer(model)
renderer.draw_agents(agent_portrayal=agent_portrayal, cmap="viridis", vmin=0, vmax=10)

page = SolaraViz(
    model,
    renderer,
    components=[GiniPlot],
    model_params=model_params,
    name="Boltzmann Wealth Model",
)

In draw_agents two attributes are then set for the first time: agent_portrayal and agent_kwargs. This, in my view, is poor design. Methods should update state, not add new state variables. Moreover, if I forget to call draw_agents before instantiating SolaraViz with renderer, I guess no agents will ever be drawn, nor can I pass the various arguments (i.e., agent_portrayal, cmap, vmin, and vmax).

Moreover, because draw_agents in turn calls the backend and if your backend is matplotlib, you are creating a new figure but not showing it (i.e., plt.show()) is not called. If you are using SolaraViz inside a jupyter notebook, this leaves an unplotted figure hanging. This will be rendered wherever you next call plt.show(), even though this might be far away from where you made the solara vizualization.

@Sahil-Chhoker, is my interpretation of how the render works correct and if so, what are your thoughts on moving agent_portrayal and agent_kwargs into the __init__ of the SpaceRenderer. As a minor point, we might want to make agent_kwargs a bit more descriptive (e.g., draw_agent_kwargs).

So to be clear, my envisioned API would become

def agent_portrayal(agent)
	return AgentPortrayalStyle(color=agent.wealth)

renderer = SpaceRenderer(model, agent_portrayal=agent_portrayal,
						 draw_agent_kwargs={"cmap"="viridis", "vmin"=0, "vmax"=10})

page = SolaraViz(
    model,
    renderer,
    components=[GiniPlot],
    model_params=model_params,
    name="Boltzmann Wealth Model",
)

quaquel avatar Nov 03 '25 11:11 quaquel

I would like to work on this , will you plz assign me the issue

dhiraj-143r avatar Nov 03 '25 20:11 dhiraj-143r

Using kwargs as a dict makes things like type-checking, autocomplete and error messages harder, right? Would make creating an object for it make sense?

Also, is it possible to call .draw_agents() multiple times on the same render? Or have different agent_kwargs for different subsets of Agents? Because then it would make sense to keep it seperate?

Excuse my probably dumb questions, I'm not that deep in the viz stack right now. Curious about what @Sahil-Chhoker thinks.


@2102508740-commits if you want to contribute, this is probably not the issue. Say hi in our chat and we will figure something out. Also you probably have chosen the most bot-like user name.

EwoutH avatar Nov 03 '25 20:11 EwoutH

  1. The content of the kwargs will depend on the backend. At present, it's already in a dict anyway, so my proposed solution does not change anything. We could have dataclasses for it, but for matplotlib in particular, what can be included in them is very large. So, I don't think it's worth it at this point.
  2. No, you cannot have different subsets of agents and separately draw them with the SpaceRenderer class. What might be possible to make work, but would require expanding the renderer, is to have multiple renderers (although I quess with their own axes on which they would be plotting.

quaquel avatar Nov 03 '25 21:11 quaquel

In draw_agents two attributes are then set for the first time: agent_portrayal and agent_kwargs. This, in my view, is poor design. Methods should update state, not add new state variables. Moreover, if I forget to call draw_agents before instantiating SolaraViz with renderer, I guess no agents will ever be drawn, nor can I pass the various arguments (i.e., agent_portrayal, cmap, vmin, and vmax).

The idea behind having separate draw functions is to keep the visualization flexible. Each draw method sets up what it needs so users can turn space, layers, and agents on or off whenever they want. This way, each part of the visualization can be controlled independently instead of being forced into one setup step.

Moreover, because draw_agents in turn calls the backend and if your backend is matplotlib, you are creating a new figure but not showing it (i.e., plt.show()) is not called. If you are using SolaraViz inside a jupyter notebook, this leaves an unplotted figure hanging. This will be rendered wherever you next call plt.show(), even though this might be far away from where you made the solara vizualization.

I wasn't aware of this bug, I'll see what I can do.

@Sahil-Chhoker, is my interpretation of how the render works correct and if so, what are your thoughts on moving agent_portrayal and agent_kwargs into the __init__ of the SpaceRenderer. As a minor point, we might want to make agent_kwargs a bit more descriptive (e.g., draw_agent_kwargs).

I do agree with the poor design part, we can move agent_portrayal and propertylayer_portrayal in the SpaceRenderer __init__, but as I explained above I want to keep the separate draw functions, so maybe we can keep the draw_agent_kwargs inside the draw function?

Also I like the idea of having multiple renderers.

Sahil-Chhoker avatar Nov 16 '25 04:11 Sahil-Chhoker

The idea behind having separate draw functions is to keep the visualization flexible. Each draw method sets up what it needs so users can turn space, layers, and agents on or off whenever they want. This way, each part of the visualization can be controlled independently instead of being forced into one setup step.

Ok. I agree with having the separate draw functions for structure, agents, and property layers. Where I struggle is with the desire to avoid a single setup function. On the one hand, I agree that this could make the input very long and a bit difficult to read. On the other hand, the current solution, which involves state updates in the draw functions, is also not ideal in my view.

What about doing something like this:

render = SpaceRenderer(model1, backend="matplotlib")
				.setup_structure(...) # to be filled in with relevant args and kwargs
				.setup_agents(...) # to be filled in with relevant args and kwargs
				.setup_property_layers(...) # to be filled in with relevant args and kwargs

This avoids having a very large init, the names are very descriptive (and of course open to suggestions for further improvements), and it avoids the matplotlib bug.

Also I like the idea of having multiple renderers.

Ok, I'll open up a seperate issue on this to split it of from the more API centered discussion here.

quaquel avatar Nov 16 '25 08:11 quaquel

What about doing something like this:

   render = SpaceRenderer(model1, backend="matplotlib")
	           .setup_structure(...) # to be filled in with relevant args and kwargs
			   .setup_agents(...) # to be filled in with relevant args and kwargs
			   .setup_property_layers(...) # to be filled in with relevant args and kwargs

This avoids having a very large init, the names are very descriptive (and of course open to suggestions for further improvements), and it avoids the matplotlib bug.

I like this, not too different from the current API but clean. I'll open up a PR in the following week. Also how does this avoids the matplotlib bug, I haven't had a chance to look at the bug carefully.

Sahil-Chhoker avatar Nov 16 '25 16:11 Sahil-Chhoker

The rough implementation would be something like

def setup_agents(agent_portrayal, *kwargs)->SpaceRenderer:
    sef.agent_portrayal = agent_portrayal
    self.agent_kwargs = kwargs
    return self

So, you don't call matplotlib at all in the setup functions, and therefore, you should not encounter the bug (to be tested, of course).

quaquel avatar Nov 16 '25 17:11 quaquel