Repository: projectmesa/mesa
Pull request #2397 (2024-10-21 17:24:09)

Altair spaces
This adds altair support for new-style grids (moore, von neumann and hex), as well as old-style continuous spaces

github-actions[bot]: Performance benchmarks:

| Model           | Size   | Init time [95% CI]      | Run time [95% CI]       |
|-----------------|--------|-------------------------|-------------------------|
| BoltzmannWealth | small  | 🔵 +2.3% [+0.6%, +4.0%] | 🔵 -0.0% [-0.3%, +0.3%] |
| BoltzmannWealth | large  | 🔵 +1.0% [+0.3%, +1.8%] | 🔵 +1.4% [+0.2%, +2.8%] |
| Schelling       | small  | 🔵 +0.1% [-0.2%, +0.4%] | 🔵 -0.1% [-0.4%, +0.2%] |
| Schelling       | large  | 🔵 -0.3% [-1.2%, +0.7%] | 🔵 -0.5% [-2.4%, +1.2%] |
| WolfSheep       | small  | 🔵 -0.6% [-1.1%, -0.1%] | 🔵 -0.4% [-1.4%, +0.6%] |
| WolfSheep       | large  | 🔵 +1.6% [-0.4%, +4.5%] | 🔵 -0.6% [-3.5%, +2.1%] |
| BoidFlockers    | small  | 🔵 -0.9% [-1.4%, -0.5%] | 🔵 -1.2% [-1.8%, -0.6%] |
| BoidFlockers    | large  | 🔵 -1.9% [-2.6%, -1.1%] | 🔵 -1.9% [-2.4%, -1.5%] |


Patch:
diff --git a/docs/howto.md b/docs/howto.md
index 7fbb08e3..f7da02e2 100644
--- a/docs/howto.md
+++ b/docs/howto.md
@@ -2,6 +2,45 @@
 
 This guide provides concise instructions and examples to help you start with common tasks in Mesa.
 
+## Implementing Different Activation Regimes
+
+
+### Random Activation
+
+self.agents.shuffle_do("<agent function>")
+
+### Random Activation By Type
+
+```python
+for agent_class in self.agent_types:
+    self.agents_by_type[agent_class].shuffle_do("<agent function>")
+```
+### Only Activating Certain Agent Types
+
+```python
+self.agents_by_type[AgentType].shuffle_do("<agent function>")
+```
+
+### Staged Activation
+
+```python
+for stage in ["stage1", "stage2", "stage3"]:
+    self.agents.do(stage)
+```
+
+If you want to  `shuffle` and/or `shuffle_between_stages` options:
+```python
+
+stages = ["stage1", "stage2", "stage3"]
+if shuffle:
+    self.random.shuffle(stages)
+for stage in stages:
+    if shuffle_between_stages:
+        self.agents.shuffle_do(stage)
+    else:
+        self.agents.do(stage)
+```
+
 ## Models with Discrete Time
 
 For models involving agents of multiple types, including those with a time attribute, you can construct a discrete-time model. This setup allows each agent to perform actions in steps that correspond to the model's discrete time.
diff --git a/docs/tutorials/MoneyModel.py b/docs/tutorials/MoneyModel.py
index 576b8e71..4d6dc02d 100644
--- a/docs/tutorials/MoneyModel.py
+++ b/docs/tutorials/MoneyModel.py
@@ -49,7 +49,7 @@ class MoneyAgent(mesa.Agent):
 class MoneyModel(mesa.Model):
     """A model with some number of agents."""
 
-    def __init__(self, N, width, height, seed=None):
+    def __init__(self, n, width, height, seed=None):
         """Initialize a MoneyModel instance.
 
         Args:
@@ -58,7 +58,7 @@ class MoneyModel(mesa.Model):
             height: Height of the grid.
         """
         super().__init__(seed=seed)
-        self.num_agents = N
+        self.num_agents = n
         self.grid = mesa.space.MultiGrid(width, height, True)
         self.schedule = mesa.time.RandomActivation(self)
 
diff --git a/docs/tutorials/intro_tutorial.ipynb b/docs/tutorials/intro_tutorial.ipynb
index 77630fe3..874fe71b 100644
--- a/docs/tutorials/intro_tutorial.ipynb
+++ b/docs/tutorials/intro_tutorial.ipynb
@@ -15,21 +15,39 @@
    "source": [
     "**Important:** \n",
     "- If you are just exploring Mesa and want the fastest way to execute the code we recommend executing this tutorial online in a Colab notebook. [![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/projectmesa/mesa/blob/main/docs/tutorials/intro_tutorial.ipynb)\n",
-    "- If you have installed mesa and are running locally, please ensure that your [Mesa version](https://pypi.org/project/Mesa/) is up-to-date in order to run this tutorial.\n",
+    "- If you have installed mesa and are running locally, please ensure that your Mesa version is greater than or equal to 3.0.0b1.\n",
     "\n",
     "## Tutorial Description\n",
     "\n",
-    "[Mesa](https://github.com/projectmesa/mesa) is a Python framework for [agent-based modeling](https://en.wikipedia.org/wiki/Agent-based_model). This tutorial will assist you in getting started. Working through the tutorial will help you discover the core features of Mesa.  Through the tutorial, you are walked through creating a starter-level model. Functionality is added progressively as the process unfolds. Should anyone find any errors, bugs, have a suggestion, or just are looking for clarification, [let us know](https://github.com/projectmesa/mesa/issues)!\n",
+    "[Mesa](https://github.com/projectmesa/mesa) is a Python framework for [agent-based modeling](https://en.wikipedia.org/wiki/Agent-based_model). This tutorial will assist you in getting started and discover some of the core features of Mesa.  The tutorial starts with the key pieces of a model and then progressively adds functionality. \n",
     "\n",
-    "The premise of this tutorial is to create a starter-level model representing agents exchanging money. This exchange of money affects wealth. \n",
+    "Should anyone find any errors, bugs, have a suggestion, or just are looking for clarification, let us know in our [chat](https://matrix.to/#/#project-mesa:matrix.org)!\n",
     "\n",
-    "Next, *space* is added to allow agents to move based on the change in wealth as time progresses.\n",
+    "The premise of this tutorial is to create a starter-level model representing agents exchanging money. \n",
     "\n",
-    "Two of Mesa's analytic tools: the *data collector* and *batch runner* are then used to examine the dynamics of this simple model. \n",
+    "The table of contents (icon on the left) shows each of the items this tutorial covers with the major sections being: \n",
     "\n",
-    "### More Tutorials: \n",
+    "- Model Description and Set Up\n",
+    "- Building the Basic Model\n",
+    "- Adding Space\n",
+    "- Collecting Data\n",
+    "- AgentSet Functionality\n",
+    "- Batch Run "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### More Mesa\n",
     "\n",
-    "Visualization: There is a separate [visualization tutorial](https://mesa.readthedocs.io/stable/tutorials/visualization_tutorial.html) that will take users through building a visualization for this model (aka Boltzmann Wealth Model)."
+    "If you are looking for other Mesa models or tools here are some additional resources. \n",
+    "\n",
+    "- Interactive Dashboard: There is a separate [visualization tutorial](https://mesa.readthedocs.io/latest/tutorials/visualization_tutorial.html) that will take users through building a dashboard for this model (aka Boltzmann Wealth Model).\n",
+    "- Classic ABMs: You can also find canonical examples of ABMs written in Mesa in the [Examples Tab](https://mesa.readthedocs.io/latest/tutorials/examples.html)\n",
+    "- More Examples: Want to integrate Reinforcement Learning or work on the Traveling Salesman Problem checkout  [Mesa Examples](https://github.com/projectmesa/mesa-examples/)\n",
+    "- Mesa-Geo: If you need an ABM with Geographic Information Systems (GIS) checkout [Mesa-Geo](https://mesa-geo.readthedocs.io/latest/)\n",
+    "- Mesa Frames: Have a large complex model that you need to speed up, check out [Mesa Frames](https://github.com/projectmesa/mesa-frames)"
    ]
   },
   {
@@ -38,7 +56,7 @@
    "source": [
     "## Model Description\n",
     "\n",
-    "This is a starter-level simulated agent-based economy. In an agent-based economy, the behavior of an individual economic agent, such as a consumer or producer, is studied in a market environment.\n",
+    "This is a simulated agent-based economy. In an agent-based economy, the behavior of an individual economic agent, such as a consumer or producer, is studied in a market environment.\n",
     "This model is drawn from the field econophysics, specifically a paper prepared by Drăgulescu et al. for additional information on the modeling assumptions used in this model. [Drăgulescu, 2002].\n",
     "\n",
     "The assumption that govern this model are:\n",
@@ -48,8 +66,9 @@
     "3. At every step of the model, an agent gives 1 unit of money (if they\n",
     "   have it) to some other agent.\n",
     "\n",
-    "Even as a starter-level model the yielded results are both interesting and unexpected to individuals unfamiliar\n",
-    "with it the specific topic. As such, this model is a good starting point to examine Mesa's core features."
+    "Even as a starter-level model it yields results that are both interesting and unexpected. \n",
+    "\n",
+    "Due to its simplicity and intrquiging results, we found ti to be a greater starter model. "
    ]
   },
   {
@@ -72,7 +91,7 @@
     "pip install --upgrade --pre mesa\n",
     "```\n",
     "\n",
-    "Install Jupyter Notebook (optional):\n",
+    "Install Jupyter notebook (optional):\n",
     "\n",
     "```bash\n",
     "pip install jupyter\n",
@@ -89,15 +108,14 @@
    ]
   },
   {
-   "cell_type": "code",
-   "execution_count": null,
+   "cell_type": "raw",
    "metadata": {},
-   "outputs": [],
    "source": [
-    "# SKIP THIS CELL unless running in colab\n",
+    "# SKIP THIS CELL unless you need to install Mesa; convert cell type from raw to code\n",
     "\n",
-    "%pip install --quiet mesa\n",
-    "# The exclamation points tell jupyter to do the command via the command line"
+    "%pip install --quiet --upgrade --pre mesa\n",
+    "print(f\"Mesa version: {mesa.__version__}\")\n",
+    "# The percent sign installs in the process associated with the notebook"
    ]
   },
   {
@@ -106,34 +124,27 @@
    "source": [
     "## Building the Sample Model\n",
     "\n",
-    "After Mesa is installed a model can be built. A jupyter notebook is recommended for this tutorial, this allows for small segments of codes to be examined one at a time. As an option this can be created using python script files.\n",
+    "After Mesa is installed a model can be built. A jupyter notebook is recommended for this tutorial, this allows for small segments of codes to be examined one at a time. \n",
     "\n",
-    "**Good Practice:** Place a model in its own folder/directory. This is not specifically required for the starter_model, but as other models become more complicated and expand multiple python scripts, documentation, discussions and notebooks may be added.\n",
+    "### Creating Model With Jupyter notebook\n",
     "\n",
-    "### Create New Folder/Directory\n",
+    "Write the model interactively in [Jupyter](http://jupyter.org/) cells.\n",
     "\n",
-    "- Using operating system commands create a new folder/directory named 'starter_model'.\n",
-    "\n",
-    "- Change into the new folder/directory.\n",
-    "\n",
-    "\n",
-    "### Creating Model With Jupyter Notebook\n",
-    "\n",
-    "Write the model interactively in [Jupyter Notebook](http://jupyter.org/) cells.\n",
-    "\n",
-    "Start Jupyter Notebook:\n",
+    "Start Jupyter:\n",
     "\n",
     "```bash\n",
-    "jupyter notebook\n",
+    "jupyter lab\n",
     "```\n",
     "\n",
-    "Create a new Notebook named `money_model.ipynb` (or whatever you want to call it).\n",
+    "Create a new notebook named `money_model.ipynb` (or whatever you want to call it).\n",
     "\n",
     "### Creating Model With Script File (IDE, Text Editor, Colab, etc.)\n",
     "\n",
     "Create a new file called `money_model.py` (or whatever you want to call it)\n",
     "\n",
-    "*Code will be added as the tutorial progresses.*\n"
+    "*Code will be added as the tutorial progresses.*\n",
+    "\n",
+    "**Good Practice:** Place a model in its own folder/directory. This is not specifically required for the starter_model, but as other models become more complicated and expand multiple python scripts, documentation, discussions and notebooks may be added."
    ]
   },
   {
@@ -148,7 +159,10 @@
    "cell_type": "code",
    "execution_count": null,
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "outputs": [],
    "source": [
@@ -173,22 +187,19 @@
     "\n",
     "First create the agent. As the tutorial progresses, more functionality will be added to the agent.\n",
     "\n",
-    "**Background:** Agents are the individual entities that act in the model. It is a good modeling practice to make certain each Agent can be uniquely identified.\n",
+    "**Background:** Agents are the individual entities that act in the model. Mesa automatically assigns each agent that is created an integer as a `unique_id.` \n",
     "\n",
-    "**Model-specific information:** Agents are the individuals that exchange money, in this case the amount of money an individual agent has is represented as wealth. Additionally, agents each have a unique identifier.\n",
+    "**Model-specific information:** Agents are the individuals that exchange money, in this case the amount of money an individual agent has is represented as wealth. \n",
     "\n",
-    "**Code implementation:** This is done by creating a new class (or object) that extends `mesa.Agent` creating a subclass of  the `Agent` class from mesa. The new class is named `MoneyAgent`. The technical details about the agent object can be found in the [mesa repo](https://github.com/projectmesa/mesa/blob/main/mesa/agent.py).\n",
+    "**Code implementation:** This is done by creating a new class (or object) that extends `mesa.Agent` creating a subclass of  the `Agent` class from mesa. The new class is named `MoneyAgent`. The inherited code of the Mesa agent object can be found in the [mesa repo](https://github.com/projectmesa/mesa/blob/main/mesa/agent.py).\n",
     "\n",
-    "\n",
-    "The `MoneyAgent` class is created with the following code:\n"
+    "The `MoneyAgent` class is created with the following code:"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {
-    "collapsed": false
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "class MoneyAgent(mesa.Agent):\n",
@@ -208,13 +219,13 @@
    "source": [
     "### Create Model\n",
     "\n",
-    "Next, create the model. Again, as the tutorial progresses, more functionality will be added to the model.\n",
+    "Next, create the model. This gives us the two basic classes of any Mesa ABM - the agent class (population of agent objects that doing something) and the manager class (a model object that manages the creation, activation, datacollection etc of the agents)\n",
     "\n",
-    "**Background:** The model can be visualized as a grid containing all the agents. The model creates, holds and manages all the agents on the grid. The model evolves in discrete time steps.\n",
+    "**Background:** The model can be visualized as a list containing all the agents. The model creates, holds and manages all the agent objects, specifically in a dictionary. The model activates agents in discrete time steps.\n",
     "\n",
-    "**Model-specific information:** When a model is created the number of agents within the model is specified. The model then creates the agents and places them on the grid. The model also contains a scheduler which controls the order in which agents are activated. The scheduler is also responsible for advancing the model by one step. The model also contains a data collector which collects data from the model. These topics will be covered in more detail later in the tutorial.\n",
+    "**Model-specific information:** When a model is created the number of agents within the model is specified. The model then creates the agents and places them in an set of agents. \n",
     "\n",
-    "**Code implementation:** This is done by creating a new class (or object) that extends `mesa.Model` and calls `super().__init__()`, creating a subclass of the `Model` class from mesa. The new class is named `MoneyModel`.  The technical details about the agent object can be found in the [mesa repo](https://github.com/projectmesa/mesa/blob/main/mesa/model.py).\n",
+    "**Code implementation:** This is done by creating a new class (or object) that extends `mesa.Model` and calls `super().__init__()`, creating a subclass of the `Model` class from mesa. The new class is named `MoneyModel`.  The technical details about the model object can be found in [model module](https://github.com/projectmesa/mesa/blob/main/mesa/model.py) and the AgentSet in the [agent module](https://github.com/projectmesa/mesa/blob/d7a3834c99a3be809abe2edc8b83610f3d4438ba/mesa/agent.py#L86). A critical point is that you can use the `seed` kwarg (keyword argument) to set a seed which controls the random number generator of the model class allowing for the reproducibility of results. \n",
     "\n",
     "The `MoneyModel` class is created with the following code:"
    ]
@@ -228,28 +239,28 @@
     "class MoneyModel(mesa.Model):\n",
     "    \"\"\"A model with some number of agents.\"\"\"\n",
     "\n",
-    "    def __init__(self, N):\n",
-    "        super().__init__()\n",
-    "        self.num_agents = N\n",
+    "    def __init__(self, n, seed=None):\n",
+    "        super().__init__(seed=seed)\n",
+    "        self.num_agents = n\n",
     "        # Create agents\n",
     "        for _ in range(self.num_agents):\n",
     "            a = MoneyAgent(self)"
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### Adding the Scheduler\n",
-    "Now the model will be modified to add a scheduler.\n",
+    "### Making the Agents `do`\n",
     "\n",
-    "**Background:** The scheduler controls the order in which agents are activated, causing the agent to take their defined action. The scheduler is also responsible for advancing the model by one step. A step is the smallest unit of time in the model, and is often referred to as a tick. The scheduler can be configured to activate agents in different orders. This can be important as the order in which agents are activated can impact the results of the model [Comer2014]. At each step of the model, one or more of the agents -- usually all of them -- are activated and take their own step, changing internally and/or interacting with one another or the environment.\n",
+    "With the basics of the Agent class and Model class created we can no activate the agents to `do` things\n",
     "\n",
-    "**Model-specific information:** A new class is named `RandomActivationByAgent` is created which extends `mesa.time.RandomActivation` creating a subclass of  the `RandomActivation` class from Mesa. This class activates all the agents once per step, in random order. Every agent is expected to have a ``step`` method. The step method is the action the agent takes when it is activated by the model schedule. We add an agent to the schedule using the `add` method; when we call the schedule's `step` method, the model shuffles the order of the agents, then activates and executes each agent's ```step``` method. The scheduler is then added to the model.\n",
+    "**Background:** Mesa's `do` function calls agent functions the grow your ABM. A step is the smallest unit of time in the model, and is often referred to as a tick. The `do` function and Python functionality can be configured to activate agents in different orders. This can be important as the order in which agents are activated can impact the results of the model [Comer2014]. At each step of the model, one or more of the agents -- usually all of them -- are activated and take their own step, changing internally and/or interacting with one another or the environment. A overview of different ways to employ the `do` function for different activation regimes can be found in the [\"How To\" Guide](https://mesa.readthedocs.io/latest/howto.html).\n",
     "\n",
-    "**Code implementation:** The technical details about the timer object can be found in the [mesa repo](https://github.com/projectmesa/mesa/blob/main/mesa/time.py). Mesa offers a few different built-in scheduler classes, with a common interface. That makes it easy to change the activation regime a given model uses, and see whether it changes the model behavior. The details pertaining to the scheduler interface can be located in the same [mesa repo](https://github.com/projectmesa/mesa/blob/main/mesa/time.py).\n",
+    "**Model-specific information:** For this section we will randomly reorder the Agent activation order using  `mesa.Agent.shuffle_do` and have the agents `step` function print the agent unique id they are assigned during the agent creation process. \n",
     "\n",
-    "With that in mind, the `MoneyAgent` code is modified below to visually show when a new agent is created. The MoneyModel code is modified by adding the RandomActivation method to the model. with the scheduler added looks like this:"
+    "**Code implementation:** Using standard ABM convention we add a `step` function to the `MoneyModel` class which calls the `mesa.Agent.shuffle_do` function. We then pass into `shuffle_do` the parameter \"step\". This tells mesa to look for and execute the `step` function in our MoneyAgent class.  "
    ]
   },
   {
@@ -268,7 +279,7 @@
     "        # Create the agent's attribute and set the initial values.\n",
     "        self.wealth = 1\n",
     "\n",
-    "    def step(self):\n",
+    "    def say_hi(self):\n",
     "        # The agent's step will go here.\n",
     "        # For demonstration purposes we will print the agent's unique_id\n",
     "        print(f\"Hi, I am an agent, you can call me {str(self.unique_id)}.\")\n",
@@ -277,23 +288,20 @@
     "class MoneyModel(mesa.Model):\n",
     "    \"\"\"A model with some number of agents.\"\"\"\n",
     "\n",
-    "    def __init__(self, N):\n",
-    "        super().__init__()\n",
-    "        self.num_agents = N\n",
-    "        # Create scheduler and assign it to the model\n",
-    "        self.schedule = mesa.time.RandomActivation(self)\n",
-    "\n",
+    "    def __init__(self, n, seed=None):\n",
+    "        super().__init__(seed=seed)\n",
+    "        self.num_agents = n\n",
+    "        \n",
     "        # Create agents\n",
     "        for _ in range(self.num_agents):\n",
-    "            a = MoneyAgent(self)\n",
-    "            # Add the agent to the scheduler\n",
-    "            self.schedule.add(a)\n",
+    "            a = MoneyAgent(self) # This calls the agent class parameter n number of times                         \n",
     "\n",
     "    def step(self):\n",
     "        \"\"\"Advance the model by one step.\"\"\"\n",
     "\n",
-    "        # The model's step will go here for now this will call the step method of each agent and print the agent's unique_id\n",
-    "        self.schedule.step()"
+    "        # This function psuedo-randomly reorders the list of agent objects and \n",
+    "        # then iterates through calling the function passed in as the parameter\n",
+    "        self.agents.shuffle_do(\"say_hi\")"
    ]
   },
   {
@@ -303,7 +311,7 @@
     "### Running the Model\n",
     "A basic model has now been created. The model can be run by creating a model object and calling the step method. The model will run for one step and print the unique_id of each agent. You may run the model for multiple steps by calling the step method multiple times.\n",
     "\n",
-    "Note: If you are using `.py` (script) files instead of `.ipynb` (Jupyter), the common convention is\n",
+    "note: If you are using `.py` (script) files instead of `.ipynb` (Jupyter), the common convention is\n",
     "to have a `run.py` in the same directory as your model code. You then (1) import the ``MoneyModel`` class,\n",
     "(2) create a model object and (3) run it for a few steps. As shown below:\n",
     "\n",
@@ -330,19 +338,40 @@
    "cell_type": "code",
    "execution_count": null,
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "outputs": [],
    "source": [
-    "# Run this step overnight and see what happens! Notice the order of the agents changes each time.\n",
+    "# Run this step a few times and see what happens! notice the order of the agents changes each time.\n",
     "starter_model.step()"
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Challenge: Change the seed from None to a number like 42 and see the impact"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Challenge: Change `shuffle_do` to just `do` and see the impact"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "#### Exercise\n",
+    "### Exercise\n",
     "Modifying the code below to have every agent print out its `wealth` when it is activated."
    ]
   },
@@ -350,7 +379,10 @@
    "cell_type": "code",
    "execution_count": null,
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "outputs": [],
    "source": [
@@ -364,9 +396,9 @@
     "        # Create the agent's variable and set the initial values.\n",
     "        self.wealth = 1\n",
     "\n",
-    "    def step(self):\n",
+    "    def say_wealth(self):\n",
     "        # The agent's step will go here.\n",
-    "        # FIXME: Need to print the agent's wealth\n",
+    "        # FIXME: need to print the agent's wealth\n",
     "        print(f\"Hi, I am an agent and I am broke!\")"
    ]
   },
