Repository: projectmesa/mesa
Pull request #1710 (2023-06-03 07:49:21)

Added a feature in mesa.time StagedActivation class
Suggested solution to issue #1709

Hi team Mesa, 

Right now the Staged Activation class only allows for functions executed at Agent level. However, one can well want to implement a function which is executed at model level (for ex, in an Economy, adjust the wage rate of all labourers, who are agents and share the same wage rate). Hence suggesting writing "model." before the function name in the object stage_list to indicate its a model level function. 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/projectmesa/mesa/pull/1710?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) Report
Patch coverage: **`55.00`**% and project coverage change: **`-0.39`** :warning:
> Comparison is base [(`6f08b07`)](https://app.codecov.io/gh/projectmesa/mesa/commit/6f08b070e4e530f82ca526b3152bfb60d3188259?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) 81.45% compared to head [(`b572cce`)](https://app.codecov.io/gh/projectmesa/mesa/pull/1710?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) 81.06%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1710      +/-   ##
==========================================
- Coverage   81.45%   81.06%   -0.39%     
==========================================
  Files          18       18              
  Lines        1402     1421      +19     
  Branches      272      280       +8     
==========================================
+ Hits         1142     1152      +10     
- Misses        214      221       +7     
- Partials       46       48       +2     
```


| [Impacted Files](https://app.codecov.io/gh/projectmesa/mesa/pull/1710?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa) | Coverage Δ | |
|---|---|---|
| [mesa/model.py](https://app.codecov.io/gh/projectmesa/mesa/pull/1710?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS9tb2RlbC5weQ==) | `100.00% <ø> (ø)` | |
| [mesa/time.py](https://app.codecov.io/gh/projectmesa/mesa/pull/1710?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS90aW1lLnB5) | `85.04% <0.00%> (-5.96%)` | :arrow_down: |
| [mesa/datacollection.py](https://app.codecov.io/gh/projectmesa/mesa/pull/1710?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa#diff-bWVzYS9kYXRhY29sbGVjdGlvbi5weQ==) | `89.36% <84.61%> (-0.89%)` | :arrow_down: |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/projectmesa/mesa/pull/1710?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=projectmesa).

rht: You can simplify the code into:
```python
        for stage in self.stage_list:
            if stage.startswith("model."):
                getattr(self.model, stage[6:])()
            else:
                for agent_key in agent_keys:
                    if agent_key in self._agents:
                        getattr(self._agents[agent_key], stage)()  # Run stage
            # We recompute the keys because some agents might have been removed
            # in the previous loop.
            agent_keys = list(self._agents.keys())
            if self.shuffle_between_stages:
                self.model.random.shuffle(agent_keys)
            self.time += self.stage_time
```
rht: A different way of doing staged activation without abstraction:
```python
# Model step
def step(self):
    self.each_agents("stage1")
    self.shuffle_agents()
    self.adjust_wages()
    self.each_agents("stage2")
    self.shuffle_agents()
```

The `self.time` is not always needed in most cases. Sometimes. having fewer abstractions means it is more customizable, that the Mesa library's structure constraint doesn't get in the way of the user's model needs.
rht: Random activation:
```python
# Model step
def step(self):
    self.shuffle_agents()
    self.do_each("step")
```

Simultaneous activation:
```python
def step(self):
    self.do_each("step")
    self.do_each("advance")
```

Random activation by type:
```python
def step(self):
    self.shuffle_agents()
    for agent_class in self.agent_types:
        self.shuffle_agents(agent_class)
        self.do_each("step", key=agent_class)
```
subhamonsey: @tpike3 , @rht thanks for the helpful suggestions. I implemented the requested changes and simplified the code. Could you please run the workflows again?
rht: One last thing, I think it needs a test case. The new code branch is not yet covered.
subhamonsey: Hey, I added unit tests and rebased my commit over the latest changes. Please could you run the workflows again? Thank you. 
tpike3: Thanks @subhamonsey 

Patch:
diff --git a/docs/useful-snippets/snippets.rst b/docs/useful-snippets/snippets.rst
index 4c7e8e41..728c4e65 100644
--- a/docs/useful-snippets/snippets.rst
+++ b/docs/useful-snippets/snippets.rst
@@ -12,6 +12,16 @@ If you have `Multiple` type agents and one of them has time attribute you can st
   if self.model.schedule.time in self.discrete_time:
       self.model.space.move_agent(self, new_pos)
 
+Implementing Model Level Functions in Staged Activation
+-------------------------------------------------------
+In staged activation, if you may want a function to be implemented only on the model level and not at the level of agents.
+For such functions, include the prefix "model." before the model function name, when defining the function list.
+For example, consider a central employment exchange which adjust the wage rate common to all laborers
+in the direction of excess demand.
+
+.. code:: python
+    stage_list=[Send_Labour_Supply, Send_Labour_Demand, model.Adjust_Wage_Rate]
+    self.schedule = StagedActivation(self,stage_list,shuffle=True)
 
 Using ```numpy.random```
 -------------
diff --git a/mesa/time.py b/mesa/time.py
index da2ded48..f0816cd6 100644
--- a/mesa/time.py
+++ b/mesa/time.py
@@ -63,7 +63,7 @@ class BaseScheduler:
         """
         if agent.unique_id in self._agents:
             raise Exception(
-                f"Agent with unique id {repr(agent.unique_id)} already added to scheduler"
+                f"Agent with unique id {agent.unique_id!r} already added to scheduler"
             )
 
         self._agents[agent.unique_id] = agent
@@ -191,7 +191,10 @@ class StagedActivation(BaseScheduler):
         # it's necessary for the keys view to be a list.
         agent_keys = self.get_agent_keys(self.shuffle)
         for stage in self.stage_list:
-            self.do_each(stage, agent_keys=agent_keys)
+            if stage.startswith("model."):
+                getattr(self.model, stage[6:])()
+            else:
+                self.do_each(stage, agent_keys=agent_keys)
             # We recompute the keys because some agents might have been removed
             # in the previous loop.
             agent_keys = self.get_agent_keys(self.shuffle_between_stages)


Test patch:
diff --git a/tests/test_time.py b/tests/test_time.py
index 3014a290..60dbf506 100644
--- a/tests/test_time.py
+++ b/tests/test_time.py
@@ -75,7 +75,7 @@ class MockModel(Model):
 
         # Make scheduler
         if activation == STAGED:
-            model_stages = ["stage_one", "stage_two"]
+            model_stages = ["stage_one", "model.model_stage", "stage_two"]
             self.schedule = StagedActivation(self, model_stages, shuffle=shuffle)
         elif activation == RANDOM:
             self.schedule = RandomActivation(self)
@@ -94,13 +94,16 @@ class MockModel(Model):
     def step(self):
         self.schedule.step()
 
+    def model_stage(self):
+        self.log.append("model_stage")
+
 
 class TestStagedActivation(TestCase):
     """
     Test the staged activation.
     """
 
-    expected_output = ["A_1", "B_1", "A_2", "B_2"]
+    expected_output = ["A_1", "B_1", "model_stage", "A_2", "B_2"]
 
     def test_no_shuffle(self):
         """
@@ -109,7 +112,7 @@ class TestStagedActivation(TestCase):
         model = MockModel(shuffle=False)
         model.step()
         model.step()
-        assert all(i == j for i, j in zip(model.log[:4], model.log[4:]))
+        assert all(i == j for i, j in zip(model.log[:5], model.log[5:]))
 
     def test_shuffle(self):
         """
@@ -119,8 +122,9 @@ class TestStagedActivation(TestCase):
         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:]
+        for output in self.expected_output[3:]:
+            assert output in model.log[3:]
+        assert self.expected_output[2] == model.log[2]
 
     def test_shuffle_shuffles_agents(self):
         model = MockModel(shuffle=True)
@@ -146,7 +150,7 @@ class TestStagedActivation(TestCase):
         """
         model = MockModel(shuffle=True, enable_kill_other_agent=True)
         model.step()
-        assert len(model.log) == 2
+        assert len(model.log) == 3
 
     def test_add_existing_agent(self):
         model = MockModel()