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
[](https://pypi.org/project/agentpy/)
[](https://github.com/JoelForamitti/agentpy/blob/master/LICENSE)
[](https://agentpy.readthedocs.io/en/latest/?badge=latest)
[](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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
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
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[](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.