This page provides a step-by-step walkthrough of the basic features of NNGT.

To run this tutorial, it is recommended to use either IPython or Jupyter, since they will provide automatic autocompletion of the various functions, as well as easy access to the docstring help.

First, import the NNGT package:

>>> import nngt

Then, you will be able to use the help from IPython by typing, for instance:

>>> nngt.Graph?

In Jupyter, the docstring can be viewed using Shift + Tab.

The source file for the tutorial can be found here: doc/examples/introductory_tutorial.py.


NNGT properties and configuration#

Upon loading, NNGT will display its current configuration, e.g.:

# ----------- #
# NNGT loaded #
# ----------- #
Graph library:  igraph 0.7.1
Multithreading: True (1 thread)
MPI:            False
Plotting:       True
NEST support:   NEST 2.14.0
Shapely:        1.6.1
SVG support:    True
DXF support:    False
Database:       False

Let’s walk through this configuration:

  • the backend used here is igraph, so all graph-theoretical tools will be derived from those of the igraph library and we’re using version 0.7.1.
  • Multithreaded algorithms will be used, currently running on only one thread (see Parallelism for more details)
  • MPI algorithms are not in use (you cannot use both MT and MPI at the same time)
  • Plotting is available because the matplotlib library is installed
  • NEST is installed on the machine (version 2.14), so NNGT automatically loaded it
  • Shapely is also available, which allows the creation of complex structures for space-embedded networks (see Geometry module for more details)
  • Importing SVG files to generate spatial structures is possible, meaning that the svg.path module is installed.
  • Importing DXF files to generate spatial structures is not possible because the dxfgrabber module is not installed.
  • Using the database is not possible because peewee is not installed.

In general, most of NNGT options can be found/set through the get_config()/set_config() functions, or made permanent by modifying the ~/.nngt/nngt.conf configuration file.

The Graph object#

Basic functions#

Let’s create an empty Graph:

g = nngt.Graph()

We can then add some nodes to it

g.new_node(10)            # create nodes 0, 1, ... to 9
print(g.node_nb(), '\n')  # returns 10

And create edges between these nodes:

g.new_edge(1, 4)          # create one connection going from 1 to 4
print(g.edge_nb())        # returns 1
g.new_edges([(0, 3), (5, 9), (9, 3)])
print(g.edge_nb(), '\n')  # returns 4

Node and edge attributes#

Adding a node with specific attributes:

g2 = nngt.Graph()

