mirror of https://github.com/gsi-upm/soil
WIP: removed stats
parent
3dc56892c1
commit
0a9c6d8b19
@ -1,264 +0,0 @@
|
||||
from pydantic import BaseModel, ValidationError, validator
|
||||
|
||||
import yaml
|
||||
import os
|
||||
import sys
|
||||
import networkx as nx
|
||||
import collections.abc
|
||||
|
||||
from . import serialization, utils, basestring, agents
|
||||
|
||||
class Config(collections.abc.Mapping):
|
||||
"""
|
||||
|
||||
1) agent type can be specified by name or by class.
|
||||
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: ::
|
||||
|
||||
[
|
||||
{'agent_type': 'agent_type_1',
|
||||
'weight': 0.2,
|
||||
'state': {
|
||||
'id': 0
|
||||
}
|
||||
},
|
||||
{'agent_type': 'agent_type_2',
|
||||
'weight': 0.8,
|
||||
'state': {
|
||||
'id': 1
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
In this example, 20% of the nodes will be marked as type
|
||||
'agent_type_1'.
|
||||
3) if no initial state is given, each node's state will be set
|
||||
to `{'id': 0}`.
|
||||
|
||||
Parameters
|
||||
---------
|
||||
name : str, optional
|
||||
name of the Simulation
|
||||
group : str, optional
|
||||
a group name can be used to link simulations
|
||||
topology (optional): networkx.Graph instance or Node-Link topology as a dict or string (will be loaded with `json_graph.node_link_graph(topology`).
|
||||
network_params : dict
|
||||
parameters used to create a topology with networkx, if no topology is given
|
||||
network_agents : dict
|
||||
definition of agents to populate the topology with
|
||||
agent_type : NetworkAgent subclass, optional
|
||||
Default type of NetworkAgent to use for nodes not specified in network_agents
|
||||
states : list, optional
|
||||
List of initial states corresponding to the nodes in the topology. Basic form is a list of integers
|
||||
whose value indicates the state
|
||||
dir_path: str, optional
|
||||
Directory path to load simulation assets (files, modules...)
|
||||
seed : str, optional
|
||||
Seed to use for the random generator
|
||||
num_trials : int, optional
|
||||
Number of independent simulation runs
|
||||
max_time : int, optional
|
||||
Maximum step/time for each simulation
|
||||
environment_params : dict, optional
|
||||
Dictionary of globally-shared environmental parameters
|
||||
environment_agents: dict, optional
|
||||
Similar to network_agents. Distribution of Agents that control the environment
|
||||
environment_class: soil.environment.Environment subclass, optional
|
||||
Class for the environment. It defailts to soil.environment.Environment
|
||||
"""
|
||||
__slots__ = 'name', 'agent_type', 'group', 'description', 'network_agents', 'environment_agents', 'states', 'default_state', 'interval', 'network_params', 'seed', 'num_trials', 'max_time', 'topology', 'schedule', 'initial_time', 'environment_params', 'environment_class', 'dir_path', '_added_to_path', 'visualization_params'
|
||||
|
||||
def __init__(self, name=None,
|
||||
group=None,
|
||||
agent_type='BaseAgent',
|
||||
network_agents=None,
|
||||
environment_agents=None,
|
||||
states=None,
|
||||
description=None,
|
||||
default_state=None,
|
||||
interval=1,
|
||||
network_params=None,
|
||||
seed=None,
|
||||
num_trials=1,
|
||||
max_time=None,
|
||||
topology=None,
|
||||
schedule=None,
|
||||
initial_time=0,
|
||||
environment_params={},
|
||||
environment_class='soil.Environment',
|
||||
dir_path=None,
|
||||
visualization_params=None,
|
||||
):
|
||||
|
||||
self.network_params = network_params
|
||||
self.name = name or 'Unnamed'
|
||||
self.description = description or 'No simulation description available'
|
||||
self.seed = str(seed or name)
|
||||
self.group = group or ''
|
||||
self.num_trials = num_trials
|
||||
self.max_time = max_time
|
||||
self.default_state = default_state or {}
|
||||
self.dir_path = dir_path or os.getcwd()
|
||||
self.interval = interval
|
||||
self.visualization_params = visualization_params or {}
|
||||
|
||||
self._added_to_path = list(x for x in [os.getcwd(), self.dir_path] if x not in sys.path)
|
||||
sys.path += self._added_to_path
|
||||
|
||||
self.topology = topology
|
||||
|
||||
self.schedule = schedule
|
||||
self.initial_time = initial_time
|
||||
|
||||
|
||||
self.environment_class = environment_class
|
||||
self.environment_params = dict(environment_params)
|
||||
|
||||
#TODO: Check agent distro vs fixed agents
|
||||
self.environment_agents = environment_agents or []
|
||||
|
||||
self.agent_type = agent_type
|
||||
|
||||
self.network_agents = network_agents or {}
|
||||
|
||||
self.states = states or {}
|
||||
|
||||
|
||||
def validate(self):
|
||||
agents._validate_states(self.states,
|
||||
self._topology)
|
||||
|
||||
def calculate(self):
|
||||
return CalculatedConfig(self)
|
||||
|
||||
def restore_path(self):
|
||||
for added in self._added_to_path:
|
||||
sys.path.remove(added)
|
||||
|
||||
def to_yaml(self):
|
||||
return yaml.dump(self.to_dict())
|
||||
|
||||
def dump_yaml(self, f=None, outdir=None):
|
||||
if not f and not outdir:
|
||||
raise ValueError('specify a file or an output directory')
|
||||
|
||||
if not f:
|
||||
f = os.path.join(outdir, '{}.dumped.yml'.format(self.name))
|
||||
|
||||
with utils.open_or_reuse(f, 'w') as f:
|
||||
f.write(self.to_yaml())
|
||||
|
||||
def to_yaml(self):
|
||||
return yaml.dump(self.to_dict())
|
||||
|
||||
# TODO: See note on getstate
|
||||
def to_dict(self):
|
||||
return dict(self)
|
||||
|
||||
def __repr__(self):
|
||||
return self.to_yaml()
|
||||
|
||||
def dump_yaml(self, f=None, outdir=None):
|
||||
if not f and not outdir:
|
||||
raise ValueError('specify a file or an output directory')
|
||||
|
||||
if not f:
|
||||
f = os.path.join(outdir, '{}.dumped.yml'.format(self.name))
|
||||
|
||||
with utils.open_or_reuse(f, 'w') as f:
|
||||
f.write(self.to_yaml())
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def __iter__(self):
|
||||
return (k for k in self.__slots__ if k[0] != '_')
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__slots__)
|
||||
|
||||
def dump_pickle(self, f=None, outdir=None):
|
||||
if not outdir and not f:
|
||||
raise ValueError('specify a file or an output directory')
|
||||
|
||||
if not f:
|
||||
f = os.path.join(outdir,
|
||||
'{}.simulation.pickle'.format(self.name))
|
||||
with utils.open_or_reuse(f, 'wb') as f:
|
||||
pickle.dump(self, f)
|
||||
|
||||
# TODO: remove this. A config should be sendable regardless. Non-pickable objects could be computed via properties and the like
|
||||
# def __getstate__(self):
|
||||
# state={}
|
||||
# for k, v in self.__dict__.items():
|
||||
# if k[0] != '_':
|
||||
# state[k] = v
|
||||
# state['topology'] = json_graph.node_link_data(self.topology)
|
||||
# state['network_agents'] = agents.serialize_definition(self.network_agents,
|
||||
# known_modules = [])
|
||||
# state['environment_agents'] = agents.serialize_definition(self.environment_agents,
|
||||
# known_modules = [])
|
||||
# state['environment_class'] = serialization.serialize(self.environment_class,
|
||||
# known_modules=['soil.environment'])[1] # func, name
|
||||
# if state['load_module'] is None:
|
||||
# del state['load_module']
|
||||
# return state
|
||||
|
||||
# # TODO: remove, same as __getstate__
|
||||
# def __setstate__(self, state):
|
||||
# self.__dict__ = state
|
||||
# self.load_module = getattr(self, 'load_module', None)
|
||||
# if self.dir_path not in sys.path:
|
||||
# sys.path += [self.dir_path, os.getcwd()]
|
||||
# self.topology = json_graph.node_link_graph(state['topology'])
|
||||
# self.network_agents = agents.calculate_distribution(agents._convert_agent_types(self.network_agents))
|
||||
# self.environment_agents = agents._convert_agent_types(self.environment_agents,
|
||||
# known_modules=[self.load_module])
|
||||
# self.environment_class = serialization.deserialize(self.environment_class,
|
||||
# known_modules=[self.load_module,
|
||||
# 'soil.environment', ]) # func, name
|
||||
|
||||
class CalculatedConfig(Config):
|
||||
def __init__(self, config):
|
||||
"""
|
||||
Returns a configuration object that replaces some "plain" attributes (e.g., `environment_class` string) into
|
||||
a Python object (`soil.environment.Environment` class).
|
||||
"""
|
||||
self._config = config
|
||||
values = dict(config)
|
||||
values['environment_class'] = self._environment_class()
|
||||
values['environment_agents'] = self._environment_agents()
|
||||
values['topology'] = self._topology()
|
||||
values['network_agents'] = self._network_agents()
|
||||
values['agent_type'] = serialization.deserialize(self.agent_type, known_modules=['soil.agents'])
|
||||
|
||||
return values
|
||||
|
||||
def _topology(self):
|
||||
topology = self._config.topology
|
||||
if topology is None:
|
||||
topology = serialization.load_network(self._config.network_params,
|
||||
dir_path=self._config.dir_path)
|
||||
|
||||
elif isinstance(topology, basestring) or isinstance(topology, dict):
|
||||
topology = json_graph.node_link_graph(topology)
|
||||
|
||||
return nx.Graph(topology)
|
||||
|
||||
def _environment_class(self):
|
||||
return serialization.deserialize(self._config.environment_class,
|
||||
known_modules=['soil.environment', ]) or Environment
|
||||
|
||||
def _environment_agents(self):
|
||||
return agents._convert_agent_types(self._config.environment_agents)
|
||||
|
||||
def _network_agents(self):
|
||||
distro = agents.calculate_distribution(self._config.network_agents,
|
||||
self._config.agent_type)
|
||||
return agents._convert_agent_types(distro)
|
||||
|
||||
def _environment_class(self):
|
||||
return serialization.deserialize(self._config.environment_class,
|
||||
known_modules=['soil.environment', ]) # func, name
|
||||
|
@ -1,111 +0,0 @@
|
||||
import pandas as pd
|
||||
|
||||
from collections import Counter
|
||||
|
||||
class Stats:
|
||||
'''
|
||||
Interface for all stats. It is not necessary, but it is useful
|
||||
if you don't plan to implement all the methods.
|
||||
'''
|
||||
|
||||
def __init__(self, simulation, name=None):
|
||||
self.name = name or type(self).__name__
|
||||
self.simulation = simulation
|
||||
|
||||
def sim_start(self):
|
||||
'''Method to call when the simulation starts'''
|
||||
pass
|
||||
|
||||
def sim_end(self):
|
||||
'''Method to call when the simulation ends'''
|
||||
return {}
|
||||
|
||||
def trial_start(self, env):
|
||||
'''Method to call when a trial starts'''
|
||||
return {}
|
||||
|
||||
def trial_end(self, env):
|
||||
'''Method to call when a trial ends'''
|
||||
return {}
|
||||
|
||||
|
||||
class distribution(Stats):
|
||||
'''
|
||||
Calculate the distribution of agent states at the end of each trial,
|
||||
the mean value, and its deviation.
|
||||
'''
|
||||
|
||||
def sim_start(self):
|
||||
self.means = []
|
||||
self.counts = []
|
||||
|
||||
def trial_end(self, env):
|
||||
df = pd.DataFrame(env.state_to_tuples())
|
||||
df = df.drop('SEED', axis=1)
|
||||
ix = df.index[-1]
|
||||
attrs = df.columns.get_level_values(0)
|
||||
vc = {}
|
||||
stats = {
|
||||
'mean': {},
|
||||
'count': {},
|
||||
}
|
||||
for a in attrs:
|
||||
t = df.loc[(ix, a)]
|
||||
try:
|
||||
stats['mean'][a] = t.mean()
|
||||
self.means.append(('mean', a, t.mean()))
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
for name, count in t.value_counts().iteritems():
|
||||
if a not in stats['count']:
|
||||
stats['count'][a] = {}
|
||||
stats['count'][a][name] = count
|
||||
self.counts.append(('count', a, name, count))
|
||||
|
||||
return stats
|
||||
|
||||
def sim_end(self):
|
||||
dfm = pd.DataFrame(self.means, columns=['metric', 'key', 'value'])
|
||||
dfc = pd.DataFrame(self.counts, columns=['metric', 'key', 'value', 'count'])
|
||||
|
||||
count = {}
|
||||
mean = {}
|
||||
|
||||
if self.means:
|
||||
res = dfm.groupby(by=['key']).agg(['mean', 'std', 'count', 'median', 'max', 'min'])
|
||||
mean = res['value'].to_dict()
|
||||
if self.counts:
|
||||
res = dfc.groupby(by=['key', 'value']).agg(['mean', 'std', 'count', 'median', 'max', 'min'])
|
||||
for k,v in res['count'].to_dict().items():
|
||||
if k not in count:
|
||||
count[k] = {}
|
||||
for tup, times in v.items():
|
||||
subkey, subcount = tup
|
||||
if subkey not in count[k]:
|
||||
count[k][subkey] = {}
|
||||
count[k][subkey][subcount] = times
|
||||
|
||||
|
||||
return {'count': count, 'mean': mean}
|
||||
|
||||
|
||||
class defaultStats(Stats):
|
||||
|
||||
def trial_end(self, env):
|
||||
c = Counter()
|
||||
c.update(a.__class__.__name__ for a in env.network_agents)
|
||||
|
||||
c2 = Counter()
|
||||
c2.update(a['id'] for a in env.network_agents)
|
||||
|
||||
return {
|
||||
'network ': {
|
||||
'n_nodes': env.G.number_of_nodes(),
|
||||
'n_edges': env.G.number_of_edges(),
|
||||
},
|
||||
'agents': {
|
||||
'model_count': dict(c),
|
||||
'state_count': dict(c2),
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from soil import simulation, stats
|
||||
from soil.utils import unflatten_dict
|
||||
|
||||
class Stats(TestCase):
|
||||
|
||||
def test_distribution(self):
|
||||
'''The distribution exporter should write the number of agents in each state'''
|
||||
config = {
|
||||
'name': 'exporter_sim',
|
||||
'network_params': {
|
||||
'generator': 'complete_graph',
|
||||
'n': 4
|
||||
},
|
||||
'agent_type': 'CounterModel',
|
||||
'max_time': 2,
|
||||
'num_trials': 5,
|
||||
'environment_params': {}
|
||||
}
|
||||
s = simulation.from_config(config)
|
||||
for env in s.run_simulation(stats=[stats.distribution]):
|
||||
pass
|
||||
# stats_res = unflatten_dict(dict(env._history['stats', -1, None]))
|
||||
allstats = s.get_stats()
|
||||
for stat in allstats:
|
||||
assert 'count' in stat
|
||||
assert 'mean' in stat
|
||||
if 'trial_id' in stat:
|
||||
assert stat['mean']['neighbors'] == 3
|
||||
assert stat['count']['total']['4'] == 4
|
||||
else:
|
||||
assert stat['count']['count']['neighbors']['3'] == 20
|
||||
assert stat['mean']['min']['neighbors'] == stat['mean']['max']['neighbors']
|
Loading…
Reference in New Issue