diff --git a/.gitignore b/.gitignore index 0e73de6..dbe20d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # IPython Notebook Checkpoints **/.ipynb_checkpoints +**/__pycache__ # Documentation builds **/_build \ No newline at end of file diff --git a/README.md b/README.md index 11b1240..86cbf7a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Agentpy is a package for the development and analysis of agent-based models in P **Documentation:** https://agentpy.readthedocs.io -**Tutorials:** https://agentpy.readthedocs.io/tutorials.html +**Tutorials:** https://agentpy.readthedocs.io/en/latest/tutorials.html **Requirements:** Python 3, NumPy, scipy, matplotlib, networkx, pandas, SALib, and ipywidgets. diff --git a/agentpy/experiment.py b/agentpy/experiment.py index b750eea..5cf8e16 100644 --- a/agentpy/experiment.py +++ b/agentpy/experiment.py @@ -24,16 +24,19 @@ class experiment(): - """ Experiment for an agent-based model. Can also be called as exp(). + """ Experiment for an agent-based model. + Allows for multiple iterations, parameter samples, and distict scenarios. Arguments: model(class): The model that the experiment should use. parameters(dict or list): Parameters or parameter sample. name(str,optional): Name of the experiment. Takes model name at default. scenarios(str or list,optional): Scenarios that should be tested, if any. - iterations(int,optional): How often to repeat the experiment. Default 1. + iterations(int,optional): How often to repeat the experiment (default 1). output_vars(bool,optional): Whether to record dynamic variables. Default False. - display(bool,optional): Whether to display simulation progress. Default False. + + Attributes: + output(data_dict): Recorded experiment data """ @@ -67,8 +70,16 @@ def __init__( self, def run(self, display=True): - """ Performs the experiment and generates `output`. """ + """ Executes the simulation of the experiment. + It will run the model once for each set of parameters, + and will repeat this process for each iteration. + Arguments: + display(bool,optional): Whether to display simulation progress (default True). + + Returns: + data_dict: Recorded experiment data, also stored in `experiment.output`. + """ parameter_sample = make_list(self.parameters,keep_none=True) scenarios = make_list(self.scenarios,keep_none=True) runs = parameter_sample * self.iterations @@ -183,13 +194,11 @@ def sample(param_ranges,mode='discrete',**kwargs): """ Returns parameter sample (list of dict) - Arguments - --------- - param_ranges: dict - mode: str - 'discrete' - tuples are given of style (value1,value2,...) - 'saltelli' - tuples are given of style (min_value,max_value) - + Arguments: + param_ranges(dict) + mode(str): Sampling method. Options are: + 'discrete' - tuples are given of style (value1,value2,...); + 'saltelli' - tuples are given of style (min_value,max_value) """ if mode == 'discrete': parameters = create_sample_discrete(param_ranges,**kwargs) diff --git a/agentpy/framework.py b/agentpy/framework.py index d86c5d5..fef976f 100644 --- a/agentpy/framework.py +++ b/agentpy/framework.py @@ -32,8 +32,9 @@ def record(self, var_keys, value = None): for var_key in make_list(var_keys): - # Create empty list if var_key is new (!) should have none until t - if var_key not in self.log: self.log[var_key] = [None] * len(self.log['t']) + # Create empty list if var_key is new + if var_key not in self.log: + self.log[var_key] = [None] * len(self.log['t']) if self.model.t not in self.log['t']: @@ -59,9 +60,7 @@ def setup(self): class agent(attr_dict): - """ - - Individual agent of an agent-based model. + """ Individual agent of an agent-based model. This class can be used as a parent class for custom agent types. Attributes can be accessed as items (see :class:`attr_dict`). Methods of :class:`agent_list` can also be used for individual agents. @@ -127,9 +126,9 @@ def neighbors(self,key=None): elif key not in self.envs.keys(): AgentpyError(f"Agent {self.id} has no network '{key}'") if key == None: AgentpyError(f"No network found for agent {self.id}") - return agent_list([n for n in self.envs[key].neighbors(self)]) # (!) list comprehension necessary? + return agent_list([n for n in self.envs[key].neighbors(self)]) - def remove(self,keys=None): + def leave(self,keys=None): """ Removes the agent from environments or the model @@ -142,7 +141,7 @@ def remove(self,keys=None): envs = self.envs # Select all environments by default self.model.agents.remove(self) # Delete from model else: - envs = { k:v for k,v in self.envs if k in keys} + envs = { k:v for k,v in self.envs.items() if k in keys} for key,env in envs.items(): env.agents.remove(self) @@ -237,30 +236,30 @@ def add_agents(self, agents, agent_class=agent, **kwargs): agents(int or agent_list): Number of new agents or list of existing agents. agent_class(class): Type of agent to be created if int is passed for agents. """ - - # (!) what if single agent is passed? - - if type(agents) == agent_list: # (!) or normal iterable with agents? - - new_agents = agents - # (!) need to add new environment to agent - elif type(agents) == int: + if isinstance(agents,int): - new_agents = agent_list() + # Create new agent instances + new_agents = agent_list([agent_class(self.model, {self.key:self}, **kwargs) for _ in range(agents)]) - for i in range(agents): - new_agents.append(agent_class(self.model, {self.key:self}, **kwargs)) - - else: raise ValueError("agents must be agent_list or int") # (!) improve error + # Add agents to master list + if self != self.model: + self.model.agents.extend( new_agents ) + + else: + + # Add existing agents + if isinstance(agents,agent_list): new_agents = agents + else: new_agents = agent_list(agents) + + # Add new environment to agents + if self != self.model: + for agent in new_agents: + agent.envs[self.key] = self # Add agents to this env self.agents.extend( new_agents ) - # Add agent to master list - if self != self.model: - self.model.agents.extend( new_agents ) - return new_agents def _env_init(self,model,key): @@ -340,26 +339,24 @@ def add_agents(self, agents, agent_class=agent, map_to_nodes=False, **kwargs): Adds agents to the network environment. Arguments: - agents(int or agent_list): Number of new agents or list of existing agents. + agents(int or agent or list or agent_list): Number of new agents or existing agents. agent_class(class): Type of agent to be created if int is passed for agents. map_to_nodes(bool,optional): Map new agents to each node of the graph (default False). Should be used if a graph with empty nodes has been passed at network creation. """ - + + # Standard adding new_agents = add_agents(self,agents,agent_class,**kwargs) - if map_to_nodes: - + # Extra network features + if map_to_nodes: # Map each agent to a node of the graph - if len(new_agents) != len(self.graph.nodes): raise ValueError(f"Number of agents ({len(new_agents)}) does not match number of nodes ({len(self.graph.nodes)})") - mapping = { i : agent for i,agent in enumerate(new_agents) } nx.relabel_nodes( self.graph , mapping = mapping, copy=False ) - else: - # Add agents to graph + # Add agents to graph as new nodes for agent in new_agents: self.graph.add_node( agent ) @@ -383,35 +380,32 @@ class env_dict(dict): """ def __init__(self,model): + super().__init__() self.model = model def of_type(self,env_type): - """ Returns an env_dict with environments of `env_type`""" + """ Returns an env_dict with selected environments """ new_dict = env_dict(self.model) selection = {k : v for k, v in self.items() if v.type == env_type} new_dict.update(selection) return new_dict - def do(self,action): + def do(self,method,*args,**kwargs): - """ Calls `action` for all environments """ + """ Calls method for all environments """ - for env in self.values(): getattr( env, action )() + for env in self.values(): getattr(env, method)(*args, **kwargs) def add_agents(self,*args,**kwargs): - """ Adds agents to all environments in self """ - - # Depreciated (!) + """ Adds agents to all environments """ - new_agents = agent_list(self.model) - new_agents.add(*args,**kwargs) - - for env in self.values(): - env.agents.add(new_agents) + for i,env in enumerate(self.values()): + if i == 0: new_agents = env.add_agents(*args,**kwargs) + else: env.add_agents( new_agents ) ### Level 1 - Model Class ### @@ -492,20 +486,18 @@ def __getattr__(self, name): def add_env(self, env_key, env_class=environment, **kwargs): - """ Creates and returns a new environment """ - - self.envs[env_key] = env_class(self.model, env_key, **kwargs) - return self.envs[env_key] - - def add_envs(self,env_keys,*args,**kwargs): - for env_key in env_keys: self.add_env(env_key,*args,**kwargs) + """ Creates a new environment """ + + for env_key in make_list(env_key): + self.envs[env_key] = env_class(self.model, env_key, **kwargs) def add_network(self, env_key, graph = None, env_class=network , **kwargs): - self.add_env(env_key, env_class=network, graph=graph, **kwargs) - - def add_networks(self,env_keys,*args,**kwargs): - for env_key in env_keys: self.add_network(env_key,*args,**kwargs) - + + """ Creates a new network environment """ + + for env_key in make_list(env_key): + self.add_env(env_key, env_class=network, graph=graph, **kwargs) + # Recording functions @@ -514,27 +506,10 @@ def measure(self, measure_key, value): """ Records an evaluation measure """ self.measure_log[measure_key] = [value] - - def record_graph(self, graph_keys): - - """ Records a network """ # (!) unfinished - - for graph_key in make_list(graph_keys): - - G = self.envs[graph_key].graph - H = nx.relabel_nodes(G, lambda x: x.id) - # (!) graph attributes? H.graph.t = self.t - if 'graphs' not in self.model.output.keys(): - self.model.output['graphs'] = [] #{'t':[]} - self.model.output['graphs'].append(H) - #if graph_key not in self.model.output['graphs'].keys(): - # self.model.output['graphs'][graph_key] = [] - #self.model.output['graphs'][graph_key].append(H) - #self.model.output['graphs']['t'].append(self.t) def record_all(self): - """ Records all model variables """ + """ Records all dynamic model variables """ keys = list(self.keys()) for key in self._int_keys: @@ -545,63 +520,57 @@ def record_all(self): # Main simulation functions def step(self): - """ - Defines the model's actions during each simulation step. - Can be overwritten and used to perform the models' main dynamics. - """ + """ Defines the model's actions during each simulation step. + Can be overwritten and used to perform the models' main dynamics.""" pass def update(self): - """ - Defines the model's actions after setup and each simulation step. - Can be overwritten and used for the recording of dynamic variables. - """ + """ Defines the model's actions after setup and each simulation step. + Can be overwritten and used for the recording of dynamic variables. """ pass def end(self): - """ - Defines the model's actions after the last simulation step. - Can be overwritten and used for final calculations and the recording of evaluation measures. - """ + """ Defines the model's actions after the last simulation step. + Can be overwritten and used to calculate evaluation measures.""" pass def stop_if(self): """ - Stops the simulation if return value is `False`. - Returns whether time-step 't' has reached the parameter 'steps'. + Stops :meth:`model.run` during an active simulation if it returns `False`. Can be overwritten with a custom function. + + Returns: + bool: Whether time-step `t` has reached the parameter `model.p.steps`. """ return self.t >= self.p.steps def stop(self): - """ Stops the simulation. """ - self.stopped = True + """ Stops :meth:`model.run` during an active simulation. """ + self._stop = True def run(self,display=True): """ Executes the simulation of the model. - - The order of events is as follows: - - - setup() - - update() - - while stop_if() returns False and stop() hasn't been called: - - t += 1 - - step() - - update() - - end() + The order of events is as follows. + The simulation starts at `t=0` and calls setup() and update(). + While stop_if() returns False and stop() hasn't been called, + the simulation repeatedly calls t+=1, step(), and update(). + After the last step, end() is called. Arguments: display(bool,optional): Whether to display simulation progress (default True). + + Returns: + data_dict: Recorded model data, also stored in `model.output`. """ t0 = datetime.now() # Time-Stamp - self.stopped = False + self._stop = False self.setup() self.update() - while not self.stop_if() and not self.stopped: + while not self.stop_if() and not self._stop: self.t += 1 self.step() @@ -609,7 +578,6 @@ def run(self,display=True): if display: print( f"\rCompleted: {self.t} steps" , end='' ) - self.stopped = True self.end() self.create_output() self.output.log['run_time'] = ct = str( datetime.now() - t0 ) diff --git a/agentpy/interactive.py b/agentpy/interactive.py index ab5dfca..6f0c23e 100644 --- a/agentpy/interactive.py +++ b/agentpy/interactive.py @@ -17,7 +17,7 @@ def animate(model, parameters, fig, axs, plot, skip_t0 = False, **kwargs): """ Returns an animation of the model simulation """ m = model(parameters) - m.stopped = False + m._stop = False m.setup() m.update() @@ -28,7 +28,7 @@ def frames(): nonlocal m, step0, step00 - while not m.stop_if() and not m.stopped: + while not m.stop_if() and not m._stop: if step0: step0 = False elif step00: step00 = False diff --git a/agentpy/output.py b/agentpy/output.py index dfe4151..a8f695a 100644 --- a/agentpy/output.py +++ b/agentpy/output.py @@ -16,10 +16,7 @@ class data_dict(attr_dict): - """ - Agentpy dictionary for output data - - """ + """ Dictionary for recorded data from :class:`model` and :class:`experiment` simulations. """ def __init__(self): super().__init__() @@ -46,6 +43,7 @@ def __repr__(self): return rep + "\n}" + def ps_keys(self): """ Returns the keys of varied parameters """ @@ -76,6 +74,7 @@ def get_pars(self, parameters = None): return dfp + def get_measures(self,measures=None,parameters=None,reset_index=True): """ Returns pandas dataframe with measures """ @@ -90,6 +89,7 @@ def get_measures(self,measures=None,parameters=None,reset_index=True): return df + def get_vars(self,var_keys=None,obj_types=None,parameters=None,reset_index=True): """ Returns pandas dataframe with variables """ diff --git a/agentpy/tools.py b/agentpy/tools.py index a59d28a..4d3277d 100644 --- a/agentpy/tools.py +++ b/agentpy/tools.py @@ -10,6 +10,8 @@ class AgentpyError(Exception): pass + + class attr_dict(dict): """ Dictionary where attributes and dict entries are identical. """ @@ -50,8 +52,6 @@ def __call__(self,*args,**kwargs): try: return [ func_obj(*args,**kwargs) for func_obj in self ] except TypeError: raise TypeError(f"Not all objects in '{type(self._super).__name__}' are callable.") - # Operators, see https://docs.python.org/3/library/operator.html - def __eq__(self, other): return type(self._super)([obj for obj,x in zip(self._super,self) if x == other]) diff --git a/docs/T01_Wealth_Transfer.ipynb b/docs/T01_Wealth_Transfer.ipynb index 7a4350f..1e0f0a2 100644 --- a/docs/T01_Wealth_Transfer.ipynb +++ b/docs/T01_Wealth_Transfer.ipynb @@ -91,7 +91,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "All agents are contained within an object of the class `model`. The model initializes with the above defined agents, the number of which will be taken from the models' parameters. In each time-step, `wealth_transfer `is activated for all agents in a random order, after which each agents `wealth` and a model variable `Gini Coefficient` are recorded." + "All agents are contained within an object of the class `model`. The model initializes with the above defined agents, the number of which will be taken from the models' parameters. In each time-step, `wealth_transfer` is activated for all agents in a random order, after which a model variable `Gini Coefficient` are recorded. At the end of the simulation, each agents `wealth` is also recorded." ] }, { @@ -136,7 +136,7 @@ "source": [ "parameters = {\n", " 'agents': 100,\n", - " 'steps': 100,\n", + " 'steps': 100\n", "}" ] }, diff --git a/docs/T02_Virus_Spread.ipynb b/docs/T02_Virus_Spread.ipynb index 81f8a64..516ca10 100644 --- a/docs/T02_Virus_Spread.ipynb +++ b/docs/T02_Virus_Spread.ipynb @@ -51164,7 +51164,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The function `sobol_sensitivity()` calculates sobol sensitivity indices for the passed results and parameter ranges, using the [SAlib]( https://salib.readthedocs.io/en/latest/basics.html) package. This adds two new categories to our results:\n", + "The function `sensitivity()` calculates sobol sensitivity indices for the passed results and parameter ranges, using the [SAlib]( https://salib.readthedocs.io/en/latest/basics.html) package. This adds two new categories to our results:\n", "\n", "- `sensitivity` - first-order sobol sensitivity indices\n", "- `sensitivity_conf` - confidence ranges for the above indices" @@ -51280,7 +51280,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can use the pandas functionalities to create a bar plot `plot_sensitivity()` that visualizes our sensitivities." + "We can use the pandas functionalities to create a bar plot `plot_sobol_indices()` that visualizes our sensitivities." ] }, { diff --git a/docs/conf.py b/docs/conf.py index f5e8cc2..ff56fc5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,7 +51,7 @@ nbsphinx_prolog = """ .. currentmodule:: agentpy .. note:: - You can download this tutorial as a Jupyter Notebook :download:`here<{{ env.doc2path(env.docname,base=None) }}>` (Save link to file) + You can download this tutorial as a Jupyter Notebook :download:`here<{{ env.doc2path(env.docname,base=None) }}>` """ # Intersphinx mapping diff --git a/docs/installation.rst b/docs/installation.rst index 19ff87a..e20ea3b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -12,7 +12,7 @@ To install the package, run the following command on your console:: Alternatively, you can also clone the latest version from GitHub:: - $ git clone hhttps://github.com/JoelForamitti/agentpy.git + $ git clone https://github.com/JoelForamitti/agentpy.git The agentpy packet can then be imported as follows (recommended):: diff --git a/docs/reference.rst b/docs/reference.rst index b054ece..ab9b4be 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -60,8 +60,8 @@ Parameter sampling .. autofunction:: sample -Multi-run experiments ---------------------- +Experiment class +---------------- .. autoclass:: experiment :members: