Full Code of JoelForamitti/agentpy for AI

master 42f5d498677c cached
76 files
27.8 MB
1.7M tokens
341 symbols
1 requests
Download .txt
Showing preview only (6,726K chars total). Download the full file or copy to clipboard to get everything.
Repository: JoelForamitti/agentpy
Branch: master
Commit: 42f5d498677c
Files: 76
Total size: 27.8 MB

Directory structure:
gitextract_t5qr14uy/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── publish.yml
│       └── test.yml
├── .gitignore
├── .readthedocs.yml
├── LICENSE
├── README.md
├── agentpy/
│   ├── __init__.py
│   ├── agent.py
│   ├── datadict.py
│   ├── examples.py
│   ├── experiment.py
│   ├── grid.py
│   ├── model.py
│   ├── network.py
│   ├── objects.py
│   ├── sample.py
│   ├── sequences.py
│   ├── space.py
│   ├── tools.py
│   ├── version.py
│   └── visualization.py
├── docs/
│   ├── Makefile
│   ├── _static/
│   │   └── css/
│   │       └── custom.css
│   ├── about.rst
│   ├── agentpy_button_network.ipynb
│   ├── agentpy_demo.py
│   ├── agentpy_flocking.ipynb
│   ├── agentpy_forest_fire.ipynb
│   ├── agentpy_segregation.ipynb
│   ├── agentpy_virus_spread.ipynb
│   ├── agentpy_wealth_transfer.ipynb
│   ├── changelog.rst
│   ├── conf.py
│   ├── contributing.rst
│   ├── guide.rst
│   ├── guide_ema.ipynb
│   ├── guide_interactive.ipynb
│   ├── guide_random.ipynb
│   ├── index.rst
│   ├── installation.rst
│   ├── make.bat
│   ├── model_library.rst
│   ├── overview.rst
│   ├── reference.rst
│   ├── reference_agents.rst
│   ├── reference_data.rst
│   ├── reference_environments.rst
│   ├── reference_examples.rst
│   ├── reference_experiment.rst
│   ├── reference_grid.rst
│   ├── reference_model.rst
│   ├── reference_network.rst
│   ├── reference_other.rst
│   ├── reference_sample.rst
│   ├── reference_sequences.rst
│   ├── reference_space.rst
│   └── reference_visualization.rst
├── paper/
│   ├── paper.bib
│   └── paper.md
├── setup.cfg
├── setup.py
└── tests/
    ├── __init__.py
    ├── test_datadict.py
    ├── test_examples.py
    ├── test_experiment.py
    ├── test_grid.py
    ├── test_init.py
    ├── test_model.py
    ├── test_network.py
    ├── test_objects.py
    ├── test_sample.py
    ├── test_sequences.py
    ├── test_space.py
    ├── test_tools.py
    └── test_visualization.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  # Maintain dependencies for GitHub Actions
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      # Check for updates to GitHub Actions every week
      interval: "weekly"


================================================
FILE: .github/workflows/publish.yml
================================================
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

name: Build and upload package

on:
  release:
    types: [published]

permissions:
  contents: read

jobs:
  deploy:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.10'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install build
    - name: Build package
      run: python -m build
    - name: Publish package
      uses: pypa/gh-action-pypi-publish@d7edd4c95736a5bc1260d38b5523f5d24338bc25
      with:
        user: __token__
        password: ${{ secrets.PYPI_API_TOKEN }}


================================================
FILE: .github/workflows/test.yml
================================================
name: Run tests

on: [push, pull_request, workflow_dispatch]

permissions:
  contents: read

jobs:
  build:

    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"]

    steps:
    - uses: actions/checkout@v3
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        python -m pip install pytest
        python -m pip install .
        if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
    - name: Test with pytest
      run: pytest



================================================
FILE: .gitignore
================================================
# Agentpy
ap_output/
docs/ap_output/

# Pycharm
.idea/

# Frome here-on generated automatically
# by https://www.toptal.com/developers/gitignore/api/python

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/
doc/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
pythonenv*

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# profiling data
.prof

================================================
FILE: .readthedocs.yml
================================================
version: 2
formats: all
build:
  os: ubuntu-22.04 # Or ubuntu-20.04
  tools:
    python: "3.9" # Adjust based on your project
    sphinx: "5.0" # Specify a compatible Sphinx version
python:
  install:
    - method: pip
      path: .
      extra_requirements: ["docs"]
sphinx:
  # Path to your Sphinx configuration file.
  configuration: docs/conf.py


================================================
FILE: LICENSE
================================================
BSD 3-Clause License

Copyright (c) 2020-2021 Joël Foramitti

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

================================================
FILE: README.md
================================================
# AgentPy - Agent-based modeling in Python

