Ejemplo n.º 1
0
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)
Ejemplo n.º 3
0
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))
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
0
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()