@@ -381,7 +413,10 @@
    "cell_type": "code",
    "execution_count": null,
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "outputs": [],
    "source": [
@@ -392,24 +427,17 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### Agent Step\n",
+    "### Agents Exchange\n",
     "\n",
-    "Returning back to the MoneyAgent the actual step process is now going to be created.\n",
+    "Returning back to the MoneyAgent the actual exchange process is now going to be created.\n",
     "\n",
     "**Background:** This is where the agent's behavior as it relates to each step or tick of the model is defined.\n",
     "\n",
     "**Model-specific information:** In this case, the agent will check its wealth, and if it has money, give one unit of it away to another random agent.\n",
     "\n",
-    "**Code implementation:** The agent's step method is called by the scheduler during each step of the model. To allow the agent to choose another agent at random, we use the `model.random` random-number generator. This works just like Python's `random` module, but with a fixed seed set when the model is instantiated, that can be used to replicate a specific model run later."
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "To pick an agent at random, we need a list of all agents. Notice that there isn't such a list explicitly in the model. The scheduler, however, does have an internal list of all the agents it is scheduled to activate.\n",
+    "**Code implementation:** The agent's step method is called by `mesa.Agent.shuffle_do(\"exchange\")`during each step of the model. To allow the agent to choose another agent at random, we use the `model.random` random-number generator. This works just like Python's `random` module, but if a fixed seed set is set when the model is instantiated (see earlier challenge), this allows users to replicate a specific model run later. Once we identify this other agent object we increase their wealth by 1 and decrease this agents wealth by one.\n",
     "\n",
-    "With that in mind, we rewrite the agent `step` method as shown below:"
+    "This updates the step function as shown below"
    ]
   },
   {
@@ -418,9 +446,6 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "import copy\n",
-    "\n",
-    "\n",
     "class MoneyAgent(mesa.Agent):\n",
     "    \"\"\"An agent with fixed initial wealth.\"\"\"\n",
     "\n",
@@ -431,13 +456,31 @@
     "        # Create the agent's variable and set the initial values.\n",
     "        self.wealth = 1\n",
     "\n",
-    "    def step(self):\n",
+    "    def exchange(self):\n",
     "        # Verify agent has some wealth\n",
     "        if self.wealth > 0:\n",
-    "            other_agent = self.random.choice(self.model.schedule.agents)\n",
+    "            other_agent = self.random.choice(self.model.agents)\n",
     "            if other_agent is not None:\n",
     "                other_agent.wealth += 1\n",
-    "                self.wealth -= 1"
+    "                self.wealth -= 1\n",
+    "\n",
+    "class MoneyModel(mesa.Model):\n",
+    "    \"\"\"A model with some number of agents.\"\"\"\n",
+    "\n",
+    "    def __init__(self, n):\n",
+    "        super().__init__()\n",
+    "        self.num_agents = n\n",
+    "        \n",
+    "        # Create agents\n",
+    "        for _ in range(self.num_agents):\n",
+    "            a = MoneyAgent(self) # This calls the agent class parameter n number of times                         \n",
+    "\n",
+    "    def step(self):\n",
+    "        \"\"\"Advance the model by one step.\"\"\"\n",
+    "\n",
+    "        # This function psuedo-randomly reorders the list of agent objects and \n",
+    "        # then iterates through calling the function passed in as the parameter\n",
+    "        self.agents.shuffle_do(\"exchange\")"
    ]
   },
   {
@@ -448,13 +491,13 @@
     "\n",
     "With that last piece in hand, it's time for the first rudimentary run of the model.\n",
     "\n",
-    "If you've written the code in its own script file (`money_model.py` or a different name) you can now modify your ``run.py`` or even launch a Jupyter Notebook. You then just follow the same three steps of (1) import your model class ``MoneyModel``, (2) create the model object and (3) run it for a few steps. If you wrote the code in one Notebook then step 1, importing, is not necessary.\n",
+    "If you've written the code in its own script file (`money_model.py` or a different name) you can now modify your ``run.py`` or even launch a Jupyter notebook. You then just follow the same three steps of (1) import your model class ``MoneyModel``, (2) create the model object and (3) run it for a few steps. If you wrote the code in one notebook then step 1, importing, is not necessary.\n",
     "\n",
     "```python\n",
     "from money_model import MoneyModel\n",
     "```\n",
     "\n",
-    "Now let's create a model with 10 agents, and run it for 10 steps."
+    "now let's create a model with 10 agents, and run it for 30 steps."
    ]
   },
   {
@@ -463,8 +506,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "model = MoneyModel(10)\n",
-    "for _ in range(10):\n",
+    "model = MoneyModel(10) # Tels the model to create 10 agents\n",
+    "for _ in range(30): #Runs the model for 10 steps; an underscore is common convention for a variable that is not used\n",
     "    model.step()"
    ]
   },
@@ -472,7 +515,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Next, we need to get some data out of the model. Specifically, we want to see the distribution of the agent's wealth. We can get the wealth values with list comprehension, and then use seaborn (or another graphics library) to visualize the data in a histogram."
+    "next, we need to get some data out of the model. Specifically, we want to see the distribution of the agent's wealth. We can get the wealth values with list comprehension, and then use seaborn (or another graphics library) to visualize the data in a histogram."
    ]
   },
   {
@@ -481,21 +524,14 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "agent_wealth = [a.wealth for a in model.schedule.agents]\n",
+    "agent_wealth = [a.wealth for a in model.agents]\n",
     "# Create a histogram with seaborn\n",
     "g = sns.histplot(agent_wealth, discrete=True)\n",
     "g.set(\n",
-    "    title=\"Wealth distribution\", xlabel=\"Wealth\", ylabel=\"Number of agents\"\n",
+    "    title=\"Wealth distribution\", xlabel=\"Wealth\", ylabel=\"number of agents\"\n",
     ");  # The semicolon is just to avoid printing the object representation"
    ]
   },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "You'll should see something like the distribution above. Yours will almost certainly look at least slightly different, since each run of the model is random, after all."
-   ]
-  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -514,23 +550,23 @@
     "for _ in range(100):\n",
     "    # Run the model\n",
     "    model = MoneyModel(10)\n",
-    "    for _ in range(10):\n",
+    "    for _ in range(30):\n",
     "        model.step()\n",
     "\n",
     "    # Store the results\n",
-    "    for agent in model.schedule.agents:\n",
+    "    for agent in model.agents:\n",
     "        all_wealth.append(agent.wealth)\n",
     "\n",
     "# Use seaborn\n",
     "g = sns.histplot(all_wealth, discrete=True)\n",
-    "g.set(title=\"Wealth distribution\", xlabel=\"Wealth\", ylabel=\"Number of agents\");"
+    "g.set(title=\"Wealth distribution\", xlabel=\"Wealth\", ylabel=\"number of agents\");"
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "This runs 100 instantiations of the model, and runs each for 10 steps. (Notice that we set the histogram bins to be integers, since agents can only have whole numbers of wealth). This distribution looks a lot smoother. By running the model 100 times, we smooth out some of the 'noise' of randomness, and get to the model's overall expected behavior.\n",
+    "This runs 100 instantiations of the model, and runs each for 10 steps. (notice that we set the histogram bins to be integers, since agents can only have whole numbers of wealth). This distribution looks a lot smoother. By running the model 100 times, we smooth out some of the 'noise' of randomness, and get to the model's overall expected behavior.\n",
     "\n",
     "This outcome might be surprising. Despite the fact that all agents, on average, give and receive one unit of money every step, the model converges to a state where most agents have a small amount of money and a small number have a lot of money."
    ]
@@ -539,15 +575,17 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### Adding space\n",
+    "## Adding space\n",
+    "\n",
+    "**Background:** Many ABMs have a spatial element, with agents moving around and interacting with nearby neighbors. Mesa has several types of [spaces](https://mesa.readthedocs.io/latest/apis/space.html) from different types of grids to networks to an in development [cell_space](https://mesa.readthedocs.io/latest/apis/experimental.html#module-experimental.cell_space.__init__). Mesa grids are divided into cells, and agents can only be on a particular cell, like pieces on a chess board. Continuous space, in contrast, allows agents to have any arbitrary position. (Think of grids vs continuous space like the difference between integers and decimals.)\n",
     "\n",
-    "Many ABMs have a spatial element, with agents moving around and interacting with nearby neighbors. Mesa currently supports two overall kinds of spaces: grid, and continuous. Grids are divided into cells, and agents can only be on a particular cell, like pieces on a chess board. Continuous space, in contrast, allows agents to have any arbitrary position. Both grids and continuous spaces are frequently [toroidal](https://en.wikipedia.org/wiki/Toroidal_graph), meaning that the edges wrap around, with cells on the right edge connected to those on the left edge, and the top to the bottom. This prevents some cells having fewer neighbors than others, or agents being able to go off the edge of the environment.\n",
+    "Both grids and continuous spaces are frequently [toroidal](https://en.wikipedia.org/wiki/Toroidal_graph), meaning that the edges wrap around, with cells on the right edge connected to those on the left edge, and the top to the bottom. This prevents some cells having fewer neighbors than others, or agents being able to go off the edge of the environment. You can envision a torous by thinking of donut.\n",
     "\n",
-    "Let's add a simple spatial element to our model by putting our agents on a grid and make them walk around at random. Instead of giving their unit of money to any random agent, they'll give it to an agent on the same cell.\n",
+    "Mesa has two main types of grids: `SingleGrid` and `MultiGrid`. `SingleGrid` enforces at most one agent per cell; `MultiGrid` allows multiple agents to be in the same cell. We are going to use `MultiGrid` and then only exchange money with agents in the same cell.\n",
     "\n",
-    "Mesa has two main types of grids: `SingleGrid` and `MultiGrid`*. `SingleGrid` enforces at most one agent per cell; `MultiGrid` allows multiple agents to be in the same cell. Since we want agents to be able to share a cell, we use `MultiGrid`.\n",
+    "**Model-specific information:** Let's add a simple spatial element to our model by putting our agents on a grid and make them walk around at random. Instead of giving their unit of money to any random agent, they'll give it to an agent on the same cell. For the Money model multiple agents can be in the same spaces and since they are on a torus the agents on the left side can exchange money with agent on the right. Agents on the top can exchange with agents on the bottom.\n",
     "\n",
