1
0
mirror of https://github.com/gsi-upm/soil synced 2025-10-27 05:38:17 +00:00

Compare commits

...

8 Commits

Author SHA1 Message Date
J. Fernando Sánchez
a2fb25c160 Version 0.30.0rc2
* Fix CLI arguments not being used when easy is passed a simulation instance
* Docs for `examples/events_and_messages/cars.py`
2022-10-18 17:02:12 +02:00
J. Fernando Sánchez
5fcf610108 Version 0.30.0rc1 2022-10-18 15:02:05 +02:00
J. Fernando Sánchez
159c9a9077 Add events 2022-10-18 13:11:01 +02:00
J. Fernando Sánchez
3776c4e5c5 Refactor
* Removed references to `set_state`
* Split some functionality from `agents` into separate files (`fsm` and
`network_agents`)
* Rename `neighboring_agents` to `neighbors`
* Delete some spurious functions
2022-10-17 21:49:31 +02:00
J. Fernando Sánchez
880a9f2a1c black formatting 2022-10-17 20:23:57 +02:00
J. Fernando Sánchez
227fdf050e Fix conditionals 2022-10-17 19:29:39 +02:00
J. Fernando Sánchez
5d759d0072 Add conditional time values 2022-10-17 13:58:14 +02:00
J. Fernando Sánchez
77d08fc592 Agent step can be a generator 2022-10-17 08:58:51 +02:00
42 changed files with 1557 additions and 968 deletions

View File

@@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3 UNRELEASED] ## [0.30 UNRELEASED]
### Added ### Added
* Simple debugging capabilities in `soil.debugging`, with a custom `pdb.Debugger` subclass that exposes commands to list agents and their status and set breakpoints on states (for FSM agents). Try it with `soil --debug <simulation file>` * Simple debugging capabilities in `soil.debugging`, with a custom `pdb.Debugger` subclass that exposes commands to list agents and their status and set breakpoints on states (for FSM agents). Try it with `soil --debug <simulation file>`
* Ability to run * Ability to run

View File

@@ -2,16 +2,17 @@ from networkx import Graph
import random import random
import networkx as nx import networkx as nx
def mygenerator(n=5, n_edges=5): def mygenerator(n=5, n_edges=5):
''' """
Just a simple generator that creates a network with n nodes and Just a simple generator that creates a network with n nodes and
n_edges edges. Edges are assigned randomly, only avoiding self loops. n_edges edges. Edges are assigned randomly, only avoiding self loops.
''' """
G = nx.Graph() G = nx.Graph()
for i in range(n): for i in range(n):
G.add_node(i) G.add_node(i)
for i in range(n_edges): for i in range(n_edges):
nodes = list(G.nodes) nodes = list(G.nodes)
n_in = random.choice(nodes) n_in = random.choice(nodes)
@@ -19,9 +20,3 @@ def mygenerator(n=5, n_edges=5):
n_out = random.choice(nodes) n_out = random.choice(nodes)
G.add_edge(n_in, n_out) G.add_edge(n_in, n_out)
return G return G

View File

@@ -2,34 +2,37 @@ from soil.agents import FSM, state, default_state
class Fibonacci(FSM): 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 = { defaults = {"prev": 1}
'prev': 1
}
@default_state @default_state
@state @state
def counting(self): def counting(self):
self.log('Stopping at {}'.format(self.now)) self.log("Stopping at {}".format(self.now))
prev, self['prev'] = self['prev'], max([self.now, self['prev']]) prev, self["prev"] = self["prev"], max([self.now, self["prev"]])
return None, self.env.timeout(prev) return None, self.env.timeout(prev)
class Odds(FSM): class Odds(FSM):
'''Agent that only executes in odd t_steps''' """Agent that only executes in odd t_steps"""
@default_state @default_state
@state @state
def odds(self): def odds(self):
self.log('Stopping at {}'.format(self.now)) self.log("Stopping at {}".format(self.now))
return None, self.env.timeout(1+self.now%2) return None, self.env.timeout(1 + self.now % 2)
if __name__ == '__main__': if __name__ == "__main__":
from soil import Simulation from soil import Simulation
s = Simulation(network_agents=[{'ids': [0], 'agent_class': Fibonacci},
{'ids': [1], 'agent_class': Odds}], s = Simulation(
network_params={"generator": "complete_graph", "n": 2}, network_agents=[
max_time=100, {"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) s.run(dry_run=True)

View File

@@ -0,0 +1,7 @@
This example can be run like with command-line options, like this:
```bash
python cars.py --level DEBUG -e summary --csv
```
This will set the `CSV` (save the agent and model data to a CSV) and `summary` (print the a summary of the data to stdout) exporters, and set the log level to DEBUG.

View File

@@ -0,0 +1,205 @@
"""
This is an example of a simplified city, where there are Passengers and Drivers that can take those passengers
from their location to their desired location.
An example scenario could play like the following:
- Drivers start in the `wandering` state, where they wander around the city until they have been assigned a journey
- Passenger(1) tells every driver that it wants to request a Journey.
- Each driver receives the request.
If Driver(2) is interested in providing the Journey, it asks Passenger(1) to confirm that it accepts Driver(2)'s request
- When Passenger(1) accepts the request, two things happen:
- Passenger(1) changes its state to `driving_home`
- Driver(2) starts moving towards the origin of the Journey
- Once Driver(2) reaches the origin, it starts moving itself and Passenger(1) to the destination of the Journey
- When Driver(2) reaches the destination (carrying Passenger(1) along):
- Driver(2) starts wondering again
- Passenger(1) dies, and is removed from the simulation
- If there are no more passengers available in the simulation, Drivers die
"""
from __future__ import annotations
from soil import *
from soil import events
from mesa.space import MultiGrid
# More complex scenarios may use more than one type of message between objects.
# A common pattern is to use `enum.Enum` to represent state changes in a request.
@dataclass
class Journey:
"""
This represents a request for a journey. Passengers and drivers exchange this object.
A journey may have a driver assigned or not. If the driver has not been assigned, this
object is considered a "request for a journey".
"""
origin: (int, int)
destination: (int, int)
tip: float
passenger: Passenger
driver: Driver = None
class City(EventedEnvironment):
"""
An environment with a grid where drivers and passengers will be placed.
The number of drivers and riders is configurable through its parameters:
:param str n_cars: The total number of drivers to add
:param str n_passengers: The number of passengers in the simulation
:param list agents: Specific agents to use in the simulation. It overrides the `n_passengers`
and `n_cars` params.
:param int height: Height of the internal grid
:param int width: Width of the internal grid
"""
def __init__(self, *args, n_cars=1, n_passengers=10,
height=100, width=100, agents=None,
model_reporters=None,
**kwargs):
self.grid = MultiGrid(width=width, height=height, torus=False)
if agents is None:
agents = []
for i in range(n_cars):
agents.append({'agent_class': Driver})
for i in range(n_passengers):
agents.append({'agent_class': Passenger})
model_reporters = model_reporters or {'earnings': 'total_earnings', 'n_passengers': 'number_passengers'}
print('REPORTERS', model_reporters)
super().__init__(*args, agents=agents, model_reporters=model_reporters, **kwargs)
for agent in self.agents:
self.grid.place_agent(agent, (0, 0))
self.grid.move_to_empty(agent)
@property
def total_earnings(self):
return sum(d.earnings for d in self.agents(agent_class=Driver))
@property
def number_passengers(self):
return self.count_agents(agent_class=Passenger)
class Driver(Evented, FSM):
pos = None
journey = None
earnings = 0
def on_receive(self, msg, sender):
'''This is not a state. It will run (and block) every time check_messages is invoked'''
if self.journey is None and isinstance(msg, Journey) and msg.driver is None:
msg.driver = self
self.journey = msg
def check_passengers(self):
'''If there are no more passengers, stop forever'''
c = self.count_agents(agent_class=Passenger)
self.info(f"Passengers left {c}")
if not c:
self.die()
@default_state
@state
def wandering(self):
'''Move around the city until a journey is accepted'''
target = None
self.check_passengers()
self.journey = None
while self.journey is None: # No potential journeys detected (see on_receive)
if target is None or not self.move_towards(target):
target = self.random.choice(self.model.grid.get_neighborhood(self.pos, moore=False))
self.check_passengers()
self.check_messages() # This will call on_receive behind the scenes, and the agent's status will be updated
yield Delta(30) # Wait at least 30 seconds before checking again
try:
# Re-send the journey to the passenger, to confirm that we have been selected
self.journey = yield self.journey.passenger.ask(self.journey, timeout=60)
except events.TimedOut:
# No journey has been accepted. Try again
self.journey = None
return
return self.driving
@state
def driving(self):
'''The journey has been accepted. Pick them up and take them to their destination'''
while self.move_towards(self.journey.origin):
yield
while self.move_towards(self.journey.destination, with_passenger=True):
yield
self.earnings += self.journey.tip
self.check_passengers()
return self.wandering
def move_towards(self, target, with_passenger=False):
'''Move one cell at a time towards a target'''
self.info(f"Moving { self.pos } -> { target }")
if target[0] == self.pos[0] and target[1] == self.pos[1]:
return False
next_pos = [self.pos[0], self.pos[1]]
for idx in [0, 1]:
if self.pos[idx] < target[idx]:
next_pos[idx] += 1
break
if self.pos[idx] > target[idx]:
next_pos[idx] -= 1
break
self.model.grid.move_agent(self, tuple(next_pos))
if with_passenger:
self.journey.passenger.pos = self.pos # This could be communicated through messages
return True
class Passenger(Evented, FSM):
pos = None
def on_receive(self, msg, sender):
'''This is not a state. It will be run synchronously every time `check_messages` is run'''
if isinstance(msg, Journey):
self.journey = msg
return msg
@default_state
@state
def asking(self):
destination = (self.random.randint(0, self.model.grid.height), self.random.randint(0, self.model.grid.width))
self.journey = None
journey = Journey(origin=self.pos,
destination=destination,
tip=self.random.randint(10, 100),
passenger=self)
timeout = 60
expiration = self.now + timeout
self.model.broadcast(journey, ttl=timeout, sender=self, agent_class=Driver)
while not self.journey:
self.info(f"Passenger at: { self.pos }. Checking for responses.")
try:
yield self.received(expiration=expiration)
except events.TimedOut:
self.info(f"Passenger at: { self.pos }. Asking for journey.")
self.model.broadcast(journey, ttl=timeout, sender=self, agent_class=Driver)
expiration = self.now + timeout
self.check_messages()
return self.driving_home
@state
def driving_home(self):
while self.pos[0] != self.journey.destination[0] or self.pos[1] != self.journey.destination[1]:
yield self.received(timeout=60)
self.info("Got home safe!")
self.die()
simulation = Simulation(name='RideHailing', model_class=City, model_params={'n_passengers': 2})
if __name__ == "__main__":
with easy(simulation) as s:
s.run()

View File

@@ -14,16 +14,18 @@ def network_portrayal(env):
# The model ensures there is 0 or 1 agent per node # The model ensures there is 0 or 1 agent per node
portrayal = dict() 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"] = [ portrayal["nodes"] = [
{ {
"id": node_id, "id": node_id,
"size": 2*(wealth+1), "size": 2 * (wealth + 1),
"color": "#CC0000" if wealth == 0 else "#007959", "color": "#CC0000" if wealth == 0 else "#007959",
# "color": "#CC0000", # "color": "#CC0000",
"label": f"{node_id}: {wealth}", "label": f"{node_id}: {wealth}",
} for (node_id, wealth) in wealths.items() }
for (node_id, wealth) in wealths.items()
] ]
portrayal["edges"] = [ portrayal["edges"] = [
@@ -41,7 +43,7 @@ def gridPortrayal(agent):
:param agent: the agent in the simulation :param agent: the agent in the simulation
:return: the portrayal dictionary :return: the portrayal dictionary
""" """
color = max(10, min(agent.wealth*10, 100)) color = max(10, min(agent.wealth * 10, 100))
return { return {
"Shape": "rect", "Shape": "rect",
"w": 1, "w": 1,
@@ -52,7 +54,7 @@ def gridPortrayal(agent):
"Text": agent.unique_id, "Text": agent.unique_id,
"x": agent.pos[0], "x": agent.pos[0],
"y": agent.pos[1], "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, 10,
1, 1,
description="Grid height", description="Grid height",
), ),
"width": UserSettableParameter( "width": UserSettableParameter(
"slider", "slider",
"width", "width",
@@ -88,16 +90,20 @@ model_params = {
10, 10,
1, 1,
description="Grid width", description="Grid width",
), ),
"agent_class": UserSettableParameter('choice', 'Agent class', value='MoneyAgent', "agent_class": UserSettableParameter(
choices=['MoneyAgent', 'SocialMoneyAgent']), "choice",
"Agent class",
value="MoneyAgent",
choices=["MoneyAgent", "SocialMoneyAgent"],
),
"generator": graph_generator, "generator": graph_generator,
} }
canvas_element = CanvasGrid(gridPortrayal, canvas_element = CanvasGrid(
model_params["width"].value, gridPortrayal, model_params["width"].value, model_params["height"].value, 500, 500
model_params["height"].value, 500, 500) )
server = ModularServer( server = ModularServer(

View File

@@ -1,9 +1,10 @@
''' """
This is an example that adds soil agents and environment in a normal This is an example that adds soil agents and environment in a normal
mesa workflow. mesa workflow.
''' """
from mesa import Agent as MesaAgent from mesa import Agent as MesaAgent
from mesa.space import MultiGrid from mesa.space import MultiGrid
# from mesa.time import RandomActivation # from mesa.time import RandomActivation
from mesa.datacollection import DataCollector from mesa.datacollection import DataCollector
from mesa.batchrunner import BatchRunner from mesa.batchrunner import BatchRunner
@@ -12,12 +13,13 @@ import networkx as nx
from soil import NetworkAgent, Environment, serialization from soil import NetworkAgent, Environment, serialization
def compute_gini(model): def compute_gini(model):
agent_wealths = [agent.wealth for agent in model.agents] agent_wealths = [agent.wealth for agent in model.agents]
x = sorted(agent_wealths) x = sorted(agent_wealths)
N = len(list(model.agents)) N = len(list(model.agents))
B = sum( xi * (N-i) for i,xi in enumerate(x) ) / (N*sum(x)) B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x))
return (1 + (1/N) - 2*B) return 1 + (1 / N) - 2 * B
class MoneyAgent(MesaAgent): class MoneyAgent(MesaAgent):
@@ -32,9 +34,8 @@ class MoneyAgent(MesaAgent):
def move(self): def move(self):
possible_steps = self.model.grid.get_neighborhood( possible_steps = self.model.grid.get_neighborhood(
self.pos, self.pos, moore=True, include_center=False
moore=True, )
include_center=False)
new_position = self.random.choice(possible_steps) new_position = self.random.choice(possible_steps)
self.model.grid.move_agent(self, new_position) self.model.grid.move_agent(self, new_position)
@@ -57,7 +58,7 @@ class SocialMoneyAgent(NetworkAgent, MoneyAgent):
def give_money(self): def give_money(self):
cellmates = set(self.model.grid.get_cell_list_contents([self.pos])) cellmates = set(self.model.grid.get_cell_list_contents([self.pos]))
friends = set(self.get_neighboring_agents()) friends = set(self.get_neighbors())
self.info("Trying to give money") self.info("Trying to give money")
self.info("Cellmates: ", cellmates) self.info("Cellmates: ", cellmates)
self.info("Friends: ", friends) self.info("Friends: ", friends)
@@ -69,6 +70,7 @@ class SocialMoneyAgent(NetworkAgent, MoneyAgent):
other.wealth += 1 other.wealth += 1
self.wealth -= 1 self.wealth -= 1
def graph_generator(n=5): def graph_generator(n=5):
G = nx.Graph() G = nx.Graph()
for ix in range(n): for ix in range(n):
@@ -78,16 +80,22 @@ def graph_generator(n=5):
class MoneyEnv(Environment): class MoneyEnv(Environment):
"""A model with some number of agents.""" """A model with some number of agents."""
def __init__(self, width, height, N, generator=graph_generator,
agent_class=SocialMoneyAgent, def __init__(
topology=None, **kwargs): self,
width,
height,
N,
generator=graph_generator,
agent_class=SocialMoneyAgent,
topology=None,
**kwargs
):
generator = serialization.deserialize(generator) generator = serialization.deserialize(generator)
agent_class = serialization.deserialize(agent_class, globs=globals()) agent_class = serialization.deserialize(agent_class, globs=globals())
topology = generator(n=N) topology = generator(n=N)
super().__init__(topology=topology, super().__init__(topology=topology, N=N, **kwargs)
N=N,
**kwargs)
self.grid = MultiGrid(width, height, False) self.grid = MultiGrid(width, height, False)
self.populate_network(agent_class=agent_class) self.populate_network(agent_class=agent_class)
@@ -99,26 +107,29 @@ class MoneyEnv(Environment):
self.grid.place_agent(agent, (x, y)) self.grid.place_agent(agent, (x, y))
self.datacollector = DataCollector( self.datacollector = DataCollector(
model_reporters={"Gini": compute_gini}, model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"}
agent_reporters={"Wealth": "wealth"}) )
if __name__ == '__main__': if __name__ == "__main__":
fixed_params = {"generator": nx.complete_graph, fixed_params = {
"width": 10, "generator": nx.complete_graph,
"network_agents": [{"agent_class": SocialMoneyAgent, "width": 10,
'weight': 1}], "network_agents": [{"agent_class": SocialMoneyAgent, "weight": 1}],
"height": 10} "height": 10,
}
variable_params = {"N": range(10, 100, 10)} variable_params = {"N": range(10, 100, 10)}
batch_run = BatchRunner(MoneyEnv, batch_run = BatchRunner(
variable_parameters=variable_params, MoneyEnv,
fixed_parameters=fixed_params, variable_parameters=variable_params,
iterations=5, fixed_parameters=fixed_params,
max_steps=100, iterations=5,
model_reporters={"Gini": compute_gini}) max_steps=100,
model_reporters={"Gini": compute_gini},
)
batch_run.run_all() batch_run.run_all()
run_data = batch_run.get_model_vars_dataframe() run_data = batch_run.get_model_vars_dataframe()

View File

@@ -4,24 +4,26 @@ from mesa.time import RandomActivation
from mesa.datacollection import DataCollector from mesa.datacollection import DataCollector
from mesa.batchrunner import BatchRunner from mesa.batchrunner import BatchRunner
def compute_gini(model): def compute_gini(model):
agent_wealths = [agent.wealth for agent in model.schedule.agents] agent_wealths = [agent.wealth for agent in model.schedule.agents]
x = sorted(agent_wealths) x = sorted(agent_wealths)
N = model.num_agents N = model.num_agents
B = sum( xi * (N-i) for i,xi in enumerate(x) ) / (N*sum(x)) B = sum(xi * (N - i) for i, xi in enumerate(x)) / (N * sum(x))
return (1 + (1/N) - 2*B) return 1 + (1 / N) - 2 * B
class MoneyAgent(Agent): class MoneyAgent(Agent):
""" An agent with fixed initial wealth.""" """An agent with fixed initial wealth."""
def __init__(self, unique_id, model): def __init__(self, unique_id, model):
super().__init__(unique_id, model) super().__init__(unique_id, model)
self.wealth = 1 self.wealth = 1
def move(self): def move(self):
possible_steps = self.model.grid.get_neighborhood( possible_steps = self.model.grid.get_neighborhood(
self.pos, self.pos, moore=True, include_center=False
moore=True, )
include_center=False)
new_position = self.random.choice(possible_steps) new_position = self.random.choice(possible_steps)
self.model.grid.move_agent(self, new_position) self.model.grid.move_agent(self, new_position)
@@ -37,8 +39,10 @@ class MoneyAgent(Agent):
if self.wealth > 0: if self.wealth > 0:
self.give_money() self.give_money()
class MoneyModel(Model): class MoneyModel(Model):
"""A model with some number of agents.""" """A model with some number of agents."""
def __init__(self, N, width, height): def __init__(self, N, width, height):
self.num_agents = N self.num_agents = N
self.grid = MultiGrid(width, height, True) self.grid = MultiGrid(width, height, True)
@@ -55,29 +59,29 @@ class MoneyModel(Model):
self.grid.place_agent(a, (x, y)) self.grid.place_agent(a, (x, y))
self.datacollector = DataCollector( self.datacollector = DataCollector(
model_reporters={"Gini": compute_gini}, model_reporters={"Gini": compute_gini}, agent_reporters={"Wealth": "wealth"}
agent_reporters={"Wealth": "wealth"}) )
def step(self): def step(self):
self.datacollector.collect(self) self.datacollector.collect(self)
self.schedule.step() self.schedule.step()
if __name__ == '__main__': if __name__ == "__main__":
fixed_params = {"width": 10, fixed_params = {"width": 10, "height": 10}
"height": 10}
variable_params = {"N": range(10, 500, 10)} variable_params = {"N": range(10, 500, 10)}
batch_run = BatchRunner(MoneyModel, batch_run = BatchRunner(
variable_params, MoneyModel,
fixed_params, variable_params,
iterations=5, fixed_params,
max_steps=100, iterations=5,
model_reporters={"Gini": compute_gini}) max_steps=100,
model_reporters={"Gini": compute_gini},
)
batch_run.run_all() batch_run.run_all()
run_data = batch_run.get_model_vars_dataframe() run_data = batch_run.get_model_vars_dataframe()
run_data.head() run_data.head()
print(run_data.Gini) print(run_data.Gini)

View File

@@ -3,84 +3,85 @@ import logging
class DumbViewer(FSM, NetworkAgent): class DumbViewer(FSM, NetworkAgent):
''' """
A viewer that gets infected via TV (if it has one) and tries to infect A viewer that gets infected via TV (if it has one) and tries to infect
its neighbors once it's infected. its neighbors once it's infected.
''' """
defaults = {
'prob_neighbor_spread': 0.5, prob_neighbor_spread = 0.5
'prob_tv_spread': 0.1, prob_tv_spread = 0.1
} has_been_infected = False
@default_state @default_state
@state @state
def neutral(self): def neutral(self):
if self['has_tv']: if self["has_tv"]:
if self.prob(self.model['prob_tv_spread']): if self.prob(self.model["prob_tv_spread"]):
return self.infected return self.infected
if self.has_been_infected:
return self.infected
@state @state
def infected(self): def infected(self):
for neighbor in self.get_neighboring_agents(state_id=self.neutral.id): for neighbor in self.get_neighbors(state_id=self.neutral.id):
if self.prob(self.model['prob_neighbor_spread']): if self.prob(self.model["prob_neighbor_spread"]):
neighbor.infect() neighbor.infect()
def infect(self): def infect(self):
''' """
This is not a state. It is a function that other agents can use to try to 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 infect this agent. DumbViewer always gets infected, but other agents like
HerdViewer might not become infected right away HerdViewer might not become infected right away
''' """
self.set_state(self.infected) self.has_been_infected = True
class HerdViewer(DumbViewer): class HerdViewer(DumbViewer):
''' """
A viewer whose probability of infection depends on the state of its neighbors. A viewer whose probability of infection depends on the state of its neighbors.
''' """
def infect(self): 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) infected = self.count_neighbors(state_id=self.infected.id)
total = self.count_neighboring_agents() total = self.count_neighbors()
prob_infect = self.model['prob_neighbor_spread'] * infected/total prob_infect = self.model["prob_neighbor_spread"] * infected / total
self.debug('prob_infect', prob_infect) self.debug("prob_infect", prob_infect)
if self.prob(prob_infect): if self.prob(prob_infect):
self.set_state(self.infected) self.has_been_infected = True
class WiseViewer(HerdViewer): class WiseViewer(HerdViewer):
''' """
A viewer that can change its mind. A viewer that can change its mind.
''' """
defaults = { defaults = {
'prob_neighbor_spread': 0.5, "prob_neighbor_spread": 0.5,
'prob_neighbor_cure': 0.25, "prob_neighbor_cure": 0.25,
'prob_tv_spread': 0.1, "prob_tv_spread": 0.1,
} }
@state @state
def cured(self): 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): for neighbor in self.get_neighbors(state_id=self.infected.id):
if self.prob(prob_cure): if self.prob(prob_cure):
try: try:
neighbor.cure() neighbor.cure()
except AttributeError: except AttributeError:
self.debug('Viewer {} cannot be cured'.format(neighbor.id)) self.debug("Viewer {} cannot be cured".format(neighbor.id))
def cure(self): def cure(self):
self.set_state(self.cured.id) self.has_been_cured = True
@state @state
def infected(self): def infected(self):
cured = max(self.count_neighboring_agents(self.cured.id), if self.has_been_cured:
1.0) return self.cured
infected = max(self.count_neighboring_agents(self.infected.id), cured = max(self.count_neighbors(self.cured.id), 1.0)
1.0) infected = max(self.count_neighbors(self.infected.id), 1.0)
prob_cure = self.model['prob_neighbor_cure'] * (cured/infected) prob_cure = self.model["prob_neighbor_cure"] * (cured / infected)
if self.prob(prob_cure): if self.prob(prob_cure):
return self.cured return self.cured
return self.set_state(super().infected)

