#!/usr/bin/env python
#-*- coding:utf-8 -*-
""" Graph classes for graph generation and management """
from copy import deepcopy
import warnings
import numpy as np
import scipy.sparse as ssp
import nngt.analysis as na
from nngt.core.graph_objects import GraphLib, GraphObject
from nngt.core.graph_datastruct import NeuralPop, Shape, Connections
from nngt.lib import InvalidArgument, save_to_file, load_from_file
from nngt.globals import (default_neuron, default_synapse, POS, WEIGHT, DELAY,
DIST, TYPE)
__all__ = [
'Graph', 'SpatialGraph', 'Network', 'SpatialNetwork'
]
#-----------------------------------------------------------------------------#
# Graph
#------------------------
#
[docs]class Graph(GraphObject):
"""
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
def num_graphs(cls):
''' Returns the number of alive instances. '''
return cls.__num_graphs
@classmethod
def from_matrix(cls, matrix, weighted=True, directed=True):
'''
Creates a :class:`~nngt.Graph` from a :class:`scipy.sparse` matrix or
a dense matrix.
Parameters
----------
matrix : :class:`scipy.sparse` matrix or :class:`numpy.array`
Adjacency matrix.
weighted : bool, optional (default: True)
Whether the graph edges have weight properties.
directed : bool, optional (default: True)
Whether the graph is directed or undirected.
Returns
-------
:class:`~nngt.Graph`
'''
shape = matrix.shape
if shape[0] != shape[1]:
raise InvalidArgument('A square matrix is required')
nodes = shape[0]
if not directed:
if issubclass(matrix.__class__, ssp.spmatrix):
if not (matrix.T != matrix).nnz == 0:
raise InvalidArgument('Incompatible directed=False option \
with non symmetric matrix provided.')
else:
if not (matrix.T == matrix).all():
raise InvalidArgument('Incompatible directed=False option \
with non symmetric matrix provided.')
edges = np.array(matrix.nonzero()).T
graph = cls(nodes,name='FromNpArray_{}'.format(cls.__num_graphs),
weighted=weighted, directed=directed)
graph.add_edges(edges)
if weighted:
weights = None
if issubclass(matrix.__class__, ssp.spmatrix):
weights = np.array(matrix[edges[:,0],edges[:,1]])[0]
else:
weights = matrix[edges[:,0],edges[:,1]]
graph.set_weights(elist=edges, wlist=weights)
return graph
@staticmethod
def from_file(filename, format="auto", delimiter=" ", secondary=";",
attributes=[], notifier="@", ignore="#"):
'''
Import a saved graph from a file.
@todo: implement population and shape loading, implement gml, dot, xml, gt
Parameters
----------
filename: str
The path to the file.
format : str, optional (default: "neighbour")
The format used to save the graph. Supported formats are:
"neighbour" (neighbour list, default if format cannot be deduced
automatically), "ssp" (scipy.sparse), "edge_list" (list of all the
edges in the graph, one edge per line, represented by a ``source
target``-pair), "gml" (gml format, default if `filename` ends with
'.gml'), "graphml" (graphml format, default if `filename` ends
with '.graphml' or '.xml'), "dot" (dot format, default if
`filename` ends with '.dot'), "gt" (only when using
`graph_tool`<http://graph-tool.skewed.de/>_ as library, detected
if `filename` ends with '.gt').
delimiter : str, optional (default " ")
Delimiter used to separate inputs in the case of custom formats
(namely "neighbour" and "edge_list")
secondary : str, optional (default: ";")
Secondary delimiter used to separate attributes in the case of
custom formats.
attributes : list, optional (default: [])
List of names for the attributes present in the file. If a
`notifier` is present in the file, names will be deduced from it;
otherwise the attributes will be numbered.
notifier : str, optional (default: "@")
Symbol specifying the following as meaningfull information.
Relevant information is formatted ``@info_name=info_value``, where
``info_name`` is in ("attributes", "directed", "name", "size") and
associated ``info_value``s are of type (``list``, ``bool``,
``str``, ``int``).
Additional notifiers are ``@type=SpatialGraph/Network/
SpatialNetwork``, which must be followed by the relevant notifiers
among ``@shape``, ``@population``, and ``@graph``.
Returns
-------
graph : :class:`~nngt.Graph` or subclass
Loaded graph.
'''
info, edges, attr, pop, shape = load_from_file(filename=filename,
format=format, delimiter=delimiter, secondary=secondary,
attributes=attributes, notifier=notifier)
graph = Graph( nodes=info["size"], name=info["name"],
directed=info["directed"] )
di_attr = {
"names": info["attributes"],
"types": info["attr_types"],
"values": [ attr[name] for name in info["attributes"] ]
}
graph.add_edges(edges, di_attr)
if pop is not None:
Network.make_network(graph, pop)
if shape is not None:
SpatialGraph.make_spatial(graph, shape)
return graph
#-------------------------------------------------------------------------#
# Constructor/destructor and properties
def __init__(self, nodes=0, name="Graph",
weighted=True, directed=True, from_graph=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.
from_graph : :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._graph_type = kwargs["type"] if "type" in kwargs else "custom"
# Init the GraphObject
super(Graph, self).__init__(nodes=nodes, g=from_graph)
# take care of the weights @todo: use those of the from_graph
if weighted:
if "weight_prop" in kwargs:
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 name(self):
''' Name of the graph. '''
return self._name
#-------------------------------------------------------------------------#
# Graph actions
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(),
from_graph=self)
if self.is_spatial():
SpatialGraph.make_spatial(gc_instance)
if self.is_network():
Network.make_network(gc_instance)
return gc_instance
def add_edges(self, lst_edges, attributes=None):
'''
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)
attributes : dict, optional (default: ``None``)
Dictionary of the form ``{ "name": [], "values": [],
"types": [] }``, containing the attributes of the new edges.
warning ::
For now attributes works only when adding edges for the first time
(i.e. adding edges to an empty graph).
@todo: add example, check the edges for self-loops and multiple edges
'''
empty = False if self.edge_nb() else True
self.new_edges(lst_edges)
n = self.edge_nb()
if empty and attributes is not None:
for name, value_type, values in zip(attributes["names"],
attributes["types"], attributes["values"]):
self._eattr.new_ea(name, value_type, values=values)
#~ 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(),
#~ from_graph=GraphObject(self._graph,prune=True) )
#~ self.clear_filters()
#~ return inhib_graph
#~
#~ def excitatory_subgraph(self):
#~ '''
#~ Create a :class:`~nngt.Graph` instance which graph contains only the
#~ excitatory edges of the current instance's :class:`GraphObject`.
#~ .. warning ::
#~ Only works for graph_tool
#~ .. todo ::
#~ Make this method library independant!
#~ '''
#~ 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
#-------------------------------------------------------------------------#
# Setters
def set_name(self, name=""):
''' set graph name '''
if name != "":
self._name = name
else:
self._name = "Graph_" + str(self.__id)
def set_edge_attribute(self, attribute, values=None, val=None,
value_type=None):
if attribute not in self.attributes():
self.new_edge_attribute(name=attribute,
value_type=value_type, values=values, val=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._eattr[attribute] = values
def set_weights(self, elist=None, wlist=None, ifrac=None, distrib=None,
distrib_prop=None, correl=None, noise_scale=None):
'''
Set the synaptic weights.
..todo ::
take elist into account in Connections.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)
def set_types(self, syn_type, nodes=None, fraction=None):
'''
Set the synaptic/connection types.
.. warning :
This is only for use in graph theoretical measurements; when
translating a :class:`~nngt.Network` to NEST, only the
:class:`~nngt.NeuralGroup`s are taken into account. This is
especially important if you set types that do not match those of
the population : be aware that the types will not be taken into
account in the simulations.
Parameters
----------
syn_type : int or string
Type of the connection among 'excitatory' (also `1`) or
'inhibitory' (also `-1`).
nodes : int, float or list, optional (default: `None`)
If `nodes` is an int, number of nodes of the required type that
will be created in the graph (all connections from inhibitory nodes
are inhibitory); if it is a float, ratio of `syn_type` nodes in the
graph; if it is a list, ids of the `syn_type` nodes.
fraction : float, optional (default: `None`)
Fraction of the selected edges that will be set as `syn_type` (if
`nodes` is not `None`, it is the fraction of the specified nodes'
edges, otherwise it is the fraction of all edges in the graph).
Returns
-------
t_list : :class:`numpy.ndarray`
List of the types in an order that matches the `edges` attribute of
the graph.
'''
inhib_nodes = nodes
if syn_type == 'excitatory' or syn_type == 1:
if issubclass(nodes.__class__, int):
inhib_nodes = graph.node_nb() - nodes
elif issubclass(nodes.__class__, float):
inhib_nodes = 1./nodes
elif hasattr(nodes, '__iter__'):
inhib_nodes = list(range(graph.node_nb()))
nodes.sort()
for node in nodes[::-1]:
del inhib_nodes[node]
return Connections.types(self, inhib_nodes, fraction)
#-------------------------------------------------------------------------#
# Getters
def attributes(self, edge=None, name=None):
'''
Attributes of the graph's edges.
Parameters
----------
edge : tuple, optional (default: ``None``)
Edge whose attribute should be displayed.
name : str, optional (default: ``None``)
Name of the desired attribute.
Returns
-------
List containing the names of the graph's attributes (synaptic weights,
delays...) if `edge` is ``None``, else a ``dict`` containing the
attributes of the edge (or the value of attribute `name` if it is not
``None``).
'''
if name is None and edge is None:
return self._eattr.keys()
elif name is None:
return self._eattr[edge]
elif edge is None:
return self._eattr[name]
else:
return self._eattr[edge][name]
def get_attribute_type(self, attribute_name):
''' Return the type of an attribute '''
return self._eattr.stored[attribute_name]
def get_name(self):
''' Get the name of the graph '''
return self._name
def get_graph_type(self):
''' Return the type of the graph (see nngt.generation) '''
return self._graph_type
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.edge_nb()/float(self.node_nb()**2)
def is_weighted(self):
''' Whether the edges have weights '''
return "weight" in self.attributes()
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
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.degree_list(node_list, deg_type, use_weights)
else:
raise InvalidArgument("Invalid degree type '{}'".format(strType))
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.betweenness(use_weights)
def get_edge_types(self):
if TYPE in self.edge_properties.keys():
return self.edge_properties[TYPE].a
else:
return repeat(1, self.edge_nb())
def get_weights(self):
''' Returns the weighted adjacency matrix as a
:class:`scipy.sparse.lil_matrix`.
'''
return self.eproperties["weight"]
def is_spatial(self):
'''
Whether the graph is embedded in space (i.e. if it has a
:class:`~nngt.Shape` attribute).
Returns ``True`` is the graph is a subclass of
:class:`~nngt.SpatialGraph`.
'''
return True if issubclass(self.__class__, SpatialGraph) else False
def is_network(self):
'''
Whether the graph is a subclass of :class:`~nngt.Network` (i.e. if it
has a :class:`~nngt.Shape` attribute).
'''
return True if issubclass(self.__class__, Network) 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,
from_graph=None, shape=None, positions=None, **kwargs):
'''
Initialize SpatialClass instance.
@todo: see what we do with the from_graph 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`
'''
self.__id = self.__class__.__max_id
self.__class__.__num_graphs += 1
self.__class__.__max_id += 1
self._shape = None
super(SpatialGraph, self).__init__(nodes, name, weighted, directed,
from_graph, **kwargs)
self._init_spatial_properties(shape, positions, **kwargs)
def __del__(self):
if self._shape is not None:
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.
'''
if positions is not None and len(positions) != self.node_nb():
raise InvalidArgument("Wrong number of neurons in `positions`.")
if shape is not None:
shape.set_parent(self)
self._shape = shape
else:
self._shape = Shape.rectangle(self,1,1)
b_rnd_pos = True if not self.node_nb() or positions is None else False
self._pos = self._shape.rnd_distrib() if b_rnd_pos else positions
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,
from_graph=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.
from_graph : :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`
'''
self.__id = self.__class__.__max_id
self._population = None
self._nest_gid = None
self.id_from_nest_gid = None
self.__class__.__num_networks += 1
self.__class__.__max_id += 1
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,
from_graph=from_graph, **kwargs)
self._init_bioproperties(population)
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(population.__class__, NeuralPop):
if self.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__))
@property
def nest_gid(self):
return self._nest_gid
@nest_gid.setter
def nest_gid(self, gids):
self._nest_gid = gids
for group in self.population.itervalues():
group._nest_gids = gids[group.id_list]
#-------------------------------------------------------------------------#
# Init tool
def _init_bioproperties(self, population):
''' Set the population attribute and link each neuron to its group. '''
if issubclass(population.__class__, NeuralPop):
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, from_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`
'''
self.__id = self.__class__.__max_id
self.__class__.__num_networks += 1
self.__class__.__max_id += 1
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,
from_graph=from_graph, **kwargs)
def __del__ (self):
super(SpatialNetwork, self).__del__()
self.__class__.__num_networks -= 1