1
0
mirror of https://github.com/gsi-upm/soil synced 2025-09-15 04:32:21 +00:00

Compare commits

..

4 Commits

Author SHA1 Message Date
J. Fernando Sánchez
65f6aa72f3 fix timeout in FSM. Improve logs 2019-02-01 19:05:07 +01:00
J. Fernando Sánchez
09e14c6e84 Add generator and programmatic examples 2018-12-20 19:25:33 +01:00
J. Fernando Sánchez
8593ac999d Swap test and build in CI. Remove tests in tags 2018-12-20 17:56:33 +01:00
J. Fernando Sánchez
90338c3549 skip-tls-verify in kaniko 2018-12-20 17:48:58 +01:00
16 changed files with 233 additions and 40 deletions

View File

@@ -1,6 +1,6 @@
stages: stages:
- build
- test - test
- build
build: build:
stage: build stage: build
@@ -11,12 +11,15 @@ build:
- docker - docker
script: script:
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG # The skip-tls-verify flag is there because our registry certificate is self signed
- /kaniko/executor --context $CI_PROJECT_DIR --skip-tls-verify --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
only: only:
- tags - tags
test: test:
except:
- tags # Avoid running tests for tags, because they are already run for the branch
tags: tags:
- docker - docker
image: python:3.7 image: python:3.7

19
CHANGELOG.md Normal file
View File

@@ -0,0 +1,19 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
### Added
### Fixed
## [0.13.7]
### Changed
* History now defaults to not backing up! This makes it more intuitive to load the history for examination, at the expense of rewriting something. That should not happen because History is only created in the Environment, and that has `backup=True`.
### Added
* Agent names are assigned based on their agent types
* Agent logging uses the agent name.
* FSM agents can now return a timeout in addition to a new state. e.g. `return self.idle, self.env.timeout(2)` will execute the *different_state* in 2 *units of time* (`t_step=now+2`).
* Example of using timeouts in FSM (custom_timeouts)
* `network_agents` entries may include an `ids` entry. If set, it should be a list of node ids that should be assigned that agent type. This complements the previous behavior of setting agent type with `weights`.

View File

@@ -26,7 +26,7 @@ But before that, let's import the soil module and networkx.
%autoreload 2 %autoreload 2
%pylab inline %pylab inline
# To display plots in the notebooed_ # To display plots in the notebook_
.. parsed-literal:: .. parsed-literal::

View File

@@ -0,0 +1,17 @@
---
name: custom-generator
description: Using a custom generator for the network
num_trials: 3
dry_run: True
max_time: 100
interval: 1
network_params:
generator: mymodule.mygenerator
# These are custom parameters
n: 10
n_edges: 5
network_agents:
- agent_type: CounterModel
weight: 1
state:
id: 0

View File

@@ -0,0 +1,27 @@
from networkx import Graph
import networkx as nx
from random import choice
def mygenerator(n=5, n_edges=5):
'''
Just a simple generator that creates a network with n nodes and
n_edges edges. Edges are assigned randomly, only avoiding self loops.
'''
G = nx.Graph()
for i in range(n):
G.add_node(i)
for i in range(n_edges):
nodes = list(G.nodes)
n_in = choice(nodes)
nodes.remove(n_in) # Avoid loops
n_out = choice(nodes)
G.add_edge(n_in, n_out)
return G

View File

@@ -0,0 +1,36 @@
from soil.agents import FSM, state, default_state
class Fibonacci(FSM):
'''Agent that only executes in t_steps that are Fibonacci numbers'''
defaults = {
'prev': 1
}
@default_state
@state
def counting(self):
self.log('Stopping at {}'.format(self.now))
prev, self['prev'] = self['prev'], max([self.now, self['prev']])
return None, self.env.timeout(prev)
class Odds(FSM):
'''Agent that only executes in odd t_steps'''
@default_state
@state
def odds(self):
self.log('Stopping at {}'.format(self.now))
return None, self.env.timeout(1+self.now%2)
if __name__ == '__main__':
import logging
logging.basicConfig(level=logging.INFO)
from soil import Simulation
s = Simulation(network_agents=[{'ids': [0], 'agent_type': Fibonacci},
{'ids': [1], 'agent_type': Odds}],
dry_run=True,
network_params={"generator": "complete_graph", "n": 2},
max_time=100,
)
s.run()

