Source code for dense.morphology.graph

# -*- coding: utf-8 -*-
#
# graph.py
#
# This file is part of DeNSE.
#
# Copyright (C) 2019 SeNEC Initiative
#
# DeNSE is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# DeNSE is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with DeNSE. If not, see <http://www.gnu.org/licenses/>.


""" Class and functions for graph creation """

from collections import defaultdict, OrderedDict

import numpy as np

from .. import _pygrowth as _pg
from ..elements import Population
from ..units import um


__all__ = [
    "SpatialNetwork",
    "SpatialMultiNetwork",
]


# --------------- #
# Network classes #
# --------------- #


[docs]class SpatialMultiNetwork(object): ''' Backup class to store network information ''' def __new__(cls, *args, **kwargs): return super().__new__(cls) def __init__(self, population, name="Graph", weighted=True, directed=True, shape=None, positions=None, multigraph=False, **kwargs): self.name = name self._population = population self.weighted = weighted self.directed = directed self.shape = shape self.positions = positions self._multigraph = multigraph # graph informations self._nodes = {int(n) for n in population} self._edge_nb = 0 self._edges = defaultdict(set) self._weights = [] # synaptic details syn_details = { # "presyn_neurites": [], # "postsyn_neurites": [], # "presyn_nodes": [], # "postsyn_nodes": [], # "presyn_segments": [], # "postsyn_segments": [], # "presyn_distances": [], # "postsyn_distances": [], # "presyn_pos": [], # "postsyn_pos": [], "distance": [], "synapse_position": [], } self._attributes = syn_details if multigraph else {"distance": []}
[docs] def new_edge(self, source, target, attributes=None, **kwargs): ''' Adding a connection to the network. Parameters ---------- source : :class:`int/node` Source node. target : :class:`int/node` Target node. attributes : :class:`dict`, optional (default: ``{}``) Dictionary containing optional edge properties. If the graph is weighted, defaults to ``{"weight": 1.}``, the unit weight for the connection. Returns ------- The new connection. ''' attributes = {} if attributes is None else attributes attr_keys = [] for k in attributes: if k != "weight" and not _with_nngt: assert k in self._attributes, \ "`" + k + "` is not a valid synaptic attribute." attr_keys.append(k) enum = self._edge_nb edge = (source, target) self._nodes.update(edge) if edge not in self._edges: self._edges[edge] = {enum} else: self._edges[edge].add(enum) self._edge_nb += 1 for k, v in attributes.items(): if k != "weight": self._attributes[k].append(v) else: self._weights.append(v)
[docs] def new_edges(self, edge_list, attributes=None, **kwargs): ''' Add a list of edges to the graph. Parameters ---------- edge_list : 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 : :class:`dict`, optional (default: ``{}``) Dictionary containing optional edge properties. If the graph is weighted, defaults to ``{"weight": ones}``, where ``ones`` is an array the same length as the `edge_list` containing a unit weight for each connection. Returns ------- Returns new edges only. ''' attr_keys = [] for k in attributes: if k != "weight" and not _with_nngt: assert k in self._attributes, \ "`" + k + "` is not a valid synaptic attribute." attr_keys.append(k) self._nodes.update(np.ravel(edge_list)) enum = self._edge_nb for i, e in enumerate(edge_list): connection = tuple(e) if connection not in self._edges: self._edges[connection] = {enum + i} else: self._edges[connection].add(enum + i) self._edge_nb += 1 for k, v in attributes.items(): if k != "weight": self._attributes[k].extend(v) else: self._weights.extend(v)
[docs] def node_nb(self): ''' Number of nodes in the network ''' return len(self._nodes)
[docs] def edge_nb(self): ''' Number of edges in the network ''' return self._edge_nb
[docs] def edge_id(self, edge, return_set=False): ''' Id of an edge ''' if edge in self._edges: ids = self._edges[edge] if len(ids) == 1 and not return_set: return next(iter(ids)) else: return ids else: raise KeyError("SpatialNetwork has no edge " + str(edge) + ".")
[docs] def has_edge(self, edge): ''' Return True if the edge is in the graph, else False ''' return edge in self._edges
@property def population(self): ''' The neuronal population ''' return self._population @property def edges_array(self): ''' Edges of the graph, sorted by order of creation, as an array of shape (edge_nb, 2). ''' edges = [] eids = [] if self._multigraph: for k, v in self._edges.items(): eids.extend(v) edges.extend([k]*len(v)) else: for k, v in self._edges.items(): eids.append(min(v)) edges.append(k) esort = np.argsort(eids) return np.array(edges, dtype=int)[esort, :]
[docs] def is_weighted(self): ''' Whether the graph has weights or not ''' return self.weighted
[docs] def is_multigraph(self): ''' Whether duplicate edges are supported ''' return self._multigraph
[docs] def get_edge_attributes(self, edges=None, name=None): ''' Attributes of the network's edges. Parameters ---------- edge : tuple or list of tuples, optional (default: ``None``) Edge whose attribute should be displayed. name : str, optional (default: ``None``) Name of the desired attribute. Returns ------- Dict containing all graph's attributes (synaptic weights, delays...) by default. If `edge` is specified, returns only the values for these edges. If `name` is specified, returns value of the attribute for each edge. Note ---- The attributes values are ordered as the edges in :func:`~dense.SpatialNetwork.edges_array` if `edges` is None. ''' eids = [] if edges is not None: if isinstance(edges[0], int): eids.extend(self.edge_id(edges, return_set=True)) else: for e in edges: eids.extend(self.edge_id(e), return_set=True) if name is not None and edges is not None: attributes = None if name == "weight": attributes = [self._weights[i] for i in eids] else: attributes = [self._attributes[name][i] for i in eids] if len(eids) == 1: return attributes[0] return attributes elif name is None and edges is None: attributes = self._attributes.copy() attributes["weight"] = self._weights return attributes elif name is None: attributes = {k: [] for k in self._attributes} attributes["weight"] = [] for eid in eids: for k, v in self._attributes.items(): attributes[k].append(v[eid]) attributes["weight"].append(self._weights[eid]) return attributes else: if name == "weight": return [v for v in self._weights] return [v for v in self._attributes[name]]
[docs] def set_edge_attribute(self, attribute, values=None, val=None, value_type=None, edges=None): ''' Set attributes to the connections between neurons. Parameters ---------- attribute : str The name of the attribute. value_type : str Type of the attribute, among 'int', 'double', 'string' values : array, optional (default: None) Values with which the edge attribute should be initialized. (must have one entry per node in the graph) val : int, float or str , optional (default: None) Identical value for all edges. value_type : str, optional (default: None) Type of the attribute, among 'int', 'double', 'string'. Only used if the attribute does not exist and must be created. edges : list of edges or array of shape (E, 2), optional (default: all) Edges whose attributes should be set. Others will remain unchanged. ''' if attribute not in self._attributes: raise RuntimeError("Unknown attribute `" + attribute + "`.") else: num_edges = self.edge_nb() if edges is None else len(edges) if values is None: if val is not None: values = np.repeat(val, num_edges) else: raise RuntimeError( "At least one of the `values` and `val` arguments " "should not be ``None``.") assert len(values) == len(edges), \ "Number of values and number of edges differ." eids = [] if edges is None: eids = range(self._edge_nb) else: for e in edges: eids.extend(self.edge_id(e, return_set=True)) if attribute == "weight": for i in eids: self._weights[i] = values[i] else: for i in eids: self._attributes[attribute][i] = values[i]
try: import nngt if nngt.__version__ < "1.1.1": import warnings warnings.warn("NNGT 1.1.1 or above is required, please update to a " "more recent version to get full graph functionalities.") raise ImportError("NNGT version is too old.") _with_nngt = True _BaseNetwork = nngt.SpatialGraph except ImportError: _with_nngt = False _BaseNetwork = SpatialMultiNetwork
[docs]class SpatialNetwork(_BaseNetwork): ''' Class containing the information about the connections between neurons in a simplified manner (keeping only one equivalent synapse to represent all the connections between two neurons). ''' def __new__(cls, *args, **kwargs): return super().__new__(cls) def __init__(self, population, name="SpatialNetwork", weighted=True, directed=True, shape=None, from_graph=None, positions=None, equivalent_multigraph=True, **kwargs): ''' Initialize SpatialNetwork instance Parameters ---------- name : string, optional (default: "SpatialNetwork") The name of this :class:`~dense.morphology.SpatialNetwork` 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:`~dense.environment.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. equivalent_multigraph : bool, optional (default: True) Whether duplicate edges should be summed into one equivalent connection or be considered as errors. Returns ------- self : :class:`~dense.morphology.SpatialNetwork` ''' num_nodes = population.size self._nodes = set(population.ids) self._multigraph = equivalent_multigraph if _with_nngt: population = nngt.NeuralPop.uniform(len(population)) # don't pass population to nngt super().__init__( nodes=num_nodes, name=name, weighted=weighted, directed=directed, shape=shape, positions=positions, from_graph=from_graph, **kwargs) self._population = population self.new_edge_attribute("multiplicity", "int", val=1) else: super().__init__( population, nodes=num_nodes, name=name, weighted=weighted, directed=directed, shape=shape, positions=positions, from_graph=from_graph, **kwargs) if population is None: raise RuntimeError("Network needs a NeuralPop to be created") @property def population(self): ''' The neuronal population ''' return self._population
[docs] def new_edge(self, source, target, attributes=None, **kwargs): ''' Adding a connection to the graph, with optional properties. Parameters ---------- source : :class:`int/node` Source node. target : :class:`int/node` Target node. attributes : :class:`dict`, optional (default: ``{}``) Dictionary containing optional edge properties. If the graph is weighted, defaults to ``{"weight": 1.}``, the unit weight for the connection (synaptic strength in NEST). Returns ------- The new connection. ''' #check attributes if attributes is None: attributes = {} # check that the edge does not already exist edge = (source, target) if source not in self._nodes: raise ValueError("There is no node {}.".format(source)) if target not in self._nodes: raise ValueError("There is no node {}.".format(target)) if not self.has_edge(edge): super().new_edge( source, target, attributes=attributes, **kwargs) else: if not self._multigraph: raise RuntimeError("Trying to add existing edge.") old_attributes = self.get_edge_attributes(edges=edge) for k in old_attributes: if k not in {"weight", "multiplicity"}: assert k in attributes, \ "Value for attribute `" + k + "`must be provided." if self.is_weighted(): weight = attributes.get("weight", 1.) + old_attributes["weight"] self.set_edge_attribute("weight", val=weight, edges=[edge]) # get multiplicity before updating the attribute values (weighting) old_mult = old_attributes.get("multiplicity", len(self._edges[edge])) if _with_nngt: self.set_edge_attribute("multiplicity", val=old_mult + 1, edges=[edge]) for k in old_attributes: if k not in {"weight", "multiplicity"}: new_val = (old_mult*old_attributes[k] + attributes[k]) / (old_mult + 1.) self.set_edge_attribute(k, val=new_val, edges=[edge]) return edge
[docs] def new_edges(self, edge_list, attributes=None, unit_strength=1., **kwargs): ''' Add a list of edges to the network. Parameters ---------- edge_list : 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 : :class:`dict`, optional (default: ``{}``) Dictionary containing optional edge properties. If the graph is weighted, defaults to ``{"weight": ones}``, where ``ones`` is an array the same length as the `edge_list` containing a unit weight for each connection (synaptic strength in NEST). unit_strength: double, optional (default: 1.) Default weight associated to one synapse; neurons having multiple synapses connecting them will get an equivalent connection of strength `unit_strength*num_synapses`. Returns ------- Returns new edges only. ''' #check attributes if attributes is None: attributes = {} edge_list = np.array(edge_list, dtype=int) for k in attributes: if k != "weight" and not _with_nngt: assert k in self._attributes, \ "`" + k + "` is not a valid synaptic attribute." assert self._nodes.issuperset(np.ravel(edge_list)), \ "Some nodes in `edge_list` do not exist in the network." if self.is_weighted() and "weight" not in attributes: attributes["weight"] = unit_strength*np.ones(len(edge_list)) enum = self.edge_nb() if not _with_nngt: # Backup instance, the existing edges need to be updated ecount = 0 for i, e in enumerate(edge_list): e = tuple(e) try: idx = self.edge_id(e) exists = True except KeyError: exists = False if exists: if self.is_weighted(): w = attributes["weight"][i] self._weights[idx] += w # weighted average for other attributes mult = len(self._edges[e]) for k, v in self._attributes.items(): # some attributes may be skipped but they must be # for all connections if k in attributes: v[idx] = (mult*v[idx] + attributes[k][i]) / (mult + 1.) else: assert not self._attributes[k], \ "Attribute '" + k + "' is required." else: if self.is_weighted(): self._weights.append(attributes["weight"][i]) # tmp dict because attributes may be skipped tmp_dict = {k: [] for k in self._attributes} for k, v in self._attributes.items(): # some attributes may be skipped but they must be # for all connections if k in attributes: tmp_dict[k].append(attributes[k][i]) for k, v in tmp_dict.items(): # if skipped, checked that it was for all connections if not v: assert not self._attributes[k], \ "Attribute '" + k + "' is required." else: self._attributes[k].extend(v) # add edge set self._edges[e] = {enum + ecount} self._edge_nb += 1 ecount += 1 else: existing = np.zeros(len(edge_list), dtype=bool) for i, e in enumerate(edge_list): try: self.edge_id(e) existing[i] = 1 except: pass num_existing = np.sum(existing) exist_edges = edge_list[existing] exist_attrs = {k: attributes[k][existing] for k in attributes} new_attrs = {k: attributes[k][~existing] for k in attributes} if self.is_weighted(): new_w = attributes["weight"][existing] # need to update them one by one because there could be multiple # duplicates for a same edge in `edge_list` for i, e in enumerate(exist_edges): old_w = self.get_weights(edges=[e])[0] self.set_weights(old_w + new_w[i], elist=[e]) # again, updating the other attributes one by one for i, e in enumerate(exist_edges): old_attrs = self.get_edge_attributes([e]) mult = old_attrs["multiplicity"] self.set_edge_attribute("multiplicity", val=mult + 1, edges=[e]) for k, v in exist_attrs.items(): if k not in {"weight", "multiplicity"}: new_val = (mult*old_attrs[k] + v[i]) / (mult + 1.) self.set_edge_attribute("multiplicity", val=new_val, edges=[e]) # new edges can also not be added in bulk directly edges = OrderedDict() multiplicity = OrderedDict() final_attrs = {k: [] for k in attributes} for i, e in enumerate(edge_list[~existing]): etuple = tuple(e) if etuple in edges: j = edges[etuple] m = multiplicity[etuple] + 1 multiplicity[etuple] = m for k in final_attrs: if k == "weight": final_attrs[k][j] += unit_strength else: final_attrs[k][j] = \ (m*final_attrs[k][j] + new_attrs[k][i])/(m + 1.) else: edges[etuple] = len(edges) multiplicity[etuple] = 1 for k in final_attrs: if k == "weight": final_attrs[k].append(unit_strength) else: final_attrs[k].append(new_attrs[k][i]) new_attrs["multiplicity"] = np.ones(len(edge_list) - num_existing, dtype=int) final_elist = list(edges.keys()) super().new_edges(final_elist, final_attrs)