Repository: projectmesa/mesa
Pull request #1162 (2022-02-20 12:07:37)

feat: Implement random activation by type
This is the least controversial way of putting in random activation by
type into the library, by simply using RandomActivationByBreed found in
sugarscape_cg and wolf_sheep (I have checked that the schedule.py in
both examples are identical).

Some differences:
- replace the term "breed" by a more general "type"
- add Mypy typings

If people want to get logging specific to one agent type, they can
either
- loop through all agents, and filter by their type
- loop through model.scheduler.agents_by_type[type_class]

We don't support agents that change their type, because in Python, you
can't have an object that changes its intrinsic type over its lifetime.

There could perhaps be a `RandomActivationByAttribute`, that groups
agents by their attributes, which may change throughout the course of a
simulation.

rht: @EwoutH check this PR out. It's much less controversial than #1142, and so require fewer points to be discussed for a consensus.
codecov[bot]: # [Codecov](https://codecov.io/gh/projectmesa/mesa/pull/1162?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) Report
> Merging [#1162](https://codecov.io/gh/projectmesa/mesa/pull/1162?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) (bbb190b) into [main](https://codecov.io/gh/projectmesa/mesa/commit/d346c4879a46052ea75884a980bcbc74fbb27766?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) (d346c48) will **decrease** coverage by `0.03%`.
> The diff coverage is `92.85%`.

[![Impacted file tree graph](https://codecov.io/gh/projectmesa/mesa/pull/1162/graphs/tree.svg?width=650&height=150&src=pr&token=OUHxz83iDi&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa)](https://codecov.io/gh/projectmesa/mesa/pull/1162?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa)

```diff
@@            Coverage Diff             @@
##             main    #1162      +/-   ##
==========================================
- Coverage   89.28%   89.25%   -0.04%     
==========================================
  Files          19       19              
  Lines        1269     1284      +15     
  Branches      259      258       -1     
==========================================
+ Hits         1133     1146      +13     
- Misses        100      101       +1     
- Partials       36       37       +1     
```


| [Impacted Files](https://codecov.io/gh/projectmesa/mesa/pull/1162?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) | Coverage Δ | |
|---|---|---|
| [mesa/time.py](https://codecov.io/gh/projectmesa/mesa/pull/1162/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS90aW1lLnB5) | `94.62% <92.85%> (-0.90%)` | :arrow_down: |
| [mesa/visualization/UserParam.py](https://codecov.io/gh/projectmesa/mesa/pull/1162/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS92aXN1YWxpemF0aW9uL1VzZXJQYXJhbS5weQ==) | `86.00% <0.00%> (-0.80%)` | :arrow_down: |
| [mesa/main.py](https://codecov.io/gh/projectmesa/mesa/pull/1162/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS9tYWluLnB5) | `96.00% <0.00%> (-0.43%)` | :arrow_down: |
| [mesa/visualization/ModularVisualization.py](https://codecov.io/gh/projectmesa/mesa/pull/1162/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS92aXN1YWxpemF0aW9uL01vZHVsYXJWaXN1YWxpemF0aW9uLnB5) | `75.20% <0.00%> (-0.41%)` | :arrow_down: |
| [mesa/datacollection.py](https://codecov.io/gh/projectmesa/mesa/pull/1162/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS9kYXRhY29sbGVjdGlvbi5weQ==) | `97.67% <0.00%> (-0.03%)` | :arrow_down: |
| [mesa/batchrunner.py](https://codecov.io/gh/projectmesa/mesa/pull/1162/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS9iYXRjaHJ1bm5lci5weQ==) | `92.25% <0.00%> (-0.03%)` | :arrow_down: |
| [mesa/space.py](https://codecov.io/gh/projectmesa/mesa/pull/1162/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS9zcGFjZS5weQ==) | `94.91% <0.00%> (-0.02%)` | :arrow_down: |

------

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

EwoutH: From an architecture perspective I agree this is the right course of action and a nice first step.

Haven't looked closely at the exact implementation, I can do that tomorrow if you want (however I think I'm not going to find much).
rht: 2 points that need consensus:

First, the `by_type` boolean is not necessary, and complicates the API without much benefit. If the user wants the regular `RandomActivation`, they can simply use `RandomActivation`.

Second, the step order of each type. I can't think of a use case when a modeler wants to do the reverse of the original insertion order of the agent types during the model initialization. I think a more general solution is to have an argument called  `agent_type_sequence`, with a type of `Optional[Sequence[Type[Agent]]]`. The modeler can specify any order they want.
EwoutH: > First, the `by_type` boolean is not necessary, and complicates the API without much benefit. If the user wants the regular `RandomActivation`, they can simply use `RandomActivation`.

Agreed, lets drop the `by_type` boolean.

> Second, the step order of each type. I can't think of a use case when a modeler wants to do the reverse of the original insertion order of the agent types during the model initialization. I think a more general solution is to have an argument called `agent_type_sequence`, with a type of `Optional[Sequence[Type[Agent]]]`. The modeler can specify any order they want.

A custom order like `Optional[Sequence[Type[Agent]]]` is indeed the most important. You can of course implement reversing or shuffling the order yourself, but it might be nice to just very quickly be able experiment and iterate with it.
rht: Shuffling may be in demand, but I'm not sure about the reverse order.
rht: I forgot to say that I removed the unnecessary `by_type` arg, and added `shuffle_types` and `shuffle_agents`. At least, featurewise, it is the same as Agents.jl. I'm tempted to say that adding custom types order argument can be deferred to a separate PR. We can always iterate on this activation before the next release.
rht: Bump. @tpike3 thoughts so far? I think this is ready to merge. Once you ack, I will squash and merge. After that, I will port my [implementation](https://github.com/AB-CE/examples/tree/master/examples/sugarscape) of Sugarscape with trading ({G1}, {M, T}) and make a PR for it.
tpike3: Dang, I am way behind... @rht and @EwoutH this is awesome.  This is the implementation I am most familiar with so am ready to merge and I agree the terms (e.g type vs breed and shuffle) are the preferred. 

> We can always iterate on this activation before the next release.

To @rht's point thinking future customizations in new PRs before the next release ... I will concur and support two additional features in later PRs
1. I concur on the ability for a customized type order. To me the ability to customize is critical as *growing* simulation in different ways is often more insightful than the actual results 
2. If I am reading it right since I did not get a chance to play around with the code but don't want to hold up progress, we should have the option to run all agents in a random order regardless of type.. in [some models it will matter](https://dblp.org/rec/phd/basesearch/Comer14) and is supporting evidence to Lorenz's seminal work on [Deterministic Nonperiodic Flow.](https://www.astro.puc.cl/~rparra/tools/PAPERS/lorenz1962.pdf)
rht: >  we should have the option to run all agents in a random order regardless of type

You can do this with:
```
if fully_random:
    self.schedule = RandomActivation(self)
else:
    self.scheduler = RandomActivationByType(self)
```

And so we can keep the API minimal.
tpike3: > > we should have the option to run all agents in a random order regardless of type
> 
> You can do this with:
> 
> ```python
> if fully_random:
>     self.schedule = RandomActivation(self)
> else:
>     self.scheduler = RandomActivationByType(self)
> ```
> 
> And so we can keep the API minimal.

@rht That is perfect! I didn't even think of that...so obvious I looked right over it.  Squash and merge at your discretion
rht: Yay! Can't wait for the sugarscape variations being in the repo (at least the ones that doesn't require Multi-level Mesa).

Patch:
diff --git a/examples/sugarscape_cg/sugarscape_cg/model.py b/examples/sugarscape_cg/sugarscape_cg/model.py
index 169176c5..0eb310ae 100644
--- a/examples/sugarscape_cg/sugarscape_cg/model.py
+++ b/examples/sugarscape_cg/sugarscape_cg/model.py
@@ -12,9 +12,9 @@ Northwestern University, Evanston, IL.
 from mesa import Model
 from mesa.space import MultiGrid
 from mesa.datacollection import DataCollector
+from mesa.time import RandomActivationByType
 
 from .agents import SsAgent, Sugar
-from .schedule import RandomActivationByBreed
 
 
 class SugarscapeCg(Model):
@@ -37,10 +37,10 @@ class SugarscapeCg(Model):
         self.width = width
         self.initial_population = initial_population
 
-        self.schedule = RandomActivationByBreed(self)
+        self.schedule = RandomActivationByType(self)
         self.grid = MultiGrid(self.height, self.width, torus=False)
         self.datacollector = DataCollector(
-            {"SsAgent": lambda m: m.schedule.get_breed_count(SsAgent)}
+            {"SsAgent": lambda m: m.schedule.get_type_count(SsAgent)}
         )
 
         # Create sugar
@@ -72,14 +72,14 @@ class SugarscapeCg(Model):
         # collect data
         self.datacollector.collect(self)
         if self.verbose:
-            print([self.schedule.time, self.schedule.get_breed_count(SsAgent)])
+            print([self.schedule.time, self.schedule.get_type_count(SsAgent)])
 
     def run_model(self, step_count=200):
 
         if self.verbose:
             print(
                 "Initial number Sugarscape Agent: ",
-                self.schedule.get_breed_count(SsAgent),
+                self.schedule.get_type_count(SsAgent),
             )
 
         for i in range(step_count):
@@ -89,5 +89,5 @@ class SugarscapeCg(Model):
             print("")
             print(
                 "Final number Sugarscape Agent: ",
-                self.schedule.get_breed_count(SsAgent),
+                self.schedule.get_type_count(SsAgent),
             )
diff --git a/examples/sugarscape_cg/sugarscape_cg/schedule.py b/examples/sugarscape_cg/sugarscape_cg/schedule.py
deleted file mode 100644
index a8cd696a..00000000
--- a/examples/sugarscape_cg/sugarscape_cg/schedule.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from collections import defaultdict
-
-from mesa.time import RandomActivation
-
-
-class RandomActivationByBreed(RandomActivation):
-    """
-    A scheduler which activates each type of agent once per step, in random
-    order, with the order reshuffled every step.
-
-    This is equivalent to the NetLogo 'ask breed...' and is generally the
-    default behavior for an ABM.
-
-    Assumes that all agents have a step() method.
-    """
-
-    def __init__(self, model):
-        super().__init__(model)
-        self.agents_by_breed = defaultdict(dict)
-
-    def add(self, agent):
-        """
-        Add an Agent object to the schedule
-
-        Args:
-            agent: An Agent to be added to the schedule.
-        """
-
-        self._agents[agent.unique_id] = agent
-        agent_class = type(agent)
-        self.agents_by_breed[agent_class][agent.unique_id] = agent
-
-    def remove(self, agent):
-        """
-        Remove all instances of a given agent from the schedule.
-        """
-
-        del self._agents[agent.unique_id]
-
-        agent_class = type(agent)
-        del self.agents_by_breed[agent_class][agent.unique_id]
-
-    def step(self, by_breed=True):
-        """
-        Executes the step of each agent breed, one at a time, in random order.
-
-        Args:
-            by_breed: If True, run all agents of a single breed before running
-                      the next one.
-        """
-        if by_breed:
-            for agent_class in self.agents_by_breed:
-                self.step_breed(agent_class)
-            self.steps += 1
-            self.time += 1
-        else:
-            super().step()
-
-    def step_breed(self, breed):
-        """
-        Shuffle order and run all agents of a given breed.
-
-        Args:
-            breed: Class object of the breed to run.
-        """
-        agent_keys = list(self.agents_by_breed[breed].keys())
-        self.model.random.shuffle(agent_keys)
-        for agent_key in agent_keys:
-            self.agents_by_breed[breed][agent_key].step()
-
-    def get_breed_count(self, breed_class):
-        """
-        Returns the current number of agents of certain breed in the queue.
-        """
-        return len(self.agents_by_breed[breed_class].values())
diff --git a/examples/wolf_sheep/wolf_sheep/model.py b/examples/wolf_sheep/wolf_sheep/model.py
index 2310a131..e916dd6f 100644
--- a/examples/wolf_sheep/wolf_sheep/model.py
+++ b/examples/wolf_sheep/wolf_sheep/model.py
@@ -12,9 +12,9 @@ Replication of the model found in NetLogo:
 from mesa import Model
 from mesa.space import MultiGrid
 from mesa.datacollection import DataCollector
+from mesa.time import RandomActivationByType
 
 from wolf_sheep.agents import Sheep, Wolf, GrassPatch
-from wolf_sheep.schedule import RandomActivationByBreed
 
 
 class WolfSheep(Model):
@@ -83,12 +83,12 @@ class WolfSheep(Model):
         self.grass_regrowth_time = grass_regrowth_time
         self.sheep_gain_from_food = sheep_gain_from_food
 
-        self.schedule = RandomActivationByBreed(self)
+        self.schedule = RandomActivationByType(self)
         self.grid = MultiGrid(self.height, self.width, torus=True)
         self.datacollector = DataCollector(
             {
-                "Wolves": lambda m: m.schedule.get_breed_count(Wolf),
-                "Sheep": lambda m: m.schedule.get_breed_count(Sheep),
+                "Wolves": lambda m: m.schedule.get_type_count(Wolf),
+                "Sheep": lambda m: m.schedule.get_type_count(Sheep),
             }
         )
 
@@ -136,21 +136,21 @@ class WolfSheep(Model):
             print(
                 [
                     self.schedule.time,
-                    self.schedule.get_breed_count(Wolf),
-                    self.schedule.get_breed_count(Sheep),
+                    self.schedule.get_type_count(Wolf),
+                    self.schedule.get_type_count(Sheep),
                 ]
             )
 
     def run_model(self, step_count=200):
 
         if self.verbose:
-            print("Initial number wolves: ", self.schedule.get_breed_count(Wolf))
-            print("Initial number sheep: ", self.schedule.get_breed_count(Sheep))
+            print("Initial number wolves: ", self.schedule.get_type_count(Wolf))
+            print("Initial number sheep: ", self.schedule.get_type_count(Sheep))
 
         for i in range(step_count):
             self.step()
 
         if self.verbose:
             print("")
-            print("Final number wolves: ", self.schedule.get_breed_count(Wolf))
-            print("Final number sheep: ", self.schedule.get_breed_count(Sheep))
+            print("Final number wolves: ", self.schedule.get_type_count(Wolf))
+            print("Final number sheep: ", self.schedule.get_type_count(Sheep))
diff --git a/examples/wolf_sheep/wolf_sheep/schedule.py b/examples/wolf_sheep/wolf_sheep/schedule.py
deleted file mode 100644
index a8cd696a..00000000
--- a/examples/wolf_sheep/wolf_sheep/schedule.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from collections import defaultdict
-
-from mesa.time import RandomActivation
-
-
-class RandomActivationByBreed(RandomActivation):
-    """
-    A scheduler which activates each type of agent once per step, in random
-    order, with the order reshuffled every step.
-
-    This is equivalent to the NetLogo 'ask breed...' and is generally the
-    default behavior for an ABM.
-
-    Assumes that all agents have a step() method.
-    """
-
-    def __init__(self, model):
-        super().__init__(model)
-        self.agents_by_breed = defaultdict(dict)
-
-    def add(self, agent):
-        """
-        Add an Agent object to the schedule
-
-        Args:
-            agent: An Agent to be added to the schedule.
-        """
-
-        self._agents[agent.unique_id] = agent
-        agent_class = type(agent)
-        self.agents_by_breed[agent_class][agent.unique_id] = agent
-
-    def remove(self, agent):
-        """
-        Remove all instances of a given agent from the schedule.
-        """
-
-        del self._agents[agent.unique_id]
-
-        agent_class = type(agent)
-        del self.agents_by_breed[agent_class][agent.unique_id]
-
-    def step(self, by_breed=True):
-        """
-        Executes the step of each agent breed, one at a time, in random order.
-
-        Args:
-            by_breed: If True, run all agents of a single breed before running
-                      the next one.
-        """
-        if by_breed:
-            for agent_class in self.agents_by_breed:
-                self.step_breed(agent_class)
-            self.steps += 1
-            self.time += 1
-        else:
-            super().step()
-
-    def step_breed(self, breed):
-        """
-        Shuffle order and run all agents of a given breed.
-
-        Args:
-            breed: Class object of the breed to run.
-        """
-        agent_keys = list(self.agents_by_breed[breed].keys())
-        self.model.random.shuffle(agent_keys)
-        for agent_key in agent_keys:
-            self.agents_by_breed[breed][agent_key].step()
-
-    def get_breed_count(self, breed_class):
-        """
-        Returns the current number of agents of certain breed in the queue.
-        """
-        return len(self.agents_by_breed[breed_class].values())
diff --git a/mesa/time.py b/mesa/time.py
index 0caaea82..1d0c2152 100644
--- a/mesa/time.py
+++ b/mesa/time.py
@@ -21,10 +21,10 @@ Key concepts:
     model has taken.
 """
 
-from collections import OrderedDict
+from collections import OrderedDict, defaultdict
 
 # mypy
-from typing import Dict, Iterator, List, Optional, Union
+from typing import Dict, Iterator, List, Optional, Union, Type
 from mesa.agent import Agent
 from mesa.model import Model
 
@@ -198,3 +198,88 @@ class StagedActivation(BaseScheduler):
             self.time += self.stage_time
 
         self.steps += 1
+
+
+class RandomActivationByType(BaseScheduler):
+    """
+    A scheduler which activates each type of agent once per step, in random
+    order, with the order reshuffled every step.
+
+    The `step_type` method is equivalent to the NetLogo 'ask [breed]...' and is
+    generally the default behavior for an ABM. The `step` method performs
+    `step_type` for each of the agent types.
+
+    Assumes that all agents have a step() method.
+
+    This implementation assumes that the type of an agent doesn't change
+    throughout the simulation.
+
+    If you want to do some computations / data collections specific to an agent
+    type, you can either:
+    - loop through all agents, and filter by their type
+    - access via `your_model.scheduler.agents_by_type[your_type_class]`
+    """
+
+    def __init__(self, model: Model) -> None:
+        super().__init__(model)
+        self.agents_by_type = defaultdict(dict)
+
+    def add(self, agent: Agent) -> None:
+        """
+        Add an Agent object to the schedule
+
+        Args:
+            agent: An Agent to be added to the schedule.
+        """
+
+        self._agents[agent.unique_id] = agent
+        agent_class: Type[Agent] = type(agent)
+        self.agents_by_type[agent_class][agent.unique_id] = agent
+
+    def remove(self, agent: Agent) -> None:
+        """
+        Remove all instances of a given agent from the schedule.
+        """
+
+        del self._agents[agent.unique_id]
+
+        agent_class: Type[Agent] = type(agent)
+        del self.agents_by_type[agent_class][agent.unique_id]
+
+    def step(self, shuffle_types: bool = True, shuffle_agents: bool = True) -> None:
+        """
+        Executes the step of each agent type, one at a time, in random order.
+
+        Args:
+            shuffle_types: If True, the order of execution of each types is
+                           shuffled.
+            shuffle_agents: If True, the order of execution of each agents in a
+                            type group is shuffled.
+        """
+        type_keys: List[Type[Agent]] = list(self.agents_by_type.keys())
+        if shuffle_types:
+            self.model.random.shuffle(type_keys)
+        for agent_class in type_keys:
+            self.step_type(agent_class, shuffle_agents=shuffle_agents)
+        self.steps += 1
+        self.time += 1
+
+    def step_type(self, type_class: Type[Agent], shuffle_agents: bool = True) -> None:
+        """
+        Shuffle order and run all agents of a given type.
+        This method is equivalent to the NetLogo 'ask [breed]...'.
+
+        Args:
+            type_class: Class object of the type to run.
+        """
+        agent_keys: List[int] = list(self.agents_by_type[type_class].keys())
+        if shuffle_agents:
+            self.model.random.shuffle(agent_keys)
+        for agent_key in agent_keys:
+            self.agents_by_type[type_class][agent_key].step()
+
+    def get_type_count(self, type_class: Type[Agent]) -> int:
+        """
+        Returns the current number of agents of certain type in the queue.
+        """
+        return len(self.agents_by_type[type_class].values())


Test patch:
diff --git a/tests/test_time.py b/tests/test_time.py
index faf61571..7733fa84 100644
--- a/tests/test_time.py
+++ b/tests/test_time.py
@@ -10,11 +10,13 @@ from mesa.time import (
     StagedActivation,
     RandomActivation,
     SimultaneousActivation,
+    RandomActivationByType,
 )
 
 RANDOM = "random"
 STAGED = "staged"
 SIMULTANEOUS = "simultaneous"
+RANDOM_BY_TYPE = "random_by_type"
 
 
 class MockAgent(Agent):
@@ -66,6 +68,8 @@ class MockModel(Model):
             self.schedule = RandomActivation(self)
         elif activation == SIMULTANEOUS:
             self.schedule = SimultaneousActivation(self)
+        elif activation == RANDOM_BY_TYPE:
+            self.schedule = RandomActivationByType(self)
         else:
             self.schedule = BaseScheduler(self)
 
@@ -184,5 +188,44 @@ class TestSimultaneousActivation(TestCase):
         assert all(map(lambda x: x == 1, agent_advances))
 
 
+class TestRandomActivationByType(TestCase):
+    """
+    Test the random activation by type.
+    TODO implement at least 2 types of agents, and test that step_type only
+    does step for one type of agents, not the entire agents.
+    """
+
+    def test_random_activation_step_shuffles(self):
+        """
+        Test the random activation by type step
+        """
+        model = MockModel(activation=RANDOM_BY_TYPE)
+        model.random = mock.Mock()
+        model.schedule.step()
+        assert model.random.shuffle.call_count == 2
+
+    def test_random_activation_step_increments_step_and_time_counts(self):
+        """
+        Test the random activation by type step increments step and time counts
+        """
+        model = MockModel(activation=RANDOM_BY_TYPE)
+        assert model.schedule.steps == 0
+        assert model.schedule.time == 0
+        model.schedule.step()
+        assert model.schedule.steps == 1
+        assert model.schedule.time == 1
+
+    def test_random_activation_step_steps_each_agent(self):
+        """
+        Test the random activation by type step causes each agent to step
+        """
+
+        model = MockModel(activation=RANDOM_BY_TYPE)
+        model.step()
+        agent_steps = [i.steps for i in model.schedule.agents]
+        # one step for each of 2 agents
+        assert all(map(lambda x: x == 1, agent_steps))
+
+
 if __name__ == "__main__":
     unittest.main()