Detecting Synchronicity with Unitary Event Analysis (UEA)
Authors
Setup
Import Libraries
Import the modules required for this notebook
import numpy as np
from matplotlib import pyplot as plt
import quantities as pq
from neo.io import NixIO
import elephant.unitary_event_analysis as ue
from elephant.spike_train_generation import StationaryPoissonProcess, NonStationaryPoissonProcess
from elephant.datasets import download_datasets
from viziphant.unitary_event_analysis import plot_ue
np.random.seed(15)
%matplotlib inlineUtility Functions
Define the utility functions required for this notebook
from neo.core import AnalogSignal
class utils:
@staticmethod
def spike_trains_with_unitary_events(base_rate, sync_rate, n_neurons, n_trials):
np.random.seed(100)
base = StationaryPoissonProcess(rate=base_rate, t_stop=2 * pq.s)
rate_signal = np.zeros(2000) * pq.Hz
rate_signal[1000:1200] = sync_rate
rate_signal = AnalogSignal(rate_signal, sampling_rate=1000 * pq.Hz)
sync = NonStationaryPoissonProcess(rate_signal)
sync_spikes = sync.generate_n_spiketrains(n_trials)
trials = []
for t in range(n_trials):
trial = []
for _ in range(n_neurons):
trial.append(base.generate_spiketrain().merge(sync_spikes[t]))
trials.append(trial)
return trials, sync_spikes
@staticmethod
def spike_trains_with_rate_step(base_rate, rate_step, n_neurons, n_trials):
np.random.seed(100)
rate_signal = np.ones(2000) * base_rate
rate_signal[1000:1200] += rate_step
rate_signal = AnalogSignal(rate_signal, sampling_rate=1000 * pq.Hz)
neuron = NonStationaryPoissonProcess(rate_signal)
trials = [neuron.generate_n_spiketrains(n_neurons) for i in range(n_trials)]
return trialsDownload Data
Download the data required for this notebook
repo_path = "tutorials/tutorial_unitary_event_analysis/data/dataset-1.nix"
filepath = download_datasets(repo_path)Section 1: Simulating Neurons with Coincident Firing
In this notebook we want to explore the unitary event analysis (UEA) method for detecting synchronous events in parallel spike trains. To test this method, we need a to simulate spike trains that contain these synchronous events. In this section, you are going to explore a custom simulation function that generates spike trains for multiple neurons. These neurons have a baseline firing rate, which determines their random spiking, as well as a synchronous rate that determines the spikes that appear synchronously in all neurons. In this section we are going to generate spike trains with this method and visualize them.
| Code | Description |
|---|---|
trials, sync_spikes = utils.spike_trains_with_unitary_events() |
Simulate spike trains that contain synchronous events and return a list of trials where each element is a list of spike trains (one per neuron) as well as a list sync_spikes that contains the synchronous spikes for every trial |
[trial[0].times for trial in trials] |
Get a list of spike .times for neuron [0] for each trial in the list of trials |
[st.times for st in sync_spikes] |
Get a list of spike .times for each spike train st in the list of sync_spikes |
plt.eventplot(x, linelength=0.5, color="red") |
Create an eventplot for the values in x with the given linelength and color |
plt.subplot(2, 1, 1) |
Create the first plot in a 2-by-1 subplot grid |
plt.subplot(2, 1, 2) |
Create the second plot in a 2-by-1 subplot grid |
Exercises
Example: Simulate 2 neurons with a base_rate of 5*pq.Hz and a sync_rate of 1*pq.Hz for 25 trials. The returned trials are a list with one element per trial and each element contains one spike train per neuron. sync_spikes is a list of spike trains in each trial that appear synchronously in both neurons.
trials, sync_spikes = utils.spike_trains_with_unitary_events(
n_neurons=2, n_trials=25, base_rate=5 * pq.Hz, sync_rate=1 * pq.Hz
)Example: Use plt.eventplot to create a rasterplot for the spike trains in trials from the first neuron.
plt.eventplot([trial[0].times for trial in trials], linelength=0.5);Exercise: Use plt.eventplot to create a rasterplot for the spike trains in trials from the second neuron.
Solution
plt.eventplot([trial[1].times for trial in trials], linelength=0.5);Exercise: Use plt.eventplot to create a rasterplot for the spike trains in sync_spikes.
Solution
plt.eventplot([s.times for s in sync_spikes], linelength=0.5);Example: Plot the spike trains from both neurons in a 2-by-1 subplot grid and plot the events in sync_spikes in "red" with a larger linelength to both subplots.
plt.subplot(2, 1, 1)
plt.eventplot([trial[0].times for trial in trials], linelength=0.5)
plt.eventplot([st.times for st in sync_spikes], color="red", linelength=1)
plt.subplot(2, 1, 2)
plt.eventplot([trial[1].times for trial in trials], linelength=0.5)
plt.eventplot([st.times for st in sync_spikes], color="red", linelength=1);Exercise: Simulate 2 neurons for with a base_rate of 10*pq.Hz and a sync_rate of 2*pq.Hz for 25 trials.
Solution
trials, sync_spikes = utils.spike_trains_with_unitary_events(
base_rate=10 * pq.Hz, sync_rate=2 * pq.Hz, n_neurons=2, n_trials=25
)Exercise: Plot the spike trains from both neurons in a 2-by-1 subplot grid and plot the events in sync_spikes in "red" with a larger linelength to both subplots.
Solution
plt.subplot(2, 1, 1)
plt.eventplot([trial[0].times for trial in trials], linelength=0.5)
plt.eventplot([st.times for st in sync_spikes], color="red", linelength=1)
plt.subplot(2, 1, 2)
plt.eventplot([trial[1].times for trial in trials], linelength=0.5)
plt.eventplot([st.times for st in sync_spikes], color="red", linelength=1);Exercise: Simulate 2 neurons for 25 trials with base_rate=5*pq.Hz and a sync_rate=2*pq.Hz. Then, plot the spike trains from both neurons in a 2-by-1 subplot grid and plot the events in sync_spikes in "red" to both subplots.
Solution
trials, sync_spikes = utils.spike_trains_with_unitary_events(
base_rate=5 * pq.Hz, sync_rate=2 * pq.Hz, n_neurons=2, n_trials=25
)
plt.subplot(2, 1, 1)
plt.eventplot([trial[0].times for trial in trials], linelength=0.5)
plt.eventplot([st.times for st in sync_spikes], color="red", linelength=1)
plt.subplot(2, 1, 2)
plt.eventplot([trial[1].times for trial in trials], linelength=0.5)
plt.eventplot([st.times for st in sync_spikes], color="red", linelength=1);Section 2: Applying Unitary Event Analysis to Simulated Data
Now we can apply UEA to the simulated data. In order to apply UEA the spike trains must be binned. The bin size is an important factor in the analysis because events are considered synchronous if they fall into the same bin. Another critical parameter is the window size in which UEA looks for synchronous events. A larger window increases the statistical power because it increases the number of data points for analysis but it also reduces the temporal precision of the method. In this section we are going to apply UEA to the simulated data and explore the effect of these parameters.
| Code | Description |
|---|---|
UE = ue.jointJ_window_analysis(trials) |
Perform unitary event analysis on the list of trials |
UE = ue.jointJ_window_analysis(trials, bin_size=1*pq.ms) |
Perform unitary event analysis on the list of trials using a bin_size of 1 ms |
plot_ue(trials, UE, significance_level=0.05) |
Plot the trials and the unitary event analysis result UE and highlight spikes that exceed the given significance threshold |
Exercises
Example: Run the ue.jointJ_window_analysis() to apply UEA to the simulated trials and plot the uea_result.
uea_result = ue.jointJ_window_analysis(trials)
plot_ue(trials, uea_result, significance_level=0.05)Exercise: Plot the unitary event analysis results again but increase the significance_level to 0.01.
Solution
plot_ue(trials, uea_result, significance_level=0.01)Exercise: Re-run the ue.jointJ_window_analysis() on the simulated trials but set the win_size=200*pq.ms and plot the results. What results did this have on the statistical significance in panel E of the UEA plot?
Solution
trials, _ = utils.spike_trains_with_unitary_events(
base_rate=5 * pq.Hz, sync_rate=2 * pq.Hz, n_neurons=2, n_trials=25
)
uea_result = ue.jointJ_window_analysis(trials, win_size=200 * pq.ms)
plot_ue(trials, uea_result, significance_level=0.05)Exercise: Reduce the win_size to 25*pq.ms, rerun the ue.jointJ_window_analysis() and plot the result. How does the temporal spread of the significant events in panel F of the UEA plot compare to the previous exercises?
Solution
trials, _ = utils.spike_trains_with_unitary_events(
base_rate=5 * pq.Hz, sync_rate=2 * pq.Hz, n_neurons=2, n_trials=25
)
uea_result = ue.jointJ_window_analysis(trials, win_size=25*pq.ms)
plot_ue(trials, uea_result, significance_level=0.05)Exercise: Further reduce the win_size to 15*pq.ms and rerun ue.jointJ_window_analysis(). Did you find any significant events?
Solution
trials, _ = utils.spike_trains_with_unitary_events(
base_rate=5 * pq.Hz, sync_rate=2 * pq.Hz, n_neurons=2, n_trials=25
)
uea_result = ue.jointJ_window_analysis(trials, win_size=15 * pq.ms)
plot_ue(trials, uea_result, significance_level=0.05)Exercise: Repeat the simulation from @exr-sim1 but simulate 100 trials and rerun the ue.jointJ_window_analysis(). Did you find any significant events now?
Solution
trials, _ = utils.spike_trains_with_unitary_events(
base_rate=5 * pq.Hz, sync_rate=2 * pq.Hz, n_neurons=2, n_trials=100
)
uea_result = ue.jointJ_window_analysis(trials, win_size=15 * pq.ms)
plot_ue(trials, uea_result, significance_level=0.05)Exercise: Simulate 2 neurons for 25 trials with base_rate=10*pq.Hz and a sync_rate=1*pq.Hz. Then, use ue.jointJ_window_analysis() with bin_size=5*pq.ms and plot the result.
Solution
trials, _ = utils.spike_trains_with_unitary_events(
base_rate=10 * pq.Hz, sync_rate=1 * pq.Hz, n_neurons=2, n_trials=25
)
UE = ue.jointJ_window_analysis(trials, bin_size=5 * pq.ms)
plot_ue(trials, UE, significance_level=0.05)Exercise: Reduce the bin_size to 1*pq.ms, rerun the ue.jointJ_window_analysis() and plot the result. How did the smaller bin size affect the number of coincident events in panel C of the plot and the number of unitary events in panel F?
Solution
UE = ue.jointJ_window_analysis(trials, bin_size=1 * pq.ms)
plot_ue(trials, UE, significance_level=0.01)Exercise: Repeat the simulation from @exr-sim2 with n_neurons=3, run the ue.jointJ_window_analysis(). How does the larger number of neurons affect the number of coincident events in panel C of the plot and the number of unitary events in panel F?
Solution
trials, _ = utils.spike_trains_with_unitary_events(
base_rate=10 * pq.Hz, sync_rate=1 * pq.Hz, n_neurons=3, n_trials=25
)
UE = ue.jointJ_window_analysis(trials)
plot_ue(trials, UE, significance_level=0.05)Section 3: Limitations and Pitfalls of UEA
In this section, we will address two critical limitations of UEA. The first is UEAs inability to deal with large numbers of neurons. This is because UEA only considers events that appear synchronously in all neurons. Thus, synchronous events are less likely the more neurons there are. The second limitation is UEA’s tendency to produce false positives if the firing rate of the neurons is highly non stationary (for example if a neuron responds with bursts in spiking to a stimulus). In the following, we will use simulations to explore these cases.
| Code | Description |
|---|---|
ph = ue.hash_from_pattern([0, 1, 1]) |
Create a hash that represents a pattern where neuron 1 is inactive and neurons 2 and 3 are active |
UE = ue.jointJ_window_analysis(trials, pattern_hash=ph) |
Use UEA to search for the patterns indicated by the pattern_hash |
trials = utils.spike_trains_with_rate_step(base_rate, rate_step, n_neurons, n_trials) |
Simulate n_neurons with random spiking following a base_rate with rate_step at 1 second for n_trials |
Exercise: The code below simulates a neuron that responds completely random and appends its spike trains to the simulated trials. Run the uea.jointJ_window_analysis() on the simulated trials. Did you find any unitary events?
neuron = StationaryPoissonProcess(rate=10 * pq.Hz, t_stop=2 * pq.s)
for t in range(len(trials)):
trials[t].append(neuron.generate_spiketrain())Solution
UE = ue.jointJ_window_analysis(trials, bin_size=5*pq.ms)
plot_ue(trials, UE, significance_level=0.05)Exercise: The code below generates a pattern_hash that encodes the pattern where neurons 1,2 and 3 are active but neuron 4 is not. Rerun the ue.jointJ_window_analysis() with pattern_hash=pattern_hash and plot the results.
pattern_hash = ue.hash_from_pattern([1, 1, 1, 0])
pattern_hash14Solution
UE = ue.jointJ_window_analysis(trials, pattern_hash=pattern_hash)
plot_ue(trials, UE, significance_level=0.01)Exercise: The code below simulates three neurons with synchronous events and inserts two neurons that are purely random at positions 2 and 4. Use unitary event analysis with a pattern_hash that only searchers for synchronous activity in neurons 1, 3 and 5 and ignores neurons 2 and 4.
trials, _ = utils.spike_trains_with_unitary_events(
base_rate=10 * pq.Hz, sync_rate=1 * pq.Hz, n_neurons=3, n_trials=25
)
neuron = StationaryPoissonProcess(rate=10 * pq.Hz, t_stop=2 * pq.s)
for t in range(len(trials)):
trials[t].insert(1, neuron.generate_spiketrain())
for t in range(len(trials)):
trials[t].insert(3, neuron.generate_spiketrain())Solution
pattern_hash = ue.hash_from_pattern([1, 0, 1, 0, 1])
UE = ue.jointJ_window_analysis(trials, pattern_hash=pattern_hash)
plot_ue(trials, UE, significance_level=0.01)Exercise: The code below simulates 2 neurons that have no synchronous events at all but that have a base_rate that increases by a rate_step exactly one second into the trial. Run the ue.jointJ_window_analysis() on the simulated trials. Can you see the increased firing rate in the plot?
trials = utils.spike_trains_with_rate_step(base_rate=3 * pq.Hz, rate_step=15 * pq.Hz, n_neurons=2, n_trials=20)Solution
UE = ue.jointJ_window_analysis(trials)
plot_ue(trials, UE, significance_level=0.05)Exercise: Simulate new trials with rate_step=30*pq.Hz. Then, rerun the ue.jointJ_window_analysis() and plot the result.
Solution
trials = utils.spike_trains_with_rate_step(
base_rate=3 * pq.Hz, rate_step=30 * pq.Hz, n_neurons=2, n_trials=20
)
UE = ue.jointJ_window_analysis(trials)
plot_ue(trials, UE, significance_level=0.05)Exercise: Rerun the ue.jointJ_window_analysis() but increase the win_size to 250*pq.ms and plot the result
Solution
UE = ue.jointJ_window_analysis(trials, win_size=250*pq.ms)
plot_ue(trials, UE, significance_level=0.05)Section 4: Applying Unitary Event Analysis to Real Data
Now that we explored UEA and its parameters using simulations, we can apply it to real data. The data in this section was recorded in the motor cortex of a monkey that was trained on a reaching task. In this task the monkey receives the signal to execute the reaching movement at one of several possible time points. It has been shown that, as the monkey learns the times at which the cue can be presented is linked to an increase in synchronous events between motor neurons around these time intervals.
Run the cell below to load the data and return a list of trials
Exercises
io = NixIO(f"{filepath}", "ro")
block = io.read_block()
trials = []
for ind in range(len(block.segments)):
trials.append(block.segments[ind].spiketrains)Exercise: Run the ue.jointJ_window_analysis on the loaded trials. First, use the default parameters. Then, vary the bin_size and win_size and observe how this affects the results
Solution
UE = ue.jointJ_window_analysis(
trials,
)
plot_ue(trials, UE, significance_level=0.05)