# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2015-2023 Tanguy Fardet
# SPDX-License-Identifier: GPL-3.0-or-later
# nngt/core/networks.py
""" Network and SpatialNetwork classes for neuroscience integration """
import numpy as np
import nngt
from nngt.lib import (InvalidArgument, nonstring_container, default_neuron,
default_synapse)
from .graph import Graph
from .spatial_graph import SpatialGraph
# ------- #
# Network #
# ------- #
[docs]
class Network(Graph):
"""
The detailed class that inherits from :class:`~nngt.Graph` and implements
additional properties to describe various biological functions
and interact with the NEST simulator.
"""
#-------------------------------------------------------------------------#
# Class attributes and methods
__num_networks = 0
__max_id = 0
[docs]
@classmethod
def num_networks(cls):
''' Returns the number of alive instances. '''
return cls.__num_networks
[docs]
@classmethod
def from_gids(cls, gids, get_connections=True, get_params=False,
neuron_model=default_neuron, neuron_param=None,
syn_model=default_synapse, syn_param=None, **kwargs):
'''
Generate a network from gids.
Warning
-------
Unless `get_connections` and `get_params` is True, or if your
population is homogeneous and you provide the required information, the
information contained by the network and its `population` attribute
will be erroneous!
To prevent conflicts the :func:`~nngt.Network.to_nest` function is not
available. If you know what you are doing, you should be able to find a
workaround...
Parameters
----------
gids : array-like
Ids of the neurons in NEST or simply user specified ids.
get_params : bool, optional (default: True)
Whether the parameters should be obtained from NEST (can be very
slow).
neuron_model : string, optional (default: None)
Name of the NEST neural model to use when simulating the activity.
neuron_param : dict, optional (default: {})
Dictionary containing the neural parameters; the default value will
make NEST use the default parameters of the model.
syn_model : string, optional (default: 'static_synapse')
NEST synaptic model to use when simulating the activity.
syn_param : dict, optional (default: {})
Dictionary containing the synaptic parameters; the default value
will make NEST use the default parameters of the model.
Returns
-------
net : :class:`~nngt.Network` or subclass
Uniform network of disconnected neurons.
'''
from nngt.lib.errors import not_implemented
if neuron_param is None:
neuron_param = {}
if syn_param is None:
syn_param = {}
# create the population
size = len(gids)
nodes = [i for i in range(size)]
group = nngt.NeuralGroup(
nodes, neuron_type=1, neuron_model=neuron_model,
neuron_param=neuron_param)
pop = nngt.NeuralPop.from_groups([group])
# create the network
net = cls(population=pop, **kwargs)
net.nest_gids = np.array(gids)
net._id_from_nest_gid = {gid: i for i, gid in enumerate(gids)}
net.to_nest = not_implemented
if get_connections:
from nngt.simulation import get_nest_adjacency
converter = {gid: i for i, gid in enumerate(gids)}
mat = get_nest_adjacency(converter)
edges = np.array(mat.nonzero()).T
w = mat.data
net.new_edges(edges, {'weight': w}, check_duplicates=False,
check_self_loops=False, check_existing=False)
if get_params:
raise NotImplementedError('`get_params` not implemented yet.')
return net
[docs]
@classmethod
def exc_and_inhib(cls, size, iratio=0.2, en_model=default_neuron,
en_param=None, in_model=default_neuron, in_param=None,
syn_spec=None, **kwargs):
'''
Generate a network containing a population of two neural groups:
inhibitory and excitatory neurons.
Parameters
----------
size : int
Number of neurons in the network.
i_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.
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.
syn_spec : dict, optional (default: static synapse)
Dictionary containg a directed edge between groups as key and the
associated synaptic parameters for the post-synaptic neurons (i.e.
those of the second group) as value. If provided, all connections
between groups will be set according to the values contained in
`syn_spec`. Valid keys are:
- `('excitatory', 'excitatory')`
- `('excitatory', 'inhibitory')`
- `('inhibitory', 'excitatory')`
- `('inhibitory', 'inhibitory')`
Returns
-------
net : :class:`~nngt.Network` or subclass
Network of disconnected excitatory and inhibitory neurons.
See also
--------
:func:`~nngt.NeuralPop.exc_and_inhib`
'''
pop = nngt.NeuralPop.exc_and_inhib(
size, iratio, en_model, en_param, in_model, in_param,
syn_spec=syn_spec)
net = cls(population=pop, **kwargs)
return net
#-------------------------------------------------------------------------#
# Constructor, destructor and attributes
def __init__(self, name="Network", weighted=True, directed=True,
copy_graph=None, population=None, inh_weight_factor=1.,
**kwargs):
'''
Initializes :class:`~nngt.Network` instance.
.. versionchanged: 2.4
Move `from_graph` to `copy_graph` to reflect changes in Graph.
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.
copy_graph : :class:`~nngt.core.GraphObject`, optional (default: None)
An optional :class:`~nngt.core.GraphObject` to serve as base.
population : :class:`nngt.NeuralPop`, (default: None)
An object containing the neural groups and their properties:
model(s) to use in NEST to simulate the neurons as well as their
parameters.
inh_weight_factor : float, optional (default: 1.)
Factor to apply to inhibitory synapses, to compensate for example
the strength difference due to timescales between excitatory and
inhibitory synapses.
Returns
-------
self : :class:`~nggt.Network`
'''
self.__id = self.__class__.__max_id
self.__class__.__num_networks += 1
self.__class__.__max_id += 1
assert directed, "Network class cannot be undirected."
if population is None:
raise InvalidArgument("Network needs a NeuralPop to be created")
nodes = population.size
if "nodes" in kwargs.keys():
assert kwargs["nodes"] == nodes, "Incompatible values for " +\
"`nodes` = {} with a `population` of size {}.".format(
kwargs["nodes"], nodes)
del kwargs["nodes"]
if "delays" not in kwargs: # set default delay to 1.
kwargs["delays"] = 1.
super().__init__(nodes=nodes, name=name, weighted=weighted,
directed=directed, copy_graph=copy_graph,
inh_weight_factor=inh_weight_factor, **kwargs)
self._init_bioproperties(population)
def __del__(self):
super().__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__, nngt.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("Network and NeuralPop must have same "
"number of neurons.")
else:
raise AttributeError("Expecting NeuralPop but received "
"'{}'".format(population.__class__.__name__))
@property
def nest_gids(self):
return self._nest_gids
@nest_gids.setter
def nest_gids(self, gids):
self._nest_gids = gids
for group in self.population.values():
group._nest_gids = gids[group.ids]
[docs]
def get_edge_types(self, edges=None):
'''
Return the type of all or a subset of the edges.
For all edges, the types are ordered according to the edges ids, i.e.
in the same order as :property:`~nngt.Graph.edges_array`.
.. versionchanged:: 2.4
Updated it to make it compatible with the default
:class:`~nngt.Graph` function, including the `edges` argument.
Parameters
----------
edges : (E, 2) array, optional (default: all edges)
Edges for which the type should be returned.
Returns
-------
the list of types (1 for excitatory, -1 for inhibitory)
'''
edges = self.edges_array if edges is None else edges
types = np.ones(len(edges))
inhib_neurons = set()
for g in self._population.values():
if g.neuron_type == -1:
inhib_neurons.update(g.ids)
for i, e in enumerate(edges):
if e[0] in inhib_neurons:
types[i] = -1
return types
[docs]
def id_from_nest_gid(self, gids):
'''
Return the ids of the nodes in the :class:`nngt.Network` instance from
the corresponding NEST gids.
Parameters
----------
gids : int or tuple
NEST gids.
Returns
-------
ids : int or tuple
Ids in the network. Same type as the requested `gids` type.
'''
if nonstring_container(gids):
return np.array([self._id_from_nest_gid[gid] for gid in gids],
dtype=int)
else:
return self._id_from_nest_gid[gids]
[docs]
def to_nest(self, send_only=None, weights=True):
'''
Send the network to NEST.
.. seealso::
:func:`~nngt.simulation.make_nest_network` for parameters
'''
from nngt.simulation import make_nest_network
if nngt._config['with_nest']:
return make_nest_network(
self, send_only=send_only, weights=weights)
else:
raise RuntimeError("NEST is not present.")
#-------------------------------------------------------------------------#
# Init tool
def _init_bioproperties(self, population):
''' Set the population attribute and link each neuron to its group. '''
self._population = None
self._nest_gids = None
self._id_from_nest_gid = None
if not hasattr(self, '_iwf'):
self._iwf = 1.
if issubclass(population.__class__, nngt.NeuralPop):
if population.is_valid or not self.node_nb():
self._population = population
nodes = population.size
# create the delay attribute if necessary
if "delay" not in self.edge_attributes:
self.set_delays()
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__))
#-------------------------------------------------------------------------#
# Setter
[docs]
def set_types(self, edge_type, nodes=None, fraction=None):
'''
.. warning::
This function is not available for :class:`~nngt.Network`
subclasses.
'''
raise NotImplementedError("Cannot be used on :class:`~nngt.Network`.")
[docs]
def get_neuron_type(self, neuron_ids):
'''
Return the type of the neurons (+1 for excitatory, -1 for inhibitory).
Parameters
----------
neuron_ids : int or tuple
NEST gids.
Returns
-------
ids : int or tuple
Ids in the network. Same type as the requested `gids` type.
'''
if is_integer(neuron_ids):
group_name = self._population._neuron_group[neuron_ids]
neuron_type = self._population[group_name].neuron_type
return neuron_type
else:
groups = (self._population._neuron_group[i] for i in neuron_ids)
types = tuple(self._population[gn].neuron_type for gn in groups)
return types
#-------------------------------------------------------------------------#
# 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's 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:`~nngt.SpatialGraph` to provide a detailed description of a real
neural network in space, i.e. with positions and biological properties to
interact with NEST.
"""
#-------------------------------------------------------------------------#
# Class attributes
__num_networks = 0
__max_id = 0
#-------------------------------------------------------------------------#
# Constructor, destructor, and attributes
def __init__(self, population=None, name="SpatialNetwork", weighted=True,
directed=True, shape=None, copy_graph=None, positions=None,
**kwargs):
'''
Initialize SpatialNetwork instance.
.. versionchanged: 2.4
Move `from_graph` to `copy_graph` to reflect changes in Graph.
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.geometry.Shape`, optional (default: None)
Shape of the neurons' environment (None leads to a square of side
1 cm)
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.geometry.Shape` object of the instance.
population : class:`~nngt.NeuralPop`, optional (default: None)
Population from which the network will be built.
Returns
-------
self : :class:`~nngt.SpatialNetwork`
'''
self.__id = self.__class__.__max_id
self.__class__.__num_networks += 1
self.__class__.__max_id += 1
super().__init__(
name=name, weighted=weighted, directed=directed,
shape=shape, positions=positions, population=population,
copy_graph=copy_graph, **kwargs)
def __del__ (self):
super().__del__()
self.__class__.__num_networks -= 1
#-------------------------------------------------------------------------#
# Setter
[docs]
def set_types(self, syn_type, nodes=None, fraction=None):
'''
.. warning::
This function is not available for :class:`~nngt.Network`
subclasses.
'''
raise NotImplementedError("Cannot be used on "
":class:`~nngt.SpatialNetwork`.")