Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch sugarscape to using property layers #2546

Merged
merged 3 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 12 additions & 65 deletions mesa/examples/advanced/sugarscape_g1mt/agents.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import math

from mesa.experimental.cell_space import CellAgent, FixedAgent
from mesa.experimental.cell_space import CellAgent


# Helper function
Expand All @@ -18,31 +18,6 @@ def get_distance(cell_1, cell_2):
return math.sqrt(dx**2 + dy**2)


class Resource(FixedAgent):
"""
Resource:
- contains an amount of sugar and spice
- grows 1 amount of sugar at each turn
- grows 1 amount of spice at each turn
"""

def __init__(self, model, max_sugar, max_spice, cell):
super().__init__(model)
self.sugar_amount = max_sugar
self.max_sugar = max_sugar
self.spice_amount = max_spice
self.max_spice = max_spice
self.cell = cell

def step(self):
"""
Growth function, adds one unit of sugar and spice each step up to
max amount
"""
self.sugar_amount = min([self.max_sugar, self.sugar_amount + 1])
self.spice_amount = min([self.max_spice, self.spice_amount + 1])


class Trader(CellAgent):
"""
Trader:
Expand Down Expand Up @@ -70,12 +45,6 @@ def __init__(
self.prices = []
self.trade_partners = []

def get_resource(self, cell):
for agent in cell.agents:
if isinstance(agent, Resource):
return agent
raise Exception(f"Resource agent not found in the position {cell.coordinate}")

def get_trader(self, cell):
"""
helper function used in self.trade_with_neighbors()
Expand All @@ -85,17 +54,6 @@ def get_trader(self, cell):
if isinstance(agent, Trader):
return agent

def is_occupied_by_other(self, cell):
"""
helper function part 1 of self.move()
"""

if cell is self.cell:
# agent's position is considered unoccupied as agent can stay there
return False
# get contents of each cell in neighborhood
return any(isinstance(a, Trader) for a in cell.agents)

def calculate_welfare(self, sugar, spice):
"""
helper function
Expand Down Expand Up @@ -264,15 +222,15 @@ def move(self):
neighboring_cells = [
cell
for cell in self.cell.get_neighborhood(self.vision, include_center=True)
if not self.is_occupied_by_other(cell)
if cell.is_empty
]

# 2. determine which move maximizes welfare

welfares = [
self.calculate_welfare(
self.sugar + self.get_resource(cell).sugar_amount,
self.spice + self.get_resource(cell).spice_amount,
self.sugar + cell.sugar,
self.spice + cell.spice,
)
for cell in neighboring_cells
]
Expand All @@ -282,6 +240,7 @@ def move(self):
# find the highest welfare in welfares
max_welfare = max(welfares)
# get the index of max welfare cells
# fixme: rewrite using enumerate and single loop
candidate_indices = [
i for i in range(len(welfares)) if math.isclose(welfares[i], max_welfare)
]
Expand All @@ -296,19 +255,17 @@ def move(self):
for cell in candidates
if math.isclose(get_distance(self.cell, cell), min_dist, rel_tol=1e-02)
]

# 4. Move Agent
self.cell = self.random.choice(final_candidates)

def eat(self):
patch = self.get_resource(self.cell)
if patch.sugar_amount > 0:
self.sugar += patch.sugar_amount
patch.sugar_amount = 0
self.sugar += self.cell.sugar
self.cell.sugar = 0
self.sugar -= self.metabolism_sugar

if patch.spice_amount > 0:
self.spice += patch.spice_amount
patch.spice_amount = 0
self.spice += self.cell.spice
self.cell.spice = 0
self.spice -= self.metabolism_spice

def maybe_die(self):
Expand All @@ -327,18 +284,8 @@ def trade_with_neighbors(self):
2- trade (2 sessions)
3- collect data
"""

neighbor_agents = [
self.get_trader(cell)
for cell in self.cell.get_neighborhood(radius=self.vision)
if self.is_occupied_by_other(cell)
]

if len(neighbor_agents) == 0:
return

# iterate through traders in neighboring cells and trade
for a in neighbor_agents:
# iterate through traders in neighboring cells and trade
for a in self.cell.get_neighborhood(radius=self.vision).agents:
self.trade(a)

return
40 changes: 23 additions & 17 deletions mesa/examples/advanced/sugarscape_g1mt/app.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
import os
import sys

sys.path.insert(0, os.path.abspath("../../../.."))


import numpy as np
import solara
from matplotlib.figure import Figure

from mesa.examples.advanced.sugarscape_g1mt.agents import Trader
from mesa.examples.advanced.sugarscape_g1mt.model import SugarscapeG1mt
from mesa.visualization import Slider, SolaraViz, make_plot_component


def SpaceDrawer(model):
def portray(g):
layers = {
"sugar": [[np.nan for j in range(g.height)] for i in range(g.width)],
"spice": [[np.nan for j in range(g.height)] for i in range(g.width)],
"trader": {"x": [], "y": [], "c": "tab:red", "marker": "o", "s": 10},
}

