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.
The source file for the tutorial can be found here: doc/examples/introductory_tutorial.py.
Note
For a list of example files, see the ‘examples’ directory on GitHub.
For specific tutorials see also:
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
:
# Adding attributes #
We can then add some nodes to it
g2 = nngt.Graph()
And create edges between these nodes:
'size': 2.,
'color': 'blue',
'a': 5,
'blob': []
Node and edge attributes#
Adding a node with specific attributes:
'blob': 'object'
}
g2.new_node(attributes=attributes, value_types=attribute_types)
print(g2.node_attributes, '\n')
# by default all nodes will have these properties with "empty" values
g2.new_node(2)
# for a double attribute like 'size', default value is NaN
print(g2.get_node_attributes(name="size"))
# for a string attribute like 'color', default value is ""
print(g2.get_node_attributes(name="color"))
# for an int attribute like 'a', default value is 0
print(g2.get_node_attributes(name='a'))
# for an object attribute like 'blob', default value is None
print(g2.get_node_attributes(name='blob'), '\n')
# attributes for multiple nodes can be set simultaneously
g2.new_node(3, attributes={'size': [4., 5., 1.], 'color': ['r', 'g', 'b']},
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”
print(g2.node_attributes['color'], '\n')
# creating attributes 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')
Adding several nodes and attributes at the same time:
g3.new_edge_attribute('rank', 'int')
g3.set_edge_attribute('rank', val=2, edges=edges[:3, :])
print(g3.edge_attributes['rank'], '\n')
Attributes can also be created afterwards:
g3.new_edges(np.random.randint(50, 100, (5, 2)), ignore_invalid=True)
print(g3.edge_attributes['rank'], '\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:
from nngt import generation as ng
from nngt import analysis as na
from nngt import plot as nplt
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.
if nngt.get_config("with_plot"):
nplt.degree_distribution(g, ('in', 'out'), num_bins=30, logx=True,
NNGT implements some fast generation tools to create several of the standard networks, such as Erdős-Rényi:
print("Clustering SF: {}".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:
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])
Warning
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.
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)
''' ------------------- #
# More group properties #
# ------------------- '''
# group can have names
named_group = Group(500, name="named_group")
print("I'm a named group!", named_group, "\n")
# NeuralGroups can store whether neurons are excitatory or inhibitory
exc = NeuralGroup(800, neuron_type=1) # excitatory group
exc2 = NeuralGroup(800, neuron_type=1) # also excitatory
inhib = NeuralGroup(200, neuron_type=-1) # inhibitory group
print("'exc2' is an excitatory group:", exc2.neuron_type == 1,
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.
''' --------------------------- #
# Creating neuronal populations #
# --------------------------- '''
pop = NeuralPop.from_groups((pyr, fsi))
# making populations from scratch
pop = nngt.NeuralPop(with_models=False) # empty population
pop.create_group(200, "first_group") # create excitatory group
pop.create_group(5, "second_group", neuron_type=-1) # create inhibitory group
print("E/I population has size", pop.size, "and contains",
len(pop), "groups:", pop.keys(), "\n")
# the two default populations
unif_pop = NeuralPop.uniform(1000) # only excitatory
ei_pop = NeuralPop.exc_and_inhib(1000, iratio=0.25) # 25% inhibitory
# check the groups inside
print("Uniform population has size", unif_pop.size, "and contains",
len(unif_pop), "group:", unif_pop.keys(), "\n")
print("E/I population has size", ei_pop.size, "and contains",
len(ei_pop), "groups:", ei_pop.keys(), "\n")
# A population can also be created from existing groups.
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)
neuron_param=params1)
burst = nngt.NeuralGroup(
nodes=200, neuron_model='aeif_psc_alpha', neuron_type=1,
neuron_param=params2)
adapt = nngt.NeuralGroup(
nodes=200, neuron_model='aeif_psc_alpha', neuron_type=1,
neuron_param=base_params)
model = 'model'
try:
import nest
nest.NodeCollection()
model = 'synapse_model'
except:
pass
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 network from this population,
using a Gaussian in-degree
'''
net = ng.gaussian_degree(
100., 15., population=pop, weights=155., delays=5.)
'''
Send the network to NEST, monitor and simulate
'''
if nngt.get_config('with_nest'):
import nngt.simulation as ns
import nest
nest.ResetKernel()
nest.SetKernelStatus({'local_num_threads': 4})
gids = net.to_nest()
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.
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 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()
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 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: