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

UI layout #1825

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 104 additions & 156 deletions mesa/experimental/jupyter_viz.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import threading

import matplotlib.pyplot as plt
import networkx as nx
import reacton.ipywidgets as widgets
import solara
from matplotlib.figure import Figure
from matplotlib.ticker import MaxNLocator

import sys
from solara.alias import rv
from .model_control import ModelController
from .user_input import UserInputs
import mesa

# Avoid interactive backend

Check failure on line 12 in mesa/experimental/jupyter_viz.py

View workflow job for this annotation

GitHub Actions / lint-ruff

Ruff (I001)

mesa/experimental/jupyter_viz.py:1:1: I001 Import block is un-sorted or un-formatted
plt.switch_backend("agg")


Expand Down Expand Up @@ -59,107 +59,110 @@
def handle_change_model_params(name: str, value: any):
set_model_parameters({**model_parameters, name: value})

# 3. Set up UI
solara.Markdown(name)
UserInputs(user_params, on_change=handle_change_model_params)
ModelController(model, play_interval, current_step, set_current_step, reset_counter)

with solara.GridFixed(columns=2):
# 4. Space
if space_drawer == "default":
# draw with the default implementation
make_space(model, agent_portrayal)
elif space_drawer:
# if specified, draw agent space with an alternate renderer
space_drawer(model, agent_portrayal)
# otherwise, do nothing (do not draw space)
@solara.component
def ColorCard(title, color, layout_type="Grid"):
with rv.Card(
style_=f"background-color: {color}; width: 100%; height: 100%"
) as main:
rv.CardTitle(children=[title])
if layout_type == "Grid":
if space_drawer == "default":
# draw with the default implementation
make_space(model, agent_portrayal)
elif space_drawer:
# if specified, draw agent space with an alternate renderer
space_drawer(model, agent_portrayal)
elif layout_type == "Measure":
for measure in measures:
if callable(measure):
# Is a custom object
measure(model)

Check warning on line 79 in mesa/experimental/jupyter_viz.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/jupyter_viz.py#L79

Added line #L79 was not covered by tests
else:
make_plot(model, measure)
return main

# 5. Plots
for measure in measures:
if callable(measure):
# Is a custom object
measure(model)
else:
make_plot(model, measure)
# 3. Set up UI

with solara.AppBar():
solara.AppBarTitle(name)

grid_layout_initial = [
{"h": 12, "i": "0", "moved": False, "w": 5, "x": 0, "y": 0},
{"h": 12, "i": "1", "moved": False, "w": 5, "x": 7, "y": 0},
]

colors = "white white".split()

# we need to store the state of the grid_layout ourselves, otherwise it will 'reset'
# each time we change resizable or draggable
grid_layout, set_grid_layout = solara.use_state(grid_layout_initial)

# render layout and plot

# jupyter
def render_in_jupyter():
with solara.Row():
with solara.Card("Controls", margin=1, elevation=2):
UserInputs(user_params, on_change=handle_change_model_params)
ModelController(

Check warning on line 107 in mesa/experimental/jupyter_viz.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/jupyter_viz.py#L106-L107

Added lines #L106 - L107 were not covered by tests
model, play_interval, current_step, set_current_step, reset_counter
)
with solara.Card("Progress", margin=1, elevation=2):
# solara.ProgressLinear(True)
solara.Markdown(md_text=f"####Step - {current_step}")

Check warning on line 112 in mesa/experimental/jupyter_viz.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/jupyter_viz.py#L112

Added line #L112 was not covered by tests

with solara.Row():
# 4. Space
if space_drawer == "default":
# draw with the default implementation
make_space(model, agent_portrayal)

Check warning on line 118 in mesa/experimental/jupyter_viz.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/jupyter_viz.py#L118

Added line #L118 was not covered by tests
elif space_drawer:
# if specified, draw agent space with an alternate renderer
space_drawer(model, agent_portrayal)

Check warning on line 121 in mesa/experimental/jupyter_viz.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/jupyter_viz.py#L121

Added line #L121 was not covered by tests
# otherwise, do nothing (do not draw space)

@solara.component
def ModelController(
model, play_interval, current_step, set_current_step, reset_counter
):
playing = solara.use_reactive(False)
thread = solara.use_reactive(None)
# We track the previous step to detect if user resets the model via
# clicking the reset button or changing the parameters. If previous_step >
# current_step, it means a model reset happens while the simulation is
# still playing.
previous_step = solara.use_reactive(0)

