Beispiel #1
0
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)
Beispiel #2
0
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
Beispiel #3
0
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
Beispiel #4
0
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
Beispiel #5
0
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)
Beispiel #6
0
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
Beispiel #8
0
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
Beispiel #9
0
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