Repository: projectmesa/mesa
Pull request #1331 (2022-05-18 16:19:19)

examples: Convert color_patches, conways_game_of_life, epstein_civil_violence, forest_fire to simple namespace


Patch:
diff --git a/examples/color_patches/color_patches/model.py b/examples/color_patches/color_patches/model.py
index 1e12d954..fb15a221 100644
--- a/examples/color_patches/color_patches/model.py
+++ b/examples/color_patches/color_patches/model.py
@@ -4,12 +4,10 @@ The model - a 2D lattice where agents live and have an opinion
 
 from collections import Counter
 
-from mesa import Model, Agent
-from mesa.time import SimultaneousActivation
-from mesa.space import Grid
+import mesa
 
 
-class ColorCell(Agent):
+class ColorCell(mesa.Agent):
     """
     Represents a cell's opinion (visualized by a color)
     """
@@ -63,7 +61,7 @@ class ColorCell(Agent):
         self._state = self._next_state
 
 
-class ColorPatches(Model):
+class ColorPatches(mesa.Model):
     """
     represents a 2D lattice where agents live
     """
@@ -74,8 +72,8 @@ class ColorPatches(Model):
         The agents next state is first determined before updating the grid
         """
 
-        self._grid = Grid(width, height, torus=False)
-        self._schedule = SimultaneousActivation(self)
+        self._grid = mesa.space.Grid(width, height, torus=False)
+        self._schedule = mesa.time.SimultaneousActivation(self)
 
         # self._grid.coord_iter()
         #  --> should really not return content + col + row
diff --git a/examples/color_patches/color_patches/server.py b/examples/color_patches/color_patches/server.py
index 1ed8750a..44c4624e 100644
--- a/examples/color_patches/color_patches/server.py
+++ b/examples/color_patches/color_patches/server.py
@@ -4,8 +4,7 @@ the drawing of the model representation on the canvas
 """
 # import webbrowser
 
-from mesa.visualization.modules import CanvasGrid
-from mesa.visualization.ModularVisualization import ModularServer
+import mesa
 
 from .model import ColorPatches
 
@@ -55,11 +54,11 @@ def color_patch_draw(cell):
     return portrayal
 
 
