def test_robust_hilbert(tsig_sine): # Generate a signal with NaNs fs, n_points, n_nans = 100, 1000, 10 sig = np.random.randn(n_points) sig[0:n_nans] = np.nan # Check has correct number of nans (not all nan), without increase_n hilb_sig = robust_hilbert(sig, False) assert sum(np.isnan(hilb_sig)) == n_nans # Check has correct number of nans (not all nan), with increase_n hilb_sig = robust_hilbert(sig, True) assert sum(np.isnan(hilb_sig)) == n_nans # Hilbert transform of sin(omega * t) = -sign(omega) * cos(omega * t) times = create_times(N_SECONDS, FS) # omega = 1 hilbert_sig = np.imag(robust_hilbert(tsig_sine)) expected_answer = np.array([-np.cos(2 * np.pi * time) for time in times]) assert np.allclose(hilbert_sig, expected_answer, atol=EPS) # omega = -1 hilbert_sig = np.imag(robust_hilbert(-tsig_sine)) expected_answer = np.array([np.cos(2 * np.pi * time) for time in times]) assert np.allclose(hilbert_sig, expected_answer, atol=EPS)
def sim_random_walk(n_seconds, fs, theta=1., mu=0., sigma=5.): """Simulate a mean-reverting random walk, as an Ornstein-Uhlenbeck process. Parameters ---------- n_seconds : float Simulation time, in seconds. fs : float Sampling rate of simulated signal, in Hz. theta : float, optional, default: 1.0 Memory scale parameter. Larger theta values create faster fluctuations. mu : float, optional, default: 0.0 Mean of the random walk. sigma : float, optional, default: 5.0 Standard deviation of the random walk. Returns ------- sig: 1d array Simulated random walk signal. Notes ----- The random walk is simulated as a discretized Ornstein-Uhlenbeck process: `dx = theta*(x-mu)*dt + sigma*dWt` Where: - mu : mean - sigma : standard deviation - theta : memory scale - dWt : increments of Wiener process, i.e. white noise References ---------- See the wikipedia page for the integral solution: https://en.wikipedia.org/wiki/Ornstein%E2%80%93Uhlenbeck_process#Formal_solution Examples -------- Simulate a Ornstein-Uhlenbeck random walk: >>> sig = sim_random_walk(n_seconds=1, fs=500, theta=1.) """ times = create_times(n_seconds, fs) x0 = mu dt = times[1] - times[0] ws = np.random.normal(size=len(times)) ex = np.exp(-theta * times) ws[0] = 0. sig = x0 * ex + mu * (1. - ex) + sigma * ex * \ np.cumsum(np.exp(theta * times) * np.sqrt(dt) * ws) return sig
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 sim_damped_oscillation(n_seconds, fs, freq, gamma, growth=None): """Simulate a damped relaxation oscillation. Parameters ---------- n_seconds : float Simulation time, in seconds. fs : float Signal sampling rate, in Hz. freq : float Oscillation frequency, in Hz. gamma : float Parametric dampening coefficient. growth : float, optional, default: None Logistic growth rate to smooth the heaviside step function. If None, a non-smoothed heaviside is used. Returns ------- sig : 1d array Simulated damped relaxation oscillation. Notes ----- - This implementation of a damped oscillation is implemented as Equation 3 of [1]_. References ---------- .. [1] Evertz, R., Hicks, D. G., & Liley, D. T. J. (2021). Alpha blocking and 1/fβ spectral scaling in resting EEG can be accounted for by a sum of damped alpha band oscillatory processes. bioRxiv. DOI: https://doi.org/10.1101/2021.08.20.457060 Examples -------- >>> sig = sim_damped_oscillation(1, 1000, 10, .1) """ times = create_times(n_seconds, fs) exp = np.exp(-1 * gamma * times) cos = np.cos(2 * np.pi * freq * times) if growth is None: logit = 1 else: # Smooth heaviside as a logit logit = 1 / (1 + np.exp(-2 * growth * times)) return exp * cos * logit
def test_phase_by_time(tsig, tsig_sine): # Check that a random signal, with a filter applied, runs & preserves shape out = phase_by_time(tsig, FS, F_RANGE) assert out.shape == tsig.shape # Check the expected answer for the test sine wave signal # The instantaneous phase of sin(t) should be piecewise linear with slope 1 phase = phase_by_time(tsig_sine, FS) # Check the first second of the signal, and create associated time axis, scaled to [0, 2pi] phase = phase[0:int(FS*1.0)] times = 2 * np.pi * create_times(1.0, FS) # Generate the expected instantaneous phase of the given signal # Phase is defined in [-pi, pi]. Since sin(t) = cos(t - pi/2), the phase should begin at # -pi/2 and increase with a slope of 1 until phase hits pi, or when t=3pi/2. Phase then # wraps around to -pi and again increases linearly with a slope of 1 expected_answer = np.array(\ [time-np.pi/2 if time <= 3*np.pi/2 else time-5*np.pi/2 for time in times]) assert np.allclose(expected_answer, phase, atol=EPS)
def test_phase_by_time(tsig, tsig_sine): # Check that a random signal, with a filter applied, runs & preserves shape out = phase_by_time(tsig, FS, (8, 12)) assert out.shape == tsig.shape # Check the expected answer for the test sine wave signal # The instantaneous phase of sin(t) should be piecewise linear with slope 1 phase = phase_by_time(tsig_sine, FS) # Create a time axis, scaled to the range of [0, 2pi] times = 2 * np.pi * create_times(N_SECONDS, FS) # Generate the expected instantaneous phase of the given signal. Phase is defined in # [-pi, pi]. Since sin(t) = cos(t - pi/2), the phase should begin at -pi/2 and increase with a slope # of 1 until phase hits pi, or when t=3pi/2. Phase then wraps around to -pi and again increases # linearly with a slope of 1. expected_answer = np.array([ time - np.pi / 2 if time <= 3 * np.pi / 2 else time - 5 * np.pi / 2 for time in times ]) assert np.allclose(expected_answer, phase, atol=EPS)
def sim_knee(n_seconds, fs, chi1, chi2, knee): """Returns a time series whose power spectrum follows the Lorentzian equation: P(f) = 1 / (f**chi1 * (f**chi2 + knee)) using a sum of sinusoids. Parameters ----------- n_seconds: float Number of seconds elapsed in the time series. fs: float Sampling rate. 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 desired power spectrum. Notes ----- 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, chi2, knee) = (-1, -2, 100) >> sim_knee(n_seconds=10, fs=10**3, chi1=-1, chi2=-2, knee=100) """ times = create_times(n_seconds, fs) sig_len = fs * n_seconds # Create the range of frequencies that appear in the power spectrum since these # will be the frequencies in the cosines we sum below freqs = np.linspace(0, fs / 2, num=int(sig_len // 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 / (f**-chi1 * (f**(-chi2 - chi1) + knee))) for f in freqs]) # Add sinusoids with a random phase shift sig = np.sum(np.array([ cosine_coeffs[ell] * np.cos(2 * np.pi * f * times + 2 * np.pi * np.random.rand()) for ell, f in enumerate(freqs) ]), axis=0) return sig
def sim_peak_oscillation(sig_ap, fs, freq, bw, height): """Simulate a signal with an aperiodic component and a specific oscillation peak. Parameters ---------- sig_ap : 1d array The timeseries of the aperiodic component. fs : float Sampling rate of ``sig_ap``. freq : float Central frequency for the gaussian peak in Hz. bw : float Bandwidth, or standard deviation, of gaussian peak in Hz. height : float Relative height of the gaussian peak at the central frequency ``freq``. Units of log10(power), over the aperiodic component. Returns ------- sig : 1d array Time series with desired power spectrum. Notes ----- - This function creates a time series whose power spectrum consists of an aperiodic component and a gaussian peak at ``freq`` with standard deviation ``bw`` and relative ``height``. - The periodic component of the signal will be sinusoidal. Examples -------- Simulate a signal with aperiodic exponent of -2 & oscillation central frequency of 20 Hz: >>> from neurodsp.sim import sim_powerlaw >>> fs = 500 >>> sig_ap = sim_powerlaw(n_seconds=10, fs=fs) >>> sig = sim_peak_oscillation(sig_ap, fs=fs, freq=20, bw=5, height=7) """ sig_len = len(sig_ap) times = create_times(sig_len / fs, fs) # Construct the aperiodic component and compute its Fourier transform # Only use the first half of the frequencies from the FFT since the signal is real sig_ap_hat = np.fft.fft(sig_ap)[0:(sig_len // 2 + 1)] # Create the range of frequencies that appear in the power spectrum since these # will be the frequencies in the cosines we sum below freqs = np.linspace(0, fs / 2, num=sig_len // 2 + 1, endpoint=True) # Construct the array of relative heights above the aperiodic power spectrum rel_heights = np.array([height * np.exp(-(lin_freq - freq) ** 2 / (2 * bw ** 2)) \ for lin_freq in freqs]) # Build an array of the sum of squares of the cosines to use in the amplitude calculation cosine_norms = np.array([norm(np.cos(2 * np.pi * lin_freq * times), 2) ** 2 \ for lin_freq in freqs]) # Build an array of the amplitude coefficients cosine_coeffs = np.array([\ (-np.real(sig_ap_hat[ell]) + np.sqrt(np.real(sig_ap_hat[ell]) ** 2 + \ (10 ** rel_heights[ell] - 1) * np.abs(sig_ap_hat[ell]) ** 2)) / cosine_norms[ell] \ for ell in range(cosine_norms.shape[0])]) # Add cosines with the respective coefficients and with a random phase shift for each one sig_periodic = np.sum(np.array([cosine_coeffs[ell] * \ np.cos(2 * np.pi * freqs[ell] * times + \ 2 * np.pi * np.random.rand()) \ for ell in range(cosine_norms.shape[0])]), axis=0) sig = sig_ap + sig_periodic return sig
def sim_synaptic_kernel(n_seconds, fs, tau_r, tau_d): """Simulate a synaptic kernel with specified time constants. Parameters ---------- n_seconds : float Length of simulated kernel in seconds. fs : float Sampling rate of simulated signal, in Hz. tau_r : float Rise time of synaptic kernel, in seconds. tau_d : float Decay time of synaptic kernel, in seconds. Returns ------- kernel : 1d array Simulated synaptic kernel. Notes ----- Three types of kernels are available, based on combinations of time constants: - tau_r == tau_d : alpha synapse - tau_r = 0 : instantaneous rise, with single exponential decay - tau_r != tau_d != 0 : double-exponential, with exponential rise and decay Examples -------- Simulate an alpha synaptic kernel: >>> kernel = sim_synaptic_kernel(n_seconds=1, fs=500, tau_r=0.25, tau_d=0.25) Simulate a double exponential synaptic kernel: >>> kernel = sim_synaptic_kernel(n_seconds=1, fs=500, tau_r=0.1, tau_d=0.3) """ # Create a times vector times = create_times(n_seconds, fs) # Kernel type: single exponential if tau_r == 0: kernel = np.exp(-times / tau_d) # Kernel type: alpha elif tau_r == tau_d: # I(t) = t/tau * exp(-t/tau) kernel = (times / tau_r) * np.exp(-times / tau_r) # Kernel type: double exponential else: if tau_r > tau_d: warn('Rise time constant should be shorter than decay time constant.') # I(t)=(tau_r/(tau_r-tau_d))*(exp(-t/tau_d)-exp(-t/tau_r)) kernel = (np.exp(-times / tau_d) - np.exp(-times / tau_r)) # Normalize the integral to 1 kernel = kernel / np.sum(kernel) return kernel