deNEST

deNEST is a Python library for specifying networks and running simulations using the NEST simulator (https://nest-simulator.org).

deNEST allows the user to concisely specify large-scale networks and simulations in hierarchically-organized declarative parameter files.

From these parameter files, a network is instantiated in NEST (neurons & their projections), and a simulation is run in sequential steps (“sessions”), during which the network parameters can be modified and the network can be stimulated, recorded, etc.

Some advantages of the declarative approach:

  • Parameters and code are separated
  • Simulations are easier to reason about, reuse, and modify
  • Parameters are more readable and succinct
  • Parameter files can be easily version controlled and diffs are smaller and more interpretable
  • Clean separation between the specification of the “network” (the simulated neuronal system) and the “simulation” (structured stimulation and recording of the network), which facilitates running different experiments using the same network
  • Parameter exploration is more easily automated

To learn how to use deNEST, please see the Overview section and the Jupyter notebook tutorials:

Installation

Local

  1. Install NEST >= v2.14.0, <3.0 by following the instructions at http://www.nest-simulator.org/installation/.

  2. Set up a Python 3 environment and install deNEST with:

    pip install denest
    

Docker

A Docker image is provided with NEST 2.20 installed, based on nest-docker.

  1. From within the repo, build the image:

    docker build --tag denest .
    
  2. Run an interactive container:

    docker run \
      -it \
      --name denest_simulation \
      --volume $(pwd):/opt/data \
      --publish 8080:8080 \
      denest \
      /bin/bash
    
  3. Install deNEST within the container:

    pip install -e .
    
  4. Use deNEST from within the container.

For more information on how to use the NEST Docker image, see nest-docker.

Overview

Definitions

Network

  • We use the term network to mean a full network in NEST, consisting of layers of units with specific models, projections of specific types with specific synapse models amongst these layers, population recorders (multimeters, spike detectors) and projection recorders (weight recorder).
    • The full network is represented in deNEST by the Network class.
  • New NEST models (neuron and generator model, synapse model or recorder model) can be specified with arbitrary parameters. During network creation, models with specific parameters are created in NEST using a nest.CopyModel() or a nest.SetDefaults() call.
    • Synapse models are represented by the SynapseModel class in deNEST. All other models are represented by the Model class.
    • Neuron and generator models are specified as leaves of the network/neuron_models parameter subtree (see section below)
    • Synapse models are specified as leaves of the network/synapse_models parameter subtree (see “Network parameters” section below)
    • Recorder models are specified as leaves of the network/recorder_models parameter subtree (see “Network parameters” section below)
  • A layer is a NEST topological layer, created with a tp.CreateLayer() call in NEST. A population is all the nodes of the same model within a layer.
    • Layers are represented by the Layer or InputLayer class in deNEST.
    • Layers can be of the type InputLayer when they are composed of generators. An extra population of parrot neurons can be automatically created and connected one-to-one to the generators, such that recording of generators’ activity is possible. Additionally, InputLayer supports shifting the origin flag of stimulators at the start of a Session.
    • Layers are specified as leaves of the network/layers parameter subtree (see “Network parameters” section below).
  • A projection model is a template specifying parameters passed to tp.ConnectLayers, and that individual projections amongst populations can inherit from.
    • Projection models are represented by the ProjectionModel class in deNEST.
    • Projection models are specified as leaves of the network/projection_models parameter subtree (see “Network parameters” section below).
  • A projection is an individual projection between layers or populations, created with a tp.ConnectLayers() call. The parameters passed to tp.ConnectLayers() are those of the “projection model” of the specific projection.
    • The list of all individual projections within the network is specified in the 'projections' parameter of the network/topology parameter subtree (see “Network parameters” section below).
  • A population recorder is a recorder connected to all the nodes of a given population. A projection recorder is a recorder connected to all the synapses of a given projection. Recorders with arbitrary parameters can be defined by creating “recorder models”. However, currently only recorders based on the ‘multimeter’, ‘spike_detector’ and ‘weight_recorder’ NEST models are supported.
    • population and projection recorders are represented by the PopulationRecorder and ProjectionRecorder classes in deNEST.
    • The list of all population recorders and projection recorders are specified in the 'population_recorders' and 'projection_recorders' parameters of the network/recorders parameter subtree (See “Network parameters” section below).

Simulation

  • A session model is a template specifying parameters inherited by individual sessions.
    • session models are specified as leaves of the session_models parameter subtree (see “Simulation parameters” section below)
  • A session is a period of simulation of a network with specific inputs and parameters, and corresponds to a single nest.Simulate() call. The parameters used by a given session are inherited from its session model.
    • A session’s parameters define the operations that may be performed before running it:
      1. Modifying the state of some units (using the Network.set_state() method)
      2. (Possibly) shift the origin flag for the InputLayer stimulators
      3. (Possibly) deactivate the recorders for that session by setting their start flag to the end of the session
    • Individual sessions are represented by the Session object in deNEST. (see “Simulation parameters” section below)
  • A simulation is a full experiment. It is represented by the Simulation object in deNEST, which contains a Network object and a list of Session objects.
    • The list of sessions run during a simulation is specified by the sessions parameter of the simulation parameter subtree (eg: sessions: ['warmup', 'noise', 'grating', 'noise', 'grating']) (see “Simulation parameters” section below).

Overview of a full simulation

A full deNEST simulation consists of the following steps:

  1. Initialize simulation (Simulation.__init__(<params_tree>)())
    1. Initialize kernel: (Simulation.init_kernel(<kernel_subtree>)())
      1. Set NEST kernel parameters
      2. Set seed for NEST’s random generator.
    2. Create network (Simulation.create_network(<network_subtree>)()):
      1. Initialize the network objects (Network.__init__(<network_subtree)())
      2. Create the objects in NEST (Network.create())
    3. Initialize the sessions (Session.__init__())
    4. Save the simulation’s metadata
      • Create and clean the output directory
      • Save the full simulation parameter tree
      • Save deNEST and NEST version information
      • Save session times
      • Save network metadata
      • Save session metadata
  2. Run the simulation (Simulation.run()). This runs each session in turn:
    1. Initialize session (Session.initialize())
      • (Possibly) reset the network
      • (Possibly) inactivate recorders for the duration of the session
      • (Possibly) shift the origin of stimulator devices to the start of the session
      • (Possibly) Change some of the network’s parameters using the Network.set_state() method
        1. Change neuron parameters
        2. Change synapse parameters
    2. Call nest.Simulate().

Specifying the simulation parameters

All parameters used by deNEST are specified in tree-like YAML files which are converted to ParamsTree objects.

In this section, we describe the ParamsTree objects, the expected structure of the full parameter tree interpreted by deNEST, and the expected formats and parameters of each of the subtrees that define the various aspects of the network and simulation.

Main parameter file

To facilitate defining parameters in separate files, denest.run() and denest.load_trees() take as input a path to a YAML file containing the relative paths of the tree-like YAML files to merge so as to define the full parameter tree (for examples, see the Example declarative specification or the params/tree_paths.yml file in the repository.).

The ParamsTree class

The ParamsTree class is instantiated from tree-like nested dictionaries. At each node, two reserved keys contain the node’s data (called 'params' and 'nest_params'). All the other keys are interpreted as named children nodes.

The 'params' key contains data interpreted by deNEST, while the 'nest_params' key contains data passed to NEST without modification.

The ParamsTree class offers a tree structure with two useful characteristics:

  • Hierarchical inheritance of ancestor’s data: This provides a concise way of defining data for nested scopes. Data common to all leaves may be specified once in the root node, while more specific data may be specified further down the tree. Data lower within the tree overrides data higher in the tree. Ancestor nodes’ params and nest_params are inherited independently.
  • (Horizontal) merging of trees: ParamsTree objects can be merged horizontally with ParamsTree.merge(). During the merging of multiple params trees, the contents of the params and nest_params data keys of nodes at the same relative position are combined. This allows splitting the deNEST parameter trees in separate files for convenience, and overriding the data of a node anywhere in the tree while preserving hierarchical inheritance.

An example parameter tree

Below is an example of a YAML file with a tree-like structure that can be loaded and represented by the ParamsTree class:

network:
  neuron_models:
    ht_neuron:
      params:                     # params common to all leaves
        nest_model: ht_neuron
      nest_params:                # nest_params common to all leaves
        g_KL: 1.0
      cortical_excitatory:
        nest_params:
          tau_spike: 1.75
          tau_m: 16.0
        l1_exc:                   # leaf
        l2_exc:                   # leaf
          nest_params:
            g_KL: 2.0     # Overrides ancestor's value
      cortical_inhibitory:
        nest_params:
          tau_m: 8.0
        l1_inh:                   # leaf

This file can be loaded into a ParamsTree object. The leaves of the resulting ParamsTree and their respective data (params and nest_params) are as follows. Note the inheritance and override of ancestor data. The nested format above is more compact and less error prone when there are a lot of shared parameters between leaves.

l1_exc:
  params:
    nest_model: ht_neuron
  nest_params:
    g_KL: 1.0
    tau_spike: 1.75
    tau_m: 16.0
l2_exc:
  params:
    nest_model: ht_neuron
  nest_params:
    g_KL: 2.0
    tau_spike: 1.75
    tau_m: 16.0
l1_inh:
  params:
    nest_model: ht_neuron
  nest_params:
    g_KL: 1.0
    tau_m: 8.0

Full parameter tree: expected structure

All the aspects of the overall simulation are specified in specific named subtrees.

The overall ParamsTree passed to denest.Simulation() is expected to have no data and the following children:

  • simulation (ParamsTree). Defines input and output paths, and the simulation steps performed. The following parameters (params field) are recognized:

    • output_dir (str): Path to the output directory. (Default: 'output')
    • input_dir (str): Path to the directory in which input files are searched for for each session. (Default: 'input')
    • sessions (list(str)): Order in which sessions are run. Elements of the list should be the name of session models defined in the session_models parameter subtree (Default: [])
  • kernel (ParamsTree): Used for NEST kernel initialization. Refer to Simulation.init_kernel() for a description of kernel parameters.

  • session_models (ParamsTree): Parameter tree, the leaves of which define session models. Refer to Sessions() for a description of session parameters.

  • network (ParamsTree): Parameter tree defining the network in NEST. Refer to Network for a full description of network parameters.

"network" parameter tree: expected structure

All network parameters are specified in the network subtree, used to initialize the Network object.

The network subtree should have no data, and the following children are expected:

  • neuron_models (ParamsTree). Parameter tree, the leaves of which define neuron models. Each leaf is used to initialize a Model object
  • synapse_models (ParamsTree). Parameter tree, the leaves of which define synapse models. Each leaf is used to initialize a SynapseModel object
  • layers (ParamsTree). Parameter tree, the leaves of which define layers. Each leaf is used to initialize a Layer or InputLayer object depending on the value of their type params parameter.
  • projection_models (ParamsTree). Parameter tree, the leaves of which define projection models. Each leaf is used to initialize a ProjectionModel object.
  • recorder_models (ParamsTree). Parameter tree, the leaves of which define recorder models. Each leaf is used to initialize a Model object.
  • topology (ParamsTree). ParamsTree object without children, the params of which may contain a projections key specifying all the individual population-to-population projections within the network as a list. Projection objects are created from the topology ParamsTree object by the Network.build_projections method. Refer to this method for a description of the topology parameter.
  • recorders (ParamsTree). ParamsTree object without children, the params of which may contain a population_recorders and a projection_recorders key specifying all the network recorders. PopulationRecorder and ProjectionRecorder objects are created from the recorders ParamsTree object by the Network.build_recorders method. Refer to this method for a description of the recorders parameter.

Running a deNEST Simulation

  • From Python (e.g. in a Jupyter notebook):

    • Using the Simulation object to run the simulation step by step:

      import denest
      
      # Path to the parameter files to use
      params_path = 'params/tree_paths.yml'
      
      # Override some parameters loaded from the file
      overrides = [
      
        # Maybe change the nest kernel's settings ?
        {'kernel': {'nest_params': {'local_num_threads': 20}}},
      
        # Maybe change a parameter for all the projections at once ?
        {'network': {'projection_models': {'nest_params': {
            'allow_autapses': true
        }}}},
      ]
      
      # Load the parameters
      params = denest.load_trees(params_path, *overrides)
      
      # Initialize the simulation
      sim = denest.Simulation(params, output_dir='output')
      
      # Run the simulation (runs all the sessions)
      sim.run()
      
    • Using the denest.run() function to run the full simulation at once:

      import denest
      
      # Path to the parameter files to use
      params_path = 'params/tree_paths.yml'
      
      # Override parameters
      overrides = []
      
      denest.run(params_path, *overrides, output_dir=None)
      
  • From the command line:

    python -m denest <tree_paths.yml> [-o <output_dir>]
    

Example declarative specification

Here is an example parameter file (in YAML) that specifies a full simulation:

params: {}
nest_params: {}
session_models:
  params:
    reset_network: false
    record: true
    shift_origin: false
  nest_params: {}
  even_rate:
    params:
      simulation_time: 50.0
      unit_changes:
      - layers:
        - input_layer
        population_name: input_exc
        change_type: constant
        from_array: false
        nest_params:
          rate: 100.0
    nest_params: {}
  warmup:
    params:
      reset_network: true
      record: false
      simulation_time: 50.0
      unit_changes:
      - layers:
        - l1
        population_name: null
        change_type: constant
        from_array: false
        nest_params:
          V_m: -70.0
      - layers:
        - input_layer
        population_name: input_exc
        change_type: constant
        from_array: false
        nest_params:
          rate: 100.0
    nest_params: {}
  arbitrary_rate:
    params:
      simulation_time: 50.0
      unit_changes:
      - layers:
        - input_layer
        population_name: input_exc
        change_type: constant
        from_array: true
        nest_params:
          rate: ./input_layer_rates_5x5x1.npy
    nest_params: {}
simulation:
  params:
    sessions:
    - warmup
    - even_rate
    - arbitrary_rate
    output_dir: ./output
    input_dir: ./params/input
  nest_params: {}
kernel:
  params:
    extension_modules: []
    nest_seed: 94
  nest_params:
    local_num_threads: 20
    resolution: 1.0
    print_time: true
    overwrite_files: true
network:
  params: {}
  nest_params: {}
  neuron_models:
    params: {}
    nest_params: {}
    ht_neuron:
      params:
        nest_model: ht_neuron
      nest_params:
        g_peak_NaP: 0.5
        g_peak_h: 0.0
        g_peak_T: 0.0
        g_peak_KNa: 0.5
        g_KL: 1.0
        E_rev_NaP: 55.0
        g_peak_AMPA: 0.1
        g_peak_NMDA: 0.15
        g_peak_GABA_A: 0.33
        g_peak_GABA_B: 0.0132
        instant_unblock_NMDA: true
        S_act_NMDA: 0.4
        V_act_NMDA: -58.0
      cortical_inhibitory:
        params: {}
        nest_params:
          theta_eq: -53.0
          tau_theta: 1.0
          tau_spike: 0.5
          tau_m: 8.0
        l1_inh:
          params: {}
          nest_params: {}
        l2_inh:
          params: {}
          nest_params: {}
      cortical_excitatory:
        params: {}
        nest_params:
          theta_eq: -51.0
          tau_theta: 2.0
          tau_spike: 1.75
          tau_m: 16.0
        l1_exc:
          params: {}
          nest_params: {}
        l2_exc:
          params: {}
          nest_params: {}
    input_exc:
      params:
        nest_model: poisson_generator
      nest_params: {}
  layers:
    params:
      type: null
    nest_params:
      rows: 5
      columns: 5
      extent:
      - 8.0
      - 8.0
      edge_wrap: true
    input_area:
      params:
        type: InputLayer
        add_parrots: true
      nest_params: {}
      input_layer:
        params:
          populations:
            input_exc: 1
        nest_params: {}
    l1_area:
      params: {}
      nest_params: {}
      l1:
        params:
          populations:
            l1_exc: 2
            l1_inh: 1
        nest_params: {}
    l2_area:
      params: {}
      nest_params: {}
      l2:
        params:
          populations:
            l2_exc: 2
            l2_inh: 1
        nest_params: {}
  synapse_models:
    params: {}
    nest_params: {}
    static_synapse:
      params:
        nest_model: static_synapse_lbl
      nest_params: {}
      input_synapse_NMDA:
        params:
          target_neuron: ht_neuron
          receptor_type: NMDA
        nest_params: {}
      input_synapse_AMPA:
        params:
          target_neuron: ht_neuron
          receptor_type: AMPA
        nest_params: {}
    ht_synapse:
      params:
        nest_model: ht_synapse
        target_neuron: ht_neuron
      nest_params: {}
      GABA_B_syn:
        params:
          receptor_type: GABA_B
        nest_params: {}
      AMPA_syn:
        params:
          receptor_type: AMPA
        nest_params: {}
      GABA_A_syn:
        params:
          receptor_type: GABA_A
        nest_params: {}
      NMDA_syn:
        params:
          receptor_type: NMDA
        nest_params: {}
  topology:
    params:
      projections:
      - source_layers:
        - input_layer
        source_population: parrot_neuron
        target_layers:
        - l1
        target_population: l1_exc
        projection_model: input_projection_AMPA
      - source_layers:
        - input_layer
        source_population: parrot_neuron
        target_layers:
        - l1
        target_population: l1_inh
        projection_model: input_projection_AMPA
      - source_layers:
        - input_layer
        source_population: parrot_neuron
        target_layers:
        - l1
        target_population: l1_inh
        projection_model: input_projection_NMDA
      - source_layers:
        - l1
        source_population: l1_exc
        target_layers:
        - l1
        target_population: l1_exc
        projection_model: horizontal_exc
      - source_layers:
        - l1
        source_population: l1_exc
        target_layers:
        - l1
        target_population: l1_inh
        projection_model: horizontal_exc
      - source_layers:
        - l1
        source_population: l1_exc
        target_layers:
        - l2
        target_population: l2_exc
        projection_model: FF_exc
      - source_layers:
        - l1
        source_population: l1_exc
        target_layers:
        - l2
        target_population: l2_inh
        projection_model: FF_exc
    nest_params: {}
  recorder_models:
    params: {}
    nest_params:
      record_to:
      - file
      - memory
      withgid: true
      withtime: true
    spike_detector:
      params:
        nest_model: spike_detector
      nest_params: {}
    weight_recorder:
      params:
        nest_model: weight_recorder
      nest_params:
        record_to:
        - file
        - memory
        withport: false
        withrport: true
    multimeter:
      params:
        nest_model: multimeter
      nest_params:
        interval: 1.0
        record_from:
        - V_m
  recorders:
    params:
      population_recorders:
      - layers: []
        populations: []
        model: multimeter
      - layers:
        - l2
        populations:
        - l2_inh
        model: multimeter
      - layers: null
        populations:
        - l2_exc
        model: multimeter
      - layers:
        - l1
        populations: null
        model: multimeter
      - layers: null
        populations: null
        model: spike_detector
      projection_recorders:
      - source_layers:
        - input_layer
        source_population: parrot_neuron
        target_layers:
        - l1
        target_population: l1_exc
        projection_model: input_projection_AMPA
        model: weight_recorder
      - source_layers:
        - l1
        source_population: l1_exc
        target_layers:
        - l1
        target_population: l1_exc
        projection_model: horizontal_exc
        model: weight_recorder
    nest_params: {}
  projection_models:
    params:
      type: topological
    nest_params:
      allow_autapses: false
      allow_multapses: false
      allow_oversized_mask: true
    horizontal_inh:
      params: {}
      nest_params:
        connection_type: divergent
        synapse_model: GABA_A_syn
        mask:
          circular:
            radius: 7.0
        kernel:
          gaussian:
            p_center: 0.25
            sigma: 7.5
        weights: 1.0
        delays:
          uniform:
            min: 1.75
            max: 2.25
    input_projection:
      params: {}
      nest_params:
        connection_type: convergent
        mask:
          circular:
            radius: 12.0
        kernel: 0.8
        weights: 1.0
        delays:
          uniform:
            min: 1.75
            max: 2.25
      input_projection_AMPA:
        params: {}
        nest_params:
          synapse_model: input_synapse_AMPA
      input_projection_NMDA:
        params: {}
        nest_params:
          synapse_model: input_synapse_NMDA
    horizontal_exc:
      params: {}
      nest_params:
        connection_type: divergent
        synapse_model: AMPA_syn
        mask:
          circular:
            radius: 12.0
        kernel:
          gaussian:
            p_center: 0.05
            sigma: 7.5
        weights: 1.0
        delays:
          uniform:
            min: 1.75
            max: 2.25
    FF_exc:
      params: {}
      nest_params:
        connection_type: convergent
        synapse_model: AMPA_syn
        mask:
          circular:
            radius: 12.0
        kernel: 0.8
        weights: 1.0
        delays:
          uniform:
            min: 1.75
            max: 2.25