1
0
mirror of https://github.com/gsi-upm/soil synced 2025-09-13 19:52:20 +00:00

Compare commits

...

20 Commits

Author SHA1 Message Date
J. Fernando Sánchez
3526fa29d7 Fix bug parallel 2018-12-09 14:06:50 +01:00
J. Fernando Sánchez
53604c1e66 Fix quickstart.rst markdown code 2018-12-09 13:10:00 +01:00
J. Fernando Sánchez
01cc8e9128 Merge branch 'refactor-imports'
* remove leftover import in example
* Update quickstart tutorial
* Add gitlab-ci
* Added missing gexf for tests
* Upgrade to python3.7 and pandas 0.3.4 because pandas has dropped support for
  python 3.4 -> There are some API changes in pandas, and I've updated the code
  accordingly.
* Set pytest as the default test runner
* Update dockerignore
* Skip testing long examples (>1000 steps)
2018-12-09 12:55:12 +01:00
J. Fernando Sánchez
a47ffa815b Fix CI. Skip testing long examples 2018-12-08 20:49:34 +01:00
J. Fernando Sánchez
b41927d7bf remove leftover import in example 2018-12-08 20:35:02 +01:00
J. Fernando Sánchez
70d033b3a9 Update dockerignore 2018-12-08 19:13:56 +01:00
J. Fernando Sánchez
3afed06656 Add gitlab-ci 2018-12-08 19:08:47 +01:00
J. Fernando Sánchez
0a7ef27844 Added missing gexf for tests 2018-12-08 18:53:12 +01:00
J. Fernando Sánchez
2e28b36f6e Python3.7, testing and bug fixes
* Upgrade to python3.7 and pandas 0.3.4 because pandas has dropped support for
python 3.4 -> There are some API changes in pandas, and I've update the code
accordingly.
* Set pytest as the default test runner
2018-12-08 18:53:06 +01:00
J. Fernando Sánchez
bd4700567e Update quickstart tutorial 2018-12-08 18:17:25 +01:00
J. Fernando Sánchez
ff1df62eec All tests pass 2018-12-08 18:17:21 +01:00
J. Fernando Sánchez
9165979b49 merge visualization branch
The web server is included as a submodule.
The dependencies for the web (tornado) are not installed by default, but they
can be installed as an extra:

```
pip install soil[web]
```

Once installed, the soil web can be used like this:

```
soil-web

OR

python -m soil.web
```

There are other minor changes:

* History re-connects to the sqlite database if it is used from a different
thread.
* Environment accepts additional parameters (so it can run simulations with
`visualization_params` or any other in the future).
* The simulator class is no longer necessary
* Logging is done in the same thread, and the simulation is run in a separate
one. This had to be done because it was creating some problems with tornado not
being able to find the current thread during logs, which caused hundreds of
repeated lines in the web "console".
* The player is slightly modified in this version. I noticed that when the
  visualization was playing, if you clicked somewhere it would change for a
  second, and then go back to the previous place. The code for the playback
  seemed too complex, especially speed control, so I rewrote some parts. I
  might've introduced new bugs.
2018-12-07 18:28:19 +01:00
J. Fernando Sánchez
078f8ace9e Merge commit '8fec544772c13efb1dc8a0589240551b9bad27cb' as 'soil/web' 2018-12-07 18:27:57 +01:00
J. Fernando Sánchez
8fec544772 Squashed 'soil/web/' content from commit 4dcd0fc
git-subtree-dir: soil/web
git-subtree-split: 4dcd0fcb3d
2018-12-07 20:30:24 +01:00
J. Fernando Sánchez
5420501d36 Fix state and networkx dynamic attributes 2018-05-07 18:59:19 +02:00
J. Fernando Sánchez
5d89827ccf Fix history bug 2018-05-04 11:21:23 +02:00
J. Fernando Sánchez
fc48ed7e09 Added history class
Now the environment does not deal with history directly, it delegates it to a
specific class. The analysis also uses history instances instead of either
using the database directly or creating a proxy environment.

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

In fact, the change was motivated by the large size of the csv files in previous
versions. This new implementation only stores results in deltas, and it fills
any necessary values when needed.
2018-05-04 10:01:49 +02:00
J. Fernando Sánchez
73c90887e8 Fix pip installation 2018-05-04 09:59:31 +02:00
J. Fernando Sánchez
497c8a55db Add workaround for geometric models
Closes soil/soil#4
2018-02-16 18:04:43 +01:00
J. Fernando Sánchez
7d1c800490 Parallelism and granular exporting options
* Graphs are not saved by default (not backwards compatible)
* Modified newsspread examples
* More granular options to save results (exporting to CSV and GEXF are now
optional)
* Updated tutorial to include exporting options
* Removed references from environment to simulation
* Added parallelism to simulations (can be turned off with a flag or argument).
2017-11-01 14:44:46 +01:00
70 changed files with 87406 additions and 3429 deletions

2
.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
**/soil_output
.*

21
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,21 @@
image: python:3.7
steps:
- build
- test
build:
stage: build
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
script:
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
only:
- tags
test:
script:
python setup.py test

View File

@@ -1,3 +1,12 @@
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]'
ENTRYPOINT ["python", "-m", "soil"]

4
Makefile Normal file
View File

@@ -0,0 +1,4 @@
test:
docker-compose exec dev python -m pytest -s -v
.PHONY: test

12
docker-compose.yml Normal file
View File

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

244
docs/configuration.rst Normal file
View 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.

View File

@@ -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>
..

View File

@@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -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,

532
examples/NewsSpread.ipynb Normal file

File diff suppressed because one or more lines are too long

80808
examples/Untitled.ipynb Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
name: simple
dir_path: "/tmp/"
num_trials: 3
dry_run: True
max_time: 100
interval: 1
seed: "CompleteSeed!"
@@ -17,6 +18,7 @@ network_agents:
- agent_type: AggregatedCounter
weight: 0.2
environment_agents: []
environment_class: Environment
environment_params:
am_i_complete: true
default_state:

File diff suppressed because one or more lines are too long

View File

@@ -68,7 +68,7 @@ network_agents:
- agent_type: HerdViewer
state:
has_tv: true
id: infected
id: neutral
weight: 1
- agent_type: HerdViewer
state:
@@ -95,7 +95,7 @@ network_agents:
- agent_type: HerdViewer
state:
has_tv: true
id: infected
id: neutral
weight: 1
- agent_type: WiseViewer
state:
@@ -121,7 +121,7 @@ network_agents:
- agent_type: WiseViewer
state:
has_tv: true
id: infected
id: neutral
weight: 1
- agent_type: WiseViewer
state:

View File

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

View File

@@ -0,0 +1,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).

View 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)

View 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

View File

@@ -1,7 +1,7 @@
---
load_module: rabbit_agents
name: rabbits_example
max_time: 1200
max_time: 500
interval: 1
seed: MySeed
agent_type: RabbitModel

View File

@@ -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">&#39;agent_type&#39;</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>

File diff suppressed because one or more lines are too long

View File

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

4
setup.cfg Normal file
View File

@@ -0,0 +1,4 @@
[aliases]
test=pytest
[tool:pytest]
addopts = --verbose

View File

@@ -1,20 +1,21 @@
import pip
import os
from setuptools import setup
# parse_requirements() returns generator of pip.req.InstallRequirement objects
from pip.req import parse_requirements
from soil import __version__
try:
install_reqs = parse_requirements(
"requirements.txt", session=pip.download.PipSession())
test_reqs = parse_requirements(
"test-requirements.txt", session=pip.download.PipSession())
except AttributeError:
install_reqs = parse_requirements("requirements.txt")
test_reqs = parse_requirements("test-requirements.txt")
install_reqs = [str(ir.req) for ir in install_reqs]
test_reqs = [str(ir.req) for ir in test_reqs]
with open(os.path.join('soil', 'VERSION')) as f:
__version__ = f.readlines()[0].strip()
assert __version__
def parse_requirements(filename):
""" load requirements from a pip requirements file """
with open(filename, 'r') as f:
lineiter = list(line.strip() for line in f)
return [line for line in lineiter if line and not line.startswith("#")]
install_reqs = parse_requirements("requirements.txt")
test_reqs = parse_requirements("test-requirements.txt")
setup(
@@ -39,10 +40,15 @@ setup(
'Operating System :: POSIX',
'Programming Language :: Python :: 3'],
install_requires=install_reqs,
extras_require={
'web': ['tornado']
},
tests_require=test_reqs,
setup_requires=['pytest-runner', ],
include_package_data=True,
entry_points={
'console_scripts':
['soil = soil.__init__:main']
['soil = soil.__init__:main',
'soil-web = soil.web.__init__:main']
})

1
soil/VERSION Normal file
View File

@@ -0,0 +1 @@
0.13.1

View File

@@ -4,7 +4,7 @@ import os
import pdb
import logging
__version__ = "0.10.1"
from .version import __version__
try:
basestring
@@ -14,12 +14,11 @@ except NameError:
logging.basicConfig()
from . import agents
from . import simulation
from . import environment
from .simulation import *
from .environment import Environment
from . import utils
from . import analysis
def main():
import argparse
from . import simulation
@@ -35,20 +34,37 @@ def main():
help='Do not store the results of the simulation.')
parser.add_argument('--pdb', action='store_true',
help='Use a pdb console in case of exception.')
parser.add_argument('--output', '-o', type=str,
parser.add_argument('--graph', '-g', action='store_true',
help='Dump GEXF graph. Defaults to false.')
parser.add_argument('--csv', action='store_true',
help='Dump history in CSV format. Defaults to false.')
parser.add_argument('--output', '-o', type=str, default="soil_output",
help='folder to write results to. It defaults to the current directory.')
parser.add_argument('--synchronous', action='store_true',
help='Run trials serially and synchronously instead of in parallel. Defaults to false.')
args = parser.parse_args()
if args.module:
if os.getcwd() not in sys.path:
sys.path.append(os.getcwd())
if args.module:
importlib.import_module(args.module)
logging.info('Loading config file: {}'.format(args.file, args.output))
logging.info('Loading config file: {}'.format(args.file))
try:
simulation.run_from_config(args.file, dump=(not args.dry_run), results_dir=args.output)
except Exception as ex:
dump = []
if not args.dry_run:
if args.csv:
dump.append('csv')
if args.graph:
dump.append('gexf')
simulation.run_from_config(args.file,
dry_run=args.dry_run,
dump=dump,
parallel=(not args.synchronous and not args.pdb),
results_dir=args.output)
except Exception:
if args.pdb:
pdb.post_mortem()
else:

View File

@@ -11,9 +11,9 @@ class CounterModel(BaseAgent):
# Outside effects
total = len(list(self.get_all_agents()))
neighbors = len(list(self.get_neighboring_agents()))
self.state['times'] = self.state.get('times', 0) + 1
self.state['neighbors'] = neighbors
self.state['total'] = total
self['times'] = self.get('times', 0) + 1
self['neighbors'] = neighbors
self['total'] = total
class AggregatedCounter(BaseAgent):
@@ -26,7 +26,7 @@ class AggregatedCounter(BaseAgent):
# Outside effects
total = len(list(self.get_all_agents()))
neighbors = len(list(self.get_neighboring_agents()))
self.state['times'] = self.state.get('times', 0) + 1
self.state['neighbors'] = self.state.get('neighbors', 0) + neighbors
self.state['total'] = total = self.state.get('total', 0) + total
self['times'] = self.get('times', 0) + 1
self['neighbors'] = self.get('neighbors', 0) + neighbors
self['total'] = total = self.get('total', 0) + total
self.debug('Running for step: {}. Total: {}'.format(self.now, total))

View File

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

View File

@@ -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):

View File

@@ -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']

View File

@@ -12,53 +12,89 @@ from copy import deepcopy
from functools import partial
import json
from functools import wraps
agent_types = {}
from .. import utils, history
class MetaAgent(type):
def __init__(cls, name, bases, nmspc):
super(MetaAgent, cls).__init__(name, bases, nmspc)
agent_types[name] = cls
class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
class BaseAgent(nxsim.BaseAgent):
"""
A special simpy BaseAgent that keeps track of its state history.
"""
defaults = {}
def __init__(self, **kwargs):
def __init__(self, environment, agent_id=None, state=None,
name='network_process', interval=None, **state_params):
# Check for REQUIRED arguments
assert environment is not None, TypeError('__init__ missing 1 required keyword argument: \'environment\'. '
'Cannot be NoneType.')
# Initialize agent parameters
self.id = agent_id
self.name = name
self.state_params = state_params
# Global parameters
self.global_topology = environment.G
self.environment_params = environment.environment_params
# Register agent to environment
self.env = environment
self._neighbors = None
self.alive = True
state = deepcopy(self.defaults)
state.update(kwargs.pop('state', {}))
kwargs['state'] = state
super().__init__(**kwargs)
real_state = deepcopy(self.defaults)
real_state.update(state or {})
self.state = real_state
self.interval = interval
if not hasattr(self, 'level'):
self.level = logging.DEBUG
self.logger = logging.getLogger('Agent-{}'.format(self.id))
self.logger = logging.getLogger('{}-Agent-{}'.format(self.env.name,
self.id))
self.logger.setLevel(self.level)
# initialize every time an instance of the agent is created
self.action = self.env.process(self.run())
@property
def state(self):
'''
Return the agent itself, which behaves as a dictionary.
Changes made to `agent.state` will be reflected in the history.
This method shouldn't be used, but is kept here for backwards compatibility.
'''
return self
@state.setter
def state(self, value):
self._state = {}
for k, v in value.items():
self[k] = v
def __getitem__(self, key):
if isinstance(key, tuple):
k, t_step = key
return self.env[self.id, t_step, k]
return self.state.get(key, None)
key, t_step = key
k = history.Key(key=key, t_step=t_step, agent_id=self.id)
return self.env[k]
return self._state.get(key, None)
def __delitem__(self, key):
del self.state[key]
self._state[key] = None
def __contains__(self, key):
return key in self.state
return key in self._state
def __setitem__(self, key, value):
self.state[key] = value
self._state[key] = value
k = history.Key(t_step=self.now,
agent_id=self.id,
key=key)
self.env[k] = value
def items(self):
return self._state.items()
def get(self, key, default=None):
return self[key] if key in self else default
@@ -72,7 +108,12 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
return None
def run(self):
interval = self.env.interval
if self.interval is not None:
interval = self.interval
elif 'interval' in self:
interval = self['interval']
else:
interval = self.env.interval
while self.alive:
res = self.step()
yield res or self.env.timeout(interval)
@@ -95,7 +136,7 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
agents = self.global_topology.nodes()
count = 0
for agent in agents:
if state_id and state_id != self.global_topology.node[agent]['agent'].state['id']:
if state_id and state_id != self.global_topology.node[agent]['agent']['id']:
continue
count += 1
return count
@@ -103,14 +144,18 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
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):
def get_agents(self, state_id=None, agent_type=None, limit_neighbors=False, iterator=False, **kwargs):
agents = self.env.agents
if limit_neighbors:
agents = super().get_agents(state_id, limit_neighbors)
else:
agents = filter(lambda x: state_id is None or x.state.get('id', None) == state_id,
self.env.agents)
def matches_all(agent):
if state_id is not None:
if agent.state.get('id', None) != state_id:
return False
if agent_type is not None:
if type(agent) != agent_type:
return False
state = agent.state
for k, v in kwargs.items():
if state.get(k, None) != v:
@@ -140,20 +185,24 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
def state(func):
'''
A state function should return either a state id, or a tuple (state_id, when)
The default value for state_id is the current state id.
The default value for when is the interval defined in the nevironment.
'''
@wraps(func)
def func_wrapper(self):
when = None
next_state = func(self)
when = None
if next_state is None:
return when
try:
next_state, when = next_state
except TypeError:
except (ValueError, TypeError):
pass
if next_state:
try:
self.state['id'] = next_state.id
except AttributeError:
raise ValueError('State id %s is not valid.' % next_state)
self.set_state(next_state)
return when
func_wrapper.id = func.__name__
@@ -166,7 +215,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 = {}
@@ -193,11 +242,13 @@ class FSM(BaseAgent, metaclass=MetaFSM):
def __init__(self, *args, **kwargs):
super(FSM, self).__init__(*args, **kwargs)
if 'id' not in self.state:
self.state['id'] = self.default_state.id
if not self.default_state:
raise ValueError('No default state specified for {}'.format(self.id))
self['id'] = self.default_state.id
def step(self):
if 'id' in self.state:
next_state = self.state['id']
next_state = self['id']
elif self.default_state:
next_state = self.default_state.id
else:
@@ -211,7 +262,135 @@ class FSM(BaseAgent, metaclass=MetaFSM):
state = state.id
if state not in self.states:
raise ValueError('{} is not a valid state'.format(state))
self.state['id'] = state
self['id'] = state
return state
def prob(prob=1):
'''
A true/False uniform distribution with a given probability.
To be used like this:
.. code-block:: python
if prob(0.3):
do_something()
'''
r = random.random()
return r < prob
def calculate_distribution(network_agents=None,
agent_type=None):
'''
Calculate the threshold values (thresholds for a uniform distribution)
of an agent distribution given the weights of each agent type.
The input has this form: ::
[
{'agent_type': 'agent_type_1',
'weight': 0.2,
'state': {
'id': 0
}
},
{'agent_type': 'agent_type_2',
'weight': 0.8,
'state': {
'id': 1
}
}
]
In this example, 20% of the nodes will be marked as type
'agent_type_1'.
'''
if network_agents:
network_agents = deepcopy(network_agents)
elif agent_type:
network_agents = [{'agent_type': agent_type}]
else:
return []
# Calculate the thresholds
total = sum(x.get('weight', 1) for x in network_agents)
acc = 0
for v in network_agents:
upper = acc + (v.get('weight', 1)/total)
v['threshold'] = [acc, upper]
acc = upper
return network_agents
def serialize_type(agent_type, known_modules=[], **kwargs):
if isinstance(agent_type, str):
return agent_type
known_modules += ['soil.agents']
return utils.serialize(agent_type, known_modules=known_modules, **kwargs)[1] # Get the name of the class
def serialize_distribution(network_agents, known_modules=[]):
'''
When serializing an agent distribution, remove the thresholds, in order
to avoid cluttering the YAML definition file.
'''
d = deepcopy(network_agents)
for v in d:
if 'threshold' in v:
del v['threshold']
v['agent_type'] = serialize_type(v['agent_type'],
known_modules=known_modules)
return d
def deserialize_type(agent_type, known_modules=[]):
if not isinstance(agent_type, str):
return agent_type
known = known_modules + ['soil.agents', 'soil.agents.custom' ]
agent_type = utils.deserializer(agent_type, known_modules=known)
return agent_type
def deserialize_distribution(ind, **kwargs):
d = deepcopy(ind)
for v in d:
v['agent_type'] = deserialize_type(v['agent_type'], **kwargs)
return d
def _validate_states(states, topology):
'''Validate states to avoid ignoring states during initialization'''
states = states or []
if isinstance(states, dict):
for x in states:
assert x in topology.node
else:
assert len(states) <= len(topology)
return states
def _convert_agent_types(ind, to_string=False, **kwargs):
'''Convenience method to allow specifying agents by class or class name.'''
if to_string:
return serialize_distribution(ind, **kwargs)
return deserialize_distribution(ind, **kwargs)
def _agent_from_distribution(distribution, value=-1):
"""Used in the initialization of agents given an agent distribution."""
if value < 0:
value = random.random()
for d in distribution:
threshold = d['threshold']
if value >= threshold[0] and value < threshold[1]:
state = {}
if 'state' in d:
state = deepcopy(d['state'])
return d['agent_type'], state
raise Exception('Distribution for value {} not found in: {}'.format(value, distribution))
from .BassModel import *

View File

