Repository: projectmesa/mesa
Pull request #170 (2015-09-12 20:07:02)

Adding multi-stage activation.
Adds a multi-stage activation scheduler, closing #166.

jackiekazil: I'm going to try to look at this, this week.

Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 418715cf..c9dd081c 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -6,9 +6,9 @@ Release History
 Next release
 ++++++++++++++++++
 
-* #170 Adding multi-stage activation
-* #169 Wolf-Sheep Cleanup
-* Updates requirements to latest libraries
+(See current PR list)
+#170 Adding multi-stage activation
+#169 Wolf-Sheep Cleanup
 
 
 0.6.7 (2015-07-11)
diff --git a/examples/WolfSheep/Readme.md b/examples/WolfSheep/Readme.md
index c1693f3b..4c9e9644 100644
--- a/examples/WolfSheep/Readme.md
+++ b/examples/WolfSheep/Readme.md
@@ -10,11 +10,4 @@ Another ABM standard. Wolf and sheep wander around the grid randomly and reprodu
  - Dynamically adding and removing agents from the schedule
  - (Eventually) visualizing a layered grid, with a grass layer, a wolf layer and a sheep layer.
 
-### To run this example
 
-* Launch the model
-```python
-python WolfSheepServer.py
-```
-* Visit your browser: http://127.0.0.1:8888/
-* In your browser hit *reset*, then *run*
\ No newline at end of file
diff --git a/examples/WolfSheep/WolfSheep.py b/examples/WolfSheep/WolfSheep.py
index 698db023..913237ec 100644
--- a/examples/WolfSheep/WolfSheep.py
+++ b/examples/WolfSheep/WolfSheep.py
@@ -7,15 +7,15 @@ Replication of the model found in NetLogo:
     http://ccl.northwestern.edu/netlogo/models/WolfSheepPredation.
     Center for Connected Learning and Computer-Based Modeling,
     Northwestern University, Evanston, IL.
+
+TODO: Implement grass
 '''
 
 import random
-from collections import defaultdict
 
 from mesa import Model, Agent
 from mesa.space import MultiGrid
 from mesa.time import RandomActivation
-from mesa.datacollection import DataCollector
 
 from RandomWalk import RandomWalker
 
@@ -25,28 +25,23 @@ class WolfSheepPredation(Model):
     Wolf-Sheep Predation Model
     '''
 
-    height = 20
-    width = 20
-
     initial_sheep = 100
     initial_wolves = 50
+    sheep_gain_from_food = 4
 
-    sheep_reproduce = 0.04
-    wolf_reproduce = 0.05
+    grass = False
 
     wolf_gain_from_food = 20
+    sheep_reproduce = 0.04
+    wolf_reproduce = 0.05
 
-    grass = False
-    grass_regrowth_time = 30
-    sheep_gain_from_food = 4
-
-    verbose = False # Print-monitoring
+    height = 20
+    width = 20
 
     def __init__(self, height=20, width=20,
-                 initial_sheep=100, initial_wolves=50,
-                 sheep_reproduce=0.04, wolf_reproduce=0.05,
-                 wolf_gain_from_food=20,
-                 grass=False, grass_regrowth_time=30, sheep_gain_from_food=4):
+                 initial_sheep=100, initial_wolves=50, sheep_reproduce=0.04,
+                 wolf_reproduce=0.05, wolf_gain_from_food=20,
+                 grass=False, sheep_gain_from_food=4):
         '''
         Create a new Wolf-Sheep model with the given parameters.
 
@@ -57,8 +52,6 @@ class WolfSheepPredation(Model):
             wolf_reproduce: Probability of each wolf reproducing each step
             wolf_gain_from_food: Energy a wolf gains from eating a sheep
             grass: Whether to have the sheep eat grass for energy
-            grass_regrowth_time: How long it takes for a grass patch to regrow
-                                 once it is eaten
             sheep_gain_from_food: Energy sheep gain from grass, if enabled.
         '''
 
@@ -71,21 +64,16 @@ class WolfSheepPredation(Model):
         self.wolf_reproduce = wolf_reproduce
         self.wolf_gain_from_food = wolf_gain_from_food
         self.grass = grass
-        self.grass_regrowth_time = grass_regrowth_time
         self.sheep_gain_from_food = sheep_gain_from_food
 
-        self.schedule = RandomActivationByBreed(self)
+        self.schedule = RandomActivation(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)})
 
         # Create sheep:
         for i in range(self.initial_sheep):
             x = random.randrange(self.width)
             y = random.randrange(self.height)