-    "*However there are more types of space to include `HexGrid`, `NetworkGrid`, and the previously mentioned `ContinuousSpace`. Similar to `mesa.time` context is retained with `mesa.space.[enter class]`. You can inspect the different classes at [mesa.space](https://github.com/projectmesa/mesa/blob/main/mesa/space.py)."
+    "**Code Implementation:** We get a random integer within the width and height of the grid space and the use Mesa's multigrid `place_agent` function to place the agent in the specified grid location."
    ]
   },
   {
@@ -566,16 +604,14 @@
     "class MoneyModel(mesa.Model):\n",
     "    \"\"\"A model with some number of agents.\"\"\"\n",
     "\n",
-    "    def __init__(self, N, width, height):\n",
-    "        super().__init__()\n",
-    "        self.num_agents = N\n",
+    "    def __init__(self, n, width, height, seed=None):\n",
+    "        super().__init__(seed=seed)\n",
+    "        self.num_agents = n\n",
     "        self.grid = mesa.space.MultiGrid(width, height, True)\n",
-    "        self.schedule = mesa.time.RandomActivation(self)\n",
     "\n",
     "        # Create agents\n",
     "        for _ in range(self.num_agents):\n",
     "            a = MoneyAgent(self)\n",
-    "            self.schedule.add(a)\n",
     "\n",
     "            # Add the agent to a random grid cell\n",
     "            x = self.random.randrange(self.grid.width)\n",
@@ -587,6 +623,8 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
+    "#### Moving in Mesa\n",
+    "\n",
     "Under the hood, each agent's position is stored in two ways: the agent is contained in the grid in the cell it is currently in, and the agent has a `pos` variable with an (x, y) coordinate tuple. The `place_agent` method adds the coordinate to the agent automatically.\n",
     "\n",
     "Now we need to add to the agents' behaviors, letting them move around and only give money to other agents in the same cell.\n",
@@ -601,7 +639,7 @@
     "        neighbors.append((x+dx, y+dy))\n",
     "```\n",
     "\n",
-    "But there's an even simpler way, using the grid's built-in `get_neighborhood` method, which returns all the neighbors of a given cell. This method can get two types of cell neighborhoods: [Moore](https://en.wikipedia.org/wiki/Moore_neighborhood) (includes all 8 surrounding squares), and [Von Neumann](https://en.wikipedia.org/wiki/Von_Neumann_neighborhood)(only up/down/left/right). It also needs an argument as to whether to include the center cell itself as one of the neighbors.\n",
+    "But there's an even simpler way, using the grid's built-in `get_neighborhood` method, which returns all the neighbors of a given cell. This method can get two types of cell neighborhoods: [Moore](https://en.wikipedia.org/wiki/Moore_neighborhood) (includes all 8 surrounding squares), and [Von neumann](https://en.wikipedia.org/wiki/Von_neumann_neighborhood)(only up/down/left/right). It also needs an argument as to whether to include the center cell itself as one of the neighbors.\n",
     "\n",
     "With that in mind, the agent's `move` method looks like this:\n",
     "\n",
@@ -680,22 +718,21 @@
     "class MoneyModel(mesa.Model):\n",
     "    \"\"\"A model with some number of agents.\"\"\"\n",
     "\n",
-    "    def __init__(self, N, width, height):\n",
-    "        super().__init__()\n",
-    "        self.num_agents = N\n",
+    "    def __init__(self, n, width, height,seed=None):\n",
+    "        super().__init__(seed=seed)\n",
+    "        self.num_agents = n\n",
     "        self.grid = mesa.space.MultiGrid(width, height, True)\n",
-    "        self.schedule = mesa.time.RandomActivation(self)\n",
+    "        \n",
     "        # Create agents\n",
     "        for _ in range(self.num_agents):\n",
     "            a = MoneyAgent(self)\n",
-    "            self.schedule.add(a)\n",
     "            # Add the agent to a random grid cell\n",
     "            x = self.random.randrange(self.grid.width)\n",
     "            y = self.random.randrange(self.grid.height)\n",
     "            self.grid.place_agent(a, (x, y))\n",
     "\n",
     "    def step(self):\n",
-    "        self.schedule.step()"
+    "        self.agents.shuffle_do(\"step\")"
    ]
   },
   {
@@ -720,7 +757,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Now let's use seaborn and numpy to visualize the number of agents residing in each cell. To do that, we create a numpy array of the same size as the grid, filled with zeros. Then we use the grid object's `coord_iter()` feature, which lets us loop over every cell in the grid, giving us each cell's positions and contents in turn."
+    "now let's use seaborn and numpy to visualize the number of agents residing in each cell. To do that, we create a numpy array of the same size as the grid, filled with zeros. Then we use the grid object's `coord_iter()` feature, which lets us loop over every cell in the grid, giving us each cell's positions and contents in turn."
    ]
   },
   {
@@ -733,25 +770,49 @@
     "for cell_content, (x, y) in model.grid.coord_iter():\n",
     "    agent_count = len(cell_content)\n",
     "    agent_counts[x][y] = agent_count\n",
-    "# Plot using seaborn, with a size of 5x5\n",
+    "# Plot using seaborn, with a visual size of 5x5\n",
     "g = sns.heatmap(agent_counts, cmap=\"viridis\", annot=True, cbar=False, square=True)\n",
-    "g.figure.set_size_inches(4, 4)\n",
-    "g.set(title=\"Number of agents on each cell of the grid\");"
+    "g.figure.set_size_inches(5, 5)\n",
+    "g.set(title=\"number of agents on each cell of the grid\");"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Challenge: Change the size of the grid"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Challenge: Change from multigrid to grid (only one agent per cell) "
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### Collecting Data\n",
+    "## Collecting Data\n",
     "\n",
-    "So far, at the end of every model run, we've had to go and write our own code to get the data out of the model. This has two problems: it isn't very efficient, and it only gives us end results. If we wanted to know the wealth of each agent at each step, we'd have to add that to the loop of executing steps, and figure out some way to store the data.\n",
+    "**Background:** So far, at the end of every model run, we've had to go and write our own code to get the data out of the model. This has two problems: it isn't very efficient, and it only gives us end results. If we wanted to know the wealth of each agent at each step, we'd have to add that to the loop of executing steps, and figure out some way to store the data.\n",
     "\n",
     "Since one of the main goals of agent-based modeling is generating data for analysis, Mesa provides a class which can handle data collection and storage for us and make it easier to analyze.\n",
     "\n",
-    "The data collector stores three categories of data: model-level variables, agent-level variables, and tables (which are a catch-all for everything else). Model- and agent-level variables are added to the data collector along with a function for collecting them. Model-level collection functions take a model object as an input, while agent-level collection functions take an agent object as an input. Both then return a value computed from the model or each agent at their current state. When the data collector’s `collect` method is called, with a model object as its argument, it applies each model-level collection function to the model, and stores the results in a dictionary, associating the current value with the current step of the model. Similarly, the method applies each agent-level collection function to each agent currently in the schedule, associating the resulting value with the step of the model, and the agent’s `unique_id`.\n",
+    "The data collector stores three categories of data: \n",
+    " - Model-level variables : Model-level collection functions take a model object as an input. Such as a function that computes a dynamic of the whole model (in this case we will compute a measure of wealth inequality based on all agents wealth)\n",
+    " - Agent-level variables: Agent-level collection functions take an agent object as an input and is typically the state of an agent attributes, in this case wealth.\n",
+    " - Tables (which are a catch-all for everything else). \n",
+    "\n",
+    "**Model-specific information:** We will collect two variables to show Mesa capabilities. At the model level, let's measure the model's [Gini Coefficient](https://en.wikipedia.org/wiki/Gini_coefficient), a measure of wealth inequality. At the agent level, we want to collect every agent's wealth at every step. \n",
     "\n",
-    "Let's add a DataCollector to the model with [`mesa.DataCollector`](https://github.com/projectmesa/mesa/blob/main/mesa/datacollection.py), and collect two variables. At the agent level, we want to collect every agent's wealth at every step. At the model level, let's measure the model's [Gini Coefficient](https://en.wikipedia.org/wiki/Gini_coefficient), a measure of wealth inequality."
+    "**Code implementation:**\n",
+    "Let's add a DataCollector to the model with [`mesa.DataCollector`](https://github.com/projectmesa/mesa/blob/main/mesa/datacollection.py), and collect the agents wealth and the gini coefficient at each time step. "
    ]
   },
   {
@@ -761,11 +822,11 @@
    "outputs": [],
    "source": [
     "def compute_gini(model):\n",
-    "    agent_wealths = [agent.wealth for agent in model.schedule.agents]\n",
+    "    agent_wealths = [agent.wealth for agent in model.agents]\n",
     "    x = sorted(agent_wealths)\n",
-    "    N = model.num_agents\n",
-    "    B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x))\n",
-    "    return 1 + (1 / N) - 2 * B\n",
+    "    n = model.num_agents\n",
+    "    B = sum(xi * (n - i) for i, xi in enumerate(x)) / (n * sum(x))\n",
+    "    return 1 + (1 / n) - 2 * B\n",
     "\n",
     "\n",
     "class MoneyAgent(mesa.Agent):\n",
@@ -792,7 +853,7 @@
     "            other.wealth += 1\n",
     "            self.wealth -= 1\n",
     "            if other == self:\n",
-    "                print(\"I JUST GAVE MONEY TO MYSELF HEHEHE!\")\n",
+    "                print(\"I JUST GAVE MOnEY TO MYSELF HEHEHE!\")\n",
     "\n",
     "    def step(self):\n",
     "        self.move()\n",
@@ -803,16 +864,14 @@
     "class MoneyModel(mesa.Model):\n",
     "    \"\"\"A model with some number of agents.\"\"\"\n",
     "\n",
-    "    def __init__(self, N, width, height):\n",
+    "    def __init__(self, n, width, height):\n",
     "        super().__init__()\n",
-    "        self.num_agents = N\n",
+    "        self.num_agents = n\n",
     "        self.grid = mesa.space.MultiGrid(width, height, True)\n",
-    "        self.schedule = mesa.time.RandomActivation(self)\n",
     "\n",
     "        # Create agents\n",
     "        for _ in range(self.num_agents):\n",
     "            a = MoneyAgent(self)\n",
-    "            self.schedule.add(a)\n",
     "            # Add the agent to a random grid cell\n",
     "            x = self.random.randrange(self.grid.width)\n",
     "            y = self.random.randrange(self.grid.height)\n",
@@ -824,7 +883,7 @@
     "\n",
     "    def step(self):\n",
     "        self.datacollector.collect(self)\n",
-    "        self.schedule.step()"
+    "        self.agents.shuffle_do(\"step\")"
    ]
   },
   {
@@ -833,7 +892,7 @@
    "source": [
     "At every step of the model, the datacollector will collect and store the model-level current Gini coefficient, as well as each agent's wealth, associating each with the current step.\n",
     "\n",
-    "We run the model just as we did above. Now is when an interactive session, especially via a Notebook, comes in handy: the DataCollector can export the data its collected as a pandas* DataFrame, for easy interactive analysis. \n",
+    "We run the model just as we did above. now is when an interactive session, especially via a notebook, comes in handy: the DataCollector can export the data its collected as a pandas* DataFrame, for easy interactive analysis. \n",
     "\n",
     "*If you are new to Python, please be aware that pandas is already installed as a dependency of Mesa and that [pandas](https://pandas.pydata.org/docs/) is a \"fast, powerful, flexible and easy to use open source data analysis and manipulation tool\". pandas is great resource to help analyze the data collected in your models."
    ]
@@ -856,6 +915,13 @@
     "To get the series of Gini coefficients as a pandas DataFrame:"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Visualizing a Model Data"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": null,
@@ -868,6 +934,13 @@
     "g.set(title=\"Gini Coefficient over Time\", ylabel=\"Gini Coefficient\");"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Visualizing an Agent Data"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -905,7 +978,7 @@
     "g.set(\n",
     "    title=\"Distribution of wealth at the end of simulation\",\n",
     "    xlabel=\"Wealth\",\n",
-    "    ylabel=\"Number of agents\",\n",
+    "    ylabel=\"number of agents\",\n",
     ");"
    ]
   },
