Mesa 2.4.0__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of Mesa might be problematic. Click here for more details.
- mesa/__init__.py +3 -5
- mesa/agent.py +105 -92
- mesa/batchrunner.py +55 -31
- mesa/datacollection.py +10 -14
- mesa/examples/README.md +37 -0
- mesa/examples/__init__.py +21 -0
- mesa/examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +116 -0
- mesa/examples/advanced/epstein_civil_violence/Readme.md +34 -0
- mesa/examples/advanced/epstein_civil_violence/__init__.py +0 -0
- mesa/examples/advanced/epstein_civil_violence/agents.py +164 -0
- mesa/examples/advanced/epstein_civil_violence/app.py +73 -0
- mesa/examples/advanced/epstein_civil_violence/model.py +114 -0
- mesa/examples/advanced/pd_grid/Readme.md +43 -0
- mesa/examples/advanced/pd_grid/__init__.py +0 -0
- mesa/examples/advanced/pd_grid/agents.py +50 -0
- mesa/examples/advanced/pd_grid/analysis.ipynb +228 -0
- mesa/examples/advanced/pd_grid/app.py +54 -0
- mesa/examples/advanced/pd_grid/model.py +71 -0
- mesa/examples/advanced/sugarscape_g1mt/Readme.md +64 -0
- mesa/examples/advanced/sugarscape_g1mt/__init__.py +0 -0
- mesa/examples/advanced/sugarscape_g1mt/agents.py +344 -0
- mesa/examples/advanced/sugarscape_g1mt/app.py +62 -0
- mesa/examples/advanced/sugarscape_g1mt/model.py +180 -0
- mesa/examples/advanced/sugarscape_g1mt/sugar-map.txt +50 -0
- mesa/examples/advanced/sugarscape_g1mt/tests.py +69 -0
- mesa/examples/advanced/wolf_sheep/Readme.md +57 -0
- mesa/examples/advanced/wolf_sheep/__init__.py +0 -0
- mesa/examples/advanced/wolf_sheep/agents.py +102 -0
- mesa/examples/advanced/wolf_sheep/app.py +84 -0
- mesa/examples/advanced/wolf_sheep/model.py +137 -0
- mesa/examples/basic/__init__.py +0 -0
- mesa/examples/basic/boid_flockers/Readme.md +22 -0
- mesa/examples/basic/boid_flockers/__init__.py +0 -0
- mesa/examples/basic/boid_flockers/agents.py +71 -0
- mesa/examples/basic/boid_flockers/app.py +58 -0
- mesa/examples/basic/boid_flockers/model.py +69 -0
- mesa/examples/basic/boltzmann_wealth_model/Readme.md +56 -0
- mesa/examples/basic/boltzmann_wealth_model/__init__.py +0 -0
- mesa/examples/basic/boltzmann_wealth_model/agents.py +31 -0
- mesa/examples/basic/boltzmann_wealth_model/app.py +74 -0
- mesa/examples/basic/boltzmann_wealth_model/model.py +43 -0
- mesa/examples/basic/boltzmann_wealth_model/st_app.py +115 -0
- mesa/examples/basic/conways_game_of_life/Readme.md +39 -0
- mesa/examples/basic/conways_game_of_life/__init__.py +0 -0
- mesa/examples/basic/conways_game_of_life/agents.py +47 -0
- mesa/examples/basic/conways_game_of_life/app.py +51 -0
- mesa/examples/basic/conways_game_of_life/model.py +31 -0
- mesa/examples/basic/conways_game_of_life/st_app.py +72 -0
- mesa/examples/basic/schelling/Readme.md +40 -0
- mesa/examples/basic/schelling/__init__.py +0 -0
- mesa/examples/basic/schelling/agents.py +26 -0
- mesa/examples/basic/schelling/analysis.ipynb +205 -0
- mesa/examples/basic/schelling/app.py +42 -0
- mesa/examples/basic/schelling/model.py +59 -0
- mesa/examples/basic/virus_on_network/Readme.md +61 -0
- mesa/examples/basic/virus_on_network/__init__.py +0 -0
- mesa/examples/basic/virus_on_network/agents.py +69 -0
- mesa/examples/basic/virus_on_network/app.py +114 -0
- mesa/examples/basic/virus_on_network/model.py +96 -0
- mesa/experimental/UserParam.py +18 -7
- mesa/experimental/__init__.py +10 -2
- mesa/experimental/cell_space/__init__.py +16 -1
- mesa/experimental/cell_space/cell.py +93 -23
- mesa/experimental/cell_space/cell_agent.py +117 -21
- mesa/experimental/cell_space/cell_collection.py +56 -19
- mesa/experimental/cell_space/discrete_space.py +92 -8
- mesa/experimental/cell_space/grid.py +33 -9
- mesa/experimental/cell_space/network.py +15 -10
- mesa/experimental/cell_space/voronoi.py +257 -0
- mesa/experimental/components/altair.py +11 -2
- mesa/experimental/components/matplotlib.py +132 -26
- mesa/experimental/devs/__init__.py +2 -0
- mesa/experimental/devs/eventlist.py +54 -15
- mesa/experimental/devs/examples/epstein_civil_violence.py +69 -38
- mesa/experimental/devs/examples/wolf_sheep.py +42 -43
- mesa/experimental/devs/simulator.py +57 -16
- mesa/experimental/{jupyter_viz.py → solara_viz.py} +151 -99
- mesa/model.py +136 -78
- mesa/space.py +208 -148
- mesa/time.py +63 -80
- mesa/visualization/__init__.py +25 -6
- mesa/visualization/components/__init__.py +83 -0
- mesa/visualization/components/altair_components.py +188 -0
- mesa/visualization/components/matplotlib_components.py +175 -0
- mesa/visualization/mpl_space_drawing.py +593 -0
- mesa/visualization/solara_viz.py +458 -0
- mesa/visualization/user_param.py +69 -0
- mesa/visualization/utils.py +9 -0
- {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/METADATA +62 -17
- mesa-3.0.0.dist-info/RECORD +95 -0
- mesa-3.0.0.dist-info/licenses/LICENSE +202 -0
- mesa-2.4.0.dist-info/licenses/LICENSE → mesa-3.0.0.dist-info/licenses/NOTICE +2 -2
- mesa/cookiecutter-mesa/cookiecutter.json +0 -8
- mesa/cookiecutter-mesa/hooks/post_gen_project.py +0 -11
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +0 -4
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.pytemplate +0 -3
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +0 -11
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +0 -60
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate +0 -36
- mesa/flat/__init__.py +0 -6
- mesa/flat/visualization.py +0 -5
- mesa/main.py +0 -63
- mesa/visualization/ModularVisualization.py +0 -1
- mesa/visualization/TextVisualization.py +0 -1
- mesa/visualization/UserParam.py +0 -1
- mesa/visualization/modules.py +0 -1
- mesa-2.4.0.dist-info/RECORD +0 -45
- /mesa/{cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}} → examples/advanced}/__init__.py +0 -0
- {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/WHEEL +0 -0
- {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Solara-based visualization for the Spatial Prisoner's Dilemma Model.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from mesa.examples.advanced.pd_grid.model import PdGrid
|
|
6
|
+
from mesa.visualization import (
|
|
7
|
+
Slider,
|
|
8
|
+
SolaraViz,
|
|
9
|
+
make_plot_component,
|
|
10
|
+
make_space_component,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def pd_agent_portrayal(agent):
|
|
15
|
+
"""
|
|
16
|
+
Portrayal function for rendering PD agents in the visualization.
|
|
17
|
+
"""
|
|
18
|
+
return {
|
|
19
|
+
"color": "blue" if agent.move == "C" else "red",
|
|
20
|
+
"marker": "s", # square marker
|
|
21
|
+
"size": 25,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Model parameters
|
|
26
|
+
model_params = {
|
|
27
|
+
"width": Slider("Grid Width", value=50, min=10, max=100, step=1),
|
|
28
|
+
"height": Slider("Grid Height", value=50, min=10, max=100, step=1),
|
|
29
|
+
"activation_order": {
|
|
30
|
+
"type": "Select",
|
|
31
|
+
"value": "Random",
|
|
32
|
+
"values": PdGrid.activation_regimes,
|
|
33
|
+
"label": "Activation Regime",
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Create grid visualization component using Altair
|
|
39
|
+
grid_viz = make_space_component(agent_portrayal=pd_agent_portrayal)
|
|
40
|
+
|
|
41
|
+
# Create plot for tracking cooperating agents over time
|
|
42
|
+
plot_component = make_plot_component("Cooperating_Agents")
|
|
43
|
+
|
|
44
|
+
# Initialize model
|
|
45
|
+
initial_model = PdGrid()
|
|
46
|
+
|
|
47
|
+
# Create visualization with all components
|
|
48
|
+
page = SolaraViz(
|
|
49
|
+
model=initial_model,
|
|
50
|
+
components=[grid_viz, plot_component],
|
|
51
|
+
model_params=model_params,
|
|
52
|
+
name="Spatial Prisoner's Dilemma",
|
|
53
|
+
)
|
|
54
|
+
page # noqa B018
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import mesa
|
|
2
|
+
from mesa.examples.advanced.pd_grid.agents import PDAgent
|
|
3
|
+
from mesa.experimental.cell_space import OrthogonalMooreGrid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PdGrid(mesa.Model):
|
|
7
|
+
"""Model class for iterated, spatial prisoner's dilemma model."""
|
|
8
|
+
|
|
9
|
+
activation_regimes = ["Sequential", "Random", "Simultaneous"]
|
|
10
|
+
|
|
11
|
+
# This dictionary holds the payoff for this agent,
|
|
12
|
+
# keyed on: (my_move, other_move)
|
|
13
|
+
|
|
14
|
+
payoff = {("C", "C"): 1, ("C", "D"): 0, ("D", "C"): 1.6, ("D", "D"): 0}
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self, width=50, height=50, activation_order="Random", payoffs=None, seed=None
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Create a new Spatial Prisoners' Dilemma Model.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
width, height: Grid size. There will be one agent per grid cell.
|
|
24
|
+
activation_order: Can be "Sequential", "Random", or "Simultaneous".
|
|
25
|
+
Determines the agent activation regime.
|
|
26
|
+
payoffs: (optional) Dictionary of (move, neighbor_move) payoffs.
|
|
27
|
+
"""
|
|
28
|
+
super().__init__(seed=seed)
|
|
29
|
+
self.activation_order = activation_order
|
|
30
|
+
self.grid = OrthogonalMooreGrid((width, height), torus=True)
|
|
31
|
+
|
|
32
|
+
if payoffs is not None:
|
|
33
|
+
self.payoff = payoffs
|
|
34
|
+
|
|
35
|
+
# Create agents
|
|
36
|
+
for x in range(width):
|
|
37
|
+
for y in range(height):
|
|
38
|
+
agent = PDAgent(self)
|
|
39
|
+
agent.cell = self.grid[(x, y)]
|
|
40
|
+
|
|
41
|
+
self.datacollector = mesa.DataCollector(
|
|
42
|
+
{
|
|
43
|
+
"Cooperating_Agents": lambda m: len(
|
|
44
|
+
[a for a in m.agents if a.move == "C"]
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
self.running = True
|
|
50
|
+
self.datacollector.collect(self)
|
|
51
|
+
|
|
52
|
+
def step(self):
|
|
53
|
+
# Activate all agents, based on the activation regime
|
|
54
|
+
match self.activation_order:
|
|
55
|
+
case "Sequential":
|
|
56
|
+
self.agents.do("step")
|
|
57
|
+
case "Random":
|
|
58
|
+
self.agents.shuffle_do("step")
|
|
59
|
+
case "Simultaneous":
|
|
60
|
+
self.agents.do("step")
|
|
61
|
+
self.agents.do("advance")
|
|
62
|
+
case _:
|
|
63
|
+
raise ValueError(f"Unknown activation order: {self.activation_order}")
|
|
64
|
+
|
|
65
|
+
# Collect data
|
|
66
|
+
self.datacollector.collect(self)
|
|
67
|
+
|
|
68
|
+
def run(self, n):
|
|
69
|
+
"""Run the model for n steps."""
|
|
70
|
+
for _ in range(n):
|
|
71
|
+
self.step()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Sugarscape Constant Growback Model with Traders
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
This is Epstein & Axtell's Sugarscape model with Traders, a detailed description is in Chapter four of
|
|
6
|
+
*Growing Artificial Societies: Social Science from the Bottom Up (1996)*. The model shows an emergent price equilibrium can happen via a decentralized dynamics.
|
|
7
|
+
|
|
8
|
+
This code generally matches the code in the Complexity Explorer Tutorial, but in `.py` instead of `.ipynb` format.
|
|
9
|
+
|
|
10
|
+
### Agents:
|
|
11
|
+
|
|
12
|
+
- **Resource**: Resource agents grow back at one unit of sugar and spice per time step up to a specified max amount and can be harvested and traded by the trader agents.
|
|
13
|
+
(if you do the interactive run, the color will be green if the resource agent has a bigger amount of sugar, or yellow if it has a bigger amount of spice)
|
|
14
|
+
- **Traders**: Trader agents have the following attributes: (1) metabolism for sugar, (2) metabolism for spice, (3) vision,
|
|
15
|
+
(4) initial sugar endowment and (5) initial spice endowment. The traverse the landscape harvesting sugar and spice and
|
|
16
|
+
trading with other agents. If they run out of sugar or spice then they are removed from the model. (red circle if you do the interactive run)
|
|
17
|
+
|
|
18
|
+
The trader agents traverse the landscape according to rule **M**:
|
|
19
|
+
- Look out as far as vision permits in the four principal lattice directions and identify the unoccupied site(s).
|
|
20
|
+
- Considering only unoccupied sites find the nearest position that produces the most welfare using the Cobb-Douglas function.
|
|
21
|
+
- Move to the new position
|
|
22
|
+
- Collect all the resources (sugar and spice) at that location
|
|
23
|
+
(Epstein and Axtell, 1996, p. 99)
|
|
24
|
+
|
|
25
|
+
The traders trade according to rule **T**:
|
|
26
|
+
- Agents and potential trade partner compute their marginal rates of substitution (MRS), if they are equal *end*.
|
|
27
|
+
- Exchange resources, with spice flowing from the agent with the higher MRS to the agent with the lower MRS and sugar
|
|
28
|
+
flowing the opposite direction.
|
|
29
|
+
- The price (p) is calculated by taking the geometric mean of the agents' MRS.
|
|
30
|
+
- If p > 1 then p units of spice are traded for 1 unit of sugar; if p < 1 then 1/p units of sugar for 1 unit of spice
|
|
31
|
+
- The trade occurs if it will (a) make both agent better off (increases MRS) and (b) does not cause the agents' MRS to
|
|
32
|
+
cross over one another otherwise *end*.
|
|
33
|
+
- This process then repeats until an *end* condition is met.
|
|
34
|
+
(Epstein and Axtell, 1996, p. 105)
|
|
35
|
+
|
|
36
|
+
The model demonstrates several Mesa concepts and features:
|
|
37
|
+
- OrthogonalMooreGrid
|
|
38
|
+
- Multiple agent types (traders, sugar, spice)
|
|
39
|
+
- Dynamically removing agents from the grid and schedule when they die
|
|
40
|
+
- Data Collection at the model and agent level
|
|
41
|
+
- custom solara matplotlib space visualization
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## How to Run
|
|
45
|
+
To run the model interactively:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
$ solara run app.py
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run.
|
|
52
|
+
|
|
53
|
+
## Files
|
|
54
|
+
|
|
55
|
+
* `model.py`: The Sugarscape Constant Growback with Traders model.
|
|
56
|
+
* `agents.py`: Defines the Trader agent class and the Resource agent class which contains an amount of sugar and spice.
|
|
57
|
+
* `app.py`: Runs a visualization server via Solara (`solara run app.py`).
|
|
58
|
+
* `sugar_map.txt`: Provides sugar and spice landscape in raster type format.
|
|
59
|
+
* `tests.py`: Has tests to ensure that the model reproduces the results in shown in Growing Artificial Societies.
|
|
60
|
+
|
|
61
|
+
## Additional Resources
|
|
62
|
+
|
|
63
|
+
- [Growing Artificial Societies](https://mitpress.mit.edu/9780262550253/growing-artificial-societies/)
|
|
64
|
+
- [Complexity Explorer Sugarscape with Traders Tutorial](https://www.complexityexplorer.org/courses/172-agent-based-models-with-python-an-introduction-to-mesa)
|
|
File without changes
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
from mesa.experimental.cell_space import CellAgent, FixedAgent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Helper function
|
|
7
|
+
def get_distance(cell_1, cell_2):
|
|
8
|
+
"""
|
|
9
|
+
Calculate the Euclidean distance between two positions
|
|
10
|
+
|
|
11
|
+
used in trade.move()
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
x1, y1 = cell_1.coordinate
|
|
15
|
+
x2, y2 = cell_2.coordinate
|
|
16
|
+
dx = x1 - x2
|
|
17
|
+
dy = y1 - y2
|
|
18
|
+
return math.sqrt(dx**2 + dy**2)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Resource(FixedAgent):
|
|
22
|
+
"""
|
|
23
|
+
Resource:
|
|
24
|
+
- contains an amount of sugar and spice
|
|
25
|
+
- grows 1 amount of sugar at each turn
|
|
26
|
+
- grows 1 amount of spice at each turn
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, model, max_sugar, max_spice, cell):
|
|
30
|
+
super().__init__(model)
|
|
31
|
+
self.sugar_amount = max_sugar
|
|
32
|
+
self.max_sugar = max_sugar
|
|
33
|
+
self.spice_amount = max_spice
|
|
34
|
+
self.max_spice = max_spice
|
|
35
|
+
self.cell = cell
|
|
36
|
+
|
|
37
|
+
def step(self):
|
|
38
|
+
"""
|
|
39
|
+
Growth function, adds one unit of sugar and spice each step up to
|
|
40
|
+
max amount
|
|
41
|
+
"""
|
|
42
|
+
self.sugar_amount = min([self.max_sugar, self.sugar_amount + 1])
|
|
43
|
+
self.spice_amount = min([self.max_spice, self.spice_amount + 1])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Trader(CellAgent):
|
|
47
|
+
"""
|
|
48
|
+
Trader:
|
|
49
|
+
- has a metabolism of sugar and spice
|
|
50
|
+
- harvest and trade sugar and spice to survive
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
model,
|
|
56
|
+
cell,
|
|
57
|
+
sugar=0,
|
|
58
|
+
spice=0,
|
|
59
|
+
metabolism_sugar=0,
|
|
60
|
+
metabolism_spice=0,
|
|
61
|
+
vision=0,
|
|
62
|
+
):
|
|
63
|
+
super().__init__(model)
|
|
64
|
+
self.cell = cell
|
|
65
|
+
self.sugar = sugar
|
|
66
|
+
self.spice = spice
|
|
67
|
+
self.metabolism_sugar = metabolism_sugar
|
|
68
|
+
self.metabolism_spice = metabolism_spice
|
|
69
|
+
self.vision = vision
|
|
70
|
+
self.prices = []
|
|
71
|
+
self.trade_partners = []
|
|
72
|
+
|
|
73
|
+
def get_resource(self, cell):
|
|
74
|
+
for agent in cell.agents:
|
|
75
|
+
if isinstance(agent, Resource):
|
|
76
|
+
return agent
|
|
77
|
+
raise Exception(f"Resource agent not found in the position {cell.coordinate}")
|
|
78
|
+
|
|
79
|
+
def get_trader(self, cell):
|
|
80
|
+
"""
|
|
81
|
+
helper function used in self.trade_with_neighbors()
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
for agent in cell.agents:
|
|
85
|
+
if isinstance(agent, Trader):
|
|
86
|
+
return agent
|
|
87
|
+
|
|
88
|
+
def is_occupied_by_other(self, cell):
|
|
89
|
+
"""
|
|
90
|
+
helper function part 1 of self.move()
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
if cell is self.cell:
|
|
94
|
+
# agent's position is considered unoccupied as agent can stay there
|
|
95
|
+
return False
|
|
96
|
+
# get contents of each cell in neighborhood
|
|
97
|
+
return any(isinstance(a, Trader) for a in cell.agents)
|
|
98
|
+
|
|
99
|
+
def calculate_welfare(self, sugar, spice):
|
|
100
|
+
"""
|
|
101
|
+
helper function
|
|
102
|
+
|
|
103
|
+
part 2 self.move()
|
|
104
|
+
self.trade()
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# calculate total resources
|
|
108
|
+
m_total = self.metabolism_sugar + self.metabolism_spice
|
|
109
|
+
# Cobb-Douglas functional form; starting on p. 97
|
|
110
|
+
# on Growing Artificial Societies
|
|
111
|
+
return sugar ** (self.metabolism_sugar / m_total) * spice ** (
|
|
112
|
+
self.metabolism_spice / m_total
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def is_starved(self):
|
|
116
|
+
"""
|
|
117
|
+
Helper function for self.maybe_die()
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
return (self.sugar <= 0) or (self.spice <= 0)
|
|
121
|
+
|
|
122
|
+
def calculate_MRS(self, sugar, spice):
|
|
123
|
+
"""
|
|
124
|
+
Helper function for
|
|
125
|
+
- self.trade()
|
|
126
|
+
- self.maybe_self_spice()
|
|
127
|
+
|
|
128
|
+
Determines what trader agent needs and can give up
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
return (spice / self.metabolism_spice) / (sugar / self.metabolism_sugar)
|
|
132
|
+
|
|
133
|
+
def calculate_sell_spice_amount(self, price):
|
|
134
|
+
"""
|
|
135
|
+
helper function for self.maybe_sell_spice() which is called from
|
|
136
|
+
self.trade()
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
if price >= 1:
|
|
140
|
+
sugar = 1
|
|
141
|
+
spice = int(price)
|
|
142
|
+
else:
|
|
143
|
+
sugar = int(1 / price)
|
|
144
|
+
spice = 1
|
|
145
|
+
return sugar, spice
|
|
146
|
+
|
|
147
|
+
def sell_spice(self, other, sugar, spice):
|
|
148
|
+
"""
|
|
149
|
+
used in self.maybe_sell_spice()
|
|
150
|
+
|
|
151
|
+
exchanges sugar and spice between traders
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
self.sugar += sugar
|
|
155
|
+
other.sugar -= sugar
|
|
156
|
+
self.spice -= spice
|
|
157
|
+
other.spice += spice
|
|
158
|
+
|
|
159
|
+
def maybe_sell_spice(self, other, price, welfare_self, welfare_other):
|
|
160
|
+
"""
|
|
161
|
+
helper function for self.trade()
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
sugar_exchanged, spice_exchanged = self.calculate_sell_spice_amount(price)
|
|
165
|
+
|
|
166
|
+
# Assess new sugar and spice amount - what if change did occur
|
|
167
|
+
self_sugar = self.sugar + sugar_exchanged
|
|
168
|
+
other_sugar = other.sugar - sugar_exchanged
|
|
169
|
+
self_spice = self.spice - spice_exchanged
|
|
170
|
+
other_spice = other.spice + spice_exchanged
|
|
171
|
+
|
|
172
|
+
# double check to ensure agents have resources
|
|
173
|
+
|
|
174
|
+
if (
|
|
175
|
+
(self_sugar <= 0)
|
|
176
|
+
or (other_sugar <= 0)
|
|
177
|
+
or (self_spice <= 0)
|
|
178
|
+
or (other_spice <= 0)
|
|
179
|
+
):
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
# trade criteria #1 - are both agents better off?
|
|
183
|
+
both_agents_better_off = (
|
|
184
|
+
welfare_self < self.calculate_welfare(self_sugar, self_spice)
|
|
185
|
+
) and (welfare_other < other.calculate_welfare(other_sugar, other_spice))
|
|
186
|
+
|
|
187
|
+
# trade criteria #2 is their mrs crossing with potential trade
|
|
188
|
+
mrs_not_crossing = self.calculate_MRS(
|
|
189
|
+
self_sugar, self_spice
|
|
190
|
+
) > other.calculate_MRS(other_sugar, other_spice)
|
|
191
|
+
|
|
192
|
+
if not (both_agents_better_off and mrs_not_crossing):
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
# criteria met, execute trade
|
|
196
|
+
self.sell_spice(other, sugar_exchanged, spice_exchanged)
|
|
197
|
+
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
def trade(self, other):
|
|
201
|
+
"""
|
|
202
|
+
helper function used in trade_with_neighbors()
|
|
203
|
+
|
|
204
|
+
other is a trader agent object
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
# sanity check to verify code is working as expected
|
|
208
|
+
assert self.sugar > 0
|
|
209
|
+
assert self.spice > 0
|
|
210
|
+
assert other.sugar > 0
|
|
211
|
+
assert other.spice > 0
|
|
212
|
+
|
|
213
|
+
# calculate marginal rate of substitution in Growing Artificial Societies p. 101
|
|
214
|
+
mrs_self = self.calculate_MRS(self.sugar, self.spice)
|
|
215
|
+
mrs_other = other.calculate_MRS(other.sugar, other.spice)
|
|
216
|
+
|
|
217
|
+
# calculate each agents welfare
|
|
218
|
+
welfare_self = self.calculate_welfare(self.sugar, self.spice)
|
|
219
|
+
welfare_other = other.calculate_welfare(other.sugar, other.spice)
|
|
220
|
+
|
|
221
|
+
if math.isclose(mrs_self, mrs_other):
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
# calculate price
|
|
225
|
+
price = math.sqrt(mrs_self * mrs_other)
|
|
226
|
+
|
|
227
|
+
if mrs_self > mrs_other:
|
|
228
|
+
# self is a sugar buyer, spice seller
|
|
229
|
+
sold = self.maybe_sell_spice(other, price, welfare_self, welfare_other)
|
|
230
|
+
# no trade - criteria not met
|
|
231
|
+
if not sold:
|
|
232
|
+
return
|
|
233
|
+
else:
|
|
234
|
+
# self is a spice buyer, sugar seller
|
|
235
|
+
sold = other.maybe_sell_spice(self, price, welfare_other, welfare_self)
|
|
236
|
+
# no trade - criteria not met
|
|
237
|
+
if not sold:
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
# Capture data
|
|
241
|
+
self.prices.append(price)
|
|
242
|
+
self.trade_partners.append(other.unique_id)
|
|
243
|
+
|
|
244
|
+
# continue trading
|
|
245
|
+
self.trade(other)
|
|
246
|
+
|
|
247
|
+
######################################################################
|
|
248
|
+
# #
|
|
249
|
+
# MAIN TRADE FUNCTIONS #
|
|
250
|
+
# #
|
|
251
|
+
######################################################################
|
|
252
|
+
|
|
253
|
+
def move(self):
|
|
254
|
+
"""
|
|
255
|
+
Function for trader agent to identify optimal move for each step in 4 parts
|
|
256
|
+
1 - identify all possible moves
|
|
257
|
+
2 - determine which move maximizes welfare
|
|
258
|
+
3 - find closest best option
|
|
259
|
+
4 - move
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
# 1. identify all possible moves
|
|
263
|
+
|
|
264
|
+
neighboring_cells = [
|
|
265
|
+
cell
|
|
266
|
+
for cell in self.cell.get_neighborhood(self.vision, include_center=True)
|
|
267
|
+
if not self.is_occupied_by_other(cell)
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
# 2. determine which move maximizes welfare
|
|
271
|
+
|
|
272
|
+
welfares = [
|
|
273
|
+
self.calculate_welfare(
|
|
274
|
+
self.sugar + self.get_resource(cell).sugar_amount,
|
|
275
|
+
self.spice + self.get_resource(cell).spice_amount,
|
|
276
|
+
)
|
|
277
|
+
for cell in neighboring_cells
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
# 3. Find closest best option
|
|
281
|
+
|
|
282
|
+
# find the highest welfare in welfares
|
|
283
|
+
max_welfare = max(welfares)
|
|
284
|
+
# get the index of max welfare cells
|
|
285
|
+
candidate_indices = [
|
|
286
|
+
i for i in range(len(welfares)) if math.isclose(welfares[i], max_welfare)
|
|
287
|
+
]
|
|
288
|
+
|
|
289
|
+
# convert index to positions of those cells
|
|
290
|
+
candidates = [neighboring_cells[i] for i in candidate_indices]
|
|
291
|
+
|
|
292
|
+
min_dist = min(get_distance(self.cell, cell) for cell in candidates)
|
|
293
|
+
|
|
294
|
+
final_candidates = [
|
|
295
|
+
cell
|
|
296
|
+
for cell in candidates
|
|
297
|
+
if math.isclose(get_distance(self.cell, cell), min_dist, rel_tol=1e-02)
|
|
298
|
+
]
|
|
299
|
+
# 4. Move Agent
|
|
300
|
+
self.cell = self.random.choice(final_candidates)
|
|
301
|
+
|
|
302
|
+
def eat(self):
|
|
303
|
+
patch = self.get_resource(self.cell)
|
|
304
|
+
if patch.sugar_amount > 0:
|
|
305
|
+
self.sugar += patch.sugar_amount
|
|
306
|
+
patch.sugar_amount = 0
|
|
307
|
+
self.sugar -= self.metabolism_sugar
|
|
308
|
+
|
|
309
|
+
if patch.spice_amount > 0:
|
|
310
|
+
self.spice += patch.spice_amount
|
|
311
|
+
patch.spice_amount = 0
|
|
312
|
+
self.spice -= self.metabolism_spice
|
|
313
|
+
|
|
314
|
+
def maybe_die(self):
|
|
315
|
+
"""
|
|
316
|
+
Function to remove Traders who have consumed all their sugar or spice
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
if self.is_starved():
|
|
320
|
+
self.remove()
|
|
321
|
+
|
|
322
|
+
def trade_with_neighbors(self):
|
|
323
|
+
"""
|
|
324
|
+
Function for trader agents to decide who to trade with in three parts
|
|
325
|
+
|
|
326
|
+
1- identify neighbors who can trade
|
|
327
|
+
2- trade (2 sessions)
|
|
328
|
+
3- collect data
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
neighbor_agents = [
|
|
332
|
+
self.get_trader(cell)
|
|
333
|
+
for cell in self.cell.get_neighborhood(radius=self.vision)
|
|
334
|
+
if self.is_occupied_by_other(cell)
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
if len(neighbor_agents) == 0:
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
# iterate through traders in neighboring cells and trade
|
|
341
|
+
for a in neighbor_agents:
|
|
342
|
+
self.trade(a)
|
|
343
|
+
|
|
344
|
+
return
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import solara
|
|
3
|
+
from matplotlib.figure import Figure
|
|
4
|
+
|
|
5
|
+
from mesa.examples.advanced.sugarscape_g1mt.agents import Trader
|
|
6
|
+
from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt
|
|
7
|
+
from mesa.visualization import SolaraViz, make_plot_component
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def SpaceDrawer(model):
|
|
11
|
+
def portray(g):
|
|
12
|
+
layers = {
|
|
13
|
+
"sugar": [[np.nan for j in range(g.height)] for i in range(g.width)],
|
|
14
|
+
"spice": [[np.nan for j in range(g.height)] for i in range(g.width)],
|
|
15
|
+
"trader": {"x": [], "y": [], "c": "tab:red", "marker": "o", "s": 10},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for agent in g.all_cells.agents:
|
|
19
|
+
i, j = agent.cell.coordinate
|
|
20
|
+
if isinstance(agent, Trader):
|
|
21
|
+
layers["trader"]["x"].append(i)
|
|
22
|
+
layers["trader"]["y"].append(j)
|
|
23
|
+
else:
|
|
24
|
+
# Don't visualize resource with value <= 1.
|
|
25
|
+
layers["sugar"][i][j] = (
|
|
26
|
+
agent.sugar_amount if agent.sugar_amount > 1 else np.nan
|
|
27
|
+
)
|
|
28
|
+
layers["spice"][i][j] = (
|
|
29
|
+
agent.spice_amount if agent.spice_amount > 1 else np.nan
|
|
30
|
+
)
|
|
31
|
+
return layers
|
|
32
|
+
|
|
33
|
+
fig = Figure()
|
|
34
|
+
ax = fig.subplots()
|
|
35
|
+
out = portray(model.grid)
|
|
36
|
+
# Sugar
|
|
37
|
+
# Important note: imshow by default draws from upper left. You have to
|
|
38
|
+
# always explicitly specify origin="lower".
|
|
39
|
+
im = ax.imshow(out["sugar"], cmap="spring", origin="lower")
|
|
40
|
+
fig.colorbar(im, orientation="vertical")
|
|
41
|
+
# Spice
|
|
42
|
+
ax.imshow(out["spice"], cmap="winter", origin="lower")
|
|
43
|
+
# Trader
|
|
44
|
+
ax.scatter(**out["trader"])
|
|
45
|
+
ax.set_axis_off()
|
|
46
|
+
return solara.FigureMatplotlib(fig)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
model_params = {
|
|
50
|
+
"width": 50,
|
|
51
|
+
"height": 50,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
model1 = SugarscapeG1mt(50, 50)
|
|
55
|
+
|
|
56
|
+
page = SolaraViz(
|
|
57
|
+
model1,
|
|
58
|
+
components=[SpaceDrawer, make_plot_component(["Trader", "Price"])],
|
|
59
|
+
name="Sugarscape {G1, M, T}",
|
|
60
|
+
play_interval=150,
|
|
61
|
+
)
|
|
62
|
+
page # noqa
|