# add a new node with attributes
attributes = {
    'size': 2.,
    'color': 'blue',
    'a': 5,
    'blob': []

attribute_types = {
    'size': 'double',
    'color': 'string',
    'a': 'int',
    'blob': 'object'

g2.new_node(attributes=attributes, value_types=attribute_types)
print(g2.node_attributes, '\n')

By default, nodes that are added without specifying attribute values will get their attributes filled with default values which depend on the type:

  • NaN for “double”
  • 0 for “int”
  • "" for “string”
  • None for “object”
# for a double attribute like 'size', default value is NaN
# for a string attribute like 'color', default value is ""
# for an int attribute like 'a', default value is 0
# for an object attribute like 'blob', default value is None
print(g2.get_node_attributes(name='blob'), '\n')

Adding several nodes and attributes at the same time:

g2.new_node(3, attributes={'size': [4., 5., 1.], 'color': ['r', 'g', 'b']},
            value_types={'size': 'double', 'color': 'string'})
print(g2.node_attributes['color'], '\n')

Attributes can also be created afterwards:

import numpy as np
g3 = nngt.Graph(nodes=100)
g3.new_node_attribute('size', 'double',
                      values=np.random.uniform(0, 20, 100))
print(g3.node_attributes['size'][:5], '\n')

All the previous techniques can also be used with new_edge() or new_edges(), and new_edge_attribute(). Note that attributes can also be set selectively:

edges = g3.new_edges(np.random.randint(0, 50, (10, 2)), ignore_invalid=True)
g3.new_edge_attribute('rank', 'int')
g3.set_edge_attribute('rank', val=2, edges=edges[:3, :])
print(g3.edge_attributes['rank'], '\n')

Generating and analyzing more complex networks#

NNGT provides a whole set of methods to connect nodes in specific fashions inside a graph. These methods are present in the nngt.generation module, and the network properties can then be plotted and analyzed via the tools present in the nngt.plot and nngt.analysis modules.

from nngt import generation as ng
from nngt import analysis as na
from nngt import plot as nplt

NNGT implements some fast generation tools to create several of the standard networks, such as Erdős-Rényi:

g = ng.erdos_renyi(nodes=1000, avg_deg=100)

if nngt.get_config("with_plot"):
    nplt.degree_distribution(g, ('in', 'total'), show=False)

print("Clustering ER: {}".format(na.global_clustering(g)))

More heterogeneous networks, with scale-free degree distribution (but no correlations like in Barabasi-Albert networks and user-defined exponents) are also implemented:

g = ng.random_scale_free(1.8, 3.2, nodes=1000, avg_deg=100)

if nngt.get_config("with_plot"):
    nplt.degree_distribution(g, ('in', 'out'), num_bins=30, logx=True,
                             logy=True, show=True)

print("Clustering SF: {}".format(na.global_clustering(g)))

For more details, see the full page on Graph generation.

Using random numbers#

By default, NNGT uses the numpy random-number generators (RNGs) which are seeded automatically when numpy is loaded.

However, you can seed the RNGs manually using the following command:

nngt.set_config("msd", 0)

which will seed the master seed to 0 (or any other value you enter). Once seeded manually, a NNGT script will always give the same results provided the same number of thread is being used.

Indeed, when using multithreading, sub-RNGs are used (one per thread). By default, these RNGs are seeded from the master seed as msd + n + 1 where n is the thread number, starting from zero. If needed, these sub-RNGs can also be seeded manually using (for 4 threads)

nngt.set_config("seeds", [1, 2, 3, 4])


When using NEST, the simulator’s RNGs must be seeded separately using the NEST commands; see the NEST user manual for details.

Structuring nodes: Group and Structure#

The Group allows the creation of nodes that belong together. You can then make a complex Structure from these groups and connect them with specific connectivities using the connect_groups() function.

''' ------------------------- #
# Creating a structured graph #
# ------------------------- '''

room1 = nngt.Group(25)
room2 = nngt.Group(50)
room3 = nngt.Group(40)
room4 = nngt.Group(35)

names = ["R1", "R2", "R3", "R4"]

struct = nngt.Structure.from_groups((room1, room2, room3, room4), names)

g = nngt.Graph(structure=struct)

for room in struct:
    nngt.generation.connect_groups(g, room, room, "all_to_all")

nngt.generation.connect_groups(g, (room1, room2), struct, "erdos_renyi",
                               avg_deg=10, ignore_invalid=True)

nngt.generation.connect_groups(g, room3, room1, "erdos_renyi", avg_deg=20)

nngt.generation.connect_groups(g, room4, room3, "erdos_renyi", avg_deg=10)

if nngt.get_config("with_plot"):
    # chord diagram
    sg = g.get_structure_graph()

    nngt.plot.chord_diagram(sg, names="name", sort="distance",
                            use_gradient=True, show=True)

    # spring-block layout    
    nngt.plot.library_draw(g, node_cmap="viridis", show=True)

For more details, see the full page on Groups, structures, and neuronal populations.

The same with neurons: NeuralGroup, NeuralPop#

The NeuralGroup allows the creation of nodes that belong together. You can then make a population from these groups and connect them with specific connectivities using the connect_groups() function.

                  neuron_param={"tau_m": 20.},

''' --------------------------- #
# Creating neuronal populations #
# --------------------------- '''

# making populations from scratch
pop = nngt.NeuralPop(with_models=False)              # empty population
syn_spec = {
    'default': {"model": "tsodyks2_synapse"},           # default connections
    ("pyramidal_cells", "pyramidal_cells"): {"U": 0.6}  # change a parameter

nest_pop = NeuralPop.from_groups([pyr, fsi], syn_spec=syn_spec)

''' ------------------------------- #
# Complex population and metagroups #
# ------------------------------- '''

# Let's model part of a cortical column with

For more details, see the full page on Groups, structures, and neuronal populations.

Real neuronal networks and NEST interaction: the Network#

Besides connectivity, the main interest of the NeuralGroup is that you can pass it the biological properties that the neurons belonging to this group will share.

Since we are using NEST, these properties are:

  • the model’s name
  • its non-default properties
  • the synapses that the neurons have and their properties
  • the type of the neurons (1 for excitatory or -1 for inhibitory)
''' Create groups with different parameters '''
# adaptive spiking neurons
base_params = {
    'E_L': -60., 'V_th': -58., 'b': 20., 'tau_w': 100.,
    'V_reset': -65., 't_ref': 2., 'g_L': 10., 'C_m': 250.
# oscillators
params1, params2 = base_params.copy(), base_params.copy()
    {'E_L': -65., 'b': 40., 'I_e': 200., 'tau_w': 400., "V_th": -57.})
# bursters
params2.update({'b': 25., 'V_reset': -55., 'tau_w': 300.})

oscill = nngt.NeuralGroup(
    nodes=400, neuron_model='aeif_psc_alpha', neuron_type=1,

burst = nngt.NeuralGroup(
    nodes=200, neuron_model='aeif_psc_alpha', neuron_type=1,

adapt = nngt.NeuralGroup(
    nodes=200, neuron_model='aeif_psc_alpha', neuron_type=1,

synapses = {
    'default': {'model': 'tsodyks2_synapse'},
    ('oscillators', 'bursters'): {'model': 'tsodyks2_synapse', 'U': 0.6},
    ('oscillators', 'oscillators'): {'model': 'tsodyks2_synapse', 'U': 0.7},
    ('oscillators', 'adaptive'): {'model': 'tsodyks2_synapse', 'U': 0.5}

Create the population that will represent the neuronal
network from these groups
pop = nngt.NeuralPop.from_groups(
    [oscill, burst, adapt],
    names=['oscillators', 'bursters', 'adaptive'], syn_spec=synapses)

Create the network from this population,
using a Gaussian in-degree
net = ng.gaussian_degree(
    100., 15., population=pop, weights=155., delays=5.)

Once this network is created, it can simply be sent to nest through the command: gids = net.to_nest(), and the NEST gids are returned.

In order to access the gids from each group, you can do:

oscill_gids = net.nest_gid[oscill.ids]

For more details to use NNGT with NEST, see Interacting with the NEST simulator.

Underlying graph objects and libraries#

Starting with version 2.0 of NNGT, the library no longer uses inheritance but composition to provide access to the underlying graph object, which is stored in the graph attribute of the Graph class.

It can simply be accessed via:

g = nngt.Graph()

library_graph = g.graph

Using graph attribute, on can directly use functions of the underlying graph library (networkx, igraph, or graph-tool) if their equivalent is not yet provided in NNGT – see Consistent tools for graph analysis for implemented functions.


One notable exception to this behaviour relates to the creation and deletion of nodes or edges, for which you have to use the functions provided by NNGT. As a general rule, any operation that might alter the graph structure should be done through NNGT and never directly by calling functions or methods on the graph attribute.

Apart from this, you can use any analysis or drawing tool from the graph library.

Example using graph-tool#

>>> import graph_tool as gt
>>> import matplotlib.pyplot as plt
>>> print(gt.centrality.closeness(g.graph))
>>> gt.draw.graph_draw(g.graph)
>>> nngt.plot.draw_network(g)
>>> plt.show()

Example using igraph#

>>> import igraph as ig
>>> import matplotlib.pyplot as plt
>>> print(g.graph.closeness(mode='out'))
>>> ig.plot(g.graph)
>>> nngt.plot.draw_network(g)
>>> plt.show()

Example using networkx#

>>> import networkx as nx
>>> import matplotlib.pyplot as plt
>>> print(nx.closeness_centrality(g.graph.reverse()))
>>> nx.draw(g.graph)
>>> nngt.plot.draw_network(g)
>>> plt.show()


People testing these 3 codes will notice that all closeness results are different (though I made sure the functions of each libraries worked on the same outgoing edges)! This example is given voluntarily to remind you, when using these libraries, to check that they indeed compute what you think they do and what are the underlying hypotheses or definitions.

To avoid such issues and make sure that results are the same with all libraries, use the functions provided in Consistent tools for graph analysis.

Go to other tutorials: