class SimulationModel(Model): def __init__(self, x_max, y_max, species, iterations): super(SimulationModel, self).__init__() self.starved = 0 self.space = ContinuousSpace(x_max, y_max, grid_width=20, grid_height=20, torus=True) self.schedule = SimultaneousActivation(self) self.iterations = iterations self.species = [] self.create_population(species) def create_population(self, species): for specie, lamarck, params in species: individuals = [] for param in params: individuals.append( self.create_individual(specie, lamarck, param)) self.schedule.add(individuals[-1]) self.species.append(individuals) def create_individual(self, specie, lamarck, param): x = random.random() * self.space.x_max y = random.random() * self.space.y_max ind = specie(self.space, x, y, lamarck, param) self.space.place_agent(ind, ind.pos) return ind def step(self): self.schedule.step() self.cleanup_corpses() self.update_iterations() def update_iterations(self): self.iterations -= 1 if not self.iterations: self.running = False def cleanup_corpses(self): for agent in filter(t_matcher(AutonomicAgent), self.schedule.agents): if agent.energy <= 0: self.schedule.remove(agent) # noinspection PyProtectedMember self.space._remove_agent(agent.pos, agent) self.starved += 1 def results(self): def get_energy(individual): return individual.eaten, individual.energy def get_energies(specie): return map(get_energy, specie) return map(get_energies, self.species)
class Company(Model): """ Model of a company; pyramidal structure with n different levels; workers at different levels weigh differently on the efficiency of the company as a whole. In the paper, the default parameters are level_sizes = [1, 5, 11, 21, 41, 81] (for a total of 160 employees) level_weights = [1.0, 0.9, 0.8, 0.6, 0.4, 0.2] """ def __init__(self, level_sizes=(1, 5, 10, 30), level_weights=(1.0, 0.8, 0.5, 0.2), age_distribution=default_age_dist, competency_distribution=default_competency_dist, dismissal_threshold=4, retirement_age=65, timestep_years=1 / 12., initial_vacancy_fraction=0.2, p_employee_leaves=1 / 24., competency_mechanism='common_sense', promotion_strategy='best'): assert len(level_sizes) == len(level_weights), \ "Incompatible dimensions (level sizes and weights)" assert hasattr(age_distribution, 'rvs'), \ "age_distribution must have a rvs method returning random values" assert hasattr(competency_distribution, 'rvs'), \ "competency_distribution must have a rvs method returning random values" assert promotion_strategy in ['best', 'worst', 'random'], \ "Unrecognized promotion_strategy" assert competency_mechanism in ['common_sense', 'peter'], \ "Unrecognized competency_mechanism" super().__init__() # Not the best way to pack all the constants I have self.level_sizes = level_sizes self.level_weights = level_weights self.age_distribution = age_distribution self.competency_distribution = competency_distribution self.dismissal_threshold = dismissal_threshold self.retirement_age = retirement_age self.timestep_years = timestep_years self.initial_vacancy_fraction = initial_vacancy_fraction self.dist_employees_leaving = bernoulli(p=p_employee_leaves) self.competency_mechanism = competency_mechanism self.promotion_strategy = promotion_strategy self.current_id = 0 self.schedule = SimultaneousActivation(self) # Create agents self.levels = [] for level_size in level_sizes: level = [] n_initial_agents = binom(n=level_size, p=1 - self.initial_vacancy_fraction).rvs() n_initial_agents = max(1, n_initial_agents) for i in range(n_initial_agents): agent = Employee(self.next_id(), self) self.schedule.add(agent) level.append(agent) self.levels.append(level) # Initialize data collection self.data_collector = DataCollector( model_reporters={'efficiency': calculate_efficiency}) def remove_employees(self): for level in self.levels: for employee in level: if employee.has_to_go: level.remove(employee) self.schedule.remove(employee) def pick_for_promotion_from(self, source_level): if self.promotion_strategy == 'best': return max(source_level, key=lambda e: e.competency) elif self.promotion_strategy == 'worst': return min(source_level, key=lambda e: e.competency) elif self.promotion_strategy == 'random': return self.random.choice(source_level) def recalculate_competency(self, employee): # Peter hypothesis: competency in new role not dependent on competency # in previous role if self.competency_mechanism == 'peter': employee.competency = self.competency_distribution.rvs() # Common-sense: competency mostly transferable from previous role elif self.competency_mechanism == 'common_sense': # TODO: abstract parameters used here random_variation = self.random.uniform(-1, 1) employee.competency += random_variation employee.competency = min(max(0, employee.competency), 10) # Clip def promote_employees(self): for upper_level, lower_level, target_size in zip( self.levels, self.levels[1:], self.level_sizes): while len(upper_level) < target_size and len(lower_level): chosen_employee = self.pick_for_promotion_from(lower_level) lower_level.remove(chosen_employee) self.recalculate_competency(chosen_employee) upper_level.append(chosen_employee) def hire_employees(self): bottom_level = self.levels[-1] bottom_level_target_size = self.level_sizes[-1] vacant_bottom_positions = bottom_level_target_size - len(bottom_level) for i in range(vacant_bottom_positions): agent = Employee(self.next_id(), self) self.schedule.add(agent) bottom_level.append(agent) def step(self): self.data_collector.collect(self) self.schedule.step() self.remove_employees() self.promote_employees() self.hire_employees()
class PandemicsModel(Model): def __init__(self, config=default_config, disease=dis.covid_disease): self.agents_count = config.citizens_count + config.policemen_count self.disease = disease self.deceased = [] self.buried = [] self.deceased_counter = 0 self.infected_counter = 0 self.grid = MultiGrid(config.width, config.height, True) self.safety_per_cell = np.ones((config.height, config.width)) self.buildings_map = np.zeros((config.height, config.width)) self.buildings_id_map = np.zeros((config.height, config.width)) self.schedule = SimultaneousActivation(self) self.datacollector = DataCollector( model_reporters={ "deceased": "deceased_counter", "infected": "infected_counter"}, agent_reporters={ "hp": lambda a: a.profile["hp"], "mask_protection": "mask_protection", "infection_day": lambda a: a.profile["infection_day"], "obedience": lambda a: a.profile["obedience"], "fear": lambda a: a.profile["fear"]} ) self.config = config self.buildings = {b["id"] : b for b in self.config.buildings} self.houses = [x for x in self.buildings.values() if x['type'] == 'house'] self.workplaces = [x for x in self.buildings.values() if x['type'] == 'workplace'] self.shops = [x for x in self.buildings.values() if x['type'] == 'shop'] self.add_buildings_to_map(self.buildings) self.street_positions = [] for x in range(self.config.width): for y in range(self.config.height): if self.buildings_map[y][x] == 0: self.street_positions.append((x, y)) self.house_to_agents = defaultdict(list) self.workplace_to_agents = defaultdict(list) self.current_location_type = None # Create agents for i in range(self.agents_count): if i < config.policemen_count: a = agent.create_distribution_policeman_agent( i, self, config.policemen_mental_features_distribution) a.assign_house(self, self.houses) elif i < config.policemen_count + config.citizens_count: a = agent.create_distribution_citizen_agent( i, self, config.citizens_mental_features_distribution) a.assign_house(self, self.houses) a.assign_workplace(self, self.workplaces) self.add_agent(a) for i in self.random.choices(self.schedule.agents, k=config.infected_count): i.start_infection() self.running = True self.steps_count = 0 self.datacollector.collect(self) # Returns (type, id) of the building where agent a is currently located def where_is_agent(self, a): (x, y) = a.pos return (self.buildings_map[y][x], self.buildings_id_map[y][x]) def compute_time_of_day(self): return self.steps_count % self.config.steps_per_day / (self.config.steps_per_day / HOURS_PER_DAY) def compute_current_location_type(self): t = self.compute_time_of_day() return self.config.day_plan[t] if t in self.config.day_plan else \ self.config.day_plan[min(self.config.day_plan.keys(), key=lambda k: k-t)] # Updates current location type based on time of day and the config schedule # Returns true if there is a change in current_location_type def update_current_location_type(self): t = self.compute_time_of_day() if t in self.config.day_plan: self.current_location_type = self.config.day_plan[t] return True return False def add_buildings_to_map(self, buildings): for b in buildings.values(): (x, y) = b["bottom-left"] for i in range(x, x+b["width"]): for j in range(y, y+b["height"]): self.buildings_map[j][i] = self.config.building_tags[b['type']] self.buildings_id_map[j][i] = b['id'] def add_agent(self, a): self.schedule.add(a) # Add the agent to a random grid cell x = self.random.randrange(self.grid.width) y = self.random.randrange(self.grid.height) self.grid.place_agent(a, (x, y)) def bury_agent(self, a): self.schedule.remove(a) self.grid.remove_agent(a) self.deceased_counter += 1 self.buried.append(a) def risk_from_agents(self, agents, weight): risk = 0 for a in agents: risk += 1 - a.mask_protection return risk*weight def evaluate_safety_per_cell(self): """ 1.0 is a perfectly safe cell - empty, with all neighbours and neighbours-of-neighbours empty as well """ for content, x, y in model.grid.coord_iter(): self.safety_per_cell[y][x] = 1 # initial value # Compute risk from (x,y) cell ring0_risk = self.risk_from_agents(content, weight=0.5) considered_cells = {(x,y)} # Compute risk coming from the neighbours of (x,y) cell neighbours = self.grid.get_neighborhood( (x,y), moore=True, include_center=False) neighbours_content = self.grid.get_cell_list_contents(neighbours) ring1_risk = self.risk_from_agents(neighbours_content, 0.25) considered_cells | set(neighbours) # Compute risk coming from # the neighbours of the neighbours of (x,y) cell neighbours_of_neighbours = set() for c in neighbours: neighbours_of_neighbours | set( self.grid.get_neighborhood( (x,y),moore=True, include_center=False)) neighbours_of_neighbours -= considered_cells ring2_risk = self.risk_from_agents( self.grid.get_cell_list_contents(neighbours_of_neighbours), 0.125) self.safety_per_cell[y][x] -= ring0_risk + ring1_risk + ring2_risk def step(self): if (self.update_current_location_type()): for a in self.schedule.agents: if (self.current_location_type is not None): b = a.select_building(self.current_location_type) a.teleport_to_building(b) else: a.teleport_to_street() self.evaluate_safety_per_cell() self.schedule.step() self.steps_count += 1 # collect data self.datacollector.collect(self) for d in self.deceased: self.bury_agent(d) self.deceased = [] def run_model(self, n): for i in range(n): self.step()
class World(Model): """ The class World which inherits from Model and is responsible for the intarations for the experiment. Attributs: gridsize: dimentions of the world grid cop_density: density of the cops placed in world citizen_density: density of the citizens in world agent_type: the alignment of agent either as cop or citizen l_state: the legitimacy state in world reduction_constant: the constant attribute which decide by what rate the state of l_state will reduce """ def __init__( self, gridsize, cop_density, citizen_density, agent_type, legitimacy, l_state, reduction_constant, active_threshold, include_wealth, rich_threshold, ): # Create a new World instance. # Args: # gridsize: the size of grid # cop_density: density of cops to be placed # citizen_density: density of citizens to be placed # agent_type: the alignment of agent either as cop or citizen # l_state: the legitimacy state # reduction_constant: the constant attribute which decide by what rate # the state of l_state will reduce self.cop_density = cop_density self.citizen_density = citizen_density self.agent_type = agent_type self.legitimacy = legitimacy self.l_state = l_state self.reduction_constant = reduction_constant self.active_threshold = active_threshold self.include_wealth = include_wealth self.rich_threshold = rich_threshold self.ap_constant = 2.3 # Agent count r_c: rich_count, r_a_c: rich_active_count, m_c: middle_count, m_a_c: middle_active_count, p_c: poor_count, p_a_c: poor_active_count,. self.r_c = 0 self.r_a_c = 0 self.m_c = 0 self.m_a_c = 0 self.p_c = 0 self.p_a_c = 0 self.mean = 0 self.kill_agents = [] self.agents_killed = 0 self.grid = MultiGrid(gridsize, gridsize, False) self.schedule = SimultaneousActivation(self) self.placement(gridsize) self.running = True def placement(self, gridsize): # Placement of agents inside the Grid # Arguments: # gridsize: Dimensions of grid unique_id = 1 if self.cop_density + self.citizen_density > 1: print("Density ratios must not exceed 1", file=sys.stderr) self.bank = Bank(1, self) for (_, x, y) in self.grid.coord_iter(): if self.random.random() < self.cop_density: a = Cop(unique_id, self) self.schedule.add(a) self.grid.place_agent(a, (x, y)) unique_id += 1 elif self.random.random() < (self.cop_density + self.citizen_density): a = Citizen( unique_id, self, hardship=self.random.random(), risk_aversion=self.random.random(), bank=self.bank, rich_threshold=self.rich_threshold, ) self.schedule.add(a) self.grid.place_agent(a, (x, y)) unique_id += 1 self.datacollector = DataCollector( model_reporters={ "Poor Grievance": lambda m: self.measure_poor_grievance(m), "Middle Grievance": lambda m: self.measure_middle_grievance(m), "Rich Grievance": lambda m: self.measure_rich_grievance(m), "Calm": lambda m: self.count_calm(m), "Revolt": lambda m: self.count_revolt(m), "Jail": lambda m: self.count_jailed(m), "Cops": lambda m: self.count_cops(m), "Rich": lambda m: self.count_rich(m), "Middle": lambda m: self.count_middle(m), "Poor": lambda m: self.count_poor(m), "Rich Wealth": lambda m: self.measure_rich_wealth(m), "Middle Wealth": lambda m: self.measure_middle_wealth(m), "Poor Wealth": lambda m: self.measure_poor_wealth(m), "Rich Confidence": lambda m: self.measure_rich_confidence(m), "Middle Confidence": lambda m: self.measure_middle_confidence(m), "Poor Confidence": lambda m: self.measure_poor_confidence(m), "Rich Hardship": lambda m: self.measure_rich_hardship(m), "Middle Hardship": lambda m: self.measure_middle_hardship(m), "Poor Hardship": lambda m: self.measure_poor_hardship(m), "Legitimacy": lambda m: self.measure_legitimacy(m), "WO Revolt": lambda m: self.wo_wealth_active(m), "WO Calm": lambda m: self.wo_wealth_calm(m), "WO Jail": lambda m: self.wo_wealth_jail(m), }) def update_agent_count(self): # Updates the number of current and active agents self.r_c = len([ a for a in self.schedule.agents if a.alignment == "Citizen" and a.status == "Rich" ]) self.r_a_c = len([ a for a in self.schedule.agents if a.alignment == "Citizen" and a.status == "Rich" and a.state == "Revolt" ]) self.m_c = len([ a for a in self.schedule.agents if a.alignment == "Citizen" and a.status == "Middle" ]) self.m_a_c = len([ a for a in self.schedule.agents if a.alignment == "Citizen" and a.status == "Middle" and a.state == "Revolt" ]) self.p_c = len([ a for a in self.schedule.agents if a.alignment == "Citizen" and a.status == "Poor" ]) self.p_a_c = len([ a for a in self.schedule.agents if a.alignment == "Citizen" and a.status == "Poor" and a.state == "Revolt" ]) def mean_wealth(self): # Calculate the mean wealth of all the citizen agents self.agents = [ agent.savings for agent in self.schedule.agents if agent.alignment == "Citizen" ] self.mean = s.mean(self.agents) def update_core(self): if self.l_state: if self.legitimacy > 0.0: self.legitimacy -= self.reduction_constant else: self.legitimacy = 0.0 def step(self): # Calculation of world attributes in one step(iteration) of execution self.update_agent_count() self.datacollector.collect(self) self.mean_wealth() self.schedule.step() self.update_core() if self.kill_agents: self.kill_agents = list(dict.fromkeys(self.kill_agents)) for i in self.kill_agents: self.grid.remove_agent(i) self.schedule.remove(i) self.kill_agents = [] total_agents = len( [a for a in self.schedule.agents if a.alignment == "Citizen"]) if total_agents < 2: self.running = False @staticmethod def count_calm(model): a = len([ a for a in model.schedule.agents if a.alignment == "Citizen" and a.state == "Calm" ]) return a @staticmethod def count_revolt(model): a = len([ a for a in model.schedule.agents if a.alignment == "Citizen" and a.state == "Revolt" ]) return a @staticmethod def count_jailed(model): a = len([ a for a in model.schedule.agents if a.alignment == "Citizen" and a.state == "Jail" ]) return a @staticmethod def count_cops(model): a = len([a for a in model.schedule.agents if a.alignment == "Cop"]) return a @staticmethod def count_rich(model): a = len([ a for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Rich" ]) return a @staticmethod def count_middle(model): a = len([ a for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Middle" ]) return a @staticmethod def count_poor(model): a = len([ a for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Poor" ]) return a @staticmethod def measure_poor_grievance(model): if model.include_wealth: confidence = [ a.grievance for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Poor" ] if confidence: total = sum(confidence) return total else: return 0 else: confidence = [ a.grievance for a in model.schedule.agents if a.alignment == "Citizen" ] return sum(confidence) @staticmethod def measure_middle_grievance(model): confidence = [ a.grievance for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Middle" ] if confidence: total = sum(confidence) return total else: return 0 @staticmethod def measure_rich_grievance(model): confidence = [ a.grievance for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Rich" ] if confidence: total = sum(confidence) return total else: return 0 @staticmethod def measure_rich_wealth(model): wealth = [ a.wealth for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Rich" ] return s.mean(wealth) if wealth else 0 @staticmethod def measure_middle_wealth(model): wealth = [ a.wealth for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Middle" ] return s.mean(wealth) if wealth else 0 @staticmethod def measure_poor_wealth(model): wealth = [ a.wealth for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Poor" ] return s.mean(wealth) if wealth else 0 @staticmethod def measure_poor_confidence(model): confidence = [ a.confidence for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Poor" ] if confidence: total = s.mean(confidence) return total else: return 0 @staticmethod def measure_middle_confidence(model): confidence = [ a.confidence for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Middle" ] if confidence: total = s.mean(confidence) return total else: return 0 @staticmethod def measure_rich_confidence(model): confidence = [ a.confidence for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Rich" ] if confidence: total = s.mean(confidence) return total else: return 0 @staticmethod def measure_total_reserves(model): return s.mean(model.bank.total_reserves) @staticmethod def measure_poor_hardship(model): confidence = [ a.hardship for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Poor" ] if confidence: total = sum(confidence) return total else: return 0 @staticmethod def measure_middle_hardship(model): confidence = [ a.hardship for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Middle" ] if confidence: total = sum(confidence) return total else: return 0 @staticmethod def measure_rich_hardship(model): confidence = [ a.hardship for a in model.schedule.agents if a.alignment == "Citizen" and a.status == "Rich" ] if confidence: total = sum(confidence) return total else: return 0 @staticmethod def measure_legitimacy(model): return model.legitimacy * 100 @staticmethod def wo_wealth_active(model): if model.include_wealth == False: active = [ a for a in model.schedule.agents if a.alignment == "Citizen" and a.state == "Revolt" ] return len(active) else: return 0 @staticmethod def wo_wealth_calm(model): if model.include_wealth == False: active = [ a for a in model.schedule.agents if a.alignment == "Citizen" and a.state == "Calm" ] return len(active) else: return 0 @staticmethod def wo_wealth_jail(model): if model.include_wealth == False: active = [ a for a in model.schedule.agents if a.alignment == "Citizen" and a.state == "Jail" ] return len(active) else: return 0
class SegregationModelModel(Model): def __init__(self, datacollector=None): super().__init__() # work from directory this file is in os.chdir(os.path.dirname(os.path.realpath(__file__))) self.schedule = SimultaneousActivation(self) self.G = nx.Graph() self.time = 0 # simple iteration counter self._generate_sites() self.grid = NetworkGrid(self.G) # make a dictionary of {hash: site} values for easy relation lookups in agent generation self.site_hashes = {h: s for s, h in dict( self.G.nodes.data('hash')).items()} self._generate_agents() self.vita_groups = [] self.datacollector = datacollector def step(self): if self.datacollector: self.datacollector.collect(self) else: warnings.warn('This Model has no DataCollector! You may want to add one in the `datacollector` attribute ' 'before running the model') self.schedule.step() while self.vita_groups: a = self.vita_groups.pop() a.unique_id = self.next_id() a.model = self self.schedule.add(a) for a in self.schedule.agents: if a.get('__void__', False): self.grid._remove_agent(a, a.pos) self.schedule.remove(a) self.time += 1 # ------------------------- INITIALIZATION HELPERS ------------------------- def _generate_agents(self): """ Called once during __init__ to create appropriate groups from the original simulation's model and add them to the model grid. Loads group data from the JSON file created during translation. """ with open("SegregationModelGroups.json", 'r') as file: j = json.load(file) for group in j: for _ in range(group['m']): a = SegregationModelAgent( self.next_id(), self, group['attr'], group['rel']) self.schedule.add(a) def _generate_sites(self): """ Called once during __init__ to load the original simulation's sites into the networkx graph. Loads site data from a JSON file created during translation. """ with open("SegregationModelSites.json", 'r') as file: j = json.load(file) for site in j: self.G.add_node( str(site['name']), hash=site['hash'], rel_name=site['rel_name']) for k, v in site['attr'].items(): self.G.nodes[str(site['name'])][k] = v # ------------------------- RUNTIME FUNCTIONS ------------------------- def get_attr(self, agent_or_node, name=None): """ Retrieves an attribute of a Mesa Agent or NetworkGrid node. :param name: A string containing the attribute to retrieve, or None :param agent_or_node: A Mesa Agent or a string corresponding to a node in the NetworkGrid :return: If agent_or_node is a string, returns the named attribute represented by it, or the node's entire attribute dictionary if name is None (note: this includes the special 'agent' attribute) If agent_or_node is an Agent, returns the named attribute of that Agent """ name = mpi(name) if name is not None else name if isinstance(agent_or_node, str): node_dict = self.grid.G.nodes[agent_or_node] return node_dict.get(name) if name is not None else node_dict elif isinstance(agent_or_node, Agent): # return getattr(agent_or_node, name, agent_or_node.namespace[name]) return agent_or_node.get(name) else: raise TypeError( f"get_attr expected a str or Agent for agent_or_node, but received {type(agent_or_node)}") def get_groups(self, node_or_model, qry=None): """ Returns a list of agents at the node or the entire model that satisfy the qry. :param node_or_model: A string corresponding to a node in the NetworkGrid, or a Mesa Model :param qry: a GroupQry namedtuple :return: a list of agents at the node satisfying the qry. """ if isinstance(node_or_model, Model): agents = node_or_model.schedule.agents elif isinstance(node_or_model, str): agents = self.grid.get_cell_list_contents([node_or_model]) else: raise TypeError( f"get_groups expects a str or Model for node_or_model, but received {type(node_or_model)}") return [a for a in agents if a.matches_qry(qry)] # if not qry: # return agents # # the code below is REALLY PAINFUL... replacing it with 'return agents` makes the code run like 20x faster # elif qry.full: # return [a for a in agents # if qry.attr.items() == {k: getattr(a, k) for k in a._attr}.items() # and qry.rel.items() == {k: getattr(a, k) for k in a._rel}.items() # and all([fn(a) for fn in qry.cond])] # else: # return [a for a in agents # if qry.attr.items() <= {k: getattr(a, k) for k in a._attr}.items() # and qry.rel.items() <= {k: getattr(a, k) for k in a._rel}.items() # and all([fn(a) for fn in qry.cond])] def get_mass(self, agent_node_model, qry=None): """ If agent_node_model is an agent, returns the number of agents with the same attributes as it, including itself. This ignores unique_id (and source_name). This is probably very unoptimized. If agent_node_model is a string corresponding to a node in the NetworkGrid, returns the number of agents at that node with the attributes specified in qry, or all agents at that node if qry is None. If agent_node_model is a Model, returns the total number of agents in the model. """ if isinstance(agent_node_model, str): return len(self.get_groups(agent_node_model, qry)) elif isinstance(agent_node_model, Agent): mod_dict = {k: v for k, v in agent_node_model.__dict__.items() if k not in ('unique_id', 'source_name')} # toss unique identifiers return sum([mod_dict == {k: v for k, v in a.__dict__.items() if k not in ('unique_id', 'source_name')} for a in self.schedule.agents]) elif isinstance(agent_node_model, Model): return len(agent_node_model.schedule.agents) else: raise TypeError(f"get_mass expects a str, Agent, or Model for agent_node_model, but received " f"{type(agent_node_model)}")
class BurglaryModel(Model): def __init__(self, N, width, height, b_rate, delta, omega, theta, mu, gamma, space): self.num_agents = N self.grid = MultiGrid(width, height, True) self.width = width self.height = height self.houses = self.width * self.height self.schedule = SimultaneousActivation(self) self.house_schedule = SimultaneousActivation(self) self.b_rate = b_rate self.delta = delta self.omega = omega self.theta = theta self.mu = mu self.kill_agents = [] self.gamma = gamma self.gen_agent = 1 - math.exp(-self.gamma*self.delta) self.total_agents = self.num_agents self.space = space a_0 = 0.2 # place houses on grid, 1 house per grid location for i in range(self.width): for j in range(self.height): num = str(i) + str(j) num = int(num) a = House(num, self, a_0, i, j, self.delta, self.omega, self.theta, self.mu, self.space) self.grid.place_agent(a, (a.x_point, a.y_point)) self.house_schedule.add(a) # place the criminals for k in range(self.num_agents): unique_id = "criminal" + str(k) criminal = Criminal(unique_id, self, self.width, self.height) self.grid.place_agent(criminal, (criminal.x_point, criminal.y_point)) self.schedule.add(criminal) # set up data collection self.datacollector = DataCollector( model_reporters={"Mean_Attractiveness": get_mean_att, "Max_Attractiveness": get_max_att, "Min_Attractiveness": get_min_att, "CrimeEvents": get_num_burgles, "Criminals": get_num_criminals, "MaxPos": get_max_att_pos}, agent_reporters={"Att": lambda x: x.att_t if x.unique_id[:1]!="c" else None}) def add_criminals(self): start_count = self.total_agents + 1 for i in range(self.houses): y = random.random() if y < self.gen_agent: unique_id = "criminal" + str(start_count) criminal = Criminal(unique_id, self, self.width, self.height) self.grid.place_agent(criminal, (criminal.x_point, criminal.y_point)) self.schedule.add(criminal) start_count = start_count + 1 self.total_agents = start_count self.num_agents = self.num_agents + 1 def step(self): self.datacollector.collect(self) # cycle through all houses and calculate updates on their attractiveness self.house_schedule.step() self.schedule.step() for row in self.kill_agents: try: self.grid.remove_agent(row) self.schedule.remove(row) self.kill_agents.remove(row) self.num_agents = self.num_agents - 1 except: self.kill_agents.remove(row) # add new criminals self.add_criminals()
class SIRSModelModel(Model): def __init__(self, datacollector=None): super().__init__() # work from directory this file is in os.chdir(os.path.dirname(os.path.realpath(__file__))) self.schedule = SimultaneousActivation(self) self.G = nx.Graph() self.time = 0 # simple iteration counter self._generate_sites() self.grid = NetworkGrid(self.G) # make a dictionary of {hash: site} values for easy relation lookups in agent generation self.site_hashes = { h: s for s, h in dict(self.G.nodes.data('hash')).items() } self._generate_agents() self.vita_groups = [] self.datacollector = datacollector def step(self): if self.datacollector: self.datacollector.collect(self) else: warnings.warn( 'This Model has no DataCollector! You may want to add one in the `datacollector` attribute ' 'before running the model') self.schedule.step() while self.vita_groups: a = self.vita_groups.pop() a.unique_id = self.next_id() a.model = self self.schedule.add(a) for a in self.schedule.agents: if a.get('__void__', False): self.grid._remove_agent(a, a.pos) self.schedule.remove(a) self.time += 1 # ------------------------- INITIALIZATION HELPERS ------------------------- def _generate_agents(self): """ Called once during __init__ to create appropriate groups from the original simulation's model and add them to the model grid. Loads group data from the JSON file created during translation. """ with open("SIRSModelGroups.json", 'r') as file: j = json.load(file) for group in j: for _ in range(group['m']): a = SIRSModelAgent(self.next_id(), self, group['attr'], group['rel']) self.schedule.add(a) def _generate_sites(self): """ Called once during __init__ to load the original simulation's sites into the networkx graph. Loads site data from a JSON file created during translation. """ with open("SIRSModelSites.json", 'r') as file: j = json.load(file) for site in j: self.G.add_node(str(site['name']), hash=site['hash'], rel_name=site['rel_name']) for k, v in site['attr'].items(): self.G.nodes[str(site['name'])][k] = v # ------------------------- RUNTIME FUNCTIONS ------------------------- def get_attr(self, agent_or_node, name=None): """ Retrieves an attribute of a Mesa Agent or NetworkGrid node. :param name: A string containing the attribute to retrieve, or None :param agent_or_node: A Mesa Agent or a string corresponding to a node in the NetworkGrid :return: If agent_or_node is a string, returns the named attribute represented by it, or the node's entire attribute dictionary if name is None (note: this includes the special 'agent' attribute) If agent_or_node is an Agent, returns the named attribute of that Agent """ name = mpi(name) if name is not None else name if isinstance(agent_or_node, str): node_dict = self.grid.G.nodes[agent_or_node] return node_dict.get(name) if name is not None else node_dict elif isinstance(agent_or_node, Agent): # return getattr(agent_or_node, name, agent_or_node.namespace[name]) return agent_or_node.get(name) else: raise TypeError( f"get_attr expected a str or Agent for agent_or_node, but received {type(agent_or_node)}" )
class InfectionModel(Model): """ Mesa model class that simulates infection spread """ def __init__(self, params: dict): """ Parameters ---------- params: dict Simulation parameters """ self.current_id = 0 # inherited variable, for id generation self.params = params # parameters self.statistics = { # statistics for data collector "infected": 0, "recovered": 0, "susceptible": 0, "vaccinated": 0, "deaths": 0, "alive": 0, "total_infections": 0, "total_recoveries": 0, } self.grid = MultiGrid(self.params['grid_width'], self.params['grid_height'], True) # grid that agents move on self.schedule = SimultaneousActivation(self) # scheduler for iterations of the simulation self.dataCollector = DataCollector(model_reporters={ # to collect data for the graph "infected": lambda m: m.statistics["infected"], "recovered": lambda m: m.statistics["recovered"], "susceptible": lambda m: m.statistics["susceptible"], "vaccinated": lambda m: m.statistics["vaccinated"], "deaths": lambda m: m.statistics["deaths"], "alive": lambda m: m.statistics["alive"], "total_infections": lambda m: m.statistics["total_infections"], "total_recoveries": lambda m: m.statistics["total_recoveries"], }) self.running = True # required for visualization, tells if simulation is done self.dead_agents = [] # when agents die, they are added to this list to be removed self.step_count = 0 # number of steps completed, required for vaccination self.vaccination_started = False # has vaccination started? # creating agents for _ in range(self.params['num_agents']): # initial state of this agent initial_state = InfectionState.INF if \ self.random.uniform(0, 1) < self.params['initial_infected_chance'] else InfectionState.SUS # by default, this won't add to total_infections which leads to incorrect results if initial_state == InfectionState.INF: self.statistics["total_infections"] += 1 # randomise position pos = self.random.randrange(self.grid.width), self.random.randrange(self.grid.height) self.add_agent(self.create_agent(initial_state), pos) def check_running(self): """ Checks and returns if the simulation is still running (there are infected people) """ for agent in self.schedule.agent_buffer(): if agent.state == InfectionState.INF: return True return False def step(self): """ Called every step """ # just to show the progress while running in console if self.step_count % 100 == 0: print(self.step_count) self.per_agent_actions() # simulate actions to be taken globally on all agents self.schedule.step() # run step for all agents # collect data at a particular frequency if self.step_count % self.params['data_collection_frequency'] == 0: self.calculate_statistics() # calculate statistics for data collector self.dataCollector.collect(self) # collect data self.step_count += 1 # if vaccination is enabled and enough time has passed if not self.vaccination_started and self.params['vaccination_start'] != -1 and \ self.step_count > self.params['vaccination_start']: self.vaccination_started = True # start vaccination for x in self.dead_agents: # remove dead agents self.remove_agent(x) self.statistics["deaths"] += 1 # add to death count self.dead_agents = [] self.running = self.check_running() # is the simulation still running? def calculate_statistics(self): """ Calculates statistics each iteration, for more efficient data collection """ # reset all iteration specific parameters self.statistics["infected"] = 0 self.statistics["recovered"] = 0 self.statistics["susceptible"] = 0 self.statistics["vaccinated"] = 0 self.statistics["alive"] = 0 for agent in self.schedule.agent_buffer(): self.statistics["alive"] += 1 if agent.state == InfectionState.INF: self.statistics["infected"] += 1 elif agent.state == InfectionState.SUS: self.statistics["susceptible"] += 1 elif agent.state == InfectionState.REC: self.statistics["recovered"] += 1 elif agent.state == InfectionState.VAC: self.statistics["vaccinated"] += 1 def per_agent_actions(self): """ Simulates actions to be taken on a global scale per agent """ # this should only occur once a day if self.step_count % 24 != 0: return for agent in self.schedule.agent_buffer(): if self.random.uniform(0, 1) < self.params['external_infection_chance']: agent.state = InfectionState.INF self.statistics["total_infections"] += 1 def create_agent(self, initial_state: InfectionState) -> PersonAgent: """ Creates an agent, and returns it Parameters ---------- initial_state : InfectionState Initial infection state of this agent Returns ------- PersonAgent The agent created """ return PersonAgent(self.next_id(), self, initial_state) def add_agent(self, agent: Agent, pos: Tuple[int, int]): """ Adds an agent to the simulation Parameters ---------- agent : agent The agent to be added pos : Tuple[int, int] The position where this agent should be on the grid """ # add to scheduler self.schedule.add(agent) # assign position self.grid.place_agent(agent, pos) def remove_agent(self, agent: Agent): """ Removes an agent from the simulation Parameters ---------- agent : Agent The agent to be removed from the simulation """ self.grid.remove_agent(agent) self.schedule.remove(agent)
class trafficSimulation(Model): def __init__(self, spawn_speed): self.spawn_speed = spawn_speed self.counter = 0 self.traffic_lights_schedule = TrafficScheduler(self) self.cars_schedule = SimultaneousActivation(self) self.grid = MultiGrid(10, 10, False) self.id = 0 self.spawnpoints = [(9, 5), (0, 4), (6, 0), (5, 9)] self.kill_agents = [] self.datacollector = DataCollector(model_reporters={"Grid": get_grid}) traffic_light_coords = [(7, 5), (4, 4), (6, 3), (5, 6)] for coord in traffic_light_coords: light = trafficLight(self.id, coord, self) self.id = self.id + 1 self.grid.place_agent(light, coord) self.traffic_lights_schedule.add(light) self.datacollector.collect(self) def get_data(self): traffic_lights = [{ "coords": { "x": agent.coords[0], "y": agent.coords[1] }, "state": stateToString(agent.state) } for agent in get_other_lights(-1, self)] cars = [{ "direction": { "x": agent.direction[0], "y": agent.direction[1] }, "coords": { "x": agent.coords[0], "y": agent.coords[1] } } for agent in self.cars_schedule.agents] return traffic_lights, cars def step(self): self.traffic_lights_schedule.step() self.cars_schedule.step() if self.counter == self.spawn_speed - 1 and random.random() > 0.5: orientation, coords = random.choice( [x for x in zip(direction.lst, self.spawnpoints)]) anyCar = any([ isinstance(agent, carAgent) for agent in self.grid.iter_neighbors(coords, False, True, 0) ]) if not anyCar: car = carAgent(self.id, coords, orientation, self) self.grid.place_agent(car, coords) self.cars_schedule.add(car) self.id = self.id + 1 self.counter = (self.counter + 1) % self.spawn_speed for x in self.kill_agents: self.grid.remove_agent(x) self.cars_schedule.remove(x) self.kill_agents.remove(x)
class ClimateMigrationModel(Model): def __init__( self, num_counties, preferences, network_type, \ climate_threshold, limited_radius=True, init_time=0): super().__init__() global TICK TICK = init_time self.num_agents = 0 self.agent_index = 0 self.preferences = preferences self.limited_radius = limited_radius self.upper_network_size = 3 self.network_type = network_type self.climate_threshold = climate_threshold self.schedule = SimultaneousActivation(self) self.G = create_graph() self.num_counties = num_counties self.nodes = self.G.nodes() self.grid = NetworkGrid(self.G) self.county_climate_ranking = [] self.county_population_list = [0] * self.num_counties self.county_flux = [0] * self.num_counties self.deaths = [] self.births = [] self.county_income = {} self.datacollector = DataCollector(model_reporters={"County Population": lambda m1: list(m1.county_population_list), "County Influx": lambda m2: list(m2.county_flux), "Deaths": lambda m3: m3.deaths, "Births": lambda m4: m4.births, "Total Population": lambda m5: m5.num_agents}) def add_agents(self): """ Adds agents based on 2013 ACS population data. """ cumulative_population_list = get_cumulative_population_list() self.county_population_list = get_population_list() county = 0 # keeps track of which county each agent should be placed in index = 0 # keeps track of each agent's unique_id # keep creating agents until county population is reached while index < cumulative_population_list[county]: # create agent agent = Household(index, self) # place agent in appropriate county self.grid.place_agent(agent, list(self.nodes)[county]) # add agent to schedule self.schedule.add(agent) # set agent's original_pos attribute agent.original_pos = agent.pos # initialize all other agent attributes agent.initialize_agent() # if running model with heterogeneous preferences, set agent preference if self.preferences: agent.initialize_preference() # update index index += 1 # if done with county and not at last county, increase county if index == cumulative_population_list[county] and county < self.num_counties - 1: county += 1 # after all agents are added, set model attributes self.num_agents = cumulative_population_list[self.num_counties-1] self.agent_index = cumulative_population_list[self.num_counties-1] def initialize_all_random_networks(self): """ Initializes random networks for all agents in model. """ for a in self.schedule.agents: a.initialize_random_network() def initialize_all_income_networks(self): """ Initializes income-based networks for all agents in model. """ for a in self.schedule.agents: a.initialize_income_network() def initialize_all_age_networks(self): """ Initializes age-based networks for all agents in model. """ for a in self.schedule.agents: a.initialize_age_network() def initialize_all_income_age_networks(self): """ Initializes income and age-based networks for all agents in model. """ for a in self.schedule.agents: a.initialize_income_age_network() def initialize_all_families(self): """ Initializes families for all agents in model. """ for a in self.schedule.agents: a.initialize_family() def update_population(self): """ Updates population by adding and removing agents. """ # keep track of number of deaths and births per county self.deaths = [0]*self.num_counties self.births = [0]*self.num_counties # remove agents (death) # loop through all agents for agent in self.schedule.agents: # source: https://www.ssa.gov/oact/STATS/table4c6.html#ss # calculate death probability by age in the united states if random.random() < 0.0001*(math.e**(0.075*agent.age)): # keep track of deaths by county self.deaths[agent.pos] += 1 # remove agent from model self.grid._remove_agent(agent, agent.pos) # remove agent from schedule self.schedule.remove(agent) # update number of agents self.num_agents -= 1 # add agents (birth) # loop through all counties for county in range(self.num_counties): # source: https://www.cdc.gov/nchs/fastats/births.htm # access current population current_population = self.county_population_list[county] # calculate how many agents should be added to_add = current_population//100 # birth rate # update number of agents self.num_agents += to_add # add specified number of agents for count in range(to_add): # update agent index self.agent_index += 1 # create new agent agent = Household(self.agent_index, self) # place agent in current county self.grid.place_agent(agent, county) # add agent to schedule self.schedule.add(agent) # initialize agent attributes and networks # agents are assumed to be 18 as that is the lower bound of a householder's age agent.age = 18 # based on age, income is assigned agent.initialize_income(random.random()) # based on income, tenure is assigned agent.initialize_tenure(random.random()) # input-specified network is initialized agent.initialize_network() # family is initialized agent.initialize_family() if self.preferences: agent.initialize_preference() # original position is set agent.original_pos = agent.pos # keep track of births by county self.births[agent.pos] += 1 # loop through counties, update population counts for county in self.nodes: self.county_population_list[county] = len(self.G.node[county]['agent']) def update_climate(self): """ Update climate variables based on NOAA's predictions. Index 1 represents number of days above 90 degrees Fahrenheit. Index 4 represents number of days with < 1 inch of rain. Index 7 represents number of days without rain. Indexes 3, 6, and 9 are the yearly increases/decreases for these estimates. The update function is a simple linear function. Note: More accurate climate data could be integrated by importing more climate explorer data. """ for n in self.nodes: self.G.node[n]['climate'][1] += self.G.node[n]['climate'][3] self.G.node[n]['climate'][4] += self.G.node[n]['climate'][6] self.G.node[n]['climate'][7] += self.G.node[n]['climate'][9] def rank_by_climate(self): """ Create an ordered list of counties, from least hot/dry climate to most hot/dry climate. """ # initialize lists to store data heat_data = [] dry_data = [] heat_dry_data = [] # loop through counties in order for county in self.nodes: # access and store all heat/dry data heat_data.append(self.G.node[county]['climate'][1]) dry_data.append(self.G.node[county]['climate'][7]) # find max heat/dry data max_heat = max(heat_data) max_dry = max(dry_data) # normalize data based on max value heat_data = [(e/max_heat) for e in heat_data] dry_data = [(e/max_dry) for e in dry_data] # add normalized data for county in range(self.num_counties): heat_dry_data.append(heat_data[county] + dry_data[county]) # convert to numpy array heat_dry_data = np.array(heat_dry_data) # returns indices that would sort an array (in this case, # returns county id's from best to worst climate) county_climate_rank = np.argsort(heat_dry_data) # convert to list, update model attribute self.county_climate_ranking = list(county_climate_rank) def update_income_counts(self): """ Update income distribution by county. """ # loop through counties in order for county in range(self.num_counties): # initialize list self.county_income[county] = [0]*10 # loop through agents for agent in self.schedule.agents: # update dictionary based on agent data self.county_income[agent.pos][agent.income-1] += 1 # income counts are printed at the beginning and end of run print(self.county_income) def get_preference_distribution(self): """ TODO: docstring """ preference_list = [0]*5 for agent in self.schedule.agents: preference_list[agent.preference] += 1 print(preference_list) def step(self): """ Advance the model by one step. """ global TICK # update climate ranking self.rank_by_climate() # advance all agents by one step self.schedule.step() # update population self.update_population() # update climate self.update_climate() # collect data self.datacollector.collect(self) # update step counter TICK += 1
class SpeedModel(Model): """ Model of the game "Spe_ed". This class controls the execution of the simulation. """ def __init__(self, width, height, nb_agents, agent_classes, initial_agents_params=None, cells=None, data_collector=None, save=False): """ Model-Initialization. :param width: Width of the field :param height: Height of the field :param nb_agents: Number of Agents :param agent_classes: List of classes of the agents that should be players in the game. The length has to be equal or greater than nb_agents :param initial_agents_params: A list of dictionaries containing initialization parameters for agents that should be initialized at the start of the simulation :param cells: A Spe_ed-cells like 2D-Array that initializes the field :param data_collector: Mesa data collector function :param save: whether or not track the games history """ super().__init__() self.data_collector = data_collector self.width = width self.height = height self.nb_agents = nb_agents self.save = save if self.save: self.history = [] if initial_agents_params is None: initial_agents_params = [{} for i in range(nb_agents)] else: initial_agents_params = copy.deepcopy(initial_agents_params) self.schedule = SimultaneousActivation(self) self.grid = MultiGrid(width, height, True) # width and height are swapped since height is rows and width is columns # an alternative to this representation would be to transpose cells everytime it is exposed # but that could be inefficient self.cells = np.zeros((height, width), dtype="int") # Init initial agents self.speed_agents = [] self.active_speed_agents = [] for i in range(nb_agents): agent_params = initial_agents_params[i] agent_params["model"] = self # set to random position/direction if no position/direction is given if "pos" not in agent_params: agent_params["pos"] = self.random.choice( list(self.grid.empties)) if "direction" not in agent_params: agent_params["direction"] = self.random.choice(list(Direction)) agent = agent_classes[i](**agent_params) # don't add agent to grid/cells if its out of bounds. But add it to the scheduler. if self.grid.out_of_bounds(agent_params["pos"]): self.schedule.add(agent) self.speed_agents.append(agent) else: self.add_agent(agent) self.speed_agents.append(agent) self.active_speed_agents.append(agent) if cells is not None: self.init_cells_and_grid(cells) def init_cells_and_grid(self, cells): """ Initializes Mesas Grid and the Model-cells the field with the information given in cells. :param cells: A Spe_ed-cells like 2D-Array that initializes the field :return: None """ self.cells = np.array(cells) # add traces to grid for y in range(self.cells.shape[0]): for x in range(self.cells.shape[1]): # cell is occupied by a collision if self.cells[y, x] == -1: agent = AgentTraceCollision(self, (x, y)) self.add_agent(agent) # cell is occupied by head or trace elif self.cells[y, x] != 0: # head of the agent is not already a entry in self.grid if len(self.grid.get_cell_list_contents((x, y))) == 0: # add trace agent = AgentTrace( self, (x, y), self.speed_agents[self.cells[y, x] - 1]) # get agent based on id self.add_agent(agent) def step(self): """ Computes one iteration of the model. :return: None """ if self.data_collector: self.data_collector.collect(self) if self.save: self.history.append(copy.deepcopy(model_to_json(self))) self.schedule.step() self.check_collisions() self.check_game_finished() def step_specific_agent(self, agent): """ Only steps one specific agent. This is only for specific applications (e.g. Multi-Minimax). Don't use this method if not necessary since it doesn't increment all model parts (e.g. time). :param cells: The agent to step :return: None """ agent.step() agent.advance() self.check_collisions() self.check_game_finished() def check_collisions(self): """ Checks every active agent for collisions with traces or other agents. Colliding agents are eliminated. :return: None """ agents_to_set_inactive = [] for agent in self.speed_agents: for t in agent.trace: cell_contents = self.grid.get_cell_list_contents(t) if len(cell_contents) > 1: if agent not in agents_to_set_inactive: agents_to_set_inactive.append(agent) self.add_agent(AgentTraceCollision(self, t)) for agent in agents_to_set_inactive: agent.set_inactive() def check_game_finished(self): """ Checks whether or not the game has finished (every agent is eliminated) and prints the result if finished. :return: None """ if len(self.active_speed_agents) <= 1: self.running = False if self.save: self.history.append(copy.deepcopy(model_to_json(self))) path = os.path.abspath("") + "/res/simulatedGames/" for entry in self.history: entry["cells"] = entry["cells"].tolist() with open( path + datetime.datetime.now().strftime( "%d-%m-%y__%H-%M-%S-%f") + ".json", "w") as f: json.dump(self.history, f, indent=4) def add_agent(self, agent): """ Adds an agent to the model. :param agent: The agent to add to the model :return: None """ self.schedule.add(agent) self.grid.place_agent(agent, agent.pos) # swapped position args since cells has the format (height, width) pos = (agent.pos[1], agent.pos[0]) if isinstance(agent, SpeedAgent): self.cells[pos] = agent.unique_id elif type(agent) is AgentTraceCollision: self.cells[pos] = -1 elif type(agent) is AgentTrace: self.cells[pos] = agent.origin.unique_id def remove_agent(self, agent): """ Removes an agent from the model. :param agent: The agent to remove from the model :return: None """ if agent in self.schedule.agents: self.schedule.remove(agent) self.grid.remove_agent(agent) def get_agent_by_id(self, unique_id): """ Returns an agent-object by its unique_id. :param unique_id: The agent id to search for :return: Agent or None if no match """ for agent in self.speed_agents: if agent.unique_id == unique_id: return agent return None