1
0
mirror of https://github.com/gsi-upm/soil synced 2025-11-04 17:38:16 +00:00

Compare commits

..

9 Commits

Author SHA1 Message Date
J. Fernando Sánchez
5d89827ccf Fix history bug 2018-05-04 11:21:23 +02:00
J. Fernando Sánchez
fc48ed7e09 Added history class
Now the environment does not deal with history directly, it delegates it to a
specific class. The analysis also uses history instances instead of either
using the database directly or creating a proxy environment.

This should make it easier to change the implementation in the future.

In fact, the change was motivated by the large size of the csv files in previous
versions. This new implementation only stores results in deltas, and it fills
any necessary values when needed.
2018-05-04 10:01:49 +02:00
J. Fernando Sánchez
73c90887e8 Fix pip installation 2018-05-04 09:59:31 +02:00
J. Fernando Sánchez
497c8a55db Add workaround for geometric models
Closes soil/soil#4
2018-02-16 18:04:43 +01:00
J. Fernando Sánchez
7d1c800490 Parallelism and granular exporting options
* Graphs are not saved by default (not backwards compatible)
* Modified newsspread examples
* More granular options to save results (exporting to CSV and GEXF are now
optional)
* Updated tutorial to include exporting options
* Removed references from environment to simulation
* Added parallelism to simulations (can be turned off with a flag or argument).
2017-11-01 14:44:46 +01:00
J. Fernando Sánchez
a4b32afa2f Fix py3.4 and pypi bugs 2017-10-19 18:28:17 +02:00
J. Fernando Sánchez
a7c51742f6 Improved docs
Fixed several bugs
Added convenience methods in soil.analysis
2017-10-19 18:06:33 +02:00
J. Fernando Sánchez
78364d89d5 Fix gephi representation. Add sqlite 2017-10-17 19:48:56 +02:00
J. Fernando Sánchez
af76f54a28 Added rabbits 2017-10-16 19:23:52 +02:00
85 changed files with 30375 additions and 3619 deletions

View File