for agent in g.all_cells.agents:
i, j = agent.cell.coordinate
if isinstance(agent, Trader):
layers["trader"]["x"].append(i)
layers["trader"]["y"].append(j)
else:
# Don't visualize resource with value <= 1.
layers["sugar"][i][j] = (
agent.sugar_amount if agent.sugar_amount > 1 else np.nan
)
layers["spice"][i][j] = (
agent.spice_amount if agent.spice_amount > 1 else np.nan
)
layers["trader"]["x"].append(i)
layers["trader"]["y"].append(j)

Check warning on line 24 in mesa/examples/advanced/sugarscape_g1mt/app.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/advanced/sugarscape_g1mt/app.py#L23-L24

Added lines #L23 - L24 were not covered by tests
return layers

fig = Figure()
Expand All @@ -36,10 +30,18 @@
# Sugar
# Important note: imshow by default draws from upper left. You have to
# always explicitly specify origin="lower".
im = ax.imshow(out["sugar"], cmap="spring", origin="lower")
im = ax.imshow(

Check warning on line 33 in mesa/examples/advanced/sugarscape_g1mt/app.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/advanced/sugarscape_g1mt/app.py#L33

Added line #L33 was not covered by tests
np.ma.masked_where(model.grid.sugar.data <= 1, model.grid.sugar.data),
cmap="spring",
origin="lower",
)
fig.colorbar(im, orientation="vertical")
# Spice
ax.imshow(out["spice"], cmap="winter", origin="lower")
ax.imshow(

Check warning on line 40 in mesa/examples/advanced/sugarscape_g1mt/app.py

View check run for this annotation

Codecov / codecov/patch

mesa/examples/advanced/sugarscape_g1mt/app.py#L40

Added line #L40 was not covered by tests
np.ma.masked_where(model.grid.spice.data <= 1, model.grid.spice.data),
cmap="winter",
origin="lower",
)
# Trader
ax.scatter(**out["trader"])
ax.set_axis_off()
Expand Down Expand Up @@ -75,7 +77,11 @@

page = SolaraViz(
model,
components=[SpaceDrawer, make_plot_component(["Trader", "Price"])],
components=[
SpaceDrawer,
make_plot_component("#Traders"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious why "#Traders" and not Just "Traders"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the name of the reporter so I had to change it here. For me # means "number of".

make_plot_component("Price"),
],
model_params=model_params,
name="Sugarscape {G1, M, T}",
play_interval=150,
Expand Down
38 changes: 23 additions & 15 deletions mesa/examples/advanced/sugarscape_g1mt/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import numpy as np

import mesa
from mesa.examples.advanced.sugarscape_g1mt.agents import Resource, Trader
from mesa.examples.advanced.sugarscape_g1mt.agents import Trader
from mesa.experimental.cell_space import OrthogonalVonNeumannGrid
from mesa.experimental.cell_space.property_layer import PropertyLayer


# Helper Functions
Expand Down Expand Up @@ -58,8 +59,8 @@ def __init__(
# Initiate width and height of sugarscape
self.width = width
self.height = height
# Initiate population attributes

# Initiate population attributes
self.enable_trade = enable_trade
self.running = True

Expand All @@ -70,25 +71,25 @@ def __init__(
# initiate datacollector
self.datacollector = mesa.DataCollector(
model_reporters={
"Trader": lambda m: len(m.agents_by_type[Trader]),
"Trade Volume": lambda m: sum(
len(a.trade_partners) for a in m.agents_by_type[Trader]
),
"#Traders": lambda m: len(m.agents),
"Trade Volume": lambda m: sum(len(a.trade_partners) for a in m.agents),
"Price": lambda m: geometric_mean(
flatten([a.prices for a in m.agents_by_type[Trader]])
flatten([a.prices for a in m.agents])
),
},
agent_reporters={"Trade Network": lambda a: get_trade(a)},
)

# read in landscape file from supplmentary material
sugar_distribution = np.genfromtxt(Path(__file__).parent / "sugar-map.txt")
spice_distribution = np.flip(sugar_distribution, 1)
# read in landscape file from supplementary material
self.sugar_distribution = np.genfromtxt(Path(__file__).parent / "sugar-map.txt")
self.spice_distribution = np.flip(self.sugar_distribution, 1)

for cell in self.grid.all_cells:
max_sugar = sugar_distribution[cell.coordinate]
max_spice = spice_distribution[cell.coordinate]
Resource(self, max_sugar, max_spice, cell)
self.grid.add_property_layer(
PropertyLayer.from_data("sugar", self.sugar_distribution)
)
self.grid.add_property_layer(
PropertyLayer.from_data("spice", self.spice_distribution)
)

Trader.create_agents(
self,
Expand Down Expand Up @@ -117,7 +118,12 @@ def step(self):
and then randomly activates traders
"""
# step Resource agents
self.agents_by_type[Resource].do("step")
self.grid.sugar.data = np.minimum(
self.grid.sugar.data + 1, self.sugar_distribution
)
self.grid.spice.data = np.minimum(
self.grid.spice.data + 1, self.spice_distribution
)

# step trader agents
# to account for agent death and removal we need a separate data structure to
Expand All @@ -142,6 +148,8 @@ def step(self):
agent.trade_with_neighbors()

# collect model level data
# fixme we can already collect agent class data
# fixme, we don't have resource agents anymore so this can be done simpler
self.datacollector.collect(self)
"""
Mesa is working on updating datacollector agent reporter
Expand Down
Loading