CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_ola.py479 linesDownload Raw Back to mne
1# Authors: The MNE-Python contributors.2# License: BSD-3-Clause3# Copyright the MNE-Python contributors.4 5import numpy as np6from scipy.signal import get_window7 8from .utils import _ensure_int, _validate_type, logger, verbose9 10###############################################################################11# Class for interpolation between adjacent points12 13 14class _Interp2:15    r"""Interpolate between two points.16 17    Parameters18    ----------19    control_points : array, shape (n_changes,)20        The control points (indices) to use.21    values : callable | array, shape (n_changes, ...)22        Callable that takes the control point and returns a list of23        arrays that must be interpolated.24    interp : str25        Can be 'zero', 'linear', 'hann', or 'cos2' (same as hann).26 27    Notes28    -----29    This will process data using overlapping windows of potentially30    different sizes to achieve a constant output value using different31    2-point interpolation schemes. For example, for linear interpolation,32    and window sizes of 6 and 17, this would look like::33 34        1 _     _35          |\   / '-.           .-'36          | \ /     '-.     .-'37          |  x         |-.-|38          | / \     .-'     '-.39          |/   \_.-'           '-.40        0 +----|----|----|----|---41          0    5   10   15   20   2542 43    """44 45    def __init__(self, control_points, values, interp="hann", *, name="Interp2"):46        # set up interpolation47        self.control_points = np.array(control_points, int).ravel()48        if not np.array_equal(np.unique(self.control_points), self.control_points):49            raise ValueError("Control points must be sorted and unique")50        if len(self.control_points) == 0:51            raise ValueError("Must be at least one control point")52        if not (self.control_points >= 0).all():53            raise ValueError(54                f"All control points must be positive (got {self.control_points[:3]})"55            )56        if isinstance(values, np.ndarray):57            values = [values]58        if isinstance(values, list | tuple):59            for v in values:60                if not (v is None or isinstance(v, np.ndarray)):61                    raise TypeError(62                        'All entries in "values" must be ndarray or None, got '63                        f"{type(v)}"64                    )65                if v is not None and v.shape[0] != len(self.control_points):66                    raise ValueError(67                        "Values, if provided, must be the same length as the number of "68                        f"control points ({len(self.control_points)}), got {v.shape[0]}"69                    )70            use_values = values71 72            def val(pt):73                idx = np.where(control_points == pt)[0][0]74                return [v[idx] if v is not None else None for v in use_values]75 76            values = val77        self.values = values78        self.n_last = None79        self._position = 0  # start at zero80        self._left_idx = 081        self._left = self._right = self._use_interp = None82        self.name = name83        known_types = ("cos2", "linear", "zero", "hann")84        if interp not in known_types:85            raise ValueError(f'interp must be one of {known_types}, got "{interp}"')86        self._interp = interp87 88    def feed_generator(self, n_pts):89        """Feed data and get interpolators as a generator."""90        self.n_last = 091        n_pts = _ensure_int(n_pts, "n_pts")92        original_position = self._position93        stop = self._position + n_pts94        logger.debug(f"    ~ {self.name} Feed {n_pts} ({self._position}-{stop})")95        used = np.zeros(n_pts, bool)96        if self._left is None:  # first one97            logger.debug(f"    ~   {self.name} Eval @ 0 ({self.control_points[0]})")98            self._left = self.values(self.control_points[0])99            if len(self.control_points) == 1:100                self._right = self._left101        n_used = 0102 103        # Left zero-order hold condition104        if self._position < self.control_points[self._left_idx]:105            n_use = min(self.control_points[self._left_idx] - self._position, n_pts)106            logger.debug(f"    ~   {self.name} Left ZOH {n_use}")107            this_sl = slice(None, n_use)108            assert used[this_sl].size == n_use109            assert not used[this_sl].any()110            used[this_sl] = True111            yield [this_sl, self._left, None, None]112            self._position += n_use113            n_used += n_use114            self.n_last += 1115 116        # Standard interpolation condition117        stop_right_idx = np.where(self.control_points >= stop)[0]118        if len(stop_right_idx) == 0:119            stop_right_idx = [len(self.control_points) - 1]120        stop_right_idx = stop_right_idx[0]121        left_idxs = np.arange(self._left_idx, stop_right_idx)122        self.n_last += max(len(left_idxs) - 1, 0)123        for bi, left_idx in enumerate(left_idxs):124            if left_idx != self._left_idx or self._right is None:125                if self._right is not None:126                    assert left_idx == self._left_idx + 1127                    self._left = self._right128                    self._left_idx += 1129                    self._use_interp = None  # need to recreate it130                eval_pt = self.control_points[self._left_idx + 1]131                logger.debug(132                    f"    ~   {self.name} Eval @ {self._left_idx + 1} ({eval_pt})"133                )134                self._right = self.values(eval_pt)135            assert self._right is not None136            left_point = self.control_points[self._left_idx]137            right_point = self.control_points[self._left_idx + 1]138            if self._use_interp is None:139                interp_span = right_point - left_point140                if self._interp == "zero":141                    self._use_interp = None142                elif self._interp == "linear":143                    self._use_interp = np.linspace(144                        1.0, 0.0, interp_span, endpoint=False145                    )146                else:  # self._interp in ('cos2', 'hann'):147                    self._use_interp = np.cos(148                        np.linspace(0, np.pi / 2.0, interp_span, endpoint=False)149                    )150                    self._use_interp *= self._use_interp151            n_use = min(stop, right_point) - self._position152            if n_use > 0:153                logger.debug(154                    f"    ~   {self.name} Interp {self._interp} {n_use} "155                    f"({left_point}-{right_point})"156                )157                interp_start = self._position - left_point158                assert interp_start >= 0159                if self._use_interp is None:160                    this_interp = None161                else:162                    this_interp = self._use_interp[interp_start : interp_start + n_use]163                    assert this_interp.size == n_use164                this_sl = slice(n_used, n_used + n_use)165                assert used[this_sl].size == n_use166                assert not used[this_sl].any()167                used[this_sl] = True168                yield [this_sl, self._left, self._right, this_interp]169                self._position += n_use170                n_used += n_use171 172        # Right zero-order hold condition173        if self.control_points[self._left_idx] <= self._position:174            n_use = stop - self._position175            if n_use > 0:176                logger.debug(f"    ~   {self.name} Right ZOH %s" % n_use)177                this_sl = slice(n_pts - n_use, None)178                assert not used[this_sl].any()179                used[this_sl] = True180                assert self._right is not None181                yield [this_sl, self._right, None, None]182                self._position += n_use183                n_used += n_use184                self.n_last += 1185        assert self._position == stop186        assert n_used == n_pts187        assert used.all()188        assert self._position == original_position + n_pts189 190    def feed(self, n_pts):191        """Feed data and get interpolated values."""192        # Convenience function for assembly193        out_arrays = None194        for o in self.feed_generator(n_pts):195            if out_arrays is None:196                out_arrays = [197                    np.empty(v.shape + (n_pts,)) if v is not None else None198                    for v in o[1]199                ]200            for ai, arr in enumerate(out_arrays):201                if arr is not None:202                    if o[3] is None:203                        arr[..., o[0]] = o[1][ai][..., np.newaxis]204                    else:205                        arr[..., o[0]] = o[1][ai][..., np.newaxis] * o[3] + o[2][ai][206                            ..., np.newaxis207                        ] * (1.0 - o[3])208        assert out_arrays is not None209        return out_arrays210 211 212###############################################################################213# Constant overlap-add processing class214 215 216def _check_store(store):217    _validate_type(store, (np.ndarray, list, tuple, _Storer), "store")218    if isinstance(store, np.ndarray):219        store = [store]220    if not isinstance(store, _Storer):221        if not all(isinstance(s, np.ndarray) for s in store):222            raise TypeError("All instances must be ndarrays")223        store = _Storer(*store)224    return store225 226 227class _COLA:228    r"""Constant overlap-add processing helper.229 230    Parameters231    ----------232    process : callable233        A function that takes a chunk of input data with shape234        ``(n_channels, n_samples)`` and processes it.235    store : ndarray | list of ndarray | _Storer236        The output data in which to store the results.237    n_total : int238        The total number of samples.239    n_samples : int240        The number of samples per window.241    n_overlap : int242        The overlap between windows.243    window : str244        The window to use. Default is "hann".245    tol : float246        The tolerance for COLA checking.247 248    Notes249    -----250    This will process data using overlapping windows to achieve a constant251    output value. For example, for ``n_total=27``, ``n_samples=10``,252    ``n_overlap=5`` and ``window='triang'``::253 254        1 _____               _______255          |    \   /\   /\   /256          |     \ /  \ /  \ /257          |      x    x    x258          |     / \  / \  / \259          |    /   \/   \/   \260        0 +----|----|----|----|----|-261          0    5   10   15   20   25262 263    This produces four windows: the first three are the requested length264    (10 samples) and the last one is longer (12 samples). The first and last265    window are asymmetric.266    """267 268    @verbose269    def __init__(270        self,271        process,272        store,273        n_total,274        n_samples,275        n_overlap,276        sfreq,277        window="hann",278        tol=1e-10,279        *,280        name="COLA",281        verbose=None,282    ):283        n_samples = _ensure_int(n_samples, "n_samples")284        n_overlap = _ensure_int(n_overlap, "n_overlap")285        n_total = _ensure_int(n_total, "n_total")286        if n_samples <= 0:287            raise ValueError(f"n_samples must be > 0, got {n_samples}")288        if n_overlap < 0:289            raise ValueError(f"n_overlap must be >= 0, got {n_overlap}")290        if n_total < 0:291            raise ValueError(f"n_total must be >= 0, got {n_total}")292        self._n_samples = int(n_samples)293        self._n_overlap = int(n_overlap)294        del n_samples, n_overlap295        if n_total < self._n_samples:296            raise ValueError(297                f"Number of samples per window ({self._n_samples}) must be at "298                f"most the total number of samples ({n_total})"299            )300        if not callable(process):301            raise TypeError(f"process must be callable, got type {type(process)}")302        self._process = process303        self._step = self._n_samples - self._n_overlap304        self._store = _check_store(store)305        self._idx = 0306        self._in_buffers = self._out_buffers = None307        self.name = name308 309        # Create our window boundaries310        window_name = window if isinstance(window, str) else "custom"311        self._window = get_window(312            window, self._n_samples, fftbins=bool((self._n_samples - 1) % 2)313        )314        self._window /= _check_cola(315            self._window, self._n_samples, self._step, window_name, tol=tol316        )317        self.starts = np.arange(0, n_total - self._n_samples + 1, self._step)318        self.stops = self.starts + self._n_samples319        delta = n_total - self.stops[-1]320        self.stops[-1] = n_total321        sfreq = float(sfreq)322        pl = "s" if len(self.starts) != 1 else ""323        logger.info(324            f"    Processing {len(self.starts):4d} data chunk{pl} of (at least) "325            f"{self._n_samples / sfreq:0.1f} s with "326            f"{self._n_overlap / sfreq:0.1f} s overlap and {window_name} windowing"327        )328        del window, window_name329        if delta > 0:330            logger.info(331                f"    The final {delta / sfreq} s will be lumped into the final window"332            )333 334    @property335    def _in_offset(self):336        """Compute from current processing window start and buffer len."""337        return self.starts[self._idx] + self._in_buffers[0].shape[-1]338 339    @verbose340    def feed(self, *datas, verbose=None, **kwargs):341        """Pass in a chunk of data."""342        # Append to our input buffer343        if self._in_buffers is None:344            self._in_buffers = [None] * len(datas)345        if len(datas) != len(self._in_buffers):346            raise ValueError(347                f"Got {len(datas)} array(s), needed {len(self._in_buffers)}"348            )349        current_offset = 0  # should be updated below350        for di, data in enumerate(datas):351            if not isinstance(data, np.ndarray) or data.ndim < 1:352                raise TypeError(353                    f"data entry {di} must be an 2D ndarray, got {type(data)}"354                )355            if self._in_buffers[di] is None:356                # In practice, users can give large chunks, so we use357                # dynamic allocation of the in buffer. We could save some358                # memory allocation by only ever processing max_len at once,359                # but this would increase code complexity.360                self._in_buffers[di] = np.empty(data.shape[:-1] + (0,), data.dtype)361            if (362                data.shape[:-1] != self._in_buffers[di].shape[:-1]363                or self._in_buffers[di].dtype != data.dtype364            ):365                raise TypeError(366                    f"data must dtype {self._in_buffers[di].dtype} and "367                    f"shape[:-1]=={self._in_buffers[di].shape[:-1]}, got dtype "368                    f"{data.dtype} shape[:-1]={data.shape[:-1]}"369                )370            # This gets updated on first iteration, so store it before it updates371            if di == 0:372                current_offset = self._in_offset373            logger.debug(374                f"    + {self.name}[{di}] Appending  "375                f"{current_offset}:{current_offset + data.shape[-1]}"376            )377            self._in_buffers[di] = np.concatenate([self._in_buffers[di], data], -1)378            if self._in_offset > self.stops[-1]:379                raise ValueError(380                    f"data (shape {data.shape}) exceeded expected total buffer size ("381                    f"{self._in_offset} > {self.stops[-1]})"382                )383        # Check to see if we can process the next chunk and dump outputs384        while self._idx < len(self.starts) and self._in_offset >= self.stops[self._idx]:385            start, stop = self.starts[self._idx], self.stops[self._idx]386            this_len = stop - start387            this_window = self._window.copy()388            if self._idx == len(self.starts) - 1:389                this_window = np.pad(390                    self._window, (0, this_len - len(this_window)), "constant"391                )392                for offset in range(self._step, len(this_window), self._step):393                    n_use = len(this_window) - offset394                    this_window[offset:] += self._window[:n_use]395            if self._idx == 0:396                for offset in range(self._n_samples - self._step, 0, -self._step):397                    this_window[:offset] += self._window[-offset:]398            this_proc = [in_[..., :this_len].copy() for in_ in self._in_buffers]399            logger.debug(400                f"    * {self.name}[:] Processing {start}:{stop} "401                f"(e.g., {this_proc[0].flat[[0, -1]]})"402            )403            if not all(404                proc.shape[-1] == this_len == this_window.size for proc in this_proc405            ):406                raise RuntimeError("internal indexing error")407            start = self._store.idx408            stop = self._store.idx + this_len409            outs = self._process(*this_proc, start=start, stop=stop, **kwargs)410            if self._out_buffers is None:411                max_len = np.max(self.stops - self.starts)412                self._out_buffers = [413                    np.zeros(o.shape[:-1] + (max_len,), o.dtype) for o in outs414                ]415            for oi, out in enumerate(outs):416                out *= this_window417                self._out_buffers[oi][..., : stop - start] += out418            self._idx += 1419            if self._idx < len(self.starts):420                next_start = self.starts[self._idx]421            else:422                next_start = self.stops[-1]423            delta = next_start - self.starts[self._idx - 1]424            logger.debug(425                f"    + {self.name}[:] Shifting input and output buffers by "426                f"{delta} samples (storing {start}:{stop})"427            )428            for di in range(len(self._in_buffers)):429                self._in_buffers[di] = self._in_buffers[di][..., delta:]430            self._store(*[o[..., :delta] for o in self._out_buffers])431            for ob in self._out_buffers:432                ob[..., :-delta] = ob[..., delta:]433                ob[..., -delta:] = 0.0434 435 436def _check_cola(win, nperseg, step, window_name, tol=1e-10):437    """Check whether the Constant OverLap Add (COLA) constraint is met."""438    # adapted from SciPy439    binsums = np.sum(440        [win[ii * step : (ii + 1) * step] for ii in range(nperseg // step)], axis=0441    )442    if nperseg % step != 0:443        binsums[: nperseg % step] += win[-(nperseg % step) :]444    const = np.median(binsums)445    deviation = np.max(np.abs(binsums - const))446    if deviation > tol:447        raise ValueError(448            f"segment length {nperseg} with step {step} for {window_name} "449            "window type does not provide a constant output "450            f"({100 * deviation / const:g}% deviation)"451        )452    return const453 454 455class _Storer:456    """Store data in chunks."""457 458    def __init__(self, *outs, picks=None):459        for oi, out in enumerate(outs):460            if not isinstance(out, np.ndarray) or out.ndim < 1:461                raise TypeError(f"outs[oi] must be >= 1D ndarray, got {out}")462        self.outs = outs463        self.idx = 0464        self.picks = picks465 466    def __call__(self, *outs):467        if len(outs) != len(self.outs) or not all(468            out.shape[-1] == outs[0].shape[-1] for out in outs469        ):470            raise ValueError("Bad outs")471        idx = (Ellipsis,)472        if self.picks is not None:473            idx += (self.picks,)474        stop = self.idx + outs[0].shape[-1]475        idx += (slice(self.idx, stop),)476        for o1, o2 in zip(self.outs, outs):477            o1[idx] = o2478        self.idx = stop479 
Aluode/PerceptionLabPortable · CoolFace