Source code for dense.plot.plot_structures

#
# -*- coding: utf-8 -*-
# plot_structures.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/>.

""" Plot structures (neurons, dendrograms...) """

from collections import deque

import matplotlib
from matplotlib.cm import get_cmap
from matplotlib.patches import Rectangle
from matplotlib.patches import PathPatch
from matplotlib.textpath import TextPath

import numpy as np

from .. import _pygrowth as _pg
from .._helpers import is_iterable
from ..environment import plot_shape
from ..elements import Population, Neuron
from ..units import *
from .plot_utils import *


# ------------ #
# Plot neurons #
# ------------ #

[docs]def plot_neurons(gid=None, mode=None, show_nodes=False, show_active_gc=True, culture=None, show_culture=True, aspect=1., soma_radius=None, active_gc="d", gc_size=2., soma_color='k', scale=50*um, scale_text=True, axon_color="indianred", dendrite_color="royalblue", subsample=1, save_path=None, title=None, axis=None, show_density=False, dstep=20., dmin=None, dmax=None, colorbar=True, show_neuron_id=False, show=True, xy_steps=None, x_min=None, x_max=None, y_min=None, y_max=None, **kwargs): ''' Plot neurons in the network. Parameters ---------- gid : int or list, optional (default: all neurons) Id(s) of the neuron(s) to plot. mode : str, optional (default: "sticks") How to draw the neurons. By default, the "sticks" mode shows the real width of the neurites. Switching to "lines" only leaves the trajectory of the growth cones, without information about the neurite width. Eventually, the "mixed" mode shows both informations superimposed. culture : :class:`~dense.environment.Shape`, optional (default: None) Shape of the environment; if the environment was already set using :func:`~dense.CreateEnvironment`. show_nodes : bool, optional (default: False) Show the branching nodes. show_active_gc : bool, optional (default: True) If True, display the tip (growth cone) of actively growing branches. show_culture : bool, optional (default: True) If True, displays the culture in which the neurons are embedded. aspect : float, optional (default: 1.) Set the aspect ratio between the `x` and `y` axes. soma : str, optional (default: "o") Shape of the soma marker using the matplotlib conventions. soma_radius : float, optional (default: real neuron radius) Size of the soma marker. active_gc : str, optional (default: "d") Shape of the active growth cone marker using the matplotlib conventions. gc_size : float, optional (default: 2.) Size of the growth cone marker. axon_color : valid matplotlib color, optional (default: "indianred") Color of the axons. dendrite_color : valid matplotlib color, optional (default: "royalblue") Color of the dendrites. soma_color : valid matplotlib color, optional (default: "k") Color of the soma. scale : length, optional (default: 50 microns) Whether a scale bar should be displayed, with axes hidden. If ``None``, then spatial measurements will be given through standard axes. subsample : int, optional (default: 1) Subsample the neurites to save memory. save_path : str, optional (default: not saved) Path where the plot should be saved, including the filename, pdf only. title : str, optional (default: no title) Title of the plot. axis : :class:`matplotlib.pyplot.Axes`, optional (default: None) Axis on which the plot should be drawn, otherwise a new one will be created. show_neuron_id : bool, optional (default: False) Whether the GID of the neuron should be displayed inside the soma. show : bool, optional (default: True) Whether the plot should be displayed immediately or not. dstep : number of bins for density level histogram dmin : minimal density for density histogram dmax : maximal scale for density xy_steps : number of spatial bins for density plot x_min, x_max, y_min, y_max : bounding bix for spatial density map **kwargs : optional arguments Details on how to plot the environment, see :func:`plot_environment`. Returns ------- axes : axis or tuple of axes if `density` is True. ''' import matplotlib.pyplot as plt from shapely.geometry import (Polygon, MultiPolygon) assert mode in (None, "lines", "sticks", "mixed"),\ "Unknown `mode` '" + mode + "'. Accepted values are 'lines', " +\ "'sticks' or 'mixed'." if show_density: subsample = 1 # plot fig, ax, ax2 = None, None, None if axis is None: fig, ax = plt.subplots() else: ax = axis fig = axis.get_figure() fig.patch.set_alpha(0.) new_lines = 0 # plotting options soma_alpha = kwargs.get("soma_alpha", 0.8) axon_alpha = kwargs.get("axon_alpha", 0.6) dend_alpha = kwargs.get("dend_alpha", 0.6) gc_color = kwargs.get("gc_color", "g") # get the objects describing the neurons if gid is None: gid = _pg.get_neurons(as_ints=True) elif not is_iterable(gid): gid = [gid] somas, growth_cones, nodes = None, None, None axon_lines, dend_lines = None, None axons, dendrites = None, None # check for loaded neurons loaded_neurons = False first_neuron = next(iter(gid)) if isinstance(first_neuron, Neuron): for n in gid: loaded_neurons += (not n._in_simulator) if loaded_neurons: mode = "lines" if mode is None else mode if mode != "lines": raise ValueError("`mode` must be 'lines' to plot loaded neurons.") somas = np.array([n.position.m for n in gid]).T somas = np.vstack((somas, [v.m for v in gid.soma_radius.values()])) axon_lines, dend_lines = [[], []], [[], []] for n in gid: points = n.axon.xy.m for i in (0, 1): axon_lines[i].extend(points[:, i]) axon_lines[i].append(np.NaN) for d in n.dendrites.values(): points = d.xy.m for i in (0, 1): dend_lines[i].extend(points[:, i]) dend_lines[i].append(np.NaN) show_active_gc = False else: mode = "sticks" if mode is None else mode if mode in ("lines", "mixed"): somas, axon_lines, dend_lines, growth_cones, nodes = \ _pg._get_pyskeleton(gid, subsample) if mode in ("sticks", "mixed"): axons, dendrites, somas = _pg._get_geom_skeleton(gid) # get the culture if necessary env_required = _pg.get_kernel_status('environment_required') if show_culture and env_required: if culture is None: culture = _pg.get_environment() plot_environment(culture, ax=ax, show=False, **kwargs) new_lines += 1 # plot the elements if mode in ("sticks", "mixed"): for a in axons.values(): plot_shape(a, axis=ax, fc=axon_color, show_contour=False, zorder=2, alpha=axon_alpha, show=False) for vd in dendrites.values(): for d in vd: plot_shape(d, axis=ax, fc=dendrite_color, show_contour=False, alpha=dend_alpha, zorder=2, show=False) if mode in ("lines", "mixed"): ax.plot(axon_lines[0], axon_lines[1], ls="-", c=axon_color) ax.plot(dend_lines[0], dend_lines[1], ls="-", c=dendrite_color) new_lines += 2 # plot the rest if required if show_nodes and mode in ("lines", "mixed"): ax.plot(nodes[0], nodes[1], ls="", marker="d", ms="1", c="k", zorder=4) new_lines += 1 if show_active_gc and mode in ("lines", "mixed"): ax.plot(growth_cones[0], growth_cones[1], ls="", marker=active_gc, c=gc_color, ms=gc_size, zorder=4) new_lines += 1 # plot the somas n = len(somas[2]) radii = somas[2] if soma_radius is None else np.repeat(soma_radius, n) if mode in ("sticks", "mixed"): radii *= 1.05 r_max = np.max(radii) r_min = np.min(radii) size = (1.5*r_min if len(gid) <= 10 else (r_min if len(gid) <= 100 else 0.7*r_min)) for i, x, y, r in zip(gid, somas[0], somas[1], radii): circle = plt.Circle( (x, y), r, color=soma_color, alpha=soma_alpha) artist = ax.add_artist(circle) artist.set_zorder(5) if show_neuron_id: str_id = str(i) xoffset = len(str_id)*0.35*size text = TextPath((x-xoffset, y-0.35*size), str_id, size=size) textpatch = PathPatch(text, edgecolor="w", facecolor="w", linewidth=0.01*size) ax.add_artist(textpatch) textpatch.set_zorder(6) # set the axis limits if (not show_culture or not env_required) and len(ax.lines) == new_lines: xs, ys = [], [] if axon_lines is not None: xs.extend(axon_lines[0]) ys.extend(axon_lines[1]) if dend_lines is not None: xs.extend(dend_lines[0]) ys.extend(dend_lines[1]) if mode in ("lines", "mixed"): _set_ax_lim(ax, xs, ys, offset=2*r_max) else: xx = [] yy = [] for a in axons.values(): xmin, ymin, xmax, ymax = a.bounds xx.extend((xmin, xmax)) yy.extend((ymin, ymax)) for vd in dendrites.values(): for d in vd: xmin, ymin, xmax, ymax = d.bounds xx.extend((xmin, xmax)) yy.extend((ymin, ymax)) _set_ax_lim(ax, xx, yy, offset=2*r_max) ax.set_aspect(aspect) if title is not None: fig.suptitle(title) if save_path is not None: if not save_path.endswith('pdf'): save_path += ".pdf" plt.savefig(save_path, format="pdf", dpi=300) if show_density: from matplotlib.colors import LogNorm fig, ax2 = plt.subplots() # https://stackoverflow.com/questions/20474549/extract-points-coordinates-from-a-polygon-in-shapely#20476150 # x,y= axons[].exterior.coords.xy def extract_neurites_coordinate(neurites): ''' from a neurite defined as polynom extract the coordinates of each segment input : p neuron or dendrite, as shapely polygon x_neurites, y_neurites : numpy arrays of x and y coordinates of axons segments y_dendrites, y_dendrites : numpy arrays of x and y coordinates of dendrites outputs : updates lists of coordinates ''' x_neurites = np.array([]) y_neurites = np.array([]) if isinstance(neurites, MultiPolygon): for p in neurites: x_neurite, y_neurite = extract_neurites_coordinate(p) x_neurites = np.concatenate((x_neurites, x_neurite)) y_neurites = np.concatenate((y_neurites, y_neurite)) elif isinstance(neurites, Polygon): x_neurite, y_neurite = neurites.exterior.coords.xy x_neurites = np.concatenate((x_neurites, x_neurite)) y_neurites = np.concatenate((y_neurites, y_neurite)) else: for index in range(len(neurites)): x_neurite, y_neurite = extract_neurites_coordinate( neurites[index]) x_neurites = np.concatenate((x_neurites, x_neurite)) y_neurites = np.concatenate((y_neurites, y_neurite)) return x_neurites, y_neurites # extract axons segments # if isinstance(axons, MultiPolygon): # for p in axons: # x_axons, y_axons = extract_neurites_coordinate(p) # else: x, y = extract_neurites_coordinate(axons) # x = x_axons # y = y_axons # extract dendrites segments if len(dendrites) > 0: # this depends if dendrites present # if isinstance(dendrites, MultiPolygon): # for p in dendrites: # x_dendrites, y_dendrites = extract_neurites_coordinate(p) # else: # x_dendrites, y_dendrites = extract_neurites_coordinate( # dendrites) x_dendrites, y_dendrites = extract_neurites_coordinate( dendrites) x = np.concatenate((x, x_dendrites)) y = np.concatenate((y, y_dendrites)) # Scaling density levels xbins = int((np.max(x) - np.min(x)) / dstep) ybins = int((np.max(y) - np.min(y)) / dstep) dstep = int(dstep) counts, xbins, ybins = np.histogram2d( x, y, bins=(dstep, dstep), range=[[x_min, x_max], [y_min, y_max]]) lims = [xbins[0], xbins[-1], ybins[0], ybins[-1]] counts[counts == 0] = np.NaN cmap = get_cmap(kwargs.get("cmap", "viridis")) cmap.set_bad((0, 0, 0, 1)) norm = None dmax = np.nanmax(counts) print("Maximal density : {}".format(dmax)) if dmin is not None and dmax is not None: n = int(dmax-dmin) norm = matplotlib.colors.BoundaryNorm( np.arange(dmin-1, dmax+1, 0), cmap.N) elif dmax is not None: n = int(dmax) norm = matplotlib.colors.BoundaryNorm( np.arange(0, dmax+1, 1), cmap.N) # data = ax2.imshow(counts.T, extent=lims, origin="lower", # vmin=0 if dmin is None else dmin, vmax=dmax, # cmap=cmap) data = ax2.imshow(counts.T, extent=lims, origin="lower", norm=norm, cmap=cmap) if colorbar: extend = "neither" if dmin is not None and dmax is not None: extend = "both" elif dmax is not None: extend = "max" elif dmin is not None: extend = "min" cb = plt.colorbar(data, ax=ax2, extend=extend) cb.set_label("Number of neurites per bin") ax2.set_aspect(aspect) ax2.set_xlabel(r"x ($\mu$ m)") ax2.set_ylabel(r"y ($\mu$ m)") if scale is not None: xmin, xmax = ax.get_xlim() ymin, ymax = ax.get_ylim() length = scale.m_as("micrometer") if xmax - xmin < 2*length: scale *= 0.2 length = scale.m_as("micrometer") x = xmin + 0.2*length y = ymin + (ymax-ymin)*0.05 ax.add_artist( Rectangle((x, y), length, 0.1*length, fill=True, facecolor='k', edgecolor='none')) plt.axis('off') stext = "(scale is {} $\mu$m)".format(length) if title is not None and scale_text: fig.suptitle(title + " " + stext) elif scale_text: fig.suptitle(stext) if show: plt.show() if show_density: return ax, ax2 return ax
# --------------- # # Plot dendrogram # # --------------- #
[docs]def plot_dendrogram(neurite, axis=None, show_node_id=False, aspect_ratio=None, vertical_diam_frac=0.2, ignore_diameter=False, show=True, **kwargs): ''' Plot the dendrogram of a neurite. Parameters ---------- neurite : :class:`~dense.elements.Neurite` object Neurite for which the dendrogram should be plotted. axis : matplotlib.Axes.axis object, optional (default: new one) Axis on which the dendrogram should be plotted. show : bool, optional (default: True) Whether the figure should be shown right away. show_node_id : bool, optional (default: False) Display each node number on the branching points. aspect_ratio : float, optional (default: variable) Whether to use a fixed aspect ratio. Automatically set to 1 if `show_node_id` is True. vertical_diam_frac : float, optional (default: 0.2) Fraction of the vertical spacing taken by the branch diameter. ignore_diameter : bool, optional (default: False) Plot all the branches with the same width. **kwargs : arguments for :class:`matplotlib.patches.Rectangle` For instance `facecolor` or `edgecolor`. Returns ------- The axis on which the plot was done. See also -------- :func:`~dense.elements.Neurite.plot_dendrogram` ''' import matplotlib.pyplot as plt tree = neurite.get_tree() if axis is None: fig, axis = plt.subplots() fig = axis.get_figure() if "facecolor" not in kwargs: kwargs["facecolor"] = "k" if "edgecolor" not in kwargs: kwargs["edgecolor"] = "none" # get the number of tips num_tips = len(tree.tips) # compute the size of the vertical spacing between branches # this should be 5 times the diameter of the first section and there # are num_tips + 1 spacing in total. init_diam = tree.root.children[0].diameter vspace = init_diam / vertical_diam_frac tot_height = (num_tips + 0.5) * vspace # compute the total length which is 1.1 times the longest distance # to soma max_dts = np.max([n.distance_to_soma() for n in tree.tips]) tot_length = 1.02*max_dts # we need to find the number of up and down children for each node up_children = {} down_children = {} diams = [] root = tree.root tips = set(tree.tips) # if diameter is ignored, set all values to default_diam default_diam = vertical_diam_frac*vspace if ignore_diameter: tree.root.diameter = default_diam queue = deque([root]) while queue: node = queue.popleft() queue.extend(node.children) if ignore_diameter: node.diameter = default_diam if len(node.children) == 1 and node != root: # gc died there, transfer children and update them parent = node.parent child_pos = 0 for i, child in enumerate(parent.children): if child == node: parent.children[i] = node.children[0] child_pos = i break # use loop to keep child memory and update properties child = parent.children[child_pos] child.dist_to_parent += node.dist_to_parent child.parent = node.parent child.diameter = 0.5*(node.diameter + child.diameter) # check up/down_children and replace node by child for key, val in up_children.items(): if node in val: up_children[key] = {n for n in val if n is not node} up_children[key].add(child) for key, val in down_children.items(): if node in val: down_children[key] = { n for n in val if n is not node} down_children[key].add(child) else: if len(node.children) == 2: up_children[node] = {node.children[0]} down_children[node] = {node.children[1]} for val in up_children.values(): if node.parent in val: val.add(node) for val in down_children.values(): if node.parent in val: val.add(node) diams.append(node.diameter) # keep only tips in up/down_children up_tips, down_tips = {}, {} for key, val in up_children.items(): up_tips[key] = val.intersection(tips) for key, val in down_children.items(): down_tips[key] = val.intersection(tips) # get max diameter for node plotting max_d = np.max(diams) # aspect ratios vbar_diam_ratio = 0.5 hv_ratio = tot_length/tot_height if show_node_id: axis.set_aspect(1.) hv_ratio = 1. vbar_diam_ratio = 1. elif aspect_ratio is not None: axis.set_aspect(aspect_ratio) hv_ratio = aspect_ratio vbar_diam_ratio /= aspect_ratio # making horizontal branches x0 = 0.01*max_dts parent_x = {} parent_y = {} children_y = {tree.root: []} queue = deque([root]) while queue: node = queue.popleft() queue.extend(node.children) parent_diam = 0 if node.parent is None else node.parent.diameter x = x0 # skip for root (parent is None) if node.parent is not None: x += node.parent.distance_to_soma() \ - vbar_diam_ratio*parent_diam*hv_ratio # get parent y y = parent_y.get(node.parent, 0.) num_up, num_down = 0.5, 0.5 if node.children: num_up = 0 if node not in up_tips else len(up_tips[node]) num_down = 0 if node not in down_tips else len(down_tips[node]) children_y[node] = [] if node in up_children.get(node.parent, [node]): y += num_down*vspace - 0.5*node.diameter else: y -= num_up*vspace + 0.5*node.diameter parent_y[node] = y parent_x[node] = x + node.dist_to_parent # ignore root if node.parent is not None: children_y[node.parent].append(y) axis.add_artist( Rectangle((x, y), node.dist_to_parent, node.diameter, fill=True, **kwargs)) # last iteration for vertical connections # set root as last node with a single child while len(root.children) == 1: root = root.children[0] if ignore_diameter: root.diameter = default_diam queue = deque([root]) while queue: node = queue.popleft() queue.extend(node.children) if node.children: x = parent_x[node] y = parent_y[node] + 0.5*node.diameter y1, y2 = children_y[node] y1, y2 = min(y1, y2), max(y1, y2) dx = 0.5*vbar_diam_ratio*node.diameter*hv_ratio if show_node_id: circle = plt.Circle( (x + dx, y), max_d, color=kwargs["facecolor"]) artist = axis.add_artist(circle) artist.set_zorder(5) str_id = str(node) xoffset = len(str_id)*0.3*max_d text = TextPath((x + dx - xoffset, y - 0.3*max_d), str_id, size=max_d) textpatch = PathPatch(text, edgecolor="w", facecolor="w", linewidth=0.01*max_d) axis.add_artist(textpatch) textpatch.set_zorder(6) axis.add_artist( Rectangle((x, y1), vbar_diam_ratio*node.diameter*hv_ratio, (y2 - y1) + 0.5*node.diameter, fill=True, **kwargs)) axis.set_xlim(0, tot_length) axis.set_ylim(np.min(list(parent_y.values())) - 0.75*vspace, np.max(list(parent_y.values())) + 0.75*vspace) plt.axis('off') fig.patch.set_alpha(0.) if show: plt.show() return axis
# ---------------- # # Plot environment # # ---------------- #
[docs]def plot_environment(culture=None, title='Environment', ax=None, m='', mc="#999999", fc="#ccccff", ec="#444444", alpha=0.5, brightness="height", show=True, **kwargs): ''' Plot the environment in which the neurons grow. Parameters ---------- culture : :class:`~dense.environment.Shape`, optional (default: None) Shape of the environment; if the environment was already set using :func:`~dense.CreateEnvironment` title : str, optional (default: 'Shape') Title of the plot. ax : :class:`matplotlib.axes.Axes`, optional (default: new axis) Optional existing axis on which the environment should be added. m : str, optional (default: invisible) Marker to plot the shape's vertices, matplotlib syntax. mc : str, optional (default: "#999999") Color of the markers. fc : str, optional (default: "#8888ff") Color of the shape's interior. ec : str, optional (default: "#444444") Color of the shape's edges. alpha : float, optional (default: 0.5) Opacity of the shape's interior. brightness : str, optional (default: height) Show how different other areas are from the 'default_area' (lower values are darker, higher values are lighter). Difference can concern the 'height', or any of the `properties` of the :class:`Area` objects. show : bool, optional (default: False) If True, the plot will be displayed immediately. ''' if culture is None: culture = _pg.get_environment() plot_shape(culture, axis=ax, m=m, mc=mc, fc=fc, ec=ec, alpha=alpha, brightness=brightness, show=show)