Source code for dense.morphology.connections

# -*- coding: utf-8 -*-
#
# connections.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 shapely.geometry import MultiPolygon

from .. import _pygrowth as _pg
from ..elements import Population
from ..units import um
from .graph import SpatialMultiNetwork, SpatialNetwork


__all__ = [
    "generate_network",
    "get_connections",
]


# ------------------------------ #
# Conection generation functions #
# ------------------------------ #

[docs]def generate_network(source_neurons=None, target_neurons=None, method="intersections", spine_density=0.5/(um**2), connection_probability=0.2, default_synaptic_strength=1., only_new_connections=False, autapse_allowed=False, multigraph=False, **kwargs): r""" Create the graph based on the neuron shapes, the spine density, and a connection probability. The number of connection made will depend on the number of contacts between an axon and a dendrite. At each contact site, the number of potential synapses is computed as: .. math:: n_{s, p} = \rho_s \cdot A_I with :math:`\rho_s` the `spine_density` and :math:`A_I` the intersection area. And the number of actual synapses is then: .. math:: N_s = n_{s,p} \cdot p_c with :math:`p_c` the connection probability. Parameters ---------- source_neurons : list of neurons, optional (default: all neurons) Neurons which will possess the pre-synaptic compartments of the connections (i.e. be connected through their axons). target_neurons : list of neurons, optional (default: all neurons) Neurons which will possess the post-synaptic compartments of the connections (i.e. be connected through their dendrites or soma) method : str, optional (default: "intersection") Method which use to generate synapses. Either "intersections" (synapses can be generated only when neurites overlap) or "spines" (neurites can be connected if they are closer than a certain distance `max_spine_length`). spine_density : float (quantity), optional (default: :math:`0.5 \\mu m^{-2}`) Number of spines per unit area, determines how many synapses are possible given an area of interaction. connection_probability : float, optional (default: 0.2) Probability of making a synapse for each spine/axon interaction which has been found geometrically. default_synaptic_strength : float, optional (default: 1.) Number caracterizing the default strength of a synapse. If `multigraph` is False, equivalent connections will always have a strength which is a multiple of this value. only_new_connections : bool, optional (default: False) If true, only the potential synapses that have been found during the last simulation run will be used; otherwise, all potential sites found since time 0 will be used. autapse_allowed : bool, optional (default: False) Whether connection from a neuron onto itself are generated if possible. multigraph : bool, optional (default: False) Whether the graph returned is simple (only one connection between each pair of neurons) or a multigraph (multiple connections can exist between every two neurons). If false, multiple connections which may exist between two neurons are merged into one equivalent connection with an increased synaptic strength and the average properties of the real connections (e.g. from three synapses of strength 1. and soma-to-soma distances :math:`120 \\mu m`, :math:`140 \\mu m`, and :math:`160 \\mu m`, one will get a single connection of strength 3. and of average length :math:`140 \\mu m`). **kwargs : optional arguments When using the "spines" `method`, an additional argument `max_spine_length` must be passed, specifying the maximum length at which neighboring neurites can still be connected through a spine (must be a dimensioned quantity, a length). If :func:`~dense.morphology.get_connections` has been called before, the network can be directly created from the returned `data` by passsing ``data=data`` in the call to ``generate_network``. See also -------- :func:`~dense.morphology.get_connections` Details on the connection algorithms are available on the graph generation page in the user manual. """ edges = kwargs.get("edges", None) positions = kwargs.get("positions", None) distances = kwargs.get("distances", None) if source_neurons is None: source_neurons = _pg.get_neurons(as_ints=True) if target_neurons is None: target_neurons = _pg.get_neurons(as_ints=True) if edges is None: edges, positions, distances = get_connections( source_neurons=source_neurons, target_neurons=target_neurons, method=method, spine_density=spine_density, connection_probability=connection_probability, only_new_connections=only_new_connections, autapse_allowed=autapse_allowed, **kwargs) population = None neurons = set(source_neurons) neurons.update(target_neurons) population = Population.from_gids(neurons) num_neurons = len(neurons) NetClass = SpatialMultiNetwork if multigraph else SpatialNetwork shape = _pg.get_environment() unit = "micrometer" if shape is None else shape.unit positions = np.array( [neuron.position.to(unit).magnitude for neuron in population]) # test if there is a network to create if not neurons and shape is None: raise RuntimeError('Cannot create a network without any neurons ' 'or environment.') network = NetClass(population=population, shape=shape, positions=positions, multigraph=multigraph) num_synapses = len(edges) # edge data data = {} # for multigraphs we keep the positions as valid attributes if multigraph and positions is not None: data["synapse_position"] = np.array(positions) if distances is not None: data["distance"] = np.array(distances) network.new_edges(edges, attributes=data, unit_strength=default_synaptic_strength) return network
[docs]def get_connections(source_neurons=None, target_neurons=None, method="intersections", spine_density=0.5/(um**2), connection_probability=0.2, autapse_allowed=False, **kwargs): """ Obtain connection between `source_neurons` and `target_neurons` through a given method for synapse generation. The number of connection made will depend on the number of contacts between an axon and a dendrite. At each contact site, the number of potential synapses is computed as: .. math:: n_{s, p} = \rho_s \cdot A_I with :math:`\rho_s` the `spine_density` and :math:`A_I` the intersection area. And the number of actual synapses is then: .. math:: N_s = n_{s,p} \cdot p_c with :math:`p_c` the connection probability. Parameters ---------- source_neurons : list of neurons, optional (default: all neurons) Neurons which will possess the pre-synaptic compartments of the connections (i.e. be connected through their axons) target_neurons : list of neurons, optional (default: all neurons) Neurons which will possess the post-synaptic compartments of the connections (i.e. be connected through their dendrites or soma) method : str, optional (default: "intersections") Method which use to generate synapses. Either "intersections" (synapses can be generated only when neurites overlap) or "spines" (neurites can be connected if they are closer than a certain distance `max_spine_length`). spine_density : float (quantity), optional (default: :math:`0.5 \mu m^{-2}`) Number of spines per unit area, determines how many synapses are made given an area of interaction. connection_probability : float, optional (default: 0.2) Probability of making a synapse for each spine/axon interaction which has been found geometrically. only_new_connections : bool, optional (default: False) If true, only the potential synapses that have been found during the last simulation run will be used; otherwise, all potential sites found since time 0 will be used. autapse_allowed : bool, optional (default: False) Whether connection from a neuron onto itself are generated if possible. **kwargs : optional arguments When using the "spines" `method`, an additional argument `max_spine_length` must be passed, specifying the maximum length at which neighboring neurites can still be connected through a spine (must be a dimensioned quantity, a length). Returns ------- edges : list of edges of shape (e, 2) The edges created. positions : list of points of shape (e, 2) The position of each synapse created. distances : list of float of length e Approximation of the cable distance between the neurons, given by the sum of the distances between the somas and the synapse. """ crossings_only, max_spine_length = None, None if method == "intersections": crossings_only = True elif method == "spines": crossings_only = False if 'max_spine_length' in kwargs: max_spine_length = kwargs["max_spine_length"].m_as("micrometer") else: raise AttributeError("`max_spine_length` must be passed if " "`method` is 'spines'.") else: raise ValueError("`method` must be either 'intersections' or 'spines'.") if source_neurons is None: source_neurons = _pg.get_neurons(as_ints=True) if target_neurons is None: target_neurons = _pg.get_neurons(as_ints=True) source_set = set(source_neurons) target_set = set(target_neurons) all_neurons = source_set.union(target_set) edges, positions, distances = [], [], [] if all_neurons: syn_density = spine_density.m_as("1 / micrometer**2") axons, dendrites, somas = _pg._get_geom_skeleton(all_neurons) soma_pos = np.array(somas)[:2, :].T if crossings_only: edges, positions, distances = _edges_from_intersections( source_set, target_set, axons, dendrites, soma_pos, syn_density, connection_probability, autapse_allowed) else: edges, positions, distances = _edges_from_spines( source_set, target_set, axons, dendrites, soma_pos, syn_density, connection_probability, autapse_allowed, max_spine_length) return edges, positions, distances
# ------------------------------ # # Python-level synapse formation # # ------------------------------ # def _get_synapses_intersection(axon_polygon, d_polygon, synapse_density, somas, connection_probability, etuple, i, j, edges, positions, distances): ''' Tool fuction to find synapses of intersecting neurites ''' intsct = axon_polygon.intersection(d_polygon) if not isinstance(intsct, MultiPolygon): intsct = [intsct] for poly in intsct: total = poly.area * synapse_density * connection_probability rnd = np.random.random() num_synapses = int(total) + (1 if (total - int(total)) < rnd else 0) if num_synapses > 0: s_soma = np.array(somas[i]) t_soma = np.array(somas[j]) pos = poly.centroid dist = np.linalg.norm(s_soma - pos) \ + np.linalg.norm(t_soma - pos) positions.extend([pos]*num_synapses) edges.extend([etuple]*num_synapses) distances.extend([dist]*num_synapses) def _edges_from_intersections(source_set, target_set, axons, dendrites, somas, synapse_density, connection_probability, autapse_allowed): """ Obtain synapses with a simple approach based only on neurite intersections. """ edges, positions, distances = [], [], [] for i, (axon_gid, axon_polygon) in enumerate(axons.items()): if axon_gid in source_set: for j, (dend_gid, vd) in enumerate(dendrites.items()): connection_allowed = (dend_gid != axon_gid or autapse_allowed) if dend_gid in target_set and connection_allowed: etuple = (axon_gid, dend_gid) for d_polygon in vd: if axon_polygon.intersects(d_polygon): _get_synapses_intersection( axon_polygon, d_polygon, synapse_density, somas, connection_probability, etuple, i, j, edges, positions, distances) return edges, positions, distances def _edges_from_spines(source_set, target_set, axons, dendrites, somas, synapse_density, connection_probability, autapse_allowed, max_spine_length): """ Create synapses based on distance and spine density. """ edges, positions, distances = [], [], [] # save dendrites buffers buffers = {} for i, (axon_gid, axon_polygon) in enumerate(axons.items()): if axon_gid in source_set: axon_polygon = axon_polygon.buffer(max_spine_length) for j, (dend_gid, vd) in enumerate(dendrites.items()): connection_allowed = (dend_gid != axon_gid or autapse_allowed) if dend_gid in target_set and connection_allowed: etuple = (axon_gid, dend_gid) for k, d_polygon in enumerate(vd): if axon_polygon.intersects(d_polygon): _get_synapses_intersection( axon_polygon, d_polygon, synapse_density, somas, connection_probability, etuple, i, j, edges, positions, distances) else: # buffer the neurite to get intersections with # extending spines d_buffer = None d_tuple = (j, k) if d_tuple not in buffers: d_buffer = d_polygon.buffer(max_spine_length) buffers[d_tuple] = d_buffer else: d_buffer = buffers[d_tuple] if axon_polygon.intersects(d_buffer): # we use a linear spine density of an area # 1 micron thick along the intersected line # i.e. linear density has same magnitude as # areal density insct_line = \ axon_polygon.intersection(d_buffer.exterior) # then compute the number of synapses total = insct_line.length * synapse_density \ * connection_probability rnd = np.random.random() num_syn = int(total) + \ (1 if (total - int(total)) < rnd else 0) if num_syn > 0: s_soma = np.array(somas[i]) t_soma = np.array(somas[j]) pos = insct_line.centroid dist = np.linalg.norm(s_soma - pos) \ + np.linalg.norm(t_soma - pos) positions.extend([pos]*num_syn) edges.extend([etuple]*num_syn) distances.extend([dist]*num_syn) return edges, positions, distances # ------ # # Future # # ------ # def _generate_network_future(source_neurons=None, target_neurons=None, method="intersections", spine_density=0.5/(um**2), default_synaptic_strength=1., only_new_connections=False, autapse_allowed=False, multigraph=False, **kwargs): """ Create the graph. THIS FUNCTION IS WORKING BUT IS TOO INEFFICIENT FOR THE MOMENT, IT REQUIRES A REWRITE OF THE C++ LEVEL ROUTINES AND MOST PROBABLY A COMPLETE REFACTORING OF THE WAY NEURITE POLYGONS ARE STORED (TYPICALLY MERGING THEM INTO BIGGER POLYGONS OVER TIME). Parameters ---------- source_neurons : list of neurons, optional (default: all neurons) Neurons which will possess the pre-synaptic compartments of the connections (i.e. be connected through their axons). target_neurons : list of neurons, optional (default: all neurons) Neurons which will possess the post-synaptic compartments of the connections (i.e. be connected through their dendrites or soma) method : str, optional (default: "intersection") Method which use to generate synapses. Either "intersections" (synapses can be generated only when neurites overlap) or "spines" (neurites can be connected if they are closer than a certain distance `max_spine_length`). spine_density : float (quantity), optional (default: :math:`0.5 \mu m^{-2}`) Number of spines per unit area, determines how many synapses are made given an area of interaction. default_synaptic_strength : float, optional (default: 1.) Number caracterizing the default strength of a synapse. If `multigraph` is False, equivalent connections will always have a strength which is a multiple of this value. only_new_connections : bool, optional (default: False) If true, only the potential synapses that have been found during the last simulation run will be used; otherwise, all potential sites found since time 0 will be used. autapse_allowed : bool, optional (default: False) Whether connection from a neuron onto itself are generated if possible. multigraph : bool, optional (default: False) Whether the graph returned is simple (only one connection between each pair of neurons) or a multigraph (multiple connections can exist between every two neurons). If false, multiple connections which may exist between two neurons are merged into one equivalent connection with an increased synaptic strength and the average properties of the real connections (e.g. from three synapses of strength 1. and soma-to-soma distances :math:`120 \mu m`, :math:`140 \mu m`, and :math:`160 \mu m`, one will get a single connection of strength 3. and of average length :math:`140 \mu m`). **kwargs : optional arguments If :func:`~dense.morphology.get_connections` has been called before, the network can be directly created from the returned `data` by passsing ``data=data`` in the call to ``generate_network``. See also -------- :func:`~dense.morphology.get_connections` Details on the connection algorithms are available on the graph generation page in the user manual. """ data = kwargs.get("data", None) if source_neurons is None: source_neurons = _pg.get_neurons(as_ints=True) if target_neurons is None: target_neurons = _pg.get_neurons(as_ints=True) if data is None: data = get_connections( source_neurons=source_neurons, target_neurons=target_neurons, method=method, spine_density=spine_density, only_new_connections=only_new_connections, autapse_allowed=autapse_allowed) population = None neurons = set(source_neurons) neurons.update(target_neurons) population = Population.from_gids(neurons) num_neurons = len(neurons) NetClass = SpatialMultiNetwork if multigraph else SpatialNetwork shape = _pg.get_environment() unit = "micrometer" if shape is None else shape.unit positions = np.array( [neuron.position.to(unit).magnitude for neuron in population]) network = NetClass(population=population, shape=shape, positions=positions) # prepare the edges elist = np.array([data["source_neuron"], data["target_neuron"]], dtype=int) num_synapses = len(elist[0]) del data["source_neuron"] del data["target_neuron"] # edge data if multigraph: # we keep the positions as valid attributes positions = None if method == "intersections": x, y = data["source_pos_x"], data["source_pos_y"] positions = [(pre, post) for pre, post in zip(x, y)] else: x1, y1 = data["source_pos_x"], data["source_pos_y"] x2, y2 = data["target_pos_x"], data["target_pos_y"] positions = [(0.5*(sx + tx), 0.5*(sy + ty)) for sx, sy, tx, ty in zip(x1, y1, x2, y2)] data["synapse_position"] = positions del data["source_pos_x"] del data["source_pos_y"] del data["target_pos_x"] del data["target_pos_y"] # we compute the length, caracterizing the travel distance between the two # neurons along the neurites data["distance"] = np.zeros(num_synapses) # @todo compute pre_segment_id, pre_fraction_along for NeuroML # data["pre_synapse_location"] = np.zeros(num_synapses) # data["post_synapse_location"] = np.zeros(num_synapses) for i in range(num_synapses): source, target = elist[:, i] s_neur = data["source_neurite"][i] s_node = data["source_node"][i] s_seg = data["source_segment"][i] t_neur = data["target_neurite"][i] t_node = data["target_node"][i] t_seg = data["target_segment"][i] s_dist_to_soma, s_dist_to_parent = _pg._get_parent_and_soma_distances( source, s_neur, s_node, s_seg) t_dist_to_soma, t_dist_to_parent = _pg._get_parent_and_soma_distances( target, t_neur, t_node, t_seg) data["distance"][i] = s_dist_to_soma + t_dist_to_soma # data["pre_synapse_location"][i] = s_dist_to_parent # data["post_synapse_location"][i] = t_dist_to_parent # remove now unnecessary data about neurite, node, and segment del data["source_neurite"] del data["source_node"] del data["source_segment"] del data["target_neurite"] del data["target_node"] del data["target_segment"] data["weight"] = np.repeat(default_synaptic_strength, num_synapses) network.new_edges(elist.T, attributes=data) return network def _get_connections_future(source_neurons=None, target_neurons=None, method="intersections", spine_density=0.5/(um**2), only_new_connections=False, autapse_allowed=False, **kwargs): """ Obtain connection between `source_neurons` and `target_neurons` through a given method for synapse generation. THIS FUNCTION IS WORKING BUT IS TOO INEFFICIENT FOR THE MOMENT, IT REQUIRES A REWRITE OF THE C++ LEVEL ROUTINES AND MOST PROBABLY A COMPLETE REFACTORING OF THE WAY NEURITE POLYGONS ARE STORED (TYPICALLY MERGING THEM INTO BIGGER POLYGONS OVER TIME). Parameters ---------- source_neurons : list of neurons, optional (default: all neurons) Neurons which will possess the pre-synaptic compartments of the connections (i.e. be connected through their axons) target_neurons : list of neurons, optional (default: all neurons) Neurons which will possess the post-synaptic compartments of the connections (i.e. be connected through their dendrites or soma) method : str, optional (default: "intersections") Method which use to generate synapses. Either "intersections" (synapses can be generated only when neurites overlap) or "spines" (neurites can be connected if they are closer than a certain distance `max_spine_length`). spine_density : float (quantity), optional (default: :math:`0.5 \mu m^{-2}`) Number of spines per unit area, determines how many synapses are made given an area of interaction. only_new_connections : bool, optional (default: False) If true, only the potential synapses that have been found during the last simulation run will be used; otherwise, all potential sites found since time 0 will be used. autapse_allowed : bool, optional (default: False) Whether connection from a neuron onto itself are generated if possible. Returns ------- data : dict Dictionary provided the entries: "source_neuron", "target_neuron", "source_neurite", "target_neurite", "source_node", "target_node", "source_segment", "target_node", "source_pos_x", "target_pos_x", "source_pos_y", and "target_pos_y". Each entry is associated to a list of length `num_synapses`. """ crossings_only = None if method == "intersections": crossings_only = True elif method == "spines": crossings_only = False else: raise ValueError("`method` must be either 'intersections' or 'spines'.") if source_neurons is None: source_neurons = _pg.get_neurons(as_ints=True) if target_neurons is None: target_neurons = _pg.get_neurons(as_ints=True) density = spine_density.m_as("1 / micrometer**2") return _pg._generate_synapses( crossings_only, density, only_new_connections, autapse_allowed, source_neurons, target_neurons)