-canvas_element = CanvasGrid(
+canvas_element = mesa.visualization.CanvasGrid(
     color_patch_draw, grid_rows, grid_cols, canvas_width, canvas_height
 )
 
-server = ModularServer(
+server = mesa.visualization.ModularServer(
     ColorPatches,
     [canvas_element],
     "Color Patches",
diff --git a/examples/conways_game_of_life/conways_game_of_life/cell.py b/examples/conways_game_of_life/conways_game_of_life/cell.py
index 4d4ef1e6..8639288d 100644
--- a/examples/conways_game_of_life/conways_game_of_life/cell.py
+++ b/examples/conways_game_of_life/conways_game_of_life/cell.py
@@ -1,7 +1,7 @@
-from mesa import Agent
+import mesa
 
 
-class Cell(Agent):
+class Cell(mesa.Agent):
     """Represents a single ALIVE or DEAD cell in the simulation."""
 
     DEAD = 0
diff --git a/examples/conways_game_of_life/conways_game_of_life/model.py b/examples/conways_game_of_life/conways_game_of_life/model.py
index 021fceff..635ccaa9 100644
--- a/examples/conways_game_of_life/conways_game_of_life/model.py
+++ b/examples/conways_game_of_life/conways_game_of_life/model.py
@@ -1,11 +1,9 @@
-from mesa import Model
-from mesa.time import SimultaneousActivation
-from mesa.space import Grid
+import mesa
 
 from .cell import Cell
 
 
-class ConwaysGameOfLife(Model):
+class ConwaysGameOfLife(mesa.Model):
     """
     Represents the 2-dimensional array of cells in Conway's
     Game of Life.
@@ -22,10 +20,10 @@ class ConwaysGameOfLife(Model):
         # computing their next state simultaneously.  This needs to
         # be done because each cell's next state depends on the current
         # state of all its neighbors -- before they've changed.
-        self.schedule = SimultaneousActivation(self)
+        self.schedule = mesa.time.SimultaneousActivation(self)
 
         # Use a simple grid, where edges wrap around.
-        self.grid = Grid(width, height, torus=True)
+        self.grid = mesa.space.Grid(width, height, torus=True)
 
         # Place a cell at each location, with some initialized to
         # ALIVE and some to DEAD.
diff --git a/examples/conways_game_of_life/conways_game_of_life/server.py b/examples/conways_game_of_life/conways_game_of_life/server.py
index 1cf6924b..4167b3d0 100644
--- a/examples/conways_game_of_life/conways_game_of_life/server.py
+++ b/examples/conways_game_of_life/conways_game_of_life/server.py
@@ -1,13 +1,12 @@
-from mesa.visualization.modules import CanvasGrid
-from mesa.visualization.ModularVisualization import ModularServer
+import mesa
 
 from .portrayal import portrayCell
 from .model import ConwaysGameOfLife
 
 
 # Make a world that is 50x50, on a 250x250 display.
-canvas_element = CanvasGrid(portrayCell, 50, 50, 250, 250)
+canvas_element = mesa.visualization.CanvasGrid(portrayCell, 50, 50, 250, 250)
 
-server = ModularServer(
+server = mesa.visualization.ModularServer(
     ConwaysGameOfLife, [canvas_element], "Game of Life", {"height": 50, "width": 50}
 )
diff --git a/examples/epstein_civil_violence/epstein_civil_violence/agent.py b/examples/epstein_civil_violence/epstein_civil_violence/agent.py
index 6a9d3e7a..c270a970 100644
--- a/examples/epstein_civil_violence/epstein_civil_violence/agent.py
+++ b/examples/epstein_civil_violence/epstein_civil_violence/agent.py
@@ -1,9 +1,9 @@
 import math
 
-from mesa import Agent
+import mesa
 
 
-class Citizen(Agent):
+class Citizen(mesa.Agent):
     """
     A member of the general population, may or may not be in active rebellion.
     Summary of rule: If grievance - risk > threshold, rebel.
@@ -124,7 +124,7 @@ class Citizen(Agent):
         )
 
 
-class Cop(Agent):
+class Cop(mesa.Agent):
     """
     A cop for life.  No defection.
     Summary of rule: Inspect local vision and arrest a random active agent.
diff --git a/examples/epstein_civil_violence/epstein_civil_violence/model.py b/examples/epstein_civil_violence/epstein_civil_violence/model.py
index 7cc68e50..33c80ea0 100644
--- a/examples/epstein_civil_violence/epstein_civil_violence/model.py
+++ b/examples/epstein_civil_violence/epstein_civil_violence/model.py
@@ -1,12 +1,9 @@
-from mesa import Model
-from mesa.time import RandomActivation
-from mesa.space import Grid
-from mesa.datacollection import DataCollector
+import mesa
 
 from .agent import Cop, Citizen
 
 
-class EpsteinCivilViolence(Model):
+class EpsteinCivilViolence(mesa.Model):
     """
     Model 1 from "Modeling civil violence: An agent-based computational
     approach," by Joshua Epstein.
@@ -62,8 +59,8 @@ class EpsteinCivilViolence(Model):
         self.movement = movement
         self.max_iters = max_iters
         self.iteration = 0
-        self.schedule = RandomActivation(self)
-        self.grid = Grid(width, height, torus=True)
+        self.schedule = mesa.time.RandomActivation(self)
+        self.grid = mesa.space.Grid(width, height, torus=True)
         model_reporters = {
             "Quiescent": lambda m: self.count_type_citizens(m, "Quiescent"),
             "Active": lambda m: self.count_type_citizens(m, "Active"),
@@ -77,7 +74,7 @@ class EpsteinCivilViolence(Model):
             "condition": lambda a: getattr(a, "condition", None),
             "arrest_probability": lambda a: getattr(a, "arrest_probability", None),
         }
-        self.datacollector = DataCollector(
+        self.datacollector = mesa.DataCollector(
             model_reporters=model_reporters, agent_reporters=agent_reporters
         )
         unique_id = 0
diff --git a/examples/epstein_civil_violence/epstein_civil_violence/server.py b/examples/epstein_civil_violence/epstein_civil_violence/server.py
index 3087127c..6b835bd2 100644
--- a/examples/epstein_civil_violence/epstein_civil_violence/server.py
+++ b/examples/epstein_civil_violence/epstein_civil_violence/server.py
@@ -1,5 +1,4 @@
-from mesa.visualization.ModularVisualization import ModularServer
-from mesa.visualization.modules import CanvasGrid
+import mesa
 
 from .model import EpsteinCivilViolence
 from .agent import Citizen, Cop
@@ -49,7 +48,7 @@ model_params = dict(
     max_jail_term=1000,
 )
 
-canvas_element = CanvasGrid(citizen_cop_portrayal, 40, 40, 480, 480)
-server = ModularServer(
+canvas_element = mesa.visualization.CanvasGrid(citizen_cop_portrayal, 40, 40, 480, 480)
+server = mesa.visualization.ModularServer(
     EpsteinCivilViolence, [canvas_element], "Epstein Civil Violence", model_params
 )
diff --git a/examples/forest_fire/forest_fire/agent.py b/examples/forest_fire/forest_fire/agent.py
index b120b3fa..34ff2aa2 100644
--- a/examples/forest_fire/forest_fire/agent.py
+++ b/examples/forest_fire/forest_fire/agent.py
@@ -1,7 +1,7 @@
-from mesa import Agent
+import mesa
 
 
-class TreeCell(Agent):
+class TreeCell(mesa.Agent):
     """
     A tree cell.
 
diff --git a/examples/forest_fire/forest_fire/model.py b/examples/forest_fire/forest_fire/model.py
index 5af9a377..de74118f 100644
--- a/examples/forest_fire/forest_fire/model.py
+++ b/examples/forest_fire/forest_fire/model.py
@@ -1,12 +1,9 @@
-from mesa import Model
-from mesa.datacollection import DataCollector
-from mesa.space import Grid
-from mesa.time import RandomActivation
+import mesa
 
 from .agent import TreeCell
 
 
-class ForestFire(Model):
+class ForestFire(mesa.Model):
     """
     Simple Forest Fire model.
     """
@@ -20,10 +17,10 @@ class ForestFire(Model):
             density: What fraction of grid cells have a tree in them.
         """
         # Set up model objects
-        self.schedule = RandomActivation(self)
-        self.grid = Grid(width, height, torus=False)
+        self.schedule = mesa.time.RandomActivation(self)
+        self.grid = mesa.space.Grid(width, height, torus=False)
 
-        self.datacollector = DataCollector(
+        self.datacollector = mesa.DataCollector(
             {
                 "Fine": lambda m: self.count_type(m, "Fine"),
                 "On Fire": lambda m: self.count_type(m, "On Fire"),
diff --git a/examples/forest_fire/forest_fire/server.py b/examples/forest_fire/forest_fire/server.py
index cc81e46e..253aea0a 100644
--- a/examples/forest_fire/forest_fire/server.py
+++ b/examples/forest_fire/forest_fire/server.py
@@ -1,6 +1,4 @@
-from mesa.visualization.modules import CanvasGrid, ChartModule, PieChartModule
-from mesa.visualization.ModularVisualization import ModularServer
-from mesa.visualization.UserParam import UserSettableParameter
+import mesa
 
 from .model import ForestFire
 
@@ -18,19 +16,23 @@ def forest_fire_portrayal(tree):
     return portrayal
 
 
-canvas_element = CanvasGrid(forest_fire_portrayal, 100, 100, 500, 500)
-tree_chart = ChartModule(
+canvas_element = mesa.visualization.CanvasGrid(
+    forest_fire_portrayal, 100, 100, 500, 500
+)
+tree_chart = mesa.visualization.ChartModule(
     [{"Label": label, "Color": color} for (label, color) in COLORS.items()]
 )
-pie_chart = PieChartModule(
+pie_chart = mesa.visualization.PieChartModule(
     [{"Label": label, "Color": color} for (label, color) in COLORS.items()]
 )
 
 model_params = {
     "height": 100,
     "width": 100,
-    "density": UserSettableParameter("slider", "Tree density", 0.65, 0.01, 1.0, 0.01),
+    "density": mesa.visualization.UserSettableParameter(
+        "slider", "Tree density", 0.65, 0.01, 1.0, 0.01
+    ),
 }
-server = ModularServer(
+server = mesa.visualization.ModularServer(
     ForestFire, [canvas_element, tree_chart, pie_chart], "Forest Fire", model_params
 )
diff --git a/examples/wolf_sheep/wolf_sheep/server.py b/examples/wolf_sheep/wolf_sheep/server.py
index e532a3b8..2bfb446a 100644
--- a/examples/wolf_sheep/wolf_sheep/server.py
+++ b/examples/wolf_sheep/wolf_sheep/server.py
@@ -51,20 +51,15 @@ model_params = {
     "grass": mesa.visualization.UserSettableParameter(
         "checkbox", "Grass Enabled", True
     ),
-    "grass_regrowth_time": mesa.visualization.UserSettableParameter(
-        "slider", "Grass Regrowth Time", 20, 1, 50
+    "grass_regrowth_time": mesa.visualization.Slider("Grass Regrowth Time", 20, 1, 50),
+    "initial_sheep": mesa.visualization.Slider(
+        "Initial Sheep Population", 100, 10, 300
     ),
-    "initial_sheep": mesa.visualization.UserSettableParameter(
-        "slider", "Initial Sheep Population", 100, 10, 300
+    "sheep_reproduce": mesa.visualization.Slider(
+        "Sheep Reproduction Rate", 0.04, 0.01, 1.0, 0.01
     ),
-    "sheep_reproduce": mesa.visualization.UserSettableParameter(
-        "slider", "Sheep Reproduction Rate", 0.04, 0.01, 1.0, 0.01
-    ),
-    "initial_wolves": mesa.visualization.UserSettableParameter(
-        "slider", "Initial Wolf Population", 50, 10, 300
-    ),
-    "wolf_reproduce": mesa.visualization.UserSettableParameter(
-        "slider",
+    "initial_wolves": mesa.visualization.Slider("Initial Wolf Population", 50, 10, 300),
+    "wolf_reproduce": mesa.visualization.Slider(
         "Wolf Reproduction Rate",
         0.05,
         0.01,
@@ -72,12 +67,10 @@ model_params = {
         0.01,
         description="The rate at which wolf agents reproduce.",
     ),
-    "wolf_gain_from_food": mesa.visualization.UserSettableParameter(
-        "slider", "Wolf Gain From Food Rate", 20, 1, 50
-    ),
-    "sheep_gain_from_food": mesa.visualization.UserSettableParameter(
-        "slider", "Sheep Gain From Food", 4, 1, 10
+    "wolf_gain_from_food": mesa.visualization.Slider(
+        "Wolf Gain From Food Rate", 20, 1, 50
     ),
+    "sheep_gain_from_food": mesa.visualization.Slider("Sheep Gain From Food", 4, 1, 10),
 }
 
 server = mesa.visualization.ModularServer(
diff --git a/mesa/visualization/ModularVisualization.py b/mesa/visualization/ModularVisualization.py
index 85ae75f9..63dea4ea 100644
--- a/mesa/visualization/ModularVisualization.py
+++ b/mesa/visualization/ModularVisualization.py
@@ -106,7 +106,7 @@ import tornado.escape
 import tornado.gen
 import webbrowser
 
-from mesa.visualization.UserParam import UserSettableParameter
+from mesa.visualization.UserParam import UserSettableParameter, UserParam
 
 # Suppress several pylint warnings for this file.
 # Attributes being defined outside of init is a Tornado feature.
@@ -120,6 +120,12 @@ D3_JS_FILE = "external/d3-7.4.3.min.js"
 CHART_JS_FILE = "external/chart-3.6.1.min.js"
 
 
+def is_user_param(val):
+    return isinstance(val, UserSettableParameter) or issubclass(
+        val.__class__, UserParam
+    )
+
+
 class VisualizationElement:
     """
     Defines an element of the visualization.
@@ -222,9 +228,7 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
 
             # Is the param editable?
             if param in self.application.user_params:
-                if isinstance(
-                    self.application.model_kwargs[param], UserSettableParameter
-                ):
+                if is_user_param(self.application.model_kwargs[param]):
                     self.application.model_kwargs[param].value = value
                 else:
                     self.application.model_kwargs[param] = value
@@ -307,7 +311,7 @@ class ModularServer(tornado.web.Application):
     def user_params(self):
         result = {}
         for param, val in self.model_kwargs.items():
-            if isinstance(val, UserSettableParameter):
+            if is_user_param(val):
                 result[param] = val.json
 
         return result
@@ -317,10 +321,9 @@ class ModularServer(tornado.web.Application):
 
         model_params = {}
         for key, val in self.model_kwargs.items():
-            if isinstance(val, UserSettableParameter):
-                if (
-                    val.param_type == "static_text"
-                ):  # static_text is never used for setting params
+            if is_user_param(val):
+                if val.param_type == "static_text":
+                    # static_text is never used for setting params
                     continue
                 model_params[key] = val.value
             else:
diff --git a/mesa/visualization/UserParam.py b/mesa/visualization/UserParam.py
index 68286ce8..5f9812b7 100644
--- a/mesa/visualization/UserParam.py
+++ b/mesa/visualization/UserParam.py
@@ -1,3 +1,10 @@
+NUMBER = "number"
+CHECKBOX = "checkbox"
+CHOICE = "choice"
+SLIDER = "slider"
+STATIC_TEXT = "static_text"
+
+
 class UserSettableParameter:
     """A class for providing options to a visualization for a given parameter.
 
@@ -35,11 +42,11 @@ class UserSettableParameter:
     static_text = UserSettableParameter('static_text', value="This is a descriptive textbox")
     """
 
-    NUMBER = "number"
-    CHECKBOX = "checkbox"
-    CHOICE = "choice"
-    SLIDER = "slider"
-    STATIC_TEXT = "static_text"
+    NUMBER = NUMBER
+    CHECKBOX = CHECKBOX
+    CHOICE = CHOICE
+    SLIDER = SLIDER
+    STATIC_TEXT = STATIC_TEXT
 
     TYPES = (NUMBER, CHECKBOX, CHOICE, SLIDER, STATIC_TEXT)
 
@@ -119,3 +126,63 @@ class UserSettableParameter:
             "_value"
         )  # Return _value as value, value is the same
         return result
+
+
+class UserParam:
+    _ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'"
+
+    @property
+    def json(self):
+        result = self.__dict__.copy()
+        result["value"] = result.pop(
+            "_value"
+        )  # Return _value as value, value is the same
+        return result
+
+
+class Slider(UserParam):
+    """
+    A number-based slider input with settable increment.
+
+    Example:
+
+    slider_option = Slider("My Slider", value=123, min_value=10, max_value=200, step=0.1)
+    """
+
+    def __init__(
+        self,
+        name="",
+        value=None,
+        min_value=None,
+        max_value=None,
+        step=1,
+        description=None,
+    ):
+        self.param_type = SLIDER
+        self.name = name
+        self._value = value
+        self.min_value = min_value
+        self.max_value = max_value
+        self.step = step
+        self.description = description
+
+        # Validate option types to make sure values are supplied properly
+        valid = not (
+            self.value is None or self.min_value is None or self.max_value is None
+        )
+
+        if not valid:
+            msg = self._ERROR_MESSAGE.format(self.param_type, name)
+            raise ValueError(msg)
+
+    @property
+    def value(self):
+        return self._value
+
+    @value.setter
+    def value(self, value):
+        self._value = value
+        if self._value < self.min_value:
+            self._value = self.min_value
+        elif self._value > self.max_value:
+            self._value = self.max_value


Test patch:
diff --git a/tests/test_usersettableparam.py b/tests/test_usersettableparam.py
index 85d767cf..99f8ee22 100644
--- a/tests/test_usersettableparam.py
+++ b/tests/test_usersettableparam.py
@@ -1,5 +1,5 @@
 from unittest import TestCase
-from mesa.visualization.UserParam import UserSettableParameter
+from mesa.visualization.UserParam import UserSettableParameter, Slider
 
 
 class TestOption(TestCase):
@@ -14,6 +14,7 @@ class TestOption(TestCase):
         self.slider_option = UserSettableParameter(
             "slider", value=123, min_value=100, max_value=200
         )
+        self.slider_option_standalone = Slider(value=123, min_value=100, max_value=200)
 
     def test_number(self):
         assert self.number_option.value == 123
@@ -33,10 +34,14 @@ class TestOption(TestCase):
         assert self.choice_option.value == "I am your default choice"
 
     def test_slider(self):
-        assert self.slider_option.value == 123
-        self.slider_option.value = 150
-        assert self.slider_option.value == 150
-        self.slider_option.value = 0
-        assert self.slider_option.value == 100
-        self.slider_option.value = 300
-        assert self.slider_option.value == 200
+        for option in [self.slider_option, self.slider_option_standalone]:
+            assert option.value == 123
+            option.value = 150
+            assert option.value == 150
+            option.value = 0
+            assert option.value == 100
+            option.value = 300
+            assert option.value == 200
+            assert option.json["value"] == 200
+        with self.assertRaises(ValueError):
+            Slider()