mirror of https://github.com/gsi-upm/soil
Added history class
Now the environment does not deal with history directly, it delegates it to a specific class. The analysis also uses history instances instead of either using the database directly or creating a proxy environment. This should make it easier to change the implementation in the future. In fact, the change was motivated by the large size of the csv files in previous versions. This new implementation only stores results in deltas, and it fills any necessary values when needed.history
parent
73c90887e8
commit
fc48ed7e09
@ -0,0 +1,8 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
dev:
|
||||||
|
build: .
|
||||||
|
volumes:
|
||||||
|
- .:/usr/src/app
|
||||||
|
tty: true
|
||||||
|
entrypoint: /bin/bash
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
0.10.2
|
0.11
|
@ -0,0 +1,231 @@
|
|||||||
|
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.replace('db.sqlite', 'backup{}.sqlite'.format(time.time()))
|
||||||
|
os.rename(db_path, newname)
|
||||||
|
self._db_path = db_path
|
||||||
|
if isinstance(db_path, str):
|
||||||
|
self._db = sqlite3.connect(db_path)
|
||||||
|
else:
|
||||||
|
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 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)
|
||||||
|
return r.value()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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 self
|
||||||
|
|
||||||
|
def __getitem__(self, k):
|
||||||
|
n = copy.copy(self)
|
||||||
|
n.filter(k)
|
||||||
|
return n.value()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._df)
|
||||||
|
|
||||||
|
|
||||||
|
Key = namedtuple('Key', ['agent_id', 't_step', 'key'])
|
||||||
|
Record = namedtuple('Record', 'agent_id t_step key value')
|
@ -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
|
|
@ -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'
|
@ -0,0 +1,90 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from soil import history, analysis
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
class TestHistory(TestCase):
|
||||||
|
|
||||||
|
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 == True
|
||||||
|
else:
|
||||||
|
assert key == 'prob'
|
||||||
|
assert value == 3
|
||||||
|
|
||||||
|
|
||||||
|
records = h['a_1', 7, None]
|
||||||
|
assert records['id'] == 'e'
|
Loading…
Reference in New Issue