Compare commits
37 Commits
TFG-David
...
0.13.4-fix
Author | SHA1 | Date | |
---|---|---|---|
|
90338c3549 | ||
|
1d532dacfe | ||
|
a1f8d8c9c5 | ||
|
de326eb331 | ||
|
04b4380c61 | ||
|
d70a0c865c | ||
|
625c28e4ee | ||
|
9749f4ca14 | ||
|
3526fa29d7 | ||
|
53604c1e66 | ||
|
01cc8e9128 | ||
|
a47ffa815b | ||
|
b41927d7bf | ||
|
70d033b3a9 | ||
|
3afed06656 | ||
|
0a7ef27844 | ||
|
2e28b36f6e | ||
|
bd4700567e | ||
|
ff1df62eec | ||
|
9165979b49 | ||
|
078f8ace9e | ||
|
8fec544772 | ||
|
5420501d36 | ||
|
5d89827ccf | ||
|
fc48ed7e09 | ||
|
73c90887e8 | ||
|
497c8a55db | ||
|
7d1c800490 | ||
|
a4b32afa2f | ||
|
a7c51742f6 | ||
|
78364d89d5 | ||
|
af76f54a28 | ||
|
dbc182c6d0 | ||
|
eafecc9e5e | ||
|
e8988015e2 | ||
|
ccc8e43416 | ||
|
347d295b09 |
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
**/soil_output
|
||||
.*
|
26
.gitlab-ci.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image:
|
||||
name: gcr.io/kaniko-project/executor:debug
|
||||
entrypoint: [""]
|
||||
tags:
|
||||
- docker
|
||||
script:
|
||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
|
||||
# The skip-tls-verify flag is there because our registry certificate is self signed
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR --skip-tls-verify --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
|
||||
only:
|
||||
- tags
|
||||
|
||||
|
||||
test:
|
||||
tags:
|
||||
- docker
|
||||
image: python:3.7
|
||||
stage: test
|
||||
script:
|
||||
- python setup.py test
|
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM python:3.7
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY test-requirements.txt requirements.txt /usr/src/app/
|
||||
RUN pip install --no-cache-dir -r test-requirements.txt -r requirements.txt
|
||||
|
||||
COPY ./ /usr/src/app
|
||||
|
||||
RUN pip install '.[web]'
|
||||
|
||||
ENTRYPOINT ["python", "-m", "soil"]
|
4
Makefile
Normal file
@@ -0,0 +1,4 @@
|
||||
test:
|
||||
docker-compose exec dev python -m pytest -s -v
|
||||
|
||||
.PHONY: test
|
@@ -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:
|
||||
|
||||
|
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
version: '3'
|
||||
services:
|
||||
dev:
|
||||
build: .
|
||||
environment:
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
volumes:
|
||||
- .:/usr/src/app
|
||||
tty: true
|
||||
entrypoint: /bin/bash
|
||||
ports:
|
||||
- '8001:8001'
|
244
docs/configuration.rst
Normal file
@@ -0,0 +1,244 @@
|
||||
Configuring a simulation
|
||||
------------------------
|
||||
|
||||
There are two ways to configure a simulation: programmatically and with a configuration file.
|
||||
In both cases, the parameters used are the same.
|
||||
The advantage of a configuration file is that it is a clean declarative description, and it makes it easier to reproduce.
|
||||
|
||||
Simulation configuration files can be formatted in ``json`` or ``yaml`` and they define all the parameters of a simulation.
|
||||
Here's an example (``example.yml``).
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
---
|
||||
name: MyExampleSimulation
|
||||
max_time: 50
|
||||
num_trials: 3
|
||||
interval: 2
|
||||
network_params:
|
||||
generator: barabasi_albert_graph
|
||||
n: 100
|
||||
m: 2
|
||||
network_agents:
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
state:
|
||||
id: content
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
state:
|
||||
id: discontent
|
||||
- agent_type: SISaModel
|
||||
weight: 8
|
||||
state:
|
||||
id: neutral
|
||||
environment_params:
|
||||
prob_infect: 0.075
|
||||
|
||||
|
||||
This example configuration will run three trials (``num_trials``) of a simulation containing a randomly generated network (``network_params``).
|
||||
The 100 nodes in the network will be SISaModel agents (``network_agents.agent_type``), which is an agent behavior that is included in Soil.
|
||||
10% of the agents (``weight=1``) will start in the content state, 10% in the discontent state, and the remaining 80% (``weight=8``) in the neutral state.
|
||||
All agents will have access to the environment (``environment_params``), 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
|
||||
|
||||
soil example.yml
|
||||
|
||||
Once the simulation finishes, its results will be stored in a folder named ``MyExampleSimulation``.
|
||||
Three 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 ``sqlite`` file with the content of the state of every network node and the environment parameters at every step of the simulation.
|
||||
|
||||
|
||||
.. code::
|
||||
|
||||
soil_output
|
||||
└── MyExampleSimulation
|
||||
├── MyExampleSimulation.dumped.yml
|
||||
├── MyExampleSimulation.simulation.pickle
|
||||
├── MyExampleSimulation_trial_0.db.sqlite
|
||||
├── MyExampleSimulation_trial_1.db.sqlite
|
||||
└── MyExampleSimulation_trial_2.db.sqlite
|
||||
|
||||
|
||||
You may also ask soil to export the states in a ``csv`` file, and the network in gephi format (``gexf``).
|
||||
|
||||
Network
|
||||
=======
|
||||
|
||||
The network topology for the simulation can be loaded from an existing network file or generated with one of the random network generation methods from networkx.
|
||||
|
||||
Loading a network
|
||||
#################
|
||||
|
||||
To load an existing network, specify its path in the configuration:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
---
|
||||
network_params:
|
||||
path: /tmp/mynetwork.gexf
|
||||
|
||||
Soil will try to guess what networkx method to use to read the file based on its extension.
|
||||
However, we only test using ``gexf`` files.
|
||||
|
||||
For simple networks, you may also include them in the configuration itself using , using the ``topology`` parameter like so:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
---
|
||||
topology:
|
||||
nodes:
|
||||
- id: First
|
||||
- id: Second
|
||||
links:
|
||||
- source: First
|
||||
target: Second
|
||||
|
||||
|
||||
Generating a random network
|
||||
###########################
|
||||
|
||||
To generate a random network using one of networkx's built-in methods, specify the `graph generation algorithm <https://networkx.github.io/documentation/development/reference/generators.html>`_ and other parameters.
|
||||
For example, the following configuration is equivalent to :code:`nx.complete_graph(n=100)`:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
network_params:
|
||||
generator: complete_graph
|
||||
n: 100
|
||||
|
||||
Environment
|
||||
============
|
||||
The environment is the place where the shared state of the simulation is stored.
|
||||
For instance, the probability of disease outbreak.
|
||||
The configuration file may specify the initial value of the environment parameters:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
environment_params:
|
||||
daily_probability_of_earthquake: 0.001
|
||||
number_of_earthquakes: 0
|
||||
|
||||
All agents have access to the environment parameters.
|
||||
|
||||
In some scenarios, it is useful to have a custom environment, to provide additional methods or to control the way agents update environment state.
|
||||
For example, if our agents play the lottery, the environment could provide a method to decide whether the agent wins, instead of leaving it to the agent.
|
||||
|
||||
|
||||
Agents
|
||||
======
|
||||
Agents are a way of modelling behavior.
|
||||
Agents can be characterized with two variables: agent type (``agent_type``) and state.
|
||||
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 and environment agent.
|
||||
|
||||
Network Agents
|
||||
##############
|
||||
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 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 (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
|
||||
|
||||
network_agents:
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
- agent_type: CounterModel
|
||||
weight: 5
|
||||
|
||||
The third option is to specify the type of agent on the node itself, e.g.:
|
||||
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
topology:
|
||||
nodes:
|
||||
- id: first
|
||||
agent_type: BaseAgent
|
||||
states:
|
||||
first:
|
||||
agent_type: SISaModel
|
||||
|
||||
|
||||
This would also work with a randomly generated network:
|
||||
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
network:
|
||||
generator: complete
|
||||
n: 5
|
||||
agent_type: BaseAgent
|
||||
states:
|
||||
- agent_type: SISaModel
|
||||
|
||||
|
||||
|
||||
In addition to agent type, you may add a custom initial state to the distribution.
|
||||
This is very useful to add the same agent type with different states.
|
||||
e.g., to populate the network with SISaModel, roughly 10% of them with a discontent state:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
network_agents:
|
||||
- agent_type: SISaModel
|
||||
weight: 9
|
||||
state:
|
||||
id: neutral
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
state:
|
||||
id: discontent
|
||||
|
||||
Lastly, the configuration may include initial state for one or more nodes.
|
||||
For instance, to add a state for the two nodes in this configuration:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
agent_type: SISaModel
|
||||
network:
|
||||
generator: complete_graph
|
||||
n: 2
|
||||
states:
|
||||
- id: content
|
||||
- id: discontent
|
||||
|
||||
|
||||
Or to add state only to specific nodes (by ``id``).
|
||||
For example, to apply special skills to Linux Torvalds in a simulation:
|
||||
|
||||
.. literalinclude:: ../examples/torvalds.yml
|
||||
:language: yaml
|
||||
|
||||
|
||||
Environment Agents
|
||||
##################
|
||||
In addition to network agents, more agents can be added to the simulation.
|
||||
These agents are programmed in much the same way as network agents, the only difference is that they will not be assigned to network nodes.
|
||||
|
||||
|
||||
.. code::
|
||||
|
||||
environment_agents:
|
||||
- agent_type: MyAgent
|
||||
state:
|
||||
mood: happy
|
||||
- agent_type: DummyAgent
|
||||
|
||||
|
||||
You may use environment agents to model events that a normal agent cannot control, such as natural disasters or chance.
|
||||
They are also useful to add behavior that has little to do with the network and the interactions within that network.
|
@@ -6,7 +6,7 @@
|
||||
Welcome to Soil's documentation!
|
||||
================================
|
||||
|
||||
Soil is an Agent-based Social Simulator in Python for modelling and simulation of Social Networks.
|
||||
Soil is an Agent-based Social Simulator in Python focused on Social Networks.
|
||||
|
||||
If you use Soil in your research, do not forget to cite this paper:
|
||||
|
||||
@@ -34,13 +34,15 @@ 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
|
||||
configuration
|
||||
Tutorial <soil_tutorial>
|
||||
|
||||
..
|
||||
|
||||
|
||||
.. Indices and tables
|
||||
|
@@ -1,7 +1,7 @@
|
||||
Installation
|
||||
------------
|
||||
|
||||
The easiest way to install Soil is through pip:
|
||||
The easiest way to install Soil is through pip, with Python >= 3.4:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 11 KiB |
BIN
docs/output_54_0.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
docs/output_54_1.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
docs/output_55_0.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
docs/output_55_1.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
docs/output_55_2.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/output_55_3.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/output_55_4.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
docs/output_55_5.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
docs/output_55_6.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/output_55_7.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/output_55_8.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/output_55_9.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/output_56_0.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
docs/output_56_1.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
docs/output_56_2.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
docs/output_56_3.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
docs/output_56_4.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
docs/output_56_5.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
docs/output_56_6.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
docs/output_56_7.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
docs/output_56_8.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
docs/output_56_9.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
docs/output_61_0.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
docs/output_63_1.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
docs/output_66_1.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
docs/output_67_1.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
docs/output_72_0.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
docs/output_72_1.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
docs/output_74_1.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/output_75_1.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
docs/output_76_1.png
Normal file
After Width: | Height: | Size: 19 KiB |
@@ -1,194 +1,93 @@
|
||||
Quickstart
|
||||
----------
|
||||
|
||||
This section shows how to run simulations from simulation configuration files.
|
||||
First of all, you need to install the package (See :doc:`installation`)
|
||||
This section shows how to run your first simulation with Soil.
|
||||
For installation instructions, see :doc:`installation`.
|
||||
|
||||
Simulation configuration files are ``json`` or ``yaml`` files that define all the parameters of a simulation.
|
||||
Here's an example (``example.yml``).
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
---
|
||||
name: MyExampleSimulation
|
||||
max_time: 50
|
||||
num_trials: 3
|
||||
timeout: 2
|
||||
network_params:
|
||||
network_type: barabasi_albert_graph
|
||||
n: 100
|
||||
m: 2
|
||||
agent_distribution:
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
state:
|
||||
id: content
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
state:
|
||||
id: discontent
|
||||
- agent_type: SISaModel
|
||||
weight: 8
|
||||
state:
|
||||
id: neutral
|
||||
environment_params:
|
||||
prob_infect: 0.075
|
||||
|
||||
Now run the simulation with the command line tool:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
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``).
|
||||
There are mainly two parts in a simulation: agent classes and simulation configuration.
|
||||
An agent class defines how the agent will behave throughout the simulation.
|
||||
The configuration includes things such as number of agents to use and their type, network topology to use, etc.
|
||||
|
||||
|
||||
.. code::
|
||||
|
||||
soil_output
|
||||
├── Sim_prob_0
|
||||
│ ├── Sim_prob_0.dumped.yml
|
||||
│ ├── Sim_prob_0.simulation.pickle
|
||||
│ ├── Sim_prob_0_trial_0.environment.csv
|
||||
│ └── Sim_prob_0_trial_0.gexf
|
||||
.. image:: soil.png
|
||||
:width: 80%
|
||||
:align: center
|
||||
|
||||
|
||||
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``).
|
||||
Soil includes several agent classes in the ``soil.agents`` module, and we will use them in this quickstart.
|
||||
If you are interested in developing your own agents classes, see :doc:`soil_tutorial`.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
To get you started, we will use this configuration (:download:`download the file <quickstart.yml>` directly):
|
||||
|
||||
Network
|
||||
=======
|
||||
|
||||
The network topology for the simulation can be loaded from an existing network file or generated with one of the random network generation methods from networkx.
|
||||
|
||||
Loading a network
|
||||
#################
|
||||
|
||||
To load an existing network, specify its path in the configuration:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
---
|
||||
network_params:
|
||||
path: /tmp/mynetwork.gexf
|
||||
|
||||
Soil will try to guess what networkx method to use to read the file based on its extension.
|
||||
However, we only test using ``gexf`` files.
|
||||
|
||||
Generating a random network
|
||||
###########################
|
||||
|
||||
To generate a random network using one of networkx's built-in methods, specify the `graph generation algorithm <https://networkx.github.io/documentation/development/reference/generators.html>`_ and other parameters.
|
||||
For example, the following configuration is equivalent to :code:`nx.complete_graph(100)`:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
network_params:
|
||||
network_type: complete_graph
|
||||
n: 100
|
||||
|
||||
Environment
|
||||
============
|
||||
The environment is the place where the shared state of the simulation is stored.
|
||||
For instance, the probability of certain events.
|
||||
The configuration file may specify the initial value of the environment parameters:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
environment_params:
|
||||
daily_probability_of_earthquake: 0.001
|
||||
number_of_earthquakes: 0
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Network Agents
|
||||
##############
|
||||
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.
|
||||
|
||||
.. 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``).
|
||||
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
|
||||
|
||||
agent_distribution:
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
- agent_type: CounterModel
|
||||
weight: 5
|
||||
|
||||
In addition to agent type, you may also add a custom initial state to the distribution.
|
||||
This is very useful to add the same agent type with different states.
|
||||
e.g., to populate the network with SISaModel, roughly 10% of them with a discontent state:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
agent_distribution:
|
||||
- agent_type: SISaModel
|
||||
weight: 9
|
||||
state:
|
||||
id: neutral
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
state:
|
||||
id: discontent
|
||||
|
||||
Lastly, the configuration may include initial state for one or more nodes.
|
||||
For instance, to add a state for the two nodes in this configuration:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
agent_type: SISaModel
|
||||
network:
|
||||
network_type: complete_graph
|
||||
n: 2
|
||||
states:
|
||||
- id: content
|
||||
- id: discontent
|
||||
|
||||
|
||||
Or to add state only to specific nodes (by ``id``).
|
||||
For example, to apply special skills to Linux Torvalds in a simulation:
|
||||
|
||||
.. literalinclude:: ../examples/torvalds.yml
|
||||
.. literalinclude:: quickstart.yml
|
||||
:language: yaml
|
||||
|
||||
The agent type used, SISa, is a very simple model.
|
||||
It only has three states (neutral, content and discontent),
|
||||
Its parameters are the probabilities to change from one state to another, either spontaneously or because of contagion from neighboring agents.
|
||||
|
||||
Environment Agents
|
||||
##################
|
||||
In addition to network agents, more agents can be added to the simulation.
|
||||
These agens are programmed in much the same way as network agents, the only difference is that they will not be assigned to network nodes.
|
||||
Running the simulation
|
||||
======================
|
||||
|
||||
To see the simulation in action, simply point soil to the configuration, and tell it to store the graph and the history of agent states and environment parameters at every point.
|
||||
|
||||
.. code::
|
||||
|
||||
environment_agents:
|
||||
- agent_type: MyAgent
|
||||
state:
|
||||
mood: happy
|
||||
- agent_type: DummyAgent
|
||||
❯ soil --graph --csv quickstart.yml [13:35:29]
|
||||
INFO:soil:Using config(s): quickstart
|
||||
INFO:soil:Dumping results to soil_output/quickstart : ['csv', 'gexf']
|
||||
INFO:soil:Starting simulation quickstart at 13:35:30.
|
||||
INFO:soil:Starting Simulation quickstart trial 0 at 13:35:30.
|
||||
INFO:soil:Finished Simulation quickstart trial 0 at 13:35:49 in 19.43677067756653 seconds
|
||||
INFO:soil:Starting Dumping simulation quickstart trial 0 at 13:35:49.
|
||||
INFO:soil:Finished Dumping simulation quickstart trial 0 at 13:35:51 in 1.7733407020568848 seconds
|
||||
INFO:soil:Dumping results to soil_output/quickstart
|
||||
INFO:soil:Finished simulation quickstart at 13:35:51 in 21.29862952232361 seconds
|
||||
|
||||
|
||||
Visualizing the results
|
||||
=======================
|
||||
The ``CSV`` file should look like this:
|
||||
|
||||
The simulation will return a dynamic graph .gexf file which could be visualized with
|
||||
.. code::
|
||||
|
||||
agent_id,t_step,key,value
|
||||
env,0,neutral_discontent_spon_prob,0.05
|
||||
env,0,neutral_discontent_infected_prob,0.1
|
||||
env,0,neutral_content_spon_prob,0.2
|
||||
env,0,neutral_content_infected_prob,0.4
|
||||
env,0,discontent_neutral,0.2
|
||||
env,0,discontent_content,0.05
|
||||
env,0,content_discontent,0.05
|
||||
env,0,variance_d_c,0.05
|
||||
env,0,variance_c_d,0.1
|
||||
|
||||
Results and visualization
|
||||
=========================
|
||||
|
||||
The environment variables are marked as ``agent_id`` env.
|
||||
Th exported values are only stored when they change.
|
||||
To find out how to get every key and value at every point in the simulation, check out the :doc:`soil_tutorial`.
|
||||
|
||||
The dynamic graph is exported as a .gexf file which could be visualized with
|
||||
`Gephi <https://gephi.org/users/download/>`__.
|
||||
Now it is your turn to experiment with the simulation.
|
||||
Change some of the parameters, such as the number of agents, the probability of becoming content, or the type of network, and see how the results change.
|
||||
|
||||
|
||||
Soil also includes a web server that allows you to upload your simulations, change parameters, and visualize the results, including a timeline of the network.
|
||||
To make it work, you have to install soil like this:
|
||||
|
||||
.. code::
|
||||
|
||||
pip install soil[web]
|
||||
|
||||
Once installed, the soil web UI can be run in two ways:
|
||||
|
||||
.. code::
|
||||
|
||||
soil-web
|
||||
|
||||
# OR
|
||||
|
||||
python -m soil.web
|
30
docs/quickstart.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: quickstart
|
||||
num_trials: 1
|
||||
max_time: 1000
|
||||
network_agents:
|
||||
- agent_type: SISaModel
|
||||
state:
|
||||
id: neutral
|
||||
weight: 1
|
||||
- agent_type: SISaModel
|
||||
state:
|
||||
id: content
|
||||
weight: 2
|
||||
network_params:
|
||||
n: 100
|
||||
k: 5
|
||||
p: 0.2
|
||||
generator: newman_watts_strogatz_graph
|
||||
environment_params:
|
||||
neutral_discontent_spon_prob: 0.05
|
||||
neutral_discontent_infected_prob: 0.1
|
||||
neutral_content_spon_prob: 0.2
|
||||
neutral_content_infected_prob: 0.4
|
||||
discontent_neutral: 0.2
|
||||
discontent_content: 0.05
|
||||
content_discontent: 0.05
|
||||
variance_d_c: 0.05
|
||||
variance_c_d: 0.1
|
||||
content_neutral: 0.1
|
||||
standard_variance: 0.1
|
BIN
docs/soil.png
Normal file
After Width: | Height: | Size: 43 KiB |
2612
docs/soil_tutorial.rst
Normal file
532
examples/NewsSpread.ipynb
Normal file
80808
examples/Untitled.ipynb
Normal file
@@ -2,8 +2,11 @@
|
||||
name: simple
|
||||
dir_path: "/tmp/"
|
||||
num_trials: 3
|
||||
dry_run: True
|
||||
max_time: 100
|
||||
interval: 1
|
||||
seed: "CompleteSeed!"
|
||||
dump: false
|
||||
network_params:
|
||||
generator: complete_graph
|
||||
n: 10
|
||||
@@ -15,10 +18,11 @@ network_agents:
|
||||
- agent_type: AggregatedCounter
|
||||
weight: 0.2
|
||||
environment_agents: []
|
||||
environment_class: Environment
|
||||
environment_params:
|
||||
am_i_complete: true
|
||||
default_state:
|
||||
incidents: 0
|
||||
states:
|
||||
- name: 'The first node'
|
||||
- name: 'The second node'
|
||||
- name: 'The second node'
|
||||
|
@@ -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}
|
@@ -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
|
767
examples/newsspread/NewsSpread.ipynb
Normal file
138
examples/newsspread/NewsSpread.yml
Normal 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
|
81
examples/newsspread/newsspread.py
Normal 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)
|
10
examples/pubcrawl/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Simulation of pubs and drinking pals that go from pub to pub.
|
||||
|
||||
Th custom environment includes a list of pubs and methods to allow agents to discover and enter pubs.
|
||||
There are two types of agents:
|
||||
|
||||
* Patron. A patron will do three things, in this order:
|
||||
* Look for other patrons to drink with
|
||||
* Look for a pub where the agent and other agents in the same group can get in.
|
||||
* While in the pub, patrons only drink, until they get drunk and taken home.
|
||||
* Police. There is only one police agent that will take any drunk patrons home (kick them out of the pub).
|
174
examples/pubcrawl/pubcrawl.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from soil.agents import FSM, state, default_state
|
||||
from soil import Environment
|
||||
from random import random, shuffle
|
||||
from itertools import islice
|
||||
import logging
|
||||
|
||||
|
||||
class CityPubs(Environment):
|
||||
'''Environment with Pubs'''
|
||||
level = logging.INFO
|
||||
|
||||
def __init__(self, *args, number_of_pubs=3, pub_capacity=10, **kwargs):
|
||||
super(CityPubs, self).__init__(*args, **kwargs)
|
||||
pubs = {}
|
||||
for i in range(number_of_pubs):
|
||||
newpub = {
|
||||
'name': 'The awesome pub #{}'.format(i),
|
||||
'open': True,
|
||||
'capacity': pub_capacity,
|
||||
'occupancy': 0,
|
||||
}
|
||||
pubs[newpub['name']] = newpub
|
||||
self['pubs'] = pubs
|
||||
|
||||
def enter(self, pub_id, *nodes):
|
||||
'''Agents will try to enter. The pub checks if it is possible'''
|
||||
try:
|
||||
pub = self['pubs'][pub_id]
|
||||
except KeyError:
|
||||
raise ValueError('Pub {} is not available'.format(pub_id))
|
||||
if not pub['open'] or (pub['capacity'] < (len(nodes) + pub['occupancy'])):
|
||||
return False
|
||||
pub['occupancy'] += len(nodes)
|
||||
for node in nodes:
|
||||
node['pub'] = pub_id
|
||||
return True
|
||||
|
||||
def available_pubs(self):
|
||||
for pub in self['pubs'].values():
|
||||
if pub['open'] and (pub['occupancy'] < pub['capacity']):
|
||||
yield pub['name']
|
||||
|
||||
def exit(self, pub_id, *node_ids):
|
||||
'''Agents will notify the pub they want to leave'''
|
||||
try:
|
||||
pub = self['pubs'][pub_id]
|
||||
except KeyError:
|
||||
raise ValueError('Pub {} is not available'.format(pub_id))
|
||||
for node_id in node_ids:
|
||||
node = self.get_agent(node_id)
|
||||
if pub_id == node['pub']:
|
||||
del node['pub']
|
||||
pub['occupancy'] -= 1
|
||||
|
||||
|
||||
class Patron(FSM):
|
||||
'''Agent that looks for friends to drink with. It will do three things:
|
||||
1) Look for other patrons to drink with
|
||||
2) Look for a bar where the agent and other agents in the same group can get in.
|
||||
3) While in the bar, patrons only drink, until they get drunk and taken home.
|
||||
'''
|
||||
level = logging.INFO
|
||||
|
||||
defaults = {
|
||||
'pub': None,
|
||||
'drunk': False,
|
||||
'pints': 0,
|
||||
'max_pints': 3,
|
||||
}
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def looking_for_friends(self):
|
||||
'''Look for friends to drink with'''
|
||||
self.info('I am looking for friends')
|
||||
available_friends = list(self.get_agents(drunk=False,
|
||||
pub=None,
|
||||
state_id=self.looking_for_friends.id))
|
||||
if not available_friends:
|
||||
self.info('Life sucks and I\'m alone!')
|
||||
return self.at_home
|
||||
befriended = self.try_friends(available_friends)
|
||||
if befriended:
|
||||
return self.looking_for_pub
|
||||
|
||||
@state
|
||||
def looking_for_pub(self):
|
||||
'''Look for a pub that accepts me and my friends'''
|
||||
if self['pub'] != None:
|
||||
return self.sober_in_pub
|
||||
self.debug('I am looking for a pub')
|
||||
group = list(self.get_neighboring_agents())
|
||||
for pub in self.env.available_pubs():
|
||||
self.debug('We\'re trying to get into {}: total: {}'.format(pub, len(group)))
|
||||
if self.env.enter(pub, self, *group):
|
||||
self.info('We\'re all {} getting in {}!'.format(len(group), pub))
|
||||
return self.sober_in_pub
|
||||
|
||||
@state
|
||||
def sober_in_pub(self):
|
||||
'''Drink up.'''
|
||||
self.drink()
|
||||
if self['pints'] > self['max_pints']:
|
||||
return self.drunk_in_pub
|
||||
|
||||
@state
|
||||
def drunk_in_pub(self):
|
||||
'''I'm out. Take me home!'''
|
||||
self.info('I\'m so drunk. Take me home!')
|
||||
self['drunk'] = True
|
||||
pass # out drunk
|
||||
|
||||
@state
|
||||
def at_home(self):
|
||||
'''The end'''
|
||||
self.debug('Life sucks. I\'m home!')
|
||||
|
||||
def drink(self):
|
||||
self['pints'] += 1
|
||||
self.debug('Cheers to that')
|
||||
|
||||
def kick_out(self):
|
||||
self.set_state(self.at_home)
|
||||
|
||||
def befriend(self, other_agent, force=False):
|
||||
'''
|
||||
Try to become friends with another agent. The chances of
|
||||
success depend on both agents' openness.
|
||||
'''
|
||||
if force or self['openness'] > random():
|
||||
self.env.add_edge(self, other_agent)
|
||||
self.info('Made some friend {}'.format(other_agent))
|
||||
return True
|
||||
return False
|
||||
|
||||
def try_friends(self, others):
|
||||
''' Look for random agents around me and try to befriend them'''
|
||||
befriended = False
|
||||
k = int(10*self['openness'])
|
||||
shuffle(others)
|
||||
for friend in islice(others, k): # random.choice >= 3.7
|
||||
if friend == self:
|
||||
continue
|
||||
if friend.befriend(self):
|
||||
self.befriend(friend, force=True)
|
||||
self.debug('Hooray! new friend: {}'.format(friend.id))
|
||||
befriended = True
|
||||
else:
|
||||
self.debug('{} does not want to be friends'.format(friend.id))
|
||||
return befriended
|
||||
|
||||
|
||||
class Police(FSM):
|
||||
'''Simple agent to take drunk people out of pubs.'''
|
||||
level = logging.INFO
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def patrol(self):
|
||||
drunksters = list(self.get_agents(drunk=True,
|
||||
state_id=Patron.drunk_in_pub.id))
|
||||
for drunk in drunksters:
|
||||
self.info('Kicking out the trash: {}'.format(drunk.id))
|
||||
drunk.kick_out()
|
||||
else:
|
||||
self.info('No trash to take out. Too bad.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from soil import simulation
|
||||
simulation.run_from_config('pubcrawl.yml',
|
||||
dry_run=True,
|
||||
dump=None,
|
||||
parallel=False)
|
26
examples/pubcrawl/pubcrawl.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: pubcrawl
|
||||
num_trials: 3
|
||||
max_time: 10
|
||||
dump: false
|
||||
network_params:
|
||||
# Generate 100 empty nodes. They will be assigned a network agent
|
||||
generator: empty_graph
|
||||
n: 30
|
||||
network_agents:
|
||||
- agent_type: pubcrawl.Patron
|
||||
description: Extroverted patron
|
||||
state:
|
||||
openness: 1.0
|
||||
weight: 9
|
||||
- agent_type: pubcrawl.Patron
|
||||
description: Introverted patron
|
||||
state:
|
||||
openness: 0.1
|
||||
weight: 1
|
||||
environment_agents:
|
||||
- agent_type: pubcrawl.Police
|
||||
environment_class: pubcrawl.CityPubs
|
||||
environment_params:
|
||||
altercations: 0
|
||||
number_of_pubs: 3
|
120
examples/rabbits/rabbit_agents.py
Normal 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()
|
23
examples/rabbits/rabbits.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
load_module: rabbit_agents
|
||||
name: rabbits_example
|
||||
max_time: 500
|
||||
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: []
|
@@ -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'
|
||||
|
23569
examples/tutorial/soil_tutorial.html
Normal file
1350
examples/tutorial/soil_tutorial.ipynb
Normal file
596
models_org.py
@@ -1,596 +0,0 @@
|
||||
from nxsim import BaseNetworkAgent
|
||||
import numpy as np
|
||||
import random
|
||||
import settings
|
||||
|
||||
settings.init()
|
||||
|
||||
##############################
|
||||
# Variables initialization #
|
||||
##############################
|
||||
def init():
|
||||
global networkStatus
|
||||
networkStatus = {} # Dict that will contain the status of every agent in the network
|
||||
|
||||
sentimentCorrelationNodeArray=[]
|
||||
for x in range(0, settings.number_of_nodes):
|
||||
sentimentCorrelationNodeArray.append({'id':x})
|
||||
# Initialize agent states. Let's assume everyone is normal.
|
||||
init_states = [{'id': 0, } for _ in range(settings.number_of_nodes)] # add keys as as necessary, but "id" must always refer to that state category
|
||||
|
||||
|
||||
####################
|
||||
# Available models #
|
||||
####################
|
||||
|
||||
class BaseBehaviour(BaseNetworkAgent):
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
self._attrs = {}
|
||||
|
||||
@property
|
||||
def attrs(self):
|
||||
now = self.env.now
|
||||
if now not in self._attrs:
|
||||
self._attrs[now] = {}
|
||||
return self._attrs[now]
|
||||
|
||||
@attrs.setter
|
||||
def attrs(self, value):
|
||||
self._attrs[self.env.now] = value
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
self.step(self.env.now)
|
||||
yield self.env.timeout(settings.timeout)
|
||||
|
||||
def step(self, now):
|
||||
networkStatus['agent_%s'% self.id] = self.to_json()
|
||||
|
||||
def to_json(self):
|
||||
final = {}
|
||||
for stamp, attrs in self._attrs.items():
|
||||
for a in attrs:
|
||||
if a not in final:
|
||||
final[a] = {}
|
||||
final[a][stamp] = attrs[a]
|
||||
return final
|
||||
|
||||
class ControlModelM2(BaseBehaviour):
|
||||
#Init infected
|
||||
init_states[random.randint(0,settings.number_of_nodes-1)] = {'id':1}
|
||||
init_states[random.randint(0,settings.number_of_nodes-1)] = {'id':1}
|
||||
|
||||
# Init beacons
|
||||
init_states[random.randint(0, settings.number_of_nodes-1)] = {'id': 4}
|
||||
init_states[random.randint(0, settings.number_of_nodes-1)] = {'id': 4}
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
|
||||
self.prob_neutral_making_denier = np.random.normal(settings.prob_neutral_making_denier, settings.standard_variance)
|
||||
|
||||
self.prob_infect = np.random.normal(settings.prob_infect, settings.standard_variance)
|
||||
|
||||
self.prob_cured_healing_infected = np.random.normal(settings.prob_cured_healing_infected, settings.standard_variance)
|
||||
self.prob_cured_vaccinate_neutral = np.random.normal(settings.prob_cured_vaccinate_neutral, settings.standard_variance)
|
||||
|
||||
self.prob_vaccinated_healing_infected = np.random.normal(settings.prob_vaccinated_healing_infected, settings.standard_variance)
|
||||
self.prob_vaccinated_vaccinate_neutral = np.random.normal(settings.prob_vaccinated_vaccinate_neutral, settings.standard_variance)
|
||||
self.prob_generate_anti_rumor = np.random.normal(settings.prob_generate_anti_rumor, settings.standard_variance)
|
||||
|
||||
def step(self, now):
|
||||
|
||||
if self.state['id'] == 0: #Neutral
|
||||
self.neutral_behaviour()
|
||||
elif self.state['id'] == 1: #Infected
|
||||
self.infected_behaviour()
|
||||
elif self.state['id'] == 2: #Cured
|
||||
self.cured_behaviour()
|
||||
elif self.state['id'] == 3: #Vaccinated
|
||||
self.vaccinated_behaviour()
|
||||
elif self.state['id'] == 4: #Beacon-off
|
||||
self.beacon_off_behaviour()
|
||||
elif self.state['id'] == 5: #Beacon-on
|
||||
self.beacon_on_behaviour()
|
||||
|
||||
self.attrs['status'] = self.state['id']
|
||||
super().step(now)
|
||||
|
||||
|
||||
def neutral_behaviour(self):
|
||||
|
||||
# Infected
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
if len(infected_neighbors)>0:
|
||||
if random.random() < self.prob_neutral_making_denier:
|
||||
self.state['id'] = 3 # Vaccinated making denier
|
||||
|
||||
def infected_behaviour(self):
|
||||
|
||||
# Neutral
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_infect:
|
||||
neighbor.state['id'] = 1 # Infected
|
||||
|
||||
def cured_behaviour(self):
|
||||
|
||||
# Vaccinate
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_cured_vaccinate_neutral:
|
||||
neighbor.state['id'] = 3 # Vaccinated
|
||||
|
||||
# Cure
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors:
|
||||
if random.random() < self.prob_cured_healing_infected:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
|
||||
def vaccinated_behaviour(self):
|
||||
|
||||
# Cure
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors:
|
||||
if random.random() < self.prob_cured_healing_infected:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
|
||||
# Vaccinate
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_cured_vaccinate_neutral:
|
||||
neighbor.state['id'] = 3 # Vaccinated
|
||||
|
||||
# Generate anti-rumor
|
||||
infected_neighbors_2 = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors_2:
|
||||
if random.random() < self.prob_generate_anti_rumor:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
def beacon_off_behaviour(self):
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
if len(infected_neighbors) > 0:
|
||||
self.state['id'] == 5 #Beacon on
|
||||
|
||||
def beacon_on_behaviour(self):
|
||||
|
||||
# Cure (M2 feature added)
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors:
|
||||
if random.random() < self.prob_generate_anti_rumor:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
neutral_neighbors_infected = neighbor.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors_infected:
|
||||
if random.random() < self.prob_generate_anti_rumor:
|
||||
neighbor.state['id'] = 3 # Vaccinated
|
||||
infected_neighbors_infected = neighbor.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors_infected:
|
||||
if random.random() < self.prob_generate_anti_rumor:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
|
||||
# Vaccinate
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_cured_vaccinate_neutral:
|
||||
neighbor.state['id'] = 3 # Vaccinated
|
||||
|
||||
|
||||
class SpreadModelM2(BaseBehaviour):
|
||||
init_states[random.randint(0,settings.number_of_nodes)] = {'id':1}
|
||||
init_states[random.randint(0,settings.number_of_nodes)] = {'id':1}
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
|
||||
self.prob_neutral_making_denier = np.random.normal(settings.prob_neutral_making_denier, settings.standard_variance)
|
||||
|
||||
self.prob_infect = np.random.normal(settings.prob_infect, settings.standard_variance)
|
||||
|
||||
self.prob_cured_healing_infected = np.random.normal(settings.prob_cured_healing_infected, settings.standard_variance)
|
||||
self.prob_cured_vaccinate_neutral = np.random.normal(settings.prob_cured_vaccinate_neutral, settings.standard_variance)
|
||||
|
||||
self.prob_vaccinated_healing_infected = np.random.normal(settings.prob_vaccinated_healing_infected, settings.standard_variance)
|
||||
self.prob_vaccinated_vaccinate_neutral = np.random.normal(settings.prob_vaccinated_vaccinate_neutral, settings.standard_variance)
|
||||
self.prob_generate_anti_rumor = np.random.normal(settings.prob_generate_anti_rumor, settings.standard_variance)
|
||||
|
||||
def step(self, now):
|
||||
|
||||
if self.state['id'] == 0: #Neutral
|
||||
self.neutral_behaviour()
|
||||
elif self.state['id'] == 1: #Infected
|
||||
self.infected_behaviour()
|
||||
elif self.state['id'] == 2: #Cured
|
||||
self.cured_behaviour()
|
||||
elif self.state['id'] == 3: #Vaccinated
|
||||
self.vaccinated_behaviour()
|
||||
|
||||
self.attrs['status'] = self.state['id']
|
||||
super().step(now)
|
||||
|
||||
|
||||
def neutral_behaviour(self):
|
||||
|
||||
# Infected
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
if len(infected_neighbors)>0:
|
||||
if random.random() < self.prob_neutral_making_denier:
|
||||
self.state['id'] = 3 # Vaccinated making denier
|
||||
|
||||
def infected_behaviour(self):
|
||||
|
||||
# Neutral
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_infect:
|
||||
neighbor.state['id'] = 1 # Infected
|
||||
|
||||
def cured_behaviour(self):
|
||||
|
||||
# Vaccinate
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_cured_vaccinate_neutral:
|
||||
neighbor.state['id'] = 3 # Vaccinated
|
||||
|
||||
# Cure
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors:
|
||||
if random.random() < self.prob_cured_healing_infected:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
|
||||
def vaccinated_behaviour(self):
|
||||
|
||||
# Cure
|
||||
infected_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors:
|
||||
if random.random() < self.prob_cured_healing_infected:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
|
||||
# Vaccinate
|
||||
neutral_neighbors = self.get_neighboring_agents(state_id=0)
|
||||
for neighbor in neutral_neighbors:
|
||||
if random.random() < self.prob_cured_vaccinate_neutral:
|
||||
neighbor.state['id'] = 3 # Vaccinated
|
||||
|
||||
# Generate anti-rumor
|
||||
infected_neighbors_2 = self.get_neighboring_agents(state_id=1)
|
||||
for neighbor in infected_neighbors_2:
|
||||
if random.random() < self.prob_generate_anti_rumor:
|
||||
neighbor.state['id'] = 2 # Cured
|
||||
|
||||
|
||||
class SISaModel(BaseBehaviour):
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
|
||||
self.neutral_discontent_spon_prob = np.random.normal(settings.neutral_discontent_spon_prob, settings.standard_variance)
|
||||
self.neutral_discontent_infected_prob = np.random.normal(settings.neutral_discontent_infected_prob,settings.standard_variance)
|
||||
self.neutral_content_spon_prob = np.random.normal(settings.neutral_content_spon_prob,settings.standard_variance)
|
||||
self.neutral_content_infected_prob = np.random.normal(settings.neutral_content_infected_prob,settings.standard_variance)
|
||||
|
||||
self.discontent_neutral = np.random.normal(settings.discontent_neutral,settings.standard_variance)
|
||||
self.discontent_content = np.random.normal(settings.discontent_content,settings.variance_d_c)
|
||||
|
||||
self.content_discontent = np.random.normal(settings.content_discontent,settings.variance_c_d)
|
||||
self.content_neutral = np.random.normal(settings.content_neutral,settings.standard_variance)
|
||||
|
||||
def step(self, now):
|
||||
|
||||
if self.state['id'] == 0:
|
||||
self.neutral_behaviour()
|
||||
if self.state['id'] == 1:
|
||||
self.discontent_behaviour()
|
||||
if self.state['id'] == 2:
|
||||
self.content_behaviour()
|
||||
|
||||
self.attrs['status'] = self.state['id']
|
||||
super().step(now)
|
||||
|
||||
|
||||
def neutral_behaviour(self):
|
||||
|
||||
#Spontaneus effects
|
||||
if random.random() < self.neutral_discontent_spon_prob:
|
||||
self.state['id'] = 1
|
||||
if random.random() < self.neutral_content_spon_prob:
|
||||
self.state['id'] = 2
|
||||
|
||||
#Infected
|
||||
discontent_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
if random.random() < len(discontent_neighbors)*self.neutral_discontent_infected_prob:
|
||||
self.state['id'] = 1
|
||||
content_neighbors = self.get_neighboring_agents(state_id=2)
|
||||
if random.random() < len(content_neighbors)*self.neutral_content_infected_prob:
|
||||
self.state['id'] = 2
|
||||
|
||||
def discontent_behaviour(self):
|
||||
|
||||
#Healing
|
||||
if random.random() < self.discontent_neutral:
|
||||
self.state['id'] = 0
|
||||
|
||||
#Superinfected
|
||||
content_neighbors = self.get_neighboring_agents(state_id=2)
|
||||
if random.random() < len(content_neighbors)*self.discontent_content:
|
||||
self.state['id'] = 2
|
||||
|
||||
def content_behaviour(self):
|
||||
|
||||
#Healing
|
||||
if random.random() < self.content_neutral:
|
||||
self.state['id'] = 0
|
||||
|
||||
#Superinfected
|
||||
discontent_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
if random.random() < len(discontent_neighbors)*self.content_discontent:
|
||||
self.state['id'] = 1
|
||||
|
||||
|
||||
class BigMarketModel(BaseBehaviour):
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
self.enterprises = settings.enterprises
|
||||
self.type = ""
|
||||
self.number_of_enterprises = len(settings.enterprises)
|
||||
|
||||
if self.id < self.number_of_enterprises: #Enterprises
|
||||
self.state['id']=self.id
|
||||
self.type="Enterprise"
|
||||
self.tweet_probability = settings.tweet_probability_enterprises[self.id]
|
||||
else: #normal users
|
||||
self.state['id']=self.number_of_enterprises
|
||||
self.type="User"
|
||||
self.tweet_probability = settings.tweet_probability_users
|
||||
self.tweet_relevant_probability = settings.tweet_relevant_probability
|
||||
self.tweet_probability_about = settings.tweet_probability_about #List
|
||||
self.sentiment_about = settings.sentiment_about #List
|
||||
|
||||
def step(self, now):
|
||||
|
||||
if(self.id < self.number_of_enterprises): # Ennterprise
|
||||
self.enterpriseBehaviour()
|
||||
else: # Usuario
|
||||
self.userBehaviour()
|
||||
for i in range(self.number_of_enterprises): # So that it never is set to 0 if there are not changes (logs)
|
||||
self.attrs['sentiment_enterprise_%s'% self.enterprises[i]] = self.sentiment_about[i]
|
||||
|
||||
super().step(now)
|
||||
|
||||
def enterpriseBehaviour(self):
|
||||
|
||||
if random.random()< self.tweet_probability: #Tweets
|
||||
aware_neighbors = self.get_neighboring_agents(state_id=self.number_of_enterprises) #Nodes neighbour users
|
||||
for x in aware_neighbors:
|
||||
if random.uniform(0,10) < 5:
|
||||
x.sentiment_about[self.id] += 0.1 #Increments for enterprise
|
||||
else:
|
||||
x.sentiment_about[self.id] -= 0.1 #Decrements for enterprise
|
||||
|
||||
# Establecemos limites
|
||||
if x.sentiment_about[self.id] > 1:
|
||||
x.sentiment_about[self.id] = 1
|
||||
if x.sentiment_about[self.id]< -1:
|
||||
x.sentiment_about[self.id] = -1
|
||||
|
||||
x.attrs['sentiment_enterprise_%s'% self.enterprises[self.id]] = x.sentiment_about[self.id]
|
||||
|
||||
|
||||
def userBehaviour(self):
|
||||
|
||||
if random.random() < self.tweet_probability: #Tweets
|
||||
if random.random() < self.tweet_relevant_probability: #Tweets something relevant
|
||||
#Tweet probability per enterprise
|
||||
for i in range(self.number_of_enterprises):
|
||||
random_num = random.random()
|
||||
if random_num < self.tweet_probability_about[i]:
|
||||
#The condition is fulfilled, sentiments are evaluated towards that enterprise
|
||||
if self.sentiment_about[i] < 0:
|
||||
#NEGATIVO
|
||||
self.userTweets("negative",i)
|
||||
elif self.sentiment_about[i] == 0:
|
||||
#NEUTRO
|
||||
pass
|
||||
else:
|
||||
#POSITIVO
|
||||
self.userTweets("positive",i)
|
||||
|
||||
def userTweets(self,sentiment,enterprise):
|
||||
aware_neighbors = self.get_neighboring_agents(state_id=self.number_of_enterprises) #Nodes neighbours users
|
||||
for x in aware_neighbors:
|
||||
if sentiment == "positive":
|
||||
x.sentiment_about[enterprise] +=0.003
|
||||
elif sentiment == "negative":
|
||||
x.sentiment_about[enterprise] -=0.003
|
||||
else:
|
||||
pass
|
||||
|
||||
# Establecemos limites
|
||||
if x.sentiment_about[enterprise] > 1:
|
||||
x.sentiment_about[enterprise] = 1
|
||||
if x.sentiment_about[enterprise] < -1:
|
||||
x.sentiment_about[enterprise] = -1
|
||||
|
||||
x.attrs['sentiment_enterprise_%s'% self.enterprises[enterprise]] = x.sentiment_about[enterprise]
|
||||
|
||||
class SentimentCorrelationModel(BaseBehaviour):
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
self.outside_effects_prob = settings.outside_effects_prob
|
||||
self.anger_prob = settings.anger_prob
|
||||
self.joy_prob = settings.joy_prob
|
||||
self.sadness_prob = settings.sadness_prob
|
||||
self.disgust_prob = settings.disgust_prob
|
||||
self.time_awareness=[]
|
||||
for i in range(4): #In this model we have 4 sentiments
|
||||
self.time_awareness.append(0) #0-> Anger, 1-> joy, 2->sadness, 3 -> disgust
|
||||
sentimentCorrelationNodeArray[self.id][self.env.now]=0
|
||||
|
||||
|
||||
def step(self, now):
|
||||
self.behaviour()
|
||||
super().step(now)
|
||||
|
||||
def behaviour(self):
|
||||
|
||||
angry_neighbors_1_time_step=[]
|
||||
joyful_neighbors_1_time_step=[]
|
||||
sad_neighbors_1_time_step=[]
|
||||
disgusted_neighbors_1_time_step=[]
|
||||
|
||||
|
||||
angry_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
for x in angry_neighbors:
|
||||
if x.time_awareness[0] > (self.env.now-500):
|
||||
angry_neighbors_1_time_step.append(x)
|
||||
num_neighbors_angry = len(angry_neighbors_1_time_step)
|
||||
|
||||
|
||||
joyful_neighbors = self.get_neighboring_agents(state_id=2)
|
||||
for x in joyful_neighbors:
|
||||
if x.time_awareness[1] > (self.env.now-500):
|
||||
joyful_neighbors_1_time_step.append(x)
|
||||
num_neighbors_joyful = len(joyful_neighbors_1_time_step)
|
||||
|
||||
|
||||
sad_neighbors = self.get_neighboring_agents(state_id=3)
|
||||
for x in sad_neighbors:
|
||||
if x.time_awareness[2] > (self.env.now-500):
|
||||
sad_neighbors_1_time_step.append(x)
|
||||
num_neighbors_sad = len(sad_neighbors_1_time_step)
|
||||
|
||||
|
||||
disgusted_neighbors = self.get_neighboring_agents(state_id=4)
|
||||
for x in disgusted_neighbors:
|
||||
if x.time_awareness[3] > (self.env.now-500):
|
||||
disgusted_neighbors_1_time_step.append(x)
|
||||
num_neighbors_disgusted = len(disgusted_neighbors_1_time_step)
|
||||
|
||||
|
||||
anger_prob= settings.anger_prob+(len(angry_neighbors_1_time_step)*settings.anger_prob)
|
||||
joy_prob= settings.joy_prob+(len(joyful_neighbors_1_time_step)*settings.joy_prob)
|
||||
sadness_prob = settings.sadness_prob+(len(sad_neighbors_1_time_step)*settings.sadness_prob)
|
||||
disgust_prob = settings.disgust_prob+(len(disgusted_neighbors_1_time_step)*settings.disgust_prob)
|
||||
outside_effects_prob= settings.outside_effects_prob
|
||||
|
||||
|
||||
num = random.random()
|
||||
|
||||
|
||||
if(num<outside_effects_prob):
|
||||
self.state['id'] = random.randint(1,4)
|
||||
|
||||
sentimentCorrelationNodeArray[self.id][self.env.now]=self.state['id'] #It is stored when it has been infected for the dynamic network
|
||||
self.time_awareness[self.state['id']-1] = self.env.now
|
||||
self.attrs['sentiment'] = self.state['id']
|
||||
|
||||
|
||||
|
||||
if(num<anger_prob):
|
||||
|
||||
self.state['id'] = 1
|
||||
sentimentCorrelationNodeArray[self.id][self.env.now]=1
|
||||
self.time_awareness[self.state['id']-1] = self.env.now
|
||||
elif (num<joy_prob+anger_prob and num>anger_prob):
|
||||
|
||||
self.state['id'] = 2
|
||||
sentimentCorrelationNodeArray[self.id][self.env.now]=2
|
||||
self.time_awareness[self.state['id']-1] = self.env.now
|
||||
elif (num<sadness_prob+anger_prob+joy_prob and num>joy_prob+anger_prob):
|
||||
|
||||
|
||||
self.state['id'] = 3
|
||||
sentimentCorrelationNodeArray[self.id][self.env.now]=3
|
||||
self.time_awareness[self.state['id']-1] = self.env.now
|
||||
elif (num<disgust_prob+sadness_prob+anger_prob+joy_prob and num>sadness_prob+anger_prob+joy_prob):
|
||||
|
||||
|
||||
self.state['id'] = 4
|
||||
sentimentCorrelationNodeArray[self.id][self.env.now]=4
|
||||
self.time_awareness[self.state['id']-1] = self.env.now
|
||||
|
||||
self.attrs['sentiment'] = self.state['id']
|
||||
|
||||
|
||||
class BassModel(BaseBehaviour):
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
self.innovation_prob = settings.innovation_prob
|
||||
self.imitation_prob = settings.imitation_prob
|
||||
sentimentCorrelationNodeArray[self.id][self.env.now]=0
|
||||
|
||||
def step(self, now):
|
||||
self.behaviour()
|
||||
super().step(now)
|
||||
|
||||
def behaviour(self):
|
||||
#Outside effects
|
||||
if random.random() < settings.innovation_prob:
|
||||
if self.state['id'] == 0:
|
||||
self.state['id'] = 1
|
||||
sentimentCorrelationNodeArray[self.id][self.env.now]=1
|
||||
else:
|
||||
pass
|
||||
|
||||
self.attrs['status'] = self.state['id']
|
||||
return
|
||||
|
||||
#Imitation effects
|
||||
if self.state['id'] == 0:
|
||||
aware_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
num_neighbors_aware = len(aware_neighbors)
|
||||
if random.random() < (settings.imitation_prob*num_neighbors_aware):
|
||||
self.state['id'] = 1
|
||||
sentimentCorrelationNodeArray[self.id][self.env.now]=1
|
||||
|
||||
else:
|
||||
pass
|
||||
self.attrs['status'] = self.state['id']
|
||||
|
||||
|
||||
class IndependentCascadeModel(BaseBehaviour):
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
self.innovation_prob = settings.innovation_prob
|
||||
self.imitation_prob = settings.imitation_prob
|
||||
self.time_awareness = 0
|
||||
sentimentCorrelationNodeArray[self.id][self.env.now]=0
|
||||
|
||||
def step(self,now):
|
||||
self.behaviour()
|
||||
super().step(now)
|
||||
|
||||
def behaviour(self):
|
||||
aware_neighbors_1_time_step=[]
|
||||
#Outside effects
|
||||
if random.random() < settings.innovation_prob:
|
||||
if self.state['id'] == 0:
|
||||
self.state['id'] = 1
|
||||
sentimentCorrelationNodeArray[self.id][self.env.now]=1
|
||||
self.time_awareness = self.env.now #To know when they have been infected
|
||||
else:
|
||||
pass
|
||||
|
||||
self.attrs['status'] = self.state['id']
|
||||
return
|
||||
|
||||
#Imitation effects
|
||||
if self.state['id'] == 0:
|
||||
aware_neighbors = self.get_neighboring_agents(state_id=1)
|
||||
for x in aware_neighbors:
|
||||
if x.time_awareness == (self.env.now-1):
|
||||
aware_neighbors_1_time_step.append(x)
|
||||
num_neighbors_aware = len(aware_neighbors_1_time_step)
|
||||
if random.random() < (settings.imitation_prob*num_neighbors_aware):
|
||||
self.state['id'] = 1
|
||||
sentimentCorrelationNodeArray[self.id][self.env.now]=1
|
||||
else:
|
||||
pass
|
||||
|
||||
self.attrs['status'] = self.state['id']
|
||||
return
|
@@ -1,6 +1,7 @@
|
||||
nxsim
|
||||
simpy
|
||||
networkx
|
||||
networkx>=2.0
|
||||
numpy
|
||||
matplotlib
|
||||
pyyaml
|
||||
pandas
|
||||
|
4
setup.cfg
Normal file
@@ -0,0 +1,4 @@
|
||||
[aliases]
|
||||
test=pytest
|
||||
[tool:pytest]
|
||||
addopts = --verbose
|
47
setup.py
@@ -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(
|
||||
@@ -28,12 +29,26 @@ setup(
|
||||
download_url='https://github.com/gsi-upm/soil/archive/{}.tar.gz'.format(
|
||||
__version__),
|
||||
keywords=['agent', 'social', 'simulator'],
|
||||
classifiers=[],
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: MacOS :: MacOS X',
|
||||
'Operating System :: Microsoft :: Windows',
|
||||
'Operating System :: POSIX',
|
||||
'Programming Language :: Python :: 3'],
|
||||
install_requires=install_reqs,
|
||||
extras_require={
|
||||
'web': ['tornado']
|
||||
|
||||
},
|
||||
tests_require=test_reqs,
|
||||
setup_requires=['pytest-runner', ],
|
||||
include_package_data=True,
|
||||
entry_points={
|
||||
'console_scripts':
|
||||
['soil = soil.__init__:main']
|
||||
['soil = soil.__init__:main',
|
||||
'soil-web = soil.web.__init__:main']
|
||||
})
|
||||
|
@@ -1,63 +0,0 @@
|
||||
---
|
||||
name: ControlModelM2_sim
|
||||
max_time: 50
|
||||
num_trials: 1
|
||||
timeout: 2
|
||||
network_params:
|
||||
generator: barabasi_albert_graph
|
||||
n: 100
|
||||
m: 2
|
||||
agent_distribution:
|
||||
- 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
|
||||
timeout: 2
|
||||
network_params:
|
||||
generator: erdos_renyi_graph
|
||||
n: 10000
|
||||
p: 0.05
|
||||
#other_agents:
|
||||
# - agent_type: DrawingAgent
|
||||
agent_distribution:
|
||||
- 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
@@ -0,0 +1 @@
|
||||
0.13.4
|
@@ -1,8 +1,10 @@
|
||||
import importlib
|
||||
import sys
|
||||
import os
|
||||
import pdb
|
||||
import logging
|
||||
|
||||
__version__ = "0.9.2"
|
||||
from .version import __version__
|
||||
|
||||
try:
|
||||
basestring
|
||||
@@ -10,16 +12,18 @@ except NameError:
|
||||
basestring = str
|
||||
|
||||
from . import agents
|
||||
from . import simulation
|
||||
from . import environment
|
||||
from .simulation import *
|
||||
from .environment import Environment
|
||||
from . import utils
|
||||
from . import settings
|
||||
|
||||
from . import analysis
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
from . import simulation
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.info('Running SOIL version: {}'.format(__version__))
|
||||
|
||||
parser = argparse.ArgumentParser(description='Run a SOIL simulation')
|
||||
parser.add_argument('file', type=str,
|
||||
nargs="?",
|
||||
@@ -27,15 +31,45 @@ def main():
|
||||
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('--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()
|
||||
|
||||
if args.module:
|
||||
if os.getcwd() not in sys.path:
|
||||
sys.path.append(os.getcwd())
|
||||
if args.module:
|
||||
importlib.import_module(args.module)
|
||||
|
||||
print('Loading config file: {}'.format(args.file))
|
||||
simulation.run_from_config(args.file)
|
||||
logging.info('Loading config file: {}'.format(args.file))
|
||||
|
||||
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),
|
||||
results_dir=args.output)
|
||||
except Exception:
|
||||
if args.pdb:
|
||||
pdb.post_mortem()
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
4
soil/__main__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@@ -1,123 +0,0 @@
|
||||
import nxsim
|
||||
from collections import OrderedDict
|
||||
from copy import deepcopy
|
||||
import json
|
||||
|
||||
from functools import wraps
|
||||
|
||||
|
||||
class BaseAgent(nxsim.BaseAgent):
|
||||
"""
|
||||
A special simpy BaseAgent that keeps track of its state history.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._history = OrderedDict()
|
||||
self._neighbors = None
|
||||
super().__init__(*args, **kwargs)
|
||||
self._history[None] = deepcopy(self.state)
|
||||
|
||||
@property
|
||||
def now(self):
|
||||
try:
|
||||
return self.env.now
|
||||
except AttributeError:
|
||||
# No environment
|
||||
return None
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
res = self.step()
|
||||
self._history[self.env.now] = deepcopy(self.state)
|
||||
yield res or self.env.timeout(self.env.interval)
|
||||
|
||||
def step(self):
|
||||
pass
|
||||
|
||||
def to_json(self):
|
||||
return json.dumps(self._history)
|
||||
|
||||
class NetworkAgent(BaseAgent, nxsim.BaseNetworkAgent):
|
||||
|
||||
def count_agents(self, state_id=None, limit_neighbors=False):
|
||||
if limit_neighbors:
|
||||
agents = self.global_topology.neighbors(self.id)
|
||||
else:
|
||||
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']:
|
||||
continue
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def count_neighboring_agents(self, state_id=None):
|
||||
return self.count_agents(state_id, limit_neighbors=True)
|
||||
|
||||
|
||||
def state(func):
|
||||
|
||||
@wraps(func)
|
||||
def func_wrapper(self):
|
||||
when = None
|
||||
next_state = func(self)
|
||||
try:
|
||||
next_state, when = next_state
|
||||
except TypeError:
|
||||
pass
|
||||
if next_state:
|
||||
try:
|
||||
self.state['id'] = next_state.id
|
||||
except AttributeError:
|
||||
raise NotImplemented('State id %s is not valid.' % next_state)
|
||||
return when
|
||||
|
||||
func_wrapper.id = func.__name__
|
||||
func_wrapper.is_default = False
|
||||
return func_wrapper
|
||||
|
||||
|
||||
def default_state(func):
|
||||
func.is_default = True
|
||||
return func
|
||||
|
||||
|
||||
class MetaFSM(type):
|
||||
def __init__(cls, name, bases, nmspc):
|
||||
super(MetaFSM, cls).__init__(name, bases, nmspc)
|
||||
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 name, func in nmspc.items():
|
||||
if hasattr(func, 'id'):
|
||||
if func.is_default:
|
||||
default_state = func
|
||||
states[func.id] = func
|
||||
cls.default_state = default_state
|
||||
cls.states = states
|
||||
|
||||
|
||||
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
|
||||
|
||||
def step(self):
|
||||
if 'id' in self.state:
|
||||
next_state = self.state['id']
|
||||
elif self.default_state:
|
||||
next_state = self.default_state.id
|
||||
else:
|
||||
raise Exception('{} has no valid state id or default state'.format(self))
|
||||
if next_state not in self.states:
|
||||
raise Exception('{} is not a valid id for {}'.format(next_state, self))
|
||||
self.states[next_state](self)
|
@@ -1,8 +1,8 @@
|
||||
import random
|
||||
from . import NetworkAgent
|
||||
from . import BaseAgent
|
||||
|
||||
|
||||
class BassModel(NetworkAgent):
|
||||
class BassModel(BaseAgent):
|
||||
"""
|
||||
Settings:
|
||||
innovation_prob
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import random
|
||||
from . import NetworkAgent
|
||||
from . import BaseAgent
|
||||
|
||||
|
||||
class BigMarketModel(NetworkAgent):
|
||||
class BigMarketModel(BaseAgent):
|
||||
"""
|
||||
Settings:
|
||||
Names:
|
||||
|
@@ -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))
|
||||
|
@@ -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"))
|
||||
|
@@ -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
|
||||
|
@@ -10,7 +10,7 @@ class SISaModel(FSM):
|
||||
|
||||
neutral_discontent_infected_prob
|
||||
|
||||
neutral_content_spong_prob
|
||||
neutral_content_spon_prob
|
||||
|
||||
neutral_content_infected_prob
|
||||
|
||||
@@ -29,27 +29,27 @@ class SISaModel(FSM):
|
||||
standard_variance
|
||||
"""
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
def __init__(self, environment, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
|
||||
self.neutral_discontent_spon_prob = np.random.normal(environment.environment_params['neutral_discontent_spon_prob'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.neutral_discontent_infected_prob = np.random.normal(environment.environment_params['neutral_discontent_infected_prob'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.neutral_content_spon_prob = np.random.normal(environment.environment_params['neutral_content_spon_prob'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.neutral_content_infected_prob = np.random.normal(environment.environment_params['neutral_content_infected_prob'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.neutral_discontent_spon_prob = np.random.normal(self.env['neutral_discontent_spon_prob'],
|
||||
self.env['standard_variance'])
|
||||
self.neutral_discontent_infected_prob = np.random.normal(self.env['neutral_discontent_infected_prob'],
|
||||
self.env['standard_variance'])
|
||||
self.neutral_content_spon_prob = np.random.normal(self.env['neutral_content_spon_prob'],
|
||||
self.env['standard_variance'])
|
||||
self.neutral_content_infected_prob = np.random.normal(self.env['neutral_content_infected_prob'],
|
||||
self.env['standard_variance'])
|
||||
|
||||
self.discontent_neutral = np.random.normal(environment.environment_params['discontent_neutral'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.discontent_content = np.random.normal(environment.environment_params['discontent_content'],
|
||||
environment.environment_params['variance_d_c'])
|
||||
self.discontent_neutral = np.random.normal(self.env['discontent_neutral'],
|
||||
self.env['standard_variance'])
|
||||
self.discontent_content = np.random.normal(self.env['discontent_content'],
|
||||
self.env['variance_d_c'])
|
||||
|
||||
self.content_discontent = np.random.normal(environment.environment_params['content_discontent'],
|
||||
environment.environment_params['variance_c_d'])
|
||||
self.content_neutral = np.random.normal(environment.environment_params['content_neutral'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.content_discontent = np.random.normal(self.env['content_discontent'],
|
||||
self.env['variance_c_d'])
|
||||
self.content_neutral = np.random.normal(self.env['content_neutral'],
|
||||
self.env['standard_variance'])
|
||||
|
||||
@state
|
||||
def neutral(self):
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import random
|
||||
from . import NetworkAgent
|
||||
from . import BaseAgent
|
||||
|
||||
|
||||
class SentimentCorrelationModel(NetworkAgent):
|
||||
class SentimentCorrelationModel(BaseAgent):
|
||||
"""
|
||||
Settings:
|
||||
outside_effects_prob
|
||||
@@ -16,7 +16,7 @@ class SentimentCorrelationModel(NetworkAgent):
|
||||
disgust_prob
|
||||
"""
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
def __init__(self, environment, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
self.outside_effects_prob = environment.environment_params['outside_effects_prob']
|
||||
self.anger_prob = environment.environment_params['anger_prob']
|
||||
|
@@ -6,49 +6,106 @@
|
||||
|
||||
|
||||
import nxsim
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
import json
|
||||
|
||||
from functools import wraps
|
||||
|
||||
|
||||
agent_types = {}
|
||||
from .. import utils, history
|
||||
|
||||
|
||||
class MetaAgent(type):
|
||||
def __init__(cls, name, bases, nmspc):
|
||||
super(MetaAgent, cls).__init__(name, bases, nmspc)
|
||||
agent_types[name] = cls
|
||||
|
||||
|
||||
class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
|
||||
class BaseAgent(nxsim.BaseAgent):
|
||||
"""
|
||||
A special simpy BaseAgent that keeps track of its state history.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._history = OrderedDict()
|
||||
defaults = {}
|
||||
|
||||
def __init__(self, environment, agent_id, 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
|
||||
|
||||
# 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 the agent itself, which behaves as a dictionary.
|
||||
Changes made to `agent.state` will be reflected in the history.
|
||||
|
||||
This method shouldn't be used, but is kept here for backwards compatibility.
|
||||
'''
|
||||
return self
|
||||
|
||||
@state.setter
|
||||
def state(self, value):
|
||||
self._state = {}
|
||||
for k, v in value.items():
|
||||
self[k] = v
|
||||
|
||||
@property
|
||||
def global_topology(self):
|
||||
return self.env.G
|
||||
|
||||
@property
|
||||
def environment_params(self):
|
||||
return self.env.environment_params
|
||||
|
||||
@environment_params.setter
|
||||
def environment_params(self, value):
|
||||
self.env.environment_params = value
|
||||
|
||||
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
|
||||
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 items(self):
|
||||
return self._state.items()
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self[key] if key in self else default
|
||||
|
||||
@property
|
||||
def now(self):
|
||||
@@ -59,19 +116,24 @@ 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):
|
||||
|
||||
def count_agents(self, state_id=None, limit_neighbors=False):
|
||||
if limit_neighbors:
|
||||
agents = self.global_topology.neighbors(self.id)
|
||||
@@ -79,30 +141,93 @@ 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, agent_type=None, limit_neighbors=False, iterator=False, **kwargs):
|
||||
agents = self.env.agents
|
||||
if limit_neighbors:
|
||||
agents = super().get_agents(state_id, limit_neighbors)
|
||||
|
||||
def matches_all(agent):
|
||||
if state_id is not None:
|
||||
if agent.state.get('id', None) != state_id:
|
||||
return False
|
||||
if agent_type is not None:
|
||||
if type(agent) != agent_type:
|
||||
return False
|
||||
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 __getstate__(self):
|
||||
'''
|
||||
Serializing an agent will lose all its running information (you cannot
|
||||
serialize an iterator), but it keeps the state and link to the environment,
|
||||
so it can be used for inspection and dumping to a file
|
||||
'''
|
||||
state = {}
|
||||
state['id'] = self.id
|
||||
state['environment'] = self.env
|
||||
state['_state'] = self._state
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
'''
|
||||
Get back a serialized agent and try to re-compose it
|
||||
'''
|
||||
self.id = state['id']
|
||||
self._state = state['_state']
|
||||
self.env = state['environment']
|
||||
|
||||
|
||||
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__
|
||||
@@ -115,7 +240,7 @@ def default_state(func):
|
||||
return func
|
||||
|
||||
|
||||
class MetaFSM(MetaAgent):
|
||||
class MetaFSM(type):
|
||||
def __init__(cls, name, bases, nmspc):
|
||||
super(MetaFSM, cls).__init__(name, bases, nmspc)
|
||||
states = {}
|
||||
@@ -142,11 +267,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 +282,141 @@ 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_type(agent_type, known_modules=[], **kwargs):
|
||||
if isinstance(agent_type, str):
|
||||
return agent_type
|
||||
known_modules += ['soil.agents']
|
||||
return utils.serialize(agent_type, known_modules=known_modules, **kwargs)[1] # Get the name of the class
|
||||
|
||||
|
||||
def serialize_distribution(network_agents, 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_type'] = serialize_type(v['agent_type'],
|
||||
known_modules=known_modules)
|
||||
return d
|
||||
|
||||
|
||||
def deserialize_type(agent_type, known_modules=[]):
|
||||
if not isinstance(agent_type, str):
|
||||
return agent_type
|
||||
known = known_modules + ['soil.agents', 'soil.agents.custom' ]
|
||||
agent_type = utils.deserializer(agent_type, known_modules=known)
|
||||
return agent_type
|
||||
|
||||
|
||||
def deserialize_distribution(ind, **kwargs):
|
||||
d = deepcopy(ind)
|
||||
for v in d:
|
||||
v['agent_type'] = deserialize_type(v['agent_type'], **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.node
|
||||
else:
|
||||
assert len(states) <= len(topology)
|
||||
return states
|
||||
|
||||
|
||||
def _convert_agent_types(ind, to_string=False, **kwargs):
|
||||
'''Convenience method to allow specifying agents by class or class name.'''
|
||||
if to_string:
|
||||
return serialize_distribution(ind, **kwargs)
|
||||
return deserialize_distribution(ind, **kwargs)
|
||||
|
||||
|
||||
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 *
|
||||
|
161
soil/analysis.py
@@ -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.deserialize(row['value_type'], row['value'])
|
||||
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
|
||||
|
@@ -1,14 +1,38 @@
|
||||
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):
|
||||
# These properties will be copied when pickling/unpickling the environment
|
||||
_CONFIG_PROPS = [ 'name',
|
||||
'states',
|
||||
'default_state',
|
||||
'interval',
|
||||
'dry_run',
|
||||
'dir_path',
|
||||
]
|
||||
|
||||
class Environment(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.Environment.get` method.
|
||||
"""
|
||||
|
||||
def __init__(self, name=None,
|
||||
network_agents=None,
|
||||
@@ -16,20 +40,33 @@ 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')
|
||||
if not dry_run:
|
||||
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 +76,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):
|
||||
@@ -50,9 +87,8 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
atype = kwargs.pop('agent_type')
|
||||
kwargs['agent_id'] = kwargs.get('agent_id', atype.__name__)
|
||||
kwargs['state'] = kwargs.get('state', {})
|
||||
a = atype(**kwargs,
|
||||
environment=self)
|
||||
self._env_agents[a.id] = weakref.ref(a)
|
||||
a = atype(environment=self, **kwargs)
|
||||
self._env_agents[a.id] = a
|
||||
|
||||
@property
|
||||
def network_agents(self):
|
||||
@@ -63,60 +99,126 @@ 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
|
||||
self.init_agent(ix, agent_distribution=network_agents)
|
||||
|
||||
def init_agent(self, agent_id, agent_distribution):
|
||||
node = self.G.nodes[agent_id]
|
||||
init = False
|
||||
state = dict(node)
|
||||
|
||||
agent_type = None
|
||||
if 'agent_type' in self.states.get(agent_id, {}):
|
||||
agent_type = self.states[agent_id]
|
||||
elif 'agent_type' in node:
|
||||
agent_type = node['agent_type']
|
||||
elif 'agent_type' in self.default_state:
|
||||
agent_type = self.default_state['agent_type']
|
||||
|
||||
if agent_type:
|
||||
agent_type = agents.deserialize_type(agent_type)
|
||||
else:
|
||||
agent_type, state = agents._agent_from_distribution(agent_distribution)
|
||||
return self.set_agent(agent_id, agent_type, state)
|
||||
|
||||
def set_agent(self, agent_id, agent_type, state=None):
|
||||
node = self.G.nodes[agent_id]
|
||||
defstate = deepcopy(self.default_state) or {}
|
||||
defstate.update(self.states.get(agent_id, {}))
|
||||
defstate.update(node.get('state', {}))
|
||||
if state:
|
||||
defstate.update(state)
|
||||
state = defstate
|
||||
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):
|
||||
if hasattr(agent1, 'id'):
|
||||
agent1 = agent1.id
|
||||
if hasattr(agent2, 'id'):
|
||||
agent2 = agent2.id
|
||||
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):
|
||||
@@ -131,7 +233,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'))
|
||||
for i in self.history_to_tuples():
|
||||
cr.writerow(i)
|
||||
|
||||
@@ -139,52 +241,107 @@ 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(agent._history.items()):
|
||||
for attribute, value in state.items():
|
||||
if attribute == 'visible':
|
||||
nowvisible = state[attribute]
|
||||
if nowvisible and not lastvisible:
|
||||
laststep = t_step
|
||||
if not nowvisible and lastvisible:
|
||||
spells.append((laststep, t_step))
|
||||
history = self[agent.id, None, None]
|
||||
if not history:
|
||||
continue
|
||||
for t_step, attribute, value in sorted(list(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
|
||||
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
|
||||
if key not in attributes:
|
||||
attributes[key] = list()
|
||||
attributes[key].append(value)
|
||||
lastattributes[attribute] = (state[attribute], t_step)
|
||||
lastvisible = nowvisible
|
||||
continue
|
||||
key = 'attr_' + attribute
|
||||
if key not in attributes:
|
||||
attributes[key] = list()
|
||||
if key not in lastattributes:
|
||||
lastattributes[key] = (value, t_step)
|
||||
elif lastattributes[key][0] != value:
|
||||
last_value, laststep = lastattributes[key]
|
||||
commit_value = (last_value, laststep, t_step)
|
||||
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, attributes, spells=spells)
|
||||
G.add_node(agent.id, spells=spells, **attributes)
|
||||
else:
|
||||
G.add_node(agent.id, attributes)
|
||||
G.add_node(agent.id, **attributes)
|
||||
|
||||
return G
|
||||
|
||||
def __getstate__(self):
|
||||
state = {}
|
||||
for prop in _CONFIG_PROPS:
|
||||
state[prop] = self.__dict__[prop]
|
||||
state['G'] = json_graph.node_link_data(self.G)
|
||||
state['environment_agents'] = self._env_agents
|
||||
state['history'] = self._history
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
for prop in _CONFIG_PROPS:
|
||||
self.__dict__[prop] = state[prop]
|
||||
self._env_agents = state['environment_agents']
|
||||
self.G = json_graph.node_link_graph(state['G'])
|
||||
self._history = state['history']
|
||||
|
||||
|
||||
SoilEnvironment = Environment
|
||||
|
291
soil/history.py
Normal file
@@ -0,0 +1,291 @@
|
||||
import time
|
||||
import os
|
||||
import pandas as pd
|
||||
import sqlite3
|
||||
import copy
|
||||
from collections import UserDict, 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:
|
||||
if backup and os.path.exists(db_path):
|
||||
newname = db_path + '.backup{}.sqlite'.format(time.time())
|
||||
os.rename(db_path, newname)
|
||||
else:
|
||||
db_path = ":memory:"
|
||||
self.db_path = db_path
|
||||
|
||||
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 = []
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
try:
|
||||
self._db.cursor()
|
||||
except (sqlite3.ProgrammingError, AttributeError):
|
||||
self.db = None # Reset the database
|
||||
return self._db
|
||||
|
||||
@db.setter
|
||||
def db(self, db_path=None):
|
||||
db_path = db_path or self.db_path
|
||||
if isinstance(db_path, str):
|
||||
self._db = sqlite3.connect(db_path)
|
||||
else:
|
||||
self._db = db_path
|
||||
|
||||
@property
|
||||
def dtypes(self):
|
||||
self.read_types()
|
||||
return {k:v[0] for k, v in self._dtypes.items()}
|
||||
|
||||
def save_tuples(self, tuples):
|
||||
'''
|
||||
Save a series of tuples, converting them to records if necessary
|
||||
'''
|
||||
self.save_records(Record(*tup) for tup in tuples)
|
||||
|
||||
def save_records(self, records):
|
||||
'''
|
||||
Save a collection of records
|
||||
'''
|
||||
for record in records:
|
||||
if not isinstance(record, Record):
|
||||
record = Record(*record)
|
||||
self.save_record(*record)
|
||||
|
||||
def save_record(self, agent_id, t_step, key, value):
|
||||
'''
|
||||
Save a collection of records to the database.
|
||||
Database writes are cached.
|
||||
'''
|
||||
value = self.convert(key, value)
|
||||
self._tups.append(Record(agent_id=agent_id,
|
||||
t_step=t_step,
|
||||
key=key,
|
||||
value=value))
|
||||
if len(self._tups) > 100:
|
||||
self.flush_cache()
|
||||
|
||||
def convert(self, key, value):
|
||||
"""Get the serialized value for a given key."""
|
||||
if key not in self._dtypes:
|
||||
self.read_types()
|
||||
if key not in self._dtypes:
|
||||
name = utils.name(value)
|
||||
serializer = utils.serializer(name)
|
||||
deserializer = utils.deserializer(name)
|
||||
self._dtypes[key] = (name, serializer, deserializer)
|
||||
with self.db:
|
||||
self.db.execute("replace into value_types (key, value_type) values (?, ?)", (key, name))
|
||||
return self._dtypes[key][1](value)
|
||||
|
||||
def recover(self, key, value):
|
||||
"""Get the deserialized value for a given key, and the serialized version."""
|
||||
if key not in self._dtypes:
|
||||
self.read_types()
|
||||
if key not in self._dtypes:
|
||||
raise ValueError("Unknown datatype for {} and {}".format(key, value))
|
||||
return self._dtypes[key][2](value)
|
||||
|
||||
|
||||
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.
|
||||
'''
|
||||
with self.db:
|
||||
for rec in self._tups:
|
||||
self.db.execute("replace into history(agent_id, t_step, key, value) values (?, ?, ?, ?)", (rec.agent_id, rec.t_step, rec.key, rec.value))
|
||||
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
|
||||
value = self.recover(key, value)
|
||||
yield agent_id, t_step, key, 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):
|
||||
self.flush_cache()
|
||||
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)
|
||||
if r.resolved:
|
||||
return r.value()
|
||||
return r
|
||||
|
||||
|
||||
|
||||
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').astype(dtype)
|
||||
if t_steps:
|
||||
df_p = df_p.reindex(t_steps, method='ffill')
|
||||
return df_p.ffill()
|
||||
|
||||
def __getstate__(self):
|
||||
state = dict(**self.__dict__)
|
||||
del state['_db']
|
||||
del state['_dtypes']
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__ = state
|
||||
self._dtypes = {}
|
||||
|
||||
|
||||
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 list(self)
|
||||
|
||||
def __getitem__(self, k):
|
||||
n = copy.copy(self)
|
||||
n.filter(k)
|
||||
if n.resolved:
|
||||
return n.value()
|
||||
return n
|
||||
|
||||
def __len__(self):
|
||||
return len(self._df)
|
||||
|
||||
def __str__(self):
|
||||
if self.resolved:
|
||||
return str(self.value())
|
||||
return '<Records for [{}]>'.format(self._filter)
|
||||
|
||||
|
||||
Key = namedtuple('Key', ['agent_id', 't_step', 'key'])
|
||||
Record = namedtuple('Record', 'agent_id t_step key value')
|
@@ -1,27 +1,28 @@
|
||||
import weakref
|
||||
import os
|
||||
import csv
|
||||
import time
|
||||
import importlib
|
||||
import sys
|
||||
import yaml
|
||||
import traceback
|
||||
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, basestring, agents
|
||||
from .environment import Environment
|
||||
from .utils import logger
|
||||
|
||||
|
||||
class SoilSimulation(NetworkSimulation):
|
||||
class Simulation(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: ::
|
||||
|
||||
@@ -44,13 +45,48 @@ class SoilSimulation(NetworkSimulation):
|
||||
'agent_type_1'.
|
||||
3) if no initial state is given, each node's state will be set
|
||||
to `{'id': 0}`.
|
||||
|
||||
Parameters
|
||||
---------
|
||||
name : str, optional
|
||||
name of the Simulation
|
||||
topology : networkx.Graph instance, optional
|
||||
network_params : dict
|
||||
parameters used to create a topology with networkx, if no topology is given
|
||||
network_agents : dict
|
||||
definition of agents to populate the topology with
|
||||
agent_type : NetworkAgent subclass, optional
|
||||
Default type of NetworkAgent to use for nodes not specified in network_agents
|
||||
states : list, optional
|
||||
List of initial states corresponding to the nodes in the topology. Basic form is a list of integers
|
||||
whose value indicates the state
|
||||
dir_path : str, optional
|
||||
Directory path where to save pickled objects
|
||||
seed : str, optional
|
||||
Seed to use for the random generator
|
||||
num_trials : int, optional
|
||||
Number of independent simulation runs
|
||||
max_time : int, optional
|
||||
Time how long the simulation should run
|
||||
environment_params : dict, optional
|
||||
Dictionary of globally-shared environmental parameters
|
||||
environment_agents: dict, optional
|
||||
Similar to network_agents. Distribution of Agents that control the environment
|
||||
environment_class: soil.environment.Environment subclass, optional
|
||||
Class for the environment. It defailts to soil.environment.Environment
|
||||
load_module : str, module name, deprecated
|
||||
If specified, soil will load the content of this module under 'soil.agents.custom'
|
||||
|
||||
|
||||
"""
|
||||
|
||||
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,
|
||||
environment_agents=None, environment_params=None):
|
||||
default_state=None, interval=1, dump=None, dry_run=False,
|
||||
dir_path=None, num_trials=1, max_time=100,
|
||||
load_module=None, seed=None,
|
||||
environment_agents=None, environment_params=None,
|
||||
environment_class=None, **kwargs):
|
||||
|
||||
if topology is None:
|
||||
topology = utils.load_network(network_params,
|
||||
@@ -58,6 +94,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 +103,78 @@ 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
|
||||
|
||||
sys.path += [self.dir_path, os.getcwd()]
|
||||
|
||||
self.environment_params = environment_params or {}
|
||||
self.environment_class = utils.deserialize(environment_class,
|
||||
known_modules=['soil.environment', ]) or Environment
|
||||
|
||||
environment_agents = environment_agents or []
|
||||
self.environment_agents = self._convert_agent_types(environment_agents)
|
||||
self.environment_agents = agents._convert_agent_types(environment_agents,
|
||||
known_modules=[self.load_module])
|
||||
|
||||
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,
|
||||
known_modules=[self.load_module])
|
||||
|
||||
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_exceptions, dry_run=dry_run or self.dry_run,
|
||||
return_env=True,
|
||||
**kwargs)
|
||||
for i in p.imap_unordered(func, range(self.num_trials)):
|
||||
if isinstance(i, Exception):
|
||||
logger.error('Trial failed:\n\t{}'.format(i.message))
|
||||
continue
|
||||
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=self.environment_class(**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 +182,27 @@ 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 run_trial_exceptions(self, *args, **kwargs):
|
||||
'''
|
||||
A wrapper for run_trial that catches exceptions and returns them.
|
||||
It is meant for async simulations
|
||||
'''
|
||||
try:
|
||||
return self.run_trial(*args, **kwargs)
|
||||
except Exception as ex:
|
||||
c = ex.__cause__
|
||||
c.message = ''.join(traceback.format_exception(type(c), c, c.__traceback__)[:])
|
||||
return c
|
||||
|
||||
def to_dict(self):
|
||||
return self.__getstate__()
|
||||
@@ -170,72 +210,80 @@ class SoilSimulation(NetworkSimulation):
|
||||
def to_yaml(self):
|
||||
return yaml.dump(self.to_dict())
|
||||
|
||||
def dump_yaml(self, dir_path=None, file_name=None):
|
||||
dir_path = dir_path or self.dir_path
|
||||
def dump_yaml(self, dir_path = None, file_name = None):
|
||||
dir_path=dir_path or self.dir_path
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
if not file_name:
|
||||
file_name = os.path.join(dir_path,
|
||||
file_name=os.path.join(dir_path,
|
||||
'{}.dumped.yml'.format(self.name))
|
||||
with open(file_name, 'w') as f:
|
||||
f.write(self.to_yaml())
|
||||
|
||||
def dump_pickle(self, dir_path=None, pickle_name=None):
|
||||
dir_path = dir_path or self.dir_path
|
||||
def dump_pickle(self, dir_path = None, pickle_name = None):
|
||||
dir_path=dir_path or self.dir_path
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
if not pickle_name:
|
||||
pickle_name = os.path.join(dir_path,
|
||||
pickle_name=os.path.join(dir_path,
|
||||
'{}.simulation.pickle'.format(self.name))
|
||||
with open(pickle_name, 'wb') as f:
|
||||
pickle.dump(self, f)
|
||||
|
||||
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={}
|
||||
for k, v in self.__dict__.items():
|
||||
if k[0] != '_':
|
||||
state[k]=v
|
||||
state['topology']=json_graph.node_link_data(self.topology)
|
||||
state['network_agents']=agents.serialize_distribution(self.network_agents,
|
||||
known_modules = [])
|
||||
state['environment_agents']=agents.serialize_distribution(self.environment_agents,
|
||||
known_modules = [])
|
||||
state['environment_class']=utils.serialize(self.environment_class,
|
||||
known_modules=['soil.environment'])[1] # func, name
|
||||
if state['load_module'] is None:
|
||||
del state['load_module']
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__ = state
|
||||
self.load_module = getattr(self, 'load_module', None)
|
||||
if self.dir_path not in sys.path:
|
||||
sys.path += [self.dir_path, os.getcwd()]
|
||||
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,
|
||||
known_modules=[self.load_module])
|
||||
self.environment_class = utils.deserialize(self.environment_class,
|
||||
known_modules=[self.load_module, 'soil.environment', ]) # func, name
|
||||
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')
|
||||
config = config[0][0]
|
||||
sim = SoilSimulation(**config)
|
||||
sim = Simulation(**config)
|
||||
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', 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)
|
||||
if dump is not None:
|
||||
config['dump'] = dump
|
||||
sim = Simulation(dir_path=dir_path, **config)
|
||||
logger.info('Dumping results to {} : {}'.format(sim.dir_path, sim.dump))
|
||||
sim.run_simulation(**kwargs)
|
||||
|
103
soil/utils.py
@@ -1,14 +1,25 @@
|
||||
import os
|
||||
import ast
|
||||
import yaml
|
||||
from time import time
|
||||
import logging
|
||||
import importlib
|
||||
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 +62,93 @@ def load_config(config):
|
||||
|
||||
|
||||
@contextmanager
|
||||
def timer(name='task', pre="", function=print, to_object=None):
|
||||
start = time()
|
||||
def timer(name='task', pre="", function=logger.info, to_object=None):
|
||||
start = time.time()
|
||||
function('{}Starting {} at {}.'.format(pre, name,
|
||||
time.strftime("%X", time.gmtime(start))))
|
||||
yield start
|
||||
end = time()
|
||||
function('{}Finished {} in {} seconds'.format(pre, name, str(end-start)))
|
||||
end = time.time()
|
||||
function('{}Finished {} at {} in {} seconds'.format(pre, name,
|
||||
time.strftime("%X", time.gmtime(end)),
|
||||
str(end-start)))
|
||||
if to_object:
|
||||
to_object.start = start
|
||||
to_object.end = end
|
||||
|
||||
|
||||
builtins = importlib.import_module('builtins')
|
||||
|
||||
def name(value, known_modules=[]):
|
||||
'''Return a name that can be imported, to serialize/deserialize an object'''
|
||||
if value is None:
|
||||
return 'None'
|
||||
if not isinstance(value, type): # Get the class name first
|
||||
value = type(value)
|
||||
tname = value.__name__
|
||||
if hasattr(builtins, tname):
|
||||
return tname
|
||||
modname = value.__module__
|
||||
if modname == '__main__':
|
||||
return tname
|
||||
if known_modules and modname in known_modules:
|
||||
return tname
|
||||
for kmod in known_modules:
|
||||
if not kmod:
|
||||
continue
|
||||
module = importlib.import_module(kmod)
|
||||
if hasattr(module, tname):
|
||||
return tname
|
||||
return '{}.{}'.format(modname, tname)
|
||||
|
||||
|
||||
def serializer(type_):
|
||||
if type_ != 'str' and hasattr(builtins, type_):
|
||||
return repr
|
||||
return lambda x: x
|
||||
|
||||
|
||||
def serialize(v, known_modules=[]):
|
||||
'''Get a text representation of an object.'''
|
||||
tname = name(v, known_modules=known_modules)
|
||||
func = serializer(tname)
|
||||
return func(v), tname
|
||||
|
||||
def deserializer(type_, known_modules=[]):
|
||||
if type_ == 'str':
|
||||
return lambda x='': x
|
||||
if type_ == 'None':
|
||||
return lambda x=None: None
|
||||
if hasattr(builtins, type_): # Check if it's a builtin type
|
||||
cls = getattr(builtins, type_)
|
||||
return lambda x=None: ast.literal_eval(x) if x is not None else cls()
|
||||
# Otherwise, see if we can find the module and the class
|
||||
modules = known_modules or []
|
||||
options = []
|
||||
|
||||
for mod in modules:
|
||||
if mod:
|
||||
options.append((mod, type_))
|
||||
|
||||
if '.' in type_: # Fully qualified module
|
||||
module, type_ = type_.rsplit(".", 1)
|
||||
options.append ((module, type_))
|
||||
|
||||
errors = []
|
||||
for modname, tname in options:
|
||||
try:
|
||||
module = importlib.import_module(modname)
|
||||
cls = getattr(module, tname)
|
||||
return getattr(cls, 'deserialize', cls)
|
||||
except (ImportError, AttributeError) as ex:
|
||||
errors.append((modname, tname, ex))
|
||||
raise Exception('Could not find type {}. Tried: {}'.format(type_, errors))
|
||||
|
||||
|
||||
def deserialize(type_, value=None, **kwargs):
|
||||
'''Get an object from a text representation'''
|
||||
if not isinstance(type_, str):
|
||||
return type_
|
||||
des = deserializer(type_, **kwargs)
|
||||
if value is None:
|
||||
return des
|
||||
return des(value)
|
||||
|
20
soil/version.py
Normal 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()
|
4
soil/web/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
output/
|
||||
tests/
|
||||
soil_output/
|
59
soil/web/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Graph Visualization with D3.js
|
||||
|
||||
The aim of this software is to provide a useful tool for visualising and analysing the result of different simulations based on graph. Once you run the simulation, you will be able to interact with the simulation in real time.
|
||||
|
||||
For this purpose, a model which tries to simulate the spread of information to comprehend the radicalism spread in a society is included. Whith all this, the main project goals could be divided in five as it is shown in the following.
|
||||
|
||||
* Simulate the spread of information through a network applied to radicalism.
|
||||
* Visualize the results of the simulation.
|
||||
* Interact with the simulation in real time.
|
||||
* Extract data from the results.
|
||||
* Show data in a right way for its research.
|
||||
|
||||
## Deploying the server
|
||||
|
||||
For deploying the application, you will only need to run the following command.
|
||||
|
||||
`python3 run.py [--name NAME] [--dump] [--port PORT] [--verbose]`
|
||||
|
||||
Where the options are detailed in the following table.
|
||||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| `--name NAME` | The name of the simulation. It will appear on the app. |
|
||||
| `--dump` | For dumping the results in server side. |
|
||||
| `--port PORT` | The port where the server will listen. |
|
||||
| `--verbose` | Verbose mode. |
|
||||
|
||||
> You can dump the results of the simulation in server side. Anyway, you will be able to download them in GEXF or JSON Graph format directly from the browser.
|
||||
|
||||
## Visualization Params
|
||||
|
||||
The configuration of the simulation is based on the simulator configuration. In this case, it follows the [SOIL](https://github.com/gsi-upm/soil) configuration syntax and for visualising the results in a more comfortable way, more params can be added in `visualization_params` dictionary.
|
||||
|
||||
* For setting a background image, the tag needed is `background image`. You can also add a `background_opacity` and `background_filter_color` if the image is so clear than you can difficult view the nodes.
|
||||
* For setting colors to the nodes, you can do it based on their properties values. Using the `color` tag, you will need to indicate the attribute key and value, and then the color you want to apply.
|
||||
* The shapes applied to a group of nodes are always the same. This means than it won't change dynamically, so you will have to indicate the property with the `shape_property` tag and add a dictionary called `shapes` in which for each value, you indicate the shape.
|
||||
All shapes have to had been downloaded before in SVG format and added to the server.
|
||||
|
||||
An example of this configuration applied to the TerroristNetworkModel is presented.
|
||||
|
||||
```yaml
|
||||
visualization_params:
|
||||
# Icons downloaded from https://www.iconfinder.com/
|
||||
shape_property: agent
|
||||
shapes:
|
||||
TrainingAreaModel: target
|
||||
HavenModel: home
|
||||
TerroristNetworkModel: person
|
||||
colors:
|
||||
- attr_id: 0
|
||||
color: '#40de40'
|
||||
- attr_id: 1
|
||||
color: red
|
||||
- attr_id: 2
|
||||
color: '#c16a6a'
|
||||
background_image: 'map_4800x2860.jpg'
|
||||
background_opacity: '0.9'
|
||||
background_filter_color: 'blue'
|
||||
```
|
255
soil/web/TerroristNetworkModel.py
Normal file
@@ -0,0 +1,255 @@
|
||||
import random
|
||||
import networkx as nx
|
||||
from soil.agents import BaseAgent, FSM, state, default_state
|
||||
from scipy.spatial import cKDTree as KDTree
|
||||
|
||||
global betweenness_centrality_global
|
||||
global degree_centrality_global
|
||||
|
||||
betweenness_centrality_global = None
|
||||
degree_centrality_global = None
|
||||
|
||||
class TerroristSpreadModel(FSM):
|
||||
"""
|
||||
Settings:
|
||||
information_spread_intensity
|
||||
|
||||
terrorist_additional_influence
|
||||
|
||||
min_vulnerability (optional else zero)
|
||||
|
||||
max_vulnerability
|
||||
|
||||
prob_interaction
|
||||
"""
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
|
||||
global betweenness_centrality_global
|
||||
global degree_centrality_global
|
||||
|
||||
if betweenness_centrality_global == None:
|
||||
betweenness_centrality_global = nx.betweenness_centrality(self.global_topology)
|
||||
if degree_centrality_global == None:
|
||||
degree_centrality_global = nx.degree_centrality(self.global_topology)
|
||||
|
||||
self.information_spread_intensity = environment.environment_params['information_spread_intensity']
|
||||
self.terrorist_additional_influence = environment.environment_params['terrorist_additional_influence']
|
||||
self.prob_interaction = environment.environment_params['prob_interaction']
|
||||
|
||||
if self['id'] == self.civilian.id: # Civilian
|
||||
self.initial_belief = random.uniform(0.00, 0.5)
|
||||
elif self['id'] == self.terrorist.id: # Terrorist
|
||||
self.initial_belief = random.uniform(0.8, 1.00)
|
||||
elif self['id'] == self.leader.id: # Leader
|
||||
self.initial_belief = 1.00
|
||||
else:
|
||||
raise Exception('Invalid state id: {}'.format(self['id']))
|
||||
|
||||
if 'min_vulnerability' in environment.environment_params:
|
||||
self.vulnerability = random.uniform( environment.environment_params['min_vulnerability'], environment.environment_params['max_vulnerability'] )
|
||||
else :
|
||||
self.vulnerability = random.uniform( 0, environment.environment_params['max_vulnerability'] )
|
||||
|
||||
self.mean_belief = self.initial_belief
|
||||
self.betweenness_centrality = betweenness_centrality_global[self.id]
|
||||
self.degree_centrality = degree_centrality_global[self.id]
|
||||
|
||||
# self.state['radicalism'] = self.mean_belief
|
||||
|
||||
def count_neighboring_agents(self, state_id=None):
|
||||
if isinstance(state_id, list):
|
||||
return len(self.get_neighboring_agents(state_id))
|
||||
else:
|
||||
return len(super().get_agents(state_id, limit_neighbors=True))
|
||||
|
||||
def get_neighboring_agents(self, state_id=None):
|
||||
if isinstance(state_id, list):
|
||||
_list = []
|
||||
for i in state_id:
|
||||
_list += super().get_agents(i, limit_neighbors=True)
|
||||
return [ neighbour for neighbour in _list if isinstance(neighbour, TerroristSpreadModel) ]
|
||||
else:
|
||||
_list = super().get_agents(state_id, limit_neighbors=True)
|
||||
return [ neighbour for neighbour in _list if isinstance(neighbour, TerroristSpreadModel) ]
|
||||
|
||||
@state
|
||||
def civilian(self):
|
||||
if self.count_neighboring_agents() > 0:
|
||||
neighbours = []
|
||||
for neighbour in self.get_neighboring_agents():
|
||||
if random.random() < self.prob_interaction:
|
||||
neighbours.append(neighbour)
|
||||
influence = sum( neighbour.degree_centrality for neighbour in neighbours )
|
||||
mean_belief = sum( neighbour.mean_belief * neighbour.degree_centrality / influence for neighbour in neighbours )
|
||||
self.initial_belief = self.mean_belief
|
||||
mean_belief = mean_belief * self.information_spread_intensity + self.initial_belief * ( 1 - self.information_spread_intensity )
|
||||
self.mean_belief = mean_belief * self.vulnerability + self.initial_belief * ( 1 - self.vulnerability )
|
||||
|
||||
if self.mean_belief >= 0.8:
|
||||
return self.terrorist
|
||||
|
||||
@state
|
||||
def leader(self):
|
||||
self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence )
|
||||
if self.count_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]) > 0:
|
||||
for neighbour in self.get_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]):
|
||||
if neighbour.betweenness_centrality > self.betweenness_centrality:
|
||||
return self.terrorist
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
if self.count_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]) > 0:
|
||||
neighbours = self.get_neighboring_agents(state_id=[self.terrorist.id, self.leader.id])
|
||||
influence = sum( neighbour.degree_centrality for neighbour in neighbours )
|
||||
mean_belief = sum( neighbour.mean_belief * neighbour.degree_centrality / influence for neighbour in neighbours )
|
||||
self.initial_belief = self.mean_belief
|
||||
self.mean_belief = mean_belief * self.vulnerability + self.initial_belief * ( 1 - self.vulnerability )
|
||||
self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence )
|
||||
|
||||
if self.count_neighboring_agents(state_id=self.leader.id) == 0 and self.count_neighboring_agents(state_id=self.terrorist.id) > 0:
|
||||
max_betweenness_centrality = self
|
||||
for neighbour in self.get_neighboring_agents(state_id=self.terrorist.id):
|
||||
if neighbour.betweenness_centrality > max_betweenness_centrality.betweenness_centrality:
|
||||
max_betweenness_centrality = neighbour
|
||||
if max_betweenness_centrality == self:
|
||||
return self.leader
|
||||
|
||||
def add_edge(self, G, source, target):
|
||||
G.add_edge(source.id, target.id, start=self.env._now)
|
||||
|
||||
def link_search(self, G, node, radius):
|
||||
pos = nx.get_node_attributes(G, 'pos')
|
||||
nodes, coords = list(zip(*pos.items()))
|
||||
kdtree = KDTree(coords) # Cannot provide generator.
|
||||
edge_indexes = kdtree.query_pairs(radius, 2)
|
||||
_list = [ edge[int(not edge.index(node))] for edge in edge_indexes if node in edge ]
|
||||
return [ G.nodes()[index]['agent'] for index in _list ]
|
||||
|
||||
def social_search(self, G, node, steps):
|
||||
nodes = list(nx.ego_graph(G, node, radius=steps).nodes())
|
||||
nodes.remove(node)
|
||||
return [ G.nodes()[index]['agent'] for index in nodes ]
|
||||
|
||||
|
||||
class TrainingAreaModel(FSM):
|
||||
"""
|
||||
Settings:
|
||||
training_influence
|
||||
|
||||
min_vulnerability
|
||||
|
||||
Requires TerroristSpreadModel.
|
||||
"""
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
self.training_influence = environment.environment_params['training_influence']
|
||||
if 'min_vulnerability' in environment.environment_params:
|
||||
self.min_vulnerability = environment.environment_params['min_vulnerability']
|
||||
else: self.min_vulnerability = 0
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def terrorist(self):
|
||||
for neighbour in self.get_neighboring_agents():
|
||||
if isinstance(neighbour, TerroristSpreadModel) and neighbour.vulnerability > self.min_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.training_influence )
|
||||
|
||||
|
||||
class HavenModel(FSM):
|
||||
"""
|
||||
Settings:
|
||||
haven_influence
|
||||
|
||||
min_vulnerability
|
||||
|
||||
max_vulnerability
|
||||
|
||||
Requires TerroristSpreadModel.
|
||||
"""
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
self.haven_influence = environment.environment_params['haven_influence']
|
||||
if 'min_vulnerability' in environment.environment_params:
|
||||
self.min_vulnerability = environment.environment_params['min_vulnerability']
|
||||
else: self.min_vulnerability = 0
|
||||
self.max_vulnerability = environment.environment_params['max_vulnerability']
|
||||
|
||||
@state
|
||||
def civilian(self):
|
||||
for neighbour_agent in self.get_neighboring_agents():
|
||||
if isinstance(neighbour_agent, TerroristSpreadModel) and neighbour_agent['id'] == neighbour_agent.civilian.id:
|
||||
for neighbour in self.get_neighboring_agents():
|
||||
if isinstance(neighbour, TerroristSpreadModel) and neighbour.vulnerability > self.min_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability * ( 1 - self.haven_influence )
|
||||
return self.civilian
|
||||
return self.terrorist
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
for neighbour in self.get_neighboring_agents():
|
||||
if isinstance(neighbour, TerroristSpreadModel) and neighbour.vulnerability < self.max_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.haven_influence )
|
||||
return self.terrorist
|
||||
|
||||
|
||||
class TerroristNetworkModel(TerroristSpreadModel):
|
||||
"""
|
||||
Settings:
|
||||
sphere_influence
|
||||
|
||||
vision_range
|
||||
|
||||
weight_social_distance
|
||||
|
||||
weight_link_distance
|
||||
"""
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
|
||||
self.vision_range = environment.environment_params['vision_range']
|
||||
self.sphere_influence = environment.environment_params['sphere_influence']
|
||||
self.weight_social_distance = environment.environment_params['weight_social_distance']
|
||||
self.weight_link_distance = environment.environment_params['weight_link_distance']
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
self.update_relationships()
|
||||
return super().terrorist()
|
||||
|
||||
@state
|
||||
def leader(self):
|
||||
self.update_relationships()
|
||||
return super().leader()
|
||||
|
||||
def update_relationships(self):
|
||||
if self.count_neighboring_agents(state_id=self.civilian.id) == 0:
|
||||
close_ups = self.link_search(self.global_topology, self.id, self.vision_range)
|
||||
step_neighbours = self.social_search(self.global_topology, self.id, self.sphere_influence)
|
||||
search = list(set(close_ups).union(step_neighbours))
|
||||
neighbours = self.get_neighboring_agents()
|
||||
search = [item for item in search if not item in neighbours and isinstance(item, TerroristNetworkModel)]
|
||||
for agent in search:
|
||||
social_distance = 1 / self.shortest_path_length(self.global_topology, self.id, agent.id)
|
||||
spatial_proximity = ( 1 - self.get_distance(self.global_topology, self.id, agent.id) )
|
||||
prob_new_interaction = self.weight_social_distance * social_distance + self.weight_link_distance * spatial_proximity
|
||||
if agent['id'] == agent.civilian.id and random.random() < prob_new_interaction:
|
||||
self.add_edge(self.global_topology, self, agent)
|
||||
break
|
||||
|
||||
def get_distance(self, G, source, target):
|
||||
source_x, source_y = nx.get_node_attributes(G, 'pos')[source]
|
||||
target_x, target_y = nx.get_node_attributes(G, 'pos')[target]
|
||||
dx = abs( source_x - target_x )
|
||||
dy = abs( source_y - target_y )
|
||||
return ( dx ** 2 + dy ** 2 ) ** ( 1 / 2 )
|
||||
|
||||
def shortest_path_length(self, G, source, target):
|
||||
try:
|
||||
return nx.shortest_path_length(G, source, target)
|
||||
except nx.NetworkXNoPath:
|
||||
return float('inf')
|
62
soil/web/TerroristNetworkModel.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
name: TerroristNetworkModel_sim
|
||||
load_module: TerroristNetworkModel
|
||||
max_time: 150
|
||||
num_trials: 1
|
||||
network_params:
|
||||
generator: random_geometric_graph
|
||||
radius: 0.2
|
||||
# generator: geographical_threshold_graph
|
||||
# theta: 20
|
||||
n: 100
|
||||
network_agents:
|
||||
- agent_type: TerroristNetworkModel
|
||||
weight: 0.8
|
||||
state:
|
||||
id: civilian # Civilians
|
||||
- agent_type: TerroristNetworkModel
|
||||
weight: 0.1
|
||||
state:
|
||||
id: leader # Leaders
|
||||
- agent_type: TrainingAreaModel
|
||||
weight: 0.05
|
||||
state:
|
||||
id: terrorist # Terrorism
|
||||
- agent_type: HavenModel
|
||||
weight: 0.05
|
||||
state:
|
||||
id: civilian # Civilian
|
||||
|
||||
environment_params:
|
||||
# TerroristSpreadModel
|
||||
information_spread_intensity: 0.7
|
||||
terrorist_additional_influence: 0.035
|
||||
max_vulnerability: 0.7
|
||||
prob_interaction: 0.5
|
||||
|
||||
# TrainingAreaModel and HavenModel
|
||||
training_influence: 0.20
|
||||
haven_influence: 0.20
|
||||
|
||||
# TerroristNetworkModel
|
||||
vision_range: 0.30
|
||||
sphere_influence: 2
|
||||
weight_social_distance: 0.035
|
||||
weight_link_distance: 0.035
|
||||
|
||||
visualization_params:
|
||||
# Icons downloaded from https://www.iconfinder.com/
|
||||
shape_property: agent
|
||||
shapes:
|
||||
TrainingAreaModel: target
|
||||
HavenModel: home
|
||||
TerroristNetworkModel: person
|
||||
colors:
|
||||
- attr_id: civilian
|
||||
color: '#40de40'
|
||||
- attr_id: terrorist
|
||||
color: red
|
||||
- attr_id: leader
|
||||
color: '#c16a6a'
|
||||
background_image: 'map_4800x2860.jpg'
|
||||
background_opacity: '0.9'
|
||||
background_filter_color: 'blue'
|