def on_value_play(change):
if previous_step.value > current_step and current_step == 0:
# We add extra checks for current_step == 0, just to be sure.
# We automatically stop the playing if a model is reset.
playing.value = False
elif model.running:
do_step()
else:
playing.value = False

def do_step():
model.step()
previous_step.value = current_step
set_current_step(model.schedule.steps)

def do_play():
model.running = True
while model.running:
do_step()

def threaded_do_play():
if thread is not None and thread.is_alive():
return
thread.value = threading.Thread(target=do_play)
thread.start()

def do_pause():
if (thread is None) or (not thread.is_alive()):
return
model.running = False
thread.join()

def do_reset():
reset_counter.value += 1

with solara.Row():
solara.Button(label="Step", color="primary", on_click=do_step)
# This style is necessary so that the play widget has almost the same
# height as typical Solara buttons.
solara.Style(
"""
.widget-play {
height: 30px;
}
"""
)
widgets.Play(
value=0,
interval=play_interval,
repeat=True,
show_repeat=False,
on_value=on_value_play,
playing=playing.value,
on_playing=playing.set,
# 5. Plots
with solara.GridFixed(columns=len(measures)):
for measure in measures:
if callable(measure):
# Is a custom object
measure(model)

Check warning on line 129 in mesa/experimental/jupyter_viz.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/jupyter_viz.py#L129

Added line #L129 was not covered by tests
else:
make_plot(model, measure)

Check warning on line 131 in mesa/experimental/jupyter_viz.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/jupyter_viz.py#L131

Added line #L131 was not covered by tests

def render_in_browser():
with solara.Sidebar():
with solara.Card("Controls", margin=1, elevation=2):
UserInputs(user_params, on_change=handle_change_model_params)
ModelController(
model, play_interval, current_step, set_current_step, reset_counter
)
with solara.Card("Progress", margin=1, elevation=2):
# solara.ProgressLinear(True)
solara.Markdown(md_text=f"####Step - {current_step}")
resizable = solara.ui_checkbox("Allow resizing", value=True)
draggable = solara.ui_checkbox("Allow dragging", value=True)

layout_types = ["Grid", "Measure"]

items = [
ColorCard(
title=layout_types[i], color=colors[i], layout_type=layout_types[i]
)
for i in range(len(grid_layout))
]
solara.GridDraggable(
items=items,
grid_layout=grid_layout,
resizable=resizable,
draggable=draggable,
on_grid_layout=set_grid_layout,
)
solara.Button(label="Reset", color="primary", on_click=do_reset)
solara.Markdown(md_text=f"**Step:** {current_step}")
# threaded_do_play is not used for now because it
# doesn't work in Google colab. We use
# ipywidgets.Play until it is fixed. The threading
# version is definite a much better implementation,
# if it works.
# solara.Button(label="▶", color="primary", on_click=viz.threaded_do_play)
# solara.Button(label="⏸︎", color="primary", on_click=viz.do_pause)
# solara.Button(label="Reset", color="primary", on_click=do_reset)

if "ipykernel" in sys.argv[0]:
render_in_jupyter()

Check warning on line 163 in mesa/experimental/jupyter_viz.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/jupyter_viz.py#L163

Added line #L163 was not covered by tests
else:
render_in_browser()


def split_model_params(model_params):
Expand All @@ -180,61 +183,6 @@
return True


@solara.component
def UserInputs(user_params, on_change=None):
"""Initialize user inputs for configurable model parameters.
Currently supports :class:`solara.SliderInt`, :class:`solara.SliderFloat`,
:class:`solara.Select`, and :class:`solara.Checkbox`.

Props:
user_params: dictionary with options for the input, including label,
min and max values, and other fields specific to the input type.
on_change: function to be called with (name, value) when the value of an input changes.
"""

for name, options in user_params.items():
# label for the input is "label" from options or name
label = options.get("label", name)
input_type = options.get("type")

def change_handler(value, name=name):
on_change(name, value)

if input_type == "SliderInt":
solara.SliderInt(
label,
value=options.get("value"),
on_value=change_handler,
min=options.get("min"),
max=options.get("max"),
step=options.get("step"),
)
elif input_type == "SliderFloat":
solara.SliderFloat(
label,
value=options.get("value"),
on_value=change_handler,
min=options.get("min"),
max=options.get("max"),
step=options.get("step"),
)
elif input_type == "Select":
solara.Select(
label,
value=options.get("value"),
on_value=change_handler,
values=options.get("values"),
)
elif input_type == "Checkbox":
solara.Checkbox(
label=label,
on_value=change_handler,
value=options.get("value"),
)
else:
raise ValueError(f"{input_type} is not a supported input type")


def make_space(model, agent_portrayal):
space_fig = Figure()
space_ax = space_fig.subplots()
Expand Down
98 changes: 98 additions & 0 deletions mesa/experimental/model_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import solara
import threading
import reacton.ipywidgets as widgets


@solara.component

Check failure on line 6 in mesa/experimental/model_control.py

View workflow job for this annotation

GitHub Actions / lint-ruff

Ruff (I001)

mesa/experimental/model_control.py:1:1: I001 Import block is un-sorted or un-formatted
def ModelController(
model, play_interval, current_step, set_current_step, reset_counter
):
playing = solara.use_reactive(False)
thread = solara.use_reactive(None)

Check warning on line 11 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L10-L11

Added lines #L10 - L11 were not covered by tests
# We track the previous step to detect if user resets the model via
# clicking the reset button or changing the parameters. If previous_step >
# current_step, it means a model reset happens while the simulation is
# still playing.
previous_step = solara.use_reactive(0)

Check warning on line 16 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L16

Added line #L16 was not covered by tests

def on_value_play(change):

Check warning on line 18 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L18

Added line #L18 was not covered by tests
if previous_step.value > current_step and current_step == 0:
# We add extra checks for current_step == 0, just to be sure.
# We automatically stop the playing if a model is reset.
playing.value = False

Check warning on line 22 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L22

Added line #L22 was not covered by tests
elif model.running:
do_step()

Check warning on line 24 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L24

Added line #L24 was not covered by tests
else:
playing.value = False

Check warning on line 26 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L26

Added line #L26 was not covered by tests

def do_step():
model.step()
previous_step.value = current_step
set_current_step(model.schedule.steps)

Check warning on line 31 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L28-L31

Added lines #L28 - L31 were not covered by tests

def do_play():
model.running = True

Check warning on line 34 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L33-L34

Added lines #L33 - L34 were not covered by tests
while model.running:
do_step()

Check warning on line 36 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L36

Added line #L36 was not covered by tests

def threaded_do_play():

Check warning on line 38 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L38

Added line #L38 was not covered by tests
if thread is not None and thread.is_alive():
return
thread.value = threading.Thread(target=do_play)
thread.start()

Check warning on line 42 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L40-L42

Added lines #L40 - L42 were not covered by tests

def do_pause():

Check warning on line 44 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L44

Added line #L44 was not covered by tests
if (thread is None) or (not thread.is_alive()):
return
model.running = False
thread.join()

Check warning on line 48 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L46-L48

Added lines #L46 - L48 were not covered by tests

def do_reset():
reset_counter.value += 1

Check warning on line 51 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L50-L51

Added lines #L50 - L51 were not covered by tests

with solara.Column():
with solara.Row(gap="10px", justify="center"):

Check failure on line 54 in mesa/experimental/model_control.py

View workflow job for this annotation

GitHub Actions / lint-ruff

Ruff (SIM117)

mesa/experimental/model_control.py:53:5: SIM117 Use a single `with` statement with multiple contexts instead of nested `with` statements
solara.Button(

Check warning on line 55 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L55

Added line #L55 was not covered by tests
label="Step",
color="primary",
text=True,
outlined=True,
on_click=do_step,
)
# This style is necessary so that the play widget has almost the same
# height as typical Solara buttons.
solara.Button(

Check warning on line 64 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L64

Added line #L64 was not covered by tests
label="Reset",
color="primary",
text=True,
outlined=True,
on_click=do_reset,
)

# with solara.Row(gap="10px", justify="center"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another line that needs to be removed.

solara.Style(

Check warning on line 73 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L73

Added line #L73 was not covered by tests
"""
.widget-play {
height: 35px;
}
"""
)
widgets.Play(

Check warning on line 80 in mesa/experimental/model_control.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/model_control.py#L80

Added line #L80 was not covered by tests
value=0,
interval=play_interval,
repeat=True,
show_repeat=False,
on_value=on_value_play,
playing=playing.value,
on_playing=playing.set,
)


# threaded_do_play is not used for now because it
# doesn't work in Google colab. We use
# ipywidgets.Play until it is fixed. The threading
# version is definite a much better implementation,
# if it works.
# solara.Button(label="▶", color="primary", on_click=viz.threaded_do_play)
# solara.Button(label="⏸︎", color="primary", on_click=viz.do_pause)
# solara.Button(label="Reset", color="primary", on_click=do_reset)
Loading
Loading