mirror of
https://github.com/gsi-upm/soil
synced 2025-10-27 05:38:17 +00:00
Compare commits
8 Commits
0efcd24d90
...
0.30.0rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2fb25c160 | ||
|
|
5fcf610108 | ||
|
|
159c9a9077 | ||
|
|
3776c4e5c5 | ||
|
|
880a9f2a1c | ||
|
|
227fdf050e | ||
|
|
5d759d0072 | ||
|
|
77d08fc592 |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
7
examples/events_and_messages/README.md
Normal file
7
examples/events_and_messages/README.md
Normal 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.
|
||||||
205
examples/events_and_messages/cars.py
Normal file
205
examples/events_and_messages/cars.py
Normal 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()
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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: {}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.20.7
|
0.30.0rc2
|
||||||
@@ -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__":
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
57
soil/agents/evented.py
Normal 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
142
soil/agents/fsm.py
Normal 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()
|
||||||
82
soil/agents/network_agents.py
Normal file
82
soil/agents/network_agents.py
Normal 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()
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
43
soil/events.py
Normal 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
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
173
soil/time.py
173
soil/time.py
@@ -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}")
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
74
tests/test_time.py
Normal 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
|
||||||
Reference in New Issue
Block a user