@@ -4,7 +4,7 @@ import glob
import yaml
from os.path import join
from . import utils
from . import utils, history
def read_data(*args, group=False, **kwargs):
@@ -15,8 +15,9 @@ def read_data(*args, group=False, **kwargs):
return list(iterable)
def _read_data(pattern, keys=None, convert_types=False,
process=None, from_csv=False, **kwargs):
def _read_data(pattern, *args, from_csv=False, process_args=None, **kwargs):
if not process_args:
process_args = {}
for folder in glob.glob(pattern):
config_file = glob.glob(join(folder, '*.yml'))[0]
config = yaml.load(open(config_file))
@@ -24,19 +25,20 @@ def _read_data(pattern, keys=None, convert_types=False,
if from_csv:
for trial_data in sorted(glob.glob(join(folder,
'*.environment.csv'))):
df = read_csv(trial_data, convert_types=convert_types)
if process:
df = process(df, **kwargs)
df = read_csv(trial_data, **kwargs)
yield config_file, df, config
else:
for trial_data in sorted(glob.glob(join(folder, '*.db.sqlite'))):
df = read_sql(trial_data, convert_types=convert_types,
keys=keys)
if process:
df = process(df, **kwargs)
df = read_sql(trial_data, **kwargs)
yield config_file, df, config
def read_sql(db, *args, **kwargs):
h = history.History(db, backup=False)
df = h.read_sql(*args, **kwargs)
return df
def read_csv(filename, keys=None, convert_types=False, **kwargs):
'''
Read a CSV in canonical form: ::
@@ -49,23 +51,12 @@ def read_csv(filename, keys=None, convert_types=False, **kwargs):
df = convert_types_slow(df)
if keys:
df = df[df['key'].isin(keys)]
return df
def read_sql(filename, keys=None, convert_types=False, limit=-1):
condition = ''
if keys:
k = map(lambda x: "\'{}\'".format(x), keys)
condition = 'where key in ({})'.format(','.join(k))
query = 'select * from history {} limit {}'.format(condition, limit)
df = pd.read_sql_query(query, 'sqlite:///{}'.format(filename))
if convert_types:
df = convert_types_slow(df)
df = process_one(df)
return df
def convert_row(row):
row['value'] = utils.convert(row['value'], row['value_type'])
row['value'] = utils.deserialize(row['value_type'], row['value'])
return row
@@ -108,8 +99,9 @@ def get_types(df):
return {k:v[0] for k,v in dtypes.iteritems()}
def process_one(df, *keys, columns=['key'], values='value',
index=['t_step', 'agent_id'], aggfunc='first', **kwargs):
def process_one(df, *keys, columns=['key', 'agent_id'], values='value',
fill=True, index=['t_step',],
aggfunc='first', **kwargs):
'''
Process a dataframe in canonical form ``(t_step, agent_id, key, value, value_type)`` into
a dataframe with a column per key
@@ -119,35 +111,29 @@ def process_one(df, *keys, columns=['key'], values='value',
if keys:
df = df[df['key'].isin(keys)]
dtypes = get_types(df)
df = df.pivot_table(values=values, index=index, columns=columns,
aggfunc=aggfunc, **kwargs)
df = df.fillna(0).astype(dtypes)
if fill:
df = fillna(df)
return df
def get_count_processed(df, *keys):
if keys:
df = df[list(keys)]
# p = df.groupby(level=0).apply(pd.Series.value_counts)
p = df.unstack().apply(pd.Series.value_counts, axis=1)
return p
def get_count(df, *keys):
if keys:
df = df[df['key'].isin(keys)]
p = df.groupby(by=['t_step', 'key', 'value']).size().unstack(level=[1,2]).fillna(0)
return p
df = df[list(keys)]
counts = pd.DataFrame()
for key in df.columns.levels[0]:
g = df[[key]].apply(pd.Series.value_counts, axis=1).fillna(0)
for value, series in g.iteritems():
counts[key, value] = series
counts.columns = pd.MultiIndex.from_tuples(counts.columns)
return counts
def get_value(df, *keys, aggfunc='sum'):
if keys:
df = df[df['key'].isin(keys)]
p = process_one(df, *keys)
p = p.groupby(level='t_step').agg(aggfunc)
return p
df = df[list(keys)]
return df.groupby(axis=1, level=0).agg(aggfunc, axis=1)
def plot_all(*args, **kwargs):
@@ -175,4 +161,6 @@ def group_trials(trials, aggfunc=['mean', 'min', 'max', 'std']):
return pd.concat(trials).groupby(level=0).agg(aggfunc).reorder_levels([2, 0,1] ,axis=1)
def fillna(df):
new_df = df.ffill(axis=0)
return new_df

View File

@@ -1,19 +1,30 @@
import os
import sqlite3
import time
import weakref
import csv
import random
import simpy
import tempfile
import pandas as pd
from copy import deepcopy
from networkx.readwrite import json_graph
import networkx as nx
import nxsim
from . import utils
from . import utils, agents, analysis, history
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
params, which are used as shared state between agents.
The environment parameters and the state of every agent can be accessed
both by using the environment as a dictionary or with the environment's
:meth:`soil.environment.Environment.get` method.
"""
def __init__(self, name=None,
network_agents=None,
@@ -22,42 +33,32 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
default_state=None,
interval=1,
seed=None,
dump=False,
simulation=None,
dry_run=False,
dir_path=None,
topology=None,
*args, **kwargs):
self.name = name or 'UnnamedEnvironment'
if isinstance(states, list):
states = dict(enumerate(states))
self.states = deepcopy(states) if states else {}
self.default_state = deepcopy(default_state) or {}
self.sim = weakref.ref(simulation)
if 'topology' not in kwargs and simulation:
kwargs['topology'] = self.sim().topology.copy()
super().__init__(*args, **kwargs)
if not topology:
topology = nx.Graph()
super().__init__(*args, topology=topology, **kwargs)
self._env_agents = {}
self.dry_run = dry_run
self.interval = interval
self.dump = dump
self.dir_path = dir_path or tempfile.mkdtemp('soil-env')
if not dry_run:
self.get_path()
self._history = history.History(name=self.name if not dry_run else None,
dir_path=self.dir_path)
# Add environment agents first, so their events get
# executed before network agents
self['SEED'] = seed or time.time()
random.seed(self['SEED'])
self.process(self.save_state())
self.environment_agents = environment_agents or []
self.network_agents = network_agents or []
if self.dump:
self._db_path = os.path.join(self.get_path(), '{}.db.sqlite'.format(self.name))
else:
self._db_path = ":memory:"
self.create_db(self._db_path)
def create_db(self, db_path=None):
db_path = db_path or self._db_path
if os.path.exists(db_path):
newname = db_path.replace('db.sqlite', 'backup{}.sqlite'.format(time.time()))
os.rename(db_path, newname)
self._db = sqlite3.connect(db_path)
with self._db:
self._db.execute('''CREATE TABLE IF NOT EXISTS history (agent_id text, t_step int, key text, value text, value_type text)''')
self['SEED'] = seed or time.time()
random.seed(self['SEED'])
@property
def agents(self):
@@ -90,20 +91,38 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
@network_agents.setter
def network_agents(self, network_agents):
if not network_agents:
return
for ix in self.G.nodes():
i = ix
node = self.G.node[i]
agent, state = utils.agent_from_distribution(network_agents)
self.set_agent(i, 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]
elif 'agent_type' in node:
agent_type = node['agent_type']
elif 'agent_type' in self.default_state:
agent_type = self.default_state['agent_type']
if agent_type:
agent_type = agents.deserialize_type(agent_type)
else:
agent_type, state = agents._agent_from_distribution(agent_distribution)
return self.set_agent(agent_id, agent_type, state)
def set_agent(self, agent_id, agent_type, state=None):
node = self.G.nodes[agent_id]
defstate = deepcopy(self.default_state)
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)
@@ -118,19 +137,28 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
return a
def add_edge(self, agent1, agent2, attrs=None):
if hasattr(agent1, 'id'):
agent1 = agent1.id
if hasattr(agent2, 'id'):
agent2 = agent2.id
return self.G.add_edge(agent1, agent2)
def run(self, *args, **kwargs):
self._save_state()
super().run(*args, **kwargs)
self._history.flush_cache()
def _save_state(self, now=None):
# for agent in self.agents:
# agent.save_state()
utils.logger.debug('Saving state @{}'.format(self.now))
with self._db:
self._db.executemany("insert into history(agent_id, t_step, key, value, value_type) values (?, ?, ?, ?, ?)", self.state_to_tuples(now=now))
self._history.save_records(self.state_to_tuples(now=now))
def save_state(self):
'''
:DEPRECATED:
Periodically save the state of the environment and the agents.
'''
self._save_state()
while self.peek() != simpy.core.Infinity:
delay = max(self.peek() - self.now, self.interval)
@@ -145,64 +173,44 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
def __getitem__(self, key):
if isinstance(key, tuple):
values = [("agent_id", key[0]),
("t_step", key[1]),
("key", key[2]),
("value", None),
("value_type", None)]
fields = list(k for k, v in values if v is None)
conditions = " and ".join("{}='{}'".format(k, v) for k, v in values if v is not None)
query = """SELECT {fields} from history""".format(fields=",".join(fields))
if conditions:
query = """{query} where {conditions}""".format(query=query,
conditions=conditions)
with self._db:
rows = self._db.execute(query).fetchall()
utils.logger.debug(rows)
results = self.rows_to_dict(rows)
return results
self._history.flush_cache()
return self._history[key]
return self.environment_params[key]
def rows_to_dict(self, rows):
if len(rows) < 1:
return None
level = len(rows[0])-2
if level == 0:
if len(rows) != 1:
raise ValueError('Cannot convert {} to dictionaries'.format(rows))
value, value_type = rows[0]
return utils.convert(value, value_type)
results = {}
for row in rows:
item = results
for i in range(level-1):
key = row[i]
if key not in item:
item[key] = {}
item = item[key]
key, value, value_type = row[level-1:]
item[key] = utils.convert(value, value_type)
return results
def __setitem__(self, key, value):
if isinstance(key, tuple):
k = history.Key(*key)
self._history.save_record(*k,
value=value)
return
self.environment_params[key] = value
self._history.save_record(agent_id='env',
t_step=self.now,
key=key,
value=value)
def __contains__(self, key):
return key in self.environment_params
def get(self, key, default=None):
'''
Get the value of an environment attribute in a
given point in the simulation (history).
If key is an attribute name, this method returns
the current value.
To get values at other times, use a
:meth: `soil.history.Key` tuple.
'''
return self[key] if key in self else default
def get_path(self, dir_path=None):
dir_path = dir_path or self.sim().dir_path
dir_path = dir_path or self.dir_path
if not os.path.exists(dir_path):
os.makedirs(dir_path)
try:
os.makedirs(dir_path)
except FileExistsError:
pass
return dir_path
def get_agent(self, agent_id):
@@ -217,7 +225,7 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
with open(csv_name, '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)
@@ -225,23 +233,45 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
G = self.history_to_graph()
graph_path = os.path.join(self.get_path(dir_path),
self.name+".gexf")
# Workaround for geometric models
# See soil/soil#4
for node in G.nodes():
if 'pos' in G.node[node]:
G.node[node]['viz'] = {"position": {"x": G.node[node]['pos'][0], "y": G.node[node]['pos'][1], "z": 0.0}}
del (G.node[node]['pos'])
nx.write_gexf(G, graph_path, version="1.2draft")
def dump(self, dir_path=None, formats=None):
if not formats:
return
functions = {
'csv': self.dump_csv,
'gexf': self.dump_gexf
}
for f in formats:
if f in functions:
functions[f](dir_path)
else:
raise ValueError('Unknown format: {}'.format(f))
def state_to_tuples(self, now=None):
if now is None:
now = self.now
for k, v in self.environment_params.items():
v, v_t = utils.repr(v)
yield 'env', now, k, v, v_t
yield history.Record(agent_id='env',
t_step=now,
key=k,
value=v)
for agent in self.agents:
for k, v in agent.state.items():
v, v_t = utils.repr(v)
yield agent.id, now, k, v, v_t
yield history.Record(agent_id=agent.id,
t_step=now,
key=k,
value=v)
def history_to_tuples(self):
with self._db:
res = self._db.execute("select agent_id, t_step, key, value, value_type from history ").fetchall()
yield from res
return self._history.to_tuples()
def history_to_graph(self):
G = nx.Graph(self.G)
@@ -256,31 +286,30 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
history = self[agent.id, None, None]
if not history:
continue
for t_step, state in reversed(sorted(list(history.items()))):
for attribute, value in state.items():
if attribute == 'visible':
nowvisible = state[attribute]
if nowvisible and not lastvisible:
laststep = t_step
if not nowvisible and lastvisible:
spells.append((laststep, t_step))
for t_step, attribute, value in sorted(list(history)):
if attribute == 'visible':
nowvisible = value
if nowvisible and not lastvisible:
laststep = t_step
if not nowvisible and lastvisible:
spells.append((laststep, t_step))
lastvisible = nowvisible
else:
key = 'attr_' + attribute
if key not in attributes:
attributes[key] = list()
if key not in lastattributes:
lastattributes[key] = (state[attribute], t_step)
elif lastattributes[key][0] != value:
last_value, laststep = lastattributes[key]
value = (last_value, t_step, laststep)
if key not in attributes:
attributes[key] = list()
attributes[key].append(value)
lastattributes[key] = (state[attribute], t_step)
lastvisible = nowvisible
continue
key = 'attr_' + attribute
if key not in attributes:
attributes[key] = list()
if key not in lastattributes:
lastattributes[key] = (value, t_step)
elif lastattributes[key][0] != value:
last_value, laststep = lastattributes[key]
commit_value = (last_value, laststep, t_step)
if key not in attributes:
attributes[key] = list()
attributes[key].append(commit_value)
lastattributes[key] = (value, t_step)
for k, v in lastattributes.items():
attributes[k].append((v[0], 0, v[1]))
attributes[k].append((v[0], v[1], None))
if lastvisible:
spells.append((laststep, None))
if spells:
@@ -289,3 +318,21 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
G.add_node(agent.id, **attributes)
return G
def __getstate__(self):
state = self.__dict__.copy()
state['G'] = json_graph.node_link_data(self.G)
state['network_agents'] = agents.serialize_distribution(self.network_agents)
state['environment_agents'] = agents._convert_agent_types(self.environment_agents,
to_string=True)
return state
def __setstate__(self, state):
self.__dict__ = state
self.G = json_graph.node_link_graph(state['G'])
self.network_agents = self.calculate_distribution(self._convert_agent_types(self.network_agents))
self.environment_agents = self._convert_agent_types(self.environment_agents)
return state
SoilEnvironment = Environment

281
soil/history.py Normal file
View File

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

View File

@@ -1,26 +1,28 @@
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 copy import deepcopy
from multiprocessing import Pool
from functools import partial
import pickle
from nxsim import NetworkSimulation
from . import agents, utils, environment, basestring
from . import utils, basestring, agents
from .environment import Environment
from .utils import logger
class SoilSimulation(NetworkSimulation):
class Simulation(NetworkSimulation):
"""
Subclass of nsim.NetworkSimulation with three main differences:
1) agent type can be specified by name or by class.
2) instead of just one type, an network_agents can be used.
2) instead of just one type, a network agents distribution can be used.
The distribution specifies the weight (or probability) of each
agent type in the topology. This is an example distribution: ::
@@ -43,13 +45,48 @@ class SoilSimulation(NetworkSimulation):
'agent_type_1'.
3) if no initial state is given, each node's state will be set
to `{'id': 0}`.
Parameters
---------
name : str, optional
name of the Simulation
topology : networkx.Graph instance, optional
network_params : dict
parameters used to create a topology with networkx, if no topology is given
network_agents : dict
definition of agents to populate the topology with
agent_type : NetworkAgent subclass, optional
Default type of NetworkAgent to use for nodes not specified in network_agents
states : list, optional
List of initial states corresponding to the nodes in the topology. Basic form is a list of integers
whose value indicates the state
dir_path : str, optional
Directory path where to save pickled objects
seed : str, optional
Seed to use for the random generator
num_trials : int, optional
Number of independent simulation runs
max_time : int, optional
Time how long the simulation should run
environment_params : dict, optional
Dictionary of globally-shared environmental parameters
environment_agents: dict, optional
Similar to network_agents. Distribution of Agents that control the environment
environment_class: soil.environment.Environment subclass, optional
Class for the environment. It defailts to soil.environment.Environment
load_module : str, module name, deprecated
If specified, soil will load the content of this module under 'soil.agents.custom'
"""
def __init__(self, name=None, topology=None, network_params=None,
network_agents=None, agent_type=None, states=None,
default_state=None, interval=1, dump=False,
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):
load_module=None, seed=None,
environment_agents=None, environment_params=None,
environment_class=None, **kwargs):
if topology is None:
topology = utils.load_network(network_params,
@@ -57,7 +94,6 @@ class SoilSimulation(NetworkSimulation):
elif isinstance(topology, basestring) or isinstance(topology, dict):
topology = json_graph.node_link_graph(topology)
self.load_module = load_module
self.topology = nx.Graph(topology)
self.network_params = network_params
@@ -69,94 +105,76 @@ class SoilSimulation(NetworkSimulation):
self.interval = interval
self.seed = str(seed) or str(time.time())
self.dump = dump
self.environment_params = environment_params or {}
self.dry_run = dry_run
if load_module:
path = sys.path + [self.dir_path]
f, fp, desc = imp.find_module(load_module, path)
imp.load_module('soil.agents.custom', f, fp, desc)
sys.path += [self.dir_path, os.getcwd()]
self.environment_params = environment_params or {}
self.environment_class = utils.deserialize(environment_class,
known_modules=['soil.environment', ]) or Environment
environment_agents = environment_agents or []
self.environment_agents = self._convert_agent_types(environment_agents)
self.environment_agents = agents._convert_agent_types(environment_agents,
known_modules=[self.load_module])
distro = self.calculate_distribution(network_agents,
agent_type)
self.network_agents = self._convert_agent_types(distro)
distro = agents.calculate_distribution(network_agents,
agent_type)
self.network_agents = agents._convert_agent_types(distro,
known_modules=[self.load_module])
self.states = self.validate_states(states,
self.topology)
self.states = agents._validate_states(states,
self.topology)
def calculate_distribution(self,
network_agents=None,
agent_type=None):
if network_agents:
network_agents = deepcopy(network_agents)
elif agent_type:
network_agents = [{'agent_type': agent_type}]
else:
return []
def run_simulation(self, *args, **kwargs):
return self.run(*args, **kwargs)
# Calculate the thresholds
total = sum(x.get('weight', 1) for x in network_agents)
acc = 0
for v in network_agents:
upper = acc + (v.get('weight', 1)/total)
v['threshold'] = [acc, upper]
acc = upper
return network_agents
def run(self, *args, **kwargs):
return list(self.run_simulation_gen(*args, **kwargs))
def serialize_distribution(self):
d = self._convert_agent_types(self.network_agents,
to_string=True)
for v in d:
if 'threshold' in v:
del v['threshold']
return d
def _convert_agent_types(self, ind, to_string=False):
d = deepcopy(ind)
for v in d:
agent_type = v['agent_type']
if to_string and not isinstance(agent_type, str):
v['agent_type'] = str(agent_type.__name__)
elif not to_string and isinstance(agent_type, str):
v['agent_type'] = agents.agent_types[agent_type]
return d
def validate_states(self, states, topology):
states = states or []
# Validate states to avoid ignoring states during
# initialization
if isinstance(states, dict):
for x in states:
assert x in self.topology.node
else:
assert len(states) <= len(self.topology)
return states
def run_simulation(self):
return self.run()
def run(self):
return list(self.run_simulation_gen())
def run_simulation_gen(self, *args, **kwargs):
with utils.timer('simulation'):
for i in range(self.num_trials):
res = self.run_trial(i)
if self.dump:
res.dump_gexf(self.dir_path)
res.dump_csv(self.dir_path)
yield res
if self.dump:
def run_simulation_gen(self, *args, parallel=False, dry_run=False,
**kwargs):
p = Pool()
with utils.timer('simulation {}'.format(self.name)):
if parallel:
func = partial(self.run_trial_exceptions, dry_run=dry_run or self.dry_run,
return_env=True,
**kwargs)
for i in p.imap_unordered(func, range(self.num_trials)):
if isinstance(i, Exception):
logger.error('Trial failed:\n\t{}'.format(i.message))
continue
yield i
else:
for i in range(self.num_trials):
yield self.run_trial(i, dry_run = dry_run or self.dry_run, **kwargs)
if not (dry_run or self.dry_run):
logger.info('Dumping results to {}'.format(self.dir_path))
self.dump_pickle(self.dir_path)
self.dump_yaml(self.dir_path)
else:
logger.info('NOT dumping results')
def run_trial(self, trial_id=0, dump=False, dir_path=None):
def get_env(self, trial_id = 0, **kwargs):
opts=self.environment_params.copy()
env_name='{}_trial_{}'.format(self.name, trial_id)
opts.update({
'name': env_name,
'topology': self.topology.copy(),
'seed': self.seed+env_name,
'initial_time': 0,
'dry_run': self.dry_run,
'interval': self.interval,
'network_agents': self.network_agents,
'states': self.states,
'default_state': self.default_state,
'environment_agents': self.environment_agents,
'dir_path': self.dir_path,
})
opts.update(kwargs)
env=self.environment_class(**opts)
return env
def run_trial(self, trial_id = 0, until = None, return_env = True, **opts):
"""Run a single trial of the simulation
Parameters
@@ -164,25 +182,27 @@ class SoilSimulation(NetworkSimulation):
trial_id : int
"""
# Set-up trial environment and graph
logger.info('Trial: {}'.format(trial_id))
env_name = '{}_trial_{}'.format(self.name, trial_id)
env = environment.SoilEnvironment(name=env_name,
topology=self.topology.copy(),
seed=self.seed+env_name,
initial_time=0,
dump=self.dump,
interval=self.interval,
network_agents=self.network_agents,
states=self.states,
default_state=self.default_state,
environment_agents=self.environment_agents,
simulation=self,
**self.environment_params)
until=until or self.max_time
env=self.get_env(trial_id = trial_id, **opts)
# Set up agents on nodes
logger.info('\tRunning')
with utils.timer('trial'):
env.run(until=self.max_time)
return env
with utils.timer('Simulation {} trial {}'.format(self.name, trial_id)):
env.run(until)
if self.dump and not self.dry_run:
with utils.timer('Dumping simulation {} trial {}'.format(self.name, trial_id)):
env.dump(formats = self.dump)
if return_env:
return env
def run_trial_exceptions(self, *args, **kwargs):
'''
A wrapper for run_trial that catches exceptions and returns them.
It is meant for async simulations
'''
try:
return self.run_trial(*args, **kwargs)
except Exception as ex:
c = ex.__cause__
c.message = ''.join(traceback.format_tb(c.__traceback__)[3:])
return c
def to_dict(self):
return self.__getstate__()
@@ -190,66 +210,80 @@ class SoilSimulation(NetworkSimulation):
def to_yaml(self):
return yaml.dump(self.to_dict())
def dump_yaml(self, dir_path=None, file_name=None):
dir_path = dir_path or self.dir_path
def dump_yaml(self, dir_path = None, file_name = None):
dir_path=dir_path or self.dir_path
if not os.path.exists(dir_path):
os.makedirs(dir_path)
if not file_name:
file_name = os.path.join(dir_path,
file_name=os.path.join(dir_path,
'{}.dumped.yml'.format(self.name))
with open(file_name, 'w') as f:
f.write(self.to_yaml())
def dump_pickle(self, dir_path=None, pickle_name=None):
dir_path = dir_path or self.dir_path
def dump_pickle(self, dir_path = None, pickle_name = None):
dir_path=dir_path or self.dir_path
if not os.path.exists(dir_path):
os.makedirs(dir_path)
if not pickle_name:
pickle_name = os.path.join(dir_path,
pickle_name=os.path.join(dir_path,
'{}.simulation.pickle'.format(self.name))
with open(pickle_name, 'wb') as f:
pickle.dump(self, f)
def __getstate__(self):
state = self.__dict__.copy()
state['topology'] = json_graph.node_link_data(self.topology)
state['network_agents'] = self.serialize_distribution()
state['environment_agents'] = self._convert_agent_types(self.environment_agents,
to_string=True)
state={}
for k, v in self.__dict__.items():
if k[0] != '_':
state[k]=v
state['topology']=json_graph.node_link_data(self.topology)
state['network_agents']=agents.serialize_distribution(self.network_agents,
known_modules = [])
state['environment_agents']=agents.serialize_distribution(self.environment_agents,
known_modules = [])
state['environment_class']=utils.serialize(self.environment_class,
known_modules=['soil.environment'])[1] # func, name
if state['load_module'] is None:
del state['load_module']
return state
def __setstate__(self, state):
self.__dict__ = state
self.load_module = getattr(self, 'load_module', None)
if self.dir_path not in sys.path:
sys.path += [self.dir_path, os.getcwd()]
self.topology = json_graph.node_link_graph(state['topology'])
self.network_agents = self._convert_agent_types(self.network_agents)
self.environment_agents = self._convert_agent_types(self.environment_agents)
self.network_agents = agents.calculate_distribution(agents._convert_agent_types(self.network_agents))
self.environment_agents = agents._convert_agent_types(self.environment_agents,
known_modules=[self.load_module])
self.environment_class = utils.deserialize(self.environment_class,
known_modules=[self.load_module, 'soil.environment', ]) # func, name
return state
def from_config(config, G=None):
def from_config(config):
config = list(utils.load_config(config))
if len(config) > 1:
raise AttributeError('Provide only one configuration')
config = config[0][0]
sim = SoilSimulation(**config)
sim = Simulation(**config)
return sim
def run_from_config(*configs, dump=True, results_dir=None, timestamp=False):
if not results_dir:
results_dir = 'soil_output'
def run_from_config(*configs, results_dir='soil_output', dump=None, timestamp=False, **kwargs):
for config_def in configs:
for config, cpath in utils.load_config(config_def):
# logger.info("Found {} config(s)".format(len(ls)))
for config, _ in utils.load_config(config_def):
name = config.get('name', 'unnamed')
logger.info("Using config(s): {name}".format(name=name))
sim = SoilSimulation(**config)
if timestamp:
sim_folder = '{}_{}'.format(sim.name,
sim_folder = '{}_{}'.format(name,
time.strftime("%Y-%m-%d_%H:%M:%S"))
else:
sim_folder = sim.name
sim.dir_path = os.path.join(results_dir, sim_folder)
sim.dump = dump
logger.info('Dumping results to {} : {}'.format(sim.dir_path, dump))
results = sim.run_simulation()
sim_folder = name
dir_path = os.path.join(results_dir, sim_folder)
if dump is not None:
config['dump'] = dump
sim = Simulation(dir_path=dir_path, **config)
logger.info('Dumping results to {} : {}'.format(sim.dir_path, sim.dump))
sim.run_simulation(**kwargs)

View File

@@ -1,7 +1,9 @@
import os
import ast
import yaml
import logging
from time import time
import importlib
import time
from glob import glob
from random import random
from copy import deepcopy
@@ -11,7 +13,7 @@ import networkx as nx
from contextlib import contextmanager
logger = logging.getLogger(__name__)
logger = logging.getLogger('soil')
logger.setLevel(logging.INFO)
@@ -61,45 +63,92 @@ def load_config(config):
@contextmanager
def timer(name='task', pre="", function=logger.info, to_object=None):
start = time()
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 agent_from_distribution(distribution, value=-1):
"""Find the agent """
if value < 0:
value = random()
for d in distribution:
threshold = d['threshold']
if value >= threshold[0] and value < threshold[1]:
state = {}
if 'state' in d:
state = deepcopy(d['state'])
return d['agent_type'], state
builtins = importlib.import_module('builtins')
raise Exception('Distribution for value {} not found in: {}'.format(value, distribution))
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 repr(v):
if isinstance(v, bool):
v = "true" if v else ""
return v, bool.__name__
return v, type(v).__name__
def serializer(type_):
if type_ != 'str' and hasattr(builtins, type_):
return repr
return lambda x: x
def convert(value, type_):
import importlib
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
def serialize(v, known_modules=[]):
'''Get a text representation of an object.'''
tname = name(v, known_modules=known_modules)
func = serializer(tname)
return func(v), tname
def deserializer(type_, known_modules=[]):
if type_ == 'str':
return lambda x='': x
if type_ == 'None':
return lambda x=None: None
if hasattr(builtins, type_): # Check if it's a builtin type
cls = getattr(builtins, type_)
return lambda x=None: ast.literal_eval(x) if x is not None else cls()
# Otherwise, see if we can find the module and the class
modules = known_modules or []
options = []
for mod in modules:
if mod:
options.append((mod, type_))
if '.' in type_: # Fully qualified module
module, type_ = type_.rsplit(".", 1)
module = importlib.import_module(module)
cls = getattr(module, type_)
return cls(value)
options.append ((module, type_))
errors = []
for modname, tname in options:
try:
module = importlib.import_module(modname)
cls = getattr(module, tname)
return getattr(cls, 'deserialize', cls)
except (ImportError, AttributeError) as ex:
errors.append((modname, tname, ex))
raise Exception('Could not find type {}. Tried: {}'.format(type_, errors))
def deserialize(type_, value=None, **kwargs):
'''Get an object from a text representation'''
if not isinstance(type_, str):
return type_
des = deserializer(type_, **kwargs)
if value is None:
return des
return des(value)

20
soil/version.py Normal file
View File

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

4
soil/web/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
__pycache__/
output/
tests/
soil_output/

59
soil/web/README.md Normal file
View File

@@ -0,0 +1,59 @@
# Graph Visualization with D3.js
The aim of this software is to provide a useful tool for visualising and analysing the result of different simulations based on graph. Once you run the simulation, you will be able to interact with the simulation in real time.
For this purpose, a model which tries to simulate the spread of information to comprehend the radicalism spread in a society is included. Whith all this, the main project goals could be divided in five as it is shown in the following.
* Simulate the spread of information through a network applied to radicalism.
* Visualize the results of the simulation.
* Interact with the simulation in real time.
* Extract data from the results.
* Show data in a right way for its research.
## Deploying the server
For deploying the application, you will only need to run the following command.
`python3 run.py [--name NAME] [--dump] [--port PORT] [--verbose]`
Where the options are detailed in the following table.
| Option | Description |
| --- | --- |
| `--name NAME` | The name of the simulation. It will appear on the app. |
| `--dump` | For dumping the results in server side. |
| `--port PORT` | The port where the server will listen. |
| `--verbose` | Verbose mode. |
> You can dump the results of the simulation in server side. Anyway, you will be able to download them in GEXF or JSON Graph format directly from the browser.
## Visualization Params
The configuration of the simulation is based on the simulator configuration. In this case, it follows the [SOIL](https://github.com/gsi-upm/soil) configuration syntax and for visualising the results in a more comfortable way, more params can be added in `visualization_params` dictionary.
* For setting a background image, the tag needed is `background image`. You can also add a `background_opacity` and `background_filter_color` if the image is so clear than you can difficult view the nodes.
* For setting colors to the nodes, you can do it based on their properties values. Using the `color` tag, you will need to indicate the attribute key and value, and then the color you want to apply.
* The shapes applied to a group of nodes are always the same. This means than it won't change dynamically, so you will have to indicate the property with the `shape_property` tag and add a dictionary called `shapes` in which for each value, you indicate the shape.
All shapes have to had been downloaded before in SVG format and added to the server.
An example of this configuration applied to the TerroristNetworkModel is presented.
```yaml
visualization_params:
# Icons downloaded from https://www.iconfinder.com/
shape_property: agent
shapes:
TrainingAreaModel: target
HavenModel: home
TerroristNetworkModel: person
colors:
- attr_id: 0
color: '#40de40'
- attr_id: 1
color: red
- attr_id: 2
color: '#c16a6a'
background_image: 'map_4800x2860.jpg'
background_opacity: '0.9'
background_filter_color: 'blue'
```

View File

@@ -0,0 +1,255 @@
import random
import networkx as nx
from soil.agents import BaseAgent, FSM, state, default_state
from scipy.spatial import cKDTree as KDTree
global betweenness_centrality_global
global degree_centrality_global
betweenness_centrality_global = None
degree_centrality_global = None
class TerroristSpreadModel(FSM):
"""
Settings:
information_spread_intensity
terrorist_additional_influence
min_vulnerability (optional else zero)
max_vulnerability
prob_interaction
"""
def __init__(self, environment=None, agent_id=0, state=()):
super().__init__(environment=environment, agent_id=agent_id, state=state)
global betweenness_centrality_global
global degree_centrality_global
if betweenness_centrality_global == None:
betweenness_centrality_global = nx.betweenness_centrality(self.global_topology)
if degree_centrality_global == None:
degree_centrality_global = nx.degree_centrality(self.global_topology)
self.information_spread_intensity = environment.environment_params['information_spread_intensity']
self.terrorist_additional_influence = environment.environment_params['terrorist_additional_influence']
self.prob_interaction = environment.environment_params['prob_interaction']
if self['id'] == self.civilian.id: # Civilian
self.initial_belief = random.uniform(0.00, 0.5)
elif self['id'] == self.terrorist.id: # Terrorist
self.initial_belief = random.uniform(0.8, 1.00)
elif self['id'] == self.leader.id: # Leader
self.initial_belief = 1.00
else:
raise Exception('Invalid state id: {}'.format(self['id']))
if 'min_vulnerability' in environment.environment_params:
self.vulnerability = random.uniform( environment.environment_params['min_vulnerability'], environment.environment_params['max_vulnerability'] )
else :
self.vulnerability = random.uniform( 0, environment.environment_params['max_vulnerability'] )
self.mean_belief = self.initial_belief
self.betweenness_centrality = betweenness_centrality_global[self.id]
self.degree_centrality = degree_centrality_global[self.id]
# self.state['radicalism'] = self.mean_belief
def count_neighboring_agents(self, state_id=None):
if isinstance(state_id, list):
return len(self.get_neighboring_agents(state_id))
else:
return len(super().get_agents(state_id, limit_neighbors=True))
def get_neighboring_agents(self, state_id=None):
if isinstance(state_id, list):
_list = []
for i in state_id:
_list += super().get_agents(i, limit_neighbors=True)
return [ neighbour for neighbour in _list if isinstance(neighbour, TerroristSpreadModel) ]
else:
_list = super().get_agents(state_id, limit_neighbors=True)
return [ neighbour for neighbour in _list if isinstance(neighbour, TerroristSpreadModel) ]
@state
def civilian(self):
if self.count_neighboring_agents() > 0:
neighbours = []
for neighbour in self.get_neighboring_agents():
if random.random() < self.prob_interaction:
neighbours.append(neighbour)
influence = sum( neighbour.degree_centrality for neighbour in neighbours )
mean_belief = sum( neighbour.mean_belief * neighbour.degree_centrality / influence for neighbour in neighbours )
self.initial_belief = self.mean_belief
mean_belief = mean_belief * self.information_spread_intensity + self.initial_belief * ( 1 - self.information_spread_intensity )
self.mean_belief = mean_belief * self.vulnerability + self.initial_belief * ( 1 - self.vulnerability )
if self.mean_belief >= 0.8:
return self.terrorist
@state
def leader(self):
self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence )
if self.count_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]) > 0:
for neighbour in self.get_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]):
if neighbour.betweenness_centrality > self.betweenness_centrality:
return self.terrorist
@state
def terrorist(self):
if self.count_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]) > 0:
neighbours = self.get_neighboring_agents(state_id=[self.terrorist.id, self.leader.id])
influence = sum( neighbour.degree_centrality for neighbour in neighbours )
mean_belief = sum( neighbour.mean_belief * neighbour.degree_centrality / influence for neighbour in neighbours )
self.initial_belief = self.mean_belief
self.mean_belief = mean_belief * self.vulnerability + self.initial_belief * ( 1 - self.vulnerability )
self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence )
if self.count_neighboring_agents(state_id=self.leader.id) == 0 and self.count_neighboring_agents(state_id=self.terrorist.id) > 0:
max_betweenness_centrality = self
for neighbour in self.get_neighboring_agents(state_id=self.terrorist.id):
if neighbour.betweenness_centrality > max_betweenness_centrality.betweenness_centrality:
max_betweenness_centrality = neighbour
if max_betweenness_centrality == self:
return self.leader
def add_edge(self, G, source, target):
G.add_edge(source.id, target.id, start=self.env._now)
def link_search(self, G, node, radius):
pos = nx.get_node_attributes(G, 'pos')
nodes, coords = list(zip(*pos.items()))
kdtree = KDTree(coords) # Cannot provide generator.
edge_indexes = kdtree.query_pairs(radius, 2)
_list = [ edge[int(not edge.index(node))] for edge in edge_indexes if node in edge ]
return [ G.nodes()[index]['agent'] for index in _list ]
def social_search(self, G, node, steps):
nodes = list(nx.ego_graph(G, node, radius=steps).nodes())
nodes.remove(node)
return [ G.nodes()[index]['agent'] for index in nodes ]
class TrainingAreaModel(FSM):
"""
Settings:
training_influence
min_vulnerability
Requires TerroristSpreadModel.
"""
def __init__(self, environment=None, agent_id=0, state=()):
super().__init__(environment=environment, agent_id=agent_id, state=state)
self.training_influence = environment.environment_params['training_influence']
if 'min_vulnerability' in environment.environment_params:
self.min_vulnerability = environment.environment_params['min_vulnerability']
else: self.min_vulnerability = 0
@default_state
@state
def terrorist(self):
for neighbour in self.get_neighboring_agents():
if isinstance(neighbour, TerroristSpreadModel) and neighbour.vulnerability > self.min_vulnerability:
neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.training_influence )
class HavenModel(FSM):
"""
Settings:
haven_influence
min_vulnerability
max_vulnerability
Requires TerroristSpreadModel.
"""
def __init__(self, environment=None, agent_id=0, state=()):
super().__init__(environment=environment, agent_id=agent_id, state=state)
self.haven_influence = environment.environment_params['haven_influence']
if 'min_vulnerability' in environment.environment_params:
self.min_vulnerability = environment.environment_params['min_vulnerability']
else: self.min_vulnerability = 0
self.max_vulnerability = environment.environment_params['max_vulnerability']
@state
def civilian(self):
for neighbour_agent in self.get_neighboring_agents():
if isinstance(neighbour_agent, TerroristSpreadModel) and neighbour_agent['id'] == neighbour_agent.civilian.id:
for neighbour in self.get_neighboring_agents():
if isinstance(neighbour, TerroristSpreadModel) and neighbour.vulnerability > self.min_vulnerability:
neighbour.vulnerability = neighbour.vulnerability * ( 1 - self.haven_influence )
return self.civilian
return self.terrorist
@state
def terrorist(self):
for neighbour in self.get_neighboring_agents():
if isinstance(neighbour, TerroristSpreadModel) and neighbour.vulnerability < self.max_vulnerability:
neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.haven_influence )
return self.terrorist
class TerroristNetworkModel(TerroristSpreadModel):
"""
Settings:
sphere_influence
vision_range
weight_social_distance
weight_link_distance
"""
def __init__(self, environment=None, agent_id=0, state=()):
super().__init__(environment=environment, agent_id=agent_id, state=state)
self.vision_range = environment.environment_params['vision_range']
self.sphere_influence = environment.environment_params['sphere_influence']
self.weight_social_distance = environment.environment_params['weight_social_distance']
self.weight_link_distance = environment.environment_params['weight_link_distance']
@state
def terrorist(self):
self.update_relationships()
return super().terrorist()
@state
def leader(self):
self.update_relationships()
return super().leader()
def update_relationships(self):
if self.count_neighboring_agents(state_id=self.civilian.id) == 0:
close_ups = self.link_search(self.global_topology, self.id, self.vision_range)
step_neighbours = self.social_search(self.global_topology, self.id, self.sphere_influence)
search = list(set(close_ups).union(step_neighbours))
neighbours = self.get_neighboring_agents()
search = [item for item in search if not item in neighbours and isinstance(item, TerroristNetworkModel)]
for agent in search:
social_distance = 1 / self.shortest_path_length(self.global_topology, self.id, agent.id)
spatial_proximity = ( 1 - self.get_distance(self.global_topology, self.id, agent.id) )
prob_new_interaction = self.weight_social_distance * social_distance + self.weight_link_distance * spatial_proximity
if agent['id'] == agent.civilian.id and random.random() < prob_new_interaction:
self.add_edge(self.global_topology, self, agent)
break
def get_distance(self, G, source, target):
source_x, source_y = nx.get_node_attributes(G, 'pos')[source]
target_x, target_y = nx.get_node_attributes(G, 'pos')[target]
dx = abs( source_x - target_x )
dy = abs( source_y - target_y )
return ( dx ** 2 + dy ** 2 ) ** ( 1 / 2 )
def shortest_path_length(self, G, source, target):
try:
return nx.shortest_path_length(G, source, target)
except nx.NetworkXNoPath:
return float('inf')

View File

@@ -0,0 +1,62 @@
name: TerroristNetworkModel_sim
load_module: TerroristNetworkModel
max_time: 150
num_trials: 1
network_params:
generator: random_geometric_graph
radius: 0.2
# generator: geographical_threshold_graph
# theta: 20
n: 100
network_agents:
- agent_type: TerroristNetworkModel
weight: 0.8
state:
id: civilian # Civilians
- agent_type: TerroristNetworkModel
weight: 0.1
state:
id: leader # Leaders
- agent_type: TrainingAreaModel
weight: 0.05
state:
id: terrorist # Terrorism
- agent_type: HavenModel
weight: 0.05
state:
id: civilian # Civilian
environment_params:
# TerroristSpreadModel
information_spread_intensity: 0.7
terrorist_additional_influence: 0.035
max_vulnerability: 0.7
prob_interaction: 0.5
# TrainingAreaModel and HavenModel
training_influence: 0.20
haven_influence: 0.20
# TerroristNetworkModel
vision_range: 0.30
sphere_influence: 2
weight_social_distance: 0.035
weight_link_distance: 0.035
visualization_params:
# Icons downloaded from https://www.iconfinder.com/
shape_property: agent
shapes:
TrainingAreaModel: target
HavenModel: home
TerroristNetworkModel: person
colors:
- attr_id: civilian
color: '#40de40'
- attr_id: terrorist
color: red
- attr_id: leader
color: '#c16a6a'
background_image: 'map_4800x2860.jpg'
background_opacity: '0.9'
background_filter_color: 'blue'

274
soil/web/__init__.py Normal file
View File

@@ -0,0 +1,274 @@
import io
import threading
import asyncio
import logging
import networkx as nx
import os
import sys
import tornado.ioloop
import tornado.web
import tornado.websocket
import tornado.escape
import tornado.gen
import yaml
import webbrowser
from contextlib import contextmanager
from time import sleep
from xml.etree.ElementTree import tostring
from tornado.concurrent import run_on_executor
from concurrent.futures import ThreadPoolExecutor
from ..simulation import SoilSimulation
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
ROOT = os.path.abspath(os.path.dirname(__file__))
MAX_WORKERS = 4
LOGGING_INTERVAL = 0.5
# Workaround to let Soil load the required modules
sys.path.append(ROOT)
class PageHandler(tornado.web.RequestHandler):
""" Handler for the HTML template which holds the visualization. """
def get(self):
self.render('index.html', port=self.application.port,
name=self.application.name)
class SocketHandler(tornado.websocket.WebSocketHandler):
""" Handler for websocket. """
executor = ThreadPoolExecutor(max_workers=MAX_WORKERS)
def open(self):
if self.application.verbose:
logger.info('Socket opened!')
def check_origin(self, origin):
return True
def on_message(self, message):
""" Receiving a message from the websocket, parse, and act accordingly. """
msg = tornado.escape.json_decode(message)
if msg['type'] == 'config_file':
if self.application.verbose:
print(msg['data'])
self.config = list(yaml.load_all(msg['data']))
if len(self.config) > 1:
error = 'Please, provide only one configuration.'
if self.application.verbose:
logger.error(error)
self.write_message({'type': 'error',
'error': error})
return
self.config = self.config[0]
self.send_log('INFO.' + self.simulation_name,
'Using config: {name}'.format(name=self.config['name']))
if 'visualization_params' in self.config:
self.write_message({'type': 'visualization_params',
'data': self.config['visualization_params']})
self.name = self.config['name']
self.run_simulation()
settings = []
for key in self.config['environment_params']:
if type(self.config['environment_params'][key]) == float or type(self.config['environment_params'][key]) == int:
if self.config['environment_params'][key] <= 1:
setting_type = 'number'
else:
setting_type = 'great_number'
elif type(self.config['environment_params'][key]) == bool:
setting_type = 'boolean'
else:
setting_type = 'undefined'
settings.append({
'label': key,
'type': setting_type,
'value': self.config['environment_params'][key]
})
self.write_message({'type': 'settings',
'data': settings})
elif msg['type'] == 'get_trial':
if self.application.verbose:
logger.info('Trial {} requested!'.format(msg['data']))
self.send_log('INFO.' + __name__, 'Trial {} requested!'.format(msg['data']))
self.write_message({'type': 'get_trial',
'data': self.get_trial(int(msg['data']))})
elif msg['type'] == 'run_simulation':
if self.application.verbose:
logger.info('Running new simulation for {name}'.format(name=self.config['name']))
self.send_log('INFO.' + self.simulation_name, 'Running new simulation for {name}'.format(name=self.config['name']))
self.config['environment_params'] = msg['data']
self.run_simulation()
elif msg['type'] == 'download_gexf':
G = self.trials[ int(msg['data']) ].history_to_graph()
for node in G.nodes():
if 'pos' in G.node[node]:
G.node[node]['viz'] = {"position": {"x": G.node[node]['pos'][0], "y": G.node[node]['pos'][1], "z": 0.0}}
del (G.node[node]['pos'])
writer = nx.readwrite.gexf.GEXFWriter(version='1.2draft')
writer.add_graph(G)
self.write_message({'type': 'download_gexf',
'filename': self.config['name'] + '_trial_' + str(msg['data']),
'data': tostring(writer.xml).decode(writer.encoding) })
elif msg['type'] == 'download_json':
G = self.trials[ int(msg['data']) ].history_to_graph()
for node in G.nodes():
if 'pos' in G.node[node]:
G.node[node]['viz'] = {"position": {"x": G.node[node]['pos'][0], "y": G.node[node]['pos'][1], "z": 0.0}}
del (G.node[node]['pos'])
self.write_message({'type': 'download_json',
'filename': self.config['name'] + '_trial_' + str(msg['data']),
'data': nx.node_link_data(G) })
else:
if self.application.verbose:
logger.info('Unexpected message!')
def update_logging(self):
try:
if (not self.log_capture_string.closed and self.log_capture_string.getvalue()):
for i in range(len(self.log_capture_string.getvalue().split('\n')) - 1):
self.send_log('INFO.' + self.simulation_name, self.log_capture_string.getvalue().split('\n')[i])
self.log_capture_string.truncate(0)
self.log_capture_string.seek(0)
finally:
if self.capture_logging:
tornado.ioloop.IOLoop.current().call_later(LOGGING_INTERVAL, self.update_logging)
def on_close(self):
if self.application.verbose:
logger.info('Socket closed!')
def send_log(self, logger, logging):
self.write_message({'type': 'log',
'logger': logger,
'logging': logging})
@property
def simulation_name(self):
return self.config.get('name', 'NoSimulationRunning')
@run_on_executor
def nonblocking(self, config):
simulation = SoilSimulation(**config)
return simulation.run()
@tornado.gen.coroutine
def run_simulation(self):
# Run simulation and capture logs
logger.info('Running simulation!')
if 'visualization_params' in self.config:
del self.config['visualization_params']
with self.logging(self.simulation_name):
try:
config = dict(**self.config)
config['dir_path'] = os.path.join(self.application.dir_path, config['name'])
config['dump'] = self.application.dump
self.trials = yield self.nonblocking(config)
self.write_message({'type': 'trials',
'data': list(trial.name for trial in self.trials) })
except Exception as ex:
error = 'Something went wrong:\n\t{}'.format(ex)
logging.info(error)
self.write_message({'type': 'error',
'error': error})
self.send_log('ERROR.' + self.simulation_name, error)
def get_trial(self, trial):
logger.info('Available trials: %s ' % len(self.trials))
logger.info('Ask for : %s' % trial)
trial = self.trials[trial]
G = trial.history_to_graph()
return nx.node_link_data(G)
@contextmanager
def logging(self, logger):
self.capture_logging = True
self.logger_application = logging.getLogger(logger)
self.log_capture_string = io.StringIO()
ch = logging.StreamHandler(self.log_capture_string)
self.logger_application.addHandler(ch)
self.update_logging()
yield self.capture_logging
sleep(0.2)
self.log_capture_string.close()
self.logger_application.removeHandler(ch)
self.capture_logging = False
return self.capture_logging
class ModularServer(tornado.web.Application):
""" Main visualization application. """
port = 8001
page_handler = (r'/', PageHandler)
socket_handler = (r'/ws', SocketHandler)
static_handler = (r'/(.*)', tornado.web.StaticFileHandler,
{'path': os.path.join(ROOT, 'static')})
local_handler = (r'/local/(.*)', tornado.web.StaticFileHandler,
{'path': ''})
handlers = [page_handler, socket_handler, static_handler, local_handler]
settings = {'debug': True,
'template_path': ROOT + '/templates'}
def __init__(self, dump=False, dir_path='output', name='SOIL', verbose=True, *args, **kwargs):
self.verbose = verbose
self.name = name
self.dump = dump
self.dir_path = dir_path
# Initializing the application itself:
super().__init__(self.handlers, **self.settings)
def launch(self, port=None):
""" Run the app. """
if port is not None:
self.port = port
url = 'http://127.0.0.1:{PORT}'.format(PORT=self.port)
print('Interface starting at {url}'.format(url=url))
self.listen(self.port)
# webbrowser.open(url)
tornado.ioloop.IOLoop.instance().start()
def run(*args, **kwargs):
asyncio.set_event_loop(asyncio.new_event_loop())
server = ModularServer(*args, **kwargs)
server.launch()
def main():
import argparse
parser = argparse.ArgumentParser(description='Visualization of a Graph Model')
parser.add_argument('--name', '-n', nargs=1, default='SOIL', help='name of the simulation')
parser.add_argument('--dump', '-d', help='dumping results in folder output', action='store_true')
parser.add_argument('--port', '-p', nargs=1, default=8001, help='port for launching the server')
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)

5
soil/web/__main__.py Normal file
View File

@@ -0,0 +1,5 @@
from . import main
if __name__ == "__main__":
main()

25
soil/web/config.yml Normal file
View File

@@ -0,0 +1,25 @@
name: ControlModelM2_sim
max_time: 50
num_trials: 2
network_params:
generator: barabasi_albert_graph
n: 100
m: 2
network_agents:
- agent_type: ControlModelM2
weight: 0.1
state:
id: 1
- agent_type: ControlModelM2
weight: 0.9
state:
id: 0
environment_params:
prob_neutral_making_denier: 0.035
prob_infect: 0.075
prob_cured_healing_infected: 0.035
prob_cured_vaccinate_neutral: 0.035
prob_vaccinated_healing_infected: 0.035
prob_vaccinated_vaccinate_neutral: 0.035
prob_generate_anti_rumor: 0.035
standard_variance: 0.055

23
soil/web/run.py Normal file
View File

@@ -0,0 +1,23 @@
import argparse
from server import ModularServer
from simulator import Simulator
def run(simulator, name="SOIL", port=8001, verbose=False):
server = ModularServer(simulator, name=(name[0] if isinstance(name, list) else name), verbose=verbose)
server.port = port
server.launch()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Visualization of a Graph Model')
parser.add_argument('--name', '-n', nargs=1, default='SOIL', help='name of the simulation')
parser.add_argument('--dump', '-d', help='dumping results in folder output', action='store_true')
parser.add_argument('--port', '-p', nargs=1, default=8001, help='port for launching the server')
parser.add_argument('--verbose', '-v', help='verbose mode', action='store_true')
args = parser.parse_args()
soil = Simulator(dump=args.dump)
run(soil, name=args.name, port=(args.port[0] if isinstance(args.port, list) else args.port), verbose=args.verbose)

View File

@@ -0,0 +1,431 @@
html, body {
height: 100%;
}
.carousel {
height: calc(100% - 150px);
}
.carousel-inner {
height: calc(100% - 50px) !important;
}
.carousel-inner .item,
.carousel-inner .item .container-fluid {
height: 100%;
}
.navbar {
box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, .2)
}
.nav.navbar-right {
margin-right: 10px !important;
}
.nav.navbar-right a {
outline: none !important;
}
.dropdown-menu > li > a:hover {
background-color: #d4d3d3;
cursor: pointer;
}
.wrapper-heading {
display: flex;
flex-direction: row;
padding: 0 !important;
}
.soil_logo {
padding: 0 !important;
border-left: none !important;
border-right: none !important;
display: flex;
justify-content: flex-end;
background-color: rgb(88, 88, 88);
}
.soil_logo > img {
max-height: 100%;
}
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
svg#graph, #configuration {
background-color: white;
margin-top: 15px;
border-style: double;
border-color: rgba(0, 0, 0, 0.35);
border-radius: 5px;
padding: 0px;
}
#timeline {
padding: 0;
margin-top: 20px;
}
#configuration {
margin-top: 15px;
padding: 15px;
border-left: none !important;
overflow: auto;
display: flex;
flex-direction: column;
align-items: inherit;
justify-content: space-evenly;
}
button {
outline: none !important;
}
.btn-toolbar.controls {
position: absolute;
right: 0;
}
.controls > .btn {
margin-left: 10px !important;
}
button.pressed {
background-color: rgb(167, 242, 168);
-webkit-animation: background 1s cubic-bezier(1,0,0,1) infinite;
animation: background 1s cubic-bezier(1,0,0,1) infinite;
cursor: default !important;
}
@-webkit-keyframes background {
50% { background-color: #dddddd; }
100% { background-color: rgb(167, 242, 168); }
}
@keyframes background {
50% { background-color: #dddddd; }
100% { background-color: rgb(167, 242, 168); }
}
#slider3 {
background: repeating-linear-gradient( 90deg, white 27px, white 30px, #fff 32px, #aaa 33px );
background-color: white;
}
hr {
margin-top: 15px !important;
margin-bottom: 15px !important;
width: 100%;
}
#update .config-item {
margin-top: 15px !important;
}
/** LOADER **/
#load {
position: absolute;
font-weight: bold;
}
#load.loader {
border: 5px solid #f3f3f3;
border-radius: 50%;
border-top: 5px solid #3498db;
border-bottom: 5px solid #3498db;
width: 30px;
height: 30px;
-webkit-animation: spin 1s linear infinite;
animation: spin 1s linear infinite;
position: absolute;
}
#load:before {
content: 'No file'
}
#load.loader:before {
content: '' !important;
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/** ALERT **/
.alert-danger {
position: absolute;
margin-top: 20px;
margin-left: 5px;
}
/** FILE BROWSER **/
.custom-file {
position: relative;
display: inline-block;
width: 100%;
height: 35px;
margin-bottom: 0;
cursor: pointer;
}
.custom-file-input {
min-width: 14rem;
max-width: 100%;
height: 35px;
margin: 0;
filter: alpha(opacity=0);
opacity: 0;
}
.custom-file-control {
position: absolute;
top: 0;
right: 0;
left: 0;
z-index: 5;
height: 35px;
padding: .5rem 1rem;
overflow: hidden;
line-height: 1.5;
color: #464a4c;
pointer-events: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: #fff;
border: 1px solid rgba(0,0,0,.15);
border-radius: .25rem;
}
.custom-file-control::before {
content: "Browse";
position: absolute;
top: -1px;
right: -1px;
bottom: -1px;
z-index: 6;
display: block;
height: 35px;
padding: .5rem 1rem;
line-height: 1.5;
color: #464a4c;
background-color: #eceeef;
border: 1px solid rgba(0,0,0,.15);
border-radius: 0 .25rem .25rem 0;
}
.custom-file-control::after {
content: attr(data-content);
}
/** TABLES **/
#percentTable {
height: 150px !important;
width: 100% !important;
}
#percentTable tr {
padding: 5px 2px;
}
#percentTable .no-data-table {
font-size: 10px;
justify-content: center;
align-items: center;
display: flex;
flex: 1;
height: 100%;
font-weight: 100;
}
hr {
margin-top: 15px !important;
margin-bottom: 15px !important;
}
#info-graph {
width: 70% !important;
}
.logo {
margin-top: -40px;
position: absolute;
right: 15px;
}
/** SLIDER **/
.speed-slider,
.link-distance-slider {
padding: 0 10px !important;
margin-top: 5px !important;
width: 100% !important;
}
.slider {
width: 100% !important;
}
.slider .slider-selection {
background-image: linear-gradient(to bottom,
rgba(36, 110, 162, 0.5) 0%,
rgba(3, 169, 224, 0.5) 100%) !important;
}
.slider-disabled .slider-selection {
opacity: 0.5;
}
.slider.slider-disabled .slider-track {
cursor: default !important;
}
table#speed,
table#link-distance {
width: 100%;
}
table#speed .min,
table#speed .max,
table#link-distance .min,
table#link-distance .max {
font-weight: normal !important;
}
/* Console */
#update, .console, .soil_logo {
padding: 10px 15px;
height: 135px;
border: 1px solid #585858;
}
#update {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.container-fluid.fixed {
padding-top: 15px;
}
.console {
background-color: rgb(88,88,88);
font-family: "Ubuntu Mono";
font-size: 14px;
font-weight: 500;
color: white;
line-height: 14px;
overflow: auto;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
width: 100%;
}
.console::-webkit-scrollbar {
width: 6px;
background-color: #F5F5F5;
}
.console::-webkit-scrollbar-thumb {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: #555;
}
/** FORMS **/
.checkbox {
margin-left: 10px !important;
}
#wrapper-settings {
padding: 15px !important;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
}
#wrapper-settings.none {
font-weight: bold;
display: flex;
flex: 1;
justify-content: center;
align-items: center;
}
#wrapper-settings.none:before {
content: 'No configuration provided';
}
#wrapper-settings .btn-group button:focus {
background: initial;
border-color: #ccc;
}
#wrapper-settings .btn-group button {
font-size: xx-small;
padding: 3px 6px;
}
.item.settings .container-fluid {
padding-top: 10px !important;
}
#wrapper-settings::-webkit-scrollbar {
width: 6px;
background-color: #F5F5F5;
}
#wrapper-settings::-webkit-scrollbar-thumb {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: #ccc;
}
/** CHARTS **/
#charts {
height: 100%;
padding-left: 0 !important;
padding-top: 15px !important;
padding-bottom: 15px !important;
}
.chart {
height: 50%;
}
.chart.no-data:before {
content: 'No data';
position: absolute;
font-size: 10px;
padding-bottom: 35px;
}
.chart.no-data {
font-weight: bold;
display: flex;
flex: 1;
justify-content: center;
align-items: center;
}
/** MODAL **/
.modal-footer,
.modal-header {
border: none !important;
}

View File

@@ -0,0 +1,72 @@
#slider3 {
margin: 0 0 10px 0;
}
.d3-slider {
position: relative;
font-family: Verdana,Arial,sans-serif;
font-size: 1.1em;
border: 1px solid #aaaaaa;
z-index: 2;
}
.d3-slider-horizontal {
height: 40px;
}
.d3-slider-range {
background:#2980b9;
left:0px;
right:0px;
height: 0.8em;
position: absolute;
}
.d3-slider-handle {
position: absolute;
width: .8em;
height: 48px;
border: 1px solid #d3d3d3;
border-radius: 4px;
background: #eee;
background: linear-gradient(to bottom, #eee 0%, #ddd 100%);
z-index: 3;
}
.d3-slider-handle:hover {
border: 1px solid #999999;
}
.d3-slider-horizontal .d3-slider-handle {
top: -.3em;
margin-left: -.4em;
}
.d3-slider-axis {
position: relative;
z-index: 1;
}
.d3-slider-axis-bottom {
top: 38px;
}
.d3-slider-axis-right {
left: .8em;
}
.d3-slider-axis path {
stroke-width: 0;
fill: none;
}
.d3-slider-axis line {
fill: none;
stroke: #aaa;
shape-rendering: crispEdges;
stroke-dasharray: 2;
}
.d3-slider-axis text {
font-size: 11px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 MiB

View File

@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="360"
height="330"
id="svg3752"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="logo_gsi_nuevo.svg"
inkscape:export-filename="/home/cif/GoogleDrive/docs/gsi/corporativo/logos-fondos/logos/logo_gsi_nuevo_740_671.png"
inkscape:export-xdpi="185.6273"
inkscape:export-ydpi="185.6273">
<defs
id="defs3754">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 526.18109 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="744.09448 : 526.18109 : 1"
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
id="perspective3760" />
<inkscape:perspective
id="perspective3730"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<filter
inkscape:collect="always"
id="filter3757"
x="-0.4412556"
width="1.8825113"
y="-0.4412556"
height="1.8825113"
color-interpolation-filters="sRGB">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.91928251"
id="feGaussianBlur3759" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.5"
inkscape:cx="187.12284"
inkscape:cy="78.411806"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1855"
inkscape:window-height="1056"
inkscape:window-x="65"
inkscape:window-y="24"
inkscape:window-maximized="1" />
<metadata
id="metadata3757">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Capa 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-722.36218)">
<g
style="display:inline"
id="g3761"
transform="matrix(2.2932314,0,0,2.2932314,-744.72199,6804.6985)">
<g
transform="translate(-161.76758,3.3349672)"
id="g2940">
<g
id="g2922">
<g
transform="translate(-79.72168,-3162.9998)"
style="font-size:40px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;fill:#00a9e0;fill-opacity:1;stroke:none;font-family:Calibri;-inkscape-font-specification:Calibri Bold"
id="flowRoot4140">
<path
d="m 688.88086,587.87773 c -6e-5,3.71096 -0.70806,6.98244 -2.12402,9.81446 -1.36724,2.83204 -3.32037,5.2002 -5.85938,7.10449 -2.53911,1.9043 -5.54203,3.32031 -9.00879,4.24805 -3.46683,0.97656 -7.22659,1.46484 -11.2793,1.46484 -2.44143,0 -4.78517,-0.19531 -7.03125,-0.58594 -2.19728,-0.34179 -4.17482,-0.78125 -5.93261,-1.31836 -1.75783,-0.58593 -3.22267,-1.17187 -4.39453,-1.75781 -1.17189,-0.58593 -2.02638,-1.12304 -2.56348,-1.61133 -0.53712,-0.5371 -0.95215,-1.34277 -1.24512,-2.41699 -0.29297,-1.12304 -0.43946,-2.75878 -0.43945,-4.90723 -10e-6,-1.416 0.0488,-2.53904 0.14648,-3.36914 0.0977,-0.87889 0.24414,-1.56248 0.43946,-2.05078 0.1953,-0.53709 0.43944,-0.87889 0.73242,-1.02539 0.29296,-0.19529 0.65917,-0.29295 1.09863,-0.29297 0.5371,2e-5 1.31835,0.3174 2.34375,0.95215 1.07421,0.58595 2.39257,1.24513 3.95508,1.97754 1.56248,0.73244 3.36912,1.41603 5.41992,2.05078 2.09959,0.63478 4.46775,0.95216 7.10449,0.95215 1.66013,10e-6 3.12497,-0.17089 4.39454,-0.5127 1.31832,-0.34178 2.44137,-0.83006 3.36914,-1.46484 0.97652,-0.63475 1.70894,-1.44041 2.19726,-2.41699 0.48825,-0.97655 0.73239,-2.09959 0.73242,-3.36914 -3e-5,-1.46482 -0.4639,-2.70994 -1.3916,-3.73535 -0.87894,-1.0742 -2.07523,-2.00193 -3.58887,-2.78321 -1.46487,-0.78122 -3.14944,-1.51364 -5.05371,-2.19726 -1.85549,-0.68357 -3.7842,-1.4404 -5.78613,-2.27051 -1.95314,-0.83005 -3.88185,-1.78219 -5.78613,-2.85645 -1.85548,-1.07418 -3.54005,-2.39254 -5.05371,-3.95507 -1.46486,-1.56246 -2.66114,-3.44235 -3.58887,-5.63965 -0.87891,-2.19722 -1.31837,-4.83394 -1.31836,-7.91016 -10e-6,-3.12494 0.61035,-5.98139 1.83106,-8.56933 1.22069,-2.63666 2.9785,-4.88275 5.27343,-6.73829 2.29491,-1.8554 5.07811,-3.29582 8.34961,-4.32128 3.32029,-1.02532 7.03122,-1.53802 11.13281,-1.53809 2.05075,7e-5 4.02829,0.14656 5.93262,0.43945 1.95309,0.29304 3.7109,0.65925 5.27344,1.09864 1.56245,0.43952 2.88081,0.9278 3.95508,1.46484 1.07417,0.48835 1.831,0.9278 2.27051,1.31836 0.48823,0.34186 0.83002,0.70807 1.02539,1.09863 0.19526,0.34186 0.34174,0.78132 0.43945,1.31836 0.0976,0.48835 0.17085,1.12311 0.21973,1.9043 0.0976,0.73248 0.14643,1.66022 0.14648,2.7832 -5e-5,1.31842 -0.0489,2.39264 -0.14648,3.22266 -0.0489,0.83013 -0.17095,1.48931 -0.36622,1.97754 -0.14653,0.48833 -0.36626,0.83013 -0.65918,1.02539 -0.29301,0.14654 -0.63481,0.21978 -1.02539,0.21972 -0.4395,6e-5 -1.12309,-0.24408 -2.05078,-0.73242 -0.92778,-0.53705 -2.09965,-1.09858 -3.51562,-1.68457 -1.36723,-0.58588 -2.97856,-1.12299 -4.83399,-1.61133 -1.80667,-0.53705 -3.88187,-0.8056 -6.22558,-0.80566 -1.66019,6e-5 -3.10062,0.17096 -4.32129,0.51269 -1.22073,0.34186 -2.22171,0.83014 -3.00293,1.46485 -0.78128,0.63482 -1.36721,1.39166 -1.75781,2.27051 -0.39065,0.83013 -0.58596,1.73345 -0.58594,2.70996 -2e-5,1.51372 0.46384,2.78325 1.3916,3.80859 0.92771,1.02544 2.14841,1.92876 3.66211,2.70996 1.51364,0.7813 3.22262,1.51372 5.12695,2.19727 1.95309,0.68363 3.90622,1.44047 5.85938,2.27051 2.00191,0.78129 3.95503,1.70902 5.85937,2.7832 1.95308,1.07425 3.68648,2.39261 5.2002,3.95508 1.51362,1.56253 2.73432,3.44241 3.66211,5.63964 0.92768,2.14847 1.39154,4.71194 1.3916,7.69043"
style="font-size:150px;fill:#00a9e0;font-family:Calibri;-inkscape-font-specification:Calibri Bold"
id="path3806" />
<path
d="m 721.10742,606.33477 c -3e-5,0.48828 -0.14651,0.92773 -0.43945,1.31836 -0.293,0.34179 -0.80569,0.63476 -1.53809,0.8789 -0.68362,0.24414 -1.61135,0.41504 -2.7832,0.5127 -1.1719,0.14648 -2.66115,0.21972 -4.46777,0.21972 -1.80666,0 -3.29592,-0.0732 -4.46778,-0.21972 -1.17189,-0.0977 -2.12403,-0.26856 -2.85644,-0.5127 -0.68361,-0.24414 -1.17189,-0.53711 -1.46485,-0.8789 -0.29297,-0.39063 -0.43946,-0.83008 -0.43945,-1.31836 l 0,-65.18555 c -10e-6,-0.48821 0.14648,-0.90325 0.43945,-1.24512 0.29296,-0.39055 0.78124,-0.70794 1.46485,-0.95215 0.73241,-0.2929 1.68455,-0.51262 2.85644,-0.65918 1.17186,-0.14641 2.66112,-0.21965 4.46778,-0.21972 1.80662,7e-5 3.29587,0.0733 4.46777,0.21972 1.17185,0.14656 2.09958,0.36628 2.7832,0.65918 0.7324,0.24421 1.24509,0.5616 1.53809,0.95215 0.29294,0.34187 0.43942,0.75691 0.43945,1.24512 l 0,65.18555 m 1.3916,-87.45118 c -3e-5,3.71103 -0.75686,6.2745 -2.2705,7.69043 -1.5137,1.4161 -4.32132,2.12411 -8.42286,2.12403 -4.1504,8e-5 -6.95802,-0.68352 -8.42285,-2.05078 -1.41602,-1.36711 -2.12403,-3.83293 -2.12402,-7.39747 -10e-6,-3.71084 0.73241,-6.27431 2.19726,-7.69042 1.51367,-1.46475 4.34569,-2.19717 8.4961,-2.19727 4.10154,1e-4 6.88474,0.70811 8.34961,2.12402 1.46481,1.36729 2.19723,3.8331 2.19726,7.39746"
style="font-size:150px;fill:#00a9e0;font-family:Calibri;-inkscape-font-specification:Calibri Bold"
id="path3808" />
</g>
<g
transform="matrix(1,0,0,1.1503876,-79.72168,-3243.7762)"
style="font-size:40px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;fill:#00a9e0;fill-opacity:1;stroke:none;font-family:Calibri;-inkscape-font-specification:Calibri Bold"
id="flowRoot2914">
<path
d="m 633.80273,545.54375 c -6e-5,2.34381 -0.26862,4.07721 -0.80566,5.2002 -0.53718,1.1231 -1.19635,1.68462 -1.97754,1.68457 l -7.69043,0 c 1.07416,1.17193 1.831,2.5147 2.27051,4.02832 0.48822,1.46489 0.73236,3.00297 0.73242,4.61425 -6e-5,3.80864 -0.63482,7.20219 -1.9043,10.18067 -1.26958,2.92972 -3.10064,5.41995 -5.49316,7.4707 -2.3438,2.00198 -5.20024,3.54007 -8.56934,4.61426 -3.32035,1.02541 -7.03128,1.53811 -11.13281,1.53808 -2.09964,3e-5 -4.10159,-0.24411 -6.00586,-0.73242 -1.90432,-0.53708 -3.36916,-1.14743 -4.39453,-1.83105 -0.58596,0.63479 -1.12307,1.39162 -1.61133,2.27051 -0.43947,0.87893 -0.6592,1.85549 -0.65918,2.92968 -2e-5,1.41604 0.61033,2.58791 1.83106,3.51563 1.26951,0.87892 3.02732,1.3672 5.27344,1.46484 l 15.89355,0.58594 c 3.71089,0.1465 7.00679,0.68361 9.8877,1.61133 2.92963,0.87892 5.37103,2.14845 7.32421,3.80859 2.00189,1.61134 3.51556,3.56446 4.54102,5.85938 1.07415,2.29492 1.61126,4.90722 1.61133,7.83691 -7e-5,3.22265 -0.70808,6.24999 -2.12403,9.08203 -1.41607,2.88085 -3.5401,5.37108 -6.37207,7.47071 -2.83208,2.09958 -6.39653,3.75974 -10.69336,4.98046 -4.24809,1.22068 -9.22855,1.83103 -14.9414,1.83106 -5.56644,-3e-5 -10.32717,-0.43948 -14.28223,-1.31836 -3.90626,-0.87893 -7.12892,-2.09963 -9.66797,-3.66211 -2.49024,-1.56252 -4.32129,-3.4424 -5.49316,-5.63965 -1.12305,-2.14845 -1.68457,-4.51661 -1.68457,-7.10449 0,-1.61134 0.19531,-3.14942 0.58594,-4.61426 0.43945,-1.46485 1.0498,-2.88086 1.83105,-4.24805 0.83007,-1.31835 1.83105,-2.58788 3.00293,-3.80859 1.17187,-1.2207 2.51464,-2.39257 4.02832,-3.51562 -2.09962,-1.12304 -3.73536,-2.63671 -4.90723,-4.54102 -1.12305,-1.95311 -1.68457,-4.07713 -1.68457,-6.37207 0,-2.88084 0.65918,-5.49314 1.97754,-7.83691 1.31835,-2.39255 3.02734,-4.54099 5.12696,-6.44532 -1.709,-1.70895 -3.07618,-3.75973 -4.10157,-6.15234 -1.02539,-2.39254 -1.53809,-5.37105 -1.53808,-8.93555 -10e-6,-3.80854 0.65917,-7.20209 1.97754,-10.18066 1.36718,-3.02728 3.24706,-5.56634 5.63965,-7.61719 2.39256,-2.09954 5.249,-3.68645 8.56933,-4.76074 3.32029,-1.12298 6.98239,-1.6845 10.98633,-1.68457 2.05075,7e-5 4.00387,0.12214 5.85937,0.36621 1.90426,0.24421 3.66207,0.58601 5.27344,1.02539 l 20.72754,0 c 0.83001,7e-5 1.48919,0.53718 1.97754,1.61133 0.53704,1.07428 0.8056,2.88092 0.80566,5.41992 m -23.65722,15.4541 c -5e-5,-3.51557 -0.97661,-6.24994 -2.92969,-8.20312 -1.95316,-1.95307 -4.71195,-2.92963 -8.27637,-2.92969 -1.80667,6e-5 -3.39358,0.31744 -4.76074,0.95215 -1.36721,0.58599 -2.51467,1.41607 -3.44238,2.49023 -0.87893,1.02545 -1.53811,2.24615 -1.97754,3.66211 -0.43948,1.36724 -0.6592,2.80767 -0.65918,4.32129 -2e-5,3.32036 0.97654,5.95707 2.92969,7.91016 1.95309,1.90433 4.66305,2.85648 8.12988,2.85644 1.85543,4e-5 3.46676,-0.29293 4.83398,-0.8789 1.36715,-0.5859 2.4902,-1.39157 3.36914,-2.417 0.9277,-1.02535 1.61129,-2.19722 2.05079,-3.51562 0.48823,-1.36714 0.73237,-2.78316 0.73242,-4.24805 m 4.32129,52.14844 c -5e-5,-2.19727 -0.87896,-3.88184 -2.63672,-5.05371 -1.75786,-1.17187 -4.17485,-1.80664 -7.25098,-1.9043 l -13.11035,-0.36621 c -1.26956,0.92774 -2.29495,1.83106 -3.07617,2.70996 -0.73245,0.83008 -1.3428,1.63574 -1.83106,2.41699 -0.43947,0.78125 -0.73244,1.53809 -0.8789,2.27051 -0.14651,0.73242 -0.21975,1.48925 -0.21973,2.27051 -2e-5,2.4414 1.22068,4.29686 3.66211,5.56641 2.49021,1.26951 5.98142,1.90428 10.47363,1.90429 2.78317,-10e-6 5.12692,-0.29298 7.03125,-0.8789 1.90426,-0.53713 3.44234,-1.26955 4.61426,-2.19727 1.17183,-0.92774 2.00191,-1.97755 2.49023,-3.14941 0.48824,-1.12306 0.73238,-2.31935 0.73243,-3.58887"
style="font-size:150px;fill:#00a9e0;font-family:Calibri;-inkscape-font-specification:Calibri Bold"
id="path3811" />
</g>
</g>
</g>
<g
transform="translate(-154.76465,-3152.2336)"
style="font-size:42px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;fill:#00629b;fill-opacity:0.98431373;stroke:none;font-family:Calibri;-inkscape-font-specification:AlArabiya Bold"
id="flowRoot3727">
<path
d="m 574.2041,625.37123 c -2e-5,1.62697 -0.23928,3.08302 -0.71777,4.36817 -0.47854,1.28516 -1.18264,2.37207 -2.11231,3.26074 -0.9297,0.88867 -2.07814,1.56543 -3.44531,2.03027 -1.3672,0.46485 -2.93947,0.69727 -4.7168,0.69727 -1.66798,0 -3.16505,-0.20508 -4.49121,-0.61523 -1.32618,-0.42383 -2.44727,-1.05957 -3.36328,-1.90723 -0.91602,-0.84765 -1.62012,-1.90039 -2.1123,-3.1582 -0.47852,-1.27148 -0.71778,-2.75488 -0.71778,-4.4502 l 0,-16.13965 c 0,-0.13669 0.041,-0.25974 0.12305,-0.36914 0.082,-0.10935 0.22558,-0.19821 0.43066,-0.2666 0.21875,-0.0683 0.49902,-0.12302 0.84082,-0.16406 0.3418,-0.041 0.7793,-0.0615 1.3125,-0.0615 0.51953,3e-5 0.95019,0.0205 1.292,0.0615 0.34179,0.041 0.61522,0.0957 0.82031,0.16406 0.20507,0.0684 0.34862,0.15725 0.43066,0.2666 0.0957,0.1094 0.14355,0.23245 0.14356,0.36914 l 0,15.66797 c -10e-6,1.05274 0.12987,1.96876 0.38965,2.74805 0.25975,0.76563 0.62889,1.40137 1.10742,1.90722 0.49218,0.50587 1.07323,0.88868 1.74316,1.14844 0.68358,0.2461 1.44237,0.36915 2.27637,0.36914 0.84764,10e-6 1.60643,-0.12988 2.27637,-0.38965 0.6699,-0.25976 1.23728,-0.63573 1.70215,-1.12793 0.46482,-0.50585 0.82029,-1.12108 1.0664,-1.8457 0.25975,-0.73827 0.38963,-1.57226 0.38965,-2.50195 l 0,-15.97559 c -2e-5,-0.13669 0.041,-0.25974 0.12305,-0.36914 0.082,-0.10935 0.22556,-0.19821 0.43066,-0.2666 0.20506,-0.0683 0.4785,-0.12302 0.82031,-0.16406 0.35545,-0.041 0.79295,-0.0615 1.3125,-0.0615 0.51951,3e-5 0.94334,0.0205 1.27149,0.0615 0.34177,0.041 0.61521,0.0957 0.82031,0.16406 0.20505,0.0684 0.34861,0.15725 0.43066,0.2666 0.082,0.1094 0.12303,0.23245 0.12305,0.36914 l 0,15.91406"
id="path3814" />
<path
d="m 598.11621,616.77846 c -2e-5,1.49025 -0.23244,2.80959 -0.69726,3.95801 -0.46487,1.14845 -1.14163,2.11915 -2.03028,2.91211 -0.88869,0.77931 -1.98244,1.37403 -3.28125,1.78418 -1.28517,0.41016 -2.80274,0.61524 -4.55273,0.61523 l -2.21485,0 0,8.46973 c 0,0.13672 -0.0479,0.25976 -0.14355,0.36914 -0.082,0.10937 -0.22559,0.19824 -0.43067,0.2666 -0.20508,0.0684 -0.47852,0.12305 -0.82031,0.16406 -0.3418,0.041 -0.7793,0.0615 -1.3125,0.0615 -0.51953,0 -0.95703,-0.0205 -1.3125,-0.0615 -0.3418,-0.041 -0.61524,-0.0957 -0.82031,-0.16406 -0.20508,-0.0684 -0.34864,-0.15723 -0.43066,-0.2666 -0.082,-0.10938 -0.12305,-0.23242 -0.12305,-0.36914 l 0,-23.8711 c 0,-0.64255 0.16406,-1.12106 0.49219,-1.43554 0.34179,-0.3281 0.78613,-0.49216 1.333,-0.49219 l 6.25489,0 c 0.62889,3e-5 1.22362,0.0274 1.78418,0.082 0.5742,0.041 1.2578,0.14358 2.05078,0.30762 0.79295,0.15042 1.59276,0.43752 2.39941,0.86133 0.8203,0.42385 1.51756,0.96389 2.0918,1.62011 0.5742,0.64261 1.0117,1.40139 1.3125,2.27637 0.30076,0.86135 0.45115,1.83205 0.45117,2.91211 m -5.63965,0.38965 c -10e-6,-0.92967 -0.16408,-1.69529 -0.49219,-2.29688 -0.32813,-0.60154 -0.73145,-1.04587 -1.20996,-1.333 -0.47852,-0.28709 -0.98438,-0.46483 -1.51757,-0.53321 -0.51955,-0.082 -1.05959,-0.12302 -1.62012,-0.12304 l -2.29688,0 0,9.00293 2.41993,0 c 0.86131,1e-5 1.57908,-0.1162 2.15332,-0.34864 0.58787,-0.2324 1.06639,-0.55369 1.43554,-0.96386 0.36913,-0.42382 0.6494,-0.92284 0.84082,-1.49707 0.19139,-0.58788 0.2871,-1.22362 0.28711,-1.90723"
id="path3816" />
<path
d="m 633.2666,634.51772 c -3e-5,0.13672 -0.0411,0.25976 -0.12305,0.36914 -0.0684,0.10937 -0.20511,0.19824 -0.41015,0.2666 -0.19144,0.0684 -0.45121,0.12305 -0.7793,0.16406 -0.32816,0.041 -0.74515,0.0615 -1.25098,0.0615 -0.49221,0 -0.90237,-0.0205 -1.23046,-0.0615 -0.32816,-0.041 -0.58792,-0.0957 -0.7793,-0.16406 -0.19144,-0.0684 -0.32815,-0.15723 -0.41016,-0.2666 -0.0821,-0.10938 -0.12307,-0.23242 -0.12304,-0.36914 l 0,-21.59473 -0.041,0 -7.69043,21.57422 c -0.0547,0.17774 -0.14357,0.32813 -0.2666,0.45117 -0.12307,0.10938 -0.29397,0.19825 -0.5127,0.2666 -0.20509,0.0684 -0.4717,0.10938 -0.7998,0.12305 -0.32814,0.0273 -0.72463,0.041 -1.18945,0.041 -0.46487,0 -0.86135,-0.0205 -1.18946,-0.0615 -0.32814,-0.0273 -0.60158,-0.0752 -0.82031,-0.14355 -0.20509,-0.082 -0.36916,-0.17774 -0.49219,-0.28711 -0.12306,-0.10938 -0.20509,-0.23926 -0.24609,-0.38965 l -7.42383,-21.57422 -0.041,0 0,21.59473 c -1e-5,0.13672 -0.041,0.25976 -0.12305,0.36914 -0.0684,0.10937 -0.20509,0.19824 -0.41016,0.2666 -0.20508,0.0684 -0.47168,0.12305 -0.7998,0.16406 -0.31446,0.041 -0.72462,0.0615 -1.23047,0.0615 -0.49219,0 -0.90235,-0.0205 -1.23047,-0.0615 -0.32813,-0.041 -0.59473,-0.0957 -0.7998,-0.16406 -0.19141,-0.0684 -0.32813,-0.15723 -0.41016,-0.2666 -0.0684,-0.10938 -0.10254,-0.23242 -0.10254,-0.36914 l 0,-23.64551 c 0,-0.69724 0.18457,-1.23044 0.55371,-1.59961 0.36914,-0.36911 0.86133,-0.55368 1.47656,-0.55371 l 3.52735,0 c 0.62889,3e-5 1.16893,0.0547 1.62011,0.16406 0.45117,0.0957 0.84081,0.26663 1.16895,0.5127 0.32811,0.23245 0.60155,0.5469 0.82031,0.94336 0.21874,0.38283 0.41015,0.86135 0.57422,1.43554 l 5.74219,15.81153 0.082,0 5.94727,-15.77051 c 0.17771,-0.57419 0.36911,-1.05955 0.57421,-1.45605 0.21873,-0.39646 0.46482,-0.71775 0.73829,-0.96387 0.28708,-0.24607 0.62204,-0.41697 1.00488,-0.5127 0.38278,-0.10935 0.82712,-0.16403 1.33301,-0.16406 l 3.62988,0 c 0.36911,3e-5 0.68356,0.0479 0.94336,0.14356 0.2734,0.0957 0.49215,0.23928 0.65625,0.43066 0.1777,0.17776 0.30758,0.40335 0.38965,0.67676 0.0957,0.25979 0.14352,0.56057 0.14355,0.90234 l 0,23.64551"
id="path3818" />
</g>
<path
transform="matrix(1.6,0,0,1.6,-233,-3460.4252)"
style="fill:#ffffff;fill-opacity:0.98431373;filter:url(#filter3757)"
d="m 445,510.49219 c 0,1.38071 -1.11929,2.5 -2.5,2.5 -1.38071,0 -2.5,-1.11929 -2.5,-2.5 0,-1.38071 1.11929,-2.5 2.5,-2.5 1.38071,0 2.5,1.11929 2.5,2.5 z"
id="path3735" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" ?><svg height="1792" viewBox="0 0 1792 1792" width="1792" xmlns="http://www.w3.org/2000/svg"><path d="M1472 992v480q0 26-19 45t-45 19h-384v-384h-256v384h-384q-26 0-45-19t-19-45v-480q0-1 .5-3t.5-3l575-474 575 474q1 2 1 6zm223-69l-62 74q-8 9-21 11h-3q-13 0-21-7l-692-577-692 577q-12 8-24 7-13-2-21-11l-62-74q-8-10-7-23.5t11-21.5l719-599q32-26 76-26t76 26l244 204v-195q0-14 9-23t23-9h192q14 0 23 9t9 23v408l219 182q10 8 11 21.5t-7 23.5z"/></svg>

After

Width:  |  Height:  |  Size: 462 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg height="512px" id="Layer_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512" width="512px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M448,448c0,0,0-26.4-2.2-40.2c-1.8-10.9-16.9-25.3-81.1-48.9c-63.2-23.2-59.3-11.9-59.3-54.6c0-27.7,14.1-11.6,23.1-64.2 c3.5-20.7,6.3-6.9,13.9-40.1c4-17.4-2.7-18.7-1.9-27c0.8-8.3,1.6-15.7,3.1-32.7C345.4,119.3,325.9,64,256,64 c-69.9,0-89.4,55.3-87.5,76.4c1.5,16.9,2.3,24.4,3.1,32.7c0.8,8.3-5.9,9.6-1.9,27c7.6,33.1,10.4,19.3,13.9,40.1 c9,52.6,23.1,36.5,23.1,64.2c0,42.8,3.9,31.5-59.3,54.6c-64.2,23.5-79.4,38-81.1,48.9C64,421.6,64,448,64,448h192H448z"/></svg>

After

Width:  |  Height:  |  Size: 812 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.0//EN' 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd'><svg enable-background="new 0 0 91.8 92.6" id="Layer_1" version="1.0" viewBox="0 0 91.8 92.6" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M46.3,3.6c-23.5,0-42.5,19-42.5,42.5s19,42.5,42.5,42.5c23.5,0,42.5-19,42.5-42.5S69.8,3.6,46.3,3.6z M72.8,52.9H53v19.8c0,2-1.6,3.6-3.6,3.6h-6.2c-2,0-3.6-1.6-3.6-3.6V52.9H19.8c-2,0-3.6-1.6-3.6-3.6v-6.2c0-2,1.6-3.6,3.6-3.6h19.8 V19.7c0-2,1.6-3.6,3.6-3.6h6.2c2,0,3.6,1.6,3.6,3.6v19.8h19.8c2,0,3.6,1.6,3.6,3.6v6.2C76.4,51.2,74.8,52.9,72.8,52.9z" fill="#1E1E1E"/></svg>

After

Width:  |  Height:  |  Size: 697 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" ?><svg height="16px" version="1.1" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><defs/><g fill="none" fill-rule="evenodd" id="Icons with numbers" stroke="none" stroke-width="1"><g fill="#000000" id="Group" transform="translate(-192.000000, -192.000000)"><path d="M201,205.917042 C203.512502,205.49553 205.495527,203.512505 205.917042,201 L203,201 L203,199 L205.917042,199 C205.495527,196.487495 203.512502,194.50447 201,194.082958 L201,197 L199,197 L199,194.082958 C196.487498,194.50447 194.504473,196.487495 194.082958,199 L197,199 L197,201 L194.082958,201 C194.504473,203.512505 196.487498,205.49553 199,205.917042 L199,203 L201,203 Z M200,208 C195.581722,208 192,204.418278 192,200 C192,195.581722 195.581722,192 200,192 C204.418278,192 208,195.581722 208,200 C208,204.418278 204.418278,208 200,208 Z M200,208" id="Oval 163"/></g></g></svg>

After

Width:  |  Height:  |  Size: 992 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 64 64" height="64px" id="Layer_1" version="1.1" viewBox="0 0 64 64" width="64px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g><path d="M52.419,15.975c0,0,1.013,1.019,1.727,0.002l1.363-1.953c0.476-0.687-0.139-1.162-0.202-1.209 l-8.265-5.775H47.04c-0.509-0.354-0.847-0.139-1.024,0.06l-0.148,0.213l-1.259,1.802c-0.006,0.007-0.71,1.119,0.416,1.707v0.001 c1.61,0.792,4.563,2.462,7.392,5.158L52.419,15.975z" fill="#241F20"/></g><g><path d="M38.512,0.071H25.488c-1.011,0-1.839,0.812-1.839,1.839v1.518c0,1.026,0.828,1.854,1.839,1.854h0.644 v1.072c0.001,1.541,0.974,1.669,1.462,1.636c0.083-0.012,0.169-0.025,0.26-0.037c0.001,0,0.013-0.003,0.013-0.003L27.866,7.95 c1.734-0.237,4.605-0.464,7.898-0.045l0.002-0.003c0,0,2.109,0.391,2.103-1.549V5.281h0.644c1.012,0,1.839-0.827,1.839-1.854V1.91 C40.351,0.884,39.523,0.071,38.512,0.071z" fill="#241F20"/></g><path d="M32,10.301c-14.808,0-26.812,12.005-26.812,26.815c0,14.807,12.004,26.812,26.812,26.812 c14.809,0,26.812-12.006,26.812-26.812C58.812,22.306,46.809,10.301,32,10.301z M33.717,37.108 c-1.575,0.002-1.709-1.094-1.717-1.41V17.155c0.046-0.645,0.381-1.86,2.248-1.546c0.037,0.005,0.072,0.009,0.111,0.014 c0.12,0.02,0.233,0.036,0.32,0.043c5.44,0.764,17.373,4.302,18.864,20.343c-0.042,0.446-0.295,1.096-1.412,1.103 C42.529,37.085,36.454,37.097,33.717,37.108z" fill="#241F20"/></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

461
soil/web/static/js/socket.js Executable file
View File

@@ -0,0 +1,461 @@
// Open the websocket connection
var ws = new WebSocket((window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host + '/ws');
// Open conection with Socket
ws.onopen = function() {
console.log('Connection opened!');
};
// Receive data from server
ws.onmessage = function(message) {
//console.log('Message received!');
var msg = JSON.parse(message.data);
switch(msg['type']) {
case 'trials':
reset_trials();
set_trials(msg['data']);
// $('#load').removeClass('loader');
break;
case 'get_trial':
console.log(msg['data']);
self.GraphVisualization.import(convertJSON(msg['data']), function() {
reset_configuration();
set_configuration();
// $('#home_menu').click(function() {
// setTimeout(function() {
// reset_timeline();
// set_timeline(msg['data']);
// }, 1000);
// });
reset_timeline();
set_timeline(msg['data']);
$('#load').hide();
});
$('#charts .chart').removeClass('no-data');
set_chart_nodes(msg['data'], chart_nodes)
set_chart_attrs(msg['data'], chart_attrs, $('.config-item #properties').val())
$('.config-item #properties').change(function() {
chart_attrs.destroy();
chart_attrs = create_chart(width_chart, height_chart, 'Time', 'Attributes', '#chart_attrs');
set_chart_attrs(msg['data'], chart_attrs, $('.config-item #properties').val())
});
break;
case 'settings':
$('#wrapper-settings').empty().removeClass('none');
initGUI(msg['data']);
break;
case 'error':
console.error(msg['error']);
_socket.error(msg['error']);
$('#load').removeClass('loader');
break;
case 'log':
$('.console').append('$ ' + msg['logger'] + ': ' + msg['logging'] + '<br/>');
$('.console').animate({ scrollTop: $('.console')[0].scrollHeight }, 'fast');
break;
case 'visualization_params':
console.log(msg['data']);
self.GraphVisualization.set_params(msg['data']['shape_property'], msg['data']['shapes'], msg['data']['colors']);
if ( msg['data']['background_image'] ) {
// $('svg#graph').css('background-image', 'linear-gradient(to bottom, rgba(0,0,0,0.4) 0%,rgba(0,0,0,0.4) 100%), url(img/background/' + msg['data']['background_image'])
// .css('background-size', '130%').css('background-position', '5% 30%').css('background-repeat', 'no-repeat');
$('<style>').text('svg line.link { stroke: white !important; stroke-width: 1.5px !important; }').appendTo($('html > head'));
$('<style>').text('svg circle.node { stroke-width: 2.5px !important; }').appendTo($('html > head'));
self.GraphVisualization.set_background('img/background/' + msg['data']['background_image'], msg['data']['background_opacity'], msg['data']['background_filter_color']);
}
break;
case 'download_gexf':
var xml_declaration = '<?xml version="1.0" encoding="utf-8"?>';
download(msg['filename'] + '.gexf', 'xml', xml_declaration + msg['data']);
break;
case 'download_json':
download(msg['filename'] + '.json', 'json', JSON.stringify(msg['data'], null, 4));
break;
default:
console.warn('Unexpected message!')
}
}
var _socket = {
send: function(message, type) {
var json = {}
json['type'] = type
json['data'] = message
ws.send(JSON.stringify(json))
},
error: function(message) {
$('#error-message').text(message);
$('.alert.alert-danger').show();
},
current_trial: undefined
};
var set_trials = function(trials) {
for ( i in trials ) {
var list_item = $('<li>').appendTo('.dropdown#trials .dropdown-menu');
$('<a>').val(i).text(trials[i]).appendTo(list_item);
}
// Select 'trials'
$('.dropdown#trials li a').click(function() {
var a = $('.dropdown-toggle .caret');
$('.dropdown-toggle').text($(this).text() + ' ').append(a);
_socket.send($(this).val(), 'get_trial');
_socket.current_trial = $(this).val();
});
// Request first trial as default
_socket.send(0, 'get_trial')
_socket.current_trial = 0
};
var reset_trials = function() {
// 'Trials' selector
$('.dropdown-menu').empty();
var a = $('.dropdown-toggle .caret');
$('.dropdown-toggle').text('Trials ').append(a);
}
var convertJSON = function(json) {
// For NetworkX Geometric Graphs get positions
json.nodes.forEach(function(node) {
var scx = d3.scale.linear().domain([0, 1]).range([0, width]);
var scy = d3.scale.linear().domain([0, 1]).range([width, 0]);
if ( node.pos ) {
node.scx = scx(node.pos[0]);
node.scy = scy(node.pos[1]);
}
delete node.pos;
});
json.links.forEach(function(link) {
link.source = json.nodes[link.source]
link.target = json.nodes[link.target]
});
// Fix spells for nodes
json.nodes.forEach(function(node) {
for (i in node.spells) {
if (node.spells[i][0] > node.spells[i][1]) {
aux = node.spells[i][0];
node.spells[i][0] = node.spells[i][1];
node.spells[i][1] = aux;
}
}
});
return json;
}
var update_statistics_table = function() {
$('#percentTable tbody').empty()
var statisticsSorted = Object.keys(self.GraphVisualization.statistics).sort(function(a,b) {
return self.GraphVisualization.statistics[b] - self.GraphVisualization.statistics[a];
});
for ( var i in statisticsSorted ) {
if ( i <= 5 ) {
// Draw table
var appendTo = '#percentTable > tbody tr:nth-child(' + Number(parseInt(i) + 1) + ')';
var propertyName = (statisticsSorted[i].includes('class')) ?
statisticsSorted[i].split('.').pop().split('\'')[0] : statisticsSorted[i];
$('<tr>').addClass('col-sm-12').appendTo('#percentTable > tbody');
$('<td>').css('background-color', self.GraphVisualization.color($('.config-item #properties').val(), statisticsSorted[i])).addClass('col-sm-1').appendTo(appendTo);
$('<td>').addClass('text-left col-sm-4').text(self.GraphVisualization.statistics[statisticsSorted[i]] + ' %').appendTo(appendTo);
$('<td>').addClass('text-right col-sm-6 property-name').text(propertyName).appendTo(appendTo);
}
}
}
var set_configuration = function() {
// Number of nodes and links info table
$('<tr>').appendTo('#info-graph > tbody');
$('<th>').text('Nodes:').appendTo('#info-graph > tbody tr:nth-child(1)');
$('<th>').text(self.GraphVisualization.nodes).addClass('text-right').appendTo('#info-graph > tbody tr:nth-child(1)');
$('<tr>').appendTo('#info-graph > tbody');
$('<th>').text('Links:').appendTo('#info-graph > tbody tr:nth-child(2)');
$('<th>').text(self.GraphVisualization.links).addClass('text-right').appendTo('#info-graph > tbody tr:nth-child(2)');
// Options of 'Select'
for ( var i in self.GraphVisualization.model['dynamic'] ) {
$('<option>').val(self.GraphVisualization.model['dynamic'][i].title)
.text(self.GraphVisualization.model['dynamic'][i].title).appendTo('#properties-dynamic');
}
for ( var i in self.GraphVisualization.model['static'] ) {
$('<option>').val(self.GraphVisualization.model['static'][i].title)
.text(self.GraphVisualization.model['static'][i].title).appendTo('#properties-static');
}
// Hide optgroups if they are empty
if ( $('#properties-dynamic').children().length === 0 ) $('#properties-dynamic').hide();
if ( $('#properties-static').children().length === 0 ) $('#properties-static').hide();
update_statistics_table();
// Enable 'Link Distance' slider
$('#link-distance-slider').slider('enable').on('change', function(value) {
self.GraphVisualization.set_link_distance(value.value.newValue);
});
// Enable 'Run configuration' button
$('#run_simulation').attr('data-toggle', 'modal').attr('data-target', '#simulation_modal');
// Enable 'Download' buttons
$('#download_modal .btn-success').prop('disabled', false);
$('#download_gexf').on('click', function() {
_socket.send(_socket.current_trial, 'download_gexf')
});
$('#download_json').on('click', function() {
_socket.send(_socket.current_trial, 'download_json')
});
}
var reset_configuration = function() {
// Information table about the graph
$('#info-graph > tbody').empty();
// 'Select' for properties
$('#properties-dynamic').empty().show();
$('#properties-static').empty().show();
// 'Link Distance' slider
$('#link-distance-slider').slider('disable').slider('setValue', 30);
// 'Download' buttons
$('#download_gexf').off();
$('#download_json').off();
}
var slider;
var set_timeline = function(graph) {
// 'Timeline' slider
var [min, max] = get_limits(graph);
var stepUnix = 1;
var minUnix = (min !== Math.min()) ? min : 0;
var maxUnix = (max !== Math.max()) ? max : minUnix + 20;
slider = d3.slider();
d3.select('#slider3').attr('width', width).call(
slider.axis(true).min(minUnix).max(maxUnix).step(stepUnix).value(minUnix)
.on('slide', function(evt, value) {
self.GraphVisualization.update_graph($('.config-item #properties').val(), value, function() {
update_statistics_table();
});
})
);
// Draw graph for the first time
self.GraphVisualization.update_graph($('.config-item #properties').val(), maxUnix, function() {
update_statistics_table();
setTimeout(function() {
self.GraphVisualization.fit();
if ( $('svg #root > image').length !== 0 ) {
$('svg #root > image').attr('height', d3.select('#root').node().getBBox().height * 1.2);
var dx = d3.select('#graph-wrapper').node().getBBox().width - d3.select('svg #root > image').node().getBBox().width;
var dy = d3.select('#graph-wrapper').node().getBBox().height - d3.select('svg #root > image').node().getBBox().height;
$('svg #root > image').attr('transform', 'translate(' + (dx / 2) + ',' + (dy / 2) + ')');
$('svg #root > rect').attr('transform', 'translate(' + (dx / 2) + ',' + (dy / 2) + ')')
.attr('width', d3.select('svg #root > image').node().getBBox().width)
.attr('height', d3.select('svg #root > image').node().getBBox().height);
}
}, 1000);
});
// 'Speed' slider
$('#speed-slider').slider('enable').on('change', function(value) {
speed = value.value.newValue;
});
// Button 'Play'
$('button#button_play').on('click', function() {
play();
});
// Button 'Pause'
$('button#button_pause').on('click', function() {
stop();
$('button#button_play').removeClass('pressed').prop("disabled", false);
});
// Button 'Zoom to Fit'
$('button#button_zoomFit').click(function() { self.GraphVisualization.fit(); });
}
var player;
function play(){
$('button#button_play').addClass('pressed').prop("disabled", true);
if (slider.value() >= slider.max()) {
slider.value(slider.min());
}
var FRAME_INTERVAL = 100;
var speed_ratio = FRAME_INTERVAL / 1000 // speed=1 => 1 step per second
nextStep = function() {
newvalue = Math.min(slider.value() + speed*speed_ratio, slider.max());
console.log("new time value", newvalue);
slider.value(newvalue);
self.GraphVisualization.update_graph($('.config-item #properties').val(), slider.value(), function () {
update_statistics_table();
});
if (newvalue < slider.max()) {
player = setTimeout(nextStep, FRAME_INTERVAL);
} else {
$('button#button_play').removeClass('pressed').prop("disabled", false);
}
}
player = setTimeout(nextStep, FRAME_INTERVAL);
}
function stop() {
clearTimeout(player);
}
var reset_timeline = function() {
// 'Timeline' slider
$('#slider3').html('');
// 'Speed' slider
// $('#speed-slider').slider('disable').slider('setValue', 1000);
// Buttons
stop();
$('button#button_play').off().removeClass('pressed').prop("disabled", false);
$('button#button_pause').off();
$('button#button_zoomFit').off();
}
var get_limits = function(graph) {
var max = Math.max();
var min = Math.min()
graph.links.forEach(function(link) {
if (link.end > max) max = link.end
if (link.start > max) max = link.start
if (link.end < min) min = link.end
if (link.start < min) min = link.start
});
graph.nodes.forEach(function(node) {
for (property in node) {
if ( Array.isArray(node[property]) ) {
for (i in node[property]) {
for (j in node[property][i]) {
if (node[property][i][j] > max) max = node[property][i][j];
if (node[property][i][j] < min) min = node[property][i][j];
}
}
}
}
})
return [min, max];
}
var set_chart_nodes = function(graph, chart) {
var [min, max] = get_limits(graph);
var data = ['nodes']
for (var i = min; i <= max; i++) {
data.push(this.GraphVisualization.get_nodes(i));
}
chart.load({
unload: true,
columns: [data]
});
}
var set_chart_attrs = function(graph, chart, property) {
var [min, max] = get_limits(graph);
var data_tmp = {}
for (var i = min; i <= max; i++) {
this.GraphVisualization.get_attributes(property, i, function(object) {
for (var value in object) {
if (!data_tmp[value]) {
var time = 0
for (var done in data_tmp)
time = (data_tmp[done].length > time) ? data_tmp[done].length - 1 : time
data_tmp[value] = Array(time).fill(0);
}
data_tmp[value].push(object[value]);
}
});
}
var data = $.map(data_tmp, function(value, index) {
value.splice(0,0,index);
return [value];
});
chart.load({
unload: true,
columns: data
});
chart.axis.labels({y: property});
}
var create_chart = function(width, height, label_x, label_y, bind_to) {
return c3.generate({
size: {
width: width,
height: height
},
data: {
columns: [],
type: 'area-spline'
},
axis: {
x: { label: label_x },
y: { label: label_y }
},
point: { show: false },
bindto: bind_to
});
}
var run_simulation = function() {
var environment_variables = {}
$('#wrapper-settings input').each(function() {
switch(this.type) {
case 'text':
environment_variables[this.id] = Number(this.value);
break;
case 'checkbox':
environment_variables[this.id] = ($(this).is(':checked')) ? true : false;
break;
case 'number':
environment_variables[this.id] = Number(this.value);
break;
default:
console.warn(this.id + ' not defined when running simulation!');
break;
}
});
return environment_variables;
}
var download = function(filename, filetype, content) {
var file = document.createElement('a');
file.setAttribute('href', 'data:text/' + filetype + ';charset=utf-8,' + encodeURIComponent(content));
file.setAttribute('download', filename);
file.click();
delete file;
}

View File

@@ -0,0 +1,99 @@
// Add model parameters that can be edited prior to a model run
var initGUI = function(model_params) {
var addBooleanInput = function(name, value) {
var checked = (value) ? 'checked' : 'value';
var wrapper = $('<div>').attr('class', 'col-sm-6').height('110px');
var input_group = $('<div>').attr('class', 'input-group').appendTo(wrapper);
var label = $('<label>').attr('for', name).attr('class', 'checkbox').appendTo(input_group);
var input = $('<input>') .attr('class', 'form-check-input').attr('id', name).attr('type', 'checkbox').attr(checked, checked).appendTo(label);
input.after(name);
$('#wrapper-settings').append(wrapper);
};
var addSliderInput = function(name, value) {
var wrapper = $('<div>').attr('class', 'col-sm-6').height('110px');
var label = $('<div>').width('100%').text(name).css('text-align', 'center').css('font-weight', 'bolder').appendTo(wrapper);
var input = $('<input>').attr('id', name).attr('type', 'text').attr('data-slider-min', '0.001').attr('data-slider-max', '1').attr('data-slider-step', '0.001').attr('data-slider-value', value).attr('data-slider-tooltip', 'hide').css('padding', '0 10px').appendTo(wrapper);
var span = $('<div>').attr('id', name + '_value').text('Current value: ').width('100%').css('padding-top', '10px').appendTo(wrapper);
var current_value = $('<span>').attr('id', name + '_number').text(value).appendTo(span);
var button_group = $('<div>').attr('class', 'btn-group').attr('role', 'group').css('position', 'absolute').css('right', '15px').appendTo(span);
var button_down = $('<button>').attr('type', 'button').attr('class', 'btn btn-default btn-default-down').appendTo(button_group);
var button_up = $('<button>').attr('type', 'button').attr('class', 'btn btn-default btn-default-down').appendTo(button_group);
$('<span>').attr('class', 'glyphicon glyphicon-chevron-down').attr('aria-hidden', 'true').appendTo(button_down);
$('<span>').attr('class', 'glyphicon glyphicon-chevron-up').attr('aria-hidden', 'true').appendTo(button_up);
$('#wrapper-settings').append(wrapper);
input.slider().on('change', function(slideEvt) {
current_value.text(slideEvt.value.newValue);
});
var timeout, interval;
button_down.on('mousedown', function() {
input.slider('setValue', input.slider('getValue') - 0.001);
current_value.text(input.slider('getValue'));
timeout = setTimeout(function() {
interval = setInterval(function() {
input.slider('setValue', input.slider('getValue') - 0.001);
current_value.text(input.slider('getValue'));
}, 30);
}, 500);
});
button_down.on('mouseup', function() {
clearTimeout(timeout);
clearInterval(interval);
});
button_up.on('mousedown', function() {
input.slider('setValue', input.slider('getValue') + 0.001);
current_value.text(input.slider('getValue'));
timeout = setTimeout(function() {
interval = setInterval(function() {
input.slider('setValue', input.slider('getValue') + 0.001);
current_value.text(input.slider('getValue'));
}, 30);
}, 500);
});
button_up.on('mouseup', function() {
clearTimeout(timeout);
clearInterval(interval);
});
};
var addNumberInput = function(name, value) {
var wrapper = $('<div>').attr('class', 'col-sm-6').height('110px');
var label = $('<div>').width('100%').text(name).css('text-align', 'center').css('font-weight', 'bolder').appendTo(wrapper);
var input = $('<input>').attr('id', name).attr('type', 'number').attr('class', 'form-control').attr('value', value).attr('min', 0).css('margin-top', '18px').appendTo(wrapper);
$('#wrapper-settings').append(wrapper);
}
var addTextBox = function(param, obj) {
var well = $('<div class="well">' + obj.value + '</div>')[0];
sidebar.append(well);
};
for (var option in model_params) {
var type = typeof(model_params[option]);
var param_str = String(option);
switch (model_params[option]['type']) {
case 'boolean':
addBooleanInput(model_params[option]['label'], model_params[option]['value']);
break;
case 'number':
addSliderInput(model_params[option]['label'], model_params[option]['value']);
break;
case 'great_number':
addNumberInput(model_params[option]['label'], model_params[option]['value']);
break;
default:
console.warn(model_params[option]['label'] + ' not defined!');
break;
}
}
};

View File

@@ -0,0 +1,429 @@
/*
D3.js Slider
Inspired by jQuery UI Slider
Copyright (c) 2013, Bjorn Sandvik - http://blog.thematicmapping.org
BSD license: http://opensource.org/licenses/BSD-3-Clause
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['d3'], factory);
} else if (typeof exports === 'object') {
if (process.browser) {
// Browserify. Import css too using cssify.
require('./d3.slider.css');
}
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory(require('d3'));
} else {
// Browser globals (root is window)
root.d3.slider = factory(root.d3);
}
}(this, function (d3) {
return function module() {
"use strict";
// Public variables width default settings
var min = 0,
max = 100,
step = 0.01,
animate = true,
orientation = "horizontal",
axis = false,
margin = 50,
value,
active = 1,
snap = false,
scale;
// Private variables
var axisScale,
dispatch = d3.dispatch("slide", "slideend"),
formatPercent = d3.format(".2%"),
tickPadding = 5,
tickFormat = d3.format(".0"),
handle1,
handle2 = null,
divRange,
sliderLength;
function slider(selection) {
selection.each(function() {
// Create scale if not defined by user
if (!scale) {
scale = d3.scale.linear().domain([min, max]);
}
// Start value
value = value || scale.domain()[0];
// DIV container
var div = d3.select(this).classed("d3-slider d3-slider-" + orientation, true);
var drag = d3.behavior.drag();
drag.on('dragend', function () {
dispatch.slideend(d3.event, value);
})
// Slider handle
//if range slider, create two
// var divRange;
if (toType(value) == "array" && value.length == 2) {
handle1 = div.append("a")
.classed("d3-slider-handle", true)
.attr("xlink:href", "#")
.attr('id', "handle-one")
.on("click", stopPropagation)
.call(drag);
handle2 = div.append("a")
.classed("d3-slider-handle", true)
.attr('id', "handle-two")
.attr("xlink:href", "#")
.on("click", stopPropagation)
.call(drag);
} else {
handle1 = div.append("a")
.classed("d3-slider-handle", true)
.attr("xlink:href", "#")
.attr('id', "handle-one")
.on("click", stopPropagation)
.call(drag);
}
// Horizontal slider
if (orientation === "horizontal") {
div.on("click", onClickHorizontal);
if (toType(value) == "array" && value.length == 2) {
divRange = d3.select(this).append('div').classed("d3-slider-range", true);
handle1.style("left", formatPercent(scale(value[ 0 ])));
divRange.style("left", formatPercent(scale(value[ 0 ])));
drag.on("drag", onDragHorizontal);
var width = 100 - parseFloat(formatPercent(scale(value[ 1 ])));
handle2.style("left", formatPercent(scale(value[ 1 ])));
divRange.style("right", width+"%");
drag.on("drag", onDragHorizontal);
} else {
handle1.style("left", formatPercent(scale(value)));
drag.on("drag", onDragHorizontal);
}
sliderLength = parseInt(div.style("width"), 10);
} else { // Vertical
div.on("click", onClickVertical);
drag.on("drag", onDragVertical);
if (toType(value) == "array" && value.length == 2) {
divRange = d3.select(this).append('div').classed("d3-slider-range-vertical", true);
handle1.style("bottom", formatPercent(scale(value[ 0 ])));
divRange.style("bottom", formatPercent(scale(value[ 0 ])));
drag.on("drag", onDragVertical);
var top = 100 - parseFloat(formatPercent(scale(value[ 1 ])));
handle2.style("bottom", formatPercent(scale(value[ 1 ])));
divRange.style("top", top+"%");
drag.on("drag", onDragVertical);
} else {
handle1.style("bottom", formatPercent(scale(value)));
drag.on("drag", onDragVertical);
}
sliderLength = parseInt(div.style("height"), 10);
}
if (axis) {
createAxis(div);
}
function createAxis(dom) {
// Create axis if not defined by user
if (typeof axis === "boolean") {
axis = d3.svg.axis()
.ticks(Math.round(sliderLength) / 100)
.tickFormat(tickFormat)
.tickPadding(tickPadding)
.orient((orientation === "horizontal") ? "bottom" : "right");
}
// Copy slider scale to move from percentages to pixels
axisScale = scale.ticks ? scale.copy().range([0, sliderLength]) : scale.copy().rangePoints([0, sliderLength], 0.5);
axis.scale(axisScale);
// Create SVG axis container
var svg = dom.append("svg")
.classed("d3-slider-axis d3-slider-axis-" + axis.orient(), true)
.on("click", stopPropagation);
var g = svg.append("g");
// Horizontal axis
if (orientation === "horizontal") {
svg.style("margin-left", -margin - 16 + "px");
svg.attr({
width: sliderLength + margin * 2,
height: margin + 30
});
if (axis.orient() === "top") {
svg.style("top", -margin + "px");
g.attr("transform", "translate(" + margin + "," + margin + ")");
} else { // bottom
g.attr("transform", "translate(" + margin + ",0)");
}
} else { // Vertical
svg.style("top", -margin + "px");
svg.attr({
width: margin,
height: sliderLength + margin * 2
});
if (axis.orient() === "left") {
svg.style("left", -margin + "px");
g.attr("transform", "translate(" + margin + "," + margin + ")");
} else { // right
g.attr("transform", "translate(" + 0 + "," + margin + ")");
}
}
g.call(axis);
}
function onClickHorizontal() {
if (toType(value) != "array") {
var pos = Math.max(0, Math.min(sliderLength, d3.event.offsetX || d3.event.layerX));
moveHandle(scale.invert ?
stepValue(scale.invert(pos / sliderLength))
: nearestTick(pos / sliderLength));
}
}
function onClickVertical() {
if (toType(value) != "array") {
var pos = sliderLength - Math.max(0, Math.min(sliderLength, d3.event.offsetY || d3.event.layerY));
moveHandle(scale.invert ?
stepValue(scale.invert(pos / sliderLength))
: nearestTick(pos / sliderLength));
}
}
function onDragHorizontal() {
if ( d3.event.sourceEvent.target.id === "handle-one") {
active = 1;
} else if ( d3.event.sourceEvent.target.id == "handle-two" ) {
active = 2;
}
var pos = Math.max(0, Math.min(sliderLength, d3.event.x));
moveHandle(scale.invert ?
stepValue(scale.invert(pos / sliderLength))
: nearestTick(pos / sliderLength));
}
function onDragVertical() {
if ( d3.event.sourceEvent.target.id === "handle-one") {
active = 1;
} else if ( d3.event.sourceEvent.target.id == "handle-two" ) {
active = 2;
}
var pos = sliderLength - Math.max(0, Math.min(sliderLength, d3.event.y))
moveHandle(scale.invert ?
stepValue(scale.invert(pos / sliderLength))
: nearestTick(pos / sliderLength));
}
function stopPropagation() {
d3.event.stopPropagation();
}
});
}
// Move slider handle on click/drag
function moveHandle(newValue) {
var currentValue = toType(value) == "array" && value.length == 2 ? value[active - 1]: value,
oldPos = formatPercent(scale(stepValue(currentValue))),
newPos = formatPercent(scale(stepValue(newValue))),
position = (orientation === "horizontal") ? "left" : "bottom";
if (oldPos !== newPos) {
if (toType(value) == "array" && value.length == 2) {
value[ active - 1 ] = newValue;
if (d3.event) {
dispatch.slide(d3.event, value );
};
} else {
if (d3.event) {
dispatch.slide(d3.event.sourceEvent || d3.event, value = newValue);
};
}
if ( value[ 0 ] >= value[ 1 ] ) return;
if ( active === 1 ) {
if (toType(value) == "array" && value.length == 2) {
(position === "left") ? divRange.style("left", newPos) : divRange.style("bottom", newPos);
}
if (animate) {
handle1.transition()
.styleTween(position, function() { return d3.interpolate(oldPos, newPos); })
.duration((typeof animate === "number") ? animate : 250);
} else {
handle1.style(position, newPos);
}
} else {
var width = 100 - parseFloat(newPos);
var top = 100 - parseFloat(newPos);
(position === "left") ? divRange.style("right", width + "%") : divRange.style("top", top + "%");
if (animate) {
handle2.transition()
.styleTween(position, function() { return d3.interpolate(oldPos, newPos); })
.duration((typeof animate === "number") ? animate : 250);
} else {
handle2.style(position, newPos);
}
}
}
}
// Calculate nearest step value
function stepValue(val) {
if (val === scale.domain()[0] || val === scale.domain()[1]) {
return val;
}
var alignValue = val;
if (snap) {
alignValue = nearestTick(scale(val));
} else{
var valModStep = (val - scale.domain()[0]) % step;
alignValue = val - valModStep;
if (Math.abs(valModStep) * 2 >= step) {
alignValue += (valModStep > 0) ? step : -step;
}
};
return alignValue;
}
// Find the nearest tick
function nearestTick(pos) {
var ticks = scale.ticks ? scale.ticks() : scale.domain();
var dist = ticks.map(function(d) {return pos - scale(d);});
var i = -1,
index = 0,
r = scale.ticks ? scale.range()[1] : scale.rangeExtent()[1];
do {
i++;
if (Math.abs(dist[i]) < r) {
r = Math.abs(dist[i]);
index = i;
};
} while (dist[i] > 0 && i < dist.length - 1);
return ticks[index];
};
// Return the type of an object
function toType(v) {
return ({}).toString.call(v).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
};
// Getter/setter functions
slider.min = function(_) {
if (!arguments.length) return min;
min = _;
return slider;
};
slider.max = function(_) {
if (!arguments.length) return max;
max = _;
return slider;
};
slider.step = function(_) {
if (!arguments.length) return step;
step = _;
return slider;
};
slider.animate = function(_) {
if (!arguments.length) return animate;
animate = _;
return slider;
};
slider.orientation = function(_) {
if (!arguments.length) return orientation;
orientation = _;
return slider;
};
slider.axis = function(_) {
if (!arguments.length) return axis;
axis = _;
return slider;
};
slider.margin = function(_) {
if (!arguments.length) return margin;
margin = _;
return slider;
};
slider.value = function(_) {
if (!arguments.length) return value;
if (value) {
moveHandle(stepValue(_));
};
value = _;
return slider;
};
slider.snap = function(_) {
if (!arguments.length) return snap;
snap = _;
return slider;
};
slider.scale = function(_) {
if (!arguments.length) return scale;
scale = _;
return slider;
};
d3.rebind(slider, dispatch, "on");
return slider;
}
}));

View File

@@ -0,0 +1,686 @@
;(function(undefined) {
"use strict";
/**
* Graph Visualization
* ===================
*
* Author: Tasio Méndez (tasiomendez)
* URL: https://github.com/tasiomendez/
* Version: 0.1
*/
// Private constants
var focus_opacity = 0.1,
radius = 8,
shape_size = 16,
required_node = ['id', 'index', 'label', 'px', 'py', 'spells', 'weight', 'x', 'y', 'pos', 'scx', 'scy'];
// Private variables
var width,
height,
graph, // JSON data for the graph
model, // Definition of the attributes of the nodes
linkedByIndex, // Nodes linked by index
name, // Name of the graph (id for svg item)
svg, // Svg item
force, // Set up the force layout
color, // Color for nodes
zoom, // Zoom
groot, // Append sections to svg to have nodes and edges separately
graph_wrapper,
glinks,
gnodes,
background_image,
background_opacity,
background_filter_color,
data_node, // Actual node data for the graph
data_link, // Actual link data for the graph
link, // Line svgs
node, // Circles for the nodes
shape_property, // Property to whom the shape will be applied
shapes, // Dictionary of shapes for nodes
colors, // Dictionary of colors for nodes
background; // Background of the graph
Number.prototype.between = function(min, max) {
var min = (min || min === 0) ? min : Math.max(),
max = (max || max === 0) ? max : Math.min();
return ( this > min && this <= max ) || ( min === 0 && this === 0 );
};
Number.prototype.is_type = function() {
if ( typeof(this) === 'number' )
return ( Number.isInteger(this) ) ? 'int' : 'float';
else
return false;
}
String.prototype.is_type = function() {
return "string";
}
var lastFocusNode;
var _helpers = {
set_node: function(node, property, time) {
// Add nodes if data has more nodes than before
node.enter().append('circle')
.attr('class', 'node')
.attr('r', radius)
.style('fill', function (d) {
if ( Array.isArray(d[property]) ) {
var color_node = _helpers.set_color(property, d[property][0][0]);
d[property].forEach(function(p) {
if ( time.between(p[1], p[2]) ) color_node = _helpers.set_color(property, p[0]);
});
return color_node;
} else {
return _helpers.set_color(property, d[property]);
}
})
.style('stroke', function(d) {
if (_helpers.set_shape(d[shape_property]) !== (-1))
if ( Array.isArray(d[property]) ) {
var color_node = _helpers.set_color(property, d[property][0][0]);
d[property].forEach(function(p) {
if ( time.between(p[1], p[2]) ) color_node = _helpers.set_color(property, p[0]);
});
return color_node;
} else {
return _helpers.set_color(property, d[property]);
}
else
return '#ffffff';
})
// Cancel zoom movement so you can move the node
.on('mousedown', function(d) {
d3.event.stopPropagation();
})
// Double-click to focus neighbours
.on('dblclick', function(d) {
d3.event.stopPropagation();
if (d === lastFocusNode) {
lastFocusNode = undefined;
node.style('opacity', 1);
link.style('opacity', 1);
} else {
lastFocusNode = d;
_helpers.set_focus(d);
}
}).call(force.drag);
// Remove nodes if data has less nodes than before
node.exit().remove();
// Update existing nodes
node.attr('class', 'node')
.attr('r', radius)
.style('fill', function (d) {
if (_helpers.set_shape(d[shape_property]) !== (-1)) {
return 'url(#' + _helpers.set_shape(d[shape_property]) + ')';
}
if ( Array.isArray(d[property]) ) {
var color_node = _helpers.set_color(property, d[property][0][0]);
d[property].forEach(function(p) {
if ( time.between(p[1], p[2]) ) color_node = _helpers.set_color(property, p[0]);
});
return color_node;
} else {
return _helpers.set_color(property, d[property]);
}
})
.style('stroke', function(d) {
if (_helpers.set_shape(d[shape_property]) !== (-1))
if ( Array.isArray(d[property]) ) {
var color_node = _helpers.set_color(property, d[property][0][0]);
d[property].forEach(function(p) {
if ( time.between(p[1], p[2]) ) color_node = _helpers.set_color(property, p[0]);
});
return color_node;
} else {
return _helpers.set_color(property, d[property]);
}
else
return '#ffffff';
})
.on('dblclick', function(d) {
d3.event.stopPropagation();
if (d === lastFocusNode) {
lastFocusNode = undefined;
node.style('opacity', 1);
link.style('opacity', 1);
} else {
lastFocusNode = d;
_helpers.set_focus(d);
}
});
},
set_link: function(link) {
// Remove links if data has more links than before
link.enter().append('line')
.attr('class', 'link')
.style('stroke-width', function (d) {
return Math.sqrt(d.value);
});
// Remove links if data has less links than before
link.exit().remove();
},
isConnected: function(source, neighbour) {
return linkedByIndex[source.id + ',' + neighbour.id] ||
linkedByIndex[neighbour.id + ',' + source.id];
},
set_focus: function(d) {
node.style('opacity', function(o) {
return _helpers.isConnected(d,o) || d.index == o.index ? 1 : focus_opacity;
});
link.style('opacity', function(o) {
return o.source.index == d.index || o.target.index == d.index ? 1 : focus_opacity;
});
},
push_once: function(array, item, key) {
for (var i in array) {
if ( array[i][key] == item[key] ) return false;
}
array.push(item);
return true;
},
set_color: function(property, value) {
if ( colors instanceof Array ) {
for ( var c in colors ) {
if ( colors[c][property] == value ) { return colors[c]['color']; }
}
return color(value);
} else {
return color(value);
}
},
set_shape: function(value) {
if ( shapes instanceof Object && shape_property ) {
for ( var s in shapes ) {
var str_value = (value.includes('class')) ? value.split('.').pop().split('\'')[0] : value;
if ( str_value == s ) return shapes[s];
}
return (-1);
} else {
return (-1);
}
}
}
/**
* Graph Visualization Core Functions
* ----------------------------------
*
* The graph visualization functions themselves.
*/
function Graph() {
// Color
color = d3.scale.category20();
// Set up the force layout
force = d3.layout.force()
.charge(-500)
.linkDistance(30)
.size([width, height]);
// Append sections to svg to have nodes and edges separately
groot = svg.append('g').attr('id', 'root');
// Set background
if ( background !== undefined ) {
var rect = groot.append('rect').attr('fill', background_filter_color);
background_image = groot.append('image').attr('href', background).style('opacity', background_opacity);
graph_wrapper = groot.append('g') .attr('id', 'graph-wrapper');
glinks = graph_wrapper.append('g') .attr('id', 'links');
gnodes = graph_wrapper.append('g') .attr('id', 'nodes');
} else {
glinks = groot.append('g') .attr('id', 'links');
gnodes = groot.append('g') .attr('id', 'nodes');
}
// Add patterns for shapes
var defs = [];
for ( var i in shapes )
if (!defs.includes(shapes[i])) defs.push(shapes[i])
svg.append('defs')
.selectAll('pattern')
.data(defs)
.enter()
.append('pattern')
.attr('id', function(d, i) {
return d;
})
.attr('patternUnits', 'objectBoundingBox')
.attr('width', 1)
.attr('height', 1)
.append('image')
.attr('href', function(d) {
return window.location.protocol + '//' + window.location.host + '/img/svg/' + d + '.svg';
})
.attr('width', shape_size)
.attr('height', shape_size);
// Zoom
zoom = d3.behavior
.zoom()
.scaleExtent([1/5, 10])
.on('zoom', function () {
//console.trace("zoom", d3.event.translate, d3.event.scale);
groot.attr('transform',
'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')');
});
// Activate zoom for the svg item
svg.style('background-color', 'rgb(255,255,255)')
.call(zoom);
// Update linkedByIndex
linkedByIndex = {};
graph.links.forEach(function(d) {
linkedByIndex[d.source.id + ',' + d.target.id] = true;
});
// Creates the graph data structure out of the json data
force.nodes(graph.nodes)
.links(graph.links)
.start();
// Now we are giving the SVGs coordinates - the force layout is generating the coordinates
// which this code is using to update the attributes of the SVG elements
force.on('tick', function () {
link.attr('x1', function (d) {
if ( d.source.scx ) return d.source.scx;
else return d.source.x;
}).attr('y1', function (d) {
if ( d.source.scy ) return d.source.scy;
else return d.source.y;
}).attr('x2', function (d) {
if ( d.target.scx ) return d.target.scx;
else return d.target.x;
}).attr('y2', function (d) {
if ( d.target.scy ) return d.target.scy;
else return d.target.y;
});
node.attr('transform', function translate(d) {
if ( d.scx || d.scy ) return 'translate(' + d.scx + ',' + d.scy + ')';
else return 'translate(' + d.x + ',' + d.y + ')';
});
});
}
function update_data(property, time) {
// Reset data
var delete_links = true;
data_node = [];
data_link = graph.links.slice();
// Nodes
graph.nodes.forEach(function(node) {
if (Array.isArray(node.spells)) {
node.spells.forEach( function(d) {
if ( time.between(d[0], d[1]) ) {
data_node.push(node);
} else {
graph.links.forEach(function(link) {
if (link.source === node || link.target === node)
data_link.splice(data_link.indexOf(link), 1);
});
}
});
} else {
data_node.push(node);
}
});
// Links
graph.links.forEach(function(link) {
if ( !(time.between(link.start, link.end)) && data_link.includes(link) )
data_link.splice(data_link.indexOf(link), 1);
});
// Reset force
force.stop()
.nodes(data_node)
.links(data_link)
.start();
// Create all the line svgs but without locations
link = glinks.selectAll('.link').data(data_link);
_helpers.set_link(link);
// Do the same with the circles for the nodes - no
node = gnodes.selectAll('.node').data(data_node);
_helpers.set_node(node, property, time);
// Node Attributes
var statistics = {}
self.GraphVisualization.statistics = {};
data_node.forEach(function(n) {
// Count node properties
if ( Array.isArray(n[property]) ) {
n[property].forEach(function(p) {
if ( time.between(p[1], p[2]) ) statistics[p[0]] = (!statistics[p[0]]) ? 1 : statistics[p[0]] + 1;
});
} else { statistics[n[property]] = (!statistics[n[property]]) ? 1 : statistics[n[property]] + 1; }
});
for ( i in statistics ) {
statistics[i] = (statistics[i] / data_node.length * 100).toFixed(2);
}
self.GraphVisualization.statistics = statistics
}
function get_models(graph) {
var models = { 'dynamic': [], 'static': [] }
graph['nodes'].forEach(function(node) {
for ( var att in node ) {
if (!required_node.includes(att)) {
if ( Array.isArray(node[att]) ) _helpers.push_once(models['dynamic'], { 'title': att, 'type': node[att][0][0].is_type() }, 'title');
else _helpers.push_once(models['static'], { 'title': att, 'type': typeof(node[att]) }, 'title');
}
}
});
return models;
}
/**
* Public API
* -----------
*
* User-accessible functions.
*/
/**
* Create the space where the graph will we drawn.
* A function that identifies the svg item.
*
* @param {object} id The id of the svg item.
* @return {object} This class.
*/
function create(id, n_height, n_width, callback) {
name = id;
svg = d3.select('svg#' + name)
.attr('width', n_width)
.attr('height', n_height)
.style('background-color', 'rgba(128,128,128,0.1)');
height = n_height;
width = n_width
if (callback) { callback(this.GraphVisualization); }
else { return this.GraphVisualization }
}
/**
* Import JSON and attributes.
* A function that imports the graph and the attributes of all the nodes.
*
* @param {object} json The json structure of the graph.
* @param {object} callback A function called at the end.
*/
function importJSON(json, callback) {
reset()
graph = json;
// Create the graph itself
Graph();
self.GraphVisualization.nodes = graph.nodes.length;
self.GraphVisualization.links = graph.links.length;
self.GraphVisualization.model = get_models(json);
// Draw graph with default property and time for the first time
update_data(self.GraphVisualization.model.dynamic[0].title, 0);
if (callback) { callback(); }
}
/**
* Set link distance.
* A function that set the link distance. If it is not called, it uses 30 as default
*
* @param {object} distance Distance.
* @param {object} callback A function called at the end.
*/
function set_link_distance(distance, callback) {
if (graph) {
force.stop().linkDistance(distance).start();
// Update radius of the nodes to see them better
var r = d3.scale.linear().domain([30, 1000]).range([8, 24]);
radius = r(distance);
node.attr('r', radius);
var s = d3.scale.linear().domain([30, 1000]).range([16, 48]);
if ( shapes instanceof Object && shape_property ) {
svg.selectAll('pattern image').attr('width', s(distance)).attr('height', s(distance));
}
if (callback) { callback(radius); }
}
}
/**
* Set background image.
* A function that set a background image.
*
* @param {object} image Path to image.
*/
function set_background(image, set_opacity, set_color) {
background = image;
background_opacity = set_opacity || 0.8;
background_filter_color = set_color || 'white';
}
/**
* Set property and instant of time.
* A function that draws the graph depends on the property and instant of time selected.
*
* @param {object} property Property to show.
* @param {object} time Instant of time.
* @param {object} callback A function called at the end.
*/
function update_graph(property, time, callback) {
if (graph) {
update_data(property, time);
if (callback) { callback(); }
}
}
/**
* Set shapes and color of graph.
* A function that set the shapes and colors of the nodes depending on their status.
*
* @param {object} set_shapes Shapes for nodes.
* @param {object} set_colors Colors for nodes.
* @param {object} callback A function called at the end.
*/
function set_params(set_shape_property, set_shapes, set_colors, callback) {
shape_property = set_shape_property;
shapes = set_shapes;
colors = set_colors;
self.GraphVisualization.shapes = shapes;
self.GraphVisualization.colors = colors;
if (callback) { callback(); }
}
/**
* Adjust the graph to the whole area.
* A function that adjust the graph to the svg item.
*
* @param {object} padding Space from the graph to the border.
* 85% by default.
* @param {object} transition Duration of the zoom action.
* 750 milliseconds by default.
* @param {object} callback A function called at the end.
*/
function zoom_to_fit(padding, transition, callback) {
var bounds = groot.node().getBBox();
var parent = groot.node().parentElement;
var fullWidth = parent.clientWidth,
fullHeight = parent.clientHeight;
var widthBounds = bounds.width,
heightBounds = bounds.height;
var midX = bounds.x + widthBounds / 2,
midY = bounds.y + heightBounds / 2;
if (widthBounds == 0 || heightBounds == 0) return; // nothing to fit
var scale = (padding || 0.85) / Math.max(widthBounds / fullWidth, heightBounds / fullHeight);
var translate = [fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY];
//console.trace("zoomFit", translate, scale);
groot
.transition()
.duration(transition || 750) // milliseconds
.call(zoom.translate(translate).scale(scale).event);
if (callback) { callback(); }
}
/**
* Reset the whole graph.
* A function that reset the svg item.
*
*/
function reset() {
d3.select('svg#' + name)
.html('')
.attr('width', width)
.attr('height', height)
.style('background-color', 'rgba(128,128,128,0.1)');
}
/**
* Get color for a value.
* A function that get the color of a node or a group of nodes.
*
* @param {object} value Value.
* @return {object} color The color in hexadecimal.
*/
function color(property, value) {
if (graph) {
return _helpers.set_color(property, value);
}
}
/**
* Get attributes at one moment given.
* A function that get the attributes of all nodes at a specific time.
*
* @param {object} time Instant of time.
* @param {object} callback A function called at the end.
* @return {object} object An object with the number of nodes.
*/
function get_attributes(property, time, callback) {
var attrs = {}
graph.nodes.forEach(function(node) {
if (Array.isArray(node.spells)) {
node.spells.forEach( function(d) {
if ( time.between(d[0], d[1]) ) {
if (Array.isArray(node[property])) {
node[property].forEach( function(p) {
if ( time.between(p[1], p[2]) ) attrs[p[0]] = (!attrs[p[0]]) ? 1 : attrs[p[0]] + 1;
});
} else { attrs[node[property]] = (!attrs[node[property]]) ? 1 : attrs[node[property]] + 1; }
}
});
} else {
if (Array.isArray(node[property])) {
node[property].forEach( function(p) {
if ( time.between(p[1], p[2]) ) attrs[p[0]] = (!attrs[p[0]]) ? 1 : attrs[p[0]] + 1;
});
} else { attrs[node[property]] = (!attrs[node[property]]) ? 1 : attrs[node[property]] + 1; }
}
});
if (callback) { callback(attrs); }
else { return attrs }
}
/**
* Get nodes at one moment given.
* A function that get the number of nodes at a specific time.
*
* @param {object} time Instant of time.
* @param {object} callback A function called at the end.
* @return {object} number The number of nodes.
*/
function get_nodes(time, callback) {
var total_nodes = 0;
graph.nodes.forEach(function(node) {
if (Array.isArray(node.spells)) {
node.spells.forEach( function(d) {
if ( time.between(d[0], d[1]) ) { total_nodes++; }
});
} else {
total_nodes++;
}
});
if (callback) { callback(total_nodes); }
else { return total_nodes }
}
/**
* Exporting
* ---------
*/
this.GraphVisualization = {
// Functions
create: create,
import: importJSON,
update_graph: update_graph,
set_params: set_params,
set_link_distance: set_link_distance,
set_background: set_background,
fit: zoom_to_fit,
reset: reset,
// Attributes
model: {},
nodes: undefined,
links: undefined,
// Getters
color: color,
shapes: shapes,
colors: colors,
get_attributes: get_attributes,
get_nodes: get_nodes,
// Statistics
statistics: {},
// Version
version: '0.1'
};
}).call(this);

View File

@@ -0,0 +1,378 @@
<!DOCTYPE html>
<head>
<!-- FAVICON -->
<link rel="shortcut icon" href="http://gsi.dit.upm.es/templates/purity_iii/favicon.ico" type="image/x-icon">
<link rel="icon" href="http://gsi.dit.upm.es/templates/purity_iii/favicon.ico" type="image/x-icon">
<!-- JQUERY -->
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<!-- BOOTSTRAP 3.3.7 -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<!-- BOOTSTRAP SLIDER -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/9.9.0/css/bootstrap-slider.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-slider/9.9.0/bootstrap-slider.js"></script>
<!-- D3.js // DATA-DRIVEN DOCUMENTS -->
<script type="text/javascript" src="http://d3js.org/d3.v3.js"></script>
<script type="text/javascript" src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script>
<!-- C3.js // D3-based reusable chart library -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.18/c3.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.18/c3.min.js"></script>
<!-- JAVASCRIPTS -->
<script type="text/javascript" src="js/visualization.js"></script>
<script type="text/javascript" src="js/timeline.js"></script>
<script type="text/javascript" src="js/socket.js"></script>
<script type="text/javascript" src="js/template.js"></script>
<!-- STYLESHEETS -->
<link rel="stylesheet" type="text/css" href="css/main.css">
<link rel="stylesheet" type="text/css" href="css/timeline.css">
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Ubuntu+Mono" />
<title>{{ name }}</title>
<script type="text/javascript">//<![CDATA[
var width = window.innerWidth * 0.75,
height = window.innerHeight * 3 / 5,
speed = 1,
play,
slider;
var width_chart = (window.innerWidth - 30) / 2 - 15,
height_chart = (window.innerHeight - 230) / 2,
chart_nodes,
chart_attrs;
window.onload = function() {
"use strict";
// Create svg, timeline and settings
self.GraphVisualization.create('graph', height, width);
$('<div>').attr('id', 'load').appendTo('#graph_container').css('left', width / 2 - 25).css('top', height / 2);
$('#configuration').css("height", height);
d3.select('#slider3').attr("width", width).call(
d3.slider().axis(true).min(0).max(100)
);
// Load a file
$('#update #file').change(function() {
var file = $('#file')[0].files[0];
$('.console').append('<br/>');
self.GraphVisualization.reset();
$('#load').show();
$('.custom-file-control').attr("data-content",
file['name'] || "Choose file..."
);
if ( file['type'] !== "application/x-yaml" ) {
console.error('File format not supported. Sorry for the inconvenience.');
_socket.error('File format not supported. Sorry for the inconvenience.');
return;
} else {
$('.alert.alert-danger').hide();
}
var fileReader = new FileReader();
if (fileReader && file) {
fileReader.readAsText(file);
fileReader.onload = function () {
var content = fileReader.result;
$('#load').show().addClass('loader');
_socket.send(content, 'config_file');
};
}
});
// Select 'attributes'
$('.config-item #properties').change(function() {
self.GraphVisualization.update_graph($(this).val(), slider.value(), function() {
update_statistics_table();
});
});
// Run simulation
$('#simulation_modal .btn-success').click(function() {
if ( !jQuery.isEmptyObject(run_simulation()) ) {
self.GraphVisualization.reset();
$('#load').show().addClass('loader');
_socket.send(run_simulation(), 'run_simulation');
$('.console').append('<br/>');
}
$('#simulation_modal').modal('hide')
});
chart_nodes = create_chart(width_chart, height_chart, 'Time', 'Number of nodes', '#chart_nodes');
chart_attrs = create_chart(width_chart, height_chart, 'Time', 'Attributes', '#chart_attrs');
// Fill modal window
$('#simulation_modal').on('show.bs.modal', function(e) {
var variables = run_simulation()
var x = 0,
row;
for (var i in variables) {
if ( x % 2 === 0 ) row = $('<tr>').appendTo('#simulation_modal table tbody');
$('<td>').text(i).appendTo(row);
$('<td>').text(variables[i]).appendTo(row);
x++;
}
});
$('#simulation_modal').on('hide.bs.modal', function(e) {
$('#simulation_modal table tbody').empty();
});
}
///]]
</script>
</head>
<body>
<div class="container-fluid fixed">
<div class="col-sm-9 wrapper-heading">
<!-- CONSOLE -->
<div class="console">
Please, upload a YAML file that defines all the parameters of a simulation. <br/>
If you don't know how to write the file, please visit this page:<br/>
http://soilsim.readthedocs.io/en/latest/quickstart.html<br/>
</div>
<!-- //CONSOLE -->
<!-- SOIL Logo -->
<div class="soil_logo" >
<img src="img/logo_soil.png" />
</div>
<!-- //SOIL Logo -->
</div>
<div id="update" class="col-sm-3">
<!-- Load File -->
<form enctype="multipart/form-data">
<label class="custom-file">
<input type="file" id="file" name="file" class="custom-file-input">
<span class="custom-file-control" data-content="Choose file..."></span>
</label>
</form>
<!-- //Load File -->
<!-- Atributos -->
<div class="config-item">
Attributes:
<select id="properties" class="form-control form-control-sm">
<optgroup id="properties-dynamic" label="Dynamics"></optgroup>
<optgroup id="properties-static" label="Statics"></optgroup>
</select>
</div>
<!-- //Atributos -->
</div>
</div>
<nav class="navbar navbar-default navbar-fixed-bottom">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">{{ name }}</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li data-target="#myCarousel" data-slide-to="0" class="active" id="home_menu"><a href='#'>Home</a></li>
<li data-target="#myCarousel" data-slide-to="1" id="settings_menu"><a href="#">Settings &amp; Charts</a></li>
<li class="dropdown" id="trials">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Trials <span class="caret"></span></a>
<ul class="dropdown-menu"></ul>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="#" id="run_simulation" role="button">Run simulation</a></li>
<li><a href="#" id="download_simulation" role="button" data-toggle="modal" data-target="#download_modal">Download</a></li>
</ul>
</div>
<script type="text/javascript">
$('.nav li[data-target="#myCarousel"]').click(function() {
$('.nav li').removeClass('active');
$(this).addClass('active');
});
</script>
</div>
</nav>
<div id="myCarousel" class="carousel slide">
<!-- Wrapper for slides -->
<div class="carousel-inner">
<!-- Wrapper Graph Container -->
<div class="item active">
<div class="container-fluid">
<div id="graph_container">
<svg id="graph" class="col-sm-9" xmlns="http://www.w3.org/2000/svg"></svg>
</div>
<div id="configuration" class="col-sm-3">
<!-- Graph Info -->
<div class="config-item" style="margin-top: 0 !important;">
<table id="info-graph">
<tbody>
<tr id="nodes"><th>Nodes:</th></tr>
<tr id="links"><th>Links:</th></tr>
</tbody>
</table>
<div class="logo pull-right">
<img src="img/logo_gsi.svg" style="height: 40px;">
</div>
</div>
<hr />
<!-- //Graph Info -->
<!-- PROPIEDADES -->
<div class="config-item">
<table id="percentTable">
<tbody><tr><th class="no-data-table">No data</th></tr></tbody>
</table>
</div>
<hr />
<!-- //PROPIEDADES -->
<!-- SPEED -->
<div class="config-item">
<table id="speed"><tbody><tr>
<th class="text-left min">min</th>
<th class="text-center">Speed</th>
<th class="text-right max">max</th>
</tr></tbody></table>
<div class="speed-slider">
<input id="speed-slider" type="text" data-slider-min="0.1" data-slider-max="100" data-slider-step="0.1" data-slider-value="1" data-slider-tooltip="hide" data-slider-enabled="false" data-slider-scale="logarithmic"/>
</div>
</div>
<hr />
<script type="text/javascript">
$('#speed-slider').slider();
</script>
<!-- //SPEED -->
<!-- LINK DISTANCE -->
<div class="config-item">
<table id="link-distance"><tbody><tr>
<th class="text-left min">min</th>
<th class="text-center">Link Distance</th>
<th class="text-right max">max</th>
</tr></tbody></table>
<div class="link-distance-slider">
<input id="link-distance-slider" type="text" data-slider-min="30" data-slider-max="1000" data-slider-step="0.01" data-slider-value="30" data-slider-tooltip="hide" data-slider-reversed="false" data-slider-enabled="false" data-slider-scale="logarithmic"/>
</div>
</div>
<script type="text/javascript">
$('#link-distance-slider').slider();
</script>
<!-- //LINK DISTANCE -->
</div>
<!-- TIMELINE -->
<div id="timeline" class="col-sm-12">
<div id="slider3" class="pull-left col-sm-9"></div>
<div class="btn-toolbar controls">
<button type="button" id="button_play" class="btn btn-lg">
<span class="glyphicon glyphicon-play" aria-hidden="true"></span>
</button>
<button type="button" id="button_pause" class="btn btn-lg">
<span class="glyphicon glyphicon-pause" aria-hidden="true"></span>
</button>
<button type="button" id="button_zoomFit" class="btn btn-lg">
<span class="glyphicon glyphicon-fullscreen" aria-hidden="true"></span>
</button>
</div>
</div>
<!-- //TIMELINE -->
<!-- ERROR ALERT -->
<div class="alert alert-danger alert-dismissable fade in" style="display: none;">
<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
<strong>Error! </strong><span id="error-message"></span>
</div>
<!-- //ERROR ALERT -->
</div>
</div>
<!-- //Wrapper Graph Container -->
<!-- Wrapper Settings -->
<div class="item" id="settings">
<div class="container-fluid">
<div class="col-sm-6" id="charts">
<div id="chart_nodes" class="chart no-data"></div>
<div id="chart_attrs" class="chart no-data"></div>
</div>
<div class="col-sm-6 none" id="wrapper-settings"></div>
</div>
</div>
</div>
<!-- //Wrapper for slides -->
</div>
<div class="modal fade" tabindex="-1" role="dialog" id="simulation_modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">New simulation</h4>
</div>
<div class="modal-body text-justify">
<p>You are going to run a new simulation, all charts and trials are going to be lost. A new ones will be available after the simulation.</p>
<p>Check your new environment variables for this simulation.</p>
<table class="table">
<thead><tr><th>Variable</th><th>Value</th><th>Variable</th><th>Value</th></tr></thead>
<tbody></tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success">Run</button>
<button type="button" class="btn btn-danger" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<div class="modal fade" tabindex="-1" role="dialog" id="download_modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Download Simulation Results</h4>
</div>
<div class="modal-body text-justify">
<p>You can download the results of the selected trial in GEXF or JSON Graph format for your personal purposes.</p>
<ul >
<li><b>GEXF</b> <i>(Graph Exchange XML Format)</i> is a language for describing complex network structures, their associated data and dynamics. It can be used to visualize the simulation with Gephi.</li>
<li><b>JSON Graph</b> generate and parse JSON serializable data for NetworkX graphs. It is a convention for modeling graph information as a JSON object that can be parsed by any JSON parser.</li>
</ul>
<p>For downloading the results of the other trials simulated, please first select them in menu.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" disabled="disabled" id="download_gexf">GEXF</button>
<button type="button" class="btn btn-success" disabled="disabled" id="download_json">JSON Graph</button>
<button type="button" class="btn btn-danger" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</body>
</html>

View File

@@ -0,0 +1 @@
pytest

16
tests/test.csv Normal file
View File

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

12
tests/test.gexf Normal file
View 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>

90
tests/test_analysis.py Normal file
View File

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

48
tests/test_examples.py Normal file
View File

@@ -0,0 +1,48 @@
from unittest import TestCase
import os
from os.path import join
from soil import utils, simulation
ROOT = os.path.abspath(os.path.dirname(__file__))
EXAMPLES = join(ROOT, '..', 'examples')
class TestExamples(TestCase):
pass
def make_example_test(path, config):
def wrapped(self):
root = os.getcwd()
os.chdir(os.path.dirname(path))
s = simulation.from_config(config)
iterations = s.max_time * s.num_trials
if iterations > 1000:
self.skipTest('This example would probably take too long')
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
os.chdir(root)
return wrapped
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(TestExamples, p.__name__, p)
del p
add_example_tests()

156
tests/test_history.py Normal file
View File

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

View File

@@ -2,17 +2,22 @@ from unittest import TestCase
import os
import yaml
import networkx as nx
from functools import partial
from os.path import join
from soil import simulation, environment, agents, utils
from soil import simulation, Environment, agents, utils, history
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):
@@ -21,6 +26,7 @@ class TestMain(TestCase):
Raise an exception otherwise.
"""
config = {
'dry_run': True,
'network_params': {
'path': join(ROOT, 'test.gexf')
}
@@ -30,6 +36,7 @@ class TestMain(TestCase):
assert len(G) == 2
with self.assertRaises(AttributeError):
config = {
'dry_run': True,
'network_params': {
'path': join(ROOT, 'unknown.extension')
}
@@ -43,6 +50,7 @@ class TestMain(TestCase):
should be used to generate a network
"""
config = {
'dry_run': True,
'network_params': {
'generator': 'barabasi_albert_graph'
}
@@ -57,6 +65,7 @@ class TestMain(TestCase):
def test_empty_simulation(self):
"""A simulation with a base behaviour should do nothing"""
config = {
'dry_run': True,
'network_params': {
'path': join(ROOT, 'test.gexf')
},
@@ -65,35 +74,39 @@ class TestMain(TestCase):
}
}
s = simulation.from_config(config)
s.run_simulation()
s.run_simulation(dry_run=True)
def test_counter_agent(self):
"""
The initial states should be applied to the agent and the
agent should be able to update its state."""
config = {
'name': 'CounterAgent',
'dry_run': True,
'network_params': {
'path': join(ROOT, 'test.gexf')
},
'agent_type': 'CounterModel',
'states': [{'neighbors': 10}, {'total': 12}],
'states': [{'times': 10}, {'times': 20}],
'max_time': 2,
'num_trials': 1,
'environment_params': {
}
}
s = simulation.from_config(config)
env = s.run_simulation()[0]
assert env.get_agent(0)['neighbors', 0] == 10
assert env.get_agent(0)['neighbors', 1] == 1
assert env.get_agent(1)['total', 0] == 12
assert env.get_agent(1)['neighbors', 1] == 1
env = s.run_simulation(dry_run=True)[0]
assert env.get_agent(0)['times', 0] == 11
assert env.get_agent(0)['times', 1] == 12
assert env.get_agent(1)['times', 0] == 21
assert env.get_agent(1)['times', 1] == 22
def test_counter_agent_history(self):
"""
The evolution of the state should be recorded in the logging agent
"""
config = {
'name': 'CounterAgent',
'dry_run': True,
'network_params': {
'path': join(ROOT, 'test.gexf')
},
@@ -108,22 +121,18 @@ class TestMain(TestCase):
}
}
s = simulation.from_config(config)
env = s.run_simulation()[0]
env = s.run_simulation(dry_run=True)[0]
for agent in env.network_agents:
last = 0
assert len(agent[None, None]) == 11
for step, total in agent['total', None].items():
if step > 0:
assert total == last + 2
last = total
assert len(agent[None, None]) == 10
for step, total in sorted(agent['total', None]):
assert total == last + 2
last = total
def test_custom_agent(self):
"""Allow for search of neighbors with a certain state_id"""
class CustomAgent(agents.BaseAgent):
def step(self):
self.state['neighbors'] = self.count_agents(state_id=0,
limit_neighbors=True)
config = {
'dry_run': True,
'network_params': {
'path': join(ROOT, 'test.gexf')
},
@@ -138,7 +147,7 @@ class TestMain(TestCase):
}
}
s = simulation.from_config(config)
env = s.run_simulation()[0]
env = s.run_simulation(dry_run=True)[0]
assert env.get_agent(0).state['neighbors'] == 1
def test_torvalds_example(self):
@@ -147,6 +156,7 @@ class TestMain(TestCase):
config['network_params']['path'] = join(EXAMPLES,
config['network_params']['path'])
s = simulation.from_config(config)
s.dry_run = True
env = s.run_simulation()[0]
for a in env.network_agents:
skill_level = a.state['skill_level']
@@ -171,13 +181,13 @@ class TestMain(TestCase):
with utils.timer('loading'):
config = utils.load_file(join(EXAMPLES, 'complete.yml'))[0]
s = simulation.from_config(config)
s.dry_run = True
with utils.timer('serializing'):
serial = s.to_yaml()
with utils.timer('recovering'):
recovered = yaml.load(serial)
with utils.timer('deleting'):
del recovered['topology']
del recovered['load_module']
assert config == recovered
def test_configuration_changes(self):
@@ -185,65 +195,113 @@ class TestMain(TestCase):
The configuration should not change after running
the simulation.
"""
config = utils.load_file('examples/complete.yml')[0]
config = utils.load_file(join(EXAMPLES, 'complete.yml'))[0]
s = simulation.from_config(config)
s.dry_run = True
for i in range(5):
s.run_simulation()
s.run_simulation(dry_run=True)
nconfig = s.to_dict()
del nconfig['topology']
del nconfig['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):
sim = simulation.SoilSimulation()
env = environment.SoilEnvironment(simulation=sim)
env = Environment(dry_run=True)
env['test'] = 'test_value'
env._save_state(now=0)
res = list(env.history_to_tuples())
assert len(res) == len(env.environment_params)
assert ('env', 0, 'test', 'test_value', 'str') in res
env._now = 1
env['test'] = 'second_value'
env._save_state(now=1)
res = list(env.history_to_tuples())
assert env['env', 0, 'test' ] == 'test_value'
assert env['env', 1, 'test' ] == 'second_value'
def test_save_geometric(self):
"""
There is a bug in networkx that prevents it from creating a GEXF file
from geometric models. We should work around it.
"""
G = nx.random_geometric_graph(20, 0.1)
env = Environment(topology=G, dry_run=True)
env.dump_gexf('/tmp/dump-gexf')
def test_save_graph(self):
'''
The history_to_graph method should return a valid networkx graph.
The state of the agent should be encoded as intervals in the nx graph.
'''
G = nx.cycle_graph(5)
distribution = agents.calculate_distribution(None, agents.BaseAgent)
env = Environment(topology=G, network_agents=distribution, dry_run=True)
env[0, 0, 'testvalue'] = 'start'
env[0, 10, 'testvalue'] = 'finish'
nG = env.history_to_graph()
values = nG.node[0]['attr_testvalue']
assert ('start', 0, 10) in values
assert ('finish', 10, None) in values
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()
for env in envs:
try:
n = config['network_params']['n']
assert len(env.get_agents()) == n
except KeyError:
pass
os.chdir(root)
return wrapped
def test_serialize_class(self):
ser, name = utils.serialize(agents.BaseAgent)
assert name == 'soil.agents.BaseAgent'
assert ser == agents.BaseAgent
class CustomAgent(agents.BaseAgent):
pass
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
ser, name = utils.serialize(CustomAgent)
assert name == 'test_main.CustomAgent'
assert ser == CustomAgent
def test_serialize_builtin_types(self):
add_example_tests()
for i in [1, None, True, False, {}, [], list(), dict()]:
ser, name = utils.serialize(i)
assert type(ser) == str
des = utils.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'
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
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'
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"