#!/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-2019 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 nest
import numpy as np
from nngt.lib import InvalidArgument, nonstring_container
from .nest_utils import nest_version, _get_nest_gids
__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 activity #
# ---------------- #
[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_gids, "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_gids]
gids = _get_nest_gids([network.nest_gids[n] for n in nodes])
if not nonstring_container(record):
record = [record]
values = {rec: {} for rec in record}
if recorder is None:
if nest_version == 3:
recorder = nest.GetNodes(properties={'model': 'multimeter'})
else:
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 (N, 2) or str
Either an array containing the ids of the spiking neurons on the first
column, then the corresponding times on the second column, 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 data.any():
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:
import matplotlib.pyplot as plt
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)
return ActivityRecord(data, {}, {})
# ----- #
# 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(np.squeeze(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
source = _get_nest_gids(source)
events = None
if nonstring_container(source[0]):
events = [nest.GetStatus(gid, "events")[0] for gid in source]
else:
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]))
if num_neurons:
prop["SpB"] += num_spikes / float(num_neurons)
# ISI
if num_spikes:
prop["ISI"] += num_neurons * (burst[1] - burst[0])\
/ float(num_spikes)
for key in 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):
import matplotlib.pyplot as plt
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()