# Tutorial¶

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.

Content:

## 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
>>> g.node_nb()     # returns 10


And create edges between these nodes:

>>> g.new_edge(1, 4)  # create on connection going from 11 to 56
>>> g.edge_nb()       # returns 1
>>> g.new_edges([(0, 3), (5, 9), (9, 3)])
>>> g.edge_nb()       # returns 4


### Node and edge attributes¶

Adding a node with specific attributes:

g2 = nngt.Graph()
g2.new_node(attributes={'size': 2., 'color': 'blue'},
value_types={'size': 'double', 'color': 'string'})
print(g2.node_attributes)


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


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))
g3.node_attributes


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, 100, (50, 2)))
g3.new_edge_attribute('rank', 'int', val=0)
g3.set_edge_attribute('rank', val=2, edges=edges[:3, :])
g3.edge_attributes


## 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)
nplt.degree_distribution(g, ('in', 'total'))
print(na.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)
nplt.degree_distribution(g, ('in', 'out'), num_bins=30, logx=True,
logy=True, show=True)
print("Clustering: {}".format(na.clustering(g)))


## Making complex populations¶

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_neural_groups() function.

num_nodes = 1000

'''
Make the population
'''

# two groups of neurons
g1 = nngt.NeuralGroup(500)  # neurons 0 to 499
g2 = nngt.NeuralGroup(500)  # neurons 500 to 999

# make population (without NEST models)
pop = nngt.NeuralPop.from_groups(
(g1, g2), ("left", "right"), with_models=False)

# create network from this population
net = nngt.Network(population=pop)

'''
Connect the groups
'''

# inter-groups (Erdos-Renyi)
prop_er1 = {"density": 0.005}
ng.connect_neural_groups(net, "left", "right", "erdos_renyi", prop_er1)

# intra-groups (Newman-Watts)
prop_nw = {
"coord_nb": 20,
"proba_shortcut": 0.1
}

ng.connect_neural_groups(net, "left", "left", "newman_watts", prop_nw)
ng.connect_neural_groups(net, "right", "right", "newman_watts", prop_nw)


## Real neuronal culture and NEST interaction: the Network¶

Besides connectivity, the main interest of the :class:~nngt.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 '''
base_params = {
'E_L': -60., 'V_th': -55., 'b': 10., 'tau_w': 100.,
'V_reset': -65., 't_ref': 1., 'g_L': 10., 'C_m': 250.
}
# oscillators
params1, params2 = base_params.copy(), base_params.copy()
params1.update({'E_L': -65., 'b': 30., 'I_e': 350., 'tau_w': 400.})
# bursters
params2.update({'b': 20., 'V_reset': -50., 'tau_w': 500.})

oscill = nngt.NeuralGroup(
nodes=400, model='aeif_psc_alpha', neuron_param=params1,
syn_model='tsodyks2_synapse', syn_param={'U': 0.5})
burst = nngt.NeuralGroup(
nodes=200, model='aeif_psc_alpha', neuron_param=params2,
syn_model='tsodyks2_synapse')
nodes=200, model='aeif_psc_alpha', neuron_param=base_params,
syn_model='tsodyks2_synapse')

'''
Create the population that will represent the neuronal
network from these groups
'''
pop = nngt.NeuralPop.from_groups(

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


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]


## Using the graph library of the NNGT object¶

As mentionned in the installation and introduction, NNGT uses existing graph library objects to store the graph. The library was designed so that most of the functions of the underlying graph library can be used directly on the Graph object.

Warning

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 thourgh NNGT and never directly using the underlying library.

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, harmonic=True))
>>> gt.draw.graph_draw(g)
>>> nngt.plot.draw_network(g)
>>> plt.show()


### Example using igraph¶

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


### Example using networkx¶

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


Note

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 even when they compute it, check how they do it!

## NNGT configuration status¶

>>> nngt.get_config()
`