def run_cfg_test(call, method, positivity=True): cfg = get_defaults(ca) cfg.method = method cfg.foilim = [0, 70] cfg.taper = 'parzen' cfg.output = 'abs' result = call(cfg) # check here just for finiteness and positivity assert np.all(np.isfinite(result.data)) if positivity: assert np.all(result.data[0, ...] >= -1e-10)
def freqanalysis(data, method='mtmfft', output='fourier', keeptrials=True, foi=None, foilim=None, pad_to_length=None, polyremoval=None, taper="hann", tapsmofrq=None, nTaper=None, keeptapers=False, toi="all", t_ftimwin=None, wavelet="Morlet", width=6, order=None, order_max=None, order_min=1, c_1=3, adaptive=False, out=None, **kwargs): """ Perform (time-)frequency analysis of Syncopy :class:`~syncopy.AnalogData` objects **Usage Summary** Options available in all analysis methods: * **output** : one of :data:`~syncopy.specest.const_def.availableOutputs`; return power spectra, complex Fourier spectra or absolute values. * **foi**/**foilim** : frequencies of interest; either array of frequencies or frequency window (not both) * **keeptrials** : return individual trials or grand average * **polyremoval** : de-trending method to use (0 = mean, 1 = linear or `None`) List of available analysis methods and respective distinct options: "mtmfft" : (Multi-)tapered Fourier transform Perform frequency analysis on time-series trial data using either a single taper window (Hanning) or many tapers based on the discrete prolate spheroidal sequence (DPSS) that maximize energy concentration in the main lobe. * **taper** : one of :data:`~syncopy.shared.const_def.availableTapers` * **tapsmofrq** : spectral smoothing box for slepian tapers (in Hz) * **nTaper** : number of orthogonal tapers for slepian tapers * **keeptapers** : return individual tapers or average * **pad_to_length**: either pad to an absolute length or set to `'nextpow2'` "mtmconvol" : (Multi-)tapered sliding window Fourier transform Perform time-frequency analysis on time-series trial data based on a sliding window short-time Fourier transform using either a single Hanning taper or multiple DPSS tapers. * **taper** : one of :data:`~syncopy.specest.const_def.availableTapers` * **tapsmofrq** : spectral smoothing box for slepian tapers (in Hz) * **nTaper** : number of orthogonal tapers for slepian tapers * **keeptapers** : return individual tapers or average * **toi** : time-points of interest; can be either an array representing analysis window centroids (in sec), a scalar between 0 and 1 encoding the percentage of overlap between adjacent windows or "all" to center a window on every sample in the data. * **t_ftimwin** : sliding window length (in sec) "wavelet" : (Continuous non-orthogonal) wavelet transform Perform time-frequency analysis on time-series trial data using a non-orthogonal continuous wavelet transform. * **wavelet** : one of :data:`~syncopy.specest.const_def.availableWavelets` * **toi** : time-points of interest; can be either an array representing time points (in sec) or "all"(pre-trimming and subsampling of results) * **width** : Nondimensional frequency constant of Morlet wavelet function (>= 6) * **order** : Order of Paul wavelet function (>= 4) or derivative order of real-valued DOG wavelets (2 = mexican hat) "superlet" : Superlet transform Perform time-frequency analysis on time-series trial data using the super-resolution superlet transform (SLT) from [Moca2021]_. * **order_max** : Maximal order of the superlet * **order_min** : Minimal order of the superlet * **c_1** : Number of cycles of the base Morlet wavelet * **adaptive** : If set to `True` perform fractional adaptive SLT, otherwise perform multiplicative SLT **Full documentation below** Parameters ---------- data : `~syncopy.AnalogData` A non-empty Syncopy :class:`~syncopy.datatype.AnalogData` object method : str Spectral estimation method, one of :data:`~syncopy.specest.const_def.availableMethods` (see below). output : str Output of spectral estimation. One of :data:`~syncopy.specest.const_def.availableOutputs` (see below); use `'pow'` for power spectrum (:obj:`numpy.float32`), `'fourier'` for complex Fourier coefficients (:obj:`numpy.complex64`) or `'abs'` for absolute values (:obj:`numpy.float32`). keeptrials : bool If `True` spectral estimates of individual trials are returned, otherwise results are averaged across trials. foi : array-like or None Frequencies of interest (Hz) for output. If desired frequencies cannot be matched exactly, the closest possible frequencies are used. If `foi` is `None` or ``foi = "all"``, all attainable frequencies (i.e., zero to Nyquist / 2) are selected. foilim : array-like (floats [fmin, fmax]) or None or "all" Frequency-window ``[fmin, fmax]`` (in Hz) of interest. Window specifications must be sorted (e.g., ``[90, 70]`` is invalid) and not NaN but may be unbounded (e.g., ``[-np.inf, 60.5]`` is valid). Edges `fmin` and `fmax` are included in the selection. If `foilim` is `None` or ``foilim = "all"``, all frequencies are selected. pad_to_length : int, None or 'nextpow2' Padding of the input data, if set to a number pads all trials to this absolute length. For instance ``pad_to_length = 2000`` pads all trials to an absolute length of 2000 samples, if and only if the longest trial contains at maximum 2000 samples. Alternatively if all trials have the same initial lengths setting `pad_to_length='nextpow2'` pads all trials to the next power of two. If `None` and trials have unequal lengths all trials are padded to match the longest trial. polyremoval : int or None Order of polynomial used for de-trending data in the time domain prior to spectral analysis. A value of 0 corresponds to subtracting the mean ("de-meaning"), ``polyremoval = 1`` removes linear trends (subtracting the least squares fit of a linear polynomial). If `polyremoval` is `None`, no de-trending is performed. Note that for spectral estimation de-meaning is very advisable and hence also the default. taper : str Only valid if `method` is `'mtmfft'` or `'mtmconvol'`. Windowing function, one of :data:`~syncopy.specest.const_def.availableTapers` (see below). tapsmofrq : float Only valid if `method` is `'mtmfft'` or `'mtmconvol'` and `taper` is `'dpss'`. The amount of spectral smoothing through multi-tapering (Hz). Note that smoothing frequency specifications are one-sided, i.e., 4 Hz smoothing means plus-minus 4 Hz, i.e., a 8 Hz smoothing box. nTaper : int or None Only valid if `method` is `'mtmfft'` or `'mtmconvol'` and `taper='dpss'`. Number of orthogonal tapers to use. It is not recommended to set the number of tapers manually! Leave at `None` for the optimal number to be set automatically. keeptapers : bool Only valid if `method` is `'mtmfft'` or `'mtmconvol'`. If `True`, return spectral estimates for each taper. Otherwise power spectrum is averaged across tapers, if and only if `output` is `pow`. toi : float or array-like or "all" **Mandatory input** for time-frequency analysis methods (`method` is either `"mtmconvol"` or `"wavelet"` or `"superlet"`). If `toi` is scalar, it must be a value between 0 and 1 indicating the percentage of overlap between time-windows specified by `t_ftimwin` (only valid if `method` is `'mtmconvol'`). If `toi` is an array it explicitly selects the centroids of analysis windows (in seconds), if `toi` is `"all"`, analysis windows are centered on all samples in the data for `method="mtmconvol"`. For wavelet based methods (`"wavelet"` or `"superlet"`) toi needs to be either an equidistant array of time points or "all". t_ftimwin : positive float Only valid if `method` is `'mtmconvol'`. Sliding window length (in seconds). wavelet : str Only valid if `method` is `'wavelet'`. Wavelet function to use, one of :data:`~syncopy.specest.const_def.availableWavelets` (see below). width : positive float Only valid if `method` is `'wavelet'` and `wavelet` is `'Morlet'`. Nondimensional frequency constant of Morlet wavelet function. This number should be >= 6, which corresponds to 6 cycles within the analysis window to ensure sufficient spectral sampling. order : positive int Only valid if `method` is `'wavelet'` and `wavelet` is `'Paul'` or `'DOG'`. Order of the wavelet function. If `wavelet` is `'Paul'`, `order` should be chosen >= 4 to ensure that the analysis window contains at least a single oscillation. At an order of 40, the Paul wavelet exhibits about the same number of cycles as the Morlet wavelet with a `width` of 6. All other supported wavelets functions are *real-valued* derivatives of Gaussians (DOGs). Hence, if `wavelet` is `'DOG'`, `order` represents the derivative order. The special case of a second order DOG yields a function known as "Mexican Hat", "Marr" or "Ricker" wavelet, which can be selected alternatively by setting `wavelet` to `'Mexican_hat'`, `'Marr'` or `'Ricker'`. **Note**: A real-valued wavelet function encodes *only* information about peaks and discontinuities in the signal and does *not* provide any information about amplitude or phase. order_max : int Only valid if `method` is `'superlet'`. Maximal order of the superlet set. Controls the maximum number of cycles within a SL together with the `c_1` parameter: c_max = c_1 * order_max order_min : int Only valid if `method` is `'superlet'`. Minimal order of the superlet set. Controls the minimal number of cycles within a SL together with the `c_1` parameter: c_min = c_1 * order_min Note that for admissability reasons c_min should be at least 3! c_1 : int Only valid if `method` is `'superlet'`. Number of cycles of the base Morlet wavelet. If set to lower than 3 increase `order_min` as to never have less than 3 cycles in a wavelet! adaptive : bool Only valid if `method` is `'superlet'`. Wether to perform multiplicative SLT or fractional adaptive SLT. If set to True, the order of the wavelet set will increase linearly with the frequencies of interest from `order_min` to `order_max`. If set to False the same SL will be used for all frequencies. out : None or :class:`SpectralData` object None if a new :class:`SpectralData` object is to be created, or an empty :class:`SpectralData` object Returns ------- spec : :class:`~syncopy.SpectralData` (Time-)frequency spectrum of input data Notes ----- .. [Moca2021] Moca, Vasile V., et al. "Time-frequency super-resolution with superlets." Nature communications 12.1 (2021): 1-18. **Options** .. autodata:: syncopy.specest.const_def.availableMethods .. autodata:: syncopy.specest.const_def.availableOutputs .. autodata:: syncopy.specest.const_def.availableTapers .. autodata:: syncopy.specest.const_def.availableWavelets Examples -------- Coming soon... See also -------- syncopy.specest.mtmfft.mtmfft : (multi-)tapered Fourier transform of multi-channel time series data syncopy.specest.mtmconvol.mtmconvol : time-frequency analysis of multi-channel time series data with a sliding window FFT syncopy.specest.wavelet.wavelet : time-frequency analysis of multi-channel time series data using a wavelet transform numpy.fft.fft : NumPy's reference FFT implementation scipy.signal.stft : SciPy's Short Time Fourier Transform """ # Make sure our one mandatory input object can be processed try: data_parser(data, varname="data", dataclass="AnalogData", writable=None, empty=False) except Exception as exc: raise exc timeAxis = data.dimord.index("time") # Get everything of interest in local namespace defaults = get_defaults(freqanalysis) lcls = locals() # check for ineffective additional kwargs check_passed_kwargs(lcls, defaults, frontend_name="freqanalysis") # Ensure a valid computational method was selected if method not in availableMethods: lgl = "'" + "or '".join(opt + "' " for opt in availableMethods) raise SPYValueError(legal=lgl, varname="method", actual=method) # Ensure a valid output format was selected if output not in spectralConversions.keys(): lgl = "'" + "or '".join(opt + "' " for opt in spectralConversions.keys()) raise SPYValueError(legal=lgl, varname="output", actual=output) # Parse all Boolean keyword arguments for vname in ["keeptrials", "keeptapers"]: if not isinstance(lcls[vname], bool): raise SPYTypeError(lcls[vname], varname=vname, expected="Bool") # If only a subset of `data` is to be processed, make some necessary adjustments # of the sampleinfo and trial lengths if data._selection is not None: sinfo = data._selection.trialdefinition[:, :2] trialList = data._selection.trials else: trialList = list(range(len(data.trials))) sinfo = data.sampleinfo lenTrials = np.diff(sinfo).squeeze() if not lenTrials.shape: lenTrials = lenTrials[None] numTrials = len(trialList) # check polyremoval if polyremoval is not None: scalar_parser(polyremoval, varname="polyremoval", ntype="int_like", lims=[0, 1]) # --- Padding --- # Sliding window FFT does not support "fancy" padding if method == "mtmconvol" and isinstance(pad_to_length, str): msg = "method 'mtmconvol' only supports in-place padding for windows " +\ "exceeding trial boundaries. Your choice of `pad_to_length = '{}'` will be ignored. " SPYWarning(msg.format(pad_to_length)) if method == 'mtmfft': # the actual number of samples in case of later padding minSampleNum = validate_padding(pad_to_length, lenTrials) else: minSampleNum = lenTrials.min() # Compute length (in samples) of shortest trial minTrialLength = minSampleNum / data.samplerate # Shortcut to data sampling interval dt = 1 / data.samplerate foi, foilim = validate_foi(foi, foilim, data.samplerate) # see also https://docs.obspy.org/_modules/obspy/signal/detrend.html#polynomial if polyremoval is not None: try: scalar_parser(polyremoval, varname="polyremoval", lims=[0, 1], ntype="int_like") except Exception as exc: raise exc # Prepare keyword dict for logging (use `lcls` to get actually provided # keyword values, not defaults set above) log_dct = {"method": method, "output": output, "keeptapers": keeptapers, "keeptrials": keeptrials, "polyremoval": polyremoval, "pad_to_length": pad_to_length} # -------------------------------- # 1st: Check time-frequency inputs # to prepare/sanitize `toi` # -------------------------------- if method in ["mtmconvol", "wavelet", "superlet"]: # Get start/end timing info respecting potential in-place selection if toi is None: raise SPYTypeError(toi, varname="toi", expected="scalar or array-like or 'all'") if data._selection is not None: tStart = data._selection.trialdefinition[:, 2] / data.samplerate else: tStart = data._t0 / data.samplerate tEnd = tStart + lenTrials / data.samplerate # for these methods only 'all' or an equidistant array # of time points (sub-sampling, trimming) are valid if method in ["wavelet", "superlet"]: valid = True if isinstance(toi, Number): valid = False elif isinstance(toi, str): if toi != "all": valid = False else: # take everything preSelect = [slice(None)] * numTrials postSelect = [slice(None)] * numTrials elif not iter(toi): valid = False # this is the sequence type - can only be an interval! else: try: array_parser(toi, varname="toi", hasinf=False, hasnan=False, lims=[tStart.min(), tEnd.max()], dims=(None,)) except Exception as exc: raise exc toi = np.array(toi) # check for equidistancy if not np.allclose(np.diff(toi, 2), np.zeros(len(toi) - 2)): valid = False # trim (preSelect) and subsample output (postSelect) else: preSelect = [] postSelect = [] # get sample intervals and relative indices from toi for tk in range(numTrials): start = int(data.samplerate * (toi[0] - tStart[tk])) stop = int(data.samplerate * (toi[-1] - tStart[tk]) + 1) preSelect.append(slice(max(0, start), max(stop, stop - start))) smpIdx = np.minimum(lenTrials[tk] - 1, data.samplerate * (toi - tStart[tk]) - start) postSelect.append(smpIdx.astype(np.intp)) # get out if sth wasn't right if not valid: lgl = "array of equidistant time-points or 'all' for wavelet based methods" raise SPYValueError(legal=lgl, varname="toi", actual=toi) # Update `log_dct` w/method-specific options (use `lcls` to get actually # provided keyword values, not defaults set in here) log_dct["toi"] = lcls["toi"] # -------------------------------------------- # Check options specific to mtm*-methods # (particularly tapers and foi/freqs alignment) # -------------------------------------------- if "mtm" in method: if method == "mtmconvol": # get the sliding window size try: scalar_parser(t_ftimwin, varname="t_ftimwin", lims=[dt, minTrialLength]) except Exception as exc: SPYInfo("Please specify 't_ftimwin' parameter.. exiting!") raise exc # this is the effective sliding window FFT sample size minSampleNum = int(t_ftimwin * data.samplerate) # Construct array of maximally attainable frequencies freqs = np.fft.rfftfreq(minSampleNum, dt) # Match desired frequencies as close as possible to # actually attainable freqs # these are the frequencies attached to the SpectralData by the CR! if foi is not None: foi, _ = best_match(freqs, foi, squash_duplicates=True) elif foilim is not None: foi, _ = best_match(freqs, foilim, span=True, squash_duplicates=True) else: msg = (f"Automatic FFT frequency selection from {freqs[0]:.1f}Hz to " f"{freqs[-1]:.1f}Hz") SPYInfo(msg) foi = freqs log_dct["foi"] = foi # Abort if desired frequency selection is empty if foi.size == 0: lgl = "non-empty frequency specification" act = "empty frequency selection" raise SPYValueError(legal=lgl, varname="foi/foilim", actual=act) # sanitize taper selection and retrieve dpss settings taper_opt = validate_taper(taper, tapsmofrq, nTaper, keeptapers, foimax=foi.max(), samplerate=data.samplerate, nSamples=minSampleNum, output=output) # Update `log_dct` w/method-specific options log_dct["taper"] = taper # only dpss returns non-empty taper_opt dict if taper_opt: log_dct["nTaper"] = taper_opt["Kmax"] log_dct["tapsmofrq"] = tapsmofrq # ------------------------------------------------------- # Now, prepare explicit compute-classes for chosen method # ------------------------------------------------------- if method == "mtmfft": check_effective_parameters(MultiTaperFFT, defaults, lcls) # method specific parameters method_kwargs = { 'samplerate': data.samplerate, 'taper': taper, 'taper_opt': taper_opt, 'nSamples': minSampleNum } # Set up compute-class specestMethod = MultiTaperFFT( foi=foi, timeAxis=timeAxis, keeptapers=keeptapers, polyremoval=polyremoval, output_fmt=output, method_kwargs=method_kwargs) elif method == "mtmconvol": check_effective_parameters(MultiTaperFFTConvol, defaults, lcls) # Process `toi` for sliding window multi taper fft, # we have to account for three scenarios: (1) center sliding # windows on all samples in (selected) trials (2) `toi` was provided as # percentage indicating the degree of overlap b/w time-windows and (3) a set # of discrete time points was provided. These three cases are encoded in # `overlap, i.e., ``overlap > 1` => all, `0 < overlap < 1` => percentage, # `overlap < 0` => discrete `toi` # overlap = None if isinstance(toi, str): if toi != "all": lgl = "`toi = 'all'` to center analysis windows on all time-points" raise SPYValueError(legal=lgl, varname="toi", actual=toi) equidistant = True overlap = np.inf elif isinstance(toi, Number): try: scalar_parser(toi, varname="toi", lims=[0, 1]) except Exception as exc: raise exc overlap = toi equidistant = True # this captures all other cases, e.i. toi is of sequence type else: overlap = -1 try: array_parser(toi, varname="toi", hasinf=False, hasnan=False, lims=[tStart.min(), tEnd.max()], dims=(None,)) except Exception as exc: raise exc toi = np.array(toi) tSteps = np.diff(toi) if (tSteps < 0).any(): lgl = "ordered list/array of time-points" act = "unsorted list/array" raise SPYValueError(legal=lgl, varname="toi", actual=act) # Account for round-off errors: if toi spacing is almost at sample interval # manually correct it if np.isclose(tSteps.min(), dt): tSteps[np.isclose(tSteps, dt)] = dt if tSteps.min() < dt: msg = f"`toi` selection too fine, max. time resolution is {dt}s" SPYWarning(msg) # This is imho a bug in NumPy - even `arange` and `linspace` may produce # arrays that are numerically not exactly equidistant - `unique` will # show several entries here - use `allclose` to identify "even" spacings equidistant = np.allclose(tSteps, [tSteps[0]] * tSteps.size) # If `toi` was 'all' or a percentage, use entire time interval of (selected) # trials and check if those trials have *approximately* equal length if toi is None: if not np.allclose(lenTrials, [minSampleNum] * lenTrials.size): msg = "processing trials of different lengths (min = {}; max = {} samples)" +\ " with `toi = 'all'`" SPYWarning(msg.format(int(minSampleNum), int(lenTrials.max()))) # number of samples per window nperseg = int(t_ftimwin * data.samplerate) halfWin = int(nperseg / 2) postSelect = slice(None) # select all is the default if 0 <= overlap <= 1: # `toi` is percentage noverlap = min(nperseg - 1, int(overlap * nperseg)) # windows get shifted exactly 1 sample # to get a spectral estimate at each sample else: noverlap = nperseg - 1 # `toi` is array if overlap < 0: # Compute necessary padding at begin/end of trials to fit sliding windows offStart = ((toi[0] - tStart) * data.samplerate).astype(np.intp) padBegin = halfWin - offStart padBegin = ((padBegin > 0) * padBegin).astype(np.intp) offEnd = ((tEnd - toi[-1]) * data.samplerate).astype(np.intp) padEnd = halfWin - offEnd padEnd = ((padEnd > 0) * padEnd).astype(np.intp) # Compute sample-indices (one slice/list per trial) from time-selections soi = [] if equidistant: # soi just trims the input data to the [toi[0], toi[-1]] interval # postSelect then subsamples the spectral esimate to the user given toi postSelect = [] for tk in range(numTrials): start = max(0, int(round(data.samplerate * (toi[0] - tStart[tk]) - halfWin))) stop = int(round(data.samplerate * (toi[-1] - tStart[tk]) + halfWin + 1)) soi.append(slice(start, max(stop, stop - start))) # chosen toi subsampling interval in sample units, min. is 1; # compute `delta_idx` s.t. stop - start / delta_idx == toi.size delta_idx = int(round((soi[0].stop - soi[0].start) / toi.size)) delta_idx = delta_idx if delta_idx > 1 else 1 postSelect = slice(None, None, delta_idx) else: for tk in range(numTrials): starts = (data.samplerate * (toi - tStart[tk]) - halfWin).astype(np.intp) starts += padBegin[tk] stops = (data.samplerate * (toi - tStart[tk]) + halfWin + 1).astype(np.intp) stops += padBegin[tk] stops = np.maximum(stops, stops - starts, dtype=np.intp) soi.append([slice(start, stop) for start, stop in zip(starts, stops)]) # postSelect here remains slice(None), as resulting spectrum # has exactly one entry for each soi # `toi` is percentage or "all" else: soi = [slice(None)] * numTrials # Collect keyword args for `mtmconvol` in dictionary method_kwargs = {"samplerate": data.samplerate, "nperseg": nperseg, "noverlap": noverlap, "taper" : taper, "taper_opt" : taper_opt} # Set up compute-class specestMethod = MultiTaperFFTConvol( soi, postSelect, equidistant=equidistant, toi=toi, foi=foi, timeAxis=timeAxis, keeptapers=keeptapers, polyremoval=polyremoval, output_fmt=output, method_kwargs=method_kwargs) elif method == "wavelet": check_effective_parameters(WaveletTransform, defaults, lcls) # Check wavelet selection if wavelet not in availableWavelets: lgl = "'" + "or '".join(opt + "' " for opt in availableWavelets) raise SPYValueError(legal=lgl, varname="wavelet", actual=wavelet) if wavelet not in ["Morlet", "Paul"]: msg = "the chosen wavelet '{}' is real-valued and does not provide " +\ "any information about amplitude or phase of the data. This wavelet function " +\ "may be used to isolate peaks or discontinuities in the signal. " SPYWarning(msg.format(wavelet)) # Check for consistency of `width`, `order` and `wavelet` if wavelet == "Morlet": try: scalar_parser(width, varname="width", lims=[1, np.inf]) except Exception as exc: raise exc wfun = getattr(spywave, wavelet)(w0=width) else: if width != lcls["width"]: msg = "option `width` has no effect for wavelet '{}'" SPYWarning(msg.format(wavelet)) if wavelet == "Paul": try: scalar_parser(order, varname="order", lims=[4, np.inf], ntype="int_like") except Exception as exc: raise exc wfun = getattr(spywave, wavelet)(m=order) elif wavelet == "DOG": try: scalar_parser(order, varname="order", lims=[1, np.inf], ntype="int_like") except Exception as exc: raise exc wfun = getattr(spywave, wavelet)(m=order) else: if order is not None: msg = "option `order` has no effect for wavelet '{}'" SPYWarning(msg.format(wavelet)) wfun = getattr(spywave, wavelet)() # automatic frequency selection if foi is None and foilim is None: scales = get_optimal_wavelet_scales( wfun.scale_from_period, # all availableWavelets sport one! int(minTrialLength * data.samplerate), dt) foi = 1 / wfun.fourier_period(scales) msg = (f"Setting frequencies of interest to {foi[0]:.1f}-" f"{foi[-1]:.1f}Hz") SPYInfo(msg) else: if foilim is not None: foi = np.arange(foilim[0], foilim[1] + 1, dtype=float) # 0 frequency is not valid foi[foi < 0.01] = 0.01 scales = wfun.scale_from_period(1 / foi) # Update `log_dct` w/method-specific options (use `lcls` to get actually # provided keyword values, not defaults set in here) log_dct["foi"] = foi log_dct["wavelet"] = lcls["wavelet"] log_dct["width"] = lcls["width"] log_dct["order"] = lcls["order"] # method specific parameters method_kwargs = { 'samplerate' : data.samplerate, 'scales' : scales, 'wavelet' : wfun } # Set up compute-class specestMethod = WaveletTransform( preSelect, postSelect, toi=toi, timeAxis=timeAxis, polyremoval=polyremoval, output_fmt=output, method_kwargs=method_kwargs) elif method == "superlet": check_effective_parameters(SuperletTransform, defaults, lcls) # check and parse superlet specific arguments if order_max is None: lgl = "Positive integer needed for order_max" raise SPYValueError(legal=lgl, varname="order_max", actual=None) else: scalar_parser( order_max, varname="order_max", lims=[1, np.inf], ntype="int_like" ) scalar_parser( order_min, varname="order_min", lims=[1, order_max], ntype="int_like" ) scalar_parser(c_1, varname="c_1", lims=[1, np.inf], ntype="int_like") # if no frequencies are user selected, take a sensitive default if foi is None and foilim is None: scales = get_optimal_wavelet_scales( superlet.scale_from_period, int(minTrialLength * data.samplerate), dt) foi = 1 / superlet.fourier_period(scales) msg = (f"Setting frequencies of interest to {foi[0]:.1f}-" f"{foi[-1]:.1f}Hz") SPYInfo(msg) else: if foilim is not None: # frequency range in 1Hz steps foi = np.arange(foilim[0], foilim[1] + 1, dtype=float) # 0 frequency is not valid foi[foi < 0.01] = 0.01 scales = superlet.scale_from_period(1. / foi) # FASLT needs ordered frequencies low - high # meaning the scales have to go high - low if adaptive: if len(scales) < 2: lgl = "A range of frequencies" act = "Single frequency" raise SPYValueError(legal=lgl, varname="foi", actual=act) if np.any(np.diff(scales) > 0): msg = "Sorting frequencies low to high for adaptive SLT.." SPYWarning(msg) scales = np.sort(scales)[::-1] log_dct["foi"] = foi log_dct["c_1"] = lcls["c_1"] log_dct["order_max"] = lcls["order_max"] log_dct["order_min"] = lcls["order_min"] # method specific parameters method_kwargs = { 'samplerate' : data.samplerate, 'scales' : scales, 'order_max' : order_max, 'order_min' : order_min, 'c_1' : c_1, 'adaptive' : adaptive } # Set up compute-class specestMethod = SuperletTransform( preSelect, postSelect, toi=toi, timeAxis=timeAxis, polyremoval=polyremoval, output_fmt=output, method_kwargs=method_kwargs) # ------------------------------------------------- # Sanitize output and call the ComputationalRoutine # ------------------------------------------------- # If provided, make sure output object is appropriate if out is not None: try: data_parser(out, varname="out", writable=True, empty=True, dataclass="SpectralData", dimord=SpectralData().dimord) except Exception as exc: raise exc new_out = False else: out = SpectralData(dimord=SpectralData._defaultDimord) new_out = True # Perform actual computation specestMethod.initialize(data, out._stackingDim, chan_per_worker=kwargs.get("chan_per_worker"), keeptrials=keeptrials) specestMethod.compute(data, out, parallel=kwargs.get("parallel"), log_dict=log_dct) # Either return newly created output object or simply quit return out if new_out else None
def wrapper_cfg(*args, **kwargs): # First, parse positional arguments for dict-type inputs (`k` counts the # no. of dicts provided) and convert tuple of positional args to list cfg = None k = 0 args = list(args) for argidx, arg in enumerate(args): if isinstance(arg, dict): cfgidx = argidx k += 1 # If a dict was found, assume it's a `cfg` dict and extract it from # the positional argument list; if more than one dict was found, abort if k == 1: cfg = args.pop(cfgidx) elif k > 1: raise SPYValueError( legal="single `cfg` input", varname="cfg", actual="{0:d} `cfg` objects in input arguments".format(k)) # Now parse provided keywords for `cfg` entry - if `cfg` was already # provided as positional argument, abort if kwargs.get("cfg") is not None: if cfg: lgl = "`cfg` either as positional or keyword argument, not both" raise SPYValueError(legal=lgl, varname="cfg") cfg = kwargs.pop("cfg") # If `cfg` was detected either in positional or keyword arguments, process it if cfg: # If `cfg` is not dict-like, abort (`StructDict` is a `dict` child) if not isinstance(cfg, dict): raise SPYTypeError(cfg, varname="cfg", expected="dictionary-like") # IMPORTANT: create a copy of `cfg` using `StructDict` constructor to # not manipulate `cfg` in user's namespace! cfg = StructDict(cfg) # FIXME # If a method is called using `cfg`, non-default values for # keyword arguments must *only* to be provided via `cfg` defaults = get_defaults(func) for key, value in kwargs.items(): if defaults.get(key, value) != value: raise SPYValueError( legal="no keyword arguments", varname=key, actual="non-default value for {}".format(key)) # Translate any existing "yes" and "no" fields to `True` and `False` for key in cfg.keys(): if str(cfg[key]) == "yes": cfg[key] = True elif str(cfg[key]) == "no": cfg[key] = False # No explicit `cfg`: rename `kwargs` to `cfg` to consolidate processing below; # IMPORTANT: this does *not* create a copy of `kwargs`, thus the `pop`-ing # below actually manipulates `kwargs` as well - crucial for the `kwargs.get("data")` # error checking! else: cfg = kwargs # If `cfg` contains keys 'data' or 'dataset' extract corresponding # entry and make it a positional argument (abort if both 'data' # and 'dataset' are present) data = cfg.pop("data", None) if cfg.get("dataset"): if data: lgl = "either 'data' or 'dataset' in `cfg`/keywords, not both" raise SPYValueError(legal=lgl, varname="cfg") data = cfg.pop("dataset") # If `cfg` did not contain `data`, look into `kwargs` if data is None: data = kwargs.pop("data", None) if kwargs.get("dataset"): if data: lgl = "either `data` or `dataset` keyword, not both" raise SPYValueError(legal=lgl, varname="data/dataset") data = kwargs.pop("dataset") # If Syncopy data object(s) were provided convert single objects to one-element # lists, ensure positional args do *not* contain add'l objects; ensure keyword # args (besides `cfg`) do *not* contain add'l objects; ensure `data` exclusively # contains Syncopy data objects. Finally, rename remaining positional arguments if data: if not isinstance(data, (tuple, list)): data = [data] if any([ isinstance(arg, spy.datatype.base_data.BaseData) for arg in args ]): lgl = "Syncopy data object(s) provided either via `cfg`/keyword or " +\ "positional arguments, not both" raise SPYValueError(legal=lgl, varname="cfg/data") if kwargs.get("data") or kwargs.get("dataset"): lgl = "Syncopy data object(s) provided either via `cfg` or as " +\ "keyword argument, not both" raise SPYValueError(legal=lgl, varname="cfg.data") if any([ not isinstance(obj, spy.datatype.base_data.BaseData) for obj in data ]): raise SPYError("`data` must be Syncopy data object(s)!") posargs = args # If `data` was not provided via `cfg` or as kw-arg, parse positional arguments if data is None: data = [] posargs = [] while args: arg = args.pop(0) if isinstance(arg, spy.datatype.base_data.BaseData): data.append(arg) else: posargs.append(arg) # Call function with unfolded `data` + modified positional/keyword args return func(*data, *posargs, **cfg)
def connectivityanalysis(data, method="coh", keeptrials=False, output="abs", foi=None, foilim=None, pad_to_length=None, polyremoval=None, taper="hann", tapsmofrq=None, nTaper=None, out=None, **kwargs): """ Perform connectivity analysis of Syncopy :class:`~syncopy.AnalogData` objects **Usage Summary** Options available in all analysis methods: * **foi**/**foilim** : frequencies of interest; either array of frequencies or frequency window (not both) * **polyremoval** : de-trending method to use (0 = mean, 1 = linear or `None`) List of available analysis methods and respective distinct options: "coh" : (Multi-) tapered coherency estimate Compute the normalized cross spectral densities between all channel combinations * **output** : one of ('abs', 'pow', 'fourier') * **taper** : one of :data:`~syncopy.shared.const_def.availableTapers` * **tapsmofrq** : spectral smoothing box for slepian tapers (in Hz) * **nTaper** : (optional) number of orthogonal tapers for slepian tapers * **pad_to_length**: either pad to an absolute length or set to `'nextpow2'` "corr" : Cross-correlations Computes the one sided (positive lags) cross-correlations between all channel combinations. The maximal lag is half the trial lengths. * **keeptrials** : set to `True` for single trial cross-correlations "granger" : Spectral Granger-Geweke causality Computes linear causality estimates between all channel combinations. The intermediate cross-spectral densities can be computed via multi-tapering. * **taper** : one of :data:`~syncopy.shared.const_def.availableTapers` * **tapsmofrq** : spectral smoothing box for slepian tapers (in Hz) * **nTaper** : (optional, not recommended) number of slepian tapers * **pad_to_length**: either pad to an absolute length or set to `'nextpow2'` Parameters ---------- data : `~syncopy.AnalogData` A non-empty Syncopy :class:`~syncopy.datatype.AnalogData` object method : str Connectivity estimation method, one of 'coh', 'corr', 'granger' output : str Relevant for cross-spectral density estimation (`method='coh'`) Use `'pow'` for absolute squared coherence, `'abs'` for absolute value of coherence and`'fourier'` for the complex valued coherency. keeptrials : bool Relevant for cross-correlations (`method='corr'`). If `True` single-trial cross-correlations are returned. foi : array-like or None Frequencies of interest (Hz) for output. If desired frequencies cannot be matched exactly, the closest possible frequencies are used. If `foi` is `None` or ``foi = "all"``, all attainable frequencies (i.e., zero to Nyquist / 2) are selected. foilim : array-like (floats [fmin, fmax]) or None or "all" Frequency-window ``[fmin, fmax]`` (in Hz) of interest. The `foi` array will be constructed in 1Hz steps from `fmin` to `fmax` (inclusive). pad_to_length : int, None or 'nextpow2' Padding of the (tapered) signal, if set to a number pads all trials to this absolute length. E.g. `pad_to_length=2000` pads all trials to 2000 samples, if and only if the longest trial is at maximum 2000 samples. Alternatively if all trials have the same initial lengths setting `pad_to_length='nextpow2'` pads all trials to the next power of two. If `None` and trials have unequal lengths all trials are padded to match the longest trial. taper : str Only valid if `method` is `'coh'` or `'granger'`. Windowing function, one of :data:`~syncopy.specest.const_def.availableTapers` tapsmofrq : float Only valid if `method` is `'coh'` or `'granger'` and `taper` is `'dpss'`. The amount of spectral smoothing through multi-tapering (Hz). Note that smoothing frequency specifications are one-sided, i.e., 4 Hz smoothing means plus-minus 4 Hz, i.e., a 8 Hz smoothing box. nTaper : int or None Only valid if `method` is `'coh'` or `'granger'` and ``taper = 'dpss'``. Number of orthogonal tapers to use. It is not recommended to set the number of tapers manually! Leave at `None` for the optimal number to be set automatically. Examples -------- Coming soon... """ # Make sure our one mandatory input object can be processed try: data_parser(data, varname="data", dataclass="AnalogData", writable=None, empty=False) except Exception as exc: raise exc timeAxis = data.dimord.index("time") # Get everything of interest in local namespace defaults = get_defaults(connectivityanalysis) lcls = locals() # check for ineffective additional kwargs check_passed_kwargs(lcls, defaults, frontend_name="connectivity") # Ensure a valid computational method was selected if method not in availableMethods: lgl = "'" + "or '".join(opt + "' " for opt in availableMethods) raise SPYValueError(legal=lgl, varname="method", actual=method) # if a subset selection is present # get sampleinfo and check for equidistancy if data._selection is not None: sinfo = data._selection.trialdefinition[:, :2] trialList = data._selection.trials # user picked discrete set of time points if isinstance(data._selection.time[0], list): lgl = "equidistant time points (toi) or time slice (toilim)" actual = "non-equidistant set of time points" raise SPYValueError(legal=lgl, varname="select", actual=actual) else: trialList = list(range(len(data.trials))) sinfo = data.sampleinfo lenTrials = np.diff(sinfo).squeeze() # check polyremoval if polyremoval is not None: scalar_parser(polyremoval, varname="polyremoval", ntype="int_like", lims=[0, 1]) # --- Padding --- if method == "corr" and pad_to_length: lgl = "`None`, no padding needed/allowed for cross-correlations" actual = f"{pad_to_length}" raise SPYValueError(legal=lgl, varname="pad_to_length", actual=actual) # the actual number of samples in case of later padding nSamples = validate_padding(pad_to_length, lenTrials) # --- Basic foi sanitization --- foi, foilim = validate_foi(foi, foilim, data.samplerate) # only now set foi array for foilim in 1Hz steps if foilim is not None: foi = np.arange(foilim[0], foilim[1] + 1, dtype=float) # Prepare keyword dict for logging (use `lcls` to get actually provided # keyword values, not defaults set above) log_dict = { "method": method, "output": output, "keeptrials": keeptrials, "polyremoval": polyremoval, "pad_to_length": pad_to_length } # --- Setting up specific Methods --- if method in ['coh', 'granger']: # --- set up computation of the single trial CSDs --- if keeptrials is not False: lgl = "False, trial averaging needed!" act = keeptrials raise SPYValueError(lgl, varname="keeptrials", actual=act) # Construct array of maximally attainable frequencies freqs = np.fft.rfftfreq(nSamples, 1 / data.samplerate) # Match desired frequencies as close as possible to # actually attainable freqs # these are the frequencies attached to the SpectralData by the CR! if foi is not None: foi, _ = best_match(freqs, foi, squash_duplicates=True) elif foilim is not None: foi, _ = best_match(freqs, foilim, span=True, squash_duplicates=True) elif foi is None and foilim is None: # Construct array of maximally attainable frequencies msg = (f"Setting frequencies of interest to {freqs[0]:.1f}-" f"{freqs[-1]:.1f}Hz") SPYInfo(msg) foi = freqs # sanitize taper selection and retrieve dpss settings taper_opt = validate_taper( taper, tapsmofrq, nTaper, keeptapers=False, # ST_CSD's always average tapers foimax=foi.max(), samplerate=data.samplerate, nSamples=nSamples, output="pow") # ST_CSD's always have this unit/norm log_dict["foi"] = foi log_dict["taper"] = taper # only dpss returns non-empty taper_opt dict if taper_opt: log_dict["nTaper"] = taper_opt["Kmax"] log_dict["tapsmofrq"] = tapsmofrq check_effective_parameters(ST_CrossSpectra, defaults, lcls) # parallel computation over trials st_compRoutine = ST_CrossSpectra(samplerate=data.samplerate, nSamples=nSamples, taper=taper, taper_opt=taper_opt, polyremoval=polyremoval, timeAxis=timeAxis, foi=foi) # hard coded as class attribute st_dimord = ST_CrossSpectra.dimord if method == 'coh': # final normalization after trial averaging av_compRoutine = NormalizeCrossSpectra(output=output) if method == 'granger': # after trial averaging # hardcoded numerical parameters av_compRoutine = GrangerCausality(rtol=1e-8, nIter=100, cond_max=1e4) if method == 'corr': if lcls['foi'] is not None: msg = 'Parameter `foi` has no effect for `corr`' SPYWarning(msg) check_effective_parameters(ST_CrossCovariance, defaults, lcls) # single trial cross-correlations if keeptrials: av_compRoutine = None # no trial average norm = True # normalize individual trials within the ST CR else: av_compRoutine = NormalizeCrossCov() norm = False # parallel computation over trials st_compRoutine = ST_CrossCovariance(samplerate=data.samplerate, polyremoval=polyremoval, timeAxis=timeAxis, norm=norm) # hard coded as class attribute st_dimord = ST_CrossCovariance.dimord # ------------------------------------------------- # Call the chosen single trial ComputationalRoutine # ------------------------------------------------- # the single trial results need a new DataSet st_out = CrossSpectralData(dimord=st_dimord) # Perform the trial-parallelized computation of the matrix quantity st_compRoutine.initialize( data, st_out._stackingDim, chan_per_worker=None, # no parallelisation over channels possible keeptrials=keeptrials) # we most likely need trial averaging! st_compRoutine.compute(data, st_out, parallel=kwargs.get("parallel"), log_dict=log_dict) # if ever needed.. # for single trial cross-corr results <-> keeptrials is True if keeptrials and av_compRoutine is None: if out is not None: msg = "Single trial processing does not support `out` argument but directly returns the results" SPYWarning(msg) return st_out # ---------------------------------------------------------------------------------- # Sanitize output and call the chosen ComputationalRoutine on the averaged ST output # ---------------------------------------------------------------------------------- # If provided, make sure output object is appropriate if out is not None: try: data_parser(out, varname="out", writable=True, empty=True, dataclass="CrossSpectralData", dimord=st_dimord) except Exception as exc: raise exc new_out = False else: out = CrossSpectralData(dimord=st_dimord) new_out = True # now take the trial average from the single trial CR as input av_compRoutine.initialize(st_out, out._stackingDim, chan_per_worker=None) av_compRoutine.pre_check() # make sure we got a trial_average av_compRoutine.compute(st_out, out, parallel=False, log_dict=log_dict) # Either return newly created output object or simply quit return out if new_out else None
def test_get_defaults(): assert get_defaults(func) == {"keyword": None}
def freqanalysis(data, method='mtmfft', output='fourier', keeptrials=True, foi=None, foilim=None, pad=None, padtype='zero', padlength=None, prepadlength=None, postpadlength=None, polyremoval=None, taper="hann", tapsmofrq=None, keeptapers=False, toi=None, t_ftimwin=None, wav="Morlet", width=6, order=None, out=None, **kwargs): """ Perform (time-)frequency analysis of Syncopy :class:`~syncopy.AnalogData` objects **Usage Summary** Options available in all analysis methods: * **output** : one of :data:`~.availableOutputs`; return power spectra, complex Fourier spectra or absolute values. * **foi**/**foilim** : frequencies of interest; either array of frequencies or frequency window (not both) * **keeptrials** : return individual trials or grand average * **polyremoval** : de-trending method to use (0 = mean, 1 = linear, 2 = quadratic, 3 = cubic, etc.) List of available analysis methods and respective distinct options: :func:`~syncopy.specest.mtmfft.mtmfft` : (Multi-)tapered Fourier transform Perform frequency analysis on time-series trial data using either a single taper window (Hanning) or many tapers based on the discrete prolate spheroidal sequence (DPSS) that maximize energy concentration in the main lobe. * **taper** : one of :data:`~.availableTapers` * **tapsmofrq** : spectral smoothing box for tapers (in Hz) * **keeptapers** : return individual tapers or average * **pad** : padding method to use (`None`, `True`, `False`, `'absolute'`, `'relative'`, `'maxlen'` or `'nextpow2'`). If `None`, then `'nextpow2'` is selected by default. * **padtype** : values to pad data with (`'zero'`, `'nan'`, `'mean'`, `'localmean'`, `'edge'` or `'mirror'`) * **padlength** : number of samples to pre-pend and/or append to each trial * **prepadlength** : number of samples to pre-pend to each trial * **postpadlength** : number of samples to append to each trial :func:`~syncopy.specest.mtmconvol.mtmconvol` : (Multi-)tapered sliding window Fourier transform Perform time-frequency analysis on time-series trial data based on a sliding window short-time Fourier transform using either a single Hanning taper or multiple DPSS tapers. * **taper** : one of :data:`~.availableTapers` * **tapsmofrq** : spectral smoothing box for tapers (in Hz) * **keeptapers** : return individual tapers or average * **pad** : flag indicating, whether or not to pad trials. If `None`, trials are padded only if sliding window centroids are too close to trial boundaries for the entire window to cover available data-points. * **toi** : time-points of interest; can be either an array representing analysis window centroids (in sec), a scalar between 0 and 1 encoding the percentage of overlap between adjacent windows or "all" to center a window on every sample in the data. * **t_ftimwin** : sliding window length (in sec) :func:`~syncopy.specest.wavelet.wavelet` : (Continuous non-orthogonal) wavelet transform Perform time-frequency analysis on time-series trial data using a non-orthogonal continuous wavelet transform. * **wav** : one of :data:`~.availableWavelets` * **toi** : time-points of interest; can be either an array representing time points (in sec) to center wavelets on or "all" to center a wavelet on every sample in the data. * **width** : Nondimensional frequency constant of Morlet wavelet function (>= 6) * **order** : Order of Paul wavelet function (>= 4) or derivative order of real-valued DOG wavelets (2 = mexican hat) **Full documentation below** Parameters ---------- data : `~syncopy.AnalogData` A non-empty Syncopy :class:`~syncopy.datatype.AnalogData` object method : str Spectral estimation method, one of :data:`~.availableMethods` (see below). output : str Output of spectral estimation. One of :data:`~.availableOutputs` (see below); use `'pow'` for power spectrum (:obj:`numpy.float32`), `'fourier'` for complex Fourier coefficients (:obj:`numpy.complex128`) or `'abs'` for absolute values (:obj:`numpy.float32`). keeptrials : bool If `True` spectral estimates of individual trials are returned, otherwise results are averaged across trials. foi : array-like or None Frequencies of interest (Hz) for output. If desired frequencies cannot be matched exactly, the closest possible frequencies are used. If `foi` is `None` or ``foi = "all"``, all attainable frequencies (i.e., zero to Nyquist / 2) are selected. foilim : array-like (floats [fmin, fmax]) or None or "all" Frequency-window ``[fmin, fmax]`` (in Hz) of interest. Window specifications must be sorted (e.g., ``[90, 70]`` is invalid) and not NaN but may be unbounded (e.g., ``[-np.inf, 60.5]`` is valid). Edges `fmin` and `fmax` are included in the selection. If `foilim` is `None` or ``foilim = "all"``, all frequencies are selected. pad : str or None or bool One of `None`, `True`, `False`, `'absolute'`, `'relative'`, `'maxlen'` or `'nextpow2'`. If `pad` is `None` or ``pad = True``, then method-specific defaults are chosen. Specifically, if `method` is `'mtmfft'` then `pad` is set to `'nextpow2'` so that all trials in `data` are padded to the next power of two higher than the sample-count of the longest (selected) trial in `data`. Conversely, time-frequency analysis methods (`'mtmconvol'` and `'wavelet'`), only perform padding if necessary, i.e., if time-window centroids are chosen too close to trial boundaries for the entire window to cover available data-points. If `pad` is `False`, then no padding is performed. Then in case of ``method = 'mtmfft'`` all trials have to have approximately the same length (up to the next even sample-count), if ``method = 'mtmconvol'`` or ``method = 'wavelet'``, window-centroids have to keep sufficient distance from trial boundaries. For more details on the padding methods `'absolute'`, `'relative'`, `'maxlen'` and `'nextpow2'` see :func:`syncopy.padding`. padtype : str Values to be used for padding. Can be `'zero'`, `'nan'`, `'mean'`, `'localmean'`, `'edge'` or `'mirror'`. See :func:`syncopy.padding` for more information. padlength : None, bool or positive int Only valid if `method` is `'mtmfft'` and `pad` is `'absolute'` or `'relative'`. Number of samples to pad data with. See :func:`syncopy.padding` for more information. prepadlength : None or bool or int Only valid if `method` is `'mtmfft'` and `pad` is `'relative'`. Number of samples to pre-pend to each trial. See :func:`syncopy.padding` for more information. postpadlength : None or bool or int Only valid if `method` is `'mtmfft'` and `pad` is `'relative'`. Number of samples to append to each trial. See :func:`syncopy.padding` for more information. polyremoval : int or None **FIXME: Not implemented yet** Order of polynomial used for de-trending data in the time domain prior to spectral analysis. A value of 0 corresponds to subtracting the mean ("de-meaning"), ``polyremoval = 1`` removes linear trends (subtracting the least squares fit of a linear polynomial), ``polyremoval = N`` for `N > 1` subtracts a polynomial of order `N` (``N = 2`` quadratic, ``N = 3`` cubic etc.). If `polyremoval` is `None`, no de-trending is performed. taper : str Only valid if `method` is `'mtmfft'` or `'mtmconvol'`. Windowing function, one of :data:`~.availableTapers` (see below). tapsmofrq : float Only valid if `method` is `'mtmfft'` or `'mtmconvol'`. The amount of spectral smoothing through multi-tapering (Hz). Note that smoothing frequency specifications are one-sided, i.e., 4 Hz smoothing means plus-minus 4 Hz, i.e., a 8 Hz smoothing box. keeptapers : bool Only valid if `method` is `'mtmfft'` or `'mtmconvol'`. If `True`, return spectral estimates for each taper, otherwise results are averaged across tapers. toi : float or array-like or "all" **Mandatory input** for time-frequency analysis methods (`method` is either `"mtmconvol"` or `"wavelet"`). If `toi` is scalar, it must be a value between 0 and 1 indicating the percentage of overlap between time-windows specified by `t_ftimwin` (only valid if `method` is `'mtmconvol'`, invalid for `'wavelet'`). If `toi` is an array it explicitly selects the centroids of analysis windows (in seconds). If `toi` is `"all"`, analysis windows are centered on all samples in the data. t_ftimwin : positive float Only valid if `method` is `'mtmconvol'`. Sliding window length (in seconds). wav : str Only valid if `method` is `'wavelet'`. Wavelet function to use, one of :data:`~.availableWavelets` (see below). width : positive float Only valid if `method` is `'wavelet'` and `wav` is `'Morlet'`. Nondimensional frequency constant of Morlet wavelet function. This number should be >= 6, which corresponds to 6 cycles within the analysis window to ensure sufficient spectral sampling. order : positive int Only valid if `method` is `'wavelet'` and `wav` is `'Paul'` or `'DOG'`. Order of the wavelet function. If `wav` is `'Paul'`, `order` should be chosen >= 4 to ensure that the analysis window contains at least a single oscillation. At an order of 40, the Paul wavelet exhibits about the same number of cycles as the Morlet wavelet with a `width` of 6. All other supported wavelets functions are *real-valued* derivatives of Gaussians (DOGs). Hence, if `wav` is `'DOG'`, `order` represents the derivative order. The special case of a second order DOG yields a function known as "Mexican Hat", "Marr" or "Ricker" wavelet, which can be selected alternatively by setting `wav` to `'Mexican_hat'`, `'Marr'` or `'Ricker'`. **Note**: A real-valued wavelet function encodes *only* information about peaks and discontinuities in the signal and does *not* provide any information about amplitude or phase. out : None or :class:`SpectralData` object None if a new :class:`SpectralData` object is to be created, or an empty :class:`SpectralData` object Returns ------- spec : :class:`~syncopy.SpectralData` (Time-)frequency spectrum of input data Notes ----- Coming soon... Examples -------- Coming soon... .. autodata:: syncopy.specest.freqanalysis.availableMethods .. autodata:: syncopy.specest.freqanalysis.availableOutputs .. autodata:: syncopy.specest.freqanalysis.availableTapers .. autodata:: syncopy.specest.freqanalysis.availableWavelets See also -------- syncopy.specest.mtmfft.mtmfft : (multi-)tapered Fourier transform of multi-channel time series data syncopy.specest.mtmconvol.mtmconvol : time-frequency analysis of multi-channel time series data with a sliding window FFT syncopy.specest.wavelet.wavelet : time-frequency analysis of multi-channel time series data using a wavelet transform numpy.fft.fft : NumPy's reference FFT implementation scipy.signal.stft : SciPy's Short Time Fourier Transform """ # Make sure our one mandatory input object can be processed try: data_parser(data, varname="data", dataclass="AnalogData", writable=None, empty=False) except Exception as exc: raise exc timeAxis = data.dimord.index("time") # Get everything of interest in local namespace defaults = get_defaults(freqanalysis) lcls = locals() # Ensure a valid computational method was selected if method not in availableMethods: lgl = "'" + "or '".join(opt + "' " for opt in availableMethods) raise SPYValueError(legal=lgl, varname="method", actual=method) # Ensure a valid output format was selected if output not in spectralConversions.keys(): lgl = "'" + "or '".join(opt + "' " for opt in spectralConversions.keys()) raise SPYValueError(legal=lgl, varname="output", actual=output) # Parse all Boolean keyword arguments for vname in ["keeptrials", "keeptapers"]: if not isinstance(lcls[vname], bool): raise SPYTypeError(lcls[vname], varname=vname, expected="Bool") # If only a subset of `data` is to be processed, make some necessary adjustments # and compute minimal sample-count across (selected) trials if data._selection is not None: trialList = data._selection.trials sinfo = np.zeros((len(trialList), 2)) for tk, trlno in enumerate(trialList): trl = data._preview_trial(trlno) tsel = trl.idx[timeAxis] if isinstance(tsel, list): sinfo[tk, :] = [0, len(tsel)] else: sinfo[tk, :] = [ trl.idx[timeAxis].start, trl.idx[timeAxis].stop ] else: trialList = list(range(len(data.trials))) sinfo = data.sampleinfo lenTrials = np.diff(sinfo).squeeze() numTrials = len(trialList) # Set default padding options: after this, `pad` is either `None`, `False` or `str` defaultPadding = {"mtmfft": "nextpow2", "mtmconvol": None, "wavelet": None} if pad is None or pad is True: pad = defaultPadding[method] # Sliding window FFT does not support "fancy" padding if method == "mtmconvol" and isinstance(pad, str): msg = "method 'mtmconvol' only supports in-place padding for windows " +\ "exceeding trial boundaries. Your choice of `pad = '{}'` will be ignored. " SPYWarning(msg.format(pad)) pad = None # Ensure padding selection makes sense: do not pad on a by-trial basis but # use the longest trial as reference and compute `padlength` from there # (only relevant for "global" padding options such as `maxlen` or `nextpow2`) if pad: if not isinstance(pad, str): raise SPYTypeError(pad, varname="pad", expected="str or None") if pad == "maxlen": padlength = lenTrials.max() prepadlength = True postpadlength = False elif pad == "nextpow2": padlength = 0 for ltrl in lenTrials: padlength = max(padlength, _nextpow2(ltrl)) pad = "absolute" prepadlength = True postpadlength = False padding(data._preview_trial(trialList[0]), padtype, pad=pad, padlength=padlength, prepadlength=prepadlength, postpadlength=postpadlength) # Compute `minSampleNum` accounting for padding minSamplePos = lenTrials.argmin() minSampleNum = padding(data._preview_trial(trialList[minSamplePos]), padtype, pad=pad, padlength=padlength, prepadlength=True).shape[timeAxis] else: if method == "mtmfft" and np.unique( (np.floor(lenTrials / 2))).size > 1: lgl = "trials of approximately equal length for method 'mtmfft'" act = "trials of unequal length" raise SPYValueError(legal=lgl, varname="data", actual=act) minSampleNum = lenTrials.min() # Compute length (in samples) of shortest trial minTrialLength = minSampleNum / data.samplerate # Basic sanitization of frequency specifications if foi is not None: if isinstance(foi, str): if foi == "all": foi = None else: raise SPYValueError(legal="'all' or `None` or list/array", varname="foi", actual=foi) else: try: array_parser(foi, varname="foi", hasinf=False, hasnan=False, lims=[0, data.samplerate / 2], dims=(None, )) except Exception as exc: raise exc foi = np.array(foi, dtype="float") if foilim is not None: if isinstance(foilim, str): if foilim == "all": foilim = None else: raise SPYValueError(legal="'all' or `None` or `[fmin, fmax]`", varname="foilim", actual=foilim) else: try: array_parser(foilim, varname="foilim", hasinf=False, hasnan=False, lims=[0, data.samplerate / 2], dims=(2, )) except Exception as exc: raise exc if foi is not None and foilim is not None: lgl = "either `foi` or `foilim` specification" act = "both" raise SPYValueError(legal=lgl, varname="foi/foilim", actual=act) # FIXME: implement detrending # see also https://docs.obspy.org/_modules/obspy/signal/detrend.html#polynomial if polyremoval is not None: raise NotImplementedError("Detrending has not been implemented yet.") try: scalar_parser(polyremoval, varname="polyremoval", lims=[0, 8], ntype="int_like") except Exception as exc: raise exc # Prepare keyword dict for logging (use `lcls` to get actually provided # keyword values, not defaults set above) log_dct = { "method": method, "output": output, "keeptapers": keeptapers, "keeptrials": keeptrials, "polyremoval": polyremoval, "pad": lcls["pad"], "padtype": lcls["padtype"], "padlength": lcls["padlength"], "foi": lcls["foi"] } # 1st: Check time-frequency inputs to prepare/sanitize `toi` if method in ["mtmconvol", "wavelet"]: # Get start/end timing info respecting potential in-place selection if toi is None: raise SPYTypeError(toi, varname="toi", expected="scalar or array-like or 'all'") if data._selection is not None: tStart = data._selection.trialdefinition[:, 2] / data.samplerate else: tStart = data._t0 / data.samplerate tEnd = tStart + lenTrials / data.samplerate # Process `toi`: we have to account for three scenarios: (1) center sliding # windows on all samples in (selected) trials (2) `toi` was provided as # percentage indicating the degree of overlap b/w time-windows and (3) a set # of discrete time points was provided. These three cases are encoded in # `overlap, i.e., ``overlap > 1` => all, `0 < overlap < 1` => percentage, # `overlap < 0` => discrete `toi` if isinstance(toi, str): if toi != "all": lgl = "`toi = 'all'` to center analysis windows on all time-points" raise SPYValueError(legal=lgl, varname="toi", actual=toi) overlap = 1.1 toi = None equidistant = True elif isinstance(toi, Number): if method == "wavelet": lgl = "array of time-points wavelets are to be centered on" act = "scalar value" raise SPYValueError(legal=lgl, varname="toi", actual=act) try: scalar_parser(toi, varname="toi", lims=[0, 1]) except Exception as exc: raise exc overlap = toi equidistant = True else: overlap = -1 try: array_parser(toi, varname="toi", hasinf=False, hasnan=False, lims=[tStart.min(), tEnd.max()], dims=(None, )) except Exception as exc: raise exc toi = np.array(toi) tSteps = np.diff(toi) if (tSteps < 0).any(): lgl = "ordered list/array of time-points" act = "unsorted list/array" raise SPYValueError(legal=lgl, varname="toi", actual=act) # This is imho a bug in NumPy - even `arange` and `linspace` may produce # arrays that are numerically not exactly equidistant - `unique` will # show several entries here - use `allclose` to identify "even" spacings equidistant = np.allclose(tSteps, [tSteps[0]] * tSteps.size) # If `toi` was 'all' or a percentage, use entire time interval of (selected) # trials and check if those trials have *approximately* equal length if toi is None: if not np.allclose(lenTrials, [minSampleNum] * lenTrials.size): msg = "processing trials of different lengths (min = {}; max = {} samples)" +\ " with `toi = 'all'`" SPYWarning(msg.format(int(minSampleNum), int(lenTrials.max()))) if pad is False: lgl = "`pad` to be `None` or `True` to permit zero-padding " +\ "at trial boundaries to accommodate windows if `0 < toi < 1` " +\ "or if `toi` is 'all'" act = "False" raise SPYValueError(legal=lgl, actual=act, varname="pad") # Code recycling: `overlap`, `equidistant` etc. are really only relevant # for `mtmconvol`, but we use padding calc below for `wavelet` as well if method == "mtmconvol": try: scalar_parser(t_ftimwin, varname="t_ftimwin", lims=[1 / data.samplerate, minTrialLength]) except Exception as exc: raise exc else: t_ftimwin = 0 nperseg = int(t_ftimwin * data.samplerate) minSampleNum = nperseg halfWin = int(nperseg / 2) # `mtmconvol`: compute no. of samples overlapping across adjacent windows if overlap < 0: # `toi` is equidistant range or disjoint points noverlap = nperseg - max(1, int(tSteps[0] * data.samplerate)) elif 0 <= overlap <= 1: # `toi` is percentage noverlap = min(nperseg - 1, int(overlap * nperseg)) else: # `toi` is "all" noverlap = nperseg - 1 # `toi` is array if overlap < 0: # Compute necessary padding at begin/end of trials to fit sliding windows offStart = ((toi[0] - tStart) * data.samplerate).astype(np.intp) padBegin = halfWin - offStart padBegin = ((padBegin > 0) * padBegin).astype(np.intp) offEnd = ((tEnd - toi[-1]) * data.samplerate).astype(np.intp) padEnd = halfWin - offEnd padEnd = ((padEnd > 0) * padEnd).astype(np.intp) # Abort if padding was explicitly forbidden if pad is False and (np.any(padBegin) or np.any(padBegin)): lgl = "windows within trial bounds" act = "windows exceeding trials no. " +\ "".join(str(trlno) + ", "\ for trlno in np.array(trialList)[(padBegin + padEnd) > 0])[:-2] raise SPYValueError(legal=lgl, varname="pad", actual=act) # Compute sample-indices (one slice/list per trial) from time-selections soi = [] if not equidistant: for tk in range(numTrials): starts = (data.samplerate * (toi - tStart[tk]) - halfWin).astype(np.intp) starts += padBegin[tk] stops = (data.samplerate * (toi - tStart[tk]) + halfWin + 1).astype(np.intp) stops += padBegin[tk] stops = np.maximum(stops, stops - starts, dtype=np.intp) soi.append([ slice(start, stop) for start, stop in zip(starts, stops) ]) else: for tk in range(numTrials): start = int(data.samplerate * (toi[0] - tStart[tk]) - halfWin) stop = int(data.samplerate * (toi[-1] - tStart[tk]) + halfWin + 1) soi.append(slice(max(0, start), max(stop, stop - start))) # `toi` is percentage or "all" else: padBegin = np.zeros((numTrials, )) padEnd = np.zeros((numTrials, )) soi = [slice(None)] * numTrials # For wavelets, we need to first trim the data (via `preSelect`), then # extract the wanted time-points (`postSelect`) if method == "wavelet": # Simply recycle the indexing work done for `mtmconvol` (i.e., `soi`) preSelect = [] if not equidistant: for tk in range(numTrials): preSelect.append(slice(soi[tk][0].start, soi[tk][-1].stop)) else: preSelect = soi # If `toi` is an array, convert "global" indices to "local" ones # (select within `preSelect`'s selection), otherwise just take all if overlap < 0: postSelect = [] for tk in range(numTrials): smpIdx = np.minimum( lenTrials[tk] - 1, data.samplerate * (toi - tStart[tk]) - offStart[tk] + padBegin[tk]) postSelect.append(smpIdx.astype(np.intp)) else: postSelect = [slice(None)] * numTrials # Update `log_dct` w/method-specific options (use `lcls` to get actually # provided keyword values, not defaults set in here) if toi is None: toi = "all" log_dct["toi"] = lcls["toi"] # Check options specific to mtm*-methods (particularly tapers and foi/freqs alignment) if "mtm" in method: # See if taper choice is supported if taper not in availableTapers: lgl = "'" + "or '".join(opt + "' " for opt in availableTapers) raise SPYValueError(legal=lgl, varname="taper", actual=taper) taper = getattr(spwin, taper) # Advanced usage: see if `taperopt` was provided - if not, leave it empty taperopt = kwargs.get("taperopt", {}) if not isinstance(taperopt, dict): raise SPYTypeError(taperopt, varname="taperopt", expected="dictionary") # Construct array of maximally attainable frequencies nFreq = int(np.floor(minSampleNum / 2) + 1) freqs = np.linspace(0, data.samplerate / 2, nFreq) # Match desired frequencies as close as possible to actually attainable freqs if foi is not None: foi, _ = best_match(freqs, foi, squash_duplicates=True) elif foilim is not None: foi, _ = best_match(freqs, foilim, span=True, squash_duplicates=True) else: foi = freqs # Abort if desired frequency selection is empty if foi.size == 0: lgl = "non-empty frequency specification" act = "empty frequency selection" raise SPYValueError(legal=lgl, varname="foi/foilim", actual=act) # Set/get `tapsmofrq` if we're working w/Slepian tapers if taper.__name__ == "dpss": # Try to derive "sane" settings by using 3/4 octave smoothing of highest `foi` # following Hipp et al. "Oscillatory Synchronization in Large-Scale # Cortical Networks Predicts Perception", Neuron, 2011 if tapsmofrq is None: foimax = foi.max() tapsmofrq = (foimax * 2**(3 / 4 / 2) - foimax * 2**(-3 / 4 / 2)) / 2 else: try: scalar_parser(tapsmofrq, varname="tapsmofrq", lims=[1, np.inf]) except Exception as exc: raise exc # Get/compute number of tapers to use (at least 1 and max. 50) nTaper = taperopt.get("Kmax", 1) if not taperopt: nTaper = int( max( 2, min( 50, np.floor(tapsmofrq * minSampleNum * 1 / data.samplerate)))) taperopt = {"NW": tapsmofrq, "Kmax": nTaper} else: nTaper = 1 # Warn the user in case `tapsmofrq` has no effect if tapsmofrq is not None and taper.__name__ != "dpss": msg = "`tapsmofrq` is only used if `taper` is `dpss`!" SPYWarning(msg) # Update `log_dct` w/method-specific options (use `lcls` to get actually # provided keyword values, not defaults set in here) log_dct["taper"] = lcls["taper"] log_dct["tapsmofrq"] = lcls["tapsmofrq"] log_dct["nTaper"] = nTaper # Check for non-default values of options not supported by chosen method kwdict = {"wav": wav, "width": width} for name, kwarg in kwdict.items(): if kwarg is not lcls[name]: msg = "option `{}` has no effect in methods `mtmfft` and `mtmconvol`!" SPYWarning(msg.format(name)) # Now, prepare explicit compute-classes for chosen method if method == "mtmfft": # Check for non-default values of options not supported by chosen method kwdict = {"t_ftimwin": t_ftimwin, "toi": toi} for name, kwarg in kwdict.items(): if kwarg is not lcls[name]: msg = "option `{}` has no effect in method `mtmfft`!" SPYWarning(msg.format(name)) # Set up compute-class specestMethod = MultiTaperFFT(samplerate=data.samplerate, foi=foi, nTaper=nTaper, timeAxis=timeAxis, taper=taper, taperopt=taperopt, tapsmofrq=tapsmofrq, pad=pad, padtype=padtype, padlength=padlength, keeptapers=keeptapers, polyremoval=polyremoval, output_fmt=output) elif method == "mtmconvol": # Set up compute-class specestMethod = MultiTaperFFTConvol(soi, list(padBegin), list(padEnd), samplerate=data.samplerate, noverlap=noverlap, nperseg=nperseg, equidistant=equidistant, toi=toi, foi=foi, nTaper=nTaper, timeAxis=timeAxis, taper=taper, taperopt=taperopt, pad=pad, padtype=padtype, padlength=padlength, prepadlength=prepadlength, postpadlength=postpadlength, keeptapers=keeptapers, polyremoval=polyremoval, output_fmt=output) elif method == "wavelet": # Check for non-default values of `taper`, `tapsmofrq`, `keeptapers` and # `t_ftimwin` (set to 0 above) kwdict = { "taper": taper, "tapsmofrq": tapsmofrq, "keeptapers": keeptapers } for name, kwarg in kwdict.items(): if kwarg is not lcls[name]: msg = "option `{}` has no effect in method `wavelet`!" SPYWarning(msg.format(name)) if t_ftimwin != 0: msg = "option `t_ftimwin` has no effect in method `wavelet`!" SPYWarning(msg) # Check wavelet selection if wav not in availableWavelets: lgl = "'" + "or '".join(opt + "' " for opt in availableWavelets) raise SPYValueError(legal=lgl, varname="wav", actual=wav) if wav not in ["Morlet", "Paul"]: msg = "the chosen wavelet '{}' is real-valued and does not provide " +\ "any information about amplitude or phase of the data. This wavelet function " +\ "may be used to isolate peaks or discontinuities in the signal. " SPYWarning(msg.format(wav)) # Check for consistency of `width`, `order` and `wav` if wav == "Morlet": try: scalar_parser(width, varname="width", lims=[1, np.inf]) except Exception as exc: raise exc wfun = getattr(spywave, wav)(w0=width) else: if width != lcls["width"]: msg = "option `width` has no effect for wavelet '{}'" SPYWarning(msg.format(wav)) if wav == "Paul": try: scalar_parser(order, varname="order", lims=[4, np.inf], ntype="int_like") except Exception as exc: raise exc wfun = getattr(spywave, wav)(m=order) elif wav == "DOG": try: scalar_parser(order, varname="order", lims=[1, np.inf], ntype="int_like") except Exception as exc: raise exc wfun = getattr(spywave, wav)(m=order) else: if order is not None: msg = "option `order` has no effect for wavelet '{}'" SPYWarning(msg.format(wav)) wfun = getattr(spywave, wav)() # Process frequency selection (`toi` was taken care of above): `foilim` # selections are wrapped into `foi` thus the seemingly weird if construct # Note: SLURM workers don't like monkey-patching, so let's pretend # `get_optimal_wavelet_scales` is a class method by passing `wfun` as its # first argument if foi is None: scales = _get_optimal_wavelet_scales( wfun, int(minTrialLength * data.samplerate), 1 / data.samplerate) if foilim is not None: foi = np.arange(foilim[0], foilim[1] + 1) if foi is not None: foi[foi < 0.01] = 0.01 scales = wfun.scale_from_period(1 / foi) scales = scales[:: -1] # FIXME: this only makes sense if `foi` was sorted -> cf Issue #94 # Update `log_dct` w/method-specific options (use `lcls` to get actually # provided keyword values, not defaults set in here) log_dct["wav"] = lcls["wav"] log_dct["width"] = lcls["width"] log_dct["order"] = lcls["order"] # Set up compute-class specestMethod = WaveletTransform(preSelect, postSelect, list(padBegin), list(padEnd), samplerate=data.samplerate, toi=toi, scales=scales, timeAxis=timeAxis, wav=wfun, polyremoval=polyremoval, output_fmt=output) # If provided, make sure output object is appropriate if out is not None: try: data_parser(out, varname="out", writable=True, empty=True, dataclass="SpectralData", dimord=SpectralData().dimord) except Exception as exc: raise exc new_out = False else: out = SpectralData(dimord=SpectralData._defaultDimord) new_out = True # Perform actual computation specestMethod.initialize(data, chan_per_worker=kwargs.get("chan_per_worker"), keeptrials=keeptrials) specestMethod.compute(data, out, parallel=kwargs.get("parallel"), log_dict=log_dct) # Either return newly created output object or simply quit return out if new_out else None
def selectdata(data, trials=None, channels=None, toi=None, toilim=None, foi=None, foilim=None, tapers=None, units=None, eventids=None, out=None, **kwargs): """ Create a new Syncopy object from a selection **Usage Notice** Syncopy offers two modes for selecting data: * **in-place** selections mark subsets of a Syncopy data object for processing via a ``select`` dictionary *without* creating a new object * **deep-copy** selections copy subsets of a Syncopy data object to keep and preserve in a new object created by :func:`~syncopy.selectdata` All Syncopy metafunctions, such as :func:`~syncopy.freqanalysis`, support **in-place** data selection via a ``select`` keyword, effectively avoiding potentially slow copy operations and saving disk space. The keys accepted by the `select` dictionary are identical to the keyword arguments discussed below. In addition, ``select = "all"`` can be used to select entire object contents. Examples >>> select = {"toilim" : [-0.25, 0]} >>> spy.freqanalysis(data, select=select) >>> # or equivalently >>> cfg = spy.get_defaults(spy.freqanalysis) >>> cfg.select = select >>> spy.freqanalysis(cfg, data) **Usage Summary** List of Syncopy data objects and respective valid data selectors: :class:`~syncopy.AnalogData` : trials, channels, toi/toilim Examples >>> spy.selectdata(data, trials=[0, 3, 5], channels=["channel01", "channel02"]) >>> cfg = spy.StructDict() >>> cfg.trials = [5, 3, 0]; cfg.toilim = [0.25, 0.5] >>> spy.selectdata(cfg, data) :class:`~syncopy.SpectralData` : trials, channels, toi/toilim, foi/foilim, tapers Examples >>> spy.selectdata(data, trials=[0, 3, 5], channels=["channel01", "channel02"]) >>> cfg = spy.StructDict() >>> cfg.foi = [30, 40, 50]; cfg.tapers = slice(2, 4) >>> spy.selectdata(cfg, data) :class:`~syncopy.EventData` : trials, toi/toilim, eventids Examples >>> spy.selectdata(data, toilim=[-1, 2.5], eventids=[0, 1]) >>> cfg = spy.StructDict() >>> cfg.trials = [0, 0, 1, 0]; cfg.eventids = slice(2, None) >>> spy.selectdata(cfg, data) :class:`~syncopy.SpikeData` : trials, toi/toilim, units, channels Examples >>> spy.selectdata(data, toilim=[-1, 2.5], units=range(0, 10)) >>> cfg = spy.StructDict() >>> cfg.toi = [1.25, 3.2]; cfg.trials = [0, 1, 2, 3] >>> spy.selectdata(cfg, data) **Note** Any property that is not specifically accessed via one of the provided selectors is taken as is, e.g., ``spy.selectdata(data, trials=[1, 2])`` selects the entire contents of trials no. 2 and 3, while ``spy.selectdata(data, channels=range(0, 50))`` selects the first 50 channels of `data` across all defined trials. Consequently, if no keywords are specified, the entire contents of `data` is selected. **Full documentation below** Parameters ---------- data : Syncopy data object A non-empty Syncopy data object. **Note** the type of `data` determines which keywords can be used. Some keywords are only valid for certain types of Syncopy objects, e.g., "freqs" is not a valid selector for an :class:`~syncopy.AnalogData` object. trials : list (integers) or None or "all" List of integers representing trial numbers to be selected; can include repetitions and need not be sorted (e.g., ``trials = [0, 1, 0, 0, 2]`` is valid) but must be finite and not NaN. If `trials` is `None`, or ``trials = "all"`` all trials are selected. channels : list (integers or strings), slice, range or None or "all" Channel-selection; can be a list of channel names (``['channel3', 'channel1']``), a list of channel indices (``[3, 5]``), a slice (``slice(3, 10)``) or range (``range(3, 10)``). Note that following Python conventions, channels are counted starting at zero, and range and slice selections are half-open intervals of the form `[low, high)`, i.e., low is included , high is excluded. Thus, ``channels = [0, 1, 2]`` or ``channels = slice(0, 3)`` selects the first up to (and including) the third channel. Selections can be unsorted and may include repetitions but must match exactly, be finite and not NaN. If `channels` is `None`, or ``channels = "all"`` all channels are selected. toi : list (floats) or None or "all" Time-points to be selected (in seconds) in each trial. Timing is expected to be on a by-trial basis (e.g., relative to trigger onsets). Selections can be approximate, unsorted and may include repetitions but must be finite and not NaN. Fuzzy matching is performed for approximate selections (i.e., selected time-points are close but not identical to timing information found in `data`) using a nearest-neighbor search for elements of `toi`. If `toi` is `None` or ``toi = "all"``, the entire time-span in each trial is selected. toilim : list (floats [tmin, tmax]) or None or "all" Time-window ``[tmin, tmax]`` (in seconds) to be extracted from each trial. Window specifications must be sorted (e.g., ``[2.2, 1.1]`` is invalid) and not NaN but may be unbounded (e.g., ``[1.1, np.inf]`` is valid). Edges `tmin` and `tmax` are included in the selection. If `toilim` is `None` or ``toilim = "all"``, the entire time-span in each trial is selected. foi : list (floats) or None or "all" Frequencies to be selected (in Hz). Selections can be approximate, unsorted and may include repetitions but must be finite and not NaN. Fuzzy matching is performed for approximate selections (i.e., selected frequencies are close but not identical to frequencies found in `data`) using a nearest- neighbor search for elements of `foi` in `data.freq`. If `foi` is `None` or ``foi = "all"``, all frequencies are selected. foilim : list (floats [fmin, fmax]) or None or "all" Frequency-window ``[fmin, fmax]`` (in Hz) to be extracted. Window specifications must be sorted (e.g., ``[90, 70]`` is invalid) and not NaN but may be unbounded (e.g., ``[-np.inf, 60.5]`` is valid). Edges `fmin` and `fmax` are included in the selection. If `foilim` is `None` or ``foilim = "all"``, all frequencies are selected. tapers : list (integers or strings), slice, range or None or "all" Taper-selection; can be a list of taper names (``['dpss-win-1', 'dpss-win-3']``), a list of taper indices (``[3, 5]``), a slice (``slice(3, 10)``) or range (``range(3, 10)``). Note that following Python conventions, tapers are counted starting at zero, and range and slice selections are half-open intervals of the form `[low, high)`, i.e., low is included , high is excluded. Thus, ``tapers = [0, 1, 2]`` or ``tapers = slice(0, 3)`` selects the first up to (and including) the third taper. Selections can be unsorted and may include repetitions but must match exactly, be finite and not NaN. If `tapers` is `None` or ``tapers = "all"``, all tapers are selected. units : list (integers or strings), slice, range or None or "all" Unit-selection; can be a list of unit names (``['unit10', 'unit3']``), a list of unit indices (``[3, 5]``), a slice (``slice(3, 10)``) or range (``range(3, 10)``). Note that following Python conventions, units are counted starting at zero, and range and slice selections are half-open intervals of the form `[low, high)`, i.e., low is included , high is excluded. Thus, ``units = [0, 1, 2]`` or ``units = slice(0, 3)`` selects the first up to (and including) the third unit. Selections can be unsorted and may include repetitions but must match exactly, be finite and not NaN. If `units` is `None` or ``units = "all"``, all units are selected. eventids : list (integers), slice, range or None or "all" Event-ID-selection; can be a list of event-id codes (``[2, 0, 1]``), slice (``slice(0, 2)``) or range (``range(0, 2)``). Note that following Python conventions, range and slice selections are half-open intervals of the form `[low, high)`, i.e., low is included , high is excluded. Selections can be unsorted and may include repetitions but must match exactly, be finite and not NaN. If `eventids` is `None` or ``eventids = "all"``, all events are selected. Returns ------- dataselection : Syncopy data object Syncopy data object of the same type as `data` but containing only the subset specified by provided selectors. Notes ----- This routine represents a convenience function for creating new Syncopy objects based on existing data entities. However, in many situations, the creation of a new object (and thus the allocation of additional disk-space) might not be necessary: all Syncopy metafunctions, such as :func:`~syncopy.freqanalysis`, support **in-place** data selection. Consider the following example: assume `data` is an :class:`~syncopy.AnalogData` object representing 220 trials of LFP recordings containing baseline (between second -0.25 and 0) and stimulus-on data (on the interval [0.25, 0.5]). To compute the baseline spectrum, data-selection does **not** have to be performed before calling :func:`~syncopy.freqanalysis` but instead can be done in-place: >>> import syncopy as spy >>> cfg = spy.get_defaults(spy.freqanalysis) >>> cfg.method = 'mtmfft' >>> cfg.taper = 'dpss' >>> cfg.output = 'pow' >>> cfg.tapsmofrq = 10 >>> # define baseline/stimulus-on ranges >>> baseSelect = {"toilim": [-0.25, 0]} >>> stimSelect = {"toilim": [0.25, 0.5]} >>> # in-place selection of baseline interval performed by `freqanalysis` >>> cfg.select = baseSelect >>> baselineSpectrum = spy.freqanalysis(cfg, data) >>> # in-place selection of stimulus-on time-frame performed by `freqanalysis` >>> cfg.select = stimSelect >>> stimonSpectrum = spy.freqanalysis(cfg, data) Especially for large data-sets, in-place data selection performed by Syncopy's metafunctions does not only save disk-space but can significantly increase performance. Examples -------- Use :func:`~syncopy.tests.misc.generate_artificial_data` to create a synthetic :class:`syncopy.AnalogData` object. >>> from syncopy.tests.misc import generate_artificial_data >>> adata = generate_artificial_data(nTrials=10, nChannels=32) Assume a hypothetical trial onset at second 2.0 with the first second of each trial representing baseline recordings. To extract only the stimulus-on period from `adata`, one could use >>> stimon = spy.selectdata(adata, toilim=[2.0, np.inf]) Note that this is equivalent to >>> stimon = adata.selectdata(toilim=[2.0, np.inf]) See also -------- :meth:`syncopy.AnalogData.selectdata` : corresponding class method :meth:`syncopy.SpectralData.selectdata` : corresponding class method :meth:`syncopy.EventData.selectdata` : corresponding class method :meth:`syncopy.SpikeData.selectdata` : corresponding class method """ # Ensure our one mandatory input is usable try: data_parser(data, varname="data", empty=False) except Exception as exc: raise exc # If provided, make sure output object is appropriate if out is not None: try: data_parser(out, varname="out", writable=True, empty=True, dataclass=data.__class__.__name__, dimord=data.dimord) except Exception as exc: raise exc new_out = False else: out = data.__class__(dimord=data.dimord) new_out = True # Pass provided selections on to `Selector` class which performs error checking data._selection = {"trials": trials, "channels": channels, "toi": toi, "toilim": toilim, "foi": foi, "foilim": foilim, "tapers": tapers, "units": units, "eventids": eventids} # Create inventory of all available selectors and actually provided values # to create a bookkeeping dict for logging provided = locals() available = get_defaults(data.selectdata) actualSelection = {} for key in available: actualSelection[key] = provided[key] # Fire up `ComputationalRoutine`-subclass to do the actual selecting/copying selectMethod = DataSelection() selectMethod.initialize(data, chan_per_worker=kwargs.get("chan_per_worker")) selectMethod.compute(data, out, parallel=kwargs.get("parallel"), log_dict=actualSelection) # Wipe data-selection slot to not alter input object data._selection = None # Either return newly created output object or simply quit return out if new_out else None
def singlepanelplot(*data, trials="all", channels="all", tapers="all", toilim=None, foilim=None, avg_channels=True, avg_tapers=True, interp="spline36", cmap="plasma", vmin=None, vmax=None, title=None, grid=None, overlay=True, fig=None, **kwargs): """ Plot contents of Syncopy data object(s) using single-panel figure(s) **Usage Summary** List of Syncopy data objects and respective valid plotting commands/selectors: :class:`~syncopy.AnalogData` : trials, channels, toi/toilim Examples >>> fig1, fig2 = spy.singlepanelplot(data1, data2, channels=["channel1", "channel2"], overlay=False) >>> cfg = spy.StructDict() >>> cfg.trials = [5, 3, 0]; cfg.toilim = [0.25, 0.5] >>> fig = spy.singlepanelplot(cfg, data1, data2, overlay=True) :class:`~syncopy.SpectralData` : trials, channels, tapers, toi/toilim, foi/foilim Examples >>> fig1, fig2 = spy.singlepanelplot(data1, data2, channels=["channel1", "channel2"], tapers=[3, 0], foilim=[30, 80], avg_channels=False, avg_tapers=True, grid=True, overlay=False) >>> cfg = spy.StructDict() >>> cfg.trials = [1, 0, 3]; cfg.toilim = [-0.25, 0.5]; cfg.vmin=0.2; cfg.vmax=1.0 >>> fig = spy.singlepanelplot(cfg, tfData1) Parameters ---------- data : Syncopy data object(s) One or more non-empty Syncopy data object(s). **Note**: if multiple datasets are provided, they must be all of the same type (e.g., :class:`~syncopy.AnalogData`) and should contain the same or at least comparable channels, trials etc. Consequently, some keywords are only valid for certain types of Syncopy objects, e.g., `foilim` is not a valid plotting-selector for an :class:`~syncopy.AnalogData` object. trials : list (integers) or None or "all" Trials to average across. Either list of integers representing trial numbers (can include repetitions and need not be sorted), "all" or `None`. If `data` is a (series of) :class:`~syncopy.AnalogData` object(s), `trials` may be `None`, so that no trial information is used and the raw contents of provided input dataset(s) is plotted (**Warning**: depending on the size of the supplied dataset(s), this might be very memory-intensive). For all other Syncopy data objects, `trials` must not be `None`. channels : list (integers or strings), slice, range or "all" Channel-selection; can be a list of channel names (``['channel3', 'channel1']``), a list of channel indices (``[3, 5]``), a slice (``slice(3, 10)``) or range (``range(3, 10)``). Selections can be unsorted and may include repetitions. If multiple input objects are provided, `channels` needs to be a valid selector for all supplied datasets. tapers : list (integers or strings), slice, range or "all" Taper-selection; can be a list of taper names (``['dpss-win-1', 'dpss-win-3']``), a list of taper indices (``[3, 5]``), a slice (``slice(3, 10)``) or range (``range(3, 10)``). Selections can be unsorted and may include repetitions but must match exactly, be finite and not NaN. If multiple input objects are provided, `tapers` needs to be a valid selector for all supplied datasets. toilim : list (floats [tmin, tmax]) or None Time-window ``[tmin, tmax]`` (in seconds) to be extracted from each trial. Window specifications must be sorted and not NaN but may be unbounded. Boundaries `tmin` and `tmax` are included in the selection. If `toilim` is `None`, the entire time-span in each trial is selected. If multiple input objects are provided, `toilim` needs to be a valid selector for all supplied datasets. **Note** `toilim` is only a valid selector if `trials` is not `None`. foilim : list (floats [fmin, fmax]) or "all" Frequency-window ``[fmin, fmax]`` (in Hz) to be extracted from each trial; Window specifications must be sorted and not NaN but may be unbounded. Boundaries `fmin` and `fmax` are included in the selection. If `foilim` is `None` or all frequencies are selected for plotting. If multiple input objects are provided, `foilim` needs to be a valid selector for all supplied datasets. avg_channels : bool If `True`, plot input dataset(s) averaged across channels specified by `channels`. If `False`, no averaging is performed resulting in multiple plots, each representing a single channel. avg_tapers : bool If `True`, plot :class:`~syncopy.SpectralData` objects averaged across tapers specified by `tapers`. If `False`, no averaging is performed resulting in multiple plots, each representing a single taper. interp : str or None Interpolation method used for plotting two-dimensional contour maps such as time-frequency power spectra. To see a list of available interpolation methods use the command ``list(mpl.image._interpd_.keys())``. Please consult the matplotlib documentation for more details. Has no effect on line-plots. cmap : str Colormap used for plotting two-dimensional contour maps such as time-frequency power spectra. To see a list of available color-maps use the command ``list(mpl.cm._cmap_registry.keys())``. Pleasee consult the matplotlib documentation for more details. Has no effect on line-plots. vmin : float or None Lower bound of data-range covered by colormap when plotting two-dimensional contour maps such as time-frequency power spectra. If `vmin` is `None` the minimal (absolute) value of the shown dataset is used. When comparing multiple contour maps, all visualizations should use the same `vmin` to ensure quantitative similarity of peak values. vmax : float or None Upper bound of data-range covered by colormap when plotting two-dimensional contour maps such as time-frequency power spectra. If `vmax` is `None` the maximal (absolute) value of the shown dataset is used. When comparing multiple contour maps, all visualizations should use the same `vmin` to ensure quantitative similarity of peak values. title : str or None If `str`, `title` specifies as axis panel-title, if `None`, an auto-generated title is used. grid : bool or None If `True`, grid-lines are drawn, if `None` or `False` no grid-lines are rendered. overlay : bool If `True`, and multiple input objects were provided, supplied datasets are plotted on top of each other (in the order of submission). If a single object was provided, ``overlay = True`` and `fig` is a :class:`~matplotlib.figure.Figure`, the supplied dataset is overlaid on top of any existing plot(s) in `fig`. **Note 1**: using an existing figure to overlay dataset(s) is only supported for figures created with this routine. **Note 2**: overlay-plotting is *not* supported for time-frequency :class:`~syncopy.SpectralData` objects. fig : matplotlib.figure.Figure or None If `None`, new :class:`~matplotlib.figure.Figure` instance(s) are created for provided input dataset(s). If `fig` is a :class:`~matplotlib.figure.Figure`, the code attempts to overlay provided input dataset(s) on top of existing plots in `fig`. **Note**: overlay-plots are only supported for figures generated with this routine. Only a single figure can be provided. Thus, in case of multiple input datasets with ``overlay = False``, any supplied `fig` is ignored. Returns ------- fig : (list of) matplotlib.figure.Figure instance(s) Either single figure (single input dataset or multiple input datasets with ``overlay = True``) or list of figures (multiple input datasets and ``overlay = False``). Notes ----- This function uses `matplotlib <https://matplotlib.org/>`_ to render data visualizations. Thus, usage of Syncopy's plotting capabilities requires a working matplotlib installation. The actual rendering is performed by class methods specific to the provided input object types (e.g., :class:`~syncopy.AnalogData`). Thus, :func:`~syncopy.singlepanelplot` is mainly a convenience function and management routine that invokes the appropriate drawing code. Data subset selection for plotting is performed using :func:`~syncopy.selectdata`, thus additional in-place data-selection via a `select` keyword is **not** supported. Examples -------- Please refer to the respective `singlepanelplot` class methods for detailed usage examples specific to the respective Syncopy data object type. See also -------- :func:`~syncopy.multipanelplot` : visualize Syncopy objects using multi-panel figure(s) :meth:`syncopy.AnalogData.singlepanelplot` : `singlepanelplot` for :class:`~syncopy.AnalogData` objects :meth:`syncopy.SpectralData.singlepanelplot` : `singlepanelplot` for :class:`~syncopy.SpectralData` objects """ # Abort if matplotlib is not available: FIXME -> `_prep_plots`? if not __plt__: raise SPYError(pltErrMsg.format("singlepanelplot")) # Collect all keywords of corresponding class-method (w/possibly user-provided # values) in dictionary defaults = get_defaults(data[0].singlepanelplot) lcls = locals() kwords = {} for kword in defaults: kwords[kword] = lcls[kword] # Call plotting manager return _anyplot(*data, overlay=overlay, method="singlepanelplot", **kwords, **kwargs)