Unitary Event Analysis (UAE) and SPADE
Authors
Unitary Event Analysis (UAE) and SPADE: Discovering Spike Synchrony with Coincidence Detection
Setup
Import Libraries
Import the modules required for this session.
import numpy as np
import quantities as pq
from matplotlib import pyplot as plt
import neo
import neo.io
import elephant.unitary_event_analysis as ue
import elephant.statistics as stats
import elephant.spike_train_surrogates as surr
from elephant.spike_train_generation import StationaryPoissonProcess, NonStationaryPoissonProcess, compound_poisson_process
from elephant.spade import spade
from viziphant.unitary_event_analysis import plot_ue
from viziphant.patterns import plot_patterns
np.random.seed(42)
%matplotlib inlineUtility Functions
Define the utility functions required for this session.
class utils:
@staticmethod
def generate_spike_trains_with_coinc(rate_b, rate_c, trial_duration, coinc_duration,
num_trials, num_units=2, unit_ids_sync=None):
"""Simulate spike trains with injected coincidences in the centre of each trial."""
if unit_ids_sync is None:
unit_ids_sync = [[1, 2]]
if not isinstance(unit_ids_sync[0], (list, tuple)):
unit_ids_sync = [list(unit_ids_sync)]
t_coinc_start = (trial_duration - coinc_duration) / 2.
t_coinc_stop = t_coinc_start + coinc_duration
sync_count = np.zeros(num_units, dtype=int)
for subset in unit_ids_sync:
for uid in subset:
sync_count[uid - 1] += 1
spike_trains = []
for _ in range(num_trials):
pre = [StationaryPoissonProcess(rate_b, t_stop=t_coinc_start).generate_spiketrain()
for _ in range(num_units)]
mid_rates = [max((rate_b - rate_c * sync_count[i]).rescale(pq.Hz).magnitude, 0) * pq.Hz
for i in range(num_units)]
mid = [StationaryPoissonProcess(mid_rates[i], t_start=t_coinc_start,
t_stop=t_coinc_stop).generate_spiketrain()
for i in range(num_units)]
post = [StationaryPoissonProcess(rate_b, t_start=t_coinc_stop,
t_stop=trial_duration).generate_spiketrain()
for _ in range(num_units)]
coinc = [StationaryPoissonProcess(rate_c, t_start=t_coinc_start,
t_stop=t_coinc_stop).generate_spiketrain()
for _ in unit_ids_sync]
trial = []
for i in range(num_units):
parts = [pre[i].times.rescale(pq.ms).magnitude,
mid[i].times.rescale(pq.ms).magnitude,
post[i].times.rescale(pq.ms).magnitude]
for j, subset in enumerate(unit_ids_sync):
if (i + 1) in subset:
parts.append(coinc[j].times.rescale(pq.ms).magnitude)
times = np.sort(np.concatenate(parts)) * pq.ms
trial.append(neo.SpikeTrain(times, t_start=0. * pq.ms, t_stop=trial_duration))
spike_trains.append(trial)
return spike_trains
@staticmethod
def compute_complexity_distribution(spike_trains, bin_size):
"""Compute the normalised complexity distribution of a list of spike trains."""
n = len(spike_trains)
complexity = stats.Complexity(spike_trains, bin_size=bin_size, binary=True)
dist = complexity.pdf().flatten().magnitude
return np.concatenate([dist, np.zeros(n - len(dist))])
@staticmethod
def generate_spike_trains_with_osc_rates(
num_trials, num_units, trial_duration,
rate_offset, rate_amplitude, rate_frequency,
coinc_offset=0 * pq.Hz, coinc_amplitude=0 * pq.Hz, coinc_frequency=0 * pq.Hz):
"""Simulate spike trains with an oscillatory background rate and optional coincidences."""
dt = 1 * pq.ms
n = int(trial_duration.rescale(pq.ms).magnitude)
t_s = np.arange(n) * dt.rescale(pq.s).magnitude
pi2 = 2 * np.pi
bg = np.maximum(
rate_offset.rescale(pq.Hz).magnitude
+ rate_amplitude.rescale(pq.Hz).magnitude
* np.sin(pi2 * rate_frequency.rescale(pq.Hz).magnitude * t_s), 0) * pq.Hz
coinc = np.maximum(
coinc_offset.rescale(pq.Hz).magnitude
+ coinc_amplitude.rescale(pq.Hz).magnitude
* np.sin(pi2 * coinc_frequency.rescale(pq.Hz).magnitude * t_s), 0) * pq.Hz
spike_trains = []
for _ in range(num_trials):
bg_sig = neo.AnalogSignal(bg, t_start=0 * pq.ms, sampling_period=dt)
coinc_sig = neo.AnalogSignal(coinc, t_start=0 * pq.ms, sampling_period=dt)
coinc_st = NonStationaryPoissonProcess(coinc_sig).generate_spiketrain()
trial = []
for _ in range(num_units):
bg_st = NonStationaryPoissonProcess(bg_sig).generate_spiketrain()
times = np.sort(np.concatenate([
bg_st.times.rescale(pq.ms).magnitude,
coinc_st.times.rescale(pq.ms).magnitude])) * pq.ms
trial.append(neo.SpikeTrain(times, t_start=0. * pq.ms, t_stop=trial_duration))
spike_trains.append(trial)
return spike_trains
@staticmethod
def load_spike_trains(filename):
"""Load all trials for one neuron from a .nix file."""
with neo.io.NixIO(filename, "ro") as io:
return [seg.spiketrains[0] for seg in io.read_block().segments]Download Data
import os
import owncloud
from tqdm import tqdm
oc = owncloud.Client.from_public_link('https://uni-bonn.sciebo.de/s/4KWyXRRFMZNSXcb', folder_password='ibots')
os.makedirs('data', exist_ok=True)
for f in tqdm(oc.list('/'), desc='Downloading Files to data/'):
if not os.path.exists(f'data{f.path}'):
oc.get_file(f.path, f'data{f.path}')Section 1: Unitary Event Analysis (UEA)
Unitary Event Analysis (UEA) detects time intervals in which two or more neurons fire synchronously more often than expected by chance given their individual firing rates. The method slides a window across trials and computes a surprise measure: positive values indicate excess synchrony, negative values indicate a deficit.
Simulating Data with Embedded Synchrony
Before applying UEA to real data, we use simulations with a known coincidence structure to understand the method and its parameters.
| Code | Description |
|---|---|
trials = utils.generate_spike_trains_with_coinc(rate_b, rate_c, trial_duration, coinc_duration, num_trials) |
Simulate num_trials trials; each trial is a list of 2 spike trains with coincidences at rate rate_c for coinc_duration in the middle |
ue_result = ue.jointJ_window_analysis(trials) |
Apply UEA to the list of trials with default parameters |
ue_result = ue.jointJ_window_analysis(trials, bin_size=1*pq.ms, win_size=100*pq.ms) |
Apply UEA with specific bin_size and win_size |
plot_ue(trials, ue_result, significance_level=0.05) |
Plot the UEA result |
Units and spike train objects: Parameters use the
quantitieslibrary — write30 * pq.Hz,1000 * pq.ms, etc. and Elephant handles the units automatically. Spike trains areneo.SpikeTrainobjects; you can pass them directly to all Elephant functions without unpacking them.
Exercises
Example: Simulate 50 trials with rate_b=30*pq.Hz, rate_c=2*pq.Hz, and a 100 ms coincidence window. Apply UEA and plot the result.
num_trials = 50
trial_duration = 1000 * pq.ms
rate_b = 30 * pq.Hz
rate_c = 2 * pq.Hz
coinc_duration = 100 * pq.ms
trials = utils.generate_spike_trains_with_coinc(
rate_b, rate_c, trial_duration, coinc_duration, num_trials
)
ue_result = ue.jointJ_window_analysis(
trials, bin_size=1 * pq.ms, win_size=100 * pq.ms
)
plot_ue(trials, ue_result, significance_level=0.05, unit_real_ids=[1, 2])Exercise: Increase num_trials to 200. Rerun generate_spike_trains_with_coinc and jointJ_window_analysis. Does the surprise in the coincidence window increase? Does the number of significantly marked events change?
Solution
num_trials = 200
trials = utils.generate_spike_trains_with_coinc(
rate_b, rate_c, trial_duration, coinc_duration, num_trials
)
ue_result = ue.jointJ_window_analysis(
trials, bin_size=1 * pq.ms, win_size=100 * pq.ms
)
plot_ue(trials, ue_result, significance_level=0.05, unit_real_ids=[1, 2])Exercise: Using the 50 trial simulation from the Example (reset num_trials=50 and rerun generate_spike_trains_with_coinc), apply jointJ_window_analysis with win_size=900*pq.ms (close to the full trial duration). Plot the result. Do you observe significant events? Why might an overly large analysis window suppress detection?
Solution
num_trials = 50
trials = utils.generate_spike_trains_with_coinc(
rate_b, rate_c, trial_duration, coinc_duration, num_trials
)
ue_result = ue.jointJ_window_analysis(
trials, bin_size=1 * pq.ms, win_size=900 * pq.ms
)
plot_ue(trials, ue_result, significance_level=0.05, unit_real_ids=[1, 2])Exercise: Print ue.hash_from_pattern([1, 1]) and ue.hash_from_pattern([1, 0]). What does each hash value encode? How would you specify that only neuron 2 should fire (pattern [0, 1])?
Solution
print(ue.hash_from_pattern([1, 1]))
print(ue.hash_from_pattern([1, 0]))
print(ue.hash_from_pattern([0, 1]))3
2
1Exercise: Simulate 50 trials with rate_b=30*pq.Hz, rate_c=2*pq.Hz, and coinc_duration=200*pq.ms (longer coincidence window). Apply UEA with win_size=100*pq.ms. Does a longer coincidence window produce more significant events? Why might the surprise value change?
Solution
trials = utils.generate_spike_trains_with_coinc(
rate_b=30 * pq.Hz, rate_c=2 * pq.Hz,
trial_duration=1000 * pq.ms, coinc_duration=200 * pq.ms,
num_trials=50
)
ue_result = ue.jointJ_window_analysis(
trials, bin_size=1 * pq.ms, win_size=100 * pq.ms
)
plot_ue(trials, ue_result, significance_level=0.05, unit_real_ids=[1, 2])Exercise: Rerun jointJ_window_analysis with win_size=25*pq.ms and plot. How does the temporal precision of the detected synchrony change compared to win_size=100*pq.ms?
Solution
trials = utils.generate_spike_trains_with_coinc(
rate_b=30 * pq.Hz, rate_c=2 * pq.Hz,
trial_duration=1000 * pq.ms, coinc_duration=100 * pq.ms,
num_trials=50
)
ue_result = ue.jointJ_window_analysis(
trials, bin_size=1 * pq.ms, win_size=25 * pq.ms
)
plot_ue(trials, ue_result, significance_level=0.05, unit_real_ids=[1, 2])Exercise: Simulate 50 trials with rate_b=80*pq.Hz and rate_c=2*pq.Hz. Apply UEA with win_size=100*pq.ms. Do you still observe significant events? Why does a higher background rate make coincidences harder to detect?
Solution
trials = utils.generate_spike_trains_with_coinc(
rate_b=80 * pq.Hz, rate_c=2 * pq.Hz,
trial_duration=1000 * pq.ms, coinc_duration=100 * pq.ms,
num_trials=50
)
ue_result = ue.jointJ_window_analysis(
trials, bin_size=1 * pq.ms, win_size=100 * pq.ms
)
plot_ue(trials, ue_result, significance_level=0.05, unit_real_ids=[1, 2])Exercise: Simulate 50 trials with rate_b=30*pq.Hz and rate_c=0.5*pq.Hz (fewer coincidences). Apply UEA with win_size=100*pq.ms. Does the number of significantly marked events decrease compared to rate_c=2*pq.Hz? What does this tell you about the sensitivity of UEA to the coincidence rate?
Solution
trials = utils.generate_spike_trains_with_coinc(
rate_b=30 * pq.Hz, rate_c=0.5 * pq.Hz,
trial_duration=1000 * pq.ms, coinc_duration=100 * pq.ms,
num_trials=50
)
ue_result = ue.jointJ_window_analysis(
trials, bin_size=1 * pq.ms, win_size=100 * pq.ms
)
plot_ue(trials, ue_result, significance_level=0.05, unit_real_ids=[1, 2])Exercise: For the 50 trial simulation with rate_b=30*pq.Hz and rate_c=2*pq.Hz, print ue_result['n_emp']. This array contains the number of observed coincidences in each window. Also print ue_result['n_exp'] for the expected count. In which windows are the two most different?
Solution
trials = utils.generate_spike_trains_with_coinc(
rate_b=30 * pq.Hz, rate_c=2 * pq.Hz,
trial_duration=1000 * pq.ms, coinc_duration=100 * pq.ms,
num_trials=50
)
ue_result = ue.jointJ_window_analysis(
trials, bin_size=1 * pq.ms, win_size=100 * pq.ms
)
print("n_emp:", ue_result['n_emp'])
print("n_exp:", ue_result['n_exp'])n_emp: [[ 9.]
[ 8.]
[ 9.]
[ 9.]
[ 9.]
[ 9.]
[ 9.]
[ 9.]
[ 9.]
[ 9.]
[10.]
[10.]
[10.]
[10.]
[ 8.]
[ 8.]
[ 9.]
[ 7.]
[ 6.]
[ 5.]
[ 5.]
[ 5.]
[ 4.]
[ 4.]
[ 5.]
[ 5.]
[ 5.]
[ 5.]
[ 5.]
[ 5.]
[ 3.]
[ 3.]
[ 3.]
[ 4.]
[ 5.]
[ 5.]
[ 4.]
[ 4.]
[ 4.]
[ 4.]
[ 4.]
[ 5.]
[ 5.]
[ 5.]
[ 4.]
[ 4.]
[ 4.]
[ 4.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 3.]
[ 3.]
[ 3.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 3.]
[ 4.]
[ 5.]
[ 4.]
[ 5.]
[ 6.]
[ 9.]
[ 9.]
[ 9.]
[11.]
[12.]
[13.]
[13.]
[14.]
[14.]
[14.]
[15.]
[17.]
[17.]
[18.]
[17.]
[16.]
[15.]
[15.]
[14.]
[14.]
[11.]
[10.]
[11.]
[ 9.]
[ 8.]
[ 7.]
[ 7.]
[ 7.]
[ 7.]
[ 7.]
[ 6.]
[ 4.]
[ 4.]
[ 3.]
[ 3.]
[ 3.]
[ 4.]
[ 4.]
[ 4.]
[ 3.]
[ 3.]
[ 4.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 1.]
[ 1.]
[ 2.]
[ 3.]
[ 3.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 2.]
[ 1.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 2.]
[ 1.]
[ 1.]
[ 2.]
[ 2.]
[ 2.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 3.]
[ 4.]
[ 4.]
[ 4.]
[ 4.]
[ 4.]
[ 6.]
[ 6.]]
n_exp: [[4.2 ]
[4.19]
[4.07]
[4.32]
[4.59]
[4.55]
[4.48]
[4.37]
[4.66]
[4.81]
[4.92]
[5.1 ]
[5.21]
[5.32]
[5.22]
[5.05]
[4.82]
[4.74]
[4.77]
[4.65]
[4.79]
[4.79]
[4.7 ]
[4.4 ]
[4.31]
[4.15]
[4.49]
[4.54]
[4.42]
[4.91]
[4.59]
[4.53]
[4.44]
[4.23]
[4.07]
[4.29]
[4.33]
[4.21]
[4.46]
[4.4 ]
[4.4 ]
[4.46]
[4.54]
[4.75]
[4.65]
[4.7 ]
[4.58]
[4.79]
[4.6 ]
[4.36]
[4.43]
[4.28]
[4.55]
[4.36]
[4.42]
[4.22]
[4.37]
[4.68]
[4.94]
[5.17]
[4.97]
[4.84]
[4.64]
[4.51]
[4.62]
[4.75]
[4.9 ]
[4.66]
[4.49]
[4.21]
[4.31]
[4.51]
[4.52]
[4.75]
[4.69]
[4.57]
[4.58]
[4.84]
[4.99]
[4.83]
[4.95]
[5.38]
[5.5 ]
[5.3 ]
[5.44]
[5.24]
[4.96]
[5.03]
[5.2 ]
[5.03]
[5.22]
[5.1 ]
[5.04]
[4.92]
[5.25]
[5.09]
[5.02]
[4.85]
[4.62]
[4.84]
[4.42]
[4.08]
[4.05]
[4.22]
[4.3 ]
[4.27]
[4.39]
[4.42]
[4.19]
[4.23]
[4.15]
[4.16]
[4.07]
[4.07]
[3.68]
[4.21]
[4.1 ]
[4.22]
[4.29]
[4.42]
[4.75]
[5.03]
[5.32]
[5.42]
[5.19]
[5.04]
[4.76]
[4.76]
[4.66]
[4.7 ]
[4.55]
[4.58]
[4.25]
[4.38]
[4.51]
[4.2 ]
[4.46]
[4.34]
[4.15]
[4.1 ]
[3.88]
[3.87]
[3.64]
[3.55]
[3.5 ]
[3.68]
[3.51]
[3.68]
[3.82]
[4.03]
[4.03]
[3.99]
[3.97]
[3.84]
[3.97]
[4.08]
[4.06]
[4.15]
[4.09]
[4.17]
[4.23]
[4.23]
[4.27]
[4.23]
[4.21]
[4.33]
[4.4 ]
[4.65]
[4.72]
[4.76]
[4.91]
[5. ]
[4.96]
[5.04]
[5.11]
[5.01]
[5.01]
[4.98]
[5.28]
[5.32]
[5.25]]Pairwise vs. Higher Order Synchrony
UEA can search for synchrony in subsets of neurons. The pattern_hash argument specifies which neurons participate. A pattern [1, 1, 1] means all three neurons must fire together; [1, 1, 0] means only neurons 1 and 2.
| Code | Description |
|---|---|
trials = utils.generate_spike_trains_with_coinc(..., num_units=3, unit_ids_sync=[[1,2,3]]) |
Simulate 3 neurons with triple coincidences |
ph = ue.hash_from_pattern([1, 1, 1]) |
Hash for the triplet pattern |
ue_result = ue.jointJ_window_analysis(trials, pattern_hash=[ph]) |
Apply UEA searching for the specified pattern |
Exercises
Example: Simulate 3 neurons with coincidences in all three (unit_ids_sync=[[1,2,3]]). Apply UEA with the triplet pattern.
trials3 = utils.generate_spike_trains_with_coinc(
rate_b=30 * pq.Hz, rate_c=2 * pq.Hz,
trial_duration=1000 * pq.ms, coinc_duration=100 * pq.ms,
num_trials=100, num_units=3, unit_ids_sync=[[1, 2, 3]]
)
ph_triplet = ue.hash_from_pattern([1, 1, 1])
ue_result3 = ue.jointJ_window_analysis(
trials3, bin_size=1 * pq.ms, win_size=100 * pq.ms, pattern_hash=[ph_triplet]
)
plot_ue(trials3, ue_result3, significance_level=0.05, unit_real_ids=[1, 2, 3])Exercise: Change unit_ids_sync=[[1,2],[2,3]] to inject only pairwise coincidences between neurons (1,2) and (2,3). Rerun generate_spike_trains_with_coinc with num_trials=100, num_units=3. Apply jointJ_window_analysis with ph_triplet and plot. Is the triplet pattern significant?
Solution
trials_pair = utils.generate_spike_trains_with_coinc(
rate_b=30 * pq.Hz, rate_c=2 * pq.Hz,
trial_duration=1000 * pq.ms, coinc_duration=100 * pq.ms,
num_trials=100, num_units=3, unit_ids_sync=[[1, 2], [2, 3]]
)
ue_result_pair = ue.jointJ_window_analysis(
trials_pair, bin_size=1 * pq.ms, win_size=100 * pq.ms, pattern_hash=[ph_triplet]
)
plot_ue(trials_pair, ue_result_pair, significance_level=0.05, unit_real_ids=[1, 2, 3])Exercise: Rerun jointJ_window_analysis on the same pairwise data with pattern_hash=[ue.hash_from_pattern([1,1,0])] (neurons 1 and 2 only). Is this pairwise pattern significant? What does this tell you about the importance of specifying the correct pattern order?
Solution
ue_result_pair12 = ue.jointJ_window_analysis(
trials_pair, bin_size=1 * pq.ms, win_size=100 * pq.ms,
pattern_hash=[ue.hash_from_pattern([1, 1, 0])]
)
plot_ue(trials_pair, ue_result_pair12, significance_level=0.05, unit_real_ids=[1, 2, 3])Non-Stationary Firing Rates
Real neuronal firing rates often oscillate in time. When both neurons share a common rate modulation, the expected coincidence count rises and falls with the rate — and UEA may report false excess synchrony even without genuine coordination. Here we examine this effect and see how the "analytic_TrialAverage" method handles it.
utils.generate_spike_trains_with_osc_rates generates spike trains with a sinusoidal background rate and an optional sinusoidal coincidence process.
| Code | Description |
|---|---|
utils.generate_spike_trains_with_osc_rates(num_trials, 2, trial_duration, rate_offset, rate_amplitude, rate_frequency) |
Oscillatory background, no injected coincidences |
utils.generate_spike_trains_with_osc_rates(..., coinc_amplitude=2*pq.Hz, coinc_frequency=2*pq.Hz) |
Oscillatory coincidences added on top |
Exercises
Example: Simulate 100 trials with a 5 Hz oscillatory background rate and no injected coincidences. Apply UEA with win_size=100*pq.ms. Does the surprise show false positives tied to the rate modulation?
num_trials = 100
trial_duration = 1000 * pq.ms
trials_osc = utils.generate_spike_trains_with_osc_rates(
num_trials=num_trials, num_units=2, trial_duration=trial_duration,
rate_offset=30 * pq.Hz, rate_amplitude=10 * pq.Hz, rate_frequency=5 * pq.Hz
)
ue_result_osc = ue.jointJ_window_analysis(
trials_osc, bin_size=1 * pq.ms, win_size=100 * pq.ms
)
plot_ue(trials_osc, ue_result_osc, significance_level=0.05, unit_real_ids=[1, 2])Exercise: Add an oscillatory coincidence process to the same background: coinc_offset=0*pq.Hz, coinc_amplitude=2*pq.Hz, coinc_frequency=2*pq.Hz. Apply UEA with win_size=100*pq.ms. Does UEA correctly detect the excess synchrony in windows where the coincidence rate is highest, despite the oscillating background?
Solution
trials_osc_coinc = utils.generate_spike_trains_with_osc_rates(
num_trials=num_trials, num_units=2, trial_duration=trial_duration,
rate_offset=30 * pq.Hz, rate_amplitude=10 * pq.Hz, rate_frequency=5 * pq.Hz,
coinc_offset=0 * pq.Hz, coinc_amplitude=2 * pq.Hz, coinc_frequency=2 * pq.Hz
)
ue_result_osc_coinc = ue.jointJ_window_analysis(
trials_osc_coinc, bin_size=1 * pq.ms, win_size=100 * pq.ms
)
plot_ue(trials_osc_coinc, ue_result_osc_coinc, significance_level=0.05, unit_real_ids=[1, 2])Exercise: Return to the oscillatory background without coincidences (trials_osc). Rerun jointJ_window_analysis with method="analytic_TrialAverage". Does this method reduce the false positives? What does trial averaging the expected count do differently from the default method?
Solution
ue_result_ta = ue.jointJ_window_analysis(
trials_osc, bin_size=1 * pq.ms, win_size=100 * pq.ms,
method="analytic_TrialAverage"
)
plot_ue(trials_osc, ue_result_ta, significance_level=0.05, unit_real_ids=[1, 2])UEA on Real Data
We now apply UEA to spike trains recorded from motor cortex of a macaque monkey during a delayed pointing task (Riehle et al., 1997). The delay duration varied from trial to trial (600–1500 ms). Thirty-six trials with the longest delay are pooled here. The monkey could anticipate the reaction signal (RS) at three earlier expected moments (ES1, ES2, ES3) before it actually occurred.
Time stamps of events relative to the preparatory signal (PS): PS = 300 ms, ES1 = 900 ms, ES2 = 1200 ms, ES3 = 1500 ms, RS = 1800 ms.
| Code | Description |
|---|---|
spiketrains = [[st1, st2] for st1, st2 in zip(spike_trains1, spike_trains2)] |
Combine two neurons into the trial-list format expected by UEA |
ue.jointJ_window_analysis(spiketrains, method="analytic_TrialAverage") |
Apply UEA using the analytic trial-average method |
plot_ue(spiketrains, ue_result, events={"PS": [300*pq.ms], ...}) |
Plot UEA result with labelled event times |
Exercises
Example: Load Data14.npy and Data15.npy and apply UEA with the behavioural event markers.
spike_trains14 = utils.load_spike_trains("data/Data14.nix")
spike_trains15 = utils.load_spike_trains("data/Data15.nix")
spiketrains_real = [[st1, st2] for st1, st2 in zip(spike_trains14, spike_trains15)]
ue_result_real = ue.jointJ_window_analysis(
spiketrains_real, bin_size=5 * pq.ms, win_size=100 * pq.ms,
method="analytic_TrialAverage"
)
events = {"PS": [300 * pq.ms], "ES1": [900 * pq.ms],
"ES2": [1200 * pq.ms], "ES3": [1500 * pq.ms], "RS": [1800 * pq.ms]}
plot_ue(spiketrains_real, ue_result_real, significance_level=0.05,
unit_real_ids=(14, 15), events=events)Exercise: Change significance_level to 0.01 in plot_ue. How many significant events remain? Around which behavioural events do they cluster?
Solution
plot_ue(spiketrains_real, ue_result_real, significance_level=0.01,
unit_real_ids=(14, 15), events=events)Exercise: Print list(ue_result_real.keys()) to see the available output fields. Then print ue_result_real["rate_avg"]. What do the two values represent, given that the recording contains two neurons?
Solution
print(list(ue_result_real.keys()))
print(ue_result_real["rate_avg"])['Js', 'indices', 'n_emp', 'n_exp', 'rate_avg', 'input_parameters']
[[0.02166667 0.01861111]
[0.02138889 0.02027778]
[0.02194444 0.01916667]
[0.0225 0.01805556]
[0.02277778 0.01777778]
[0.02305555 0.0175 ]
[0.0225 0.0175 ]
[0.0225 0.0175 ]
[0.02111111 0.01777778]
[0.02194444 0.01861111]
[0.02222222 0.01916667]
[0.02194444 0.01944444]
[0.02277778 0.01888889]
[0.02166667 0.01833333]
[0.0225 0.0175 ]
[0.02416667 0.0175 ]
[0.02305555 0.01722222]
[0.0225 0.01805556]
[0.02166667 0.01861111]
[0.02361111 0.02027778]
[0.02388889 0.02055556]
[0.02416667 0.01861111]
[0.02361111 0.01861111]
[0.02388889 0.01888889]
[0.02361111 0.01833333]
[0.02388889 0.01833333]
[0.02472222 0.01916667]
[0.025 0.01861111]
[0.02583333 0.02 ]
[0.02527778 0.01972222]
[0.025 0.01972222]
[0.025 0.01972222]
[0.02472222 0.01972222]
[0.02555556 0.01888889]
[0.02472222 0.01972222]
[0.02416667 0.02 ]
[0.02555556 0.01944444]
[0.02555556 0.01888889]
[0.02611111 0.01861111]
[0.02583333 0.01833333]
[0.02555556 0.01805556]
[0.02611111 0.01944444]
[0.02583333 0.02027778]
[0.02694444 0.02 ]
[0.02666667 0.02055556]
[0.02722222 0.02055556]
[0.0275 0.01888889]
[0.0275 0.01888889]
[0.02694444 0.01888889]
[0.02805555 0.01944444]
[0.02888889 0.01916667]
[0.02833333 0.01861111]
[0.02805555 0.01888889]
[0.0275 0.01916667]
[0.02722222 0.01916667]
[0.02694444 0.01916667]
[0.02666667 0.01916667]
[0.02666667 0.01972222]
[0.02611111 0.01916667]
[0.02444444 0.01916667]
[0.02444444 0.01805556]
[0.02416667 0.01694444]
[0.02527778 0.01666667]
[0.02388889 0.01722222]
[0.025 0.01694444]
[0.02472222 0.01805556]
[0.02388889 0.01861111]
[0.02333333 0.01972222]
[0.02388889 0.01916667]
[0.02361111 0.01805556]
[0.02388889 0.01888889]
[0.02555556 0.01861111]
[0.025 0.01916667]
[0.02555556 0.01972222]
[0.02666667 0.01972222]
[0.02638889 0.01916667]
[0.02666667 0.01888889]
[0.02666667 0.01916667]
[0.02694444 0.01944444]
[0.02916667 0.01888889]
[0.03 0.02 ]
[0.03055556 0.02055556]
[0.02944444 0.02027778]
[0.02972222 0.02083333]
[0.02944444 0.02166667]
[0.02972222 0.02111111]
[0.02972222 0.02083333]
[0.03138889 0.01944444]
[0.0325 0.01805556]
[0.0325 0.01833333]
[0.03222222 0.01833333]
[0.03083333 0.01944444]
[0.03083333 0.0175 ]
[0.03111111 0.01666667]
[0.03194444 0.01722222]
[0.0325 0.0175 ]
[0.03138889 0.01833333]
[0.03138889 0.0175 ]
[0.0325 0.0175 ]
[0.03166667 0.01694444]
[0.03166667 0.01555556]
[0.03083333 0.01555556]
[0.03083333 0.01555556]
[0.03083333 0.015 ]
[0.03027778 0.01388889]
[0.02972222 0.01444445]
[0.03166667 0.01444445]
[0.02944444 0.01472222]
[0.02944444 0.01527778]
[0.02861111 0.01527778]
[0.02861111 0.01527778]
[0.02916667 0.015 ]
[0.03027778 0.01583333]
[0.02972222 0.01694444]
[0.02888889 0.01611111]
[0.02805555 0.01555556]
[0.02805555 0.015 ]
[0.02861111 0.01444445]
[0.02777778 0.015 ]
[0.02694444 0.015 ]
[0.02638889 0.01583333]
[0.02861111 0.01611111]
[0.02888889 0.01638889]
[0.02944444 0.01583333]
[0.02944444 0.01555556]
[0.02972222 0.01527778]
[0.02805555 0.01555556]
[0.02888889 0.01583333]
[0.02861111 0.01638889]
[0.02888889 0.01611111]
[0.02972222 0.01583333]
[0.02888889 0.01555556]
[0.02833333 0.01555556]
[0.02888889 0.01583333]
[0.02888889 0.01527778]
[0.02972222 0.01527778]
[0.03055556 0.01472222]
[0.03111111 0.015 ]
[0.03111111 0.01527778]
[0.03277778 0.01527778]
[0.03305556 0.015 ]
[0.03138889 0.01416667]
[0.03194444 0.01388889]
[0.03166667 0.01388889]
[0.0325 0.01388889]
[0.0325 0.01333333]
[0.03333333 0.01277778]
[0.03333333 0.01305556]
[0.03222222 0.01222222]
[0.03333333 0.01333333]
[0.0325 0.01277778]
[0.03472222 0.01222222]
[0.03472222 0.01222222]
[0.03444445 0.01138889]
[0.035 0.01194444]
[0.03472222 0.0125 ]
[0.03444445 0.01277778]
[0.03472222 0.01222222]
[0.03388889 0.01083333]
[0.03333333 0.01111111]
[0.03361111 0.01083333]
[0.03472222 0.01138889]
[0.03361111 0.01111111]
[0.03416667 0.01083333]
[0.03416667 0.01083333]
[0.03472222 0.01027778]
[0.03333333 0.01 ]
[0.03472222 0.00944444]
[0.03555556 0.01083333]
[0.03527778 0.00972222]
[0.035 0.01 ]
[0.03388889 0.01055556]
[0.03305556 0.01111111]
[0.0325 0.01055556]
[0.03194444 0.01027778]
[0.03194444 0.01083333]
[0.03305556 0.01027778]
[0.03222222 0.01027778]
[0.0325 0.01027778]
[0.0325 0.01027778]
[0.03194444 0.01 ]
[0.03111111 0.01 ]
[0.03222222 0.01083333]
[0.03222222 0.01138889]
[0.03166667 0.01166667]
[0.03083333 0.01194444]
[0.03166667 0.01222222]
[0.03138889 0.01222222]
[0.03083333 0.01111111]
[0.03083333 0.01138889]
[0.03138889 0.01111111]
[0.03222222 0.01083333]
[0.03277778 0.00972222]
[0.03444445 0.01 ]
[0.03416667 0.01027778]
[0.03361111 0.00916667]
[0.03222222 0.00972222]
[0.03166667 0.00972222]
[0.03277778 0.01 ]
[0.03222222 0.01 ]
[0.03194444 0.01083333]
[0.03166667 0.01083333]
[0.03194444 0.00972222]
[0.03083333 0.00944444]
[0.03083333 0.00944444]
[0.03027778 0.00944444]
[0.02972222 0.00944444]
[0.02888889 0.00944444]
[0.02861111 0.00916667]
[0.02888889 0.00944444]
[0.02888889 0.00944444]
[0.02777778 0.00916667]
[0.02833333 0.00944444]
[0.02833333 0.00944444]
[0.02777778 0.00972222]
[0.02805555 0.00972222]
[0.02888889 0.00972222]
[0.02888889 0.01055556]
[0.02861111 0.01027778]
[0.03 0.01055556]
[0.03 0.01027778]
[0.03 0.01055556]
[0.03055556 0.01083333]
[0.03138889 0.01138889]
[0.03194444 0.01166667]
[0.0325 0.01138889]
[0.03305556 0.01138889]
[0.03388889 0.0125 ]
[0.03472222 0.0125 ]
[0.03416667 0.01194444]
[0.03444445 0.01194444]
[0.03388889 0.01222222]
[0.0325 0.0125 ]
[0.03166667 0.01277778]
[0.03166667 0.01222222]
[0.03194444 0.01194444]
[0.03138889 0.01194444]
[0.03111111 0.01166667]
[0.03027778 0.0125 ]
[0.03027778 0.0125 ]
[0.03111111 0.0125 ]
[0.03055556 0.01166667]
[0.02833333 0.01138889]
[0.02861111 0.01083333]
[0.02833333 0.01055556]
[0.02805555 0.01083333]
[0.0275 0.01083333]
[0.02694444 0.01 ]
[0.02638889 0.01 ]
[0.02638889 0.01 ]
[0.02527778 0.01055556]
[0.02583333 0.01 ]
[0.02666667 0.01 ]
[0.02611111 0.00972222]
[0.0275 0.00972222]
[0.02694444 0.01055556]
[0.02722222 0.01027778]
[0.02888889 0.01055556]
[0.02944444 0.01055556]
[0.02833333 0.01055556]
[0.02777778 0.01138889]
[0.02805555 0.01194444]
[0.02916667 0.01222222]
[0.02833333 0.0125 ]
[0.02777778 0.01222222]
[0.02916667 0.01277778]
[0.02888889 0.01277778]
[0.02805555 0.01194444]
[0.02805555 0.01277778]
[0.0275 0.0125 ]
[0.02944444 0.01166667]
[0.03 0.01166667]
[0.02888889 0.01111111]
[0.02888889 0.01194444]
[0.02805555 0.01166667]
[0.02833333 0.01194444]
[0.02833333 0.01194444]
[0.0275 0.01138889]
[0.02694444 0.01111111]
[0.02611111 0.01138889]
[0.02555556 0.01027778]
[0.02555556 0.00944444]
[0.02472222 0.00916667]
[0.02583333 0.00861111]
[0.02638889 0.00833333]
[0.025 0.0075 ]
[0.02527778 0.00777778]
[0.02611111 0.00833333]
[0.02694444 0.00722222]
[0.02722222 0.00861111]
[0.02583333 0.00888889]
[0.02472222 0.01 ]
[0.02666667 0.01083333]
[0.02694444 0.01055556]
[0.02611111 0.01083333]
[0.02527778 0.01 ]
[0.02555556 0.00972222]
[0.02583333 0.01 ]
[0.02638889 0.01027778]
[0.0275 0.01027778]
[0.0275 0.01 ]
[0.0275 0.01027778]
[0.02722222 0.01055556]
[0.02638889 0.01055556]
[0.02722222 0.01055556]
[0.02722222 0.01055556]
[0.02888889 0.00972222]
[0.02833333 0.00944444]
[0.02694444 0.01 ]
[0.02694444 0.00944444]
[0.02611111 0.00916667]
[0.02694444 0.00861111]
[0.02555556 0.00833333]
[0.02555556 0.00805556]
[0.02694444 0.00805556]
[0.02833333 0.00777778]
[0.0275 0.00888889]
[0.0275 0.00888889]
[0.02666667 0.00833333]
[0.02638889 0.00777778]
[0.02694444 0.00777778]
[0.02805555 0.00805556]
[0.03 0.00805556]
[0.02972222 0.00805556]
[0.02777778 0.00833333]
[0.02805555 0.00888889]
[0.02555556 0.00888889]
[0.02583333 0.00861111]
[0.02611111 0.00833333]
[0.02611111 0.00888889]
[0.02777778 0.01 ]
[0.0275 0.00972222]
[0.02777778 0.00944444]
[0.02722222 0.00944444]
[0.02583333 0.00944444]
[0.02583333 0.00972222]
[0.02555556 0.00888889]
[0.02416667 0.00833333]
[0.02555556 0.00861111]
[0.02555556 0.00888889]
[0.02694444 0.00972222]
[0.02555556 0.01 ]
[0.02472222 0.00972222]
[0.02388889 0.01055556]
[0.02527778 0.01027778]
[0.02555556 0.01027778]
[0.02638889 0.01055556]
[0.02638889 0.01083333]
[0.02611111 0.01111111]
[0.02694444 0.01027778]
[0.02694444 0.00972222]
[0.02694444 0.00972222]
[0.02722222 0.00972222]
[0.02694444 0.00944444]
[0.02722222 0.00972222]
[0.02611111 0.00972222]
[0.0275 0.00944444]
[0.02861111 0.00972222]
[0.02722222 0.01027778]
[0.02666667 0.00944444]
[0.02388889 0.00861111]
[0.02388889 0.00833333]
[0.0225 0.00833333]
[0.02222222 0.0075 ]
[0.02055556 0.00805556]
[0.01972222 0.0075 ]
[0.01888889 0.0075 ]
[0.01777778 0.00777778]
[0.01722222 0.00722222]
[0.01555556 0.00694444]
[0.01333333 0.00638889]
[0.01222222 0.00638889]
[0.01138889 0.00611111]
[0.01138889 0.00611111]
[0.01083333 0.00583333]
[0.01027778 0.00638889]
[0.00861111 0.00666667]
[0.00722222 0.00638889]
[0.00638889 0.00555556]
[0.00638889 0.00583333]
[0.00666667 0.00611111]
[0.00583333 0.00638889]
[0.00611111 0.00666667]
[0.00638889 0.00638889]
[0.00694444 0.00583333]
[0.00666667 0.00583333]
[0.00722222 0.00583333]
[0.00805556 0.00638889]
[0.00861111 0.00666667]
[0.00916667 0.00638889]
[0.01055556 0.00638889]
[0.01111111 0.00611111]
[0.01166667 0.00583333]
[0.01194444 0.00611111]
[0.01194444 0.00611111]
[0.01222222 0.00555556]
[0.01333333 0.00583333]
[0.01361111 0.00583333]
[0.01444445 0.00555556]
[0.015 0.00527778]
[0.01527778 0.00527778]] 1/msExercise: Print ue_result_real['Js'].shape. What do the two dimensions of this array represent? Then print np.max(ue_result_real['Js']) to find the peak surprise value and ue_result_real['input_parameters'] to inspect the analysis settings.
Solution
print(ue_result_real['Js'].shape)
print(np.max(ue_result_real['Js']))
print(ue_result_real['input_parameters'])(401,)
2.2375653
{'pattern_hash': [3], 'bin_size': array(5.) * ms, 'win_size': array(100.) * ms, 'win_step': array(5.) * ms, 'method': 'analytic_TrialAverage', 't_start': array(0.) * ms, 't_stop': array(2100.) * ms, 'n_surrogates': 100}Exercise: Rerun jointJ_window_analysis on the real data with win_size=50*pq.ms and plot. How does the temporal spread of the significant events change compared to the 100 ms window?
Solution
ue_result_50 = ue.jointJ_window_analysis(
spiketrains_real, bin_size=5 * pq.ms, win_size=50 * pq.ms,
method="analytic_TrialAverage"
)
plot_ue(spiketrains_real, ue_result_50, significance_level=0.05,
unit_real_ids=(14, 15), events=events)Exercise: Rerun with win_size=250*pq.ms and plot. How do the surprise values in the coincidence windows compare to the win_size=50*pq.ms case?
Solution
ue_result_250 = ue.jointJ_window_analysis(
spiketrains_real, bin_size=5 * pq.ms, win_size=250 * pq.ms,
method="analytic_TrialAverage"
)
plot_ue(spiketrains_real, ue_result_250, significance_level=0.05,
unit_real_ids=(14, 15), events=events)Exercise: Based on the three window sizes you have tested (50, 100, 250 ms), which window size gives the highest surprise values in the expected coincidence windows? Print np.max(ue_result_real['Js']) for each result and compare. What does this suggest about the temporal precision of the synchrony in this dataset?
Solution
print("win_size=50ms:", np.max(ue_result_50['Js']))
print("win_size=100ms:", np.max(ue_result_real['Js']))
print("win_size=250ms:", np.max(ue_result_250['Js']))win_size=50ms: 1.5370407
win_size=100ms: 2.2375653
win_size=250ms: 3.3351748Section 2: Population Spiking and the Complexity Distribution
The complexity distribution counts how often exactly neurons fire synchronously in a time bin. For independent neurons, this distribution is governed by the individual firing rates. Correlations produce excess synchrony at high complexities. By comparing the complexity distribution of the data to an independent surrogate, we can detect higher order correlations without specifying which neurons are involved.
The Compound Poisson Process (CPP) is a model that generates correlated spike trains: a Poisson process fires an event that is copied to neurons simultaneously (with probability given by the amplitude distribution). This makes it ideal for simulating population synchrony with controlled structure.
| Code | Description |
|---|---|
sts = compound_poisson_process(rate, amplitude_distribution, t_stop) |
Generate spike trains from a CPP; length of amplitude_distribution minus 1 is the number of neurons |
complexity = stats.Complexity(sts, bin_size=1*pq.ms, binary=True) |
Compute the complexity object |
complexity.pdf().flatten().magnitude |
Get the normalised complexity distribution |
complexity.time_histogram |
Get the population spike count over time |
Exercises
Example: Generate 100 spike trains from a CPP with assembly_size=20 and a 5% probability of a synchronous event. Plot the raster, population histogram, and both the amplitude and complexity distributions.
num_units = 100
rate = 3 * pq.Hz
t_stop = 10 * pq.s
assembly_size = 20
p_coinc = 0.05
jitter = 0 * pq.ms
amp_dist = np.zeros(num_units + 1)
amp_dist[1] = 1. - p_coinc
amp_dist[assembly_size] = p_coinc
sts = compound_poisson_process(rate=rate, amplitude_distribution=amp_dist,
t_stop=t_stop, shift=jitter)
complexity = stats.Complexity(sts, bin_size=1 * pq.ms, binary=True)
dist_cpp = utils.compute_complexity_distribution(sts, 1 * pq.ms)
fig, axes = plt.subplots(2, 2, figsize=(12, 6))
for i, st in enumerate(sts):
axes[0, 0].plot(st.rescale("ms").magnitude, [i] * len(st), ".k", ms=1)
axes[0, 0].set_xlabel("Time (ms)")
axes[0, 0].set_ylabel("Neuron")
axes[0, 0].set_title("Raster")
pophist = complexity.time_histogram
axes[1, 0].plot(pophist.times.rescale("ms"), pophist[:, 0])
axes[1, 0].set_xlabel("Time (ms)")
axes[1, 0].set_ylabel("Count")
axes[1, 0].set_title("Population histogram")
axes[0, 1].bar(np.arange(len(amp_dist)), amp_dist)
axes[0, 1].set_xlabel("Complexity k")
axes[0, 1].set_ylabel("Probability")
axes[0, 1].set_title("Amplitude distribution")
axes[1, 1].plot(np.arange(num_units), dist_cpp, ".-")
axes[1, 1].set_xlabel("Complexity k")
axes[1, 1].set_ylabel("Probability")
axes[1, 1].set_title("Complexity distribution")
plt.tight_layout()Exercise: Change assembly_size to 5 and rerun. Where does the peak of the complexity distribution shift? What does this tell you about the order of synchrony in the data?
Solution
assembly_size = 5
amp_dist = np.zeros(num_units + 1)
amp_dist[1] = 1. - p_coinc
amp_dist[assembly_size] = p_coinc
sts = compound_poisson_process(rate=rate, amplitude_distribution=amp_dist,
t_stop=t_stop, shift=jitter)
dist_cpp = utils.compute_complexity_distribution(sts, 1 * pq.ms)
plt.plot(np.arange(num_units), dist_cpp, ".-")
plt.xlabel("Complexity k")
plt.ylabel("Probability")
plt.title(f"Complexity distribution (assembly_size={assembly_size})")Exercise: Set assembly_size=2 (pairwise synchrony) and p_coinc=0.10. Rerun the CPP generation and the surrogate comparison. Where does the excess synchrony peak in the difference distribution? Is it harder or easier to detect than assembly_size=20?
Solution
assembly_size = 2
p_coinc = 0.10
amp_dist = np.zeros(num_units + 1)
amp_dist[1] = 1. - p_coinc
amp_dist[assembly_size] = p_coinc
sts = compound_poisson_process(rate=rate, amplitude_distribution=amp_dist,
t_stop=t_stop, shift=jitter)
dist_cpp = utils.compute_complexity_distribution(sts, 1 * pq.ms)
surr_sts = [surr.surrogates(st, n_surrogates=1, method="randomise_spikes")[0] for st in sts]
dist_surr = utils.compute_complexity_distribution(surr_sts, 1 * pq.ms)
dist_diff = dist_cpp - dist_surr
plt.plot(np.arange(num_units), dist_diff)
plt.axhline(0, color="k", lw=0.5)
plt.xlabel("Complexity k")
plt.ylabel("Difference")
plt.title(f"CPP minus surrogate (assembly_size={assembly_size}, p_coinc={p_coinc})")Exercise: Reset assembly_size=20, p_coinc=0.05, jitter=0*pq.ms. Change rate=10*pq.Hz (higher firing rate) and regenerate the CPP. Compute the surrogate and difference distributions. Does increasing the firing rate make the assembly synchrony easier or harder to detect?
Solution
assembly_size = 20
p_coinc = 0.05
jitter = 0 * pq.ms
rate = 10 * pq.Hz
amp_dist = np.zeros(num_units + 1)
amp_dist[1] = 1. - p_coinc
amp_dist[assembly_size] = p_coinc
sts = compound_poisson_process(rate=rate, amplitude_distribution=amp_dist,
t_stop=t_stop, shift=jitter)
dist_cpp = utils.compute_complexity_distribution(sts, 1 * pq.ms)
surr_sts = [surr.surrogates(st, n_surrogates=1, method="randomise_spikes")[0] for st in sts]
dist_surr = utils.compute_complexity_distribution(surr_sts, 1 * pq.ms)
dist_diff = dist_cpp - dist_surr
plt.plot(np.arange(num_units), dist_diff)
plt.axhline(0, color="k", lw=0.5)
plt.xlabel("Complexity k")
plt.ylabel("Difference")
plt.title(f"CPP minus surrogate (rate={rate})")Example: Generate independent surrogate spike trains by randomising spike times and compare the complexity distributions of the CPP and the surrogates.
surr_sts = [surr.surrogates(st, n_surrogates=1, method="randomise_spikes")[0] for st in sts]
dist_surr = utils.compute_complexity_distribution(surr_sts, 1 * pq.ms)
dist_diff = dist_cpp - dist_surr
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].plot(np.arange(num_units), dist_cpp, label="CPP")
axes[0].plot(np.arange(num_units), dist_surr, label="surrogate")
axes[0].set_xlabel("Complexity k")
axes[0].set_ylabel("Probability")
axes[0].set_title("Complexity distributions")
axes[0].legend()
axes[1].plot(np.arange(num_units), dist_diff)
axes[1].axhline(0, color="k", lw=0.5)
axes[1].set_xlabel("Complexity k")
axes[1].set_ylabel("Difference")
axes[1].set_title("CPP minus surrogate")
plt.tight_layout()Exercise: Reset assembly_size=20 and change p_coinc to 0.20. Rerun the CPP generation and the surrogate comparison. Is the excess synchrony more clearly visible in the difference plot?
Solution
assembly_size = 20
p_coinc = 0.20
rate = 3 * pq.Hz
amp_dist = np.zeros(num_units + 1)
amp_dist[1] = 1. - p_coinc
amp_dist[assembly_size] = p_coinc
sts = compound_poisson_process(rate=rate, amplitude_distribution=amp_dist,
t_stop=t_stop, shift=jitter)
dist_cpp = utils.compute_complexity_distribution(sts, 1 * pq.ms)
surr_sts = [surr.surrogates(st, n_surrogates=1, method="randomise_spikes")[0] for st in sts]
dist_surr = utils.compute_complexity_distribution(surr_sts, 1 * pq.ms)
dist_diff = dist_cpp - dist_surr
plt.plot(np.arange(num_units), dist_diff)
plt.axhline(0, color="k", lw=0.5)
plt.xlabel("Complexity k")
plt.ylabel("Difference")
plt.title(f"CPP minus surrogate (p_coinc={p_coinc})")Exercise: Set p_coinc=0.05, assembly_size=20, and jitter=5*pq.ms. Regenerate the CPP with compound_poisson_process and compute a surrogate with surr.surrogates. Recompute dist_cpp, dist_surr, and dist_diff = dist_cpp - dist_surr using bin_size=1*pq.ms. Plot dist_diff. Is the excess synchrony at complexity k=assembly_size still visible?
Solution
p_coinc = 0.05
assembly_size = 20
jitter = 5 * pq.ms
amp_dist = np.zeros(num_units + 1)
amp_dist[1] = 1. - p_coinc
amp_dist[assembly_size] = p_coinc
sts = compound_poisson_process(rate=3 * pq.Hz, amplitude_distribution=amp_dist,
t_stop=t_stop, shift=jitter)
dist_cpp = utils.compute_complexity_distribution(sts, 1 * pq.ms)
surr_sts = [surr.surrogates(st, n_surrogates=1, method="randomise_spikes")[0] for st in sts]
dist_surr = utils.compute_complexity_distribution(surr_sts, 1 * pq.ms)
dist_diff = dist_cpp - dist_surr
plt.plot(np.arange(num_units), dist_diff)
plt.axhline(0, color="k", lw=0.5)
plt.xlabel("Complexity k")
plt.ylabel("Difference")
plt.title(f"CPP minus surrogate (jitter={jitter})")Exercise: Recompute dist_cpp, dist_surr, and dist_diff using bin_size=10*pq.ms in both calls to utils.compute_complexity_distribution. Is the excess synchrony now detectable? What does this tell you about the relationship between bin size and the temporal precision of synchrony detection?
Solution
dist_cpp = utils.compute_complexity_distribution(sts, 10 * pq.ms)
surr_sts = [surr.surrogates(st, n_surrogates=1, method="randomise_spikes")[0] for st in sts]
dist_surr = utils.compute_complexity_distribution(surr_sts, 10 * pq.ms)
dist_diff = dist_cpp - dist_surr
plt.plot(np.arange(num_units), dist_diff)
plt.axhline(0, color="k", lw=0.5)
plt.xlabel("Complexity k")
plt.ylabel("Difference")
plt.title("CPP minus surrogate (bin_size=10ms, jitter=5ms)")Exercise: Reset p_coinc=0.05, assembly_size=20, jitter=0*pq.ms. Compute the complexity difference distribution with bin_size=1*pq.ms and then with bin_size=20*pq.ms. At what bin size does the excess synchrony disappear? What does this tell you about the temporal width of the coincidences?
Solution
p_coinc = 0.05
assembly_size = 20
jitter = 0 * pq.ms
amp_dist = np.zeros(num_units + 1)
amp_dist[1] = 1. - p_coinc
amp_dist[assembly_size] = p_coinc
sts = compound_poisson_process(rate=3 * pq.Hz, amplitude_distribution=amp_dist,
t_stop=t_stop, shift=jitter)
for bs in [1 * pq.ms, 20 * pq.ms]:
dist_cpp = utils.compute_complexity_distribution(sts, bs)
surr_sts = [surr.surrogates(st, n_surrogates=1, method="randomise_spikes")[0] for st in sts]
dist_surr = utils.compute_complexity_distribution(surr_sts, bs)
dist_diff = dist_cpp - dist_surr
plt.figure()
plt.plot(np.arange(num_units), dist_diff)
plt.axhline(0, color="k", lw=0.5)
plt.xlabel("Complexity k")
plt.ylabel("Difference")
plt.title(f"CPP minus surrogate (bin_size={bs})")Section 3: SPADE: Detecting Spike Patterns
SPADE (Spike PAttern Detection and Evaluation) finds recurring synchronous patterns in massively parallel spike trains. It mines the data for frequently co-occurring spikes (frequent itemset mining) and then tests their significance using surrogate data. Each detected pattern has a signature (n_neurons, n_occurrences). SPADE can handle hundreds of neurons and find patterns that occur across any subset of them.
| Code | Description |
|---|---|
results = spade(sts, binsize, winlen) |
Run SPADE; winlen is the pattern window length in bins |
results = spade(sts, binsize, winlen, n_surr=100) |
Run SPADE with 100 surrogates for significance testing |
results = spade(sts, binsize, winlen, min_occ=3, min_neu=3) |
Only report patterns with ≥3 occurrences and ≥3 neurons |
results = spade(sts, binsize, winlen, n_surr=100, psr_param=[0, 1, 0]) |
Run SPADE with pattern set reduction; removes patterns that are subsets of a larger significant pattern |
patterns = results["patterns"] |
List of detected patterns |
[p["signature"] for p in patterns] |
Get the (n_neurons, n_occurrences) signature of every pattern |
np.stack(sigs) |
Stack a list of signatures into a (n_patterns, 2) array |
plot_patterns(sts, patterns[0]) |
Plot spike trains and highlight the first pattern |
[p for p in patterns if p["pvalue"] < 0.05] |
Filter significant patterns by p-value |
Exercises
Example: Generate a CPP with an assembly of 10 neurons embedded in 90 independent neurons. Apply SPADE and print the first detected pattern.
assembly_size = 10
p_coinc = 0.05
amp_dist_sip = np.zeros(assembly_size + 1)
amp_dist_sip[1] = 1. - p_coinc
amp_dist_sip[assembly_size] = p_coinc
sts_spade = compound_poisson_process(rate=3 * pq.Hz, amplitude_distribution=amp_dist_sip,
t_stop=15 * pq.s)
for _ in range(90):
sts_spade.append(StationaryPoissonProcess(rate=3 * pq.Hz, t_stop=15 * pq.s).generate_spiketrain())
print(f"Total neurons: {len(sts_spade)}")
results = spade(sts_spade, binsize=5 * pq.ms, winlen=1)
patterns = results["patterns"]
print(f"Patterns detected: {len(patterns)}")
patterns[0]Total neurons: 100
Time for data mining: 0.09879827499389648
Patterns detected: 744{'itemset': (np.int64(41), np.int64(78)),
'windows_ids': (np.int64(2800), np.int64(2937)),
'neurons': [np.int64(41), np.int64(78)],
'lags': array([0.]) * ms,
'times': array([14000., 14685.]) * ms,
'signature': (2, 2),
'pvalue': -1}Exercise: Get the "signature" field from each pattern p in patterns. Collect them in a list called sigs and print the first five entries. What do the two numbers in each signature represent?
Solution
sigs = [p["signature"] for p in patterns]
print(sigs[:5])[(2, 2), (2, 2), (2, 2), (2, 3), (2, 2)]Exercise: Stack the list with np.stack(sigs) to form a 2D array sig. Print its shape. Which column gives the number of neurons in the pattern and which gives the number of occurrences?
Solution
sig = np.stack(sigs)
print(sig.shape)(744, 2)Exercise: Use np.argmax(sig[:, 0]) to find the index of the pattern with the most neurons and np.argmax(sig[:, 1]) for the most frequently occurring pattern. Print both patterns.
Solution
print(patterns[np.argmax(sig[:, 0])])
print(patterns[np.argmax(sig[:, 1])]){'itemset': (np.int64(5), np.int64(7), np.int64(6), np.int64(0), np.int64(9), np.int64(2), np.int64(3), np.int64(4), np.int64(8), np.int64(21), np.int64(1)), 'windows_ids': (np.int64(1517), np.int64(2032)), 'neurons': [np.int64(5), np.int64(7), np.int64(6), np.int64(0), np.int64(9), np.int64(2), np.int64(3), np.int64(4), np.int64(8), np.int64(21), np.int64(1)], 'lags': array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) * ms, 'times': array([ 7585., 10160.]) * ms, 'signature': (11, 2), 'pvalue': -1}
{'itemset': (np.int64(7), np.int64(6)), 'windows_ids': (np.int64(74), np.int64(293), np.int64(319), np.int64(414), np.int64(997), np.int64(1517), np.int64(1782), np.int64(1793), np.int64(1892), np.int64(1934), np.int64(1947), np.int64(2032), np.int64(2290), np.int64(2853)), 'neurons': [np.int64(7), np.int64(6)], 'lags': array([0.]) * ms, 'times': array([ 370., 1465., 1595., 2070., 4985., 7585., 8910., 8965.,
9460., 9670., 9735., 10160., 11450., 14265.]) * ms, 'signature': (2, 14), 'pvalue': -1}Exercise: Rerun spade() with min_occ=3 and min_neu=3 to discard rare or small patterns. How many patterns remain? Print the signature of the most frequent remaining pattern.
Solution
results_filt = spade(sts_spade, binsize=5 * pq.ms, winlen=1, min_occ=3, min_neu=3)
patterns_filt = results_filt["patterns"]
print(f"Patterns remaining: {len(patterns_filt)}")
sigs_filt = [p["signature"] for p in patterns_filt]
sig_filt = np.stack(sigs_filt)
print(patterns_filt[np.argmax(sig_filt[:, 1])]["signature"])Time for data mining: 0.0629730224609375
Patterns remaining: 1
(10, 12)Exercise: Run spade() with n_surr=100. Assign the result to results_sig. Print results_sig["pvalue_spectrum"].
Solution
results_sig = spade(sts_spade, binsize=5 * pq.ms, winlen=1, n_surr=100)
print(results_sig["pvalue_spectrum"])Time for data mining: 0.07610344886779785
Time for pvalue spectrum computation: 12.285647869110107
[[2, np.uint16(2), np.float64(1.0)], [2, np.uint16(3), np.float64(1.0)], [2, np.uint16(4), np.float64(1.0)], [2, np.uint16(5), np.float64(1.0)], [2, np.uint16(6), np.float64(0.82)], [2, np.uint16(7), np.float64(0.18)], [2, np.uint16(8), np.float64(0.03)], [2, np.uint16(9), np.float64(0.01)], [3, np.uint16(2), np.float64(1.0)], [3, np.uint16(3), np.float64(0.4)], [3, np.uint16(4), np.float64(0.04)], [4, np.uint16(2), np.float64(0.32)], [4, np.uint16(3), np.float64(0.01)], [5, np.uint16(2), np.float64(0.02)]]Exercise: From results_sig["patterns"], keep only patterns where p["pvalue"] < 0.05. Store them in sig_patterns and print how many were found.
Solution
sig_patterns = [p for p in results_sig["patterns"] if p["pvalue"] < 0.05]
print(f"Significant patterns: {len(sig_patterns)}")Significant patterns: 10Exercise: For each significant pattern in sig_patterns, print p['neurons'] to see which neuron IDs participate. Do the detected neurons correspond to the assembly you injected? Print the expected assembly neuron IDs by checking which spike trains in sts_spade came from the CPP: list(range(assembly_size)).
Solution
for p in sig_patterns:
print(p['neurons'])
print("Expected assembly:", list(range(assembly_size)))[np.int64(5), np.int64(7), np.int64(6), np.int64(0), np.int64(9), np.int64(2), np.int64(3), np.int64(4), np.int64(8), np.int64(21), np.int64(1)]
[np.int64(5), np.int64(7), np.int64(6), np.int64(0), np.int64(9), np.int64(2), np.int64(3), np.int64(4), np.int64(8), np.int64(1)]
[np.int64(5), np.int64(0)]
[np.int64(5), np.int64(2)]
[np.int64(7), np.int64(6)]
[np.int64(6), np.int64(8)]
[np.int64(0), np.int64(4)]
[np.int64(9), np.int64(1)]
[np.int64(2), np.int64(1)]
[np.int64(3), np.int64(4)]
Expected assembly: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]Exercise: Plot the significant patterns using plot_patterns(sts_spade, sig_patterns).
Solution
plot_patterns(sts_spade, sig_patterns)Exercise: Rerun spade() with n_surr=100 and psr_param=[0, 1, 0] (pattern set reduction). How many significant patterns remain after removing patterns that are subsets of a larger detected pattern?
Solution
results_psr = spade(sts_spade, binsize=5 * pq.ms, winlen=1, n_surr=100, psr_param=[0, 1, 0])
sig_patterns_psr = [p for p in results_psr["patterns"] if p["pvalue"] < 0.05]
print(f"Significant patterns after PSR: {len(sig_patterns_psr)}")Time for data mining: 0.07648754119873047
Time for pvalue spectrum computation: 12.020894050598145