class CrisisWorld(MetaModel): ''' Basic MetaModel for running multiple iterations with a fixed population. By default, creates agents with random military strength, assets and bloc membership. Fixed agent parameters (e.g. learning rates) can be passed as a dictionary. To generate more complex behaviors, override the make_agents method. By default, the log is a list of run qualities. Override assess_run method as needed to change that. ''' model_class = CrisisModel agent_class = CrisisAgent agent_count = 10 agent_args = {} def __init__(self, agent_class, agent_count, agent_args={}, seed=None): ''' Instantiate a new CrisisWorld model. Args: agent_class: Class to instantiate the agents agent_count: How many agents to instantiate with. agent_args: Dictionary of arguments to pass to all agents. seed: Random seed to launch the model with. ''' self.agent_class = agent_class self.agent_count = agent_count self.agent_args = agent_args super().__init__(self.model_class, agents_per_model=2, seed=seed) # Instantiate data collector self.dc = DataCollector( tables={ "Interactions": ["Step", "A", "B", "Outcome", "SPE", "quality"], "Agents": ["Name", "Assets", "Capability", "Bloc"] }) for agent in self.agents: row = { "Name": agent.name, "Assets": agent.assets, "Capability": agent.mil_strength, "Bloc": agent.bloc } self.dc.add_table_row("Agents", row) def make_agents(self): ''' Create self.agent_count agents. ''' self.agents = [] for i in range(self.agent_count): m = random.randrange(1, 100) # Mil strength a = random.randrange(1, 100) # Assets b = random.randrange(2) # Bloc agent_args = dict(learning_rate=0.1, discount_factor=0.9, assets=a, mil_strength=m, bloc=b, name=i) for arg, val in self.agent_args.items(): agent_args[arg] = val a = self.agent_class(**agent_args) self.agents.append(a) def step(self): ''' Pair up all agents at random and have them interact. ''' random.shuffle(self.agents) for agent in self.agents: alters = [a for a in self.agents if a is not agent] random.shuffle(alters) for alter in alters: model = self.model_class([agent, alter]) model.run() self.assess_run(model) self.steps += 1 def assess_run(self, model): ''' Log the model outcome and equilibrium outcome, and compute similarity. ''' spe_model = model.find_equilibrium() a, b = model.agents q = model.log.tversky_index(spe_model.log) row = { "Step": self.steps, "A": a.name, "B": b.name, "Outcome": model.current_node, "SPE": spe_model.current_node, "quality": q } self.dc.add_table_row("Interactions", row)
class RankingModel(Model): """The ranking model class.""" DECIMAL_PLACES = 2 NORMALIZED_SCORE_RANGE = [0, 100] def __init__(self, number_of_agents, attributes, settings=None, random_seed=None): """Constructor for the RankingModel class. :param number_of_agents: The number of agents. :param attributes: The list of attributes. :param settings: The settings dictionary. :param random_seed: The seed for the random number generator. """ super().__init__() LOGGER.debug('number_of_agents = %f', number_of_agents) LOGGER.debug('attributes = %s', attributes) LOGGER.debug('settings = %s', settings) LOGGER.debug('random_seed = %s', random_seed) self.reset_randomizer(random_seed) self.agents = [] self.attributes = attributes self.settings = settings if settings is not None else {} # The RandomActivation scheduler activates all the agents once per # step, in random order. self.schedule = RandomActivation(self) # Create and schedule ranking agents. for agent_count in range(1, number_of_agents + 1): unique_id = "University {}".format(agent_count) agent = RankingAgent(unique_id, self) self.agents.append(agent) self.schedule.add(agent) # Setup tables to add to the data collector tables = { 'ranking': ['element', 'period', 'position', 'score', 'normalized_score'], 'societal_value': ['period', 'societal_value'], 'ranking_dynamics': ['period', 'distance', 'society_delta', 'gamma'] } # Add a table per attribute for attribute in self.attributes: tables[attribute.name] = [ 'element', 'period', 'funding', 'production', 'valuation', 'weight', 'score' ] # Setup a data collector self.data_collector = DataCollector(tables=tables) def run(self, number_of_steps): """Run the model for the input number of time steps. :param number_of_steps: The number of time steps to run the model. """ for _ in range(number_of_steps): self.step() def step(self): """Advance the model by one step.""" # When we call the schedule’s step method, it shuffles the order of the # agents, then activates them all, one at a time. self.schedule.step() # Update the agent ranking self._update_ranking() # Update the agent attribute scores self._update_attribute_scores() # Update the societal value table self._update_societal_value() # Update the ranking dynamics table self._update_ranking_dynamics() # Collect data. self.data_collector.collect(self) def _current_high_score(self): """Get the current high score. :return: The high score value. """ scores = [] for agent in self.agents: scores.append(agent.score) return max(scores) def _normalize_score(self, score): """Normalize the score. :param score: The score to normalize. :return: The normalized score. """ score_interval = [0, self._current_high_score()] return int( round(np.interp(score, score_interval, self.NORMALIZED_SCORE_RANGE))) def _update_ranking(self): """Update each agent's ranking based on agent score.""" # Get the current agent scores. agent_scores = [] for agent in self.agents: agent_scores.append([ agent.unique_id, self.schedule.time, round(agent.score, self.DECIMAL_PLACES), self._normalize_score(agent.score) ]) # Get the ranking columns from the ranking table. ranking_columns = list( self.data_collector.get_table_dataframe('ranking')) # Remove the position column since it will be added back after ranking. ranking_columns.remove('position') agent_rank = pd.DataFrame(agent_scores, columns=ranking_columns) # Use pandas data frame to rank the agents. agent_rank['position'] =\ agent_rank['score'].rank(method='min', ascending=False).astype(int) # Add the records as a row in the ranking table. for row in agent_rank.to_dict('records'): self.data_collector.add_table_row('ranking', row) def _update_attribute_scores(self): """Update each agent's attribute scores and related values.""" # Initialize the attribute scores list. attribute_scores = [[] for _ in range(len(self.attributes))] # For each agent and each attribute append to the attribute scores list. for agent in self.agents: for index, attribute in enumerate(self.attributes): step_index = self.schedule.time - 1 funds = agent.attribute_funding[attribute.name][step_index] produce = agent.attribute_production[ attribute.name][step_index] value = agent.attribute_valuation[attribute.name][step_index] weight = agent.attribute_weight[attribute.name][step_index] attributes = [ agent.unique_id, self.schedule.time, round(funds, self.DECIMAL_PLACES), round(produce, self.DECIMAL_PLACES), round(value, self.DECIMAL_PLACES), round(weight, self.DECIMAL_PLACES), round(value * weight, self.DECIMAL_PLACES) ] attribute_scores[index].append(attributes) # Add a table per attribute for index, attribute in enumerate(self.attributes): attribute_columns = list( self.data_collector.get_table_dataframe(attribute.name)) attribute_score = pd.DataFrame(attribute_scores[index], columns=attribute_columns) # Add the attribute scores as a row in the attribute score table. for row in attribute_score.to_dict('records'): self.data_collector.add_table_row(attribute.name, row) def _update_ranking_dynamics(self): """Update the ranking dynamics table.""" # If we are not the the second scheduled time step yet then return. if self.schedule.time < 2: return distance = 0 ranking = self.data_collector.get_table_dataframe('ranking') for agent, data_frame in ranking.groupby('element'): delta = 0 for _, row in data_frame.iterrows(): if row['period'] == self.schedule.time - 1: delta = row['position'] elif row['period'] == self.schedule.time: delta -= row['position'] if delta > 0: distance += delta LOGGER.debug("agent = %s distance = %f", agent, distance) society = self.data_collector.get_table_dataframe('societal_value') society_t = 0 society_t_minus_one = 0 for _, row in society.iterrows(): if row['period'] == self.schedule.time - 1: society_t_minus_one = row['societal_value'] elif row['period'] == self.schedule.time: society_t = row['societal_value'] society_delta = society_t - society_t_minus_one # Calculate gamma gamma = 0 if society_delta > 0: gamma = distance / society_delta # Build the ranking dynamics row. ranking_dynamics_row = { 'period': self.schedule.time, 'distance': distance, 'society_delta': round(society_delta, self.DECIMAL_PLACES), 'gamma': gamma } # Add the ranking dynamics row to the ranking dynamics table. self.data_collector.add_table_row('ranking_dynamics', ranking_dynamics_row) def _update_societal_value(self): """Update the societal value table.""" # Sum the production values over all agents and all of their attributes. sum_production_values = 0 for agent in self.agents: for attribute in self.attributes: step_index = self.schedule.time - 1 produce = agent.attribute_production[ attribute.name][step_index] sum_production_values += produce # Build the societal value row. societal_value_row = { 'period': self.schedule.time, 'societal_value': round(sum_production_values, self.DECIMAL_PLACES) } # Add the societal value row to the societal value table. self.data_collector.add_table_row('societal_value', societal_value_row)
class EgyptSim(Model): """ Simulation Model for wealth distribution represented by grain in ancient Egypt """ # Variable declarations for non python programmer sanity # Map variables height = 30 width = 30 # Simulation Variables timeSpan = 500 currentTime = 0 startingSettlements = 14 startingHouseholds = 7 startingHouseholdSize = 5 startingGrain = 3000 minAmbition = 0.1 minCompetency = 0.5 generationalVariation = 0.9 knowledgeRadius = 20 distanceCost = 10 fallowLimit = 4 popGrowthRate = 0.1 fission = False fissionChance = 0.7 rental = True rentalRate = 0.5 totalPopulation = startingSettlements * startingHouseholds * startingHouseholdSize totalGrain = startingGrain * startingHouseholds startingPopulation = totalPopulation projectedHistoricalPopulation = totalPopulation maxHouseholdGrain = startingGrain # Step variables mu = 0 sigma = 0 alpha = 0 beta = 0 # Visualisation description = "A model simulating wealth growth and distribution in Ancient Egypt.\n\nThe model allows one to see how variables such as the flooding of the Nile, human character traits and random chance effect the acquisition and distribution of wealth." # List of identifiers and colors for settlements SETDICT = {"s1": "#FF0000", "s2": "#FF4500", "s3": "#BC8F8F", "s4": "#00FF00", "s5": "#00FFFF", "s6": "#0000FF", "s7": "#FF00FF", "s8": "#FF1493", "s9": "#708090", "s10": "#DC143C", "s11": "#FF8C00", "s12": "#FF69B4", "s13": "#800000", "s14": "#7CFC00", "s15": "#008B8B", "s16": "#483D8B", "s17": "#4B0082", "s18": "#FF69B4", "s19": "#000000", "s20": "#8B4513"} def __init__(self, height: int = 30, width: int = 30, timeSpan: int = 500, startingSettlements: int = 14, startingHouseholds: int = 7, startingHouseholdSize: int = 5, startingGrain: int = 3000, minAmbition: float = 0.1, minCompetency: float = 0.5, generationalVariation: float = 0.9, knowledgeRadius: int = 20, distanceCost: int = 10, fallowLimit: int = 4, popGrowthRate: float = 0.1, fission: bool = False, fissionChance: float = 0.7, rental: bool = True, rentalRate: float = 0.5): """ Create a new EgyptSim model Args: height: The height of the simulation grid width: The width of the simulation grid timeSpan: The number of years over which the model is to run startingSettlements: The starting number of Settlements startingHouseholds: The starting number of Households per Settlement startingHouseholdSize: The starting number of workers in a Household startingGrain: The starting amount of grain for each Household minAmbition: The minimum ambition value for a Household minCompetency: The minimum competency value for a Household generationalVariation: The difference between generations of a Household knowledgeRadius: How far outside ther Settlement a Household can "see" distanceCost: The cost to move grain per cell away from a settlemnt fallowLimit: The number of years a field can lay fallow before it is harvested popGrowthRate: The rate at which the population grows fission: If Household fission (Moving between settlements) is allowed fissionChance: The chance fission occuring rental: If land rental is allowed rentalRate: The rate at which households will rent land """ super().__init__() # Set Parameters # Map size self.height = height self.width = width # If the number of starting settlements is greater than the maximum reasonable number of households considering territory and farming area # Considers that each household needs at least two field to survive at a minimum number of members, a Settlment needs 9 (territory) + 2 * households # Tiles to survive if startingSettlements > ((width - 1) * height) // (9 + (startingHouseholds * 2)): if startingSettlements > 20: self.startingSettlements = 20 else: self.startingSettlements = ((height - 1) * width) // (9 + (startingHouseholds * 2)) print("Too many starting settlements to support the settlements and household, truncating to: ", self.startingSettlements) else: self.startingSettlements = startingSettlements # Simulation Variables self.timeSpan = timeSpan self.currentTime = 0 self.startingHouseholds = startingHouseholds self.startingHouseholdSize = startingHouseholdSize self.startingGrain = startingGrain self.minAmbition = minAmbition self.minCompetency = minCompetency self.generationalVariation = generationalVariation self.knowledgeRadius = knowledgeRadius self.distanceCost = distanceCost self.fallowLimit = fallowLimit self.popGrowthRate = popGrowthRate self.fission = fission self.fissionChance = fissionChance self.rental = rental self.rentalRate = rentalRate self.totalGrain = startingGrain * startingHouseholds * startingSettlements self.totalPopulation = startingSettlements * startingHouseholds * startingHouseholdSize self.startingPopulation = self.totalPopulation self.projectedHistoricalPopulation = self.startingPopulation self.maxHouseholdGrain = startingGrain # Scheduler and Grid self.schedule = EgyptSchedule(self) self.grid = MultiGrid(height = self.height, width = self.width, torus=False) # Define specific tables for data collection purposes setlist = [] for i in range(self.startingSettlements): setlist.append("s" + str(i + 1) + "_Population") tables = {"Settlement Population": setlist} # Data collection self.datacollector = DataCollector(model_reporters = {"Households": lambda m: m.schedule.get_breed_count(Household), "Settlements": lambda m: m.schedule.get_breed_count(Settlement), "Total Grain": lambda m: m.totalGrain, "Total Population": lambda m: m.totalPopulation, "Projected Hisorical Poulation (0.1% Growth)": lambda m: m.projectedHistoricalPopulation, "Gini-Index": gini, "Maximum Settlement Population": maxSetPop, "Minimum Settlement Population": minSetPop, "Mean Settlement Poulation" : meanSetPop, "Maximum Household Wealth": maxHWealth, "Minimum Household Wealth": minHWealth, "Mean Household Wealth" : meanHWealth, "Number of households with < 33% of wealthiest grain holding": lowerThirdGrainHoldings, "Number of households with 33 - 66% of wealthiest grain holding": middleThirdGrainHoldings, "Number of households with > 66% of wealthiest grain holding": upperThirdGrainHoldings }, tables = tables) self.setup() self.running = True self.collectTableData() self.datacollector.collect(self) def collectTableData(self): setPops = {} for s in self.schedule.get_breed(Settlement): setPops[s.unique_id + "_Population"] = s.population self.datacollector.add_table_row("Settlement Population", setPops, True) def setupMapBase(self): """ Create the grid as field and river """ for agent, x, y in self.grid.coord_iter(): # If on left edge, make a river if x == 0: uid = "r" + str(x) + "|" + str(y) river = River(uid, self, (x, y)) self.grid.place_agent(river, (x, y)) # Otherwise make a field else: uid = "f" + str(x) + "|" + str(y) field = Field(uid, self, (x, y), 0.0) self.grid.place_agent(field, (x, y)) self.schedule.add(field) def setupSettlementsHouseholds(self): """ Add settlements and households to the simulation """ h = 1 for i in range(self.startingSettlements): # Loop untill a suitable location is found while True: x = self.random.randrange(1, self.width) y = self.random.randrange(self.height) flag = False cell = self.grid.get_cell_list_contents((x, y)) # Check that tile is available for agent in cell: if agent.settlementTerritory: break else: flag = True break if flag: break # Add settlement to the grid population = self.startingHouseholds * self.startingHouseholdSize uid = "s" + str(i + 1) # Use a custom id for the datacollector settlement = Settlement(uid, self, (x, y), population, self.startingHouseholds, uid, self.SETDICT[uid]) self.grid.place_agent(settlement, (x, y)) # Set the surrounding fields as territory local = self.grid.get_neighbors((x, y), moore=True, include_center=True, radius=1) for a in local: a.settlementTerritory = True # Add households for the settlement to the scheduler for j in range(self.startingHouseholds): huid = "h" + str(h) # Use a custom id for the datacollector ambition = np.random.uniform(self.minAmbition, 1) competency = np.random.uniform(self.minCompetency, 1) genCount = self.random.randrange(5) + 10 household = Household(huid, self, settlement, (x, y), self.startingGrain, self.startingHouseholdSize, ambition, competency, genCount) # ! Dont add household to grid, is redundant self.schedule.add(household) h += 1 # Add settlement to the scheduler self.schedule.add(settlement) def setup(self): """ Setup model parameters """ self.setupMapBase() self.setupSettlementsHouseholds() def step(self): self.currentTime += 1 self.maxHouseholdGrain = 0 self.setupFlood() self.schedule.step() self.projectedHistoricalPopulation = round(self.startingPopulation * ((1.001) ** self.currentTime)) self.datacollector.collect(self) # Add settlement data to table self.collectTableData() # Cease running once time limit is reached or everyone is dead if self.currentTime >= self.timeSpan or self.totalPopulation == 0: self.running = False def setupFlood(self): """ Sets up common variables used for the flood method in Fields """ self.mu = random.randint(0, 10) + 5 self.sigma = random.randint(0, 5) + 5 self.alpha = (2 * self.sigma ** 2) self.beta = 1 / (self.sigma * math.sqrt(2 * math.pi))
class CrisisWorld(MetaModel): ''' Basic MetaModel for running multiple iterations with a fixed population. By default, creates agents with random military strength, assets and bloc membership. Fixed agent parameters (e.g. learning rates) can be passed as a dictionary. To generate more complex behaviors, override the make_agents method. By default, the log is a list of run qualities. Override assess_run method as needed to change that. ''' model_class = CrisisModel agent_class = CrisisAgent agent_count = 10 agent_args = {} def __init__(self, agent_class, agent_count, agent_args={}, seed=None): ''' Instantiate a new CrisisWorld model. Args: agent_class: Class to instantiate the agents agent_count: How many agents to instantiate with. agent_args: Dictionary of arguments to pass to all agents. seed: Random seed to launch the model with. ''' self.agent_class = agent_class self.agent_count = agent_count self.agent_args = agent_args super().__init__(self.model_class, agents_per_model=2, seed=seed) # Instantiate data collector self.dc = DataCollector(tables={ "Interactions": ["Step", "A", "B", "Outcome", "SPE", "quality"], "Agents": ["Name", "Assets", "Capability", "Bloc"] }) for agent in self.agents: row = {"Name": agent.name, "Assets": agent.assets, "Capability": agent.mil_strength, "Bloc": agent.bloc} self.dc.add_table_row("Agents", row) def make_agents(self): ''' Create self.agent_count agents. ''' self.agents = [] for i in range(self.agent_count): m = random.randrange(1, 100) # Mil strength a = random.randrange(1,100) # Assets b = random.randrange(2) # Bloc agent_args = dict(learning_rate=0.1, discount_factor=0.9, assets=a, mil_strength=m, bloc=b, name=i) for arg, val in self.agent_args.items(): agent_args[arg] = val a = self.agent_class(**agent_args) self.agents.append(a) def step(self): ''' Pair up all agents at random and have them interact. ''' random.shuffle(self.agents) for agent in self.agents: alters = [a for a in self.agents if a is not agent] random.shuffle(alters) for alter in alters: model = self.model_class([agent, alter]) model.run() self.assess_run(model) self.steps += 1 def assess_run(self, model): ''' Log the model outcome and equilibrium outcome, and compute similarity. ''' spe_model = model.find_equilibrium() a, b = model.agents q = model.log.tversky_index(spe_model.log) row = {"Step": self.steps, "A": a.name, "B": b.name, "Outcome": model.current_node, "SPE": spe_model.current_node, "quality": q} self.dc.add_table_row("Interactions", row)
class SimModel(Model): """A model with N agents.""" def __init__(self, N, model): self.num_agents = N self.schedule = BaseScheduler(self) self.model_step = 0 self.attr = {} # transfer properties from graph structure to model #idea: no need for a graph at all, just a class of info! #make graph optional, and add a module to extract info from graph #in that case, perhaps delete all copying below and just use the object self.info = model.info self.data = model.data self.recorder = model.recorder self.states = model.state.keys() self.state = model.state self.edge = model.edge self.attr = model.model_attr self.agent_attr = model.agent_attr # state probabilities self.next_state = {} self.pr_next_state = defaultdict(list) for from_state in self.states: self.next_state[from_state] = list(model.edge[from_state].keys()) for to_state in self.next_state[from_state]: p = model.edge[from_state][to_state]['p'] self.pr_next_state[from_state].append(p) # model attributes for attr, value in self.attr.items(): if callable(value): self.attr[attr] = value() elif isinstance(value, numbers.Number): self.attr[attr] = value else: print("Error: The attribute must be assigned a function or a fixed number") # collect node attributes from graph #node_attr = {attr for state in g.nodes() for attr in g.node[state].keys()} self.state_attr = defaultdict() self.state_attr = {state : model.state[state] for state in model.state.keys()} # state attributes that are functions get an explicit value based on the function here for state in model.state.keys(): for attr in self.state_attr[state].keys(): #what if a state has no attributes, errro here? if 'dist' in str(self.state_attr[state][attr]): self.state_attr[state][attr]=eval(self.state_attr[state][attr]) #default collect and counters self.history = {} self.sum_agents = {} for state in self.states: self.sum_agents[state] = 0 #collects attributes to be updated in a list (updates only apply to agents who are alive and ) self.agent_updates = {k : v['update'] for k, v in self.agent_attr.items() if (self.agent_attr[k]['update']!=False) & (self.agent_attr[k]['condition']=='alive')} # Create agents for i in range(self.num_agents): a = SimAgent(i, self) self.schedule.add(a) ##initalize datacollection #create list of tables used to record values tables = {self.recorder[k]['table']: self.recorder[k]['collect_vars'] for k in self.recorder} collect = {state: eval("lambda m: m.sum_agents['{state}']".format(state=state)) for state in self.states} # self.datacollector = DataCollector(model_reporters=collect, # tables=tables) self.datacollector = DataCollector(model_reporters=collect, agent_reporters={"history": lambda a: a.history}, tables=tables) def step(self): self.datacollector.collect(self) # check for model level triggers for name in self.recorder.keys(): if self.recorder[name]['level']=='model': if self.recorder[name]['trigger'](self): values = {var: self.attr[var] for var in self.recorder[name]['collect_vars']} self.datacollector.add_table_row(self.recorder[name], row=values) self.model_step += 1 self.schedule.step() def run_model(self, n): for i in range(n): self.step()