Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
01cc8e9128 | ||
|
a47ffa815b | ||
|
b41927d7bf | ||
|
70d033b3a9 | ||
|
3afed06656 | ||
|
0a7ef27844 | ||
|
2e28b36f6e | ||
|
bd4700567e | ||
|
ff1df62eec | ||
|
9165979b49 | ||
|
078f8ace9e | ||
|
8fec544772 | ||
|
5420501d36 | ||
|
5d89827ccf | ||
|
fc48ed7e09 | ||
|
73c90887e8 | ||
|
497c8a55db | ||
|
7d1c800490 |
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
**/soil_output
|
||||
.*
|
21
.gitlab-ci.yml
Normal 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
|
11
Dockerfile
@@ -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
@@ -0,0 +1,4 @@
|
||||
test:
|
||||
docker-compose exec dev python -m pytest -s -v
|
||||
|
||||
.PHONY: test
|
12
docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
version: '3'
|
||||
services:
|
||||
dev:
|
||||
build: .
|
||||
environment:
|
||||
PYTHONDONTWRITEBYTECODE: 1
|
||||
volumes:
|
||||
- .:/usr/src/app
|
||||
tty: true
|
||||
entrypoint: /bin/bash
|
||||
ports:
|
||||
- '8001:8001'
|
244
docs/configuration.rst
Normal file
@@ -0,0 +1,244 @@
|
||||
Configuring a simulation
|
||||
------------------------
|
||||
|
||||
There are two ways to configure a simulation: programmatically and with a configuration file.
|
||||
In both cases, the parameters used are the same.
|
||||
The advantage of a configuration file is that it is a clean declarative description, and it makes it easier to reproduce.
|
||||
|
||||
Simulation configuration files can be formatted in ``json`` or ``yaml`` and they define all the parameters of a simulation.
|
||||
Here's an example (``example.yml``).
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
---
|
||||
name: MyExampleSimulation
|
||||
max_time: 50
|
||||
num_trials: 3
|
||||
interval: 2
|
||||
network_params:
|
||||
generator: barabasi_albert_graph
|
||||
n: 100
|
||||
m: 2
|
||||
network_agents:
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
state:
|
||||
id: content
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
state:
|
||||
id: discontent
|
||||
- agent_type: SISaModel
|
||||
weight: 8
|
||||
state:
|
||||
id: neutral
|
||||
environment_params:
|
||||
prob_infect: 0.075
|
||||
|
||||
|
||||
This example configuration will run three trials (``num_trials``) of a simulation containing a randomly generated network (``network_params``).
|
||||
The 100 nodes in the network will be SISaModel agents (``network_agents.agent_type``), which is an agent behavior that is included in Soil.
|
||||
10% of the agents (``weight=1``) will start in the content state, 10% in the discontent state, and the remaining 80% (``weight=8``) in the neutral state.
|
||||
All agents will have access to the environment (``environment_params``), which only contains one variable, ``prob_infected``.
|
||||
The state of the agents will be updated every 2 seconds (``interval``).
|
||||
|
||||
Now run the simulation with the command line tool:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
soil example.yml
|
||||
|
||||
Once the simulation finishes, its results will be stored in a folder named ``MyExampleSimulation``.
|
||||
Three types of objects are saved by default: a pickle of the simulation; a ``YAML`` representation of the simulation (which can be used to re-launch it); and for every trial, a ``sqlite`` file with the content of the state of every network node and the environment parameters at every step of the simulation.
|
||||
|
||||
|
||||
.. code::
|
||||
|
||||
soil_output
|
||||
└── MyExampleSimulation
|
||||
├── MyExampleSimulation.dumped.yml
|
||||
├── MyExampleSimulation.simulation.pickle
|
||||
├── MyExampleSimulation_trial_0.db.sqlite
|
||||
├── MyExampleSimulation_trial_1.db.sqlite
|
||||
└── MyExampleSimulation_trial_2.db.sqlite
|
||||
|
||||
|
||||
You may also ask soil to export the states in a ``csv`` file, and the network in gephi format (``gexf``).
|
||||
|
||||
Network
|
||||
=======
|
||||
|
||||
The network topology for the simulation can be loaded from an existing network file or generated with one of the random network generation methods from networkx.
|
||||
|
||||
Loading a network
|
||||
#################
|
||||
|
||||
To load an existing network, specify its path in the configuration:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
---
|
||||
network_params:
|
||||
path: /tmp/mynetwork.gexf
|
||||
|
||||
Soil will try to guess what networkx method to use to read the file based on its extension.
|
||||
However, we only test using ``gexf`` files.
|
||||
|
||||
For simple networks, you may also include them in the configuration itself using , using the ``topology`` parameter like so:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
---
|
||||
topology:
|
||||
nodes:
|
||||
- id: First
|
||||
- id: Second
|
||||
links:
|
||||
- source: First
|
||||
target: Second
|
||||
|
||||
|
||||
Generating a random network
|
||||
###########################
|
||||
|
||||
To generate a random network using one of networkx's built-in methods, specify the `graph generation algorithm <https://networkx.github.io/documentation/development/reference/generators.html>`_ and other parameters.
|
||||
For example, the following configuration is equivalent to :code:`nx.complete_graph(n=100)`:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
network_params:
|
||||
generator: complete_graph
|
||||
n: 100
|
||||
|
||||
Environment
|
||||
============
|
||||
The environment is the place where the shared state of the simulation is stored.
|
||||
For instance, the probability of disease outbreak.
|
||||
The configuration file may specify the initial value of the environment parameters:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
environment_params:
|
||||
daily_probability_of_earthquake: 0.001
|
||||
number_of_earthquakes: 0
|
||||
|
||||
All agents have access to the environment parameters.
|
||||
|
||||
In some scenarios, it is useful to have a custom environment, to provide additional methods or to control the way agents update environment state.
|
||||
For example, if our agents play the lottery, the environment could provide a method to decide whether the agent wins, instead of leaving it to the agent.
|
||||
|
||||
|
||||
Agents
|
||||
======
|
||||
Agents are a way of modelling behavior.
|
||||
Agents can be characterized with two variables: agent type (``agent_type``) and state.
|
||||
Only one agent is executed at a time (generally, every ``interval`` seconds), and it has access to its state and the environment parameters.
|
||||
Through the environment, it can access the network topology and the state of other agents.
|
||||
|
||||
There are three three types of agents according to how they are added to the simulation: network agents and environment agent.
|
||||
|
||||
Network Agents
|
||||
##############
|
||||
Network agents are attached to a node in the topology.
|
||||
The configuration file allows you to specify how agents will be mapped to topology nodes.
|
||||
|
||||
The simplest way is to specify a single type of agent.
|
||||
Hence, every node in the network will be associated to an agent of that type.
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
agent_type: SISaModel
|
||||
|
||||
It is also possible to add more than one type of agent to the simulation, and to control the ratio of each type (using the ``weight`` property).
|
||||
For instance, with following configuration, it is five times more likely for a node to be assigned a CounterModel type than a SISaModel type.
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
network_agents:
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
- agent_type: CounterModel
|
||||
weight: 5
|
||||
|
||||
The third option is to specify the type of agent on the node itself, e.g.:
|
||||
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
topology:
|
||||
nodes:
|
||||
- id: first
|
||||
agent_type: BaseAgent
|
||||
states:
|
||||
first:
|
||||
agent_type: SISaModel
|
||||
|
||||
|
||||
This would also work with a randomly generated network:
|
||||
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
network:
|
||||
generator: complete
|
||||
n: 5
|
||||
agent_type: BaseAgent
|
||||
states:
|
||||
- agent_type: SISaModel
|
||||
|
||||
|
||||
|
||||
In addition to agent type, you may add a custom initial state to the distribution.
|
||||
This is very useful to add the same agent type with different states.
|
||||
e.g., to populate the network with SISaModel, roughly 10% of them with a discontent state:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
network_agents:
|
||||
- agent_type: SISaModel
|
||||
weight: 9
|
||||
state:
|
||||
id: neutral
|
||||
- agent_type: SISaModel
|
||||
weight: 1
|
||||
state:
|
||||
id: discontent
|
||||
|
||||
Lastly, the configuration may include initial state for one or more nodes.
|
||||
For instance, to add a state for the two nodes in this configuration:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
agent_type: SISaModel
|
||||
network:
|
||||
generator: complete_graph
|
||||
n: 2
|
||||
states:
|
||||
- id: content
|
||||
- id: discontent
|
||||
|
||||
|
||||
Or to add state only to specific nodes (by ``id``).
|
||||
For example, to apply special skills to Linux Torvalds in a simulation:
|
||||
|
||||
.. literalinclude:: ../examples/torvalds.yml
|
||||
:language: yaml
|
||||
|
||||
|
||||
Environment Agents
|
||||
##################
|
||||
In addition to network agents, more agents can be added to the simulation.
|
||||
These agents are programmed in much the same way as network agents, the only difference is that they will not be assigned to network nodes.
|
||||
|
||||
|
||||
.. code::
|
||||
|
||||
environment_agents:
|
||||
- agent_type: MyAgent
|
||||
state:
|
||||
mood: happy
|
||||
- agent_type: DummyAgent
|
||||
|
||||
|
||||
You may use environment agents to model events that a normal agent cannot control, such as natural disasters or chance.
|
||||
They are also useful to add behavior that has little to do with the network and the interactions within that network.
|
@@ -6,7 +6,7 @@
|
||||
Welcome to Soil's documentation!
|
||||
================================
|
||||
|
||||
Soil is an Agent-based Social Simulator in Python for modelling and simulation of Social Networks.
|
||||
Soil is an Agent-based Social Simulator in Python focused on Social Networks.
|
||||
|
||||
If you use Soil in your research, do not forget to cite this paper:
|
||||
|
||||
@@ -39,6 +39,7 @@ If you use Soil in your research, do not forget to cite this paper:
|
||||
|
||||
installation
|
||||
quickstart
|
||||
configuration
|
||||
Tutorial <soil_tutorial>
|
||||
|
||||
..
|
||||
|
@@ -1,197 +1,94 @@
|
||||
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`.
|
||||
The configuration is the following:
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
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.
|
||||
You may :download:`download the file <quickstart.yml>` directly.
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
pip install soil[web]
|
||||
```
|
||||
|
||||
Once installed, the soil web UI can be run in two ways:
|
||||
|
||||
```
|
||||
soil-web
|
||||
|
||||
OR
|
||||
|
||||
python -m soil.web
|
||||
```
|
30
docs/quickstart.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: quickstart
|
||||
num_trials: 1
|
||||
max_time: 1000
|
||||
network_agents:
|
||||
- agent_type: SISaModel
|
||||
state:
|
||||
id: neutral
|
||||
weight: 1
|
||||
- agent_type: SISaModel
|
||||
state:
|
||||
id: content
|
||||
weight: 2
|
||||
network_params:
|
||||
n: 100
|
||||
k: 5
|
||||
p: 0.2
|
||||
generator: newman_watts_strogatz_graph
|
||||
environment_params:
|
||||
neutral_discontent_spon_prob: 0.05
|
||||
neutral_discontent_infected_prob: 0.1
|
||||
neutral_content_spon_prob: 0.2
|
||||
neutral_content_infected_prob: 0.4
|
||||
discontent_neutral: 0.2
|
||||
discontent_content: 0.05
|
||||
content_discontent: 0.05
|
||||
variance_d_c: 0.05
|
||||
variance_c_d: 0.1
|
||||
content_neutral: 0.1
|
||||
standard_variance: 0.1
|
BIN
docs/soil.png
Normal file
After Width: | Height: | Size: 43 KiB |
@@ -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
80808
examples/Untitled.ipynb
Normal 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:
|
||||
|
@@ -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:
|
||||
|
@@ -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)
|
||||
|
10
examples/pubcrawl/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Simulation of pubs and drinking pals that go from pub to pub.
|
||||
|
||||
Th custom environment includes a list of pubs and methods to allow agents to discover and enter pubs.
|
||||
There are two types of agents:
|
||||
|
||||
* Patron. A patron will do three things, in this order:
|
||||
* Look for other patrons to drink with
|
||||
* Look for a pub where the agent and other agents in the same group can get in.
|
||||
* While in the pub, patrons only drink, until they get drunk and taken home.
|
||||
* Police. There is only one police agent that will take any drunk patrons home (kick them out of the pub).
|
174
examples/pubcrawl/pubcrawl.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from soil.agents import FSM, state, default_state
|
||||
from soil import Environment
|
||||
from random import random, shuffle
|
||||
from itertools import islice
|
||||
import logging
|
||||
|
||||
|
||||
class CityPubs(Environment):
|
||||
'''Environment with Pubs'''
|
||||
level = logging.INFO
|
||||
|
||||
def __init__(self, *args, number_of_pubs=3, pub_capacity=10, **kwargs):
|
||||
super(CityPubs, self).__init__(*args, **kwargs)
|
||||
pubs = {}
|
||||
for i in range(number_of_pubs):
|
||||
newpub = {
|
||||
'name': 'The awesome pub #{}'.format(i),
|
||||
'open': True,
|
||||
'capacity': pub_capacity,
|
||||
'occupancy': 0,
|
||||
}
|
||||
pubs[newpub['name']] = newpub
|
||||
self['pubs'] = pubs
|
||||
|
||||
def enter(self, pub_id, *nodes):
|
||||
'''Agents will try to enter. The pub checks if it is possible'''
|
||||
try:
|
||||
pub = self['pubs'][pub_id]
|
||||
except KeyError:
|
||||
raise ValueError('Pub {} is not available'.format(pub_id))
|
||||
if not pub['open'] or (pub['capacity'] < (len(nodes) + pub['occupancy'])):
|
||||
return False
|
||||
pub['occupancy'] += len(nodes)
|
||||
for node in nodes:
|
||||
node['pub'] = pub_id
|
||||
return True
|
||||
|
||||
def available_pubs(self):
|
||||
for pub in self['pubs'].values():
|
||||
if pub['open'] and (pub['occupancy'] < pub['capacity']):
|
||||
yield pub['name']
|
||||
|
||||
def exit(self, pub_id, *node_ids):
|
||||
'''Agents will notify the pub they want to leave'''
|
||||
try:
|
||||
pub = self['pubs'][pub_id]
|
||||
except KeyError:
|
||||
raise ValueError('Pub {} is not available'.format(pub_id))
|
||||
for node_id in node_ids:
|
||||
node = self.get_agent(node_id)
|
||||
if pub_id == node['pub']:
|
||||
del node['pub']
|
||||
pub['occupancy'] -= 1
|
||||
|
||||
|
||||
class Patron(FSM):
|
||||
'''Agent that looks for friends to drink with. It will do three things:
|
||||
1) Look for other patrons to drink with
|
||||
2) Look for a bar where the agent and other agents in the same group can get in.
|
||||
3) While in the bar, patrons only drink, until they get drunk and taken home.
|
||||
'''
|
||||
level = logging.INFO
|
||||
|
||||
defaults = {
|
||||
'pub': None,
|
||||
'drunk': False,
|
||||
'pints': 0,
|
||||
'max_pints': 3,
|
||||
}
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def looking_for_friends(self):
|
||||
'''Look for friends to drink with'''
|
||||
self.info('I am looking for friends')
|
||||
available_friends = list(self.get_agents(drunk=False,
|
||||
pub=None,
|
||||
state_id=self.looking_for_friends.id))
|
||||
if not available_friends:
|
||||
self.info('Life sucks and I\'m alone!')
|
||||
return self.at_home
|
||||
befriended = self.try_friends(available_friends)
|
||||
if befriended:
|
||||
return self.looking_for_pub
|
||||
|
||||
@state
|
||||
def looking_for_pub(self):
|
||||
'''Look for a pub that accepts me and my friends'''
|
||||
if self['pub'] != None:
|
||||
return self.sober_in_pub
|
||||
self.debug('I am looking for a pub')
|
||||
group = list(self.get_neighboring_agents())
|
||||
for pub in self.env.available_pubs():
|
||||
self.debug('We\'re trying to get into {}: total: {}'.format(pub, len(group)))
|
||||
if self.env.enter(pub, self, *group):
|
||||
self.info('We\'re all {} getting in {}!'.format(len(group), pub))
|
||||
return self.sober_in_pub
|
||||
|
||||
@state
|
||||
def sober_in_pub(self):
|
||||
'''Drink up.'''
|
||||
self.drink()
|
||||
if self['pints'] > self['max_pints']:
|
||||
return self.drunk_in_pub
|
||||
|
||||
@state
|
||||
def drunk_in_pub(self):
|
||||
'''I'm out. Take me home!'''
|
||||
self.info('I\'m so drunk. Take me home!')
|
||||
self['drunk'] = True
|
||||
pass # out drunk
|
||||
|
||||
@state
|
||||
def at_home(self):
|
||||
'''The end'''
|
||||
self.debug('Life sucks. I\'m home!')
|
||||
|
||||
def drink(self):
|
||||
self['pints'] += 1
|
||||
self.debug('Cheers to that')
|
||||
|
||||
def kick_out(self):
|
||||
self.set_state(self.at_home)
|
||||
|
||||
def befriend(self, other_agent, force=False):
|
||||
'''
|
||||
Try to become friends with another agent. The chances of
|
||||
success depend on both agents' openness.
|
||||
'''
|
||||
if force or self['openness'] > random():
|
||||
self.env.add_edge(self, other_agent)
|
||||
self.info('Made some friend {}'.format(other_agent))
|
||||
return True
|
||||
return False
|
||||
|
||||
def try_friends(self, others):
|
||||
''' Look for random agents around me and try to befriend them'''
|
||||
befriended = False
|
||||
k = int(10*self['openness'])
|
||||
shuffle(others)
|
||||
for friend in islice(others, k): # random.choice >= 3.7
|
||||
if friend == self:
|
||||
continue
|
||||
if friend.befriend(self):
|
||||
self.befriend(friend, force=True)
|
||||
self.debug('Hooray! new friend: {}'.format(friend.id))
|
||||
befriended = True
|
||||
else:
|
||||
self.debug('{} does not want to be friends'.format(friend.id))
|
||||
return befriended
|
||||
|
||||
|
||||
class Police(FSM):
|
||||
'''Simple agent to take drunk people out of pubs.'''
|
||||
level = logging.INFO
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def patrol(self):
|
||||
drunksters = list(self.get_agents(drunk=True,
|
||||
state_id=Patron.drunk_in_pub.id))
|
||||
for drunk in drunksters:
|
||||
self.info('Kicking out the trash: {}'.format(drunk.id))
|
||||
drunk.kick_out()
|
||||
else:
|
||||
self.info('No trash to take out. Too bad.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from soil import simulation
|
||||
simulation.run_from_config('pubcrawl.yml',
|
||||
dry_run=True,
|
||||
dump=None,
|
||||
parallel=False)
|
26
examples/pubcrawl/pubcrawl.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: pubcrawl
|
||||
num_trials: 3
|
||||
max_time: 10
|
||||
dump: false
|
||||
network_params:
|
||||
# Generate 100 empty nodes. They will be assigned a network agent
|
||||
generator: empty_graph
|
||||
n: 30
|
||||
network_agents:
|
||||
- agent_type: pubcrawl.Patron
|
||||
description: Extroverted patron
|
||||
state:
|
||||
openness: 1.0
|
||||
weight: 9
|
||||
- agent_type: pubcrawl.Patron
|
||||
description: Introverted patron
|
||||
state:
|
||||
openness: 0.1
|
||||
weight: 1
|
||||
environment_agents:
|
||||
- agent_type: pubcrawl.Police
|
||||
environment_class: pubcrawl.CityPubs
|
||||
environment_params:
|
||||
altercations: 0
|
||||
number_of_pubs: 3
|
@@ -1,7 +1,7 @@
|
||||
---
|
||||
load_module: rabbit_agents
|
||||
name: rabbits_example
|
||||
max_time: 1200
|
||||
max_time: 500
|
||||
interval: 1
|
||||
seed: MySeed
|
||||
agent_type: RabbitModel
|
||||
|
@@ -12327,7 +12327,7 @@ Notice how node 0 is the only one with a TV.</p>
|
||||
<span class="n">MAX_TIME</span> <span class="o">=</span> <span class="mi">100</span>
|
||||
<span class="n">EVENT_TIME</span> <span class="o">=</span> <span class="mi">10</span>
|
||||
|
||||
<span class="n">sim</span> <span class="o">=</span> <span class="n">soil</span><span class="o">.</span><span class="n">simulation</span><span class="o">.</span><span class="n">SoilSimulation</span><span class="p">(</span><span class="n">topology</span><span class="o">=</span><span class="n">G</span><span class="p">,</span>
|
||||
<span class="n">sim</span> <span class="o">=</span> <span class="n">soil</span><span class="o">.</span><span class="n">Simulation</span><span class="p">(</span><span class="n">topology</span><span class="o">=</span><span class="n">G</span><span class="p">,</span>
|
||||
<span class="n">num_trials</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span>
|
||||
<span class="n">max_time</span><span class="o">=</span><span class="n">MAX_TIME</span><span class="p">,</span>
|
||||
<span class="n">environment_agents</span><span class="o">=</span><span class="p">[{</span><span class="s1">'agent_type'</span><span class="p">:</span> <span class="n">NewsEnvironmentAgent</span><span class="p">,</span>
|
||||
@@ -21883,7 +21883,7 @@ bgAAAABJRU5ErkJggg==
|
||||
|
||||
|
||||
<div class="output_subarea output_stream output_stdout output_text">
|
||||
<pre>267M ../rabbits/soil_output/rabbits_example/
|
||||
<pre>267M ../rabbits/soil_output/rabbits_example/
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
nxsim
|
||||
simpy
|
||||
networkx
|
||||
networkx>=2.0
|
||||
numpy
|
||||
matplotlib
|
||||
pyyaml
|
||||
|
4
setup.cfg
Normal file
@@ -0,0 +1,4 @@
|
||||
[aliases]
|
||||
test=pytest
|
||||
[tool:pytest]
|
||||
addopts = --verbose
|
36
setup.py
@@ -1,20 +1,21 @@
|
||||
import pip
|
||||
import os
|
||||
from setuptools import setup
|
||||
# parse_requirements() returns generator of pip.req.InstallRequirement objects
|
||||
from pip.req import parse_requirements
|
||||
from soil import __version__
|
||||
|
||||
try:
|
||||
install_reqs = parse_requirements(
|
||||
"requirements.txt", session=pip.download.PipSession())
|
||||
test_reqs = parse_requirements(
|
||||
"test-requirements.txt", session=pip.download.PipSession())
|
||||
except AttributeError:
|
||||
install_reqs = parse_requirements("requirements.txt")
|
||||
test_reqs = parse_requirements("test-requirements.txt")
|
||||
|
||||
install_reqs = [str(ir.req) for ir in install_reqs]
|
||||
test_reqs = [str(ir.req) for ir in test_reqs]
|
||||
with open(os.path.join('soil', 'VERSION')) as f:
|
||||
__version__ = f.readlines()[0].strip()
|
||||
assert __version__
|
||||
|
||||
|
||||
def parse_requirements(filename):
|
||||
""" load requirements from a pip requirements file """
|
||||
with open(filename, 'r') as f:
|
||||
lineiter = list(line.strip() for line in f)
|
||||
return [line for line in lineiter if line and not line.startswith("#")]
|
||||
|
||||
|
||||
install_reqs = parse_requirements("requirements.txt")
|
||||
test_reqs = parse_requirements("test-requirements.txt")
|
||||
|
||||
|
||||
setup(
|
||||
@@ -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
@@ -0,0 +1 @@
|
||||
0.13.0
|
@@ -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:
|
||||
|
@@ -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))
|
||||
|
@@ -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"))
|
||||
|
@@ -10,7 +10,7 @@ class SISaModel(FSM):
|
||||
|
||||
neutral_discontent_infected_prob
|
||||
|
||||
neutral_content_spong_prob
|
||||
neutral_content_spon_prob
|
||||
|
||||
neutral_content_infected_prob
|
||||
|
||||
@@ -29,27 +29,27 @@ class SISaModel(FSM):
|
||||
standard_variance
|
||||
"""
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
def __init__(self, environment, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
|
||||
self.neutral_discontent_spon_prob = np.random.normal(environment.environment_params['neutral_discontent_spon_prob'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.neutral_discontent_infected_prob = np.random.normal(environment.environment_params['neutral_discontent_infected_prob'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.neutral_content_spon_prob = np.random.normal(environment.environment_params['neutral_content_spon_prob'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.neutral_content_infected_prob = np.random.normal(environment.environment_params['neutral_content_infected_prob'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.neutral_discontent_spon_prob = np.random.normal(self.env['neutral_discontent_spon_prob'],
|
||||
self.env['standard_variance'])
|
||||
self.neutral_discontent_infected_prob = np.random.normal(self.env['neutral_discontent_infected_prob'],
|
||||
self.env['standard_variance'])
|
||||
self.neutral_content_spon_prob = np.random.normal(self.env['neutral_content_spon_prob'],
|
||||
self.env['standard_variance'])
|
||||
self.neutral_content_infected_prob = np.random.normal(self.env['neutral_content_infected_prob'],
|
||||
self.env['standard_variance'])
|
||||
|
||||
self.discontent_neutral = np.random.normal(environment.environment_params['discontent_neutral'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.discontent_content = np.random.normal(environment.environment_params['discontent_content'],
|
||||
environment.environment_params['variance_d_c'])
|
||||
self.discontent_neutral = np.random.normal(self.env['discontent_neutral'],
|
||||
self.env['standard_variance'])
|
||||
self.discontent_content = np.random.normal(self.env['discontent_content'],
|
||||
self.env['variance_d_c'])
|
||||
|
||||
self.content_discontent = np.random.normal(environment.environment_params['content_discontent'],
|
||||
environment.environment_params['variance_c_d'])
|
||||
self.content_neutral = np.random.normal(environment.environment_params['content_neutral'],
|
||||
environment.environment_params['standard_variance'])
|
||||
self.content_discontent = np.random.normal(self.env['content_discontent'],
|
||||
self.env['variance_c_d'])
|
||||
self.content_neutral = np.random.normal(self.env['content_neutral'],
|
||||
self.env['standard_variance'])
|
||||
|
||||
@state
|
||||
def neutral(self):
|
||||
|
@@ -16,7 +16,7 @@ class SentimentCorrelationModel(BaseAgent):
|
||||
disgust_prob
|
||||
"""
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
def __init__(self, environment, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
self.outside_effects_prob = environment.environment_params['outside_effects_prob']
|
||||
self.anger_prob = environment.environment_params['anger_prob']
|
||||
|
@@ -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 *
|
||||
|
@@ -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
|
||||
|
@@ -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
@@ -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')
|
@@ -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)
|
||||
|
115
soil/utils.py
@@ -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
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ROOT = os.path.dirname(__file__)
|
||||
DEFAULT_FILE = os.path.join(ROOT, 'VERSION')
|
||||
|
||||
|
||||
def read_version(versionfile=DEFAULT_FILE):
|
||||
try:
|
||||
with open(versionfile) as f:
|
||||
return f.read().strip()
|
||||
except IOError: # pragma: no cover
|
||||
logger.error(('Running an unknown version of {}.'
|
||||
'Be careful!.').format(__name__))
|
||||
return '0.0'
|
||||
|
||||
|
||||
__version__ = read_version()
|
4
soil/web/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
output/
|
||||
tests/
|
||||
soil_output/
|
59
soil/web/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Graph Visualization with D3.js
|
||||
|
||||
The aim of this software is to provide a useful tool for visualising and analysing the result of different simulations based on graph. Once you run the simulation, you will be able to interact with the simulation in real time.
|
||||
|
||||
For this purpose, a model which tries to simulate the spread of information to comprehend the radicalism spread in a society is included. Whith all this, the main project goals could be divided in five as it is shown in the following.
|
||||
|
||||
* Simulate the spread of information through a network applied to radicalism.
|
||||
* Visualize the results of the simulation.
|
||||
* Interact with the simulation in real time.
|
||||
* Extract data from the results.
|
||||
* Show data in a right way for its research.
|
||||
|
||||
## Deploying the server
|
||||
|
||||
For deploying the application, you will only need to run the following command.
|
||||
|
||||
`python3 run.py [--name NAME] [--dump] [--port PORT] [--verbose]`
|
||||
|
||||
Where the options are detailed in the following table.
|
||||
|
||||
| Option | Description |
|
||||
| --- | --- |
|
||||
| `--name NAME` | The name of the simulation. It will appear on the app. |
|
||||
| `--dump` | For dumping the results in server side. |
|
||||
| `--port PORT` | The port where the server will listen. |
|
||||
| `--verbose` | Verbose mode. |
|
||||
|
||||
> You can dump the results of the simulation in server side. Anyway, you will be able to download them in GEXF or JSON Graph format directly from the browser.
|
||||
|
||||
## Visualization Params
|
||||
|
||||
The configuration of the simulation is based on the simulator configuration. In this case, it follows the [SOIL](https://github.com/gsi-upm/soil) configuration syntax and for visualising the results in a more comfortable way, more params can be added in `visualization_params` dictionary.
|
||||
|
||||
* For setting a background image, the tag needed is `background image`. You can also add a `background_opacity` and `background_filter_color` if the image is so clear than you can difficult view the nodes.
|
||||
* For setting colors to the nodes, you can do it based on their properties values. Using the `color` tag, you will need to indicate the attribute key and value, and then the color you want to apply.
|
||||
* The shapes applied to a group of nodes are always the same. This means than it won't change dynamically, so you will have to indicate the property with the `shape_property` tag and add a dictionary called `shapes` in which for each value, you indicate the shape.
|
||||
All shapes have to had been downloaded before in SVG format and added to the server.
|
||||
|
||||
An example of this configuration applied to the TerroristNetworkModel is presented.
|
||||
|
||||
```yaml
|
||||
visualization_params:
|
||||
# Icons downloaded from https://www.iconfinder.com/
|
||||
shape_property: agent
|
||||
shapes:
|
||||
TrainingAreaModel: target
|
||||
HavenModel: home
|
||||
TerroristNetworkModel: person
|
||||
colors:
|
||||
- attr_id: 0
|
||||
color: '#40de40'
|
||||
- attr_id: 1
|
||||
color: red
|
||||
- attr_id: 2
|
||||
color: '#c16a6a'
|
||||
background_image: 'map_4800x2860.jpg'
|
||||
background_opacity: '0.9'
|
||||
background_filter_color: 'blue'
|
||||
```
|
255
soil/web/TerroristNetworkModel.py
Normal file
@@ -0,0 +1,255 @@
|
||||
import random
|
||||
import networkx as nx
|
||||
from soil.agents import BaseAgent, FSM, state, default_state
|
||||
from scipy.spatial import cKDTree as KDTree
|
||||
|
||||
global betweenness_centrality_global
|
||||
global degree_centrality_global
|
||||
|
||||
betweenness_centrality_global = None
|
||||
degree_centrality_global = None
|
||||
|
||||
class TerroristSpreadModel(FSM):
|
||||
"""
|
||||
Settings:
|
||||
information_spread_intensity
|
||||
|
||||
terrorist_additional_influence
|
||||
|
||||
min_vulnerability (optional else zero)
|
||||
|
||||
max_vulnerability
|
||||
|
||||
prob_interaction
|
||||
"""
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
|
||||
global betweenness_centrality_global
|
||||
global degree_centrality_global
|
||||
|
||||
if betweenness_centrality_global == None:
|
||||
betweenness_centrality_global = nx.betweenness_centrality(self.global_topology)
|
||||
if degree_centrality_global == None:
|
||||
degree_centrality_global = nx.degree_centrality(self.global_topology)
|
||||
|
||||
self.information_spread_intensity = environment.environment_params['information_spread_intensity']
|
||||
self.terrorist_additional_influence = environment.environment_params['terrorist_additional_influence']
|
||||
self.prob_interaction = environment.environment_params['prob_interaction']
|
||||
|
||||
if self['id'] == self.civilian.id: # Civilian
|
||||
self.initial_belief = random.uniform(0.00, 0.5)
|
||||
elif self['id'] == self.terrorist.id: # Terrorist
|
||||
self.initial_belief = random.uniform(0.8, 1.00)
|
||||
elif self['id'] == self.leader.id: # Leader
|
||||
self.initial_belief = 1.00
|
||||
else:
|
||||
raise Exception('Invalid state id: {}'.format(self['id']))
|
||||
|
||||
if 'min_vulnerability' in environment.environment_params:
|
||||
self.vulnerability = random.uniform( environment.environment_params['min_vulnerability'], environment.environment_params['max_vulnerability'] )
|
||||
else :
|
||||
self.vulnerability = random.uniform( 0, environment.environment_params['max_vulnerability'] )
|
||||
|
||||
self.mean_belief = self.initial_belief
|
||||
self.betweenness_centrality = betweenness_centrality_global[self.id]
|
||||
self.degree_centrality = degree_centrality_global[self.id]
|
||||
|
||||
# self.state['radicalism'] = self.mean_belief
|
||||
|
||||
def count_neighboring_agents(self, state_id=None):
|
||||
if isinstance(state_id, list):
|
||||
return len(self.get_neighboring_agents(state_id))
|
||||
else:
|
||||
return len(super().get_agents(state_id, limit_neighbors=True))
|
||||
|
||||
def get_neighboring_agents(self, state_id=None):
|
||||
if isinstance(state_id, list):
|
||||
_list = []
|
||||
for i in state_id:
|
||||
_list += super().get_agents(i, limit_neighbors=True)
|
||||
return [ neighbour for neighbour in _list if isinstance(neighbour, TerroristSpreadModel) ]
|
||||
else:
|
||||
_list = super().get_agents(state_id, limit_neighbors=True)
|
||||
return [ neighbour for neighbour in _list if isinstance(neighbour, TerroristSpreadModel) ]
|
||||
|
||||
@state
|
||||
def civilian(self):
|
||||
if self.count_neighboring_agents() > 0:
|
||||
neighbours = []
|
||||
for neighbour in self.get_neighboring_agents():
|
||||
if random.random() < self.prob_interaction:
|
||||
neighbours.append(neighbour)
|
||||
influence = sum( neighbour.degree_centrality for neighbour in neighbours )
|
||||
mean_belief = sum( neighbour.mean_belief * neighbour.degree_centrality / influence for neighbour in neighbours )
|
||||
self.initial_belief = self.mean_belief
|
||||
mean_belief = mean_belief * self.information_spread_intensity + self.initial_belief * ( 1 - self.information_spread_intensity )
|
||||
self.mean_belief = mean_belief * self.vulnerability + self.initial_belief * ( 1 - self.vulnerability )
|
||||
|
||||
if self.mean_belief >= 0.8:
|
||||
return self.terrorist
|
||||
|
||||
@state
|
||||
def leader(self):
|
||||
self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence )
|
||||
if self.count_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]) > 0:
|
||||
for neighbour in self.get_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]):
|
||||
if neighbour.betweenness_centrality > self.betweenness_centrality:
|
||||
return self.terrorist
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
if self.count_neighboring_agents(state_id=[self.terrorist.id, self.leader.id]) > 0:
|
||||
neighbours = self.get_neighboring_agents(state_id=[self.terrorist.id, self.leader.id])
|
||||
influence = sum( neighbour.degree_centrality for neighbour in neighbours )
|
||||
mean_belief = sum( neighbour.mean_belief * neighbour.degree_centrality / influence for neighbour in neighbours )
|
||||
self.initial_belief = self.mean_belief
|
||||
self.mean_belief = mean_belief * self.vulnerability + self.initial_belief * ( 1 - self.vulnerability )
|
||||
self.mean_belief = self.mean_belief ** ( 1 - self.terrorist_additional_influence )
|
||||
|
||||
if self.count_neighboring_agents(state_id=self.leader.id) == 0 and self.count_neighboring_agents(state_id=self.terrorist.id) > 0:
|
||||
max_betweenness_centrality = self
|
||||
for neighbour in self.get_neighboring_agents(state_id=self.terrorist.id):
|
||||
if neighbour.betweenness_centrality > max_betweenness_centrality.betweenness_centrality:
|
||||
max_betweenness_centrality = neighbour
|
||||
if max_betweenness_centrality == self:
|
||||
return self.leader
|
||||
|
||||
def add_edge(self, G, source, target):
|
||||
G.add_edge(source.id, target.id, start=self.env._now)
|
||||
|
||||
def link_search(self, G, node, radius):
|
||||
pos = nx.get_node_attributes(G, 'pos')
|
||||
nodes, coords = list(zip(*pos.items()))
|
||||
kdtree = KDTree(coords) # Cannot provide generator.
|
||||
edge_indexes = kdtree.query_pairs(radius, 2)
|
||||
_list = [ edge[int(not edge.index(node))] for edge in edge_indexes if node in edge ]
|
||||
return [ G.nodes()[index]['agent'] for index in _list ]
|
||||
|
||||
def social_search(self, G, node, steps):
|
||||
nodes = list(nx.ego_graph(G, node, radius=steps).nodes())
|
||||
nodes.remove(node)
|
||||
return [ G.nodes()[index]['agent'] for index in nodes ]
|
||||
|
||||
|
||||
class TrainingAreaModel(FSM):
|
||||
"""
|
||||
Settings:
|
||||
training_influence
|
||||
|
||||
min_vulnerability
|
||||
|
||||
Requires TerroristSpreadModel.
|
||||
"""
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
self.training_influence = environment.environment_params['training_influence']
|
||||
if 'min_vulnerability' in environment.environment_params:
|
||||
self.min_vulnerability = environment.environment_params['min_vulnerability']
|
||||
else: self.min_vulnerability = 0
|
||||
|
||||
@default_state
|
||||
@state
|
||||
def terrorist(self):
|
||||
for neighbour in self.get_neighboring_agents():
|
||||
if isinstance(neighbour, TerroristSpreadModel) and neighbour.vulnerability > self.min_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.training_influence )
|
||||
|
||||
|
||||
class HavenModel(FSM):
|
||||
"""
|
||||
Settings:
|
||||
haven_influence
|
||||
|
||||
min_vulnerability
|
||||
|
||||
max_vulnerability
|
||||
|
||||
Requires TerroristSpreadModel.
|
||||
"""
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
self.haven_influence = environment.environment_params['haven_influence']
|
||||
if 'min_vulnerability' in environment.environment_params:
|
||||
self.min_vulnerability = environment.environment_params['min_vulnerability']
|
||||
else: self.min_vulnerability = 0
|
||||
self.max_vulnerability = environment.environment_params['max_vulnerability']
|
||||
|
||||
@state
|
||||
def civilian(self):
|
||||
for neighbour_agent in self.get_neighboring_agents():
|
||||
if isinstance(neighbour_agent, TerroristSpreadModel) and neighbour_agent['id'] == neighbour_agent.civilian.id:
|
||||
for neighbour in self.get_neighboring_agents():
|
||||
if isinstance(neighbour, TerroristSpreadModel) and neighbour.vulnerability > self.min_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability * ( 1 - self.haven_influence )
|
||||
return self.civilian
|
||||
return self.terrorist
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
for neighbour in self.get_neighboring_agents():
|
||||
if isinstance(neighbour, TerroristSpreadModel) and neighbour.vulnerability < self.max_vulnerability:
|
||||
neighbour.vulnerability = neighbour.vulnerability ** ( 1 - self.haven_influence )
|
||||
return self.terrorist
|
||||
|
||||
|
||||
class TerroristNetworkModel(TerroristSpreadModel):
|
||||
"""
|
||||
Settings:
|
||||
sphere_influence
|
||||
|
||||
vision_range
|
||||
|
||||
weight_social_distance
|
||||
|
||||
weight_link_distance
|
||||
"""
|
||||
|
||||
def __init__(self, environment=None, agent_id=0, state=()):
|
||||
super().__init__(environment=environment, agent_id=agent_id, state=state)
|
||||
|
||||
self.vision_range = environment.environment_params['vision_range']
|
||||
self.sphere_influence = environment.environment_params['sphere_influence']
|
||||
self.weight_social_distance = environment.environment_params['weight_social_distance']
|
||||
self.weight_link_distance = environment.environment_params['weight_link_distance']
|
||||
|
||||
@state
|
||||
def terrorist(self):
|
||||
self.update_relationships()
|
||||
return super().terrorist()
|
||||
|
||||
@state
|
||||
def leader(self):
|
||||
self.update_relationships()
|
||||
return super().leader()
|
||||
|
||||
def update_relationships(self):
|
||||
if self.count_neighboring_agents(state_id=self.civilian.id) == 0:
|
||||
close_ups = self.link_search(self.global_topology, self.id, self.vision_range)
|
||||
step_neighbours = self.social_search(self.global_topology, self.id, self.sphere_influence)
|
||||
search = list(set(close_ups).union(step_neighbours))
|
||||
neighbours = self.get_neighboring_agents()
|
||||
search = [item for item in search if not item in neighbours and isinstance(item, TerroristNetworkModel)]
|
||||
for agent in search:
|
||||
social_distance = 1 / self.shortest_path_length(self.global_topology, self.id, agent.id)
|
||||
spatial_proximity = ( 1 - self.get_distance(self.global_topology, self.id, agent.id) )
|
||||
prob_new_interaction = self.weight_social_distance * social_distance + self.weight_link_distance * spatial_proximity
|
||||
if agent['id'] == agent.civilian.id and random.random() < prob_new_interaction:
|
||||
self.add_edge(self.global_topology, self, agent)
|
||||
break
|
||||
|
||||
def get_distance(self, G, source, target):
|
||||
source_x, source_y = nx.get_node_attributes(G, 'pos')[source]
|
||||
target_x, target_y = nx.get_node_attributes(G, 'pos')[target]
|
||||
dx = abs( source_x - target_x )
|
||||
dy = abs( source_y - target_y )
|
||||
return ( dx ** 2 + dy ** 2 ) ** ( 1 / 2 )
|
||||
|
||||
def shortest_path_length(self, G, source, target):
|
||||
try:
|
||||
return nx.shortest_path_length(G, source, target)
|
||||
except nx.NetworkXNoPath:
|
||||
return float('inf')
|
62
soil/web/TerroristNetworkModel.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
name: TerroristNetworkModel_sim
|
||||
load_module: TerroristNetworkModel
|
||||
max_time: 150
|
||||
num_trials: 1
|
||||
network_params:
|
||||
generator: random_geometric_graph
|
||||
radius: 0.2
|
||||
# generator: geographical_threshold_graph
|
||||
# theta: 20
|
||||
n: 100
|
||||
network_agents:
|
||||
- agent_type: TerroristNetworkModel
|
||||
weight: 0.8
|
||||
state:
|
||||
id: civilian # Civilians
|
||||
- agent_type: TerroristNetworkModel
|
||||
weight: 0.1
|
||||
state:
|
||||
id: leader # Leaders
|
||||
- agent_type: TrainingAreaModel
|
||||
weight: 0.05
|
||||
state:
|
||||
id: terrorist # Terrorism
|
||||
- agent_type: HavenModel
|
||||
weight: 0.05
|
||||
state:
|
||||
id: civilian # Civilian
|
||||
|
||||
environment_params:
|
||||
# TerroristSpreadModel
|
||||
information_spread_intensity: 0.7
|
||||
terrorist_additional_influence: 0.035
|
||||
max_vulnerability: 0.7
|
||||
prob_interaction: 0.5
|
||||
|
||||
# TrainingAreaModel and HavenModel
|
||||
training_influence: 0.20
|
||||
haven_influence: 0.20
|
||||
|
||||
# TerroristNetworkModel
|
||||
vision_range: 0.30
|
||||
sphere_influence: 2
|
||||
weight_social_distance: 0.035
|
||||
weight_link_distance: 0.035
|
||||
|
||||
visualization_params:
|
||||
# Icons downloaded from https://www.iconfinder.com/
|
||||
shape_property: agent
|
||||
shapes:
|
||||
TrainingAreaModel: target
|
||||
HavenModel: home
|
||||
TerroristNetworkModel: person
|
||||
colors:
|
||||
- attr_id: civilian
|
||||
color: '#40de40'
|
||||
- attr_id: terrorist
|
||||
color: red
|
||||
- attr_id: leader
|
||||
color: '#c16a6a'
|
||||
background_image: 'map_4800x2860.jpg'
|
||||
background_opacity: '0.9'
|
||||
background_filter_color: 'blue'
|
274
soil/web/__init__.py
Normal 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
@@ -0,0 +1,5 @@
|
||||
from . import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
25
soil/web/config.yml
Normal 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
@@ -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)
|
431
soil/web/static/css/main.css
Normal 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;
|
||||
}
|
72
soil/web/static/css/timeline.css
Normal 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;
|
||||
}
|
BIN
soil/web/static/img/background/map.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
soil/web/static/img/background/map_4800x2860.jpg
Normal file
After Width: | Height: | Size: 8.0 MiB |
140
soil/web/static/img/logo_gsi.svg
Normal 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 |
BIN
soil/web/static/img/logo_soil.png
Normal file
After Width: | Height: | Size: 101 KiB |
1
soil/web/static/img/svg/home.svg
Normal 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 |
1
soil/web/static/img/svg/person.svg
Normal 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 |
1
soil/web/static/img/svg/plus.svg
Normal 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 |
1
soil/web/static/img/svg/target.svg
Normal 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 |
1
soil/web/static/img/svg/time.svg
Normal 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
@@ -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;
|
||||
}
|
99
soil/web/static/js/template.js
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
429
soil/web/static/js/timeline.js
Normal 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;
|
||||
|
||||
}
|
||||
}));
|
686
soil/web/static/js/visualization.js
Normal 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);
|
378
soil/web/templates/index.html
Normal 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 & 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">×</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">×</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">×</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>
|
@@ -0,0 +1 @@
|
||||
pytest
|
16
tests/test.csv
Normal 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
|
|
12
tests/test.gexf
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<gexf version="1.2" xmlns="http://www.gexf.net/1.2draft" xmlns:viz="http://www.gexf.net/1.2draft/viz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<graph defaultedgetype="undirected" mode="static">
|
||||
<nodes>
|
||||
<node id="0" label="0" />
|
||||
<node id="1" label="1" />
|
||||
</nodes>
|
||||
<edges>
|
||||
<edge id="0" source="0" target="1" />
|
||||
</edges>
|
||||
</graph>
|
||||
</gexf>
|
90
tests/test_analysis.py
Normal 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
@@ -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
@@ -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
|
@@ -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"
|
||||
|