├── README.md ├── schelling.py └── hawkdove.py /README.md: -------------------------------------------------------------------------------- 1 | Hawk Dove Model 2 | =============== 3 | 4 | A simulation of Hawk Dove game theory in python. The code was adapted from python-hawkdove. Requires python-matplotlib 5 | 6 | Run with 7 | 8 | python hawkdove.py 9 | 10 | Schelling Model 11 | =============== 12 | 13 | A simulation of Schelling's model of segregation in python. The code was taken from https://www.binpress.com/simulating-segregation-with-python/. 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /schelling.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import matplotlib.pyplot as plt 3 | import itertools 4 | import random 5 | import copy 6 | 7 | # This is taken from 8 | # https://www.binpress.com/simulating-segregation-with-python/ 9 | 10 | 11 | class Schelling: 12 | def __init__(self, width, height, empty_ratio, similarity_threshold, n_iterations, races = 2): 13 | self.width = width 14 | self.height = height 15 | self.races = races 16 | self.empty_ratio = empty_ratio 17 | self.similarity_threshold = similarity_threshold 18 | self.n_iterations = n_iterations 19 | self.empty_houses = [] 20 | self.agents = {} 21 | 22 | # The populate method is used at the beginning of the simulation. This method randomly distributes people in the grid. 23 | 24 | def populate(self): 25 | self.all_houses = list(itertools.product(range(self.width),range(self.height))) 26 | random.shuffle(self.all_houses) 27 | 28 | self.n_empty = int( self.empty_ratio * len(self.all_houses) ) 29 | self.empty_houses = self.all_houses[:self.n_empty] 30 | 31 | self.remaining_houses = self.all_houses[self.n_empty:] 32 | houses_by_race = [self.remaining_houses[i::self.races] for i in range(self.races)] 33 | for i in range(self.races): 34 | #create agents for each race 35 | self.agents.update( 36 | dict(zip(houses_by_race[i], [i+1]*len(houses_by_race[i]))).items() 37 | ) 38 | # The is_unsatisfied method takes the (x, y) coordinates of a house as arguments, checks the ratio of neighbors of similar color, and returns True if the ratio is above the happiness threshold, otherwise it returns False. 39 | 40 | def is_unsatisfied(self, x, y): 41 | 42 | race = self.agents[(x,y)] 43 | count_similar = 0 44 | count_different = 0 45 | 46 | if x > 0 and y > 0 and (x-1, y-1) not in self.empty_houses: 47 | if self.agents[(x-1, y-1)] == race: 48 | count_similar += 1 49 | else: 50 | count_different += 1 51 | if y > 0 and (x,y-1) not in self.empty_houses: 52 | if self.agents[(x,y-1)] == race: 53 | count_similar += 1 54 | else: 55 | count_different += 1 56 | if x < (self.width-1) and y > 0 and (x+1,y-1) not in self.empty_houses: 57 | if self.agents[(x+1,y-1)] == race: 58 | count_similar += 1 59 | else: 60 | count_different += 1 61 | if x > 0 and (x-1,y) not in self.empty_houses: 62 | if self.agents[(x-1,y)] == race: 63 | count_similar += 1 64 | else: 65 | count_different += 1 66 | if x < (self.width-1) and (x+1,y) not in self.empty_houses: 67 | if self.agents[(x+1,y)] == race: 68 | count_similar += 1 69 | else: 70 | count_different += 1 71 | if x > 0 and y < (self.height-1) and (x-1,y+1) not in self.empty_houses: 72 | if self.agents[(x-1,y+1)] == race: 73 | count_similar += 1 74 | else: 75 | count_different += 1 76 | if x > 0 and y < (self.height-1) and (x,y+1) not in self.empty_houses: 77 | if self.agents[(x,y+1)] == race: 78 | count_similar += 1 79 | else: 80 | count_different += 1 81 | if x < (self.width-1) and y < (self.height-1) and (x+1,y+1) not in self.empty_houses: 82 | if self.agents[(x+1,y+1)] == race: 83 | count_similar += 1 84 | else: 85 | count_different += 1 86 | 87 | if (count_similar+count_different) == 0: 88 | return False 89 | else: 90 | return float(count_similar)/(count_similar+count_different) < self.similarity_threshold 91 | 92 | # The update method checks if each person in the grid is unsatisfied, if yes it assigns the person to a randomly chosen empty house. It runs this process n_iterations times. 93 | 94 | def update(self): 95 | for i in range(self.n_iterations): 96 | self.old_agents = copy.deepcopy(self.agents) 97 | n_changes = 0 98 | for agent in self.old_agents: 99 | if self.is_unsatisfied(agent[0], agent[1]): 100 | agent_race = self.agents[agent] 101 | empty_house = random.choice(self.empty_houses) 102 | self.agents[empty_house] = agent_race 103 | del self.agents[agent] 104 | self.empty_houses.remove(empty_house) 105 | self.empty_houses.append(agent) 106 | n_changes += 1 107 | print (n_changes) 108 | if n_changes == 0: 109 | break 110 | 111 | #The move_to_empty method takes the (x, y) coordinates of a house as arguments, and moves the person living in the (x, y) house to an empty house. This method is called within the updatemethod to move the unsatisfied people to empty houses. 112 | 113 | def move_to_empty(self, x, y): 114 | race = self.agents[(x,y)] 115 | empty_house = random.choice(self.empty_houses) 116 | self.updated_agents[empty_house] = race 117 | del self.updated_agents[(x, y)] 118 | self.empty_houses.remove(empty_house) 119 | self.empty_houses.append((x, y)) 120 | 121 | #The plot method is used to draw the whole city and people living in the city. We can call this method at anytime to check the distribution of people in the city. This method takes two arguments, titleand file_name. 122 | 123 | def plot(self, title, file_name): 124 | fig, ax = plt.subplots() 125 | #If you want to run the simulation with more than 7 colors, you should set agent_colors accordingly 126 | agent_colors = {1:'b', 2:'r', 3:'g', 4:'c', 5:'m', 6:'y', 7:'k'} 127 | for agent in self.agents: 128 | ax.scatter(agent[0]+0.5, agent[1]+0.5, color=agent_colors[self.agents[agent]]) 129 | 130 | ax.set_title(title, fontsize=10, fontweight='bold') 131 | ax.set_xlim([0, self.width]) 132 | ax.set_ylim([0, self.height]) 133 | ax.set_xticks([]) 134 | ax.set_yticks([]) 135 | plt.savefig(file_name) 136 | 137 | 138 | # Now that we have our implementation of the Schelling class, we can 139 | # run different simulations and plot the results. We’ll build three 140 | # simulations with the following characteristics: 141 | 142 | # width = 50, and height = 50 (2500 houses) 143 | # 30% of empty houses 144 | # Similarity Threshold = 30% (for Simulation 1), Similarity Threshold = 50% (for Simulation 2), and Similarity Threshold = 70% (for Simulation 3) 145 | # Maximum number of iterations = 500 146 | # Number of races = 2 147 | 148 | # We start by creating and populating the cities. 149 | 150 | 151 | schelling_1 = Schelling(50, 50, 0.3, 0.3, 500, 2) 152 | schelling_1.populate() 153 | 154 | schelling_2 = Schelling(50, 50, 0.3, 0.5, 500, 2) 155 | schelling_2.populate() 156 | 157 | schelling_3 = Schelling(50, 50, 0.3, 0.7, 500, 2) 158 | schelling_3.populate() 159 | 160 | #Next, we plot the city at the initial phase. Note that the Similarity threshold has no effect on the initial state of the city. 161 | 162 | schelling_1.plot('Schelling Model with 2 colors: Initial State', 'schelling_2_initial.png') 163 | 164 | plt.show() 165 | 166 | # Next, we run the update method, and plot the final distribution for both Similarity thresholds. 167 | 168 | schelling_1.update() 169 | 170 | schelling_1.plot('Schelling Model with 2 colors: Final State with Similarity Threshold 30%', 'schelling_2_30_final.png') 171 | 172 | schelling_2.update() 173 | schelling_2.plot('Schelling Model with 2 colors: Final State with Similarity Threshold 50%', 'schelling_2_50_final.png') 174 | 175 | schelling_3.update() 176 | schelling_3.plot('Schelling Model with 2 colors: Final State with Similarity Threshold 70%', 'schelling_2_80_final.png') 177 | 178 | plt.show() 179 | -------------------------------------------------------------------------------- /hawkdove.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | from random import choice, randint, shuffle 3 | import time 4 | 5 | STARTING_DOVES = 100 6 | STARTING_HAWKS = 100 7 | STARTING_POPULATION = STARTING_HAWKS + STARTING_DOVES 8 | 9 | ROUNDS = 1000 10 | STARTING_ENERGY = 100; 11 | 12 | MAX_AGENTS = 100 13 | MIN_FOOD_PER_ROUND = 20 14 | MAX_FOOD_PER_ROUND = 70 15 | 16 | ENERGY_REQUIRED_FOR_REPRODUCTION = 250 17 | ENERGY_LOSS_PER_ROUND = 2 18 | ENERGY_COST_OF_BLUFFING = 10 19 | ENERGY_LOSS_FROM_FIGHTING = 100 20 | ENERGY_REQUIRED_FOR_LIVING = 20 21 | 22 | STATUS_ACTIVE = "active" 23 | STATUS_ASLEEP = "asleep" 24 | 25 | TYPE_HAWK = "hawk" 26 | TYPE_DOVE = "dove" 27 | 28 | agents = [] 29 | 30 | # Graph stuff 31 | graph_hawk_points = [] 32 | graph_dove_points = [] 33 | 34 | # Profiling 35 | 36 | 37 | class Agent: 38 | id = 0 39 | agent_type = None 40 | status = STATUS_ACTIVE 41 | energy = STARTING_ENERGY 42 | 43 | 44 | def main(): 45 | init() 46 | 47 | current_round = 1 48 | death_count = 0 49 | dead_hawks = 0 50 | dead_doves = 0 51 | breed_count = 0 52 | main_tic = time.process_time() 53 | 54 | while current_round <= ROUNDS and len(agents) > 2: 55 | tic = time.process_time() 56 | awakenAgents() 57 | food = getFood()*MAX_AGENTS/float(len(agents)) 58 | 59 | # This could be optimized further by creating a list every time 60 | # that only has active agents, so it isn't iterating over entire list every time 61 | # Instead just shuffle the list and go through 2 at a time 62 | shuffle(agents) 63 | for i in range(0,len(agents)-1,2): 64 | agent =agents[i] 65 | nemesis = agents[i+1] 66 | if agent is None or nemesis is None: break 67 | compete(agent, nemesis, food) 68 | 69 | # Energy cost of 'living' 70 | for agent in agents: 71 | agent.energy -= ENERGY_LOSS_PER_ROUND 72 | 73 | round_dead_hawks, round_dead_doves = cull() 74 | round_hawk_babies, round_dove_babies = breed() 75 | death_count += (round_dead_hawks + round_dead_doves) 76 | breed_count += (round_hawk_babies + round_dove_babies) 77 | 78 | 79 | toc = time.process_time() 80 | 81 | print("ROUND %d" % current_round) 82 | print("Food produced : %d" % food) 83 | print("Population : Hawks-> %d, Doves-> %d" % (getAgentCountByType(TYPE_HAWK), getAgentCountByType(TYPE_DOVE))) 84 | print("Dead hawks : %d" % round_dead_hawks) 85 | print("Dead doves : %d" % round_dead_doves) 86 | print("Hawk babies : %s" % round_hawk_babies) 87 | print("Dove babies : %s" % round_dove_babies) 88 | print("Hawks : %s" % getPercByType(TYPE_HAWK)) 89 | print("Doves : %s" % getPercByType(TYPE_DOVE)) 90 | print("----") 91 | print("Round Processing time : %s" % getTimeFormatted(toc - tic)) 92 | print("Elapsed time : %s\n" % getTimeFormatted(time.process_time() - main_tic)) 93 | 94 | # Plot 95 | graph_hawk_points.append(getAgentCountByType(TYPE_HAWK)) 96 | graph_dove_points.append(getAgentCountByType(TYPE_DOVE)) 97 | 98 | current_round += 1 99 | 100 | 101 | main_toc = time.process_time() 102 | 103 | print("=============================================================") 104 | print("Total dead agents : %d" % death_count) 105 | print("Total breeding agents : %d" % breed_count) 106 | print("Total rounds completed : %d" % (current_round - 1)) 107 | print("Total population size : %s" % len(agents)) 108 | print("Hawks : %s" % getPercByType(TYPE_HAWK)) 109 | print("Doves : %s" % getPercByType(TYPE_DOVE)) 110 | print("Processing time : %s" % getTimeFormatted(main_toc - main_tic)) 111 | print("=============================================================") 112 | 113 | 114 | def init(): 115 | 116 | for x in range(0,STARTING_DOVES): 117 | a = Agent() 118 | a.agent_type = TYPE_DOVE 119 | agents.append(a) 120 | 121 | for x2 in range(0,STARTING_HAWKS): 122 | a2 = Agent() 123 | a2.agent_type = TYPE_HAWK 124 | agents.append(a2) 125 | 126 | 127 | def getAvgFromList(list): 128 | return float( sum(list) / len(list) ) 129 | 130 | 131 | def getTimeFormatted(seconds): 132 | m, s = divmod(seconds, 60) 133 | return "%02d:%02d" % (m, s) 134 | 135 | 136 | def getFood(): 137 | return randint(MIN_FOOD_PER_ROUND, MAX_FOOD_PER_ROUND) 138 | 139 | 140 | def getPercByType(agent_type): 141 | perc = float(getAgentCountByType(agent_type)) / float(len(agents)) 142 | return '{percent:.2%}'.format(percent=perc) 143 | 144 | 145 | def getAliveAgentsCount(): 146 | return getAgentCountByStatus(STATUS_ACTIVE) + getAgentCountByStatus(STATUS_ASLEEP) 147 | 148 | 149 | def getRandomAgents(): 150 | nemesis = None 151 | active_agents = list(generateAgentsByStatus(STATUS_ACTIVE)) 152 | if len(active_agents) < 2: 153 | return None, None 154 | max_index = len(active_agents) - 1 155 | agent = active_agents[ randint(0, max_index) ] 156 | while nemesis is None: 157 | n = active_agents[ randint(0, max_index) ] 158 | if n is not agent: 159 | nemesis = n 160 | 161 | return agent, nemesis 162 | 163 | 164 | def awakenAgents(): 165 | for agent in agents: 166 | agent.status = STATUS_ACTIVE 167 | 168 | 169 | def generateAgentsByType(agent_type): 170 | for agent in agents: 171 | if agent.agent_type == agent_type: 172 | yield agent 173 | 174 | 175 | def generateAgentsByStatus(status): 176 | for agent in agents: 177 | if agent.status == status: 178 | yield agent 179 | 180 | 181 | def getEnergyFromFood(food): 182 | return food # 1 to 1 183 | 184 | 185 | def getAgentCountByStatus(status): 186 | count = len( list(generateAgentsByStatus(status)) ) 187 | return count 188 | 189 | 190 | def getAgentCountByType(agent_type): 191 | return len( list(generateAgentsByType(agent_type)) ) 192 | 193 | 194 | def compete(agent1, agent2, food): 195 | # winner = choice([agent, nemesis]) 196 | # agent2 = agent if (agent1 is nemesis) else nemesis 197 | 198 | if agent1.agent_type == TYPE_HAWK and agent2.agent_type == TYPE_HAWK: 199 | # Random agent1 chosen, agent2 gets injured, agent1 gets food 200 | agent1.energy += getEnergyFromFood(food)/2 201 | agent1.energy -= ENERGY_LOSS_FROM_FIGHTING 202 | agent2.energy += getEnergyFromFood(food)/2 203 | agent2.energy -= ENERGY_LOSS_FROM_FIGHTING 204 | 205 | if agent1.agent_type == TYPE_HAWK and agent2.agent_type == TYPE_DOVE: 206 | agent1.energy += getEnergyFromFood(food) 207 | agent2.energy -= ENERGY_COST_OF_BLUFFING 208 | 209 | if agent1.agent_type == TYPE_DOVE and agent2.agent_type == TYPE_HAWK: 210 | agent2.energy += getEnergyFromFood(food) 211 | agent1.energy -= ENERGY_COST_OF_BLUFFING 212 | 213 | if agent1.agent_type == TYPE_DOVE and agent2.agent_type == TYPE_DOVE: 214 | agent1.energy += getEnergyFromFood(food)/2 215 | agent1.energy -= ENERGY_COST_OF_BLUFFING 216 | agent2.energy += getEnergyFromFood(food)/2 217 | agent2.energy -= ENERGY_COST_OF_BLUFFING 218 | 219 | agent2.status = agent1.status = STATUS_ASLEEP 220 | 221 | 222 | def getNewAgent(agent_type, starting_energy=STARTING_ENERGY, status=STATUS_ASLEEP): 223 | agent = Agent() 224 | agent.agent_type = agent_type 225 | agent.status = status 226 | agent.energy = starting_energy 227 | return agent 228 | 229 | 230 | def breed(): 231 | """ 232 | If agent can breed, it halves its energy and produces 233 | two babies with starting energy (parent energy / 2) 234 | """ 235 | hawk_babies = 0 236 | dove_babies = 0 237 | for agent in agents: 238 | if agent.energy > ENERGY_REQUIRED_FOR_REPRODUCTION: 239 | baby_agent_a = getNewAgent(agent.agent_type, (agent.energy/2)) 240 | baby_agent_b = getNewAgent(agent.agent_type, (agent.energy/2)) 241 | 242 | agents.append(baby_agent_b) 243 | agents.append(baby_agent_a) 244 | 245 | agent.energy /= 2 246 | 247 | if agent.agent_type == TYPE_DOVE: dove_babies += 2 248 | if agent.agent_type == TYPE_HAWK: hawk_babies += 2 249 | 250 | 251 | return hawk_babies, dove_babies 252 | 253 | 254 | def cull(): 255 | 256 | dead_hawks = 0 257 | dead_doves = 0 258 | for index, agent in enumerate(agents): 259 | if agent.energy < ENERGY_REQUIRED_FOR_LIVING: 260 | if agent.agent_type == TYPE_DOVE: dead_doves += 1 261 | if agent.agent_type == TYPE_HAWK: dead_hawks += 1 262 | del agents[index] 263 | 264 | 265 | return dead_hawks, dead_doves 266 | 267 | 268 | main() 269 | 270 | try: 271 | from pylab import * 272 | except ImportError: 273 | exit() 274 | else: 275 | plot(graph_dove_points) 276 | plot(graph_hawk_points) 277 | show() 278 | --------------------------------------------------------------------------------