-            energy = random.randrange(2 * self.sheep_gain_from_food)
-            sheep = Sheep(self.grid, (x, y), True, energy)
+            sheep = Sheep(self.grid, (x, y), True)
             self.grid.place_agent(sheep, (x, y))
             self.schedule.add(sheep)
 
@@ -98,48 +86,11 @@ class WolfSheepPredation(Model):
             self.grid.place_agent(wolf, (x, y))
             self.schedule.add(wolf)
 
-        # Create grass patches
-        if self.grass:
-            for agent, x, y in self.grid.coord_iter():
-
-                fully_grown = random.choice([True, False])
-
-                if fully_grown:
-                    countdown = self.grass_regrowth_time
-                else:
-                    countdown = random.randrange(self.grass_regrowth_time)
-
-                patch = GrassPatch(fully_grown, countdown)
-                self.grid.place_agent(patch, (x, y))
-                self.schedule.add(patch)
-
         self.running = True
 
     def step(self):
         self.schedule.step()
-        self.datacollector.collect(self)
-        if self.verbose:
-            print([self.schedule.time,
-                self.schedule.get_breed_count(Wolf),
-                self.schedule.get_breed_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))
-
-        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))
 
 class Sheep(RandomWalker, Agent):
     '''
@@ -148,43 +99,16 @@ class Sheep(RandomWalker, Agent):
     The init is the same as the RandomWalker.
     '''
 
-    energy = None
-
-    def __init__(self, grid, pos, moore, energy=None):
-        super().__init__(grid, pos, moore)
-        self.energy = energy
-
     def step(self, model):
         '''
         A model step. Move, then eat grass and reproduce.
         '''
         self.random_move()
-        living = True
-
-        if model.grass:
-            # Reduce energy
-            self.energy -= 1
-
-            # If there is grass available, eat it
-            this_cell = model.grid.get_cell_list_contents([self.pos])
-            grass_patch = [obj for obj in this_cell 
-                           if isinstance(obj, GrassPatch)][0]
-            if grass_patch.fully_grown:
-                self.energy += model.sheep_gain_from_food
-                grass_patch.fully_grown = False
-
-            # Death
-            if self.energy < 0:
-                model.grid._remove_agent(self.pos, self)
-                model.schedule.remove(self)
-                living = False
-
-        if living and random.random() < model.sheep_reproduce:
+        if random.random() < model.sheep_reproduce:
             # Create a new sheep:
-            if model.grass:
-                self.energy /= 2
-            lamb = Sheep(self.grid, self.pos, self.moore, self.energy)
-            model.grid.place_agent(lamb, self.pos)
+            x, y = self.pos
+            lamb = Sheep(self.grid, self.pos, self.moore)
+            model.grid[y][x].add(lamb)
             model.schedule.add(lamb)
 
 
@@ -205,125 +129,20 @@ class Wolf(RandomWalker, Agent):
 
         # If there are sheep present, eat one
         x, y = self.pos
-        this_cell = model.grid.get_cell_list_contents([self.pos])
+        this_cell = model.grid[y][x]
         sheep = [obj for obj in this_cell if isinstance(obj, Sheep)]
         if len(sheep) > 0:
             sheep_to_eat = random.choice(sheep)
             self.energy += model.wolf_gain_from_food
 
             # Kill the sheep
-            model.grid._remove_agent(self.pos, sheep_to_eat)
+            model.grid[y][x].remove(sheep_to_eat)
             model.schedule.remove(sheep_to_eat)
 
