#!/usr/bin/env python
#-*- coding:utf-8 -*-
#
# This file is part of the NNGT project to generate and analyze
# neuronal networks and their activity.
# Copyright (C) 2015-2017 Tanguy Fardet
#
# This program 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 3 of the License, or
# (at your option) any later version.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
""" Analyze the activity of a network """
from collections import namedtuple
from copy import deepcopy
import weakref
import matplotlib.pyplot as plt
import nest
import numpy as np
from nngt.lib import InvalidArgument, nonstring_container
__all__ = [
"ActivityRecord",
"activity_types",
"analyze_raster",
"get_recording",
]
#-----------------------------------------------------------------------------#
# Finding the various activities
#------------------------
#
[docs]class ActivityRecord:
'''
Class to record the properties of the simulated activity.
'''
def __init__(self, spike_data, phases, properties, parameters=None):
'''
Initialize the instance using `spike_data` (store proxy to an optional
`network`) and compute the properties of provided data.
Parameters
----------
spike_data : 2D array
Array of shape (num_spikes, 2), containing the senders on the 1st
row and the times on the 2nd row.
phases : dict
Limits of the different phases in the simulated period.
properties : dict
Values of the different properties of the activity (e.g.
"firing_rate", "IBI"...).
parameters : dict, optional (default: None)
Parameters used to compute the phases.
Note
----
The firing rate is computed as num_spikes / total simulation time,
the period is the sum of an IBI and a bursting period.
'''
self._data = spike_data
self._phases = phases.copy()
self._properties = properties.copy()
self.parameters = parameters
[docs] def simplify():
raise NotImplementedError("Will be implemented soon.")
@property
def data(self):
'''
Returns the (N, 2) array of (senders, spike times).
'''
return self._data
@property
def phases(self):
'''
Return the phases detected:
- "bursting" for periods of high activity where a large fraction
of the network is recruited.
- "quiescent" for periods of low activity
- "mixed" for firing rate in between "quiescent" and "bursting".
- "localized" for periods of high activity but where only a small
fraction of the network is recruited.
Note
----
See `parameters` for details on the conditions used to
differenciate these phases.
'''
return self._phases
@property
def properties(self):
'''
Returns the properties of the activity.
Contains the following entries:
- "firing_rate": average value in Hz for 1 neuron in the network.
- "bursting": True if there were bursts of activity detected.
- "burst_duration", "IBI", "ISI", and "period" in ms, if
"bursting" is True.
- "SpB" (Spikes per Burst): average number of spikes per neuron
during a burst.
'''
return self._properties
#-----------------------------------------------------------------------------#
# analyse acitivity
#------------------------
#
[docs]def get_recording(network, record, recorder=None, nodes=None):
'''
Return the evolution of some recorded values for each neuron.
Parameters
----------
network : :class:`nngt.Network`
Network for which the activity was simulated.
record : str or list
Name of the record(s) to obtain.
recorder : tuple of ints, optional (default: all multimeters)
GID of the "spike_detector" objects recording the network activity.
nodes : array-like, optional (default: all nodes)
NNGT ids of the nodes for which the recording should be returned.
Returns
-------
values : dict of dict of arrays
Dictionary containing, for each `record`, an M array with the
recorded values for n-th neuron is stored under entry `n` (integer).
A `times` entry is also added; it should be the same size for all
records, otherwise an error will be raised.
Examples
--------
After the creation of a :class:`~nngt.Network` called ``net``, use the
following code: ::
import nest
rec, _ = monitor_nodes(
net.nest_gid, "multimeter", {"record_from": ["V_m"]}, net)
nest.Simulate(100.)
recording = nngt.simulation.get_recording(net, "V_m")
# access the membrane potential of first neuron + the times
V_m = recording["V_m"][0]
times = recording["times"]
'''
if nodes is None:
nodes = [network.id_from_nest_gid(n) for n in network.nest_gid]
gids = [network.nest_gid[n] for n in nodes]
if not nonstring_container(record):
record = [record]
values = {rec: {} for rec in record}
if recorder is None:
recorder = nest.GetNodes(
(0,), properties={'model': 'multimeter'})
times = None
for rec in recorder:
events = nest.GetStatus(rec, "events")[0]
senders = events["senders"]
if times is not None:
assert times == events["times"], "Different times between the " +\
"recorders; check the params."
times = events["times"]
values["times"] = times[senders == senders[0]]
for rec_name in record:
for idx, gid in zip(nodes, gids):
ids = senders == senders[gid]
values[rec_name][idx] = events[rec_name][ids]
return values
[docs]def activity_types(spike_detector, limits, network=None,
phase_coeff=(0.5, 10.), mbis=0.5, mfb=0.2, mflb=0.05,
skip_bursts=0, simplify=False, fignums=[], show=False):
'''
Analyze the spiking pattern of a neural network.
@todo:
think about inserting t=0. and t=simtime at the beginning and at the
end of ``times``.
Parameters
----------
spike_detector : NEST node(s) (tuple or list of tuples)
The recording device that monitored the network's spikes.
limits : tuple of floats
Time limits of the simulation region which should be studied (in ms).
network : :class:`~nngt.Network`, optional (default: None)
Neural network that was analyzed
phase_coeff : tuple of floats, optional (default: (0.2, 5.))
A phase is considered `bursting' when the interspike between all spikes
that compose it is smaller than ``phase_coeff[0] / avg_rate`` (where
``avg_rate`` is the average firing rate), `quiescent' when it is
greater that ``phase_coeff[1] / avg_rate``, `mixed' otherwise.
mbis : float, optional (default: 0.5)
Maximum interspike interval allowed for two spikes to be considered in
the same burst (in ms).
mfb : float, optional (default: 0.2)
Minimal fraction of the neurons that should participate for a burst to
be validated (i.e. if the interspike is smaller that the limit BUT the
number of participating neurons is too small, the phase will be
considered as `localized`).
mflb : float, optional (default: 0.05)
Minimal fraction of the neurons that should participate for a local
burst to be validated (i.e. if the interspike is smaller that the limit
BUT the number of participating neurons is too small, the phase will be
considered as `mixed`).
skip_bursts : int, optional (default: 0)
Skip the `skip_bursts` first bursts to consider only the permanent
regime.
simplify: bool, optional (default: False)
If ``True``, `mixed` phases that are contiguous to a burst are
incorporated to it.
return_steps : bool, optional (default: False)
If ``True``, a second dictionary, `phases_steps` will also be returned.
@todo: not implemented yet
fignums : list, optional (default: [])
Indices of figures on which the periods can be drawn.
show : bool, optional (default: False)
Whether the figures should be displayed.
Note
----
Effects of `skip_bursts` and `limits[0]` are cumulative: the `limits[0]`
first milliseconds are ignored, then the `skip_bursts` first bursts of the
remaining activity are ignored.
Returns
-------
phases : dict
Dictionary containing the time intervals (in ms) for all four phases
(`bursting', `quiescent', `mixed', and `localized`) as lists.
E.g: ``phases["bursting"]`` could give ``[[123.5,334.2],
[857.1,1000.6]]``.
'''
# check if there are several recorders
senders, times = [], []
if True in nest.GetStatus(spike_detector, "to_file"):
for fpath in nest.GetStatus(spike_detector, "record_to"):
data = _get_data(fpath)
times.extend(data[:, 1])
senders.extend(data[:, 0])
else:
for events in nest.GetStatus(spike_detector, "events"):
times.extend(events["times"])
senders.extend(events["senders"])
idx_sort = np.argsort(times)
times = np.array(times)[idx_sort]
senders = np.array(senders)[idx_sort]
# compute phases and properties
data = np.array((senders, times))
phases, fr = _analysis(times, senders, limits, network=network,
phase_coeff=phase_coeff, mbis=mbis, mfb=mfb, mflb=mflb,
simplify=simplify)
properties = _compute_properties(data, phases, fr, skip_bursts)
kwargs = {
"limits": limits,
"phase_coeff": phase_coeff,
"mbis": mbis,
"mfb": mfb,
"mflb": mflb,
"simplify": simplify
}
# plot if required
if show:
_plot_phases(phases, fignums)
return ActivityRecord(data, phases, properties, kwargs)
[docs]def analyze_raster(raster=None, limits=None, network=None,
phase_coeff=(0.5, 10.), mbis=0.5, mfb=0.2, mflb=0.05,
skip_bursts=0, skip_ms=0., simplify=False, fignums=[],
show=False):
'''
Return the activity types for a given raster.
Parameters
----------
raster : array-like or str
Either an array containing the ids of the spiking neurons and the
corresponding time, or the path to a NEST .gdf recording.
limits : tuple of floats
Time limits of the simulation regrion which should be studied (in ms).
network : :class:`~nngt.Network`, optional (default: None)
Network on which the recorded activity was simulated.
phase_coeff : tuple of floats, optional (default: (0.2, 5.))
A phase is considered `bursting' when the interspike between all spikes
that compose it is smaller than ``phase_coeff[0] / avg_rate`` (where
``avg_rate`` is the average firing rate), `quiescent' when it is
greater that ``phase_coeff[1] / avg_rate``, `mixed' otherwise.
mbis : float, optional (default: 0.5)
Maximum interspike interval allowed for two spikes to be considered in
the same burst (in ms).
mfb : float, optional (default: 0.2)
Minimal fraction of the neurons that should participate for a burst to
be validated (i.e. if the interspike is smaller that the limit BUT the
number of participating neurons is too small, the phase will be
considered as `localized`).
mflb : float, optional (default: 0.05)
Minimal fraction of the neurons that should participate for a local
burst to be validated (i.e. if the interspike is smaller that the limit
BUT the number of participating neurons is too small, the phase will be
considered as `mixed`).
skip_bursts : int, optional (default: 0)
Skip the `skip_bursts` first bursts to consider only the permanent
regime.
simplify: bool, optional (default: False)
If ``True``, `mixed` phases that are contiguous to a burst are
incorporated to it.
fignums : list, optional (default: [])
Indices of figures on which the periods can be drawn.
show : bool, optional (default: False)
Whether the figures should be displayed.
Note
----
Effects of `skip_bursts` and `limits[0]` are cumulative: the
`limits[0]` first milliseconds are ignored, then the `skip_bursts`
first bursts of the remaining activity are ignored.
Returns
-------
activity : ActivityRecord
Object containing the phases and the properties of the activity
from these phases.
'''
data = _get_data(raster) if isinstance(raster, str) else raster
if limits is None:
limits = [np.min(data[:, 1]), np.max(data[:, 1])]
kwargs = {
"limits": limits,
"phase_coeff": phase_coeff,
"mbis": mbis,
"mfb": mfb,
"mflb": mflb,
"simplify": simplify
}
# compute phases and properties
phases, fr = _analysis(data[:, 1], data[:, 0], limits, network=network,
phase_coeff=phase_coeff, mbis=mbis, mfb=mfb, mflb=mflb,
simplify=simplify)
properties = _compute_properties(data.T, phases, fr, skip_bursts)
# plot if required
if show:
if fignums:
_plot_phases(phases, fignums)
else:
fig, ax = plt.subplots()
ax.scatter(data[:, 1], data[:, 0])
_plot_phases(phases, [fig.number])
return ActivityRecord(data, phases, properties, kwargs)
# ----- #
# Tools #
# ----- #
def _get_data(source):
'''
Returns the (times, senders) array.
Parameters
----------
source : list or str
Indices of spike detectors or path to the .gdf files.
Returns
-------
data : 2D array of shape (N, 2)
'''
data = [[],[]]
is_string = isinstance(source, str)
if is_string:
source = [source]
elif nonstring_container(source) and isinstance(source[0], str):
is_string = True
if is_string:
for path in source:
tmp = np.loadtxt(path)
data[0].extend(tmp[:, 0])
data[1].extend(tmp[:, 1])
else:
source_shape = np.shape(source)
if len(source_shape) == 2:
# source is directly the data
if source_shape[0] == 2 and source_shape[1] != 2:
return np.array(source).T
else:
return np.array(source)
else:
# source contains gids
events = nest.GetStatus(source, "events")
for ev in events:
data[0].extend(ev["senders"])
data[1].extend(ev["times"])
idx_sort = np.argsort(data[1])
return np.array(data)[:, idx_sort].T
def _find_phases(times, phases, lim_burst, lim_quiet, simplify):
'''
Find the time limits of the different phases.
'''
diff = np.diff(times).tolist()[::-1]
i = 0
previous = { "bursting": -2, "mixed": -2, "quiescent": -2 }
while diff:
tau = diff.pop()
while True:
if tau < lim_burst: # bursting phase
if previous["bursting"] == i-1:
phases["bursting"][-1][1] = times[i+1]
else:
if simplify and previous["mixed"] == i-1:
start_mixed = phases["mixed"][-1][0]
phases["bursting"].append([start_mixed, times[i+1]])
del phases["mixed"][-1]
else:
phases["bursting"].append([times[i], times[i+1]])
previous["bursting"] = i
i+=1
break
elif tau > lim_quiet:
if previous["quiescent"] == i-1:
phases["quiescent"][-1][1] = times[i+1]
else:
phases["quiescent"].append([times[i], times[i+1]])
previous["quiescent"] = i
i+=1
break
else:
if previous["mixed"] == i-1:
phases["mixed"][-1][1] = times[i+1]
previous["mixed"] = i
else:
if simplify and previous["bursting"] == i-1:
phases["bursting"][-1][1] = times[i+1]
previous["bursting"] = i
else:
phases["mixed"].append([times[i], times[i+1]])
previous["mixed"] = i
i+=1
break
def _check_burst_size(phases, senders, times, network, mflb, mfb):
'''
Check that bursting periods involve at least a fraction mfb of the neurons.
'''
transfer, destination = [], {}
n = len(set(senders)) if network is None else network.node_nb()
for i,burst in enumerate(phases["bursting"]):
idx_start = np.where(times==burst[0])[0][0]
idx_end = np.where(times==burst[1])[0][0]
participating_frac = len(set(senders[idx_start:idx_end])) / float(n)
if participating_frac < mflb:
transfer.append(i)
destination[i] = "mixed"
elif participating_frac < mfb:
transfer.append(i)
destination[i] = "localized"
for i in transfer[::-1]:
phase = phases["bursting"].pop(i)
phases[destination[i]].insert(0, phase)
remove = []
i = 0
while i < len(phases['mixed']):
mixed = phases['mixed'][i]
j=i+1
for span in phases['mixed'][i+1:]:
if span[0] == mixed[1]:
mixed[1] = span[1]
remove.append(j)
elif span[1] == mixed[0]:
mixed[0] = span[0]
remove.append(j)
j+=1
i+=1
remove = list(set(remove))
remove.sort()
for i in remove[::-1]:
del phases["mixed"][i]
def _analysis(times, senders, limits, network=None,
phase_coeff=(0.5, 10.), mbis=0.5, mfb=0.2, mflb=0.05,
simplify=False):
# prepare the phases and check the validity of the data
phases = {
"bursting": [],
"mixed": [],
"quiescent": [],
"localized": []
}
num_spikes, avg_rate = len(times), 0.
if num_spikes:
num_neurons = (len(np.unique(senders)) if network is None
else network.node_nb())
# set the studied region
if limits[0] >= times[0]:
idx_start = np.where(times >= limits[0])[0][0]
times = times[idx_start:]
senders = senders[idx_start:]
if limits[1] <= times[-1]:
idx_end = np.where(times <= limits[1])[0][-1]
times = times[:idx_end]
senders = senders[:idx_end]
# get the average firing rate to differenciate the phases
simtime = limits[1] - limits[0]
lim_burst, lim_quiet = 0., 0.
avg_rate = num_spikes / float(simtime)
lim_burst = max(phase_coeff[0] / avg_rate, mbis)
lim_quiet = min(phase_coeff[1] / avg_rate, 10.)
# find the phases
_find_phases(times, phases, lim_burst, lim_quiet, simplify)
_check_burst_size(phases, senders, times, network, mflb, mfb)
avg_rate *= 1000. / float(num_neurons)
return phases, avg_rate
def _compute_properties(data, phases, fr, skip_bursts):
'''
Compute the properties from the spike times and phases.
Parameters
----------
data : 2D array, shape (N, 2)
Spike times and senders.
phases : dict
The phases.
fr : double
Firing rate.
Returns
-------
prop : dict
Properties of the activity. Contains the following pairs:
- "firing_rate": average value in Hz for 1 neuron in the network.
- "bursting": True if there were bursts of activity detected.
- "burst_duration", "ISI", and "IBI" in ms, if "bursting" is True.
- "SpB": average number of spikes per burst for one neuron.
'''
prop = {}
times = data[1, :]
# firing rate (in Hz, normalized for 1 neuron)
prop["firing_rate"] = fr
num_bursts = len(phases["bursting"])
init_val = 0. if num_bursts > skip_bursts else np.NaN
if num_bursts:
prop["bursting"] = True
prop.update({
"burst_duration": init_val,
"IBI": init_val,
"ISI": init_val,
"SpB": init_val,
"period": init_val})
else:
prop["bursting"] = False
for i, burst in enumerate(phases["bursting"]):
if i >= skip_bursts:
# burst_duration
prop["burst_duration"] += burst[1] - burst[0]
# IBI
if i > 0:
end_older_burst = phases["bursting"][i-1][1]
prop["IBI"] += burst[0]-end_older_burst
# get num_spikes inside the burst, divide by num_neurons
idxs = np.where((times >= burst[0])*(times <= burst[1]))[0]
num_spikes = len(times[idxs])
num_neurons = len(set(data[0, :][idxs]))
prop["SpB"] += num_spikes / float(num_neurons)
# ISI
prop["ISI"] += num_neurons * (burst[1] - burst[0])\
/ float(num_spikes)
for key in iter(prop.keys()):
if key not in ("bursting", "firing_rate") and num_bursts > skip_bursts:
prop[key] /= float(num_bursts - skip_bursts)
if num_bursts > skip_bursts:
prop["period"] = prop["IBI"] + prop["burst_duration"]
if num_bursts and prop["SpB"] < 2.:
prop["ISI"] = np.NaN
return prop
def _plot_phases(phases, fignums):
colors = ('r', 'orange', 'g', 'b')
names = ('bursting', 'mixed', 'localized', 'quiescent')
for fignum in fignums:
fig = plt.figure(fignum)
for ax in fig.axes:
for phase, color in zip(names, colors):
for span in phases[phase]:
ax.axvspan(span[0], span[1], facecolor=color,
alpha=0.2)
plt.show()