Contributors’ guide

Welcome! You are now entering the contributors’ guide. This page is for people interested in the details needed to contribute to NNMT. We want this Python toolbox to be a place for the Neuroscientific community to collect analytical methods for neuronal network model analysis. So we are glad that you made your way here and hope that you will find what you are looking for. Good luck and have a lot of fun!

Contact us

Contributions to our GitHub repository can be made via the GitHub fork and pull request workflow.

Any concrete suggestions or problems should be reported as issues on GitHub.

Setting up a developer environment

Fork NNMT on GitHub and download the source code to your local machine:

git clone git@github.com:<your-github-profile>/nnmt.git
cd nnmt

Then you need to install the package and all requirements. We recommend using conda and the provided environment.yaml:

conda env create -f environment.yaml
conda activate nnmt

Alternatively, you might install NNMT and its requirements in editable mode using pip:

pip install -e .

After the installation, we recommend running the tests, which will take a few minutes (see Tests for more details):

pytest

If all tests pass (except for skipped tests), you should be ready to go.

Structure of repository

Sketch of structure of NNMT repository

Design principles

The best description of the ideas behind the design of the toolbox can be found in our paper:

A mean-field toolbox for spiking neuronal network model analysis

Here we collect some principles that should be followed when writing new code for the toolbox:

  • All calculations are to be done in SI units. We do not use Python quantity packages like pint or quantitites inside the actual calculations because this often causes problems in combination with special functions (e.g. erf or zetac from SciPy). Although we do use pint for converting parameters including units from yaml files to dictionaries. For more detail see the models section.
  • Resuse as much code as possible. If two functions in different submodules (e.g. in lif.exp and lif.delta) use the same function, the function should be put into a higher module at a higher level. In the lif module we introduced the _general module which serves this purpose. Keep in mind that if two modules that need a similar function are not both part of the same submodule, it might be sensible to combine them in a new submodule.
  • The package’s structure is supposed to be adapted in a flexible, non-dogmatic way. If the canonical split into neuron type, synapse type doesn’t fit, feel free to adjust the submodule structure accordingly. An inspiration to us was the submodule structure of SciPy, which (at least it seemed so to us) is rather free and fitted to the needs at hand.

Tools

Tools are Python functions and constitute the core of NNMT. They actually perform the calculations.

We decided to sort them into different submodules. Originally, starting off with tools for LIF neurons, we thought the most sensible split is according to neuron type (e.g. LIF, binary, etc.) and then, if required, another split according to synapse type (e.g. delta, exponential). But analytical theories of neuronal network models are quite versatile. Therefore other ways of sorting the tools might be more appropriate for different tools.

It is vital that all tools have meaningful names and comprehensive docstrings (see documentation section for more details).

If you make any well-thought-out decisions in the implementation of a tool, for example for optimization purposes, you need to write comments that clearly state the reasons for you to do so. Otherwise, someone else might come across your lines of code a few years later and change it, because it looked unnecessarily cumbersome at first sight, thereby destroying all your precious efforts.

_Tools

Tools with an underscore are where the job is done. Underscored tools should

  • get all parameters needed for a calculation directly as arguments.
  • perform the calculations.
  • assert that all arguments have valid values. For example, they need to check whether parameters that only should be positive are negative. You could use the check decorators defined in nnmt.utils for this.
  • raise warnings if valid parameter regime is left. For example if the assumptions made in the underlying theory are not fulfilled by the parameters.
  • raise errors if return values are meaningless, for example if negative rates would be returned.

Have a look at the source code of nnmt.lif.exp._firing_rate_shift() if you would like to see an example.

Wrappers

To make an underscored tool compatible with the convience layer, a.k.a. models, it gets a wrapper withouth an underscore. The non underscored wrappers should

  • expect an nnmt.model as argument.
  • check that all parameters and results needed are stored in the model.
  • invoces the nnmt.utils._cache() function to store the results in the model instance.

Have a look at the source code of nnmt.lif.exp.firing_rates() if you would like to see an example.

Models

Models are Python classes that serve as containers for network parameters, analysis parameters, and results. They come with some convenience methods for changing parameters, saving, and loading results.

Typically, one wants to define parameters in some sort of parameter file (we usually use yaml files for this), load them, and then calculate further, dependent parameters from these. The details of how these dependent parameters are calculated depend on the model that one is planning to investigate. Defining a custom model class allows users to do this in an organized setting.

A model should

  • be a subclass of the generic nnmt.models.Network class and invoke the parent __init__() method. The model’s __init__() method should start with

    def __init__(self, network_params=None, analysis_params=None, file=None):
        super().__init__(network_params, analysis_params, file)
    
  • implement the _instantiate() method:

    def _instantiate(self, new_network_params, new_analysis_params):
        return <MyModel>(new_network_params, new_analysis_params)
    

    _instantiate() is invoked when <MyModel>.change_parameters() is called and a new instance of the model is created.

  • calculate dependent parameters when instantiated. For that purpose, you can add methods to your subclass and call them in the __init__() method. Note that if you use yaml files to store parameters including units, the loaded parameters are Pint Quantity objects at this point. You might run into problems if imported functions from different packages are not compatible with Quantity objects.

  • call self._convert_param_dicts_to_base_units_and_strip_units() at the end of the __init__() method. This will convert all calculated parameters to SI units, strip the respective units off, and store them in the dictionary input_units.

The microcircuit model nnmt.models.Microcircuit is a good example of how a model class looks like.

If you would like to add your model to nnmt.models, do not forget to add

from .<my_model_module> import *

to nnmt.models.__init__.py.

Input and output functions

The submodule nnmt.input_output contains all functions which called for all input output related actions, like saving and loading results, or loading parameter files.

We have written a very basic wrapper which allows storing and loading Python dictionaries in HDF5 files.

Similarly, we have written wrappers which allow loading dictionaries of Pint Quantity objects as yaml files and vice versa.

Utility functions

The submodule nnmt.utils is a collection of convenient functions used frequently throughout NNMT.

There you find the nnmt.utils._cache() function, which is called by the wrappers of the underscored tools. It stores the results returned by a underscored tool in the result related dictionaries of the model that was passed to the wrapper. If you are looking for an example of how to apply the _cache function have a look at the source code of nnmt.lif.exp.firing_rates().

The module contains some decorators used to check that parameters passed to a _tool have valid values. E.g. nnmt.utils._check_positive_params() or nnmt.utils._check_k_in_fast_synaptic_regime().

Finally, it contains some utility functions for handling Pint Quantity objects.

Tests

All tools, models, and utilities should be tested using our pytest test suite. We have collected all the details in the section about Tests.

Documentation

We automatically create this documentation using Sphinx. Sphinx collects all the docstrings and the rst files in nnmt/docs/source/ and creates these beautiful documentation html files.

Compiling the docs

In order to compile the documentation, you have to change your working directory to nnmt/ and install and activate the provided conda environment

conda env create -f environment.yaml
conda activate nnmt

Change you working directory to nnmt/docs/ and run the following commands

make clean
make html

This will compile the documentation and create the folder build/. Now you can access the documentation using your preferred browser by opening the file build/html/index.html.

Special files

nnmt/docs/source/conf.py defines all the Sphinx configurations, which extensions are used, and details about the html output.

nnmt/docs/source/index.rst configures the access page to the documentation.

rst files

Everything you see in this documentation that is not part of a docstring is written in the rst files in nnmt/docs/source/.

Docstrings

We try to follow the PEP 8 standard and the NumPy docstring conventions as close as possible.

Dosctrings should be written using rst syntax, which allows cross-references and citations.

Modules

Each module needs to start with its own docstring, beginning with a short description of its content, followed by a list of the functions which are to be shown in the documentation. The functions can be divided by different headings, followed by an Sphinx autosummary directive. A generic example of such a docstring would be

'''
This module contains functions for doing awesome things.

My Favorites
************

.. autosummary::
    :toctree: _toctree/my_module/

    function1
    function2

'''

Example: nnmt.input_output

Functions

Wrappers of _tools should reference the respective _tools.

The docstrings of _tools should give a detailed explanation of all their arguments.

Examples: nnmt.lif.exp._firing_rates(), nnmt.lif.exp.firing_rates()

Models

Example: nnmt.models.Microcircuit

Versioning

We follow Semantic Versioning. To cite the linked page:

Given a version number MAJOR.MINOR.PATCH, increment the:

  1. MAJOR version when you make incompatible API changes,
  2. MINOR version when you add functionality in a backwards compatible manner, and
  3. PATCH version when you make backwards compatible bug fixes.

Before releasing a new version, the version number should be updated in

  • setup.py,
  • nnmt/__init__.py, and
  • nnmt/docs/source/conf.py,

a release note should be added, and the list of authors and contributors, as well as the acknowledgements should be updated if required.

How to publish a new release

Please follow our step-by-step guide for publishing a new release.