mirror of
https://github.com/gsi-upm/soil
synced 2025-10-19 09:48:29 +00:00
Compare commits
29 Commits
0.12.0
...
1cf85ea450
Author | SHA1 | Date | |
---|---|---|---|
|
1cf85ea450 | ||
|
c32e167fb8 | ||
|
5f68b5321d | ||
|
2a2843bd19 | ||
|
d1006bd55c | ||
|
9bc036d185 | ||
|
a3ea434f23 | ||
|
65f6aa72f3 | ||
|
09e14c6e84 | ||
|
8593ac999d | ||
|
90338c3549 | ||
|
1d532dacfe | ||
|
a1f8d8c9c5 | ||
|
de326eb331 | ||
|
04b4380c61 | ||
|
d70a0c865c | ||
|
625c28e4ee | ||
|
9749f4ca14 | ||
|
3526fa29d7 | ||
|
53604c1e66 | ||
|
01cc8e9128 | ||
|
a47ffa815b | ||
|
b41927d7bf | ||
|
70d033b3a9 | ||
|
3afed06656 | ||
|
0a7ef27844 | ||
|
2e28b36f6e | ||
|
bd4700567e | ||
|
ff1df62eec |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
**/soil_output
|
||||
.*
|
28
.gitlab-ci.yml
Normal file
28
.gitlab-ci.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
|
||||
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:
|
||||
except:
|
||||
- tags # Avoid running tests for tags, because they are already run for the branch
|
||||
tags:
|
||||
- docker
|
||||
image: python:3.7
|
||||
stage: test
|
||||
script:
|
||||
- python setup.py test
|
49
CHANGELOG.md
Normal file
49
CHANGELOG.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.14.0]
|
||||
### Added
|
||||
* Loading configuration from template definitions in the yaml, in preparation for SALib support.
|
||||
The definition of the variables and their possible values (i.e., a problem in SALib terms), as well as a sampler function, can be provided.
|
||||
Soil uses this definition and the template to generate a set of configurations.
|
||||
* Simulation group names, to link related simulations. For now, they are only used to group all simulations in the same group under the same folder.
|
||||
* Exporters unify exporting/dumping results and other files to disk. If `dry_run` is set to `True`, exporters will write to stdout instead of a file (useful for testing/debugging).
|
||||
* Distribution exporter, to write statistics about values and value_counts in every simulation. The results are dumped to two CSV files.
|
||||
|
||||
### Changed
|
||||
* `dir_path` is now the directory for resources (modules, files)
|
||||
* Environments and simulations do not export or write anything by default. That task is delegated to Exporters
|
||||
|
||||
### Removed
|
||||
* The output dir for environments and simulations (see Exporters)
|
||||
* DrawingAgent, because it wrote to disk and was not being used. We provide a partial alternative in the form of the GraphDrawing exporter. A complete alternative will be provided once the network at each state can be accessed by exporters.
|
||||
|
||||
## Fixed
|
||||
* Modules with custom agents/environments failed to load when they were run from outside the directory of the definition file. Modules are now loaded from the directory of the simulation file in addition to the working directory
|
||||
* Memory databases (in history) can now be shared between threads.
|
||||
* Testing all examples, not just subdirectories
|
||||
|
||||
## [0.13.8]
|
||||
### Changed
|
||||
* Moved TerroristNetworkModel to examples
|
||||
### Added
|
||||
* `get_agents` and `count_agents` methods now accept lists as inputs. They can be used to retrieve agents from node ids
|
||||
* `subgraph` in BaseAgent
|
||||
* `agents.select` method, to filter out agents
|
||||
* `skip_test` property in yaml definitions, to force skipping some examples
|
||||
* `agents.Geo`, with a search function based on postition
|
||||
* `BaseAgent.ego_search` to get nodes from the ego network of a node
|
||||
* `BaseAgent.degree` and `BaseAgent.betweenness`
|
||||
### Fixed
|
||||
|
||||
## [0.13.7]
|
||||
### Changed
|
||||
* History now defaults to not backing up! This makes it more intuitive to load the history for examination, at the expense of rewriting something. That should not happen because History is only created in the Environment, and that has `backup=True`.
|
||||
### Added
|
||||
* Agent names are assigned based on their agent types
|
||||
* Agent logging uses the agent name.
|
||||
* FSM agents can now return a timeout in addition to a new state. e.g. `return self.idle, self.env.timeout(2)` will execute the *different_state* in 2 *units of time* (`t_step=now+2`).
|
||||
* Example of using timeouts in FSM (custom_timeouts)
|
||||
* `network_agents` entries may include an `ids` entry. If set, it should be a list of node ids that should be assigned that agent type. This complements the previous behavior of setting agent type with `weights`.
|
@@ -1,4 +1,11 @@
|
||||
FROM python:3.4-onbuild
|
||||
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]'
|
||||
|
||||
|
@@ -1,4 +1,7 @@
|
||||
include requirements.txt
|
||||
include test-requirements.txt
|
||||
include README.rst
|
||||
graft soil
|
||||
graft soil
|
||||
global-exclude __pycache__
|
||||
global-exclude soil_output
|
||||
global-exclude *.py[co]
|
||||
|
7
Makefile
Normal file
7
Makefile
Normal file
@@ -0,0 +1,7 @@
|
||||
quick-test:
|
||||
docker-compose exec dev python -m pytest -s -v
|
||||
|
||||
test:
|
||||
docker run -t -v $$PWD:/usr/src/app -w /usr/src/app python:3.7 python setup.py test
|
||||
|
||||
.PHONY: test
|
@@ -2,6 +2,8 @@ version: '3'
|
||||
services:
|
||||
dev:
|
||||
build: .
|
||||
environment:
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
volumes:
|
||||
- .:/usr/src/app
|
||||
tty: true
|
||||
|
244
docs/configuration.rst
Normal file
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:
|
||||
|
||||
@@ -39,6 +39,7 @@ If you use Soil in your research, do not forget to cite this paper:
|
||||
|
||||
installation
|
||||
quickstart
|
||||
configuration
|
||||
Tutorial <soil_tutorial>
|
||||
|
||||
..
|
||||
|
@@ -1,197 +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
|
||||
interval: 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
|
||||
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.
|
||||
|
||||
|
||||
This example configuration will run three trials of a simulation containing a randomly generated network.
|
||||
The 100 nodes in the network will be SISaModel agents, 10% of them will start in the content state, 10% in the discontent state, and the remaining 80% in the neutral state.
|
||||
All agents will have access to the environment, which only contains one variable, ``prob_infected``.
|
||||
The state of the agents will be updated every 2 seconds (``interval``).
|
||||
|
||||
Now run the simulation with the command line tool:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
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 (which can be used to re-launch it); and for every trial, a csv file with the content of the state of every network node and the environment parameters at every step of the simulation, as well as the network in gephi format (``gexf``).
|
||||
.. image:: soil.png
|
||||
:width: 80%
|
||||
:align: center
|
||||
|
||||
|
||||
.. code::
|
||||
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`.
|
||||
|
||||
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
|
||||
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 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
|
||||
|
||||
Any agent has unrestricted access to the environment.
|
||||
However, for the sake of simplicity, we recommend limiting environment updates to environment agents.
|
||||
|
||||
Agents
|
||||
======
|
||||
Agents are a way of modelling behavior.
|
||||
Agents can be characterized with two variables: an agent type (``agent_type``) and its state.
|
||||
Only one agent is executed at a time (generally, every ``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
|
||||
|
||||
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
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
BIN
docs/soil.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
@@ -26,7 +26,7 @@ But before that, let's import the soil module and networkx.
|
||||
%autoreload 2
|
||||
|
||||
%pylab inline
|
||||
# To display plots in the notebooed_
|
||||
# To display plots in the notebook_
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
@@ -214,7 +214,7 @@ nodes in that network. Notice how node 0 is the only one with a TV.
|
||||
MAX_TIME = 100
|
||||
EVENT_TIME = 10
|
||||
|
||||
sim = soil.simulation.SoilSimulation(topology=G,
|
||||
sim = soil.Simulation(topology=G,
|
||||
num_trials=1,
|
||||
max_time=MAX_TIME,
|
||||
environment_agents=[{'agent_type': NewsEnvironmentAgent,
|
||||
@@ -2531,7 +2531,7 @@ Dealing with bigger data
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
267M ../rabbits/soil_output/rabbits_example/
|
||||
267M ../rabbits/soil_output/rabbits_example/
|
||||
|
||||
|
||||
If we tried to load the entire history, we would probably run out of
|
||||
|
@@ -2,14 +2,22 @@
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 1,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"start_time": "2017-11-02T09:48:41.843Z"
|
||||
},
|
||||
"scrolled": false
|
||||
},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Populating the interactive namespace from numpy and matplotlib\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import soil\n",
|
||||
"import networkx as nx\n",
|
||||
@@ -39,26 +47,216 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"total 288K\r\n",
|
||||
"drwxr-xr-x 7 j users 4.0K May 23 12:48 .\r\n",
|
||||
"drwxr-xr-x 15 j users 20K May 7 18:59 ..\r\n",
|
||||
"-rw-r--r-- 1 j users 451 Oct 17 2017 complete.yml\r\n",
|
||||
"drwxr-xr-x 2 j users 4.0K Feb 18 11:22 .ipynb_checkpoints\r\n",
|
||||
"drwxr-xr-x 2 j users 4.0K Oct 17 2017 long_running\r\n",
|
||||
"-rw-r--r-- 1 j users 1.2K May 23 12:49 .nbgrader.log\r\n",
|
||||
"drwxr-xr-x 4 j users 4.0K May 4 11:23 newsspread\r\n",
|
||||
"-rw-r--r-- 1 j users 225K May 4 11:23 NewsSpread.ipynb\r\n",
|
||||
"drwxr-xr-x 4 j users 4.0K May 4 11:21 rabbits\r\n",
|
||||
"-rw-r--r-- 1 j users 42 Jul 3 2017 torvalds.edgelist\r\n",
|
||||
"-rw-r--r-- 1 j users 245 Oct 13 2017 torvalds.yml\r\n",
|
||||
"drwxr-xr-x 4 j users 4.0K May 4 11:23 tutorial\r\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"!ls "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"start_time": "2017-11-02T09:48:43.440Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"---\r\n",
|
||||
"default_state: {}\r\n",
|
||||
"load_module: newsspread\r\n",
|
||||
"environment_agents: []\r\n",
|
||||
"environment_params:\r\n",
|
||||
" prob_neighbor_spread: 0.0\r\n",
|
||||
" prob_tv_spread: 0.01\r\n",
|
||||
"interval: 1\r\n",
|
||||
"max_time: 30\r\n",
|
||||
"name: Sim_all_dumb\r\n",
|
||||
"network_agents:\r\n",
|
||||
"- agent_type: DumbViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: false\r\n",
|
||||
" weight: 1\r\n",
|
||||
"- agent_type: DumbViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: true\r\n",
|
||||
" weight: 1\r\n",
|
||||
"network_params:\r\n",
|
||||
" generator: barabasi_albert_graph\r\n",
|
||||
" n: 500\r\n",
|
||||
" m: 5\r\n",
|
||||
"num_trials: 50\r\n",
|
||||
"---\r\n",
|
||||
"default_state: {}\r\n",
|
||||
"load_module: newsspread\r\n",
|
||||
"environment_agents: []\r\n",
|
||||
"environment_params:\r\n",
|
||||
" prob_neighbor_spread: 0.0\r\n",
|
||||
" prob_tv_spread: 0.01\r\n",
|
||||
"interval: 1\r\n",
|
||||
"max_time: 30\r\n",
|
||||
"name: Sim_half_herd\r\n",
|
||||
"network_agents:\r\n",
|
||||
"- agent_type: DumbViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: false\r\n",
|
||||
" weight: 1\r\n",
|
||||
"- agent_type: DumbViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: true\r\n",
|
||||
" weight: 1\r\n",
|
||||
"- agent_type: HerdViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: false\r\n",
|
||||
" weight: 1\r\n",
|
||||
"- agent_type: HerdViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: true\r\n",
|
||||
" weight: 1\r\n",
|
||||
"network_params:\r\n",
|
||||
" generator: barabasi_albert_graph\r\n",
|
||||
" n: 500\r\n",
|
||||
" m: 5\r\n",
|
||||
"num_trials: 50\r\n",
|
||||
"---\r\n",
|
||||
"default_state: {}\r\n",
|
||||
"load_module: newsspread\r\n",
|
||||
"environment_agents: []\r\n",
|
||||
"environment_params:\r\n",
|
||||
" prob_neighbor_spread: 0.0\r\n",
|
||||
" prob_tv_spread: 0.01\r\n",
|
||||
"interval: 1\r\n",
|
||||
"max_time: 30\r\n",
|
||||
"name: Sim_all_herd\r\n",
|
||||
"network_agents:\r\n",
|
||||
"- agent_type: HerdViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: true\r\n",
|
||||
" id: neutral\r\n",
|
||||
" weight: 1\r\n",
|
||||
"- agent_type: HerdViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: true\r\n",
|
||||
" id: neutral\r\n",
|
||||
" weight: 1\r\n",
|
||||
"network_params:\r\n",
|
||||
" generator: barabasi_albert_graph\r\n",
|
||||
" n: 500\r\n",
|
||||
" m: 5\r\n",
|
||||
"num_trials: 50\r\n",
|
||||
"---\r\n",
|
||||
"default_state: {}\r\n",
|
||||
"load_module: newsspread\r\n",
|
||||
"environment_agents: []\r\n",
|
||||
"environment_params:\r\n",
|
||||
" prob_neighbor_spread: 0.0\r\n",
|
||||
" prob_tv_spread: 0.01\r\n",
|
||||
" prob_neighbor_cure: 0.1\r\n",
|
||||
"interval: 1\r\n",
|
||||
"max_time: 30\r\n",
|
||||
"name: Sim_wise_herd\r\n",
|
||||
"network_agents:\r\n",
|
||||
"- agent_type: HerdViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: true\r\n",
|
||||
" id: neutral\r\n",
|
||||
" weight: 1\r\n",
|
||||
"- agent_type: WiseViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: true\r\n",
|
||||
" weight: 1\r\n",
|
||||
"network_params:\r\n",
|
||||
" generator: barabasi_albert_graph\r\n",
|
||||
" n: 500\r\n",
|
||||
" m: 5\r\n",
|
||||
"num_trials: 50\r\n",
|
||||
"---\r\n",
|
||||
"default_state: {}\r\n",
|
||||
"load_module: newsspread\r\n",
|
||||
"environment_agents: []\r\n",
|
||||
"environment_params:\r\n",
|
||||
" prob_neighbor_spread: 0.0\r\n",
|
||||
" prob_tv_spread: 0.01\r\n",
|
||||
" prob_neighbor_cure: 0.1\r\n",
|
||||
"interval: 1\r\n",
|
||||
"max_time: 30\r\n",
|
||||
"name: Sim_all_wise\r\n",
|
||||
"network_agents:\r\n",
|
||||
"- agent_type: WiseViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: true\r\n",
|
||||
" id: neutral\r\n",
|
||||
" weight: 1\r\n",
|
||||
"- agent_type: WiseViewer\r\n",
|
||||
" state:\r\n",
|
||||
" has_tv: true\r\n",
|
||||
" weight: 1\r\n",
|
||||
"network_params:\r\n",
|
||||
" generator: barabasi_albert_graph\r\n",
|
||||
" n: 500\r\n",
|
||||
" m: 5\r\n",
|
||||
"network_params:\r\n",
|
||||
" generator: barabasi_albert_graph\r\n",
|
||||
" n: 500\r\n",
|
||||
" m: 5\r\n",
|
||||
"num_trials: 50\r\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"!cat NewsSpread.yml"
|
||||
"!cat newsspread/NewsSpread.yml"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 10,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"start_time": "2017-11-02T09:48:43.879Z"
|
||||
}
|
||||
},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"ename": "ValueError",
|
||||
"evalue": "No objects to concatenate",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"\u001b[0;31m----------------------------------------------------------------------\u001b[0m",
|
||||
"\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)",
|
||||
"\u001b[0;32m<ipython-input-10-bae848826594>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mevodumb\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0manalysis\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread_data\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'soil_output/Sim_all_dumb/'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgroup\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mprocess\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0manalysis\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_count\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mkeys\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'id'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m;\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
|
||||
"\u001b[0;32m~/git/lab.gsi/soil/soil/soil/analysis.py\u001b[0m in \u001b[0;36mread_data\u001b[0;34m(group, *args, **kwargs)\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[0miterable\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_read_data\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mgroup\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 13\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mgroup_trials\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0miterable\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 14\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 15\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0miterable\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
|
||||
"\u001b[0;32m~/git/lab.gsi/soil/soil/soil/analysis.py\u001b[0m in \u001b[0;36mgroup_trials\u001b[0;34m(trials, aggfunc)\u001b[0m\n\u001b[1;32m 159\u001b[0m \u001b[0mtrials\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtrials\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 160\u001b[0m \u001b[0mtrials\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtuple\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32melse\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtrials\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 161\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mpd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconcat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtrials\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgroupby\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlevel\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0magg\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maggfunc\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreorder_levels\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m,\u001b[0m\u001b[0maxis\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 162\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 163\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
|
||||
"\u001b[0;32m~/.local/lib/python3.6/site-packages/pandas/core/reshape/concat.py\u001b[0m in \u001b[0;36mconcat\u001b[0;34m(objs, axis, join, join_axes, ignore_index, keys, levels, names, verify_integrity, copy)\u001b[0m\n\u001b[1;32m 210\u001b[0m \u001b[0mkeys\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mkeys\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlevels\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mlevels\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnames\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mnames\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 211\u001b[0m \u001b[0mverify_integrity\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mverify_integrity\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 212\u001b[0;31m copy=copy)\n\u001b[0m\u001b[1;32m 213\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mop\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_result\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 214\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
|
||||
"\u001b[0;32m~/.local/lib/python3.6/site-packages/pandas/core/reshape/concat.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, objs, axis, join, join_axes, keys, levels, names, ignore_index, verify_integrity, copy)\u001b[0m\n\u001b[1;32m 243\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 244\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mobjs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 245\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'No objects to concatenate'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 246\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 247\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mkeys\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
|
||||
"\u001b[0;31mValueError\u001b[0m: No objects to concatenate"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"evodumb = analysis.read_data('soil_output/Sim_all_dumb/', group=True, process=analysis.get_count, keys=['id']);"
|
||||
]
|
||||
@@ -302,7 +500,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.6.2"
|
||||
"version": "3.6.5"
|
||||
},
|
||||
"toc": {
|
||||
"colors": {
|
||||
|
80808
examples/Untitled.ipynb
Normal file
80808
examples/Untitled.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: simple
|
||||
group: tests
|
||||
dir_path: "/tmp/"
|
||||
num_trials: 3
|
||||
max_time: 100
|
||||
interval: 1
|
||||
seed: "CompleteSeed!"
|
||||
dump: false
|
||||
network_params:
|
||||
generator: complete_graph
|
||||
n: 10
|
||||
@@ -17,6 +17,7 @@ network_agents:
|
||||
- agent_type: AggregatedCounter
|
||||
weight: 0.2
|
||||
environment_agents: []
|
||||
environment_class: Environment
|
||||
environment_params:
|
||||
am_i_complete: true
|
||||
default_state:
|
||||
|
16
examples/custom_generator/custom_generator.yml
Normal file
16
examples/custom_generator/custom_generator.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: custom-generator
|
||||
description: Using a custom generator for the network
|
||||
num_trials: 3
|
||||
max_time: 100
|
||||
interval: 1
|
||||
network_params:
|
||||
generator: mymodule.mygenerator
|
||||
# These are custom parameters
|
||||
n: 10
|
||||
n_edges: 5
|
||||
network_agents:
|
||||
- agent_type: CounterModel
|
||||
weight: 1
|
||||
state:
|
||||
id: 0
|
27
examples/custom_generator/mymodule.py
Normal file
27
examples/custom_generator/mymodule.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from networkx import Graph
|
||||
import networkx as nx
|
||||
from random import choice
|
||||
|
||||
def mygenerator(n=5, n_edges=5):
|
||||
'''
|
||||
Just a simple generator that creates a network with n nodes and
|
||||
n_edges edges. Edges are assigned randomly, only avoiding self loops.
|
||||
'''
|
||||
G = nx.Graph()
|
||||
|
||||
for i in range(n):
|
||||
G.add_node(i)
|
||||
|
||||
for i in range(n_edges):
|
||||
nodes = list(G.nodes)
|
||||
n_in = choice(nodes)
|
||||
nodes.remove(n_in) # Avoid loops
|
||||
n_out = choice(nodes)
|
||||
G.add_edge(n_in, n_out)
|
||||
return G
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
35
examples/custom_timeouts/custom_timeouts.py
Normal file
35
examples/custom_timeouts/custom_timeouts.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from soil.agents import FSM, state, default_state
|
||||
|
||||
|
||||
class Fibonacci(FSM):
|
||||
'''Agent that only executes in t_steps that are Fibonacci numbers'''
|
||||
|
||||
defaults = {
|
||||
'prev': 1
|
||||
}
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def counting(self):
|
||||
self.log('Stopping at {}'.format(self.now))
|
||||
prev, self['prev'] = self['prev'], max([self.now, self['prev']])
|
||||
return None, self.env.timeout(prev)
|
||||
|
||||
class Odds(FSM):
|
||||
'''Agent that only executes in odd t_steps'''
|
||||
@default_state
|
||||
@state
|
||||
def odds(self):
|
||||
self.log('Stopping at {}'.format(self.now))
|
||||
return None, self.env.timeout(1+self.now%2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
from soil import Simulation
|
||||
s = Simulation(network_agents=[{'ids': [0], 'agent_type': Fibonacci},
|
||||
{'ids': [1], 'agent_type': Odds}],
|
||||
network_params={"generator": "complete_graph", "n": 2},
|
||||
max_time=100,
|
||||
)
|
||||
s.run(dry_run=True)
|
@@ -6,7 +6,7 @@ environment_params:
|
||||
prob_neighbor_spread: 0.0
|
||||
prob_tv_spread: 0.01
|
||||
interval: 1
|
||||
max_time: 30
|
||||
max_time: 300
|
||||
name: Sim_all_dumb
|
||||
network_agents:
|
||||
- agent_type: DumbViewer
|
||||
@@ -30,7 +30,7 @@ environment_params:
|
||||
prob_neighbor_spread: 0.0
|
||||
prob_tv_spread: 0.01
|
||||
interval: 1
|
||||
max_time: 30
|
||||
max_time: 300
|
||||
name: Sim_half_herd
|
||||
network_agents:
|
||||
- agent_type: DumbViewer
|
||||
@@ -62,7 +62,7 @@ environment_params:
|
||||
prob_neighbor_spread: 0.0
|
||||
prob_tv_spread: 0.01
|
||||
interval: 1
|
||||
max_time: 30
|
||||
max_time: 300
|
||||
name: Sim_all_herd
|
||||
network_agents:
|
||||
- agent_type: HerdViewer
|
||||
@@ -89,7 +89,7 @@ environment_params:
|
||||
prob_tv_spread: 0.01
|
||||
prob_neighbor_cure: 0.1
|
||||
interval: 1
|
||||
max_time: 30
|
||||
max_time: 300
|
||||
name: Sim_wise_herd
|
||||
network_agents:
|
||||
- agent_type: HerdViewer
|
||||
@@ -115,7 +115,7 @@ environment_params:
|
||||
prob_tv_spread: 0.01
|
||||
prob_neighbor_cure: 0.1
|
||||
interval: 1
|
||||
max_time: 30
|
||||
max_time: 300
|
||||
name: Sim_all_wise
|
||||
network_agents:
|
||||
- agent_type: WiseViewer
|
||||
|
1
examples/programmatic/.gitignore
vendored
Normal file
1
examples/programmatic/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Programmatic*
|
38
examples/programmatic/programmatic.py
Normal file
38
examples/programmatic/programmatic.py
Normal file
@@ -0,0 +1,38 @@
|
||||
'''
|
||||
Example of a fully programmatic simulation, without definition files.
|
||||
'''
|
||||
from soil import Simulation, agents
|
||||
from networkx import Graph
|
||||
import logging
|
||||
|
||||
|
||||
def mygenerator():
|
||||
# Add only a node
|
||||
G = Graph()
|
||||
G.add_node(1)
|
||||
return G
|
||||
|
||||
|
||||
class MyAgent(agents.FSM):
|
||||
|
||||
@agents.default_state
|
||||
@agents.state
|
||||
def neutral(self):
|
||||
self.info('I am running')
|
||||
|
||||
|
||||
s = Simulation(name='Programmatic',
|
||||
network_params={'generator': mygenerator},
|
||||
num_trials=1,
|
||||
max_time=100,
|
||||
agent_type=MyAgent,
|
||||
dry_run=True)
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
envs = s.run()
|
||||
|
||||
s.dump_yaml()
|
||||
|
||||
for env in envs:
|
||||
env.dump_csv()
|
10
examples/pubcrawl/README.md
Normal file
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
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
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
|
@@ -1,7 +1,7 @@
|
||||
---
|
||||
load_module: rabbit_agents
|
||||
name: rabbits_example
|
||||
max_time: 1200
|
||||
max_time: 500
|
||||
interval: 1
|
||||
seed: MySeed
|
||||
agent_type: RabbitModel
|
||||
|
29
examples/template.yml
Normal file
29
examples/template.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
vars:
|
||||
bounds:
|
||||
x1: [0, 1]
|
||||
x2: [1, 2]
|
||||
fixed:
|
||||
x3: ["a", "b", "c"]
|
||||
sampler: "SALib.sample.morris.sample"
|
||||
samples: 10
|
||||
template: |
|
||||
group: simple
|
||||
num_trials: 1
|
||||
interval: 1
|
||||
max_time: 2
|
||||
seed: "CompleteSeed!"
|
||||
dump: false
|
||||
network_params:
|
||||
generator: complete_graph
|
||||
n: 10
|
||||
network_agents:
|
||||
- agent_type: CounterModel
|
||||
weight: {{ x1 }}
|
||||
state:
|
||||
id: 0
|
||||
- agent_type: AggregatedCounter
|
||||
weight: {{ 1 - x1 }}
|
||||
environment_params:
|
||||
name: {{ x3 }}
|
||||
skip_test: true
|
208
examples/terrorism/TerroristNetworkModel.py
Normal file
208
examples/terrorism/TerroristNetworkModel.py
Normal file
@@ -0,0 +1,208 @@
|
||||
import random
|
||||
import networkx as nx
|
||||
from soil.agents import Geo, NetworkAgent, FSM, state, default_state
|
||||
from soil import Environment
|
||||
|
||||
|
||||
class TerroristSpreadModel(FSM, Geo):
|
||||
"""
|
||||
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)
|
||||
|
||||
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.mean_belief = random.uniform(0.00, 0.5)
|
||||
elif self['id'] == self.terrorist.id: # Terrorist
|
||||
self.mean_belief = random.uniform(0.8, 1.00)
|
||||
elif self['id'] == self.leader.id: # Leader
|
||||
self.mean_belief = 1.00
|
||||
else:
|
||||
raise Exception('Invalid state id: {}'.format(self['id']))
|
||||
|
||||
if 'min_vulnerability' in 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'] )
|
||||
|
||||
|
||||
@state
|
||||
def civilian(self):
|
||||
neighbours = list(self.get_neighboring_agents(agent_type=TerroristSpreadModel))
|
||||
if len(neighbours) > 0:
|
||||
# Only interact with some of the neighbors
|
||||
interactions = list(n for n in neighbours if random.random() <= self.prob_interaction)
|
||||
influence = sum( self.degree(i) for i in interactions )
|
||||
mean_belief = sum( i.mean_belief * self.degree(i) / influence for i in interactions )
|
||||
mean_belief = mean_belief * self.information_spread_intensity + self.mean_belief * ( 1 - self.information_spread_intensity )
|
||||
self.mean_belief = mean_belief * self.vulnerability + self.mean_belief * ( 1 - self.vulnerability )
|
||||
|
||||
if self.mean_belief >= 0.8:
|
||||
return self.terrorist
|
||||
|
||||
@state
|
||||
def leader(self):
|
||||
self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence )
|
||||
for neighbour in self.get_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]):
|
||||
if self.betweenness(neighbour) > self.betweenness(self):
|
||||
return self.terrorist
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
neighbours = self.get_agents(state_id=[self.terrorist.id, self.leader.id],
|
||||
agent_type=TerroristSpreadModel,
|
||||
limit_neighbors=True)
|
||||
if len(neighbours) > 0:
|
||||
influence = sum( self.degree(n) for n in neighbours )
|
||||
mean_belief = sum( n.mean_belief * self.degree(n) / influence for n in neighbours )
|
||||
mean_belief = mean_belief * self.vulnerability + self.mean_belief * ( 1 - self.vulnerability )
|
||||
self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence )
|
||||
|
||||
# Check if there are any leaders in the group
|
||||
leaders = list(filter(lambda x: x.state.id == self.leader.id, neighbours))
|
||||
if not leaders:
|
||||
# Check if this is the potential leader
|
||||
# Stop once it's found. Otherwise, set self as leader
|
||||
for neighbour in neighbours:
|
||||
if self.betweenness(self) < self.betweenness(neighbour):
|
||||
return
|
||||
return self.leader
|
||||
|
||||
|
||||
class TrainingAreaModel(FSM, Geo):
|
||||
"""
|
||||
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(agent_type=TerroristSpreadModel):
|
||||
if neighbour.vulnerability > self.min_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.training_influence )
|
||||
|
||||
|
||||
class HavenModel(FSM, Geo):
|
||||
"""
|
||||
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']
|
||||
|
||||
def get_occupants(self, **kwargs):
|
||||
return self.get_neighboring_agents(agent_type=TerroristSpreadModel, **kwargs)
|
||||
|
||||
@state
|
||||
def civilian(self):
|
||||
civilians = self.get_occupants(state_id=self.civilian.id)
|
||||
if not civilians:
|
||||
return self.terrorist
|
||||
|
||||
for neighbour in self.get_occupants():
|
||||
if neighbour.vulnerability > self.min_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability * ( 1 - self.haven_influence )
|
||||
return self.civilian
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
for neighbour in self.get_occupants():
|
||||
if neighbour.vulnerability < self.max_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.haven_influence )
|
||||
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 = set(self.geo_search(radius=self.vision_range, agent_type=TerroristNetworkModel))
|
||||
step_neighbours = set(self.ego_search(self.sphere_influence, agent_type=TerroristNetworkModel, center=False))
|
||||
neighbours = set(agent.id for agent in self.get_neighboring_agents(agent_type=TerroristNetworkModel))
|
||||
search = (close_ups | step_neighbours) - neighbours
|
||||
for agent in self.get_agents(search):
|
||||
social_distance = 1 / self.shortest_path_length(agent.id)
|
||||
spatial_proximity = ( 1 - self.get_distance(agent.id) )
|
||||
prob_new_interaction = self.weight_social_distance * social_distance + self.weight_link_distance * spatial_proximity
|
||||
if agent['id'] == agent.civilian.id and random.random() < prob_new_interaction:
|
||||
self.add_edge(agent)
|
||||
break
|
||||
|
||||
def get_distance(self, target):
|
||||
source_x, source_y = nx.get_node_attributes(self.global_topology, 'pos')[self.id]
|
||||
target_x, target_y = nx.get_node_attributes(self.global_topology, 'pos')[target]
|
||||
dx = abs( source_x - target_x )
|
||||
dy = abs( source_y - target_y )
|
||||
return ( dx ** 2 + dy ** 2 ) ** ( 1 / 2 )
|
||||
|
||||
def shortest_path_length(self, target):
|
||||
try:
|
||||
return nx.shortest_path_length(self.global_topology, self.id, target)
|
||||
except nx.NetworkXNoPath:
|
||||
return float('inf')
|
@@ -60,3 +60,4 @@ visualization_params:
|
||||
background_image: 'map_4800x2860.jpg'
|
||||
background_opacity: '0.9'
|
||||
background_filter_color: 'blue'
|
||||
skip_test: true # This simulation takes too long for automated tests.
|
@@ -12327,7 +12327,7 @@ Notice how node 0 is the only one with a TV.</p>
|
||||
<span class="n">MAX_TIME</span> <span class="o">=</span> <span class="mi">100</span>
|
||||
<span class="n">EVENT_TIME</span> <span class="o">=</span> <span class="mi">10</span>
|
||||
|
||||
<span class="n">sim</span> <span class="o">=</span> <span class="n">soil</span><span class="o">.</span><span class="n">simulation</span><span class="o">.</span><span class="n">SoilSimulation</span><span class="p">(</span><span class="n">topology</span><span class="o">=</span><span class="n">G</span><span class="p">,</span>
|
||||
<span class="n">sim</span> <span class="o">=</span> <span class="n">soil</span><span class="o">.</span><span class="n">Simulation</span><span class="p">(</span><span class="n">topology</span><span class="o">=</span><span class="n">G</span><span class="p">,</span>
|
||||
<span class="n">num_trials</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span>
|
||||
<span class="n">max_time</span><span class="o">=</span><span class="n">MAX_TIME</span><span class="p">,</span>
|
||||
<span class="n">environment_agents</span><span class="o">=</span><span class="p">[{</span><span class="s1">'agent_type'</span><span class="p">:</span> <span class="n">NewsEnvironmentAgent</span><span class="p">,</span>
|
||||
@@ -21883,7 +21883,7 @@ bgAAAABJRU5ErkJggg==
|
||||
|
||||
|
||||
<div class="output_subarea output_stream output_stdout output_text">
|
||||
<pre>267M ../rabbits/soil_output/rabbits_example/
|
||||
<pre>267M ../rabbits/soil_output/rabbits_example/
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -426,7 +426,7 @@
|
||||
"MAX_TIME = 100\n",
|
||||
"EVENT_TIME = 10\n",
|
||||
"\n",
|
||||
"sim = soil.simulation.SoilSimulation(topology=G,\n",
|
||||
"sim = soil.Simulation(topology=G,\n",
|
||||
" num_trials=1,\n",
|
||||
" max_time=MAX_TIME,\n",
|
||||
" environment_agents=[{'agent_type': NewsEnvironmentAgent,\n",
|
||||
|
@@ -1,7 +1,10 @@
|
||||
nxsim
|
||||
nxsim>=0.1.2
|
||||
simpy
|
||||
networkx>=2.0
|
||||
numpy
|
||||
matplotlib
|
||||
pyyaml
|
||||
pandas
|
||||
pyyaml>=5.1
|
||||
pandas>=0.23
|
||||
scipy==1.2.1 # scipy 1.3.0rc1 is not compatible with salib
|
||||
SALib>=1.3
|
||||
Jinja2
|
||||
|
4
setup.cfg
Normal file
4
setup.cfg
Normal file
@@ -0,0 +1,4 @@
|
||||
[aliases]
|
||||
test=pytest
|
||||
[tool:pytest]
|
||||
addopts = --verbose
|
@@ -1 +1 @@
|
||||
0.12.0
|
||||
0.13.8
|
||||
|
@@ -11,19 +11,20 @@ try:
|
||||
except NameError:
|
||||
basestring = str
|
||||
|
||||
logging.basicConfig()
|
||||
|
||||
from . import agents
|
||||
from . import simulation
|
||||
from . import environment
|
||||
from . import utils
|
||||
from .simulation import *
|
||||
from .environment import Environment
|
||||
from .history import History
|
||||
from . import serialization
|
||||
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="?",
|
||||
@@ -43,28 +44,34 @@ def main():
|
||||
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.')
|
||||
parser.add_argument('-e', '--exporter', action='append',
|
||||
help='Export environment and/or simulations using this exporter')
|
||||
|
||||
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)
|
||||
|
||||
logging.info('Loading config file: {}'.format(args.file, args.output))
|
||||
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')
|
||||
exporters = list(args.exporter or [])
|
||||
if args.csv:
|
||||
exporters.append('CSV')
|
||||
if args.graph:
|
||||
exporters.append('Gexf')
|
||||
exp_params = {}
|
||||
if args.dry_run:
|
||||
exp_params['copy_to'] = sys.stdout
|
||||
simulation.run_from_config(args.file,
|
||||
dry_run=args.dry_run,
|
||||
dump=dump,
|
||||
parallel=(not args.synchronous and not args.pdb),
|
||||
results_dir=args.output)
|
||||
except Exception as ex:
|
||||
exporters=exporters,
|
||||
parallel=(not args.synchronous),
|
||||
outdir=args.output,
|
||||
exporter_params=exp_params)
|
||||
except Exception:
|
||||
if args.pdb:
|
||||
pdb.post_mortem()
|
||||
else:
|
||||
|
@@ -22,11 +22,17 @@ class AggregatedCounter(BaseAgent):
|
||||
in each step and adds it to its state.
|
||||
"""
|
||||
|
||||
defaults = {
|
||||
'times': 0,
|
||||
'neighbors': 0,
|
||||
'total': 0
|
||||
}
|
||||
|
||||
def step(self):
|
||||
# Outside effects
|
||||
total = len(list(self.get_all_agents()))
|
||||
self['times'] += 1
|
||||
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['neighbors'] += neighbors
|
||||
total = len(list(self.get_all_agents()))
|
||||
self['total'] += total
|
||||
self.debug('Running for step: {}. Total: {}'.format(self.now, total))
|
||||
|
@@ -1,18 +0,0 @@
|
||||
from . import BaseAgent
|
||||
|
||||
import os.path
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
import networkx as nx
|
||||
|
||||
|
||||
class DrawingAgent(BaseAgent):
|
||||
"""
|
||||
Agent that draws the state of the network.
|
||||
"""
|
||||
|
||||
def step(self):
|
||||
# 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.get_path(), "graph-"+str(self.env.now)+".png"))
|
@@ -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):
|
||||
|
@@ -16,7 +16,7 @@ class SentimentCorrelationModel(BaseAgent):
|
||||
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']
|
||||
|
@@ -10,42 +10,37 @@ import logging
|
||||
from collections import OrderedDict
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from scipy.spatial import cKDTree as KDTree
|
||||
import json
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from .. import utils, history
|
||||
|
||||
agent_types = {}
|
||||
from .. import serialization, history
|
||||
|
||||
|
||||
class MetaAgent(type):
|
||||
def __init__(cls, name, bases, nmspc):
|
||||
super(MetaAgent, cls).__init__(name, bases, nmspc)
|
||||
agent_types[name] = cls
|
||||
def as_node(agent):
|
||||
if isinstance(agent, BaseAgent):
|
||||
return agent.id
|
||||
return agent
|
||||
|
||||
|
||||
class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
|
||||
class BaseAgent(nxsim.BaseAgent):
|
||||
"""
|
||||
A special simpy BaseAgent that keeps track of its state history.
|
||||
"""
|
||||
|
||||
defaults = {}
|
||||
|
||||
def __init__(self, environment=None, agent_id=None, state=None,
|
||||
name='network_process', interval=None, **state_params):
|
||||
def __init__(self, environment, agent_id, state=None,
|
||||
name=None, 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.name = name or '{}[{}]'.format(type(self).__name__, self.id)
|
||||
self.state_params = state_params
|
||||
|
||||
# Global parameters
|
||||
self.global_topology = environment.G
|
||||
self.environment_params = environment.environment_params
|
||||
|
||||
# Register agent to environment
|
||||
self.env = environment
|
||||
|
||||
@@ -58,8 +53,7 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
|
||||
|
||||
if not hasattr(self, 'level'):
|
||||
self.level = logging.DEBUG
|
||||
self.logger = logging.getLogger('{}-Agent-{}'.format(self.env.name,
|
||||
self.id))
|
||||
self.logger = logging.getLogger(self.env.name)
|
||||
self.logger.setLevel(self.level)
|
||||
|
||||
# initialize every time an instance of the agent is created
|
||||
@@ -81,6 +75,18 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
|
||||
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):
|
||||
key, t_step = key
|
||||
@@ -134,46 +140,25 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
|
||||
def step(self):
|
||||
pass
|
||||
|
||||
def to_json(self):
|
||||
return json.dumps(self.state)
|
||||
def count_agents(self, **kwargs):
|
||||
return len(list(self.get_agents(**kwargs)))
|
||||
|
||||
def count_agents(self, state_id=None, limit_neighbors=False):
|
||||
def count_neighboring_agents(self, state_id=None, **kwargs):
|
||||
return len(super().get_neighboring_agents(state_id=state_id, **kwargs))
|
||||
|
||||
def get_neighboring_agents(self, state_id=None, **kwargs):
|
||||
return self.get_agents(limit_neighbors=True, state_id=state_id, **kwargs)
|
||||
|
||||
def get_agents(self, agents=None, limit_neighbors=False, **kwargs):
|
||||
if limit_neighbors:
|
||||
agents = self.global_topology.neighbors(self.id)
|
||||
agents = super().get_agents(limit_neighbors=limit_neighbors)
|
||||
else:
|
||||
agents = self.global_topology.nodes()
|
||||
count = 0
|
||||
for agent in agents:
|
||||
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 len(super().get_agents(state_id, limit_neighbors=True))
|
||||
|
||||
def get_agents(self, state_id=None, limit_neighbors=False, iterator=False, **kwargs):
|
||||
if limit_neighbors:
|
||||
agents = super().get_agents(state_id, limit_neighbors)
|
||||
else:
|
||||
agents = filter(lambda x: state_id is None or x.state.get('id', None) == state_id,
|
||||
self.env.agents)
|
||||
|
||||
def matches_all(agent):
|
||||
state = agent.state
|
||||
for k, v in kwargs.items():
|
||||
if state.get(k, None) != v:
|
||||
return False
|
||||
return True
|
||||
|
||||
f = filter(matches_all, agents)
|
||||
if iterator:
|
||||
return f
|
||||
return list(f)
|
||||
agents = self.env.get_agents(agents)
|
||||
return select(agents, **kwargs)
|
||||
|
||||
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)
|
||||
message = "\t{:10}@{:>5}:\t{}".format(self.name, self.now, message)
|
||||
for k, v in kwargs:
|
||||
message += " {k}={v} ".format(k, v)
|
||||
extra = {}
|
||||
@@ -186,32 +171,97 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
|
||||
|
||||
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 add_edge(self, node1, node2, **attrs):
|
||||
node1 = as_node(node1)
|
||||
node2 = as_node(node2)
|
||||
|
||||
for n in [node1, node2]:
|
||||
if n not in self.global_topology.nodes(data=False):
|
||||
raise ValueError('"{}" not in the graph'.format(n))
|
||||
return self.global_topology.add_edge(node1, node2, **attrs)
|
||||
|
||||
def subgraph(self, center=True, **kwargs):
|
||||
include = [self] if center else []
|
||||
return self.global_topology.subgraph(n.id for n in self.get_agents(**kwargs)+include)
|
||||
|
||||
|
||||
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.
|
||||
'''
|
||||
class NetworkAgent(BaseAgent):
|
||||
|
||||
@wraps(func)
|
||||
def func_wrapper(self):
|
||||
next_state = func(self)
|
||||
when = None
|
||||
if next_state is None:
|
||||
def add_edge(self, other, **kwargs):
|
||||
return super(NetworkAgent, self).add_edge(node1=self.id, node2=other, **kwargs)
|
||||
|
||||
def ego_search(self, steps=1, center=False, node=None, **kwargs):
|
||||
'''Get a list of nodes in the ego network of *node* of radius *steps*'''
|
||||
node = as_node(node if node is not None else self)
|
||||
G = self.subgraph(**kwargs)
|
||||
return nx.ego_graph(G, node, center=center, radius=steps).nodes()
|
||||
|
||||
def degree(self, node, force=False):
|
||||
node = as_node(node)
|
||||
if force or (not hasattr(self.env, '_degree')) or getattr(self.env, '_last_step', 0) < self.now:
|
||||
self.env._degree = nx.degree_centrality(self.global_topology)
|
||||
self.env._last_step = self.now
|
||||
return self.env._degree[node]
|
||||
|
||||
def betweenness(self, node, force=False):
|
||||
node = as_node(node)
|
||||
if force or (not hasattr(self.env, '_betweenness')) or getattr(self.env, '_last_step', 0) < self.now:
|
||||
self.env._betweenness = nx.betweenness_centrality(self.global_topology)
|
||||
self.env._last_step = self.now
|
||||
return self.env._betweenness[node]
|
||||
|
||||
|
||||
def state(name=None):
|
||||
def decorator(func, name=None):
|
||||
'''
|
||||
A state function should return either a state id, or a tuple (state_id, when)
|
||||
The default value for state_id is the current state id.
|
||||
The default value for when is the interval defined in the environment.
|
||||
'''
|
||||
|
||||
@wraps(func)
|
||||
def func_wrapper(self):
|
||||
next_state = func(self)
|
||||
when = None
|
||||
if next_state is None:
|
||||
return when
|
||||
try:
|
||||
next_state, when = next_state
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if next_state:
|
||||
self.set_state(next_state)
|
||||
return when
|
||||
try:
|
||||
next_state, when = next_state
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if next_state:
|
||||
self.set_state(next_state)
|
||||
return when
|
||||
|
||||
func_wrapper.id = func.__name__
|
||||
func_wrapper.is_default = False
|
||||
return func_wrapper
|
||||
func_wrapper.id = name or func.__name__
|
||||
func_wrapper.is_default = False
|
||||
return func_wrapper
|
||||
|
||||
if callable(name):
|
||||
return decorator(name)
|
||||
else:
|
||||
return partial(decorator, name=name)
|
||||
|
||||
|
||||
def default_state(func):
|
||||
@@ -219,7 +269,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 = {}
|
||||
@@ -259,7 +309,7 @@ class FSM(BaseAgent, metaclass=MetaFSM):
|
||||
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)
|
||||
return self.states[next_state](self)
|
||||
|
||||
def set_state(self, state):
|
||||
if hasattr(state, 'id'):
|
||||
@@ -285,6 +335,9 @@ def prob(prob=1):
|
||||
return r < prob
|
||||
|
||||
|
||||
STATIC_THRESHOLD = (-1, -1)
|
||||
|
||||
|
||||
def calculate_distribution(network_agents=None,
|
||||
agent_type=None):
|
||||
'''
|
||||
@@ -316,28 +369,54 @@ def calculate_distribution(network_agents=None,
|
||||
elif agent_type:
|
||||
network_agents = [{'agent_type': agent_type}]
|
||||
else:
|
||||
return []
|
||||
raise ValueError('Specify a distribution or a default agent type')
|
||||
|
||||
# Calculate the thresholds
|
||||
total = sum(x.get('weight', 1) for x in network_agents)
|
||||
acc = 0
|
||||
for v in network_agents:
|
||||
if 'ids' in v:
|
||||
v['threshold'] = STATIC_THRESHOLD
|
||||
continue
|
||||
upper = acc + (v.get('weight', 1)/total)
|
||||
v['threshold'] = [acc, upper]
|
||||
acc = upper
|
||||
return network_agents
|
||||
|
||||
|
||||
def _serialize_distribution(network_agents):
|
||||
d = _convert_agent_types(network_agents,
|
||||
to_string=True)
|
||||
def serialize_type(agent_type, known_modules=[], **kwargs):
|
||||
if isinstance(agent_type, str):
|
||||
return agent_type
|
||||
known_modules += ['soil.agents']
|
||||
return serialization.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 = serialization.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
|
||||
|
||||
|
||||
@@ -352,33 +431,83 @@ def _validate_states(states, topology):
|
||||
return states
|
||||
|
||||
|
||||
def _convert_agent_types(ind, to_string=False):
|
||||
def _convert_agent_types(ind, to_string=False, **kwargs):
|
||||
'''Convenience method to allow specifying agents by class or class name.'''
|
||||
d = deepcopy(ind)
|
||||
for v in d:
|
||||
agent_type = v['agent_type']
|
||||
if to_string and not isinstance(agent_type, str):
|
||||
v['agent_type'] = str(agent_type.__name__)
|
||||
elif not to_string and isinstance(agent_type, str):
|
||||
v['agent_type'] = agent_types[agent_type]
|
||||
return d
|
||||
if to_string:
|
||||
return serialize_distribution(ind, **kwargs)
|
||||
return deserialize_distribution(ind, **kwargs)
|
||||
|
||||
|
||||
def _agent_from_distribution(distribution, value=-1):
|
||||
def _agent_from_distribution(distribution, value=-1, agent_id=None):
|
||||
"""Used in the initialization of agents given an agent distribution."""
|
||||
if value < 0:
|
||||
value = random.random()
|
||||
for d in distribution:
|
||||
for d in sorted(distribution, key=lambda x: x['threshold']):
|
||||
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
|
||||
# Check if the definition matches by id (first) or by threshold
|
||||
if not ((agent_id is not None and threshold == STATIC_THRESHOLD and agent_id in d['ids']) or \
|
||||
(value >= threshold[0] and value < threshold[1])):
|
||||
continue
|
||||
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))
|
||||
|
||||
|
||||
class Geo(NetworkAgent):
|
||||
'''In this type of network, nodes have a "pos" attribute.'''
|
||||
|
||||
def geo_search(self, radius, node=None, center=False, **kwargs):
|
||||
'''Get a list of nodes whose coordinates are closer than *radius* to *node*.'''
|
||||
node = as_node(node if node is not None else self)
|
||||
|
||||
G = self.subgraph(**kwargs)
|
||||
|
||||
pos = nx.get_node_attributes(G, 'pos')
|
||||
if not pos:
|
||||
return []
|
||||
nodes, coords = list(zip(*pos.items()))
|
||||
kdtree = KDTree(coords) # Cannot provide generator.
|
||||
indices = kdtree.query_ball_point(pos[node], radius)
|
||||
return [nodes[i] for i in indices if center or (nodes[i] != node)]
|
||||
|
||||
|
||||
def select(agents, state_id=None, agent_type=None, ignore=None, iterator=False, **kwargs):
|
||||
|
||||
if state_id is not None:
|
||||
try:
|
||||
state_id = tuple(state_id)
|
||||
except TypeError:
|
||||
state_id = tuple([state_id])
|
||||
if agent_type is not None:
|
||||
try:
|
||||
agent_type = tuple(agent_type)
|
||||
except TypeError:
|
||||
agent_type = tuple([agent_type])
|
||||
|
||||
def matches_all(agent):
|
||||
if state_id is not None:
|
||||
if agent.state.get('id', None) not in state_id:
|
||||
return False
|
||||
if agent_type is not None:
|
||||
if not isinstance(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 ignore:
|
||||
f = filter(lambda x: x not in ignore, f)
|
||||
if iterator:
|
||||
return f
|
||||
return list(f)
|
||||
|
||||
|
||||
from .BassModel import *
|
||||
from .BigMarketModel import *
|
||||
from .IndependentCascadeModel import *
|
||||
@@ -386,4 +515,3 @@ from .ModelM2 import *
|
||||
from .SentimentCorrelationModel import *
|
||||
from .SISaModel import *
|
||||
from .CounterModel import *
|
||||
from .DrawingAgent import *
|
||||
|
@@ -4,7 +4,7 @@ import glob
|
||||
import yaml
|
||||
from os.path import join
|
||||
|
||||
from . import utils, history
|
||||
from . import serialization, history
|
||||
|
||||
|
||||
def read_data(*args, group=False, **kwargs):
|
||||
@@ -34,7 +34,7 @@ def _read_data(pattern, *args, from_csv=False, process_args=None, **kwargs):
|
||||
|
||||
|
||||
def read_sql(db, *args, **kwargs):
|
||||
h = history.History(db, backup=False)
|
||||
h = history.History(db_path=db, backup=False)
|
||||
df = h.read_sql(*args, **kwargs)
|
||||
return df
|
||||
|
||||
@@ -56,7 +56,7 @@ def read_csv(filename, keys=None, convert_types=False, **kwargs):
|
||||
|
||||
|
||||
def convert_row(row):
|
||||
row['value'] = utils.convert(row['value'], row['value_type'])
|
||||
row['value'] = serialization.deserialize(row['value_type'], row['value'])
|
||||
return row
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ def get_count(df, *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)
|
||||
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)
|
||||
|
@@ -4,18 +4,26 @@ import time
|
||||
import csv
|
||||
import random
|
||||
import simpy
|
||||
import yaml
|
||||
import tempfile
|
||||
import pandas as pd
|
||||
from copy import deepcopy
|
||||
from collections import Counter
|
||||
from networkx.readwrite import json_graph
|
||||
|
||||
import networkx as nx
|
||||
import nxsim
|
||||
|
||||
from . import utils, agents, analysis, history
|
||||
from . import serialization, agents, analysis, history, utils
|
||||
|
||||
# These properties will be copied when pickling/unpickling the environment
|
||||
_CONFIG_PROPS = [ 'name',
|
||||
'states',
|
||||
'default_state',
|
||||
'interval',
|
||||
]
|
||||
|
||||
class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
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
|
||||
@@ -23,7 +31,7 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
|
||||
The environment parameters and the state of every agent can be accessed
|
||||
both by using the environment as a dictionary or with the environment's
|
||||
:meth:`soil.environment.SoilEnvironment.get` method.
|
||||
:meth:`soil.environment.Environment.get` method.
|
||||
"""
|
||||
|
||||
def __init__(self, name=None,
|
||||
@@ -33,8 +41,6 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
default_state=None,
|
||||
interval=1,
|
||||
seed=None,
|
||||
dry_run=False,
|
||||
dir_path=None,
|
||||
topology=None,
|
||||
*args, **kwargs):
|
||||
self.name = name or 'UnnamedEnvironment'
|
||||
@@ -46,12 +52,9 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
topology = nx.Graph()
|
||||
super().__init__(*args, topology=topology, **kwargs)
|
||||
self._env_agents = {}
|
||||
self.dry_run = dry_run
|
||||
self.interval = interval
|
||||
self.dir_path = dir_path or tempfile.mkdtemp('soil-env')
|
||||
self.get_path()
|
||||
self._history = history.History(name=self.name if not dry_run else None,
|
||||
dir_path=self.dir_path)
|
||||
self._history = history.History(name=self.name,
|
||||
backup=True)
|
||||
# Add environment agents first, so their events get
|
||||
# executed before network agents
|
||||
self.environment_agents = environment_agents or []
|
||||
@@ -90,23 +93,45 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
|
||||
@network_agents.setter
|
||||
def network_agents(self, network_agents):
|
||||
if not network_agents:
|
||||
return
|
||||
self._network_agents = network_agents
|
||||
for ix in self.G.nodes():
|
||||
agent, state = agents._agent_from_distribution(network_agents)
|
||||
self.set_agent(ix, agent_type=agent, state=state)
|
||||
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]['agent_type']
|
||||
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)
|
||||
elif agent_distribution:
|
||||
agent_type, state = agents._agent_from_distribution(agent_distribution, agent_id=agent_id)
|
||||
else:
|
||||
serialization.logger.debug('Skipping node {}'.format(agent_id))
|
||||
return
|
||||
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)
|
||||
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
|
||||
state.update(node.get('state', {}))
|
||||
a = agent_type(environment=self,
|
||||
agent_id=agent_id,
|
||||
state=state)
|
||||
a = None
|
||||
if agent_type:
|
||||
state = defstate
|
||||
a = agent_type(environment=self,
|
||||
agent_id=agent_id,
|
||||
state=state)
|
||||
node['agent'] = a
|
||||
return a
|
||||
|
||||
@@ -117,18 +142,23 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
a['visible'] = True
|
||||
return a
|
||||
|
||||
def add_edge(self, agent1, agent2, attrs=None):
|
||||
return self.G.add_edge(agent1, agent2)
|
||||
def add_edge(self, agent1, agent2, start=None, **attrs):
|
||||
if hasattr(agent1, 'id'):
|
||||
agent1 = agent1.id
|
||||
if hasattr(agent2, 'id'):
|
||||
agent2 = agent2.id
|
||||
start = start or self.now
|
||||
return self.G.add_edge(agent1, agent2, **attrs)
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
self._save_state()
|
||||
self.log_stats()
|
||||
super().run(*args, **kwargs)
|
||||
self._history.flush_cache()
|
||||
self.log_stats()
|
||||
|
||||
def _save_state(self, now=None):
|
||||
# for agent in self.agents:
|
||||
# agent.save_state()
|
||||
utils.logger.debug('Saving state @{}'.format(self.now))
|
||||
serialization.logger.debug('Saving state @{}'.format(self.now))
|
||||
self._history.save_records(self.state_to_tuples(now=now))
|
||||
|
||||
def save_state(self):
|
||||
@@ -139,7 +169,7 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
self._save_state()
|
||||
while self.peek() != simpy.core.Infinity:
|
||||
delay = max(self.peek() - self.now, self.interval)
|
||||
utils.logger.debug('Step: {}'.format(self.now))
|
||||
serialization.logger.debug('Step: {}'.format(self.now))
|
||||
ev = self.event()
|
||||
ev._ok = True
|
||||
# Schedule the event with minimum priority so
|
||||
@@ -181,35 +211,23 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
'''
|
||||
return self[key] if key in self else default
|
||||
|
||||
def get_path(self, dir_path=None):
|
||||
dir_path = dir_path or self.dir_path
|
||||
if not os.path.exists(dir_path):
|
||||
try:
|
||||
os.makedirs(dir_path)
|
||||
except FileExistsError:
|
||||
pass
|
||||
return dir_path
|
||||
|
||||
def get_agent(self, agent_id):
|
||||
return self.G.node[agent_id]['agent']
|
||||
|
||||
def get_agents(self):
|
||||
return list(self.agents)
|
||||
def get_agents(self, nodes=None):
|
||||
if nodes is None:
|
||||
return list(self.agents)
|
||||
return [self.G.node[i]['agent'] for i in nodes]
|
||||
|
||||
def dump_csv(self, dir_path=None):
|
||||
csv_name = os.path.join(self.get_path(dir_path),
|
||||
'{}.environment.csv'.format(self.name))
|
||||
|
||||
with open(csv_name, 'w') as f:
|
||||
def dump_csv(self, f):
|
||||
with utils.open_or_reuse(f, 'w') as f:
|
||||
cr = csv.writer(f)
|
||||
cr.writerow(('agent_id', 't_step', 'key', 'value', 'value_type'))
|
||||
cr.writerow(('agent_id', 't_step', 'key', 'value'))
|
||||
for i in self.history_to_tuples():
|
||||
cr.writerow(i)
|
||||
|
||||
def dump_gexf(self, dir_path=None):
|
||||
def dump_gexf(self, f):
|
||||
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():
|
||||
@@ -217,9 +235,9 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
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")
|
||||
nx.write_gexf(G, f, version="1.2draft")
|
||||
|
||||
def dump(self, dir_path=None, formats=None):
|
||||
def dump(self, *args, formats=None, **kwargs):
|
||||
if not formats:
|
||||
return
|
||||
functions = {
|
||||
@@ -228,10 +246,13 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
}
|
||||
for f in formats:
|
||||
if f in functions:
|
||||
functions[f](dir_path)
|
||||
functions[f](*args, **kwargs)
|
||||
else:
|
||||
raise ValueError('Unknown format: {}'.format(f))
|
||||
|
||||
def dump_sqlite(self, f):
|
||||
return self._history.dump(f)
|
||||
|
||||
def state_to_tuples(self, now=None):
|
||||
if now is None:
|
||||
now = self.now
|
||||
@@ -296,18 +317,40 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
|
||||
return G
|
||||
|
||||
def stats(self):
|
||||
stats = {}
|
||||
stats['network'] = {}
|
||||
stats['network']['n_nodes'] = self.G.number_of_nodes()
|
||||
stats['network']['n_edges'] = self.G.number_of_edges()
|
||||
c = Counter()
|
||||
c.update(a.__class__.__name__ for a in self.network_agents)
|
||||
stats['agents'] = {}
|
||||
stats['agents']['model_count'] = dict(c)
|
||||
c2 = Counter()
|
||||
c2.update(a['id'] for a in self.network_agents)
|
||||
stats['agents']['state_count'] = dict(c2)
|
||||
stats['params'] = self.environment_params
|
||||
return stats
|
||||
|
||||
def log_stats(self):
|
||||
stats = self.stats()
|
||||
serialization.logger.info('Environment stats: \n{}'.format(yaml.dump(stats, default_flow_style=False)))
|
||||
|
||||
def __getstate__(self):
|
||||
state = self.__dict__.copy()
|
||||
state = {}
|
||||
for prop in _CONFIG_PROPS:
|
||||
state[prop] = self.__dict__[prop]
|
||||
state['G'] = json_graph.node_link_data(self.G)
|
||||
state['network_agents'] = agents._serialize_distribution(self.network_agents)
|
||||
state['environment_agents'] = agents._convert_agent_types(self.environment_agents,
|
||||
to_string=True)
|
||||
del state['_queue']
|
||||
state['environment_agents'] = self._env_agents
|
||||
state['history'] = self._history
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__ = 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.network_agents = self.calculate_distribution(self._convert_agent_types(self.network_agents))
|
||||
self.environment_agents = self._convert_agent_types(self.environment_agents)
|
||||
return state
|
||||
self._history = state['history']
|
||||
|
||||
|
||||
SoilEnvironment = Environment
|
||||
|
175
soil/exporters.py
Normal file
175
soil/exporters.py
Normal file
@@ -0,0 +1,175 @@
|
||||
import os
|
||||
import time
|
||||
from io import BytesIO
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import networkx as nx
|
||||
import pandas as pd
|
||||
|
||||
from .serialization import deserialize
|
||||
from .utils import open_or_reuse, logger, timer
|
||||
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
def for_sim(simulation, names, *args, **kwargs):
|
||||
'''Return the set of exporters for a simulation, given the exporter names'''
|
||||
exporters = []
|
||||
for name in names:
|
||||
mod = deserialize(name, known_modules=['soil.exporters'])
|
||||
exporters.append(mod(simulation, *args, **kwargs))
|
||||
return exporters
|
||||
|
||||
|
||||
class DryRunner(BytesIO):
|
||||
def __init__(self, fname, *args, copy_to=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__fname = fname
|
||||
self.__copy_to = copy_to
|
||||
|
||||
def write(self, txt):
|
||||
if self.__copy_to:
|
||||
self.__copy_to.write('{}:::{}'.format(self.__fname, txt))
|
||||
try:
|
||||
super().write(txt)
|
||||
except TypeError:
|
||||
super().write(bytes(txt, 'utf-8'))
|
||||
|
||||
def close(self):
|
||||
logger.info('**Not** written to {} (dry run mode):\n\n{}\n\n'.format(self.__fname,
|
||||
self.getvalue().decode()))
|
||||
super().close()
|
||||
|
||||
|
||||
class Exporter:
|
||||
'''
|
||||
Interface for all exporters. It is not necessary, but it is useful
|
||||
if you don't plan to implement all the methods.
|
||||
'''
|
||||
|
||||
def __init__(self, simulation, outdir=None, dry_run=None, copy_to=None):
|
||||
self.sim = simulation
|
||||
outdir = outdir or os.getcwd()
|
||||
self.outdir = os.path.join(outdir,
|
||||
simulation.group or '',
|
||||
simulation.name)
|
||||
self.dry_run = dry_run
|
||||
self.copy_to = copy_to
|
||||
|
||||
def start(self):
|
||||
'''Method to call when the simulation starts'''
|
||||
|
||||
def end(self):
|
||||
'''Method to call when the simulation ends'''
|
||||
|
||||
def trial_end(self, env):
|
||||
'''Method to call when a trial ends'''
|
||||
|
||||
def output(self, f, mode='w', **kwargs):
|
||||
if self.dry_run:
|
||||
f = DryRunner(f, copy_to=self.copy_to)
|
||||
else:
|
||||
try:
|
||||
if not os.path.isabs(f):
|
||||
f = os.path.join(self.outdir, f)
|
||||
except TypeError:
|
||||
pass
|
||||
return open_or_reuse(f, mode=mode, **kwargs)
|
||||
|
||||
|
||||
class Default(Exporter):
|
||||
'''Default exporter. Writes CSV and sqlite results, as well as the simulation YAML'''
|
||||
|
||||
def start(self):
|
||||
if not self.dry_run:
|
||||
logger.info('Dumping results to %s', self.outdir)
|
||||
self.sim.dump_yaml(outdir=self.outdir)
|
||||
else:
|
||||
logger.info('NOT dumping results')
|
||||
|
||||
def trial_end(self, env):
|
||||
if not self.dry_run:
|
||||
with timer('Dumping simulation {} trial {}'.format(self.sim.name,
|
||||
env.name)):
|
||||
with self.output('{}.sqlite'.format(env.name), mode='wb') as f:
|
||||
env.dump_sqlite(f)
|
||||
|
||||
|
||||
class CSV(Exporter):
|
||||
def trial_end(self, env):
|
||||
if not self.dry_run:
|
||||
with timer('[CSV] Dumping simulation {} trial {}'.format(self.sim.name,
|
||||
env.name)):
|
||||
with self.output('{}.csv'.format(env.name)) as f:
|
||||
env.dump_csv(f)
|
||||
|
||||
|
||||
class Gexf(Exporter):
|
||||
def trial_end(self, env):
|
||||
if not self.dry_run:
|
||||
with timer('[CSV] Dumping simulation {} trial {}'.format(self.sim.name,
|
||||
env.name)):
|
||||
with self.output('{}.gexf'.format(env.name), mode='wb') as f:
|
||||
env.dump_gexf(f)
|
||||
|
||||
|
||||
class Dummy(Exporter):
|
||||
|
||||
def start(self):
|
||||
with self.output('dummy', 'w') as f:
|
||||
f.write('simulation started @ {}\n'.format(time.time()))
|
||||
|
||||
def trial_end(self, env):
|
||||
with self.output('dummy', 'w') as f:
|
||||
for i in env.history_to_tuples():
|
||||
f.write(','.join(map(str, i)))
|
||||
f.write('\n')
|
||||
|
||||
def end(self):
|
||||
with self.output('dummy', 'a') as f:
|
||||
f.write('simulation ended @ {}\n'.format(time.time()))
|
||||
|
||||
|
||||
class Distribution(Exporter):
|
||||
'''
|
||||
Write the distribution of agent states at the end of each trial,
|
||||
the mean value, and its deviation.
|
||||
'''
|
||||
|
||||
def start(self):
|
||||
self.means = []
|
||||
self.counts = []
|
||||
|
||||
def trial_end(self, env):
|
||||
df = env[None, None, None].df()
|
||||
ix = df.index[-1]
|
||||
attrs = df.columns.levels[0]
|
||||
vc = {}
|
||||
stats = {}
|
||||
for a in attrs:
|
||||
t = df.loc[(ix, a)]
|
||||
try:
|
||||
self.means.append(('mean', a, t.mean()))
|
||||
except TypeError:
|
||||
for name, count in t.value_counts().iteritems():
|
||||
self.counts.append(('count', a, name, count))
|
||||
|
||||
def end(self):
|
||||
dfm = pd.DataFrame(self.means, columns=['metric', 'key', 'value'])
|
||||
dfc = pd.DataFrame(self.counts, columns=['metric', 'key', 'value', 'count'])
|
||||
dfm = dfm.groupby(by=['key']).agg(['mean', 'std', 'count', 'median', 'max', 'min'])
|
||||
dfc = dfc.groupby(by=['key', 'value']).agg(['mean', 'std', 'count', 'median', 'max', 'min'])
|
||||
with self.output('counts.csv') as f:
|
||||
dfc.to_csv(f)
|
||||
with self.output('metrics.csv') as f:
|
||||
dfm.to_csv(f)
|
||||
|
||||
class GraphDrawing(Exporter):
|
||||
|
||||
def trial_end(self, env):
|
||||
# Outside effects
|
||||
f = plt.figure()
|
||||
nx.draw(env.G, node_size=10, width=0.2, pos=nx.spring_layout(env.G, scale=100), ax=f.add_subplot(111))
|
||||
with open('graph-{}.png'.format(env.name)) as f:
|
||||
f.savefig(f)
|
160
soil/history.py
160
soil/history.py
@@ -3,9 +3,14 @@ import os
|
||||
import pandas as pd
|
||||
import sqlite3
|
||||
import copy
|
||||
from collections import UserDict, Iterable, namedtuple
|
||||
import logging
|
||||
import tempfile
|
||||
|
||||
from . import utils
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from collections import UserDict, namedtuple
|
||||
|
||||
from . import serialization
|
||||
|
||||
|
||||
class History:
|
||||
@@ -13,100 +18,139 @@ 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))
|
||||
def __init__(self, name=None, db_path=None, backup=False):
|
||||
self._db = None
|
||||
|
||||
if db_path is None:
|
||||
db_path = ":memory:"
|
||||
else:
|
||||
if backup and os.path.exists(db_path):
|
||||
newname = db_path + '.backup{}.sqlite'.format(time.time())
|
||||
os.rename(db_path, newname)
|
||||
if not name:
|
||||
name = time.time()
|
||||
_, db_path = tempfile.mkstemp(suffix='{}.sqlite'.format(name))
|
||||
|
||||
if backup and os.path.exists(db_path):
|
||||
newname = db_path + '.backup{}.sqlite'.format(time.time())
|
||||
os.rename(db_path, newname)
|
||||
|
||||
self.db_path = db_path
|
||||
|
||||
self.db = db_path
|
||||
|
||||
with self.db:
|
||||
logger.debug('Creating database {}'.format(self.db_path))
|
||||
self.db.execute('''CREATE TABLE IF NOT EXISTS history (agent_id text, t_step int, key text, value text text)''')
|
||||
self.db.execute('''CREATE TABLE IF NOT EXISTS value_types (key text, value_type text)''')
|
||||
self.db.execute('''CREATE UNIQUE INDEX IF NOT EXISTS idx_history ON history (agent_id, t_step, key);''')
|
||||
self._dtypes = {}
|
||||
self._tups = []
|
||||
|
||||
def conversors(self, key):
|
||||
"""Get the serializer and deserializer for a given key."""
|
||||
if key not in self._dtypes:
|
||||
self.read_types()
|
||||
return self._dtypes[key]
|
||||
|
||||
@property
|
||||
def db(self):
|
||||
try:
|
||||
self._db.cursor()
|
||||
except sqlite3.ProgrammingError:
|
||||
except (sqlite3.ProgrammingError, AttributeError):
|
||||
self.db = None # Reset the database
|
||||
return self._db
|
||||
|
||||
@db.setter
|
||||
def db(self, db_path=None):
|
||||
self._close()
|
||||
db_path = db_path or self.db_path
|
||||
if isinstance(db_path, str):
|
||||
logger.debug('Connecting to database {}'.format(db_path))
|
||||
self._db = sqlite3.connect(db_path)
|
||||
else:
|
||||
self._db = db_path
|
||||
|
||||
def _close(self):
|
||||
if self._db is None:
|
||||
return
|
||||
self.flush_cache()
|
||||
self._db.close()
|
||||
self._db = None
|
||||
|
||||
@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):
|
||||
with self.db:
|
||||
for rec in records:
|
||||
if not isinstance(rec, Record):
|
||||
rec = Record(*rec)
|
||||
if rec.key not in self._dtypes:
|
||||
name = utils.name(rec.value)
|
||||
serializer = utils.serializer(name)
|
||||
deserializer = utils.deserializer(name)
|
||||
self._dtypes[rec.key] = (name, serializer, deserializer)
|
||||
self.db.execute("replace into value_types (key, value_type) values (?, ?)", (rec.key, name))
|
||||
self.db.execute("replace into history(agent_id, t_step, key, value) values (?, ?, ?, ?)", (rec.agent_id, rec.t_step, rec.key, rec.value))
|
||||
'''
|
||||
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, *args, **kwargs):
|
||||
self._tups.append(Record(*args, **kwargs))
|
||||
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 = serialization.name(value)
|
||||
serializer = serialization.serializer(name)
|
||||
deserializer = serialization.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.
|
||||
'''
|
||||
self.save_records(self._tups)
|
||||
logger.debug('Flushing cache {}'.format(self.db_path))
|
||||
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
|
||||
_, _ , des = self.conversors(key)
|
||||
yield agent_id, t_step, key, des(value)
|
||||
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)
|
||||
with self.db:
|
||||
res = self.db.execute("select key, value_type from value_types ").fetchall()
|
||||
for k, v in res:
|
||||
serializer = serialization.serializer(v)
|
||||
deserializer = serialization.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 []
|
||||
@@ -120,8 +164,6 @@ class History:
|
||||
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()
|
||||
@@ -176,11 +218,27 @@ class History:
|
||||
for k, v in self._dtypes.items():
|
||||
if k in df_p:
|
||||
dtype, _, deserial = v
|
||||
df_p[k] = df_p[k].fillna(method='ffill').fillna(deserial()).astype(dtype)
|
||||
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 = {}
|
||||
self._db = None
|
||||
|
||||
def dump(self, f):
|
||||
self._close()
|
||||
for line in open(self.db_path, 'rb'):
|
||||
f.write(line)
|
||||
|
||||
|
||||
class Records():
|
||||
|
||||
@@ -230,10 +288,13 @@ class Records():
|
||||
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:
|
||||
except KeyError as ex:
|
||||
return self.dtypes[f.key][2]()
|
||||
return list(self)
|
||||
|
||||
def df(self):
|
||||
return self._df
|
||||
|
||||
def __getitem__(self, k):
|
||||
n = copy.copy(self)
|
||||
n.filter(k)
|
||||
@@ -249,6 +310,5 @@ class Records():
|
||||
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')
|
||||
|
201
soil/serialization.py
Normal file
201
soil/serialization.py
Normal file
@@ -0,0 +1,201 @@
|
||||
import os
|
||||
import logging
|
||||
import ast
|
||||
import sys
|
||||
import importlib
|
||||
from glob import glob
|
||||
from itertools import product, chain
|
||||
|
||||
import yaml
|
||||
import networkx as nx
|
||||
|
||||
from jinja2 import Template
|
||||
|
||||
|
||||
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):
|
||||
path = os.path.join(dir_path, path)
|
||||
extension = os.path.splitext(path)[1][1:]
|
||||
kwargs = {}
|
||||
if extension == 'gexf':
|
||||
kwargs['version'] = '1.2draft'
|
||||
kwargs['node_type'] = int
|
||||
try:
|
||||
method = getattr(nx.readwrite, 'read_' + extension)
|
||||
except AttributeError:
|
||||
raise AttributeError('Unknown format')
|
||||
return method(path, **kwargs)
|
||||
|
||||
net_args = network_params.copy()
|
||||
if 'generator' not in net_args:
|
||||
return nx.Graph()
|
||||
|
||||
net_gen = net_args.pop('generator')
|
||||
|
||||
if dir_path not in sys.path:
|
||||
sys.path.append(dir_path)
|
||||
|
||||
method = deserializer(net_gen,
|
||||
known_modules=['networkx.generators',])
|
||||
|
||||
return method(**net_args)
|
||||
|
||||
|
||||
def load_file(infile):
|
||||
with open(infile, 'r') as f:
|
||||
return list(chain.from_iterable(map(expand_template, load_string(f))))
|
||||
|
||||
|
||||
def load_string(string):
|
||||
yield from yaml.load_all(string, Loader=yaml.FullLoader)
|
||||
|
||||
|
||||
def expand_template(config):
|
||||
if 'template' not in config:
|
||||
yield config
|
||||
return
|
||||
if 'vars' not in config:
|
||||
raise ValueError(('You must provide a definition of variables'
|
||||
' for the template.'))
|
||||
|
||||
template = Template(config['template'])
|
||||
|
||||
sampler_name = config.get('sampler', 'SALib.sample.morris.sample')
|
||||
n_samples = int(config.get('samples', 100))
|
||||
sampler = deserializer(sampler_name)
|
||||
bounds = config['vars']['bounds']
|
||||
|
||||
problem = {
|
||||
'num_vars': len(bounds),
|
||||
'names': list(bounds.keys()),
|
||||
'bounds': list(v for v in bounds.values())
|
||||
}
|
||||
samples = sampler(problem, n_samples)
|
||||
|
||||
lists = config['vars'].get('lists', {})
|
||||
names = list(lists.keys())
|
||||
values = list(lists.values())
|
||||
combs = list(product(*values))
|
||||
|
||||
allnames = names + problem['names']
|
||||
allvalues = [(list(i[0])+list(i[1])) for i in product(combs, samples)]
|
||||
params = list(map(lambda x: dict(zip(allnames, x)), allvalues))
|
||||
|
||||
|
||||
blank_str = template.render({k: 0 for k in allnames})
|
||||
blank = list(load_string(blank_str))
|
||||
if len(blank) > 1:
|
||||
raise ValueError('Templates must not return more than one configuration')
|
||||
if 'name' in blank[0]:
|
||||
raise ValueError('Templates cannot be named, use group instead')
|
||||
|
||||
confs = []
|
||||
for ps in params:
|
||||
string = template.render(ps)
|
||||
for c in load_string(string):
|
||||
yield c
|
||||
|
||||
|
||||
def load_files(*patterns, **kwargs):
|
||||
for pattern in patterns:
|
||||
for i in glob(pattern, **kwargs):
|
||||
for config in load_file(i):
|
||||
path = os.path.abspath(i)
|
||||
if 'dir_path' not in config:
|
||||
config['dir_path'] = os.path.dirname(path)
|
||||
yield config, path
|
||||
|
||||
|
||||
def load_config(config):
|
||||
if isinstance(config, dict):
|
||||
yield config, None
|
||||
else:
|
||||
yield from load_files(config)
|
||||
|
||||
|
||||
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(type_) != str: # Already deserialized
|
||||
return type_
|
||||
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 (ModuleNotFoundError, 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)
|
@@ -1,8 +1,9 @@
|
||||
import os
|
||||
import time
|
||||
import imp
|
||||
import importlib
|
||||
import sys
|
||||
import yaml
|
||||
import traceback
|
||||
import networkx as nx
|
||||
from networkx.readwrite import json_graph
|
||||
from multiprocessing import Pool
|
||||
@@ -12,11 +13,13 @@ import pickle
|
||||
|
||||
from nxsim import NetworkSimulation
|
||||
|
||||
from . import utils, environment, basestring, agents
|
||||
from . import serialization, utils, basestring, agents
|
||||
from .environment import Environment
|
||||
from .utils import logger
|
||||
from .exporters import for_sim as exporters_for_sim
|
||||
|
||||
|
||||
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.
|
||||
@@ -43,45 +46,84 @@ 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
|
||||
group : str, optional
|
||||
a group name can be used to link simulations
|
||||
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 to load simulation assets (files, modules...)
|
||||
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,
|
||||
|
||||
def __init__(self, name=None, group=None, topology=None, network_params=None,
|
||||
network_agents=None, agent_type=None, states=None,
|
||||
default_state=None, interval=1, dump=None, dry_run=False,
|
||||
dir_path=None, num_trials=1, max_time=100,
|
||||
agent_module=None, load_module=None, seed=None,
|
||||
environment_agents=None, environment_params=None, **kwargs):
|
||||
|
||||
if topology is None:
|
||||
topology = utils.load_network(network_params,
|
||||
dir_path=dir_path)
|
||||
elif isinstance(topology, basestring) or isinstance(topology, dict):
|
||||
topology = json_graph.node_link_graph(topology)
|
||||
default_state=None, interval=1, num_trials=1,
|
||||
max_time=100, load_module=None, seed=None,
|
||||
dir_path=None, environment_agents=None,
|
||||
environment_params=None, environment_class=None,
|
||||
**kwargs):
|
||||
|
||||
self.seed = str(seed) or str(time.time())
|
||||
self.load_module = load_module
|
||||
self.topology = nx.Graph(topology)
|
||||
self.network_params = network_params
|
||||
self.name = name or 'UnnamedSimulation'
|
||||
self.name = name or 'Unnamed_' + time.strftime("%Y-%m-%d_%H:%M:%S")
|
||||
self.group = group or None
|
||||
self.num_trials = num_trials
|
||||
self.max_time = max_time
|
||||
self.default_state = default_state or {}
|
||||
self.dir_path = dir_path or os.getcwd()
|
||||
self.interval = interval
|
||||
self.seed = str(seed) or str(time.time())
|
||||
self.dump = dump
|
||||
self.dry_run = dry_run
|
||||
self.environment_params = environment_params or {}
|
||||
|
||||
if load_module:
|
||||
path = sys.path + [self.dir_path, os.getcwd()]
|
||||
f, fp, desc = imp.find_module(load_module, path)
|
||||
imp.load_module('soil.agents.custom', f, fp, desc)
|
||||
sys.path += list(x for x in [os.getcwd(), self.dir_path] if x not in sys.path)
|
||||
|
||||
if topology is None:
|
||||
topology = serialization.load_network(network_params,
|
||||
dir_path=self.dir_path)
|
||||
elif isinstance(topology, basestring) or isinstance(topology, dict):
|
||||
topology = json_graph.node_link_graph(topology)
|
||||
self.topology = nx.Graph(topology)
|
||||
|
||||
|
||||
self.environment_params = environment_params or {}
|
||||
self.environment_class = serialization.deserialize(environment_class,
|
||||
known_modules=['soil.environment', ]) or Environment
|
||||
|
||||
environment_agents = environment_agents or []
|
||||
self.environment_agents = agents._convert_agent_types(environment_agents)
|
||||
self.environment_agents = agents._convert_agent_types(environment_agents,
|
||||
known_modules=[self.load_module])
|
||||
|
||||
distro = agents.calculate_distribution(network_agents,
|
||||
agent_type)
|
||||
self.network_agents = agents._convert_agent_types(distro)
|
||||
self.network_agents = agents._convert_agent_types(distro,
|
||||
known_modules=[self.load_module])
|
||||
|
||||
self.states = agents._validate_states(states,
|
||||
self.topology)
|
||||
@@ -90,28 +132,51 @@ class SoilSimulation(NetworkSimulation):
|
||||
return self.run(*args, **kwargs)
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
return list(self.run_simulation_gen(*args, **kwargs))
|
||||
'''Run the simulation and return the list of resulting environments'''
|
||||
return list(self._run_simulation_gen(*args, **kwargs))
|
||||
|
||||
def _run_sync_or_async(self, parallel=False, *args, **kwargs):
|
||||
if parallel:
|
||||
p = Pool()
|
||||
func = partial(self.run_trial_exceptions,
|
||||
*args,
|
||||
**kwargs)
|
||||
for i in p.imap_unordered(func, range(self.num_trials)):
|
||||
if isinstance(i, Exception):
|
||||
logger.error('Trial failed:\n\t%s', i.message)
|
||||
continue
|
||||
yield i
|
||||
else:
|
||||
for i in range(self.num_trials):
|
||||
yield self.run_trial(i,
|
||||
*args,
|
||||
**kwargs)
|
||||
|
||||
def _run_simulation_gen(self, *args, parallel=False, dry_run=False,
|
||||
exporters=None, outdir=None, exporter_params={}, **kwargs):
|
||||
logger.info('Using exporters: %s', exporters or [])
|
||||
logger.info('Output directory: %s', outdir)
|
||||
exporters = exporters_for_sim(self,
|
||||
exporters or [],
|
||||
dry_run=dry_run,
|
||||
outdir=outdir,
|
||||
**exporter_params)
|
||||
|
||||
def run_simulation_gen(self, *args, parallel=False, dry_run=False,
|
||||
**kwargs):
|
||||
p = Pool()
|
||||
with utils.timer('simulation {}'.format(self.name)):
|
||||
if parallel:
|
||||
func = partial(self.run_trial, dry_run=dry_run or self.dry_run,
|
||||
return_env=not parallel, **kwargs)
|
||||
for i in p.imap_unordered(func, range(self.num_trials)):
|
||||
yield i
|
||||
else:
|
||||
for i in range(self.num_trials):
|
||||
yield self.run_trial(i, dry_run=dry_run or self.dry_run, **kwargs)
|
||||
if not (dry_run or self.dry_run):
|
||||
logger.info('Dumping results to {}'.format(self.dir_path))
|
||||
self.dump_pickle(self.dir_path)
|
||||
self.dump_yaml(self.dir_path)
|
||||
else:
|
||||
logger.info('NOT dumping results')
|
||||
for exporter in exporters:
|
||||
exporter.start()
|
||||
|
||||
def get_env(self, trial_id=0, **kwargs):
|
||||
for env in self._run_sync_or_async(*args, parallel=parallel,
|
||||
**kwargs):
|
||||
for exporter in exporters:
|
||||
exporter.trial_end(env)
|
||||
yield env
|
||||
|
||||
for exporter in exporters:
|
||||
exporter.end()
|
||||
|
||||
def get_env(self, trial_id = 0, **kwargs):
|
||||
'''Create an environment for a trial of the simulation'''
|
||||
opts = self.environment_params.copy()
|
||||
env_name = '{}_trial_{}'.format(self.name, trial_id)
|
||||
opts.update({
|
||||
@@ -119,19 +184,17 @@ class SoilSimulation(NetworkSimulation):
|
||||
'topology': self.topology.copy(),
|
||||
'seed': self.seed+env_name,
|
||||
'initial_time': 0,
|
||||
'dry_run': self.dry_run,
|
||||
'interval': self.interval,
|
||||
'network_agents': self.network_agents,
|
||||
'states': self.states,
|
||||
'default_state': self.default_state,
|
||||
'environment_agents': self.environment_agents,
|
||||
'dir_path': self.dir_path,
|
||||
})
|
||||
opts.update(kwargs)
|
||||
env = environment.SoilEnvironment(**opts)
|
||||
env = self.environment_class(**opts)
|
||||
return env
|
||||
|
||||
def run_trial(self, trial_id=0, until=None, return_env=True, **opts):
|
||||
def run_trial(self, trial_id=0, until=None, **opts):
|
||||
"""Run a single trial of the simulation
|
||||
|
||||
Parameters
|
||||
@@ -140,15 +203,22 @@ class SoilSimulation(NetworkSimulation):
|
||||
"""
|
||||
# Set-up trial environment and graph
|
||||
until = until or self.max_time
|
||||
env = self.get_env(trial_id=trial_id, **opts)
|
||||
env = self.get_env(trial_id = trial_id, **opts)
|
||||
# Set up agents on nodes
|
||||
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
|
||||
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__()
|
||||
@@ -156,64 +226,81 @@ 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
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
if not file_name:
|
||||
file_name = os.path.join(dir_path,
|
||||
'{}.dumped.yml'.format(self.name))
|
||||
with open(file_name, 'w') as f:
|
||||
|
||||
def dump_yaml(self, f=None, outdir=None):
|
||||
if not f and not outdir:
|
||||
raise ValueError('specify a file or an output directory')
|
||||
|
||||
if not f:
|
||||
f = os.path.join(outdir, '{}.dumped.yml'.format(self.name))
|
||||
|
||||
with utils.open_or_reuse(f, '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
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
if not pickle_name:
|
||||
pickle_name = os.path.join(dir_path,
|
||||
'{}.simulation.pickle'.format(self.name))
|
||||
with open(pickle_name, 'wb') as f:
|
||||
def dump_pickle(self, f=None, outdir=None):
|
||||
if not outdir and not f:
|
||||
raise ValueError('specify a file or an output directory')
|
||||
|
||||
if not f:
|
||||
f = os.path.join(outdir,
|
||||
'{}.simulation.pickle'.format(self.name))
|
||||
with utils.open_or_reuse(f, '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'] = agents._serialize_distribution(self.network_agents)
|
||||
state['environment_agents'] = agents._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'] = serialization.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 = agents.calculate_distribution(agents._convert_agent_types(self.network_agents))
|
||||
self.environment_agents = agents._convert_agent_types(self.environment_agents)
|
||||
self.environment_agents = agents._convert_agent_types(self.environment_agents,
|
||||
known_modules=[self.load_module])
|
||||
self.environment_class = serialization.deserialize(self.environment_class,
|
||||
known_modules=[self.load_module, 'soil.environment', ]) # func, name
|
||||
return state
|
||||
|
||||
|
||||
def from_config(config):
|
||||
config = list(utils.load_config(config))
|
||||
def all_from_config(config):
|
||||
configs = list(serialization.load_config(config))
|
||||
for config, _ in configs:
|
||||
sim = Simulation(**config)
|
||||
yield sim
|
||||
|
||||
|
||||
def from_config(conf_or_path):
|
||||
config = list(serialization.load_config(conf_or_path))
|
||||
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, results_dir='soil_output', dry_run=False, dump=None, timestamp=False, **kwargs):
|
||||
def run_from_config(*configs, **kwargs):
|
||||
for config_def in configs:
|
||||
# logger.info("Found {} config(s)".format(len(ls)))
|
||||
for config, _ in utils.load_config(config_def):
|
||||
for config, path in serialization.load_config(config_def):
|
||||
name = config.get('name', 'unnamed')
|
||||
logger.info("Using config(s): {name}".format(name=name))
|
||||
|
||||
if timestamp:
|
||||
sim_folder = '{}_{}'.format(name,
|
||||
time.strftime("%Y-%m-%d_%H:%M:%S"))
|
||||
else:
|
||||
sim_folder = name
|
||||
dir_path = os.path.join(results_dir, sim_folder)
|
||||
sim = SoilSimulation(dir_path=dir_path, dump=dump, **config)
|
||||
logger.info('Dumping results to {} : {}'.format(sim.dir_path, sim.dump))
|
||||
dir_path = config.pop('dir_path', os.path.dirname(path))
|
||||
sim = Simulation(dir_path=dir_path,
|
||||
**config)
|
||||
sim.run_simulation(**kwargs)
|
||||
|
104
soil/utils.py
104
soil/utils.py
@@ -1,105 +1,37 @@
|
||||
import os
|
||||
import yaml
|
||||
import logging
|
||||
import importlib
|
||||
from time import time
|
||||
from glob import glob
|
||||
from random import random
|
||||
from copy import deepcopy
|
||||
|
||||
import networkx as nx
|
||||
import time
|
||||
import os
|
||||
|
||||
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):
|
||||
path = os.path.join(dir_path, path)
|
||||
extension = os.path.splitext(path)[1][1:]
|
||||
kwargs = {}
|
||||
if extension == 'gexf':
|
||||
kwargs['version'] = '1.2draft'
|
||||
kwargs['node_type'] = int
|
||||
try:
|
||||
method = getattr(nx.readwrite, 'read_' + extension)
|
||||
except AttributeError:
|
||||
raise AttributeError('Unknown format')
|
||||
return method(path, **kwargs)
|
||||
|
||||
net_args = network_params.copy()
|
||||
net_type = net_args.pop('generator')
|
||||
|
||||
method = getattr(nx.generators, net_type)
|
||||
return method(**net_args)
|
||||
|
||||
|
||||
def load_file(infile):
|
||||
with open(infile, 'r') as f:
|
||||
return list(yaml.load_all(f))
|
||||
|
||||
|
||||
def load_files(*patterns):
|
||||
for pattern in patterns:
|
||||
for i in glob(pattern):
|
||||
for config in load_file(i):
|
||||
yield config, os.path.abspath(i)
|
||||
|
||||
|
||||
def load_config(config):
|
||||
if isinstance(config, dict):
|
||||
yield config, None
|
||||
else:
|
||||
yield from load_files(config)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def timer(name='task', pre="", function=logger.info, to_object=None):
|
||||
start = time()
|
||||
function('{}Starting {} at {}.'.format(pre, name, start))
|
||||
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
|
||||
|
||||
|
||||
def repr(v):
|
||||
func = serializer(v)
|
||||
tname = name(v)
|
||||
return func(v), tname
|
||||
def safe_open(path, *args, **kwargs):
|
||||
outdir = os.path.dirname(path)
|
||||
if outdir and not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
return open(path, *args, **kwargs)
|
||||
|
||||
|
||||
def name(v):
|
||||
return type(v).__name__
|
||||
|
||||
|
||||
def serializer(type_):
|
||||
if type_ == 'bool':
|
||||
return lambda x: "true" if x else ""
|
||||
return lambda x: x
|
||||
|
||||
|
||||
def deserializer(type_):
|
||||
def open_or_reuse(f, *args, **kwargs):
|
||||
try:
|
||||
# Check if it's a builtin type
|
||||
module = importlib.import_module('builtins')
|
||||
cls = getattr(module, type_)
|
||||
except AttributeError:
|
||||
# if not, separate module and class
|
||||
module, type_ = type_.rsplit(".", 1)
|
||||
module = importlib.import_module(module)
|
||||
cls = getattr(module, type_)
|
||||
return cls
|
||||
|
||||
|
||||
def convert(value, type_):
|
||||
return deserializer(type_)(value)
|
||||
return safe_open(f, *args, **kwargs)
|
||||
except TypeError:
|
||||
return f
|
||||
|
@@ -1,255 +0,0 @@
|
||||
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')
|
@@ -19,7 +19,7 @@ from xml.etree.ElementTree import tostring
|
||||
from tornado.concurrent import run_on_executor
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from ..simulation import SoilSimulation
|
||||
from ..simulation import Simulation
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
@@ -168,7 +168,7 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
|
||||
|
||||
@run_on_executor
|
||||
def nonblocking(self, config):
|
||||
simulation = SoilSimulation(**config)
|
||||
simulation = Simulation(**config)
|
||||
return simulation.run()
|
||||
|
||||
@tornado.gen.coroutine
|
||||
@@ -180,7 +180,7 @@ class SocketHandler(tornado.websocket.WebSocketHandler):
|
||||
with self.logging(self.simulation_name):
|
||||
try:
|
||||
config = dict(**self.config)
|
||||
config['dir_path'] = os.path.join(self.application.dir_path, config['name'])
|
||||
config['outdir'] = os.path.join(self.application.outdir, config['name'])
|
||||
config['dump'] = self.application.dump
|
||||
self.trials = yield self.nonblocking(config)
|
||||
|
||||
@@ -232,12 +232,12 @@ class ModularServer(tornado.web.Application):
|
||||
settings = {'debug': True,
|
||||
'template_path': ROOT + '/templates'}
|
||||
|
||||
def __init__(self, dump=False, dir_path='output', name='SOIL', verbose=True, *args, **kwargs):
|
||||
def __init__(self, dump=False, outdir='output', name='SOIL', verbose=True, *args, **kwargs):
|
||||
|
||||
self.verbose = verbose
|
||||
self.name = name
|
||||
self.dump = dump
|
||||
self.dir_path = dir_path
|
||||
self.outdir = outdir
|
||||
|
||||
# Initializing the application itself:
|
||||
super().__init__(self.handlers, **self.settings)
|
||||
@@ -271,4 +271,4 @@ def main():
|
||||
parser.add_argument('--verbose', '-v', help='verbose mode', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
run(name=args.name, port=(args.port[0] if isinstance(args.port, list) else args.port), verbose=args.verbose)
|
||||
run(name=args.name, port=(args.port[0] if isinstance(args.port, list) else args.port), verbose=args.verbose)
|
@@ -0,0 +1 @@
|
||||
pytest
|
12
tests/test.gexf
Normal file
12
tests/test.gexf
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<gexf version="1.2" xmlns="http://www.gexf.net/1.2draft" xmlns:viz="http://www.gexf.net/1.2draft/viz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<graph defaultedgetype="undirected" mode="static">
|
||||
<nodes>
|
||||
<node id="0" label="0" />
|
||||
<node id="1" label="1" />
|
||||
</nodes>
|
||||
<edges>
|
||||
<edge id="0" source="0" target="1" />
|
||||
</edges>
|
||||
</graph>
|
||||
</gexf>
|
@@ -39,7 +39,6 @@ class TestAnalysis(TestCase):
|
||||
agent should be able to update its state."""
|
||||
config = {
|
||||
'name': 'analysis',
|
||||
'dry_run': True,
|
||||
'seed': 'seed',
|
||||
'network_params': {
|
||||
'generator': 'complete_graph',
|
||||
@@ -53,7 +52,7 @@ class TestAnalysis(TestCase):
|
||||
}
|
||||
}
|
||||
s = simulation.from_config(config)
|
||||
self.env = s.run_simulation()[0]
|
||||
self.env = s.run_simulation(dry_run=True)[0]
|
||||
|
||||
def test_saved(self):
|
||||
env = self.env
|
||||
@@ -65,7 +64,7 @@ class TestAnalysis(TestCase):
|
||||
|
||||
def test_count(self):
|
||||
env = self.env
|
||||
df = analysis.read_sql(env._history._db)
|
||||
df = analysis.read_sql(env._history.db_path)
|
||||
res = analysis.get_count(df, 'SEED', 'id')
|
||||
assert res['SEED']['seedanalysis_trial_0'].iloc[0] == 1
|
||||
assert res['SEED']['seedanalysis_trial_0'].iloc[-1] == 1
|
||||
|
54
tests/test_examples.py
Normal file
54
tests/test_examples.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from unittest import TestCase
|
||||
import os
|
||||
from os.path import join
|
||||
|
||||
from soil import serialization, simulation
|
||||
|
||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
EXAMPLES = join(ROOT, '..', 'examples')
|
||||
|
||||
FORCE_TESTS = os.environ.get('FORCE_TESTS', '')
|
||||
|
||||
|
||||
class TestExamples(TestCase):
|
||||
pass
|
||||
|
||||
|
||||
def make_example_test(path, config):
|
||||
def wrapped(self):
|
||||
root = os.getcwd()
|
||||
for s in simulation.all_from_config(path):
|
||||
iterations = s.max_time * s.num_trials
|
||||
if iterations > 1000:
|
||||
s.max_time = 100
|
||||
s.num_trials = 1
|
||||
if config.get('skip_test', False) and not FORCE_TESTS:
|
||||
self.skipTest('Example ignored.')
|
||||
envs = s.run_simulation(dry_run=True)
|
||||
assert envs
|
||||
for env in envs:
|
||||
assert env
|
||||
try:
|
||||
n = config['network_params']['n']
|
||||
assert len(list(env.network_agents)) == n
|
||||
assert env.now > 2 # It has run
|
||||
assert env.now <= config['max_time'] # But not further than allowed
|
||||
except KeyError:
|
||||
pass
|
||||
return wrapped
|
||||
|
||||
|
||||
def add_example_tests():
|
||||
for config, path in serialization.load_files(
|
||||
join(EXAMPLES, '*', '*.yml'),
|
||||
join(EXAMPLES, '*.yml'),
|
||||
):
|
||||
p = make_example_test(path=path, config=config)
|
||||
fname = os.path.basename(path)
|
||||
p.__name__ = 'test_example_file_%s' % fname
|
||||
p.__doc__ = '%s should be a valid configuration' % fname
|
||||
setattr(TestExamples, p.__name__, p)
|
||||
del p
|
||||
|
||||
|
||||
add_example_tests()
|
110
tests/test_exporters.py
Normal file
110
tests/test_exporters.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import os
|
||||
import io
|
||||
import tempfile
|
||||
import shutil
|
||||
from time import time
|
||||
|
||||
from unittest import TestCase
|
||||
from soil import exporters
|
||||
from soil.utils import safe_open
|
||||
from soil import simulation
|
||||
|
||||
|
||||
class Dummy(exporters.Exporter):
|
||||
started = False
|
||||
trials = 0
|
||||
ended = False
|
||||
total_time = 0
|
||||
|
||||
def start(self):
|
||||
self.__class__.started = True
|
||||
|
||||
def trial_end(self, env):
|
||||
assert env
|
||||
self.__class__.trials += 1
|
||||
self.__class__.total_time += env.now
|
||||
|
||||
def end(self):
|
||||
self.__class__.ended = True
|
||||
|
||||
|
||||
class Exporters(TestCase):
|
||||
def test_basic(self):
|
||||
config = {
|
||||
'name': 'exporter_sim',
|
||||
'network_params': {},
|
||||
'agent_type': 'CounterModel',
|
||||
'max_time': 2,
|
||||
'num_trials': 5,
|
||||
'environment_params': {}
|
||||
}
|
||||
s = simulation.from_config(config)
|
||||
s.run_simulation(exporters=[Dummy], dry_run=True)
|
||||
assert Dummy.started
|
||||
assert Dummy.ended
|
||||
assert Dummy.trials == 5
|
||||
assert Dummy.total_time == 2*5
|
||||
|
||||
def test_distribution(self):
|
||||
'''The distribution exporter should write the number of agents in each state'''
|
||||
config = {
|
||||
'name': 'exporter_sim',
|
||||
'network_params': {
|
||||
'generator': 'complete_graph',
|
||||
'n': 4
|
||||
},
|
||||
'agent_type': 'CounterModel',
|
||||
'max_time': 2,
|
||||
'num_trials': 5,
|
||||
'environment_params': {}
|
||||
}
|
||||
output = io.StringIO()
|
||||
s = simulation.from_config(config)
|
||||
s.run_simulation(exporters=[exporters.Distribution], dry_run=True, exporter_params={'copy_to': output})
|
||||
result = output.getvalue()
|
||||
assert 'count' in result
|
||||
assert 'SEED,Noneexporter_sim_trial_3,1,,1,1,1,1' in result
|
||||
|
||||
def test_writing(self):
|
||||
'''Try to write CSV, GEXF, sqlite and YAML (without dry_run)'''
|
||||
n_trials = 5
|
||||
config = {
|
||||
'name': 'exporter_sim',
|
||||
'network_params': {
|
||||
'generator': 'complete_graph',
|
||||
'n': 4
|
||||
},
|
||||
'agent_type': 'CounterModel',
|
||||
'max_time': 2,
|
||||
'num_trials': n_trials,
|
||||
'environment_params': {}
|
||||
}
|
||||
output = io.StringIO()
|
||||
s = simulation.from_config(config)
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
envs = s.run_simulation(exporters=[
|
||||
exporters.Default,
|
||||
exporters.CSV,
|
||||
exporters.Gexf,
|
||||
exporters.Distribution,
|
||||
],
|
||||
outdir=tmpdir,
|
||||
exporter_params={'copy_to': output})
|
||||
result = output.getvalue()
|
||||
|
||||
simdir = os.path.join(tmpdir, s.group or '', s.name)
|
||||
with open(os.path.join(simdir, '{}.dumped.yml'.format(s.name))) as f:
|
||||
result = f.read()
|
||||
assert result
|
||||
|
||||
try:
|
||||
for e in envs:
|
||||
with open(os.path.join(simdir, '{}.gexf'.format(e.name))) as f:
|
||||
result = f.read()
|
||||
assert result
|
||||
|
||||
with open(os.path.join(simdir, '{}.csv'.format(e.name))) as f:
|
||||
result = f.read()
|
||||
assert result
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
@@ -116,18 +116,41 @@ class TestHistory(TestCase):
|
||||
db_path = os.path.join(DBROOT, 'test')
|
||||
h = history.History(db_path=db_path)
|
||||
h.save_tuples(tuples)
|
||||
h.flush_cache()
|
||||
assert os.path.exists(db_path)
|
||||
|
||||
# Recover the data
|
||||
recovered = history.History(db_path=db_path, backup=False)
|
||||
recovered = history.History(db_path=db_path)
|
||||
assert recovered['a_1', 0, 'id'] == 'v'
|
||||
assert recovered['a_1', 4, 'id'] == 'e'
|
||||
|
||||
# Using the same name should create a backup copy
|
||||
# Using backup=True should create a backup copy, and initialize an empty history
|
||||
newhistory = history.History(db_path=db_path, backup=True)
|
||||
backuppaths = glob(db_path + '.backup*.sqlite')
|
||||
assert len(backuppaths) == 1
|
||||
backuppath = backuppaths[0]
|
||||
assert newhistory._db_path == h._db_path
|
||||
assert newhistory.db_path == h.db_path
|
||||
assert os.path.exists(backuppath)
|
||||
assert not len(newhistory[None, None, None])
|
||||
assert len(newhistory[None, None, None]) == 0
|
||||
|
||||
def test_history_tuples(self):
|
||||
"""
|
||||
The data recovered should be equal to the one recorded.
|
||||
"""
|
||||
tuples = (
|
||||
('a_1', 0, 'id', 'v'),
|
||||
('a_1', 1, 'id', 'a'),
|
||||
('a_1', 2, 'id', 'l'),
|
||||
('a_1', 3, 'id', 'u'),
|
||||
('a_1', 4, 'id', 'e'),
|
||||
('env', 1, 'prob', 1),
|
||||
('env', 2, 'prob', 2),
|
||||
('env', 3, 'prob', 3),
|
||||
('a_2', 7, 'finished', True),
|
||||
)
|
||||
h = history.History()
|
||||
h.save_tuples(tuples)
|
||||
recovered = list(h.to_tuples())
|
||||
assert recovered
|
||||
for i in recovered:
|
||||
assert i in tuples
|
||||
|
@@ -1,19 +1,26 @@
|
||||
from unittest import TestCase
|
||||
|
||||
import os
|
||||
import io
|
||||
import yaml
|
||||
import pickle
|
||||
import networkx as nx
|
||||
from functools import partial
|
||||
|
||||
from os.path import join
|
||||
from soil import simulation, environment, agents, utils
|
||||
from soil import (simulation, Environment, agents, serialization,
|
||||
history, utils)
|
||||
|
||||
|
||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
EXAMPLES = join(ROOT, '..', 'examples')
|
||||
|
||||
|
||||
class CustomAgent(agents.BaseAgent):
|
||||
def step(self):
|
||||
self.state['neighbors'] = self.count_agents(state_id=0,
|
||||
limit_neighbors=True)
|
||||
|
||||
class TestMain(TestCase):
|
||||
|
||||
def test_load_graph(self):
|
||||
@@ -22,22 +29,20 @@ class TestMain(TestCase):
|
||||
Raise an exception otherwise.
|
||||
"""
|
||||
config = {
|
||||
'dry_run': True,
|
||||
'network_params': {
|
||||
'path': join(ROOT, 'test.gexf')
|
||||
}
|
||||
}
|
||||
G = utils.load_network(config['network_params'])
|
||||
G = serialization.load_network(config['network_params'])
|
||||
assert G
|
||||
assert len(G) == 2
|
||||
with self.assertRaises(AttributeError):
|
||||
config = {
|
||||
'dry_run': True,
|
||||
'network_params': {
|
||||
'path': join(ROOT, 'unknown.extension')
|
||||
}
|
||||
}
|
||||
G = utils.load_network(config['network_params'])
|
||||
G = serialization.load_network(config['network_params'])
|
||||
print(G)
|
||||
|
||||
def test_generate_barabasi(self):
|
||||
@@ -46,22 +51,20 @@ class TestMain(TestCase):
|
||||
should be used to generate a network
|
||||
"""
|
||||
config = {
|
||||
'dry_run': True,
|
||||
'network_params': {
|
||||
'generator': 'barabasi_albert_graph'
|
||||
}
|
||||
}
|
||||
with self.assertRaises(TypeError):
|
||||
G = utils.load_network(config['network_params'])
|
||||
G = serialization.load_network(config['network_params'])
|
||||
config['network_params']['n'] = 100
|
||||
config['network_params']['m'] = 10
|
||||
G = utils.load_network(config['network_params'])
|
||||
G = serialization.load_network(config['network_params'])
|
||||
assert len(G) == 100
|
||||
|
||||
def test_empty_simulation(self):
|
||||
"""A simulation with a base behaviour should do nothing"""
|
||||
config = {
|
||||
'dry_run': True,
|
||||
'network_params': {
|
||||
'path': join(ROOT, 'test.gexf')
|
||||
},
|
||||
@@ -78,7 +81,6 @@ class TestMain(TestCase):
|
||||
agent should be able to update its state."""
|
||||
config = {
|
||||
'name': 'CounterAgent',
|
||||
'dry_run': True,
|
||||
'network_params': {
|
||||
'path': join(ROOT, 'test.gexf')
|
||||
},
|
||||
@@ -102,7 +104,6 @@ class TestMain(TestCase):
|
||||
"""
|
||||
config = {
|
||||
'name': 'CounterAgent',
|
||||
'dry_run': True,
|
||||
'network_params': {
|
||||
'path': join(ROOT, 'test.gexf')
|
||||
},
|
||||
@@ -127,12 +128,7 @@ class TestMain(TestCase):
|
||||
|
||||
def test_custom_agent(self):
|
||||
"""Allow for search of neighbors with a certain state_id"""
|
||||
class CustomAgent(agents.BaseAgent):
|
||||
def step(self):
|
||||
self.state['neighbors'] = self.count_agents(state_id=0,
|
||||
limit_neighbors=True)
|
||||
config = {
|
||||
'dry_run': True,
|
||||
'network_params': {
|
||||
'path': join(ROOT, 'test.gexf')
|
||||
},
|
||||
@@ -152,12 +148,11 @@ class TestMain(TestCase):
|
||||
|
||||
def test_torvalds_example(self):
|
||||
"""A complete example from a documentation should work."""
|
||||
config = utils.load_file(join(EXAMPLES, 'torvalds.yml'))[0]
|
||||
config = serialization.load_file(join(EXAMPLES, 'torvalds.yml'))[0]
|
||||
config['network_params']['path'] = join(EXAMPLES,
|
||||
config['network_params']['path'])
|
||||
s = simulation.from_config(config)
|
||||
s.dry_run = True
|
||||
env = s.run_simulation()[0]
|
||||
env = s.run_simulation(dry_run=True)[0]
|
||||
for a in env.network_agents:
|
||||
skill_level = a.state['skill_level']
|
||||
if a.id == 'Torvalds':
|
||||
@@ -179,17 +174,14 @@ class TestMain(TestCase):
|
||||
should be equivalent to the configuration file used
|
||||
"""
|
||||
with utils.timer('loading'):
|
||||
config = utils.load_file(join(EXAMPLES, 'complete.yml'))[0]
|
||||
config = serialization.load_file(join(EXAMPLES, 'complete.yml'))[0]
|
||||
s = simulation.from_config(config)
|
||||
s.dry_run = True
|
||||
with utils.timer('serializing'):
|
||||
serial = s.to_yaml()
|
||||
with utils.timer('recovering'):
|
||||
recovered = yaml.load(serial)
|
||||
with utils.timer('deleting'):
|
||||
del recovered['topology']
|
||||
del recovered['load_module']
|
||||
del recovered['dry_run']
|
||||
assert config == recovered
|
||||
|
||||
def test_configuration_changes(self):
|
||||
@@ -197,25 +189,16 @@ class TestMain(TestCase):
|
||||
The configuration should not change after running
|
||||
the simulation.
|
||||
"""
|
||||
config = utils.load_file('examples/complete.yml')[0]
|
||||
config = serialization.load_file(join(EXAMPLES, 'complete.yml'))[0]
|
||||
s = simulation.from_config(config)
|
||||
s.dry_run = True
|
||||
for i in range(5):
|
||||
s.run_simulation(dry_run=True)
|
||||
nconfig = s.to_dict()
|
||||
del nconfig['topology']
|
||||
del nconfig['dry_run']
|
||||
del nconfig['load_module']
|
||||
assert config == nconfig
|
||||
|
||||
def test_examples(self):
|
||||
"""
|
||||
Make sure all examples in the examples folder are correct
|
||||
"""
|
||||
pass
|
||||
|
||||
def test_row_conversion(self):
|
||||
env = environment.SoilEnvironment(dry_run=True)
|
||||
env = Environment()
|
||||
env['test'] = 'test_value'
|
||||
|
||||
res = list(env.history_to_tuples())
|
||||
@@ -234,8 +217,9 @@ class TestMain(TestCase):
|
||||
from geometric models. We should work around it.
|
||||
"""
|
||||
G = nx.random_geometric_graph(20, 0.1)
|
||||
env = environment.SoilEnvironment(topology=G, dry_run=True)
|
||||
env.dump_gexf('/tmp/dump-gexf')
|
||||
env = Environment(topology=G)
|
||||
f = io.BytesIO()
|
||||
env.dump_gexf(f)
|
||||
|
||||
def test_save_graph(self):
|
||||
'''
|
||||
@@ -245,7 +229,7 @@ class TestMain(TestCase):
|
||||
'''
|
||||
G = nx.cycle_graph(5)
|
||||
distribution = agents.calculate_distribution(None, agents.BaseAgent)
|
||||
env = environment.SoilEnvironment(topology=G, network_agents=distribution, dry_run=True)
|
||||
env = Environment(topology=G, network_agents=distribution)
|
||||
env[0, 0, 'testvalue'] = 'start'
|
||||
env[0, 10, 'testvalue'] = 'finish'
|
||||
nG = env.history_to_graph()
|
||||
@@ -253,33 +237,103 @@ class TestMain(TestCase):
|
||||
assert ('start', 0, 10) in values
|
||||
assert ('finish', 10, None) in values
|
||||
|
||||
def test_serialize_class(self):
|
||||
ser, name = serialization.serialize(agents.BaseAgent)
|
||||
assert name == 'soil.agents.BaseAgent'
|
||||
assert ser == agents.BaseAgent
|
||||
|
||||
def make_example_test(path, config):
|
||||
def wrapped(self):
|
||||
root = os.getcwd()
|
||||
os.chdir(os.path.dirname(path))
|
||||
s = simulation.from_config(config)
|
||||
envs = s.run_simulation(dry_run=True)
|
||||
assert envs
|
||||
for env in envs:
|
||||
assert env
|
||||
try:
|
||||
n = config['network_params']['n']
|
||||
assert len(env.get_agents()) == n
|
||||
except KeyError:
|
||||
pass
|
||||
os.chdir(root)
|
||||
return wrapped
|
||||
ser, name = serialization.serialize(CustomAgent)
|
||||
assert name == 'test_main.CustomAgent'
|
||||
assert ser == CustomAgent
|
||||
pickle.dumps(ser)
|
||||
|
||||
def test_serialize_builtin_types(self):
|
||||
|
||||
for i in [1, None, True, False, {}, [], list(), dict()]:
|
||||
ser, name = serialization.serialize(i)
|
||||
assert type(ser) == str
|
||||
des = serialization.deserialize(name, ser)
|
||||
assert i == des
|
||||
|
||||
def test_serialize_agent_type(self):
|
||||
'''A class from soil.agents should be serialized without the module part'''
|
||||
ser = agents.serialize_type(CustomAgent)
|
||||
assert ser == 'test_main.CustomAgent'
|
||||
ser = agents.serialize_type(agents.BaseAgent)
|
||||
assert ser == 'BaseAgent'
|
||||
pickle.dumps(ser)
|
||||
|
||||
def test_deserialize_agent_distribution(self):
|
||||
agent_distro = [
|
||||
{
|
||||
'agent_type': 'CounterModel',
|
||||
'weight': 1
|
||||
},
|
||||
{
|
||||
'agent_type': 'test_main.CustomAgent',
|
||||
'weight': 2
|
||||
},
|
||||
]
|
||||
converted = agents.deserialize_distribution(agent_distro)
|
||||
assert converted[0]['agent_type'] == agents.CounterModel
|
||||
assert converted[1]['agent_type'] == CustomAgent
|
||||
pickle.dumps(converted)
|
||||
|
||||
def test_serialize_agent_distribution(self):
|
||||
agent_distro = [
|
||||
{
|
||||
'agent_type': agents.CounterModel,
|
||||
'weight': 1
|
||||
},
|
||||
{
|
||||
'agent_type': CustomAgent,
|
||||
'weight': 2
|
||||
},
|
||||
]
|
||||
converted = agents.serialize_distribution(agent_distro)
|
||||
assert converted[0]['agent_type'] == 'CounterModel'
|
||||
assert converted[1]['agent_type'] == 'test_main.CustomAgent'
|
||||
pickle.dumps(converted)
|
||||
|
||||
def test_pickle_agent_environment(self):
|
||||
env = Environment(name='Test')
|
||||
a = agents.BaseAgent(environment=env, agent_id=25)
|
||||
|
||||
a['key'] = 'test'
|
||||
|
||||
pickled = pickle.dumps(a)
|
||||
recovered = pickle.loads(pickled)
|
||||
|
||||
assert recovered.env.name == 'Test'
|
||||
assert list(recovered.env._history.to_tuples())
|
||||
assert recovered['key', 0] == 'test'
|
||||
assert recovered['key'] == 'test'
|
||||
|
||||
def test_history(self):
|
||||
'''Test storing in and retrieving from history (sqlite)'''
|
||||
h = history.History()
|
||||
h.save_record(agent_id=0, t_step=0, key="test", value="hello")
|
||||
assert h[0, 0, "test"] == "hello"
|
||||
|
||||
def test_subgraph(self):
|
||||
'''An agent should be able to subgraph the global topology'''
|
||||
G = nx.Graph()
|
||||
G.add_node(3)
|
||||
G.add_edge(1, 2)
|
||||
distro = agents.calculate_distribution(agent_type=agents.NetworkAgent)
|
||||
env = Environment(name='Test', topology=G, network_agents=distro)
|
||||
lst = list(env.network_agents)
|
||||
|
||||
a2 = env.get_agent(2)
|
||||
a3 = env.get_agent(3)
|
||||
assert len(a2.subgraph(limit_neighbors=True)) == 2
|
||||
assert len(a3.subgraph(limit_neighbors=True)) == 1
|
||||
assert len(a3.subgraph(limit_neighbors=True, center=False)) == 0
|
||||
assert len(a3.subgraph(agent_type=agents.NetworkAgent)) == 3
|
||||
|
||||
def test_templates(self):
|
||||
'''Loading a template should result in several configs'''
|
||||
configs = serialization.load_file(join(EXAMPLES, 'template.yml'))
|
||||
assert len(configs) > 0
|
||||
|
||||
|
||||
def add_example_tests():
|
||||
for config, path in utils.load_config(join(EXAMPLES, '*.yml')):
|
||||
p = make_example_test(path=path, config=config)
|
||||
fname = os.path.basename(path)
|
||||
p.__name__ = 'test_example_file_%s' % fname
|
||||
p.__doc__ = '%s should be a valid configuration' % fname
|
||||
setattr(TestMain, p.__name__, p)
|
||||
del p
|
||||
|
||||
|
||||
add_example_tests()
|
||||
|
Reference in New Issue
Block a user