Repository: projectmesa/mesa
Pull request #1272 (2022-04-10 03:53:04)

feat: Implement Slider UserParam class
Partially addresses #1235.

codecov[bot]: # [Codecov](https://codecov.io/gh/projectmesa/mesa/pull/1272?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) Report
> Merging [#1272](https://codecov.io/gh/projectmesa/mesa/pull/1272?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) (c41c466) into [main](https://codecov.io/gh/projectmesa/mesa/commit/e5491079e48c95693af20afa81289a24b8bd3138?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) (e549107) will **decrease** coverage by `0.55%`.
> The diff coverage is `72.34%`.

```diff
@@            Coverage Diff             @@
##             main    #1272      +/-   ##
==========================================
- Coverage   89.29%   88.74%   -0.56%     
==========================================
  Files          19       19              
  Lines        1233     1270      +37     
  Branches      243      248       +5     
==========================================
+ Hits         1101     1127      +26     
- Misses         99      109      +10     
- Partials       33       34       +1     
```


| [Impacted Files](https://codecov.io/gh/projectmesa/mesa/pull/1272?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) | Coverage Δ | |
|---|---|---|
| [mesa/visualization/ModularVisualization.py](https://codecov.io/gh/projectmesa/mesa/pull/1272/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS92aXN1YWxpemF0aW9uL01vZHVsYXJWaXN1YWxpemF0aW9uLnB5) | `76.19% <71.42%> (+0.38%)` | :arrow_up: |
| [mesa/visualization/UserParam.py](https://codecov.io/gh/projectmesa/mesa/pull/1272/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS92aXN1YWxpemF0aW9uL1VzZXJQYXJhbS5weQ==) | `79.54% <72.50%> (-7.25%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/projectmesa/mesa/pull/1272?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/projectmesa/mesa/pull/1272?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa). Last update [e549107...c41c466](https://codecov.io/gh/projectmesa/mesa/pull/1272?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa).

rht: Given that changing from `UserSettableParameter` to `Slider` in the wolf-sheep example won't work on 0.9.0, I need to remove the change to the wolf-sheep in this PR. This means that the examples can only be changed once there is a 0.10.0 or 1.0.0 release with the `Slider`.
Corvince: I think the change itself looks good, but I would find it a bit weird to have only Slider separated into its own Class. I know this is temporary, but maybe you could target `projectmesa/userparam` instead of `projectmesa/main` and we collect the individual UserParameter PRs there and once everything is ready we merge into `main`. This way the PRs remain small until everything is ready and merging `userparam` into `main` should be a fast review, since everything has been reviewed before. 
rht: That will do, only if this PR contains breaking change. But the `Slider` class is an addition to the existing `UserSettableParameters`, and so it can be in `main` at any time without breaking compatibility.

One concern with accumulating the changes into a feature branch is that the regular rebases on top of main may require manual intervention, because of the huge diff.
Corvince: I know the changes are not breaking, but I just find it confusing to have only a set of X UserParameters outside of UserSettableParameters, where X changes over time and is not guaranteed to eventually include all Parameters. 

And what do you mean with regular rebases? I am not experienced with rebases vs merges, is this necessary for rebases? For merging this isn't needed until a merge conflict arises and in that case you have to resolve that either way. 
rht: I don't find it a problem if the main branch doesn't contain all the features yet. It is only problematic if the next release doesn't have all the new user params class. This PR is quick to implement anyway.

Rebase is necessary if the branch becomes far behind the main branch. It will also have conflicts if the diff can't be resolved automatically.

> Given that changing from UserSettableParameter to Slider in the wolf-sheep example won't work on 0.9.0, I need to remove the change to the wolf-sheep in this PR. This means that the examples can only be changed once there is a 0.10.0 or 1.0.0 release with the Slider.

For this, I think it is fine for this PR to modify the examples, as long as people reading the RTD are aware that they are on the development branch, not release.
Corvince: > I don't find it a problem if the main branch doesn't contain all the features yet. It is only problematic if the next release doesn't have all the new user params class. This PR is quick to implement anyway.

Yes, but until the other user param classes are merged this either becomes a blocker for a new release or we ship with an inconsistent state. Just imagine someone finding a critical bug tomorrow and we need to release new version asap. 

But anyway since this isn't such an important feature at all I think it is better to move forward - so I am pro merging if at least one other person from the core developer teams agrees that this is a good feature (I already said so myself in #1235 , but no one else joined the discussion so far)

Patch:
diff --git a/examples/bank_reserves/bank_reserves/server.py b/examples/bank_reserves/bank_reserves/server.py
index 4010bdcb..927d0808 100644
--- a/examples/bank_reserves/bank_reserves/server.py
+++ b/examples/bank_reserves/bank_reserves/server.py
@@ -11,12 +11,13 @@ Accessed on: November 2, 2017
 Author of original code: Taylor Mutch
 """
 
+# The colors here are taken from Matplotlib's tab10 palette
 # Green
-RICH_COLOR = "#46FF33"
+RICH_COLOR = "#2ca02c"
 # Red
-POOR_COLOR = "#FF3C33"
+POOR_COLOR = "#d62728"
 # Blue
-MID_COLOR = "#3349FF"
+MID_COLOR = "#1f77b4"
 
 
 def person_portrayal(agent):
diff --git a/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py b/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py
index 9bed6bd4..3f3da5dd 100644
--- a/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py
+++ b/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py
@@ -1,7 +1,7 @@
-from mesa.visualization.ModularVisualization import VisualizationElement
+import mesa
 
 
-class SimpleCanvas(VisualizationElement):
+class SimpleCanvas(mesa.visualization.VisualizationElement):
     local_includes = ["boid_flockers/simple_continuous_canvas.js"]
     portrayal_method = None
     canvas_height = 500
diff --git a/examples/boid_flockers/boid_flockers/boid.py b/examples/boid_flockers/boid_flockers/boid.py
index 30c71732..d8f45222 100644
--- a/examples/boid_flockers/boid_flockers/boid.py
+++ b/examples/boid_flockers/boid_flockers/boid.py
@@ -1,9 +1,8 @@
+import mesa
 import numpy as np
 
-from mesa import Agent
 
-
-class Boid(Agent):
+class Boid(mesa.Agent):
     """
     A Boid-style flocker agent.
 
diff --git a/examples/boid_flockers/boid_flockers/model.py b/examples/boid_flockers/boid_flockers/model.py
index b118bcc4..00a08d76 100644
--- a/examples/boid_flockers/boid_flockers/model.py
+++ b/examples/boid_flockers/boid_flockers/model.py
@@ -5,16 +5,13 @@ A Mesa implementation of Craig Reynolds's Boids flocker model.
 Uses numpy arrays to represent vectors.
 """
 
+import mesa
 import numpy as np
 
-from mesa import Model
-from mesa.space import ContinuousSpace
-from mesa.time import RandomActivation
-
 from .boid import Boid
 
 
-class BoidFlockers(Model):
+class BoidFlockers(mesa.Model):
     """
     Flocker model class. Handles agent creation, placement and scheduling.
     """
@@ -47,8 +44,8 @@ class BoidFlockers(Model):
         self.vision = vision
         self.speed = speed
         self.separation = separation
-        self.schedule = RandomActivation(self)
-        self.space = ContinuousSpace(width, height, True)
+        self.schedule = mesa.time.RandomActivation(self)
+        self.space = mesa.space.ContinuousSpace(width, height, True)
         self.factors = dict(cohere=cohere, separate=separate, match=match)
         self.make_agents()
         self.running = True
diff --git a/examples/boid_flockers/boid_flockers/server.py b/examples/boid_flockers/boid_flockers/server.py
index 2517092f..4906df69 100644
--- a/examples/boid_flockers/boid_flockers/server.py
+++ b/examples/boid_flockers/boid_flockers/server.py
@@ -1,4 +1,4 @@
-from mesa.visualization.ModularVisualization import ModularServer
+import mesa
 
 from .model import BoidFlockers
 from .SimpleContinuousModule import SimpleCanvas
@@ -18,4 +18,6 @@ model_params = {
     "separation": 2,
 }
 
-server = ModularServer(BoidFlockers, [boid_canvas], "Boids", model_params)
+server = mesa.visualization.ModularServer(
+    BoidFlockers, [boid_canvas], "Boids", model_params
+)
diff --git a/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py b/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py
index f394cd37..76ebc516 100644
--- a/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py
+++ b/examples/boltzmann_wealth_model/boltzmann_wealth_model/model.py
@@ -1,7 +1,4 @@
-from mesa import Agent, Model
-from mesa.time import RandomActivation
-from mesa.space import MultiGrid
-from mesa.datacollection import DataCollector
+import mesa
 
 
 def compute_gini(model):
@@ -12,7 +9,7 @@ def compute_gini(model):
     return 1 + (1 / N) - 2 * B
 
 
-class BoltzmannWealthModel(Model):
+class BoltzmannWealthModel(mesa.Model):
     """A simple model of an economy where agents exchange currency at random.
 
     All the agents begin with one unit of currency, and each time step can give
@@ -22,9 +19,9 @@ class BoltzmannWealthModel(Model):
 
     def __init__(self, N=100, width=10, height=10):
         self.num_agents = N
-        self.grid = MultiGrid(width, height, True)
-        self.schedule = RandomActivation(self)
-        self.datacollector = DataCollector(
+        self.grid = mesa.space.MultiGrid(width, height, True)
+        self.schedule = mesa.time.RandomActivation(self)
+        self.datacollector = mesa.DataCollector(
             model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"}
         )
         # Create agents
@@ -49,7 +46,7 @@ class BoltzmannWealthModel(Model):
             self.step()
 
 
-class MoneyAgent(Agent):
+class MoneyAgent(mesa.Agent):
     """An agent with fixed initial wealth."""
 
     def __init__(self, unique_id, model):
diff --git a/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py b/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py
index 8d4abff9..b3a1e22b 100644
--- a/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py
+++ b/examples/boltzmann_wealth_model/boltzmann_wealth_model/server.py
@@ -1,9 +1,6 @@
-from mesa.visualization.ModularVisualization import ModularServer
-from .model import BoltzmannWealthModel
+import mesa
 
-from mesa.visualization.modules import CanvasGrid
-from mesa.visualization.modules import ChartModule
-from mesa.visualization.UserParam import UserSettableParameter
+from .model import BoltzmannWealthModel
 
 
 def agent_portrayal(agent):
@@ -19,13 +16,13 @@ def agent_portrayal(agent):
     return portrayal
 
 
-grid = CanvasGrid(agent_portrayal, 10, 10, 500, 500)
-chart = ChartModule(
+grid = mesa.visualization.CanvasGrid(agent_portrayal, 10, 10, 500, 500)
+chart = mesa.visualization.ChartModule(
     [{"Label": "Gini", "Color": "#0000FF"}], data_collector_name="datacollector"
 )
 
 model_params = {
-    "N": UserSettableParameter(
+    "N": mesa.visualization.UserSettableParameter(
         "slider",
         "Number of agents",
         100,
@@ -38,5 +35,7 @@ model_params = {
     "height": 10,
 }
 
-server = ModularServer(BoltzmannWealthModel, [grid, chart], "Money Model", model_params)
+server = mesa.visualization.ModularServer(
+    BoltzmannWealthModel, [grid, chart], "Money Model", model_params
+)
 server.port = 8521
diff --git a/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/model.py b/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/model.py
index 0d996ca4..181daec4 100644
--- a/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/model.py
+++ b/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/model.py
@@ -1,10 +1,6 @@
-from mesa import Agent, Model
-from mesa.time import RandomActivation
-from mesa.datacollection import DataCollector
+import mesa
 import networkx as nx
 
-from mesa.space import NetworkGrid
-
 
 def compute_gini(model):
     agent_wealths = [agent.wealth for agent in model.schedule.agents]
@@ -14,7 +10,7 @@ def compute_gini(model):
     return 1 + (1 / N) - 2 * B
 
 
-class BoltzmannWealthModelNetwork(Model):
+class BoltzmannWealthModelNetwork(mesa.Model):
     """A model with some number of agents."""
 
     def __init__(self, num_agents=7, num_nodes=10):
@@ -22,14 +18,14 @@ class BoltzmannWealthModelNetwork(Model):
         self.num_agents = num_agents
         self.num_nodes = num_nodes if num_nodes >= self.num_agents else self.num_agents
         self.G = nx.erdos_renyi_graph(n=self.num_nodes, p=0.5)
-        self.grid = NetworkGrid(self.G)
-        self.schedule = RandomActivation(self)
-        self.datacollector = DataCollector(
+        self.grid = mesa.space.NetworkGrid(self.G)
+        self.schedule = mesa.time.RandomActivation(self)
+        self.datacollector = mesa.DataCollector(
             model_reporters={"Gini": compute_gini},
             agent_reporters={"Wealth": lambda _: _.wealth},
         )
 
-        list_of_random_nodes = self.random.sample(self.G.nodes(), self.num_agents)
+        list_of_random_nodes = self.random.sample(list(self.G), self.num_agents)
 
         # Create agents
         for i in range(self.num_agents):
@@ -51,7 +47,7 @@ class BoltzmannWealthModelNetwork(Model):
             self.step()
 
 
-class MoneyAgent(Agent):
+class MoneyAgent(mesa.Agent):
     """An agent with fixed initial wealth."""
 
     def __init__(self, unique_id, model):
diff --git a/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/server.py b/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/server.py
index e24629cc..a8e48cce 100644
--- a/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/server.py
+++ b/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/server.py
@@ -1,7 +1,5 @@
-from mesa.visualization.ModularVisualization import ModularServer
-from mesa.visualization.UserParam import UserSettableParameter
-from mesa.visualization.modules import ChartModule
-from mesa.visualization.modules import NetworkModule
+import mesa
+
 from .model import BoltzmannWealthModelNetwork
 
 
@@ -29,13 +27,13 @@ def network_portrayal(G):
     return portrayal
 
 
-grid = NetworkModule(network_portrayal, 500, 500)
-chart = ChartModule(
+grid = mesa.visualization.NetworkModule(network_portrayal, 500, 500)
+chart = mesa.visualization.ChartModule(
     [{"Label": "Gini", "Color": "Black"}], data_collector_name="datacollector"
 )
 
 model_params = {
-    "num_agents": UserSettableParameter(
+    "num_agents": mesa.visualization.UserSettableParameter(
         "slider",
         "Number of agents",
         7,
@@ -44,7 +42,7 @@ model_params = {
         1,
         description="Choose how many agents to include in the model",
     ),
-    "num_nodes": UserSettableParameter(
+    "num_nodes": mesa.visualization.UserSettableParameter(
         "slider",
         "Number of nodes",
         10,
@@ -56,7 +54,7 @@ model_params = {
     ),
 }
 
-server = ModularServer(
+server = mesa.visualization.ModularServer(
     BoltzmannWealthModelNetwork, [grid, chart], "Money Model", model_params
 )
 server.port = 8521
diff --git a/examples/charts/charts/agents.py b/examples/charts/charts/agents.py
index a45ad725..67a0956b 100644
--- a/examples/charts/charts/agents.py
+++ b/examples/charts/charts/agents.py
@@ -9,11 +9,12 @@ Author of NetLogo code:
     Northwestern University, Evanston, IL.
 """
 
-from mesa import Agent
+import mesa
+
 from charts.random_walk import RandomWalker
 
 
-class Bank(Agent):
+class Bank(mesa.Agent):
     def __init__(self, unique_id, model, reserve_percent=50):
         # initialize the parent class with required parameters
         super().__init__(unique_id, model)
diff --git a/examples/charts/charts/model.py b/examples/charts/charts/model.py
index 394e1820..0f26e9b5 100644
--- a/examples/charts/charts/model.py
+++ b/examples/charts/charts/model.py
@@ -9,13 +9,10 @@ Author of NetLogo code:
     Northwestern University, Evanston, IL.
 """
 
-from charts.agents import Bank, Person
-from mesa import Model
-from mesa.space import MultiGrid
-from mesa.datacollection import DataCollector
-from mesa.time import RandomActivation
+import mesa
 import numpy as np
 
+from charts.agents import Bank, Person
 
 """
 If you want to perform a parameter sweep, call batch_run.py instead of run.py.
@@ -82,7 +79,7 @@ def get_total_loans(model):
     return np.sum(agent_loans)
 
 
-class Charts(Model):
+class Charts(mesa.Model):
 
     # grid height
     grid_h = 20
@@ -103,13 +100,13 @@ class Charts(Model):
         self.height = height
         self.width = width
         self.init_people = init_people
-        self.schedule = RandomActivation(self)
-        self.grid = MultiGrid(self.width, self.height, torus=True)
+        self.schedule = mesa.time.RandomActivation(self)
+        self.grid = mesa.space.MultiGrid(self.width, self.height, torus=True)
         # rich_threshold is the amount of savings a person needs to be considered "rich"
         self.rich_threshold = rich_threshold
         self.reserve_percent = reserve_percent
         # see datacollector functions above
-        self.datacollector = DataCollector(
+        self.datacollector = mesa.DataCollector(
             model_reporters={
                 "Rich": get_num_rich_agents,
                 "Poor": get_num_poor_agents,
diff --git a/examples/charts/charts/random_walk.py b/examples/charts/charts/random_walk.py
index 448b6a4f..7e067881 100644
--- a/examples/charts/charts/random_walk.py
+++ b/examples/charts/charts/random_walk.py
@@ -8,10 +8,10 @@ Original Author: Jackie Kazil
 Generalized behavior for random walking, one grid cell at a time.
 """
 
-from mesa import Agent
+import mesa
 
 
-class RandomWalker(Agent):
+class RandomWalker(mesa.Agent):
     """
     Class implementing random walker methods in a generalized manner.
     Not intended to be used on its own, but to inherit its methods to multiple
diff --git a/examples/charts/charts/server.py b/examples/charts/charts/server.py
index 3aaea6da..195a3b5a 100644
--- a/examples/charts/charts/server.py
+++ b/examples/charts/charts/server.py
@@ -1,11 +1,5 @@
-from mesa.visualization.ModularVisualization import ModularServer
-from mesa.visualization.modules import (
-    CanvasGrid,
-    ChartModule,
-    BarChartModule,
-    PieChartModule,
-)
-from mesa.visualization.UserParam import UserSettableParameter
+import mesa
+
 from charts.agents import Person
 from charts.model import Charts
 
@@ -17,12 +11,13 @@ Accessed on: November 2, 2017
 Author of original code: Taylor Mutch
 """
 
+# The colors here are taken from Matplotlib's tab10 palette
 # Green
-RICH_COLOR = "#46FF33"
+RICH_COLOR = "#2ca02c"
 # Red
-POOR_COLOR = "#FF3C33"
+POOR_COLOR = "#d62728"
 # Blue
-MID_COLOR = "#3349FF"
+MID_COLOR = "#1f77b4"
 
 
 def person_portrayal(agent):
@@ -55,10 +50,10 @@ def person_portrayal(agent):
 
 # dictionary of user settable parameters - these map to the model __init__ parameters
 model_params = {
-    "init_people": UserSettableParameter(
+    "init_people": mesa.visualization.UserSettableParameter(
         "slider", "People", 25, 1, 200, description="Initial Number of People"
     ),
-    "rich_threshold": UserSettableParameter(
+    "rich_threshold": mesa.visualization.UserSettableParameter(
         "slider",
         "Rich Threshold",
         10,
@@ -66,7 +61,7 @@ model_params = {
         20,
         description="Upper End of Random Initial Wallet Amount",
     ),
-    "reserve_percent": UserSettableParameter(
+    "reserve_percent": mesa.visualization.UserSettableParameter(
         "slider",
         "Reserves",
         50,
@@ -77,10 +72,10 @@ model_params = {
 }
 
 # set the portrayal function and size of the canvas for visualization
-canvas_element = CanvasGrid(person_portrayal, 20, 20, 500, 500)
+canvas_element = mesa.visualization.CanvasGrid(person_portrayal, 20, 20, 500, 500)
 
 # map data to chart in the ChartModule
-line_chart = ChartModule(
+line_chart = mesa.visualization.ChartModule(
     [
         {"Label": "Rich", "Color": RICH_COLOR},
         {"Label": "Poor", "Color": POOR_COLOR},
@@ -88,7 +83,7 @@ line_chart = ChartModule(
     ]
 )
 
-model_bar = BarChartModule(
+model_bar = mesa.visualization.BarChartModule(
     [
         {"Label": "Rich", "Color": RICH_COLOR},
         {"Label": "Poor", "Color": POOR_COLOR},
@@ -96,14 +91,14 @@ model_bar = BarChartModule(
     ]
 )
 
-agent_bar = BarChartModule(
+agent_bar = mesa.visualization.BarChartModule(
     [{"Label": "Wealth", "Color": MID_COLOR}],
     scope="agent",
     sorting="ascending",
     sort_by="Wealth",
 )
 
-pie_chart = PieChartModule(
+pie_chart = mesa.visualization.PieChartModule(
     [
         {"Label": "Rich", "Color": RICH_COLOR},
         {"Label": "Middle Class", "Color": MID_COLOR},
@@ -112,7 +107,7 @@ pie_chart = PieChartModule(
 )
 
 # create instance of Mesa ModularServer
-server = ModularServer(
+server = mesa.visualization.ModularServer(
     Charts,
     [canvas_element, line_chart, model_bar, agent_bar, pie_chart],
     "Mesa Charts",
diff --git a/examples/color_patches/color_patches/server.py b/examples/color_patches/color_patches/server.py
index e0636c24..1ed8750a 100644
--- a/examples/color_patches/color_patches/server.py
+++ b/examples/color_patches/color_patches/server.py
@@ -46,7 +46,8 @@ def color_patch_draw(cell):
     :return: the portrayal dictionary.
 
     """
-    assert cell is not None
+    if cell is None:
+        raise AssertionError
     portrayal = {"Shape": "rect", "w": 1, "h": 1, "Filled": "true", "Layer": 0}
     portrayal["x"] = cell.get_row()
     portrayal["y"] = cell.get_col()
diff --git a/examples/conways_game_of_life/conways_game_of_life/portrayal.py b/examples/conways_game_of_life/conways_game_of_life/portrayal.py
index a2e88760..4f68468d 100644
--- a/examples/conways_game_of_life/conways_game_of_life/portrayal.py
+++ b/examples/conways_game_of_life/conways_game_of_life/portrayal.py
@@ -5,7 +5,8 @@ def portrayCell(cell):
     :param cell:  the cell in the simulation
     :return: the portrayal dictionary.
     """
-    assert cell is not None
+    if cell is None:
+        raise AssertionError
     return {
         "Shape": "rect",
         "w": 1,
diff --git a/examples/epstein_civil_violence/epstein_civil_violence/model.py b/examples/epstein_civil_violence/epstein_civil_violence/model.py
index effc882a..7cc68e50 100644
--- a/examples/epstein_civil_violence/epstein_civil_violence/model.py
+++ b/examples/epstein_civil_violence/epstein_civil_violence/model.py
@@ -67,7 +67,7 @@ class EpsteinCivilViolence(Model):
         model_reporters = {
             "Quiescent": lambda m: self.count_type_citizens(m, "Quiescent"),
             "Active": lambda m: self.count_type_citizens(m, "Active"),
-            "Jailed": lambda m: self.count_jailed(m),
+            "Jailed": self.count_jailed,
         }
         agent_reporters = {
             "x": lambda a: a.pos[0],
diff --git a/examples/hex_snowflake/hex_snowflake/portrayal.py b/examples/hex_snowflake/hex_snowflake/portrayal.py
index 802de1cc..a0a4020e 100644
--- a/examples/hex_snowflake/hex_snowflake/portrayal.py
+++ b/examples/hex_snowflake/hex_snowflake/portrayal.py
@@ -5,7 +5,8 @@ def portrayCell(cell):
     :param cell:  the cell in the simulation
     :return: the portrayal dictionary.
     """
-    assert cell is not None
+    if cell is None:
+        raise AssertionError
     return {
         "Shape": "hex",
         "r": 1,
diff --git a/examples/pd_grid/pd_grid/portrayal.py b/examples/pd_grid/pd_grid/portrayal.py
index 490d9433..a7df44a4 100644
--- a/examples/pd_grid/pd_grid/portrayal.py
+++ b/examples/pd_grid/pd_grid/portrayal.py
@@ -5,7 +5,8 @@ def portrayPDAgent(agent):
     :param agent:  the agent in the simulation
     :return: the portrayal dictionary
     """
-    assert agent is not None
+    if agent is None:
+        raise AssertionError
     return {
         "Shape": "rect",
         "w": 1,
diff --git a/examples/virus_on_network/virus_on_network/model.py b/examples/virus_on_network/virus_on_network/model.py
index abeb8ca9..6f54f7e9 100644
--- a/examples/virus_on_network/virus_on_network/model.py
+++ b/examples/virus_on_network/virus_on_network/model.py
@@ -81,7 +81,7 @@ class VirusOnNetwork(Model):
             self.grid.place_agent(a, node)
 
         # Infect some nodes
-        infected_nodes = self.random.sample(self.G.nodes(), self.initial_outbreak_size)
+        infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size)
         for a in self.grid.get_cell_list_contents(infected_nodes):
             a.state = State.INFECTED
 
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/batchrunner.py b/mesa/batchrunner.py
index 0a59c982..486726c9 100644
--- a/mesa/batchrunner.py
+++ b/mesa/batchrunner.py
@@ -226,9 +226,6 @@ class VariableParameterError(ParameterError):
         "These parameters were given with non-sequence values: {}"
     )
 
-    def __init__(self, bad_names):
-        super().__init__(bad_names)
-
 
 class FixedBatchRunner:
     """This class is instantiated with a model class, and model parameters
diff --git a/mesa/datacollection.py b/mesa/datacollection.py
index 469c67f3..524158de 100644
--- a/mesa/datacollection.py
+++ b/mesa/datacollection.py
@@ -237,7 +237,7 @@ class DataCollector:
 
         """
         all_records = itertools.chain.from_iterable(self._agent_records.values())
-        rep_names = [rep_name for rep_name in self.agent_reporters]
+        rep_names = list(self.agent_reporters)
 
         df = pd.DataFrame.from_records(
             data=all_records,
diff --git a/mesa/space.py b/mesa/space.py
index 6e47b9b1..f9a72309 100644
--- a/mesa/space.py
+++ b/mesa/space.py
@@ -508,15 +508,6 @@ class SingleGrid(Grid):
 
     empties: Set[Coordinate] = set()
 
-    def __init__(self, width: int, height: int, torus: bool) -> None:
-        """Create a new single-item grid.
-
-        Args:
-            width, height: The width and width of the grid
-            torus: Boolean whether the grid wraps or not.
-        """
-        super().__init__(width, height, torus)
-
     def position_agent(
         self, agent: Agent, x: Union[int, str] = "random", y: Union[int, str] = "random"
     ) -> None:
diff --git a/mesa/visualization/ModularVisualization.py b/mesa/visualization/ModularVisualization.py
index 3c302eed..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
@@ -263,9 +267,11 @@ class ModularServer(tornado.web.Application):
     EXCLUDE_LIST = ("width", "height")
 
     def __init__(
-        self, model_cls, visualization_elements, name="Mesa Model", model_params={}
+        self, model_cls, visualization_elements, name="Mesa Model", model_params=None
     ):
         """Create a new visualization server with the given elements."""
+        if model_params is None:
+            model_params = {}
         # Prep visualization elements:
         self.visualization_elements = visualization_elements
         self.package_js_includes = set()
@@ -305,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
@@ -315,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 4c149496..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)
 
@@ -53,9 +60,11 @@ class UserSettableParameter:
         min_value=None,
         max_value=None,
         step=1,
-        choices=list(),
+        choices=None,
         description=None,
     ):
+        if choices is None:
+            choices = list()
         if param_type not in self.TYPES:
             raise ValueError(f"{param_type} is not a valid Option type")
         self.param_type = param_type
@@ -117,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_examples.py b/tests/test_examples.py
index 7ca71b72..9baa31c8 100644
--- a/tests/test_examples.py
+++ b/tests/test_examples.py
@@ -31,7 +31,7 @@ class TestExamples(unittest.TestCase):
             yield
         finally:
             os.chdir(old_cwd)
-            added = [m for m in sys.modules.keys() if m not in old_sys_modules]
+            added = [m for m in sys.modules if m not in old_sys_modules]
             for mod in added:
                 del sys.modules[mod]
             sys.modules.update(old_sys_modules)
diff --git a/tests/test_lifespan.py b/tests/test_lifespan.py
index 6fb9d832..280b8bee 100644
--- a/tests/test_lifespan.py
+++ b/tests/test_lifespan.py
@@ -86,7 +86,7 @@ class TestAgentLifespan(unittest.TestCase):
         assert self.df.steps.max() == 1
 
     def test_agent_lifetime(self):
-        lifetimes = self.df.groupby(["AgentID"]).agg({"Step": lambda x: len(x)})
+        lifetimes = self.df.groupby(["AgentID"]).agg({"Step": len})
         assert lifetimes.Step.max() == 2
 
 
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()