Example #1
0
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
Example #2
0
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
Example #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
Example #4
0
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
Example #5
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
Example #6
0
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
Example #7
0
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
Example #8
0
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
Example #9
0
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
Example #10
0
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
Example #11
0
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
Example #12
0
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
Example #13
0
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