diff --git a/examples/custom_generator/mymodule.py b/examples/custom_generator/mymodule.py index 85226e0..241ddcb 100644 --- a/examples/custom_generator/mymodule.py +++ b/examples/custom_generator/mymodule.py @@ -2,16 +2,17 @@ from networkx import Graph import random import networkx as nx + def mygenerator(n=5, n_edges=5): - ''' + """ Just a simple generator that creates a network with n nodes and n_edges edges. Edges are assigned randomly, only avoiding self loops. - ''' + """ G = nx.Graph() for i in range(n): G.add_node(i) - + for i in range(n_edges): nodes = list(G.nodes) n_in = random.choice(nodes) @@ -19,9 +20,3 @@ def mygenerator(n=5, n_edges=5): n_out = random.choice(nodes) G.add_edge(n_in, n_out) return G - - - - - - diff --git a/examples/custom_timeouts/custom_timeouts.py b/examples/custom_timeouts/custom_timeouts.py index b269c0a..838ccfc 100644 --- a/examples/custom_timeouts/custom_timeouts.py +++ b/examples/custom_timeouts/custom_timeouts.py @@ -2,34 +2,37 @@ from soil.agents import FSM, state, default_state class Fibonacci(FSM): - '''Agent that only executes in t_steps that are Fibonacci numbers''' + """Agent that only executes in t_steps that are Fibonacci numbers""" - defaults = { - 'prev': 1 - } + defaults = {"prev": 1} @default_state @state def counting(self): - self.log('Stopping at {}'.format(self.now)) - prev, self['prev'] = self['prev'], max([self.now, self['prev']]) + self.log("Stopping at {}".format(self.now)) + prev, self["prev"] = self["prev"], max([self.now, self["prev"]]) return None, self.env.timeout(prev) class Odds(FSM): - '''Agent that only executes in odd t_steps''' + """Agent that only executes in odd t_steps""" + @default_state @state def odds(self): - self.log('Stopping at {}'.format(self.now)) - return None, self.env.timeout(1+self.now%2) + self.log("Stopping at {}".format(self.now)) + return None, self.env.timeout(1 + self.now % 2) -if __name__ == '__main__': +if __name__ == "__main__": from soil import Simulation - s = Simulation(network_agents=[{'ids': [0], 'agent_class': Fibonacci}, - {'ids': [1], 'agent_class': Odds}], - network_params={"generator": "complete_graph", "n": 2}, - max_time=100, - ) + + s = Simulation( + network_agents=[ + {"ids": [0], "agent_class": Fibonacci}, + {"ids": [1], "agent_class": Odds}, + ], + network_params={"generator": "complete_graph", "n": 2}, + max_time=100, + ) s.run(dry_run=True) diff --git a/examples/mesa/server.py b/examples/mesa/server.py index 7fe820f..ea00658 100644 --- a/examples/mesa/server.py +++ b/examples/mesa/server.py @@ -14,16 +14,18 @@ def network_portrayal(env): # The model ensures there is 0 or 1 agent per node portrayal = dict() - wealths = {node_id: data['agent'].wealth for (node_id, data) in env.G.nodes(data=True)} + wealths = { + node_id: data["agent"].wealth for (node_id, data) in env.G.nodes(data=True) + } portrayal["nodes"] = [ { "id": node_id, - "size": 2*(wealth+1), + "size": 2 * (wealth + 1), "color": "#CC0000" if wealth == 0 else "#007959", # "color": "#CC0000", "label": f"{node_id}: {wealth}", - } for (node_id, wealth) in wealths.items() - + } + for (node_id, wealth) in wealths.items() ] portrayal["edges"] = [ @@ -41,7 +43,7 @@ def gridPortrayal(agent): :param agent: the agent in the simulation :return: the portrayal dictionary """ - color = max(10, min(agent.wealth*10, 100)) + color = max(10, min(agent.wealth * 10, 100)) return { "Shape": "rect", "w": 1, @@ -52,7 +54,7 @@ def gridPortrayal(agent): "Text": agent.unique_id, "x": agent.pos[0], "y": agent.pos[1], - "Color": f"rgba(31, 10, 255, 0.{color})" + "Color": f"rgba(31, 10, 255, 0.{color})", } @@ -79,7 +81,7 @@ model_params = { 10, 1, description="Grid height", - ), + ), "width": UserSettableParameter( "slider", "width", @@ -88,16 +90,20 @@ model_params = { 10, 1, description="Grid width", - ), - "agent_class": UserSettableParameter('choice', 'Agent class', value='MoneyAgent', - choices=['MoneyAgent', 'SocialMoneyAgent']), + ), + "agent_class": UserSettableParameter( + "choice", + "Agent class", + value="MoneyAgent", + choices=["MoneyAgent", "SocialMoneyAgent"], + ), "generator": graph_generator, } -canvas_element = CanvasGrid(gridPortrayal, - model_params["width"].value, - model_params["height"].value, 500, 500) +canvas_element = CanvasGrid( + gridPortrayal, model_params["width"].value, model_params["height"].value, 500, 500 +) server = ModularServer( diff --git a/examples/mesa/social_wealth.py b/examples/mesa/social_wealth.py index c8b1701..b4ae99f 100644 --- a/examples/mesa/social_wealth.py +++ b/examples/mesa/social_wealth.py @@ -1,9 +1,10 @@ -''' +""" This is an example that adds soil agents and environment in a normal mesa workflow. -''' +""" from mesa import Agent as MesaAgent from mesa.space import MultiGrid + # from mesa.time import RandomActivation from mesa.datacollection import DataCollector from mesa.batchrunner import BatchRunner @@ -12,12 +13,13 @@ import networkx as nx from soil import NetworkAgent, Environment, serialization + def compute_gini(model): agent_wealths = [agent.wealth for agent in model.agents] x = sorted(agent_wealths) N = len(list(model.agents)) - B = sum( xi * (N-i) for i,xi in enumerate(x) ) / (N*sum(x)) - return (1 + (1/N) - 2*B) + B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x)) + return 1 + (1 / N) - 2 * B class MoneyAgent(MesaAgent): @@ -32,9 +34,8 @@ class MoneyAgent(MesaAgent): def move(self): possible_steps = self.model.grid.get_neighborhood( - self.pos, - moore=True, - include_center=False) + self.pos, moore=True, include_center=False + ) new_position = self.random.choice(possible_steps) self.model.grid.move_agent(self, new_position) @@ -69,6 +70,7 @@ class SocialMoneyAgent(NetworkAgent, MoneyAgent): other.wealth += 1 self.wealth -= 1 + def graph_generator(n=5): G = nx.Graph() for ix in range(n): @@ -78,16 +80,22 @@ def graph_generator(n=5): class MoneyEnv(Environment): """A model with some number of agents.""" - def __init__(self, width, height, N, generator=graph_generator, - agent_class=SocialMoneyAgent, - topology=None, **kwargs): + + def __init__( + self, + width, + height, + N, + generator=graph_generator, + agent_class=SocialMoneyAgent, + topology=None, + **kwargs + ): generator = serialization.deserialize(generator) agent_class = serialization.deserialize(agent_class, globs=globals()) topology = generator(n=N) - super().__init__(topology=topology, - N=N, - **kwargs) + super().__init__(topology=topology, N=N, **kwargs) self.grid = MultiGrid(width, height, False) self.populate_network(agent_class=agent_class) @@ -99,26 +107,29 @@ class MoneyEnv(Environment): self.grid.place_agent(agent, (x, y)) self.datacollector = DataCollector( - model_reporters={"Gini": compute_gini}, - agent_reporters={"Wealth": "wealth"}) + model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"} + ) -if __name__ == '__main__': +if __name__ == "__main__": - fixed_params = {"generator": nx.complete_graph, - "width": 10, - "network_agents": [{"agent_class": SocialMoneyAgent, - 'weight': 1}], - "height": 10} + fixed_params = { + "generator": nx.complete_graph, + "width": 10, + "network_agents": [{"agent_class": SocialMoneyAgent, "weight": 1}], + "height": 10, + } variable_params = {"N": range(10, 100, 10)} - batch_run = BatchRunner(MoneyEnv, - variable_parameters=variable_params, - fixed_parameters=fixed_params, - iterations=5, - max_steps=100, - model_reporters={"Gini": compute_gini}) + batch_run = BatchRunner( + MoneyEnv, + variable_parameters=variable_params, + fixed_parameters=fixed_params, + iterations=5, + max_steps=100, + model_reporters={"Gini": compute_gini}, + ) batch_run.run_all() run_data = batch_run.get_model_vars_dataframe() diff --git a/examples/mesa/wealth.py b/examples/mesa/wealth.py index c7934de..ca0d9bf 100644 --- a/examples/mesa/wealth.py +++ b/examples/mesa/wealth.py @@ -4,24 +4,26 @@ from mesa.time import RandomActivation from mesa.datacollection import DataCollector from mesa.batchrunner import BatchRunner + def compute_gini(model): agent_wealths = [agent.wealth for agent in model.schedule.agents] x = sorted(agent_wealths) N = model.num_agents - B = sum( xi * (N-i) for i,xi in enumerate(x) ) / (N*sum(x)) - return (1 + (1/N) - 2*B) + B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x)) + return 1 + (1 / N) - 2 * B + class MoneyAgent(Agent): - """ An agent with fixed initial wealth.""" + """An agent with fixed initial wealth.""" + def __init__(self, unique_id, model): super().__init__(unique_id, model) self.wealth = 1 def move(self): possible_steps = self.model.grid.get_neighborhood( - self.pos, - moore=True, - include_center=False) + self.pos, moore=True, include_center=False + ) new_position = self.random.choice(possible_steps) self.model.grid.move_agent(self, new_position) @@ -37,8 +39,10 @@ class MoneyAgent(Agent): if self.wealth > 0: self.give_money() + class MoneyModel(Model): """A model with some number of agents.""" + def __init__(self, N, width, height): self.num_agents = N self.grid = MultiGrid(width, height, True) @@ -55,29 +59,29 @@ class MoneyModel(Model): self.grid.place_agent(a, (x, y)) self.datacollector = DataCollector( - model_reporters={"Gini": compute_gini}, - agent_reporters={"Wealth": "wealth"}) + model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"} + ) def step(self): self.datacollector.collect(self) self.schedule.step() -if __name__ == '__main__': +if __name__ == "__main__": - fixed_params = {"width": 10, - "height": 10} + fixed_params = {"width": 10, "height": 10} variable_params = {"N": range(10, 500, 10)} - batch_run = BatchRunner(MoneyModel, - variable_params, - fixed_params, - iterations=5, - max_steps=100, - model_reporters={"Gini": compute_gini}) + batch_run = BatchRunner( + MoneyModel, + variable_params, + fixed_params, + iterations=5, + max_steps=100, + model_reporters={"Gini": compute_gini}, + ) batch_run.run_all() run_data = batch_run.get_model_vars_dataframe() run_data.head() print(run_data.Gini) - diff --git a/examples/newsspread/newsspread.py b/examples/newsspread/newsspread.py index 14d666f..f747f8e 100644 --- a/examples/newsspread/newsspread.py +++ b/examples/newsspread/newsspread.py @@ -3,84 +3,83 @@ import logging class DumbViewer(FSM, NetworkAgent): - ''' + """ A viewer that gets infected via TV (if it has one) and tries to infect its neighbors once it's infected. - ''' + """ + defaults = { - 'prob_neighbor_spread': 0.5, - 'prob_tv_spread': 0.1, + "prob_neighbor_spread": 0.5, + "prob_tv_spread": 0.1, } @default_state @state def neutral(self): - if self['has_tv']: - if self.prob(self.model['prob_tv_spread']): + if self["has_tv"]: + if self.prob(self.model["prob_tv_spread"]): return self.infected @state def infected(self): for neighbor in self.get_neighboring_agents(state_id=self.neutral.id): - if self.prob(self.model['prob_neighbor_spread']): + if self.prob(self.model["prob_neighbor_spread"]): neighbor.infect() def infect(self): - ''' + """ This is not a state. It is a function that other agents can use to try to infect this agent. DumbViewer always gets infected, but other agents like HerdViewer might not become infected right away - ''' + """ self.set_state(self.infected) class HerdViewer(DumbViewer): - ''' + """ A viewer whose probability of infection depends on the state of its neighbors. - ''' + """ def infect(self): - '''Notice again that this is NOT a state. See DumbViewer.infect for reference''' + """Notice again that this is NOT a state. See DumbViewer.infect for reference""" infected = self.count_neighboring_agents(state_id=self.infected.id) total = self.count_neighboring_agents() - prob_infect = self.model['prob_neighbor_spread'] * infected/total - self.debug('prob_infect', prob_infect) + prob_infect = self.model["prob_neighbor_spread"] * infected / total + self.debug("prob_infect", prob_infect) if self.prob(prob_infect): self.set_state(self.infected) class WiseViewer(HerdViewer): - ''' + """ A viewer that can change its mind. - ''' + """ defaults = { - 'prob_neighbor_spread': 0.5, - 'prob_neighbor_cure': 0.25, - 'prob_tv_spread': 0.1, + "prob_neighbor_spread": 0.5, + "prob_neighbor_cure": 0.25, + "prob_tv_spread": 0.1, } @state def cured(self): - prob_cure = self.model['prob_neighbor_cure'] + prob_cure = self.model["prob_neighbor_cure"] for neighbor in self.get_neighboring_agents(state_id=self.infected.id): if self.prob(prob_cure): try: neighbor.cure() except AttributeError: - self.debug('Viewer {} cannot be cured'.format(neighbor.id)) + self.debug("Viewer {} cannot be cured".format(neighbor.id)) def cure(self): self.set_state(self.cured.id) @state def infected(self): - cured = max(self.count_neighboring_agents(self.cured.id), - 1.0) - infected = max(self.count_neighboring_agents(self.infected.id), - 1.0) - prob_cure = self.model['prob_neighbor_cure'] * (cured/infected) + cured = max(self.count_neighboring_agents(self.cured.id), 1.0) + infected = max(self.count_neighboring_agents(self.infected.id), 1.0) + prob_cure = self.model["prob_neighbor_cure"] * (cured / infected) if self.prob(prob_cure): return self.cured return self.set_state(super().infected) diff --git a/examples/programmatic/programmatic.py b/examples/programmatic/programmatic.py index 3b9f86f..0cb912f 100644 --- a/examples/programmatic/programmatic.py +++ b/examples/programmatic/programmatic.py @@ -1,6 +1,6 @@ -''' +""" Example of a fully programmatic simulation, without definition files. -''' +""" from soil import Simulation, agents from networkx import Graph import logging @@ -14,21 +14,22 @@ def mygenerator(): class MyAgent(agents.FSM): - @agents.default_state @agents.state def neutral(self): - self.debug('I am running') + self.debug("I am running") if agents.prob(0.2): - self.info('This runs 2/10 times on average') + self.info("This runs 2/10 times on average") -s = Simulation(name='Programmatic', - network_params={'generator': mygenerator}, - num_trials=1, - max_time=100, - agent_class=MyAgent, - dry_run=True) +s = Simulation( + name="Programmatic", + network_params={"generator": mygenerator}, + num_trials=1, + max_time=100, + agent_class=MyAgent, + dry_run=True, +) # By default, logging will only print WARNING logs (and above). diff --git a/examples/pubcrawl/pubcrawl.py b/examples/pubcrawl/pubcrawl.py index 9fd1b04..110a44c 100644 --- a/examples/pubcrawl/pubcrawl.py +++ b/examples/pubcrawl/pubcrawl.py @@ -5,7 +5,8 @@ import logging class CityPubs(Environment): - '''Environment with Pubs''' + """Environment with Pubs""" + level = logging.INFO def __init__(self, *args, number_of_pubs=3, pub_capacity=10, **kwargs): @@ -13,51 +14,52 @@ class CityPubs(Environment): pubs = {} for i in range(number_of_pubs): newpub = { - 'name': 'The awesome pub #{}'.format(i), - 'open': True, - 'capacity': pub_capacity, - 'occupancy': 0, + "name": "The awesome pub #{}".format(i), + "open": True, + "capacity": pub_capacity, + "occupancy": 0, } - pubs[newpub['name']] = newpub - self['pubs'] = pubs + pubs[newpub["name"]] = newpub + self["pubs"] = pubs def enter(self, pub_id, *nodes): - '''Agents will try to enter. The pub checks if it is possible''' + """Agents will try to enter. The pub checks if it is possible""" try: - pub = self['pubs'][pub_id] + pub = self["pubs"][pub_id] except KeyError: - raise ValueError('Pub {} is not available'.format(pub_id)) - if not pub['open'] or (pub['capacity'] < (len(nodes) + pub['occupancy'])): + raise ValueError("Pub {} is not available".format(pub_id)) + if not pub["open"] or (pub["capacity"] < (len(nodes) + pub["occupancy"])): return False - pub['occupancy'] += len(nodes) + pub["occupancy"] += len(nodes) for node in nodes: - node['pub'] = pub_id + node["pub"] = pub_id return True def available_pubs(self): - for pub in self['pubs'].values(): - if pub['open'] and (pub['occupancy'] < pub['capacity']): - yield pub['name'] + for pub in self["pubs"].values(): + if pub["open"] and (pub["occupancy"] < pub["capacity"]): + yield pub["name"] def exit(self, pub_id, *node_ids): - '''Agents will notify the pub they want to leave''' + """Agents will notify the pub they want to leave""" try: - pub = self['pubs'][pub_id] + pub = self["pubs"][pub_id] except KeyError: - raise ValueError('Pub {} is not available'.format(pub_id)) + raise ValueError("Pub {} is not available".format(pub_id)) for node_id in node_ids: node = self.get_agent(node_id) - if pub_id == node['pub']: - del node['pub'] - pub['occupancy'] -= 1 + if pub_id == node["pub"]: + del node["pub"] + pub["occupancy"] -= 1 class Patron(FSM, NetworkAgent): - '''Agent that looks for friends to drink with. It will do three things: - 1) Look for other patrons to drink with - 2) Look for a bar where the agent and other agents in the same group can get in. - 3) While in the bar, patrons only drink, until they get drunk and taken home. - ''' + """Agent that looks for friends to drink with. It will do three things: + 1) Look for other patrons to drink with + 2) Look for a bar where the agent and other agents in the same group can get in. + 3) While in the bar, patrons only drink, until they get drunk and taken home. + """ + level = logging.DEBUG pub = None @@ -69,13 +71,13 @@ class Patron(FSM, NetworkAgent): @default_state @state def looking_for_friends(self): - '''Look for friends to drink with''' - self.info('I am looking for friends') - available_friends = list(self.get_agents(drunk=False, - pub=None, - state_id=self.looking_for_friends.id)) + """Look for friends to drink with""" + self.info("I am looking for friends") + available_friends = list( + self.get_agents(drunk=False, pub=None, state_id=self.looking_for_friends.id) + ) if not available_friends: - self.info('Life sucks and I\'m alone!') + self.info("Life sucks and I'm alone!") return self.at_home befriended = self.try_friends(available_friends) if befriended: @@ -83,93 +85,91 @@ class Patron(FSM, NetworkAgent): @state def looking_for_pub(self): - '''Look for a pub that accepts me and my friends''' - if self['pub'] != None: + """Look for a pub that accepts me and my friends""" + if self["pub"] != None: return self.sober_in_pub - self.debug('I am looking for a pub') + self.debug("I am looking for a pub") group = list(self.get_neighboring_agents()) for pub in self.model.available_pubs(): - self.debug('We\'re trying to get into {}: total: {}'.format(pub, len(group))) + self.debug("We're trying to get into {}: total: {}".format(pub, len(group))) if self.model.enter(pub, self, *group): - self.info('We\'re all {} getting in {}!'.format(len(group), pub)) + self.info("We're all {} getting in {}!".format(len(group), pub)) return self.sober_in_pub @state def sober_in_pub(self): - '''Drink up.''' + """Drink up.""" self.drink() - if self['pints'] > self['max_pints']: + if self["pints"] > self["max_pints"]: return self.drunk_in_pub @state def drunk_in_pub(self): - '''I'm out. Take me home!''' - self.info('I\'m so drunk. Take me home!') - self['drunk'] = True + """I'm out. Take me home!""" + self.info("I'm so drunk. Take me home!") + self["drunk"] = True if self.kicked_out: return self.at_home pass # out drun @state def at_home(self): - '''The end''' + """The end""" others = self.get_agents(state_id=Patron.at_home.id, limit_neighbors=True) - self.debug('I\'m home. Just like {} of my friends'.format(len(others))) - + self.debug("I'm home. Just like {} of my friends".format(len(others))) + def drink(self): - self['pints'] += 1 - self.debug('Cheers to that') - + self["pints"] += 1 + self.debug("Cheers to that") + def kick_out(self): self.kicked_out = True def befriend(self, other_agent, force=False): - ''' + """ Try to become friends with another agent. The chances of success depend on both agents' openness. - ''' - if force or self['openness'] > self.random.random(): + """ + if force or self["openness"] > self.random.random(): self.add_edge(self, other_agent) - self.info('Made some friend {}'.format(other_agent)) + self.info("Made some friend {}".format(other_agent)) return True return False def try_friends(self, others): - ''' Look for random agents around me and try to befriend them''' + """Look for random agents around me and try to befriend them""" befriended = False - k = int(10*self['openness']) + k = int(10 * self["openness"]) self.random.shuffle(others) for friend in islice(others, k): # random.choice >= 3.7 if friend == self: continue if friend.befriend(self): self.befriend(friend, force=True) - self.debug('Hooray! new friend: {}'.format(friend.id)) + self.debug("Hooray! new friend: {}".format(friend.id)) befriended = True else: - self.debug('{} does not want to be friends'.format(friend.id)) + self.debug("{} does not want to be friends".format(friend.id)) return befriended class Police(FSM): - '''Simple agent to take drunk people out of pubs.''' + """Simple agent to take drunk people out of pubs.""" + level = logging.INFO @default_state @state def patrol(self): - drunksters = list(self.get_agents(drunk=True, - state_id=Patron.drunk_in_pub.id)) + drunksters = list(self.get_agents(drunk=True, state_id=Patron.drunk_in_pub.id)) for drunk in drunksters: - self.info('Kicking out the trash: {}'.format(drunk.id)) + self.info("Kicking out the trash: {}".format(drunk.id)) drunk.kick_out() else: - self.info('No trash to take out. Too bad.') + self.info("No trash to take out. Too bad.") -if __name__ == '__main__': +if __name__ == "__main__": from soil import simulation - simulation.run_from_config('pubcrawl.yml', - dry_run=True, - dump=None, - parallel=False) + + simulation.run_from_config("pubcrawl.yml", dry_run=True, dump=None, parallel=False) diff --git a/examples/rabbits/basic/rabbit_agents.py b/examples/rabbits/basic/rabbit_agents.py index 60a0d15..b28d2e9 100644 --- a/examples/rabbits/basic/rabbit_agents.py +++ b/examples/rabbits/basic/rabbit_agents.py @@ -5,7 +5,6 @@ import math class RabbitEnv(Environment): - @property def num_rabbits(self): return self.count_agents(agent_class=Rabbit) @@ -27,7 +26,7 @@ class Rabbit(NetworkAgent, FSM): @default_state @state def newborn(self): - self.info('I am a newborn.') + self.info("I am a newborn.") self.age = 0 self.offspring = 0 return self.youngling @@ -36,7 +35,7 @@ class Rabbit(NetworkAgent, FSM): def youngling(self): self.age += 1 if self.age >= self.sexual_maturity: - self.info(f'I am fertile! My age is {self.age}') + self.info(f"I am fertile! My age is {self.age}") return self.fertile @state @@ -60,11 +59,11 @@ class Male(Rabbit): return self.dead # Males try to mate - for f in self.model.agents(agent_class=Female, - state_id=Female.fertile.id, - limit=self.max_females): - self.debug('FOUND A FEMALE: ', repr(f), self.mating_prob) - if self.prob(self['mating_prob']): + for f in self.model.agents( + agent_class=Female, state_id=Female.fertile.id, limit=self.max_females + ): + self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob) + if self.prob(self["mating_prob"]): f.impregnate(self) break # Take a break @@ -83,14 +82,14 @@ class Female(Rabbit): return self.pregnant def impregnate(self, male): - self.info(f'impregnated by {repr(male)}') + self.info(f"impregnated by {repr(male)}") self.mate = male self.pregnancy = 0 - self.number_of_babies = int(8+4*self.random.random()) + self.number_of_babies = int(8 + 4 * self.random.random()) @state def pregnant(self): - self.info('I am pregnant') + self.info("I am pregnant") self.age += 1 if self.age >= self.life_expectancy: @@ -100,18 +99,17 @@ class Female(Rabbit): self.pregnancy += 1 return - self.info('Having {} babies'.format(self.number_of_babies)) + self.info("Having {} babies".format(self.number_of_babies)) for i in range(self.number_of_babies): state = {} agent_class = self.random.choice([Male, Female]) - child = self.model.add_node(agent_class=agent_class, - **state) + child = self.model.add_node(agent_class=agent_class, **state) child.add_edge(self) try: child.add_edge(self.mate) self.model.agents[self.mate].offspring += 1 except ValueError: - self.debug('The father has passed away') + self.debug("The father has passed away") self.offspring += 1 self.mate = None @@ -119,32 +117,34 @@ class Female(Rabbit): return self.fertile def die(self): - if 'pregnancy' in self and self['pregnancy'] > -1: - self.info('A mother has died carrying a baby!!') + if "pregnancy" in self and self["pregnancy"] > -1: + self.info("A mother has died carrying a baby!!") return super().die() class RandomAccident(BaseAgent): - def step(self): rabbits_alive = self.model.G.number_of_nodes() if not rabbits_alive: return self.die() - prob_death = self.model.get('prob_death', 1e-100)*math.floor(math.log10(max(1, rabbits_alive))) - self.debug('Killing some rabbits with prob={}!'.format(prob_death)) + prob_death = self.model.get("prob_death", 1e-100) * math.floor( + math.log10(max(1, rabbits_alive)) + ) + self.debug("Killing some rabbits with prob={}!".format(prob_death)) for i in self.iter_agents(agent_class=Rabbit): if i.state_id == i.dead.id: continue if self.prob(prob_death): - self.info('I killed a rabbit: {}'.format(i.id)) + self.info("I killed a rabbit: {}".format(i.id)) rabbits_alive -= 1 i.die() - self.debug('Rabbits alive: {}'.format(rabbits_alive)) + self.debug("Rabbits alive: {}".format(rabbits_alive)) -if __name__ == '__main__': +if __name__ == "__main__": from soil import easy - with easy('rabbits.yml') as sim: + + with easy("rabbits.yml") as sim: sim.run() diff --git a/examples/rabbits/improved/rabbit_agents.py b/examples/rabbits/improved/rabbit_agents.py index c7d995d..0f45d9a 100644 --- a/examples/rabbits/improved/rabbit_agents.py +++ b/examples/rabbits/improved/rabbit_agents.py @@ -7,7 +7,6 @@ import math class RabbitEnv(Environment): - @property def num_rabbits(self): return self.count_agents(agent_class=Rabbit) @@ -36,7 +35,7 @@ class Rabbit(FSM, NetworkAgent): @default_state @state def newborn(self): - self.info('I am a newborn.') + self.info("I am a newborn.") self.birth = self.now self.offspring = 0 return self.youngling, Delta(self.sexual_maturity - self.age) @@ -44,7 +43,7 @@ class Rabbit(FSM, NetworkAgent): @state def youngling(self): if self.age >= self.sexual_maturity: - self.info(f'I am fertile! My age is {self.age}') + self.info(f"I am fertile! My age is {self.age}") return self.fertile @state @@ -66,11 +65,11 @@ class Male(Rabbit): return self.dead # Males try to mate - for f in self.model.agents(agent_class=Female, - state_id=Female.fertile.id, - limit=self.max_females): - self.debug('FOUND A FEMALE: ', repr(f), self.mating_prob) - if self.prob(self['mating_prob']): + for f in self.model.agents( + agent_class=Female, state_id=Female.fertile.id, limit=self.max_females + ): + self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob) + if self.prob(self["mating_prob"]): f.impregnate(self) break # Do not try to impregnate other females @@ -94,32 +93,31 @@ class Female(Rabbit): return self.now - self.conception def impregnate(self, male): - self.info(f'impregnated by {repr(male)}') + self.info(f"impregnated by {repr(male)}") self.mate = male self.conception = self.now - self.number_of_babies = int(8+4*self.random.random()) + self.number_of_babies = int(8 + 4 * self.random.random()) @state def pregnant(self): - self.debug('I am pregnant') + self.debug("I am pregnant") if self.age > self.life_expectancy: self.info("Dying before giving birth") return self.die() if self.pregnancy >= self.gestation: - self.info('Having {} babies'.format(self.number_of_babies)) + self.info("Having {} babies".format(self.number_of_babies)) for i in range(self.number_of_babies): state = {} agent_class = self.random.choice([Male, Female]) - child = self.model.add_node(agent_class=agent_class, - **state) + child = self.model.add_node(agent_class=agent_class, **state) child.add_edge(self) if self.mate: child.add_edge(self.mate) self.mate.offspring += 1 else: - self.debug('The father has passed away') + self.debug("The father has passed away") self.offspring += 1 self.mate = None @@ -127,31 +125,33 @@ class Female(Rabbit): def die(self): if self.pregnancy is not None: - self.info('A mother has died carrying a baby!!') + self.info("A mother has died carrying a baby!!") return super().die() class RandomAccident(BaseAgent): - def step(self): rabbits_alive = self.model.G.number_of_nodes() if not rabbits_alive: return self.die() - prob_death = self.model.get('prob_death', 1e-100)*math.floor(math.log10(max(1, rabbits_alive))) - self.debug('Killing some rabbits with prob={}!'.format(prob_death)) + prob_death = self.model.get("prob_death", 1e-100) * math.floor( + math.log10(max(1, rabbits_alive)) + ) + self.debug("Killing some rabbits with prob={}!".format(prob_death)) for i in self.iter_agents(agent_class=Rabbit): if i.state_id == i.dead.id: continue if self.prob(prob_death): - self.info('I killed a rabbit: {}'.format(i.id)) + self.info("I killed a rabbit: {}".format(i.id)) rabbits_alive -= 1 i.die() - self.debug('Rabbits alive: {}'.format(rabbits_alive)) + self.debug("Rabbits alive: {}".format(rabbits_alive)) -if __name__ == '__main__': +if __name__ == "__main__": from soil import easy - with easy('rabbits.yml') as sim: + + with easy("rabbits.yml") as sim: sim.run() diff --git a/examples/random_delays/random_delays.py b/examples/random_delays/random_delays.py index 8455e5e..1bed03e 100644 --- a/examples/random_delays/random_delays.py +++ b/examples/random_delays/random_delays.py @@ -1,42 +1,43 @@ -''' +""" Example of setting a Example of a fully programmatic simulation, without definition files. -''' +""" from soil import Simulation, agents from soil.time import Delta - class MyAgent(agents.FSM): - ''' + """ An agent that first does a ping - ''' + """ - defaults = {'pong_counts': 2} + defaults = {"pong_counts": 2} @agents.default_state @agents.state def ping(self): - self.info('Ping') - return self.pong, Delta(self.random.expovariate(1/16)) + self.info("Ping") + return self.pong, Delta(self.random.expovariate(1 / 16)) @agents.state def pong(self): - self.info('Pong') + self.info("Pong") self.pong_counts -= 1 self.info(str(self.pong_counts)) if self.pong_counts < 1: return self.die() - return None, Delta(self.random.expovariate(1/16)) - - -s = Simulation(name='Programmatic', - network_agents=[{'agent_class': MyAgent, 'id': 0}], - topology={'nodes': [{'id': 0}], 'links': []}, - num_trials=1, - max_time=100, - agent_class=MyAgent, - dry_run=True) + return None, Delta(self.random.expovariate(1 / 16)) + + +s = Simulation( + name="Programmatic", + network_agents=[{"agent_class": MyAgent, "id": 0}], + topology={"nodes": [{"id": 0}], "links": []}, + num_trials=1, + max_time=100, + agent_class=MyAgent, + dry_run=True, +) envs = s.run() diff --git a/examples/terrorism/TerroristNetworkModel.py b/examples/terrorism/TerroristNetworkModel.py index bf5045f..8fa6563 100644 --- a/examples/terrorism/TerroristNetworkModel.py +++ b/examples/terrorism/TerroristNetworkModel.py @@ -20,56 +20,83 @@ class TerroristSpreadModel(FSM, Geo): def __init__(self, model=None, unique_id=0, state=()): super().__init__(model=model, unique_id=unique_id, state=state) - self.information_spread_intensity = model.environment_params['information_spread_intensity'] - self.terrorist_additional_influence = model.environment_params['terrorist_additional_influence'] - self.prob_interaction = model.environment_params['prob_interaction'] - - if self['id'] == self.civilian.id: # Civilian + self.information_spread_intensity = model.environment_params[ + "information_spread_intensity" + ] + self.terrorist_additional_influence = model.environment_params[ + "terrorist_additional_influence" + ] + self.prob_interaction = model.environment_params["prob_interaction"] + + if self["id"] == self.civilian.id: # Civilian self.mean_belief = self.random.uniform(0.00, 0.5) - elif self['id'] == self.terrorist.id: # Terrorist + elif self["id"] == self.terrorist.id: # Terrorist self.mean_belief = self.random.uniform(0.8, 1.00) - elif self['id'] == self.leader.id: # Leader + elif self["id"] == self.leader.id: # Leader self.mean_belief = 1.00 else: - raise Exception('Invalid state id: {}'.format(self['id'])) - - if 'min_vulnerability' in model.environment_params: - self.vulnerability = self.random.uniform( model.environment_params['min_vulnerability'], model.environment_params['max_vulnerability'] ) - else : - self.vulnerability = self.random.uniform( 0, model.environment_params['max_vulnerability'] ) + raise Exception("Invalid state id: {}".format(self["id"])) + if "min_vulnerability" in model.environment_params: + self.vulnerability = self.random.uniform( + model.environment_params["min_vulnerability"], + model.environment_params["max_vulnerability"], + ) + else: + self.vulnerability = self.random.uniform( + 0, model.environment_params["max_vulnerability"] + ) @state def civilian(self): neighbours = list(self.get_neighboring_agents(agent_class=TerroristSpreadModel)) if len(neighbours) > 0: # Only interact with some of the neighbors - interactions = list(n for n in neighbours if self.random.random() <= self.prob_interaction) - influence = sum( self.degree(i) for i in interactions ) - mean_belief = sum( i.mean_belief * self.degree(i) / influence for i in interactions ) - mean_belief = mean_belief * self.information_spread_intensity + self.mean_belief * ( 1 - self.information_spread_intensity ) - self.mean_belief = mean_belief * self.vulnerability + self.mean_belief * ( 1 - self.vulnerability ) - + interactions = list( + n for n in neighbours if self.random.random() <= self.prob_interaction + ) + influence = sum(self.degree(i) for i in interactions) + mean_belief = sum( + i.mean_belief * self.degree(i) / influence for i in interactions + ) + mean_belief = ( + mean_belief * self.information_spread_intensity + + self.mean_belief * (1 - self.information_spread_intensity) + ) + self.mean_belief = mean_belief * self.vulnerability + self.mean_belief * ( + 1 - self.vulnerability + ) + if self.mean_belief >= 0.8: return self.terrorist @state def leader(self): - self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence ) - for neighbour in self.get_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]): + self.mean_belief = self.mean_belief ** (1 - self.terrorist_additional_influence) + for neighbour in self.get_neighboring_agents( + state_id=[self.terrorist.id, self.leader.id] + ): if self.betweenness(neighbour) > self.betweenness(self): return self.terrorist @state def terrorist(self): - neighbours = self.get_agents(state_id=[self.terrorist.id, self.leader.id], - agent_class=TerroristSpreadModel, - limit_neighbors=True) + neighbours = self.get_agents( + state_id=[self.terrorist.id, self.leader.id], + agent_class=TerroristSpreadModel, + limit_neighbors=True, + ) if len(neighbours) > 0: - influence = sum( self.degree(n) for n in neighbours ) - mean_belief = sum( n.mean_belief * self.degree(n) / influence for n in neighbours ) - mean_belief = mean_belief * self.vulnerability + self.mean_belief * ( 1 - self.vulnerability ) - self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence ) + influence = sum(self.degree(n) for n in neighbours) + mean_belief = sum( + n.mean_belief * self.degree(n) / influence for n in neighbours + ) + mean_belief = mean_belief * self.vulnerability + self.mean_belief * ( + 1 - self.vulnerability + ) + self.mean_belief = self.mean_belief ** ( + 1 - self.terrorist_additional_influence + ) # Check if there are any leaders in the group leaders = list(filter(lambda x: x.state.id == self.leader.id, neighbours)) @@ -82,21 +109,29 @@ class TerroristSpreadModel(FSM, Geo): return self.leader def ego_search(self, steps=1, center=False, node=None, **kwargs): - '''Get a list of nodes in the ego network of *node* of radius *steps*''' + """Get a list of nodes in the ego network of *node* of radius *steps*""" node = as_node(node if node is not None else self) G = self.subgraph(**kwargs) return nx.ego_graph(G, node, center=center, radius=steps).nodes() def degree(self, node, force=False): node = as_node(node) - if force or (not hasattr(self.model, '_degree')) or getattr(self.model, '_last_step', 0) < self.now: + if ( + force + or (not hasattr(self.model, "_degree")) + or getattr(self.model, "_last_step", 0) < self.now + ): self.model._degree = nx.degree_centrality(self.G) self.model._last_step = self.now return self.model._degree[node] def betweenness(self, node, force=False): node = as_node(node) - if force or (not hasattr(self.model, '_betweenness')) or getattr(self.model, '_last_step', 0) < self.now: + if ( + force + or (not hasattr(self.model, "_betweenness")) + or getattr(self.model, "_last_step", 0) < self.now + ): self.model._betweenness = nx.betweenness_centrality(self.G) self.model._last_step = self.now return self.model._betweenness[node] @@ -114,17 +149,20 @@ class TrainingAreaModel(FSM, Geo): def __init__(self, model=None, unique_id=0, state=()): super().__init__(model=model, unique_id=unique_id, state=state) - self.training_influence = model.environment_params['training_influence'] - if 'min_vulnerability' in model.environment_params: - self.min_vulnerability = model.environment_params['min_vulnerability'] - else: self.min_vulnerability = 0 + self.training_influence = model.environment_params["training_influence"] + if "min_vulnerability" in model.environment_params: + self.min_vulnerability = model.environment_params["min_vulnerability"] + else: + self.min_vulnerability = 0 @default_state @state def terrorist(self): for neighbour in self.get_neighboring_agents(agent_class=TerroristSpreadModel): if neighbour.vulnerability > self.min_vulnerability: - neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.training_influence ) + neighbour.vulnerability = neighbour.vulnerability ** ( + 1 - self.training_influence + ) class HavenModel(FSM, Geo): @@ -141,11 +179,12 @@ class HavenModel(FSM, Geo): def __init__(self, model=None, unique_id=0, state=()): super().__init__(model=model, unique_id=unique_id, state=state) - self.haven_influence = model.environment_params['haven_influence'] - if 'min_vulnerability' in model.environment_params: - self.min_vulnerability = model.environment_params['min_vulnerability'] - else: self.min_vulnerability = 0 - self.max_vulnerability = model.environment_params['max_vulnerability'] + self.haven_influence = model.environment_params["haven_influence"] + if "min_vulnerability" in model.environment_params: + self.min_vulnerability = model.environment_params["min_vulnerability"] + else: + self.min_vulnerability = 0 + self.max_vulnerability = model.environment_params["max_vulnerability"] def get_occupants(self, **kwargs): return self.get_neighboring_agents(agent_class=TerroristSpreadModel, **kwargs) @@ -158,14 +197,18 @@ class HavenModel(FSM, Geo): for neighbour in self.get_occupants(): if neighbour.vulnerability > self.min_vulnerability: - neighbour.vulnerability = neighbour.vulnerability * ( 1 - self.haven_influence ) + neighbour.vulnerability = neighbour.vulnerability * ( + 1 - self.haven_influence + ) return self.civilian @state def terrorist(self): for neighbour in self.get_occupants(): if neighbour.vulnerability < self.max_vulnerability: - neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.haven_influence ) + neighbour.vulnerability = neighbour.vulnerability ** ( + 1 - self.haven_influence + ) return self.terrorist @@ -184,10 +227,10 @@ class TerroristNetworkModel(TerroristSpreadModel): def __init__(self, model=None, unique_id=0, state=()): super().__init__(model=model, unique_id=unique_id, state=state) - self.vision_range = model.environment_params['vision_range'] - self.sphere_influence = model.environment_params['sphere_influence'] - self.weight_social_distance = model.environment_params['weight_social_distance'] - self.weight_link_distance = model.environment_params['weight_link_distance'] + self.vision_range = model.environment_params["vision_range"] + self.sphere_influence = model.environment_params["sphere_influence"] + self.weight_social_distance = model.environment_params["weight_social_distance"] + self.weight_link_distance = model.environment_params["weight_link_distance"] @state def terrorist(self): @@ -201,27 +244,48 @@ class TerroristNetworkModel(TerroristSpreadModel): def update_relationships(self): if self.count_neighboring_agents(state_id=self.civilian.id) == 0: - close_ups = set(self.geo_search(radius=self.vision_range, agent_class=TerroristNetworkModel)) - step_neighbours = set(self.ego_search(self.sphere_influence, agent_class=TerroristNetworkModel, center=False)) - neighbours = set(agent.id for agent in self.get_neighboring_agents(agent_class=TerroristNetworkModel)) + close_ups = set( + self.geo_search( + radius=self.vision_range, agent_class=TerroristNetworkModel + ) + ) + step_neighbours = set( + self.ego_search( + self.sphere_influence, + agent_class=TerroristNetworkModel, + center=False, + ) + ) + neighbours = set( + agent.id + for agent in self.get_neighboring_agents( + agent_class=TerroristNetworkModel + ) + ) search = (close_ups | step_neighbours) - neighbours for agent in self.get_agents(search): social_distance = 1 / self.shortest_path_length(agent.id) - spatial_proximity = ( 1 - self.get_distance(agent.id) ) - prob_new_interaction = self.weight_social_distance * social_distance + self.weight_link_distance * spatial_proximity - if agent['id'] == agent.civilian.id and self.random.random() < prob_new_interaction: + spatial_proximity = 1 - self.get_distance(agent.id) + prob_new_interaction = ( + self.weight_social_distance * social_distance + + self.weight_link_distance * spatial_proximity + ) + if ( + agent["id"] == agent.civilian.id + and self.random.random() < prob_new_interaction + ): self.add_edge(agent) break def get_distance(self, target): - source_x, source_y = nx.get_node_attributes(self.G, 'pos')[self.id] - target_x, target_y = nx.get_node_attributes(self.G, 'pos')[target] - dx = abs( source_x - target_x ) - dy = abs( source_y - target_y ) - return ( dx ** 2 + dy ** 2 ) ** ( 1 / 2 ) + source_x, source_y = nx.get_node_attributes(self.G, "pos")[self.id] + target_x, target_y = nx.get_node_attributes(self.G, "pos")[target] + dx = abs(source_x - target_x) + dy = abs(source_y - target_y) + return (dx**2 + dy**2) ** (1 / 2) def shortest_path_length(self, target): try: return nx.shortest_path_length(self.G, self.id, target) except nx.NetworkXNoPath: - return float('inf') + return float("inf") diff --git a/soil/__init__.py b/soil/__init__.py index a49462b..92bc79f 100644 --- a/soil/__init__.py +++ b/soil/__init__.py @@ -225,6 +225,7 @@ def easy(cfg, pdb=False, debug=False, **kwargs): except Exception as e: if os.environ.get("SOIL_POSTMORTEM"): from .debugging import post_mortem + print(traceback.format_exc()) post_mortem() ex = e @@ -232,5 +233,6 @@ def easy(cfg, pdb=False, debug=False, **kwargs): if ex: raise ex + if __name__ == "__main__": main(do_run=True) diff --git a/soil/__main__.py b/soil/__main__.py index 9ad5c4f..0c76791 100644 --- a/soil/__main__.py +++ b/soil/__main__.py @@ -1,7 +1,9 @@ from . import main as init_main + def main(): init_main(do_run=True) -if __name__ == '__main__': + +if __name__ == "__main__": init_main(do_run=True) diff --git a/soil/agents/__init__.py b/soil/agents/__init__.py index b316caa..714b15e 100644 --- a/soil/agents/__init__.py +++ b/soil/agents/__init__.py @@ -43,9 +43,9 @@ class MetaAgent(ABCMeta): } for attr, func in namespace.items(): - if attr == 'step' and inspect.isgeneratorfunction(func): + if attr == "step" and inspect.isgeneratorfunction(func): orig_func = func - new_nmspc['_MetaAgent__coroutine'] = None + new_nmspc["_MetaAgent__coroutine"] = None @wraps(func) def func(self): @@ -62,10 +62,10 @@ class MetaAgent(ABCMeta): func.is_default = False new_nmspc[attr] = func elif ( - isinstance(func, types.FunctionType) - or isinstance(func, property) - or isinstance(func, classmethod) - or attr[0] == "_" + isinstance(func, types.FunctionType) + or isinstance(func, property) + or isinstance(func, classmethod) + or attr[0] == "_" ): new_nmspc[attr] = func elif attr == "defaults": @@ -303,7 +303,7 @@ class NetworkAgent(BaseAgent): return G def remove_node(self): - print(f'Removing node for {self.unique_id}: {self.node_id}') + print(f"Removing node for {self.unique_id}: {self.node_id}") self.G.remove_node(self.node_id) self.node_id = None diff --git a/soil/environment.py b/soil/environment.py index fb4823f..8245ca0 100644 --- a/soil/environment.py +++ b/soil/environment.py @@ -146,7 +146,7 @@ class BaseEnvironment(Model): if unique_id is None: unique_id = self.next_id() - kwargs['unique_id'] = unique_id + kwargs["unique_id"] = unique_id a = self._agent_from_dict(kwargs) self.schedule.add(a) @@ -169,7 +169,9 @@ class BaseEnvironment(Model): Advance one step in the simulation, and update the data collection and scheduler appropriately """ super().step() - self.logger.info(f"--- Step: {self.schedule.steps:^5} - Time: {self.now:^5} ---") + self.logger.info( + f"--- Step: {self.schedule.steps:^5} - Time: {self.now:^5} ---" + ) self.schedule.step() self.datacollector.collect(self) @@ -236,7 +238,7 @@ class NetworkEnvironment(BaseEnvironment): node_id = agent.get("node_id", None) if node_id is None: node_id = network.find_unassigned(self.G, random=self.random) - self.G.nodes[node_id]['agent'] = None + self.G.nodes[node_id]["agent"] = None agent["node_id"] = node_id agent["unique_id"] = unique_id agent["topology"] = self.G @@ -271,7 +273,7 @@ class NetworkEnvironment(BaseEnvironment): G=self.G, shuffle=True, random=self.random ) if node_id is None: - node_id = f'node_for_{unique_id}' + node_id = f"node_for_{unique_id}" if node_id not in self.G.nodes: self.G.add_node(node_id) @@ -280,17 +282,23 @@ class NetworkEnvironment(BaseEnvironment): self.G.nodes[node_id]["agent"] = None # Reserve a = self.add_agent( - unique_id=unique_id, agent_class=agent_class, topology=self.G, node_id=node_id, **kwargs + unique_id=unique_id, + agent_class=agent_class, + topology=self.G, + node_id=node_id, + **kwargs, ) a["visible"] = True return a def add_agent(self, *args, **kwargs): a = super().add_agent(*args, **kwargs) - if 'node_id' in a: + if "node_id" in a: if a.node_id == 24: - import pdb;pdb.set_trace() - assert self.G.nodes[a.node_id]['agent'] == a + import pdb + + pdb.set_trace() + assert self.G.nodes[a.node_id]["agent"] == a return a def agent_for_node_id(self, node_id): diff --git a/soil/exporters.py b/soil/exporters.py index 405b2f8..6efe70a 100644 --- a/soil/exporters.py +++ b/soil/exporters.py @@ -202,7 +202,12 @@ class summary(Exporter): for (t, df) in self.get_dfs(env): if not len(df): continue - msg = indent(str(df.describe()), ' ') - logger.info(dedent(f''' + msg = indent(str(df.describe()), " ") + logger.info( + dedent( + f""" Dataframe {t}: - ''') + msg) + """ + ) + + msg + ) diff --git a/soil/simulation.py b/soil/simulation.py index fc50ab8..f5738d4 100644 --- a/soil/simulation.py +++ b/soil/simulation.py @@ -226,7 +226,9 @@ Model stats: ) model.step() - if model.schedule.time < until: # Simulation ended (no more steps) before the expected time + if ( + model.schedule.time < until + ): # Simulation ended (no more steps) before the expected time model.schedule.time = until return model diff --git a/soil/time.py b/soil/time.py index 26c4259..e7acbac 100644 --- a/soil/time.py +++ b/soil/time.py @@ -174,7 +174,6 @@ class TimedActivation(BaseScheduler): agent.alive = False continue - if not getattr(agent, "alive", True): self.remove(agent) continue