Skip to content

Commit

Permalink
Update tutorial and viz
Browse files Browse the repository at this point in the history
- Update tutorial to be SIR model
- Update Viz to allow for different markers; default agent to circle
-Update tests
  • Loading branch information
tpike3 committed Aug 13, 2024
1 parent 235839c commit f0b0ab2
Show file tree
Hide file tree
Showing 6 changed files with 1,980 additions and 257 deletions.
16 changes: 16 additions & 0 deletions docs/tutorials/data/TorontoNeighbourhoods.geojson

Large diffs are not rendered by default.

1,898 changes: 1,789 additions & 109 deletions docs/tutorials/intro_tutorial.ipynb

Large diffs are not rendered by default.

213 changes: 119 additions & 94 deletions mesa_geo/visualization/geojupyter_viz.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import sys

import matplotlib.pyplot as plt
import mesa.experimental.components.matplotlib as components_matplotlib
import solara
Expand Down Expand Up @@ -30,16 +28,54 @@ def Card(
map_drawer,
center_default,
zoom,
scroll_wheel_zoom,
current_step,
color,
layout_type,
):
"""
Parameters
----------
model : Mesa Model Object
A pointer to the Mesa Model object this allows the visual to get get
model information, such as scheduler and space.
measures : List
Plots associated with model typically from datacollector that represent
critical information collected from the model.
agent_portrayal : Dictionary
Contains details of how visualization should plot key elements of the
such as agent color etc
map_drawer : Method
Function that generates map from GIS data of model
center_default : List
Latitude and Longitude of where center of map should be located
zoom : Int
Zoom level at which to intialize the map
scroll_wheel_zoom: Boolean
True of False on whether user can zoom on map with mouse scroll wheel
default is True
current_step : Int
Number on which step is the model
color : String
Background color for visual
layout_type : String
Type of layout Map or Measure
Returns
-------
main : Solara object
Visualization of model
"""

with rv.Card(
style_=f"background-color: {color}; width: 100%; height: 100%"
) as main:
if "Map" in layout_type:
rv.CardTitle(children=["Map"])
leaflet_viz.map(model, map_drawer, zoom, center_default)
leaflet_viz.map(model, map_drawer, zoom, center_default, scroll_wheel_zoom)