@@ -3,7 +3,7 @@
Soil is an extensible and user-friendly Agent-based Social Simulator for Social Networks.
Learn how to run your own simulations with our [documentation](http://soilsim.readthedocs.io).
Follow our [tutorial](notebooks/soil_tutorial.ipynb) to develop your own agent models.
Follow our [tutorial](examples/tutorial/soil_tutorial.ipynb) to develop your own agent models.
If you use Soil in your research, don't forget to cite this paper:

8
docker-compose.yml Normal file
View File

@@ -0,0 +1,8 @@
version: '3'
services:
dev:
build: .
volumes:
- .:/usr/src/app
tty: true
entrypoint: /bin/bash

File diff suppressed because it is too large Load Diff

View File

@@ -34,13 +34,14 @@ If you use Soil in your research, do not forget to cite this paper:
.. toctree::
:maxdepth: 2
:maxdepth: 0
:caption: Learn more about soil:
installation
quickstart
Tutorial - Spreading news
Tutorial <soil_tutorial>
..
.. Indices and tables

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/output_54_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/output_54_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/output_55_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/output_55_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/output_55_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/output_55_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/output_55_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/output_55_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/output_55_6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/output_55_7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/output_55_8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/output_55_9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/output_56_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/output_56_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/output_56_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/output_56_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/output_56_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/output_56_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/output_56_6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/output_56_7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/output_56_8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/output_56_9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/output_61_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/output_63_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/output_66_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/output_67_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
docs/output_72_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
docs/output_72_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
docs/output_74_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/output_75_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/output_76_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -13,7 +13,7 @@ Here's an example (``example.yml``).
name: MyExampleSimulation
max_time: 50
num_trials: 3
timeout: 2
interval: 2
network_params:
network_type: barabasi_albert_graph
n: 100
@@ -34,6 +34,12 @@ Here's an example (``example.yml``).
environment_params:
prob_infect: 0.075
This example configuration will run three trials of a simulation containing a randomly generated network.
The 100 nodes in the network will be SISaModel agents, 10% of them will start in the content state, 10% in the discontent state, and the remaining 80% in the neutral state.
All agents will have access to the environment, which only contains one variable, ``prob_infected``.
The state of the agents will be updated every 2 seconds (``interval``).
Now run the simulation with the command line tool:
.. code:: bash
@@ -41,7 +47,7 @@ Now run the simulation with the command line tool:
soil example.yml
Once the simulation finishes, its results will be stored in a folder named ``MyExampleSimulation``.
Four types of objects are saved by default: a pickle of the simulation, a ``YAML`` representation of the simulation (to re-launch it), for every trial, a csv file with the content of the state of every network node and the environment parameters at every step of the simulation as well as the network in gephi format (``gexf``).
Four types of objects are saved by default: a pickle of the simulation; a ``YAML`` representation of the simulation (which can be used to re-launch it); and for every trial, a csv file with the content of the state of every network node and the environment parameters at every step of the simulation, as well as the network in gephi format (``gexf``).
.. code::
@@ -54,12 +60,6 @@ Four types of objects are saved by default: a pickle of the simulation, a ``YAML
│   └── Sim_prob_0_trial_0.gexf
This example configuration will run three trials of a simulation containing a randomly generated network.
The 100 nodes in the network will be SISaModel agents, 10% of them will start in the content state, 10% in the discontent state, and the remaining 80% in the neutral state.
All agents will have access to the environment, which only contains one variable, ``prob_infected``.
The state of the agents will be updated every 2 seconds (``timeout``).
Network
=======
@@ -94,7 +94,7 @@ For example, the following configuration is equivalent to :code:`nx.complete_gra
Environment
============
The environment is the place where the shared state of the simulation is stored.
For instance, the probability of certain events.
For instance, the probability of disease outbreak.
The configuration file may specify the initial value of the environment parameters:
.. code:: yaml
@@ -103,14 +103,17 @@ The configuration file may specify the initial value of the environment paramete
daily_probability_of_earthquake: 0.001
number_of_earthquakes: 0
Any agent has unrestricted access to the environment.
However, for the sake of simplicity, we recommend limiting environment updates to environment agents.
Agents
======
Agents are a way of modelling behavior.
Agents can be characterized with two variables: an agent type (``agent_type``) and its state.
Only one agent is executed at a time (generally, every ``timeout`` seconds), and it has access to its state and the environment parameters.
Only one agent is executed at a time (generally, every ``interval`` seconds), and it has access to its state and the environment parameters.
Through the environment, it can access the network topology and the state of other agents.
There are three three types of agents according to how they are added to the simulation: network agents, environment agent, and other agents.
There are three three types of agents according to how they are added to the simulation: network agents and environment agent.
Network Agents
##############
@@ -118,13 +121,13 @@ Network agents are attached to a node in the topology.
The configuration file allows you to specify how agents will be mapped to topology nodes.
The simplest way is to specify a single type of agent.
Hence, every node in the network will have an associated agent of that type.
Hence, every node in the network will be associated to an agent of that type.
.. code:: yaml
agent_type: SISaModel
It is also possible to add more than one type of agent to the simulation, and to control the ratio of each type (``weight``).
It is also possible to add more than one type of agent to the simulation, and to control the ratio of each type (using the ``weight`` property).
For instance, with following configuration, it is five times more likely for a node to be assigned a CounterModel type than a SISaModel type.
.. code:: yaml

2612
docs/soil_tutorial.rst Normal file

File diff suppressed because it is too large Load Diff

334
examples/NewsSpread.ipynb Normal file

File diff suppressed because one or more lines are too long

View File

@@ -4,6 +4,8 @@ dir_path: "/tmp/"
num_trials: 3
max_time: 100
interval: 1
seed: "CompleteSeed!"
dump: false
network_params:
generator: complete_graph
n: 10
@@ -21,4 +23,4 @@ default_state:
incidents: 0
states:
- name: 'The first node'
- name: 'The second node'
- name: 'The second node'

View File

@@ -1,17 +0,0 @@
default_state: {}
environment_agents: []
environment_params: {prob_neighbor_spread: 0.0, prob_tv_spread: 0.01}
interval: 1
max_time: 20
name: Sim_prob_0
network_agents:
- agent_type: NewsSpread
state: {has_tv: false}
weight: 1
- agent_type: NewsSpread
state: {has_tv: true}
weight: 2
network_params: {generator: erdos_renyi_graph, n: 500, p: 0.1}
num_trials: 1
states:
- {has_tv: true}

View File

@@ -1,20 +0,0 @@
import soil
import random
class NewsSpread(soil.agents.FSM):
@soil.agents.default_state
@soil.agents.state
def neutral(self):
r = random.random()
if self['has_tv'] and r < self.env['prob_tv_spread']:
return self.infected
return
@soil.agents.state
def infected(self):
prob_infect = self.env['prob_neighbor_spread']
for neighbor in self.get_neighboring_agents(state_id=self.neutral.id):
r = random.random()
if r < prob_infect:
neighbor.state['id'] = self.infected.id
return

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,138 @@
---
default_state: {}
load_module: newsspread
environment_agents: []
environment_params:
prob_neighbor_spread: 0.0
prob_tv_spread: 0.01
interval: 1
max_time: 30
name: Sim_all_dumb
network_agents:
- agent_type: DumbViewer
state:
has_tv: false
weight: 1
- agent_type: DumbViewer
state:
has_tv: true
weight: 1
network_params:
generator: barabasi_albert_graph
n: 500
m: 5
num_trials: 50
---
default_state: {}
load_module: newsspread
environment_agents: []
environment_params:
prob_neighbor_spread: 0.0
prob_tv_spread: 0.01
interval: 1
max_time: 30
name: Sim_half_herd
network_agents:
- agent_type: DumbViewer
state:
has_tv: false
weight: 1
- agent_type: DumbViewer
state:
has_tv: true
weight: 1
- agent_type: HerdViewer
state:
has_tv: false
weight: 1
- agent_type: HerdViewer
state:
has_tv: true
weight: 1
network_params:
generator: barabasi_albert_graph
n: 500
m: 5
num_trials: 50
---
default_state: {}
load_module: newsspread
environment_agents: []
environment_params:
prob_neighbor_spread: 0.0
prob_tv_spread: 0.01
interval: 1
max_time: 30
name: Sim_all_herd
network_agents:
- agent_type: HerdViewer
state:
has_tv: true
id: neutral
weight: 1
- agent_type: HerdViewer
state:
has_tv: true
id: neutral
weight: 1
network_params:
generator: barabasi_albert_graph
n: 500
m: 5
num_trials: 50
---
default_state: {}
load_module: newsspread
environment_agents: []
environment_params:
prob_neighbor_spread: 0.0
prob_tv_spread: 0.01
prob_neighbor_cure: 0.1
interval: 1
max_time: 30
name: Sim_wise_herd
network_agents:
- agent_type: HerdViewer
state:
has_tv: true
id: neutral
weight: 1
- agent_type: WiseViewer
state:
has_tv: true
weight: 1
network_params:
generator: barabasi_albert_graph
n: 500
m: 5
num_trials: 50
---
default_state: {}
load_module: newsspread
environment_agents: []
environment_params:
prob_neighbor_spread: 0.0
prob_tv_spread: 0.01
prob_neighbor_cure: 0.1
interval: 1
max_time: 30
name: Sim_all_wise
network_agents:
- agent_type: WiseViewer
state:
has_tv: true
id: neutral
weight: 1
- agent_type: WiseViewer
state:
has_tv: true
weight: 1
network_params:
generator: barabasi_albert_graph
n: 500
m: 5
network_params:
generator: barabasi_albert_graph
n: 500
m: 5
num_trials: 50

View File

@@ -0,0 +1,81 @@
from soil.agents import FSM, state, default_state, prob
import logging
class DumbViewer(FSM):
'''
A viewer that gets infected via TV (if it has one) and tries to infect
its neighbors once it's infected.
'''
defaults = {
'prob_neighbor_spread': 0.5,
'prob_tv_spread': 0.1,
}
@default_state
@state
def neutral(self):
if self['has_tv']:
if prob(self.env['prob_tv_spread']):
self.set_state(self.infected)
@state
def infected(self):
for neighbor in self.get_neighboring_agents(state_id=self.neutral.id):
if prob(self.env['prob_neighbor_spread']):
neighbor.infect()
def infect(self):
self.set_state(self.infected)
class HerdViewer(DumbViewer):
'''
A viewer whose probability of infection depends on the state of its neighbors.
'''
level = logging.DEBUG
def infect(self):
infected = self.count_neighboring_agents(state_id=self.infected.id)
total = self.count_neighboring_agents()
prob_infect = self.env['prob_neighbor_spread'] * infected/total
self.debug('prob_infect', prob_infect)
if prob(prob_infect):
self.set_state(self.infected.id)
class WiseViewer(HerdViewer):
'''
A viewer that can change its mind.
'''
defaults = {
'prob_neighbor_spread': 0.5,
'prob_neighbor_cure': 0.25,
'prob_tv_spread': 0.1,
}
@state
def cured(self):
prob_cure = self.env['prob_neighbor_cure']
for neighbor in self.get_neighboring_agents(state_id=self.infected.id):
if prob(prob_cure):
try:
neighbor.cure()
except AttributeError:
self.debug('Viewer {} cannot be cured'.format(neighbor.id))
def cure(self):
self.set_state(self.cured.id)
@state
def infected(self):
cured = max(self.count_neighboring_agents(self.cured.id),
1.0)
infected = max(self.count_neighboring_agents(self.infected.id),
1.0)
prob_cure = self.env['prob_neighbor_cure'] * (cured/infected)
if prob(prob_cure):
return self.cure()
return self.set_state(super().infected)

View File

@@ -0,0 +1,120 @@
from soil.agents import FSM, state, default_state, BaseAgent
from enum import Enum
from random import random, choice
from itertools import islice
import logging
import math
class Genders(Enum):
male = 'male'
female = 'female'
class RabbitModel(FSM):
level = logging.INFO
defaults = {
'age': 0,
'gender': Genders.male.value,
'mating_prob': 0.001,
'offspring': 0,
}
sexual_maturity = 4*30
life_expectancy = 365 * 3
gestation = 33
pregnancy = -1
max_females = 5
@default_state
@state
def newborn(self):
self['age'] += 1
if self['age'] >= self.sexual_maturity:
return self.fertile
@state
def fertile(self):
self['age'] += 1
if self['age'] > self.life_expectancy:
return self.dead
if self['gender'] == Genders.female.value:
return
# Males try to mate
females = self.get_agents(state_id=self.fertile.id, gender=Genders.female.value, limit_neighbors=False)
for f in islice(females, self.max_females):
r = random()
if r < self['mating_prob']:
self.impregnate(f)
break # Take a break
def impregnate(self, whom):
if self['gender'] == Genders.female.value:
raise NotImplementedError('Females cannot impregnate')
whom['pregnancy'] = 0
whom['mate'] = self.id
whom.set_state(whom.pregnant)
self.debug('{} impregnating: {}. {}'.format(self.id, whom.id, whom.state))
@state
def pregnant(self):
self['age'] += 1
if self['age'] > self.life_expectancy:
return self.dead
self['pregnancy'] += 1
self.debug('Pregnancy: {}'.format(self['pregnancy']))
if self['pregnancy'] >= self.gestation:
number_of_babies = int(8+4*random())
self.info('Having {} babies'.format(number_of_babies))
for i in range(number_of_babies):
state = {}
state['gender'] = choice(list(Genders)).value
child = self.env.add_node(self.__class__, state)
self.env.add_edge(self.id, child.id)
self.env.add_edge(self['mate'], child.id)
# self.add_edge()
self.debug('A BABY IS COMING TO LIFE')
self.env['rabbits_alive'] = self.env.get('rabbits_alive', self.global_topology.number_of_nodes())+1
self.debug('Rabbits alive: {}'.format(self.env['rabbits_alive']))
self['offspring'] += 1
self.env.get_agent(self['mate'])['offspring'] += 1
del self['mate']
self['pregnancy'] = -1
return self.fertile
@state
def dead(self):
self.info('Agent {} is dying'.format(self.id))
if 'pregnancy' in self and self['pregnancy'] > -1:
self.info('A mother has died carrying a baby!!')
self.die()
return
class RandomAccident(BaseAgent):
level = logging.DEBUG
def step(self):
rabbits_total = self.global_topology.number_of_nodes()
rabbits_alive = self.env.get('rabbits_alive', rabbits_total)
prob_death = self.env.get('prob_death', 1e-100)*math.floor(math.log10(max(1, rabbits_alive)))
self.debug('Killing some rabbits with prob={}!'.format(prob_death))
for i in self.env.network_agents:
if i.state['id'] == i.dead.id:
continue
r = random()
if r < prob_death:
self.debug('I killed a rabbit: {}'.format(i.id))
rabbits_alive = self.env['rabbits_alive'] = rabbits_alive -1
self.log('Rabbits alive: {}'.format(self.env['rabbits_alive']))
i.set_state(i.dead)
self.log('Rabbits alive: {}/{}'.format(rabbits_alive, rabbits_total))
if self.count_agents(state_id=RabbitModel.dead.id) == self.global_topology.number_of_nodes():
self.die()

View File

@@ -0,0 +1,23 @@
---
load_module: rabbit_agents
name: rabbits_example
max_time: 1200
interval: 1
seed: MySeed
agent_type: RabbitModel
environment_agents:
- agent_type: RandomAccident
environment_params:
prob_death: 0.001
default_state:
mating_prob: 0.01
topology:
nodes:
- id: 1
state:
gender: female
- id: 0
state:
gender: male
directed: true
links: []

View File

@@ -1,6 +1,6 @@
---
name: torvalds_example
max_time: 1
max_time: 10
interval: 2
agent_type: CounterModel
default_state:
@@ -11,4 +11,4 @@ states:
Torvalds:
skill_level: 'God'
balkian:
skill_level: 'developer'
skill_level: 'developer'

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
nxsim
simpy
networkx
networkx>=2.0
numpy
matplotlib
pyyaml
pandas

View File

@@ -1,20 +1,21 @@
import pip
import os
from setuptools import setup
# parse_requirements() returns generator of pip.req.InstallRequirement objects
from pip.req import parse_requirements
from soil import __version__
try:
install_reqs = parse_requirements(
"requirements.txt", session=pip.download.PipSession())
test_reqs = parse_requirements(
"test-requirements.txt", session=pip.download.PipSession())
except AttributeError:
install_reqs = parse_requirements("requirements.txt")
test_reqs = parse_requirements("test-requirements.txt")
install_reqs = [str(ir.req) for ir in install_reqs]
test_reqs = [str(ir.req) for ir in test_reqs]
with open(os.path.join('soil', 'VERSION')) as f:
__version__ = f.readlines()[0].strip()
assert __version__
def parse_requirements(filename):
""" load requirements from a pip requirements file """
with open(filename, 'r') as f:
lineiter = list(line.strip() for line in f)
return [line for line in lineiter if line and not line.startswith("#")]
install_reqs = parse_requirements("requirements.txt")
test_reqs = parse_requirements("test-requirements.txt")
setup(

View File

@@ -1,61 +0,0 @@
---
name: ControlModelM2_sim
max_time: 50
num_trials: 1
network_params:
generator: barabasi_albert_graph
n: 100
m: 2
network_agents:
- agent_type: ControlModelM2
weight: 0.1
state:
id: 1
- agent_type: ControlModelM2
weight: 0.9
state:
id: 0
environment_params:
prob_neutral_making_denier: 0.035
prob_infect: 0.075
prob_cured_healing_infected: 0.035
prob_cured_vaccinate_neutral: 0.035
prob_vaccinated_healing_infected: 0.035
prob_vaccinated_vaccinate_neutral: 0.035
prob_generate_anti_rumor: 0.035
standard_variance: 0.055
---
name: SISA_sm
max_time: 50
num_trials: 2
network_params:
generator: erdos_renyi_graph
n: 1000
p: 0.05
#other_agents:
# - agent_type: DrawingAgent
network_agents:
- agent_type: SISaModel
weight: 1
state:
id: content
- agent_type: SISaModel
weight: 1
state:
id: neutral
- agent_type: SISaModel
weight: 1
state:
id: discontent
environment_params:
neutral_discontent_spon_prob: 0.04
neutral_discontent_infected_prob: 0.04
neutral_content_spon_prob: 0.18
neutral_content_infected_prob: 0.02
discontent_neutral: 0.13
discontent_content: 0.07
variance_d_c: 0.02
content_discontent: 0.009
variance_c_d: 0.003
content_neutral: 0.088
standard_variance: 0.055

1
soil/VERSION Normal file
View File

@@ -0,0 +1 @@
0.11.1

View File

@@ -1,19 +1,23 @@
import importlib
import sys
import os
import pdb
import logging
__version__ = "0.9.7"
from .version import __version__
try:
basestring
except NameError:
basestring = str
logging.basicConfig()
from . import agents
from . import simulation
from . import environment
from . import utils
from . import settings
from . import analysis
def main():
@@ -29,8 +33,16 @@ def main():
help='file containing the code of any custom agents.')
parser.add_argument('--dry-run', '--dry', action='store_true',
help='Do not store the results of the simulation.')
parser.add_argument('--output', '-o', type=str,
parser.add_argument('--pdb', action='store_true',
help='Use a pdb console in case of exception.')
parser.add_argument('--graph', '-g', action='store_true',
help='Dump GEXF graph. Defaults to false.')
parser.add_argument('--csv', action='store_true',
help='Dump history in CSV format. Defaults to false.')
parser.add_argument('--output', '-o', type=str, default="soil_output",
help='folder to write results to. It defaults to the current directory.')
parser.add_argument('--synchronous', action='store_true',
help='Run trials serially and synchronously instead of in parallel. Defaults to false.')
args = parser.parse_args()
@@ -38,8 +50,25 @@ def main():
sys.path.append(os.getcwd())
importlib.import_module(args.module)
print('Loading config file: {}'.format(args.file, args.output))
simulation.run_from_config(args.file, dump=(not args.dry_run), results_dir=args.output)
logging.info('Loading config file: {}'.format(args.file, args.output))
try:
dump = []
if not args.dry_run:
if args.csv:
dump.append('csv')
if args.graph:
dump.append('gexf')
simulation.run_from_config(args.file,
dry_run=args.dry_run,
dump=dump,
parallel=(not args.synchronous and not args.pdb),
results_dir=args.output)
except Exception as ex:
if args.pdb:
pdb.post_mortem()
else:
raise
if __name__ == '__main__':

View File

@@ -1,32 +1,4 @@
import importlib
import sys
import argparse
from . import simulation
def main():
parser = argparse.ArgumentParser(description='Run a SOIL simulation')
parser.add_argument('file', type=str,
nargs="?",
default='simulation.yml',
help='python module containing the simulation configuration.')
parser.add_argument('--module', '-m', type=str,
help='file containing the code of any custom agents.')
parser.add_argument('--dry-run', '--dry', action='store_true',
help='Do not store the results of the simulation.')
parser.add_argument('--output', '-o', type=str,
help='folder to write results to. It defaults to the current directory.')
args = parser.parse_args()
if args.module:
sys.path.append(os.getcwd())
importlib.import_module(args.module)
print('Loading config file: {}'.format(args.file, args.output))
simulation.run_from_config(args.file, dump=not args.dry_run, results_dir=args.output)
from . import main
if __name__ == '__main__':
main()

View File

@@ -1,8 +1,8 @@
import random
from . import NetworkAgent
from . import BaseAgent
class BassModel(NetworkAgent):
class BassModel(BaseAgent):
"""
Settings:
innovation_prob

View File

@@ -1,8 +1,8 @@
import random
from . import NetworkAgent
from . import BaseAgent
class BigMarketModel(NetworkAgent):
class BigMarketModel(BaseAgent):
"""
Settings:
Names:

View File

@@ -1,7 +1,7 @@
from . import NetworkAgent
from . import BaseAgent
class CounterModel(NetworkAgent):
class CounterModel(BaseAgent):
"""
Dummy behaviour. It counts the number of nodes in the network and neighbors
in each step and adds it to its state.
@@ -9,14 +9,14 @@ class CounterModel(NetworkAgent):
def step(self):
# Outside effects
total = len(self.get_all_agents())
neighbors = len(self.get_neighboring_agents())
self.state['times'] = self.state.get('times', 0) + 1
self.state['neighbors'] = neighbors
self.state['total'] = total
total = len(list(self.get_all_agents()))
neighbors = len(list(self.get_neighboring_agents()))
self['times'] = self.get('times', 0) + 1
self['neighbors'] = neighbors
self['total'] = total
class AggregatedCounter(NetworkAgent):
class AggregatedCounter(BaseAgent):
"""
Dummy behaviour. It counts the number of nodes in the network and neighbors
in each step and adds it to its state.
@@ -24,8 +24,9 @@ class AggregatedCounter(NetworkAgent):
def step(self):
# Outside effects
total = len(self.get_all_agents())
neighbors = len(self.get_neighboring_agents())
self.state['times'] = self.state.get('times', 0) + 1
self.state['neighbors'] = self.state.get('neighbors', 0) + neighbors
self.state['total'] = self.state.get('total', 0) + total
total = len(list(self.get_all_agents()))
neighbors = len(list(self.get_neighboring_agents()))
self['times'] = self.get('times', 0) + 1
self['neighbors'] = self.get('neighbors', 0) + neighbors
self['total'] = total = self.get('total', 0) + total
self.debug('Running for step: {}. Total: {}'.format(self.now, total))

View File

@@ -15,4 +15,4 @@ class DrawingAgent(BaseAgent):
# Outside effects
f = plt.figure()
nx.draw(self.env.G, node_size=10, width=0.2, pos=nx.spring_layout(self.env.G, scale=100), ax=f.add_subplot(111))
f.savefig(os.path.join(self.env.sim().dir_path, "graph-"+str(self.env.now)+".png"))
f.savefig(os.path.join(self.env.get_path(), "graph-"+str(self.env.now)+".png"))

View File

@@ -1,9 +1,9 @@
import random
import numpy as np
from . import NetworkAgent
from . import BaseAgent
class SpreadModelM2(NetworkAgent):
class SpreadModelM2(BaseAgent):
"""
Settings:
prob_neutral_making_denier
@@ -104,7 +104,7 @@ class SpreadModelM2(NetworkAgent):
neighbor.state['id'] = 2 # Cured
class ControlModelM2(NetworkAgent):
class ControlModelM2(BaseAgent):
"""
Settings:
prob_neutral_making_denier

View File

@@ -1,9 +1,9 @@
import random
import numpy as np
from . import FSM, NetworkAgent, state
from . import FSM, state
class SISaModel(FSM, NetworkAgent):
class SISaModel(FSM):
"""
Settings:
neutral_discontent_spon_prob

View File

@@ -1,8 +1,8 @@
import random
from . import NetworkAgent
from . import BaseAgent
class SentimentCorrelationModel(NetworkAgent):
class SentimentCorrelationModel(BaseAgent):
"""
Settings:
outside_effects_prob

View File

@@ -6,12 +6,15 @@
import nxsim
import logging
from collections import OrderedDict
from copy import deepcopy
from functools import partial
import json
from functools import wraps
from .. import utils, history
agent_types = {}
@@ -27,28 +30,72 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
A special simpy BaseAgent that keeps track of its state history.
"""
def __init__(self, *args, **kwargs):
self._history = OrderedDict()
defaults = {}
def __init__(self, environment=None, agent_id=None, state=None,
name='network_process', interval=None, **state_params):
# Check for REQUIRED arguments
assert environment is not None, TypeError('__init__ missing 1 required keyword argument: \'environment\'. '
'Cannot be NoneType.')
# Initialize agent parameters
self.id = agent_id
self.name = name
self.state_params = state_params
# Global parameters
self.global_topology = environment.G
self.environment_params = environment.environment_params
# Register agent to environment
self.env = environment
self._neighbors = None
super().__init__(*args, **kwargs)
self.alive = True
real_state = deepcopy(self.defaults)
real_state.update(state or {})
self._state = real_state
self.interval = interval
if not hasattr(self, 'level'):
self.level = logging.DEBUG
self.logger = logging.getLogger('{}-Agent-{}'.format(self.env.name,
self.id))
self.logger.setLevel(self.level)
# initialize every time an instance of the agent is created
self.action = self.env.process(self.run())
@property
def state(self):
return self._state
@state.setter
def state(self, value):
for k, v in value.items():
self[k] = v
def __getitem__(self, key):
if isinstance(key, tuple):
k, t_step = key
if k is not None:
if t_step is not None:
return self._history[t_step][k]
else:
return {tt: tv.get(k, None) for tt, tv in self._history.items()}
else:
return self._history[t_step]
return self.state[key]
key, t_step = key
k = history.Key(key=key, t_step=t_step, agent_id=self.id)
return self.env[k]
return self.state.get(key, None)
def __delitem__(self, key):
self.state[key] = None
def __contains__(self, key):
return key in self.state
def __setitem__(self, key, value):
self.state[key] = value
k = history.Key(t_step=self.now,
agent_id=self.id,
key=key)
self.env[k] = value
def save_state(self):
self._history[self.now] = deepcopy(self.state)
def get(self, key, default=None):
return self[key] if key in self else default
@property
def now(self):
@@ -59,18 +106,26 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
return None
def run(self):
while True:
if self.interval is not None:
interval = self.interval
elif 'interval' in self:
interval = self['interval']
else:
interval = self.env.interval
while self.alive:
res = self.step()
yield res or self.env.timeout(self.env.interval)
yield res or self.env.timeout(interval)
def die(self, remove=False):
self.alive = False
if remove:
super().die()
def step(self):
pass
def to_json(self):
return json.dumps(self._history)
class NetworkAgent(BaseAgent, nxsim.BaseNetworkAgent):
return json.dumps(self.state)
def count_agents(self, state_id=None, limit_neighbors=False):
if limit_neighbors:
@@ -79,30 +134,69 @@ class NetworkAgent(BaseAgent, nxsim.BaseNetworkAgent):
agents = self.global_topology.nodes()
count = 0
for agent in agents:
if state_id and state_id != self.global_topology.node[agent]['agent'].state['id']:
if state_id and state_id != self.global_topology.node[agent]['agent']['id']:
continue
count += 1
return count
def count_neighboring_agents(self, state_id=None):
return self.count_agents(state_id, limit_neighbors=True)
return len(super().get_agents(state_id, limit_neighbors=True))
def get_agents(self, state_id=None, limit_neighbors=False, iterator=False, **kwargs):
if limit_neighbors:
agents = super().get_agents(state_id, limit_neighbors)
else:
agents = filter(lambda x: state_id is None or x.state.get('id', None) == state_id,
self.env.agents)
def matches_all(agent):
state = agent.state
for k, v in kwargs.items():
if state.get(k, None) != v:
return False
return True
f = filter(matches_all, agents)
if iterator:
return f
return list(f)
def log(self, message, *args, level=logging.INFO, **kwargs):
message = message + " ".join(str(i) for i in args)
message = "\t@{:>5}:\t{}".format(self.now, message)
for k, v in kwargs:
message += " {k}={v} ".format(k, v)
extra = {}
extra['now'] = self.now
extra['id'] = self.id
return self.logger.log(level, message, extra=extra)
def debug(self, *args, **kwargs):
return self.log(*args, level=logging.DEBUG, **kwargs)
def info(self, *args, **kwargs):
return self.log(*args, level=logging.INFO, **kwargs)
def state(func):
'''
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 nevironment.
'''
@wraps(func)
def func_wrapper(self):
when = None
next_state = func(self)
when = None
if next_state is None:
return when
try:
next_state, when = next_state
except TypeError:
except (ValueError, TypeError):
pass
if next_state:
try:
self.state['id'] = next_state.id
except AttributeError:
raise NotImplemented('State id %s is not valid.' % next_state)
self.set_state(next_state)
return when
func_wrapper.id = func.__name__
@@ -142,11 +236,13 @@ class FSM(BaseAgent, metaclass=MetaFSM):
def __init__(self, *args, **kwargs):
super(FSM, self).__init__(*args, **kwargs)
if 'id' not in self.state:
self.state['id'] = self.default_state.id
if not self.default_state:
raise ValueError('No default state specified for {}'.format(self.id))
self['id'] = self.default_state.id
def step(self):
if 'id' in self.state:
next_state = self.state['id']
next_state = self['id']
elif self.default_state:
next_state = self.default_state.id
else:
@@ -155,6 +251,123 @@ class FSM(BaseAgent, metaclass=MetaFSM):
raise Exception('{} is not a valid id for {}'.format(next_state, self))
self.states[next_state](self)
def set_state(self, state):
if hasattr(state, 'id'):
state = state.id
if state not in self.states:
raise ValueError('{} is not a valid state'.format(state))
self['id'] = state
return state
def prob(prob=1):
'''
A true/False uniform distribution with a given probability.
To be used like this:
.. code-block:: python
if prob(0.3):
do_something()
'''
r = random.random()
return r < prob
def calculate_distribution(network_agents=None,
agent_type=None):
'''
Calculate the threshold values (thresholds for a uniform distribution)
of an agent distribution given the weights of each agent type.
The input has this form: ::
[
{'agent_type': 'agent_type_1',
'weight': 0.2,
'state': {
'id': 0
}
},
{'agent_type': 'agent_type_2',
'weight': 0.8,
'state': {
'id': 1
}
}
]
In this example, 20% of the nodes will be marked as type
'agent_type_1'.
'''
if network_agents:
network_agents = deepcopy(network_agents)
elif agent_type:
network_agents = [{'agent_type': agent_type}]
else:
return []
# Calculate the thresholds
total = sum(x.get('weight', 1) for x in network_agents)
acc = 0
for v in network_agents:
upper = acc + (v.get('weight', 1)/total)
v['threshold'] = [acc, upper]
acc = upper
return network_agents
def _serialize_distribution(network_agents):
d = _convert_agent_types(network_agents,
to_string=True)
'''
When serializing an agent distribution, remove the thresholds, in order
to avoid cluttering the YAML definition file.
'''
for v in d:
if 'threshold' in v:
del v['threshold']
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.node
else:
assert len(states) <= len(topology)
return states
def _convert_agent_types(ind, to_string=False):
'''Convenience method to allow specifying agents by class or class name.'''
d = deepcopy(ind)
for v in d:
agent_type = v['agent_type']
if to_string and not isinstance(agent_type, str):
v['agent_type'] = str(agent_type.__name__)
elif not to_string and isinstance(agent_type, str):
v['agent_type'] = agent_types[agent_type]
return d
def _agent_from_distribution(distribution, value=-1):
"""Used in the initialization of agents given an agent distribution."""
if value < 0:
value = random.random()
for d in distribution:
threshold = d['threshold']
if value >= threshold[0] and value < threshold[1]:
state = {}
if 'state' in d:
state = deepcopy(d['state'])
return d['agent_type'], state
raise Exception('Distribution for value {} not found in: {}'.format(value, distribution))
from .BassModel import *
from .BigMarketModel import *

View File

@@ -4,20 +4,163 @@ import glob
import yaml
from os.path import join
from . import utils, history
def get_data(pattern, process=True, attributes=None):
def read_data(*args, group=False, **kwargs):
iterable = _read_data(*args, **kwargs)
if group:
return group_trials(iterable)
else:
return list(iterable)
def _read_data(pattern, *args, from_csv=False, process_args=None, **kwargs):
if not process_args:
process_args = {}
for folder in glob.glob(pattern):
config_file = glob.glob(join(folder, '*.yml'))[0]
config = yaml.load(open(config_file))
for trial_data in sorted(glob.glob(join(folder, '*.environment.csv'))):
df = pd.read_csv(trial_data)
if process:
if attributes is not None:
df = df[df['attribute'].isin(attributes)]
df = df.pivot_table(values='attribute', index='tstep', columns=['value'], aggfunc='count').fillna(0)
yield config_file, df, config
df = None
if from_csv:
for trial_data in sorted(glob.glob(join(folder,
'*.environment.csv'))):
df = read_csv(trial_data, **kwargs)
yield config_file, df, config
else:
for trial_data in sorted(glob.glob(join(folder, '*.db.sqlite'))):
df = read_sql(trial_data, **kwargs)
yield config_file, df, config
def read_sql(db, *args, **kwargs):
h = history.History(db, backup=False)
df = h.read_sql(*args, **kwargs)
return df
def read_csv(filename, keys=None, convert_types=False, **kwargs):
'''
Read a CSV in canonical form: ::
<agent_id, t_step, key, value, value_type>
'''
df = pd.read_csv(filename)
if convert_types:
df = convert_types_slow(df)
if keys:
df = df[df['key'].isin(keys)]
df = process_one(df)
return df
def convert_row(row):
row['value'] = utils.convert(row['value'], row['value_type'])
return row
def convert_types_slow(df):
'''This is a slow operation.'''
dtypes = get_types(df)
for k, v in dtypes.items():
t = df[df['key']==k]
t['value'] = t['value'].astype(v)
df = df.apply(convert_row, axis=1)
return df
def split_df(df):
'''
Split a dataframe in two dataframes: one with the history of agents,
and one with the environment history
'''
envmask = (df['agent_id'] == 'env')
n_env = envmask.sum()
if n_env == len(df):
return df, None
elif n_env == 0:
return None, df
agents, env = [x for _, x in df.groupby(envmask)]
return env, agents
def process(df, **kwargs):
'''
Process a dataframe in canonical form ``(t_step, agent_id, key, value, value_type)`` into
two dataframes with a column per key: one with the history of the agents, and one for the
history of the environment.
'''
env, agents = split_df(df)
return process_one(env, **kwargs), process_one(agents, **kwargs)
def get_types(df):
dtypes = df.groupby(by=['key'])['value_type'].unique()
return {k:v[0] for k,v in dtypes.iteritems()}
def process_one(df, *keys, columns=['key', 'agent_id'], values='value',
fill=True, index=['t_step',],
aggfunc='first', **kwargs):
'''
Process a dataframe in canonical form ``(t_step, agent_id, key, value, value_type)`` into
a dataframe with a column per key
'''
if df is None:
return df
if keys:
df = df[df['key'].isin(keys)]
df = df.pivot_table(values=values, index=index, columns=columns,
aggfunc=aggfunc, **kwargs)
if fill:
df = fillna(df)
return df
def get_count(df, *keys):
if keys:
df = df[list(keys)]
counts = pd.DataFrame()
for key in df.columns.levels[0]:
g = df[key].apply(pd.Series.value_counts, axis=1).fillna(0)
for value, series in g.iteritems():
counts[key, value] = series
counts.columns = pd.MultiIndex.from_tuples(counts.columns)
return counts
def get_value(df, *keys, aggfunc='sum'):
if keys:
df = df[list(keys)]
return df.groupby(axis=1, level=0).agg(aggfunc, axis=1)
def plot_all(*args, **kwargs):
for config_file, df, config in sorted(get_data(*args, **kwargs)):
'''
Read all the trial data and plot the result of applying a function on them.
'''
dfs = do_all(*args, **kwargs)
ps = []
for line in dfs:
f, df, config = line
df.plot(title=config['name'])
ps.append(df)
return ps
def do_all(pattern, func, *keys, include_env=False, **kwargs):
for config_file, df, config in read_data(pattern, keys=keys):
p = func(df, *keys, **kwargs)
p.plot(title=config['name'])
yield config_file, p, config
def group_trials(trials, aggfunc=['mean', 'min', 'max', 'std']):
trials = list(trials)
trials = list(map(lambda x: x[1] if isinstance(x, tuple) else x, trials))
return pd.concat(trials).groupby(level=0).agg(aggfunc).reorder_levels([2, 0,1] ,axis=1)
def fillna(df):
new_df = df.ffill(axis=0)
return new_df

View File

@@ -1,14 +1,30 @@
import os
import sqlite3
import time
import csv
import weakref
from random import random
import random
import simpy
import tempfile
import pandas as pd
from copy import deepcopy
from networkx.readwrite import json_graph
import networkx as nx
import nxsim
from . import utils, agents, analysis, history
class SoilEnvironment(nxsim.NetworkEnvironment):
"""
The environment is key in a simulation. It contains the network topology,
a reference to network and environment agents, as well as the environment
params, which are used as shared state between agents.
The environment parameters and the state of every agent can be accessed
both by using the environment as a dictionary or with the environment's
:meth:`soil.environment.SoilEnvironment.get` method.
"""
def __init__(self, name=None,
network_agents=None,
@@ -16,20 +32,32 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
states=None,
default_state=None,
interval=1,
seed=None,
dry_run=False,
dir_path=None,
topology=None,
*args, **kwargs):
self.name = name or 'UnnamedEnvironment'
self.states = deepcopy(states) or {}
if isinstance(states, list):
states = dict(enumerate(states))
self.states = deepcopy(states) if states else {}
self.default_state = deepcopy(default_state) or {}
super().__init__(*args, **kwargs)
if not topology:
topology = nx.Graph()
super().__init__(*args, topology=topology, **kwargs)
self._env_agents = {}
self._history = {}
self.dry_run = dry_run
self.interval = interval
self.logger = None
self.dir_path = dir_path or tempfile.mkdtemp('soil-env')
self.get_path()
self._history = history.History(name=self.name if not dry_run else None,
dir_path=self.dir_path)
# Add environment agents first, so their events get
# executed before network agents
self.environment_agents = environment_agents or []
self.network_agents = network_agents or []
self.process(self.save_state())
self['SEED'] = seed or time.time()
random.seed(self['SEED'])
@property
def agents(self):
@@ -39,7 +67,7 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
@property
def environment_agents(self):
for ref in self._env_agents.values():
yield ref()
yield ref
@environment_agents.setter
def environment_agents(self, environment_agents):
@@ -51,7 +79,7 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
kwargs['agent_id'] = kwargs.get('agent_id', atype.__name__)
kwargs['state'] = kwargs.get('state', {})
a = atype(environment=self, **kwargs)
self._env_agents[a.id] = weakref.ref(a)
self._env_agents[a.id] = a
@property
def network_agents(self):
@@ -62,60 +90,104 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
@network_agents.setter
def network_agents(self, network_agents):
if not network_agents:
return
for ix in self.G.nodes():
i = ix
node = self.G.node[i]
v = random()
found = False
for d in network_agents:
threshold = d['threshold']
if v >= threshold[0] and v < threshold[1]:
agent = d['agent_type']
state = None
if 'state' in d:
state = deepcopy(d['state'])
else:
try:
state = self.states[i]
except (IndexError, KeyError):
state = deepcopy(self.default_state)
node['agent'] = agent(environment=self,
agent_id=i,
state=state)
found = True
break
assert found
agent, state = agents._agent_from_distribution(network_agents)
self.set_agent(ix, agent_type=agent, state=state)
def set_agent(self, agent_id, agent_type, state=None):
node = self.G.nodes[agent_id]
defstate = deepcopy(self.default_state)
defstate.update(self.states.get(agent_id, {}))
if state:
defstate.update(state)
state = defstate
state.update(node.get('state', {}))
a = agent_type(environment=self,
agent_id=agent_id,
state=state)
node['agent'] = a
return a
def add_node(self, agent_type, state=None):
agent_id = int(len(self.G.nodes()))
self.G.add_node(agent_id)
a = self.set_agent(agent_id, agent_type, state)
a['visible'] = True
return a
def add_edge(self, agent1, agent2, attrs=None):
return self.G.add_edge(agent1, agent2)
def run(self, *args, **kwargs):
self._save_state()
super().run(*args, **kwargs)
self._save_state()
self._history.flush_cache()
def _save_state(self):
for agent in self.agents:
agent.save_state()
self._history[self.now] = deepcopy(self.environment_params)
def _save_state(self, now=None):
# for agent in self.agents:
# agent.save_state()
utils.logger.debug('Saving state @{}'.format(self.now))
self._history.save_records(self.state_to_tuples(now=now))
def save_state(self):
while True:
'''
:DEPRECATED:
Periodically save the state of the environment and the agents.
'''
self._save_state()
while self.peek() != simpy.core.Infinity:
delay = max(self.peek() - self.now, self.interval)
utils.logger.debug('Step: {}'.format(self.now))
ev = self.event()
ev._ok = True
# Schedule the event with minimum priority so
# that it executes after all agents are done
self.schedule(ev, -1, self.interval)
# that it executes before all agents
self.schedule(ev, -999, delay)
yield ev
self._save_state()
def __getitem__(self, key):
if isinstance(key, tuple):
self._history.flush_cache()
return self._history[key]
return self.environment_params[key]
def __setitem__(self, key, value):
if isinstance(key, tuple):
k = history.Key(*key)
self._history.save_record(*k,
value=value)
return
self.environment_params[key] = value
self._history.save_record(agent_id='env',
t_step=self.now,
key=key,
value=value)
def __contains__(self, key):
return key in self.environment_params
def get(self, key, default=None):
'''
Get the value of an environment attribute in a
given point in the simulation (history).
If key is an attribute name, this method returns
the current value.
To get values at other times, use a
:meth: `soil.history.Key` tuple.
'''
return self[key] if key in self else default
def get_path(self, dir_path=None):
dir_path = dir_path or self.sim().dir_path
dir_path = dir_path or self.dir_path
if not os.path.exists(dir_path):
os.makedirs(dir_path)
try:
os.makedirs(dir_path)
except FileExistsError:
pass
return dir_path
def get_agent(self, agent_id):
@@ -130,7 +202,7 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
with open(csv_name, 'w') as f:
cr = csv.writer(f)
cr.writerow(('agent_id', 'tstep', 'attribute', 'value'))
cr.writerow(('agent_id', 't_step', 'key', 'value', 'value_type'))
for i in self.history_to_tuples():
cr.writerow(i)
@@ -138,28 +210,60 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
G = self.history_to_graph()
graph_path = os.path.join(self.get_path(dir_path),
self.name+".gexf")
# Workaround for geometric models
# See soil/soil#4
for node in G.nodes():
if 'pos' in G.node[node]:
G.node[node]['viz'] = {"position": {"x": G.node[node]['pos'][0], "y": G.node[node]['pos'][1], "z": 0.0}}
del (G.node[node]['pos'])
nx.write_gexf(G, graph_path, version="1.2draft")
def history_to_tuples(self):
for tstep, state in self._history.items():
for attribute, value in state.items():
yield ('env', tstep, attribute, value)
def dump(self, dir_path=None, formats=None):
if not formats:
return
functions = {
'csv': self.dump_csv,
'gexf': self.dump_gexf
}
for f in formats:
if f in functions:
functions[f](dir_path)
else:
raise ValueError('Unknown format: {}'.format(f))
def state_to_tuples(self, now=None):
if now is None:
now = self.now
for k, v in self.environment_params.items():
yield history.Record(agent_id='env',
t_step=now,
key=k,
value=v)
for agent in self.agents:
for tstep, state in agent._history.items():
for attribute, value in state.items():
yield (agent.id, tstep, attribute, value)
for k, v in agent.state.items():
yield history.Record(agent_id=agent.id,
t_step=now,
key=k,
value=v)
def history_to_tuples(self):
return self._history.to_tuples()
def history_to_graph(self):
G = nx.Graph(self.G)
for agent in self.agents:
for agent in self.network_agents:
attributes = {'agent': str(agent.__class__)}
lastattributes = {}
spells = []
lastvisible = False
laststep = None
for t_step, state in reversed(list(agent._history.items())):
history = self[agent.id, None, None]
if not history:
continue
for t_step, state in reversed(sorted(list(history.items()))):
for attribute, value in state.items():
if attribute == 'visible':
nowvisible = state[attribute]
@@ -170,15 +274,20 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
lastvisible = nowvisible
else:
if attribute not in lastattributes or lastattributes[attribute][0] != value:
laststep = lastattributes.get(attribute,
(None, None))[1]
value = (state[attribute], t_step, laststep)
key = 'attr_' + attribute
key = 'attr_' + attribute
if key not in attributes:
attributes[key] = list()
if key not in lastattributes:
lastattributes[key] = (state[attribute], t_step)
elif lastattributes[key][0] != value:
last_value, laststep = lastattributes[key]
value = (last_value, t_step, laststep)
if key not in attributes:
attributes[key] = list()
attributes[key].append(value)
lastattributes[attribute] = (state[attribute], t_step)
lastattributes[key] = (state[attribute], t_step)
for k, v in lastattributes.items():
attributes[k].append((v[0], 0, v[1]))
if lastvisible:
spells.append((laststep, None))
if spells:
@@ -187,3 +296,19 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
G.add_node(agent.id, **attributes)
return G
def __getstate__(self):
state = self.__dict__.copy()
state['G'] = json_graph.node_link_data(self.G)
state['network_agents'] = agents._serialize_distribution(self.network_agents)
state['environment_agents'] = agents._convert_agent_types(self.environment_agents,
to_string=True)
del state['_queue']
return state
def __setstate__(self, state):
self.__dict__ = state
self.G = json_graph.node_link_graph(state['G'])
self.network_agents = self.calculate_distribution(self._convert_agent_types(self.network_agents))
self.environment_agents = self._convert_agent_types(self.environment_agents)
return state

231
soil/history.py Normal file
View File

@@ -0,0 +1,231 @@
import time
import os
import pandas as pd
import sqlite3
import copy
from collections import UserDict, Iterable, namedtuple
from . import utils
class History:
"""
Store and retrieve values from a sqlite database.
"""
def __init__(self, db_path=None, name=None, dir_path=None, backup=True):
if db_path is None and name:
db_path = os.path.join(dir_path or os.getcwd(),
'{}.db.sqlite'.format(name))
if db_path is None:
db_path = ":memory:"
else:
if backup and os.path.exists(db_path):
newname = db_path + '.backup{}.sqlite'.format(time.time())
os.rename(db_path, newname)
self._db_path = db_path
if isinstance(db_path, str):
self._db = sqlite3.connect(db_path)
else:
self._db = db_path
with self._db:
self._db.execute('''CREATE TABLE IF NOT EXISTS history (agent_id text, t_step int, key text, value text text)''')
self._db.execute('''CREATE TABLE IF NOT EXISTS value_types (key text, value_type text)''')
self._db.execute('''CREATE UNIQUE INDEX IF NOT EXISTS idx_history ON history (agent_id, t_step, key);''')
self._dtypes = {}
self._tups = []
def conversors(self, key):
"""Get the serializer and deserializer for a given key."""
if key not in self._dtypes:
self.read_types()
return self._dtypes[key]
@property
def dtypes(self):
return {k:v[0] for k, v in self._dtypes.items()}
def save_tuples(self, tuples):
self.save_records(Record(*tup) for tup in tuples)
def save_records(self, records):
with self._db:
for rec in records:
if not isinstance(rec, Record):
rec = Record(*rec)
if rec.key not in self._dtypes:
name = utils.name(rec.value)
serializer = utils.serializer(name)
deserializer = utils.deserializer(name)
self._dtypes[rec.key] = (name, serializer, deserializer)
self._db.execute("replace into value_types (key, value_type) values (?, ?)", (rec.key, name))
self._db.execute("replace into history(agent_id, t_step, key, value) values (?, ?, ?, ?)", (rec.agent_id, rec.t_step, rec.key, rec.value))
def save_record(self, *args, **kwargs):
self._tups.append(Record(*args, **kwargs))
if len(self._tups) > 100:
self.flush_cache()
def flush_cache(self):
'''
Use a cache to save state changes to avoid opening a session for every change.
The cache will be flushed at the end of the simulation, and when history is accessed.
'''
self.save_records(self._tups)
self._tups = list()
def to_tuples(self):
self.flush_cache()
with self._db:
res = self._db.execute("select agent_id, t_step, key, value from history ").fetchall()
for r in res:
agent_id, t_step, key, value = r
_, _ , des = self.conversors(key)
yield agent_id, t_step, key, des(value)
def read_types(self):
with self._db:
res = self._db.execute("select key, value_type from value_types ").fetchall()
for k, v in res:
serializer = utils.serializer(v)
deserializer = utils.deserializer(v)
self._dtypes[k] = (v, serializer, deserializer)
def __getitem__(self, key):
key = Key(*key)
agent_ids = [key.agent_id] if key.agent_id is not None else []
t_steps = [key.t_step] if key.t_step is not None else []
keys = [key.key] if key.key is not None else []
df = self.read_sql(agent_ids=agent_ids,
t_steps=t_steps,
keys=keys)
r = Records(df, filter=key, dtypes=self._dtypes)
return r.value()
def read_sql(self, keys=None, agent_ids=None, t_steps=None, convert_types=False, limit=-1):
self.read_types()
def escape_and_join(v):
if v is None:
return
return ",".join(map(lambda x: "\'{}\'".format(x), v))
filters = [("key in ({})".format(escape_and_join(keys)), keys),
("agent_id in ({})".format(escape_and_join(agent_ids)), agent_ids)
]
filters = list(k[0] for k in filters if k[1])
last_df = None
if t_steps:
# Look for the last value before the minimum step in the query
min_step = min(t_steps)
last_filters = ['t_step < {}'.format(min_step),]
last_filters = last_filters + filters
condition = ' and '.join(last_filters)
last_query = '''
select h1.*
from history h1
inner join (
select agent_id, key, max(t_step) as t_step
from history
where {condition}
group by agent_id, key
) h2
on h1.agent_id = h2.agent_id and
h1.key = h2.key and
h1.t_step = h2.t_step
'''.format(condition=condition)
last_df = pd.read_sql_query(last_query, self._db)
filters.append("t_step >= '{}' and t_step <= '{}'".format(min_step, max(t_steps)))
condition = ''
if filters:
condition = 'where {} '.format(' and '.join(filters))
query = 'select * from history {} limit {}'.format(condition, limit)
df = pd.read_sql_query(query, self._db)
if last_df is not None:
df = pd.concat([df, last_df])
df_p = df.pivot_table(values='value', index=['t_step'],
columns=['key', 'agent_id'],
aggfunc='first')
for k, v in self._dtypes.items():
if k in df_p:
dtype, _, deserial = v
df_p[k] = df_p[k].fillna(method='ffill').fillna(deserial()).astype(dtype)
if t_steps:
df_p = df_p.reindex(t_steps, method='ffill')
return df_p.ffill()
class Records():
def __init__(self, df, filter=None, dtypes=None):
if not filter:
filter = Key(agent_id=None,
t_step=None,
key=None)
self._df = df
self._filter = filter
self.dtypes = dtypes or {}
super().__init__()
def mask(self, tup):
res = ()
for i, k in zip(tup[:-1], self._filter):
if k is None:
res = res + (i,)
res = res + (tup[-1],)
return res
def filter(self, newKey):
f = list(self._filter)
for ix, i in enumerate(f):
if i is None:
f[ix] = newKey
self._filter = Key(*f)
@property
def resolved(self):
return sum(1 for i in self._filter if i is not None) == 3
def __iter__(self):
for column, series in self._df.iteritems():
key, agent_id = column
for t_step, value in series.iteritems():
r = Record(t_step=t_step,
agent_id=agent_id,
key=key,
value=value)
yield self.mask(r)
def value(self):
if self.resolved:
f = self._filter
try:
i = self._df[f.key][str(f.agent_id)]
ix = i.index.get_loc(f.t_step, method='ffill')
return i.iloc[ix]
except KeyError:
return self.dtypes[f.key][2]()
return self
def __getitem__(self, k):
n = copy.copy(self)
n.filter(k)
return n.value()
def __len__(self):
return len(self._df)
Key = namedtuple('Key', ['agent_id', 't_step', 'key'])
Record = namedtuple('Record', 'agent_id t_step key value')

View File

@@ -1,27 +1,26 @@
import weakref
import os
import csv
import time
import imp
import sys
import yaml
import networkx as nx
from networkx.readwrite import json_graph
from copy import deepcopy
from random import random
from matplotlib import pyplot as plt
from multiprocessing import Pool
from functools import partial
import pickle
from nxsim import NetworkSimulation
from . import agents, utils, environment, basestring
from . import utils, environment, basestring, agents
from .utils import logger
class SoilSimulation(NetworkSimulation):
"""
Subclass of nsim.NetworkSimulation with three main differences:
1) agent type can be specified by name or by class.
2) instead of just one type, an network_agents can be used.
2) instead of just one type, a network agents distribution can be used.
The distribution specifies the weight (or probability) of each
agent type in the topology. This is an example distribution: ::
@@ -47,9 +46,9 @@ class SoilSimulation(NetworkSimulation):
"""
def __init__(self, name=None, topology=None, network_params=None,
network_agents=None, agent_type=None, states=None,
default_state=None, interval=1,
dir_path=None, num_trials=3, max_time=100,
agent_module=None,
default_state=None, interval=1, dump=None, dry_run=False,
dir_path=None, num_trials=1, max_time=100,
agent_module=None, load_module=None, seed=None,
environment_agents=None, environment_params=None):
if topology is None:
@@ -58,6 +57,7 @@ class SoilSimulation(NetworkSimulation):
elif isinstance(topology, basestring) or isinstance(topology, dict):
topology = json_graph.node_link_graph(topology)
self.load_module = load_module
self.topology = nx.Graph(topology)
self.network_params = network_params
self.name = name or 'UnnamedSimulation'
@@ -66,78 +66,72 @@ class SoilSimulation(NetworkSimulation):
self.default_state = default_state or {}
self.dir_path = dir_path or os.getcwd()
self.interval = interval
self.seed = str(seed) or str(time.time())
self.dump = dump
self.dry_run = dry_run
self.environment_params = environment_params or {}
if load_module:
path = sys.path + [self.dir_path, os.getcwd()]
f, fp, desc = imp.find_module(load_module, path)
imp.load_module('soil.agents.custom', f, fp, desc)
environment_agents = environment_agents or []
self.environment_agents = self._convert_agent_types(environment_agents)
self.environment_agents = agents._convert_agent_types(environment_agents)
distro = self.calculate_distribution(network_agents,
agent_type)
self.network_agents = self._convert_agent_types(distro)
distro = agents.calculate_distribution(network_agents,
agent_type)
self.network_agents = agents._convert_agent_types(distro)
self.states = self.validate_states(states,
topology)
self.states = agents._validate_states(states,
self.topology)
def calculate_distribution(self,
network_agents=None,
agent_type=None):
if network_agents:
network_agents = deepcopy(network_agents)
elif agent_type:
network_agents = [{'agent_type': agent_type}]
else:
return []
def run_simulation(self, *args, **kwargs):
return self.run(*args, **kwargs)
# Calculate the thresholds
total = sum(x.get('weight', 1) for x in network_agents)
acc = 0
for v in network_agents:
upper = acc + (v.get('weight', 1)/total)
v['threshold'] = [acc, upper]
acc = upper
return network_agents
def run(self, *args, **kwargs):
return list(self.run_simulation_gen(*args, **kwargs))
def serialize_distribution(self):
d = self._convert_agent_types(self.network_agents,
to_string=True)
for v in d:
if 'threshold' in v:
del v['threshold']
return d
def run_simulation_gen(self, *args, parallel=False, dry_run=False,
**kwargs):
p = Pool()
with utils.timer('simulation {}'.format(self.name)):
if parallel:
func = partial(self.run_trial, dry_run=dry_run or self.dry_run,
return_env=not parallel, **kwargs)
for i in p.imap_unordered(func, range(self.num_trials)):
yield i
else:
for i in range(self.num_trials):
yield self.run_trial(i, dry_run=dry_run or self.dry_run, **kwargs)
if not (dry_run or self.dry_run):
logger.info('Dumping results to {}'.format(self.dir_path))
self.dump_pickle(self.dir_path)
self.dump_yaml(self.dir_path)
else:
logger.info('NOT dumping results')
def _convert_agent_types(self, ind, to_string=False):
d = deepcopy(ind)
for v in d:
agent_type = v['agent_type']
if to_string and not isinstance(agent_type, str):
v['agent_type'] = str(agent_type.__name__)
elif not to_string and isinstance(agent_type, str):
v['agent_type'] = agents.agent_types[agent_type]
return d
def get_env(self, trial_id=0, **kwargs):
opts = self.environment_params.copy()
env_name = '{}_trial_{}'.format(self.name, trial_id)
opts.update({
'name': env_name,
'topology': self.topology.copy(),
'seed': self.seed+env_name,
'initial_time': 0,
'dry_run': self.dry_run,
'interval': self.interval,
'network_agents': self.network_agents,
'states': self.states,
'default_state': self.default_state,
'environment_agents': self.environment_agents,
'dir_path': self.dir_path,
})
opts.update(kwargs)
env = environment.SoilEnvironment(**opts)
return env
def validate_states(self, states, topology):
states = states or []
# Validate states to avoid ignoring states during
# initialization
if isinstance(states, dict):
for x in states:
assert x in self.topology.node
else:
assert len(states) <= len(self.topology)
return states
def run_simulation(self):
return self.run()
def run(self):
return list(self.run_simulation_gen())
def run_simulation_gen(self):
with utils.timer('simulation'):
for i in range(self.num_trials):
yield self.run_trial(i)
def run_trial(self, trial_id=0):
def run_trial(self, trial_id=0, until=None, return_env=True, **opts):
"""Run a single trial of the simulation
Parameters
@@ -145,24 +139,16 @@ class SoilSimulation(NetworkSimulation):
trial_id : int
"""
# Set-up trial environment and graph
print('Trial: {}'.format(trial_id))
env_name = '{}_trial_{}'.format(self.name, trial_id)
env = environment.SoilEnvironment(name=env_name,
topology=self.topology.copy(),
initial_time=0,
interval=self.interval,
network_agents=self.network_agents,
states=self.states,
default_state=self.default_state,
environment_agents=self.environment_agents,
**self.environment_params)
env.sim = weakref.ref(self)
until = until or self.max_time
env = self.get_env(trial_id=trial_id, **opts)
# Set up agents on nodes
print('\tRunning')
with utils.timer('trial'):
env.run(until=self.max_time)
return env
with utils.timer('Simulation {} trial {}'.format(self.name, trial_id)):
env.run(until)
if self.dump and not self.dry_run:
with utils.timer('Dumping simulation {} trial {}'.format(self.name, trial_id)):
env.dump(formats=self.dump)
if return_env:
return env
def to_dict(self):
return self.__getstate__()
@@ -193,20 +179,20 @@ class SoilSimulation(NetworkSimulation):
def __getstate__(self):
state = self.__dict__.copy()
state['topology'] = json_graph.node_link_data(self.topology)
state['network_agents'] = self.serialize_distribution()
state['environment_agents'] = self._convert_agent_types(self.environment_agents,
to_string=True)
state['network_agents'] = agents._serialize_distribution(self.network_agents)
state['environment_agents'] = agents._convert_agent_types(self.environment_agents,
to_string=True)
return state
def __setstate__(self, state):
self.__dict__ = state
self.topology = json_graph.node_link_graph(state['topology'])
self.network_agents = self._convert_agent_types(self.network_agents)
self.environment_agents = self._convert_agent_types(self.environment_agents)
self.network_agents = agents.calculate_distribution(agents._convert_agent_types(self.network_agents))
self.environment_agents = agents._convert_agent_types(self.environment_agents)
return state
def from_config(config, G=None):
def from_config(config):
config = list(utils.load_config(config))
if len(config) > 1:
raise AttributeError('Provide only one configuration')
@@ -215,27 +201,19 @@ def from_config(config, G=None):
return sim
def run_from_config(*configs, dump=True, results_dir=None, timestamp=False):
if not results_dir:
results_dir = 'soil_output'
def run_from_config(*configs, results_dir='soil_output', dry_run=False, dump=None, timestamp=False, **kwargs):
for config_def in configs:
for config, cpath in utils.load_config(config_def):
# logger.info("Found {} config(s)".format(len(ls)))
for config, _ in utils.load_config(config_def):
name = config.get('name', 'unnamed')
print("Using config(s): {name}".format(name=name))
logger.info("Using config(s): {name}".format(name=name))
sim = SoilSimulation(**config)
if timestamp:
sim_folder = '{}_{}'.format(sim.name,
sim_folder = '{}_{}'.format(name,
time.strftime("%Y-%m-%d_%H:%M:%S"))
else:
sim_folder = sim.name
dir_path = os.path.join(results_dir,
sim_folder)
results = sim.run_simulation()
if dump:
sim.dump_pickle(dir_path)
sim.dump_yaml(dir_path)
for env in results:
env.dump_gexf(dir_path)
env.dump_csv(dir_path)
sim_folder = name
dir_path = os.path.join(results_dir, sim_folder)
sim = SoilSimulation(dir_path=dir_path, dump=dump, **config)
logger.info('Dumping results to {} : {}'.format(sim.dir_path, sim.dump))
sim.run_simulation(**kwargs)

View File

@@ -1,14 +1,24 @@
import os
import yaml
import logging
import importlib
from time import time
from glob import glob
from random import random
from copy import deepcopy
import networkx as nx
from contextlib import contextmanager
logger = logging.getLogger('soil')
logger.setLevel(logging.INFO)
def load_network(network_params, dir_path=None):
if network_params is None:
return nx.Graph()
path = network_params.get('path', None)
if path:
if dir_path and not os.path.isabs(path):
@@ -51,11 +61,45 @@ def load_config(config):
@contextmanager
def timer(name='task', pre="", function=print, to_object=None):
def timer(name='task', pre="", function=logger.info, to_object=None):
start = time()
function('{}Starting {} at {}.'.format(pre, name, start))
yield start
end = time()
function('{}Finished {} in {} seconds'.format(pre, name, str(end-start)))
if to_object:
to_object.start = start
to_object.end = end
def repr(v):
func = serializer(v)
tname = name(v)
return func(v), tname
def name(v):
return type(v).__name__
def serializer(type_):
if type_ == 'bool':
return lambda x: "true" if x else ""
return lambda x: x
def deserializer(type_):
try:
# Check if it's a builtin type
module = importlib.import_module('builtins')
cls = getattr(module, type_)
except AttributeError:
# if not, separate module and class
module, type_ = type_.rsplit(".", 1)
module = importlib.import_module(module)
cls = getattr(module, type_)
return cls
def convert(value, type_):
return deserializer(type_)(value)

20
soil/version.py Normal file
View File

@@ -0,0 +1,20 @@
import os
import logging
logger = logging.getLogger(__name__)
ROOT = os.path.dirname(__file__)
DEFAULT_FILE = os.path.join(ROOT, 'VERSION')
def read_version(versionfile=DEFAULT_FILE):
try:
with open(versionfile) as f:
return f.read().strip()
except IOError: # pragma: no cover
logger.error(('Running an unknown version of {}.'
'Be careful!.').format(__name__))
return '0.0'
__version__ = read_version()

16
tests/test.csv Normal file
View File

@@ -0,0 +1,16 @@
agent_id,t_step,key,value,value_type
a0,0,hello,w,str
a0,1,hello,o,str
a0,2,hello,r,str
a0,3,hello,l,str
a0,4,hello,d,str
a0,5,hello,!,str
env,1,started,,bool
env,2,started,True,bool
env,7,started,,bool
a0,0,hello,w,str
a0,1,hello,o,str
a0,2,hello,r,str
a0,3,hello,l,str
a0,4,hello,d,str
a0,5,hello,!,str
1 agent_id t_step key value value_type
2 a0 0 hello w str
3 a0 1 hello o str
4 a0 2 hello r str
5 a0 3 hello l str
6 a0 4 hello d str
7 a0 5 hello ! str
8 env 1 started bool
9 env 2 started True bool
10 env 7 started bool
11 a0 0 hello w str
12 a0 1 hello o str
13 a0 2 hello r str
14 a0 3 hello l str
15 a0 4 hello d str
16 a0 5 hello ! str

90
tests/test_analysis.py Normal file
View File

@@ -0,0 +1,90 @@
from unittest import TestCase
import os
import pandas as pd
import yaml
from functools import partial
from os.path import join
from soil import simulation, analysis, agents
ROOT = os.path.abspath(os.path.dirname(__file__))
class Ping(agents.FSM):
defaults = {
'count': 0,
}
@agents.default_state
@agents.state
def even(self):
self['count'] += 1
return self.odd
@agents.state
def odd(self):
self['count'] += 1
return self.even
class TestAnalysis(TestCase):
# Code to generate a simple sqlite history
def setUp(self):
"""
The initial states should be applied to the agent and the
agent should be able to update its state."""
config = {
'name': 'analysis',
'dry_run': True,
'seed': 'seed',
'network_params': {
'generator': 'complete_graph',
'n': 2
},
'agent_type': Ping,
'states': [{'interval': 1}, {'interval': 2}],
'max_time': 30,
'num_trials': 1,
'environment_params': {
}
}
s = simulation.from_config(config)
self.env = s.run_simulation()[0]
def test_saved(self):
env = self.env
assert env.get_agent(0)['count', 0] == 1
assert env.get_agent(0)['count', 29] == 30
assert env.get_agent(1)['count', 0] == 1
assert env.get_agent(1)['count', 29] == 15
assert env['env', 29, None]['SEED'] == env['env', 29, 'SEED']
def test_count(self):
env = self.env
df = analysis.read_sql(env._history._db)
res = analysis.get_count(df, 'SEED', 'id')
assert res['SEED']['seedanalysis_trial_0'].iloc[0] == 1
assert res['SEED']['seedanalysis_trial_0'].iloc[-1] == 1
assert res['id']['odd'].iloc[0] == 2
assert res['id']['even'].iloc[0] == 0
assert res['id']['odd'].iloc[-1] == 1
assert res['id']['even'].iloc[-1] == 1
def test_value(self):
env = self.env
df = analysis.read_sql(env._history._db)
res_sum = analysis.get_value(df, 'count')
assert res_sum['count'].iloc[0] == 2
import numpy as np
res_mean = analysis.get_value(df, 'count', aggfunc=np.mean)
assert res_mean['count'].iloc[0] == 1
res_total = analysis.get_value(df)
res_total['SEED'].iloc[0] == 'seedanalysis_trial_0'

133
tests/test_history.py Normal file
View File

@@ -0,0 +1,133 @@
from unittest import TestCase
import os
import shutil
from glob import glob
from soil import history
ROOT = os.path.abspath(os.path.dirname(__file__))
DBROOT = os.path.join(ROOT, 'testdb')
class TestHistory(TestCase):
def setUp(self):
if not os.path.exists(DBROOT):
os.makedirs(DBROOT)
def tearDown(self):
if os.path.exists(DBROOT):
shutil.rmtree(DBROOT)
def test_history(self):
"""
"""
tuples = (
('a_0', 0, 'id', 'h'),
('a_0', 1, 'id', 'e'),
('a_0', 2, 'id', 'l'),
('a_0', 3, 'id', 'l'),
('a_0', 4, 'id', 'o'),
('a_1', 0, 'id', 'v'),
('a_1', 1, 'id', 'a'),
('a_1', 2, 'id', 'l'),
('a_1', 3, 'id', 'u'),
('a_1', 4, 'id', 'e'),
('env', 1, 'prob', 1),
('env', 3, 'prob', 2),
('env', 5, 'prob', 3),
('a_2', 7, 'finished', True),
)
h = history.History()
h.save_tuples(tuples)
# assert h['env', 0, 'prob'] == 0
for i in range(1, 7):
assert h['env', i, 'prob'] == ((i-1)//2)+1
for i, k in zip(range(5), 'hello'):
assert h['a_0', i, 'id'] == k
for record, value in zip(h['a_0', None, 'id'], 'hello'):
t_step, val = record
assert val == value
for i, k in zip(range(5), 'value'):
assert h['a_1', i, 'id'] == k
for i in range(5, 8):
assert h['a_1', i, 'id'] == 'e'
for i in range(7):
assert h['a_2', i, 'finished'] == False
assert h['a_2', 7, 'finished']
def test_history_gen(self):
"""
"""
tuples = (
('a_1', 0, 'id', 'v'),
('a_1', 1, 'id', 'a'),
('a_1', 2, 'id', 'l'),
('a_1', 3, 'id', 'u'),
('a_1', 4, 'id', 'e'),
('env', 1, 'prob', 1),
('env', 2, 'prob', 2),
('env', 3, 'prob', 3),
('a_2', 7, 'finished', True),
)
h = history.History()
h.save_tuples(tuples)
for t_step, key, value in h['env', None, None]:
assert t_step == value
assert key == 'prob'
records = list(h[None, 7, None])
assert len(records) == 3
for i in records:
agent_id, key, value = i
if agent_id == 'a_1':
assert key == 'id'
assert value == 'e'
elif agent_id == 'a_2':
assert key == 'finished'
assert value
else:
assert key == 'prob'
assert value == 3
records = h['a_1', 7, None]
assert records['id'] == 'e'
def test_history_file(self):
"""
History should be saved to a file
"""
tuples = (
('a_1', 0, 'id', 'v'),
('a_1', 1, 'id', 'a'),
('a_1', 2, 'id', 'l'),
('a_1', 3, 'id', 'u'),
('a_1', 4, 'id', 'e'),
('env', 1, 'prob', 1),
('env', 2, 'prob', 2),
('env', 3, 'prob', 3),
('a_2', 7, 'finished', True),
)
db_path = os.path.join(DBROOT, 'test')
h = history.History(db_path=db_path)
h.save_tuples(tuples)
assert os.path.exists(db_path)
# Recover the data
recovered = history.History(db_path=db_path, backup=False)
assert recovered['a_1', 0, 'id'] == 'v'
assert recovered['a_1', 4, 'id'] == 'e'
# Using the same name should create a backup copy
newhistory = history.History(db_path=db_path, backup=True)
backuppaths = glob(db_path + '.backup*.sqlite')
assert len(backuppaths) == 1
backuppath = backuppaths[0]
assert newhistory._db_path == h._db_path
assert os.path.exists(backuppath)
assert not len(newhistory[None, None, None])

View File

@@ -2,10 +2,11 @@ from unittest import TestCase
import os
import yaml
import networkx as nx
from functools import partial
from os.path import join
from soil import simulation, agents, utils
from soil import simulation, environment, agents, utils
ROOT = os.path.abspath(os.path.dirname(__file__))
@@ -21,6 +22,7 @@ class TestMain(TestCase):
Raise an exception otherwise.
"""
config = {
'dry_run': True,
'network_params': {
'path': join(ROOT, 'test.gexf')
}
@@ -30,6 +32,7 @@ class TestMain(TestCase):
assert len(G) == 2
with self.assertRaises(AttributeError):
config = {
'dry_run': True,
'network_params': {
'path': join(ROOT, 'unknown.extension')
}
@@ -43,6 +46,7 @@ class TestMain(TestCase):
should be used to generate a network
"""
config = {
'dry_run': True,
'network_params': {
'generator': 'barabasi_albert_graph'
}
@@ -57,43 +61,48 @@ class TestMain(TestCase):
def test_empty_simulation(self):
"""A simulation with a base behaviour should do nothing"""
config = {
'dry_run': True,
'network_params': {
'path': join(ROOT, 'test.gexf')
},
'agent_type': 'NetworkAgent',
'agent_type': 'BaseAgent',
'environment_params': {
}
}
s = simulation.from_config(config)
s.run_simulation()
s.run_simulation(dry_run=True)
def test_counter_agent(self):
"""
The initial states should be applied to the agent and the
agent should be able to update its state."""
config = {
'name': 'CounterAgent',
'dry_run': True,
'network_params': {
'path': join(ROOT, 'test.gexf')
},
'agent_type': 'CounterModel',
'states': [{'neighbors': 10}, {'total': 12}],
'states': [{'times': 10}, {'times': 20}],
'max_time': 2,
'num_trials': 1,
'environment_params': {
}
}
s = simulation.from_config(config)
env = s.run_simulation()[0]
assert env.get_agent(0)['neighbors', 0] == 10
assert env.get_agent(0)['neighbors', 1] == 1
assert env.get_agent(1)['total', 0] == 12
assert env.get_agent(1)['neighbors', 1] == 1
env = s.run_simulation(dry_run=True)[0]
assert env.get_agent(0)['times', 0] == 11
assert env.get_agent(0)['times', 1] == 12
assert env.get_agent(1)['times', 0] == 21
assert env.get_agent(1)['times', 1] == 22
def test_counter_agent_history(self):
"""
The evolution of the state should be recorded in the logging agent
"""
config = {
'name': 'CounterAgent',
'dry_run': True,
'network_params': {
'path': join(ROOT, 'test.gexf')
},
@@ -108,22 +117,22 @@ class TestMain(TestCase):
}
}
s = simulation.from_config(config)
env = s.run_simulation()[0]
env = s.run_simulation(dry_run=True)[0]
for agent in env.network_agents:
last = 0
assert len(agent._history) == 11
for step, total in agent['total', None].items():
if step > 0:
assert total == last + 2
last = total
assert len(agent[None, None]) == 10
for step, total in sorted(agent['total', None]):
assert total == last + 2
last = total
def test_custom_agent(self):
"""Allow for search of neighbors with a certain state_id"""
class CustomAgent(agents.NetworkAgent):
class CustomAgent(agents.BaseAgent):
def step(self):
self.state['neighbors'] = self.count_agents(state_id=0,
limit_neighbors=True)
config = {
'dry_run': True,
'network_params': {
'path': join(ROOT, 'test.gexf')
},
@@ -138,7 +147,7 @@ class TestMain(TestCase):
}
}
s = simulation.from_config(config)
env = s.run_simulation()[0]
env = s.run_simulation(dry_run=True)[0]
assert env.get_agent(0).state['neighbors'] == 1
def test_torvalds_example(self):
@@ -147,6 +156,7 @@ class TestMain(TestCase):
config['network_params']['path'] = join(EXAMPLES,
config['network_params']['path'])
s = simulation.from_config(config)
s.dry_run = True
env = s.run_simulation()[0]
for a in env.network_agents:
skill_level = a.state['skill_level']
@@ -171,12 +181,15 @@ class TestMain(TestCase):
with utils.timer('loading'):
config = utils.load_file(join(EXAMPLES, 'complete.yml'))[0]
s = simulation.from_config(config)
s.dry_run = True
with utils.timer('serializing'):
serial = s.to_yaml()
with utils.timer('recovering'):
recovered = yaml.load(serial)
with utils.timer('deleting'):
del recovered['topology']
del recovered['load_module']
del recovered['dry_run']
assert config == recovered
def test_configuration_changes(self):
@@ -186,16 +199,43 @@ class TestMain(TestCase):
"""
config = utils.load_file('examples/complete.yml')[0]
s = simulation.from_config(config)
s.dry_run = True
for i in range(5):
s.run_simulation()
s.run_simulation(dry_run=True)
nconfig = s.to_dict()
del nconfig['topology']
del nconfig['dry_run']
del nconfig['load_module']
assert config == nconfig
def test_examples(self):
"""
Make sure all examples in the examples folder are correct
"""
pass
def test_row_conversion(self):
env = environment.SoilEnvironment(dry_run=True)
env['test'] = 'test_value'
res = list(env.history_to_tuples())
assert len(res) == len(env.environment_params)
env._now = 1
env['test'] = 'second_value'
res = list(env.history_to_tuples())
assert env['env', 0, 'test' ] == 'test_value'
assert env['env', 1, 'test' ] == 'second_value'
def test_save_geometric(self):
"""
There is a bug in networkx that prevents it from creating a GEXF file
from geometric models. We should work around it.
"""
G = nx.random_geometric_graph(20,0.1)
env = environment.SoilEnvironment(topology=G, dry_run=True)
env.dump_gexf('/tmp/dump-gexf')
def make_example_test(path, config):
@@ -203,11 +243,15 @@ def make_example_test(path, config):
root = os.getcwd()
os.chdir(os.path.dirname(path))
s = simulation.from_config(config)
envs = s.run_simulation()
envs = s.run_simulation(dry_run=True)
assert envs
for env in envs:
n = config['network_params'].get('n', None)
if n is not None:
assert env
try:
n = config['network_params']['n']
assert len(env.get_agents()) == n
except KeyError:
pass
os.chdir(root)
return wrapped