#!/usr/bin/env python
#-*- coding:utf-8 -*-
""" Graph classes for graph generation and management """
import warnings
from copy import deepcopy
from numpy import multiply
from scipy.sparse import lil_matrix
from nngt.globals import (default_neuron, default_synapse, POS, WEIGHT, DELAY,
DIST, TYPE)
from nngt.core.graph_objects import GraphLib, GraphObject
from nngt.core.graph_datastruct import NeuralPop, Shape, Connections
import nngt.analysis as na
from nngt.lib import InvalidArgument
#-----------------------------------------------------------------------------#
# Graph
#------------------------
#
[docs]class Graph(object):
"""
The basic class that contains a :class:`graph_tool.Graph` and some
of is properties or methods to easily access them.
:ivar id: :class:`int`
unique id that identifies the instance.
:ivar graph: :class:`~nngt.core.GraphObject`
main attribute of the class instance.
"""
#-------------------------------------------------------------------------#
# Class properties
__num_graphs = 0
__max_id = 0
#~ __di_property_func = {
#~ "reciprocity": reciprocity, "clustering": clustering,
#~ "assortativity": assortativity, "diameter": diameter,
#~ "scc": num_scc, "wcc": num_wcc, "radius": spectral_radius,
#~ "num_iedges": num_iedges }
#~ __properties = __di_property_func.keys()
@classmethod
[docs] def num_graphs(cls):
''' Returns the number of alive instances. '''
return cls.__num_graphs
#-------------------------------------------------------------------------#
# Constructor/destructor and properties
def __init__(self, nodes=0, name="Graph",
weighted=True, directed=True, libgraph=None, **kwargs):
'''
Initialize Graph instance
Parameters
----------
nodes : int, optional (default: 0)
Number of nodes in the graph.
name : string, optional (default: "Graph")
The name of this :class:`Graph` instance.
weighted : bool, optional (default: True)
Whether the graph edges have weight properties.
directed : bool, optional (default: True)
Whether the graph is directed or undirected.
libgraph : :class:`~nngt.core.GraphObject`, optional
An optional :class:`~nngt.core.GraphObject` to serve as base.
Returns
-------
self : :class:`~nggt.core.Graph`
'''
self.__id = self.__class__.__max_id
self._name = name
self._directed = directed
self._edges = []
# create the graphlib graph
if libgraph is not None:
self._graph = GraphObject.to_graph_object(libgraph)
else:
self._graph = GraphObject(nodes=nodes, directed=directed)
# take care of the weights @todo: use those of the libgraph
if weighted:
if "weight_prop" in kwargs.keys():
self._w = kwargs["weight_prop"]
else:
self._w = {"distrib": "constant"}
self.set_weights()
# update the counters
self.__class__.__num_graphs += 1
self.__class__.__max_id += 1
def __del__(self):
self.__class__.__num_graphs -= 1
@property
def id(self):
''' unique :class:`int` identifying the instance '''
return self.__id
@property
def graph(self):
''' :class:`graph_tool.Graph` attribute of the instance '''
return self._graph
@graph.setter
def graph(self, new_graph):
if isinstance(new_graph, GraphLib):
self._graph = GraphObject.to_graph_object(new_graph)
elif isinstance(new_graph, GraphObject):
self._graph = new_graph
else:
raise TypeError("The object passed is not a \
GraphObject but a {}".format(new_graph.__class__.__name__))
@property
def name(self):
''' name of the graph '''
return self._name
@property
def edges(self):
return self._edges
#-------------------------------------------------------------------------#
# Graph actions
[docs] def copy(self):
'''
Returns a deepcopy of the current :class:`~nngt.core.Graph`
instance
'''
gc_instance = Graph(name=self._name+'_copy',
weighted=self.is_weighted(),
graph=self._graph.copy())
return gc_instance
[docs] def add_edges(self, lst_edges):
'''
Add a list of edges to the graph.
Parameters
----------
lst_edges : list of 2-tuples or np.array of shape (edge_nb, 2)
List of the edges that should be added as tuples (source, target)
@todo: add example, check the edges for self-loops and multiple edges
'''
self._graph.new_edges(lst_edges)
self._edges.extend(lst_edges)
[docs] def inhibitory_subgraph(self):
''' Create a :class:`~nngt.core.Graph` instance which graph
contains only the inhibitory edges of the current instance's
:class:`graph_tool.Graph` '''
eprop_b_type = self._graph.new_edge_property(
"bool",-self._graph.edge_properties[TYPE].a+1)
self._graph.set_edge_filter(eprop_b_type)
inhib_graph = Graph( name=self._name + '_inhib',
weighted=self.is_weighted(),
graph=GraphObject(self._graph,prune=True) )
self._graph.clear_filters()
return inhib_graph
[docs] def excitatory_subgraph(self):
''' create a :class:`~nngt.core.Graph` instance which graph
contains only the excitatory edges of the current instance's
:class:`GraphObject` '''
eprop_b_type = self._graph.new_edge_property(
"bool",self._graph.edge_properties[TYPE].a+1)
self._graph.set_edge_filter(eprop_b_type)
exc_graph = Graph( name=self._name + '_exc',
weighted=self.is_weighted(),
graph=GraphObject(self._graph,prune=True) )
self._graph.clear_filters()
return exc_graph
[docs] def adjacency_matrix(self, typed=True, weighted=True):
'''
Returns the adjacency matrix of the graph as a
:class:`scipy.sparse.csr_matrix`.
Parameters
----------
weighted : bool or string, optional (default: True)
If True, each entry ``adj[i,j] = w_ij`` where ``w_ij`` is the
strength of the connection from `i` to `j`, if False, ``adj[i,j] =
0. or 1.``. Weighted can also be a string describing an edge
attribute (e.g. if "distance" refers to an edge attribute ``dist``,
then ``ajacency_matrix("distance")`` will return
``adj[i,i] = dist_ij``).
Returns
-------
adj : :class:`scipy.sparse.csr_matrix`
The adjacency matrix of the graph.
'''
return na.adjacency_matrix(self, typed=typed, weighted=weighted)
def clear_edges(self):
''' Remove all the edges in the graph. '''
self._graph.clear_edges()
[docs] def clear_edges(self):
''' Remove all the edges in the graph. '''
self._graph.clear_edges()
n = self.node_nb()
#-------------------------------------------------------------------------#
# Setters
[docs] def set_name(self, name=""):
''' set graph name '''
if name != "":
self._name = name
else:
self._name = "Graph_" + str(self.__id)
[docs] def set_edge_attribute(self, attribute, values=None, val=None,
value_type=None):
if attribute not in self.attributes():
self._graph.new_edge_attribute(attribute, value_type, values, val)
else:
num_edges = self.edge_nb()
if values is None:
if val is not None:
values = np.repeat(val,num_edges)
else:
raise InvalidArgument("At least one of the `values` and \
`val` arguments should not be ``None``.")
self._graph._eattr[attribute] = values
[docs] def set_weights(self, elist=None, wlist=None, distrib=None,
distrib_prop=None, correl=None, noise_scale=None):
'''
Set the synaptic weights.
Parameters
----------
elist : class:`numpy.array`, optional (default: None)
List of the edges (for user defined weights).
wlist : class:`numpy.array`, optional (default: None)
List of the weights (for user defined weights).
distrib : class:`string`, optional (default: None)
Type of distribution (choose among "constant", "uniform",
"gaussian", "lognormal", "lin_corr", "log_corr").
distrib_prop : dict, optional (default: {})
Dictoinary containing the properties of the weight distribution.
correl : class:`string`, optional (default: None)
Property to which the weights should be correlated.
noise_scale : class:`int`, optional (default: None)
Scale of the multiplicative Gaussian noise that should be applied
on the weights.
'''
if distrib is None:
distrib = self._w["distrib"]
if distrib_prop is None:
distrib_prop = (self._w["distrib_prop"] if "distrib_prop" in
self._w.keys() else {})
if correl is None:
correl = self._w["correl"] if "correl" in self._w.keys() else None
Connections.weights(self, elist=elist, wlist=wlist, distrib=distrib,
correl=correl, distrib_prop=distrib_prop, noise_scale=noise_scale)
#-------------------------------------------------------------------------#
# Getters
[docs] def attributes(self):
''' List of the graph's attributes (synaptic weights, delays...) '''
return self._graph._eattr.keys()
[docs] def get_name(self):
''' Get the name of the graph '''
return self.__di_prop["name"]
[docs] def node_nb(self):
''' Number of nodes in the graph '''
return self._graph.node_nb()
[docs] def edge_nb(self):
''' Number of edges in the graph '''
return self._graph.edge_nb()
[docs] def get_density(self):
'''
Density of the graph: :math:`\\frac{E}{N^2}`, where `E` is the number of
edges and `N` the number of nodes.
'''
return self._graph.edge_nb()/float(self._graph.node_nb()**2)
[docs] def is_weighted(self):
''' Whether the edges have weights '''
return "weight" in self.attributes()
[docs] def is_directed(self):
''' Whether the graph is directed or not '''
return self._directed
#~ def get_property(self, s_property):
#~ ''' Return the desired property or None for an incorrect one. '''
#~ if s_property in Graph.__properties:
#~ return Graph.__di_property_func[s_property](self._graph)
#~ else:
#~ warnings.warn("Ignoring request for unknown property \
#~ '{}'".format(s_property))
#~ return None
#~ def get_properties(self, a_properties):
#~ '''
#~ Return a dictionary containing the desired properties
#~
#~ Parameters
#~ ----------
#~ a_properties : sequence
#~ List or tuple of strings of the property names.
#~
#~ Returns
#~ -------
#~ di_result : dict
#~ A dictionary of values with the property names as keys.
#~ '''
#~ di_result = { prop: self.get_property(prop) for prop in a_properties }
#~ return di_result
[docs] def get_degrees(self, node_list=None, deg_type="total", use_weights=True):
'''
Degree sequence of all the nodes.
Parameters
----------
node_list : list, optional (default: None)
List of the nodes which degree should be returned
deg_type : string, optional (default: "total")
Degree type (among 'in', 'out' or 'total').
use_weights : bool, optional (default: True)
Whether to use weighted (True) or simple degrees (False).
Returns
-------
:class:`numpy.array` or None (if an invalid type is asked).
'''
valid_types = ("in", "out", "total")
if deg_type in valid_types:
return self._graph.degree_list(node_list, deg_type, use_weights)
else:
warnings.warn("Ignoring invalid degree type '{}'".format(strType))
return None
[docs] def get_betweenness(self, use_weights=True):
'''
Betweenness centrality sequence of all nodes and edges.
Parameters
----------
use_weights : bool, optional (default: True)
Whether to use weighted (True) or simple degrees (False).
Returns
-------
node_betweenness : :class:`numpy.array`
Betweenness of the nodes.
edge_betweenness : :class:`numpy.array`
Betweenness of the edges.
'''
return self._graph.betweenness(use_weights)
[docs] def get_edge_types(self):
if TYPE in self._graph.edge_properties.keys():
return self._graph.edge_properties[TYPE].a
else:
return repeat(1, self._graph.edge_nb())
[docs] def get_weights(self):
''' Returns the weighted adjacency matrix as a
:class:`scipy.sparse.lil_matrix`.
'''
return self._graph.eproperties["weight"]
[docs] def is_spatial(self):
'''
Whether the graph is embedded in space (has a :class:`~nngt.Shape`
attribute).
'''
return True if issubclass(self.__class__, SpatialGraph) else False
#-----------------------------------------------------------------------------#
# SpatialGraph
#------------------------
#
[docs]class SpatialGraph(Graph):
"""
The detailed class that inherits from :class:`Graph` and implements
additional properties to describe various biological functions
and interact with the NEST simulator.
:ivar shape: :class:`~nngt.Shape`
Shape of the neurons environment.
:ivar positions: :class:`numpy.array`
Positions of the neurons.
:ivar graph: :class:`~nngt.GraphObject`
Main attribute of the class instance.
"""
#-------------------------------------------------------------------------#
# Class properties
__num_graphs = 0
__max_id = 0
@classmethod
[docs] def make_spatial(graph, shape=Shape(), positions=None):
if isinstance(graph, Network):
graph.__class__ = SpatialNetwork
else:
graph.__class__ = SpatialGraph
graph._init_spatial_properties(shape, positions)
#-------------------------------------------------------------------------#
# Constructor, destructor, attributes
def __init__(self, nodes=0, name="Graph", weighted=True, directed=True,
libgraph=None, shape=None, positions=None, **kwargs):
'''
Initialize SpatialClass instance.
@todo: see what we do with the libgraph argument
Parameters
----------
nodes : int, optional (default: 0)
Number of nodes in the graph.
name : string, optional (default: "Graph")
The name of this :class:`Graph` instance.
weighted : bool, optional (default: True)
Whether the graph edges have weight properties.
directed : bool, optional (default: True)
Whether the graph is directed or undirected.
shape : :class:`~nngt.core.Shape`, optional (default: None)
Shape of the neurons' environment (None leads to Shape())
positions : :class:`numpy.array`, optional (default: None)
Positions of the neurons; if not specified and `nodes` is not 0,
then neurons will be reparted at random inside the
:class:`~nngt.core.Shape` object of the instance.
Returns
-------
self : :class:`~nggt.Graph`
'''
super(SpatialGraph, self).__init__(nodes, name, weighted, directed,
libgraph, **kwargs)
self.__id = self.__class__.__max_id
self._init_spatial_properties(shape, positions, **kwargs)
self.__class__.__num_graphs += 1
self.__class__.__max_id += 1
self.__b_valid_properties = True
def __del__(self):
self._shape._parent = None
self._shape = None
super(SpatialGraph, self).__del__()
self.__class__.__num_graphs -= 1
@property
def shape(self):
return self._shape
@property
def position(self):
return self._pos
#-------------------------------------------------------------------------#
# Init tool
def _init_spatial_properties(self, shape, positions=None, **kwargs):
'''
Create the positions of the neurons from the graph `shape` attribute
and computes the connections distances.
'''
self._shape = shape if shape is not None else Shape(self)
b_rnd_pos = ( True if not self.node_nb() or positions is None
else len(positions) != self.node_nb() )
pos = self._shape.rnd_distrib() if b_rnd_pos else positions
self._pos = pos
Connections.distances(self)
#-----------------------------------------------------------------------------#
# Network
#------------------------
#
[docs]class Network(Graph):
"""
The detailed class that inherits from :class:`Graph` and implements
additional properties to describe various biological functions
and interact with the NEST simulator.
:ivar population: :class:`~nngt.NeuralPop`
Object reparting the neurons into groups with specific properties.
:ivar graph: :class:`~nngt.core.GraphObject`
Main attribute of the class instance
:ivar nest_gid: :class:`numpy.array`
Array containing the NEST gid associated to each neuron; it is ``None``
until a NEST network has been created.
:ivar id_from_nest_gid: dict
Dictionary mapping each NEST gid to the corresponding neuron index in
the :class:`nngt.~Network`
"""
#-------------------------------------------------------------------------#
# Class attributes and methods
__num_networks = 0
__max_id = 0
@classmethod
[docs] def num_networks(cls):
''' Returns the number of alive instances. '''
return cls.__num_networks
@classmethod
@classmethod
[docs] def ei_network(cls, size, ei_ratio=0.2, en_model=default_neuron,
en_param={}, es_model=default_synapse, es_param={},
in_model=default_neuron, in_param={}, is_model=default_synapse,
is_param={}):
'''
Generate a network containing a population of two neural groups:
inhibitory and excitatory neurons.
Parameters
----------
size : int
Number of neurons in the network.
ei_ratio : double, optional (default: 0.2)
Ratio of inhibitory neurons: :math:`\\frac{N_i}{N_e+N_i}`.
en_model : string, optional (default: 'aeif_cond_alpha')
Nest model for the excitatory neuron.
en_param : dict, optional (default: {})
Dictionary of parameters for the the excitatory neuron.
es_model : string, optional (default: 'static_synapse')
NEST model for the excitatory synapse.
es_param : dict, optional (default: {})
Dictionary containing the excitatory synaptic parameters.
in_model : string, optional (default: 'aeif_cond_alpha')
Nest model for the inhibitory neuron.
in_param : dict, optional (default: {})
Dictionary of parameters for the the inhibitory neuron.
is_model : string, optional (default: 'static_synapse')
NEST model for the inhibitory synapse.
is_param : dict, optional (default: {})
Dictionary containing the inhibitory synaptic parameters.
Returns
-------
net : :class:`~nngt.Network` or subclass
Network of disconnected excitatory and inhibitory neurons.
'''
pop = NeuralPop.ei_population(size, ei_ratio, None, en_model, en_param,
es_model, es_param, in_model, in_param, is_model, is_param)
print(Network is cls)
net = cls(population=pop)
return net
@staticmethod
[docs] def make_network(graph, neural_pop):
'''
Turn a :class:`~nngt.Graph` object into a :class:`~nngt.Network`, or a
:class:`~nngt.SpatialGraph` into a :class:`~nngt.SpatialNetwork`.
Parameters
----------
graph : :class:`~nngt.Graph` or :class:`~nngt.SpatialGraph`
Graph to convert
neural_pop : :class:`~nngt.NeuralPop`
Population to associate to the new :class:`~nngt.Network`
Notes
-----
In-place operation that directly converts the original graph.
'''
if isinstance(graph, SpatialGraph):
graph.__class__ = SpatialNetwork
else:
graph.__class__ = Network
graph.population = neural_pop
Connections.delays(graph)
#-------------------------------------------------------------------------#
# Constructor, destructor and attributes
def __init__(self, name="Graph", weighted=True, directed=True,
libgraph=None, population=None, **kwargs):
'''
Initializes :class:`~nngt.Network` instance.
Parameters
----------
nodes : int, optional (default: 0)
Number of nodes in the graph.
name : string, optional (default: "Graph")
The name of this :class:`Graph` instance.
weighted : bool, optional (default: True)
Whether the graph edges have weight properties.
directed : bool, optional (default: True)
Whether the graph is directed or undirected.
libgraph : :class:`~nngt.core.GraphObject`, optional (default: None)
An optional :class:`~nngt.core.GraphObject` to serve as base.
@todo:
population : :class:`NeuralPop`, (default: None)
A tuple containing the model(s) to use in NEST to simulate the
neurons as well as a dictionary containing the parameters for the
neuron.
Returns
-------
self : :class:`~nggt.core.Graph`
'''
if population is None:
raise InvalidArgument("Network needs a NeuralPop to be created")
nodes = population.size
if "nodes" in kwargs.keys():
del kwargs["nodes"]
super(Network, self).__init__(nodes=nodes, name=name,
weighted=weighted, directed=directed,
libgraph=libgraph, **kwargs)
self.__id = self.__class__.__max_id
self._init_bioproperties(population)
self.nest_gid = None
self.id_from_nest_gid = None
self.__class__.__num_networks += 1
self.__class__.__max_id += 1
self.__b_valid_properties = True
def __del__(self):
super(Network, self).__del__()
self.__class__.__num_networks -= 1
@property
def population(self):
'''
:class:`~nngt.NeuralPop` that divides the neurons into groups with
specific properties.
'''
return self._population
@population.setter
def population(self, population):
if issubclass(NeuralPop, population.__class__):
if self._graph.node_nb() == population.size:
if population.is_valid:
self._population = population
else:
raise AttributeError("NeuralPop is not valid (not all \
neurons are associated to a group).")
else:
raise AttributeError("{} and NeuralPop must have same number \
of neurons".format(self.__class__.__name__))
else:
raise AttributeError("Expecting NeuralPop but received \
{}".format(pop.__class__.__name__))
#-------------------------------------------------------------------------#
# Init tool
def _init_bioproperties(self, population):
''' Set the population attribute and link each neuron to its group. '''
if issubclass(NeuralPop, population.__class__):
if population.is_valid:
self._population = population
nodes = population.size
# create the delay attribute
Connections.delays(self)
else:
raise AttributeError("NeuralPop is not valid (not all \
neurons are associated to a group).")
else:
raise AttributeError("Expected NeuralPop but received \
{}".format(pop.__class__.__name__))
#-------------------------------------------------------------------------#
# Getter
[docs] def neuron_properties(self, idx_neuron):
'''
Properties of a neuron in the graph.
Parameters
----------
idx_neuron : int
Index of a neuron in the graph.
Returns
-------
dict of the neuron properties.
'''
group_name = self._population._neuron_group[idx_neuron]
return self._population[group_name].properties()
#-----------------------------------------------------------------------------#
# SpatialNetwork
#------------------------
#
[docs]class SpatialNetwork(Network,SpatialGraph):
"""
Class that inherits from :class:`~nngt.Network` and :class:`SpatialGraph`
to provide a detailed description of a real neural network in space, i.e.
with positions and biological properties to interact with NEST.
:ivar shape: :class:`nngt.core.Shape`
Shape of the neurons environment.
:ivar positions: :class:`numpy.array`
Positions of the neurons.
:ivar population: :class:`~nngt.NeuralPop`
Object reparting the neurons into groups with specific properties.
:ivar graph: :class:`~nngt.core.GraphObject`
Main attribute of the class instance.
:ivar nest_gid: :class:`numpy.array`
Array containing the NEST gid associated to each neuron; it is ``None``
until a NEST network has been created.
:ivar id_from_nest_gid: dict
Dictionary mapping each NEST gid to the corresponding neuron index in
the :class:`nngt.~SpatialNetwork`
"""
#-------------------------------------------------------------------------#
# Class attributes
__num_networks = 0
__max_id = 0
#-------------------------------------------------------------------------#
# Constructor, destructor, and attributes
def __init__(self, population, name="Graph", weighted=True, directed=True,
shape=None, graph=None, positions=None, **kwargs):
'''
Initialize Graph instance
Parameters
----------
name : string, optional (default: "Graph")
The name of this :class:`Graph` instance.
weighted : bool, optional (default: True)
Whether the graph edges have weight properties.
directed : bool, optional (default: True)
Whether the graph is directed or undirected.
shape : :class:`~nngt.core.Shape`, optional (default: None)
Shape of the neurons' environment (None leads to Shape())
positions : :class:`numpy.array`, optional (default: None)
Positions of the neurons; if not specified and `nodes` != 0, then
neurons will be reparted at random inside the
:class:`~nngt.core.Shape` object of the instance.
population : class:`~nngt.NeuralPop`, optional (default: None)
Returns
-------
self : :class:`~nggt.core.Graph`
'''
if population is None:
raise InvalidArgument("Network needs a NeuralPop to be created")
nodes = population.size
super(SpatialNetwork, self).__init__(
nodes=nodes, name=name, weighted=weighted, directed=directed,
shape=shape, positions=positions, population=population, **kwargs)
self.__id = self.__class__.__max_id
self.__class__.__num_networks += 1
self.__class__.__max_id += 1
self.__b_valid_properties = True
def __del__ (self):
super(SpatialNetwork, self).__del__()
self.__class__.__num_networks -= 1