def make_bursts(n_seconds, fs, is_oscillating, cycle): """Create a bursting time series by tiling when oscillations occur. Parameters ---------- n_seconds : float Simulation time, in seconds. fs : float Sampling rate of simulated signal, in Hz. is_oscillating : 1d array of bool Definition of whether each cycle is bursting or not. cycle : 1d array The cycle to use for bursts. Returns ------- burst_sig : 1d array Simulated bursty oscillation. """ n_samples = compute_nsamples(n_seconds, fs) n_samples_cycle = len(cycle) burst_sig = np.zeros([n_samples]) for sig_ind, is_osc in zip(range(0, n_samples, n_samples_cycle), is_oscillating): # If set as an oscillating cycle, add cycle to signal # The sample check is to check there are enough samples left to add a full cycle # If there are not, this skipps the add, leaving zeros instead of adding part of a cycle if is_osc and sig_ind + n_samples_cycle < n_samples: burst_sig[sig_ind:sig_ind + n_samples_cycle] = cycle return burst_sig
def sim_gaussian_cycle(n_seconds, fs, std, center=.5): """Simulate a cycle of a gaussian. Parameters ---------- n_seconds : float Length of cycle window in seconds. fs : float Sampling frequency of the cycle simulation. std : float Standard deviation of the gaussian kernel, in seconds. center : float, optional, default: 0.5 The center of the gaussian. Returns ------- cycle : 1d array Simulated gaussian cycle. Examples -------- Simulate a cycle of a gaussian wave: >>> cycle = sim_gaussian_cycle(n_seconds=0.2, fs=500, std=0.025) """ xs = np.linspace(0, 1, compute_nsamples(n_seconds, fs)) cycle = np.exp(-(xs - center)**2 / (2 * std**2)) return cycle
def sim_knee(n_seconds, fs, chi1, chi2, knee): """Simulate a signal whose power spectrum has a 1/f structure with a knee. Parameters ---------- n_seconds : float Simulation time, in seconds. fs : float Sampling rate of simulated signal, in Hz. chi1 : float Power law exponent before the knee. chi2 : float Power law exponent added to chi1 after the knee. knee : float Location of the knee in Hz. Returns ------- sig : 1d array Time series with the desired power spectrum. Notes ----- This simulated time series has a power spectrum that follows the Lorentzian equation: `P(f) = 1 / (f**chi1 * (f**chi2 + knee))` - This simulation creates this power spectrum shape using a sum of sinusoids. - The slope of the log power spectrum before the knee is chi1 whereas after the knee it is chi2, but only when the sign of chi1 and chi2 are the same. Examples -------- Simulate a time series with chi1 of -1, chi2 of -2, and knee of 100: >> sim_knee(n_seconds=10, fs=1000, chi1=-1, chi2=-2, knee=100) """ times = create_times(n_seconds, fs) n_samples = compute_nsamples(n_seconds, fs) # Create frequencies for the power spectrum, which will be freqs of the summed cosines freqs = np.linspace(0, fs / 2, num=int(n_samples // 2 + 1), endpoint=True) # Drop the DC component freqs = freqs[1:] # Map the frequencies under the (square root) Lorentzian # This will give us the amplitude coefficients for the sinusoids cosine_coeffs = np.array([np.sqrt(1 / (freq ** -chi1 * (freq ** (-chi2 - chi1) + knee))) \ for freq in freqs]) # Add sinusoids with a random phase shift sig = np.sum(np.array([cosine_coeffs[ell] * \ np.cos(2 * np.pi * freq * times + 2 * np.pi * np.random.rand()) \ for ell, freq in enumerate(freqs)]), axis=0) return sig
def check_sim_output(sig, n_seconds=None, fs=None): """Helper function to check some basic properties of simulated signals.""" n_seconds = N_SECONDS if not n_seconds else n_seconds fs = FS if not fs else fs exp_n_samples = compute_nsamples(n_seconds, fs) assert isinstance(sig, np.ndarray) assert len(sig) == exp_n_samples assert sum(np.isnan(sig)) == 0
def sim_poisson_pop(n_seconds, fs, n_neurons=1000, firing_rate=2): """Simulate a Poisson population. Parameters ---------- n_seconds : float Simulation time, in seconds. fs : float Sampling rate of simulated signal, in Hz. n_neurons : int, optional, default: 1000 Number of neurons in the simulated population. firing_rate : float, optional, default: 2 Firing rate of individual neurons in the population. Returns ------- sig : 1d array Simulated population activity. Notes ----- The simulated signal is essentially white noise, but satisfies the Poisson property, i.e. mean(X) = var(X). The lambda parameter of the Poisson process (total rate) is determined as firing rate * number of neurons, i.e. summation of Poisson processes is still a Poisson processes. Note that the Gaussian approximation for a sum of Poisson processes is only a good approximation for large lambdas. Examples -------- Simulate a Poisson population: >>> sig = sim_poisson_pop(n_seconds=1, fs=500, n_neurons=1000, firing_rate=2) """ # Poisson population rate signal scales with # of neurons and individual rate lam = n_neurons * firing_rate # Variance is equal to the mean sig = np.random.normal(loc=lam, scale=lam**0.5, size=compute_nsamples(n_seconds, fs)) # Enforce that sig is non-negative in cases of low firing rate sig[np.where(sig < 0.)] = 0. return sig
def sim_skewed_gaussian_cycle(n_seconds, fs, center, std, alpha, height=1): """Simulate a cycle of a skewed gaussian. Parameters ---------- n_seconds : float Length of cycle window in seconds. fs : float Sampling frequency of the cycle simulation. center : float The center of the skewed gaussian. std : float Standard deviation of the gaussian kernel, in seconds. alpha : float Magnitude and direction of the skew. height : float, optional, default: 1. Maximum value of the cycle. Returns ------- cycle : 1d array Output values for skewed gaussian function. """ n_samples = compute_nsamples(n_seconds, fs) # Gaussian distribution cycle = sim_gaussian_cycle(n_seconds, fs, std, center) # Skewed cumulative distribution function. # Assumes time are centered around 0. Adjust to center around non-zero. times = np.linspace(-1, 1, n_samples) cdf = norm.cdf(alpha * ((times - ((center * 2) - 1)) / std)) # Skew the gaussian cycle = cycle * cdf # Rescale height cycle = (cycle / np.max(cycle)) * height return cycle
def sim_action_potential(n_seconds, fs, centers, stds, alphas, heights): """Simulate an action potential as the sum of skewed gaussians. Parameters ---------- n_seconds : float Length of cycle window in seconds. fs : float Sampling frequency of the cycle simulation. centers : array-like or float Times where the peak occurs in the pre-skewed gaussian. stds : array-like or float Standard deviations of the gaussian kernels, in seconds. alphas : array-like or float Magnitiude and direction of the skew. heights : array-like or float Maximum value of the cycles. Returns ------- cycle : 1d array Simulated spike cycle. """ # Prevent circular import from neurodsp.sim.cycles import sim_skewed_gaussian_cycle # Determine number of parameters and repeat if necessary params = [] n_params = [] for param in [centers, stds, alphas, heights]: if isinstance(param, (tuple, list, np.ndarray)): n_params.append(len(param)) else: param = repeat(param) params.append(param) # Parameter checking if len(n_params) != 0 and len(set(n_params)) != 1: raise ValueError('Unequal lengths between two or more of {centers, stds, alphas, heights}.') # Simulate elif len(n_params) == 0: # Single gaussian cycle = sim_skewed_gaussian_cycle(n_seconds, fs, centers, stds, alphas, heights) else: # Multiple gaussians cycle = np.zeros((n_params[0], compute_nsamples(n_seconds, fs))) for idx, (center, std, alpha, height) in enumerate(zip(*params)): cycle[idx] = sim_skewed_gaussian_cycle(n_seconds, fs, center, std, alpha, height) cycle = np.sum(cycle, axis=0) return cycle
def sim_synaptic_current(n_seconds, fs, n_neurons=1000, firing_rate=2., tau_r=0., tau_d=0.01, t_ker=None): """Simulate a signal as a synaptic current, which has 1/f characteristics with a knee. Parameters ---------- n_seconds : float Simulation time, in seconds. fs : float Sampling rate of simulated signal, in Hz. n_neurons : int, optional, default: 1000 Number of neurons in the simulated population. firing_rate : float, optional, default: 2 Firing rate of individual neurons in the population. tau_r : float, optional, default: 0. Rise time of synaptic kernel, in seconds. tau_d : float, optional, default: 0.01 Decay time of synaptic kernel, in seconds. t_ker : float, optional Length of time of the simulated synaptic kernel, in seconds. Returns ------- sig : 1d array Simulated synaptic current. Notes ----- - This simulation is based on the one used in [1]_. - The resulting signal is most similar to unsigned intracellular current or conductance change. References ---------- .. [1] Gao, R., Peterson, E. J., & Voytek, B. (2017). Inferring synaptic excitation/inhibition balance from field potentials. NeuroImage, 158, 70–78. DOI: https://doi.org/10.1016/j.neuroimage.2017.06.078 Examples -------- Simulate a synaptic current signal: >>> sig = sim_synaptic_current(n_seconds=1, fs=500) """ # If not provided, compute t_ker as a function of decay time constant if t_ker is None: t_ker = 5. * tau_d # Simulate an extra bit because the convolution will trim & turn off normalization sig = sim_poisson_pop((n_seconds + t_ker), fs, n_neurons, firing_rate, mean=None, variance=None) ker = sim_synaptic_kernel(t_ker, fs, tau_r, tau_d) sig = np.convolve(sig, ker, 'valid')[:compute_nsamples(n_seconds, fs)] return sig
def sim_frac_gaussian_noise(n_seconds, fs, chi=0, hurst=None): """Simulate a timeseries as fractional gaussian noise. Parameters ---------- n_seconds : float Simulation time, in seconds. fs : float Sampling rate of simulated signal, in Hz. chi: float, optional, default: 0 Desired power law exponent of the spectrum of the signal. Must be in the range (-1, 1). hurst : float, optional, default: None Desired Hurst parameter, which must be in the range (0, 1). If provided, this value overwrites the `chi` parameter. Returns ------- sig: 1d array Simulated fractional gaussian noise time series. Notes ----- The time series can be specified with either a desired power law exponent, or alternatively with a specified Hurst parameter. The Hurst parameter is not the Hurst exponent as defined in rescaled range analysis. The Hurst parameter is defined for self-similar processes such that Y(at) = a^H Y(t) for all a > 0, where this equality holds in distribution. The relationship between the power law exponent chi and the Hurst parameter for fractional gaussian noise is chi = 2 * hurst - 1. For more information, consult [1]_. References ---------- .. [1] Eke, A., Herman, P., Kocsis, L., & Kozak, L. R. (2002). Fractal characterization of complexity in temporal physiological signals. Physiological Measurement, 23(1), R1–R38. DOI: https://doi.org/10.1088/0967-3334/23/1/201 Examples -------- Simulate fractional gaussian noise with a power law decay of 0 (white noise): >>> sig = sim_frac_gaussian_noise(n_seconds=1, fs=500, chi=0) Simulate fractional gaussian noise with a Hurst parameter of 0.5 (also white noise): >>> sig = sim_frac_gaussian_noise(n_seconds=1, fs=500, hurst=0.5) """ if hurst is not None: check_param_range(hurst, 'hurst', (0, 1)) else: check_param_range(chi, 'chi', (-1, 1)) # Infer the hurst parameter from chi hurst = (-chi + 1.) / 2 # Compute the number of samples for the simulated time series n_samples = compute_nsamples(n_seconds, fs) # Define helper function for computing the auto-covariance def autocov(hurst): return lambda k: 0.5 * (np.abs(k - 1) ** (2 * hurst) - 2 * \ k ** (2 * hurst) + (k + 1) ** (2 * hurst)) # Build the autocovariance matrix gamma = np.arange(0, n_samples) gamma = np.apply_along_axis(autocov(hurst), 0, gamma) autocov_matrix = toeplitz(gamma) # Use the Cholesky factor to transform white noise to get the desired time series white_noise = np.random.randn(n_samples) cholesky_factor = cholesky(autocov_matrix, lower=True) sig = cholesky_factor @ white_noise return sig
def sim_powerlaw(n_seconds, fs, exponent=-2.0, f_range=None, **filter_kwargs): """Simulate a power law time series, with a specified exponent. Parameters ---------- n_seconds : float Simulation time, in seconds. fs : float Sampling rate of simulated signal, in Hz. exponent : float, optional, default: -2 Desired power-law exponent, of the form P(f)=f^exponent. f_range : list of [float, float] or None, optional Frequency range to filter simulated data, as [f_lo, f_hi], in Hz. **filter_kwargs : kwargs, optional Keyword arguments to pass to `filter_signal`. Returns ------- sig : 1d array Time-series with the desired power law exponent. Notes ----- - Powerlaw data with exponents is created by spectrally rotating white noise [1]_. References ---------- .. [1] Timmer, J., & Konig, M. (1995). On Generating Power Law Noise. Astronomy and Astrophysics, 300, 707–710. Examples -------- Simulate a power law signal, with an exponent of -2 (brown noise): >>> sig = sim_powerlaw(n_seconds=1, fs=500, exponent=-2.0) Simulate a power law signal, with a highpass filter applied at 2 Hz: >>> sig = sim_powerlaw(n_seconds=1, fs=500, exponent=-1.5, f_range=(2, None)) """ # Compute the number of samples for the simulated time series n_samples = compute_nsamples(n_seconds, fs) # Get the number of samples to simulate for the signal # If signal is to be filtered, with FIR, add extra to compensate for edges if f_range and filter_kwargs.get('filter_type', None) != 'iir': pass_type = infer_passtype(f_range) filt_len = compute_filter_length( fs, pass_type, *check_filter_definition(pass_type, f_range), n_seconds=filter_kwargs.get('n_seconds', None), n_cycles=filter_kwargs.get('n_cycles', 3)) n_samples += filt_len + 1 # Simulate the powerlaw data sig = _create_powerlaw(n_samples, fs, exponent) if f_range is not None: sig = filter_signal(sig, fs, infer_passtype(f_range), f_range, remove_edges=True, **filter_kwargs) # Drop the edges, that were compensated for, if not using FIR filter if not filter_kwargs.get('filter_type', None) == 'iir': sig, _ = remove_nans(sig) return sig
def sim_variable_oscillation(n_seconds, fs, freqs, cycle='sine', phase=0, **cycle_params): """Simulate an oscillation that varies in frequency and cycle parameters. Parameters ---------- n_seconds : float or None Simulation time, in seconds. If None, the simulation time is based on `freqs` and the length of `cycle_params`. If a float, the signal may be truncated or contain trailing zeros if not exact. fs : float Signal sampling rate, in Hz. freqs : float or list Oscillation frequencies. cycle : {'sine', 'asine', 'sawtooth', 'gaussian', 'exp', '2exp'} or callable Type of oscillation cycle to simulate. See `sim_cycle` for details on cycle types and parameters. phase : float or {'min', 'max'}, optional, default: 0 If non-zero, applies a phase shift to the oscillation by rotating the cycle. If a float, the shift is defined as a relative proportion of cycle, between [0, 1]. If 'min' or 'max', the cycle is shifted to start at it's minima or maxima. **cycle_params Parameter floats or variable lists for each cycle. Returns ------- sig : 1d array Simulated bursty oscillation. Examples -------- Simulate one second of an oscillation with a varying center frequency: >>> freqs = np.tile([10, 11, 10, 9], 5) >>> sig = sim_variable_oscillation(1, 1000, freqs) Simulate an oscillation with varying frequency and parameters for a given number of cycles: >>> freqs = [ 5, 10, 15, 20] >>> rdsyms = [.2, .4, .6, .8] >>> sig = sim_variable_oscillation(None, 1000, freqs, cycle='asine', rdsym=rdsyms) """ # Ensure param lists are the same length param_keys = cycle_params.keys() param_values = list(cycle_params.values()) param_lengths = np.array([ len(params) for params in param_values if isinstance(params, (list, np.ndarray)) ]) # Determine the number of cycles if isinstance(freqs, (np.ndarray, list)): n_cycles = len(freqs) elif len(param_lengths) > 0: n_cycles = param_lengths[0] else: n_cycles = 1 # Ensure freqs is iterable and an array freqs = np.array([freqs] * n_cycles) if isinstance(freqs, (int, float)) else freqs freqs = np.array(freqs) if not isinstance(freqs, np.ndarray) else freqs # Ensure lengths of variable params are equal if ~(param_lengths == len(freqs)).all(): raise ValueError( 'Length of cycle_params lists and freqs must be equal.') # Ensure all kwargs params are iterable for idx, param in enumerate(param_values): if not isinstance(param, (list, np.ndarray)): param_values[idx] = [param] * n_cycles param_values = np.array(param_values).transpose() # Collect params for each cycle separately cycle_params = [dict(zip(param_keys, params)) for params in param_values] cycle_params = [{}] * len(freqs) if len( cycle_params) == 0 else cycle_params # Determine start/end indices cyc_lens = [int(np.ceil(1 / freq * fs)) for freq in freqs] ends = np.cumsum(cyc_lens, dtype=int) starts = [0, *ends[:-1]] # Simulate n_samples = np.sum(cyc_lens) if n_seconds is None else compute_nsamples( n_seconds, fs) sig = np.zeros(n_samples) for freq, params, start, end in zip(freqs, cycle_params, starts, ends): if start > n_samples or end > n_samples: break n_seconds_cycle = int(np.ceil(fs / freq)) / fs sig[start:end] = sim_normalized_cycle(n_seconds_cycle, fs, cycle, phase, **params) return sig
def sim_oscillation(n_seconds, fs, freq, cycle='sine', phase=0, **cycle_params): """Simulate an oscillation. Parameters ---------- n_seconds : float Simulation time, in seconds. fs : float Signal sampling rate, in Hz. freq : float Oscillation frequency. cycle : {'sine', 'asine', 'sawtooth', 'gaussian', 'exp', '2exp', 'exp_cos', 'asym_harmonic'} or callable What type of oscillation cycle to simulate. See `sim_cycle` for details on cycle types and parameters. phase : float or {'min', 'max'}, optional, default: 0 If non-zero, applies a phase shift to the oscillation by rotating the cycle. If a float, the shift is defined as a relative proportion of cycle, between [0, 1]. If 'min' or 'max', the cycle is shifted to start at it's minima or maxima. **cycle_params Parameters for the simulated oscillation cycle. Returns ------- sig : 1d array Simulated oscillation. Examples -------- Simulate a continuous sinusoidal oscillation at 5 Hz: >>> sig = sim_oscillation(n_seconds=1, fs=500, freq=5) Simulate an asymmetric oscillation at 15 Hz, with a phase shift: >>> sig = sim_oscillation(n_seconds=1, fs=500, freq=15, ... cycle='asine', phase=0.5, rdsym=0.75) """ # Figure out how many cycles are needed for the signal n_cycles = int(np.ceil(n_seconds * freq)) # Compute the number of seconds per cycle for the requested frequency # The rounding is needed to get a value that works with the sampling rate n_seconds_cycle = int(np.ceil(fs / freq)) / fs # Create a single cycle of an oscillation, for the requested frequency cycle = sim_cycle(n_seconds_cycle, fs, cycle, phase, **cycle_params) # Tile the cycle, to create the desired oscillation sig = np.tile(cycle, n_cycles) # Truncate the length of the signal to be the number of expected samples n_samples = compute_nsamples(n_seconds, fs) sig = sig[:n_samples] return sig
def sim_asine_cycle(n_seconds, fs, rdsym, side='both'): """Simulate a cycle of an asymmetric sine wave. Parameters ---------- n_seconds : float Length of cycle window in seconds. Note that this is NOT the period of the cycle, but the length of the returned array that contains the cycle, which can be (and usually is) much shorter. fs : float Sampling frequency of the cycle simulation. rdsym : float Rise-decay symmetry of the cycle, as fraction of the period in the rise time, where: = 0.5 - symmetric (sine wave) < 0.5 - shorter rise, longer decay > 0.5 - longer rise, shorter decay side : {'both', 'peak', 'trough'} Which side of the cycle to make asymmetric. Returns ------- cycle : 1d array Simulated asymmetric cycle. Examples -------- Simulate a 2 Hz asymmetric sine cycle: >>> cycle = sim_asine_cycle(n_seconds=0.5, fs=500, rdsym=0.75) """ check_param_range(rdsym, 'rdsym', [0., 1.]) check_param_options(side, 'side', ['both', 'peak', 'trough']) # Determine number of samples n_samples = compute_nsamples(n_seconds, fs) half_sample = int(n_samples / 2) # Check for an odd number of samples (for half peaks, we need to fix this later) remainder = n_samples % 2 # Calculate number of samples rising n_rise = int(np.round(n_samples * rdsym)) n_rise1 = int(np.ceil(n_rise / 2)) n_rise2 = int(np.floor(n_rise / 2)) # Calculate number of samples decaying n_decay = n_samples - n_rise n_decay1 = half_sample - n_rise1 # Create phase definition for cycle with both extrema being asymmetric if side == 'both': phase = np.hstack([ np.linspace(0, np.pi / 2, n_rise1 + 1), np.linspace(np.pi / 2, -np.pi / 2, n_decay + 1)[1:-1], np.linspace(-np.pi / 2, 0, n_rise2 + 1)[:-1] ]) # Create phase definition for cycle with only one extrema being asymmetric elif side == 'peak': half_sample += 1 if bool(remainder) else 0 phase = np.hstack([ np.linspace(0, np.pi / 2, n_rise1 + 1), np.linspace(np.pi / 2, np.pi, n_decay1 + 1)[1:-1], np.linspace(-np.pi, 0, half_sample + 1)[:-1] ]) elif side == 'trough': half_sample -= 1 if not bool(remainder) else 0 phase = np.hstack([ np.linspace(0, np.pi, half_sample + 1)[:-1], np.linspace(-np.pi, -np.pi / 2, n_decay1 + 1), np.linspace(-np.pi / 2, 0, n_rise1 + 1)[:-1] ]) # Convert phase definition to signal cycle = np.sin(phase) return cycle