if "Measure" in layout_type:
rv.CardTitle(children=["Measure"])
Expand All @@ -65,23 +101,50 @@ def GeoJupyterViz(
# parameters for leaflet_viz
view=None,
zoom=None,
scroll_wheel_zoom=True,
tiles=xyz.OpenStreetMap.Mapnik,
center_point=None, # Due to projection challenges in calculation allow user to specify center point
):
"""Initialize a component to visualize a model.
Args:
model_class: class of the model to instantiate
model_params: parameters for initializing the model
measures: list of callables or data attributes to plot
name: name for display
agent_portrayal: options for rendering agents (dictionary)
space_drawer: method to render the agent space for
the model; default implementation is the `SpaceMatplotlib` component;
simulations with no space to visualize should
specify `space_drawer=False`
play_interval: play interval (default: 150)
center_point: list of center coords
"""
Parameters
----------
model_class : Mesa Model Object
A pointer to the Mesa Model object this allows the visual to get get
model information, such as scheduler and space.
model_params : Dictionary
Parameters of model with key being the paramter and values being the options
measures : List, optional
Plots associated with model typically from datacollector that represent
critical information collected from the model. The default is None.
name : String, optional
Name of simulation to appear on visual. The default is None.
agent_portrayal : Dictionary, optional
Dictionary of how the agent showed appear. The default is None.
play_interval : INT, optional
Rendering interval of model. The default is 150.
# parameters for leaflet_viz
view : List, optional
Bounds of map to be displayed; must be set with zoom. The default is None.
zoom : Int, optional
Zoom level of map on leaflet
scroll_wheel_zoom : Boolean, optional
True of False for whether or not to enable scroll wheel. The default is True.
Recommend False is in jupyter due to multiple scroll whell options
tiles : Data source for GIS data, optional
Data Source for GIS map data. The default is xyz.OpenStreetMap.Mapnik.
# Due to projection challenges in calculation allow user to specify
center_point : List, optional
Option to pass in center coordinates of map The default is None.. The default is None.
Returns
-------
Provides information to Card to render model
"""

if name is None:
name = model_class.__name__

Expand Down Expand Up @@ -118,6 +181,7 @@ def handle_change_model_params(name: str, value: any):
view=view,
zoom=zoom,
tiles=tiles,
scroll_wheel_zoom=scroll_wheel_zoom,
)
layers = map_drawer.render(model)

Expand All @@ -126,86 +190,47 @@ def handle_change_model_params(name: str, value: any):
center_default = center_point
else:
bounds = layers["layers"]["total_bounds"]
center_default = list((bounds[2:] + bounds[:2]) / 2)
center_default = [

Check warning on line 193 in mesa_geo/visualization/geojupyter_viz.py

View check run for this annotation

Codecov / codecov/patch

mesa_geo/visualization/geojupyter_viz.py#L193

Added line #L193 was not covered by tests
(bounds[0][0] + bounds[1][0]) / 2,
(bounds[0][1] + bounds[1][1]) / 2,
]

# Build base data structure for layout
layout_types = [{"Map": "default"}]

if measures:
layout_types += [{"Measure": elem} for elem in range(len(measures))]

def render_in_jupyter():
# TODO: Build API to allow users to set rows and columns
# call in property of model layers geospace line; use 1 column to prevent map overlap
grid_layout_initial = jv.make_initial_grid_layout(layout_types=layout_types)
grid_layout, set_grid_layout = solara.use_state(grid_layout_initial)

with solara.Row(
justify="space-between", style={"flex-grow": "1"}
) and solara.GridFixed(columns=2):
with solara.Sidebar():
with solara.Card("Controls", margin=1, elevation=2):
jv.UserInputs(user_params, on_change=handle_change_model_params)
jv.ModelController(model, play_interval, current_step, reset_counter)
solara.Markdown(md_text=f"###Step - {current_step}")

# Builds Solara component of map
leaflet_viz.map_jupyter(model, map_drawer, zoom, center_default)

# Place measurement in separate row
with solara.Row(
justify="space-between",
style={"flex-grow": "1"},
):
# 5. Plots
for measure in measures:
if callable(measure):
# Is a custom object
measure(model)
else:
components_matplotlib.PlotMatplotlib(
model, measure, dependencies=[current_step.value]
)

def render_in_browser():
# determine center point
if center_point:
center_default = center_point
else:
bounds = layers["layers"]["total_bounds"]
center_default = list((bounds[2:] + bounds[:2]) / 2)

# if space drawer is disabled, do not include it
layout_types = [{"Map": "default"}]

if measures:
layout_types += [{"Measure": elem} for elem in range(len(measures))]

grid_layout_initial = jv.make_initial_grid_layout(layout_types=layout_types)
grid_layout, set_grid_layout = solara.use_state(grid_layout_initial)

with solara.Sidebar():
with solara.Card("Controls", margin=1, elevation=2):
jv.UserInputs(user_params, on_change=handle_change_model_params)
jv.ModelController(model, play_interval, current_step, reset_counter)
with solara.Card("Progress", margin=1, elevation=2):
solara.Markdown(md_text=f"####Step - {current_step}")

items = [
Card(
model,
measures,
agent_portrayal,
map_drawer,
center_default,
zoom,
current_step,
color="white",
layout_type=layout_types[i],
)
for i in range(len(layout_types))
]

solara.GridDraggable(
items=items,
grid_layout=grid_layout,
resizable=True,
draggable=True,
on_grid_layout=set_grid_layout,
with solara.Card("Progress", margin=1, elevation=2):
solara.Markdown(md_text=f"####Step - {current_step}")

items = [
Card(
model,
measures,
agent_portrayal,
map_drawer,
center_default,
zoom,
scroll_wheel_zoom,
current_step,
color="white",
layout_type=layout_types[i],
)

if ("ipykernel" in sys.argv[0]) or ("colab_kernel_launcher.py" in sys.argv[0]):
# When in Jupyter or Google Colab
render_in_jupyter()
else:
render_in_browser()
for i in range(len(layout_types))
]

solara.GridDraggable(
items=items,
grid_layout=grid_layout,
resizable=True,
draggable=True,
on_grid_layout=set_grid_layout,
)
70 changes: 31 additions & 39 deletions mesa_geo/visualization/leaflet_viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@


@solara.component
def map(model, map_drawer, zoom, center_default):
def map(model, map_drawer, zoom, center_default, scroll_wheel_zoom):
# render map in browser
zoom_map = solara.reactive(zoom)
center = solara.reactive(center_default)
Expand All @@ -29,7 +29,7 @@ def map(model, map_drawer, zoom, center_default):
ipyleaflet.Map.element(
zoom=zoom_map.value,
center=center.value,
scroll_wheel_zoom=True,
scroll_wheel_zoom=scroll_wheel_zoom,
layers=[
ipyleaflet.TileLayer.element(url=base_map["url"]),
ipyleaflet.GeoJSON.element(data=layers["agents"][0]),
Expand All @@ -38,28 +38,6 @@ def map(model, map_drawer, zoom, center_default):
)


@solara.component
def map_jupyter(model, map_drawer, zoom, center_default):
zoom_map = solara.reactive(zoom)
center = solara.reactive(center_default)

base_map = map_drawer.tiles
layers = map_drawer.render(model)

# prevents overlap of map with measures
with solara.Column(style={"isolation": "isolate"}):
ipyleaflet.Map.element(
zoom=zoom_map.value,
center=center.value,
scroll_wheel_zoom=True,
layers=[
ipyleaflet.TileLayer.element(url=base_map["url"]),
ipyleaflet.GeoJSON.element(data=layers["agents"][0]),
*layers["agents"][1],
],
)


@dataclass
class LeafletViz:
"""A dataclass defining the portrayal of a GeoAgent in Leaflet map.
Expand Down Expand Up @@ -89,6 +67,7 @@ def __init__(
portrayal_method,
view,
zoom,
scroll_wheel_zoom,
tiles,
):
"""
Expand All @@ -102,8 +81,7 @@ def __init__(
:param zoom: The initial zoom level of the map. Must be set together with view.
If both view and zoom are None, the map will be centered on the total bounds
of the space. Default is None.
:param map_width: The width of the map in pixels. Default is 500.
:param map_height: The height of the map in pixels. Default is 500.
:param scroll_wheel_zoom: Boolean whether not user can scroll on map with mouse wheel
:param tiles: An optional tile layer to use. Can be a :class:`RasterWebTile` or
a :class:`xyzservices.TileProvider`. Default is `xyzservices.providers.OpenStreetMap.Mapnik`.
Expand Down Expand Up @@ -217,8 +195,22 @@ def _get_marker(self, location, properties):
return ipyleaflet.Circle(location=location, **properties)
elif marker == "CircleMarker":
return ipyleaflet.CircleMarker(location=location, **properties)
elif marker == "Marker" or marker == "Icon" or marker == "AwesomeIcon":
elif marker == "Marker":
return ipyleaflet.Marker(location=location, **properties)
elif marker == "Icon":
icon_url = properties["icon_url"]
icon_size = properties.get("icon_size", [20, 20])
icon_properties = properties.get("icon_properties", {})
icon = ipyleaflet.Icon(

Check warning on line 204 in mesa_geo/visualization/leaflet_viz.py

View check run for this annotation

Codecov / codecov/patch

mesa_geo/visualization/leaflet_viz.py#L201-L204

Added lines #L201 - L204 were not covered by tests
icon_url=icon_url, icon_size=icon_size, **icon_properties
)
return ipyleaflet.Marker(location=location, icon=icon, **properties)

Check warning on line 207 in mesa_geo/visualization/leaflet_viz.py

View check run for this annotation

Codecov / codecov/patch

mesa_geo/visualization/leaflet_viz.py#L207

Added line #L207 was not covered by tests
elif marker == "AwesomeIcon":
name = properties["name"]
icon_properties = properties.get("icon_properties", {})
icon = ipyleaflet.AwesomeIcon(name=name, **icon_properties)
return ipyleaflet.Marker(location=location, icon=icon, **properties)

else:
raise ValueError(
f"Unsupported marker type:{marker}",
Expand All @@ -245,16 +237,16 @@ def _render_agents(self, model):
point_markers.append(self._get_marker(location, properties))
else:
agent_portrayal.style = properties
agent_portrayal = dataclasses.asdict(
agent_portrayal,
dict_factory=lambda x: {k: v for (k, v) in x if v is not None},
)

feature_collection["features"].append(
{
"type": "Feature",
"geometry": mapping(transformed_geometry),
"properties": agent_portrayal,
}
)
agent_portrayal = dataclasses.asdict(
agent_portrayal,
dict_factory=lambda x: {k: v for (k, v) in x if v is not None},
)

feature_collection["features"].append(
{
"type": "Feature",
"geometry": mapping(transformed_geometry),
"properties": agent_portrayal,
}
)
return [feature_collection, point_markers]
Loading

0 comments on commit f0b0ab2

Please sign in to comment.