View File

@@ -1,6 +1,6 @@
''' """
Example of a fully programmatic simulation, without definition files. Example of a fully programmatic simulation, without definition files.
''' """
from soil import Simulation, agents from soil import Simulation, agents
from networkx import Graph from networkx import Graph
import logging import logging
@@ -14,21 +14,22 @@ def mygenerator():
class MyAgent(agents.FSM): class MyAgent(agents.FSM):
@agents.default_state @agents.default_state
@agents.state @agents.state
def neutral(self): def neutral(self):
self.debug('I am running') self.debug("I am running")
if agents.prob(0.2): 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', s = Simulation(
network_params={'generator': mygenerator}, name="Programmatic",
num_trials=1, network_params={"generator": mygenerator},
max_time=100, num_trials=1,
agent_class=MyAgent, max_time=100,
dry_run=True) agent_class=MyAgent,
dry_run=True,
)
# By default, logging will only print WARNING logs (and above). # By default, logging will only print WARNING logs (and above).

View File

@@ -5,7 +5,8 @@ import logging
class CityPubs(Environment): class CityPubs(Environment):
'''Environment with Pubs''' """Environment with Pubs"""
level = logging.INFO level = logging.INFO
def __init__(self, *args, number_of_pubs=3, pub_capacity=10, **kwargs): def __init__(self, *args, number_of_pubs=3, pub_capacity=10, **kwargs):
@@ -13,68 +14,70 @@ class CityPubs(Environment):
pubs = {} pubs = {}
for i in range(number_of_pubs): for i in range(number_of_pubs):
newpub = { newpub = {
'name': 'The awesome pub #{}'.format(i), "name": "The awesome pub #{}".format(i),
'open': True, "open": True,
'capacity': pub_capacity, "capacity": pub_capacity,
'occupancy': 0, "occupancy": 0,
} }
pubs[newpub['name']] = newpub pubs[newpub["name"]] = newpub
self['pubs'] = pubs self["pubs"] = pubs
def enter(self, pub_id, *nodes): 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: try:
pub = self['pubs'][pub_id] pub = self["pubs"][pub_id]
except KeyError: except KeyError:
raise ValueError('Pub {} is not available'.format(pub_id)) raise ValueError("Pub {} is not available".format(pub_id))
if not pub['open'] or (pub['capacity'] < (len(nodes) + pub['occupancy'])): if not pub["open"] or (pub["capacity"] < (len(nodes) + pub["occupancy"])):
return False return False
pub['occupancy'] += len(nodes) pub["occupancy"] += len(nodes)
for node in nodes: for node in nodes:
node['pub'] = pub_id node["pub"] = pub_id
return True return True
def available_pubs(self): def available_pubs(self):
for pub in self['pubs'].values(): for pub in self["pubs"].values():
if pub['open'] and (pub['occupancy'] < pub['capacity']): if pub["open"] and (pub["occupancy"] < pub["capacity"]):
yield pub['name'] yield pub["name"]
def exit(self, pub_id, *node_ids): 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: try:
pub = self['pubs'][pub_id] pub = self["pubs"][pub_id]
except KeyError: 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: for node_id in node_ids:
node = self.get_agent(node_id) node = self.get_agent(node_id)
if pub_id == node['pub']: if pub_id == node["pub"]:
del node['pub'] del node["pub"]
pub['occupancy'] -= 1 pub["occupancy"] -= 1
class Patron(FSM, NetworkAgent): class Patron(FSM, NetworkAgent):
'''Agent that looks for friends to drink with. It will do three things: """Agent that looks for friends to drink with. It will do three things:
1) Look for other patrons to drink with 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. 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. 3) While in the bar, patrons only drink, until they get drunk and taken home.
''' """
level = logging.DEBUG level = logging.DEBUG
pub = None pub = None
drunk = False drunk = False
pints = 0 pints = 0
max_pints = 3 max_pints = 3
kicked_out = False
@default_state @default_state
@state @state
def looking_for_friends(self): def looking_for_friends(self):
'''Look for friends to drink with''' """Look for friends to drink with"""
self.info('I am looking for friends') self.info("I am looking for friends")
available_friends = list(self.get_agents(drunk=False, available_friends = list(
pub=None, self.get_agents(drunk=False, pub=None, state_id=self.looking_for_friends.id)
state_id=self.looking_for_friends.id)) )
if not available_friends: if not available_friends:
self.info('Life sucks and I\'m alone!') self.info("Life sucks and I'm alone!")
return self.at_home return self.at_home
befriended = self.try_friends(available_friends) befriended = self.try_friends(available_friends)
if befriended: if befriended:
@@ -82,91 +85,91 @@ class Patron(FSM, NetworkAgent):
@state @state
def looking_for_pub(self): def looking_for_pub(self):
'''Look for a pub that accepts me and my friends''' """Look for a pub that accepts me and my friends"""
if self['pub'] != None: if self["pub"] != None:
return self.sober_in_pub 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()) group = list(self.get_neighbors())
for pub in self.model.available_pubs(): 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): 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 return self.sober_in_pub
@state @state
def sober_in_pub(self): def sober_in_pub(self):
'''Drink up.''' """Drink up."""
self.drink() self.drink()
if self['pints'] > self['max_pints']: if self["pints"] > self["max_pints"]:
return self.drunk_in_pub return self.drunk_in_pub
@state @state
def drunk_in_pub(self): def drunk_in_pub(self):
'''I'm out. Take me home!''' """I'm out. Take me home!"""
self.info('I\'m so drunk. Take me home!') self.info("I'm so drunk. Take me home!")
self['drunk'] = True self["drunk"] = True
pass # out drunk if self.kicked_out:
return self.at_home
pass # out drun
@state @state
def at_home(self): def at_home(self):
'''The end''' """The end"""
others = self.get_agents(state_id=Patron.at_home.id, limit_neighbors=True) 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): def drink(self):
self['pints'] += 1 self["pints"] += 1
self.debug('Cheers to that') self.debug("Cheers to that")
def kick_out(self): def kick_out(self):
self.set_state(self.at_home) self.kicked_out = True
def befriend(self, other_agent, force=False): def befriend(self, other_agent, force=False):
''' """
Try to become friends with another agent. The chances of Try to become friends with another agent. The chances of
success depend on both agents' openness. 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.add_edge(self, other_agent)
self.info('Made some friend {}'.format(other_agent)) self.info("Made some friend {}".format(other_agent))
return True return True
return False return False
def try_friends(self, others): 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 befriended = False
k = int(10*self['openness']) k = int(10 * self["openness"])
self.random.shuffle(others) self.random.shuffle(others)
for friend in islice(others, k): # random.choice >= 3.7 for friend in islice(others, k): # random.choice >= 3.7
if friend == self: if friend == self:
continue continue
if friend.befriend(self): if friend.befriend(self):
self.befriend(friend, force=True) self.befriend(friend, force=True)
self.debug('Hooray! new friend: {}'.format(friend.id)) self.debug("Hooray! new friend: {}".format(friend.id))
befriended = True befriended = True
else: 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 return befriended
class Police(FSM): class Police(FSM):
'''Simple agent to take drunk people out of pubs.''' """Simple agent to take drunk people out of pubs."""
level = logging.INFO level = logging.INFO
@default_state @default_state
@state @state
def patrol(self): def patrol(self):
drunksters = list(self.get_agents(drunk=True, drunksters = list(self.get_agents(drunk=True, state_id=Patron.drunk_in_pub.id))
state_id=Patron.drunk_in_pub.id))
for drunk in drunksters: 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() drunk.kick_out()
else: 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 from soil import simulation
simulation.run_from_config('pubcrawl.yml',
dry_run=True, simulation.run_from_config("pubcrawl.yml", dry_run=True, dump=None, parallel=False)
dump=None,
parallel=False)

View File

@@ -2,3 +2,13 @@ There are two similar implementations of this simulation.
- `basic`. Using simple primites - `basic`. Using simple primites
- `improved`. Using more advanced features such as the `time` module to avoid unnecessary computations (i.e., skip steps), and generator functions. - `improved`. Using more advanced features such as the `time` module to avoid unnecessary computations (i.e., skip steps), and generator functions.
The examples can be run directly in the terminal, and they accept command like arguments.
For example, to enable the CSV exporter and the Summary exporter, while setting `max_time` to `100` and `seed` to `CustomSeed`:
```
python rabbit_agents.py --set max_time=100 --csv -e summary --set 'seed="CustomSeed"'
```
To learn more about how this functionality works, check out the `soil.easy` function.

View File

@@ -1,13 +1,10 @@
from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment
from soil.time import Delta
from enum import Enum
from collections import Counter from collections import Counter
import logging import logging
import math import math
class RabbitEnv(Environment): class RabbitEnv(Environment):
@property @property
def num_rabbits(self): def num_rabbits(self):
return self.count_agents(agent_class=Rabbit) return self.count_agents(agent_class=Rabbit)
@@ -21,8 +18,7 @@ class RabbitEnv(Environment):
return self.count_agents(agent_class=Female) return self.count_agents(agent_class=Female)
class Rabbit(NetworkAgent, FSM):
class Rabbit(FSM, NetworkAgent):
sexual_maturity = 30 sexual_maturity = 30
life_expectancy = 300 life_expectancy = 300
@@ -30,7 +26,7 @@ class Rabbit(FSM, NetworkAgent):
@default_state @default_state
@state @state
def newborn(self): def newborn(self):
self.info('I am a newborn.') self.info("I am a newborn.")
self.age = 0 self.age = 0
self.offspring = 0 self.offspring = 0
return self.youngling return self.youngling
@@ -39,7 +35,7 @@ class Rabbit(FSM, NetworkAgent):
def youngling(self): def youngling(self):
self.age += 1 self.age += 1
if self.age >= self.sexual_maturity: 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 return self.fertile
@state @state
@@ -63,17 +59,18 @@ class Male(Rabbit):
return self.dead return self.dead
# Males try to mate # Males try to mate
for f in self.model.agents(agent_class=Female, for f in self.model.agents(
state_id=Female.fertile.id, agent_class=Female, state_id=Female.fertile.id, limit=self.max_females
limit=self.max_females): ):
self.debug('FOUND A FEMALE: ', repr(f), self.mating_prob) self.debug("FOUND A FEMALE: ", repr(f), self.mating_prob)
if self.prob(self['mating_prob']): if self.prob(self["mating_prob"]):
f.impregnate(self) f.impregnate(self)
break # Take a break break # Take a break
class Female(Rabbit): class Female(Rabbit):
gestation = 30 gestation = 10
pregnancy = -1
@state @state
def fertile(self): def fertile(self):
@@ -81,70 +78,73 @@ class Female(Rabbit):
self.age += 1 self.age += 1
if self.age > self.life_expectancy: if self.age > self.life_expectancy:
return self.dead return self.dead
if self.pregnancy >= 0:
return self.pregnant
def impregnate(self, male): def impregnate(self, male):
self.info(f'{repr(male)} impregnating female {repr(self)}') self.info(f"impregnated by {repr(male)}")
self.mate = male self.mate = male
self.pregnancy = -1 self.pregnancy = 0
self.set_state(self.pregnant, when=self.now) self.number_of_babies = int(8 + 4 * self.random.random())
self.number_of_babies = int(8+4*self.random.random())
@state @state
def pregnant(self): def pregnant(self):
self.debug('I am pregnant') self.info("I am pregnant")
self.age += 1 self.age += 1
self.pregnancy += 1
if self.prob(self.age / self.life_expectancy): if self.age >= self.life_expectancy:
return self.die() return self.die()
if self.pregnancy >= self.gestation: if self.pregnancy < self.gestation:
self.info('Having {} babies'.format(self.number_of_babies)) self.pregnancy += 1
for i in range(self.number_of_babies): return
state = {}
agent_class = self.random.choice([Male, Female])
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.offspring += 1 self.info("Having {} babies".format(self.number_of_babies))
self.mate = None for i in range(self.number_of_babies):
return self.fertile state = {}
agent_class = self.random.choice([Male, Female])
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")
@state self.offspring += 1
def dead(self): self.mate = None
super().dead() self.pregnancy = -1
if 'pregnancy' in self and self['pregnancy'] > -1: return self.fertile
self.info('A mother has died carrying a baby!!')
def die(self):
if "pregnancy" in self and self["pregnancy"] > -1:
self.info("A mother has died carrying a baby!!")
return super().die()
class RandomAccident(BaseAgent): class RandomAccident(BaseAgent):
level = logging.INFO
def step(self): def step(self):
rabbits_alive = self.model.G.number_of_nodes() rabbits_alive = self.model.G.number_of_nodes()
if not rabbits_alive: if not rabbits_alive:
return self.die() return self.die()
prob_death = self.model.get('prob_death', 1e-100)*math.floor(math.log10(max(1, rabbits_alive))) prob_death = self.model.get("prob_death", 1e-100) * math.floor(
self.debug('Killing some rabbits with prob={}!'.format(prob_death)) 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): for i in self.iter_agents(agent_class=Rabbit):
if i.state_id == i.dead.id: if i.state_id == i.dead.id:
continue continue
if self.prob(prob_death): 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 rabbits_alive -= 1
i.set_state(i.dead) 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 from soil import easy
sim = easy('rabbits.yml')
sim.run() with easy("rabbits.yml") as sim:
sim.run()

View File

@@ -1,130 +1,157 @@
from soil.agents import FSM, state, default_state, BaseAgent, NetworkAgent from soil import FSM, state, default_state, BaseAgent, NetworkAgent, Environment
from soil.time import Delta, When, NEVER from soil.time import Delta
from enum import Enum from enum import Enum
from collections import Counter
import logging import logging
import math import math
class RabbitModel(FSM, NetworkAgent): class RabbitEnv(Environment):
@property
def num_rabbits(self):
return self.count_agents(agent_class=Rabbit)
mating_prob = 0.005 @property
offspring = 0 def num_males(self):
return self.count_agents(agent_class=Male)
@property
def num_females(self):
return self.count_agents(agent_class=Female)
class Rabbit(FSM, NetworkAgent):
sexual_maturity = 30
life_expectancy = 300
birth = None birth = None
sexual_maturity = 3 @property
life_expectancy = 30 def age(self):
if self.birth is None:
return None
return self.now - self.birth
@default_state @default_state
@state @state
def newborn(self): def newborn(self):
self.info("I am a newborn.")
self.birth = self.now self.birth = self.now
self.info(f'I am a newborn.') self.offspring = 0
self.model['rabbits_alive'] = self.model.get('rabbits_alive', 0) + 1 return self.youngling, Delta(self.sexual_maturity - self.age)
# Here we can skip the `youngling` state by using a coroutine/generator. @state
while self.age < self.sexual_maturity: def youngling(self):
interval = self.sexual_maturity - self.age if self.age >= self.sexual_maturity:
yield Delta(interval) self.info(f"I am fertile! My age is {self.age}")
return self.fertile
self.info(f'I am fertile! My age is {self.age}')
return self.fertile
@property
def age(self):
return self.now - self.birth
@state @state
def fertile(self): def fertile(self):
raise Exception("Each subclass should define its fertile state") raise Exception("Each subclass should define its fertile state")
def step(self): @state
super().step() def dead(self):
if self.prob(self.age / self.life_expectancy): self.die()
return self.die()
class Male(RabbitModel): class Male(Rabbit):
max_females = 5 max_females = 5
mating_prob = 0.001
@state @state
def fertile(self): def fertile(self):
# 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))
if self.prob(self['mating_prob']):
f.impregnate(self)
break # Take a break, don't try to impregnate the rest
class Female(RabbitModel):
due_date = None
age_of_pregnancy = None
gestation = 10
mate = None
@state
def fertile(self):
return self.fertile, NEVER
@state
def pregnant(self):
self.info('I am pregnant')
if self.age > self.life_expectancy: if self.age > self.life_expectancy:
return self.dead return self.dead
self.due_date = self.now + self.gestation # 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"]):
f.impregnate(self)
break # Do not try to impregnate other females
number_of_babies = int(8+4*self.random.random())
while self.now < self.due_date: class Female(Rabbit):
yield When(self.due_date) gestation = 10
conception = None
self.info('Having {} babies'.format(number_of_babies))
for i in range(number_of_babies):
agent_class = self.random.choice([Male, Female])
child = self.model.add_node(agent_class=agent_class,
topology=self.topology)
self.model.add_edge(self, child)
self.model.add_edge(self.mate, child)
self.offspring += 1
self.model.agents[self.mate].offspring += 1
self.mate = None
self.due_date = None
return self.fertile
@state @state
def dead(self): def fertile(self):
super().dead() # Just wait for a Male
if self.due_date is not None: if self.age > self.life_expectancy:
self.info('A mother has died carrying a baby!!') return self.dead
if self.conception is not None:
return self.pregnant
@property
def pregnancy(self):
if self.conception is None:
return None
return self.now - self.conception
def impregnate(self, male): def impregnate(self, male):
self.info(f'{repr(male)} impregnating female {repr(self)}') self.info(f"impregnated by {repr(male)}")
self.mate = male self.mate = male
self.set_state(self.pregnant, when=self.now) self.conception = self.now
self.number_of_babies = int(8 + 4 * self.random.random())
@state
def pregnant(self):
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))
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.add_edge(self)
if self.mate:
child.add_edge(self.mate)
self.mate.offspring += 1
else:
self.debug("The father has passed away")
self.offspring += 1
self.mate = None
return self.fertile
def die(self):
if self.pregnancy is not None:
self.info("A mother has died carrying a baby!!")
return super().die()
class RandomAccident(BaseAgent): class RandomAccident(BaseAgent):
level = logging.INFO
def step(self): def step(self):
rabbits_total = self.model.topology.number_of_nodes() rabbits_alive = self.model.G.number_of_nodes()
if 'rabbits_alive' not in self.model:
self.model['rabbits_alive'] = 0 if not rabbits_alive:
rabbits_alive = self.model.get('rabbits_alive', rabbits_total) 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(
for i in self.model.network_agents: math.log10(max(1, rabbits_alive))
if i.state.id == i.dead.id: )
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 continue
if self.prob(prob_death): 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 = self.model['rabbits_alive'] = rabbits_alive -1 rabbits_alive -= 1
i.set_state(i.dead) i.die()
self.debug('Rabbits alive: {}/{}'.format(rabbits_alive, rabbits_total)) self.debug("Rabbits alive: {}".format(rabbits_alive))
if self.model.count_agents(state_id=RabbitModel.dead.id) == self.model.topology.number_of_nodes():
self.die()
if __name__ == "__main__":
from soil import easy
with easy("rabbits.yml") as sim:
sim.run()

View File

@@ -7,11 +7,10 @@ description: null
group: null group: null
interval: 1.0 interval: 1.0
max_time: 100 max_time: 100
model_class: soil.environment.Environment model_class: rabbit_agents.RabbitEnv
model_params: model_params:
agents: agents:
topology: true topology: true
agent_class: rabbit_agents.RabbitModel
distribution: distribution:
- agent_class: rabbit_agents.Male - agent_class: rabbit_agents.Male
weight: 1 weight: 1
@@ -34,5 +33,10 @@ model_params:
nodes: nodes:
- id: 1 - id: 1
- id: 0 - id: 0
model_reporters:
num_males: 'num_males'
num_females: 'num_females'
num_rabbits: |
py:lambda env: env.num_males + env.num_females
extra: extra:
visualization_params: {} visualization_params: {}

View File

@@ -1,42 +1,43 @@
''' """
Example of setting a Example of setting a
Example of a fully programmatic simulation, without definition files. Example of a fully programmatic simulation, without definition files.
''' """
from soil import Simulation, agents from soil import Simulation, agents
from soil.time import Delta from soil.time import Delta
class MyAgent(agents.FSM): class MyAgent(agents.FSM):
''' """
An agent that first does a ping An agent that first does a ping
''' """
defaults = {'pong_counts': 2} defaults = {"pong_counts": 2}
@agents.default_state @agents.default_state
@agents.state @agents.state
def ping(self): def ping(self):
self.info('Ping') self.info("Ping")
return self.pong, Delta(self.random.expovariate(1/16)) return self.pong, Delta(self.random.expovariate(1 / 16))
@agents.state @agents.state
def pong(self): def pong(self):
self.info('Pong') self.info("Pong")
self.pong_counts -= 1 self.pong_counts -= 1
self.info(str(self.pong_counts)) self.info(str(self.pong_counts))
if self.pong_counts < 1: if self.pong_counts < 1:
return self.die() return self.die()
return None, Delta(self.random.expovariate(1/16)) return None, Delta(self.random.expovariate(1 / 16))
s = Simulation(name='Programmatic', s = Simulation(
network_agents=[{'agent_class': MyAgent, 'id': 0}], name="Programmatic",
topology={'nodes': [{'id': 0}], 'links': []}, network_agents=[{"agent_class": MyAgent, "id": 0}],
num_trials=1, topology={"nodes": [{"id": 0}], "links": []},
max_time=100, num_trials=1,
agent_class=MyAgent, max_time=100,
dry_run=True) agent_class=MyAgent,
dry_run=True,
)
envs = s.run() envs = s.run()

View File

@@ -20,56 +20,83 @@ class TerroristSpreadModel(FSM, Geo):
def __init__(self, model=None, unique_id=0, state=()): def __init__(self, model=None, unique_id=0, state=()):
super().__init__(model=model, unique_id=unique_id, state=state) super().__init__(model=model, unique_id=unique_id, state=state)
self.information_spread_intensity = model.environment_params['information_spread_intensity'] self.information_spread_intensity = model.environment_params[
self.terrorist_additional_influence = model.environment_params['terrorist_additional_influence'] "information_spread_intensity"
self.prob_interaction = model.environment_params['prob_interaction'] ]
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 if self["id"] == self.civilian.id: # Civilian
self.mean_belief = self.random.uniform(0.00, 0.5) 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) 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 self.mean_belief = 1.00
else: else:
raise Exception('Invalid state id: {}'.format(self['id'])) 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'] )
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 @state
def civilian(self): def civilian(self):
neighbours = list(self.get_neighboring_agents(agent_class=TerroristSpreadModel)) neighbours = list(self.get_neighbors(agent_class=TerroristSpreadModel))
if len(neighbours) > 0: if len(neighbours) > 0:
# Only interact with some of the neighbors # Only interact with some of the neighbors
interactions = list(n for n in neighbours if self.random.random() <= self.prob_interaction) interactions = list(
influence = sum( self.degree(i) for i in interactions ) n for n in neighbours if self.random.random() <= self.prob_interaction
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 ) influence = sum(self.degree(i) for i in interactions)
self.mean_belief = mean_belief * self.vulnerability + self.mean_belief * ( 1 - self.vulnerability ) 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: if self.mean_belief >= 0.8:
return self.terrorist return self.terrorist
@state @state
def leader(self): def leader(self):
self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence ) 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]): for neighbour in self.get_neighbors(
state_id=[self.terrorist.id, self.leader.id]
):
if self.betweenness(neighbour) > self.betweenness(self): if self.betweenness(neighbour) > self.betweenness(self):
return self.terrorist return self.terrorist
@state @state
def terrorist(self): def terrorist(self):
neighbours = self.get_agents(state_id=[self.terrorist.id, self.leader.id], neighbours = self.get_agents(
agent_class=TerroristSpreadModel, state_id=[self.terrorist.id, self.leader.id],
limit_neighbors=True) agent_class=TerroristSpreadModel,
limit_neighbors=True,
)
if len(neighbours) > 0: if len(neighbours) > 0:
influence = sum( self.degree(n) for n in neighbours ) 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 = sum(
mean_belief = mean_belief * self.vulnerability + self.mean_belief * ( 1 - self.vulnerability ) n.mean_belief * self.degree(n) / influence for n in neighbours
self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence ) )
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 # Check if there are any leaders in the group
leaders = list(filter(lambda x: x.state.id == self.leader.id, neighbours)) leaders = list(filter(lambda x: x.state.id == self.leader.id, neighbours))
@@ -82,21 +109,29 @@ class TerroristSpreadModel(FSM, Geo):
return self.leader return self.leader
def ego_search(self, steps=1, center=False, node=None, **kwargs): 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) node = as_node(node if node is not None else self)
G = self.subgraph(**kwargs) G = self.subgraph(**kwargs)
return nx.ego_graph(G, node, center=center, radius=steps).nodes() return nx.ego_graph(G, node, center=center, radius=steps).nodes()
def degree(self, node, force=False): def degree(self, node, force=False):
node = as_node(node) 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._degree = nx.degree_centrality(self.G)
self.model._last_step = self.now self.model._last_step = self.now
return self.model._degree[node] return self.model._degree[node]
def betweenness(self, node, force=False): def betweenness(self, node, force=False):
node = as_node(node) 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._betweenness = nx.betweenness_centrality(self.G)
self.model._last_step = self.now self.model._last_step = self.now
return self.model._betweenness[node] return self.model._betweenness[node]
@@ -114,17 +149,20 @@ class TrainingAreaModel(FSM, Geo):
def __init__(self, model=None, unique_id=0, state=()): def __init__(self, model=None, unique_id=0, state=()):
super().__init__(model=model, unique_id=unique_id, state=state) super().__init__(model=model, unique_id=unique_id, state=state)
self.training_influence = model.environment_params['training_influence'] self.training_influence = model.environment_params["training_influence"]
if 'min_vulnerability' in model.environment_params: if "min_vulnerability" in model.environment_params:
self.min_vulnerability = model.environment_params['min_vulnerability'] self.min_vulnerability = model.environment_params["min_vulnerability"]
else: self.min_vulnerability = 0 else:
self.min_vulnerability = 0
@default_state @default_state
@state @state
def terrorist(self): def terrorist(self):
for neighbour in self.get_neighboring_agents(agent_class=TerroristSpreadModel): for neighbour in self.get_neighbors(agent_class=TerroristSpreadModel):
if neighbour.vulnerability > self.min_vulnerability: 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): class HavenModel(FSM, Geo):
@@ -141,14 +179,15 @@ class HavenModel(FSM, Geo):
def __init__(self, model=None, unique_id=0, state=()): def __init__(self, model=None, unique_id=0, state=()):
super().__init__(model=model, unique_id=unique_id, state=state) super().__init__(model=model, unique_id=unique_id, state=state)
self.haven_influence = model.environment_params['haven_influence'] self.haven_influence = model.environment_params["haven_influence"]
if 'min_vulnerability' in model.environment_params: if "min_vulnerability" in model.environment_params:
self.min_vulnerability = model.environment_params['min_vulnerability'] self.min_vulnerability = model.environment_params["min_vulnerability"]
else: self.min_vulnerability = 0 else:
self.max_vulnerability = model.environment_params['max_vulnerability'] self.min_vulnerability = 0
self.max_vulnerability = model.environment_params["max_vulnerability"]
def get_occupants(self, **kwargs): def get_occupants(self, **kwargs):
return self.get_neighboring_agents(agent_class=TerroristSpreadModel, **kwargs) return self.get_neighbors(agent_class=TerroristSpreadModel, **kwargs)
@state @state
def civilian(self): def civilian(self):
@@ -158,14 +197,18 @@ class HavenModel(FSM, Geo):
for neighbour in self.get_occupants(): for neighbour in self.get_occupants():
if neighbour.vulnerability > self.min_vulnerability: 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 return self.civilian
@state @state
def terrorist(self): def terrorist(self):
for neighbour in self.get_occupants(): for neighbour in self.get_occupants():
if neighbour.vulnerability < self.max_vulnerability: 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 return self.terrorist
@@ -184,10 +227,10 @@ class TerroristNetworkModel(TerroristSpreadModel):
def __init__(self, model=None, unique_id=0, state=()): def __init__(self, model=None, unique_id=0, state=()):
super().__init__(model=model, unique_id=unique_id, state=state) super().__init__(model=model, unique_id=unique_id, state=state)
self.vision_range = model.environment_params['vision_range'] self.vision_range = model.environment_params["vision_range"]
self.sphere_influence = model.environment_params['sphere_influence'] self.sphere_influence = model.environment_params["sphere_influence"]
self.weight_social_distance = model.environment_params['weight_social_distance'] self.weight_social_distance = model.environment_params["weight_social_distance"]
self.weight_link_distance = model.environment_params['weight_link_distance'] self.weight_link_distance = model.environment_params["weight_link_distance"]
@state @state
def terrorist(self): def terrorist(self):
@@ -200,28 +243,49 @@ class TerroristNetworkModel(TerroristSpreadModel):
return super().leader() return super().leader()
def update_relationships(self): def update_relationships(self):
if self.count_neighboring_agents(state_id=self.civilian.id) == 0: if self.count_neighbors(state_id=self.civilian.id) == 0:
close_ups = set(self.geo_search(radius=self.vision_range, agent_class=TerroristNetworkModel)) close_ups = set(
step_neighbours = set(self.ego_search(self.sphere_influence, agent_class=TerroristNetworkModel, center=False)) self.geo_search(
neighbours = set(agent.id for agent in self.get_neighboring_agents(agent_class=TerroristNetworkModel)) 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_neighbors(
agent_class=TerroristNetworkModel
)
)
search = (close_ups | step_neighbours) - neighbours search = (close_ups | step_neighbours) - neighbours
for agent in self.get_agents(search): for agent in self.get_agents(search):
social_distance = 1 / self.shortest_path_length(agent.id) social_distance = 1 / self.shortest_path_length(agent.id)
spatial_proximity = ( 1 - self.get_distance(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 prob_new_interaction = (
if agent['id'] == agent.civilian.id and self.random.random() < 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) self.add_edge(agent)
break break
def get_distance(self, target): def get_distance(self, target):
source_x, source_y = nx.get_node_attributes(self.G, 'pos')[self.id] 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] target_x, target_y = nx.get_node_attributes(self.G, "pos")[target]
dx = abs( source_x - target_x ) dx = abs(source_x - target_x)
dy = abs( source_y - target_y ) dy = abs(source_y - target_y)
return ( dx ** 2 + dy ** 2 ) ** ( 1 / 2 ) return (dx**2 + dy**2) ** (1 / 2)
def shortest_path_length(self, target): def shortest_path_length(self, target):
try: try:
return nx.shortest_path_length(self.G, self.id, target) return nx.shortest_path_length(self.G, self.id, target)
except nx.NetworkXNoPath: except nx.NetworkXNoPath:
return float('inf') return float("inf")

View File

@@ -1 +1 @@
0.20.7 0.30.0rc2

View File

@@ -5,6 +5,7 @@ import sys
import os import os
import logging import logging
import traceback import traceback
from contextlib import contextmanager
from .version import __version__ from .version import __version__
@@ -16,7 +17,7 @@ except NameError:
from .agents import * from .agents import *
from . import agents from . import agents
from .simulation import * from .simulation import *
from .environment import Environment from .environment import Environment, EventedEnvironment
from . import serialization from . import serialization
from .utils import logger from .utils import logger
from .time import * from .time import *
@@ -30,8 +31,12 @@ def main(
*, *,
do_run=False, do_run=False,
debug=False, debug=False,
pdb=False,
**kwargs, **kwargs,
): ):
if isinstance(cfg, Simulation):
sim = cfg
import argparse import argparse
from . import simulation from . import simulation
@@ -42,7 +47,7 @@ def main(
"file", "file",
type=str, type=str,
nargs="?", nargs="?",
default=cfg, default=cfg if sim is None else '',
help="Configuration file for the simulation (e.g., YAML or JSON)", help="Configuration file for the simulation (e.g., YAML or JSON)",
) )
parser.add_argument( parser.add_argument(
@@ -148,30 +153,41 @@ def main(
if output is None: if output is None:
output = args.output output = args.output
logger.info("Loading config file: {}".format(args.file))
debug = debug or args.debug debug = debug or args.debug
if args.pdb or debug: if args.pdb or debug:
args.synchronous = True args.synchronous = True
os.environ["SOIL_POSTMORTEM"] = "true"
res = [] res = []
try: try:
exp_params = {} exp_params = {}
if not os.path.exists(args.file): if sim:
logger.error("Please, input a valid file") logger.info("Loading simulation instance")
return sim.dry_run = args.dry_run
sim.exporters = exporters
sim.parallel = parallel
sim.outdir = output
sims = [sim, ]
else:
logger.info("Loading config file: {}".format(args.file))
if not os.path.exists(args.file):
logger.error("Please, input a valid file")
return
sims = list(simulation.iter_from_config(
args.file,
dry_run=args.dry_run,
exporters=exporters,
parallel=parallel,
outdir=output,
exporter_params=exp_params,
**kwargs,
))
for sim in sims:
for sim in simulation.iter_from_config(
args.file,
dry_run=args.dry_run,
exporters=exporters,
parallel=parallel,
outdir=output,
exporter_params=exp_params,
**kwargs,
):
if args.set: if args.set:
for s in args.set: for s in args.set:
k, v = s.split("=", 1)[:2] k, v = s.split("=", 1)[:2]
@@ -214,8 +230,17 @@ def main(
return res return res
def easy(cfg, debug=False, **kwargs): @contextmanager
return main(cfg, **kwargs)[0] def easy(cfg, pdb=False, debug=False, **kwargs):
try:
yield main(cfg, debug=debug, pdb=pdb, **kwargs)[0]
except Exception as e:
if os.environ.get("SOIL_POSTMORTEM"):
from .debugging import post_mortem
print(traceback.format_exc())
post_mortem()
raise
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -20,7 +20,7 @@ class BassModel(FSM):
self.sentimentCorrelation = 1 self.sentimentCorrelation = 1
return self.aware return self.aware
else: else:
aware_neighbors = self.get_neighboring_agents(state_id=self.aware.id) aware_neighbors = self.get_neighbors(state_id=self.aware.id)
num_neighbors_aware = len(aware_neighbors) num_neighbors_aware = len(aware_neighbors)
if self.prob((self["imitation_prob"] * num_neighbors_aware)): if self.prob((self["imitation_prob"] * num_neighbors_aware)):
self.sentimentCorrelation = 1 self.sentimentCorrelation = 1

View File

@@ -24,14 +24,14 @@ class BigMarketModel(FSM):
self.type = "" self.type = ""
if self.id < len(self.enterprises): # Enterprises if self.id < len(self.enterprises): # Enterprises
self.set_state(self.enterprise.id) self._set_state(self.enterprise.id)
self.type = "Enterprise" self.type = "Enterprise"
self.tweet_probability = environment.environment_params[ self.tweet_probability = environment.environment_params[
"tweet_probability_enterprises" "tweet_probability_enterprises"
][self.id] ][self.id]
else: # normal users else: # normal users
self.type = "User" self.type = "User"
self.set_state(self.user.id) self._set_state(self.user.id)
self.tweet_probability = environment.environment_params[ self.tweet_probability = environment.environment_params[
"tweet_probability_users" "tweet_probability_users"
] ]
@@ -49,7 +49,7 @@ class BigMarketModel(FSM):
def enterprise(self): def enterprise(self):
if self.random.random() < self.tweet_probability: # Tweets if self.random.random() < self.tweet_probability: # Tweets
aware_neighbors = self.get_neighboring_agents( aware_neighbors = self.get_neighbors(
state_id=self.number_of_enterprises state_id=self.number_of_enterprises
) # Nodes neighbour users ) # Nodes neighbour users
for x in aware_neighbors: for x in aware_neighbors:
@@ -96,7 +96,7 @@ class BigMarketModel(FSM):
] = self.sentiment_about[i] ] = self.sentiment_about[i]
def userTweets(self, sentiment, enterprise): def userTweets(self, sentiment, enterprise):
aware_neighbors = self.get_neighboring_agents( aware_neighbors = self.get_neighbors(
state_id=self.number_of_enterprises state_id=self.number_of_enterprises
) # Nodes neighbours users ) # Nodes neighbours users
for x in aware_neighbors: for x in aware_neighbors:

View File

@@ -14,7 +14,7 @@ class CounterModel(NetworkAgent):
def step(self): def step(self):
# Outside effects # Outside effects
total = len(list(self.model.schedule._agents)) total = len(list(self.model.schedule._agents))
neighbors = len(list(self.get_neighboring_agents())) neighbors = len(list(self.get_neighbors()))
self["times"] = self.get("times", 0) + 1 self["times"] = self.get("times", 0) + 1
self["neighbors"] = neighbors self["neighbors"] = neighbors
self["total"] = total self["total"] = total
@@ -33,7 +33,7 @@ class AggregatedCounter(NetworkAgent):
def step(self): def step(self):
# Outside effects # Outside effects
self["times"] += 1 self["times"] += 1
neighbors = len(list(self.get_neighboring_agents())) neighbors = len(list(self.get_neighbors()))
self["neighbors"] += neighbors self["neighbors"] += neighbors
total = len(list(self.model.schedule.agents)) total = len(list(self.model.schedule.agents))
self["total"] += total self["total"] += total

View File

@@ -36,7 +36,7 @@ class IndependentCascadeModel(BaseAgent):
# Imitation effects # Imitation effects
if self.state["id"] == 0: if self.state["id"] == 0:
aware_neighbors = self.get_neighboring_agents(state_id=1) aware_neighbors = self.get_neighbors(state_id=1)
for x in aware_neighbors: for x in aware_neighbors:
if x.state["time_awareness"] == (self.env.now - 1): if x.state["time_awareness"] == (self.env.now - 1):
aware_neighbors_1_time_step.append(x) aware_neighbors_1_time_step.append(x)

View File

@@ -71,7 +71,7 @@ class SpreadModelM2(BaseAgent):
def neutral_behaviour(self): def neutral_behaviour(self):
# Infected # Infected
infected_neighbors = self.get_neighboring_agents(state_id=1) infected_neighbors = self.get_neighbors(state_id=1)
if len(infected_neighbors) > 0: if len(infected_neighbors) > 0:
if self.prob(self.prob_neutral_making_denier): if self.prob(self.prob_neutral_making_denier):
self.state["id"] = 3 # Vaccinated making denier self.state["id"] = 3 # Vaccinated making denier
@@ -79,7 +79,7 @@ class SpreadModelM2(BaseAgent):
def infected_behaviour(self): def infected_behaviour(self):
# Neutral # Neutral
neutral_neighbors = self.get_neighboring_agents(state_id=0) neutral_neighbors = self.get_neighbors(state_id=0)
for neighbor in neutral_neighbors: for neighbor in neutral_neighbors:
if self.prob(self.prob_infect): if self.prob(self.prob_infect):
neighbor.state["id"] = 1 # Infected neighbor.state["id"] = 1 # Infected
@@ -87,13 +87,13 @@ class SpreadModelM2(BaseAgent):
def cured_behaviour(self): def cured_behaviour(self):
# Vaccinate # Vaccinate
neutral_neighbors = self.get_neighboring_agents(state_id=0) neutral_neighbors = self.get_neighbors(state_id=0)
for neighbor in neutral_neighbors: for neighbor in neutral_neighbors:
if self.prob(self.prob_cured_vaccinate_neutral): if self.prob(self.prob_cured_vaccinate_neutral):
neighbor.state["id"] = 3 # Vaccinated neighbor.state["id"] = 3 # Vaccinated
# Cure # Cure
infected_neighbors = self.get_neighboring_agents(state_id=1) infected_neighbors = self.get_neighbors(state_id=1)
for neighbor in infected_neighbors: for neighbor in infected_neighbors:
if self.prob(self.prob_cured_healing_infected): if self.prob(self.prob_cured_healing_infected):
neighbor.state["id"] = 2 # Cured neighbor.state["id"] = 2 # Cured
@@ -101,19 +101,19 @@ class SpreadModelM2(BaseAgent):
def vaccinated_behaviour(self): def vaccinated_behaviour(self):
# Cure # Cure
infected_neighbors = self.get_neighboring_agents(state_id=1) infected_neighbors = self.get_neighbors(state_id=1)
for neighbor in infected_neighbors: for neighbor in infected_neighbors:
if self.prob(self.prob_cured_healing_infected): if self.prob(self.prob_cured_healing_infected):
neighbor.state["id"] = 2 # Cured neighbor.state["id"] = 2 # Cured
# Vaccinate # Vaccinate
neutral_neighbors = self.get_neighboring_agents(state_id=0) neutral_neighbors = self.get_neighbors(state_id=0)
for neighbor in neutral_neighbors: for neighbor in neutral_neighbors:
if self.prob(self.prob_cured_vaccinate_neutral): if self.prob(self.prob_cured_vaccinate_neutral):
neighbor.state["id"] = 3 # Vaccinated neighbor.state["id"] = 3 # Vaccinated
# Generate anti-rumor # Generate anti-rumor
infected_neighbors_2 = self.get_neighboring_agents(state_id=1) infected_neighbors_2 = self.get_neighbors(state_id=1)
for neighbor in infected_neighbors_2: for neighbor in infected_neighbors_2:
if self.prob(self.prob_generate_anti_rumor): if self.prob(self.prob_generate_anti_rumor):
neighbor.state["id"] = 2 # Cured neighbor.state["id"] = 2 # Cured
@@ -191,7 +191,7 @@ class ControlModelM2(BaseAgent):
self.state["visible"] = False self.state["visible"] = False
# Infected # Infected
infected_neighbors = self.get_neighboring_agents(state_id=1) infected_neighbors = self.get_neighbors(state_id=1)
if len(infected_neighbors) > 0: if len(infected_neighbors) > 0:
if self.random(self.prob_neutral_making_denier): if self.random(self.prob_neutral_making_denier):
self.state["id"] = 3 # Vaccinated making denier self.state["id"] = 3 # Vaccinated making denier
@@ -199,7 +199,7 @@ class ControlModelM2(BaseAgent):
def infected_behaviour(self): def infected_behaviour(self):
# Neutral # Neutral
neutral_neighbors = self.get_neighboring_agents(state_id=0) neutral_neighbors = self.get_neighbors(state_id=0)
for neighbor in neutral_neighbors: for neighbor in neutral_neighbors:
if self.prob(self.prob_infect): if self.prob(self.prob_infect):
neighbor.state["id"] = 1 # Infected neighbor.state["id"] = 1 # Infected
@@ -209,13 +209,13 @@ class ControlModelM2(BaseAgent):
self.state["visible"] = True self.state["visible"] = True
# Vaccinate # Vaccinate
neutral_neighbors = self.get_neighboring_agents(state_id=0) neutral_neighbors = self.get_neighbors(state_id=0)
for neighbor in neutral_neighbors: for neighbor in neutral_neighbors:
if self.prob(self.prob_cured_vaccinate_neutral): if self.prob(self.prob_cured_vaccinate_neutral):
neighbor.state["id"] = 3 # Vaccinated neighbor.state["id"] = 3 # Vaccinated
# Cure # Cure
infected_neighbors = self.get_neighboring_agents(state_id=1) infected_neighbors = self.get_neighbors(state_id=1)
for neighbor in infected_neighbors: for neighbor in infected_neighbors:
if self.prob(self.prob_cured_healing_infected): if self.prob(self.prob_cured_healing_infected):
neighbor.state["id"] = 2 # Cured neighbor.state["id"] = 2 # Cured
@@ -224,47 +224,47 @@ class ControlModelM2(BaseAgent):
self.state["visible"] = True self.state["visible"] = True
# Cure # Cure
infected_neighbors = self.get_neighboring_agents(state_id=1) infected_neighbors = self.get_neighbors(state_id=1)
for neighbor in infected_neighbors: for neighbor in infected_neighbors:
if self.prob(self.prob_cured_healing_infected): if self.prob(self.prob_cured_healing_infected):
neighbor.state["id"] = 2 # Cured neighbor.state["id"] = 2 # Cured
# Vaccinate # Vaccinate
neutral_neighbors = self.get_neighboring_agents(state_id=0) neutral_neighbors = self.get_neighbors(state_id=0)
for neighbor in neutral_neighbors: for neighbor in neutral_neighbors:
if self.prob(self.prob_cured_vaccinate_neutral): if self.prob(self.prob_cured_vaccinate_neutral):
neighbor.state["id"] = 3 # Vaccinated neighbor.state["id"] = 3 # Vaccinated
# Generate anti-rumor # Generate anti-rumor
infected_neighbors_2 = self.get_neighboring_agents(state_id=1) infected_neighbors_2 = self.get_neighbors(state_id=1)
for neighbor in infected_neighbors_2: for neighbor in infected_neighbors_2:
if self.prob(self.prob_generate_anti_rumor): if self.prob(self.prob_generate_anti_rumor):
neighbor.state["id"] = 2 # Cured neighbor.state["id"] = 2 # Cured
def beacon_off_behaviour(self): def beacon_off_behaviour(self):
self.state["visible"] = False self.state["visible"] = False
infected_neighbors = self.get_neighboring_agents(state_id=1) infected_neighbors = self.get_neighbors(state_id=1)
if len(infected_neighbors) > 0: if len(infected_neighbors) > 0:
self.state["id"] == 5 # Beacon on self.state["id"] == 5 # Beacon on
def beacon_on_behaviour(self): def beacon_on_behaviour(self):
self.state["visible"] = False self.state["visible"] = False
# Cure (M2 feature added) # Cure (M2 feature added)
infected_neighbors = self.get_neighboring_agents(state_id=1) infected_neighbors = self.get_neighbors(state_id=1)
for neighbor in infected_neighbors: for neighbor in infected_neighbors:
if self.prob(self.prob_generate_anti_rumor): if self.prob(self.prob_generate_anti_rumor):
neighbor.state["id"] = 2 # Cured neighbor.state["id"] = 2 # Cured
neutral_neighbors_infected = neighbor.get_neighboring_agents(state_id=0) neutral_neighbors_infected = neighbor.get_neighbors(state_id=0)
for neighbor in neutral_neighbors_infected: for neighbor in neutral_neighbors_infected:
if self.prob(self.prob_generate_anti_rumor): if self.prob(self.prob_generate_anti_rumor):
neighbor.state["id"] = 3 # Vaccinated neighbor.state["id"] = 3 # Vaccinated
infected_neighbors_infected = neighbor.get_neighboring_agents(state_id=1) infected_neighbors_infected = neighbor.get_neighbors(state_id=1)
for neighbor in infected_neighbors_infected: for neighbor in infected_neighbors_infected:
if self.prob(self.prob_generate_anti_rumor): if self.prob(self.prob_generate_anti_rumor):
neighbor.state["id"] = 2 # Cured neighbor.state["id"] = 2 # Cured
# Vaccinate # Vaccinate
neutral_neighbors = self.get_neighboring_agents(state_id=0) neutral_neighbors = self.get_neighbors(state_id=0)
for neighbor in neutral_neighbors: for neighbor in neutral_neighbors:
if self.prob(self.prob_cured_vaccinate_neutral): if self.prob(self.prob_cured_vaccinate_neutral):
neighbor.state["id"] = 3 # Vaccinated neighbor.state["id"] = 3 # Vaccinated

View File

@@ -69,10 +69,10 @@ class SISaModel(FSM):
return self.content return self.content
# Infected # Infected
discontent_neighbors = self.count_neighboring_agents(state_id=self.discontent) discontent_neighbors = self.count_neighbors(state_id=self.discontent)
if self.prob(scontent_neighbors * self.neutral_discontent_infected_prob): if self.prob(scontent_neighbors * self.neutral_discontent_infected_prob):
return self.discontent return self.discontent
content_neighbors = self.count_neighboring_agents(state_id=self.content.id) content_neighbors = self.count_neighbors(state_id=self.content.id)
if self.prob(s * self.neutral_content_infected_prob): if self.prob(s * self.neutral_content_infected_prob):
return self.content return self.content
return self.neutral return self.neutral
@@ -84,7 +84,7 @@ class SISaModel(FSM):
return self.neutral return self.neutral
# Superinfected # Superinfected
content_neighbors = self.count_neighboring_agents(state_id=self.content.id) content_neighbors = self.count_neighbors(state_id=self.content.id)
if self.prob(s * self.discontent_content): if self.prob(s * self.discontent_content):
return self.content return self.content
return self.discontent return self.discontent
@@ -96,9 +96,7 @@ class SISaModel(FSM):
return self.neutral return self.neutral
# Superinfected # Superinfected
discontent_neighbors = self.count_neighboring_agents( discontent_neighbors = self.count_neighbors(state_id=self.discontent.id)
state_id=self.discontent.id
)
if self.prob(scontent_neighbors * self.content_discontent): if self.prob(scontent_neighbors * self.content_discontent):
self.discontent self.discontent
return self.content return self.content

View File

@@ -41,25 +41,25 @@ class SentimentCorrelationModel(BaseAgent):
sad_neighbors_1_time_step = [] sad_neighbors_1_time_step = []
disgusted_neighbors_1_time_step = [] disgusted_neighbors_1_time_step = []
angry_neighbors = self.get_neighboring_agents(state_id=1) angry_neighbors = self.get_neighbors(state_id=1)
for x in angry_neighbors: for x in angry_neighbors:
if x.state["time_awareness"][0] > (self.env.now - 500): if x.state["time_awareness"][0] > (self.env.now - 500):
angry_neighbors_1_time_step.append(x) angry_neighbors_1_time_step.append(x)
num_neighbors_angry = len(angry_neighbors_1_time_step) num_neighbors_angry = len(angry_neighbors_1_time_step)
joyful_neighbors = self.get_neighboring_agents(state_id=2) joyful_neighbors = self.get_neighbors(state_id=2)
for x in joyful_neighbors: for x in joyful_neighbors:
if x.state["time_awareness"][1] > (self.env.now - 500): if x.state["time_awareness"][1] > (self.env.now - 500):
joyful_neighbors_1_time_step.append(x) joyful_neighbors_1_time_step.append(x)
num_neighbors_joyful = len(joyful_neighbors_1_time_step) num_neighbors_joyful = len(joyful_neighbors_1_time_step)
sad_neighbors = self.get_neighboring_agents(state_id=3) sad_neighbors = self.get_neighbors(state_id=3)
for x in sad_neighbors: for x in sad_neighbors:
if x.state["time_awareness"][2] > (self.env.now - 500): if x.state["time_awareness"][2] > (self.env.now - 500):
sad_neighbors_1_time_step.append(x) sad_neighbors_1_time_step.append(x)
num_neighbors_sad = len(sad_neighbors_1_time_step) num_neighbors_sad = len(sad_neighbors_1_time_step)
disgusted_neighbors = self.get_neighboring_agents(state_id=4) disgusted_neighbors = self.get_neighbors(state_id=4)
for x in disgusted_neighbors: for x in disgusted_neighbors:
if x.state["time_awareness"][3] > (self.env.now - 500): if x.state["time_awareness"][3] > (self.env.now - 500):
disgusted_neighbors_1_time_step.append(x) disgusted_neighbors_1_time_step.append(x)

View File

@@ -29,10 +29,6 @@ def as_node(agent):
IGNORED_FIELDS = ("model", "logger") IGNORED_FIELDS = ("model", "logger")
class DeadAgent(Exception):
pass
class MetaAgent(ABCMeta): class MetaAgent(ABCMeta):
def __new__(mcls, name, bases, namespace): def __new__(mcls, name, bases, namespace):
defaults = {} defaults = {}
@@ -44,10 +40,36 @@ class MetaAgent(ABCMeta):
new_nmspc = { new_nmspc = {
"_defaults": defaults, "_defaults": defaults,
"_last_return": None,
"_last_except": None,
} }
for attr, func in namespace.items(): for attr, func in namespace.items():
if ( if attr == "step" and inspect.isgeneratorfunction(func):
orig_func = func
new_nmspc["_coroutine"] = None
@wraps(func)
def func(self):
while True:
if not self._coroutine:
self._coroutine = orig_func(self)
try:
if self._last_except:
return self._coroutine.throw(self._last_except)
else:
return self._coroutine.send(self._last_return)
except StopIteration as ex:
self._coroutine = None
return ex.value
finally:
self._last_return = None
self._last_except = None
func.id = name or func.__name__
func.is_default = False
new_nmspc[attr] = func
elif (
isinstance(func, types.FunctionType) isinstance(func, types.FunctionType)
or isinstance(func, property) or isinstance(func, property)
or isinstance(func, classmethod) or isinstance(func, classmethod)
@@ -176,11 +198,15 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
def die(self): def die(self):
self.info(f"agent dying") self.info(f"agent dying")
self.alive = False self.alive = False
try:
self.model.schedule.remove(self)
except KeyError:
pass
return time.NEVER return time.NEVER
def step(self): def step(self):
if not self.alive: if not self.alive:
raise DeadAgent(self.unique_id) raise time.DeadAgent(self.unique_id)
return super().step() or time.Delta(self.interval) return super().step() or time.Delta(self.interval)
def log(self, message, *args, level=logging.INFO, **kwargs): def log(self, message, *args, level=logging.INFO, **kwargs):
@@ -229,215 +255,6 @@ class BaseAgent(MesaAgent, MutableMapping, metaclass=MetaAgent):
return f"{self.__class__.__name__}({self.unique_id})" return f"{self.__class__.__name__}({self.unique_id})"
class NetworkAgent(BaseAgent):
def __init__(self, *args, topology, node_id, **kwargs):
super().__init__(*args, **kwargs)
assert topology is not None
assert node_id is not None
self.G = topology
assert self.G
self.node_id = node_id
def count_neighboring_agents(self, state_id=None, **kwargs):
return len(self.get_neighboring_agents(state_id=state_id, **kwargs))
def get_neighboring_agents(self, **kwargs):
return list(self.iter_agents(limit_neighbors=True, **kwargs))
def add_edge(self, other):
self.topology.add_edge(self.node_id, other.node_id)
@property
def node(self):
return self.topology.nodes[self.node_id]
def iter_agents(self, unique_id=None, *, limit_neighbors=False, **kwargs):
unique_ids = None
if isinstance(unique_id, list):
unique_ids = set(unique_id)
elif unique_id is not None:
unique_ids = set(
[
unique_id,
]
)
if limit_neighbors:
neighbor_ids = set()
for node_id in self.G.neighbors(self.node_id):
if self.G.nodes[node_id].get("agent") is not None:
neighbor_ids.add(node_id)
if unique_ids:
unique_ids = unique_ids & neighbor_ids
else:
unique_ids = neighbor_ids
if not unique_ids:
return
unique_ids = list(unique_ids)
yield from super().iter_agents(unique_id=unique_ids, **kwargs)
def subgraph(self, center=True, **kwargs):
include = [self] if center else []
G = self.G.subgraph(
n.node_id for n in list(self.get_agents(**kwargs) + include)
)
return G
def remove_node(self):
self.G.remove_node(self.node_id)
def add_edge(self, other, edge_attr_dict=None, *edge_attrs):
if self.node_id not in self.G.nodes(data=False):
raise ValueError(
"{} not in list of existing agents in the network".format(
self.unique_id
)
)
if other.node_id not in self.G.nodes(data=False):
raise ValueError(
"{} not in list of existing agents in the network".format(other)
)
self.G.add_edge(
self.node_id, other.node_id, edge_attr_dict=edge_attr_dict, *edge_attrs
)
def die(self, remove=True):
if remove:
self.remove_node()
return super().die()
def state(name=None):
def decorator(func, name=None):
"""
A state function should return either a state id, or a tuple (state_id, when)
The default value for state_id is the current state id.
The default value for when is the interval defined in the environment.
"""
if inspect.isgeneratorfunction(func):
orig_func = func
@wraps(func)
def func(self):
while True:
if not self._coroutine:
self._coroutine = orig_func(self)
try:
n = next(self._coroutine)
if n:
return None, n
return
except StopIteration as ex:
self._coroutine = None
next_state = ex.value
if next_state is not None:
self.set_state(next_state)
return next_state
func.id = name or func.__name__
func.is_default = False
return func
if callable(name):
return decorator(name)
else:
return partial(decorator, name=name)
def default_state(func):
func.is_default = True
return func
class MetaFSM(MetaAgent):
def __new__(mcls, name, bases, namespace):
states = {}
# Re-use states from inherited classes
default_state = None
for i in bases:
if isinstance(i, MetaFSM):
for state_id, state in i._states.items():
if state.is_default:
default_state = state
states[state_id] = state
# Add new states
for attr, func in namespace.items():
if hasattr(func, "id"):
if func.is_default:
default_state = func
states[func.id] = func
namespace.update(
{
"_default_state": default_state,
"_states": states,
}
)
return super(MetaFSM, mcls).__new__(
mcls=mcls, name=name, bases=bases, namespace=namespace
)
class FSM(BaseAgent, metaclass=MetaFSM):
def __init__(self, *args, **kwargs):
super(FSM, self).__init__(*args, **kwargs)
if not hasattr(self, "state_id"):
if not self._default_state:
raise ValueError(
"No default state specified for {}".format(self.unique_id)
)
self.state_id = self._default_state.id
self._coroutine = None
self.set_state(self.state_id)
def step(self):
self.debug(f"Agent {self.unique_id} @ state {self.state_id}")
default_interval = super().step()
next_state = self._states[self.state_id](self)
when = None
try:
next_state, *when = next_state
if not when:
when = None
elif len(when) == 1:
when = when[0]
else:
raise ValueError(
"Too many values returned. Only state (and time) allowed"
)
except TypeError:
pass
if next_state is not None:
self.set_state(next_state)
return when or default_interval
def set_state(self, state, when=None):
if hasattr(state, "id"):
state = state.id
if state not in self._states:
raise ValueError("{} is not a valid state".format(state))
self.state_id = state
if when is not None:
self.model.schedule.add(self, when=when)
return state
def die(self):
return self.dead, super().die()
@state
def dead(self):
return self.die()
def prob(prob, random): def prob(prob, random):
""" """
A true/False uniform distribution with a given probability. A true/False uniform distribution with a given probability.
@@ -503,7 +320,7 @@ def calculate_distribution(network_agents=None, agent_class=None):
return network_agents return network_agents
def serialize_type(agent_class, known_modules=[], **kwargs): def _serialize_type(agent_class, known_modules=[], **kwargs):
if isinstance(agent_class, str): if isinstance(agent_class, str):
return agent_class return agent_class
known_modules += ["soil.agents"] known_modules += ["soil.agents"]
@@ -512,20 +329,7 @@ def serialize_type(agent_class, known_modules=[], **kwargs):
] # Get the name of the class ] # Get the name of the class
def serialize_definition(network_agents, known_modules=[]): def _deserialize_type(agent_class, known_modules=[]):
"""
When serializing an agent distribution, remove the thresholds, in order
to avoid cluttering the YAML definition file.
"""
d = deepcopy(list(network_agents))
for v in d:
if "threshold" in v:
del v["threshold"]
v["agent_class"] = serialize_type(v["agent_class"], known_modules=known_modules)
return d
def deserialize_type(agent_class, known_modules=[]):
if not isinstance(agent_class, str): if not isinstance(agent_class, str):
return agent_class return agent_class
known = known_modules + ["soil.agents", "soil.agents.custom"] known = known_modules + ["soil.agents", "soil.agents.custom"]
@@ -533,108 +337,6 @@ def deserialize_type(agent_class, known_modules=[]):
return agent_class return agent_class
def deserialize_definition(ind, **kwargs):
d = deepcopy(ind)
for v in d:
v["agent_class"] = deserialize_type(v["agent_class"], **kwargs)
return d
def _validate_states(states, topology):
"""Validate states to avoid ignoring states during initialization"""
states = states or []
if isinstance(states, dict):
for x in states:
assert x in topology.nodes
else:
assert len(states) <= len(topology)
return states
def _convert_agent_classs(ind, to_string=False, **kwargs):
"""Convenience method to allow specifying agents by class or class name."""
if to_string:
return serialize_definition(ind, **kwargs)
return deserialize_definition(ind, **kwargs)
# def _agent_from_definition(definition, random, value=-1, unique_id=None):
# """Used in the initialization of agents given an agent distribution."""
# if value < 0:
# value = random.random()
# for d in sorted(definition, key=lambda x: x.get('threshold')):
# threshold = d.get('threshold', (-1, -1))
# # Check if the definition matches by id (first) or by threshold
# if (unique_id is not None and unique_id in d.get('ids', [])) or \
# (value >= threshold[0] and value < threshold[1]):
# state = {}
# if 'state' in d:
# state = deepcopy(d['state'])
# return d['agent_class'], state
# raise Exception('Definition for value {} not found in: {}'.format(value, definition))
# def _definition_to_dict(definition, random, size=None, default_state=None):
# state = default_state or {}
# agents = {}
# remaining = {}
# if size:
# for ix in range(size):
# remaining[ix] = copy(state)
# else:
# remaining = defaultdict(lambda x: copy(state))
# distro = sorted([item for item in definition if 'weight' in item])
# id = 0
# def init_agent(item, id=ix):
# while id in agents:
# id += 1
# agent = remaining[id]
# agent['state'].update(copy(item.get('state', {})))
# agents[agent.unique_id] = agent
# del remaining[id]
# return agent
# for item in definition:
# if 'ids' in item:
# ids = item['ids']
# del item['ids']
# for id in ids:
# agent = init_agent(item, id)
# for item in definition:
# if 'number' in item:
# times = item['number']
# del item['number']
# for times in range(times):
# if size:
# ix = random.choice(remaining.keys())
# agent = init_agent(item, id)
# else:
# agent = init_agent(item)
# if not size:
# return agents
# if len(remaining) < 0:
# raise Exception('Invalid definition. Too many agents to add')
# total_weight = float(sum(s['weight'] for s in distro))
# unit = size / total_weight
# for item in distro:
# times = unit * item['weight']
# del item['weight']
# for times in range(times):
# ix = random.choice(remaining.keys())
# agent = init_agent(item, id)
# return agents
class AgentView(Mapping, Set): class AgentView(Mapping, Set):
"""A lazy-loaded list of agents.""" """A lazy-loaded list of agents."""
@@ -718,7 +420,7 @@ def filter_agents(
state_id = tuple([state_id]) state_id = tuple([state_id])
if agent_class is not None: if agent_class is not None:
agent_class = deserialize_type(agent_class) agent_class = _deserialize_type(agent_class)
try: try:
agent_class = tuple(agent_class) agent_class = tuple(agent_class)
except TypeError: except TypeError:
@@ -758,14 +460,6 @@ def from_config(
default = cfg or config.AgentConfig() default = cfg or config.AgentConfig()
if not isinstance(cfg, config.AgentConfig): if not isinstance(cfg, config.AgentConfig):
cfg = config.AgentConfig(**cfg) cfg = config.AgentConfig(**cfg)
return _agents_from_config(cfg, topology=topology, random=random)
def _agents_from_config(
cfg: config.AgentConfig, topology: nx.Graph, random
) -> List[Dict[str, Any]]:
if cfg and not isinstance(cfg, config.AgentConfig):
cfg = config.AgentConfig(**cfg)
agents = [] agents = []
@@ -933,6 +627,9 @@ def _from_distro(
return agents return agents
from .network_agents import *
from .fsm import *
from .evented import *
from .BassModel import * from .BassModel import *
from .BigMarketModel import * from .BigMarketModel import *
from .IndependentCascadeModel import * from .IndependentCascadeModel import *

57
soil/agents/evented.py Normal file
View File

@@ -0,0 +1,57 @@
from . import BaseAgent
from ..events import Message, Tell, Ask, Reply, TimedOut
from ..time import Cond
from functools import partial
from collections import deque
class Evented(BaseAgent):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._inbox = deque()
self._received = 0
self._processed = 0
def on_receive(self, *args, **kwargs):
pass
def received(self, expiration=None, timeout=None):
current = self._received
if expiration is None:
expiration = float('inf') if timeout is None else self.now + timeout
if expiration < self.now:
raise ValueError("Invalid expiration time")
def ready(agent):
return agent._received > current or agent.now >= expiration
def value(agent):
if agent.now > expiration:
raise TimedOut("No message received")
c = Cond(func=ready, return_func=value)
c._checked = True
return c
def tell(self, msg, sender):
self._received += 1
self._inbox.append(Tell(payload=msg, sender=sender))
def ask(self, msg, timeout=None):
self._received += 1
ask = Ask(payload=msg)
self._inbox.append(ask)
expiration = float('inf') if timeout is None else self.now + timeout
return ask.replied(expiration=expiration)
def check_messages(self):
while self._inbox:
msg = self._inbox.popleft()
self._processed += 1
if msg.expired(self.now):
continue
reply = self.on_receive(msg.payload, sender=msg.sender)
if isinstance(msg, Ask):
msg.reply = reply

142
soil/agents/fsm.py Normal file
View File

@@ -0,0 +1,142 @@
from . import MetaAgent, BaseAgent
from functools import partial, wraps
import inspect
def state(name=None):
def decorator(func, name=None):
"""
A state function should return either a state id, or a tuple (state_id, when)
The default value for state_id is the current state id.
The default value for when is the interval defined in the environment.
"""
if inspect.isgeneratorfunction(func):
orig_func = func
@wraps(func)
def func(self):
while True:
if not self._coroutine:
self._coroutine = orig_func(self)
try:
if self._last_except:
n = self._coroutine.throw(self._last_except)
else:
n = self._coroutine.send(self._last_return)
if n:
return None, n
return n
except StopIteration as ex:
self._coroutine = None
next_state = ex.value
if next_state is not None:
self._set_state(next_state)
return next_state
finally:
self._last_return = None
self._last_except = None
func.id = name or func.__name__
func.is_default = False
return func
if callable(name):
return decorator(name)
else:
return partial(decorator, name=name)
def default_state(func):
func.is_default = True
return func
class MetaFSM(MetaAgent):
def __new__(mcls, name, bases, namespace):
states = {}
# Re-use states from inherited classes
default_state = None
for i in bases:
if isinstance(i, MetaFSM):
for state_id, state in i._states.items():
if state.is_default:
default_state = state
states[state_id] = state
# Add new states
for attr, func in namespace.items():
if hasattr(func, "id"):
if func.is_default:
default_state = func
states[func.id] = func
namespace.update(
{
"_default_state": default_state,
"_states": states,
}
)
return super(MetaFSM, mcls).__new__(
mcls=mcls, name=name, bases=bases, namespace=namespace
)
class FSM(BaseAgent, metaclass=MetaFSM):
def __init__(self, **kwargs):
super(FSM, self).__init__(**kwargs)
if not hasattr(self, "state_id"):
if not self._default_state:
raise ValueError(
"No default state specified for {}".format(self.unique_id)
)
self.state_id = self._default_state.id
self._coroutine = None
self._set_state(self.state_id)
def step(self):
self.debug(f"Agent {self.unique_id} @ state {self.state_id}")
default_interval = super().step()
next_state = self._states[self.state_id](self)
when = None
try:
next_state, *when = next_state
if not when:
when = None
elif len(when) == 1:
when = when[0]
else:
raise ValueError(
"Too many values returned. Only state (and time) allowed"
)
except TypeError:
pass
if next_state is not None:
self._set_state(next_state)
return when or default_interval
def _set_state(self, state, when=None):
if hasattr(state, "id"):
state = state.id
if state not in self._states:
raise ValueError("{} is not a valid state".format(state))
self.state_id = state
if when is not None:
self.model.schedule.add(self, when=when)
return state
def die(self):
return self.dead, super().die()
@state
def dead(self):
return self.die()

View File

@@ -0,0 +1,82 @@
from . import BaseAgent
class NetworkAgent(BaseAgent):
def __init__(self, *args, topology, node_id, **kwargs):
super().__init__(*args, **kwargs)
assert topology is not None
assert node_id is not None
self.G = topology
assert self.G
self.node_id = node_id
def count_neighbors(self, state_id=None, **kwargs):
return len(self.get_neighbors(state_id=state_id, **kwargs))
def get_neighbors(self, **kwargs):
return list(self.iter_agents(limit_neighbors=True, **kwargs))
@property
def node(self):
return self.G.nodes[self.node_id]
def iter_agents(self, unique_id=None, *, limit_neighbors=False, **kwargs):
unique_ids = None
if isinstance(unique_id, list):
unique_ids = set(unique_id)
elif unique_id is not None:
unique_ids = set(
[
unique_id,
]
)
if limit_neighbors:
neighbor_ids = set()
for node_id in self.G.neighbors(self.node_id):
if self.G.nodes[node_id].get("agent") is not None:
neighbor_ids.add(node_id)
if unique_ids:
unique_ids = unique_ids & neighbor_ids
else:
unique_ids = neighbor_ids
if not unique_ids:
return
unique_ids = list(unique_ids)
yield from super().iter_agents(unique_id=unique_ids, **kwargs)
def subgraph(self, center=True, **kwargs):
include = [self] if center else []
G = self.G.subgraph(
n.node_id for n in list(self.get_agents(**kwargs) + include)
)
return G
def remove_node(self):
print(f"Removing node for {self.unique_id}: {self.node_id}")
self.G.remove_node(self.node_id)
self.node_id = None
def add_edge(self, other, edge_attr_dict=None, *edge_attrs):
if self.node_id not in self.G.nodes(data=False):
raise ValueError(
"{} not in list of existing agents in the network".format(
self.unique_id
)
)
if other.node_id not in self.G.nodes(data=False):
raise ValueError(
"{} not in list of existing agents in the network".format(other)
)
self.G.add_edge(
self.node_id, other.node_id, edge_attr_dict=edge_attr_dict, *edge_attrs
)
def die(self, remove=True):
if not self.alive:
return None
if remove:
self.remove_node()
return super().die()

View File

@@ -30,9 +30,9 @@ def wrapcmd(func):
class Debug(pdb.Pdb): class Debug(pdb.Pdb):
def __init__(self, *args, skip_soil=False, **kwargs): def __init__(self, *args, skip_soil=False, **kwargs):
skip = kwargs.get("skip", []) skip = kwargs.get("skip", [])
skip.append("soil")
if skip_soil: if skip_soil:
skip.append("soil") skip.append("soil")
skip.append("contextlib")
skip.append("soil.*") skip.append("soil.*")
skip.append("mesa.*") skip.append("mesa.*")
super(Debug, self).__init__(*args, skip=skip, **kwargs) super(Debug, self).__init__(*args, skip=skip, **kwargs)
@@ -181,7 +181,7 @@ def set_trace(frame=None, **kwargs):
debugger.set_trace(frame) debugger.set_trace(frame)
def post_mortem(traceback=None): def post_mortem(traceback=None, **kwargs):
global debugger global debugger
if debugger is None: if debugger is None:
debugger = Debug(**kwargs) debugger = Debug(**kwargs)

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import os import os
import sqlite3 import sqlite3
import math import math
import random
import logging import logging
import inspect import inspect
@@ -19,7 +18,7 @@ import networkx as nx
from mesa import Model from mesa import Model
from mesa.datacollection import DataCollector from mesa.datacollection import DataCollector
from . import agents as agentmod, config, serialization, utils, time, network from . import agents as agentmod, config, serialization, utils, time, network, events
class BaseEnvironment(Model): class BaseEnvironment(Model):
@@ -142,12 +141,12 @@ class BaseEnvironment(Model):
"The environment has not been scheduled, so it has no sense of time" "The environment has not been scheduled, so it has no sense of time"
) )
def add_agent(self, agent_class, unique_id=None, **kwargs): def add_agent(self, unique_id=None, **kwargs):
a = None
if unique_id is None: if unique_id is None:
unique_id = self.next_id() unique_id = self.next_id()
a = agent_class(model=self, unique_id=unique_id, **args) kwargs["unique_id"] = unique_id
a = self._agent_from_dict(kwargs)
self.schedule.add(a) self.schedule.add(a)
return a return a
@@ -169,7 +168,9 @@ class BaseEnvironment(Model):
Advance one step in the simulation, and update the data collection and scheduler appropriately Advance one step in the simulation, and update the data collection and scheduler appropriately
""" """
super().step() super().step()
self.logger.info(f"--- Step {self.now:^5} ---") self.logger.info(
f"--- Step: {self.schedule.steps:^5} - Time: {self.now:^5} ---"
)
self.schedule.step() self.schedule.step()
self.datacollector.collect(self) self.datacollector.collect(self)
@@ -236,6 +237,7 @@ class NetworkEnvironment(BaseEnvironment):
node_id = agent.get("node_id", None) node_id = agent.get("node_id", None)
if node_id is None: if node_id is None:
node_id = network.find_unassigned(self.G, random=self.random) node_id = network.find_unassigned(self.G, random=self.random)
self.G.nodes[node_id]["agent"] = None
agent["node_id"] = node_id agent["node_id"] = node_id
agent["unique_id"] = unique_id agent["unique_id"] = unique_id
agent["topology"] = self.G agent["topology"] = self.G
@@ -269,18 +271,31 @@ class NetworkEnvironment(BaseEnvironment):
node_id = network.find_unassigned( node_id = network.find_unassigned(
G=self.G, shuffle=True, random=self.random G=self.G, shuffle=True, random=self.random
) )
if node_id is None:
node_id = f"node_for_{unique_id}"
if node_id in G.nodes: if node_id not in self.G.nodes:
self.G.nodes[node_id]["agent"] = None # Reserve
else:
self.G.add_node(node_id) self.G.add_node(node_id)
assert "agent" not in self.G.nodes[node_id]
self.G.nodes[node_id]["agent"] = None # Reserve
a = self.add_agent( a = self.add_agent(
unique_id=unique_id, agent_class=agent_class, node_id=node_id, **kwargs unique_id=unique_id,
agent_class=agent_class,
topology=self.G,
node_id=node_id,
**kwargs,
) )
a["visible"] = True a["visible"] = True
return a return a
def add_agent(self, *args, **kwargs):
a = super().add_agent(*args, **kwargs)
if "node_id" in a:
assert self.G.nodes[a.node_id]["agent"] == a
return a
def agent_for_node_id(self, node_id): def agent_for_node_id(self, node_id):
return self.G.nodes[node_id].get("agent") return self.G.nodes[node_id].get("agent")
@@ -296,3 +311,14 @@ class NetworkEnvironment(BaseEnvironment):
Environment = NetworkEnvironment Environment = NetworkEnvironment
class EventedEnvironment(Environment):
def broadcast(self, msg, sender, expiration=None, ttl=None, **kwargs):
for agent in self.agents(**kwargs):
self.logger.info(f'Telling {repr(agent)}: {msg} ttl={ttl}')
try:
agent._inbox.append(events.Tell(payload=msg, sender=sender, expiration=expiration if ttl is None else self.now+ttl))
except AttributeError:
self.info(f'Agent {agent.unique_id} cannot receive events')

43
soil/events.py Normal file
View File

@@ -0,0 +1,43 @@
from .time import Cond
from dataclasses import dataclass, field
from typing import Any
from uuid import uuid4
class Event:
pass
@dataclass
class Message:
payload: Any
sender: Any = None
expiration: float = None
id: int = field(default_factory=uuid4)
def expired(self, when):
return self.expiration is not None and self.expiration < when
class Reply(Message):
source: Message
class Ask(Message):
reply: Message = None
def replied(self, expiration=None):
def ready(agent):
return self.reply is not None or agent.now > expiration
def value(agent):
if agent.now > expiration:
raise TimedOut(f'No answer received for {self}')
return self.reply
return Cond(func=ready, return_func=value)
class Tell(Message):
pass
class TimedOut(Exception):
pass

View File

@@ -3,6 +3,7 @@ import sys
from time import time as current_time from time import time as current_time
from io import BytesIO from io import BytesIO
from sqlalchemy import create_engine from sqlalchemy import create_engine
from textwrap import dedent, indent
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@@ -86,6 +87,22 @@ class Exporter:
pass pass
return open_or_reuse(f, mode=mode, **kwargs) return open_or_reuse(f, mode=mode, **kwargs)
def get_dfs(self, env):
yield from get_dc_dfs(env.datacollector, trial_id=env.id)
def get_dc_dfs(dc, trial_id=None):
dfs = {
"env": dc.get_model_vars_dataframe(),
"agents": dc.get_agent_vars_dataframe(),
}
for table_name in dc.tables:
dfs[table_name] = dc.get_table_dataframe(table_name)
if trial_id:
for (name, df) in dfs.items():
df["trial_id"] = trial_id
yield from dfs.items()
class default(Exporter): class default(Exporter):
"""Default exporter. Writes sqlite results, as well as the simulation YAML""" """Default exporter. Writes sqlite results, as well as the simulation YAML"""
@@ -98,7 +115,7 @@ class default(Exporter):
with self.output(self.simulation.name + ".dumped.yml") as f: with self.output(self.simulation.name + ".dumped.yml") as f:
f.write(self.simulation.to_yaml()) f.write(self.simulation.to_yaml())
self.dbpath = os.path.join(self.outdir, f"{self.simulation.name}.sqlite") self.dbpath = os.path.join(self.outdir, f"{self.simulation.name}.sqlite")
try_backup(self.dbpath, move=True) try_backup(self.dbpath, remove=True)
def trial_end(self, env): def trial_end(self, env):
if self.dry_run: if self.dry_run:
@@ -111,24 +128,10 @@ class default(Exporter):
engine = create_engine(f"sqlite:///{self.dbpath}", echo=False) engine = create_engine(f"sqlite:///{self.dbpath}", echo=False)
dc = env.datacollector for (t, df) in self.get_dfs(env):
for (t, df) in get_dc_dfs(dc, trial_id=env.id):
df.to_sql(t, con=engine, if_exists="append") df.to_sql(t, con=engine, if_exists="append")
def get_dc_dfs(dc, trial_id=None):
dfs = {
"env": dc.get_model_vars_dataframe(),
"agents": dc.get_agent_vars_dataframe(),
}
for table_name in dc.tables:
dfs[table_name] = dc.get_table_dataframe(table_name)
if trial_id:
for (name, df) in dfs.items():
df['trial_id'] = trial_id
yield from dfs.items()
class csv(Exporter): class csv(Exporter):
"""Export the state of each environment (and its agents) in a separate CSV file""" """Export the state of each environment (and its agents) in a separate CSV file"""
@@ -139,7 +142,7 @@ class csv(Exporter):
self.simulation.name, env.id, self.outdir self.simulation.name, env.id, self.outdir
) )
): ):
for (df_name, df) in get_dc_dfs(env.datacollector, trial_id=env.id): for (df_name, df) in self.get_dfs(env):
with self.output("{}.{}.csv".format(env.id, df_name)) as f: with self.output("{}.{}.csv".format(env.id, df_name)) as f:
df.to_csv(f) df.to_csv(f)
@@ -192,52 +195,19 @@ class graphdrawing(Exporter):
f.savefig(f) f.savefig(f)
""" class summary(Exporter):
Convert an environment into a NetworkX graph """Print a summary of each trial to sys.stdout"""
"""
def trial_end(self, env):
def env_to_graph(env, history=None): for (t, df) in self.get_dfs(env):
G = nx.Graph(env.G) if not len(df):
for agent in env.network_agents:
attributes = {"agent": str(agent.__class__)}
lastattributes = {}
spells = []
lastvisible = False
laststep = None
if not history:
history = sorted(list(env.state_to_tuples()))
for _, t_step, attribute, value in history:
if attribute == "visible":
nowvisible = value
if nowvisible and not lastvisible:
laststep = t_step
if not nowvisible and lastvisible:
spells.append((laststep, t_step))
lastvisible = nowvisible
continue continue
key = "attr_" + attribute msg = indent(str(df.describe()), " ")
if key not in attributes: logger.info(
attributes[key] = list() dedent(
if key not in lastattributes: f"""
lastattributes[key] = (value, t_step) Dataframe {t}:
elif lastattributes[key][0] != value: """
last_value, laststep = lastattributes[key] )
commit_value = (last_value, laststep, t_step) + msg
if key not in attributes: )
attributes[key] = list()
attributes[key].append(commit_value)
lastattributes[key] = (value, t_step)
for k, v in lastattributes.items():
attributes[k].append((v[0], v[1], None))
if lastvisible:
spells.append((laststep, None))
if spells:
G.add_node(agent.id, spells=spells, **attributes)
else:
G.add_node(agent.id, **attributes)
return G

View File

@@ -65,10 +65,8 @@ def find_unassigned(G, shuffle=False, random=random):
random.shuffle(candidates) random.shuffle(candidates)
for next_id, data in candidates: for next_id, data in candidates:
if "agent" not in data: if "agent" not in data:
node_id = next_id return next_id
break return None
return node_id
def dump_gexf(G, f): def dump_gexf(G, f):

View File

@@ -21,7 +21,6 @@ import pickle
from . import serialization, exporters, utils, basestring, agents from . import serialization, exporters, utils, basestring, agents
from .environment import Environment from .environment import Environment
from .utils import logger, run_and_return_exceptions from .utils import logger, run_and_return_exceptions
from .time import INFINITY
from .config import Config, convert_old from .config import Config, convert_old
@@ -48,7 +47,7 @@ class Simulation:
max_time: float = float("inf") max_time: float = float("inf")
max_steps: int = -1 max_steps: int = -1
interval: int = 1 interval: int = 1
num_trials: int = 3 num_trials: int = 1
parallel: Optional[bool] = None parallel: Optional[bool] = None
exporters: Optional[List[str]] = field(default_factory=list) exporters: Optional[List[str]] = field(default_factory=list)
outdir: Optional[str] = None outdir: Optional[str] = None
@@ -194,7 +193,7 @@ class Simulation:
# Set up agents on nodes # Set up agents on nodes
def is_done(): def is_done():
return False return not model.running
if until and hasattr(model.schedule, "time"): if until and hasattr(model.schedule, "time"):
prev = is_done prev = is_done
@@ -226,6 +225,11 @@ Model stats:
f'Simulation time {model.schedule.time}/{until}. Next: {getattr(model.schedule, "next_time", model.schedule.time + self.interval)}' f'Simulation time {model.schedule.time}/{until}. Next: {getattr(model.schedule, "next_time", model.schedule.time + self.interval)}'
) )
model.step() model.step()
if (
model.schedule.time < until
): # Simulation ended (no more steps) before the expected time
model.schedule.time = until
return model return model
def to_dict(self): def to_dict(self):

View File

@@ -2,6 +2,10 @@ from mesa.time import BaseScheduler
from queue import Empty from queue import Empty
from heapq import heappush, heappop, heapify from heapq import heappush, heappop, heapify
import math import math
from inspect import getsource
from numbers import Number
from .utils import logger from .utils import logger
from mesa import Agent as MesaAgent from mesa import Agent as MesaAgent
@@ -9,15 +13,76 @@ from mesa import Agent as MesaAgent
INFINITY = float("inf") INFINITY = float("inf")
class DeadAgent(Exception):
pass
class When: class When:
def __init__(self, time): def __init__(self, time):
if isinstance(time, When): if isinstance(time, When):
return time return time
self._time = time self._time = time
def abs(self, time): def next(self, time):
return self._time return self._time
def abs(self, time):
return self
def __repr__(self):
return str(f"When({self._time})")
def __lt__(self, other):
if isinstance(other, Number):
return self._time < other
return self._time < other.next(self._time)
def __gt__(self, other):
if isinstance(other, Number):
return self._time > other
return self._time > other.next(self._time)
def ready(self, agent):
return self._time <= agent.model.schedule.time
def return_value(self, agent):
return None
class Cond(When):
def __init__(self, func, delta=1, return_func=lambda agent: None):
self._func = func
self._delta = delta
self._checked = False
self._return_func = return_func
def next(self, time):
if self._checked:
return time + self._delta
return time
def abs(self, time):
return self
def ready(self, agent):
self._checked = True
return self._func(agent)
def return_value(self, agent):
return self._return_func(agent)
def __eq__(self, other):
return False
def __lt__(self, other):
return True
def __gt__(self, other):
return False
def __repr__(self):
return str(f'Cond("{getsource(self._func)}")')
NEVER = When(INFINITY) NEVER = When(INFINITY)
@@ -27,11 +92,19 @@ class Delta(When):
self._delta = delta self._delta = delta
def __eq__(self, other): def __eq__(self, other):
return self._delta == other._delta if isinstance(other, Delta):
return self._delta == other._delta
return False
def abs(self, time): def abs(self, time):
return When(self._delta + time)
def next(self, time):
return time + self._delta return time + self._delta
def __repr__(self):
return str(f"Delta({self._delta})")
class TimedActivation(BaseScheduler): class TimedActivation(BaseScheduler):
"""A scheduler which activates each agent when the agent requests. """A scheduler which activates each agent when the agent requests.
@@ -47,14 +120,17 @@ class TimedActivation(BaseScheduler):
def add(self, agent: MesaAgent, when=None): def add(self, agent: MesaAgent, when=None):
if when is None: if when is None:
when = self.time when = When(self.time)
elif not isinstance(when, When):
when = When(when)
if agent.unique_id in self._agents: if agent.unique_id in self._agents:
self._queue.remove((self._next[agent.unique_id], agent.unique_id))
del self._agents[agent.unique_id] del self._agents[agent.unique_id]
heapify(self._queue) if agent.unique_id in self._next:
self._queue.remove((self._next[agent.unique_id], agent))
heapify(self._queue)
heappush(self._queue, (when, agent.unique_id))
self._next[agent.unique_id] = when self._next[agent.unique_id] = when
heappush(self._queue, (when, agent))
super().add(agent) super().add(agent)
def step(self) -> None: def step(self) -> None:
@@ -63,42 +139,77 @@ class TimedActivation(BaseScheduler):
an agent will signal when it wants to be scheduled next. an agent will signal when it wants to be scheduled next.
""" """
self.logger.debug(f"Simulation step {self.next_time}") self.logger.debug(f"Simulation step {self.time}")
if not self.model.running: if not self.model.running:
return return
self.time = self.next_time when = NEVER
when = self.time
while self._queue and self._queue[0][0] == self.time: to_process = []
(when, agent_id) = heappop(self._queue) skipped = []
self.logger.debug(f"Stepping agent {agent_id}") next_time = INFINITY
agent = self._agents[agent_id] ix = 0
returned = agent.step()
if not getattr(agent, "alive", True): self.logger.debug(f"Queue length: {len(self._queue)}")
self.remove(agent)
while self._queue:
(when, agent) = self._queue[0]
if when > self.time:
break
heappop(self._queue)
if when.ready(agent):
try:
agent._last_return = when.return_value(agent)
except Exception as ex:
agent._last_except = ex
self._next.pop(agent.unique_id, None)
to_process.append(agent)
continue continue
when = (returned or Delta(1)).abs(self.time) next_time = min(next_time, when.next(self.time))
if when < self.time: self._next[agent.unique_id] = next_time
raise Exception( skipped.append((when, agent))
"Cannot schedule an agent for a time in the past ({} < {})".format(
when, self.time
)
)
self._next[agent_id] = when if self._queue:
heappush(self._queue, (when, agent_id)) next_time = min(next_time, self._queue[0][0].next(self.time))
self._queue = [*skipped, *self._queue]
for agent in to_process:
self.logger.debug(f"Stepping agent {agent}")
try:
returned = ((agent.step() or Delta(1))).abs(self.time)
except DeadAgent:
if agent.unique_id in self._next:
del self._next[agent.unique_id]
agent.alive = False
continue
if not getattr(agent, "alive", True):
continue
value = returned.next(self.time)
agent._last_return = value
if value < self.time:
raise Exception(
f"Cannot schedule an agent for a time in the past ({when} < {self.time})"
)
if value < INFINITY:
next_time = min(value, next_time)
self._next[agent.unique_id] = returned
heappush(self._queue, (returned, agent))
else:
assert not self._next[agent.unique_id]
self.steps += 1 self.steps += 1
self.logger.debug(f"Updating time step: {self.time} -> {next_time}")
self.time = next_time
if not self._queue: if not self._queue or next_time == INFINITY:
self.time = INFINITY
self.next_time = INFINITY
self.model.running = False self.model.running = False
return self.time return self.time
self.next_time = self._queue[0][0]
self.logger.debug(f"Next step: {self.next_time}")

View File

@@ -47,7 +47,7 @@ def timer(name="task", pre="", function=logger.info, to_object=None):
to_object.end = end to_object.end = end
def try_backup(path, move=False): def try_backup(path, remove=False):
if not os.path.exists(path): if not os.path.exists(path):
return None return None
outdir = os.path.dirname(path) outdir = os.path.dirname(path)
@@ -59,9 +59,7 @@ def try_backup(path, move=False):
backup_dir = os.path.join(outdir, "backup") backup_dir = os.path.join(outdir, "backup")
if not os.path.exists(backup_dir): if not os.path.exists(backup_dir):
os.makedirs(backup_dir) os.makedirs(backup_dir)
newpath = os.path.join( newpath = os.path.join(backup_dir, "{}@{}".format(os.path.basename(path), stamp))
backup_dir, "{}@{}".format(os.path.basename(path), stamp)
)
if move: if move:
move(path, newpath) move(path, newpath)
else: else:

View File

@@ -13,14 +13,57 @@ class Dead(agents.FSM):
class TestMain(TestCase): class TestMain(TestCase):
def test_die_raises_exception(self):
d = Dead(unique_id=0, model=environment.Environment())
d.step()
with pytest.raises(agents.DeadAgent):
d.step()
def test_die_returns_infinity(self): def test_die_returns_infinity(self):
'''The last step of a dead agent should return time.INFINITY'''
d = Dead(unique_id=0, model=environment.Environment()) d = Dead(unique_id=0, model=environment.Environment())
ret = d.step().abs(0) ret = d.step().abs(0)
print(ret, "next") print(ret, "next")
assert ret == stime.INFINITY assert ret == stime.NEVER
def test_die_raises_exception(self):
'''A dead agent should raise an exception if it is stepped after death'''
d = Dead(unique_id=0, model=environment.Environment())
d.step()
with pytest.raises(stime.DeadAgent):
d.step()
def test_agent_generator(self):
'''
The step function of an agent could be a generator. In that case, the state of the
agent will be resumed after every call to step.
'''
a = 0
class Gen(agents.BaseAgent):
def step(self):
nonlocal a
for i in range(5):
yield
a += 1
e = environment.Environment()
g = Gen(model=e, unique_id=e.next_id())
e.schedule.add(g)
for i in range(5):
e.step()
assert a == i
def test_state_decorator(self):
class MyAgent(agents.FSM):
run = 0
@agents.default_state
@agents.state('original')
def root(self):
self.run += 1
return self.other
@agents.state
def other(self):
self.run += 1
e = environment.Environment()
a = MyAgent(model=e, unique_id=e.next_id())
a.step()
assert a.run == 1
a.step()
assert a.run == 2

View File

@@ -50,7 +50,6 @@ class Exporters(TestCase):
for env in s.run_simulation(exporters=[Dummy], dry_run=True): for env in s.run_simulation(exporters=[Dummy], dry_run=True):
assert len(env.agents) == 1 assert len(env.agents) == 1
assert env.now == max_time
assert Dummy.started assert Dummy.started
assert Dummy.ended assert Dummy.ended

View File

@@ -160,32 +160,12 @@ class TestMain(TestCase):
def test_serialize_agent_class(self): def test_serialize_agent_class(self):
"""A class from soil.agents should be serialized without the module part""" """A class from soil.agents should be serialized without the module part"""
ser = agents.serialize_type(CustomAgent) ser = agents._serialize_type(CustomAgent)
assert ser == "test_main.CustomAgent" assert ser == "test_main.CustomAgent"
ser = agents.serialize_type(agents.BaseAgent) ser = agents._serialize_type(agents.BaseAgent)
assert ser == "BaseAgent" assert ser == "BaseAgent"
pickle.dumps(ser) pickle.dumps(ser)
def test_deserialize_agent_distribution(self):
agent_distro = [
{"agent_class": "CounterModel", "weight": 1},
{"agent_class": "test_main.CustomAgent", "weight": 2},
]
converted = agents.deserialize_definition(agent_distro)
assert converted[0]["agent_class"] == agents.CounterModel
assert converted[1]["agent_class"] == CustomAgent
pickle.dumps(converted)
def test_serialize_agent_distribution(self):
agent_distro = [
{"agent_class": agents.CounterModel, "weight": 1},
{"agent_class": CustomAgent, "weight": 2},
]
converted = agents.serialize_definition(agent_distro)
assert converted[0]["agent_class"] == "CounterModel"
assert converted[1]["agent_class"] == "test_main.CustomAgent"
pickle.dumps(converted)
def test_templates(self): def test_templates(self):
"""Loading a template should result in several configs""" """Loading a template should result in several configs"""
configs = serialization.load_file(join(EXAMPLES, "template.yml")) configs = serialization.load_file(join(EXAMPLES, "template.yml"))

74
tests/test_time.py Normal file
View File

@@ -0,0 +1,74 @@
from unittest import TestCase
from soil import time, agents, environment
class TestMain(TestCase):
def test_cond(self):
'''
A condition should match a When if the concition is True
'''
t = time.Cond(lambda t: True)
f = time.Cond(lambda t: False)
for i in range(10):
w = time.When(i)
assert w == t
assert w is not f
def test_cond(self):
'''
Comparing a Cond to a Delta should always return False
'''
c = time.Cond(lambda t: False)
d = time.Delta(1)
assert c is not d
def test_cond_env(self):
'''
'''
times_started = []
times_awakened = []
times = []
done = 0
class CondAgent(agents.BaseAgent):
def step(self):
nonlocal done
times_started.append(self.now)
while True:
yield time.Cond(lambda agent: agent.model.schedule.time >= 10)
times_awakened.append(self.now)
if self.now >= 10:
break
done += 1
env = environment.Environment(agents=[{'agent_class': CondAgent}])
while env.schedule.time < 11:
env.step()
times.append(env.now)
assert env.schedule.time == 11
assert times_started == [0]
assert times_awakened == [10]
assert done == 1
# The first time will produce the Cond.
# Since there are no other agents, time will not advance, but the number
# of steps will.
assert env.schedule.steps == 12
assert len(times) == 12
while env.schedule.time < 12:
env.step()
times.append(env.now)
assert env.schedule.time == 12
assert times_started == [0, 11]
assert times_awakened == [10, 11]
assert done == 2
# Once more to yield the cond, another one to continue
assert env.schedule.steps == 14
assert len(times) == 14