Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
9165979b49 | ||
|
078f8ace9e | ||
|
8fec544772 | ||
|
5420501d36 | ||
|
5d89827ccf | ||
|
fc48ed7e09 | ||
|
73c90887e8 |
@@ -1,3 +1,5 @@
|
||||
FROM python:3.4-onbuild
|
||||
|
||||
RUN pip install '.[web]'
|
||||
|
||||
ENTRYPOINT ["python", "-m", "soil"]
|
||||
|
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
version: '3'
|
||||
services:
|
||||
dev:
|
||||
build: .
|
||||
volumes:
|
||||
- .:/usr/src/app
|
||||
tty: true
|
||||
entrypoint: /bin/bash
|
||||
ports:
|
||||
- '8001:8001'
|
334
examples/NewsSpread.ipynb
Normal file
@@ -1,6 +1,6 @@
|
||||
nxsim
|
||||
simpy
|
||||
networkx
|
||||
networkx>=2.0
|
||||
numpy
|
||||
matplotlib
|
||||
pyyaml
|
||||
|
34
setup.py
@@ -1,21 +1,22 @@
|
||||
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:
|
||||
|
||||
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")
|
||||
|
||||
install_reqs = [str(ir.req) for ir in install_reqs]
|
||||
test_reqs = [str(ir.req) for ir in test_reqs]
|
||||
|
||||
|
||||
setup(
|
||||
name='soil',
|
||||
@@ -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.12.0
|
@@ -4,7 +4,7 @@ import os
|
||||
import pdb
|
||||
import logging
|
||||
|
||||
__version__ = "0.10.1"
|
||||
from .version import __version__
|
||||
|
||||
try:
|
||||
basestring
|
||||
@@ -62,7 +62,7 @@ def main():
|
||||
simulation.run_from_config(args.file,
|
||||
dry_run=args.dry_run,
|
||||
dump=dump,
|
||||
parallel=(not args.synchronous),
|
||||
parallel=(not args.synchronous and not args.pdb),
|
||||
results_dir=args.output)
|
||||
except Exception as ex:
|
||||
if args.pdb:
|
||||
|
@@ -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))
|
||||
|
@@ -14,7 +14,7 @@ import json
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from .. import utils
|
||||
from .. import utils, history
|
||||
|
||||
agent_types = {}
|
||||
|
||||
@@ -32,33 +32,77 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
|
||||
|
||||
defaults = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, environment=None, agent_id=None, state=None,
|
||||
name='network_process', interval=None, **state_params):
|
||||
# Check for REQUIRED arguments
|
||||
assert environment is not None, TypeError('__init__ missing 1 required keyword argument: \'environment\'. '
|
||||
'Cannot be NoneType.')
|
||||
# Initialize agent parameters
|
||||
self.id = agent_id
|
||||
self.name = name
|
||||
self.state_params = state_params
|
||||
|
||||
# Global parameters
|
||||
self.global_topology = environment.G
|
||||
self.environment_params = environment.environment_params
|
||||
|
||||
# Register agent to environment
|
||||
self.env = environment
|
||||
|
||||
self._neighbors = None
|
||||
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.env.name, 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,6 +116,11 @@ class BaseAgent(nxsim.BaseAgent, metaclass=MetaAgent):
|
||||
return None
|
||||
|
||||
def run(self):
|
||||
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()
|
||||
@@ -95,7 +144,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
|
||||
@@ -197,11 +246,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:
|
||||
@@ -215,7 +266,7 @@ 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
|
||||
|
||||
|
||||
|
@@ -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,18 +51,7 @@ 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
@@ -5,16 +5,26 @@ 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, agents
|
||||
from . import utils, agents, analysis, history
|
||||
|
||||
|
||||
class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
"""
|
||||
The environment is key in a simulation. It contains the network topology,
|
||||
a reference to network and environment agents, as well as the environment
|
||||
params, which are used as shared state between agents.
|
||||
|
||||
The environment parameters and the state of every agent can be accessed
|
||||
both by using the environment as a dictionary or with the environment's
|
||||
:meth:`soil.environment.SoilEnvironment.get` method.
|
||||
"""
|
||||
|
||||
def __init__(self, name=None,
|
||||
network_agents=None,
|
||||
@@ -38,28 +48,16 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
self._env_agents = {}
|
||||
self.dry_run = dry_run
|
||||
self.interval = interval
|
||||
self.dir_path = dir_path or tempfile.mkdtemp('soil-env')
|
||||
self.get_path()
|
||||
self._history = history.History(name=self.name if not dry_run else None,
|
||||
dir_path=self.dir_path)
|
||||
# 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 []
|
||||
self.dir_path = dir_path or tempfile.mkdtemp('soil-env')
|
||||
if self.dry_run:
|
||||
self._db_path = ":memory:"
|
||||
else:
|
||||
self._db_path = os.path.join(self.get_path(), '{}.db.sqlite'.format(self.name))
|
||||
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):
|
||||
@@ -95,10 +93,8 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
if not network_agents:
|
||||
return
|
||||
for ix in self.G.nodes():
|
||||
i = ix
|
||||
node = self.G.node[i]
|
||||
agent, state = agents._agent_from_distribution(network_agents)
|
||||
self.set_agent(i, agent_type=agent, state=state)
|
||||
self.set_agent(ix, agent_type=agent, state=state)
|
||||
|
||||
def set_agent(self, agent_id, agent_type, state=None):
|
||||
node = self.G.nodes[agent_id]
|
||||
@@ -125,16 +121,21 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
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)
|
||||
@@ -149,64 +150,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.dir_path
|
||||
if not os.path.exists(dir_path):
|
||||
try:
|
||||
os.makedirs(dir_path)
|
||||
except FileExistsError:
|
||||
pass
|
||||
return dir_path
|
||||
|
||||
def get_agent(self, agent_id):
|
||||
@@ -255,17 +236,19 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
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)
|
||||
@@ -280,31 +263,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():
|
||||
for t_step, attribute, value in sorted(list(history)):
|
||||
if attribute == 'visible':
|
||||
nowvisible = state[attribute]
|
||||
nowvisible = value
|
||||
if nowvisible and not lastvisible:
|
||||
laststep = t_step
|
||||
if not nowvisible and lastvisible:
|
||||
spells.append((laststep, t_step))
|
||||
|
||||
lastvisible = nowvisible
|
||||
else:
|
||||
continue
|
||||
key = 'attr_' + attribute
|
||||
if key not in attributes:
|
||||
attributes[key] = list()
|
||||
if key not in lastattributes:
|
||||
lastattributes[key] = (state[attribute], t_step)
|
||||
lastattributes[key] = (value, t_step)
|
||||
elif lastattributes[key][0] != value:
|
||||
last_value, laststep = lastattributes[key]
|
||||
value = (last_value, t_step, laststep)
|
||||
commit_value = (last_value, laststep, t_step)
|
||||
if key not in attributes:
|
||||
attributes[key] = list()
|
||||
attributes[key].append(value)
|
||||
lastattributes[key] = (state[attribute], t_step)
|
||||
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:
|
||||
@@ -317,14 +299,10 @@ class SoilEnvironment(nxsim.NetworkEnvironment):
|
||||
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['network_agents'] = agents._serialize_distribution(self.network_agents)
|
||||
state['environment_agents'] = agents._convert_agent_types(self.environment_agents,
|
||||
to_string=True)
|
||||
del state['_queue']
|
||||
import inspect
|
||||
for k, v in state.items():
|
||||
if inspect.isgeneratorfunction(v):
|
||||
print(k, v, type(v))
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
|
254
soil/history.py
Normal file
@@ -0,0 +1,254 @@
|
||||
import time
|
||||
import os
|
||||
import pandas as pd
|
||||
import sqlite3
|
||||
import copy
|
||||
from collections import UserDict, Iterable, namedtuple
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
class History:
|
||||
"""
|
||||
Store and retrieve values from a sqlite database.
|
||||
"""
|
||||
|
||||
def __init__(self, db_path=None, name=None, dir_path=None, backup=True):
|
||||
if db_path is None and name:
|
||||
db_path = os.path.join(dir_path or os.getcwd(),
|
||||
'{}.db.sqlite'.format(name))
|
||||
if db_path is None:
|
||||
db_path = ":memory:"
|
||||
else:
|
||||
if backup and os.path.exists(db_path):
|
||||
newname = db_path + '.backup{}.sqlite'.format(time.time())
|
||||
os.rename(db_path, newname)
|
||||
self.db_path = db_path
|
||||
|
||||
self.db = db_path
|
||||
|
||||
with self.db:
|
||||
self.db.execute('''CREATE TABLE IF NOT EXISTS history (agent_id text, t_step int, key text, value text text)''')
|
||||
self.db.execute('''CREATE TABLE IF NOT EXISTS value_types (key text, value_type text)''')
|
||||
self.db.execute('''CREATE UNIQUE INDEX IF NOT EXISTS idx_history ON history (agent_id, t_step, key);''')
|
||||
self._dtypes = {}
|
||||
self._tups = []
|
||||
|
||||
def conversors(self, key):
|
||||
"""Get the serializer and deserializer for a given key."""
|
||||
if key not in self._dtypes:
|
||||
self.read_types()
|
||||
return self._dtypes[key]
|
||||
|
||||
@property
|
||||
def 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):
|
||||
return {k:v[0] for k, v in self._dtypes.items()}
|
||||
|
||||
def save_tuples(self, tuples):
|
||||
self.save_records(Record(*tup) for tup in tuples)
|
||||
|
||||
def save_records(self, records):
|
||||
with self.db:
|
||||
for rec in records:
|
||||
if not isinstance(rec, Record):
|
||||
rec = Record(*rec)
|
||||
if rec.key not in self._dtypes:
|
||||
name = utils.name(rec.value)
|
||||
serializer = utils.serializer(name)
|
||||
deserializer = utils.deserializer(name)
|
||||
self._dtypes[rec.key] = (name, serializer, deserializer)
|
||||
self.db.execute("replace into value_types (key, value_type) values (?, ?)", (rec.key, name))
|
||||
self.db.execute("replace into history(agent_id, t_step, key, value) values (?, ?, ?, ?)", (rec.agent_id, rec.t_step, rec.key, rec.value))
|
||||
|
||||
def save_record(self, *args, **kwargs):
|
||||
self._tups.append(Record(*args, **kwargs))
|
||||
if len(self._tups) > 100:
|
||||
self.flush_cache()
|
||||
|
||||
def flush_cache(self):
|
||||
'''
|
||||
Use a cache to save state changes to avoid opening a session for every change.
|
||||
The cache will be flushed at the end of the simulation, and when history is accessed.
|
||||
'''
|
||||
self.save_records(self._tups)
|
||||
self._tups = list()
|
||||
|
||||
def to_tuples(self):
|
||||
self.flush_cache()
|
||||
with self.db:
|
||||
res = self.db.execute("select agent_id, t_step, key, value from history ").fetchall()
|
||||
for r in res:
|
||||
agent_id, t_step, key, value = r
|
||||
_, _ , des = self.conversors(key)
|
||||
yield agent_id, t_step, key, des(value)
|
||||
|
||||
def read_types(self):
|
||||
with self.db:
|
||||
res = self.db.execute("select key, value_type from value_types ").fetchall()
|
||||
for k, v in res:
|
||||
serializer = utils.serializer(v)
|
||||
deserializer = utils.deserializer(v)
|
||||
self._dtypes[k] = (v, serializer, deserializer)
|
||||
|
||||
def __getitem__(self, key):
|
||||
key = Key(*key)
|
||||
agent_ids = [key.agent_id] if key.agent_id is not None else []
|
||||
t_steps = [key.t_step] if key.t_step is not None else []
|
||||
keys = [key.key] if key.key is not None else []
|
||||
|
||||
df = self.read_sql(agent_ids=agent_ids,
|
||||
t_steps=t_steps,
|
||||
keys=keys)
|
||||
r = Records(df, filter=key, dtypes=self._dtypes)
|
||||
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').fillna(deserial()).astype(dtype)
|
||||
if t_steps:
|
||||
df_p = df_p.reindex(t_steps, method='ffill')
|
||||
return df_p.ffill()
|
||||
|
||||
|
||||
class Records():
|
||||
|
||||
def __init__(self, df, filter=None, dtypes=None):
|
||||
if not filter:
|
||||
filter = Key(agent_id=None,
|
||||
t_step=None,
|
||||
key=None)
|
||||
self._df = df
|
||||
self._filter = filter
|
||||
self.dtypes = dtypes or {}
|
||||
super().__init__()
|
||||
|
||||
def mask(self, tup):
|
||||
res = ()
|
||||
for i, k in zip(tup[:-1], self._filter):
|
||||
if k is None:
|
||||
res = res + (i,)
|
||||
res = res + (tup[-1],)
|
||||
return res
|
||||
|
||||
def filter(self, newKey):
|
||||
f = list(self._filter)
|
||||
for ix, i in enumerate(f):
|
||||
if i is None:
|
||||
f[ix] = newKey
|
||||
self._filter = Key(*f)
|
||||
|
||||
@property
|
||||
def resolved(self):
|
||||
return sum(1 for i in self._filter if i is not None) == 3
|
||||
|
||||
def __iter__(self):
|
||||
for column, series in self._df.iteritems():
|
||||
key, agent_id = column
|
||||
for t_step, value in series.iteritems():
|
||||
r = Record(t_step=t_step,
|
||||
agent_id=agent_id,
|
||||
key=key,
|
||||
value=value)
|
||||
yield self.mask(r)
|
||||
|
||||
def value(self):
|
||||
if self.resolved:
|
||||
f = self._filter
|
||||
try:
|
||||
i = self._df[f.key][str(f.agent_id)]
|
||||
ix = i.index.get_loc(f.t_step, method='ffill')
|
||||
return i.iloc[ix]
|
||||
except KeyError:
|
||||
return self.dtypes[f.key][2]()
|
||||
return 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')
|
@@ -20,7 +20,7 @@ class SoilSimulation(NetworkSimulation):
|
||||
"""
|
||||
Subclass of nsim.NetworkSimulation with three main differences:
|
||||
1) agent type can be specified by name or by class.
|
||||
2) instead of just one type, an network_agents can be used.
|
||||
2) instead of just one type, a network agents distribution can be used.
|
||||
The distribution specifies the weight (or probability) of each
|
||||
agent type in the topology. This is an example distribution: ::
|
||||
|
||||
@@ -49,7 +49,7 @@ class SoilSimulation(NetworkSimulation):
|
||||
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):
|
||||
environment_agents=None, environment_params=None, **kwargs):
|
||||
|
||||
if topology is None:
|
||||
topology = utils.load_network(network_params,
|
||||
@@ -95,16 +95,16 @@ class SoilSimulation(NetworkSimulation):
|
||||
def run_simulation_gen(self, *args, parallel=False, dry_run=False,
|
||||
**kwargs):
|
||||
p = Pool()
|
||||
with utils.timer('simulation'):
|
||||
with utils.timer('simulation {}'.format(self.name)):
|
||||
if parallel:
|
||||
func = partial(self.run_trial, dry_run=dry_run,
|
||||
func = partial(self.run_trial, dry_run=dry_run or self.dry_run,
|
||||
return_env=not parallel, **kwargs)
|
||||
for i in p.imap_unordered(func, range(self.num_trials)):
|
||||
yield i
|
||||
else:
|
||||
for i in range(self.num_trials):
|
||||
yield self.run_trial(i, dry_run=dry_run, **kwargs)
|
||||
if not dry_run or self.dry_run:
|
||||
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)
|
||||
@@ -192,7 +192,7 @@ class SoilSimulation(NetworkSimulation):
|
||||
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')
|
||||
@@ -201,9 +201,10 @@ def from_config(config, G=None):
|
||||
return sim
|
||||
|
||||
|
||||
def run_from_config(*configs, results_dir='soil_output', dump=None, timestamp=False, **kwargs):
|
||||
def run_from_config(*configs, results_dir='soil_output', dry_run=False, dump=None, timestamp=False, **kwargs):
|
||||
for config_def in configs:
|
||||
for config, cpath in utils.load_config(config_def):
|
||||
# logger.info("Found {} config(s)".format(len(ls)))
|
||||
for config, _ in utils.load_config(config_def):
|
||||
name = config.get('name', 'unnamed')
|
||||
logger.info("Using config(s): {name}".format(name=name))
|
||||
|
||||
@@ -215,4 +216,4 @@ def run_from_config(*configs, results_dir='soil_output', dump=None, timestamp=Fa
|
||||
dir_path = os.path.join(results_dir, sim_folder)
|
||||
sim = SoilSimulation(dir_path=dir_path, dump=dump, **config)
|
||||
logger.info('Dumping results to {} : {}'.format(sim.dir_path, sim.dump))
|
||||
results = sim.run_simulation(**kwargs)
|
||||
sim.run_simulation(**kwargs)
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import yaml
|
||||
import logging
|
||||
import importlib
|
||||
from time import time
|
||||
from glob import glob
|
||||
from random import random
|
||||
@@ -72,13 +73,22 @@ def timer(name='task', pre="", function=logger.info, to_object=None):
|
||||
|
||||
|
||||
def repr(v):
|
||||
if isinstance(v, bool):
|
||||
v = "true" if v else ""
|
||||
return v, bool.__name__
|
||||
return v, type(v).__name__
|
||||
func = serializer(v)
|
||||
tname = name(v)
|
||||
return func(v), tname
|
||||
|
||||
def convert(value, type_):
|
||||
import importlib
|
||||
|
||||
def name(v):
|
||||
return type(v).__name__
|
||||
|
||||
|
||||
def serializer(type_):
|
||||
if type_ == 'bool':
|
||||
return lambda x: "true" if x else ""
|
||||
return lambda x: x
|
||||
|
||||
|
||||
def deserializer(type_):
|
||||
try:
|
||||
# Check if it's a builtin type
|
||||
module = importlib.import_module('builtins')
|
||||
@@ -88,4 +98,8 @@ def convert(value, type_):
|
||||
module, type_ = type_.rsplit(".", 1)
|
||||
module = importlib.import_module(module)
|
||||
cls = getattr(module, type_)
|
||||
return cls(value)
|
||||
return cls
|
||||
|
||||
|
||||
def convert(value, type_):
|
||||
return deserializer(type_)(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>
|
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
|
|
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'
|
133
tests/test_history.py
Normal file
@@ -0,0 +1,133 @@
|
||||
from unittest import TestCase
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from glob import glob
|
||||
|
||||
from soil import history
|
||||
|
||||
|
||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
DBROOT = os.path.join(ROOT, 'testdb')
|
||||
|
||||
|
||||
class TestHistory(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
if not os.path.exists(DBROOT):
|
||||
os.makedirs(DBROOT)
|
||||
|
||||
def tearDown(self):
|
||||
if os.path.exists(DBROOT):
|
||||
shutil.rmtree(DBROOT)
|
||||
|
||||
def test_history(self):
|
||||
"""
|
||||
"""
|
||||
tuples = (
|
||||
('a_0', 0, 'id', 'h'),
|
||||
('a_0', 1, 'id', 'e'),
|
||||
('a_0', 2, 'id', 'l'),
|
||||
('a_0', 3, 'id', 'l'),
|
||||
('a_0', 4, 'id', 'o'),
|
||||
('a_1', 0, 'id', 'v'),
|
||||
('a_1', 1, 'id', 'a'),
|
||||
('a_1', 2, 'id', 'l'),
|
||||
('a_1', 3, 'id', 'u'),
|
||||
('a_1', 4, 'id', 'e'),
|
||||
('env', 1, 'prob', 1),
|
||||
('env', 3, 'prob', 2),
|
||||
('env', 5, 'prob', 3),
|
||||
('a_2', 7, 'finished', True),
|
||||
)
|
||||
h = history.History()
|
||||
h.save_tuples(tuples)
|
||||
# assert h['env', 0, 'prob'] == 0
|
||||
for i in range(1, 7):
|
||||
assert h['env', i, 'prob'] == ((i-1)//2)+1
|
||||
|
||||
|
||||
for i, k in zip(range(5), 'hello'):
|
||||
assert h['a_0', i, 'id'] == k
|
||||
for record, value in zip(h['a_0', None, 'id'], 'hello'):
|
||||
t_step, val = record
|
||||
assert val == value
|
||||
|
||||
for i, k in zip(range(5), 'value'):
|
||||
assert h['a_1', i, 'id'] == k
|
||||
for i in range(5, 8):
|
||||
assert h['a_1', i, 'id'] == 'e'
|
||||
for i in range(7):
|
||||
assert h['a_2', i, 'finished'] == False
|
||||
assert h['a_2', 7, 'finished']
|
||||
|
||||
def test_history_gen(self):
|
||||
"""
|
||||
"""
|
||||
tuples = (
|
||||
('a_1', 0, 'id', 'v'),
|
||||
('a_1', 1, 'id', 'a'),
|
||||
('a_1', 2, 'id', 'l'),
|
||||
('a_1', 3, 'id', 'u'),
|
||||
('a_1', 4, 'id', 'e'),
|
||||
('env', 1, 'prob', 1),
|
||||
('env', 2, 'prob', 2),
|
||||
('env', 3, 'prob', 3),
|
||||
('a_2', 7, 'finished', True),
|
||||
)
|
||||
h = history.History()
|
||||
h.save_tuples(tuples)
|
||||
for t_step, key, value in h['env', None, None]:
|
||||
assert t_step == value
|
||||
assert key == 'prob'
|
||||
|
||||
records = list(h[None, 7, None])
|
||||
assert len(records) == 3
|
||||
for i in records:
|
||||
agent_id, key, value = i
|
||||
if agent_id == 'a_1':
|
||||
assert key == 'id'
|
||||
assert value == 'e'
|
||||
elif agent_id == 'a_2':
|
||||
assert key == 'finished'
|
||||
assert value
|
||||
else:
|
||||
assert key == 'prob'
|
||||
assert value == 3
|
||||
|
||||
records = h['a_1', 7, None]
|
||||
assert records['id'] == 'e'
|
||||
|
||||
def test_history_file(self):
|
||||
"""
|
||||
History should be saved to a file
|
||||
"""
|
||||
tuples = (
|
||||
('a_1', 0, 'id', 'v'),
|
||||
('a_1', 1, 'id', 'a'),
|
||||
('a_1', 2, 'id', 'l'),
|
||||
('a_1', 3, 'id', 'u'),
|
||||
('a_1', 4, 'id', 'e'),
|
||||
('env', 1, 'prob', 1),
|
||||
('env', 2, 'prob', 2),
|
||||
('env', 3, 'prob', 3),
|
||||
('a_2', 7, 'finished', True),
|
||||
)
|
||||
db_path = os.path.join(DBROOT, 'test')
|
||||
h = history.History(db_path=db_path)
|
||||
h.save_tuples(tuples)
|
||||
assert os.path.exists(db_path)
|
||||
|
||||
# Recover the data
|
||||
recovered = history.History(db_path=db_path, backup=False)
|
||||
assert recovered['a_1', 0, 'id'] == 'v'
|
||||
assert recovered['a_1', 4, 'id'] == 'e'
|
||||
|
||||
# Using the same name should create a backup copy
|
||||
newhistory = history.History(db_path=db_path, backup=True)
|
||||
backuppaths = glob(db_path + '.backup*.sqlite')
|
||||
assert len(backuppaths) == 1
|
||||
backuppath = backuppaths[0]
|
||||
assert newhistory._db_path == h._db_path
|
||||
assert os.path.exists(backuppath)
|
||||
assert not len(newhistory[None, None, None])
|
@@ -22,6 +22,7 @@ class TestMain(TestCase):
|
||||
Raise an exception otherwise.
|
||||
"""
|
||||
config = {
|
||||
'dry_run': True,
|
||||
'network_params': {
|
||||
'path': join(ROOT, 'test.gexf')
|
||||
}
|
||||
@@ -31,6 +32,7 @@ class TestMain(TestCase):
|
||||
assert len(G) == 2
|
||||
with self.assertRaises(AttributeError):
|
||||
config = {
|
||||
'dry_run': True,
|
||||
'network_params': {
|
||||
'path': join(ROOT, 'unknown.extension')
|
||||
}
|
||||
@@ -44,6 +46,7 @@ class TestMain(TestCase):
|
||||
should be used to generate a network
|
||||
"""
|
||||
config = {
|
||||
'dry_run': True,
|
||||
'network_params': {
|
||||
'generator': 'barabasi_albert_graph'
|
||||
}
|
||||
@@ -58,6 +61,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')
|
||||
},
|
||||
@@ -74,11 +78,12 @@ class TestMain(TestCase):
|
||||
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': {
|
||||
@@ -86,10 +91,10 @@ class TestMain(TestCase):
|
||||
}
|
||||
s = simulation.from_config(config)
|
||||
env = s.run_simulation(dry_run=True)[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
|
||||
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):
|
||||
"""
|
||||
@@ -97,6 +102,7 @@ class TestMain(TestCase):
|
||||
"""
|
||||
config = {
|
||||
'name': 'CounterAgent',
|
||||
'dry_run': True,
|
||||
'network_params': {
|
||||
'path': join(ROOT, 'test.gexf')
|
||||
},
|
||||
@@ -114,9 +120,8 @@ class TestMain(TestCase):
|
||||
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 len(agent[None, None]) == 10
|
||||
for step, total in sorted(agent['total', None]):
|
||||
assert total == last + 2
|
||||
last = total
|
||||
|
||||
@@ -127,6 +132,7 @@ class TestMain(TestCase):
|
||||
self.state['neighbors'] = self.count_agents(state_id=0,
|
||||
limit_neighbors=True)
|
||||
config = {
|
||||
'dry_run': True,
|
||||
'network_params': {
|
||||
'path': join(ROOT, 'test.gexf')
|
||||
},
|
||||
@@ -150,7 +156,8 @@ class TestMain(TestCase):
|
||||
config['network_params']['path'] = join(EXAMPLES,
|
||||
config['network_params']['path'])
|
||||
s = simulation.from_config(config)
|
||||
env = s.run_simulation(dry_run=True)[0]
|
||||
s.dry_run = True
|
||||
env = s.run_simulation()[0]
|
||||
for a in env.network_agents:
|
||||
skill_level = a.state['skill_level']
|
||||
if a.id == 'Torvalds':
|
||||
@@ -174,14 +181,15 @@ class TestMain(TestCase):
|
||||
with utils.timer('loading'):
|
||||
config = utils.load_file(join(EXAMPLES, 'complete.yml'))[0]
|
||||
s = simulation.from_config(config)
|
||||
s.dry_run = True
|
||||
with utils.timer('serializing'):
|
||||
serial = s.to_yaml()
|
||||
with utils.timer('recovering'):
|
||||
recovered = yaml.load(serial)
|
||||
with utils.timer('deleting'):
|
||||
del recovered['topology']
|
||||
del recovered['dry_run']
|
||||
del recovered['load_module']
|
||||
del recovered['dry_run']
|
||||
assert config == recovered
|
||||
|
||||
def test_configuration_changes(self):
|
||||
@@ -191,6 +199,7 @@ class TestMain(TestCase):
|
||||
"""
|
||||
config = utils.load_file('examples/complete.yml')[0]
|
||||
s = simulation.from_config(config)
|
||||
s.dry_run = True
|
||||
for i in range(5):
|
||||
s.run_simulation(dry_run=True)
|
||||
nconfig = s.to_dict()
|
||||
@@ -206,17 +215,14 @@ class TestMain(TestCase):
|
||||
pass
|
||||
|
||||
def test_row_conversion(self):
|
||||
sim = simulation.SoilSimulation()
|
||||
env = environment.SoilEnvironment(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'
|
||||
@@ -231,6 +237,22 @@ class TestMain(TestCase):
|
||||
env = environment.SoilEnvironment(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.SoilEnvironment(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):
|
||||
|