-        # Death or reproduction
-        if self.energy < 0:
-            model.grid._remove_agent(self.pos, self)
-            model.schedule.remove(self)
-        else:
-            if random.random() < model.wolf_reproduce:
-                # Create a new wolf cub
-                self.energy /= 2
-                cub = Wolf(self.grid, self.pos, self.moore, self.energy)
-                model.grid.place_agent(cub, cub.pos)
-                model.schedule.add(cub)
-
-
-class GrassPatch(Agent):
-    '''
-    A patch of grass that grows at a fixed rate and it is eaten by sheep
-    '''
-
-    def __init__(self, fully_grown, countdown):
-        '''
-        Creates a new patch of grass
-
-        Args:
-            grown: (boolean) Whether the patch of grass is fully grown or not
-            countdown: Time for the patch of grass to be fully grown again
-        '''
-        self.fully_grown = fully_grown
-        self.countdown = countdown
-
-    def step(self, model):
-        if not self.fully_grown:
-            if self.countdown <= 0:
-                # Set as fully grown
-                self.fully_grown = True
-                self.countdown = model.grass_regrowth_time
-            else:
-                self.countdown -= 1
-
-
-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(model) method.
-    '''
-    agents_by_breed = defaultdict(list)
-
-    def __init__(self, model):
-        super().__init__(model)
-        self.agents_by_breed = defaultdict(list)
-
-    def add(self, agent):
-        '''
-        Add an Agent object to the schedule
-
-        Args:
-            agent: An Agent to be added to the schedule.
-        '''
-
-        self.agents.append(agent)
-        agent_class = type(agent)
-        self.agents_by_breed[agent_class].append(agent)
-
-    def remove(self, agent):
-        '''
-        Remove all instances of a given agent from the schedule.
-        '''
-
-        while agent in self.agents:
-            self.agents.remove(agent)
-
-        agent_class = type(agent)
-        while agent in self.agents_by_breed[agent_class]:
-            self.agents_by_breed[agent_class].remove(agent)
-
-    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.
-        '''
-        agents = self.agents_by_breed[breed]
-        random.shuffle(agents)
-        for agent in agents:
-            agent.step(self.model)
-
-    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])
+        # Reproduction:
+        if random.random() < model.wolf_reproduce:
+            # Create a new wolf cub
+            cub = Wolf(self.grid, self.pos, self.moore, self.energy / 2)
+            self.energy = self.energy / 2
+            model.grid[y][x].add(cub)
+            model.schedule.add(cub)
diff --git a/examples/WolfSheep/WolfSheepServer.py b/examples/WolfSheep/WolfSheepServer.py
index 77a04484..a8f85d4c 100644
--- a/examples/WolfSheep/WolfSheepServer.py
+++ b/examples/WolfSheep/WolfSheepServer.py
@@ -1,6 +1,6 @@
-from WolfSheep import Wolf, Sheep, GrassPatch, WolfSheepPredation
+from WolfSheep import Wolf, Sheep, WolfSheepPredation
 from mesa.visualization.ModularVisualization import ModularServer
-from mesa.visualization.modules import CanvasGrid, ChartModule
+from mesa.visualization.modules import CanvasGrid
 
 
 def wolf_sheep_portrayal(agent):
@@ -13,30 +13,16 @@ def wolf_sheep_portrayal(agent):
     if type(agent) is Sheep:
         portrayal["Color"] = "#666666"
         portrayal["r"] = 0.8
-        portrayal["Layer"] = 1
+        portrayal["Layer"] = 0
 
     elif type(agent) is Wolf:
         portrayal["Color"] = "#AA0000"
         portrayal["r"] = 0.5
-        portrayal["Layer"] = 2
-
-    elif type(agent) is GrassPatch:
-        if agent.fully_grown:
-            portrayal["Color"] = "#00AA00"
-        else:
-            portrayal["Color"] = "#D6F5D6"
-        portrayal["Shape"] = "rect"
-        portrayal["Layer"] = 0
-        portrayal["w"] = 1
-        portrayal["h"] = 1
-
-
+        portrayal["Layer"] = 1
     return portrayal
 
 canvas_element = CanvasGrid(wolf_sheep_portrayal, 20, 20, 500, 500)
-chart_element = ChartModule([{"Label": "Wolves", "Color": "#AA0000"},
-    {"Label": "Sheep", "Color": "#666666"}])
 
-server = ModularServer(WolfSheepPredation, [canvas_element, chart_element],
-                      "WolfSheep", grass=True)
+server = ModularServer(WolfSheepPredation, [canvas_element],
+                      "WolfSheep")
 server.launch()
diff --git a/examples/WolfSheep/__init__.py b/examples/WolfSheep/__init__.py
index bf30c8b9..0f751c24 100644
--- a/examples/WolfSheep/__init__.py
+++ b/examples/WolfSheep/__init__.py
@@ -2,6 +2,5 @@ from WolfSheep import WolfSheepPredation
 from WolfSheepVisualization import WolfSheepVisualization
 
 if __name__ == "__main__":
