Source code for syncopy.synthdata.spikes
# -*- coding: utf-8 -*-
#
# Synthetic spike data generators for testing and tutorials
#
# Builtin/3rd party package imports
import numpy as np
# syncopy imports
from syncopy import SpikeData
from syncopy.shared.kwarg_decorators import unwrap_cfg
# ---- Synthetic SpikeData ----
[docs]@unwrap_cfg
def poisson_noise(
nTrials=10,
nSpikes=10000,
nChannels=3,
nUnits=10,
intensity=0.1,
samplerate=10000,
seed=None,
):
"""
Poisson (Shot-)noise generator
The expected trial length in samples is given by:
``nSpikes`` / (``intensity`` * ``nTrials``)
Dividing again by the ``samplerate` gives the
expected trial length in seconds.
Individual trial lengths get randomly
shortened by up to 10% of this expected length.
The trigger offsets are also
randomized between 5% and 20% of the shortest
trial length.
Lastly, the distribution of the Poisson ``intensity`` along channels and units
has uniformly randomized weights, meaning that typically
you get very active channels/units and some which are almost quiet.
Parameters
----------
nTrials : int
Number of trials
nSpikes : int
The total number of spikes over all trials to generate
nChannels : int
Number of channels
nUnits : int
Number of units
intensity : int
Expected number of spikes per sampling interval
samplerate : float
Sampling rate in Hz
seed: None or int, passed on to `np.random.default_rng`.
Set to an int to get reproducible results.
Returns
-------
sdata : :class:`~syncopy.SpikeData`
The generated spike data
Notes
-----
Originally conceived by `Alejandro Tlaie Boria https://github.com/atlaie_`
Examples
--------
With `nSpikes=20_000`, `samplerate=10_000`, `nTrials=10` and the default `intensity=0.1`
we can expect a trial length of about 2 seconds:
>>> spike_data = poisson_noise(nTrials=10, nSpikes=20_000, samplerate=10_000)
Example output of the 1st trial [start, end] in seconds:
>>> spike_data.trialintervals[0]
>>> array([-0.3004, 1.6459])
Which is close to 2 seconds.
"""
# uniform random weights
def get_rdm_weights(size, seed=seed):
rng = np.random.default_rng(seed)
pvec = rng.uniform(size=size)
return pvec / pvec.sum()
# total length of all trials combined
rng = np.random.default_rng(seed)
T_max = int(nSpikes / intensity)
spike_samples = np.sort(rng.choice(range(T_max), size=nSpikes, replace=False))
channels = rng.choice(np.arange(nChannels), p=get_rdm_weights(nChannels), size=nSpikes, replace=True)
uvec = np.arange(nUnits)
pvec = get_rdm_weights(nUnits)
units = rng.choice(uvec, p=pvec, size=nSpikes, replace=True)
# originally fixed trial size
step = T_max // nTrials
trl_intervals = np.arange(T_max + 1, step=step)
# 1st trial
idx_start = trl_intervals[:-1]
idx_end = trl_intervals[1:] - 1
# now randomize trial length a bit, max 10% size difference
idx_end = idx_end - np.r_[rng.integers(step // 10, size=nTrials - 1), 0]
shortest_trial = np.min(idx_end - idx_start)
idx_offset = -rng.choice(
np.arange(0.05 * shortest_trial, 0.2 * shortest_trial, dtype=int),
size=nTrials,
replace=True,
)
trldef = np.vstack([idx_start, idx_end, idx_offset]).T
data = np.vstack([spike_samples, channels, units]).T
sdata = SpikeData(
data=data,
trialdefinition=trldef,
dimord=["sample", "channel", "unit"],
samplerate=samplerate,
)
return sdata