diff --git a/examples/aco_tsp/aco_tsp/model.py b/examples/aco_tsp/aco_tsp/model.py index a7b44f74..0b08ba93 100644 --- a/examples/aco_tsp/aco_tsp/model.py +++ b/examples/aco_tsp/aco_tsp/model.py @@ -3,6 +3,7 @@ import mesa import networkx as nx import numpy as np +from mesa.experimental.cell_space import CellAgent, Network @dataclass @@ -77,7 +78,7 @@ def from_tsp_file(cls, file_path: str) -> "TSPGraph": return cls(g) -class AntTSP(mesa.Agent): +class AntTSP(CellAgent): """ An agent """ @@ -93,6 +94,7 @@ def __init__(self, model, alpha: float = 1.0, beta: float = 5.0): self._traveled_distance = 0 self.tsp_solution = [] self.tsp_distance = 0 + self.graph = self.model.grid.G def calculate_pheromone_delta(self, q: float = 100): results = {} @@ -102,23 +104,31 @@ def calculate_pheromone_delta(self, q: float = 100): return results + def move_to(self, cell) -> None: + self._cities_visited.append(cell) + if self.cell: + self._traveled_distance += self.graph[self.cell.coordinate][ + cell.coordinate + ]["distance"] + super().move_to(cell) + def decide_next_city(self): # Random # new_city = self.random.choice(list(self.model.all_cities - set(self.cities_visited))) # Choose closest city not yet visited - g = self.model.grid.G - current_city = self.pos - neighbors = list(g.neighbors(current_city)) + neighbors = self.cell.neighborhood candidates = [n for n in neighbors if n not in self._cities_visited] if len(candidates) == 0: - return current_city + return self.cell # p_ij(t) = 1/Z*[(tau_ij)**alpha * (1/distance)**beta] results = [] for city in candidates: val = ( - (g[current_city][city]["pheromone"]) ** self.alpha - * (g[current_city][city]["visibility"]) ** self.beta + (self.graph[self.cell.coordinate][city.coordinate]["pheromone"]) + ** self.alpha + * (self.graph[self.cell.coordinate][city.coordinate]["visibility"]) + ** self.beta ) results.append(val) @@ -126,7 +136,7 @@ def decide_next_city(self): norm = results.sum() results /= norm - new_city = self.model.random.choices(candidates, weights=results)[0] + new_city = self.random.choices(candidates, weights=results)[0] return new_city @@ -135,16 +145,13 @@ def step(self): Modify this method to change what an individual agent will do during each step. Can include logic based on neighbors states. """ - g = self.model.grid.G - for idx in range(self.model.num_cities - 1): + + for _ in range(self.model.num_cities - 1): # Pick a random city that isn't in the list of cities visited - current_city = self.pos new_city = self.decide_next_city() - self._cities_visited.append(new_city) - self.model.grid.move_agent(self, new_city) - self._traveled_distance += g[current_city][new_city]["distance"] + self.move_to(new_city) - self.tsp_solution = self._cities_visited.copy() + self.tsp_solution = [entry.coordinate for entry in self._cities_visited] self.tsp_distance = self._traveled_distance self._cities_visited = [] self._traveled_distance = 0 @@ -173,14 +180,15 @@ def __init__( self.num_cities = tsp_graph.num_cities self.all_cities = set(range(self.num_cities)) self.max_steps = max_steps - self.grid = mesa.space.NetworkGrid(tsp_graph.g) + self.grid = Network(tsp_graph.g, random=self.random) for _ in range(self.num_agents): agent = AntTSP(model=self, alpha=ant_alpha, beta=ant_beta) - city = tsp_graph.cities[self.random.randrange(self.num_cities)] - self.grid.place_agent(agent, city) - agent._cities_visited.append(city) + city = self.grid.all_cells.select_random_cell() + agent.move_to(city) + # self.grid.place_agent(agent, city) + # agent._cities_visited.append(city) # FIXME should be endogenous to agent self.num_steps = 0 self.best_path = None diff --git a/examples/bank_reserves/BankReservesModel_Data.csv b/examples/bank_reserves/BankReservesModel_Data.csv new file mode 100644 index 00000000..f9596ef9 --- /dev/null +++ b/examples/bank_reserves/BankReservesModel_Data.csv @@ -0,0 +1,251 @@ +,RunId,iteration,Step,init_people,rich_threshold,reserve_percent,Rich,Poor,Middle Class,Savings,Wallets,Money,Loans,AgentID,Wealth +0,0,0,1000,25,5,5,11,5,7,251,0,251,173,1,2 +1,0,0,1000,25,5,5,11,5,7,251,0,251,173,2,44 +2,0,0,1000,25,5,5,11,5,7,251,0,251,173,3,-22 +3,0,0,1000,25,5,5,11,5,7,251,0,251,173,4,-10 +4,0,0,1000,25,5,5,11,5,7,251,0,251,173,5,8 +5,0,0,1000,25,5,5,11,5,7,251,0,251,173,6,17 +6,0,0,1000,25,5,5,11,5,7,251,0,251,173,7,5 +7,0,0,1000,25,5,5,11,5,7,251,0,251,173,8,1 +8,0,0,1000,25,5,5,11,5,7,251,0,251,173,9,2 +9,0,0,1000,25,5,5,11,5,7,251,0,251,173,10,51 +10,0,0,1000,25,5,5,11,5,7,251,0,251,173,11,14 +11,0,0,1000,25,5,5,11,5,7,251,0,251,173,12,-22 +12,0,0,1000,25,5,5,11,5,7,251,0,251,173,13,10 +13,0,0,1000,25,5,5,11,5,7,251,0,251,173,14,57 +14,0,0,1000,25,5,5,11,5,7,251,0,251,173,15,7 +15,0,0,1000,25,5,5,11,5,7,251,0,251,173,16,3 +16,0,0,1000,25,5,5,11,5,7,251,0,251,173,17,12 +17,0,0,1000,25,5,5,11,5,7,251,0,251,173,18,-64 +18,0,0,1000,25,5,5,11,5,7,251,0,251,173,19,-2 +19,0,0,1000,25,5,5,11,5,7,251,0,251,173,20,9 +20,0,0,1000,25,5,5,11,5,7,251,0,251,173,21,7 +21,0,0,1000,25,5,5,11,5,7,251,0,251,173,22,-1 +22,0,0,1000,25,5,5,11,5,7,251,0,251,173,23,-23 +23,0,0,1000,25,5,5,11,5,7,251,0,251,173,24,2 +24,0,0,1000,25,5,5,11,5,7,251,0,251,173,25,-29 +25,1,0,1000,25,10,5,12,8,3,422,5,427,251,1,38 +26,1,0,1000,25,10,5,12,8,3,422,5,427,251,2,30 +27,1,0,1000,25,10,5,12,8,3,422,5,427,251,3,-21 +28,1,0,1000,25,10,5,12,8,3,422,5,427,251,4,65 +29,1,0,1000,25,10,5,12,8,3,422,5,427,251,5,40 +30,1,0,1000,25,10,5,12,8,3,422,5,427,251,6,10 +31,1,0,1000,25,10,5,12,8,3,422,5,427,251,7,-12 +32,1,0,1000,25,10,5,12,8,3,422,5,427,251,8,8 +33,1,0,1000,25,10,5,12,8,3,422,5,427,251,9,30 +34,1,0,1000,25,10,5,12,8,3,422,5,427,251,10,20 +35,1,0,1000,25,10,5,12,8,3,422,5,427,251,11,-5 +36,1,0,1000,25,10,5,12,8,3,422,5,427,251,12,-38 +37,1,0,1000,25,10,5,12,8,3,422,5,427,251,13,-12 +38,1,0,1000,25,10,5,12,8,3,422,5,427,251,14,27 +39,1,0,1000,25,10,5,12,8,3,422,5,427,251,15,-29 +40,1,0,1000,25,10,5,12,8,3,422,5,427,251,16,6 +41,1,0,1000,25,10,5,12,8,3,422,5,427,251,17,-37 +42,1,0,1000,25,10,5,12,8,3,422,5,427,251,18,27 +43,1,0,1000,25,10,5,12,8,3,422,5,427,251,19,-38 +44,1,0,1000,25,10,5,12,8,3,422,5,427,251,20,-10 +45,1,0,1000,25,10,5,12,8,3,422,5,427,251,21,-49 +46,1,0,1000,25,10,5,12,8,3,422,5,427,251,22,37 +47,1,0,1000,25,10,5,12,8,3,422,5,427,251,23,35 +48,1,0,1000,25,10,5,12,8,3,422,5,427,251,24,37 +49,1,0,1000,25,10,5,12,8,3,422,5,427,251,25,12 +50,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,1,-81 +51,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,2,27 +52,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,3,-56 +53,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,4,53 +54,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,5,86 +55,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,6,77 +56,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,7,20 +57,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,8,-28 +58,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,9,51 +59,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,10,-72 +60,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,11,-96 +61,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,12,81 +62,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,13,-43 +63,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,14,66 +64,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,15,70 +65,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,16,7 +66,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,17,32 +67,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,18,21 +68,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,19,64 +69,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,20,-54 +70,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,21,11 +71,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,22,-30 +72,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,23,-21 +73,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,24,24 +74,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,25,-4 +75,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,26,-12 +76,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,27,-67 +77,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,28,53 +78,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,29,79 +79,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,30,-9 +80,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,31,-6 +81,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,32,-2 +82,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,33,45 +83,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,34,85 +84,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,35,23 +85,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,36,-32 +86,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,37,-36 +87,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,38,-75 +88,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,39,-67 +89,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,40,-44 +90,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,41,-13 +91,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,42,-53 +92,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,43,105 +93,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,44,20 +94,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,45,-77 +95,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,46,36 +96,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,47,-35 +97,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,48,-87 +98,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,49,-70 +99,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,50,98 +100,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,51,109 +101,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,52,-93 +102,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,53,-18 +103,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,54,72 +104,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,55,38 +105,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,56,-33 +106,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,57,43 +107,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,58,87 +108,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,59,-2 +109,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,60,29 +110,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,61,123 +111,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,62,-22 +112,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,63,47 +113,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,64,108 +114,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,65,-53 +115,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,66,-3 +116,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,67,-58 +117,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,68,-16 +118,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,69,-28 +119,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,70,41 +120,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,71,-60 +121,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,72,-69 +122,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,73,93 +123,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,74,9 +124,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,75,37 +125,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,76,-108 +126,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,77,-69 +127,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,78,75 +128,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,79,-11 +129,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,80,-18 +130,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,81,16 +131,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,82,-32 +132,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,83,12 +133,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,84,8 +134,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,85,25 +135,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,86,16 +136,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,87,-99 +137,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,88,-15 +138,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,89,33 +139,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,90,12 +140,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,91,43 +141,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,92,2 +142,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,93,-139 +143,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,94,25 +144,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,95,101 +145,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,96,78 +146,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,97,44 +147,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,98,3 +148,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,99,-44 +149,2,0,1000,100,5,5,50,42,8,2563,7,2570,2219,100,-59 +150,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,1,-22 +151,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,2,27 +152,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,3,60 +153,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,4,89 +154,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,5,11 +155,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,6,12 +156,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,7,-47 +157,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,8,60 +158,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,9,34 +159,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,10,2 +160,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,11,5 +161,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,12,-27 +162,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,13,-13 +163,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,14,105 +164,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,15,-63 +165,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,16,138 +166,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,17,65 +167,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,18,40 +168,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,19,-68 +169,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,20,39 +170,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,21,-27 +171,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,22,64 +172,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,23,50 +173,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,24,-86 +174,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,25,21 +175,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,26,42 +176,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,27,-2 +177,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,28,-124 +178,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,29,90 +179,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,30,-39 +180,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,31,-40 +181,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,32,21 +182,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,33,55 +183,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,34,60 +184,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,35,71 +185,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,36,-27 +186,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,37,66 +187,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,38,48 +188,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,39,-63 +189,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,40,74 +190,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,41,3 +191,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,42,13 +192,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,43,-20 +193,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,44,0 +194,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,45,-51 +195,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,46,45 +196,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,47,44 +197,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,48,-110 +198,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,49,-95 +199,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,50,-21 +200,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,51,-46 +201,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,52,-34 +202,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,53,31 +203,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,54,-42 +204,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,55,52 +205,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,56,39 +206,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,57,112 +207,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,58,39 +208,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,59,-57 +209,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,60,108 +210,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,61,-33 +211,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,62,28 +212,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,63,7 +213,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,64,33 +214,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,65,-22 +215,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,66,69 +216,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,67,-77 +217,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,68,-58 +218,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,69,19 +219,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,70,27 +220,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,71,-41 +221,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,72,63 +222,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,73,97 +223,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,74,60 +224,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,75,32 +225,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,76,36 +226,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,77,-48 +227,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,78,-47 +228,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,79,45 +229,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,80,-5 +230,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,81,-27 +231,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,82,13 +232,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,83,48 +233,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,84,-69 +234,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,85,-55 +235,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,86,-41 +236,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,87,-31 +237,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,88,-14 +238,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,89,-103 +239,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,90,-29 +240,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,91,64 +241,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,92,-74 +242,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,93,1 +243,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,94,-72 +244,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,95,57 +245,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,96,6 +246,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,97,-15 +247,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,98,-15 +248,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,99,-46 +249,3,0,1000,100,10,5,49,42,9,2598,33,2631,2046,100,58 diff --git a/examples/bank_reserves/bank_reserves/agents.py b/examples/bank_reserves/bank_reserves/agents.py index 11b52563..eb953ab3 100644 --- a/examples/bank_reserves/bank_reserves/agents.py +++ b/examples/bank_reserves/bank_reserves/agents.py @@ -10,7 +10,7 @@ Northwestern University, Evanston, IL. """ -from .random_walk import RandomWalker +from mesa.experimental.cell_space import CellAgent class Bank: @@ -44,10 +44,10 @@ def bank_balance(self): # subclass of RandomWalker, which is subclass to Mesa Agent -class Person(RandomWalker): +class Person(CellAgent): def __init__(self, model, moore, bank, rich_threshold): # init parent class with required parameters - super().__init__(model, moore=moore) + super().__init__(model) # the amount each person has in savings self.savings = 0 # total loan amount person has outstanding @@ -65,31 +65,28 @@ def __init__(self, model, moore, bank, rich_threshold): def do_business(self): """check if person has any savings, any money in wallet, or if the bank can loan them any money""" - if self.savings > 0 or self.wallet > 0 or self.bank.bank_to_loan > 0: + if (self.savings > 0 or self.wallet > 0 or self.bank.bank_to_loan > 0) and len( + self.cell.agents + ) > 1: # create list of people at my location (includes self) - my_cell = self.model.grid.get_cell_list_contents([self.pos]) - # check if other people are at my location - if len(my_cell) > 1: - # set customer to self for while loop condition - customer = self - while customer == self: - """select a random person from the people at my location - to trade with""" - customer = self.random.choice(my_cell) - # 50% chance of trading with customer + # set customer to self for while loop condition + customer = self + while customer == self: + customer = self.random.choice(self.cell.agents) + # 50% chance of trading with customer + if self.random.randint(0, 1) == 0: + # 50% chance of trading $5 if self.random.randint(0, 1) == 0: - # 50% chance of trading $5 - if self.random.randint(0, 1) == 0: - # give customer $5 from my wallet - # (may result in negative wallet) - customer.wallet += 5 - self.wallet -= 5 - # 50% chance of trading $2 - else: - # give customer $2 from my wallet - # (may result in negative wallet) - customer.wallet += 2 - self.wallet -= 2 + # give customer $5 from my wallet + # (may result in negative wallet) + customer.wallet += 5 + self.wallet -= 5 + # 50% chance of trading $2 + else: + # give customer $2 from my wallet + # (may result in negative wallet) + customer.wallet += 2 + self.wallet -= 2 def balance_books(self): # check if wallet is negative from trading with customer @@ -178,7 +175,7 @@ def take_out_loan(self, amount): def step(self): # move to a cell in my Moore neighborhood - self.random_move() + self.cell = self.cell.neighborhood.select_random_cell() # trade self.do_business() # deposit money or take out a loan diff --git a/examples/bank_reserves/bank_reserves/model.py b/examples/bank_reserves/bank_reserves/model.py index ae6b0a0f..2671980d 100644 --- a/examples/bank_reserves/bank_reserves/model.py +++ b/examples/bank_reserves/bank_reserves/model.py @@ -12,6 +12,7 @@ import mesa import numpy as np +from mesa.experimental.cell_space import OrthogonalMooreGrid from .agents import Bank, Person @@ -78,7 +79,7 @@ def get_total_loans(model): return np.sum(agent_loans) -class BankReserves(mesa.Model): +class BankReservesModel(mesa.Model): """ This model is a Mesa implementation of the Bank Reserves model from NetLogo. It is a highly abstracted, simplified model of an economy, with only one @@ -117,7 +118,9 @@ def __init__( self.width = width self.init_people = init_people - self.grid = mesa.space.MultiGrid(self.width, self.height, torus=True) + self.grid = OrthogonalMooreGrid( + (self.width, self.height), torus=True, random=self.random + ) # rich_threshold is the amount of savings a person needs to be considered "rich" self.rich_threshold = rich_threshold self.reserve_percent = reserve_percent @@ -145,7 +148,7 @@ def __init__( y = self.random.randrange(self.height) p = Person(self, True, self.bank, self.rich_threshold) # place the Person object on the grid at coordinates (x, y) - self.grid.place_agent(p, (x, y)) + p.move_to(self.grid[(x, y)]) self.running = True self.datacollector.collect(self) diff --git a/examples/bank_reserves/bank_reserves/random_walk.py b/examples/bank_reserves/bank_reserves/random_walk.py deleted file mode 100644 index 0d0258ab..00000000 --- a/examples/bank_reserves/bank_reserves/random_walk.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Citation: -The following code is a copy from random_walk.py at -https://github.com/projectmesa/mesa/blob/main/examples/wolf_sheep/wolf_sheep/random_walk.py -Accessed on: November 2, 2017 -Original Author: Jackie Kazil - -Generalized behavior for random walking, one grid cell at a time. -""" - -import mesa - - -class RandomWalker(mesa.Agent): - """ - Class implementing random walker methods in a generalized manner. - Not intended to be used on its own, but to inherit its methods to multiple - other agents. - """ - - grid = None - x = None - y = None - # use a Moore neighborhood - moore = True - - def __init__(self, model, moore=True): - """ - grid: The MultiGrid object in which the agent lives. - x: The agent's current x coordinate - y: The agent's current y coordinate - moore: If True, may move in all 8 directions. - Otherwise, only up, down, left, right. - """ - super().__init__(model) - self.moore = moore - - def random_move(self): - """ - Step one cell in any allowable direction. - """ - # Pick the next cell from the adjacent cells. - next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True) - next_move = self.random.choice(next_moves) - # Now move: - self.model.grid.move_agent(self, next_move) diff --git a/examples/bank_reserves/bank_reserves/server.py b/examples/bank_reserves/bank_reserves/server.py index 79a4c97a..6fea0561 100644 --- a/examples/bank_reserves/bank_reserves/server.py +++ b/examples/bank_reserves/bank_reserves/server.py @@ -1,7 +1,7 @@ import mesa from .agents import Person -from .model import BankReserves +from .model import BankReservesModel """ Citation: @@ -83,7 +83,7 @@ def person_portrayal(agent): # create instance of Mesa ModularServer server = mesa.visualization.ModularServer( - BankReserves, + BankReservesModel, [canvas_element, chart_element], "Bank Reserves Model", model_params=model_params, diff --git a/examples/bank_reserves/batch_run.py b/examples/bank_reserves/batch_run.py index 00b08a63..2903fd59 100644 --- a/examples/bank_reserves/batch_run.py +++ b/examples/bank_reserves/batch_run.py @@ -24,165 +24,19 @@ every step of every run. """ -import itertools - import mesa -import numpy as np import pandas as pd -from bank_reserves.agents import Bank, Person - -# Start of datacollector functions - - -def get_num_rich_agents(model): - """list of rich agents""" - - rich_agents = [a for a in model.agents if a.savings > model.rich_threshold] - # return number of rich agents - return len(rich_agents) - - -def get_num_poor_agents(model): - """list of poor agents""" - - poor_agents = [a for a in model.agents if a.loans > 10] - # return number of poor agents - return len(poor_agents) - - -def get_num_mid_agents(model): - """list of middle class agents""" - - mid_agents = [ - a for a in model.agents if a.loans < 10 and a.savings < model.rich_threshold - ] - # return number of middle class agents - return len(mid_agents) - - -def get_total_savings(model): - """list of amounts of all agents' savings""" - - agent_savings = [a.savings for a in model.agents] - # return the sum of agents' savings - return np.sum(agent_savings) - - -def get_total_wallets(model): - """list of amounts of all agents' wallets""" - - agent_wallets = [a.wallet for a in model.agents] - # return the sum of all agents' wallets - return np.sum(agent_wallets) - - -def get_total_money(model): - """sum of all agents' wallets""" - - wallet_money = get_total_wallets(model) - # sum of all agents' savings - savings_money = get_total_savings(model) - # return sum of agents' wallets and savings for total money - return wallet_money + savings_money - - -def get_total_loans(model): - """list of amounts of all agents' loans""" - - agent_loans = [a.loans for a in model.agents] - # return sum of all agents' loans - return np.sum(agent_loans) - - -def track_params(model): - return (model.init_people, model.rich_threshold, model.reserve_percent) - - -def track_run(model): - return model.uid - - -class BankReservesModel(mesa.Model): - # id generator to track run number in batch run data - id_gen = itertools.count(1) - - # grid height - grid_h = 20 - # grid width - grid_w = 20 - - """init parameters "init_people", "rich_threshold", and "reserve_percent" - are all set via Slider""" - - def __init__( - self, - height=grid_h, - width=grid_w, - init_people=2, - rich_threshold=10, - reserve_percent=50, - ): - super().__init__() - self.uid = next(self.id_gen) - self.height = height - self.width = width - self.init_people = init_people - - self.grid = mesa.space.MultiGrid(self.width, self.height, torus=True) - # rich_threshold is the amount of savings a person needs to be considered "rich" - self.rich_threshold = rich_threshold - self.reserve_percent = reserve_percent - # see datacollector functions above - self.datacollector = mesa.DataCollector( - model_reporters={ - "Rich": get_num_rich_agents, - "Poor": get_num_poor_agents, - "Middle Class": get_num_mid_agents, - "Savings": get_total_savings, - "Wallets": get_total_wallets, - "Money": get_total_money, - "Loans": get_total_loans, - "Model Params": track_params, - "Run": track_run, - }, - agent_reporters={"Wealth": "wealth"}, - ) - - # create a single bank object for the model - self.bank = Bank(self, self.reserve_percent) - - # create people for the model according to number of people set by user - for i in range(self.init_people): - # set x coordinate as a random number within the width of the grid - x = self.random.randrange(self.width) - # set y coordinate as a random number within the height of the grid - y = self.random.randrange(self.height) - p = Person(i, (x, y), self, True, self.bank, self.rich_threshold) - # place the Person object on the grid at coordinates (x, y) - self.grid.place_agent(p, (x, y)) - - self.running = True - - def step(self): - # collect data - self.datacollector.collect(self) - # tell all the agents in the model to run their step function - self.agents.shuffle_do("step") - - def run_model(self): - for i in range(self.run_time): - self.step() - - -# parameter lists for each parameter to be tested in batch run -br_params = { - "init_people": [25, 100], - "rich_threshold": [5, 10], - "reserve_percent": 5, -} +from bank_reserves.model import BankReservesModel def main(): + # parameter lists for each parameter to be tested in batch run + br_params = { + "init_people": [25, 100], + "rich_threshold": [5, 10], + "reserve_percent": 5, + } + # The existing batch run logic here data = mesa.batch_run( BankReservesModel, diff --git a/examples/boltzmann_wealth_model_experimental/model.py b/examples/boltzmann_wealth_model_experimental/model.py new file mode 100644 index 00000000..0dd39404 --- /dev/null +++ b/examples/boltzmann_wealth_model_experimental/model.py @@ -0,0 +1,69 @@ +import mesa + + +def compute_gini(model): + agent_wealths = [agent.wealth for agent in model.agents] + x = sorted(agent_wealths) + N = model.num_agents + B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x)) + return 1 + (1 / N) - 2 * B + + +class BoltzmannWealthModel(mesa.Model): + """A simple model of an economy where agents exchange currency at random. + + All the agents begin with one unit of currency, and each time step can give + a unit of currency to another agent. Note how, over time, this produces a + highly skewed distribution of wealth. + """ + + def __init__(self, N=100, width=10, height=10): + super().__init__() + self.num_agents = N + self.grid = mesa.experimental.cell_space.OrthogonalMooreGrid( + (width, height), torus=True, random=self.random + ) + + self.datacollector = mesa.DataCollector( + model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"} + ) + # Create agents + for _ in range(self.num_agents): + agent = MoneyAgent(self) + + # Add the agent to a random grid cell + x = self.random.randrange(width) + y = self.random.randrange(height) + agent.move_to(self.grid[(x, y)]) + + self.running = True + self.datacollector.collect(self) + + def step(self): + self.agents.shuffle().do("step") + # collect data + self.datacollector.collect(self) + + def run_model(self, n): + for i in range(n): + self.step() + + +class MoneyAgent(mesa.experimental.cell_space.CellAgent): + """An agent with fixed initial wealth.""" + + def __init__(self, model): + super().__init__(model) + self.wealth = 1 + + def give_money(self): + cellmates = [agent for agent in self.cell.agents if agent is not self] + if len(cellmates) > 0: + other = self.random.choice(cellmates) + other.wealth += 1 + self.wealth -= 1 + + def step(self): + self.cell = self.cell.neighborhood.select_random_cell() + if self.wealth > 0: + self.give_money() diff --git a/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/model.py b/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/model.py index a61cf4bf..fa671ce3 100644 --- a/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/model.py +++ b/examples/boltzmann_wealth_model_network/boltzmann_wealth_model_network/model.py @@ -18,7 +18,9 @@ def __init__(self, num_agents=7, num_nodes=10): self.num_agents = num_agents self.num_nodes = num_nodes if num_nodes >= self.num_agents else self.num_agents self.G = nx.erdos_renyi_graph(n=self.num_nodes, p=0.5) - self.grid = mesa.space.NetworkGrid(self.G) + self.grid = mesa.experimental.cell_space.Network( + self.G, random=self.random, capacity=1 + ) self.datacollector = mesa.DataCollector( model_reporters={"Gini": compute_gini}, @@ -28,11 +30,11 @@ def __init__(self, num_agents=7, num_nodes=10): list_of_random_nodes = self.random.sample(list(self.G), self.num_agents) # Create agents - for i in range(self.num_agents): - a = MoneyAgent(self) + for position in list_of_random_nodes: + agent = MoneyAgent(self) # Add the agent to a random node - self.grid.place_agent(a, list_of_random_nodes[i]) + agent.move_to(self.grid[position]) self.running = True self.datacollector.collect(self) @@ -47,31 +49,24 @@ def run_model(self, n): self.step() -class MoneyAgent(mesa.Agent): +class MoneyAgent(mesa.experimental.cell_space.CellAgent): """An agent with fixed initial wealth.""" def __init__(self, model): super().__init__(model) self.wealth = 1 - def move(self): - possible_steps = [ - node - for node in self.model.grid.get_neighborhood(self.pos, include_center=False) - if self.model.grid.is_cell_empty(node) - ] - if len(possible_steps) > 0: - new_position = self.random.choice(possible_steps) - self.model.grid.move_agent(self, new_position) - def give_money(self): - neighbors = self.model.grid.get_neighbors(self.pos, include_center=False) + neighbors = [agent for agent in self.cell.neighborhood.agents if not self] if len(neighbors) > 0: other = self.random.choice(neighbors) other.wealth += 1 self.wealth -= 1 def step(self): - self.move() + empty_neighbors = [cell for cell in self.cell.neighborhood if cell.is_empty] + if empty_neighbors: + self.cell = self.random.choice(empty_neighbors) + if self.wealth > 0: self.give_money() diff --git a/examples/caching_and_replay/model.py b/examples/caching_and_replay/model.py index e4bc23c7..cd35e88b 100644 --- a/examples/caching_and_replay/model.py +++ b/examples/caching_and_replay/model.py @@ -1,9 +1,10 @@ """This file was copied over from the original Schelling mesa example.""" import mesa +from mesa.experimental.cell_space import CellAgent, OrthogonalMooreGrid -class SchellingAgent(mesa.Agent): +class SchellingAgent(CellAgent): """ Schelling segregation agent """ @@ -21,15 +22,13 @@ def __init__(self, model, agent_type): def step(self): similar = 0 - for neighbor in self.model.grid.iter_neighbors( - self.pos, moore=True, radius=self.model.radius - ): - if neighbor.type == self.type: + for agent in self.cell.get_neighborhood(radius=self.model.radius).agents: + if agent.type == self.type: similar += 1 # If unhappy, move: if similar < self.model.homophily: - self.model.grid.move_to_empty(self) + self.cell = self.model.grid.select_random_empty_cell() else: self.model.happy += 1 @@ -69,7 +68,7 @@ def __init__( self.homophily = homophily self.radius = radius - self.grid = mesa.space.SingleGrid(width, height, torus=True) + self.grid = OrthogonalMooreGrid((width, height), torus=True) self.happy = 0 self.datacollector = mesa.DataCollector( @@ -80,11 +79,11 @@ def __init__( # We use a grid iterator that returns # the coordinates of a cell as well as # its contents. (coord_iter) - for _, pos in self.grid.coord_iter(): + for cell in self.grid.all_cells: if self.random.random() < self.density: agent_type = 1 if self.random.random() < self.minority_pc else 0 agent = SchellingAgent(self, agent_type) - self.grid.place_agent(agent, pos) + agent.cell = cell self.datacollector.collect(self) diff --git a/examples/charts/charts/agents.py b/examples/charts/charts/agents.py index 11b52563..64dc0d91 100644 --- a/examples/charts/charts/agents.py +++ b/examples/charts/charts/agents.py @@ -10,7 +10,7 @@ Northwestern University, Evanston, IL. """ -from .random_walk import RandomWalker +from mesa.experimental.cell_space import CellAgent class Bank: @@ -44,10 +44,10 @@ def bank_balance(self): # subclass of RandomWalker, which is subclass to Mesa Agent -class Person(RandomWalker): - def __init__(self, model, moore, bank, rich_threshold): +class Person(CellAgent): + def __init__(self, model, bank, rich_threshold): # init parent class with required parameters - super().__init__(model, moore=moore) + super().__init__(model) # the amount each person has in savings self.savings = 0 # total loan amount person has outstanding @@ -67,14 +67,14 @@ def do_business(self): bank can loan them any money""" if self.savings > 0 or self.wallet > 0 or self.bank.bank_to_loan > 0: # create list of people at my location (includes self) - my_cell = self.model.grid.get_cell_list_contents([self.pos]) + my_cell = [a for a in self.cell.agents if a != self] # check if other people are at my location if len(my_cell) > 1: # set customer to self for while loop condition customer = self while customer == self: - """select a random person from the people at my location - to trade with""" + # select a random person from the people at my location + # to trade with customer = self.random.choice(my_cell) # 50% chance of trading with customer if self.random.randint(0, 1) == 0: @@ -178,7 +178,7 @@ def take_out_loan(self, amount): def step(self): # move to a cell in my Moore neighborhood - self.random_move() + self.cell = self.cell.neighborhood.select_random_cell() # trade self.do_business() # deposit money or take out a loan diff --git a/examples/charts/charts/model.py b/examples/charts/charts/model.py index 679f3176..22d3945d 100644 --- a/examples/charts/charts/model.py +++ b/examples/charts/charts/model.py @@ -12,6 +12,7 @@ import mesa import numpy as np +from mesa.experimental.cell_space import OrthogonalMooreGrid from .agents import Bank, Person @@ -100,7 +101,7 @@ def __init__( self.width = width self.init_people = init_people - self.grid = mesa.space.MultiGrid(self.width, self.height, torus=True) + self.grid = OrthogonalMooreGrid((self.width, self.height), torus=True) # rich_threshold is the amount of savings a person needs to be considered "rich" self.rich_threshold = rich_threshold self.reserve_percent = reserve_percent @@ -126,9 +127,9 @@ def __init__( # set x, y coords randomly within the grid x = self.random.randrange(self.width) y = self.random.randrange(self.height) - p = Person(self, True, self.bank, self.rich_threshold) + p = Person(self, self.bank, self.rich_threshold) # place the Person object on the grid at coordinates (x, y) - self.grid.place_agent(p, (x, y)) + p.move_to(self.grid[(x, y)]) self.running = True self.datacollector.collect(self) diff --git a/examples/charts/charts/random_walk.py b/examples/charts/charts/random_walk.py deleted file mode 100644 index 0d0258ab..00000000 --- a/examples/charts/charts/random_walk.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Citation: -The following code is a copy from random_walk.py at -https://github.com/projectmesa/mesa/blob/main/examples/wolf_sheep/wolf_sheep/random_walk.py -Accessed on: November 2, 2017 -Original Author: Jackie Kazil - -Generalized behavior for random walking, one grid cell at a time. -""" - -import mesa - - -class RandomWalker(mesa.Agent): - """ - Class implementing random walker methods in a generalized manner. - Not intended to be used on its own, but to inherit its methods to multiple - other agents. - """ - - grid = None - x = None - y = None - # use a Moore neighborhood - moore = True - - def __init__(self, model, moore=True): - """ - grid: The MultiGrid object in which the agent lives. - x: The agent's current x coordinate - y: The agent's current y coordinate - moore: If True, may move in all 8 directions. - Otherwise, only up, down, left, right. - """ - super().__init__(model) - self.moore = moore - - def random_move(self): - """ - Step one cell in any allowable direction. - """ - # Pick the next cell from the adjacent cells. - next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True) - next_move = self.random.choice(next_moves) - # Now move: - self.model.grid.move_agent(self, next_move) diff --git a/examples/color_patches/color_patches/model.py b/examples/color_patches/color_patches/model.py index 3cd08a6c..ca1aa263 100644 --- a/examples/color_patches/color_patches/model.py +++ b/examples/color_patches/color_patches/model.py @@ -7,34 +7,28 @@ import mesa -class ColorCell(mesa.Agent): +class ColorCell(mesa.experimental.cell_space.CellAgent): """ Represents a cell's opinion (visualized by a color) """ OPINIONS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] - def __init__(self, pos, model, initial_state): + def __init__(self, model, initial_state): """ Create a cell, in the given state, at the given row, col position. """ super().__init__(model) - self._row = pos[0] - self._col = pos[1] - self._state = initial_state - self._next_state = None + self.state = initial_state + self.next_state = None def get_col(self): """Return the col location of this cell.""" - return self._col + return self.cell.coordinate[0] def get_row(self): """Return the row location of this cell.""" - return self._row - - def get_state(self): - """Return the current state (OPINION) of this cell.""" - return self._state + return self.cell.coordinate[1] def determine_opinion(self): """ @@ -43,8 +37,8 @@ def determine_opinion(self): A choice is made at random in case of a tie The next state is stored until all cells have been polled """ - _neighbor_iter = self.model.grid.iter_neighbors((self._row, self._col), True) - neighbors_opinion = Counter(n.get_state() for n in _neighbor_iter) + neighbors = self.cell.neighborhood.agents + neighbors_opinion = Counter(n.state for n in neighbors) # Following is a a tuple (attribute, occurrences) polled_opinions = neighbors_opinion.most_common() tied_opinions = [] @@ -52,13 +46,13 @@ def determine_opinion(self): if neighbor[1] == polled_opinions[0][1]: tied_opinions.append(neighbor) - self._next_state = self.random.choice(tied_opinions)[0] + self.next_state = self.random.choice(tied_opinions)[0] def assume_opinion(self): """ Set the state of the agent to the next state """ - self._state = self._next_state + self.state = self.next_state class ColorPatches(mesa.Model): @@ -72,18 +66,18 @@ def __init__(self, width=20, height=20): The agents next state is first determined before updating the grid """ super().__init__() - self._grid = mesa.space.SingleGrid(width, height, torus=False) + self._grid = mesa.experimental.cell_space.OrthogonalMooreGrid( + (width, height), torus=False + ) # self._grid.coord_iter() # --> should really not return content + col + row # -->but only col & row # for (contents, col, row) in self._grid.coord_iter(): # replaced content with _ to appease linter - for _, (row, col) in self._grid.coord_iter(): - cell = ColorCell( - (row, col), self, ColorCell.OPINIONS[self.random.randrange(0, 16)] - ) - self._grid.place_agent(cell, (row, col)) + for cell in self._grid.all_cells: + agent = ColorCell(self, ColorCell.OPINIONS[self.random.randrange(0, 16)]) + agent.move_to(cell) self.running = True diff --git a/examples/epstein_civil_violence/epstein_civil_violence/agent.py b/examples/epstein_civil_violence/epstein_civil_violence/agent.py index b746a5a4..edd1d1eb 100644 --- a/examples/epstein_civil_violence/epstein_civil_violence/agent.py +++ b/examples/epstein_civil_violence/epstein_civil_violence/agent.py @@ -3,13 +3,23 @@ import mesa -class Citizen(mesa.Agent): +class EpsteinAgent(mesa.experimental.cell_space.CellAgent): + def update_neighbors(self): + """ + Look around and see who my neighbors are + """ + self.neighborhood = self.cell.get_neighborhood(radius=self.vision) + + self.neighbors = self.neighborhood.agents + self.empty_neighbors = [c for c in self.neighborhood if c.is_empty] + + +class Citizen(EpsteinAgent): """ A member of the general population, may or may not be in active rebellion. Summary of rule: If grievance - risk > threshold, rebel. Attributes: - x, y: Grid coordinates hardship: Agent's 'perceived hardship (i.e., physical or economic privation).' Exogenous, drawn from U(0,1). regime_legitimacy: Agent's perception of regime legitimacy, equal @@ -30,7 +40,6 @@ class Citizen(mesa.Agent): def __init__( self, model, - pos, hardship, regime_legitimacy, risk_aversion, @@ -40,7 +49,7 @@ def __init__( """ Create a new Citizen. Args: - x, y: Grid coordinates + model: the model to which the agent belongs hardship: Agent's 'perceived hardship (i.e., physical or economic privation).' Exogenous, drawn from U(0,1). regime_legitimacy: Agent's perception of regime legitimacy, equal @@ -53,8 +62,6 @@ def __init__( model: model instance """ super().__init__(model) - self.breed = "citizen" - self.pos = pos self.hardship = hardship self.regime_legitimacy = regime_legitimacy self.risk_aversion = risk_aversion @@ -79,32 +86,21 @@ def step(self): self.condition = "Active" else: self.condition = "Quiescent" - if self.model.movement and self.empty_neighbors: - new_pos = self.random.choice(self.empty_neighbors) - self.model.grid.move_agent(self, new_pos) - def update_neighbors(self): - """ - Look around and see who my neighbors are - """ - self.neighborhood = self.model.grid.get_neighborhood( - self.pos, moore=True, radius=self.vision - ) - self.neighbors = self.model.grid.get_cell_list_contents(self.neighborhood) - self.empty_neighbors = [ - c for c in self.neighborhood if self.model.grid.is_cell_empty(c) - ] + if self.model.movement and self.empty_neighbors: + new_cell = self.random.choice(self.empty_neighbors) + self.move_to(new_cell) def update_estimated_arrest_probability(self): """ Based on the ratio of cops to actives in my neighborhood, estimate the p(Arrest | I go active). """ - cops_in_vision = len([c for c in self.neighbors if c.breed == "cop"]) + cops_in_vision = len([c for c in self.neighbors if isinstance(c, Cop)]) actives_in_vision = 1.0 # citizen counts herself for c in self.neighbors: if ( - c.breed == "citizen" + isinstance(c, Citizen) and c.condition == "Active" and c.jail_sentence == 0 ): @@ -114,7 +110,7 @@ def update_estimated_arrest_probability(self): ) -class Cop(mesa.Agent): +class Cop(EpsteinAgent): """ A cop for life. No defection. Summary of rule: Inspect local vision and arrest a random active agent. @@ -126,7 +122,7 @@ class Cop(mesa.Agent): able to inspect """ - def __init__(self, model, pos, vision): + def __init__(self, model, vision): """ Create a new Cop. Args: @@ -136,8 +132,6 @@ def __init__(self, model, pos, vision): model: model instance """ super().__init__(model) - self.breed = "cop" - self.pos = pos self.vision = vision def step(self): @@ -149,7 +143,7 @@ def step(self): active_neighbors = [] for agent in self.neighbors: if ( - agent.breed == "citizen" + isinstance(agent, Citizen) and agent.condition == "Active" and agent.jail_sentence == 0 ): @@ -161,16 +155,4 @@ def step(self): arrestee.condition = "Quiescent" if self.model.movement and self.empty_neighbors: new_pos = self.random.choice(self.empty_neighbors) - self.model.grid.move_agent(self, new_pos) - - def update_neighbors(self): - """ - Look around and see who my neighbors are. - """ - self.neighborhood = self.model.grid.get_neighborhood( - self.pos, moore=True, radius=self.vision - ) - self.neighbors = self.model.grid.get_cell_list_contents(self.neighborhood) - self.empty_neighbors = [ - c for c in self.neighborhood if self.model.grid.is_cell_empty(c) - ] + self.move_to(new_pos) diff --git a/examples/epstein_civil_violence/epstein_civil_violence/model.py b/examples/epstein_civil_violence/epstein_civil_violence/model.py index b278f23e..8bf06bf1 100644 --- a/examples/epstein_civil_violence/epstein_civil_violence/model.py +++ b/examples/epstein_civil_violence/epstein_civil_violence/model.py @@ -59,7 +59,9 @@ def __init__( self.max_iters = max_iters self.iteration = 0 - self.grid = mesa.space.SingleGrid(width, height, torus=True) + self.grid = mesa.experimental.cell_space.OrthogonalMooreGrid( + (width, height), capacity=1, torus=True + ) model_reporters = { "Quiescent": lambda m: self.count_type_citizens(m, "Quiescent"), @@ -68,9 +70,9 @@ def __init__( "Cops": self.count_cops, } agent_reporters = { - "x": lambda a: a.pos[0], - "y": lambda a: a.pos[1], - "breed": lambda a: a.breed, + "x": lambda a: a.cell.coordinate[0], + "y": lambda a: a.cell.coordinate[1], + "breed": lambda a: type(a).__name__, "jail_sentence": lambda a: getattr(a, "jail_sentence", None), "condition": lambda a: getattr(a, "condition", None), "arrest_probability": lambda a: getattr(a, "arrest_probability", None), @@ -81,22 +83,21 @@ def __init__( if self.cop_density + self.citizen_density > 1: raise ValueError("Cop density + citizen density must be less than 1") - for contents, (x, y) in self.grid.coord_iter(): + for cell in self.grid.all_cells: if self.random.random() < self.cop_density: - cop = Cop(self, (x, y), vision=self.cop_vision) - self.grid[x][y] = cop + cop = Cop(self, vision=self.cop_vision) + cop.move_to(cell) elif self.random.random() < (self.cop_density + self.citizen_density): citizen = Citizen( self, - (x, y), hardship=self.random.random(), regime_legitimacy=self.legitimacy, risk_aversion=self.random.random(), threshold=self.active_threshold, vision=self.citizen_vision, ) - self.grid[x][y] = citizen + citizen.move_to(cell) self.running = True self.datacollector.collect(self) @@ -117,34 +118,29 @@ def count_type_citizens(model, condition, exclude_jailed=True): """ Helper method to count agents by Quiescent/Active. """ - count = 0 - for agent in model.agents: - if agent.breed == "cop": - continue - if exclude_jailed and agent.jail_sentence > 0: - continue - if agent.condition == condition: - count += 1 - return count + citizens = model.agents_by_type[Citizen] + + if exclude_jailed: + return len( + [ + c + for c in citizens + if (c.condition == condition) and (c.jail_sentence == 0) + ] + ) + else: + return len([c for c in citizens if c.condition == condition]) @staticmethod def count_jailed(model): """ Helper method to count jailed agents. """ - count = 0 - for agent in model.agents: - if agent.breed == "citizen" and agent.jail_sentence > 0: - count += 1 - return count + return len([a for a in model.agents_by_type[Citizen] if a.jail_sentence > 0]) @staticmethod def count_cops(model): """ Helper method to count jailed agents. """ - count = 0 - for agent in model.agents: - if agent.breed == "cop": - count += 1 - return count + return len(model.agents_by_type[Cop]) diff --git a/examples/forest_fire/Forest Fire Model.ipynb b/examples/forest_fire/Forest Fire Model.ipynb index 189672ed..c1f96a4c 100644 --- a/examples/forest_fire/Forest Fire Model.ipynb +++ b/examples/forest_fire/Forest Fire Model.ipynb @@ -23,19 +23,35 @@ }, { "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], + "execution_count": 2, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-12T18:20:45.476090Z", + "start_time": "2024-10-12T18:20:45.461936Z" + } + }, + "outputs": [ + { + "ename": "ImportError", + "evalue": "cannot import name 'BatchRunner' from 'mesa.batchrunner' (/Users/jhkwakkel/Documents/GitHub/mesa/mesa/batchrunner.py)", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mImportError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[2], line 7\u001b[0m\n\u001b[1;32m 4\u001b[0m get_ipython()\u001b[38;5;241m.\u001b[39mrun_line_magic(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mmatplotlib\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124minline\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mmesa\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Model\n\u001b[0;32m----> 7\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mmesa\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mbatchrunner\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m BatchRunner\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mmesa\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mdatacollection\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m DataCollector\n", + "\u001b[0;31mImportError\u001b[0m: cannot import name 'BatchRunner' from 'mesa.batchrunner' (/Users/jhkwakkel/Documents/GitHub/mesa/mesa/batchrunner.py)" + ] + } + ], "source": [ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "\n", "%matplotlib inline\n", "\n", - "from mesa import Agent, Model\n", + "from mesa import Model\n", "from mesa.batchrunner import BatchRunner\n", - "from mesa.datacollection import DataCollector\n", - "from mesa.space import Grid" + "from mesa.datacollection import DataCollector" ] }, { @@ -46,44 +62,50 @@ "\n", "Most models consist of basically two things: agents, and an world for the agents to be in. The Forest Fire model has only one kind of agent: a tree. A tree can either be unburned, on fire, or already burned. The environment is a grid, where each cell can either be empty or contain a tree.\n", "\n", - "First, let's define our tree agent. The agent needs to be assigned **x** and **y** coordinates on the grid, and that's about it. We could assign agents a condition to be in, but for now let's have them all start as being 'Fine'. Since the agent doesn't move, and there is only at most one tree per cell, we can use a tuple of its coordinates as a unique identifier.\n", + "First, let's define our tree agent. The agent needs to be assigned a cell on the grid, and that's about it. We could assign agents a condition to be in, but for now let's have them all start as being 'Fine'. Since the agent doesn't move, we use `FixedAgent` as the parent class.\n", "\n", - "Next, we define the agent's **step** method. This gets called whenever the agent needs to act in the world and takes the *model* object to which it belongs as an input. The tree's behavior is simple: If it is currently on fire, it spreads the fire to any trees above, below, to the left and the right of it that are not themselves burned out or on fire; then it burns itself out. " + "Next, we define the agent's **step** method. This gets called whenever the agent needs to act in the world. The tree's behavior is simple: If it is currently on fire, it spreads the fire to any neighboring trees that are not burning or have not burned down." ] }, { "cell_type": "code", - "execution_count": 2, - "metadata": {}, + "execution_count": 3, + "metadata": { + "ExecuteTime": { + "end_time": "2024-10-12T18:22:50.333734Z", + "start_time": "2024-10-12T18:22:50.046775Z" + } + }, "outputs": [], "source": [ - "class TreeCell(Agent):\n", + "from mesa.experimental.cell_space import FixedAgent\n", + "\n", + "\n", + "class TreeCell(FixedAgent):\n", " \"\"\"\n", " A tree cell.\n", "\n", " Attributes:\n", - " x, y: Grid coordinates\n", " condition: Can be \"Fine\", \"On Fire\", or \"Burned Out\"\n", - " unique_id: int\n", + "\n", " \"\"\"\n", "\n", - " def __init__(self, model, pos):\n", + " def __init__(self, model, cell):\n", " \"\"\"\n", " Create a new tree.\n", " Args:\n", - " pos: The tree's coordinates on the grid.\n", + " model: standard model reference for agent.\n", " \"\"\"\n", " super().__init__(model)\n", - " self.pos = pos\n", " self.condition = \"Fine\"\n", + " self.cell = cell\n", "\n", " def step(self):\n", " \"\"\"\n", " If the tree is on fire, spread it to fine trees nearby.\n", " \"\"\"\n", " if self.condition == \"On Fire\":\n", - " neighbors = self.model.grid.get_neighbors(self.pos, moore=False)\n", - " for neighbor in neighbors:\n", + " for neighbor in self.cell.neighborhood.agents:\n", " if neighbor.condition == \"Fine\":\n", " neighbor.condition = \"On Fire\"\n", " self.condition = \"Burned Out\"" @@ -93,7 +115,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we need to define the model object itself. The main thing the model needs is the grid, which the trees are placed on. But since the model is dynamic, it also needs to include time -- it needs a schedule, to manage the trees activation as they spread the fire from one to the other.\n", + "Now we need to define the model object itself. The main thing the model needs is the grid, which the trees are placed on. We can choose different kinds of grids such as a von Neumann grid where any cell has 4 neighbors (left, right, top and bottom) or Moore grid where a cell has eigh neighbors.\n", "\n", "The model also needs a few parameters: how large the grid is and what the density of trees on it will be. Density will be the key parameter we'll explore below.\n", "\n", @@ -110,12 +132,15 @@ "metadata": {}, "outputs": [], "source": [ + "from mesa.experimental.cell_space import OrthogonalMooreGrid\n", + "\n", + "\n", "class ForestFire(Model):\n", " \"\"\"\n", " Simple Forest Fire model.\n", " \"\"\"\n", "\n", - " def __init__(self, width, height, density):\n", + " def __init__(self, width=100, height=100, density=0.65, seed=None):\n", " \"\"\"\n", " Create a new forest fire model.\n", "\n", @@ -123,9 +148,12 @@ " width, height: The size of the grid to model\n", " density: What fraction of grid cells have a tree in them.\n", " \"\"\"\n", + " super().__init__(seed=seed)\n", + "\n", " # Set up model objects\n", - " self.grid = Grid(width, height, torus=False)\n", - " self.dc = DataCollector(\n", + "\n", + " self.grid = OrthogonalMooreGrid((width, height), capacity=1)\n", + " self.datacollector = DataCollector(\n", " {\n", " \"Fine\": lambda m: self.count_type(m, \"Fine\"),\n", " \"On Fire\": lambda m: self.count_type(m, \"On Fire\"),\n", @@ -134,23 +162,25 @@ " )\n", "\n", " # Place a tree in each cell with Prob = density\n", - " for x in range(self.width):\n", - " for y in range(self.height):\n", - " if self.random.random() < density:\n", - " # Create a tree\n", - " new_tree = TreeCell(self, (x, y))\n", - " # Set all trees in the first column on fire.\n", - " if x == 0:\n", - " new_tree.condition = \"On Fire\"\n", - " self.grid[x][y] = new_tree\n", + " for cell in self.grid.all_cells:\n", + " if self.random.random() < density:\n", + " # Create a tree\n", + " new_tree = TreeCell(self, cell)\n", + " # Set all trees in the first column on fire.\n", + " if cell.coordinate[0] == 0:\n", + " new_tree.condition = \"On Fire\"\n", + "\n", " self.running = True\n", + " self.datacollector.collect(self)\n", "\n", " def step(self):\n", " \"\"\"\n", " Advance the model by one step.\n", " \"\"\"\n", " self.agents.shuffle_do(\"step\")\n", - " self.dc.collect(self)\n", + " # collect data\n", + " self.datacollector.collect(self)\n", + "\n", " # Halt if no more fire\n", " if self.count_type(self, \"On Fire\") == 0:\n", " self.running = False\n", @@ -160,11 +190,7 @@ " \"\"\"\n", " Helper method to count trees in a given condition in a given model.\n", " \"\"\"\n", - " count = 0\n", - " for tree in model.agents:\n", - " if tree.condition == tree_condition:\n", - " count += 1\n", - " return count" + " return len(model.agents.select(lambda x: x.condition == tree_condition))" ] }, { diff --git a/examples/forest_fire/forest_fire/agent.py b/examples/forest_fire/forest_fire/agent.py index c9b4e975..125f42f8 100644 --- a/examples/forest_fire/forest_fire/agent.py +++ b/examples/forest_fire/forest_fire/agent.py @@ -1,18 +1,16 @@ -import mesa +from mesa.experimental.cell_space import FixedAgent -class TreeCell(mesa.Agent): +class TreeCell(FixedAgent): """ A tree cell. Attributes: - x, y: Grid coordinates condition: Can be "Fine", "On Fire", or "Burned Out" - unique_id: int """ - def __init__(self, model): + def __init__(self, model, cell): """ Create a new tree. Args: @@ -20,13 +18,14 @@ def __init__(self, model): """ super().__init__(model) self.condition = "Fine" + self.cell = cell def step(self): """ If the tree is on fire, spread it to fine trees nearby. """ if self.condition == "On Fire": - for neighbor in self.model.grid.iter_neighbors(self.pos, True): + for neighbor in self.cell.neighborhood.agents: if neighbor.condition == "Fine": neighbor.condition = "On Fire" self.condition = "Burned Out" diff --git a/examples/forest_fire/forest_fire/model.py b/examples/forest_fire/forest_fire/model.py index e23a9e9e..14eca705 100644 --- a/examples/forest_fire/forest_fire/model.py +++ b/examples/forest_fire/forest_fire/model.py @@ -1,4 +1,5 @@ import mesa +from mesa.experimental.cell_space import OrthogonalMooreGrid from .agent import TreeCell @@ -8,7 +9,7 @@ class ForestFire(mesa.Model): Simple Forest Fire model. """ - def __init__(self, width=100, height=100, density=0.65): + def __init__(self, width=100, height=100, density=0.65, seed=None): """ Create a new forest fire model. @@ -16,11 +17,11 @@ def __init__(self, width=100, height=100, density=0.65): width, height: The size of the grid to model density: What fraction of grid cells have a tree in them. """ - super().__init__() - # Set up model objects + super().__init__(seed=seed) - self.grid = mesa.space.SingleGrid(width, height, torus=False) + # Set up model objects + self.grid = OrthogonalMooreGrid((width, height), capacity=1) self.datacollector = mesa.DataCollector( { "Fine": lambda m: self.count_type(m, "Fine"), @@ -30,14 +31,13 @@ def __init__(self, width=100, height=100, density=0.65): ) # Place a tree in each cell with Prob = density - for contents, (x, y) in self.grid.coord_iter(): + for cell in self.grid.all_cells: if self.random.random() < density: # Create a tree - new_tree = TreeCell(self) + new_tree = TreeCell(self, cell) # Set all trees in the first column on fire. - if x == 0: + if cell.coordinate[0] == 0: new_tree.condition = "On Fire" - self.grid.place_agent(new_tree, (x, y)) self.running = True self.datacollector.collect(self) @@ -59,8 +59,4 @@ def count_type(model, tree_condition): """ Helper method to count trees in a given condition in a given model. """ - count = 0 - for tree in model.agents: - if tree.condition == tree_condition: - count += 1 - return count + return len(model.agents.select(lambda x: x.condition == tree_condition)) diff --git a/examples/hex_snowflake/hex_snowflake/cell.py b/examples/hex_snowflake/hex_snowflake/cell.py index 7e2367c5..f68223a7 100644 --- a/examples/hex_snowflake/hex_snowflake/cell.py +++ b/examples/hex_snowflake/hex_snowflake/cell.py @@ -1,39 +1,35 @@ -import mesa +from mesa.experimental.cell_space import FixedAgent -class Cell(mesa.Agent): +class Cell(FixedAgent): """Represents a single ALIVE or DEAD cell in the simulation.""" DEAD = 0 ALIVE = 1 - def __init__(self, pos, model, init_state=DEAD): + def __init__(self, cell, model, init_state=DEAD): """ Create a cell, in the given state, at the given x, y position. """ super().__init__(model) - self.x, self.y = pos + self.cell = cell self.state = init_state - self._nextState = None - self.isConsidered = False + self._next_state = None + self.is_considered = False @property - def isAlive(self): + def is_alive(self): return self.state == self.ALIVE - @property - def neighbors(self): - return self.model.grid.iter_neighbors((self.x, self.y)) - @property def considered(self): - return self.isConsidered is True + return self.is_considered is True def determine_state(self): """ Compute if the cell will be dead or alive at the next tick. A dead cell will become alive if it has only one neighbor. The state is not - changed here, but is just computed and stored in self._nextState, + changed here, but is just computed and stored in self._next_state, because our current state may still be necessary for our neighbors to calculate their next state. When a cell is made alive, its neighbors are able to be considered @@ -41,20 +37,22 @@ def determine_state(self): for performance reasons. """ # assume no state change - self._nextState = self.state + self._next_state = self.state - if not self.isAlive and self.isConsidered: + if not self.is_alive and self.is_considered: # Get the neighbors and apply the rules on whether to be alive or dead # at the next tick. - live_neighbors = sum(neighbor.isAlive for neighbor in self.neighbors) + live_neighbors = sum( + neighbor.is_alive for neighbor in self.cell.neighborhood.agents + ) if live_neighbors == 1: - self._nextState = self.ALIVE - for a in self.neighbors: - a.isConsidered = True + self._next_state = self.ALIVE + for a in self.cell.neighborhood.agents: + a.is_considered = True def assume_state(self): """ Set the state to the new computed state """ - self.state = self._nextState + self.state = self._next_state diff --git a/examples/hex_snowflake/hex_snowflake/model.py b/examples/hex_snowflake/hex_snowflake/model.py index 349d41b0..0ea43138 100644 --- a/examples/hex_snowflake/hex_snowflake/model.py +++ b/examples/hex_snowflake/hex_snowflake/model.py @@ -1,4 +1,5 @@ import mesa +from mesa.experimental.cell_space import HexGrid from .cell import Cell @@ -15,19 +16,17 @@ def __init__(self, width=50, height=50): """ super().__init__() # Use a hexagonal grid, where edges wrap around. - self.grid = mesa.space.HexSingleGrid(width, height, torus=True) + self.grid = HexGrid((width, height), capacity=1, torus=True) # Place a dead cell at each location. - for contents, pos in self.grid.coord_iter(): - cell = Cell(pos, self) - self.grid.place_agent(cell, pos) + for entry in self.grid.all_cells: + Cell(entry, self) # activate the center(ish) cell. - centerishCell = self.grid[width // 2][height // 2] - - centerishCell.state = 1 - for a in centerishCell.neighbors: - a.isConsidered = True + centerish_cell = self.grid[(width // 2, height // 2)] + centerish_cell.agents[0].state = 1 + for a in centerish_cell.neighborhood.agents: + a.is_considered = True self.running = True diff --git a/examples/hotelling_law/hotelling_law/agents.py b/examples/hotelling_law/hotelling_law/agents.py index deaec596..d9077b82 100644 --- a/examples/hotelling_law/hotelling_law/agents.py +++ b/examples/hotelling_law/hotelling_law/agents.py @@ -2,10 +2,10 @@ import random import numpy as np -from mesa import Agent +from mesa.experimental.cell_space import CellAgent -class StoreAgent(Agent): +class StoreAgent(CellAgent): """An agent representing a store with a price and ability to move and adjust prices.""" @@ -22,10 +22,8 @@ def __init__(self, model, price=10, can_move=True, strategy="Budget"): # / differential (Premium) def estimate_market_share(self, new_position=None): - position = new_position if new_position else self.pos - nearby_consumers = self.model.grid.get_neighborhood( - position, moore=True, include_center=False, radius=8 - ) + position = new_position if new_position else self.cell + nearby_consumers = position.get_neighborhood(radius=8).agents # Filter nearby agents to include only ConsumerAgents. nearby_consumers = [ @@ -45,24 +43,23 @@ def move(self): if not self.can_move: return - best_position = self.pos + best_position = self.cell best_market_share = self.estimate_market_share() - for neighbor in self.model.grid.get_neighborhood( - self.pos, moore=True, include_center=False - ): + for neighbor in self.cell.neighborhood: potential_market_share = self.estimate_market_share(new_position=neighbor) if potential_market_share >= best_market_share: best_market_share = potential_market_share best_position = neighbor - self.model.grid.move_agent(self, best_position) + + self.cell = best_position def adjust_price(self): # Calculate competitor prices and the average competitor price competitor_prices = [ store.price - for store in self.model.get_store_agents() - if store.unique_id != self.unique_id + for store in self.model.agents_by_type[StoreAgent] + if store is not self ] average_competitor_price = ( np.mean(competitor_prices) if competitor_prices else self.price @@ -71,7 +68,7 @@ def adjust_price(self): # Calculate the current and average market share current_market_share = self.market_share all_market_shares = [ - store.market_share for store in self.model.get_store_agents() + store.market_share for store in self.model.agents_by_type[StoreAgent] ] average_market_share = np.mean(all_market_shares) if all_market_shares else 0 @@ -119,8 +116,8 @@ def adjust_price(self): def identify_competitors(self): competitors = [] - for agent in self.model.agents: - if isinstance(agent, StoreAgent) and agent.unique_id != self.unique_id: + for agent in self.model.agents_by_type[StoreAgent]: + if agent is not self: # Estimate market overlap as a measure of competition overlap = self.estimate_market_overlap(agent) if overlap > 0: # If there's any market overlap, @@ -133,7 +130,7 @@ def estimate_market_overlap(self, other_store): This could be based on shared consumer base or other factors.""" overlap = 0 - for consumer in self.model.get_consumer_agents(): + for consumer in self.model.agents_by_type[ConsumerAgent]: preferred_store = consumer.determine_preferred_store() if preferred_store in (self, other_store): overlap += 1 @@ -156,7 +153,7 @@ def step(self): self.adjust_price() -class ConsumerAgent(Agent): +class ConsumerAgent(CellAgent): """A consumer agent that chooses a store based on price and distance.""" @@ -165,8 +162,8 @@ def __init__(self, model): self.preferred_store = None def determine_preferred_store(self): - consumer_preference = self.model.consumer_preferences - stores = self.model.get_store_agents() + consumer_preference = self.model.agents_by_type[ConsumerAgent] + stores = self.model.agents_by_type[StoreAgent] if len(stores) == 0: # Check if the stores AgentSet is empty return None @@ -177,11 +174,15 @@ def determine_preferred_store(self): for store in stores: # Calculate score based on consumer preference if consumer_preference == "proximity": - score = self.euclidean_distance(self.pos, store.pos) + score = self.euclidean_distance( + self.cell.coordinate, store.cell.coordinate + ) elif consumer_preference == "price": score = store.price else: # Default case includes both proximity and price - score = store.price + self.euclidean_distance(self.pos, store.pos) + score = store.price + self.euclidean_distance( + self.cell.coordinate, store.cell.coordinate + ) # Update the list of best stores if a new minimum score is found if score < min_score: diff --git a/examples/hotelling_law/hotelling_law/model.py b/examples/hotelling_law/hotelling_law/model.py index d2bc19bc..e6e7f5ef 100644 --- a/examples/hotelling_law/hotelling_law/model.py +++ b/examples/hotelling_law/hotelling_law/model.py @@ -2,9 +2,8 @@ import numpy as np from mesa import Model -from mesa.agent import AgentSet from mesa.datacollection import DataCollector -from mesa.space import MultiGrid +from mesa.experimental.cell_space import OrthogonalMooreGrid from .agents import ConsumerAgent, StoreAgent @@ -76,11 +75,12 @@ def __init__( consumer_preferences="default", environment_type="grid", mobility_rate=80, + seed=None, ): # Initialize the model with parameters for number of agents, # grid size, mode of operation,environment type, # and mobility rate. - super().__init__() + super().__init__(seed=seed) # Total number of store agents in the model. self.num_agents = N_stores # Total number of consumers @@ -93,18 +93,15 @@ def __init__( self.consumer_preferences = consumer_preferences # Type of environment ('grid' or 'line'). self.environment_type = environment_type - # Initialize AgentSets for store and consumer agents - self.store_agents = AgentSet([], self) - self.consumer_agents = AgentSet([], self) # Initialize the spatial grid based on the specified environment type. if environment_type == "grid": - self.grid = MultiGrid( - width, height, True + self.grid = OrthogonalMooreGrid( + (width, height), True ) # A grid where multiple agents can occupy the same cell. elif environment_type == "line": - self.grid = MultiGrid( - 1, height, True + self.grid = OrthogonalMooreGrid( + (1, height), True ) # A grid representing a line (single occupancy per cell). self._initialize_agents() @@ -142,7 +139,12 @@ def get_store_price_lambda(unique_id): """Return a lambda function that gets the price of a store by its unique ID.""" return lambda m: next( - (agent.price for agent in m.store_agents if agent.unique_id == unique_id), 0 + ( + agent.price + for agent in m.agents_by_type[StoreAgent] + if agent.unique_id == unique_id + ), + 0, ) @staticmethod @@ -152,7 +154,7 @@ def get_market_share_lambda(unique_id): return lambda m: next( ( agent.market_share - for agent in m.store_agents + for agent in m.agents_by_type[StoreAgent] if agent.unique_id == unique_id ), 0, @@ -165,7 +167,7 @@ def get_revenue_lambda(unique_id): return lambda m: next( ( agent.market_share * agent.price - for agent in m.store_agents + for agent in m.agents_by_type[StoreAgent] if agent.unique_id == unique_id ), 0, @@ -186,29 +188,20 @@ def _initialize_agents(self): agent = StoreAgent(self, can_move=can_move, strategy=strategy) - self.store_agents.add(agent) - # Randomly place agents on the grid for a grid environment. - x = self.random.randrange(self.grid.width) - y = self.random.randrange(self.grid.height) - self.grid.place_agent(agent, (x, y)) + x = self.random.randrange(self.grid.dimensions[0]) + y = self.random.randrange(self.grid.dimensions[1]) + agent.cell = self.grid[(x, y)] # Place consumer agents for _ in range(self.num_consumers): # Ensure unique ID across all agents consumer = ConsumerAgent(self) - self.consumer_agents.add(consumer) # Place consumer randomly on the grid - x = self.random.randrange(self.grid.width) - y = self.random.randrange(self.grid.height) - self.grid.place_agent(consumer, (x, y)) - - def get_store_agents(self): - return self.store_agents - - def get_consumer_agents(self): - return self.consumer_agents + x = self.random.randrange(self.grid.dimensions[0]) + y = self.random.randrange(self.grid.dimensions[1]) + consumer.cell = self.grid[(x, y)] # Method to advance the simulation by one step. def step(self): @@ -222,10 +215,10 @@ def step(self): def recalculate_market_share(self): # Reset market share for all stores directly - for store in self.store_agents: + for store in self.agents_by_type[StoreAgent]: store.market_share = 0 - for consumer in self.consumer_agents: + for consumer in self.agents_by_type[ConsumerAgent]: preferred_store = consumer.determine_preferred_store() if preferred_store: preferred_store.market_share += 1 @@ -249,16 +242,18 @@ def export_data(self): def compute_average_price(self): if len(self.store_agents) == 0: return 0 - return np.mean([agent.price for agent in self.store_agents]) + return np.mean([agent.price for agent in self.agents_by_type[StoreAgent]]) # Function to compute the average market share for all store agents, def compute_average_market_share(self): if not self.store_agents: return 0 - total_consumers = sum(agent.market_share for agent in self.store_agents) - average_market_share = total_consumers / len(self.store_agents) + total_consumers = sum( + agent.market_share for agent in self.agents_by_type[StoreAgent] + ) + average_market_share = total_consumers / len(self.agents_by_type[StoreAgent]) return average_market_share def compute_price_variance(self): - return np.var([agent.price for agent in self.store_agents]) + return np.var([agent.price for agent in self.agents_by_type[StoreAgent]]) diff --git a/examples/pd_grid/pd_grid/agent.py b/examples/pd_grid/pd_grid/agent.py index 85121327..4890b749 100644 --- a/examples/pd_grid/pd_grid/agent.py +++ b/examples/pd_grid/pd_grid/agent.py @@ -1,7 +1,7 @@ -import mesa +from mesa.experimental.cell_space import CellAgent -class PDAgent(mesa.Agent): +class PDAgent(CellAgent): """Agent member of the iterated, spatial prisoner's dilemma model.""" def __init__(self, model, starting_move=None): @@ -22,14 +22,15 @@ def __init__(self, model, starting_move=None): self.next_move = None @property - def isCooroperating(self): + def is_cooroperating(self): return self.move == "C" def step(self): """Get the best neighbor's move, and change own move accordingly if better than own score.""" - neighbors = self.model.grid.get_neighbors(self.pos, True, include_center=True) + # neighbors = self.model.grid.get_neighbors(self.pos, True, include_center=True) + neighbors = [*list(self.cell.neighborhood.agents), self] best_neighbor = max(neighbors, key=lambda a: a.score) self.next_move = best_neighbor.move @@ -41,7 +42,7 @@ def advance(self): self.score += self.increment_score() def increment_score(self): - neighbors = self.model.grid.get_neighbors(self.pos, True) + neighbors = self.cell.neighborhood.agents if self.model.activation_order == "Simultaneous": moves = [neighbor.next_move for neighbor in neighbors] else: diff --git a/examples/pd_grid/pd_grid/model.py b/examples/pd_grid/pd_grid/model.py index b1c2a05c..38ef5f5b 100644 --- a/examples/pd_grid/pd_grid/model.py +++ b/examples/pd_grid/pd_grid/model.py @@ -1,4 +1,5 @@ import mesa +from mesa.experimental.cell_space import OrthogonalMooreGrid from .agent import PDAgent @@ -25,15 +26,18 @@ def __init__( Determines the agent activation regime. payoffs: (optional) Dictionary of (move, neighbor_move) payoffs. """ - super().__init__() + super().__init__(seed=seed) self.activation_order = activation_order - self.grid = mesa.space.SingleGrid(width, height, torus=True) + self.grid = OrthogonalMooreGrid((width, height), torus=True) + + if payoffs is not None: + self.payoff = payoffs # Create agents for x in range(width): for y in range(height): agent = PDAgent(self) - self.grid.place_agent(agent, (x, y)) + agent.cell = self.grid[(x, y)] self.datacollector = mesa.DataCollector( { diff --git a/examples/schelling_experimental/model.py b/examples/schelling_experimental/model.py new file mode 100644 index 00000000..862d8c0e --- /dev/null +++ b/examples/schelling_experimental/model.py @@ -0,0 +1,73 @@ +import mesa +from mesa.experimental.cell_space import CellAgent, OrthogonalMooreGrid + + +class SchellingAgent(CellAgent): + """ + Schelling segregation agent + """ + + def __init__(self, model, agent_type): + """ + Create a new Schelling agent. + + Args: + agent_type: Indicator for the agent's type (minority=1, majority=0) + """ + super().__init__(model) + self.type = agent_type + + def step(self): + similar = 0 + for neighbor in self.cell.neighborhood.agents: + if neighbor.type == self.type: + similar += 1 + + # If unhappy, move: + if similar < self.model.homophily: + self.cell = self.model.grid.select_random_empty_cell() + else: + self.model.happy += 1 + + +class Schelling(mesa.Model): + """ + Model class for the Schelling segregation model. + """ + + def __init__(self, width=20, height=20, density=0.8, minority_pc=0.2, homophily=3): + super().__init__() + self.width = width + self.height = height + self.homophily = homophily + + self.grid = OrthogonalMooreGrid((width, height), torus=True) + + self.happy = 0 + self.datacollector = mesa.DataCollector( + {"happy": "happy"}, # Model-level count of happy agents + ) + + # Set up agents + # We use a grid iterator that returns + # the coordinates of a cell as well as + # its contents. (coord_iter) + for cell in self.grid.all_cells: + if self.random.random() < density: + agent_type = 1 if self.random.random() < minority_pc else 0 + agent = SchellingAgent(self, agent_type) + agent.cell = cell + + self.datacollector.collect(self) + + def step(self): + """ + Run one step of the model. If All agents are happy, halt the model. + """ + self.happy = 0 # Reset counter of happy agents + self.agents.shuffle_do("step") + # collect data + self.datacollector.collect(self) + + if self.happy == len(self.agents): + self.running = False diff --git a/examples/shape_example/shape_example/model.py b/examples/shape_example/shape_example/model.py index 2fa82fd7..bb7c4717 100644 --- a/examples/shape_example/shape_example/model.py +++ b/examples/shape_example/shape_example/model.py @@ -1,4 +1,5 @@ import mesa +from mesa.experimental.cell_space import OrthogonalMooreGrid class Walker(mesa.Agent): @@ -13,27 +14,21 @@ def __init__(self, N=2, width=20, height=10): super().__init__() self.N = N # num of agents self.headings = ((1, 0), (0, 1), (-1, 0), (0, -1)) # tuples are fast - self.grid = mesa.space.SingleGrid(width, height, torus=False) + self.grid = OrthogonalMooreGrid((width, height), torus=True) self.make_walker_agents() self.running = True def make_walker_agents(self): - unique_id = 0 - while True: - if unique_id == self.N: - break - x = self.random.randrange(self.grid.width) - y = self.random.randrange(self.grid.height) - pos = (x, y) + for _ in range(self.N): + x = self.random.randrange(self.grid.dimensions[0]) + y = self.random.randrange(self.grid.dimensions[1]) + cell = self.grid[(x, y)] heading = self.random.choice(self.headings) # heading = (1, 0) - if self.grid.is_cell_empty(pos): - print(f"Creating agent {unique_id} at ({x}, {y})") + if cell.is_empty: a = Walker(self, heading) - - self.grid.place_agent(a, pos) - unique_id += 1 + a.cell = cell def step(self): self.agents.shuffle_do("step") diff --git a/examples/sugarscape_cg/sugarscape_cg/agents.py b/examples/sugarscape_cg/sugarscape_cg/agents.py index a75f07c2..01b24012 100644 --- a/examples/sugarscape_cg/sugarscape_cg/agents.py +++ b/examples/sugarscape_cg/sugarscape_cg/agents.py @@ -1,64 +1,60 @@ import math -import mesa +from mesa.experimental.cell_space import CellAgent, FixedAgent -def get_distance(pos_1, pos_2): +def get_distance(cell_1, cell_2): """Get the distance between two point Args: pos_1, pos_2: Coordinate tuples for both points. """ - x1, y1 = pos_1 - x2, y2 = pos_2 + x1, y1 = cell_1.coordinate + x2, y2 = cell_2.coordinate dx = x1 - x2 dy = y1 - y2 return math.sqrt(dx**2 + dy**2) -class SsAgent(mesa.Agent): - def __init__(self, model, moore=False, sugar=0, metabolism=0, vision=0): +class SsAgent(CellAgent): + def __init__(self, model, cell, sugar=0, metabolism=0, vision=0): super().__init__(model) - self.moore = moore + self.cell = cell self.sugar = sugar self.metabolism = metabolism self.vision = vision - def get_sugar(self, pos): - this_cell = self.model.grid.get_cell_list_contents([pos]) - for agent in this_cell: - if type(agent) is Sugar: + def get_sugar(self, cell): + for agent in cell.agents: + if isinstance(agent, Sugar): return agent - def is_occupied(self, pos): - this_cell = self.model.grid.get_cell_list_contents([pos]) - return any(isinstance(agent, SsAgent) for agent in this_cell) + def is_occupied(self, cell): + return any(isinstance(agent, SsAgent) for agent in cell.agents) def move(self): # Get neighborhood within vision neighbors = [ - i - for i in self.model.grid.get_neighborhood( - self.pos, self.moore, False, radius=self.vision - ) - if not self.is_occupied(i) + cell + for cell in self.cell.get_neighborhood(radius=self.vision) + if not self.is_occupied(cell) ] - neighbors.append(self.pos) + neighbors.append(self.cell) # Look for location with the most sugar - max_sugar = max(self.get_sugar(pos).amount for pos in neighbors) + max_sugar = max(self.get_sugar(cell).amount for cell in neighbors) candidates = [ - pos for pos in neighbors if self.get_sugar(pos).amount == max_sugar + cell for cell in neighbors if self.get_sugar(cell).amount == max_sugar ] # Narrow down to the nearest ones - min_dist = min(get_distance(self.pos, pos) for pos in candidates) + min_dist = min(get_distance(self.cell, cell) for cell in candidates) final_candidates = [ - pos for pos in candidates if get_distance(self.pos, pos) == min_dist + cell for cell in candidates if get_distance(self.cell, cell) == min_dist ] self.random.shuffle(final_candidates) - self.model.grid.move_agent(self, final_candidates[0]) + self.cell = final_candidates[0] def eat(self): - sugar_patch = self.get_sugar(self.pos) + sugar_patch = self.get_sugar(self.cell) self.sugar = self.sugar - self.metabolism + sugar_patch.amount sugar_patch.amount = 0 @@ -66,15 +62,15 @@ def step(self): self.move() self.eat() if self.sugar <= 0: - self.model.grid.remove_agent(self) self.remove() -class Sugar(mesa.Agent): - def __init__(self, model, max_sugar): +class Sugar(FixedAgent): + def __init__(self, model, max_sugar, cell): super().__init__(model) self.amount = max_sugar self.max_sugar = max_sugar + self.cell = cell def step(self): self.amount = min([self.max_sugar, self.amount + 1]) diff --git a/examples/sugarscape_cg/sugarscape_cg/model.py b/examples/sugarscape_cg/sugarscape_cg/model.py index 667e9b37..8569320b 100644 --- a/examples/sugarscape_cg/sugarscape_cg/model.py +++ b/examples/sugarscape_cg/sugarscape_cg/model.py @@ -12,6 +12,7 @@ from pathlib import Path import mesa +from mesa.experimental.cell_space import OrthogonalVonNeumannGrid from .agents import SsAgent, Sugar @@ -23,21 +24,25 @@ class SugarscapeCg(mesa.Model): verbose = True # Print-monitoring - def __init__(self, width=50, height=50, initial_population=100): + def __init__(self, width=50, height=50, initial_population=100, seed=None): """ - Create a new Constant Growback model with the given parameters. + Create a new constant grow back model with the given parameters. Args: + width (int): Width of the Sugarscape 2 Constant Growback model. + height (int): Height of the Sugarscape 2 Constant Growback model. initial_population: Number of population to start with + seed (int): Seed for the random number generator + """ - super().__init__() + super().__init__(seed=seed) # Set parameters self.width = width self.height = height self.initial_population = initial_population - self.grid = mesa.space.MultiGrid(self.width, self.height, torus=False) + self.grid = OrthogonalVonNeumannGrid((self.width, self.height), torus=True) self.datacollector = mesa.DataCollector( {"SsAgent": lambda m: len(m.agents_by_type[SsAgent])} ) @@ -46,10 +51,9 @@ def __init__(self, width=50, height=50, initial_population=100): import numpy as np sugar_distribution = np.genfromtxt(Path(__file__).parent / "sugar-map.txt") - for _, (x, y) in self.grid.coord_iter(): - max_sugar = sugar_distribution[x, y] - sugar = Sugar(self, max_sugar) - self.grid.place_agent(sugar, (x, y)) + for cell in self.grid.all_cells: + max_sugar = sugar_distribution[cell.coordinate] + Sugar(self, max_sugar, cell) # Create agent: for i in range(self.initial_population): @@ -58,8 +62,8 @@ def __init__(self, width=50, height=50, initial_population=100): sugar = self.random.randrange(6, 25) metabolism = self.random.randrange(2, 4) vision = self.random.randrange(1, 6) - ssa = SsAgent(self, False, sugar, metabolism, vision) - self.grid.place_agent(ssa, (x, y)) + cell = self.grid[(x, y)] + SsAgent(self, cell, sugar, metabolism, vision) self.running = True self.datacollector.collect(self) diff --git a/examples/sugarscape_g1mt/sugarscape_g1mt/model.py b/examples/sugarscape_g1mt/sugarscape_g1mt/model.py index 91bb001d..d51b8bc0 100644 --- a/examples/sugarscape_g1mt/sugarscape_g1mt/model.py +++ b/examples/sugarscape_g1mt/sugarscape_g1mt/model.py @@ -2,6 +2,7 @@ import mesa import numpy as np +from mesa.experimental.cell_space import OrthogonalVonNeumannGrid from .resource_agents import Resource from .trader_agents import Trader @@ -69,7 +70,7 @@ def __init__( self.running = True # initiate mesa grid class - self.grid = mesa.space.MultiGrid(self.width, self.height, torus=False) + self.grid = OrthogonalVonNeumannGrid((self.width, self.height), torus=False) # initiate datacollector self.datacollector = mesa.DataCollector( model_reporters={ @@ -88,11 +89,10 @@ def __init__( sugar_distribution = np.genfromtxt(Path(__file__).parent / "sugar-map.txt") spice_distribution = np.flip(sugar_distribution, 1) - for _, (x, y) in self.grid.coord_iter(): - max_sugar = sugar_distribution[x, y] - max_spice = spice_distribution[x, y] - resource = Resource(self, max_sugar, max_spice) - self.grid.place_agent(resource, (x, y)) + 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) for _ in range(self.initial_population): # get agent position @@ -111,18 +111,18 @@ def __init__( ) # give agents vision vision = int(self.random.uniform(self.vision_min, self.vision_max + 1)) + + cell = self.grid[(x, y)] # create Trader object - trader = Trader( + Trader( self, - moore=False, + cell, sugar=sugar, spice=spice, metabolism_sugar=metabolism_sugar, metabolism_spice=metabolism_spice, vision=vision, ) - # place agent - self.grid.place_agent(trader, (x, y)) def step(self): """ diff --git a/examples/sugarscape_g1mt/sugarscape_g1mt/resource_agents.py b/examples/sugarscape_g1mt/sugarscape_g1mt/resource_agents.py index 042f1672..d9f27694 100644 --- a/examples/sugarscape_g1mt/sugarscape_g1mt/resource_agents.py +++ b/examples/sugarscape_g1mt/sugarscape_g1mt/resource_agents.py @@ -1,7 +1,7 @@ -import mesa +from mesa.experimental.cell_space import FixedAgent -class Resource(mesa.Agent): +class Resource(FixedAgent): """ Resource: - contains an amount of sugar and spice @@ -9,12 +9,13 @@ class Resource(mesa.Agent): - grows 1 amount of spice at each turn """ - def __init__(self, model, max_sugar, max_spice): + 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): """ diff --git a/examples/sugarscape_g1mt/sugarscape_g1mt/trader_agents.py b/examples/sugarscape_g1mt/sugarscape_g1mt/trader_agents.py index 2c63a8a1..579f3470 100644 --- a/examples/sugarscape_g1mt/sugarscape_g1mt/trader_agents.py +++ b/examples/sugarscape_g1mt/sugarscape_g1mt/trader_agents.py @@ -1,26 +1,26 @@ import math -import mesa +from mesa.experimental.cell_space import CellAgent from .resource_agents import Resource # Helper function -def get_distance(pos_1, pos_2): +def get_distance(cell_1, cell_2): """ Calculate the Euclidean distance between two positions used in trade.move() """ - x1, y1 = pos_1 - x2, y2 = pos_2 + x1, y1 = cell_1.coordinate + x2, y2 = cell_2.coordinate dx = x1 - x2 dy = y1 - y2 return math.sqrt(dx**2 + dy**2) -class Trader(mesa.Agent): +class Trader(CellAgent): """ Trader: - has a metabolism of sugar and spice @@ -30,7 +30,7 @@ class Trader(mesa.Agent): def __init__( self, model, - moore=False, + cell, sugar=0, spice=0, metabolism_sugar=0, @@ -38,7 +38,7 @@ def __init__( vision=0, ): super().__init__(model) - self.moore = moore + self.cell = cell self.sugar = sugar self.spice = spice self.metabolism_sugar = metabolism_sugar @@ -47,35 +47,31 @@ def __init__( self.prices = [] self.trade_partners = [] - def get_resource(self, pos): - this_cell = self.model.grid.get_cell_list_contents(pos) - for agent in this_cell: - if type(agent) is Resource: + 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 {pos}") + raise Exception(f"Resource agent not found in the position {cell.coordinate}") - def get_trader(self, pos): + def get_trader(self, cell): """ helper function used in self.trade_with_neighbors() """ - this_cell = self.model.grid.get_cell_list_contents(pos) - - for agent in this_cell: + for agent in cell.agents: if isinstance(agent, Trader): return agent - def is_occupied_by_other(self, pos): + def is_occupied_by_other(self, cell): """ helper function part 1 of self.move() """ - if pos == self.pos: + 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 - this_cell = self.model.grid.get_cell_list_contents(pos) - return any(isinstance(a, Trader) for a in this_cell) + return any(isinstance(a, Trader) for a in cell.agents) def calculate_welfare(self, sugar, spice): """ @@ -202,7 +198,7 @@ def trade(self, other): if math.isclose(mrs_self, mrs_other): return - # calcualte price + # calculate price price = math.sqrt(mrs_self * mrs_other) if mrs_self > mrs_other: @@ -242,22 +238,20 @@ def move(self): # 1. identify all possible moves - neighbors = [ - i - for i in self.model.grid.get_neighborhood( - self.pos, self.moore, True, self.vision - ) - if not self.is_occupied_by_other(i) + neighboring_cells = [ + cell + for cell in self.cell.get_neighborhood(self.vision, include_center=True) + if not self.is_occupied_by_other(cell) ] # 2. determine which move maximizes welfare welfares = [ self.calculate_welfare( - self.sugar + self.get_resource(pos).sugar_amount, - self.spice + self.get_resource(pos).spice_amount, + self.sugar + self.get_resource(cell).sugar_amount, + self.spice + self.get_resource(cell).spice_amount, ) - for pos in neighbors + for cell in neighboring_cells ] # 3. Find closest best option @@ -270,22 +264,20 @@ def move(self): ] # convert index to positions of those cells - candidates = [neighbors[i] for i in candidate_indices] + candidates = [neighboring_cells[i] for i in candidate_indices] - min_dist = min(get_distance(self.pos, pos) for pos in candidates) + min_dist = min(get_distance(self.cell, cell) for cell in candidates) final_candidates = [ - pos - for pos in candidates - if math.isclose(get_distance(self.pos, pos), min_dist, rel_tol=1e-02) + cell + for cell in candidates + if math.isclose(get_distance(self.cell, cell), min_dist, rel_tol=1e-02) ] - final_candidate = self.random.choice(final_candidates) - # 4. Move Agent - self.model.grid.move_agent(self, final_candidate) + self.cell = self.random.choice(final_candidates) def eat(self): - patch = self.get_resource(self.pos) + patch = self.get_resource(self.cell) if patch.sugar_amount > 0: self.sugar += patch.sugar_amount patch.sugar_amount = 0 @@ -302,7 +294,6 @@ def maybe_die(self): """ if self.is_starved(): - self.model.grid.remove_agent(self) self.remove() def trade_with_neighbors(self): @@ -315,11 +306,9 @@ def trade_with_neighbors(self): """ neighbor_agents = [ - self.get_trader(pos) - for pos in self.model.grid.get_neighborhood( - self.pos, self.moore, False, self.vision - ) - if self.is_occupied_by_other(pos) + 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: @@ -327,7 +316,6 @@ def trade_with_neighbors(self): # iterate through traders in neighboring cells and trade for a in neighbor_agents: - if a: - self.trade(a) + self.trade(a) return diff --git a/examples/wolf_sheep/wolf_sheep/agents.py b/examples/wolf_sheep/wolf_sheep/agents.py index c0b06f3a..8e71988b 100644 --- a/examples/wolf_sheep/wolf_sheep/agents.py +++ b/examples/wolf_sheep/wolf_sheep/agents.py @@ -1,93 +1,81 @@ -import mesa +from mesa.experimental.cell_space import CellAgent, FixedAgent -from .random_walk import RandomWalker +class Animal(CellAgent): + """The base animal class.""" -class Sheep(RandomWalker): - """ - A sheep that walks around, reproduces (asexually) and gets eaten. - - The init is the same as the RandomWalker. - """ - - energy = None + def __init__(self, model, energy, p_reproduce, energy_from_food, cell): + """Initializes an animal. - def __init__(self, model, moore, energy=None): - super().__init__(model, moore=moore) + Args: + model: a model instance + energy: starting amount of energy + p_reproduce: probability of sexless reproduction + energy_from_food: energy obtained from 1 unit of food + cell: the cell in which the animal starts + """ + super().__init__(model) self.energy = energy + self.p_reproduce = p_reproduce + self.energy_from_food = energy_from_food + self.cell = cell + + def spawn_offspring(self): + """Create offspring.""" + self.energy /= 2 + self.__class__( + self.model, + self.energy, + self.p_reproduce, + self.energy_from_food, + self.cell, + ) + + def feed(self): ... def step(self): - """ - A model step. Move, then eat grass and reproduce. - """ - self.random_move() - living = True - - if self.model.grass: - # Reduce energy - self.energy -= 1 - - # If there is grass available, eat it - this_cell = self.model.grid.get_cell_list_contents([self.pos]) - grass_patch = next(obj for obj in this_cell if isinstance(obj, GrassPatch)) - if grass_patch.fully_grown: - self.energy += self.model.sheep_gain_from_food - grass_patch.fully_grown = False + """One step of the agent.""" + self.cell = self.cell.neighborhood.select_random_cell() + self.energy -= 1 - # Death - if self.energy < 0: - self.model.grid.remove_agent(self) - self.remove() - living = False + self.feed() - if living and self.random.random() < self.model.sheep_reproduce: - # Create a new sheep: - if self.model.grass: - self.energy /= 2 - lamb = Sheep(self.model, self.moore, self.energy) - self.model.grid.place_agent(lamb, self.pos) + if self.energy < 0: + self.remove() + elif self.random.random() < self.p_reproduce: + self.spawn_offspring() -class Wolf(RandomWalker): - """ - A wolf that walks around, reproduces (asexually) and eats sheep. - """ +class Sheep(Animal): + """A sheep that walks around, reproduces (asexually) and gets eaten.""" - energy = None + def feed(self): + """If possible eat the food in the current location.""" + # If there is grass available, eat it + if self.model.grass: + grass_patch = next( + obj for obj in self.cell.agents if isinstance(obj, GrassPatch) + ) + if grass_patch.fully_grown: + self.energy += self.energy_from_food + grass_patch.fully_grown = False - def __init__(self, model, moore, energy=None): - super().__init__(model, moore=moore) - self.energy = energy - def step(self): - self.random_move() - self.energy -= 1 +class Wolf(Animal): + """A wolf that walks around, reproduces (asexually) and eats sheep.""" - # If there are sheep present, eat one - x, y = self.pos - this_cell = self.model.grid.get_cell_list_contents([self.pos]) - sheep = [obj for obj in this_cell if isinstance(obj, Sheep)] + def feed(self): + """If possible eat the food in the current location.""" + sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)] if len(sheep) > 0: sheep_to_eat = self.random.choice(sheep) - self.energy += self.model.wolf_gain_from_food + self.energy += self.energy_from_food # Kill the sheep - self.model.grid.remove_agent(sheep_to_eat) sheep_to_eat.remove() - # Death or reproduction - if self.energy < 0: - self.model.grid.remove_agent(self) - self.remove() - else: - if self.random.random() < self.model.wolf_reproduce: - # Create a new wolf cub - self.energy /= 2 - cub = Wolf(self.model, self.moore, self.energy) - self.model.grid.place_agent(cub, self.pos) - -class GrassPatch(mesa.Agent): +class GrassPatch(FixedAgent): """ A patch of grass that grows at a fixed rate and it is eaten by sheep """ diff --git a/examples/wolf_sheep/wolf_sheep/model.py b/examples/wolf_sheep/wolf_sheep/model.py index 85b60b73..5b43b791 100644 --- a/examples/wolf_sheep/wolf_sheep/model.py +++ b/examples/wolf_sheep/wolf_sheep/model.py @@ -10,6 +10,7 @@ """ import mesa +from mesa.experimental.cell_space import OrthogonalMooreGrid from .agents import GrassPatch, Sheep, Wolf @@ -50,6 +51,7 @@ def __init__( grass=False, grass_regrowth_time=30, sheep_gain_from_food=4, + seed=None, ): """ Create a new Wolf-Sheep model with the given parameters. @@ -65,20 +67,16 @@ def __init__( once it is eaten sheep_gain_from_food: Energy sheep gain from grass, if enabled. """ - super().__init__() + super().__init__(seed=None) # Set parameters self.width = width self.height = height self.initial_sheep = initial_sheep self.initial_wolves = initial_wolves - self.sheep_reproduce = sheep_reproduce - 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.grid = mesa.space.MultiGrid(self.width, self.height, torus=True) + self.grid = OrthogonalMooreGrid((self.width, self.height), torus=True) collectors = { "Wolves": lambda m: len(m.agents_by_type[Wolf]), @@ -95,20 +93,20 @@ def __init__( x = self.random.randrange(self.width) y = self.random.randrange(self.height) energy = self.random.randrange(2 * self.sheep_gain_from_food) - sheep = Sheep(self, True, energy) - self.grid.place_agent(sheep, (x, y)) + Sheep( + self, energy, sheep_reproduce, sheep_gain_from_food, self.grid[(x, y)] + ) # Create wolves for _ in range(self.initial_wolves): x = self.random.randrange(self.width) y = self.random.randrange(self.height) energy = self.random.randrange(2 * self.wolf_gain_from_food) - wolf = Wolf(self, True, energy) - self.grid.place_agent(wolf, (x, y)) + Wolf(self, energy, wolf_reproduce, wolf_gain_from_food, self.grid[(x, y)]) # Create grass patches if self.grass: - for agent, (x, y) in self.grid.coord_iter(): + for cell in self.grid.all_cells: fully_grown = self.random.choice([True, False]) if fully_grown: @@ -117,7 +115,7 @@ def __init__( countdown = self.random.randrange(self.grass_regrowth_time) patch = GrassPatch(self, fully_grown, countdown) - self.grid.place_agent(patch, (x, y)) + patch.cell = cell self.running = True self.datacollector.collect(self) @@ -128,7 +126,7 @@ def step(self): # Conceptually, it can be argued that this should be modelled differently. self.random.shuffle(self.agent_types) for agent_type in self.agent_types: - self.agents_by_type[agent_type].do("step") + self.agents_by_type[agent_type].shuffle_do("step") # collect data self.datacollector.collect(self) diff --git a/examples/wolf_sheep/wolf_sheep/random_walk.py b/examples/wolf_sheep/wolf_sheep/random_walk.py deleted file mode 100644 index a204f9cc..00000000 --- a/examples/wolf_sheep/wolf_sheep/random_walk.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Generalized behavior for random walking, one grid cell at a time. -""" - -import mesa - - -class RandomWalker(mesa.Agent): - """ - Class implementing random walker methods in a generalized manner. - - Not intended to be used on its own, but to inherit its methods to multiple - other agents. - """ - - grid = None - x = None - y = None - moore = True - - def __init__(self, model, moore=True): - """ - grid: The MultiGrid object in which the agent lives. - x: The agent's current x coordinate - y: The agent's current y coordinate - moore: If True, may move in all 8 directions. - Otherwise, only up, down, left, right. - """ - super().__init__(model) - self.moore = moore - - def random_move(self): - """ - Step one cell in any allowable direction. - """ - # Pick the next cell from the adjacent cells. - next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True) - next_move = self.random.choice(next_moves) - # Now move: - self.model.grid.move_agent(self, next_move) diff --git a/examples/wolf_sheep/wolf_sheep/test_random_walk.py b/examples/wolf_sheep/wolf_sheep/test_random_walk.py deleted file mode 100644 index 393a46b1..00000000 --- a/examples/wolf_sheep/wolf_sheep/test_random_walk.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -Testing the RandomWalker by having an ABM composed only of random walker -agents. -""" - -from mesa import Model -from mesa.space import MultiGrid -from mesa.visualization.TextVisualization import TextGrid, TextVisualization -from wolf_sheep.random_walk import RandomWalker - - -class WalkerAgent(RandomWalker): - """ - Agent which only walks around. - """ - - def step(self): - self.random_move() - - -class WalkerWorld(Model): - """ - Random walker world. - """ - - height = 10 - width = 10 - - def __init__(self, width, height, agent_count): - """ - Create a new WalkerWorld. - - Args: - width, height: World size. - agent_count: How many agents to create. - """ - self.height = height - self.width = width - self.grid = MultiGrid(self.width, self.height, torus=True) - self.agent_count = agent_count - - # Create agents - for i in range(self.agent_count): - x = self.random.randrange(self.width) - y = self.random.randrange(self.height) - a = WalkerAgent(i, (x, y), self, True) - self.grid.place_agent(a, (x, y)) - - def step(self): - self.agents.shuffle_do("step") - - -class WalkerWorldViz(TextVisualization): - """ - ASCII Visualization for a WalkerWorld agent. - Each cell is displayed as the number of agents currently in that cell. - """ - - def __init__(self, model): - """ - Create a new visualization for a WalkerWorld instance. - - args: - model: An instance of a WalkerWorld model. - """ - self.model = model - grid_viz = TextGrid(self.model.grid, None) - grid_viz.converter = lambda x: str(len(x)) - self.elements = [grid_viz] - - -if __name__ == "__main__": - print("Testing 10x10 world, with 50 random walkers, for 10 steps.") - model = WalkerWorld(10, 10, 50) - viz = WalkerWorldViz(model) - for i in range(10): - print("Step:", str(i)) - viz.step()