-    model = WolfSheepPredation(grass=True)
-    model.run_model()
-    # viz = WolfSheepVisualization(model)
\ No newline at end of file
+    model = WolfSheepPredation()
+    viz = WolfSheepVisualization(model)
diff --git a/mesa/time.py b/mesa/time.py
index a1a73491..03479b9a 100644
--- a/mesa/time.py
+++ b/mesa/time.py
@@ -133,3 +133,58 @@ class SimultaneousActivation(BaseScheduler):
             agent.advance(self.model)
         self.steps += 1
         self.time += 1
+
+
+class StagedActivation(BaseScheduler):
+    '''
+    A scheduler which allows agent activation to be divided into several stages
+    instead of a single `step` method. All agents execute one stage before
+    moving on to the next.
+
+    Agents must have all the stage methods implemented. Stage methods take a
+    model object as their only argument.
+
+    This schedule tracks steps and time separately. Time advances in fractional
+    increments of 1 / (# of stages), meaning that 1 step = 1 unit of time.
+    '''
+
+    stage_list = []
+    shuffle = False
+    shuffle_between_stages = False
+    stage_time = 1
+
+    def __init__(self, model, stage_list=["step"], shuffle=False,
+            shuffle_between_stages=False):
+        '''
+        Create an empty Staged Activation schedule.
+
+        Args:
+            model: Model object associated with the schedule.
+            stage_list: List of strings of names of stages to run, in the
+                         order to run them in.
+            shuffle: If True, shuffle the order of agents each step.
+            shuffle_between_stages: If True, shuffle the agents after each
+                                    stage; otherwise, only shuffle at the start
+                                    of each step.
+        '''
+        super().__init__(model)
+        self.stage_list = stage_list
+        self.shuffle = shuffle
+        self.shuffle_between_stages = shuffle_between_stages
+        self.stage_time = 1 / len(self.stage_list)
+
+    def step(self):
+        '''
+        Executes all the stages of all agents.
+        '''
+
+        if self.shuffle:
+            random.shuffle(self.agents)
+        for stage in self.stage_list:
+            for agent in self.agents:
+                getattr(agent, stage)(self.model)  # Run stage
+            if self.shuffle_between_stages:
+                random.shuffle(self.agents)
+            self.time += self.stage_time
+
+        self.steps += 1
diff --git a/requirements.txt b/requirements.txt
index 145bebbd..7c0fc65e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
-numpy==1.10.4
-pandas==0.17.1
-ipython==4.1.1
+numpy==1.9.1
+pandas==0.15.2
+ipython==3.0.0
 
 tornado==4.0.2
-flake8==2.5.2
-scipy==0.17.0
\ No newline at end of file
+flake8==2.4.0
+scipy==0.15.1


Test patch:
diff --git a/tests/test_time.py b/tests/test_time.py
new file mode 100644
index 00000000..b0df62e1
--- /dev/null
+++ b/tests/test_time.py
@@ -0,0 +1,65 @@
+'''
+Test the advanced schedulers.
+'''
+
+import unittest
+from mesa import Model, Agent
+from mesa.time import StagedActivation
+
+
+class MockStagedAgent(Agent):
+    '''
+    Minimalistic agent for testing purposes.
+    '''
+
+    def __init__(self, name):
+        self.unique_id = name
+
+    def stage_one(self, model):
+        model.log.append(self.unique_id + "_1")
+
+    def stage_two(self, model):
+        model.log.append(self.unique_id + "_2")
+
+
+class MockModel(Model):
+
+    def __init__(self, shuffle):
+        self.log = []
+        model_stages = ["stage_one", "stage_two"]
+        self.schedule = StagedActivation(self, model_stages, shuffle=shuffle)
+
+        # Make agents
+        for name in ["A", "B"]:
+            agent = MockStagedAgent(name)
+            self.schedule.add(agent)
+
+    def step(self):
+        self.schedule.step()
+
+
+class TestStagedActivation(unittest.TestCase):
+    '''
+    Test the staged activation.
+    '''
+
+    expected_output = ["A_1", "B_1", "A_2", "B_2"]
+
+    def test_no_shuffle(self):
+        '''
+        Testing staged activation without shuffling.
+        '''
+        model = MockModel(False)
+        model.step()
+        assert model.log == self.expected_output
+
+    def test_shuffle(self):
+        '''
+        Test staged activation with shuffling
+        '''
+        model = MockModel(True)
+        model.step()
+        for output in self.expected_output[:2]:
+            assert output in model.log[:2]
+        for output in self.expected_output[2:]:
+            assert output in model.log[2:]