[![PyPI](https://img.shields.io/pypi/v/agentpy)](https://pypi.org/project/agentpy/)
[![GitHub](https://img.shields.io/github/license/joelforamitti/agentpy)](https://github.com/JoelForamitti/agentpy/blob/master/LICENSE)
[![Documentation Status](https://readthedocs.org/projects/agentpy/badge/?version=latest)](https://agentpy.readthedocs.io/en/latest/?badge=latest)
[![DOI](https://joss.theoj.org/papers/10.21105/joss.03065/status.svg)](https://doi.org/10.21105/joss.03065)

AgentPy is an open-source library for the development and analysis of agent-based models in Python.
The framework integrates the tasks of model design, interactive simulations, numerical experiments,
and data analysis within a single environment. The package is optimized for interactive computing
with [IPython](http://ipython.org/) and [Jupyter](https://jupyter.org/).

**Note:** AgentPy is no longer under active development. For new projects, we recommend using [MESA](https://mesa.readthedocs.io/stable/).

**Installation:** `pip install agentpy`

**Documentation:** https://agentpy.readthedocs.io

**JOSS publication:** https://doi.org/10.21105/joss.03065

Please cite this software as follows:

    Foramitti, J., (2021). AgentPy: A package for agent-based modeling in Python.
    Journal of Open Source Software, 6(62), 3065, https://doi.org/10.21105/joss.03065


================================================
FILE: agentpy/__init__.py
================================================
"""
Agentpy - Agent-based modeling in Python
Copyright (c) 2020-2021 Joël Foramitti

Documentation: https://agentpy.readthedocs.io/
Examples: https://agentpy.readthedocs.io/en/latest/model_library.html
Source: https://github.com/JoelForamitti/agentpy
"""

__all__ = [
    '__version__',
    'Model',
    'Agent',
    'AgentList', 'AgentDList', 'AgentSet',
    'AgentIter', 'AgentDListIter', 'AttrIter',
    'Grid', 'GridIter',
    'Space',
    'Network', 'AgentNode',
    'Experiment',
    'DataDict',
    'Sample', 'Values', 'Range', 'IntRange',
    'gridplot', 'animate',
    'AttrDict'
]

from .version import __version__

from .model import Model
from .agent import Agent
from .sequences import AgentList, AgentDList, AgentSet
from .sequences import AgentIter, AgentDListIter, AttrIter
from .grid import Grid, GridIter
from .space import Space
from .network import Network, AgentNode
from .experiment import Experiment
from .datadict import DataDict
from .sample import Sample, Values, Range, IntRange
from .visualization import gridplot, animate
from .tools import AttrDict


================================================
FILE: agentpy/agent.py
================================================
"""
Agentpy Agent Module
Content: Agent Classes
"""

from .objects import Object
from .sequences import AgentList
from .tools import AgentpyError, make_list


class Agent(Object):
    """ Template for an individual agent.

    Arguments:
        model (Model): The model instance.
        **kwargs: Will be forwarded to :func:`Agent.setup`.

    Attributes:
        id (int): Unique identifier of the agent.
        log (dict): Recorded variables of the agent.
        type (str): Class name of the agent.
        model (Model): The model instance.
        p (AttrDict): The model parameters.
        vars (list of str): Names of the agent's custom variables.
    """

    def __init__(self, model, *args, **kwargs):
        super().__init__(model)
        self.setup(*args, **kwargs)


================================================
FILE: agentpy/datadict.py
================================================
"""
Agentpy Output Module
Content: DataDict class for output data
"""

import pandas as pd
import os
from os import listdir, makedirs
from os.path import getmtime, join
from SALib.analyze import sobol
from .tools import AttrDict, make_list, AgentpyError
import json
import numpy as np


class NpEncoder(json.JSONEncoder):
    """ Adds support for numpy number formats to json. """
    # By Jie Yang https://stackoverflow.com/a/57915246
    def default(self, obj):
        if isinstance(obj, np.integer):
            return int(obj)
        elif isinstance(obj, np.floating):
            return float(obj)
        elif isinstance(obj, np.ndarray):
            return obj.tolist()
        elif isinstance(obj, np.bool_):
            return bool(obj)
        else:
            return super(NpEncoder, self).default(obj)


def _last_exp_id(name, path):
    """ Identifies existing experiment data and return highest id. """

    output_dirs = listdir(path)
    exp_dirs = [s for s in output_dirs if name in s]
    if exp_dirs:
        ids = [int(s.split('_')[-1]) for s in exp_dirs]
        return max(ids)
    else:
        return None


# TODO Create DataSubDict without methods
class DataDict(AttrDict):
    """ Nested dictionary for output data of simulations.
    Items can be accessed like attributes.
    Attributes can differ from the standard ones listed below.

    Attributes:
        info (dict):
            Metadata of the simulation.
        parameters (DataDict):
            Simulation parameters.
        variables (DataDict):
            Recorded variables, separatedper object type.
        reporters (pandas.DataFrame):
            Reported outcomes of the simulation.
        sensitivity (DataDict):
            Sensitivity data, if calculated.
    """

    def __repr__(self, indent=False):
        rep = ""
        if not indent:
            rep += "DataDict {"
        i = '    ' if indent else ''
        for k, v in self.items():
            rep += f"\n{i}'{k}': "
            if isinstance(v, (int, float, np.integer, np.floating)):
                rep += f"{v} {type(v)}"
            elif isinstance(v, str):
                x0 = f"(length {len(v)})"
                x = f"...' {x0}" if len(v) > 20 else "'"
                rep += f"'{v[:30]}{x} {type(v)}"
            elif isinstance(v, pd.DataFrame):
                lv = len(list(v.columns))
                rv = len(list(v.index))
                rep += f"DataFrame with {lv} " \
                       f"variable{'s' if lv != 1 else ''} " \
                       f"and {rv} row{'s' if rv != 1 else ''}"
            elif isinstance(v, DataDict):
                rep += f"{v.__repr__(indent=True)}"
            elif isinstance(v, dict):
                lv = len(list(v.keys()))
                rep += f"Dictionary with {lv} key{'s' if lv != 1 else ''}"
            elif isinstance(v, list):
                lv = len(v)
                rep += f"List with {lv} entr{'ies' if lv != 1 else 'y'}"
            else:
                rep += f"Object of type {type(v)}"
        if not indent:
            rep += "\n}"
        return rep

    def _short_repr(self):
        len_ = len(self.keys())
        return f"DataDict {{{len_} entr{'y' if len_ == 1 else 'ies'}}}"

    def __eq__(self, other):
        """ Check equivalence of two DataDicts."""
        if not isinstance(other, DataDict):
            return False
        for key, item in self.items():
            if key not in other:
                return False
            if isinstance(item, pd.DataFrame):
                if not self[key].equals(other[key]):
                    return False
            elif not self[key] == other[key]:
                return False
        return True

    def __ne__(self, other):
        return not self.__eq__(other)

    # Data analysis --------------------------------------------------------- #

    @staticmethod
    def _sobol_set_df_index(df, p_keys, reporter):
        df['parameter'] = p_keys
        df['reporter'] = reporter
        df.set_index(['reporter', 'parameter'], inplace=True)

    def calc_sobol(self, reporters=None, **kwargs):
        """ Calculates Sobol Sensitivity Indices
        using :func:`SALib.analyze.sobol.analyze`.
        Data must be from an :class:`Experiment` with a :class:`Sample`
        that was generated with the method 'saltelli'.
        If the experiment had more than one iteration,
        the mean value between iterations will be taken.

        Arguments:
            reporters (str or list of str, optional): The reporters that should
                be used for the analysis. If none are passed,
                all existing reporters except 'seed' are used.
            **kwargs: Will be forwarded to :func:`SALib.analyze.sobol.analyze`.

        Returns:
            DataDict: The DataDict itself with an added category 'sensitivity'.
        """

        if not self.parameters.log['type'] == 'saltelli':
            raise AgentpyError("Sampling method must be 'saltelli'.")
        if self.info['iterations'] == 1:
            reporters_df = self.reporters
        else:
            reporters_df = self.reporters.groupby('sample_id').mean()

        # STEP 1 - Load salib problem from parameter log
        param_ranges_salib = self.parameters.log['salib_problem']
        calc_second_order = self.parameters.log['calc_second_order']

        # STEP 2 - Calculate Sobol Sensitivity Indices
        if reporters is None:
            reporters = reporters_df.columns
            if 'seed' in reporters:
                reporters = reporters.drop('seed')
        elif isinstance(reporters, str):
            reporters = [reporters]
        p_keys = self._combine_pars(sample=True, constants=False).keys()
        dfs_list = [[] for _ in range(4 if calc_second_order else 2)]

        for reporter in reporters:
            y = np.array(reporters_df[reporter])
            si = sobol.analyze(param_ranges_salib, y, calc_second_order, **kwargs)

            # Make dataframes out of S1 and ST sensitivities
            keyss = [['S1', 'ST'], ['S1_conf', 'ST_conf']]
            for keys, dfs in zip(keyss, dfs_list[0:2]):
                s = {k[0:2]: v for k, v in si.items() if k in keys}
                df = pd.DataFrame(s)
                self._sobol_set_df_index(df, p_keys, reporter)
                dfs.append(df)

            # Make dataframes out S2 sensitivities
            if calc_second_order:
                for key, dfs in zip(['S2', 'S2_conf'], dfs_list[2:4]):
                    df = pd.DataFrame(si[key])
                    self._sobol_set_df_index(df, p_keys, reporter)
                    dfs.append(df)

        # Combine dataframes for each reporter
        self['sensitivity'] = sdict = DataDict()
        sdict['sobol'] = pd.concat(dfs_list[0])
        sdict['sobol_conf'] = pd.concat(dfs_list[1])

        if calc_second_order:
            # Add Second-Order to self
            dfs_si = [sdict['sobol'], pd.concat(dfs_list[2])]
            dfs_si_conf = [sdict['sobol_conf'], pd.concat(dfs_list[3])]
            sdict['sobol'] = pd.concat(dfs_si, axis=1)
            sdict['sobol_conf'] = pd.concat(dfs_si_conf, axis=1)

            # Create Multi-Index for Columns
            arrays = [["S1", "ST"] + ["S2"] * len(p_keys), [""] * 2 + list(p_keys)]
            tuples = list(zip(*arrays))
            index = pd.MultiIndex.from_tuples(tuples, names=["order", "parameter"])
            sdict['sobol'].columns = index
            sdict['sobol_conf'].columns = index.copy()

        return self

    # Data arrangement ------------------------------------------------------ #

    def _combine_vars(self, obj_types=True, var_keys=True):
        """ Returns pandas dataframe with combined variables """

        # Retrieve variables
        if 'variables' in self:
            vs = self['variables']
        else:
            return None

        if len(vs.keys()) == 1:
            return list(vs.values())[0]  # Return df if vs has only one entry
        elif isinstance(vs, DataDict):
            df_dict = dict(vs)  # Convert to dict if vs is DataDict

        # Remove dataframes that don't include any of the selected var_keys
        if var_keys is not True:
            df_dict = {k: v for k, v in df_dict.items()
                       if any(x in v.columns for x in make_list(var_keys))}

        # Select object types
        if obj_types is not True:
            df_dict = {k: v for k, v in df_dict.items()
                       if k in make_list(obj_types)}

        # Add 'obj_id' before 't' for model df
        model_type = self.info['model_type']
        if model_type in list(df_dict.keys()):
            df = df_dict[model_type]
            df['obj_id'] = 0
            indexes = list(df.index.names)
            indexes.insert(-1, 'obj_id')
            df = df.reset_index()
            df = df.set_index(indexes)
            df_dict[model_type] = df

        # Return none if empty
        if df_dict == {}:
            return None

        # Create dataframe
        df = pd.concat(df_dict)  # Dict keys (obj_type) will be added to index
        df.index = df.index.set_names('obj_type', level=0)  # Rename new index

        # Select var_keys
        if var_keys is not True:
            # make_list prevents conversion to pd.Series for single value
            df = df[make_list(var_keys)]

        return df

    def _dict_pars_to_df(self, dict_pars):
        n = self.info['sample_size'] if 'sample_size' in self.info else 1
        d = {k: [v] * n for k, v in dict_pars.items()}
        i = pd.Index(list(range(n)), name='sample_id')
        return pd.DataFrame(d, index=i)

    def _combine_pars(self, sample=True, constants=True):
        """ Returns pandas dataframe with parameters and sample_id """
        # Cancel if there are no parameters
        if 'parameters' not in self:
            return None
        dfp = pd.DataFrame()
        if sample and 'sample' in self.parameters:
            dfp = self.parameters.sample.copy()
            if constants and 'constants' in self.parameters:
                for k, v in self.parameters.constants.items():
                    dfp[k] = v
        elif constants and 'constants' in self.parameters:
            dfp = self._dict_pars_to_df(self.parameters.constants)
        # Cancel if no parameters have been selected
        if dfp is None or dfp.empty is True:
            return None
        # Remove seed parameter as the actually used seed is reported per run
        if 'seed' in dfp:
            del dfp['seed']
        return dfp

    def arrange(self, variables=False, reporters=False, parameters=False,
                constants=False, obj_types=True, index=False):
        """ Combines and/or filters data based on passed arguments.

        Arguments:
            variables (bool or str or list of str, optional):
                Key or list of keys of variables to include in the dataframe.
                If True, all available variables are selected.
                If False (default), no variables are selected.
            reporters (bool or str or list of str, optional):
                Key or list of keys of reporters to include in the dataframe.
                If True, all available reporters are selected.
                If False (default), no reporters are selected.
            parameters (bool or str or list of str, optional):
                Key or list of keys of parameters to include in the dataframe.
                If True, all non-constant parameters are selected.
                If False (default), no parameters are selected.
            constants (bool, optional):
                Include constants if 'parameters' is True (default False).
            obj_types (str or list of str, optional):
                Agent and/or environment types to include in the dataframe.
                If True (default), all objects are selected.
                If False, no objects are selected.
            index (bool, optional):
                Whether to keep original multi-index structure (default False).

        Returns:
            pandas.DataFrame: The newly arranged dataframe.
        """

        dfv = dfm = dfp = df = None

        # Step 1: Variables
        if variables is not False:
            dfv = self._combine_vars(obj_types, variables)

        # Step 2: Measures
        if reporters is not False:
            dfm = self.reporters
            if reporters is not True:  # Select reporter keys
                # make_list prevents conversion to pd.Series for single value
                dfm = dfm[make_list(reporters)]

        # Step 3: Parameters
        if parameters is True:
            dfp = self._combine_pars(constants=constants)
        elif parameters is not False:
            dfp = self._combine_pars()
            dfp = dfp[make_list(parameters)]

        # Step 4: Combine dataframes
        if dfv is not None and dfm is not None:
            # Combine variables & measures
            index_keys = dfv.index.names
            dfm = dfm.reset_index()
            dfv = dfv.reset_index()
            df = pd.concat([dfm, dfv])
            df = df.set_index(index_keys)
        elif dfv is not None:
            df = dfv
        elif dfm is not None:
            df = dfm
        if dfp is not None:
            if df is None:
                df = dfp
            else:  # Combine df with parameters
                if df is not None and isinstance(df.index, pd.MultiIndex):
                    dfp = dfp.reindex(df.index, level='sample_id')
                df = pd.concat([df, dfp], axis=1)

        if df is None:
            return pd.DataFrame()

        # Step 6: Reset index
        if not index:
            df = df.reset_index()

        return df

    def arrange_reporters(self):
        """ Common use case of :obj:`DataDict.arrange`
        with `reporters=True` and `parameters=True`. """
        return self.arrange(variables=False, reporters=True, parameters=True)

    def arrange_variables(self):
        """ Common use case of :obj:`DataDict.arrange`
        with `variables=True` and `parameters=True`. """
        return self.arrange(variables=True, reporters=False, parameters=True)

    # Saving and loading data ----------------------------------------------- #

    def save(self, exp_name=None, exp_id=None, path='ap_output', display=True):
        """ Writes data to directory `{path}/{exp_name}_{exp_id}/`.

        Works only for entries that are of type :class:`DataDict`,
        :class:`pandas.DataFrame`, or serializable with JSON
        (int, float, str, dict, list). Numpy objects will be converted
        to standard objects, if possible.

        Arguments:
            exp_name (str, optional): Name of the experiment to be saved.
                If none is passed, `self.info['model_type']` is used.
            exp_id (int, optional): Number of the experiment.
                Note that passing an existing id can overwrite existing data.
                If none is passed, a new id is generated.
            path (str, optional): Target directory (default 'ap_output').
            display (bool, optional): Display saving progress (default True).
        """

        # Create output directory if it doesn't exist
        if path not in listdir():
            makedirs(path)

        # Set exp_name
        if exp_name is None:
            if 'info' in self and 'model_type' in self.info:
                exp_name = self.info['model_type']
            else:
                exp_name = 'Unnamed'

        exp_name = exp_name.replace(" ", "_")

        # Set exp_id
        if exp_id is None:
            exp_id = _last_exp_id(exp_name, path)
            if exp_id is None:
                exp_id = 1
            else:
                exp_id += 1

        # Create new directory for output
        directory = f'{exp_name}_{exp_id}'
        path_dir = f'{path}/{directory}'
        if directory not in listdir(path):
            makedirs(path_dir)

        # Save experiment data
        for key, output in self.items():

            if isinstance(output, pd.DataFrame):
                output.to_csv(f'{path_dir}/{key}.csv')

            elif isinstance(output, DataDict):
                for k, o in output.items():

                    if isinstance(o, pd.DataFrame):
                        o.to_csv(f'{path_dir}/{key}_{k}.csv')
                    elif isinstance(o, dict):
                        with open(f'{path_dir}/{key}_{k}.json', 'w') as fp:
                            json.dump(o, fp, cls=NpEncoder)

            else:  # Use JSON for other object types
                try:
                    with open(f'{path_dir}/{key}.json', 'w') as fp:
                        json.dump(output, fp, cls=NpEncoder)
                except TypeError as e:
                    print(f"Warning: Object '{key}' could not be saved. "
                          f"(Reason: {e})")
                    os.remove(f'{path_dir}/{key}.json')

            # TODO Support grids & graphs
            # elif t == nx.Graph:
            #    nx.write_graphml(output, f'{path}/{key}.graphml')

        if display:
            print(f"Data saved to {path_dir}")

    def _load(self, exp_name=None, exp_id=None,
              path='ap_output', display=True):

        def load_file(path, file, display):
            if display:
                print(f'Loading {file} - ', end='')
            i_cols = ['sample_id', 'iteration', 'obj_id', 't']
            ext = file.split(".")[-1]
            path = path + file
            try:
                if ext == 'csv':
                    obj = pd.read_csv(path) # Convert .csv into DataFrane
                    index = [i for i in i_cols if i in obj.columns]
                    if index:  # Set potential index columns
                        obj = obj.set_index(index)
                elif ext == 'json':
                    # Convert .json with json decoder
                    with open(path, 'r') as fp:
                        obj = json.load(fp)
                    # Convert dict to AttrDict
                    if isinstance(obj, dict):
                        obj = AttrDict(obj)
                # TODO Support grids & graphs
                # elif ext == 'graphml':
                #    self[key] = nx.read_graphml(path)
                else:
                    raise ValueError(f"File type '{ext}' not supported")
                if display:
                    print('Successful')
                return obj
            except Exception as e:
                print(f'Error: {e}')

        # Prepare for loading
        if exp_name is None:
            # Choose latest modified experiment
            exp_names = listdir(path)
            paths = [join(path, d) for d in exp_names]
            latest_exp = exp_names[paths.index(max(paths, key=getmtime))]
            exp_name = latest_exp.rsplit('_', 1)[0]

        exp_name = exp_name.replace(" ", "_")
        if exp_id is None:
            exp_id = _last_exp_id(exp_name, path)
            if exp_id is None:
                raise FileNotFoundError(f"No experiment found with "
                                        f"name '{exp_name}' in path '{path}'")
        path = f'{path}/{exp_name}_{exp_id}/'
        if display:
            print(f'Loading from directory {path}')

        # Loading data
        for file in listdir(path):
            if 'variables_' in file:
                if 'variables' not in self:
                    self['variables'] = DataDict()
                ext = file.split(".")[-1]
                key = file[:-(len(ext) + 1)].replace('variables_', '')
                self['variables'][key] = load_file(path, file, display)
            elif 'parameters_' in file:
                ext = file.split(".")[-1]
                key = file[:-(len(ext) + 1)].replace('parameters_', '')
                if 'parameters' not in self:
                    self['parameters'] = DataDict()
                self['parameters'][key] = load_file(path, file, display)
            else:
                ext = file.split(".")[-1]
                key = file[:-(len(ext) + 1)]
                self[key] = load_file(path, file, display)
        return self

    @classmethod
    def load(cls, exp_name=None, exp_id=None, path='ap_output', display=True):
        """ Reads data from directory `{path}/{exp_name}_{exp_id}/`.

            Arguments:
                exp_name (str, optional): Experiment name.
                    If none is passed, the most recent experiment is chosen.
                exp_id (int, optional): Id number of the experiment.
                    If none is passed, the highest available id used.
                path (str, optional): Target directory (default 'ap_output').
                display (bool, optional): Display loading progress (default True).

            Returns:
                DataDict: The loaded data from the chosen experiment.
        """
        return cls()._load(exp_name, exp_id, path, display)


================================================
FILE: agentpy/examples.py
================================================
import agentpy as ap
import numpy as np


def gini(x):

    """ Calculate Gini Coefficient """
    # By Warren Weckesser https://stackoverflow.com/a/39513799

    x = np.array(x)
    mad = np.abs(np.subtract.outer(x, x)).mean()  # Mean absolute difference
    rmad = mad / np.mean(x)  # Relative mean absolute difference
    return 0.5 * rmad


class WealthAgent(ap.Agent):

    """ An agent with wealth """

    def setup(self):

        self.wealth = 1

    def wealth_transfer(self):

        if self.wealth > 0:

            partner = self.model.agents.random()
            partner.wealth += 1
            self.wealth -= 1


class WealthModel(ap.Model):

    """
    Demonstration model of random wealth transfers.

    See Also:
        Notebook in the model library: :doc:`agentpy_wealth_transfer`

    Arguments:
        parameters (dict):

            - agents (int): Number of agents.
            - steps (int, optional): Number of time-steps.
    """

    def setup(self):
        self.agents = ap.AgentList(self, self.p.agents, WealthAgent)

    def step(self):
        self.agents.wealth_transfer()

    def update(self):
        self.gini = gini(self.agents.wealth)
        self.record('gini')

    def end(self):
        self.report('gini')


class SegregationAgent(ap.Agent):

    def setup(self):
        """ Initiate agent attributes. """
        self.grid = self.model.grid
        self.random = self.model.random
        self.group = self.random.choice(range(self.p.n_groups))
        self.share_similar = 0
        self.happy = False

    def update_happiness(self):
        """ Be happy if rate of similar neighbors is high enough. """
        neighbors = self.grid.neighbors(self)
        similar = len([n for n in neighbors if n.group == self.group])
        ln = len(neighbors)
        self.share_similar = similar / ln if ln > 0 else 0
        self.happy = self.share_similar >= self.p.want_similar

    def find_new_home(self):
        """ Move to random free spot and update free spots. """
        new_spot = self.random.choice(self.model.grid.empty)
        self.grid.move_to(self, new_spot)


class SegregationModel(ap.Model):
    """
    Demonstration model of segregation dynamics.

    See Also:
        Notebook in the model library: :doc:`agentpy_segregation`

    Arguments:
        parameters (dict):

            - want_similar (float):
              Percentage of similar neighbors
              for agents to be happy
            - n_groups (int): Number of groups
            - density (float): Density of population
            - size (int): Height and length of the grid
            - steps (int, optional): Maximum number of steps
    """

    def setup(self):

        # Parameters
        s = self.p.size
        n = self.n = int(self.p.density * (s ** 2))

        # Create grid and agents
        self.grid = ap.Grid(self, (s, s), track_empty=True)
        self.agents = ap.AgentList(self, n, SegregationAgent)
        self.grid.add_agents(self.agents, random=True, empty=True)

    def update(self):
        # Update list of unhappy people
        self.agents.update_happiness()
        self.unhappy = self.agents.select(self.agents.happy == False)

        # Stop simulation if all are happy
        if len(self.unhappy) == 0:
            self.stop()

    def step(self):
        # Move unhappy people to new location
        self.unhappy.find_new_home()

    def get_segregation(self):
        # Calculate average percentage of similar neighbors
        return round(sum(self.agents.share_similar) / self.n, 2)

    def end(self):
        # Measure segregation at the end of the simulation
        self.report('segregation', self.get_segregation())

================================================
FILE: agentpy/experiment.py
================================================
"""
Agentpy Experiment Module
Content: Experiment class
"""

import warnings
import pandas as pd
import random as rd

from os import sys

from .version import __version__
from datetime import datetime, timedelta
from .tools import make_list
from .datadict import DataDict
from .sample import Sample, Range, IntRange, Values
from joblib import Parallel,delayed


class Experiment:
    """ Experiment that can run an agent-based model
    over for multiple iterations and parameter combinations
    and generate combined output data.

    Arguments:
        model (type):
            The model class for the experiment to use.
        sample (dict or list of dict or Sample, optional):
            Parameter combination(s) to test in the experiment (default None).
        iterations (int, optional):
            How often to repeat every parameter combination (default 1).
        record (bool, optional):
            Keep the record of dynamic variables (default False).
        randomize (bool, optional):
            Generate different random seeds for every iteration (default True).
            If True, the parameter 'seed' will be used to initialize a random
            seed generator for every parameter combination in the sample.
            If False, the same seed will be used for every iteration.
            If no parameter 'seed' is defined, this option has no effect.
            For more information, see :doc:`guide_random` .
        **kwargs:
            Will be forwarded to all model instances created by the experiment.

    Attributes:
        output(DataDict): Recorded experiment data
    """

    def __init__(self, model_class, sample=None, iterations=1,
                 record=False, randomize=True, **kwargs):

        self.model = model_class
        self.output = DataDict()
        self.iterations = iterations
        self.record = record
        self._model_kwargs = kwargs
        self.name = model_class.__name__

        # Prepare sample
        if isinstance(sample, Sample):
            self.sample = list(sample)
            self._sample_log = sample._log
        else:
            self.sample = make_list(sample, keep_none=True)
            self._sample_log = None

        # Prepare runs
        len_sample = len(self.sample)
        iter_range = range(iterations) if iterations > 1 else [None]
        sample_range = range(len_sample) if len_sample > 1 else [None]
        self.run_ids = [(sample_id, iteration)
                        for sample_id in sample_range
                        for iteration in iter_range]
        self.n_runs = len(self.run_ids)

        # Prepare seeds
        if randomize and sample is not None \
                and any(['seed' in p for p in self.sample]):
            if len_sample > 1:
                rngs = [rd.Random(p['seed'])
                        if 'seed' in p else rd.Random() for p in self.sample]
                self._random = {
                    (sample_id, iteration): rngs[sample_id].getrandbits(128)
                    for sample_id in sample_range
                    for iteration in iter_range
                }
            else:
                p = list(self.sample)[0]
                seed = p['seed']
                ranges = (Range, IntRange, Values)
                if isinstance(seed, ranges):
                    seed = seed.vdef
                rng = rd.Random(seed)
                self._random = {
                    (None, iteration): rng.getrandbits(128)
                    for iteration in iter_range
                }
        else:
            self._random = None

        # Prepare output
        self.output.info = {
            'model_type': model_class.__name__,
            'time_stamp': str(datetime.now()),
            'agentpy_version': __version__,
            'python_version': sys.version[:5],
            'experiment': True,
            'scheduled_runs': self.n_runs,
            'completed': False,
            'random': randomize,
            'record': record,
            'sample_size': len(self.sample),
            'iterations': iterations
        }
        self._parameters_to_output()

    def _parameters_to_output(self):
        """ Document parameters (separately for fixed & variable). """
        df = pd.DataFrame(self.sample)
        df.index.rename('sample_id', inplace=True)
        fixed_pars = {}
        for col in df.columns:
            s = df[col]
            if len(s.unique()) == 1:
                fixed_pars[s.name] = df[col][0]
                df.drop(col, inplace=True, axis=1)
        self.output['parameters'] = DataDict()
        if fixed_pars:
            self.output['parameters']['constants'] = fixed_pars
        if not df.empty:
            self.output['parameters']['sample'] = df
        if self._sample_log:
            self.output['parameters']['log'] = self._sample_log

    @staticmethod
    def _add_single_output_to_combined(single_output, combined_output):
        """Append results from single run to combined output.
        Each key in single_output becomes a key in combined_output.
        DataDicts entries become dicts with lists of values.
        Other entries become lists of values. """
        for key, value in single_output.items():
            if key in ['parameters', 'info']:  # Skip parameters & info
                continue
            if isinstance(value, DataDict):  # Handle subdicts
                if key not in combined_output:  # New key
                    combined_output[key] = {}  # as dict
                for obj_type, obj_df in single_output[key].items():
                    if obj_type not in combined_output[key]:  # New subkey
                        combined_output[key][obj_type] = []  # as list
                    combined_output[key][obj_type].append(obj_df)
            else:  # Handle other output types
                if key not in combined_output:  # New key
                    combined_output[key] = []  # as list
                combined_output[key].append(value)

    def _combine_dataframes(self, combined_output):
        """ Combines data from combined output.
        Dataframes are combined with concat.
        Dicts are transformed to DataDict.
        Other objects are kept as original.
        Combined data is written to self.output. """
        for key, values in combined_output.items():
            if values and all([isinstance(value, pd.DataFrame)
                               for value in values]):
                self.output[key] = pd.concat(values)  # Df are combined
            elif isinstance(values, dict):  # Dict is transformed to DataDict
                self.output[key] = DataDict()
                for sk, sv in values.items():
                    if all([isinstance(v, pd.DataFrame) for v in sv]):
                        self.output[key][sk] = pd.concat(sv)  # Df are combined
                    else:  # Other objects are kept as original TODO TESTS
                        self.output[key][sk] = sv
            elif key != 'info':  # Other objects are kept as original TODO TESTS
                self.output[key] = values

    def _single_sim(self, run_id):
        """ Perform a single simulation."""
        sample_id = 0 if run_id[0] is None else run_id[0]
        parameters = self.sample[sample_id]
        model = self.model(parameters, _run_id=run_id, **self._model_kwargs)
        if self._random:
            results = model.run(display=False, seed=self._random[run_id])
        else:
            results = model.run(display=False)
        if 'variables' in results and self.record is False:
            del results['variables']  # Remove dynamic variables from record
        return results

    # TODO AgentPy 0.2.0 - Remove pool argument
    def run(self, n_jobs=1, pool=None, display=True, **kwargs):
        """ Perform the experiment.
        The simulation will run the model once for each set of parameters
        and will repeat this process for the set number of iterations.
        Simulation results will be stored in `Experiment.output`.
        Parallel processing is supported based on :func:`joblib.Parallel`.

        Arguments:
            n_jobs (int, optional):
                Number of processes to run in parallel (default 1).
                If 1, no parallel processing is used. If -1, all CPUs are used.
                Will be forwarded to :func:`joblib.Parallel`.
            pool (multiprocessing.Pool, optional):
                [This argument is depreciated.
                Please use 'n_jobs' instead.]
                Pool of active processes for parallel processing.
                If none is passed, normal processing is used.
            display (bool, optional):
                Display simulation progress (default True).
            **kwargs:
                Additional keyword arguments for :func:`joblib.Parallel`.

        Returns:
            DataDict: Recorded experiment data.

        Examples:

            To run a normal experiment::

                exp = ap.Experiment(MyModel, parameters)
                results = exp.run()

            To use parallel processing on all CPUs with status updates::

                exp = ap.Experiment(MyModel, parameters)
                results = exp.run(n_jobs=-1, verbose=10)
        """

        if display:
            n_runs = self.n_runs
            print(f"Scheduled runs: {n_runs}")
        t0 = datetime.now()  # Time-Stamp Start
        combined_output = {}

        # Parallel processing with joblib
        if n_jobs != 1:
            # output_list = pool.map(self._single_sim, self.run_ids)
            output_list = Parallel(n_jobs=n_jobs, **kwargs)(
                delayed(self._single_sim)(i) for i in self.run_ids)
            for single_output in output_list:
                self._add_single_output_to_combined(
                    single_output, combined_output)

        # Normal processing
        elif pool is None:
            i = -1
            for run_id in self.run_ids:
                self._add_single_output_to_combined(
                    self._single_sim(run_id), combined_output)
                if display:
                    i += 1
                    td = (datetime.now() - t0).total_seconds()
                    te = timedelta(seconds=int(td / (i + 1)
                                               * (n_runs - i - 1)))
                    print(f"\rCompleted: {i + 1}, "
                          f"estimated time remaining: {te}", end='')
            if display:
                print("")  # Because the last print ended without a line-break

        # Parallel processing with multiprocessing (TODO to depreciate)
        else:
            warnings.warn(
                "The argument 'pool' in Experiment.run() is depreciated. "
                "Please use 'n_jobs' instead.")
            if display:
                print(f"Using parallel processing.")
                print(f"Active processes: {pool._processes}")
            output_list = pool.map(self._single_sim, self.run_ids)
            #Parallel(n_jobs=num_cores)(delayed(job)(BoidsModel,5,i) for i in tqdm(sample))
            #Parallel(n_jobs=1)(delayed(sqrt)(i**2) for i in range(10))
            for single_output in output_list:
                self._add_single_output_to_combined(
                    single_output, combined_output)

        self._combine_dataframes(combined_output)
        self.end()
        self.output.info['completed'] = True
        self.output.info['run_time'] = ct = str(datetime.now() - t0)

        if display:
            print(f"Experiment finished\nRun time: {ct}")

        return self.output

    def end(self):
        """ Defines the experiment's actions after the last simulation.
        Can be overwritten for final calculations and reporting."""
        pass


================================================
FILE: agentpy/grid.py
================================================
"""
Agentpy Grid Module
Content: Class for discrete spatial environments
"""

import itertools
import numpy as np
import random as rd
import collections.abc as abc
import numpy.lib.recfunctions as rfs
from .objects import SpatialEnvironment
from .tools import make_list, make_matrix, AgentpyError, ListDict
from .sequences import AgentSet, AgentIter, AgentList


class _IterArea:
    """ Iteratable object that takes either a numpy matrix or an iterable
    as an input. If the object is an ndarray, it is flattened and iterated
    over the contents of each element chained together. Otherwise, it is
    simply iterated over the object.

    Arguments:
        area: Area of sets of elements.
        exclude: Element to exclude. Assumes that element is in area.
    """

    def __init__(self, area, exclude=None):
        self.area = area
        self.exclude = exclude

    def __len__(self):
        if isinstance(self.area, np.ndarray):
            len_ = sum([len(s) for s in self.area.flat])
        else:
            len_ = len(self.area)
        if self.exclude:
            len_ -= 1  # Assumes that exclude is in Area
        return len_

    def __iter__(self):
        if self.exclude:
            if isinstance(self.area, np.ndarray):
                return itertools.filterfalse(
                    lambda x: x is self.exclude,
                    itertools.chain.from_iterable(self.area.flat)
                )
            else:
                return itertools.filterfalse(
                    lambda x: x is self.exclude, self.area)
        else:
            if isinstance(self.area, np.ndarray):
                return itertools.chain.from_iterable(self.area.flat)
            else:
                return iter(self.area)


class GridIter(AgentIter):
    """ Iterator over objects in :class:`Grid` that supports slicing.

    Examples:

        Create a model with a 10 by 10 grid
        with one agent in each position::

            model = ap.Model()
            agents = ap.AgentList(model, 100)
            grid = ap.Grid(model, (10, 10))
            grid.add_agents(agents)

        The following returns an iterator over the agents in all position::

            >>> grid.agents
            GridIter (100 objects)

        The following returns an iterator over the agents
        in the top-left quarter of the grid::

            >>> grid.agents[0:5, 0:5]
            GridIter (25 objects)
    """

    def __init__(self, model, iter_, items):
        super().__init__(model, iter_)
        object.__setattr__(self, '_items', items)

    def __getitem__(self, item):
        sub_area = self._items[item]
        return GridIter(self._model, _IterArea(sub_area), sub_area)


class Grid(SpatialEnvironment):
    """ Environment that contains agents with a discrete spatial topology,
    supporting multiple agents and attribute fields per cell.
    For a continuous spatial topology, see :class:`Space`.

    This class can be used as a parent class for custom grid types.
    All agentpy model objects call the method :func:`setup` after creation,
    and can access class attributes like dictionary items.

    Arguments:
        model (Model):
            The model instance.
        shape (tuple of int):
            Size of the grid.
            The length of the tuple defines the number of dimensions,
            and the values in the tuple define the length of each dimension.
        torus (bool, optional):
            Whether to connect borders (default False).
            If True, the grid will be toroidal, meaning that agents who
            move over a border will re-appear on the opposite side.
            If False, they will remain at the edge of the border.
        track_empty (bool, optional):
            Whether to keep track of empty cells (default False).
            If true, empty cells can be accessed via :obj:`Grid.empty`.
        check_border (bool, optional):
            Ensure that agents stay within border (default True).
            Can be set to False for faster performance.
        **kwargs: Will be forwarded to :func:`Grid.setup`.

    Attributes:
        agents (GridIter):
            Iterator over all agents in the grid.
        positions (dict of Agent):
            Dictionary linking each agent instance to its position.
        grid (numpy.rec.array):
            Structured numpy record array with a field 'agents'
            that holds an :class:`AgentSet` in each position.
        shape (tuple of int):
            Length of each dimension.
        ndim (int):
            Number of dimensions.
        all (list):
            List of all positions in the grid.
        empty (ListDict):
            List of unoccupied positions, only available
            if the Grid was initiated with `track_empty=True`.
    """

    @staticmethod
    def _agent_field(field_name, shape, model):
        # Prepare structured array filled with empty agent sets
        array = np.empty(shape, dtype=[(field_name, object)])
        it = np.nditer(array, flags=['refs_ok', 'multi_index'])
        for _ in it:
            array[it.multi_index] = AgentSet(model)
        return array

    def __init__(self, model, shape, torus=False,
                 track_empty=False, check_border=True, **kwargs):

        super().__init__(model)

        self._track_empty = track_empty
        self._check_border = check_border
        self._torus = torus

        self.positions = {}
        self.grid = np.rec.array(self._agent_field('agents', shape, model))
        self.shape = tuple(shape)
        self.ndim = len(self.shape)
        self.all = list(itertools.product(*[range(x) for x in shape]))
        self.empty = ListDict(self.all) if track_empty else None

        self._set_var_ignore()
        self.setup(**kwargs)

    @property
    def agents(self):
        return GridIter(self.model, self.positions.keys(), self.grid.agents)

    # Add and remove agents ------------------------------------------------- #

    def _add_agent(self, agent, position, field):
        position = tuple(position)
        self.grid[field][position].add(agent)  # Add agent to grid
        self.positions[agent] = position  # Add agent position to dict

    def add_agents(self, agents, positions=None, random=False, empty=False):
        """ Adds agents to the grid environment.

        Arguments:
            agents (Sequence of Agent):
                Iterable of agents to be added.
            positions (Sequence of positions, optional):
                The positions of the agents.
                Must have the same length as 'agents',
                with each entry being a tuple of integers.
                If none is passed, positions will be chosen automatically
                based on the arguments 'random' and 'empty':

                - random and empty:
                  Random selection without repetition from `Grid.empty`.
                - random and not empty:
                  Random selection with repetition from `Grid.all`.
                - not random and empty:
                  Iterative selection from `Grid.empty`.
                - not random and not empty:
                  Iterative selection from `Grid.all`.

            random (bool, optional):
                Whether to choose random positions (default False).
            empty (bool, optional):
                Whether to choose only empty cells (default False).
                Can only be True if Grid was initiated with `track_empty=True`.
        """

        field = 'agents'

        if empty and not self._track_empty:
            raise AgentpyError(
                "To use 'Grid.add_agents()' with 'empty=True', "
                "Grid must be iniated with 'track_empty=True'.")

        # Choose positions
        if positions:
            pass
        elif random:
            n = len(agents)
            if empty:
                positions = self.model.random.sample(self.empty, k=n)
            else:
                positions = self.model.random.choices(self.all, k=n)
        else:
            if empty:
                positions = list(self.empty)  # Soft copy
            else:
                positions = itertools.cycle(self.all)

        if empty and len(positions) < len(agents):
            raise AgentpyError("Cannot add more agents than empty positions.")

        if self._track_empty:
            for agent, position in zip(agents, positions):
                self._add_agent(agent, position, field)
                if position in self.empty:
                    self.empty.remove(position)
        else:
            for agent, position in zip(agents, positions):
                self._add_agent(agent, position, field)

    def remove_agents(self, agents):
        """ Removes agents from the environment. """
        for agent in make_list(agents):
            pos = self.positions[agent]  # Get position
            self.grid.agents[pos].remove(agent)  # Remove agent from grid
            del self.positions[agent]  # Remove agent from position dict
            if self._track_empty:
                self.empty.append(pos)  # Add position to free spots

    # Move and select agents ------------------------------------------------ #

    @staticmethod
    def _border_behavior(position, shape, torus):
                
        # Connected - Jump to other side
        if torus:
            new_position = tuple(x % x_max for x, x_max
                                 in zip(position, shape))

        # Not connected - Stop at border
        else:
            new_position = tuple(np.clip(position, 0, 
                                         np.array(shape)-1))
                    
        return new_position

    def move_to(self, agent, pos):
        """ Moves agent to new position.

        Arguments:
            agent (Agent): Instance of the agent.
            pos (tuple of int): New position of the agent.
        """

        pos_old = self.positions[agent]
        if pos != pos_old:

            # Grid options
            if self._check_border:
                pos = self._border_behavior(pos, self.shape, self._torus)
            if self._track_empty:
                if len(self.grid.agents[pos_old]) == 1:
                    if pos in self.empty:
                        self.empty.replace(pos, pos_old)
                    else:
                        self.empty.append(pos_old)
                elif pos in self.empty:
                    self.empty.remove(pos)

            self.grid.agents[pos_old].remove(agent)
            self.grid.agents[pos].add(agent)
            self.positions[agent] = pos

    def move_by(self, agent, path):
        """ Moves agent to new position, relative to current position.

        Arguments:
            agent (Agent): Instance of the agent.
            path (tuple of int): Relative change of position.
        """
        pos = [p + c for p, c in zip(self.positions[agent], path)]
        self.move_to(agent, tuple(pos))

    def neighbors(self, agent, distance=1):
        """ Select neighbors of an agent within a given distance.

        Arguments:
            agent (Agent): Instance of the agent.
            distance (int, optional):
                Number of cells to cover in each direction,
                including diagonally connected cells (default 1).

        Returns:
            AgentIter: Iterator over the selected neighbors.
        """

        pos = self.positions[agent]

        # TODO Change method upon initiation
        # Case 1: Toroidal
        if self._torus:
            slices = [(p-distance, p+distance+1) for p in pos]
            new_slices = []
            for (x_from, x_to), x_max in zip(slices, self.shape):
                if distance >= x_max//2 :
                    sl_tupl = [(0, x_max)]
                elif x_to > x_max:
                    sl_tupl = [(x_from, x_max), (0, x_to - x_max)]
                elif x_from < 0:
                    sl_tupl = [(x_max + x_from, x_max), (0, x_to)]
                else:
                    sl_tupl = [(x_from, x_to)]
                new_slices.append(sl_tupl)
            areas = []
            for slices in itertools.product(*new_slices):
                slices = tuple(slice(*sl) for sl in slices)
                areas.append(self.grid.agents[slices])
            # TODO Exclude in every area inefficient
            area_iters = [_IterArea(area, exclude=agent) for area in areas]
            # TODO Can only be iterated on once
            return AgentIter(self.model,
                             itertools.chain.from_iterable(area_iters))

        # Case 2: Non-toroidal
        else:
            slices = tuple(slice(p-distance if p-distance >= 0 else 0,
                                  p+distance+1) for p in pos)
            area = self.grid.agents[slices]
            # Iterator over all agents in area, exclude original agent
            return AgentIter(self.model, _IterArea(area, exclude=agent))

    # Fields and attributes ------------------------------------------------- #

    def apply(self, func, field='agents'):
        """ Applies a function to each grid position,
        end returns an `numpy.ndarray` of return values.

        Arguments:
            func (function): Function that takes cell content as input.
            field (str, optional): Field to use (default 'agents').
        """
        return np.vectorize(func)(self.grid[field])

    def attr_grid(self, attr_key, otypes='f', field='agents'):
        """ Returns a grid with the value of the attribute of the agent
        in each position, using :class:`numpy.vectorize`.
        Positions with no agent will contain `numpy.nan`.
        Should only be used for grids with zero or one agents per cell.
        Other kinds of attribute grids can be created with :func:`Grid.apply`.

        Arguments:
            attr_key (str): Name of the attribute.
            otypes (str or list of dtypes, optional):
                Data type of returned grid (default float).
                For more information, see :class:`numpy.vectorize`.
            field (str, optional): Field to use (default 'agents').
        """

        f = np.vectorize(
            lambda x: getattr(next(iter(x)), attr_key) if x else np.nan,
            otypes=otypes)
        return f(self.grid[field])

    def add_field(self, key, values=None):
        """
        Add an attribute field to the grid.

        Arguments:
            key (str):
                Name of the field.
            values (optional):
                Single value or :class:`numpy.ndarray`
                of values (default None).
        """

        if not isinstance(values, (np.ndarray, list)):
            values = np.full(np.product(self.shape), fill_value=values)
        if len(values.shape) > 1:
            values = values.reshape(-1)

        # Create attribute as a numpy field
        self.grid = rfs.append_fields(
            self.grid, key, values, usemask=False, asrecarray=True
            ).reshape(self.grid.shape)

        # Create attribute as reference to field
        setattr(self, key, self.grid[key])

    def del_field(self, key):
        """
        Delete a attribute field from the grid.

        Arguments:
            key (str): Name of the field.
        """

        self.grid = rfs.drop_fields(
            self.grid, key, usemask=False, asrecarray=True)
        delattr(self, key)


================================================
FILE: agentpy/model.py
================================================
"""
Agentpy Model Module
Content: Main class for agent-based models
"""

import numpy as np
import pandas as pd
import random

from os import sys
from datetime import datetime
from .version import __version__
from .datadict import DataDict
from .objects import Object
from .network import Network
from .sample import Range, Values
from .grid import Grid
from .space import Space
from .tools import AttrDict, AgentpyError, make_list, InfoStr
from .sequences import AgentList


class Model(Object):
    """
    Template of an agent-based model.

    Arguments:
        parameters (dict, optional):
            Dictionary of the model's parameters.
            Default values will be selected from entries of type
            :class:`Range`, :class:`IntRange`, and :class:`Values`.
            The following parameters will be used automatically:

            - steps (int, optional):
              Defines the maximum number of time-steps.
              If none is passed, there will be no step limit.
            - seed (int, optional):
              Used to initiate the model's random number generators.
              If none is passed, a random seed will be generated.
            - report_seed (bool, optional):
              Whether to document the random seed used (default True).

        **kwargs: Will be forwarded to :func:`Model.setup`.

    Attributes:
        type (str): The model's class name.
        info (InfoStr): Information about the model's current state.
        p (AttrDict): The model's parameters.
        t (int): Current time-step of the model.
        id (int): The model's object id, which will always be zero.
        random (random.Random): Random number generator.
        nprandom (numpy.random.Generator): Numpy random number generator.
        var_keys (list): Names of the model's custom variables.
        running (bool): Indicates whether the model is currently running.
        log (dict): The model's recorded variables.
        reporters (dict): The model's documented reporters.
        output (DataDict): Output data after a completed simulation.

    Examples:

        To define a custom model with a custom agent type::

            class MyAgent(ap.Agent):

                def setup(self):
                    # Initialize an attribute with a parameter
                    self.my_attribute = self.p.my_parameter

                def agent_method(self):
                    # Define custom actions here
                    pass

            class MyModel(ap.Model):

                def setup(self):
                    # Called at the start of the simulation
                    self.agents = ap.AgentList(self, self.p.agents, MyAgent)

                def step(self):
                    # Called at every simulation step
                    self.agents.agent_method()  # Call a method for every agent

                def update(self):
                    # Called after setup as well as after each step
                    self.agents.record('my_attribute')  # Record variable

                def end(self):
                    # Called at the end of the simulation
                    self.report('my_reporter', 1)  # Report a simulation result

        To run a simulation::

            parameters = {
                'my_parameter': 42,
                'agents': 10,
                'steps': 10  # Used automatically to define simulation length
            }

            model = MyModel(parameters)
            results = model.run()
    """

    def __init__(self, parameters=None, _run_id=None, **kwargs):

        # Prepare parameters
        self.p = AttrDict()
        if parameters:
            for k, v in parameters.items():
                if isinstance(v, (Range, Values)):
                    v = v.vdef
                self.p[k] = v

        # Iniate model as model object with id 0
        self._id_counter = -1
        super().__init__(self)

        # Simulation attributes
        self.t = 0
        self.running = False
        self._run_id = _run_id

        # Random number generators
        # Can be re-initiated with seed by Model.run()
        self.random = random.Random()
        self.nprandom = np.random.default_rng()

        # Recording
        self._logs = {}
        self.reporters = {}
        self.output = DataDict()
        self.output.info = {
            'model_type': self.type,
            'time_stamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            'agentpy_version': __version__,
            'python_version': sys.version[:5],
            'experiment': False,
            'completed': False
        }

        # Private variables
        self._steps = None
        self._partly_run = False
        self._setup_kwargs = kwargs
        self._set_var_ignore()

    def __repr__(self):
        return self.type

    # Class Methods --------------------------------------------------------- #

    @classmethod
    def as_function(cls, **kwargs):
        """ Converts the model into a function that can be used with the
        `ema_workbench <https://emaworkbench.readthedocs.io/>`_ library.

        Arguments:
            **kwargs: Additional keyword arguments that will passed
                to the model in addition to the parameters.

        Returns:
            function:
                The model as a function that takes
                parameter values as keyword arguments and
                returns a dictionary of reporters.
        """

        superkwargs = kwargs

        def agentpy_model_as_function(**kwargs):
            model = cls(kwargs, **superkwargs)
            model.run(display=False)
            return model.reporters

        agentpy_model_as_function.__doc__ = f"""
        Performs a simulation of the model '{cls.__name__}'.
        
        Arguments:
            **kwargs: Keyword arguments with parameter values.

        Returns:
            dict: Reporters of the model.
        """

        return agentpy_model_as_function

    # Properties ------------------------------------------------------------ #

    @property
    def info(self):
        rep = f"Agent-based model {{"
        items = list(self.__dict__.items())
        for k, v in items:
            if k[0] != '_':
                v = v._short_repr() if '_short_repr' in dir(v) else v
                rep += f"\n'{k}': {v}"
        rep += '\n}'
        return InfoStr(rep)

    # Handling object ids --------------------------------------------------- #

    def _new_id(self):
        """ Returns a new unique object id (int). """
        self._id_counter += 1
        return self._id_counter

    # Recording ------------------------------------------------------------- #

    def report(self, rep_keys, value=None):
        """ Reports a new simulation result.
        Reporters are meant to be 'summary statistics' or 'evaluation measures'
        of the simulation as a whole, and only one value can be stored per run.
        In comparison, variables that are recorded with :func:`Model.record`
        can be recorded multiple times for each time-step and object.

        Arguments:
            rep_keys (str or list of str):
                Name(s) of the reporter(s) to be documented.
            value (int or float, optional): Value to be reported.
                The same value will be used for all `rep_keys`.
                If none is given, the values of object attributes
                with the same name as each rep_key will be used.

        Examples:

            Store a reporter `x` with a value `42`::

                model.report('x', 42)

            Define a custom model that stores a reporter `sum_id`
            with the sum of all agent ids at the end of the simulation::

                class MyModel(ap.Model):
                    def setup(self):
                        agents = ap.AgentList(self, self.p.agents)
                    def end(self):
                        self.report('sum_id', sum(self.agents.id))

            Running an experiment over different numbers of agents for this
            model yields the following datadict of reporters::

                >>> sample = ap.sample({'agents': (1, 3)}, 3)
                >>> exp = ap.Experiment(MyModel, sample)
                >>> results = exp.run()
                >>> results.reporters
                        sum_id
                run_id
                0            1
                1            3
                2            6
        """
        for rep_key in make_list(rep_keys):
            if value is not None:
                self.reporters[rep_key] = value
            else:
                self.reporters[rep_key] = getattr(self, rep_key)

    # Placeholder methods for custom simulation methods --------------------- #

    def setup(self):
        """ Defines the model's actions before the first simulation step.
        Can be overwritten to initiate agents and environments."""
        pass

    def step(self):
        """ Defines the model's actions
        during each simulation step (excluding `t==0`).
        Can be overwritten to define the models' main dynamics."""
        pass

    def update(self):
        """ Defines the model's actions
        after each simulation step (including `t==0`).
        Can be overwritten for the recording of dynamic variables. """
        pass

    def end(self):
        """ Defines the model's actions after the last simulation step.
        Can be overwritten for final calculations and reporting."""
        pass

    # Simulation routines (in line with ipysimulate) ------------------------ #

    def set_parameters(self, parameters):
        """ Adds and/or updates the parameters of the model. """
        self.p.update(parameters)

    def sim_setup(self, steps=None, seed=None):
        """ Prepares time-step 0 of the simulation.
        Initiates (additional) steps and the two random number generators,
        and then calls :func:`Model.setup` and :func:`Model.update`. """

        # Prepare random number generators if initial run
        if self._partly_run is False:
            if seed is None:
                if 'seed' in self.p:
                    seed = self.p['seed']  # Take seed from parameters
                else:
                    seed = random.getrandbits(128)
            if not ('report_seed' in self.p and not self.p['report_seed']):
                self.report('seed', seed)
            self.random = random.Random(seed)
            npseed = self.random.getrandbits(128)
            self.nprandom = np.random.default_rng(seed=npseed)

        # Prepare simulation steps
        if steps is None:
            self._steps = self.p['steps'] if 'steps' in self.p else np.nan
        else:
            self._steps = self.t + steps

        # Initiate simulation
        self.running = True
        self._partly_run = True

        # Execute setup and first update
        self.setup(**self._setup_kwargs)
        self.update()

        # Stop simulation if t too high
        if self.t >= self._steps:
            self.running = False

    def sim_step(self):
        """ Proceeds the simulation by one step, incrementing `Model.t` by 1
        and then calling :func:`Model.step` and :func:`Model.update`."""
        self.t += 1
        self.step()
        self.update()
        if self.t >= self._steps:
            self.running = False

    def sim_reset(self):
        """ Reset model to initial conditions. """
        # TODO Remove attributes
        self.record = super().record
        self.__init__(parameters=self.p,
                      _run_id=self._run_id,
                      **self._setup_kwargs)

    # Main simulation method for direct use --------------------------------- #

    def stop(self):
        """ Stops :meth:`Model.run` during an active simulation. """
        self.running = False

    def run(self, steps=None, seed=None, display=True):
        """ Executes the simulation of the model.
        Can also be used to continue a partly-run simulation
        for a given number of additional steps.

        It starts by calling :func:`Model.run_setup` and then calls
        :func:`Model.run_step` until the method :func:`Model.stop` is called
        or `steps` is reached. After that, :func:`Model.end` and
        :func:`Model.create_output` are called. The simulation results can
        be found in :attr:`Model.output`.

        Arguments:
            steps (int, optional):
                Number of (additional) steps for the simulation to run.
                If passed, the parameter 'Model.p.steps' will be ignored.
                The simulation can still be stopped with :func:'Model.stop'.
            seed (int, optional):
                Seed to initialize the model's random number generators.
                If none is given, the parameter 'Model.p.seed' is used.
                If there is no such parameter, a random seed will be used.
                For a partly-run simulation, this argument will be ignored.
            display (bool, optional):
                Whether to display simulation progress (default True).

        Returns:
            DataDict: Recorded variables and reporters.

        """

        dt0 = datetime.now()
        self.sim_setup(steps, seed)
        while self.running:
            self.sim_step()
            if display:
                print(f"\rCompleted: {self.t} steps", end='')
        self.end()
        self.create_output()

        self.output.info['completed'] = True
        self.output.info['created_objects'] = self._id_counter
        self.output.info['completed_steps'] = self.t
        self.output.info['run_time'] = ct = str(datetime.now() - dt0)

        if display:
            print(f"\nRun time: {ct}\nSimulation finished")

        return self.output

    # Data management ------------------------------------------------------- #

    def create_output(self):
        """ Generates a :class:`DataDict` with dataframes of all recorded
        variables and reporters, which will be stored in :obj:`Model.output`.
        """

        def output_from_obj_list(self, log_dict, columns):
            # Aggregate logs per object type
            # Log dict structure: {obj_type: obj_id: log}
            obj_types = {}
            for obj_type, log_subdict in log_dict.items():

                if obj_type not in obj_types.keys():
                    obj_types[obj_type] = {}

                for obj_id, log in log_subdict.items():

                    # Add object id/key to object log
                    log['obj_id'] = [obj_id] * len(log['t'])

                    # Add object log to aggregate log
                    for k, v in log.items():
                        if k not in obj_types[obj_type]:
                            obj_types[obj_type][k] = []
                        obj_types[obj_type][k].extend(v)

            # Transform logs into dataframes
            for obj_type, log in obj_types.items():
                if obj_type == self.type:
                    del log['obj_id']
                    index_keys = ['t']
                else:
                    index_keys = ['obj_id', 't']
                df = pd.DataFrame(log)
                for k, v in columns.items():
                    df[k] = v  # Set additional index columns
                df = df.set_index(list(columns.keys()) + index_keys)
                self.output['variables'][obj_type] = df

        # 1 - Document parameters
        if self.p:
            self.output['parameters'] = DataDict()
            self.output['parameters']['constants'] = self.p.copy()

        # 2 - Define additional index columns
        columns = {}
        if self._run_id is not None:
            if self._run_id[0] is not None:
                columns['sample_id'] = self._run_id[0]
            if len(self._run_id) > 1 and self._run_id[1] is not None:
                columns['iteration'] = self._run_id[1]

        # 3 - Create variable output
        if self._logs:
            self.output['variables'] = DataDict()
            output_from_obj_list(self, self._logs, columns)

        # 4 - Create reporters output
        if self.reporters:
            d = {k: [v] for k, v in self.reporters.items()}
            for key, value in columns.items():
                d[key] = value
            df = pd.DataFrame(d)
            if columns:
                df = df.set_index(list(columns.keys()))
            self.output['reporters'] = df




================================================
FILE: agentpy/network.py
================================================
""" Agentpy Network Module """

import itertools
import networkx as nx
from .objects import Object
from .sequences import AgentList, AgentIter, AttrIter
from .tools import make_list


class AgentNode(set):
    """ Node of :class:`Network`. Functions like a set of agents. """

    # TODO Connector between AgentNode attributes and the networkx attr dict

    def __init__(self, label):
        self.label = label

    def __hash__(self):
        return id(self)

    def __repr__(self):
        return f"AgentNode ({self.label})"


class Network(Object):
    """ Agent environment with a graph topology.
    Every node of the network is a :class:`AgentNode` that can hold
    multiple agents as well as node attributes.

    This class can be used as a parent class for custom network types.
    All agentpy model objects call the method :func:`setup` after creation,
    and can access class attributes like dictionary items.

    Arguments:
        model (Model): The model instance.
        graph (networkx.Graph, optional): The environments' graph.
            Can also be a DiGraph, MultiGraph, or MultiDiGraph.
            Nodes will be converted to :class:`AgentNode`,
            with their original label being kept as `AgentNode.label`.
            If none is passed, an empty :class:`networkx.Graph` is created.
        **kwargs: Will be forwarded to :func:`Network.setup`.

    Attributes:
        graph (networkx.Graph): The network's graph instance.
        agents (AgentIter): Iterator over the network's agents.
        nodes (AttrIter): Iterator over the network's nodes.
    """

    def __init__(self, model, graph=None, **kwargs):

        super().__init__(model)
        self._i = -1  # Node label counter
        self.positions = {}  # Agent Instance : Node reference

        if graph is None:
            self.graph = nx.Graph()
        else:
            nodes = graph.nodes
            self._i = len(nodes)
            mapping = {i: AgentNode(label=i) for i in nodes}
            self.graph = nx.relabel_nodes(graph, mapping=mapping)

        self._set_var_ignore()
        self.setup(**kwargs)

    @property
    def agents(self):
        return AgentIter(self.model, self.positions.keys())

    @property
    def nodes(self):
        return AttrIter(self.graph.nodes)

    # Add and remove nodes -------------------------------------------------- #

    def add_node(self, label=None):
        """ Adds a new node to the network.

        Arguments:
            label (int or string, optional): Unique name of the node,
                which must be different from all other nodes.
                If none is passed, an integer number will be chosen.

        Returns:
            AgentNode: The newly created node.
        """
        self._i += 1
        if label is None:
            label = self._i
        node = AgentNode(label=label)
        self.graph.add_node(node)
        return node

    def remove_node(self, node):
        """ Removes a node from the network.

        Arguments:
            node (AgentNode): Node to be removed.
        """
        self.remove_agents(node)
        self.graph.remove_node(node)

    # Add and remove agents ------------------------------------------------- #

    def add_agents(self, agents, positions=None):
        """ Adds agents to the network environment.

        Arguments:
            agents (Sequence of Agent):
                Instance or iterable of agents to be added.
            positions (Sequence of AgentNode, optional):
                The positions of the agents.
                Must have the same length as 'agents',
                with each entry being an :class:`AgentNode` of the network.
                If none is passed, new nodes will be created for each agent.
        """

        if positions is None:
            for agent in agents:
                node = self.add_node()
                node.add(agent)
                self.positions[agent] = node
        else:
            for agent, node in zip(agents, positions):
                node.add(agent)
                self.positions[agent] = node

    def remove_agents(self, agents):
        """ Removes agents from the network. """
        for agent in make_list(agents):
            self.positions[agent].remove(agent)
            del self.positions[agent]

    # Move and select agents ------------------------------------------------ #

    def move_to(self, agent, node):
        """ Moves agent to new position.

        Arguments:
            agent (Agent): Instance of the agent.
            node (AgentNode): New position of the agent.
        """

        node.add(agent)
        self.positions[agent].remove(agent)
        self.positions[agent] = node

    def neighbors(self, agent):
        """ Select agents from neighboring nodes.
        Does not include other agents from the agents' own node.

        Arguments:
            agent (Agent): Instance of the agent.

        Returns:
            AgentIter: Iterator over the selected neighbors.
        """

        # TODO Improve
        nodes = self.graph.neighbors(self.positions[agent])
        return AgentIter(self.model, itertools.chain.from_iterable(nodes))


================================================
FILE: agentpy/objects.py
================================================
"""
Agentpy Objects Module
Content: Base classes for agents and environment
"""

from .sequences import AgentList
from .tools import AgentpyError, make_list


class Object:
    """ Base class for all objects of an agent-based models. """

    def __init__(self, model):
        self._var_ignore = []

        self.id = model._new_id()  # Assign id to new object
        self.type = type(self).__name__
        self.log = {}

        self.model = model
        self.p = model.p

    def __repr__(self):
        return f"{self.type} (Obj {self.id})"

    def __getattr__(self, key):
        raise AttributeError(f"No attribute '{key}'.")

    def __getitem__(self, key):
        return getattr(self, key)

    def __setitem__(self, key, value):
        setattr(self, key, value)

    def _set_var_ignore(self):
        """Store current attributes to separate them from custom variables"""
        self._var_ignore = [k for k in self.__dict__.keys() if k[0] != '_']

    @property
    def vars(self):
        return [k for k in self.__dict__.keys()
                if k[0] != '_'
                and k not in self._var_ignore]

    def record(self, var_keys, value=None):
        """ Records an object's variables at the current time-step.
        Recorded variables can be accessed via the object's `log` attribute
        and will be saved to the model's output at the end of a simulation.

        Arguments:
            var_keys (str or list of str):
                Names of the variables to be recorded.
            value (optional): Value to be recorded.
                The same value will be used for all `var_keys`.
                If none is given, the values of object attributes
                with the same name as each var_key will be used.

        Notes:
            Recording mutable objects like lists can lead to wrong results
            if the object's content will be changed during the simulation.
            Make a copy of the list or record each list entry seperately.

        Examples:

            Record the existing attributes `x` and `y` of an object `a`::

                a.record(['x', 'y'])

            Record a variable `z` with the value `1` for an object `a`::

                a.record('z', 1)

            Record all variables of an object::

                a.record(a.vars)
        """

        # Initial record call

        # Connect log to the model's dict of logs
        if self.type not in self.model._logs:
            self.model._logs[self.type] = {}
        self.model._logs[self.type][self.id] = self.log
        self.log['t'] = [self.model.t]  # Initiate time dimension

        # Perform initial recording
        for var_key in make_list(var_keys):
            v = getattr(self, var_key) if value is None else value
            self.log[var_key] = [v]

        # Set default recording function from now on
        self.record = self._record  # noqa

    def _record(self, var_keys, value=None):

        for var_key in make_list(var_keys):

            # Create empty lists
            if var_key not in self.log:
                self.log[var_key] = [None] * len(self.log['t'])

            if self.model.t != self.log['t'][-1]:

                # Create empty slot for new documented time step
                for v in self.log.values():
                    v.append(None)

                # Store time step
                self.log['t'][-1] = self.model.t

            if value is None:
                v = getattr(self, var_key)
            else:
                v = value

            self.log[var_key][-1] = v

    def setup(self, **kwargs):
        """This empty method is called automatically at the objects' creation.
        Can be overwritten in custom sub-classes
        to define initial attributes and actions.

        Arguments:
            **kwargs: Keyword arguments that have been passed to
                :class:`Agent` or :func:`Model.add_agents`.
                If the original setup method is used,
                they will be set as attributes of the object.

        Examples:
            The following setup initializes an object with three variables::

                def setup(self, y):
                    self.x = 0  # Value defined locally
                    self.y = y  # Value defined in kwargs
                    self.z = self.p.z  # Value defined in parameters
        """

        for k, v in kwargs.items():
            setattr(self, k, v)


class SpatialEnvironment(Object):

    def record_positions(self, label='p'):
        """ Records the positions of each agent.

        Arguments:
            label (string, optional):
                Name under which to record each position (default p).
                A number will be added for each coordinate (e.g. p1, p2, ...).
        """
        for agent, pos in self.positions.items():
            for i, p in enumerate(pos):
                agent.record(label+str(i), p)


================================================
FILE: agentpy/sample.py
================================================
"""
Agentpy Sampling Module
Content: Sampling functions
"""

# TODO Latin Hypercube
# TODO Random distribution samples
# TODO Store meta-info for later analysis

import itertools
import random
import numpy as np

from SALib.sample import saltelli
from .tools import param_tuples_to_salib, InfoStr, AgentpyError


class Range:
    """ A range of parameter values
    that can be used to create a :class:`Sample`.

    Arguments:
        vmin (float, optional):
            Minimum value for this parameter (default 0).
        vmax (float, optional):
            Maximum value for this parameter (default 1).
        vdef (float, optional):
            Default value. Default value. If none is passed, `vmin` is used.
    """

    def __init__(self, vmin=0, vmax=1, vdef=None):
        self.vmin = vmin
        self.vmax = vmax
        self.vdef = vdef if vdef else vmin
        self.ints = False

    def __repr__(self):
        return f"Parameter range from {self.vmin} to {self.vmax}"


class IntRange(Range):
    """ A range of integer parameter values
    that can be used to create a :class:`Sample`.
    Similar to :class:`Range`,
    but sampled values will be rounded and converted to integer.

    Arguments:
        vmin (int, optional):
            Minimum value for this parameter (default 0).
        vmax (int, optional):
            Maximum value for this parameter (default 1).
        vdef (int, optional):
            Default value. If none is passed, `vmin` is used.
    """

    def __init__(self, vmin=0, vmax=1, vdef=None):
        self.vmin = int(round(vmin))
        self.vmax = int(round(vmax))
        self.vdef = int(round(vdef)) if vdef else vmin
        self.ints = True

    def __repr__(self):
        return f"Integer parameter range from {self.vmin} to {self.vmax}"


class Values:
    """ A pre-defined set of discrete parameter values
    that can be used to create a :class:`Sample`.

    Arguments:
        *args:
            Possible values for this parameter.
        vdef:
            Default value. If none is passed, the first passed value is used.
    """

    def __init__(self, *args, vdef=None):
        self.values = args
        self.vdef = vdef if vdef else args[0]

    def __len__(self):
        return len(self.values)

    def __repr__(self):
        return f"Set of {len(self.values)} parameter values"


class Sample:
    """ A sequence of parameter combinations
    that can be used for :class:`Experiment`.

    Arguments:

        parameters (dict):
            Dictionary of parameter keys and values.
            Entries of type :class:`Range` and :class:`Values`
            will be sampled based on chosen `method` and `n`.
            Other types wil be interpreted as constants.

        n (int, optional):
            Sampling factor used by chosen `method` (default None).

        method (str, optional):
            Method to use to create parameter combinations
            from entries of type :class:`Range`. Options are:

            - ``linspace`` (default):
              Arange `n` evenly spaced values for each :class:`Range`
              and combine them with given :class:`Values` and constants.
              Additional keyword arguments:

                - ``product`` (bool, optional):
                  Return all possible combinations (default True).
                  If False, value sets are 'zipped' so that the i-th
                  parameter combination contains the i-th entry of each
                  value set. Requires all value sets to have the same length.

            - ``saltelli``:
              Apply Saltelli's sampling scheme,
              using :func:`SALib.sample.saltelli.sample` with `N=n`.
              This enables the analysis of Sobol Sensitivity Indices
              with :func:`DataDict.calc_sobol` after the experiment.
              Additional keyword arguments:

                - ``calc_second_order`` (bool, optional):
                  Whether to calculate second-order indices (default True).

        randomize (bool, optional):
            Whether to use the constant parameter 'seed' to generate different
            random seeds for every parameter combination (default True).
            If False, every parameter combination will have the same seed.
            If there is no constant parameter 'seed',
            this option has no effect.

        **kwargs: Additional keyword arguments for chosen `method`.

    """

    def __init__(self, parameters, n=None,
                 method='linspace', randomize=True, **kwargs):

        self._log = {'type': method, 'n': n, 'randomized': False}
        self._sample = getattr(self, f"_{method}")(parameters, n, **kwargs)
        if 'seed' in parameters and randomize:
            ranges = (Range, IntRange, Values)
            if not isinstance(parameters['seed'], ranges):
                seed = parameters['seed']
                self._log['randomized'] = True
                self._log['seed'] = seed
                self._assign_random_seeds(seed)

    def __repr__(self):
        return f"Sample of {len(self)} parameter combinations"

    def __iter__(self):
        return iter(self._sample)

    def __len__(self):
        return len(self._sample)

    # Sampling methods ------------------------------------------------------ #

    def _assign_random_seeds(self, seed):
        rng = random.Random(seed)
        for parameters in self._sample:
            parameters['seed'] = rng.getrandbits(128)

    @staticmethod
    def _linspace(parameters, n, product=True):

        params = {}
        for k, v in parameters.items():
            if isinstance(v, Range):
                if n is None:
                    raise AgentpyError(
                        "Argument 'n' must be defined for Sample "
                        "if there are parameters of type Range.")
                if v.ints:
                    p_range = np.linspace(v.vmin, v.vmax+1, n)
                    p_range = [int(pv)-1 if pv == v.vmax+1 else int(pv)
                               for pv in p_range]
                else:
                    p_range = np.linspace(v.vmin, v.vmax, n)
                params[k] = p_range
            elif isinstance(v, Values):
                params[k] = v.values
            else:
                params[k] = [v]

        if product:
            # All possible combinations
            combos = list(itertools.product(*params.values()))
            sample = [{k: v for k, v in zip(params.keys(), c)} for c in combos]
        else:
            # Parallel combinations (index by index)
            r = range(min([len(v) for v in params.values()]))
            sample = [{k: v[i] for k, v in params.items()} for i in r]

        return sample

    def _saltelli(self, params, n, calc_second_order=True):

        # STEP 0 - Find variable parameters and check type
        param_ranges_tuples = {}
        for k, v in params.items():
            if isinstance(v, Range):
                if v.ints:
                    # Integer conversion rounds down, +1 includes last integer
                    param_ranges_tuples[k] = (v.vmin, v.vmax+1)
                else:
                    param_ranges_tuples[k] = (v.vmin, v.vmax)
            elif isinstance(v, Values):
                param_ranges_tuples[k] = (0, len(v))

        # STEP 1 - Convert param_ranges to SALib Format
        param_ranges_salib = param_tuples_to_salib(param_ranges_tuples)

        # STEP 2 - Create SALib Sample
        salib_sample = saltelli.sample(param_ranges_salib, n, calc_second_order)

        # STEP 3 - Convert back to Agentpy Parameter Dict List and adjust values
        ap_sample = []

        for param_instance in salib_sample:

            parameters = {}
            parameters.update(params)

            for i, key in enumerate(param_ranges_tuples.keys()):
                p = param_instance[i]

                # Convert to integer
                if isinstance(params[key], Range) and params[key].ints:
                    p = int(p) - 1 if p == params[key].vmax+1 else int(p)

                # Convert to value
                if isinstance(params[key], Values):
                    p = int(p) - 1 if p == len(params[key]) else int(p)
                    p = params[key].values[p]  # Find value
                parameters[key] = p

            ap_sample.append(parameters)

        # STEP 4 - Log
        self._log['salib_problem'] = param_ranges_salib
        self._log['calc_second_order'] = calc_second_order

        return ap_sample




================================================
FILE: agentpy/sequences.py
================================================
"""
Agentpy Lists Module
Content: Lists for objects, environments, and agents
"""

import itertools
import agentpy as ap
import numpy as np
from .tools import AgentpyError, ListDict
from collections.abc import Sequence


class AgentSequence:
    """ Base class for agenpty sequences. """

    def __repr__(self):
        len_ = len(list(self))
        s = 's' if len_ != 1 else ''
        return f"{type(self).__name__} ({len_} object{s})"

    def __getattr__(self, name):
        """ Return callable list of attributes """
        if name[0] == '_':  # Private variables are looked up normally
            # Gives numpy conversion correct error for __array_struct__ lookup
            super().__getattr__(name)
        else:
            return AttrIter(self, attr=name)

    def _set(self, key, value):
        object.__setattr__(self, key, value)

    @staticmethod
    def _obj_gen(model, n, cls, *args, **kwargs):
        """ Generate objects for sequence. """

        if cls is None:
            cls = ap.Agent

        if args != tuple():
            raise AgentpyError(
                "Sequences no longer accept extra arguments without a keyword."
                f" Please assign a keyword to the following arguments: {args}")

        for i in range(n):
            # AttrIter values get broadcasted among agents
            i_kwargs = {k: arg[i] if isinstance(arg, AttrIter) else arg
                        for k, arg in kwargs.items()}
            yield cls(model, **i_kwargs)


# Attribute List ------------------------------------------------------------ #

class AttrIter(AgentSequence, Sequence):
    """ Iterator over an attribute of objects in a sequence.
    Length, items access, and representation work like with a normal list.
    Calls are forwarded to each entry and return a list of return values.
    Boolean operators are applied to each entry and return a list of bools.
    Arithmetic operators are applied to each entry and return a new list.
    If applied to another `AttrList`, the first entry of the first list
    will be matched with the first entry of the second list, and so on.
    Else, the same value will be applied to each entry of the list.
    See :class:`AgentList` for examples.
    """

    def __init__(self, source, attr=None):
        self.source = source
        self.attr = attr

    def __repr__(self):
        return repr(list(self))

    @staticmethod
    def _iter_attr(a, s):
        for o in s:
            yield getattr(o, a)

    def __iter__(self):
        """ Iterate through source list based on attribute. """
        if self.attr:
            return self._iter_attr(self.attr, self.source)
        else:
            return iter(self.source)

    def __len__(self):
        return len(self.source)

    def __getitem__(self, key):
        """ Get item from source list. """
        if self.attr:
            return getattr(self.source[key], self.attr)
        else:
            return self.source[key]

    def __setitem__(self, key, value):
        """ Set item to source list. """
        if self.attr:
            setattr(self.source[key], self.attr, value)
        else:
            self.source[key] = value

    def __call__(self, *args, **kwargs):
        return AttrIter([func_obj(*args, **kwargs) for func_obj in self])

    def __eq__(self, other):
        return [obj == other for obj in self]

    def __ne__(self, other):
        return [obj != other for obj in self]

    def __lt__(self, other):
        return [obj < other for obj in self]

    def __le__(self, other):
        return [obj <= other for obj in self]

    def __gt__(self, other):
        return [obj > other for obj in self]

    def __ge__(self, other):
        return [obj >= other for obj in self]

    def __add__(self, v):
        if isinstance(v, AttrIter):
            return AttrIter([x + y for x, y in zip(self, v)])
        else:
            return AttrIter([x + v for x in self])

    def __sub__(self, v):
        if isinstance(v, AttrIter):
            return AttrIter([x - y for x, y in zip(self, v)])
        else:
            return AttrIter([x - v for x in self])

    def __mul__(self, v):
        if isinstance(v, AttrIter):
            return AttrIter([x * y for x, y in zip(self, v)])
        else:
            return AttrIter([x * v for x in self])

    def __truediv__(self, v):
        if isinstance(v, AttrIter):
            return AttrIter([x / y for x, y in zip(self, v)])
        else:
            return AttrIter([x / v for x in self])

    def __iadd__(self, v):
        return self + v

    def __isub__(self, v):
        return self - v

    def __imul__(self, v):
        return self * v

    def __itruediv__(self, v):
        return self / v


# Object Containers --------------------------------------------------------- #

def _random(model, gen, obj_list, n=1, replace=False):
    """ Creates a random sample of agents.

    Arguments:
        n (int, optional): Number of agents (default 1).
        replace (bool, optional):
            Select with replacement (default False).
            If True, the same agent can be selected more than once.

    Returns:
        AgentIter: The selected agents.
    """
    if n == 1:
        selection = [gen.choice(obj_list)]
    elif replace is False:
        selection = gen.sample(obj_list, k=n)
    else:
        selection = gen.choices(obj_list, k=n)
    return AgentIter(model, selection)


class AgentList(AgentSequence, list):
    """ List of agentpy objects.
    Attribute calls and assignments are applied to all agents
    and return an :class:`AttrIter` with the attributes of each agent.
    This also works for method calls, which returns a list of return values.
    Arithmetic operators can further be used to manipulate agent attributes,
    and boolean operators can be used to filter the list based on agents'
    attributes. Standard :class:`list` methods can also be used.

    Arguments:
        model (Model): The model instance.
        objs (int or Sequence, optional):
            An integer number of new objects to be created,
            or a sequence of existing objects (default empty).
        cls (type, optional): Class for the creation of new objects.
        **kwargs:
            Keyword arguments are forwarded
            to the constructor of the new objects.
            Keyword arguments with sequences of type :class:`AttrIter` will be
            broadcasted, meaning that the first value will be assigned
            to the first object, the second to the second, and so forth.
            Otherwise, the same value will be assigned to all objects.

    Examples:

        Prepare an :class:`AgentList` with three agents::

            >>> model = ap.Model()
            >>> agents = model.add_agents(3)
            >>> agents
            AgentList [3 agents]

        The assignment operator can be used to set a variable for each agent.
        When the variable is called, an :class:`AttrList` is returned::

            >>> agents.x = 1
            >>> agents.x
            AttrList of 'x': [1, 1, 1]

        One can also set different variables for each agent
        by passing another :class:`AttrList`::

            >>> agents.y = ap.AttrIter([1, 2, 3])
            >>> agents.y
            AttrList of 'y': [1, 2, 3]

        Arithmetic operators can be used in a similar way.
        If an :class:`AttrList` is passed, different values are used for
        each agent. Otherwise, the same value is used for all agents::

            >>> agents.x = agents.x + agents.y
            >>> agents.x
            AttrList of 'x': [2, 3, 4]

            >>> agents.x *= 2
            >>> agents.x
            AttrList of 'x': [4, 6, 8]

        Attributes of specific agents can be changed through setting items::

            >>> agents.x[2] = 10
            >>> agents.x
            AttrList of 'x': [4, 6, 10]

        Boolean operators can be used to select a subset of agents::

            >>> subset = agents(agents.x > 5)
            >>> subset
            AgentList [2 agents]

            >>> subset.x
            AttrList of attribute 'x': [6, 8]
    """

    def __init__(self, model, objs=(), cls=None, *args, **kwargs):
        if isinstance(objs, int):
            objs = self._obj_gen(model, objs, cls, *args, **kwargs)
        super().__init__(objs)
        super().__setattr__('model', model)
        super().__setattr__('ndim', 1)

    def __setattr__(self, name, value):
        if isinstance(value, AttrIter):
            # Apply each value to each agent
            for obj, v in zip(self, value):
                setattr(obj, name, v)
        else:
            # Apply single value to all agents
            for obj in self:
                setattr(obj, name, value)

    def __add__(self, other):
        agents = AgentList(self.model, self)
        agents.extend(other)
        return agents

    def select(self, selection):
        """ Returns a new :class:`AgentList` based on `selection`.

        Arguments:
            selection (list of bool): List with same length as the agent list.
                Positions that return True will be selected.
        """
        return AgentList(self.model, [a for a, s in zip(self, selection) if s])

    def random(self, n=1, replace=False):
        """ Creates a random sample of agents.

        Arguments:
            n (int, optional): Number of agents (default 1).
            replace (bool, optional):
                Select with replacement (default False).
                If True, the same agent can be selected more than once.

        Returns:
            AgentIter: The selected agents.
        """
        return _random(self.model, self.model.random, self, n, replace)

    def sort(self, var_key, reverse=False):
        """ Sorts the list in-place, and returns self.

        Arguments:
            var_key (str): Attribute of the lists' objects, based on which
                the list will be sorted from lowest value to highest.
            reverse (bool, optional): Reverse sorting (default False).
        """
        super().sort(key=lambda x: x[var_key], reverse=reverse)
        return self

    def shuffle(self):
        """ Shuffles the list in-place, and returns self. """
        self.model.random.shuffle(self)
        return self


class AgentDList(AgentSequence, ListDict):
    """ Ordered collection of agentpy objects.
    This container behaves similar to :class:`AgentList` in most aspects,
    but comes with additional features for object removal and lookup.

    The key differences to :class:`AgentList` are the following:

    - Faster removal of objects.
    - Faster lookup if object is part of group.
    - No duplicates are allowed.
    - The order of agents in the group cannot be changed.
    - Removal of agents changes the order of the group.
    - :func:`AgentDList.buffer` makes it possible to
      remove objects from the group while iterating over the group.
    - :func:`AgentDList.shuffle` returns an iterator
      instead of shuffling in-place.

    Arguments:
        model (Model): The model instance.
        objs (int or Sequence, optional):
            An integer number of new objects to be created,
            or a sequence of existing objects (default empty).
        cls (type, optional): Class for the creation of new objects.
        **kwargs:
            Keyword arguments are forwarded
            to the constructor of the new objects.
            Keyword arguments with sequences of type :class:`AttrIter` will be
            broadcasted, meaning that the first value will be assigned
            to the first object, the second to the second, and so forth.
            Otherwise, the same value will be assigned to all objects.

    """

    def __init__(self, model, objs=(), cls=None, *args, **kwargs):
        if isinstance(objs, int):
            objs = self._obj_gen(model, objs, cls, *args, **kwargs)

        self._set('model', model)
        self._set('ndim', 1)
        self._set('items', [])
        self._set('item_to_position', {})

        self.model = model
        self.item_to_position = {}
        self.items = []
        for obj in objs:
            self.append(obj)

    def __setattr__(self, name, value):
        if isinstance(value, AttrIter):
            # Apply each value to each agent
            for obj, v in zip(self, value):
                setattr(obj, name, v)
        else:
            # Apply single value to all agents
            for obj in self:
                setattr(obj, name, value)

    def __add__(self, other):
        agents = AgentDList(self.model, self)
        agents.extend(other)
        return agents

    def random(self, n=1, replace=False):
        """ Creates a random sample of agents.

        Arguments:
            n (int, optional): Number of agents (default 1).
            replace (bool, optional):
                Select with replacement (default False).
                If True, the same agent can be selected more than once.

        Returns:
            AgentIter: The selected agents.
        """
        return _random(self.model, self.model.random, self.items, n, replace)

    def select(self, selection):
        """ Returns a new :class:`AgentList` based on `selection`.

        Arguments:
            selection (list of bool): List with same length as the agent list.
                Positions that return True will be selected.
        """
        return AgentList(
            self.model, [a for a, s in zip(self.items, selection) if s])

    def sort(self, var_key, reverse=False):
        """ Returns a new sorted :class:`AgentList`.

        Arguments:
            var_key (str): Attribute of the lists' objects, based on which
                the list will be sorted from lowest value to highest.
            reverse (bool, optional): Reverse sorting (default False).
        """
        agentlist = AgentList(self.model, self)
        agentlist.sort(var_key=var_key, reverse=reverse)
        return agentlist

    def shuffle(self):
        """ Return :class:`AgentIter` over the content of the group
         with the order of objects being shuffled. """
        return AgentDListIter(self.model, self, shuffle=True)

    def buffer(self):
        """ Return :class:`AgentIter` over the content of the group
         that supports deletion of objects from the group during iteration. """
        return AgentDListIter(self.model, self, buffer=True)


class AgentSet(AgentSequence, set):
    """ Unordered collection of agentpy objects.

    Arguments:
        model (Model): The model instance.
        objs (int or Sequence, optional):
            An integer number of new objects to be created,
            or a sequence of existing objects (default empty).
        cls (type, optional): Class for the creation of new objects.
        **kwargs:
            Keyword arguments are forwarded
            to the constructor of the new objects.
            Keyword arguments with sequences of type :class:`AttrIter` will be
            broadcasted, meaning that the first value will be assigned
            to the first object, the second to the second, and so forth.
            Otherwise, the same value will be assigned to all objects.
    """

    def __init__(self, model, objs=(), cls=None, *args, **kwargs):
        if isinstance(objs, int):
            objs = self._obj_gen(model, objs, cls, *args, **kwargs)
        super().__init__(objs)
        super().__setattr__('model', model)
        super().__setattr__('ndim', 1)


class AgentIter(AgentSequence):
    """ Iterator over agentpy objects. """

    def __init__(self, model, source=()):
        object.__setattr__(self, '_model', model)
        object.__setattr__(self, '_source', source)

    def __getitem__(self, item):
        raise AgentpyError(
            'AgentIter has to be converted to list for item lookup.')

    def __iter__(self):
        return iter(self._source)

    def __len__(self):
        return len(self._source)

    def __setattr__(self, name, value):
        if isinstance(value, AttrIter):
            # Apply each value to each agent
            for obj, v in zip(self, value):
                setattr(obj, name, v)
        else:
            # Apply single value to all agents
            for obj in self:
                setattr(obj, name, value)

    def to_list(self):
        """Returns an :class:`AgentList` of the iterator. """
        return AgentList(self._model, self)

    def to_dlist(self):
        """Returns an :class:`AgentDList` of the iterator. """
        return AgentDList(self._model, self)


class AgentDListIter(AgentIter):
    """ Iterator over agentpy objects in an :class:`AgentDList`. """

    def __init__(self, model, source=(), shuffle=False, buffer=False):
        object.__setattr__(self, '_model', model)
        object.__setattr__(self, '_source', source)
        object.__setattr__(self, '_shuffle', shuffle)
        object.__setattr__(self, '_buffer', buffer)

    def __iter__(self):
        if self._buffer:
            return self._buffered_iter()
        elif self._shuffle:
            items = self._source.items.copy()
            self._model.random.shuffle(items)
            return iter(items)
        else:
            return iter(self._source)

    def buffer(self):
        object.__setattr__(self, '_buffer', True)
        return self

    def shuffle(self):
        object.__setattr__(self, '_shuffle', True)
        return self

    def _buffered_iter(self):
        """ Iterate over source. """
        items = self._source.items.copy()
        if self._shuffle:
            self._model.random.shuffle(items)
        for a in items:
            if a in self._source:
                yield a


================================================
FILE: agentpy/space.py
================================================
"""
Agentpy Space Module
Content: Class for continuous spatial environments
"""

# TODO Add option of space without shape (infinite)
# TODO Custom iterator for neighbors() & select() for performance

import itertools
import numpy as np
import random as rd
import collections.abc as abc
from scipy import spatial
from .objects import SpatialEnvironment
from .tools import make_list, make_matrix
from .sequences import AgentList, AgentIter


class Space(SpatialEnvironment):
    """ Environment that contains agents with a continuous spatial topology.
    To add new space environments to a model, use :func:`Model.add_space`.
    For a discrete spatial topology, see :class:`Grid`.

    This class can be used as a parent class for custom space types.
    All agentpy model objects call the method :func:`setup` after creation,
    and can access class attributes like dictionary items.

    Arguments:
        model (Model): The model instance.
        shape (tuple of float): Size of the space.
            The length of the tuple defines the number of dimensions,
            and the values in the tuple define the length of each dimension.
        torus (bool, optional):
            Whether to connect borders (default False).
            If True, the space will be toroidal, meaning that agents who
            move over a border will re-appear on the opposite side.
            If False, they will remain at the edge of the border.
        **kwargs: Will be forwarded to :func:`Space.setup`.

    Attributes:
        agents (AgentIter):
            Iterator over all agents in the space.
        positions (dict of Agent):
            Dictionary linking each agent instance to its position.
        shape (tuple of float):
            Length of each spatial dimension.
        ndim (int):
            Number of dimensions.
        kdtree (scipy.spatial.cKDTree or None):
            KDTree of agent positions for neighbor lookup.
            Will be recalculated if agents have moved.
            If there are no agents, tree is None.
    """

    def __init__(self, model, shape, torus=False, **kwargs):

        super().__init__(model)

        self._torus = torus
        self._cKDTree = None
        self._sorted_agents = None
        self._sorted_agent_points = None

        self.positions = {}
        self.shape = tuple(shape)
        self.ndim = len(self.shape)

        self._set_var_ignore()
        self.setup(**kwargs)

    @property
    def agents(self):
        return AgentIter(self.model, self.positions.keys())

    @property
    def kdtree(self):
        # Create new KDTree if necessary
        if self._cKDTree is None and len(self.agents) > 0:
            self._sorted_agents = []
            self._sorted_agent_points = []
            for a in self.agents:
                self._sorted_agents.append(a)
                self._sorted_agent_points.append(self.positions[a])
            if self._torus:
                self._cKDTree = spatial.cKDTree(self._sorted_agent_points,
                                                boxsize=self.shape)
            else:
                self._cKDTree = spatial.cKDTree(self._sorted_agent_points)
        return self._cKDTree  # Return existing or new KDTree

    # Add and remove agents ------------------------------------------------- #

    def add_agents(self, agents, positions=None, random=False):
        """ Adds agents to the space environment.

        Arguments:
            agents (Sequence of Agent):
                Instance or iterable of agents to be added.
            positions (Sequence of positions, optional):
                The positions of the agents.
                Must have the same length as 'agents',
                with each entry being a position (array of float).
                If none is passed, all positions will be either be zero
                or random based on the argument 'random'.
            random (bool, optional):
                Whether to choose random positions (default False).
        """

        self._cKDTree = None  # Reset KDTree
        if not positions:
            n_agents = len(agents)
            if random:
                positions = [[self.model.random.random() * d_max
                              for d_max in self.shape]
                             for _ in range(n_agents)]
            else:
                positions = [np.zeros(self.ndim) for _ in range(n_agents)]

        for agent, pos in zip(agents, positions):

            pos = pos if isinstance(pos, np.ndarray) else np.array(pos)
            self.positions[agent] = pos  # Add pos to agent_dict

    def remove_agents(self, agents):
        """ Removes agents from the space. """
        self._cKDTree = None  # Reset KDTree
        for agent in make_list(agents):
            del self.positions[agent]  # Remove agent from env

    # Move and select agents ------------------------------------------------ #

    @staticmethod
    def _border_behavior(position, shape, torus):
        # Border behavior

        # Connected - Jump to other side
        if torus:
            for i in range(len(position)):
                while position[i] > shape[i]:
                    position[i] -= shape[i]
                while position[i] < 0:
                    position[i] += shape[i]

        # Not connected - Stop at border
        else:
            for i in range(len(position)):
                if position[i] > shape[i]:
                    position[i] = shape[i]
                elif position[i] < 0:
                    position[i] = 0

    def move_to(self, agent, pos):
        """ Moves agent to new position.

        Arguments:
            agent (Agent): Instance of the agent.
            pos (array_like): New position of the agent.
        """

        self._cKDTree = None  # Reset KDTree
        self._border_behavior(pos, self.shape, self._torus)
        self.positions[agent][...] = pos  # In-place

    def move_by(self, agent, path):
        """ Moves agent to new position, relative to current position.

        Arguments:
            agent (Agent): Instance of the agent.
            path (array_like): Relative change of position.
        """
        pos = [p + c for p, c in zip(self.positions[agent], path)]
        self.move_to(agent, pos)

    def neighbors(self, agent, distance):
        """ Select agent neighbors within a given distance.
        Takes into account wether space is toroidal.

        Arguments:
            agent (Agent): Instance of the agent.
            distance (float):
                Radius around the agent in which to search for neighbors.

        Returns:
            AgentIter: Iterator over the selected neighbors.
        """

        list_ids = self.kdtree.query_ball_point(
            self.positions[agent], distance)

        agents = [self._sorted_agents[list_id] for list_id in list_ids]
        agents.remove(agent)  # Remove original agent
        return AgentIter(self.model, agents)

    def select(self, center, radius):
        """ Select agents within a given area.

        Arguments:
            center (array_like): Coordinates of the center of the search area.
            radius (float): Radius around the center in which to search.

        Returns:
            AgentIter: Iterator over the selected agents.
        """
        if self.kdtree:
            list_ids = self.kdtree.query_ball_point(center, radius)
            agents = [self._sorted_agents[list_id] for list_id in list_ids]
            return AgentIter(self.model, agents)
        else:
            return AgentIter(self.model)


================================================
FILE: agentpy/tools.py
================================================
"""
Agentpy Tools Module
Content: Errors, generators, and base classes
"""

from numpy import ndarray
from collections.abc import Sequence

class AgentpyError(Exception):
    pass


def make_none(*args, **kwargs):
    return None


class InfoStr(str):
    """ String that is displayed in user-friendly format. """
    def __repr__(self):
        return self


def make_matrix(shape, loc_type=make_none, list_type=list, pos=None):
    """ Returns a nested list with given shape and class instance. """

    if pos is None:
        pos = ()

    if len(shape) == 1:
        return list_type([loc_type(pos+(i,))
                          for i in range(shape[0])])
    return list_type([make_matrix(shape[1:], loc_type, list_type, pos+(i,))
                      for i in range(shape[0])])


def make_list(element, keep_none=False):
    """ Turns element into a list of itself
    if it is not of type list or tuple. """

    if element is None and not keep_none:
        element = []  # Convert none to empty list
    if not isinstance(element, (list, tuple, set, ndarray)):
        element = [element]
    elif isinstance(element, (tuple, set)):
        element = list(element)

    return element


def param_tuples_to_salib(param_ranges_tuples):
    """ Convert param_ranges to SALib Format """

    param_ranges_salib = {
        'num_vars': len(param_ranges_tuples),
        'names': list(param_ranges_tuples.keys()),
        'bounds': []
    }

    for var_key, var_range in param_ranges_tuples.items():
        param_ranges_salib['bounds'].append([var_range[0], var_range[1]])

    return param_ranges_salib


class AttrDict(dict):
    """ Dictionary where attribute calls are handled like item calls.

    Examples:

        >>> ad = ap.AttrDict()
        >>> ad['a'] = 1
        >>> ad.a
        1

        >>> ad.b = 2
        >>> ad['b']
        2
    """

    def __init__(self, *args, **kwargs):
        if args == (None, ):
            args = ()  # Empty tuple
        super().__init__(*args, **kwargs)

    def __getattr__(self, name):
        try:
            return self.__getitem__(name)
        except KeyError:
            # Important for pickle to work
            raise AttributeError(name)

    def __setattr__(self, name, value):
        self.__setitem__(name, value)

    def __delattr__(self, item):
        del self[item]

    def _short_repr(self):
        len_ = len(self.keys())
        return f"AttrDict ({len_} entr{'y' if len_ == 1 else 'ies'})"


class ListDict(Sequence):
    """ List with fast deletion & lookup. """
    # H/T Amber https://stackoverflow.com/a/15993515/14396787

    def __init__(self, iterable):
        self.item_to_position = {}
        self.items = []
        for item in iterable:
            self.append(item)

    def __iter__(self):
        return iter(self.items)

    def __len__(self):
        return len(self.items)

    def __getitem__(self, item):
        return self.items[item]

    def __contains__(self, item):
        return item in self.item_to_position

    def extend(self, seq):
        for s in seq:
            self.append(s)

    def append(self, item):
        if item in self.item_to_position:
            return
        self.items.append(item)
        self.item_to_position[item] = len(self.items)-1

    def replace(self, old_item, new_item):
        position = self.item_to_position.pop(old_item)
        self.item_to_position[new_item] = position
        self.items[position] = new_item

    def remove(self, item):
        position = self.item_to_position.pop(item)
        last_item = self.items.pop()
        if position != len(self.items):
            self.items[position] = last_item
            self.item_to_position[last_item] = position

    def pop(self, index):
        """ Remove an object from the group by index. """
        self.remove(self[index])


================================================
FILE: agentpy/version.py
================================================
try:
    from importlib import metadata
except ImportError:
    # Running on pre-3.8 Python
    import importlib_metadata as metadata  # noqa

__version__ = metadata.version('agentpy')


================================================
FILE: agentpy/visualization.py
================================================
"""
Agentpy Visualization Module
Content: Animations and Gridplot
"""

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from matplotlib.colors import to_rgba
from matplotlib.animation import FuncAnimation
from SALib.analyze import sobol

from .tools import make_list, param_tuples_to_salib


def animate(model, fig, axs, plot, steps=None, seed=None,
            skip=0, fargs=(), **kwargs):
    """ Returns an animation of the model simulation,
    using :func:`matplotlib.animation.FuncAnimation`.

    Arguments:
        model (Model): The model instance.
        fig (matplotlib.figure.Figure): Figure for the animation.
        axs (matplotlib.axes.Axes or list): Axis or list of axis of the figure.
        plot (function): Function that takes the arguments `model, axs, *fargs`
            and creates the desired plots on each axis at each time-step.
        steps(int, optional):
            Number of (additional) steps for the simulation to run.
            If passed, the parameter 'Model.p.steps' will be ignored.
            The simulation can still be stopped with :func:'Model.stop'.
            If there is no step-limit through either this argument or
            the parameter 'Model.p.steps', the animation will stop at t=10000.
        seed (int, optional):
            Seed for the models random number generators.
            If none is given, the parameter 'Model.p.seed' will be used.
            If there is no such parameter, a random seed will be used.
        skip (int, optional):
            Steps to skip before the animation starts (default 0).
        fargs (tuple, optional): Forwarded fo the `plot` function.
        **kwargs: Forwarded to :func:`matplotlib.animation.FuncAnimation`.

    Examples:
        An animation can be generated as follows::

            def my_plot(model, ax):
                pass  # Call pyplot functions here
            
            fig, ax = plt.subplots() 
            my_model = MyModel(parameters)
            animation = ap.animate(my_model, fig, ax, my_plot)

        One way to display the resulting animation object in Jupyter::

            from IPython.display import HTML
            HTML(animation.to_jshtml())
    """

    model.sim_setup(steps, seed)
    model.create_output()
    pre_steps = 0

    for _ in range(skip):
        model.sim_step()

    def frames():
        nonlocal model, pre_steps
        if model.running is True:
            while model.running:
                if pre_steps < 2:  # Frames iterates twice before starting plot
                    pre_steps += 1
                else:
                    model.sim_step()
                    model.create_output()
                yield model.t
        else:  # Yield current if model stops before the animation starts
            yield model.t

    def update(t, m, axs, *fargs):  # noqa
        nonlocal pre_steps
        for ax in make_list(axs):
            # Clear axes before each plot
            ax.clear()
        plot(m, axs, *fargs)  # Perform plot

    save_count = 10000 if model._steps is np.nan else model._steps + 1

    ani = FuncAnimation(
        fig, update,
        frames=frames,
        fargs=(model, axs, *fargs),
        save_count=save_count,  # Limits animation to 100 steps otherwise
        **kwargs)  # noqa

    plt.close()  # Don't display static plot
    return ani


def _apply_colors(grid, color_dict, convert):
    if color_dict is not None:
        if None in color_dict:
            def func(v):
                return color_dict[None] if np.isnan(v) else color_dict[v]
        else:
            def func(v):
                return np.nan if np.isnan(v) else color_dict[v]
        grid = np.vectorize(func)(grid)
    if convert is True:
        def func(v):
            # TODO Can be improved
            if isinstance(v, str):
                if v == 'nan':
                    return 0., 0., 0., 0.
                else:
                    return to_rgba(v)
            elif np.isnan(v):
                return 0., 0., 0., 0.
            else:
                return to_rgba(v)
        grid = np.vectorize(func)(grid)
        grid = np.moveaxis(grid, 0, 2)
    return grid


def gridplot(grid, color_dict=None, convert=False, ax=None, **kwargs):
    """ Visualizes values on a two-dimensional grid with
    :func:`matplotlib.pyplot.imshow`.
    
    Arguments:
        grid (numpy.array): Two-dimensional array with values.
            numpy.nan values will be plotted as empty patches.
        color_dict (dict, optional): Dictionary that translates
            each value in `grid` to a color specification.
            If there is an entry `None`, it will be used for all NaN values.
        convert (bool, optional): Convert values to rgba vectors,
             using :func:`matplotlib.colors.to_rgba` (default False).
        ax (matplotlib.pyplot.axis, optional): Axis to be used for plot.
        **kwargs: Forwarded to :func:`matplotlib.pyplot.imshow`.
        
    Returns:
        :class:`matplotlib.image.AxesImage`  
    """
    # TODO Make feature for legend
    if color_dict is not None or convert:
        grid = _apply_colors(grid, color_dict, convert)
    if ax:
        im = ax.imshow(grid, **kwargs)
    else:
        im = plt.imshow(grid, **kwargs)
    return im


================================================
FILE: docs/Makefile
================================================
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS    ?=
SPHINXBUILD   ?= sphinx-build
SOURCEDIR     = .
BUILDDIR      = _build

# Put it first so that "make" without argument is like "make help".
help:
	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)


================================================
FILE: docs/_static/css/custom.css
================================================
/* Increase max width */
.wy-nav-content {
    max-width: 850px !important;
}

/* For alignment of jshtml animations */
.anim-state label {
 display:unset !important
}

================================================
FILE: docs/about.rst
================================================
.. currentmodule:: agentpy

=====
About
=====

Agentpy has been created by Joël Foramitti and is
available under the open-source `BSD 3-Clause <https://github.com/JoelForamitti/agentpy/blob/master/LICENSE>`_ license.
Source files can be found on the `GitHub repository <https://github.com/joelforamitti/agentpy>`_.

Thanks to everyone who has contributed
or supported the developement of this package:

- Jeroen C.J.M. van den Bergh
- Ivan Savin
- James Millington
- Martí Bosch
- Sebastian Benthall
- Bhakti Stephan Onggo

This project has benefited from an ERC Advanced Grant
from the European Research Council (ERC)
under the European Union's Horizon 2020 research and innovation programme
(grant agreement n° 741087).

Parts of this package where created with Cookiecutter_
and the `audreyr/cookiecutter-pypackage`_ project template.

.. _Cookiecutter: https://github.com/audreyr/cookiecutter
.. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage


================================================
FILE: docs/agentpy_button_network.ipynb
================================================
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Button network\n",
    "\n",
    "This notebook presents an agent-based model of randomly connecting buttons.\n",
    "It demonstrates how to use the [agentpy](https://agentpy.readthedocs.io) package\n",
    "to work with networks and visualize averaged time-series for discrete parameter samples."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Model design\n",
    "import agentpy as ap\n",
    "import networkx as nx\n",
    "\n",
    "# Visualization\n",
    "import seaborn as sns"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## About the model\n",
    "\n",
    "This model is based on the [Agentbase Button model](http://agentbase.org/model.html?f4c4388138450bdf9732) by Wybo Wiersma and the following analogy from [Stuart Kauffman](http://www.pbs.org/lifebeyondearth/resources/intkauffmanpop.html): \n",
    "\n",
    "> \"Suppose you take 10,000 buttons and spread them out on a hardwood floor. You have a large spool of red thread. Now, what you do is you pick up a random pair of buttons and you tie them together with a piece of red thread. Put them down and pick up another random pair of buttons and tie them together with a red thread, and you just keep doing this. Every now and then lift up a button and see how many buttons you've lifted with your first button. A connective cluster of buttons is called a cluster or a component. When you have 10,000 buttons and only a few threads that tie them together, most of the times you'd pick up a button you'll pick up a single button. \n",
    ">\n",
    ">As the ratio of threads to buttons increases, you're going to start to get larger clusters, three or four buttons tied together; then larger and larger clusters. At some point, you will have a number of intermediate clusters, and when you add a few more threads, you'll have linked up the intermediate-sized clusters into one giant cluster.\n",
    ">\n",
    ">So that if you plot on an axis, the ratio of threads to buttons: 10,000 buttons and no threads; 10,000 buttons and 5,000 threads; and so on, you'll get a curve that is flat, and then all of a sudden it shoots up when you get this giant cluster. This steep curve is in fact evidence of a phase transition.\n",
    ">\n",
    ">If there were an infinite number of threads and an infinite number of buttons and one just tuned the ratios, this would be a step function; it would come up in a sudden jump. So it's a phase transition like ice freezing.\n",
    ">\n",
    ">Now, the image you should take away from this is if you connect enough buttons all of a sudden they all go connected. To think about the origin of life, we have to think about the same thing.\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Model definition"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ButtonModel(ap.Model):\n",
    "    \n",
    "    def setup(self):\n",
    "        \n",
    "        # Create a graph with n agents\n",
    "        self.buttons = ap.Network(self)\n",
    "        self.agents = ap.AgentList(self, self.p.n)\n",
    "        self.buttons.add_agents(self.agents)\n",
    "        self.agents.node = self.buttons.nodes\n",
    "        self.threads = 0\n",
    "        \n",
    "    def update(self):\n",
    "        \n",
    "        # Record size of the biggest cluster\n",
    "        clusters = nx.connected_components(self.buttons.graph)\n",
    "        max_cluster_size = max([len(g) for g in clusters]) / self.p.n\n",
    "        self.record('max_cluster_size', max_cluster_size)\n",
    "        \n",
    "        # Record threads to button ratio\n",
    "        self.record('threads_to_button', self.threads / self.p.n)\n",
    "    \n",
    "    def step(self):\n",
    "        \n",
    "        # Create random edges based on parameters\n",
    "        for _ in range(int(self.p.n * self.p.speed)):  \n",
    "            self.buttons.graph.add_edge(*self.agents.random(2).node)\n",
    "            self.threads += 1         "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Multi-run experiment"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Scheduled runs: 75\n",
      "Completed: 75, estimated time remaining: 0:00:00\n",
      "Experiment finished\n",
      "Run time: 0:00:36.012666\n"
     ]
    }
   ],
   "source": [
    "# Define parameter ranges\n",
    "parameter_ranges = {\n",
    "    'steps': 30,  # Number of simulation steps\n",
    "    'speed': 0.05,  # Speed of connections per step\n",
    "    'n': ap.Values(100, 1000, 10000)  # Number of agents\n",
    "}\n",
    "\n",
    "# Create sample for different values of n\n",
    "sample = ap.Sample(parameter_ranges) \n",
    "\n",
    "# Keep dynamic variables\n",
    "exp = ap.Experiment(ButtonModel, sample, iterations=25, record=True) \n",
    "\n",
    "# Perform 75 separate simulations (3 parameter combinations * 25 repetitions)\n",
    "results = exp.run() "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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hBD1kiQhhBCiXpIkhBBC1EuShIhqu3btonfv3s22RPZP3XPPPTzxxBMt8rNrO+ecc/j8889bOgzRDkmSEFGnLb0hRsNzOVSii4a4ROsgSUK0KS3V4hCirZIkIaLKb3/7W3bv3s1NN93EgAEDePfddwGYP38+Z511FkOGDOG5556rPv8vf/kLt99+O7/5zW849dRTeeuttygpKeG+++5j2LBhDB8+nCeeeALTNAHYsWMHV199NUOGDGHIkCH8+te/pri4uPrx1q1bx4UXXsiAAQO444478Pv91ccKCwv5xS9+waBBgxg8eDCTJk3CsqwGP5cXX3wRgI8++ojzzjuPQYMGcdVVV7F58+YG/W7Wrl3L2LFjOe2007j33nurY/v3v//N5ZdfXufc3r17s337dv7f//t/zJ8/n5deeokBAwZw0003HVVc55xzDi+99BLjxo1j4MCBB/1uRBvW0uuCCPFTZ599tvrss8+UUkrt3LlT9erVS/3+979XFRUVav369apfv37qhx9+UErZ692ccMIJ6oMPPlCmaaqKigp1yy23qAceeECVlZWpffv2qYsvvlj985//VEoptW3bNrVs2TLl9/tVQUGBmjRpknrooYeUUkr5/X511llnqdmzZ6tAIKDeffdddcIJJ6jHH39cKaXUn/70J/XAAw+oQCCgAoGA+vLLL5VlWQ1+LkoptWXLFnXyySerZcuWqUAgoF544QU1cuRI5ff7j/g45513ntq9e7cqKipSEydOrI7rzTffVJdddlmd83v16qW2bdumlFLq7rvvrj73aOM6++yz1cUXX6z27NmjioqK1OjRo9Vrr7122JhF2yAtCdEq3HrrrXg8Hvr06UOfPn3YsGFD9bFTTjmFkSNHous6paWlfPLJJ9x33334fD5SUlK49tprWbhwIQDHHXccZ555Ji6Xi+TkZK677jq+/PJLAL799luCwSDXXHMNTqeT0aNHc9JJJ1X/HIfDwd69e9m9ezdOp5NBgwahaVpEz2PRokX8/Oc/58wzz8TpdHLDDTdQWVnJN998c8Rrr7jiCjIzM0lMTOTmm2+ufk6NoSFxXXXVVWRkZJCYmMjZZ5/N+vXrG+3ni+jVbvaTEK1bampq9fder5fy8vLq27V3G9u9ezehUIhhw4ZV32dZVvVeDfv27eOPf/wjX331FWVlZSiliI+PByA/P5+MjIw6b/y190e44YYbeOaZZ7j++usBmDhxYvVeAA2Vn59f5zF1Xa/eX+JIau83kZWVRX5+fkQ/+1jjqtrqFez/g8b8+SJ6SZIQrV7tN/UOHTrgcrlYvnw5DsfBf96PP/44mqYxf/58EhMT+fDDD/nDH/4A2G+CeXl5KKWqH3P37t107twZgNjYWO655x7uueceNm3axDXXXMNJJ53E0KFDGxxreno6mzZtqr6tlKrezvRIam9vunv3btLT0wH7DbuysrL62N69e+tc15DWzrHEJdo26W4SUSc1NZWdO3ce1bXp6emceeaZPPLII5SWlmJZFjt27GDlypUAlJWV4fP5iIuLIy8vj//7v/+rvvaUU07B4XAwZ84cgsEg77//PmvXrq0+/vHHH7N9+3aUUsTFxWEYxhHfgH/6XMaMGcMnn3zCF198QTAY5O9//zsul4sBAwYc8bm99tpr7Nmzh/379/P8888zduxYAPr06cP333/P+vXr8fv9/OUvf6lzXUpKCrt27WqyuETbJklCRJ0pU6bw3HPPMWjQIN57772Ir3/00UcJBoPVI4Fuv/326k/Xt956K+vWrWPQoEFMmTKFUaNGVV/ncrn4y1/+wltvvcXgwYNZtGgR5557bvXx7du3c9111zFgwAAmTpzI5Zdfzumnn97g5/LSSy/RvXt3HnvsMR588EFOP/10Pv74Y55//nlcLtcRn1dOTg7XX389I0eOpEuXLtx8880AdOvWjV/+8pdce+21jBo1ioEDB9a5bsKECfzwww8MGjSIW265pdHjEm2bbDokhBCiXtKSEEIIUS8pXAtxDHbv3s155513yGMLFy6sM2KoOR5HiMYm3U1CCCHqJd1NQggh6iVJQgghRL0kSQghhKiXJAkhhBD1kiQhhBCiXv8fNmlnDWkQrm8AAAAASUVORK5CYII=\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Plot averaged time-series for discrete parameter samples\n",
    "sns.set_theme() \n",
    "sns.lineplot(\n",
    "    data=results.arrange_variables(), \n",
    "    x='threads_to_button', \n",
    "    y='max_cluster_size', \n",
    "    hue='n'\n",
    ");"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}


================================================
FILE: docs/agentpy_demo.py
================================================
import agentpy as ap





class MoneyAgent(ap.Agent):

    def setup(self):
        self.wealth = 1

    def wealth_transfer(self):
        if self.wealth == 0:
            return
        a = self.model.agents.random()
        a.wealth += 1
        self.wealth -= 1



class MoneyModel(ap.Model):

    def setup(self):
        self.agents = ap.AgentList(
            self, self.p.n, MoneyAgent)

    def step(self):
        self.agents.record('wealth')
        self.agents.wealth_transfer()










# Perform single run
parameters = {'n': 10, 'steps': 10}
model = MoneyModel(parameters)
results = model.run()

# Perform multiple runs
variable_params = {
    'n': ap.IntRange(10, 500), 
    'steps': 10
}
sample = ap.Sample(variable_params, n=49)
exp = ap.Experiment(
    MoneyModel,
    sample,
    iterations=5,
    record=True
)
results = exp.run()

================================================
FILE: docs/agentpy_flocking.ipynb
================================================
[File too large to display: 21.4 MB]

================================================
FILE: docs/agentpy_forest_fire.ipynb
================================================
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Forest fire"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This notebook presents an agent-based model that simulates a forest fire.\n",
    "It demonstrates how to use the [agentpy](https://agentpy.readthedocs.io) package to work with a spatial grid and create animations, and perform a parameter sweep. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Model design\n",
    "import agentpy as ap\n",
    "\n",
    "# Visualization\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "import IPython"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## About the model\n",
    "\n",
    "The model ist based on the [NetLogo FireSimple model](http://ccl.northwestern.edu/netlogo/models/FireSimple) by Uri Wilensky and William Rand, who describe it as follows:\n",
    "\n",
    "> \"This model simulates the spread of a fire through a forest. It shows that the fire's chance of reaching the right edge of the forest depends critically on the density of trees. This is an example of a common feature of complex systems, the presence of a non-linear threshold or critical parameter. [...] \n",
    ">\n",
    "> The fire starts on the left edge of the forest, and spreads to neighboring trees. The fire spreads in four directions: north, east, south, and west.\n",
    ">\n",
    ">The model assumes there is no wind. So, the fire must have trees along its path in order to advance. That is, the fire cannot skip over an unwooded area (patch), so such a patch blocks the fire's motion in that direction.\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Model definition"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class ForestModel(ap.Model):\n",
    "    \n",
    "    def setup(self):\n",
    "        \n",
    "        # Create agents (trees) \n",
    "        n_trees = int(self.p['Tree density'] * (self.p.size**2))\n",
    "        trees = self.agents = ap.AgentList(self, n_trees)\n",
    "        \n",
    "        # Create grid (forest)\n",
    "        self.forest = ap.Grid(self, [self.p.size]*2, track_empty=True)      \n",
    "        self.forest.add_agents(trees, random=True, empty=True)\n",
    "        \n",
    "        # Initiate a dynamic variable for all trees\n",
    "        # Condition 0: Alive, 1: Burning, 2: Burned\n",
    "        self.agents.condition = 0 \n",
    "        \n",
    "        # Start a fire from the left side of the grid\n",
    "        unfortunate_trees = self.forest.agents[0:self.p.size, 0:2]\n",
    "        unfortunate_trees.condition = 1 \n",
    "        \n",
    "    def step(self):\n",
    "        \n",
    "        # Select burning trees\n",
    "        burning_trees = self.agents.select(self.agents.condition == 1)\n",
    "\n",
    "        # Spread fire \n",
    "        for tree in burning_trees:\n",
    "            for neighbor in self.forest.neighbors(tree):\n",
    "                if neighbor.condition == 0:\n",
    "                    neighbor.condition = 1 # Neighbor starts burning\n",
    "            tree.condition = 2 # Tree burns out  \n",
    "        \n",
    "        # Stop simulation if no fire is left\n",
    "        if len(burning_trees) == 0: \n",
    "            self.stop()\n",
    "            \n",
    "    def end(self):\n",
    "        \n",
    "        # Document a measure at the end of the simulation\n",
    "        burned_trees = len(self.agents.select(self.agents.condition == 2))\n",
    "        self.report('Percentage of burned trees', \n",
    "                    burned_trees / len(self.agents))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Single-run animation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define parameters\n",
    "\n",
    "parameters = {\n",
    "    'Tree density': 0.6, # Percentage of grid covered by trees\n",
    "    'size': 50, # Height and length of the grid\n",
    "    'steps': 100,\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "<link rel=\"stylesheet\"\n",
       "href=\"https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css\">\n",
       "<script language=\"javascript\">\n",
       "  function isInternetExplorer() {\n",
       "    ua = navigator.userAgent;\n",
       "    /* MSIE used to detect old browsers and Trident used to newer ones*/\n",
       "    return ua.indexOf(\"MSIE \") > -1 || ua.indexOf(\"Trident/\") > -1;\n",
       "  }\n",
       "\n",
       "  /* Define the Animation class */\n",
       "  function Animation(frames, img_id, slider_id, interval, loop_select_id){\n",
       "    this.img_id = img_id;\n",
       "    this.slider_id = slider_id;\n",
       "    this.loop_select_id = loop_select_id;\n",
       "    this.interval = interval;\n",
       "    this.current_frame = 0;\n",
       "    this.direction = 0;\n",
       "    this.timer = null;\n",
       "    this.frames = new Array(frames.length);\n",
       "\n",
       "    for (var i=0; i<frames.length; i++)\n",
       "    {\n",
       "     this.frames[i] = new Image();\n",
       "     this.frames[i].src = frames[i];\n",
       "    }\n",
       "    var slider = document.getElementById(this.slider_id);\n",
       "    slider.max = this.frames.length - 1;\n",
       "    if (isInternetExplorer()) {\n",
       "        // switch from oninput to onchange because IE <= 11 does not conform\n",
       "        // with W3C specification. It ignores oninput and onchange behaves\n",
       "        // like oninput. In contrast, Mircosoft Edge behaves correctly.\n",
       "        slider.setAttribute('onchange', slider.getAttribute('oninput'));\n",
       "        slider.setAttribute('oninput', null);\n",
       "    }\n",
       "    this.set_frame(this.current_frame);\n",
       "  }\n",
       "\n",
       "  Animation.prototype.get_loop_state = function(){\n",
       "    var button_group = document[this.loop_select_id].state;\n",
       "    for (var i = 0; i < button_group.length; i++) {\n",
       "        var button = button_group[i];\n",
       "        if (button.checked) {\n",
       "            return button.value;\n",
       "        }\n",
       "    }\n",
       "    return undefined;\n",
       "  }\n",
       "\n",
       "  Animation.prototype.set_frame = function(frame){\n",
       "    this.current_frame = frame;\n",
       "    document.getElementById(this.img_id).src =\n",
       "            this.frames[this.current_frame].src;\n",
       "    document.getElementById(this.slider_id).value = this.current_frame;\n",
       "  }\n",
       "\n",
       "  Animation.prototype.next_frame = function()\n",
       "  {\n",
       "    this.set_frame(Math.min(this.frames.length - 1, this.current_frame + 1));\n",
       "  }\n",
       "\n",
       "  Animation.prototype.previous_frame = function()\n",
       "  {\n",
       "    this.set_frame(Math.max(0, this.current_frame - 1));\n",
       "  }\n",
       "\n",
       "  Animation.prototype.first_frame = function()\n",
       "  {\n",
       "    this.set_frame(0);\n",
       "  }\n",
       "\n",
       "  Animation.prototype.last_frame = function()\n",
       "  {\n",
       "    this.set_frame(this.frames.length - 1);\n",
       "  }\n",
       "\n",
       "  Animation.prototype.slower = function()\n",
       "  {\n",
       "    this.interval /= 0.7;\n",
       "    if(this.direction > 0){this.play_animation();}\n",
       "    else if(this.direction < 0){this.reverse_animation();}\n",
       "  }\n",
       "\n",
       "  Animation.prototype.faster = function()\n",
       "  {\n",
       "    this.interval *= 0.7;\n",
       "    if(this.direction > 0){this.play_animation();}\n",
       "    else if(this.direction < 0){this.reverse_animation();}\n",
       "  }\n",
       "\n",
       "  Animation.prototype.anim_step_forward = function()\n",
       "  {\n",
       "    this.current_frame += 1;\n",
       "    if(this.current_frame < this.frames.length){\n",
       "      this.set_frame(this.current_frame);\n",
       "    }else{\n",
       "      var loop_state = this.get_loop_state();\n",
       "      if(loop_state == \"loop\"){\n",
       "        this.first_frame();\n",
       "      }else if(loop_state == \"reflect\"){\n",
       "        this.last_frame();\n",
       "        this.reverse_animation();\n",
       "      }else{\n",
       "        this.pause_animation();\n",
       "        this.last_frame();\n",
       "      }\n",
       "    }\n",
       "  }\n",
       "\n",
       "  Animation.prototype.anim_step_reverse = function()\n",
       "  {\n",
       "    this.current_frame -= 1;\n",
       "    if(this.current_frame >= 0){\n",
       "      this.set_frame(this.current_frame);\n",
       "    }else{\n",
       "      var loop_state = this.get_loop_state();\n",
       "      if(loop_state == \"loop\"){\n",
       "        this.last_frame();\n",
       "      }else if(loop_state == \"reflect\"){\n",
       "        this.first_frame();\n",
       "        this.play_animation();\n",
       "      }else{\n",
       "        this.pause_animation();\n",
       "        this.first_frame();\n",
       "      }\n",
       "    }\n",
       "  }\n",
       "\n",
       "  Animation.prototype.pause_animation = function()\n",
       "  {\n",
       "    this.direction = 0;\n",
       "    if (this.timer){\n",
       "      clearInterval(this.timer);\n",
       "      this.timer = null;\n",
       "    }\n",
       "  }\n",
       "\n",
       "  Animation.prototype.play_animation = function()\n",
       "  {\n",
       "    this.pause_animation();\n",
       "    this.direction = 1;\n",
       "    var t = this;\n",
       "    if (!this.timer) this.timer = setInterval(function() {\n",
       "        t.anim_step_forward();\n",
       "    }, this.interval);\n",
       "  }\n",
       "\n",
       "  Animation.prototype.reverse_animation = function()\n",
       "  {\n",
       "    this.pause_animation();\n",
       "    this.direction = -1;\n",
       "    var t = this;\n",
       "    if (!this.timer) this.timer = setInterval(function() {\n",
       "        t.anim_step_reverse();\n",
       "    }, this.interval);\n",
       "  }\n",
       "</script>\n",
       "\n",
       "<style>\n",
       ".animation {\n",
       "    display: inline-block;\n",
       "    text-align: center;\n",
       "}\n",
       "input[type=range].anim-slider {\n",
       "    width: 374px;\n",
       "    margin-left: auto;\n",
       "    margin-right: auto;\n",
       "}\n",
       ".anim-buttons {\n",
       "    margin: 8px 0px;\n",
       "}\n",
       ".anim-buttons button {\n",
       "    padding: 0;\n",
       "    width: 36px;\n",
       "}\n",
       ".anim-state label {\n",
       "    margin-right: 8px;\n",
       "}\n",
       ".anim-state input {\n",
       "    margin: 0;\n",
       "    vertical-align: middle;\n",
       "}\n",
       "</style>\n",
       "\n",
       "<div class=\"animation\">\n",
       "  <img id=\"_anim_img9c68c06d8fd4476e84efb39ee579d4f9\">\n",
       "  <div class=\"anim-controls\">\n",
       "    <input id=\"_anim_slider9c68c06d8fd4476e84efb39ee579d4f9\" type=\"range\" class=\"anim-slider\"\n",
       "           name=\"points\" min=\"0\" max=\"1\" step=\"1\" value=\"0\"\n",
       "           oninput=\"anim9c68c06d8fd4476e84efb39ee579d4f9.set_frame(parseInt(this.value));\"></input>\n",
       "    <div class=\"anim-buttons\">\n",
       "      <button title=\"Decrease speed\" onclick=\"anim9c68c06d8fd4476e84efb39ee579d4f9.slower()\">\n",
       "          <i class=\"fa fa-minus\"></i></button>\n",
       "      <button title=\"First frame\" onclick=\"anim9c68c06d8fd4476e84efb39ee579d4f9.first_frame()\">\n",
       "        <i class=\"fa fa-fast-backward\"></i></button>\n",
       "      <button title=\"Previous frame\" onclick=\"anim9c68c06d8fd4476e84efb39ee579d4f9.previous_frame()\">\n",
       "          <i class=\"fa fa-step-backward\"></i></button>\n",
       "      <button title=\"Play backwards\" onclick=\"anim9c68c06d8fd4476e84efb39ee579d4f9.reverse_animation()\">\n",
       "          <i class=\"fa fa-play fa-flip-horizontal\"></i></button>\n",
       "      <button title=\"Pause\" onclick=\"anim9c68c06d8fd4476e84efb39ee579d4f9.pause_animation()\">\n",
       "          <i class=\"fa fa-pause\"></i></button>\n",
       "      <button title=\"Play\" onclick=\"anim9c68c06d8fd4476e84efb39ee579d4f9.play_animation()\">\n",
       "          <i class=\"fa fa-play\"></i></button>\n",
       "      <button title=\"Next frame\" onclick=\"anim9c68c06d8fd4476e84efb39ee579d4f9.next_frame()\">\n",
       "          <i class=\"fa fa-step-forward\"></i></button>\n",
       "      <button title=\"Last frame\" onclick=\"anim9c68c06d8fd4476e84efb39ee579d4f9.last_frame()\">\n",
       "          <i class=\"fa fa-fast-forward\"></i></button>\n",
       "      <button title=\"Increase speed\" onclick=\"anim9c68c06d8fd4476e84efb39ee579d4f9.faster()\">\n",
       "          <i class=\"fa fa-plus\"></i></button>\n",
       "    </div>\n",
       "    <form title=\"Repetition mode\" action=\"#n\" name=\"_anim_loop_select9c68c06d8fd4476e84efb39ee579d4f9\"\n",
       "          class=\"anim-state\">\n",
       "      <input type=\"radio\" name=\"state\" value=\"once\" id=\"_anim_radio1_9c68c06d8fd4476e84efb39ee579d4f9\"\n",
       "             >\n",
       "      <label for=\"_anim_radio1_9c68c06d8fd4476e84efb39ee579d4f9\">Once</label>\n",
       "      <input type=\"radio\" name=\"state\" value=\"loop\" id=\"_anim_radio2_9c68c06d8fd4476e84efb39ee579d4f9\"\n",
       "             checked>\n",
       "      <label for=\"_anim_radio2_9c68c06d8fd4476e84efb39ee579d4f9\">Loop</label>\n",
       "      <input type=\"radio\" name=\"state\" value=\"reflect\" id=\"_anim_radio3_9c68c06d8fd4476e84efb39ee579d4f9\"\n",
       "             >\n",
       "      <label for=\"_anim_radio3_9c68c06d8fd4476e84efb39ee579d4f9\">Reflect</label>\n",
       "    </form>\n",
       "  </div>\n",
       "</div>\n",
       "\n",
       "\n",
       "<script language=\"javascript\">\n",
       "  /* Instantiate the Animation class. */\n",
       "  /* The IDs given should match those used in the template above. */\n",
       "  (function() {\n",
       "    var img_id = \"_anim_img9c68c06d8fd4476e84efb39ee579d4f9\";\n",
       "    var slider_id = \"_anim_slider9c68c06d8fd4476e84efb39ee579d4f9\";\n",
       "    var loop_select_id = \"_anim_loop_select9c68c06d8fd4476e84efb39ee579d4f9\";\n",
       "    var frames = new Array(57);\n",
       "    \n",
       "  frames[0] = \"data:image/png;base64,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
Download .txt
gitextract_t5qr14uy/

├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── publish.yml
│       └── test.yml
├── .gitignore
├── .readthedocs.yml
├── LICENSE
├── README.md
├── agentpy/
│   ├── __init__.py
│   ├── agent.py
│   ├── datadict.py
│   ├── examples.py
│   ├── experiment.py
│   ├── grid.py
│   ├── model.py
│   ├── network.py
│   ├── objects.py
│   ├── sample.py
│   ├── sequences.py
│   ├── space.py
│   ├── tools.py
│   ├── version.py
│   └── visualization.py
├── docs/
│   ├── Makefile
│   ├── _static/
│   │   └── css/
│   │       └── custom.css
│   ├── about.rst
│   ├── agentpy_button_network.ipynb
│   ├── agentpy_demo.py
│   ├── agentpy_flocking.ipynb
│   ├── agentpy_forest_fire.ipynb
│   ├── agentpy_segregation.ipynb
│   ├── agentpy_virus_spread.ipynb
│   ├── agentpy_wealth_transfer.ipynb
│   ├── changelog.rst
│   ├── conf.py
│   ├── contributing.rst
│   ├── guide.rst
│   ├── guide_ema.ipynb
│   ├── guide_interactive.ipynb
│   ├── guide_random.ipynb
│   ├── index.rst
│   ├── installation.rst
│   ├── make.bat
│   ├── model_library.rst
│   ├── overview.rst
│   ├── reference.rst
│   ├── reference_agents.rst
│   ├── reference_data.rst
│   ├── reference_environments.rst
│   ├── reference_examples.rst
│   ├── reference_experiment.rst
│   ├── reference_grid.rst
│   ├── reference_model.rst
│   ├── reference_network.rst
│   ├── reference_other.rst
│   ├── reference_sample.rst
│   ├── reference_sequences.rst
│   ├── reference_space.rst
│   └── reference_visualization.rst
├── paper/
│   ├── paper.bib
│   └── paper.md
├── setup.cfg
├── setup.py
└── tests/
    ├── __init__.py
    ├── test_datadict.py
    ├── test_examples.py
    ├── test_experiment.py
    ├── test_grid.py
    ├── test_init.py
    ├── test_model.py
    ├── test_network.py
    ├── test_objects.py
    ├── test_sample.py
    ├── test_sequences.py
    ├── test_space.py
    ├── test_tools.py
    └── test_visualization.py
Download .txt
SYMBOL INDEX (341 symbols across 27 files)

FILE: agentpy/agent.py
  class Agent (line 11) | class Agent(Object):
    method __init__ (line 27) | def __init__(self, model, *args, **kwargs):

FILE: agentpy/datadict.py
  class NpEncoder (line 16) | class NpEncoder(json.JSONEncoder):
    method default (line 19) | def default(self, obj):
  function _last_exp_id (line 32) | def _last_exp_id(name, path):
  class DataDict (line 45) | class DataDict(AttrDict):
    method __repr__ (line 63) | def __repr__(self, indent=False):
    method _short_repr (line 96) | def _short_repr(self):
    method __eq__ (line 100) | def __eq__(self, other):
    method __ne__ (line 114) | def __ne__(self, other):
    method _sobol_set_df_index (line 120) | def _sobol_set_df_index(df, p_keys, reporter):
    method calc_sobol (line 125) | def calc_sobol(self, reporters=None, **kwargs):
    method _combine_vars (line 206) | def _combine_vars(self, obj_types=True, var_keys=True):
    method _dict_pars_to_df (line 256) | def _dict_pars_to_df(self, dict_pars):
    method _combine_pars (line 262) | def _combine_pars(self, sample=True, constants=True):
    method arrange (line 283) | def arrange(self, variables=False, reporters=False, parameters=False,
    method arrange_reporters (line 362) | def arrange_reporters(self):
    method arrange_variables (line 367) | def arrange_variables(self):
    method save (line 374) | def save(self, exp_name=None, exp_id=None, path='ap_output', display=T...
    method _load (line 450) | def _load(self, exp_name=None, exp_id=None,
    method load (line 522) | def load(cls, exp_name=None, exp_id=None, path='ap_output', display=Tr...

FILE: agentpy/examples.py
  function gini (line 5) | def gini(x):
  class WealthAgent (line 16) | class WealthAgent(ap.Agent):
    method setup (line 20) | def setup(self):
    method wealth_transfer (line 24) | def wealth_transfer(self):
  class WealthModel (line 33) | class WealthModel(ap.Model):
    method setup (line 48) | def setup(self):
    method step (line 51) | def step(self):
    method update (line 54) | def update(self):
    method end (line 58) | def end(self):
  class SegregationAgent (line 62) | class SegregationAgent(ap.Agent):
    method setup (line 64) | def setup(self):
    method update_happiness (line 72) | def update_happiness(self):
    method find_new_home (line 80) | def find_new_home(self):
  class SegregationModel (line 86) | class SegregationModel(ap.Model):
    method setup (line 105) | def setup(self):
    method update (line 116) | def update(self):
    method step (line 125) | def step(self):
    method get_segregation (line 129) | def get_segregation(self):
    method end (line 133) | def end(self):

FILE: agentpy/experiment.py
  class Experiment (line 20) | class Experiment:
    method __init__ (line 48) | def __init__(self, model_class, sample=None, iterations=1,
    method _parameters_to_output (line 116) | def _parameters_to_output(self):
    method _add_single_output_to_combined (line 135) | def _add_single_output_to_combined(single_output, combined_output):
    method _combine_dataframes (line 155) | def _combine_dataframes(self, combined_output):
    method _single_sim (line 175) | def _single_sim(self, run_id):
    method run (line 189) | def run(self, n_jobs=1, pool=None, display=True, **kwargs):
    method end (line 283) | def end(self):

FILE: agentpy/grid.py
  class _IterArea (line 16) | class _IterArea:
    method __init__ (line 27) | def __init__(self, area, exclude=None):
    method __len__ (line 31) | def __len__(self):
    method __iter__ (line 40) | def __iter__(self):
  class GridIter (line 57) | class GridIter(AgentIter):
    method __init__ (line 82) | def __init__(self, model, iter_, items):
    method __getitem__ (line 86) | def __getitem__(self, item):
  class Grid (line 91) | class Grid(SpatialEnvironment):
    method _agent_field (line 140) | def _agent_field(field_name, shape, model):
    method __init__ (line 148) | def __init__(self, model, shape, torus=False,
    method agents (line 168) | def agents(self):
    method _add_agent (line 173) | def _add_agent(self, agent, position, field):
    method add_agents (line 178) | def add_agents(self, agents, positions=None, random=False, empty=False):
    method remove_agents (line 241) | def remove_agents(self, agents):
    method _border_behavior (line 253) | def _border_behavior(position, shape, torus):
    method move_to (line 267) | def move_to(self, agent, pos):
    method move_by (line 294) | def move_by(self, agent, path):
    method neighbors (line 304) | def neighbors(self, agent, distance=1):
    method apply (line 354) | def apply(self, func, field='agents'):
    method attr_grid (line 364) | def attr_grid(self, attr_key, otypes='f', field='agents'):
    method add_field (line 384) | def add_field(self, key, values=None):
    method del_field (line 409) | def del_field(self, key):

FILE: agentpy/model.py
  class Model (line 23) | class Model(Object):
    method __init__ (line 103) | def __init__(self, parameters=None, _run_id=None, **kwargs):
    method __repr__ (line 146) | def __repr__(self):
    method as_function (line 152) | def as_function(cls, **kwargs):
    method info (line 189) | def info(self):
    method _new_id (line 201) | def _new_id(self):
    method report (line 208) | def report(self, rep_keys, value=None):
    method setup (line 259) | def setup(self):
    method step (line 264) | def step(self):
    method update (line 270) | def update(self):
    method end (line 276) | def end(self):
    method set_parameters (line 283) | def set_parameters(self, parameters):
    method sim_setup (line 287) | def sim_setup(self, steps=None, seed=None):
    method sim_step (line 323) | def sim_step(self):
    method sim_reset (line 332) | def sim_reset(self):
    method stop (line 342) | def stop(self):
    method run (line 346) | def run(self, steps=None, seed=None, display=True):
    method create_output (line 396) | def create_output(self):

FILE: agentpy/network.py
  class AgentNode (line 10) | class AgentNode(set):
    method __init__ (line 15) | def __init__(self, label):
    method __hash__ (line 18) | def __hash__(self):
    method __repr__ (line 21) | def __repr__(self):
  class Network (line 25) | class Network(Object):
    method __init__ (line 49) | def __init__(self, model, graph=None, **kwargs):
    method agents (line 67) | def agents(self):
    method nodes (line 71) | def nodes(self):
    method add_node (line 76) | def add_node(self, label=None):
    method remove_node (line 94) | def remove_node(self, node):
    method add_agents (line 105) | def add_agents(self, agents, positions=None):
    method remove_agents (line 128) | def remove_agents(self, agents):
    method move_to (line 136) | def move_to(self, agent, node):
    method neighbors (line 148) | def neighbors(self, agent):

FILE: agentpy/objects.py
  class Object (line 10) | class Object:
    method __init__ (line 13) | def __init__(self, model):
    method __repr__ (line 23) | def __repr__(self):
    method __getattr__ (line 26) | def __getattr__(self, key):
    method __getitem__ (line 29) | def __getitem__(self, key):
    method __setitem__ (line 32) | def __setitem__(self, key, value):
    method _set_var_ignore (line 35) | def _set_var_ignore(self):
    method vars (line 40) | def vars(self):
    method record (line 45) | def record(self, var_keys, value=None):
    method _record (line 94) | def _record(self, var_keys, value=None):
    method setup (line 118) | def setup(self, **kwargs):
  class SpatialEnvironment (line 142) | class SpatialEnvironment(Object):
    method record_positions (line 144) | def record_positions(self, label='p'):

FILE: agentpy/sample.py
  class Range (line 18) | class Range:
    method __init__ (line 31) | def __init__(self, vmin=0, vmax=1, vdef=None):
    method __repr__ (line 37) | def __repr__(self):
  class IntRange (line 41) | class IntRange(Range):
    method __init__ (line 56) | def __init__(self, vmin=0, vmax=1, vdef=None):
    method __repr__ (line 62) | def __repr__(self):
  class Values (line 66) | class Values:
    method __init__ (line 77) | def __init__(self, *args, vdef=None):
    method __len__ (line 81) | def __len__(self):
    method __repr__ (line 84) | def __repr__(self):
  class Sample (line 88) | class Sample:
    method __init__ (line 139) | def __init__(self, parameters, n=None,
    method __repr__ (line 152) | def __repr__(self):
    method __iter__ (line 155) | def __iter__(self):
    method __len__ (line 158) | def __len__(self):
    method _assign_random_seeds (line 163) | def _assign_random_seeds(self, seed):
    method _linspace (line 169) | def _linspace(parameters, n, product=True):
    method _saltelli (line 201) | def _saltelli(self, params, n, calc_second_order=True):

FILE: agentpy/sequences.py
  class AgentSequence (line 13) | class AgentSequence:
    method __repr__ (line 16) | def __repr__(self):
    method __getattr__ (line 21) | def __getattr__(self, name):
    method _set (line 29) | def _set(self, key, value):
    method _obj_gen (line 33) | def _obj_gen(model, n, cls, *args, **kwargs):
  class AttrIter (line 53) | class AttrIter(AgentSequence, Sequence):
    method __init__ (line 65) | def __init__(self, source, attr=None):
    method __repr__ (line 69) | def __repr__(self):
    method _iter_attr (line 73) | def _iter_attr(a, s):
    method __iter__ (line 77) | def __iter__(self):
    method __len__ (line 84) | def __len__(self):
    method __getitem__ (line 87) | def __getitem__(self, key):
    method __setitem__ (line 94) | def __setitem__(self, key, value):
    method __call__ (line 101) | def __call__(self, *args, **kwargs):
    method __eq__ (line 104) | def __eq__(self, other):
    method __ne__ (line 107) | def __ne__(self, other):
    method __lt__ (line 110) | def __lt__(self, other):
    method __le__ (line 113) | def __le__(self, other):
    method __gt__ (line 116) | def __gt__(self, other):
    method __ge__ (line 119) | def __ge__(self, other):
    method __add__ (line 122) | def __add__(self, v):
    method __sub__ (line 128) | def __sub__(self, v):
    method __mul__ (line 134) | def __mul__(self, v):
    method __truediv__ (line 140) | def __truediv__(self, v):
    method __iadd__ (line 146) | def __iadd__(self, v):
    method __isub__ (line 149) | def __isub__(self, v):
    method __imul__ (line 152) | def __imul__(self, v):
    method __itruediv__ (line 155) | def __itruediv__(self, v):
  function _random (line 161) | def _random(model, gen, obj_list, n=1, replace=False):
  class AgentList (line 182) | class AgentList(AgentSequence, list):
    method __init__ (line 256) | def __init__(self, model, objs=(), cls=None, *args, **kwargs):
    method __setattr__ (line 263) | def __setattr__(self, name, value):
    method __add__ (line 273) | def __add__(self, other):
    method select (line 278) | def select(self, selection):
    method random (line 287) | def random(self, n=1, replace=False):
    method sort (line 301) | def sort(self, var_key, reverse=False):
    method shuffle (line 312) | def shuffle(self):
  class AgentDList (line 318) | class AgentDList(AgentSequence, ListDict):
    method __init__ (line 351) | def __init__(self, model, objs=(), cls=None, *args, **kwargs):
    method __setattr__ (line 366) | def __setattr__(self, name, value):
    method __add__ (line 376) | def __add__(self, other):
    method random (line 381) | def random(self, n=1, replace=False):
    method select (line 395) | def select(self, selection):
    method sort (line 405) | def sort(self, var_key, reverse=False):
    method shuffle (line 417) | def shuffle(self):
    method buffer (line 422) | def buffer(self):
  class AgentSet (line 428) | class AgentSet(AgentSequence, set):
    method __init__ (line 446) | def __init__(self, model, objs=(), cls=None, *args, **kwargs):
  class AgentIter (line 454) | class AgentIter(AgentSequence):
    method __init__ (line 457) | def __init__(self, model, source=()):
    method __getitem__ (line 461) | def __getitem__(self, item):
    method __iter__ (line 465) | def __iter__(self):
    method __len__ (line 468) | def __len__(self):
    method __setattr__ (line 471) | def __setattr__(self, name, value):
    method to_list (line 481) | def to_list(self):
    method to_dlist (line 485) | def to_dlist(self):
  class AgentDListIter (line 490) | class AgentDListIter(AgentIter):
    method __init__ (line 493) | def __init__(self, model, source=(), shuffle=False, buffer=False):
    method __iter__ (line 499) | def __iter__(self):
    method buffer (line 509) | def buffer(self):
    method shuffle (line 513) | def shuffle(self):
    method _buffered_iter (line 517) | def _buffered_iter(self):

FILE: agentpy/space.py
  class Space (line 19) | class Space(SpatialEnvironment):
    method __init__ (line 55) | def __init__(self, model, shape, torus=False, **kwargs):
    method agents (line 72) | def agents(self):
    method kdtree (line 76) | def kdtree(self):
    method add_agents (line 93) | def add_agents(self, agents, positions=None, random=False):
    method remove_agents (line 124) | def remove_agents(self, agents):
    method _border_behavior (line 133) | def _border_behavior(position, shape, torus):
    method move_to (line 152) | def move_to(self, agent, pos):
    method move_by (line 164) | def move_by(self, agent, path):
    method neighbors (line 174) | def neighbors(self, agent, distance):
    method select (line 194) | def select(self, center, radius):

FILE: agentpy/tools.py
  class AgentpyError (line 9) | class AgentpyError(Exception):
  function make_none (line 13) | def make_none(*args, **kwargs):
  class InfoStr (line 17) | class InfoStr(str):
    method __repr__ (line 19) | def __repr__(self):
  function make_matrix (line 23) | def make_matrix(shape, loc_type=make_none, list_type=list, pos=None):
  function make_list (line 36) | def make_list(element, keep_none=False):
  function param_tuples_to_salib (line 50) | def param_tuples_to_salib(param_ranges_tuples):
  class AttrDict (line 65) | class AttrDict(dict):
    method __init__ (line 80) | def __init__(self, *args, **kwargs):
    method __getattr__ (line 85) | def __getattr__(self, name):
    method __setattr__ (line 92) | def __setattr__(self, name, value):
    method __delattr__ (line 95) | def __delattr__(self, item):
    method _short_repr (line 98) | def _short_repr(self):
  class ListDict (line 103) | class ListDict(Sequence):
    method __init__ (line 107) | def __init__(self, iterable):
    method __iter__ (line 113) | def __iter__(self):
    method __len__ (line 116) | def __len__(self):
    method __getitem__ (line 119) | def __getitem__(self, item):
    method __contains__ (line 122) | def __contains__(self, item):
    method extend (line 125) | def extend(self, seq):
    method append (line 129) | def append(self, item):
    method replace (line 135) | def replace(self, old_item, new_item):
    method remove (line 140) | def remove(self, item):
    method pop (line 147) | def pop(self, index):

FILE: agentpy/visualization.py
  function animate (line 17) | def animate(model, fig, axs, plot, steps=None, seed=None,
  function _apply_colors (line 99) | def _apply_colors(grid, color_dict, convert):
  function gridplot (line 125) | def gridplot(grid, color_dict=None, convert=False, ax=None, **kwargs):

FILE: docs/agentpy_demo.py
  class MoneyAgent (line 7) | class MoneyAgent(ap.Agent):
    method setup (line 9) | def setup(self):
    method wealth_transfer (line 12) | def wealth_transfer(self):
  class MoneyModel (line 21) | class MoneyModel(ap.Model):
    method setup (line 23) | def setup(self):
    method step (line 27) | def step(self):

FILE: tests/test_datadict.py
  function test_combine_vars (line 14) | def test_combine_vars():
  class MyModel (line 71) | class MyModel(ap.Model):
    method step (line 72) | def step(self):
  function test_repr (line 80) | def test_repr():
  class AgentType1 (line 87) | class AgentType1(ap.Agent):
    method setup (line 88) | def setup(self):
    method action (line 91) | def action(self):
  class AgentType2 (line 95) | class AgentType2(AgentType1):
    method setup (line 96) | def setup(self):
    method action (line 100) | def action(self):
  class EnvType3 (line 104) | class EnvType3(ap.Agent):
    method setup (line 105) | def setup(self):
    method action (line 109) | def action(self):
  class EnvType4 (line 113) | class EnvType4(ap.Agent):
    method setup (line 114) | def setup(self):
    method action (line 117) | def action(self):
  class ModelType0 (line 121) | class ModelType0(ap.Model):
    method setup (line 123) | def setup(self):
    method step (line 132) | def step(self):
    method end (line 136) | def end(self):
  function test_testing_model (line 140) | def test_testing_model():
  function arrange_things (line 158) | def arrange_things(results):
  function test_datadict_arrange_for_single_run (line 172) | def test_datadict_arrange_for_single_run():
  function test_datadict_arrange_for_multi_run (line 190) | def test_datadict_arrange_for_multi_run():
  function test_datadict_arrange_measures (line 206) | def test_datadict_arrange_measures():
  function test_datadict_arrange_variables (line 214) | def test_datadict_arrange_variables():
  function test_automatic_loading (line 222) | def test_automatic_loading():
  function test_saved_equals_loaded (line 246) | def test_saved_equals_loaded():
  class WeirdObject (line 265) | class WeirdObject:
  function test_save_load (line 269) | def test_save_load():
  function test_load_unreadable (line 297) | def test_load_unreadable():
  class SobolModel (line 308) | class SobolModel(ap.Model):
    method step (line 309) | def step(self):
  function test_calc_sobol (line 314) | def test_calc_sobol():

FILE: tests/test_examples.py
  function test_WealthModel (line 7) | def test_WealthModel():
  function test_SegregationModel (line 18) | def test_SegregationModel():

FILE: tests/test_experiment.py
  class MyModel (line 11) | class MyModel(ap.Model):
    method setup (line 13) | def setup(self):
  function test_basics (line 18) | def test_basics():
  function test_parallel_processing (line 30) | def test_parallel_processing():
  function test_random (line 50) | def test_random():

FILE: tests/test_grid.py
  function make_grid (line 7) | def make_grid(s, n=0, track_empty=False, agent_cls=ap.Agent):
  function test_general (line 15) | def test_general():
  function test_add_agents (line 21) | def test_add_agents():
  function test_remove (line 95) | def test_remove():
  function test_grid_iter (line 113) | def test_grid_iter():
  function test_attr_grid (line 122) | def test_attr_grid():
  function test_apply (line 127) | def test_apply():
  function test_move (line 132) | def test_move():
  function test_move_empty_multiple_agents (line 147) | def test_move_empty_multiple_agents():
  function test_move_torus (line 162) | def test_move_torus():
  function test_neighbors (line 190) | def test_neighbors():
  function test_neighbors_with_torus (line 198) | def test_neighbors_with_torus():
  function test_field (line 232) | def test_field():
  function test_record_positions (line 257) | def test_record_positions():

FILE: tests/test_init.py
  function test_version (line 5) | def test_version():

FILE: tests/test_model.py
  function test_run (line 9) | def test_run():
  function test_default_parameter_choice (line 30) | def test_default_parameter_choice():
  function test_report_and_as_function (line 36) | def test_report_and_as_function():
  function test_update_parameters (line 52) | def test_update_parameters():
  function test_sim_methods (line 61) | def test_sim_methods():
  function test_run_seed (line 87) | def test_run_seed():
  function test_stop (line 105) | def test_stop():
  function test_setup (line 119) | def test_setup():
  function test_create_output (line 154) | def test_create_output():
  function test_report_seed (line 175) | def test_report_seed():

FILE: tests/test_network.py
  function test_add_agents (line 6) | def test_add_agents():
  function test_move_agent (line 42) | def test_move_agent():
  function test_remove_agents (line 63) | def test_remove_agents():

FILE: tests/test_objects.py
  function test_basics (line 5) | def test_basics():
  function test_record (line 20) | def test_record():
  function test_record_all (line 35) | def test_record_all():

FILE: tests/test_sample.py
  function test_repr (line 7) | def test_repr():
  function test_seed (line 18) | def test_seed():
  function test_errors (line 31) | def test_errors():
  function test_linspace_product (line 37) | def test_linspace_product():
  function test_linspace_zip (line 76) | def test_linspace_zip():
  function test_sample_saltelli (line 85) | def test_sample_saltelli():
  function test_sample_saltelli_second (line 111) | def test_sample_saltelli_second():

FILE: tests/test_sequences.py
  function test_basics (line 7) | def test_basics():
  function test_kwargs (line 40) | def test_kwargs():
  function test_add (line 57) | def test_add():
  function test_agent_group (line 81) | def test_agent_group():
  function test_attr_list (line 126) | def test_attr_list():
  function test_select (line 154) | def test_select():
  function test_random (line 179) | def test_random():
  function test_sort (line 204) | def test_sort():
  function test_arithmetics (line 223) | def test_arithmetics():
  function test_remove (line 262) | def test_remove():

FILE: tests/test_space.py
  function make_space (line 7) | def make_space(s, n=0, torus=False):
  function test_general (line 18) | def test_general():
  function test_KDTree (line 25) | def test_KDTree():
  function test_add_agents_random (line 35) | def test_add_agents_random():
  function test_remove (line 43) | def test_remove():
  function test_positions (line 52) | def test_positions():

FILE: tests/test_tools.py
  function test_InfoStr (line 7) | def test_InfoStr():
  function test_make_list (line 11) | def test_make_list():
  function test_make_matrix (line 20) | def test_make_matrix():
  function test_attr_dict (line 30) | def test_attr_dict():
  function test_ListDict (line 43) | def test_ListDict():

FILE: tests/test_visualization.py
  function test_gridplot (line 7) | def test_gridplot():
  function test_animation (line 34) | def test_animation():
Condensed preview — 76 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (7,150K chars).
[
  {
    "path": ".github/dependabot.yml",
    "chars": 218,
    "preview": "version: 2\nupdates:\n  # Maintain dependencies for GitHub Actions\n  - package-ecosystem: \"github-actions\"\n    directory: "
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 1088,
    "preview": "# This workflow will upload a Python Package using Twine when a release is created\n# For more information see: https://d"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 717,
    "preview": "name: Run tests\n\non: [push, pull_request, workflow_dispatch]\n\npermissions:\n  contents: read\n\njobs:\n  build:\n\n    runs-on"
  },
  {
    "path": ".gitignore",
    "chars": 2205,
    "preview": "# Agentpy\r\nap_output/\r\ndocs/ap_output/\r\n\r\n# Pycharm\r\n.idea/\r\n\r\n# Frome here-on generated automatically\r\n# by https://www"
  },
  {
    "path": ".readthedocs.yml",
    "chars": 365,
    "preview": "version: 2\r\nformats: all\r\nbuild:\r\n  os: ubuntu-22.04 # Or ubuntu-20.04\r\n  tools:\r\n    python: \"3.9\" # Adjust based on yo"
  },
  {
    "path": "LICENSE",
    "chars": 1531,
    "preview": "BSD 3-Clause License\r\n\r\nCopyright (c) 2020-2021 Joël Foramitti\r\n\r\nRedistribution and use in source and binary forms, wit"
  },
  {
    "path": "README.md",
    "chars": 1412,
    "preview": "# AgentPy - Agent-based modeling in Python\r\n\r\n[![PyPI](https://img.shields.io/pypi/v/agentpy)](https://pypi.org/project/"
  },
  {
    "path": "agentpy/__init__.py",
    "chars": 1079,
    "preview": "\"\"\"\nAgentpy - Agent-based modeling in Python\nCopyright (c) 2020-2021 Joël Foramitti\n\nDocumentation: https://agentpy.read"
  },
  {
    "path": "agentpy/agent.py",
    "chars": 785,
    "preview": "\"\"\"\nAgentpy Agent Module\nContent: Agent Classes\n\"\"\"\n\nfrom .objects import Object\nfrom .sequences import AgentList\nfrom ."
  },
  {
    "path": "agentpy/datadict.py",
    "chars": 20983,
    "preview": "\"\"\"\nAgentpy Output Module\nContent: DataDict class for output data\n\"\"\"\n\nimport pandas as pd\nimport os\nfrom os import list"
  },
  {
    "path": "agentpy/examples.py",
    "chars": 3699,
    "preview": "import agentpy as ap\nimport numpy as np\n\n\ndef gini(x):\n\n    \"\"\" Calculate Gini Coefficient \"\"\"\n    # By Warren Weckesser"
  },
  {
    "path": "agentpy/experiment.py",
    "chars": 11771,
    "preview": "\"\"\"\nAgentpy Experiment Module\nContent: Experiment class\n\"\"\"\n\nimport warnings\nimport pandas as pd\nimport random as rd\n\nfr"
  },
  {
    "path": "agentpy/grid.py",
    "chars": 15816,
    "preview": "\"\"\"\r\nAgentpy Grid Module\r\nContent: Class for discrete spatial environments\r\n\"\"\"\r\n\r\nimport itertools\r\nimport numpy as np\r"
  },
  {
    "path": "agentpy/model.py",
    "chars": 16900,
    "preview": "\"\"\"\r\nAgentpy Model Module\r\nContent: Main class for agent-based models\r\n\"\"\"\r\n\r\nimport numpy as np\r\nimport pandas as pd\r\ni"
  },
  {
    "path": "agentpy/network.py",
    "chars": 5351,
    "preview": "\"\"\" Agentpy Network Module \"\"\"\r\n\r\nimport itertools\r\nimport networkx as nx\r\nfrom .objects import Object\r\nfrom .sequences "
  },
  {
    "path": "agentpy/objects.py",
    "chars": 4923,
    "preview": "\"\"\"\nAgentpy Objects Module\nContent: Base classes for agents and environment\n\"\"\"\n\nfrom .sequences import AgentList\nfrom ."
  },
  {
    "path": "agentpy/sample.py",
    "chars": 8536,
    "preview": "\"\"\"\nAgentpy Sampling Module\nContent: Sampling functions\n\"\"\"\n\n# TODO Latin Hypercube\n# TODO Random distribution samples\n#"
  },
  {
    "path": "agentpy/sequences.py",
    "chars": 18262,
    "preview": "\"\"\"\r\nAgentpy Lists Module\r\nContent: Lists for objects, environments, and agents\r\n\"\"\"\r\n\r\nimport itertools\r\nimport agentpy"
  },
  {
    "path": "agentpy/space.py",
    "chars": 7575,
    "preview": "\"\"\"\nAgentpy Space Module\nContent: Class for continuous spatial environments\n\"\"\"\n\n# TODO Add option of space without shap"
  },
  {
    "path": "agentpy/tools.py",
    "chars": 3846,
    "preview": "\"\"\"\nAgentpy Tools Module\nContent: Errors, generators, and base classes\n\"\"\"\n\nfrom numpy import ndarray\nfrom collections.a"
  },
  {
    "path": "agentpy/version.py",
    "chars": 185,
    "preview": "try:\n    from importlib import metadata\nexcept ImportError:\n    # Running on pre-3.8 Python\n    import importlib_metadat"
  },
  {
    "path": "agentpy/visualization.py",
    "chars": 5295,
    "preview": "\"\"\"\nAgentpy Visualization Module\nContent: Animations and Gridplot\n\"\"\"\n\nimport numpy as np\nimport pandas as pd\nimport mat"
  },
  {
    "path": "docs/Makefile",
    "chars": 634,
    "preview": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the "
  },
  {
    "path": "docs/_static/css/custom.css",
    "chars": 167,
    "preview": "/* Increase max width */\n.wy-nav-content {\n    max-width: 850px !important;\n}\n\n/* For alignment of jshtml animations */\n"
  },
  {
    "path": "docs/about.rst",
    "chars": 985,
    "preview": ".. currentmodule:: agentpy\n\n=====\nAbout\n=====\n\nAgentpy has been created by Joël Foramitti and is\navailable under the ope"
  },
  {
    "path": "docs/agentpy_button_network.ipynb",
    "chars": 32481,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Button network\\n\",\n    \"\\n\",\n    "
  },
  {
    "path": "docs/agentpy_demo.py",
    "chars": 852,
    "preview": "import agentpy as ap\n\n\n\n\n\nclass MoneyAgent(ap.Agent):\n\n    def setup(self):\n        self.wealth = 1\n\n    def wealth_tran"
  },
  {
    "path": "docs/agentpy_forest_fire.ipynb",
    "chars": 847557,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Forest fire\"\n   ]\n  },\n  {\n   \"ce"
  },
  {
    "path": "docs/agentpy_segregation.ipynb",
    "chars": 617517,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Segregation\"\n   ]\n  },\n  {\n   \"ce"
  },
  {
    "path": "docs/agentpy_virus_spread.ipynb",
    "chars": 4874376,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Virus spread\"\n   ]\n  },\n  {\n   \"c"
  },
  {
    "path": "docs/agentpy_wealth_transfer.ipynb",
    "chars": 38452,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Wealth transfer\"\n   ]\n  },\n  {\n  "
  },
  {
    "path": "docs/changelog.rst",
    "chars": 13630,
    "preview": ".. currentmodule:: agentpy\r\n\r\n=========\r\nChangelog\r\n=========\r\n\r\n0.1.5 (December 2021)\r\n---------------------\r\n\r\n- :func"
  },
  {
    "path": "docs/conf.py",
    "chars": 3346,
    "preview": "# Configuration file for the Sphinx documentation builder.\r\n# https://www.sphinx-doc.org/en/master/usage/configuration.h"
  },
  {
    "path": "docs/contributing.rst",
    "chars": 3556,
    "preview": ".. currentmodule:: agentpy\n\n==========\nContribute\n==========\n\nContributions are welcome, and they are greatly appreciate"
  },
  {
    "path": "docs/guide.rst",
    "chars": 525,
    "preview": "===========\nUser Guides\n===========\n\nThis section contains interactive notebooks with common applications of the agentpy"
  },
  {
    "path": "docs/guide_ema.ipynb",
    "chars": 11378,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"convenient-principle\",\n   \"metadata\": {},\n   \"source\": [\n    \"# "
  },
  {
    "path": "docs/guide_interactive.ipynb",
    "chars": 8118,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"economic-purple\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Inter"
  },
  {
    "path": "docs/guide_random.ipynb",
    "chars": 40736,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"headed-amino\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Randomne"
  },
  {
    "path": "docs/index.rst",
    "chars": 2300,
    "preview": ".. currentmodule:: agentpy\r\n\r\n========================================\r\nAgentPy - Agent-based modeling in Python\r\n======"
  },
  {
    "path": "docs/installation.rst",
    "chars": 1703,
    "preview": ".. currentmodule:: agentpy\r\n.. highlight:: shell\r\n\r\n============\r\nInstallation\r\n============\r\n\r\nTo install the latest re"
  },
  {
    "path": "docs/make.bat",
    "chars": 795,
    "preview": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sp"
  },
  {
    "path": "docs/model_library.rst",
    "chars": 527,
    "preview": "=============\r\nModel Library\r\n=============\r\n\r\nWelcome to the agentpy model library.\r\nBelow you can find a set of demons"
  },
  {
    "path": "docs/overview.rst",
    "chars": 11768,
    "preview": ".. currentmodule:: agentpy\r\n\r\n========\r\nOverview\r\n========\r\n\r\nThis section provides an overview over the main classes an"
  },
  {
    "path": "docs/reference.rst",
    "chars": 360,
    "preview": ".. currentmodule:: agentpy\r\n\r\n=============\r\nAPI Reference\r\n=============\r\n\r\n.. :caption: Contents:\r\n.. toctree::\r\n   :m"
  },
  {
    "path": "docs/reference_agents.rst",
    "chars": 378,
    "preview": ".. currentmodule:: agentpy\r\n\r\n======\r\nAgents\r\n======\r\n\r\nAgent-based models can contain multiple agents of different type"
  },
  {
    "path": "docs/reference_data.rst",
    "chars": 655,
    "preview": ".. currentmodule:: agentpy\r\n\r\n=============\r\nData analysis\r\n=============\r\n\r\nThis module offers tools to access, arrange"
  },
  {
    "path": "docs/reference_environments.rst",
    "chars": 968,
    "preview": ".. currentmodule:: agentpy\r\n\r\n============\r\nEnvironments\r\n============\r\n\r\nEnvironments are objects in which agents can i"
  },
  {
    "path": "docs/reference_examples.rst",
    "chars": 303,
    "preview": ".. currentmodule:: agentpy.examples\n\n========\nExamples\n========\n\nThe following example models are presented in the :doc:"
  },
  {
    "path": "docs/reference_experiment.rst",
    "chars": 111,
    "preview": ".. currentmodule:: agentpy\r\n\r\n===========\r\nExperiments\r\n===========\r\n\r\n.. autoclass:: Experiment\r\n    :members:"
  },
  {
    "path": "docs/reference_grid.rst",
    "chars": 230,
    "preview": ".. currentmodule:: agentpy\r\n\r\n======================\r\nDiscrete spaces (Grid)\r\n======================\r\n\r\n.. autoclass:: G"
  },
  {
    "path": "docs/reference_model.rst",
    "chars": 758,
    "preview": ".. currentmodule:: agentpy\r\n\r\n==================\r\nAgent-based models\r\n==================\r\n\r\nThe :class:`Model` contains "
  },
  {
    "path": "docs/reference_network.rst",
    "chars": 250,
    "preview": ".. currentmodule:: agentpy\r\n\r\n==========================\r\nGraph topologies (Network)\r\n==========================\r\n\r\n.. a"
  },
  {
    "path": "docs/reference_other.rst",
    "chars": 95,
    "preview": ".. currentmodule:: agentpy\r\n\r\n=====\r\nOther\r\n=====\r\n\r\n.. autoclass:: AttrDict\r\n    :members:\r\n\r\n"
  },
  {
    "path": "docs/reference_sample.rst",
    "chars": 291,
    "preview": ".. currentmodule:: agentpy\r\n\r\n=================\r\nParameter samples\r\n=================\r\n\r\nValue sets and ranges\r\n########"
  },
  {
    "path": "docs/reference_sequences.rst",
    "chars": 1293,
    "preview": ".. currentmodule:: agentpy\r\n\r\n=========\r\nSequences\r\n=========\r\n\r\nThis module offers various data structures to create an"
  },
  {
    "path": "docs/reference_space.rst",
    "chars": 175,
    "preview": ".. currentmodule:: agentpy\r\n\r\n=========================\r\nContinuous spaces (Space)\r\n=========================\r\n\r\n.. auto"
  },
  {
    "path": "docs/reference_visualization.rst",
    "chars": 132,
    "preview": ".. currentmodule:: agentpy\r\n\r\n=============\r\nVisualization\r\n=============\r\n\r\n.. autofunction:: animate\r\n\r\n.. autofunctio"
  },
  {
    "path": "paper/paper.bib",
    "chars": 3492,
    "preview": "% Encoding: windows-1252\r\n\r\n@Article{Madsen2019,\r\n  author    = {Jens Koed Madsen and Richard Bailey and Ernesto Carrell"
  },
  {
    "path": "paper/paper.md",
    "chars": 9460,
    "preview": "---\r\ntitle: 'AgentPy: A package for agent-based modeling in Python'\r\ntags:\r\n  - Agent-based modeling\r\n  - Complex system"
  },
  {
    "path": "setup.cfg",
    "chars": 729,
    "preview": "[metadata]\r\nversion = 0.1.6.dev\r\n\r\n[options]\r\npackages = find:\r\npython_requires = >=3.6\r\nsetup_requires = pytest-runner\r"
  },
  {
    "path": "setup.py",
    "chars": 710,
    "preview": "import setuptools\r\n\r\nwith open(\"README.md\", \"r\") as fh:\r\n    long_description = fh.read()\r\n\r\nsetuptools.setup(\r\n    name"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/test_datadict.py",
    "chars": 10381,
    "preview": "import pytest\r\nimport agentpy as ap\r\nimport numpy as np\r\nimport pandas as pd\r\nimport shutil\r\nimport os\r\n\r\nfrom agentpy.t"
  },
  {
    "path": "tests/test_examples.py",
    "chars": 647,
    "preview": "import pytest\nimport agentpy as ap\nfrom agentpy.examples import *\n\n# Test that examples run without errors\n\ndef test_Wea"
  },
  {
    "path": "tests/test_experiment.py",
    "chars": 2256,
    "preview": "import pytest\r\nimport agentpy as ap\r\nimport pandas as pd\r\nimport shutil\r\nimport os\r\nimport multiprocessing as mp\r\n\r\nfrom"
  },
  {
    "path": "tests/test_grid.py",
    "chars": 8251,
    "preview": "import pytest\r\nimport agentpy as ap\r\nimport numpy as np\r\nfrom agentpy.tools import AgentpyError\r\n\r\n\r\ndef make_grid(s, n="
  },
  {
    "path": "tests/test_init.py",
    "chars": 143,
    "preview": "import agentpy as ap\nimport pkg_resources\n\n\ndef test_version():\n\n    assert ap.__version__ == pkg_resources.get_distribu"
  },
  {
    "path": "tests/test_model.py",
    "chars": 5181,
    "preview": "import pytest\nimport numpy as np\nimport agentpy as ap\nimport random\n\nfrom agentpy.tools import AgentpyError\n\n\ndef test_r"
  },
  {
    "path": "tests/test_network.py",
    "chars": 2129,
    "preview": "import pytest\r\nimport networkx as nx\r\nimport agentpy as ap\r\n\r\n\r\ndef test_add_agents():\r\n\r\n    # Add agents to existing n"
  },
  {
    "path": "tests/test_objects.py",
    "chars": 1143,
    "preview": "import pytest\r\nimport agentpy as ap\r\nfrom agentpy.tools import AgentpyError\r\n\r\ndef test_basics():\r\n    model = ap.Model("
  },
  {
    "path": "tests/test_sample.py",
    "chars": 4520,
    "preview": "import pytest\r\nimport agentpy as ap\r\nfrom SALib.sample import saltelli\r\nfrom agentpy.tools import AgentpyError\r\n\r\n\r\ndef "
  },
  {
    "path": "tests/test_sequences.py",
    "chars": 8540,
    "preview": "import pytest\r\nimport agentpy as ap\r\nimport numpy as np\r\nfrom agentpy.tools import AgentpyError\r\n\r\n\r\ndef test_basics():\r"
  },
  {
    "path": "tests/test_space.py",
    "chars": 2656,
    "preview": "import pytest\nimport agentpy as ap\nimport numpy as np\nimport scipy\n\n\ndef make_space(s, n=0, torus=False):\n\n    model = a"
  },
  {
    "path": "tests/test_tools.py",
    "chars": 1313,
    "preview": "import pytest\r\nimport agentpy as ap\r\n\r\nfrom agentpy.tools import *\r\n\r\n\r\ndef test_InfoStr():\r\n    assert InfoStr('yay')._"
  },
  {
    "path": "tests/test_visualization.py",
    "chars": 1556,
    "preview": "import pytest\r\nimport agentpy as ap\r\nimport numpy as np\r\nimport matplotlib.pyplot as plt\r\n\r\n\r\ndef test_gridplot():\r\n    "
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the JoelForamitti/agentpy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 76 files (27.8 MB), approximately 1.7M tokens, and a symbol index with 341 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!