@@ -913,7 +986,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Or to plot the wealth of a given agent (in this example, agent 14):"
+    "Or to plot the wealth of a given agent (in this example, agent 7):"
    ]
   },
   {
@@ -923,11 +996,11 @@
    "outputs": [],
    "source": [
     "# Get the wealth of agent 14 over time\n",
-    "one_agent_wealth = agent_wealth.xs(14, level=\"AgentID\")\n",
+    "one_agent_wealth = agent_wealth.xs(7, level=\"AgentID\")\n",
     "\n",
-    "# Plot the wealth of agent 14 over time\n",
+    "# Plot the wealth of agent 7 over time\n",
     "g = sns.lineplot(data=one_agent_wealth, x=\"Step\", y=\"Wealth\")\n",
-    "g.set(title=\"Wealth of agent 14 over time\");"
+    "g.set(title=\"Wealth of agent 7 over time\");"
    ]
   },
   {
@@ -1010,9 +1083,292 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### Batch Run\n",
+    "## AgentSet Functionality\n",
     "\n",
-    "Like we mentioned above, you usually won't run a model only once, but multiple times, with fixed parameters to find the overall distributions the model generates, and with varying parameters to analyze how they drive the model's outputs and behaviors. Instead of needing to write nested for-loops for each model, Mesa provides a [`batch_run`](https://github.com/projectmesa/mesa/blob/main/mesa/batchrunner.py) function which automates it for you.\n",
+    "**Background:** With Mesa's AgentSet approach users can also [manage agents](https://mesa.readthedocs.io/latest/overview.html#agentset-and-model-agents) in several ways. \n",
+    "\n",
+    "**Model-specific information:** We will show three agent management techniques just to demonstrate the capability\n",
+    "1. **Applying Methods** We will shuffle the agents and move them and then have them exchange money without reordering them\n",
+    "2. **Selecting** We will institute a policy that has the rich agents give money to the  poor agents\n",
+    "3. **GroupBy** We will group agents together based on wealth"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Applying Methods\n",
+    "\n",
+    "**Code Implementation** In this variation we accomplish the same process using the AgentSet features. We remove the step function entirely, shuffle the agent order have them execute the `move` function. Then we have them execute the `give_money` function to get a similar result as we saw in the Adding Space Section. "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class MoneyAgent(mesa.Agent):\n",
+    "    \"\"\"An agent with fixed initial wealth.\"\"\"\n",
+    "\n",
+    "    def __init__(self, model):\n",
+    "        super().__init__(model)\n",
+    "        self.wealth = 1\n",
+    "\n",
+    "    def move(self):\n",
+    "        possible_steps = self.model.grid.get_neighborhood(\n",
+    "            self.pos, moore=True, include_center=False\n",
+    "        )\n",
+    "        new_position = self.random.choice(possible_steps)\n",
+    "        self.model.grid.move_agent(self, new_position)\n",
+    "\n",
+    "    def give_money(self):\n",
+    "        if self.wealth > 0: \n",
+    "            cellmates = self.model.grid.get_cell_list_contents([self.pos])\n",
+    "            if len(cellmates) > 1:\n",
+    "                other_agent = self.random.choice(cellmates)\n",
+    "                other_agent.wealth += 1\n",
+    "                self.wealth -= 1\n",
+    "  \n",
+    "\n",
+    "class MoneyModel(mesa.Model):\n",
+    "    \"\"\"A model with some number of agents.\"\"\"\n",
+    "\n",
+    "    def __init__(self, n, width, height,seed=None):\n",
+    "        super().__init__(seed=seed)\n",
+    "        self.num_agents = n\n",
+    "        self.grid = mesa.space.MultiGrid(width, height, True)\n",
+    "        \n",
+    "        # Create agents\n",
+    "        for _ in range(self.num_agents):\n",
+    "            a = MoneyAgent(self)\n",
+    "            # Add the agent to a random grid cell\n",
+    "            x = self.random.randrange(self.grid.width)\n",
+    "            y = self.random.randrange(self.grid.height)\n",
+    "            self.grid.place_agent(a, (x, y))\n",
+    "\n",
+    "        self.datacollector = mesa.DataCollector(\n",
+    "            model_reporters={\"Gini\": compute_gini}, agent_reporters={\"Wealth\": \"wealth\"}\n",
+    "        )\n",
+    "\n",
+    "    def step(self):\n",
+    "        self.datacollector.collect(self)\n",
+    "        self.agents.shuffle_do(\"move\")\n",
+    "        self.agents.do(\"give_money\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Then we set up our model object and run it for 20 steps"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "model = MoneyModel(100, 10, 10)\n",
+    "for _ in range(20):\n",
+    "    model.step()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Then we plot our model result on the grid"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "agent_counts = np.zeros((model.grid.width, model.grid.height))\n",
+    "for cell_content, (x, y) in model.grid.coord_iter():\n",
+    "    agent_count = len(cell_content)\n",
+    "    agent_counts[x][y] = agent_count\n",
+    "# Plot using seaborn, with a visual size of 5x5\n",
+    "g = sns.heatmap(agent_counts, cmap=\"viridis\", annot=True, cbar=False, square=True)\n",
+    "g.figure.set_size_inches(5, 5)\n",
+    "g.set(title=\"Number of agents on each cell of the grid\");"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Selecting \n",
+    "\n",
+    "**Model-specific Information:** For this variation of the model we are going to institute a policy that rich agents give money to poor agent\n",
+    "\n",
+    "**Code Implementation:** We will use `agents.select` to separate the agents into rich and poor agents. If there are rich agents then they are the only ones who give money. "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class MoneyAgent(mesa.Agent):\n",
+    "    \"\"\"An agent with fixed initial wealth.\"\"\"\n",
+    "\n",
+    "    def __init__(self, model):\n",
+    "        super().__init__(model)\n",
+    "        self.wealth = 1\n",
+    "\n",
+    "    def give_money(self, poor_agents):\n",
+    "        if self.wealth > 0: \n",
+    "            other_agent = self.random.choice(poor_agents)\n",
+    "            other_agent.wealth += 1\n",
+    "            self.wealth -= 1\n",
+    "  \n",
+    "\n",
+    "class MoneyModel(mesa.Model):\n",
+    "    \"\"\"A model with some number of agents.\"\"\"\n",
+    "\n",
+    "    def __init__(self, n):\n",
+    "        super().__init__()\n",
+    "        self.num_agents = n\n",
+    "                \n",
+    "        # Create agents\n",
+    "        for _ in range(self.num_agents):\n",
+    "            a = MoneyAgent(self)\n",
+    "\n",
+    "        self.datacollector = mesa.DataCollector(\n",
+    "            model_reporters={\"Gini\": compute_gini}, agent_reporters={\"Wealth\": \"wealth\"}\n",
+    "        )\n",
+    "            \n",
+    "    def step(self):\n",
+    "        self.datacollector.collect(self)\n",
+    "        # Get lists of rich and poor agents\n",
+    "        rich_agents = model.agents.select(lambda a: a.wealth >= 3)\n",
+    "        poor_agents = model.agents.select(lambda a: a.wealth < 3)\n",
+    "        # When there is rich agents only have them give money to the poor agents\n",
+    "        if len(rich_agents) > 0: \n",
+    "            rich_agents.shuffle_do(\"give_money\", poor_agents)\n",
+    "        else: \n",
+    "            poor_agents.shuffle_do(\"give_money\", poor_agents)           "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We now run the model, collect the data, and plot the results."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "model = MoneyModel(100)\n",
+    "for _ in range(20):\n",
+    "    model.step()    \n",
+    "\n",
+    "\n",
+    "data = model.datacollector.get_agent_vars_dataframe()\n",
+    "# Use seaborn\n",
+    "g = sns.histplot(data[\"Wealth\"], discrete=True)\n",
+    "g.set(title=\"Wealth distribution\", xlabel=\"Wealth\", ylabel=\"number of agents\");"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Group By \n",
+    "\n",
+    "**Model-specific implementation:** In this case we will give agents an attribute of enthnicity of Green, Blue or Mixed. Green and Blue agents only give money to their ethnicity while Mixed can give money to anyone.\n",
+    "\n",
+    "**Code Implementation**: Using `groupby` we will execute the above logic in our code passing a list of grouped agents into our `give_money` function.  To ensure we can plot wealth by group we also need to add ethnicity to our datacollector. "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class MoneyAgent(mesa.Agent):\n",
+    "    \"\"\"An agent with fixed initial wealth.\"\"\"\n",
+    "\n",
+    "    def __init__(self, model, ethnicity):\n",
+    "        super().__init__(model)\n",
+    "        self.wealth = 1\n",
+    "        self.ethnicity = ethnicity\n",
+    "\n",
+    "    def give_money(self, similars):\n",
+    "        if self.wealth > 0: \n",
+    "            other_agent = self.random.choice(similars)\n",
+    "            other_agent.wealth += 1\n",
+    "            self.wealth -= 1\n",
+    "  \n",
+    "\n",
+    "class MoneyModel(mesa.Model):\n",
+    "    \"\"\"A model with some number of agents.\"\"\"\n",
+    "\n",
+    "    def __init__(self, n):\n",
+    "        super().__init__()\n",
+    "        self.num_agents = n\n",
+    "                \n",
+    "        # Create a list of our different ethnicities\n",
+    "        ethnicities = [\"Green\", \"Blue\", \"Mixed\"]\n",
+    "        \n",
+    "        # Create agents\n",
+    "        for _ in range(self.num_agents):\n",
+    "            a = MoneyAgent(self, self.random.choice(ethnicities))\n",
+    "\n",
+    "        self.datacollector = mesa.DataCollector(\n",
+    "            model_reporters={\"Gini\": compute_gini}, agent_reporters={\"Wealth\": \"wealth\", \"Ethnicity\":\"ethnicity\"}\n",
+    "        )\n",
+    "            \n",
+    "    def step(self):\n",
+    "        self.datacollector.collect(self)\n",
+    "        # groupby returns a dictionary of the different ethnicities with a list of agents\n",
+    "        grouped_agents = grouped_agents = model.agents.groupby(\"ethnicity\")\n",
+    "\n",
+    "        for ethnic, similars in grouped_agents: \n",
+    "            if ethnic != \"Mixed\": \n",
+    "                similars.shuffle_do(\"give_money\", similars)\n",
+    "            else: \n",
+    "                similars.shuffle_do(\"give_money\", self.agents) # This allows mixed to trade with anyone \n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Run the model\n",
+    "model = MoneyModel(100)\n",
+    "for _ in range(20):\n",
+    "    model.step()\n",
+    "  \n",
+    "# get the data\n",
+    "data = model.datacollector.get_agent_vars_dataframe()\n",
+    "# assign histogram colors\n",
+    "palette = {'Green': 'green', 'Blue': 'blue', 'Mixed': 'purple'} \n",
+    "sns.histplot(data=data, x='Wealth', hue='Ethnicity',discrete=True, palette=palette)\n",
+    "g.set(title=\"Wealth distribution\", xlabel=\"Wealth\", ylabel=\"number of agents\");"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Batch Run\n",
+    "\n",
+    "Like we mentioned above, you usually won't run a model only once, but multiple times, with fixed parameters to find the overall distributions the model generates, and with varying parameters to analyze how they drive the model's outputs and behaviors. This is commonly referred to as parameter sweeps. Instead of needing to write nested for-loops for each model, Mesa provides a [`batch_run`](https://github.com/projectmesa/mesa/blob/main/mesa/batchrunner.py) function which automates it for you.\n",
     "\n",
     "The batch runner also requires an additional variable `self.running` for the MoneyModel class. This variable enables conditional shut off of the model once a condition is met. In this example it will be set as True indefinitely."
    ]
@@ -1034,27 +1390,25 @@
    "outputs": [],
    "source": [
     "def compute_gini(model):\n",
-    "    agent_wealths = [agent.wealth for agent in model.schedule.agents]\n",
+    "    agent_wealths = [agent.wealth for agent in model.agents]\n",
     "    x = sorted(agent_wealths)\n",
-    "    N = model.num_agents\n",
-    "    B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x))\n",
-    "    return 1 + (1 / N) - 2 * B\n",
+    "    n = model.num_agents\n",
+    "    B = sum(xi * (n - i) for i, xi in enumerate(x)) / (n * sum(x))\n",
+    "    return 1 + (1 / n) - 2 * B\n",
     "\n",
     "\n",
     "class MoneyModel(mesa.Model):\n",
     "    \"\"\"A model with some number of agents.\"\"\"\n",
     "\n",
-    "    def __init__(self, N, width, height):\n",
-    "        super().__init__()\n",
-    "        self.num_agents = N\n",
+    "    def __init__(self, n, width, height, seed=None):\n",
+    "        super().__init__(seed=seed)\n",
+    "        self.num_agents = n\n",
     "        self.grid = mesa.space.MultiGrid(width, height, True)\n",
-    "        self.schedule = mesa.time.RandomActivation(self)\n",
     "        self.running = True\n",
     "\n",
     "        # Create agents\n",
     "        for _ in range(self.num_agents):\n",
     "            a = MoneyAgent(self)\n",
-    "            self.schedule.add(a)\n",
     "            # Add the agent to a random grid cell\n",
     "            x = self.random.randrange(self.grid.width)\n",
     "            y = self.random.randrange(self.grid.height)\n",
@@ -1067,7 +1421,8 @@
     "\n",
     "    def step(self):\n",
     "        self.datacollector.collect(self)\n",
-    "        self.schedule.step()\n",
+    "        self.agents.shuffle_do(\"move\")\n",
+    "        self.agents.shuffle_do(\"give_money\")\n",
     "\n",
     "\n",
     "class MoneyAgent(mesa.Agent):\n",
@@ -1093,13 +1448,6 @@
     "            self.wealth -= 1\n",
     "            self.steps_not_given = 0\n",
     "        else:\n",
-    "            self.steps_not_given += 1\n",
-    "\n",
-    "    def step(self):\n",
-    "        self.move()\n",
-    "        if self.wealth > 0:\n",
-    "            self.give_money()\n",
-    "        else:\n",
     "            self.steps_not_given += 1"
    ]
   },
@@ -1107,23 +1455,23 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "#### Batch run\n",
+    "#### Batch run parameters\n",
     "\n",
     "We call `batch_run` with the following arguments:\n",
     "\n",
     "* `model_cls`\n",
     "  The model class that is used for the batch run.\n",
     "* `parameters`\n",
-    "  A dictionary containing all the parameters of the model class and desired values to use for the batch run as key-value pairs. Each value can either be fixed ( e.g. `{\"height\": 10, \"width\": 10}`) or an iterable (e.g. `{\"N\": range(10, 500, 10)}`). `batch_run` will then generate all possible parameter combinations based on this dictionary and run the model `iterations` times for each combination.\n",
+    "  A dictionary containing all the parameters of the model class and desired values to use for the batch run as key-value pairs. Each value can either be fixed ( e.g. `{\"height\": 10, \"width\": 10}`) or an iterable (e.g. `{\"n\": range(10, 500, 10)}`). `batch_run` will then generate all possible parameter combinations based on this dictionary and run the model `iterations` times for each combination.\n",
     "* `number_processes`\n",
     "  If not specified, defaults to 1. Set it to `None` to use all the available processors.\n",
-    "  Note: Multiprocessing does make debugging challenging. If your parameter sweeps are resulting in unexpected errors set `number_processes=1`.\n",
+    "  note: Multiprocessing does make debugging challenging. If your parameter sweeps are resulting in unexpected errors set `number_processes=1`.\n",
     "* `iterations`\n",
     "  The number of iterations to run each parameter combination for. Optional. If not specified, defaults to 1.\n",
     "* `data_collection_period`\n",
     "  The length of the period (number of steps) after which the model and agent reporters collect data. Optional. If not specified, defaults to -1, i.e. only at the end of each episode.\n",
     "* `max_steps`\n",
-    "  The maximum number of time steps after which the model halts. An episode does either end when `self.running` of the model class is set to `False` or when `model.schedule.steps == max_steps` is reached. Optional. If not specified, defaults to 1000.\n",
+    "  The maximum number of time steps after which the model halts. An episode does either end when `self.running` of the model class is set to `False` or when `model.steps == max_steps` is reached. Optional. If not specified, defaults to 1000.\n",
     "* `display_progress`\n",
     "  Display the batch run progress. Optional. If not specified, defaults to `True`."
    ]