1
examples/programmatic/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
Programmatic*

View File

@@ -0,0 +1,38 @@
'''
Example of a fully programmatic simulation, without definition files.
'''
from soil import Simulation, agents
from networkx import Graph
import logging
def mygenerator():
# Add only a node
G = Graph()
G.add_node(1)
return G
class MyAgent(agents.FSM):
@agents.default_state
@agents.state
def neutral(self):
self.info('I am running')
s = Simulation(name='Programmatic',
network_params={'generator': mygenerator},
num_trials=1,
max_time=100,
agent_type=MyAgent,
dry_run=True)
logging.basicConfig(level=logging.INFO)
envs = s.run()
s.dump_yaml()
for env in envs:
env.dump_csv()

View File

@@ -1 +1 @@
0.13.4 0.13.7

View File

@@ -14,6 +14,7 @@ except NameError:
from . import agents from . import agents
from .simulation import * from .simulation import *
from .environment import Environment from .environment import Environment
from .history import History
from . import utils from . import utils
from . import analysis from . import analysis

View File

@@ -25,13 +25,13 @@ class BaseAgent(nxsim.BaseAgent):
defaults = {} defaults = {}
def __init__(self, environment, agent_id, state=None, def __init__(self, environment, agent_id, state=None,
name='network_process', interval=None, **state_params): name=None, interval=None, **state_params):
# Check for REQUIRED arguments # Check for REQUIRED arguments
assert environment is not None, TypeError('__init__ missing 1 required keyword argument: \'environment\'. ' assert environment is not None, TypeError('__init__ missing 1 required keyword argument: \'environment\'. '
'Cannot be NoneType.') 'Cannot be NoneType.')
# Initialize agent parameters # Initialize agent parameters
self.id = agent_id self.id = agent_id
self.name = name self.name = name or '{}[{}]'.format(type(self).__name__, self.id)
self.state_params = state_params self.state_params = state_params
# Register agent to environment # Register agent to environment
@@ -46,7 +46,7 @@ class BaseAgent(nxsim.BaseAgent):
if not hasattr(self, 'level'): if not hasattr(self, 'level'):
self.level = logging.DEBUG self.level = logging.DEBUG
self.logger = logging.getLogger('{}-Agent-{}'.format(self.env.name, self.logger = logging.getLogger('{}.{}'.format(self.env.name,
self.id)) self.id))
self.logger.setLevel(self.level) self.logger.setLevel(self.level)
@@ -174,7 +174,7 @@ class BaseAgent(nxsim.BaseAgent):
def log(self, message, *args, level=logging.INFO, **kwargs): def log(self, message, *args, level=logging.INFO, **kwargs):
message = message + " ".join(str(i) for i in args) message = message + " ".join(str(i) for i in args)
message = "\t@{:>5}:\t{}".format(self.now, message) message = "\t{:10}@{:>5}:\t{}".format(self.name, self.now, message)
for k, v in kwargs: for k, v in kwargs:
message += " {k}={v} ".format(k, v) message += " {k}={v} ".format(k, v)
extra = {} extra = {}
@@ -280,7 +280,7 @@ class FSM(BaseAgent, metaclass=MetaFSM):
raise Exception('{} has no valid state id or default state'.format(self)) raise Exception('{} has no valid state id or default state'.format(self))
if next_state not in self.states: if next_state not in self.states:
raise Exception('{} is not a valid id for {}'.format(next_state, self)) raise Exception('{} is not a valid id for {}'.format(next_state, self))
self.states[next_state](self) return self.states[next_state](self)
def set_state(self, state): def set_state(self, state):
if hasattr(state, 'id'): if hasattr(state, 'id'):
@@ -306,6 +306,9 @@ def prob(prob=1):
return r < prob return r < prob
STATIC_THRESHOLD = (-1, -1)
def calculate_distribution(network_agents=None, def calculate_distribution(network_agents=None,
agent_type=None): agent_type=None):
''' '''
@@ -343,6 +346,9 @@ def calculate_distribution(network_agents=None,
total = sum(x.get('weight', 1) for x in network_agents) total = sum(x.get('weight', 1) for x in network_agents)
acc = 0 acc = 0
for v in network_agents: for v in network_agents:
if 'ids' in v:
v['threshold'] = STATIC_THRESHOLD
continue
upper = acc + (v.get('weight', 1)/total) upper = acc + (v.get('weight', 1)/total)
v['threshold'] = [acc, upper] v['threshold'] = [acc, upper]
acc = upper acc = upper
@@ -403,13 +409,16 @@ def _convert_agent_types(ind, to_string=False, **kwargs):
return deserialize_distribution(ind, **kwargs) return deserialize_distribution(ind, **kwargs)
def _agent_from_distribution(distribution, value=-1): def _agent_from_distribution(distribution, value=-1, agent_id=None):
"""Used in the initialization of agents given an agent distribution.""" """Used in the initialization of agents given an agent distribution."""
if value < 0: if value < 0:
value = random.random() value = random.random()
for d in distribution: for d in sorted(distribution, key=lambda x: x['threshold']):
threshold = d['threshold'] threshold = d['threshold']
if value >= threshold[0] and value < threshold[1]: # Check if the definition matches by id (first) or by threshold
if not ((agent_id is not None and threshold == STATIC_THRESHOLD and agent_id in d['ids']) or \
(value >= threshold[0] and value < threshold[1])):
continue
state = {} state = {}
if 'state' in d: if 'state' in d:
state = deepcopy(d['state']) state = deepcopy(d['state'])

View File

@@ -4,9 +4,11 @@ import time
import csv import csv
import random import random
import simpy import simpy
import yaml
import tempfile import tempfile
import pandas as pd import pandas as pd
from copy import deepcopy from copy import deepcopy
from collections import Counter
from networkx.readwrite import json_graph from networkx.readwrite import json_graph
import networkx as nx import networkx as nx
@@ -60,7 +62,8 @@ class Environment(nxsim.NetworkEnvironment):
if not dry_run: if not dry_run:
self.get_path() self.get_path()
self._history = history.History(name=self.name if not dry_run else None, self._history = history.History(name=self.name if not dry_run else None,
dir_path=self.dir_path) dir_path=self.dir_path,
backup=True)
# Add environment agents first, so their events get # Add environment agents first, so their events get
# executed before network agents # executed before network agents
self.environment_agents = environment_agents or [] self.environment_agents = environment_agents or []
@@ -111,7 +114,7 @@ class Environment(nxsim.NetworkEnvironment):
agent_type = None agent_type = None
if 'agent_type' in self.states.get(agent_id, {}): if 'agent_type' in self.states.get(agent_id, {}):
agent_type = self.states[agent_id] agent_type = self.states[agent_id]['agent_type']
elif 'agent_type' in node: elif 'agent_type' in node:
agent_type = node['agent_type'] agent_type = node['agent_type']
elif 'agent_type' in self.default_state: elif 'agent_type' in self.default_state:
@@ -119,8 +122,8 @@ class Environment(nxsim.NetworkEnvironment):
if agent_type: if agent_type:
agent_type = agents.deserialize_type(agent_type) agent_type = agents.deserialize_type(agent_type)
else: elif agent_distribution:
agent_type, state = agents._agent_from_distribution(agent_distribution) agent_type, state = agents._agent_from_distribution(agent_distribution, agent_id=agent_id)
return self.set_agent(agent_id, agent_type, state) return self.set_agent(agent_id, agent_type, state)
def set_agent(self, agent_id, agent_type, state=None): def set_agent(self, agent_id, agent_type, state=None):
@@ -130,6 +133,8 @@ class Environment(nxsim.NetworkEnvironment):
defstate.update(node.get('state', {})) defstate.update(node.get('state', {}))
if state: if state:
defstate.update(state) defstate.update(state)
a = None
if agent_type:
state = defstate state = defstate
a = agent_type(environment=self, a = agent_type(environment=self,
agent_id=agent_id, agent_id=agent_id,
@@ -153,8 +158,10 @@ class Environment(nxsim.NetworkEnvironment):
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
self._save_state() self._save_state()
self.log_stats()
super().run(*args, **kwargs) super().run(*args, **kwargs)
self._history.flush_cache() self._history.flush_cache()
self.log_stats()
def _save_state(self, now=None): def _save_state(self, now=None):
# for agent in self.agents: # for agent in self.agents:
@@ -327,6 +334,25 @@ class Environment(nxsim.NetworkEnvironment):
return G return G
def stats(self):
stats = {}
stats['network'] = {}
stats['network']['n_nodes'] = self.G.number_of_nodes()
stats['network']['n_edges'] = self.G.number_of_edges()
c = Counter()
c.update(a.__class__.__name__ for a in self.network_agents)
stats['agents'] = {}
stats['agents']['model_count'] = dict(c)
c2 = Counter()
c2.update(a['id'] for a in self.network_agents)
stats['agents']['state_count'] = dict(c2)
stats['params'] = self.environment_params
return stats
def log_stats(self):
stats = self.stats()
utils.logger.info('Environment stats: \n{}'.format(yaml.dump(stats, default_flow_style=False)))
def __getstate__(self): def __getstate__(self):
state = {} state = {}
for prop in _CONFIG_PROPS: for prop in _CONFIG_PROPS:

View File

@@ -3,6 +3,10 @@ import os
import pandas as pd import pandas as pd
import sqlite3 import sqlite3
import copy import copy
import logging
logger = logging.getLogger(__name__)
from collections import UserDict, namedtuple from collections import UserDict, namedtuple
from . import utils from . import utils
@@ -13,7 +17,7 @@ class History:
Store and retrieve values from a sqlite database. Store and retrieve values from a sqlite database.
""" """
def __init__(self, db_path=None, name=None, dir_path=None, backup=True): def __init__(self, db_path=None, name=None, dir_path=None, backup=False):
if db_path is None and name: if db_path is None and name:
db_path = os.path.join(dir_path or os.getcwd(), db_path = os.path.join(dir_path or os.getcwd(),
'{}.db.sqlite'.format(name)) '{}.db.sqlite'.format(name))
@@ -28,6 +32,7 @@ class History:
self.db = db_path self.db = db_path
with self.db: with self.db:
logger.debug('Creating database {}'.format(self.db_path))
self.db.execute('''CREATE TABLE IF NOT EXISTS history (agent_id text, t_step int, key text, value text text)''') self.db.execute('''CREATE TABLE IF NOT EXISTS 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 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.db.execute('''CREATE UNIQUE INDEX IF NOT EXISTS idx_history ON history (agent_id, t_step, key);''')
@@ -46,6 +51,7 @@ class History:
def db(self, db_path=None): def db(self, db_path=None):
db_path = db_path or self.db_path db_path = db_path or self.db_path
if isinstance(db_path, str): if isinstance(db_path, str):
logger.debug('Connecting to database {}'.format(db_path))
self._db = sqlite3.connect(db_path) self._db = sqlite3.connect(db_path)
else: else:
self._db = db_path self._db = db_path
@@ -110,6 +116,7 @@ class History:
Use a cache to save state changes to avoid opening a session for every change. 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. The cache will be flushed at the end of the simulation, and when history is accessed.
''' '''
logger.debug('Flushing cache {}'.format(self.db_path))
with self.db: with self.db:
for rec in self._tups: for rec in self._tups:
self.db.execute("replace into history(agent_id, t_step, key, value) values (?, ?, ?, ?)", (rec.agent_id, rec.t_step, rec.key, rec.value)) self.db.execute("replace into history(agent_id, t_step, key, value) values (?, ?, ?, ?)", (rec.agent_id, rec.t_step, rec.key, rec.value))

View File

@@ -88,14 +88,8 @@ class Simulation(NetworkSimulation):
environment_agents=None, environment_params=None, environment_agents=None, environment_params=None,
environment_class=None, **kwargs): environment_class=None, **kwargs):
if topology is None: self.seed = str(seed) or str(time.time())
topology = utils.load_network(network_params,
dir_path=dir_path)
elif isinstance(topology, basestring) or isinstance(topology, dict):
topology = json_graph.node_link_graph(topology)
self.load_module = load_module self.load_module = load_module
self.topology = nx.Graph(topology)
self.network_params = network_params self.network_params = network_params
self.name = name or 'UnnamedSimulation' self.name = name or 'UnnamedSimulation'
self.num_trials = num_trials self.num_trials = num_trials
@@ -103,12 +97,19 @@ class Simulation(NetworkSimulation):
self.default_state = default_state or {} self.default_state = default_state or {}
self.dir_path = dir_path or os.getcwd() self.dir_path = dir_path or os.getcwd()
self.interval = interval self.interval = interval
self.seed = str(seed) or str(time.time())
self.dump = dump self.dump = dump
self.dry_run = dry_run self.dry_run = dry_run
sys.path += [self.dir_path, os.getcwd()] sys.path += [self.dir_path, os.getcwd()]
if topology is None:
topology = utils.load_network(network_params,
dir_path=self.dir_path)
elif isinstance(topology, basestring) or isinstance(topology, dict):
topology = json_graph.node_link_graph(topology)
self.topology = nx.Graph(topology)
self.environment_params = environment_params or {} self.environment_params = environment_params or {}
self.environment_class = utils.deserialize(environment_class, self.environment_class = utils.deserialize(environment_class,
known_modules=['soil.environment', ]) or Environment known_modules=['soil.environment', ]) or Environment

View File

@@ -1,5 +1,6 @@
import os import os
import ast import ast
import sys
import yaml import yaml
import logging import logging
import importlib import importlib
@@ -36,9 +37,14 @@ def load_network(network_params, dir_path=None):
return method(path, **kwargs) return method(path, **kwargs)
net_args = network_params.copy() net_args = network_params.copy()
net_type = net_args.pop('generator') net_gen = net_args.pop('generator')
if dir_path not in sys.path:
sys.path.append(dir_path)
method = deserializer(net_gen,
known_modules=['networkx.generators',])
method = getattr(nx.generators, net_type)
return method(**net_args) return method(**net_args)
@@ -114,6 +120,8 @@ def serialize(v, known_modules=[]):
return func(v), tname return func(v), tname
def deserializer(type_, known_modules=[]): def deserializer(type_, known_modules=[]):
if type(type_) != str: # Already deserialized
return type_
if type_ == 'str': if type_ == 'str':
return lambda x='': x return lambda x='': x
if type_ == 'None': if type_ == 'None':

View File

@@ -120,18 +120,18 @@ class TestHistory(TestCase):
assert os.path.exists(db_path) assert os.path.exists(db_path)
# Recover the data # Recover the data
recovered = history.History(db_path=db_path, backup=False) recovered = history.History(db_path=db_path)
assert recovered['a_1', 0, 'id'] == 'v' assert recovered['a_1', 0, 'id'] == 'v'
assert recovered['a_1', 4, 'id'] == 'e' assert recovered['a_1', 4, 'id'] == 'e'
# Using the same name should create a backup copy # Using backup=True should create a backup copy, and initialize an empty history
newhistory = history.History(db_path=db_path, backup=True) newhistory = history.History(db_path=db_path, backup=True)
backuppaths = glob(db_path + '.backup*.sqlite') backuppaths = glob(db_path + '.backup*.sqlite')
assert len(backuppaths) == 1 assert len(backuppaths) == 1
backuppath = backuppaths[0] backuppath = backuppaths[0]
assert newhistory.db_path == h.db_path assert newhistory.db_path == h.db_path
assert os.path.exists(backuppath) assert os.path.exists(backuppath)
assert not len(newhistory[None, None, None]) assert len(newhistory[None, None, None]) == 0
def test_history_tuples(self): def test_history_tuples(self):
""" """