mirror of
				https://github.com/gsi-upm/soil
				synced 2025-10-30 23:28:17 +00:00 
			
		
		
		
	WIP: removed stats
This commit is contained in:
		| @@ -14,7 +14,6 @@ network_agents: | ||||
|     weight: 1 | ||||
| environment_class: social_wealth.MoneyEnv | ||||
| environment_params: | ||||
|   num_mesa_agents: 5 | ||||
|   mesa_agent_type: social_wealth.MoneyAgent | ||||
|   N: 10 | ||||
|   width: 50 | ||||
|   | ||||
| @@ -71,10 +71,9 @@ class SocialMoneyAgent(NetworkAgent, MoneyAgent): | ||||
|  | ||||
| class MoneyEnv(Environment): | ||||
|     """A model with some number of agents.""" | ||||
|     def __init__(self, N, width, height, *args, network_params, **kwargs): | ||||
|     def __init__(self, width, height, *args, topologies, **kwargs): | ||||
|  | ||||
|         network_params['n'] = N | ||||
|         super().__init__(*args, network_params=network_params, **kwargs) | ||||
|         super().__init__(*args, topologies=topologies, **kwargs) | ||||
|         self.grid = MultiGrid(width, height, False) | ||||
|  | ||||
|         # Create agents | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| from soil.agents import FSM, state, default_state, prob | ||||
| from soil.agents import FSM, NetworkAgent, state, default_state, prob | ||||
| import logging | ||||
|  | ||||
|  | ||||
| class DumbViewer(FSM): | ||||
| class DumbViewer(FSM, NetworkAgent): | ||||
|     ''' | ||||
|     A viewer that gets infected via TV (if it has one) and tries to infect | ||||
|     its neighbors once it's infected. | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| from soil.agents import FSM, state, default_state | ||||
| from soil.agents import FSM, NetworkAgent, state, default_state | ||||
| from soil import Environment | ||||
| from random import random, shuffle | ||||
| from itertools import islice | ||||
| @@ -53,7 +53,7 @@ class CityPubs(Environment): | ||||
|                 pub['occupancy'] -= 1 | ||||
|  | ||||
|  | ||||
| class Patron(FSM): | ||||
| class Patron(FSM, NetworkAgent): | ||||
|     '''Agent that looks for friends to drink with. It will do three things: | ||||
|         1) Look for other patrons to drink with | ||||
|         2) Look for a bar where the agent and other agents in the same group can get in. | ||||
| @@ -151,7 +151,7 @@ class Patron(FSM): | ||||
|         return befriended | ||||
|  | ||||
|  | ||||
| class Police(FSM): | ||||
| class Police(FSM, NetworkAgent): | ||||
|     '''Simple agent to take drunk people out of pubs.''' | ||||
|     level = logging.INFO | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ class Genders(Enum): | ||||
|     female = 'female' | ||||
|  | ||||
|  | ||||
| class RabbitModel(FSM): | ||||
| class RabbitModel(FSM, NetworkAgent): | ||||
|  | ||||
|     defaults = { | ||||
|         'age': 0, | ||||
| @@ -110,12 +110,12 @@ class Female(RabbitModel): | ||||
|             self.info('A mother has died carrying a baby!!') | ||||
|  | ||||
|  | ||||
| class RandomAccident(NetworkAgent): | ||||
| class RandomAccident(BaseAgent): | ||||
|  | ||||
|     level = logging.DEBUG | ||||
|  | ||||
|     def step(self): | ||||
|         rabbits_total = self.topology.number_of_nodes() | ||||
|         rabbits_total = self.env.topology.number_of_nodes() | ||||
|         if 'rabbits_alive' not in self.env: | ||||
|             self.env['rabbits_alive'] = 0 | ||||
|         rabbits_alive = self.env.get('rabbits_alive', rabbits_total) | ||||
| @@ -131,5 +131,5 @@ class RandomAccident(NetworkAgent): | ||||
|                 self.log('Rabbits alive: {}'.format(self.env['rabbits_alive'])) | ||||
|                 i.set_state(i.dead) | ||||
|         self.log('Rabbits alive: {}/{}'.format(rabbits_alive, rabbits_total)) | ||||
|         if self.count_agents(state_id=RabbitModel.dead.id) == self.topology.number_of_nodes(): | ||||
|         if self.env.count_agents(state_id=RabbitModel.dead.id) == self.env.topology.number_of_nodes(): | ||||
|             self.die() | ||||
|   | ||||
| @@ -55,6 +55,7 @@ class BaseAgent(MesaAgent, MutableMapping): | ||||
|             raise Exception() | ||||
|         assert isinstance(unique_id, int) | ||||
|         super().__init__(unique_id=unique_id, model=model) | ||||
|  | ||||
|         self.name = str(name) if name else'{}[{}]'.format(type(self).__name__, self.unique_id) | ||||
|  | ||||
|  | ||||
| @@ -78,6 +79,9 @@ class BaseAgent(MesaAgent, MutableMapping): | ||||
|             if not hasattr(self, k) or getattr(self, k) is None: | ||||
|                 setattr(self, k, v) | ||||
|  | ||||
|     def __hash__(self): | ||||
|         return hash(self.unique_id) | ||||
|  | ||||
|     # TODO: refactor to clean up mesa compatibility | ||||
|     @property | ||||
|     def id(self): | ||||
| @@ -185,16 +189,14 @@ class BaseAgent(MesaAgent, MutableMapping): | ||||
| # Agent = BaseAgent | ||||
|  | ||||
| class NetworkAgent(BaseAgent): | ||||
|     def __init__(self, | ||||
|                  *args, | ||||
|                  graph_name: str, | ||||
|                  node_id: int = None, | ||||
|                  **kwargs, | ||||
|     ): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.graph_name = graph_name | ||||
|         self.topology = self.env.topologies[self.graph_name] | ||||
|         self.node_id = node_id | ||||
|  | ||||
|     @property | ||||
|     def topology(self): | ||||
|         return self.env.topology_for(self.unique_id) | ||||
|  | ||||
|     @property | ||||
|     def node_id(self): | ||||
|         return self.env.node_id_for(self.unique_id) | ||||
|  | ||||
|     @property | ||||
|     def G(self): | ||||
| @@ -215,15 +217,19 @@ class NetworkAgent(BaseAgent): | ||||
|             it = islice(it, limit) | ||||
|         return list(it) | ||||
|  | ||||
|     def iter_agents(self, agents=None, limit_neighbors=False, **kwargs): | ||||
|     def iter_agents(self, unique_id=None, limit_neighbors=False, **kwargs): | ||||
|         if limit_neighbors: | ||||
|             agents = self.topology.neighbors(self.unique_id) | ||||
|             unique_id = [self.topology.nodes[node]['agent_id'] for node in self.topology.neighbors(self.node_id)] | ||||
|             if not unique_id: | ||||
|                 return | ||||
|  | ||||
|         yield from self.model.agents(unique_id=unique_id, **kwargs) | ||||
|  | ||||
|         return self.model.agents(ids=agents, **kwargs) | ||||
|  | ||||
|     def subgraph(self, center=True, **kwargs): | ||||
|         include = [self] if center else [] | ||||
|         return self.topology.subgraph(n.unique_id for n in list(self.get_agents(**kwargs))+include) | ||||
|         G = self.topology.subgraph(n.node_id for n in list(self.get_agents(**kwargs)+include)) | ||||
|         return G | ||||
|  | ||||
|     def remove_node(self, unique_id): | ||||
|         self.topology.remove_node(unique_id) | ||||
| @@ -366,7 +372,7 @@ def prob(prob=1): | ||||
|  | ||||
|  | ||||
| def calculate_distribution(network_agents=None, | ||||
|                            agent_type=None): | ||||
|                            agent_class=None): | ||||
|     ''' | ||||
|     Calculate the threshold values (thresholds for a uniform distribution) | ||||
|     of an agent distribution given the weights of each agent type. | ||||
| @@ -374,13 +380,13 @@ def calculate_distribution(network_agents=None, | ||||
|     The input has this form: :: | ||||
|  | ||||
|             [ | ||||
|             {'agent_type': 'agent_type_1', | ||||
|             {'agent_class': 'agent_class_1', | ||||
|                 'weight': 0.2, | ||||
|                 'state': { | ||||
|                     'id': 0 | ||||
|                 } | ||||
|             }, | ||||
|             {'agent_type': 'agent_type_2', | ||||
|             {'agent_class': 'agent_class_2', | ||||
|                 'weight': 0.8, | ||||
|                 'state': { | ||||
|                     'id': 1 | ||||
| @@ -389,12 +395,12 @@ def calculate_distribution(network_agents=None, | ||||
|             ] | ||||
|  | ||||
|     In this example, 20% of the nodes will be marked as type | ||||
|     'agent_type_1'. | ||||
|     'agent_class_1'. | ||||
|     ''' | ||||
|     if network_agents: | ||||
|         network_agents = [deepcopy(agent) for agent in network_agents if not hasattr(agent, 'id')] | ||||
|     elif agent_type: | ||||
|         network_agents = [{'agent_type': agent_type}] | ||||
|     elif agent_class: | ||||
|         network_agents = [{'agent_class': agent_class}] | ||||
|     else: | ||||
|         raise ValueError('Specify a distribution or a default agent type') | ||||
|  | ||||
| @@ -414,11 +420,11 @@ def calculate_distribution(network_agents=None, | ||||
|     return network_agents | ||||
|  | ||||
|  | ||||
| def serialize_type(agent_type, known_modules=[], **kwargs): | ||||
|     if isinstance(agent_type, str): | ||||
|         return agent_type | ||||
| def serialize_type(agent_class, known_modules=[], **kwargs): | ||||
|     if isinstance(agent_class, str): | ||||
|         return agent_class | ||||
|     known_modules += ['soil.agents'] | ||||
|     return serialization.serialize(agent_type, known_modules=known_modules, **kwargs)[1] # Get the name of the class | ||||
|     return serialization.serialize(agent_class, known_modules=known_modules, **kwargs)[1] # Get the name of the class | ||||
|  | ||||
|  | ||||
| def serialize_definition(network_agents, known_modules=[]): | ||||
| @@ -430,23 +436,23 @@ def serialize_definition(network_agents, known_modules=[]): | ||||
|     for v in d: | ||||
|         if 'threshold' in v: | ||||
|             del v['threshold'] | ||||
|         v['agent_type'] = serialize_type(v['agent_type'], | ||||
|         v['agent_class'] = serialize_type(v['agent_class'], | ||||
|                                          known_modules=known_modules) | ||||
|     return d | ||||
|  | ||||
|  | ||||
| def deserialize_type(agent_type, known_modules=[]): | ||||
|     if not isinstance(agent_type, str): | ||||
|         return agent_type | ||||
| def deserialize_type(agent_class, known_modules=[]): | ||||
|     if not isinstance(agent_class, str): | ||||
|         return agent_class | ||||
|     known = known_modules + ['soil.agents', 'soil.agents.custom' ] | ||||
|     agent_type = serialization.deserializer(agent_type, known_modules=known) | ||||
|     return agent_type | ||||
|     agent_class = serialization.deserializer(agent_class, known_modules=known) | ||||
|     return agent_class | ||||
|  | ||||
|  | ||||
| def deserialize_definition(ind, **kwargs): | ||||
|     d = deepcopy(ind) | ||||
|     for v in d: | ||||
|         v['agent_type'] = deserialize_type(v['agent_type'], **kwargs) | ||||
|         v['agent_class'] = deserialize_type(v['agent_class'], **kwargs) | ||||
|     return d | ||||
|  | ||||
|  | ||||
| @@ -461,7 +467,7 @@ def _validate_states(states, topology): | ||||
|     return states | ||||
|  | ||||
|  | ||||
| def _convert_agent_types(ind, to_string=False, **kwargs): | ||||
| def _convert_agent_classs(ind, to_string=False, **kwargs): | ||||
|     '''Convenience method to allow specifying agents by class or class name.''' | ||||
|     if to_string: | ||||
|         return serialize_definition(ind, **kwargs) | ||||
| @@ -480,7 +486,7 @@ def _agent_from_definition(definition, value=-1, unique_id=None): | ||||
|             state = {} | ||||
|             if 'state' in d: | ||||
|                 state = deepcopy(d['state']) | ||||
|             return d['agent_type'], state | ||||
|             return d['agent_class'], state | ||||
|  | ||||
|     raise Exception('Definition for value {} not found in: {}'.format(value, definition)) | ||||
|  | ||||
| @@ -576,8 +582,11 @@ class AgentView(Mapping, Set): | ||||
|                 return group[agent_id] | ||||
|         raise ValueError(f"Agent {agent_id} not found") | ||||
|  | ||||
|     def filter(self, *group_ids, **kwargs): | ||||
|         yield from filter_groups(self._agents, group_ids=group_ids, **kwargs) | ||||
|     def filter(self, *args, **kwargs): | ||||
|         yield from filter_groups(self._agents, *args, **kwargs) | ||||
|  | ||||
|     def one(self, *args, **kwargs): | ||||
|         return next(filter_groups(self._agents, *args, **kwargs)) | ||||
|  | ||||
|     def __call__(self, *args, **kwargs): | ||||
|         return list(self.filter(*args, **kwargs)) | ||||
| @@ -586,41 +595,57 @@ class AgentView(Mapping, Set): | ||||
|         return any(agent_id in g for g in self._agents) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return str(list(a.id for a in self)) | ||||
|         return str(list(a.unique_id for a in self)) | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return f"{self.__class__.__name__}({self})" | ||||
|  | ||||
|  | ||||
| def filter_groups(groups, group_ids=None, **kwargs): | ||||
| def filter_groups(groups, *, group=None, **kwargs): | ||||
|     assert isinstance(groups, dict) | ||||
|     if group_ids: | ||||
|         groups = list(groups[g] for g in group_ids if g in groups) | ||||
|  | ||||
|     if group is not None and not isinstance(group, list): | ||||
|         group = [group] | ||||
|  | ||||
|     if group: | ||||
|         groups = list(groups[g] for g in group if g in groups) | ||||
|     else: | ||||
|         groups = list(groups.values()) | ||||
|      | ||||
|  | ||||
|     agents = chain.from_iterable(filter_group(g, **kwargs) for g in groups) | ||||
|  | ||||
|     yield from agents | ||||
|  | ||||
|  | ||||
| def filter_group(group, ids=None, state_id=None, agent_type=None, ignore=None, state=None, **kwargs): | ||||
| def filter_group(group, *id_args, unique_id=None, state_id=None, agent_class=None, ignore=None, state=None, **kwargs): | ||||
|     ''' | ||||
|     Filter agents given as a dict, by the criteria given as arguments (e.g., certain type or state id). | ||||
|     ''' | ||||
|     assert isinstance(group, dict) | ||||
|  | ||||
|     ids = [] | ||||
|  | ||||
|     if unique_id is not None: | ||||
|         if isinstance(unique_id, list): | ||||
|             ids += unique_id | ||||
|         else: | ||||
|             ids.append(unique_id) | ||||
|  | ||||
|     if id_args: | ||||
|         ids += id_args | ||||
|  | ||||
|     if state_id is not None and not isinstance(state_id, (tuple, list)): | ||||
|         state_id = tuple([state_id]) | ||||
|  | ||||
|     if agent_type is not None: | ||||
|     if agent_class is not None: | ||||
|         agent_class = deserialize_type(agent_class) | ||||
|         try: | ||||
|             agent_type = tuple(agent_type) | ||||
|             agent_class = tuple(agent_class) | ||||
|         except TypeError: | ||||
|             agent_type = tuple([agent_type]) | ||||
|             agent_class = tuple([agent_class]) | ||||
|  | ||||
|     if ids: | ||||
|         agents = (v[aid] for aid in ids if aid in group) | ||||
|         agents = (group[aid] for aid in ids if aid in group) | ||||
|     else: | ||||
|         agents = (a for a in group.values()) | ||||
|  | ||||
| @@ -631,8 +656,8 @@ def filter_group(group, ids=None, state_id=None, agent_type=None, ignore=None, s | ||||
|     if state_id is not None: | ||||
|         f = filter(lambda agent: agent.get('state_id', None) in state_id, f) | ||||
|  | ||||
|     if agent_type is not None: | ||||
|         f = filter(lambda agent: isinstance(agent, agent_type), f) | ||||
|     if agent_class is not None: | ||||
|         f = filter(lambda agent: isinstance(agent, agent_class), f) | ||||
|  | ||||
|     state = state or dict() | ||||
|     state.update(kwargs) | ||||
| @@ -660,7 +685,7 @@ def _group_from_config(cfg: config.AgentConfig, default: config.SingleAgentConfi | ||||
|     if cfg.fixed is not None: | ||||
|         agents = _from_fixed(cfg.fixed, topology=cfg.topology, default=default, env=env) | ||||
|     if cfg.distribution: | ||||
|         n = cfg.n or len(env.topologies[cfg.topology]) | ||||
|         n = cfg.n or len(env.topologies[cfg.topology or default.topology]) | ||||
|         target = n - len(agents) | ||||
|         agents.update(_from_distro(cfg.distribution, target, | ||||
|                                    topology=cfg.topology or default.topology, | ||||
| @@ -674,6 +699,8 @@ def _group_from_config(cfg: config.AgentConfig, default: config.SingleAgentConfi | ||||
|             else: | ||||
|                 filtered = list(agents) | ||||
|  | ||||
|             if attrs.n > len(filtered): | ||||
|                 raise ValueError(f'Not enough agents to sample. Got {len(filtered)}, expected >= {attrs.n}') | ||||
|             for agent in random.sample(filtered, attrs.n): | ||||
|                 agent.state.update(attrs.state) | ||||
|  | ||||
| @@ -684,18 +711,20 @@ def _from_fixed(lst: List[config.FixedAgentConfig], topology: str, default: conf | ||||
|     agents = {} | ||||
|  | ||||
|     for fixed in lst: | ||||
|       agent_id = fixed.agent_id | ||||
|       if agent_id is None: | ||||
|           agent_id = env.next_id() | ||||
|         agent_id = fixed.agent_id | ||||
|         if agent_id is None: | ||||
|             agent_id = env.next_id() | ||||
|  | ||||
|       cls = serialization.deserialize(fixed.agent_class or default.agent_class) | ||||
|       state = fixed.state.copy() | ||||
|       state.update(default.state) | ||||
|       agent = cls(unique_id=agent_id, | ||||
|                   model=env, | ||||
|                   graph_name=fixed.topology or topology or default.topology, | ||||
|                   **state) | ||||
|       agents[agent.unique_id] = agent | ||||
|         cls = serialization.deserialize(fixed.agent_class or default.agent_class) | ||||
|         state = fixed.state.copy() | ||||
|         state.update(default.state) | ||||
|         agent = cls(unique_id=agent_id, | ||||
|                     model=env, | ||||
|                     **state) | ||||
|         topology = fixed.topology if (fixed.topology is not None) else (topology or default.topology) | ||||
|         if topology: | ||||
|             env.agent_to_node(agent_id, topology, fixed.node_id) | ||||
|         agents[agent.unique_id] = agent | ||||
|  | ||||
|     return agents | ||||
|  | ||||
| @@ -741,8 +770,12 @@ def _from_distro(distro: List[config.AgentDistro], | ||||
|         cls = classes[idx] | ||||
|         agent_id = env.next_id() | ||||
|         state = d.state.copy() | ||||
|         state.update(default.state) | ||||
|         agent = cls(unique_id=agent_id, model=env, graph_name=d.topology or topology or default.topology, **state) | ||||
|         if default: | ||||
|             state.update(default.state) | ||||
|         agent = cls(unique_id=agent_id, model=env, **state) | ||||
|         topology = d.topology if (d.topology is not None) else topology or default.topology | ||||
|         if topology: | ||||
|             env.agent_to_node(agent.unique_id, topology) | ||||
|         assert agent.name is not None | ||||
|         assert agent.name != 'None' | ||||
|         assert agent.name | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import sys | ||||
|  | ||||
| from typing import Any, Callable, Dict, List, Optional, Union, Type | ||||
| from pydantic import BaseModel, Extra | ||||
| import networkx as nx | ||||
|  | ||||
| class General(BaseModel): | ||||
|     id: str = 'Unnamed Simulation' | ||||
| @@ -50,9 +51,12 @@ class NetParams(BaseModel, extra=Extra.allow): | ||||
| class NetConfig(BaseModel): | ||||
|     group: str = 'network' | ||||
|     params: Optional[NetParams] | ||||
|     topology: Optional[Topology] | ||||
|     topology: Optional[Union[Topology, nx.Graph]] | ||||
|     path: Optional[str] | ||||
|  | ||||
|     class Config: | ||||
|         arbitrary_types_allowed = True | ||||
|  | ||||
|     @staticmethod | ||||
|     def default(): | ||||
|         return NetConfig(topology=None, params=None) | ||||
| @@ -77,7 +81,8 @@ class EnvConfig(BaseModel): | ||||
| class SingleAgentConfig(BaseModel): | ||||
|     agent_class: Optional[Union[Type, str]] = None | ||||
|     agent_id: Optional[int] = None | ||||
|     topology: Optional[str] = 'default' | ||||
|     topology: Optional[str] = None | ||||
|     node_id: Optional[Union[int, str]] = None | ||||
|     name: Optional[str] = None | ||||
|     state: Optional[Dict[str, Any]] = {} | ||||
|  | ||||
| @@ -186,9 +191,7 @@ def convert_old(old, strict=True): | ||||
|         if 'agent_id' in agent: | ||||
|             agent['name'] = agent['agent_id'] | ||||
|             del agent['agent_id'] | ||||
|             agents['environment']['fixed'].append(updated_agent(agent)) | ||||
|         else: | ||||
|             agents['environment']['distribution'].append(updated_agent(agent)) | ||||
|         agents['environment']['fixed'].append(updated_agent(agent)) | ||||
|  | ||||
|     by_weight = [] | ||||
|     fixed = [] | ||||
| @@ -206,10 +209,10 @@ def convert_old(old, strict=True): | ||||
|  | ||||
|     if 'agent_type' in old and (not fixed and not by_weight): | ||||
|         agents['network']['topology'] = 'default' | ||||
|         by_weight = [{'agent_type': old['agent_type']}] | ||||
|         by_weight = [{'agent_class': old['agent_type']}] | ||||
|  | ||||
|      | ||||
|     # TODO: translate states | ||||
|     # TODO: translate states properly | ||||
|     if 'states' in old: | ||||
|         states = old['states'] | ||||
|         if isinstance(states, dict): | ||||
| @@ -217,7 +220,7 @@ def convert_old(old, strict=True): | ||||
|         else: | ||||
|             states = enumerate(states) | ||||
|         for (k, v) in states: | ||||
|             override.append({'filter': {'id': k}, | ||||
|             override.append({'filter': {'node_id': k}, | ||||
|                              'state': v | ||||
|             }) | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
| @@ -15,6 +15,7 @@ from networkx.readwrite import json_graph | ||||
| import networkx as nx | ||||
|  | ||||
| from mesa import Model | ||||
| from mesa.datacollection import DataCollector | ||||
|  | ||||
| from . import serialization, agents, analysis, utils, time, config, network | ||||
|  | ||||
| @@ -41,6 +42,9 @@ class Environment(Model): | ||||
|                  interval=1, | ||||
|                  agents: Dict[str, config.AgentConfig] = {}, | ||||
|                  topologies: Dict[str, config.NetConfig] = {}, | ||||
|                  agent_reporters: Optional[Any] = None, | ||||
|                  model_reporters: Optional[Any] = None, | ||||
|                  tables: Optional[Any] = None, | ||||
|                  **env_params): | ||||
|  | ||||
|         super().__init__() | ||||
| @@ -61,6 +65,7 @@ class Environment(Model): | ||||
|  | ||||
|  | ||||
|         self.topologies = {} | ||||
|         self._node_ids = {} | ||||
|         for (name, cfg) in topologies.items(): | ||||
|             self.set_topology(cfg=cfg, | ||||
|                               graph=name) | ||||
| @@ -72,6 +77,7 @@ class Environment(Model): | ||||
|         self['SEED'] = seed | ||||
|  | ||||
|         self.logger = utils.logger.getChild(self.id) | ||||
|         self.datacollector = DataCollector(model_reporters, agent_reporters, tables) | ||||
|  | ||||
|     @property | ||||
|     def topology(self): | ||||
| @@ -79,8 +85,7 @@ class Environment(Model): | ||||
|  | ||||
|     @property | ||||
|     def network_agents(self): | ||||
|         yield from self.agents(agent_type=agents.NetworkAgent, iterator=False) | ||||
|  | ||||
|         yield from self.agents(agent_class=agents.NetworkAgent) | ||||
|  | ||||
|     @staticmethod | ||||
|     def from_config(conf: config.Config, trial_id, **kwargs) -> Environment: | ||||
| @@ -91,9 +96,10 @@ class Environment(Model): | ||||
|         seed = '{}_{}'.format(conf.general.seed, trial_id) | ||||
|         id = '{}_trial_{}'.format(conf.general.id, trial_id).replace('.', '-') | ||||
|         opts = conf.environment.params.copy() | ||||
|         dir_path = conf.general.dir_path | ||||
|         opts.update(conf) | ||||
|         opts.update(kwargs) | ||||
|         env = serialization.deserialize(conf.environment.environment_class)(env_id=id, seed=seed, **opts) | ||||
|         env = serialization.deserialize(conf.environment.environment_class)(env_id=id, seed=seed, dir_path=dir_path, **opts) | ||||
|         return env | ||||
|  | ||||
|     @property | ||||
| @@ -103,12 +109,31 @@ class Environment(Model): | ||||
|         raise Exception('The environment has not been scheduled, so it has no sense of time') | ||||
|  | ||||
|  | ||||
|     def topology_for(self, agent_id): | ||||
|         return self.topologies[self._node_ids[agent_id][0]] | ||||
|  | ||||
|     def node_id_for(self, agent_id): | ||||
|         return self._node_ids[agent_id][1] | ||||
|  | ||||
|     def set_topology(self, cfg=None, dir_path=None, graph='default'): | ||||
|         self.topologies[graph] = network.from_config(cfg, dir_path=dir_path) | ||||
|         topology = cfg | ||||
|         if not isinstance(cfg, nx.Graph): | ||||
|             topology = network.from_config(cfg, dir_path=dir_path or self.dir_path) | ||||
|  | ||||
|         self.topologies[graph] = topology | ||||
|  | ||||
|     @property | ||||
|     def agents(self): | ||||
|         return agents.AgentView(self._agents) | ||||
|      | ||||
|     def count_agents(self, *args, **kwargs): | ||||
|         return sum(1 for i in self.find_all(*args, **kwargs)) | ||||
|  | ||||
|     def find_all(self, *args, **kwargs): | ||||
|         return agents.AgentView(self._agents).filter(*args, **kwargs) | ||||
|  | ||||
|     def find_one(self, *args, **kwargs): | ||||
|         return agents.AgentView(self._agents).one(*args, **kwargs) | ||||
|  | ||||
|     @agents.setter | ||||
|     def agents(self, agents_def: Dict[str, config.AgentConfig]): | ||||
| @@ -117,37 +142,47 @@ class Environment(Model): | ||||
|             for a in d.values(): | ||||
|                 self.schedule.add(a) | ||||
|  | ||||
|  | ||||
|     # @property | ||||
|     # def network_agents(self): | ||||
|     #     for i in self.G.nodes(): | ||||
|     #         node = self.G.nodes[i] | ||||
|     #         if 'agent' in node: | ||||
|     #             yield node['agent'] | ||||
|  | ||||
|     def init_agent(self, agent_id, agent_definitions, graph='default'): | ||||
|         node = self.topologies[graph].nodes[agent_id] | ||||
|         init = False | ||||
|         state = dict(node) | ||||
|  | ||||
|         agent_type = None | ||||
|         if 'agent_type' in self.states.get(agent_id, {}): | ||||
|             agent_type = self.states[agent_id]['agent_type'] | ||||
|         elif 'agent_type' in node: | ||||
|             agent_type = node['agent_type'] | ||||
|         elif 'agent_type' in self.default_state: | ||||
|             agent_type = self.default_state['agent_type'] | ||||
|         agent_class = None | ||||
|         if 'agent_class' in self.states.get(agent_id, {}): | ||||
|             agent_class = self.states[agent_id]['agent_class'] | ||||
|         elif 'agent_class' in node: | ||||
|             agent_class = node['agent_class'] | ||||
|         elif 'agent_class' in self.default_state: | ||||
|             agent_class = self.default_state['agent_class'] | ||||
|  | ||||
|         if agent_type: | ||||
|             agent_type = agents.deserialize_type(agent_type) | ||||
|         if agent_class: | ||||
|             agent_class = agents.deserialize_type(agent_class) | ||||
|         elif agent_definitions: | ||||
|             agent_type, state = agents._agent_from_definition(agent_definitions, unique_id=agent_id) | ||||
|             agent_class, state = agents._agent_from_definition(agent_definitions, unique_id=agent_id) | ||||
|         else: | ||||
|             serialization.logger.debug('Skipping node {}'.format(agent_id)) | ||||
|             return | ||||
|         return self.set_agent(agent_id, agent_type, state) | ||||
|         return self.set_agent(agent_id, agent_class, state) | ||||
|  | ||||
|     def set_agent(self, agent_id, agent_type, state=None, graph='default'): | ||||
|     def agent_to_node(self, agent_id, graph_name='default', node_id=None, shuffle=False): | ||||
|         #TODO: test | ||||
|         if node_id is None: | ||||
|             G = self.topologies[graph_name] | ||||
|             candidates = list(G.nodes(data=True)) | ||||
|             if shuffle: | ||||
|                 random.shuffle(candidates) | ||||
|             for next_id, data in candidates: | ||||
|                 if data.get('agent_id', None) is None: | ||||
|                     node_id = next_id | ||||
|                     data['agent_id'] = agent_id | ||||
|                     break | ||||
|                      | ||||
|  | ||||
|         self._node_ids[agent_id] = (graph_name, node_id) | ||||
|         print(self._node_ids) | ||||
|  | ||||
|  | ||||
|     def set_agent(self, agent_id, agent_class, state=None, graph='default'): | ||||
|         node = self.topologies[graph].nodes[agent_id] | ||||
|         defstate = deepcopy(self.default_state) or {} | ||||
|         defstate.update(self.states.get(agent_id, {})) | ||||
| @@ -155,9 +190,9 @@ class Environment(Model): | ||||
|         if state: | ||||
|             defstate.update(state) | ||||
|         a = None | ||||
|         if agent_type: | ||||
|         if agent_class: | ||||
|             state = defstate | ||||
|             a = agent_type(model=self, | ||||
|             a = agent_class(model=self, | ||||
|                            unique_id=agent_id | ||||
|             ) | ||||
|  | ||||
| @@ -168,10 +203,10 @@ class Environment(Model): | ||||
|         self.schedule.add(a) | ||||
|         return a | ||||
|  | ||||
|     def add_node(self, agent_type, state=None, graph='default'): | ||||
|     def add_node(self, agent_class, state=None, graph='default'): | ||||
|         agent_id = int(len(self.topologies[graph].nodes())) | ||||
|         self.topologies[graph].add_node(agent_id) | ||||
|         a = self.set_agent(agent_id, agent_type, state, graph=graph) | ||||
|         a = self.set_agent(agent_id, agent_class, state, graph=graph) | ||||
|         a['visible'] = True | ||||
|         return a | ||||
|  | ||||
| @@ -201,6 +236,7 @@ class Environment(Model): | ||||
|         ''' | ||||
|         super().step() | ||||
|         self.schedule.step() | ||||
|         self.datacollector.collect(self) | ||||
|  | ||||
|     def run(self, until, *args, **kwargs): | ||||
|         until = until or float('inf') | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import os | ||||
| import csv as csvlib | ||||
| from time import time as current_time | ||||
| from io import BytesIO | ||||
| from sqlalchemy import create_engine | ||||
| @@ -59,7 +58,7 @@ class Exporter: | ||||
|         '''Method to call when the simulation starts''' | ||||
|         pass | ||||
|  | ||||
|     def sim_end(self, stats): | ||||
|     def sim_end(self): | ||||
|         '''Method to call when the simulation ends''' | ||||
|         pass | ||||
|  | ||||
| @@ -67,7 +66,7 @@ class Exporter: | ||||
|         '''Method to call when a trial start''' | ||||
|         pass | ||||
|  | ||||
|     def trial_end(self, env, stats): | ||||
|     def trial_end(self, env): | ||||
|         '''Method to call when a trial ends''' | ||||
|         pass | ||||
|  | ||||
| @@ -115,31 +114,35 @@ class default(Exporter): | ||||
|     #               self.simulation.dump_sqlite(f) | ||||
|  | ||||
|  | ||||
| def get_dc_dfs(dc): | ||||
|     dfs = {'env': dc.get_model_vars_dataframe(), | ||||
|         'agents': dc.get_agent_vars_dataframe } | ||||
|     for table_name in dc.tables: | ||||
|         dfs[table_name] = dc.get_table_dataframe(table_name) | ||||
|     yield from dfs.items()  | ||||
|  | ||||
|  | ||||
| class csv(Exporter): | ||||
|  | ||||
|     '''Export the state of each environment (and its agents) in a separate CSV file''' | ||||
|     def trial_end(self, env, stats): | ||||
|     def trial_end(self, env): | ||||
|         with timer('[CSV] Dumping simulation {} trial {} @ dir {}'.format(self.simulation.name, | ||||
|                                                                           env.name, | ||||
|                                                                           env.id, | ||||
|                                                                           self.outdir)): | ||||
|  | ||||
|             with self.output('{}.stats.{}.csv'.format(env.name, stats.name)) as f: | ||||
|                 statwriter = csvlib.writer(f, delimiter='\t', quotechar='"', quoting=csvlib.QUOTE_ALL) | ||||
|  | ||||
|                 for stat in stats: | ||||
|                     statwriter.writerow(stat) | ||||
|             for (df_name, df) in get_dc_dfs(env.datacollector): | ||||
|                 with self.output('{}.stats.{}.csv'.format(env.id, df_name)) as f: | ||||
|                     df.to_csv(f) | ||||
|  | ||||
|  | ||||
| class gexf(Exporter): | ||||
|     def trial_end(self, env, stats): | ||||
|     def trial_end(self, env): | ||||
|         if self.dry_run: | ||||
|             logger.info('Not dumping GEXF in dry_run mode') | ||||
|             return | ||||
|  | ||||
|         with timer('[GEXF] Dumping simulation {} trial {}'.format(self.simulation.name, | ||||
|                                                                   env.name)): | ||||
|             with self.output('{}.gexf'.format(env.name), mode='wb') as f: | ||||
|                                                                   env.id)): | ||||
|             with self.output('{}.gexf'.format(env.id), mode='wb') as f: | ||||
|                 self.dump_gexf(env, f) | ||||
|  | ||||
|     def dump_gexf(self, env, f): | ||||
| @@ -159,25 +162,25 @@ class dummy(Exporter): | ||||
|         with self.output('dummy', 'w') as f: | ||||
|             f.write('simulation started @ {}\n'.format(current_time())) | ||||
|  | ||||
|     def trial_end(self, env, stats): | ||||
|     def trial_start(self, env): | ||||
|         with self.output('dummy', 'w') as f: | ||||
|             for i in stats: | ||||
|                 f.write(','.join(map(str, i))) | ||||
|                 f.write('\n') | ||||
|             f.write('trial started@ {}\n'.format(current_time())) | ||||
|  | ||||
|     def sim_end(self, stats): | ||||
|     def trial_end(self, env): | ||||
|         with self.output('dummy', 'w') as f: | ||||
|             f.write('trial ended@ {}\n'.format(current_time())) | ||||
|  | ||||
|     def sim_end(self): | ||||
|         with self.output('dummy', 'a') as f: | ||||
|             f.write('simulation ended @ {}\n'.format(current_time())) | ||||
|  | ||||
|  | ||||
|  | ||||
| class graphdrawing(Exporter): | ||||
|  | ||||
|     def trial_end(self, env, stats): | ||||
|     def trial_end(self, env): | ||||
|         # Outside effects | ||||
|         f = plt.figure() | ||||
|         nx.draw(env.G, node_size=10, width=0.2, pos=nx.spring_layout(env.G, scale=100), ax=f.add_subplot(111)) | ||||
|         with open('graph-{}.png'.format(env.name)) as f: | ||||
|         with open('graph-{}.png'.format(env.id)) as f: | ||||
|             f.savefig(f) | ||||
|  | ||||
| ''' | ||||
|   | ||||
| @@ -16,7 +16,6 @@ from . import serialization, utils, basestring, agents | ||||
| from .environment import Environment | ||||
| from .utils import logger | ||||
| from .exporters import default | ||||
| from .stats import defaultStats | ||||
|  | ||||
| from .config import Config, convert_old | ||||
|  | ||||
| @@ -71,8 +70,8 @@ class Simulation: | ||||
|                                      **kwargs) | ||||
|  | ||||
|     def run_gen(self, parallel=False, dry_run=False, | ||||
|                 exporters=[default, ], stats=[], outdir=None, exporter_params={}, | ||||
|                 stats_params={}, log_level=None, | ||||
|                 exporters=[default, ], outdir=None, exporter_params={}, | ||||
|                 log_level=None, | ||||
|                 **kwargs): | ||||
|         '''Run the simulation and yield the resulting environments.''' | ||||
|         if log_level: | ||||
| @@ -85,15 +84,8 @@ class Simulation: | ||||
|                                                   dry_run=dry_run, | ||||
|                                                   outdir=outdir, | ||||
|                                                   **exporter_params) | ||||
|         stats = serialization.deserialize_all(simulation=self, | ||||
|                                              names=stats, | ||||
|                                              known_modules=['soil.stats',], | ||||
|                                              **stats_params) | ||||
|  | ||||
|         with utils.timer('simulation {}'.format(self.config.general.id)): | ||||
|             for stat in stats: | ||||
|                 stat.sim_start() | ||||
|  | ||||
|             for exporter in exporters: | ||||
|                 exporter.sim_start() | ||||
|  | ||||
| @@ -104,32 +96,13 @@ class Simulation: | ||||
|                 for exporter in exporters: | ||||
|                     exporter.trial_start(env) | ||||
|  | ||||
|                 collected = list(stat.trial_end(env) for stat in stats) | ||||
|  | ||||
|                 saved = self._update_stats(collected, t_step=env.now, trial_id=env.id) | ||||
|  | ||||
|                 for exporter in exporters: | ||||
|                     exporter.trial_end(env, saved) | ||||
|                     exporter.trial_end(env) | ||||
|  | ||||
|                 yield env | ||||
|  | ||||
|             collected = list(stat.end() for stat in stats) | ||||
|             saved = self._update_stats(collected) | ||||
|  | ||||
|             for stat in stats: | ||||
|                 stat.sim_end() | ||||
|  | ||||
|             for exporter in exporters: | ||||
|                 exporter.sim_end(saved) | ||||
|  | ||||
|     def _update_stats(self, collection, **kwargs): | ||||
|         stats = dict(kwargs) | ||||
|         for stat in collection: | ||||
|             stats.update(stat) | ||||
|         return stats | ||||
|  | ||||
|     def log_stats(self, stats): | ||||
|         logger.info('Stats: \n{}'.format(yaml.dump(stats, default_flow_style=False))) | ||||
|                 exporter.sim_end() | ||||
|  | ||||
|     def get_env(self, trial_id=0, **kwargs): | ||||
|         '''Create an environment for a trial of the simulation''' | ||||
|   | ||||
							
								
								
									
										111
									
								
								soil/stats.py
									
									
									
									
									
								
							
							
						
						
									
										111
									
								
								soil/stats.py
									
									
									
									
									
								
							| @@ -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), | ||||
|             } | ||||
|         } | ||||
| @@ -29,11 +29,11 @@ agents: | ||||
|         weight: 0.6 | ||||
|     override: | ||||
|       - filter: | ||||
|           id: 0 | ||||
|           node_id: 0 | ||||
|         state: | ||||
|           name: 'The first node' | ||||
|       - filter: | ||||
|           id: 1 | ||||
|           node_id: 1 | ||||
|         state: | ||||
|           name: 'The second node' | ||||
|  | ||||
|   | ||||
| @@ -71,11 +71,11 @@ class TestConfig(TestCase): | ||||
|         s = simulation.from_config(cfg) | ||||
|         env = s.get_env() | ||||
|         assert len(env.topologies['default'].nodes) == 10 | ||||
|         assert len(env.agents('network')) == 10 | ||||
|         assert len(env.agents('environment')) == 1 | ||||
|         assert len(env.agents(group='network')) == 10 | ||||
|         assert len(env.agents(group='environment')) == 1 | ||||
|          | ||||
|         assert sum(1 for a in env.agents('network') if isinstance(a, agents.CounterModel)) == 4 | ||||
|         assert sum(1 for a in env.agents('network') if isinstance(a, agents.AggregatedCounter)) == 6 | ||||
|         assert sum(1 for a in env.agents(group='network', agent_type=agents.CounterModel)) == 4 | ||||
|         assert sum(1 for a in env.agents(group='network', agent_type=agents.AggregatedCounter)) == 6 | ||||
|  | ||||
| def make_example_test(path, cfg): | ||||
|     def wrapped(self): | ||||
|   | ||||
| @@ -7,8 +7,6 @@ from unittest import TestCase | ||||
| from soil import exporters | ||||
| from soil import simulation | ||||
|  | ||||
| from soil.stats import distribution | ||||
|  | ||||
| class Dummy(exporters.Exporter): | ||||
|     started = False | ||||
|     trials = 0 | ||||
| @@ -22,13 +20,13 @@ class Dummy(exporters.Exporter): | ||||
|         self.__class__.called_start += 1 | ||||
|         self.__class__.started = True | ||||
|  | ||||
|     def trial_end(self, env, stats): | ||||
|     def trial_end(self, env): | ||||
|         assert env | ||||
|         self.__class__.trials += 1 | ||||
|         self.__class__.total_time += env.now | ||||
|         self.__class__.called_trial += 1 | ||||
|  | ||||
|     def sim_end(self, stats): | ||||
|     def sim_end(self): | ||||
|         self.__class__.ended = True | ||||
|         self.__class__.called_end += 1 | ||||
|  | ||||
| @@ -78,7 +76,6 @@ class Exporters(TestCase): | ||||
|                                     exporters.csv, | ||||
|                                     exporters.gexf, | ||||
|                                 ], | ||||
|                                 stats=[distribution,], | ||||
|                                 dry_run=False, | ||||
|                                 outdir=tmpdir, | ||||
|                                 exporter_params={'copy_to': output}) | ||||
|   | ||||
| @@ -10,7 +10,7 @@ from functools import partial | ||||
|  | ||||
| from os.path import join | ||||
| from soil import (simulation, Environment, agents, network, serialization, | ||||
|                   utils) | ||||
|                   utils, config) | ||||
| from soil.time import Delta | ||||
|  | ||||
| ROOT = os.path.abspath(os.path.dirname(__file__)) | ||||
| @@ -200,7 +200,6 @@ class TestMain(TestCase): | ||||
|             recovered = yaml.load(serial, Loader=yaml.SafeLoader) | ||||
|         for (k, v) in config.items(): | ||||
|             assert recovered[k] == v | ||||
|         # assert config == recovered | ||||
|  | ||||
|     def test_configuration_changes(self): | ||||
|         """ | ||||
| @@ -294,11 +293,13 @@ class TestMain(TestCase): | ||||
|         G.add_node(3) | ||||
|         G.add_edge(1, 2) | ||||
|         distro = agents.calculate_distribution(agent_type=agents.NetworkAgent) | ||||
|         env = Environment(name='Test', topology=G, network_agents=distro) | ||||
|         distro[0]['topology'] = 'default' | ||||
|         aconfig = config.AgentConfig(distribution=distro, topology='default') | ||||
|         env = Environment(name='Test', topologies={'default': G}, agents={'network': aconfig}) | ||||
|         lst = list(env.network_agents) | ||||
|  | ||||
|         a2 = env.get_agent(2) | ||||
|         a3 = env.get_agent(3) | ||||
|         a2 = env.find_one(node_id=2) | ||||
|         a3 = env.find_one(node_id=3) | ||||
|         assert len(a2.subgraph(limit_neighbors=True)) == 2 | ||||
|         assert len(a3.subgraph(limit_neighbors=True)) == 1 | ||||
|         assert len(a3.subgraph(limit_neighbors=True, center=False)) == 0 | ||||
|   | ||||
| @@ -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'] | ||||
		Reference in New Issue
	
	Block a user