@@ -1141,14 +1489,14 @@
     "\n",
     "**Important:** Since for the latter changes at each time step might be interesting, we set `data_collection_period=1`. By default, it only collects data at the end of each episode.\n",
     "\n",
-    "Note: The total number of runs is 245 (= 49 different populations * 5 iterations per population). However, the resulting list of dictionaries will be of length 6186250 (= 250 average agents per population * 49 different populations * 5 iterations per population * 101 steps per iteration). "
+    "note: The total number of runs is 245 (= 49 different populations * 5 iterations per population). However, the resulting list of dictionaries will be of length 6186250 (= 250 average agents per population * 49 different populations * 5 iterations per population * 101 steps per iteration). "
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "**Note for Windows OS users:** If you are running this tutorial in Jupyter, make sure that you set `number_processes = 1` (single process). If `number_processes` is greater than 1, it is less straightforward to set up. You can read [Mesa's how-to guide](https://github.com/projectmesa/mesa/blob/main/docs/howto.md), in 'Using multi-process `batch_run` on Windows' section for how to do it."
+    "**note for Windows OS users:** If you are running this tutorial in Jupyter, make sure that you set `number_processes = 1` (single process). If `number_processes` is greater than 1, it is less straightforward to set up. You can read [Mesa's how-to guide](https://github.com/projectmesa/mesa/blob/main/docs/howto.md), in 'Using multi-process `batch_run` on Windows' section for how to do it."
    ]
   },
   {
@@ -1157,7 +1505,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "params = {\"width\": 10, \"height\": 10, \"N\": range(5, 100, 5)}\n",
+    "params = {\"width\": 10, \"height\": 10, \"n\": range(5, 100, 5)}\n",
     "\n",
     "results = mesa.batch_run(\n",
     "    MoneyModel,\n",
@@ -1177,6 +1525,13 @@
     "To further analyze the return of the `batch_run` function, we convert the list of dictionaries to a Pandas DataFrame and print its keys."
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### Batch Run Analysis and Visualization"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": null,
@@ -1191,7 +1546,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "First, we want to take a closer look at how the Gini coefficient at the end of each episode changes as we increase the size of the population. For this, we filter our results to only contain the data of one agent (the Gini coefficient will be the same for the entire population at any time) at the 100th step of each episode and then scatter-plot the values for the Gini coefficient over the the number of agents. Notice there are five values for each population size since we set `iterations=5` when calling the batch run."
+    "First, we want to take a closer look at how the Gini coefficient at the end of each episode changes as we increase the size of the population. For this, we filter our results to only contain the data of one agent (the Gini coefficient will be the same for the entire population at any time) at the 100th step of each episode and then scatter-plot the values for the Gini coefficient over the the number of agents. notice there are five values for each population size since we set `iterations=5` when calling the batch run."
    ]
   },
   {
@@ -1201,13 +1556,13 @@
    "outputs": [],
    "source": [
     "# Filter the results to only contain the data of one agent (the Gini coefficient will be the same for the entire population at any time) at the 100th step of each episode\n",
-    "results_filtered = results_df[(results_df.AgentID == 0) & (results_df.Step == 100)]\n",
-    "results_filtered[[\"iteration\", \"N\", \"Gini\"]].reset_index(\n",
+    "results_filtered = results_df[(results_df.AgentID == 1) & (results_df.Step == 100)]\n",
+    "results_filtered[[\"iteration\", \"n\", \"Gini\"]].reset_index(\n",
     "    drop=True\n",
     ").head()  # Create a scatter plot\n",
-    "g = sns.scatterplot(data=results_filtered, x=\"N\", y=\"Gini\")\n",
+    "g = sns.scatterplot(data=results_filtered, x=\"n\", y=\"Gini\")\n",
     "g.set(\n",
-    "    xlabel=\"Number of agents\",\n",
+    "    xlabel=\"number of agents\",\n",
     "    ylabel=\"Gini coefficient\",\n",
     "    title=\"Gini coefficient vs. number of agents\",\n",
     ");"
@@ -1216,7 +1571,10 @@
   {
    "cell_type": "markdown",
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "source": [
     "We can create different kinds of plot from this filtered DataFrame. For example, a point plot with error bars."
@@ -1226,15 +1584,18 @@
    "cell_type": "code",
    "execution_count": null,
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "outputs": [],
    "source": [
     "# Create a point plot with error bars\n",
-    "g = sns.pointplot(data=results_filtered, x=\"N\", y=\"Gini\", linestyle='none')\n",
+    "g = sns.pointplot(data=results_filtered, x=\"n\", y=\"Gini\", linestyle='None')\n",
     "g.figure.set_size_inches(8, 4)\n",
     "g.set(\n",
-    "    xlabel=\"Number of agents\",\n",
+    "    xlabel=\"number of agents\",\n",
     "    ylabel=\"Gini coefficient\",\n",
     "    title=\"Gini coefficient vs. number of agents\",\n",
     ");"
@@ -1247,7 +1608,7 @@
     "Second, we want to display the agent's wealth at each time step of one specific episode. To do this, we again filter our large data frame, this time with a fixed number of agents and only for a specific iteration of that population.\n",
     "To print the results, we convert the filtered data frame to a string specifying the desired columns to print. \n",
     "\n",
-    "Pandas has built-in functions to convert to a lot of different data formats. For example, to display as a table in a Jupyter Notebook, we can use the `to_html()` function which takes the same arguments as `to_string()` (see commented lines)."
+    "Pandas has built-in functions to convert to a lot of different data formats. For example, to display as a table in a Jupyter notebook, we can use the `to_html()` function which takes the same arguments as `to_string()` (see commented lines)."
    ]
   },
   {
@@ -1257,14 +1618,14 @@
    "outputs": [],
    "source": [
     "# First, we filter the results\n",
-    "one_episode_wealth = results_df[(results_df.N == 10) & (results_df.iteration == 2)]\n",
+    "one_episode_wealth = results_df[(results_df.n == 10) & (results_df.iteration == 2)]\n",
     "# Then, print the columns of interest of the filtered data frame\n",
     "print(\n",
     "    one_episode_wealth.to_string(\n",
     "        index=False, columns=[\"Step\", \"AgentID\", \"Wealth\"], max_rows=10\n",
     "    )\n",
     ")\n",
-    "# For a prettier display we can also convert the data frame to html, uncomment to test in a Jupyter Notebook\n",
+    "# For a prettier display we can also convert the data frame to html, uncomment to test in a Jupyter notebook\n",
     "# from IPython.display import display, HTML\n",
     "# display(HTML(one_episode_wealth.to_html(index=False, columns=['Step', 'AgentID', 'Wealth'], max_rows=25)))"
    ]
@@ -1283,7 +1644,7 @@
    "outputs": [],
    "source": [
     "results_one_episode = results_df[\n",
-    "    (results_df.N == 10) & (results_df.iteration == 1) & (results_df.AgentID == 0)\n",
+    "    (results_df.n == 10) & (results_df.iteration == 1) & (results_df.AgentID == 1)\n",
     "]\n",
     "print(results_one_episode.to_string(index=False, columns=[\"Step\", \"Gini\"], max_rows=10))"
    ]
@@ -1291,22 +1652,34 @@
   {
    "cell_type": "markdown",
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "source": [
     "### Analyzing model reporters: Comparing 5 scenarios\n",
-    "Other insights might be gathered when we compare the Gini coefficient of different scenarios. For example, we can compare the Gini coefficient of a population with 25 agents to the Gini coefficient of a population with 400 agents. While doing this, we increase the number of iterations to 25 to get a better estimate of the Gini coefficient for each population size and get usable error estimations."
+    "Other insights might be gathered when we compare the Gini coefficient of different scenarios. For example, we can compare the Gini coefficient of a population with 25 agents to the Gini coefficient of a population with 400 agents. While doing this, we increase the number of iterations to 25 to get a better estimate of the Gini coefficient for each population size and get usable error estimations.\n",
+    "\n",
+    "As we look varying the parameters to see the impact on model outcomes, it is critical to again point that users can set the random seed. Due to the often inherent randomness with ABMs the seed becomes crucial for: \n",
+    "- **Reproducibility** - Being able to replicate the ABM results\n",
+    "- **Sensitivity Analysis** - Identifying how sensitive/robust your model results are to random fluctuations\n",
+    "\n",
+    "Treating the seed as an additional parameter and running numerous scenarios allows us to see the impact of randomness on this model. "
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "outputs": [],
    "source": [
-    "params = {\"width\": 10, \"height\": 10, \"N\": [5, 10, 20, 40, 80]}\n",
+    "params = {\"seed\":None,\"width\": 10, \"height\": 10, \"n\": [5, 10, 20, 40, 80]}\n",
     "\n",
     "results_5s = mesa.batch_run(\n",
     "    MoneyModel,\n",
@@ -1325,12 +1698,15 @@
    "cell_type": "code",
    "execution_count": null,
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "outputs": [],
    "source": [
     "# Again filter the results to only contain the data of one agent (the Gini coefficient will be the same for the entire population at any time)\n",
-    "results_5s_df_filtered = results_5s_df[(results_5s_df.AgentID == 0)]\n",
+    "results_5s_df_filtered = results_5s_df[(results_5s_df.AgentID == 1)]\n",
     "results_5s_df_filtered.head(3)"
    ]
   },
@@ -1338,7 +1714,10 @@
    "cell_type": "code",
    "execution_count": null,
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "outputs": [],
    "source": [
@@ -1347,7 +1726,7 @@
     "    data=results_5s_df,\n",
     "    x=\"Step\",\n",
     "    y=\"Gini\",\n",
-    "    hue=\"N\",\n",
+    "    hue=\"n\",\n",
     "    errorbar=(\"ci\", 95),\n",
     "    palette=\"tab10\",\n",
     ")\n",
@@ -1359,19 +1738,35 @@
   {
    "cell_type": "markdown",
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "source": [
     "In this case it looks like the Gini coefficient increases slower for smaller populations. This can be because of different things, either because the Gini coefficient is a measure of inequality and the smaller the population, the more likely it is that the agents are all in the same wealth class, or because there are less interactions between agents in smaller populations, which means that the wealth of an agent is less likely to change."
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Challenge: Treat the seed as a parameter and see the impact on the Gini Coefficient. \n",
+    "# You can also plot the seeds against the Gini Coefficient by changing the \"hue\" parameter in sns.lineplot function. "
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "source": [
-    "### Analyzing agent reporters\n",
+    "### Analyzing agent reporters: Comparing 5 scenarios\n",
     "From the agents we collected the wealth and the number of consecutive rounds without a transaction. We can compare the 5 different population sizes by plotting the average number of consecutive rounds without a transaction for each population size.\n",
     "\n",
     "Note that we're aggregating multiple times here: First we take the average of all agents for each single replication. Then we plot the averages for all replications, with the error band showing the 95% confidence interval of that first average (over all agents). So this error band is representing the uncertainty of the mean value of the number of consecutive rounds without a transaction for each population size."
@@ -1381,13 +1776,16 @@
    "cell_type": "code",
    "execution_count": null,
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "outputs": [],
    "source": [
     "# Calculate the mean of the wealth and the number of consecutive rounds for all agents in each episode\n",
     "agg_results_df = (\n",
-    "    results_5s_df.groupby([\"iteration\", \"N\", \"Step\"])\n",
+    "    results_5s_df.groupby([\"iteration\", \"n\", \"Step\"])\n",
     "    .agg({\"Wealth\": \"mean\", \"Steps_not_given\": \"mean\"})\n",
     "    .reset_index()\n",
     ")\n",
@@ -1398,13 +1796,16 @@
    "cell_type": "code",
    "execution_count": null,
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "outputs": [],
    "source": [
     "# Create a line plot with error bars\n",
     "g = sns.lineplot(\n",
-    "    data=agg_results_df, x=\"Step\", y=\"Steps_not_given\", hue=\"N\", palette=\"tab10\"\n",
+    "    data=agg_results_df, x=\"Step\", y=\"Steps_not_given\", hue=\"n\", palette=\"tab10\"\n",
     ")\n",
     "g.figure.set_size_inches(8, 4)\n",
     "g.set(\n",
@@ -1416,7 +1817,10 @@
   {
    "cell_type": "markdown",
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "source": [
     "It can be clearly seen that the lower the number of agents, the higher the number of consecutive rounds without a transaction. This is because the agents have fewer interactions with each other and therefore the wealth of an agent is less likely to change."
@@ -1425,10 +1829,13 @@
   {
    "cell_type": "markdown",
    "metadata": {
-    "collapsed": false
+    "collapsed": false,
+    "jupyter": {
+     "outputs_hidden": false
+    }
    },
    "source": [
-    "#### General steps for analyzing results\n",
+    "### General steps for analyzing results\n",
     "\n",
     "Many other analysis are possible based on the policies, scenarios and uncertainties that you might be interested in. In general, you can follow these steps to do your own analysis:\n",
     "\n",
@@ -1441,11 +1848,29 @@
     "7. Plot the data and analyze the results."
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Challenge update the model, conduct a batch run with a parameter sweep, and visualize your results"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Next Steps\n",
+    "\n",
+    "Check out the [visualization tutorial](https://mesa.readthedocs.io/latest/tutorials/visualization_tutorial.html) on how to build interactive dashboards for your models."
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### Happy Modeling!\n",
+    "## Happy Modeling!\n",
     "\n",
     "This document is a work in progress.  If you see any errors, exclusions or have any problems please contact [us](https://github.com/projectmesa/mesa/issues)."
    ]
@@ -1477,7 +1902,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.10.12"
+   "version": "3.12.5"
   },
   "widgets": {
    "state": {},
diff --git a/docs/tutorials/visualization_tutorial.ipynb b/docs/tutorials/visualization_tutorial.ipynb
index 1ae055c2..0957f6b6 100644
--- a/docs/tutorials/visualization_tutorial.ipynb
+++ b/docs/tutorials/visualization_tutorial.ipynb
@@ -34,21 +34,14 @@
    ]
   },
   {
-   "cell_type": "code",
-   "execution_count": null,
+   "cell_type": "raw",
    "metadata": {
     "tags": []
    },
-   "outputs": [],
    "source": [
-    "# Install and import the latest Mesa pre-release version\n",
-    "%pip install --quiet --upgrade --pre mesa\n",
-    "import mesa\n",
-    "print(f\"Mesa version: {mesa.__version__}\")\n",
-    "\n",
-    "# You can either define the BoltzmannWealthModel (aka MoneyModel) or install it\n",
-    "\n",
-    "from mesa.examples import BoltzmannWealthModel"
+    "# Install and import the latest Mesa pre-release version; convert cell type from raw to code\n",
+    "%pip install solara # Prevents from installing solara pre-release\n",
+    "%pip install --quiet --upgrade --pre mesa\n"
    ]
   },
   {
@@ -58,6 +51,29 @@
     "Mesa's grid visualizer works by looping over every cell in a grid, and generating a portrayal for every agent it finds. A portrayal is a dictionary (which can easily be turned into a JSON object) which tells Matplotlib the color and size of the scatterplot markers (each signifying an agent). The only thing we need to provide is a function which takes an agent, and returns a portrayal dictionary. Here's the simplest one: it'll draw each agent as a blue, filled circle, with a radius size of 50."
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Part 1 - Basic Dashboard\n",
+    "\n",
+    "**Note:**  Due to the computational cost of running multiple dashboards it is recommended that at the end of each part you restart your kernel and then only run the the cells in that portion of the tutorial (e.g. Part 1). Each portion is entirely self contained."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import mesa\n",
+    "print(f\"Mesa version: {mesa.__version__}\")\n",
+    "\n",
+    "from mesa.visualization import SolaraViz, make_plot_measure, make_space_matplotlib\n",
+    "# Import the local MoneyModel.py\n",
+    "from MoneyModel import MoneyModel\n"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": null,
@@ -122,10 +138,8 @@
    },
    "outputs": [],
    "source": [
-    "from mesa.visualization import SolaraViz, make_plot_measure, make_space_matplotlib\n",
-    "\n",
     "# Create initial model instance\n",
-    "model1 = BoltzmannWealthModel(50, 10, 10)\n",
+    "model1 = MoneyModel(50, 10, 10)\n",
     "\n",
     "SpaceGraph = make_space_matplotlib(agent_portrayal)\n",
     "GiniPlot = make_plot_measure(\"Gini\")\n",
@@ -144,7 +158,10 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "#### Changing the agents\n",
+    "## Part 2 - Dynamic Agent Representation \n",
+    "\n",
+    "Due to the computational cost of running multiple dashboards it is recommended that at the end of each part you restart your kernel and then only run the import cell and the cells in that portion of the part of the tutorial (e.g. Part 2)\n",
+    "\n",
     "\n",
     "In the visualization above, all we could see is the agents moving around -- but not how much money they had, or anything else of interest. Let's change it so that agents who are broke (wealth 0) are drawn in red, smaller. (TODO: Currently, we can't predict the drawing order of the circles, so a broke agent may be overshadowed by a wealthy agent. We should fix this by doing a hollow circle instead)\n",
     "In addition to size and color, an agent's shape can also be customized when using the default drawer. The allowed values for shapes can be found [here](https://matplotlib.org/stable/api/markers_api.html).\n",
@@ -152,6 +169,20 @@
     "To do this, we go back to our `agent_portrayal` code and add some code to change the portrayal based on the agent properties and launch the server again."
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import mesa\n",
+    "print(f\"Mesa version: {mesa.__version__}\")\n",
+    "\n",
+    "from mesa.visualization import SolaraViz, make_plot_measure, make_space_matplotlib\n",
+    "# Import the local MoneyModel.py\n",
+    "from MoneyModel import MoneyModel\n"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": null,
@@ -164,7 +195,20 @@
     "    if agent.wealth > 0:\n",
     "        size = 50\n",
     "        color = \"tab:blue\"\n",
-    "    return {\"size\": size, \"color\": color}"
+    "    return {\"size\": size, \"color\": color}\n",
+    "\n",
+    "model_params = {\n",
+    "    \"n\": {\n",
+    "        \"type\": \"SliderInt\",\n",
+    "        \"value\": 50,\n",
+    "        \"label\": \"Number of agents:\",\n",
+    "        \"min\": 10,\n",
+    "        \"max\": 100,\n",
+    "        \"step\": 1,\n",
+    "    },\n",
+    "    \"width\": 10,\n",
+    "    \"height\": 10,\n",
+    "}"
    ]
   },
   {
@@ -173,6 +217,12 @@
    "metadata": {},
    "outputs": [],
    "source": [
+    "# Create initial model instance\n",
+    "model1 = MoneyModel(50, 10, 10)\n",
+    "\n",
+    "SpaceGraph = make_space_matplotlib(agent_portrayal)\n",
+    "GiniPlot = make_plot_measure(\"Gini\")\n",
+    "\n",
     "page = SolaraViz(\n",
     "    model1,\n",
     "    components=[SpaceGraph, GiniPlot],\n",
@@ -187,7 +237,9 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "### Building your own visualization component\n",
+    "## Part 3 - Custom Components \n",
+    "\n",
+    "Due to the computational cost of running multiple dashboards it is recommended that at the end of each part you restart your kernel and then only run the import cell and the cells in that portion of the part of the tutorial (e.g. Part 3)\n",
     "\n",
     "**Note:** This section is for users who have a basic familiarity with Python's Matplotlib plotting library.\n",
     "\n",
@@ -198,6 +250,63 @@
     "**Note:** Due to the way solara works we need to trigger an update whenever the underlying model changes. For this you need to register an update counter with every component."
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import mesa\n",
+    "print(f\"Mesa version: {mesa.__version__}\")\n",
+    "\n",
+    "from mesa.visualization import SolaraViz, make_plot_measure, make_space_matplotlib\n",
+    "# Import the local MoneyModel.py\n",
+    "from MoneyModel import MoneyModel\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def agent_portrayal(agent):\n",
+    "    size = 10\n",
+    "    color = \"tab:red\"\n",
+    "    if agent.wealth > 0:\n",
+    "        size = 50\n",
+    "        color = \"tab:blue\"\n",
+    "    return {\"size\": size, \"color\": color}\n",
+    "\n",
+    "model_params = {\n",
+    "    \"n\": {\n",
+    "        \"type\": \"SliderInt\",\n",
+    "        \"value\": 50,\n",
+    "        \"label\": \"Number of agents:\",\n",
+    "        \"min\": 10,\n",
+    "        \"max\": 100,\n",
+    "        \"step\": 1,\n",
+    "    },\n",
+    "    \"width\": 10,\n",
+    "    \"height\": 10,\n",
+    "}"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from mesa.visualization import SolaraViz, make_plot_measure, make_space_matplotlib\n",
+    "\n",
+    "# Create initial model instance\n",
+    "model1 = MoneyModel(50, 10, 10)\n",
+    "\n",
+    "SpaceGraph = make_space_matplotlib(agent_portrayal)\n",
+    "GiniPlot = make_plot_measure(\"Gini\")"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": null,
@@ -222,20 +331,32 @@
     "    solara.FigureMatplotlib(fig)"
    ]
   },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "In a notebook environment we can directly display the visualization by calling it with the model instance"
-   ]
-  },
   {
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
-    "Histogram(model1)"
+    "def agent_portrayal(agent):\n",
+    "    size = 10\n",
+    "    color = \"tab:red\"\n",
+    "    if agent.wealth > 0:\n",
+    "        size = 50\n",
+    "        color = \"tab:blue\"\n",
+    "    return {\"size\": size, \"color\": color}\n",
+    "\n",
+    "model_params = {\n",
+    "    \"n\": {\n",
+    "        \"type\": \"SliderInt\",\n",
+    "        \"value\": 50,\n",
+    "        \"label\": \"Number of agents:\",\n",
+    "        \"min\": 10,\n",
+    "        \"max\": 100,\n",
+    "        \"step\": 1,\n",
+    "    },\n",
+    "    \"width\": 10,\n",
+    "    \"height\": 10,\n",
+    "}"
    ]
   },
   {
@@ -261,6 +382,22 @@
     "page"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "You can even run the visuals independently by calling it with the model instance"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "Histogram(model1)"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -273,7 +410,7 @@
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "Python 3",
+   "display_name": "Python 3 (ipykernel)",
    "language": "python",
    "name": "python3"
   },
@@ -287,7 +424,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.9.6"
+   "version": "3.12.5"
   },
   "nbsphinx": {
    "execute": "never"
diff --git a/mesa/agent.py b/mesa/agent.py
index 200dab6f..6b6c3d2e 100644
--- a/mesa/agent.py
+++ b/mesa/agent.py
@@ -21,6 +21,8 @@ from random import Random
 # mypy
 from typing import TYPE_CHECKING, Any, Literal, overload
 
+import numpy as np
+
 if TYPE_CHECKING:
     # We ensure that these are not imported during runtime to prevent cyclic
     # dependency.
@@ -85,9 +87,14 @@ class Agent:
 
     @property
     def random(self) -> Random:
-        """Return a seeded rng."""
+        """Return a seeded stdlib rng."""
         return self.model.random
 
+    @property
+    def rng(self) -> np.random.Generator:
+        """Return a seeded np.random rng."""
+        return self.model.rng
+
 
 class AgentSet(MutableSet, Sequence):
     """A collection class that represents an ordered set of agents within an agent-based model (ABM).
@@ -294,15 +301,17 @@ class AgentSet(MutableSet, Sequence):
 
         It's a fast, optimized version of calling shuffle() followed by do().
         """
-        agents = list(self._agents.keys())
-        self.random.shuffle(agents)
+        weakrefs = list(self._agents.keyrefs())
+        self.random.shuffle(weakrefs)
 
         if isinstance(method, str):
-            for agent in agents:
-                getattr(agent, method)(*args, **kwargs)
+            for ref in weakrefs:
+                if (agent := ref()) is not None:
+                    getattr(agent, method)(*args, **kwargs)
         else:
-            for agent in agents:
-                method(agent, *args, **kwargs)
+            for ref in weakrefs:
+                if (agent := ref()) is not None:
+                    method(agent, *args, **kwargs)
 
         return self
 
diff --git a/mesa/examples/basic/boid_flockers/app.py b/mesa/examples/basic/boid_flockers/app.py
index 02fde323..482d582b 100644
--- a/mesa/examples/basic/boid_flockers/app.py
+++ b/mesa/examples/basic/boid_flockers/app.py
@@ -1,7 +1,6 @@
+from mesa.examples.basic.boid_flockers.model import BoidFlockers
 from mesa.visualization import Slider, SolaraViz, make_space_matplotlib
 
-from .model import BoidFlockers
-
 
 def boid_draw(agent):
     if not agent.neighbors:  # Only for the first Frame
diff --git a/mesa/examples/basic/boid_flockers/model.py b/mesa/examples/basic/boid_flockers/model.py
index 2bc59216..a05caab1 100644
--- a/mesa/examples/basic/boid_flockers/model.py
+++ b/mesa/examples/basic/boid_flockers/model.py
@@ -7,8 +7,7 @@ Uses numpy arrays to represent vectors.
 import numpy as np
 
 import mesa
-
-from .agents import Boid
+from mesa.examples.basic.boid_flockers.agents import Boid
 
 
 class BoidFlockers(mesa.Model):
diff --git a/mesa/examples/basic/boltzmann_wealth_model/app.py b/mesa/examples/basic/boltzmann_wealth_model/app.py
index ebbb1ab3..7e3f41e6 100644
--- a/mesa/examples/basic/boltzmann_wealth_model/app.py
+++ b/mesa/examples/basic/boltzmann_wealth_model/app.py
@@ -1,11 +1,10 @@
+from mesa.examples.basic.boltzmann_wealth_model.model import BoltzmannWealthModel
 from mesa.visualization import (
     SolaraViz,
     make_plot_measure,
     make_space_matplotlib,
 )
 
-from .model import BoltzmannWealthModel
-
 
 def agent_portrayal(agent):
     size = 10
diff --git a/mesa/examples/basic/boltzmann_wealth_model/model.py b/mesa/examples/basic/boltzmann_wealth_model/model.py
index a8e96a79..03ef5a21 100644
--- a/mesa/examples/basic/boltzmann_wealth_model/model.py
+++ b/mesa/examples/basic/boltzmann_wealth_model/model.py
@@ -1,6 +1,5 @@
 import mesa
-
-from .agents import MoneyAgent
+from mesa.examples.basic.boltzmann_wealth_model.agents import MoneyAgent
 
 
 class BoltzmannWealthModel(mesa.Model):
diff --git a/mesa/examples/basic/conways_game_of_life/app.py b/mesa/examples/basic/conways_game_of_life/app.py
index bff865a8..d7838efa 100644
--- a/mesa/examples/basic/conways_game_of_life/app.py
+++ b/mesa/examples/basic/conways_game_of_life/app.py
@@ -4,7 +4,8 @@ import altair as alt
 import numpy as np
 import pandas as pd
 import streamlit as st
-from model import ConwaysGameOfLife
+
+from mesa.examples.basic.conways_game_of_life.model import ConwaysGameOfLife
 
 model = st.title("Conway's Game of Life")
 num_ticks = st.slider("Select number of Steps", min_value=1, max_value=100, value=50)
diff --git a/mesa/examples/basic/conways_game_of_life/model.py b/mesa/examples/basic/conways_game_of_life/model.py
index cc3fa996..6e81f690 100644
--- a/mesa/examples/basic/conways_game_of_life/model.py
+++ b/mesa/examples/basic/conways_game_of_life/model.py
@@ -1,8 +1,7 @@
 from mesa import Model
+from mesa.examples.basic.conways_game_of_life.agents import Cell
 from mesa.space import SingleGrid
 
-from .agents import Cell
-
 
 class ConwaysGameOfLife(Model):
     """Represents the 2-dimensional array of cells in Conway's Game of Life."""
diff --git a/mesa/examples/basic/schelling/app.py b/mesa/examples/basic/schelling/app.py
index 51989203..72ae6ddc 100644
--- a/mesa/examples/basic/schelling/app.py
+++ b/mesa/examples/basic/schelling/app.py
@@ -1,5 +1,6 @@
 import solara
 
+from mesa.examples.basic.schelling.model import Schelling
 from mesa.visualization import (
     Slider,
     SolaraViz,
@@ -7,8 +8,6 @@ from mesa.visualization import (
     make_space_matplotlib,
 )
 
-from .model import Schelling
-
 
 def get_happy_agents(model):
     """Display a text count of how many happy agents there are."""
diff --git a/mesa/examples/basic/schelling/model.py b/mesa/examples/basic/schelling/model.py
index fba0de7d..aa390f14 100644
--- a/mesa/examples/basic/schelling/model.py
+++ b/mesa/examples/basic/schelling/model.py
@@ -1,7 +1,6 @@
 import mesa
 from mesa import Model
-
-from .agents import SchellingAgent
+from mesa.examples.basic.schelling.agents import SchellingAgent
 
 
 class Schelling(Model):
diff --git a/mesa/examples/basic/virus_on_network/app.py b/mesa/examples/basic/virus_on_network/app.py
index 0e85e8ac..0183d256 100644
--- a/mesa/examples/basic/virus_on_network/app.py
+++ b/mesa/examples/basic/virus_on_network/app.py
@@ -4,10 +4,13 @@ import solara
 from matplotlib.figure import Figure
 from matplotlib.ticker import MaxNLocator
 
+from mesa.examples.basic.virus_on_network.model import (
+    State,
+    VirusOnNetwork,
+    number_infected,
+)
 from mesa.visualization import Slider, SolaraViz, make_space_matplotlib
 
-from .model import State, VirusOnNetwork, number_infected
-
 
 def agent_portrayal(graph):
     def get_agent(node):
diff --git a/mesa/examples/basic/virus_on_network/model.py b/mesa/examples/basic/virus_on_network/model.py
index 04e457e8..73419958 100644
--- a/mesa/examples/basic/virus_on_network/model.py
+++ b/mesa/examples/basic/virus_on_network/model.py
@@ -4,8 +4,7 @@ import networkx as nx
 
 import mesa
 from mesa import Model
-
-from .agents import State, VirusAgent
+from mesa.examples.basic.virus_on_network.agents import State, VirusAgent
 
 
 def number_state(model, state):
diff --git a/mesa/visualization/components/altair.py b/mesa/visualization/components/altair.py
index 3dcb7e2c..5aeee847 100644
--- a/mesa/visualization/components/altair.py
+++ b/mesa/visualization/components/altair.py
@@ -7,6 +7,8 @@ import solara
 with contextlib.suppress(ImportError):
     import altair as alt
 
+from mesa.experimental.cell_space import DiscreteSpace, Grid
+from mesa.space import ContinuousSpace, _Grid
 from mesa.visualization.utils import update_counter
 
 
@@ -29,35 +31,101 @@ def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
     if space is None:
         # Sometimes the space is defined as model.space instead of model.grid
         space = model.space
+
     chart = _draw_grid(space, agent_portrayal)
     solara.FigureAltair(chart)
 
 
+def _get_agent_data_old__discrete_space(space, agent_portrayal):
+    """Format agent portrayal data for old-style discrete spaces.
+
+    Args:
+        space: the mesa.space._Grid instance
+        agent_portrayal: the agent portrayal callable
+
+    Returns:
+        list of dicts
+
+    """
+    all_agent_data = []
+    for content, (x, y) in space.coord_iter():
+        if not content:
+            continue
+        if not hasattr(content, "__iter__"):
+            # Is a single grid
+            content = [content]  # noqa: PLW2901
+        for agent in content:
+            # use all data from agent portrayal, and add x,y coordinates
+            agent_data = agent_portrayal(agent)
+            agent_data["x"] = x
+            agent_data["y"] = y
+            all_agent_data.append(agent_data)
+    return all_agent_data
+
+
+def _get_agent_data_new_discrete_space(space: DiscreteSpace, agent_portrayal):
+    """Format agent portrayal data for new-style discrete spaces.
+
+    Args:
+        space: the mesa.experiment.cell_space.Grid instance
+        agent_portrayal: the agent portrayal callable
+
+    Returns:
+        list of dicts
+
+    """
+    all_agent_data = []
+
+    for cell in space.all_cells:
+        for agent in cell.agents:
+            agent_data = agent_portrayal(agent)
+            agent_data["x"] = cell.coordinate[0]
+            agent_data["y"] = cell.coordinate[1]
+            all_agent_data.append(agent_data)
+    return all_agent_data
+
+
+def _get_agent_data_continuous_space(space: ContinuousSpace, agent_portrayal):
+    """Format agent portrayal data for continuous space.
+
+    Args:
+        space: the ContinuousSpace instance
+        agent_portrayal: the agent portrayal callable
+
+    Returns:
+        list of dicts
+    """
+    all_agent_data = []
+    for agent in space._agent_to_index:
+        agent_data = agent_portrayal(agent)
+        agent_data["x"] = agent.pos[0]
+        agent_data["y"] = agent.pos[1]
+        all_agent_data.append(agent_data)
+    return all_agent_data
+
+
 def _draw_grid(space, agent_portrayal):
-    def portray(g):
-        all_agent_data = []
-        for content, (x, y) in g.coord_iter():
-            if not content:
-                continue
-            if not hasattr(content, "__iter__"):
-                # Is a single grid
-                content = [content]  # noqa: PLW2901
-            for agent in content:
-                # use all data from agent portrayal, and add x,y coordinates
-                agent_data = agent_portrayal(agent)
-                agent_data["x"] = x
-                agent_data["y"] = y
-                all_agent_data.append(agent_data)
-        return all_agent_data
-
-    all_agent_data = portray(space)
+    match space:
+        case Grid():
+            all_agent_data = _get_agent_data_new_discrete_space(space, agent_portrayal)
+        case _Grid():
+            all_agent_data = _get_agent_data_old__discrete_space(space, agent_portrayal)
+        case ContinuousSpace():
+            all_agent_data = _get_agent_data_continuous_space(space, agent_portrayal)
+        case _:
+            raise NotImplementedError(
+                f"visualizing {type(space)} is currently not supported through altair"
+            )
+
     invalid_tooltips = ["color", "size", "x", "y"]
 
+    x_y_type = "ordinal" if not isinstance(space, ContinuousSpace) else "nominal"
+
     encoding_dict = {
         # no x-axis label
-        "x": alt.X("x", axis=None, type="ordinal"),
+        "x": alt.X("x", axis=None, type=x_y_type),
         # no y-axis label
-        "y": alt.Y("y", axis=None, type="ordinal"),
+        "y": alt.Y("y", axis=None, type=x_y_type),
         "tooltip": [
             alt.Tooltip(key, type=alt.utils.infer_vegalite_type([value]))
             for key, value in all_agent_data[0].items()
diff --git a/mesa/visualization/components/matplotlib.py b/mesa/visualization/components/matplotlib.py
index 11fa9d87..bea633d6 100644
--- a/mesa/visualization/components/matplotlib.py
+++ b/mesa/visualization/components/matplotlib.py
@@ -55,6 +55,8 @@ def SpaceMatplotlib(
     # https://stackoverflow.com/questions/67524641/convert-multiple-isinstance-checks-to-structural-pattern-matching
     match space:
         case mesa.space._Grid():
+            _draw_grid(space, space_ax, agent_portrayal, propertylayer_portrayal, model)
+        case mesa.space.ContinuousSpace():
             _draw_continuous_space(space, space_ax, agent_portrayal, model)
         case mesa.space.NetworkGrid():
             _draw_network_grid(space, space_ax, agent_portrayal)


Test patch:
diff --git a/tests/test_agent.py b/tests/test_agent.py
index aa5ff935..70183ca0 100644
--- a/tests/test_agent.py
+++ b/tests/test_agent.py
@@ -162,6 +162,14 @@ def test_agent_membership():
     assert AgentTest(model) not in agentset
 
 
+def test_agent_rng():
+    """Test whether agent.random and agent.rng are equal to model.random and model.rng."""
+    model = Model(seed=42)
+    agent = Agent(model)
+    assert agent.random is model.random
+    assert agent.rng is model.rng
+
+
 def test_agent_add_remove_discard():
     """Test adding, removing and discarding agents from AgentSet."""
     model = Model()
@@ -472,6 +480,29 @@ def test_agentset_shuffle_do():
         original_order != shuffled_order
     ), "The order should be different after shuffle_do"
 
+    class AgentWithRemove(Agent):
+        def __init__(self, model):
+            super().__init__(model)
+            self.is_alive = True
+
+        def remove(self):
+            super().remove()
+            self.is_alive = False
+
+        def step(self):
+            if not self.is_alive:
+                raise Exception
+
+            agent_to_remove = self.random.choice(self.model.agents)
+
+            if agent_to_remove is not self:
+                agent_to_remove.remove()
+
+    model = Model(seed=32)
+    for _ in range(100):
+        AgentWithRemove(model)
+    model.agents.shuffle_do("step")
+
 
 def test_agentset_get_attribute():
     """Test AgentSet.get for attributes."""