mirror of
https://github.com/gsi-upm/soil
synced 2024-12-22 00:08:12 +00:00
WIP
This commit is contained in:
parent
6f7481769e
commit
bbaed636a8
@ -39,6 +39,7 @@ As of this writing,
|
||||
This is a non-exhaustive list of tasks to achieve compatibility:
|
||||
|
||||
* Environments.agents and mesa.Agent.agents are not the same. env is a property, and it only takes into account network and environment agents. Might rename environment_agents to other_agents or sth like that
|
||||
|
||||
- [ ] Integrate `soil.Simulation` with mesa's runners:
|
||||
- [ ] `soil.Simulation` could mimic/become a `mesa.batchrunner`
|
||||
- [ ] Integrate `soil.Environment` with `mesa.Model`:
|
||||
|
@ -88,9 +88,18 @@ For example, the following configuration is equivalent to :code:`nx.complete_gra
|
||||
|
||||
Environment
|
||||
============
|
||||
|
||||
The environment is the place where the shared state of the simulation is stored.
|
||||
For instance, the probability of disease outbreak.
|
||||
The configuration file may specify the initial value of the environment parameters:
|
||||
That means both global parameters, such as the probability of disease outbreak.
|
||||
But it also means other data, such as a map, or a network topology that connects multiple agents.
|
||||
As a result, it is also typical to add custom functions in an environment that help agents interact with each other and with the state of the simulation.
|
||||
|
||||
Last but not least, an environment controls when and how its agents will be executed.
|
||||
By default, soil environments incorporate a ``soil.time.TimedActivation`` model for agent execution (more on this on the following section).
|
||||
|
||||
Soil environments are very similar, and often interchangeable with, mesa models (``mesa.Model``).
|
||||
|
||||
A configuration may specify the initial value of the environment parameters:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
@ -98,23 +107,33 @@ The configuration file may specify the initial value of the environment paramete
|
||||
daily_probability_of_earthquake: 0.001
|
||||
number_of_earthquakes: 0
|
||||
|
||||
All agents have access to the environment parameters.
|
||||
All agents have access to the environment (and its parameters).
|
||||
|
||||
In some scenarios, it is useful to have a custom environment, to provide additional methods or to control the way agents update environment state.
|
||||
For example, if our agents play the lottery, the environment could provide a method to decide whether the agent wins, instead of leaving it to the agent.
|
||||
|
||||
|
||||
Agents
|
||||
======
|
||||
|
||||
Agents are a way of modelling behavior.
|
||||
Agents can be characterized with two variables: agent type (``agent_type``) and state.
|
||||
Only one agent is executed at a time (generally, every ``interval`` seconds), and it has access to its state and the environment parameters.
|
||||
The agent type is a ``soil.Agent`` class, which contains the code that encapsulates the behavior of the agent.
|
||||
The state is a set of variables, which may change during the simulation, and that the code may use to control the behavior.
|
||||
All agents provide a ``step`` method either explicitly or implicitly (by inheriting it from a superclass), which controls how the agent will behave in each step of the simulation.
|
||||
|
||||
When and how agent steps are executed in a simulation depends entirely on the ``environment``.
|
||||
Most environments will internally use a scheduler (``mesa.time.BaseScheduler``), which controls the activation of agents.
|
||||
|
||||
In soil, we generally used the ``soil.time.TimedActivation`` scheduler, which allows agents to specify when their next activation will happen, defaulting to a
|
||||
|
||||
When an agent's step is executed (generally, every ``interval`` seconds), the agent has access to its state and the environment.
|
||||
Through the environment, it can access the network topology and the state of other agents.
|
||||
|
||||
There are three three types of agents according to how they are added to the simulation: network agents and environment agent.
|
||||
There are two types of agents according to how they are added to the simulation: network agents and environment agent.
|
||||
|
||||
Network Agents
|
||||
##############
|
||||
|
||||
Network agents are attached to a node in the topology.
|
||||
The configuration file allows you to specify how agents will be mapped to topology nodes.
|
||||
|
||||
@ -125,7 +144,9 @@ Hence, every node in the network will be associated to an agent of that type.
|
||||
|
||||
agent_type: SISaModel
|
||||
|
||||
It is also possible to add more than one type of agent to the simulation, and to control the ratio of each type (using the ``weight`` property).
|
||||
It is also possible to add more than one type of agent to the simulation.
|
||||
|
||||
To control the ratio of each type (using the ``weight`` property).
|
||||
For instance, with following configuration, it is five times more likely for a node to be assigned a CounterModel type than a SISaModel type.
|
||||
|
||||
.. code:: yaml
|
||||
|
@ -1,27 +1,38 @@
|
||||
---
|
||||
name: simple
|
||||
group: tests
|
||||
dir_path: "/tmp/"
|
||||
num_trials: 3
|
||||
max_time: 100
|
||||
interval: 1
|
||||
seed: "CompleteSeed!"
|
||||
network_params:
|
||||
generator: complete_graph
|
||||
n: 10
|
||||
network_agents:
|
||||
- agent_type: CounterModel
|
||||
weight: 1
|
||||
general:
|
||||
name: simple
|
||||
group: tests
|
||||
dir_path: "/tmp/"
|
||||
num_trials: 3
|
||||
max_time: 100
|
||||
interval: 1
|
||||
seed: "CompleteSeed!"
|
||||
network:
|
||||
group:
|
||||
network
|
||||
params:
|
||||
generator: complete_graph
|
||||
n: 10
|
||||
environment:
|
||||
environment_class: Environment
|
||||
params:
|
||||
am_i_complete: true
|
||||
agents:
|
||||
default:
|
||||
agent_class: CounterModel
|
||||
state:
|
||||
state_id: 0
|
||||
- agent_type: AggregatedCounter
|
||||
weight: 0.2
|
||||
environment_agents: []
|
||||
environment_class: Environment
|
||||
environment_params:
|
||||
am_i_complete: true
|
||||
default_state:
|
||||
incidents: 0
|
||||
states:
|
||||
- name: 'The first node'
|
||||
- name: 'The second node'
|
||||
times: 1
|
||||
environment:
|
||||
fixed:
|
||||
- agent_id: 'Environment Agent 1'
|
||||
agent_class: CounterModel
|
||||
state:
|
||||
times: 10
|
||||
network:
|
||||
distribution:
|
||||
- agent_class: CounterModel
|
||||
weight: 1
|
||||
state:
|
||||
state_id: 0
|
||||
- agent_class: AggregatedCounter
|
||||
weight: 0.2
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
default_state: {}
|
||||
load_module: newsspread
|
||||
environment_agents: []
|
||||
environment_params:
|
||||
prob_neighbor_spread: 0.0
|
||||
@ -9,11 +8,11 @@ interval: 1
|
||||
max_time: 300
|
||||
name: Sim_all_dumb
|
||||
network_agents:
|
||||
- agent_type: DumbViewer
|
||||
- agent_type: newsspread.DumbViewer
|
||||
state:
|
||||
has_tv: false
|
||||
weight: 1
|
||||
- agent_type: DumbViewer
|
||||
- agent_type: newsspread.DumbViewer
|
||||
state:
|
||||
has_tv: true
|
||||
weight: 1
|
||||
@ -24,7 +23,6 @@ network_params:
|
||||
num_trials: 50
|
||||
---
|
||||
default_state: {}
|
||||
load_module: newsspread
|
||||
environment_agents: []
|
||||
environment_params:
|
||||
prob_neighbor_spread: 0.0
|
||||
@ -33,19 +31,19 @@ interval: 1
|
||||
max_time: 300
|
||||
name: Sim_half_herd
|
||||
network_agents:
|
||||
- agent_type: DumbViewer
|
||||
- agent_type: newsspread.DumbViewer
|
||||
state:
|
||||
has_tv: false
|
||||
weight: 1
|
||||
- agent_type: DumbViewer
|
||||
- agent_type: newsspread.DumbViewer
|
||||
state:
|
||||
has_tv: true
|
||||
weight: 1
|
||||
- agent_type: HerdViewer
|
||||
- agent_type: newsspread.HerdViewer
|
||||
state:
|
||||
has_tv: false
|
||||
weight: 1
|
||||
- agent_type: HerdViewer
|
||||
- agent_type: newsspread.HerdViewer
|
||||
state:
|
||||
has_tv: true
|
||||
weight: 1
|
||||
@ -56,7 +54,6 @@ network_params:
|
||||
num_trials: 50
|
||||
---
|
||||
default_state: {}
|
||||
load_module: newsspread
|
||||
environment_agents: []
|
||||
environment_params:
|
||||
prob_neighbor_spread: 0.0
|
||||
@ -65,12 +62,12 @@ interval: 1
|
||||
max_time: 300
|
||||
name: Sim_all_herd
|
||||
network_agents:
|
||||
- agent_type: HerdViewer
|
||||
- agent_type: newsspread.HerdViewer
|
||||
state:
|
||||
has_tv: true
|
||||
state_id: neutral
|
||||
weight: 1
|
||||
- agent_type: HerdViewer
|
||||
- agent_type: newsspread.HerdViewer
|
||||
state:
|
||||
has_tv: true
|
||||
state_id: neutral
|
||||
@ -82,7 +79,6 @@ network_params:
|
||||
num_trials: 50
|
||||
---
|
||||
default_state: {}
|
||||
load_module: newsspread
|
||||
environment_agents: []
|
||||
environment_params:
|
||||
prob_neighbor_spread: 0.0
|
||||
@ -92,12 +88,12 @@ interval: 1
|
||||
max_time: 300
|
||||
name: Sim_wise_herd
|
||||
network_agents:
|
||||
- agent_type: HerdViewer
|
||||
- agent_type: newsspread.HerdViewer
|
||||
state:
|
||||
has_tv: true
|
||||
state_id: neutral
|
||||
weight: 1
|
||||
- agent_type: WiseViewer
|
||||
- agent_type: newsspread.WiseViewer
|
||||
state:
|
||||
has_tv: true
|
||||
weight: 1
|
||||
@ -108,7 +104,6 @@ network_params:
|
||||
num_trials: 50
|
||||
---
|
||||
default_state: {}
|
||||
load_module: newsspread
|
||||
environment_agents: []
|
||||
environment_params:
|
||||
prob_neighbor_spread: 0.0
|
||||
@ -118,12 +113,12 @@ interval: 1
|
||||
max_time: 300
|
||||
name: Sim_all_wise
|
||||
network_agents:
|
||||
- agent_type: WiseViewer
|
||||
- agent_type: newsspread.WiseViewer
|
||||
state:
|
||||
has_tv: true
|
||||
state_id: neutral
|
||||
weight: 1
|
||||
- agent_type: WiseViewer
|
||||
- agent_type: newsspread.WiseViewer
|
||||
state:
|
||||
has_tv: true
|
||||
weight: 1
|
||||
|
@ -1,5 +1,4 @@
|
||||
---
|
||||
load_module: rabbit_agents
|
||||
name: rabbits_example
|
||||
max_time: 100
|
||||
interval: 1
|
||||
|
@ -1,5 +1,4 @@
|
||||
name: TerroristNetworkModel_sim
|
||||
load_module: TerroristNetworkModel
|
||||
max_time: 150
|
||||
num_trials: 1
|
||||
network_params:
|
||||
@ -9,19 +8,19 @@ network_params:
|
||||
# theta: 20
|
||||
n: 100
|
||||
network_agents:
|
||||
- agent_type: TerroristNetworkModel
|
||||
- agent_type: TerroristNetworkModel.TerroristNetworkModel
|
||||
weight: 0.8
|
||||
state:
|
||||
id: civilian # Civilians
|
||||
- agent_type: TerroristNetworkModel
|
||||
- agent_type: TerroristNetworkModel.TerroristNetworkModel
|
||||
weight: 0.1
|
||||
state:
|
||||
id: leader # Leaders
|
||||
- agent_type: TrainingAreaModel
|
||||
- agent_type: TerroristNetworkModel.TrainingAreaModel
|
||||
weight: 0.05
|
||||
state:
|
||||
id: terrorist # Terrorism
|
||||
- agent_type: HavenModel
|
||||
- agent_type: TerroristNetworkModel.HavenModel
|
||||
weight: 0.05
|
||||
state:
|
||||
id: civilian # Civilian
|
||||
|
@ -7,3 +7,4 @@ SALib>=1.3
|
||||
Jinja2
|
||||
Mesa>=0.8.9
|
||||
tsih>=0.1.6
|
||||
pydantic>=1.9
|
||||
|
378
soil/config.py
378
soil/config.py
@ -1,251 +1,183 @@
|
||||
from __future__ import annotations
|
||||
from pydantic import BaseModel, ValidationError, validator, root_validator
|
||||
|
||||
import yaml
|
||||
import os
|
||||
import sys
|
||||
import networkx as nx
|
||||
import collections.abc
|
||||
|
||||
from . import serialization, utils, basestring, agents
|
||||
from typing import Any, Callable, Dict, List, Optional, Union, Type
|
||||
from pydantic import BaseModel, Extra
|
||||
|
||||
class Config(collections.abc.Mapping):
|
||||
"""
|
||||
class General(BaseModel):
|
||||
id: str = 'Unnamed Simulation'
|
||||
group: str = None
|
||||
dir_path: str = None
|
||||
num_trials: int = 1
|
||||
max_time: float = 100
|
||||
interval: float = 1
|
||||
seed: str = ""
|
||||
|
||||
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', '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'
|
||||
|
||||
def __init__(self, name=None,
|
||||
group=None,
|
||||
agent_type='BaseAgent',
|
||||
network_agents=None,
|
||||
environment_agents=None,
|
||||
states=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):
|
||||
|
||||
self.network_params = network_params
|
||||
self.name = name or 'Unnamed'
|
||||
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._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
|
||||
@staticmethod
|
||||
def default():
|
||||
return General()
|
||||
|
||||
|
||||
self.environment_class = environment_class
|
||||
self.environment_params = dict(environment_params)
|
||||
# Could use TypeAlias in python >= 3.10
|
||||
nodeId = int
|
||||
|
||||
#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 {}
|
||||
class Node(BaseModel):
|
||||
id: nodeId
|
||||
state: Dict[str, Any]
|
||||
|
||||
|
||||
def validate(self):
|
||||
agents._validate_states(self.states,
|
||||
self._topology)
|
||||
class Edge(BaseModel):
|
||||
source: nodeId
|
||||
target: nodeId
|
||||
value: float = 1
|
||||
|
||||
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())
|
||||
class Topology(BaseModel):
|
||||
nodes: List[Node]
|
||||
directed: bool
|
||||
links: List[Edge]
|
||||
|
||||
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))
|
||||
class NetParams(BaseModel, extra=Extra.allow):
|
||||
generator: Union[Callable, str]
|
||||
n: int
|
||||
|
||||
with utils.open_or_reuse(f, 'w') as f:
|
||||
f.write(self.to_yaml())
|
||||
|
||||
def to_yaml(self):
|
||||
return yaml.dump(self.to_dict())
|
||||
class NetConfig(BaseModel):
|
||||
group: str = 'network'
|
||||
params: Optional[NetParams]
|
||||
topology: Optional[Topology]
|
||||
path: Optional[str]
|
||||
|
||||
# TODO: See note on getstate
|
||||
def to_dict(self):
|
||||
return self.__getstate__()
|
||||
|
||||
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'])
|
||||
@staticmethod
|
||||
def default():
|
||||
return NetConfig(topology=None, params=None)
|
||||
|
||||
@root_validator
|
||||
def validate_all(cls, values):
|
||||
if 'params' not in values and 'topology' not in values:
|
||||
raise ValueError('You must specify either a topology or the parameters to generate a graph')
|
||||
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)
|
||||
class EnvConfig(BaseModel):
|
||||
environment_class: Union[Type, str] = 'soil.Environment'
|
||||
params: Dict[str, Any] = {}
|
||||
schedule: Union[Type, str] = 'soil.time.TimedActivation'
|
||||
|
||||
return nx.Graph(topology)
|
||||
@staticmethod
|
||||
def default():
|
||||
return EnvConfig()
|
||||
|
||||
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)
|
||||
class SingleAgentConfig(BaseModel):
|
||||
agent_class: Union[Type, str] = 'soil.Agent'
|
||||
agent_id: Optional[Union[str, int]] = None
|
||||
params: Dict[str, Any] = {}
|
||||
state: Dict[str, Any] = {}
|
||||
|
||||
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
|
||||
class AgentDistro(SingleAgentConfig):
|
||||
weight: Optional[float] = None
|
||||
n: Optional[int] = None
|
||||
|
||||
@root_validator
|
||||
def validate_all(cls, values):
|
||||
if 'weight' in values and 'count' in values:
|
||||
raise ValueError("You may either specify a weight in the distribution or an agent count")
|
||||
return values
|
||||
|
||||
|
||||
class AgentConfig(SingleAgentConfig):
|
||||
n: Optional[int] = None
|
||||
distribution: Optional[List[AgentDistro]] = None
|
||||
fixed: Optional[List[SingleAgentConfig]] = None
|
||||
|
||||
@staticmethod
|
||||
def default():
|
||||
return AgentConfig()
|
||||
|
||||
|
||||
class Config(BaseModel, extra=Extra.forbid):
|
||||
general: General = General.default()
|
||||
network: Optional[NetConfig] = None
|
||||
environment: EnvConfig = EnvConfig.default()
|
||||
agents: Dict[str, AgentConfig] = {}
|
||||
|
||||
|
||||
def convert_old(old):
|
||||
'''
|
||||
Try to convert old style configs into the new format.
|
||||
|
||||
This is still a work in progress and might not work in many cases.
|
||||
'''
|
||||
new = {}
|
||||
|
||||
|
||||
general = {}
|
||||
for k in ['id',
|
||||
'group',
|
||||
'dir_path',
|
||||
'num_trials',
|
||||
'max_time',
|
||||
'interval',
|
||||
'seed']:
|
||||
if k in old:
|
||||
general[k] = old[k]
|
||||
|
||||
network = {'group': 'network'}
|
||||
|
||||
|
||||
if 'network_params' in old and old['network_params']:
|
||||
for (k, v) in old['network_params'].items():
|
||||
if k == 'path':
|
||||
network['path'] = v
|
||||
else:
|
||||
network.setdefault('params', {})[k] = v
|
||||
|
||||
if 'topology' in old:
|
||||
network['topology'] = old['topology']
|
||||
|
||||
agents = {
|
||||
'environment': {
|
||||
'fixed': []
|
||||
},
|
||||
'network': {},
|
||||
'default': {},
|
||||
}
|
||||
|
||||
if 'agent_type' in old:
|
||||
agents['default']['agent_class'] = old['agent_type']
|
||||
|
||||
if 'default_state' in old:
|
||||
agents['default']['state'] = old['default_state']
|
||||
|
||||
|
||||
def updated_agent(agent):
|
||||
newagent = dict(agent)
|
||||
newagent['agent_class'] = newagent['agent_type']
|
||||
del newagent['agent_type']
|
||||
return newagent
|
||||
|
||||
for agent in old.get('environment_agents', []):
|
||||
agents['environment']['fixed'].append(updated_agent(agent))
|
||||
|
||||
for agent in old.get('network_agents', []):
|
||||
agents['network'].setdefault('distribution', []).append(updated_agent(agent))
|
||||
|
||||
environment = {'params': {}}
|
||||
if 'environment_class' in old:
|
||||
environment['environment_class'] = old['environment_class']
|
||||
|
||||
for (k, v) in old.get('environment_params', {}).items():
|
||||
environment['params'][k] = v
|
||||
|
||||
|
||||
return Config(general=general,
|
||||
network=network,
|
||||
environment=environment,
|
||||
agents=agents)
|
||||
|
264
soil/config_old.py
Normal file
264
soil/config_old.py
Normal file
@ -0,0 +1,264 @@
|
||||
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
|
||||
|
@ -16,13 +16,6 @@ from tsih import Record
|
||||
|
||||
from . import serialization, agents, analysis, utils, time, config
|
||||
|
||||
# These properties will be copied when pickling/unpickling the environment
|
||||
_CONFIG_PROPS = [ 'name',
|
||||
'states',
|
||||
'default_state',
|
||||
'interval',
|
||||
]
|
||||
|
||||
class Environment(Model):
|
||||
"""
|
||||
The environment is key in a simulation. It contains the network topology,
|
||||
@ -34,76 +27,62 @@ class Environment(Model):
|
||||
:meth:`soil.environment.Environment.get` method.
|
||||
"""
|
||||
|
||||
def __init__(self, name=None,
|
||||
network_agents=None,
|
||||
environment_agents=None,
|
||||
states=None,
|
||||
default_state=None,
|
||||
interval=1,
|
||||
network_params=None,
|
||||
seed=None,
|
||||
topology=None,
|
||||
def __init__(self,
|
||||
env_id,
|
||||
seed='default',
|
||||
schedule=None,
|
||||
initial_time=0,
|
||||
environment_params=None,
|
||||
env_params=None,
|
||||
dir_path=None,
|
||||
**kwargs):
|
||||
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.schedule = schedule
|
||||
if schedule is None:
|
||||
self.schedule = time.TimedActivation()
|
||||
|
||||
self.name = name or 'UnnamedEnvironment'
|
||||
self.seed = '{}_{}'.format(seed, env_id)
|
||||
self.id = env_id
|
||||
|
||||
self.dir_path = dir_path or os.getcwd()
|
||||
|
||||
if schedule is None:
|
||||
schedule = time.TimedActivation()
|
||||
self.schedule = schedule
|
||||
|
||||
seed = seed or current_time()
|
||||
|
||||
random.seed(seed)
|
||||
|
||||
if isinstance(states, list):
|
||||
states = dict(enumerate(states))
|
||||
self.states = deepcopy(states) if states else {}
|
||||
self.default_state = deepcopy(default_state) or {}
|
||||
|
||||
if topology is None:
|
||||
network_params = network_params or {}
|
||||
topology = serialization.load_network(network_params,
|
||||
dir_path=dir_path)
|
||||
if not topology:
|
||||
topology = nx.Graph()
|
||||
self.G = nx.Graph(topology)
|
||||
|
||||
self.environment_params = environment_params or {}
|
||||
self.environment_params.update(kwargs)
|
||||
self.set_topology(topology=topology,
|
||||
network_params=network_params)
|
||||
|
||||
self.agents = agents or {}
|
||||
|
||||
self.env_params = env_params or {}
|
||||
self.env_params.update(kwargs)
|
||||
|
||||
self._env_agents = {}
|
||||
self.interval = interval
|
||||
self['SEED'] = seed
|
||||
|
||||
if network_agents:
|
||||
distro = agents.calculate_distribution(network_agents)
|
||||
self.network_agents = agents._convert_agent_types(distro)
|
||||
else:
|
||||
self.network_agents = []
|
||||
|
||||
environment_agents = environment_agents or []
|
||||
if environment_agents:
|
||||
distro = agents.calculate_distribution(environment_agents)
|
||||
environment_agents = agents._convert_agent_types(distro)
|
||||
self.environment_agents = environment_agents
|
||||
|
||||
self.logger = utils.logger.getChild(self.name)
|
||||
|
||||
@staticmethod
|
||||
def from_config(conf: config.Config, trial_id, **kwargs) -> Environment:
|
||||
'''Create an environment for a trial of the simulation'''
|
||||
|
||||
conf = config.Config(conf, **kwargs)
|
||||
conf.seed = '{}_{}'.format(conf.seed, trial_id)
|
||||
conf.name = '{}_trial_{}'.format(conf.name, trial_id).replace('.', '-')
|
||||
opts = conf.environment_params.copy()
|
||||
conf = conf
|
||||
if kwargs:
|
||||
conf = config.Config(**conf.dict(exclude_defaults=True), **kwargs)
|
||||
seed = '{}_{}'.format(conf.general.seed, trial_id)
|
||||
id = '{}_trial_{}'.format(conf.general.id, trial_id).replace('.', '-')
|
||||
opts = conf.environment.params.copy()
|
||||
opts.update(conf)
|
||||
opts.update(kwargs)
|
||||
env = serialization.deserialize(conf.environment_class)(**opts)
|
||||
env = serialization.deserialize(conf.environment.environment_class)(env_id=id, seed=seed, **opts)
|
||||
return env
|
||||
|
||||
@property
|
||||
@ -112,21 +91,30 @@ class Environment(Model):
|
||||
return self.schedule.time
|
||||
raise Exception('The environment has not been scheduled, so it has no sense of time')
|
||||
|
||||
|
||||
def set_topology(self, topology, network_params=None, dir_path=None):
|
||||
if topology is None:
|
||||
network_params = network_params or {}
|
||||
topology = serialization.load_network(network_params,
|
||||
dir_path=dir_path or self.dir_path)
|
||||
if not topology:
|
||||
topology = nx.Graph()
|
||||
self.G = nx.Graph(topology)
|
||||
|
||||
@property
|
||||
def agents(self):
|
||||
yield from self.environment_agents
|
||||
yield from self.network_agents
|
||||
for agents in self.agents.values():
|
||||
yield from agents
|
||||
|
||||
@property
|
||||
def environment_agents(self):
|
||||
for ref in self._env_agents.values():
|
||||
yield ref
|
||||
@agents.setter
|
||||
def agents(self, agents):
|
||||
self.agents = {}
|
||||
|
||||
@environment_agents.setter
|
||||
def environment_agents(self, environment_agents):
|
||||
self._environment_agents = environment_agents
|
||||
|
||||
self._env_agents = agents._definition_to_dict(definition=environment_agents)
|
||||
for (k, v) in agents.items():
|
||||
self.agents[k] = agents.from_config(v)
|
||||
for agent in self.agents.get('network', []):
|
||||
node = self.G.nodes[agent.unique_id]
|
||||
node['agent'] = agent
|
||||
|
||||
@property
|
||||
def network_agents(self):
|
||||
@ -135,12 +123,6 @@ class Environment(Model):
|
||||
if 'agent' in node:
|
||||
yield node['agent']
|
||||
|
||||
@network_agents.setter
|
||||
def network_agents(self, network_agents):
|
||||
self._network_agents = network_agents
|
||||
for ix in self.G.nodes():
|
||||
self.init_agent(ix, agent_definitions=network_agents)
|
||||
|
||||
def init_agent(self, agent_id, agent_definitions):
|
||||
node = self.G.nodes[agent_id]
|
||||
init = False
|
||||
@ -251,20 +233,20 @@ class Environment(Model):
|
||||
value=value)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.environment_params
|
||||
return key in self.env_params
|
||||
|
||||
def get(self, key, default=None):
|
||||
'''
|
||||
Get the value of an environment attribute.
|
||||
Return `default` if the value is not set.
|
||||
'''
|
||||
return self.environment_params.get(key, default)
|
||||
return self.env_params.get(key, default)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.environment_params.get(key)
|
||||
return self.env_params.get(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
return self.environment_params.__setitem__(key, value)
|
||||
return self.env_params.__setitem__(key, value)
|
||||
|
||||
def get_agent(self, agent_id):
|
||||
return self.G.nodes[agent_id]['agent']
|
||||
@ -292,7 +274,7 @@ class Environment(Model):
|
||||
yield from self._agent_to_tuples(agent, now)
|
||||
return
|
||||
|
||||
for k, v in self.environment_params.items():
|
||||
for k, v in self.env_params.items():
|
||||
yield Record(dict_id='env',
|
||||
t_step=now,
|
||||
key=k,
|
||||
@ -300,23 +282,5 @@ class Environment(Model):
|
||||
for agent in self.agents:
|
||||
yield from self._agent_to_tuples(agent, now)
|
||||
|
||||
def __getstate__(self):
|
||||
state = {}
|
||||
for prop in _CONFIG_PROPS:
|
||||
state[prop] = self.__dict__[prop]
|
||||
state['G'] = json_graph.node_link_data(self.G)
|
||||
state['environment_agents'] = self._env_agents
|
||||
state['schedule'] = self.schedule
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
for prop in _CONFIG_PROPS:
|
||||
self.__dict__[prop] = state[prop]
|
||||
self._env_agents = state['environment_agents']
|
||||
self.G = json_graph.node_link_graph(state['G'])
|
||||
# self._env = None
|
||||
self.schedule = state['schedule']
|
||||
self._queue = []
|
||||
|
||||
|
||||
SoilEnvironment = Environment
|
||||
|
@ -2,6 +2,8 @@ import os
|
||||
import csv as csvlib
|
||||
from time import time as current_time
|
||||
from io import BytesIO
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import networkx as nx
|
||||
@ -48,8 +50,8 @@ class Exporter:
|
||||
self.simulation = simulation
|
||||
outdir = outdir or os.path.join(os.getcwd(), 'soil_output')
|
||||
self.outdir = os.path.join(outdir,
|
||||
simulation.config.group or '',
|
||||
simulation.config.name)
|
||||
simulation.config.general.group or '',
|
||||
simulation.config.general.id)
|
||||
self.dry_run = dry_run
|
||||
self.copy_to = copy_to
|
||||
|
||||
@ -84,24 +86,33 @@ class Exporter:
|
||||
class default(Exporter):
|
||||
'''Default exporter. Writes sqlite results, as well as the simulation YAML'''
|
||||
|
||||
def sim_start(self):
|
||||
if not self.dry_run:
|
||||
logger.info('Dumping results to %s', self.outdir)
|
||||
self.simulation.dump_yaml(outdir=self.outdir)
|
||||
else:
|
||||
logger.info('NOT dumping results')
|
||||
# def sim_start(self):
|
||||
# if not self.dry_run:
|
||||
# logger.info('Dumping results to %s', self.outdir)
|
||||
# self.simulation.dump_yaml(outdir=self.outdir)
|
||||
# else:
|
||||
# logger.info('NOT dumping results')
|
||||
|
||||
def trial_start(self, env, stats):
|
||||
if not self.dry_run:
|
||||
with timer('Dumping simulation {} trial {}'.format(self.simulation.name,
|
||||
env.name)):
|
||||
with self.output('{}.sqlite'.format(env.name), mode='wb') as f:
|
||||
env.dump_sqlite(f)
|
||||
# def trial_start(self, env, stats):
|
||||
# if not self.dry_run:
|
||||
# with timer('Dumping simulation {} trial {}'.format(self.simulation.name,
|
||||
# env.name)):
|
||||
# engine = create_engine('sqlite:///{}.sqlite'.format(env.name), echo=False)
|
||||
|
||||
def sim_end(self, stats):
|
||||
with timer('Dumping simulation {}\'s stats'.format(self.simulation.name)):
|
||||
with self.output('{}.sqlite'.format(self.simulation.name), mode='wb') as f:
|
||||
self.simulation.dump_sqlite(f)
|
||||
# dc = env.datacollector
|
||||
# tables = {'env': dc.get_model_vars_dataframe(),
|
||||
# 'agents': dc.get_agent_vars_dataframe(),
|
||||
# 'agents': dc.get_agent_vars_dataframe()}
|
||||
# for table in dc.tables:
|
||||
# tables[table] = dc.get_table_dataframe(table)
|
||||
# for (t, df) in tables.items():
|
||||
# df.to_sql(t, con=engine)
|
||||
|
||||
# def sim_end(self, stats):
|
||||
# with timer('Dumping simulation {}\'s stats'.format(self.simulation.name)):
|
||||
# engine = create_engine('sqlite:///{}.sqlite'.format(self.simulation.name), echo=False)
|
||||
# with self.output('{}.sqlite'.format(self.simulation.name), mode='wb') as f:
|
||||
# self.simulation.dump_sqlite(f)
|
||||
|
||||
|
||||
|
||||
|
@ -51,8 +51,6 @@ def load_network(network_params, dir_path=None):
|
||||
return G
|
||||
|
||||
|
||||
|
||||
|
||||
def load_file(infile):
|
||||
folder = os.path.dirname(infile)
|
||||
if folder not in sys.path:
|
||||
@ -138,7 +136,9 @@ def load_config(config):
|
||||
|
||||
builtins = importlib.import_module('builtins')
|
||||
|
||||
def name(value, known_modules=[]):
|
||||
KNOWN_MODULES = ['soil', ]
|
||||
|
||||
def name(value, known_modules=KNOWN_MODULES):
|
||||
'''Return a name that can be imported, to serialize/deserialize an object'''
|
||||
if value is None:
|
||||
return 'None'
|
||||
@ -167,7 +167,7 @@ def serializer(type_):
|
||||
return lambda x: x
|
||||
|
||||
|
||||
def serialize(v, known_modules=[]):
|
||||
def serialize(v, known_modules=KNOWN_MODULES):
|
||||
'''Get a text representation of an object.'''
|
||||
tname = name(v, known_modules=known_modules)
|
||||
func = serializer(tname)
|
||||
@ -176,7 +176,7 @@ def serialize(v, known_modules=[]):
|
||||
|
||||
IS_CLASS = re.compile(r"<class '(.*)'>")
|
||||
|
||||
def deserializer(type_, known_modules=[]):
|
||||
def deserializer(type_, known_modules=KNOWN_MODULES):
|
||||
if type(type_) != str: # Already deserialized
|
||||
return type_
|
||||
if type_ == 'str':
|
||||
@ -194,10 +194,9 @@ def deserializer(type_, known_modules=[]):
|
||||
return getattr(cls, 'deserialize', cls)
|
||||
|
||||
# Otherwise, see if we can find the module and the class
|
||||
modules = known_modules or []
|
||||
options = []
|
||||
|
||||
for mod in modules:
|
||||
for mod in known_modules:
|
||||
if mod:
|
||||
options.append((mod, type_))
|
||||
|
||||
@ -226,7 +225,7 @@ def deserialize(type_, value=None, **kwargs):
|
||||
return des(value)
|
||||
|
||||
|
||||
def deserialize_all(names, *args, known_modules=['soil'], **kwargs):
|
||||
def deserialize_all(names, *args, known_modules=KNOWN_MODULES, **kwargs):
|
||||
'''Return the list of deserialized objects'''
|
||||
objects = []
|
||||
for name in names:
|
||||
|
@ -18,7 +18,7 @@ from .utils import logger
|
||||
from .exporters import default
|
||||
from .stats import defaultStats
|
||||
|
||||
from .config import Config
|
||||
from .config import Config, convert_old
|
||||
|
||||
|
||||
#TODO: change documentation for simulation
|
||||
@ -34,18 +34,21 @@ class Simulation:
|
||||
|
||||
def __init__(self, config=None,
|
||||
**kwargs):
|
||||
|
||||
if bool(config) == bool(kwargs):
|
||||
raise ValueError("Specify either a configuration or the parameters to initialize a configuration")
|
||||
|
||||
if kwargs:
|
||||
config = Config(**kwargs)
|
||||
cfg = {}
|
||||
if config:
|
||||
cfg.update(config.dict(include_defaults=False))
|
||||
cfg.update(kwargs)
|
||||
config = Config(**cfg)
|
||||
if not config:
|
||||
raise ValueError("You need to specify a simulation configuration")
|
||||
|
||||
self.config = config
|
||||
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.config.name
|
||||
return self.config.general.id
|
||||
|
||||
def run_simulation(self, *args, **kwargs):
|
||||
return self.run(*args, **kwargs)
|
||||
@ -58,13 +61,13 @@ class Simulation:
|
||||
if parallel and not os.environ.get('SENPY_DEBUG', None):
|
||||
p = Pool()
|
||||
func = partial(self.run_trial_exceptions, **kwargs)
|
||||
for i in p.imap_unordered(func, range(self.config.num_trials)):
|
||||
for i in p.imap_unordered(func, range(self.config.general.num_trials)):
|
||||
if isinstance(i, Exception):
|
||||
logger.error('Trial failed:\n\t%s', i.message)
|
||||
continue
|
||||
yield i
|
||||
else:
|
||||
for i in range(self.config.num_trials):
|
||||
for i in range(self.config.general.num_trials):
|
||||
yield self.run_trial(trial_id=i,
|
||||
**kwargs)
|
||||
|
||||
@ -88,7 +91,7 @@ class Simulation:
|
||||
known_modules=['soil.stats',],
|
||||
**stats_params)
|
||||
|
||||
with utils.timer('simulation {}'.format(self.config.name)):
|
||||
with utils.timer('simulation {}'.format(self.config.general.id)):
|
||||
for stat in stats:
|
||||
stat.sim_start()
|
||||
|
||||
@ -157,11 +160,11 @@ class Simulation:
|
||||
if log_level:
|
||||
logger.setLevel(log_level)
|
||||
# Set-up trial environment and graph
|
||||
until = until or self.config.max_time
|
||||
until = until or self.config.general.max_time
|
||||
|
||||
env = Environment.from_config(self.config, trial_id=trial_id)
|
||||
# Set up agents on nodes
|
||||
with utils.timer('Simulation {} trial {}'.format(self.config.name, trial_id)):
|
||||
with utils.timer('Simulation {} trial {}'.format(self.config.general.id, trial_id)):
|
||||
env.run(until)
|
||||
return env
|
||||
|
||||
@ -194,15 +197,22 @@ def from_config(conf_or_path):
|
||||
sim = Simulation(**config)
|
||||
return sim
|
||||
|
||||
def from_old_config(conf_or_path):
|
||||
config = list(serialization.load_config(conf_or_path))
|
||||
if len(config) > 1:
|
||||
raise AttributeError('Provide only one configuration')
|
||||
config = convert_old(config[0][0])
|
||||
return Simulation(config)
|
||||
|
||||
|
||||
def run_from_config(*configs, **kwargs):
|
||||
for config_def in configs:
|
||||
# logger.info("Found {} config(s)".format(len(ls)))
|
||||
for config, path in serialization.load_config(config_def):
|
||||
name = config.get('name', 'unnamed')
|
||||
name = config.general.id
|
||||
logger.info("Using config(s): {name}".format(name=name))
|
||||
|
||||
dir_path = config.pop('dir_path', os.path.dirname(path))
|
||||
dir_path = config.general.dir_path or os.path.dirname(path)
|
||||
sim = Simulation(dir_path=dir_path,
|
||||
**config)
|
||||
sim.run_simulation(**kwargs)
|
||||
|
32
tests/old_complete.yml
Normal file
32
tests/old_complete.yml
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
name: simple
|
||||
group: tests
|
||||
dir_path: "/tmp/"
|
||||
num_trials: 3
|
||||
max_time: 100
|
||||
interval: 1
|
||||
seed: "CompleteSeed!"
|
||||
network_params:
|
||||
generator: complete_graph
|
||||
n: 10
|
||||
network_agents:
|
||||
- agent_type: CounterModel
|
||||
weight: 1
|
||||
state:
|
||||
state_id: 0
|
||||
- agent_type: AggregatedCounter
|
||||
weight: 0.2
|
||||
environment_agents:
|
||||
- agent_id: 'Environment Agent 1'
|
||||
agent_type: CounterModel
|
||||
state:
|
||||
times: 10
|
||||
environment_class: Environment
|
||||
environment_params:
|
||||
am_i_complete: true
|
||||
agent_type: CounterModel
|
||||
default_state:
|
||||
times: 1
|
||||
states:
|
||||
- name: 'The first node'
|
||||
- name: 'The second node'
|
62
tests/test_config.py
Normal file
62
tests/test_config.py
Normal file
@ -0,0 +1,62 @@
|
||||
from unittest import TestCase
|
||||
import os
|
||||
from os.path import join
|
||||
|
||||
from soil import serialization, config
|
||||
|
||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
EXAMPLES = join(ROOT, '..', 'examples')
|
||||
|
||||
FORCE_TESTS = os.environ.get('FORCE_TESTS', '')
|
||||
|
||||
|
||||
class TestConfig(TestCase):
|
||||
|
||||
def test_conversion(self):
|
||||
new = serialization.load_file(join(EXAMPLES, "complete.yml"))[0]
|
||||
old = serialization.load_file(join(ROOT, "old_complete.yml"))[0]
|
||||
converted = config.convert_old(old).dict(skip_defaults=True)
|
||||
for (k, v) in new.items():
|
||||
assert v == converted[k]
|
||||
|
||||
|
||||
def make_example_test(path, cfg):
|
||||
def wrapped(self):
|
||||
root = os.getcwd()
|
||||
s = config.Config(**cfg)
|
||||
import pdb;pdb.set_trace()
|
||||
# for s in simulation.all_from_config(path):
|
||||
# iterations = s.config.max_time * s.config.num_trials
|
||||
# if iterations > 1000:
|
||||
# s.config.max_time = 100
|
||||
# s.config.num_trials = 1
|
||||
# if config.get('skip_test', False) and not FORCE_TESTS:
|
||||
# self.skipTest('Example ignored.')
|
||||
# envs = s.run_simulation(dry_run=True)
|
||||
# assert envs
|
||||
# for env in envs:
|
||||
# assert env
|
||||
# try:
|
||||
# n = config['network_params']['n']
|
||||
# assert len(list(env.network_agents)) == n
|
||||
# assert env.now > 0 # It has run
|
||||
# assert env.now <= config['max_time'] # But not further than allowed
|
||||
# except KeyError:
|
||||
# pass
|
||||
return wrapped
|
||||
|
||||
|
||||
def add_example_tests():
|
||||
for config, path in serialization.load_files(
|
||||
join(EXAMPLES, '*', '*.yml'),
|
||||
join(EXAMPLES, '*.yml'),
|
||||
):
|
||||
p = make_example_test(path=path, cfg=config)
|
||||
fname = os.path.basename(path)
|
||||
p.__name__ = 'test_example_file_%s' % fname
|
||||
p.__doc__ = '%s should be a valid configuration' % fname
|
||||
setattr(TestConfig, p.__name__, p)
|
||||
del p
|
||||
|
||||
|
||||
add_example_tests()
|
@ -18,10 +18,10 @@ def make_example_test(path, config):
|
||||
def wrapped(self):
|
||||
root = os.getcwd()
|
||||
for s in simulation.all_from_config(path):
|
||||
iterations = s.max_time * s.num_trials
|
||||
iterations = s.config.max_time * s.config.num_trials
|
||||
if iterations > 1000:
|
||||
s.max_time = 100
|
||||
s.num_trials = 1
|
||||
s.config.max_time = 100
|
||||
s.config.num_trials = 1
|
||||
if config.get('skip_test', False) and not FORCE_TESTS:
|
||||
self.skipTest('Example ignored.')
|
||||
envs = s.run_simulation(dry_run=True)
|
||||
|
128
tests/test_history.py
Normal file
128
tests/test_history.py
Normal file
@ -0,0 +1,128 @@
|
||||
from unittest import TestCase
|
||||
|
||||
import os
|
||||
import io
|
||||
import yaml
|
||||
import copy
|
||||
import pickle
|
||||
import networkx as nx
|
||||
from functools import partial
|
||||
|
||||
from os.path import join
|
||||
from soil import (simulation, Environment, agents, serialization,
|
||||
utils)
|
||||
from soil.time import Delta
|
||||
from tsih import NoHistory, History
|
||||
|
||||
|
||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
EXAMPLES = join(ROOT, '..', 'examples')
|
||||
|
||||
|
||||
class CustomAgent(agents.FSM):
|
||||
@agents.default_state
|
||||
@agents.state
|
||||
def normal(self):
|
||||
self.neighbors = self.count_agents(state_id='normal',
|
||||
limit_neighbors=True)
|
||||
@agents.state
|
||||
def unreachable(self):
|
||||
return
|
||||
|
||||
class TestHistory(TestCase):
|
||||
|
||||
def test_counter_agent_history(self):
|
||||
"""
|
||||
The evolution of the state should be recorded in the logging agent
|
||||
"""
|
||||
config = {
|
||||
'name': 'CounterAgent',
|
||||
'network_params': {
|
||||
'path': join(ROOT, 'test.gexf')
|
||||
},
|
||||
'network_agents': [{
|
||||
'agent_type': 'AggregatedCounter',
|
||||
'weight': 1,
|
||||
'state': {'state_id': 0}
|
||||
|
||||
}],
|
||||
'max_time': 10,
|
||||
'environment_params': {
|
||||
}
|
||||
}
|
||||
s = simulation.from_config(config)
|
||||
env = s.run_simulation(dry_run=True)[0]
|
||||
for agent in env.network_agents:
|
||||
last = 0
|
||||
assert len(agent[None, None]) == 11
|
||||
for step, total in sorted(agent['total', None]):
|
||||
assert total == last + 2
|
||||
last = total
|
||||
|
||||
def test_row_conversion(self):
|
||||
env = Environment(history=True)
|
||||
env['test'] = 'test_value'
|
||||
|
||||
res = list(env.history_to_tuples())
|
||||
assert len(res) == len(env.environment_params)
|
||||
|
||||
env.schedule.time = 1
|
||||
env['test'] = 'second_value'
|
||||
res = list(env.history_to_tuples())
|
||||
|
||||
assert env['env', 0, 'test' ] == 'test_value'
|
||||
assert env['env', 1, 'test' ] == 'second_value'
|
||||
|
||||
def test_nohistory(self):
|
||||
'''
|
||||
Make sure that no history(/sqlite) is used by default
|
||||
'''
|
||||
env = Environment(topology=nx.Graph(), network_agents=[])
|
||||
assert isinstance(env._history, NoHistory)
|
||||
|
||||
def test_save_graph_history(self):
|
||||
'''
|
||||
The history_to_graph method should return a valid networkx graph.
|
||||
|
||||
The state of the agent should be encoded as intervals in the nx graph.
|
||||
'''
|
||||
G = nx.cycle_graph(5)
|
||||
distribution = agents.calculate_distribution(None, agents.BaseAgent)
|
||||
env = Environment(topology=G, network_agents=distribution, history=True)
|
||||
env[0, 0, 'testvalue'] = 'start'
|
||||
env[0, 10, 'testvalue'] = 'finish'
|
||||
nG = env.history_to_graph()
|
||||
values = nG.nodes[0]['attr_testvalue']
|
||||
assert ('start', 0, 10) in values
|
||||
assert ('finish', 10, None) in values
|
||||
|
||||
def test_save_graph_nohistory(self):
|
||||
'''
|
||||
The history_to_graph method should return a valid networkx graph.
|
||||
|
||||
When NoHistory is used, only the last known value is known
|
||||
'''
|
||||
G = nx.cycle_graph(5)
|
||||
distribution = agents.calculate_distribution(None, agents.BaseAgent)
|
||||
env = Environment(topology=G, network_agents=distribution, history=False)
|
||||
env.get_agent(0)['testvalue'] = 'start'
|
||||
env.schedule.time = 10
|
||||
env.get_agent(0)['testvalue'] = 'finish'
|
||||
nG = env.history_to_graph()
|
||||
values = nG.nodes[0]['attr_testvalue']
|
||||
assert ('start', 0, None) not in values
|
||||
assert ('finish', 10, None) in values
|
||||
|
||||
def test_pickle_agent_environment(self):
|
||||
env = Environment(name='Test', history=True)
|
||||
a = agents.BaseAgent(model=env, unique_id=25)
|
||||
|
||||
a['key'] = 'test'
|
||||
|
||||
pickled = pickle.dumps(a)
|
||||
recovered = pickle.loads(pickled)
|
||||
|
||||
assert recovered.env.name == 'Test'
|
||||
assert list(recovered.env._history.to_tuples())
|
||||
assert recovered['key', 0] == 'test'
|
||||
assert recovered['key'] == 'test'
|
@ -3,6 +3,7 @@ from unittest import TestCase
|
||||
import os
|
||||
import io
|
||||
import yaml
|
||||
import copy
|
||||
import pickle
|
||||
import networkx as nx
|
||||
from functools import partial
|
||||
@ -11,8 +12,6 @@ from os.path import join
|
||||
from soil import (simulation, Environment, agents, serialization,
|
||||
utils)
|
||||
from soil.time import Delta
|
||||
from tsih import NoHistory, History
|
||||
|
||||
|
||||
ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
EXAMPLES = join(ROOT, '..', 'examples')
|
||||
@ -79,9 +78,31 @@ class TestMain(TestCase):
|
||||
'environment_params': {
|
||||
}
|
||||
}
|
||||
s = simulation.from_config(config)
|
||||
s = simulation.from_old_config(config)
|
||||
s.run_simulation(dry_run=True)
|
||||
|
||||
|
||||
def test_network_agent(self):
|
||||
"""
|
||||
The initial states should be applied to the agent and the
|
||||
agent should be able to update its state."""
|
||||
config = {
|
||||
'name': 'CounterAgent',
|
||||
'network_params': {
|
||||
'generator': nx.complete_graph,
|
||||
'n': 2,
|
||||
},
|
||||
'agent_type': 'CounterModel',
|
||||
'states': {
|
||||
0: {'times': 10},
|
||||
1: {'times': 20},
|
||||
},
|
||||
'max_time': 2,
|
||||
'num_trials': 1,
|
||||
'environment_params': {
|
||||
}
|
||||
}
|
||||
s = simulation.from_old_config(config)
|
||||
def test_counter_agent(self):
|
||||
"""
|
||||
The initial states should be applied to the agent and the
|
||||
@ -98,41 +119,13 @@ class TestMain(TestCase):
|
||||
'environment_params': {
|
||||
}
|
||||
}
|
||||
s = simulation.from_config(config)
|
||||
s = simulation.from_old_config(config)
|
||||
env = s.run_simulation(dry_run=True)[0]
|
||||
assert env.get_agent(0)['times', 0] == 11
|
||||
assert env.get_agent(0)['times', 1] == 12
|
||||
assert env.get_agent(1)['times', 0] == 21
|
||||
assert env.get_agent(1)['times', 1] == 22
|
||||
|
||||
def test_counter_agent_history(self):
|
||||
"""
|
||||
The evolution of the state should be recorded in the logging agent
|
||||
"""
|
||||
config = {
|
||||
'name': 'CounterAgent',
|
||||
'network_params': {
|
||||
'path': join(ROOT, 'test.gexf')
|
||||
},
|
||||
'network_agents': [{
|
||||
'agent_type': 'AggregatedCounter',
|
||||
'weight': 1,
|
||||
'state': {'state_id': 0}
|
||||
|
||||
}],
|
||||
'max_time': 10,
|
||||
'environment_params': {
|
||||
}
|
||||
}
|
||||
s = simulation.from_config(config)
|
||||
env = s.run_simulation(dry_run=True)[0]
|
||||
for agent in env.network_agents:
|
||||
last = 0
|
||||
assert len(agent[None, None]) == 10
|
||||
for step, total in sorted(agent['total', None]):
|
||||
assert total == last + 2
|
||||
last = total
|
||||
|
||||
def test_custom_agent(self):
|
||||
"""Allow for search of neighbors with a certain state_id"""
|
||||
config = {
|
||||
@ -148,7 +141,7 @@ class TestMain(TestCase):
|
||||
'environment_params': {
|
||||
}
|
||||
}
|
||||
s = simulation.from_config(config)
|
||||
s = simulation.from_old_config(config)
|
||||
env = s.run_simulation(dry_run=True)[0]
|
||||
assert env.get_agent(1).count_agents(state_id='normal') == 2
|
||||
assert env.get_agent(1).count_agents(state_id='normal', limit_neighbors=True) == 1
|
||||
@ -159,7 +152,7 @@ class TestMain(TestCase):
|
||||
config = serialization.load_file(join(EXAMPLES, 'torvalds.yml'))[0]
|
||||
config['network_params']['path'] = join(EXAMPLES,
|
||||
config['network_params']['path'])
|
||||
s = simulation.from_config(config)
|
||||
s = simulation.from_old_config(config)
|
||||
env = s.run_simulation(dry_run=True)[0]
|
||||
for a in env.network_agents:
|
||||
skill_level = a.state['skill_level']
|
||||
@ -178,19 +171,23 @@ class TestMain(TestCase):
|
||||
|
||||
def test_yaml(self):
|
||||
"""
|
||||
The YAML version of a newly created simulation
|
||||
should be equivalent to the configuration file used
|
||||
The YAML version of a newly created configuration should be equivalent
|
||||
to the configuration file used.
|
||||
Values not present in the original config file should have reasonable
|
||||
defaults.
|
||||
"""
|
||||
with utils.timer('loading'):
|
||||
config = serialization.load_file(join(EXAMPLES, 'complete.yml'))[0]
|
||||
s = simulation.from_config(config)
|
||||
s = simulation.from_old_config(config)
|
||||
with utils.timer('serializing'):
|
||||
serial = s.to_yaml()
|
||||
serial = s.config.to_yaml()
|
||||
with utils.timer('recovering'):
|
||||
recovered = yaml.load(serial, Loader=yaml.SafeLoader)
|
||||
with utils.timer('deleting'):
|
||||
del recovered['topology']
|
||||
assert config == recovered
|
||||
for (k, v) in config.items():
|
||||
assert recovered[k] == v
|
||||
# assert config == recovered
|
||||
|
||||
def test_configuration_changes(self):
|
||||
"""
|
||||
@ -198,26 +195,13 @@ class TestMain(TestCase):
|
||||
the simulation.
|
||||
"""
|
||||
config = serialization.load_file(join(EXAMPLES, 'complete.yml'))[0]
|
||||
s = simulation.from_config(config)
|
||||
s = simulation.from_old_config(config)
|
||||
init_config = copy.copy(s.config)
|
||||
|
||||
s.run_simulation(dry_run=True)
|
||||
nconfig = s.to_dict()
|
||||
del nconfig['topology']
|
||||
assert config == nconfig
|
||||
|
||||
def test_row_conversion(self):
|
||||
env = Environment(history=True)
|
||||
env['test'] = 'test_value'
|
||||
|
||||
res = list(env.history_to_tuples())
|
||||
assert len(res) == len(env.environment_params)
|
||||
|
||||
env.schedule.time = 1
|
||||
env['test'] = 'second_value'
|
||||
res = list(env.history_to_tuples())
|
||||
|
||||
assert env['env', 0, 'test' ] == 'test_value'
|
||||
assert env['env', 1, 'test' ] == 'second_value'
|
||||
nconfig = s.config
|
||||
# del nconfig['to
|
||||
assert init_config == nconfig
|
||||
|
||||
def test_save_geometric(self):
|
||||
"""
|
||||
@ -229,51 +213,15 @@ class TestMain(TestCase):
|
||||
f = io.BytesIO()
|
||||
env.dump_gexf(f)
|
||||
|
||||
def test_nohistory(self):
|
||||
'''
|
||||
Make sure that no history(/sqlite) is used by default
|
||||
'''
|
||||
env = Environment(topology=nx.Graph(), network_agents=[])
|
||||
assert isinstance(env._history, NoHistory)
|
||||
|
||||
def test_save_graph_history(self):
|
||||
'''
|
||||
The history_to_graph method should return a valid networkx graph.
|
||||
|
||||
The state of the agent should be encoded as intervals in the nx graph.
|
||||
'''
|
||||
G = nx.cycle_graph(5)
|
||||
distribution = agents.calculate_distribution(None, agents.BaseAgent)
|
||||
env = Environment(topology=G, network_agents=distribution, history=True)
|
||||
env[0, 0, 'testvalue'] = 'start'
|
||||
env[0, 10, 'testvalue'] = 'finish'
|
||||
nG = env.history_to_graph()
|
||||
values = nG.nodes[0]['attr_testvalue']
|
||||
assert ('start', 0, 10) in values
|
||||
assert ('finish', 10, None) in values
|
||||
|
||||
def test_save_graph_nohistory(self):
|
||||
'''
|
||||
The history_to_graph method should return a valid networkx graph.
|
||||
|
||||
When NoHistory is used, only the last known value is known
|
||||
'''
|
||||
G = nx.cycle_graph(5)
|
||||
distribution = agents.calculate_distribution(None, agents.BaseAgent)
|
||||
env = Environment(topology=G, network_agents=distribution, history=False)
|
||||
env.get_agent(0)['testvalue'] = 'start'
|
||||
env.schedule.time = 10
|
||||
env.get_agent(0)['testvalue'] = 'finish'
|
||||
nG = env.history_to_graph()
|
||||
values = nG.nodes[0]['attr_testvalue']
|
||||
assert ('start', 0, None) not in values
|
||||
assert ('finish', 10, None) in values
|
||||
|
||||
def test_serialize_class(self):
|
||||
ser, name = serialization.serialize(agents.BaseAgent)
|
||||
ser, name = serialization.serialize(agents.BaseAgent, known_modules=[])
|
||||
assert name == 'soil.agents.BaseAgent'
|
||||
assert ser == agents.BaseAgent
|
||||
|
||||
ser, name = serialization.serialize(agents.BaseAgent, known_modules=['soil', ])
|
||||
assert name == 'BaseAgent'
|
||||
assert ser == agents.BaseAgent
|
||||
|
||||
ser, name = serialization.serialize(CustomAgent)
|
||||
assert name == 'test_main.CustomAgent'
|
||||
assert ser == CustomAgent
|
||||
@ -327,20 +275,6 @@ class TestMain(TestCase):
|
||||
assert converted[1]['agent_type'] == 'test_main.CustomAgent'
|
||||
pickle.dumps(converted)
|
||||
|
||||
def test_pickle_agent_environment(self):
|
||||
env = Environment(name='Test', history=True)
|
||||
a = agents.BaseAgent(model=env, unique_id=25)
|
||||
|
||||
a['key'] = 'test'
|
||||
|
||||
pickled = pickle.dumps(a)
|
||||
recovered = pickle.loads(pickled)
|
||||
|
||||
assert recovered.env.name == 'Test'
|
||||
assert list(recovered.env._history.to_tuples())
|
||||
assert recovered['key', 0] == 'test'
|
||||
assert recovered['key'] == 'test'
|
||||
|
||||
def test_subgraph(self):
|
||||
'''An agent should be able to subgraph the global topology'''
|
||||
G = nx.Graph()
|
||||
@ -371,7 +305,7 @@ class TestMain(TestCase):
|
||||
'num_trials': 50,
|
||||
'environment_params': {}
|
||||
}
|
||||
s = simulation.from_config(config)
|
||||
s = simulation.from_old_config(config)
|
||||
runs = list(s.run_simulation(dry_run=True))
|
||||
over = list(x.now for x in runs if x.now>2)
|
||||
assert len(runs) == config['num_trials']
|
||||
|
Loading…
Reference in New Issue
Block a user