Source code for tad.metrics.avalanches

from __future__ import annotations

from dataclasses import dataclass
from typing import List, Optional, Sequence, Tuple

import numpy as np

from tad.raster import Raster, ChannelId


[docs] @dataclass(frozen=True) class AvalancheResult: """ Result of avalanche extraction on a raster. Parameters ---------- sizes Avalanche sizes, one per avalanche. Definition depends on `size_definition`. lifetimes Avalanche lifetimes in number of bins (Δt units), one per avalanche. dt Bin width used to define avalanches. tstart Start time of analysis window used for binning. tstop Stop time of analysis window used for binning. channels Channel IDs included in the analysis (row order of internal matrices). size_definition Size definition used: - 1: sum over bins of (# active electrodes in bin) - 2: number of distinct electrodes active at least once in avalanche bin_edges Bin edges used, shape (n_bins+1,). active_bins Boolean array indicating which bins were active (at least one spike in any channel), shape (n_bins,). intervals_bins List of (start_bin, stop_bin) bin indices for each avalanche, where stop_bin is exclusive. """ sizes: np.ndarray lifetimes: np.ndarray dt: float tstart: float tstop: float channels: List[ChannelId] size_definition: int bin_edges: np.ndarray active_bins: np.ndarray intervals_bins: List[Tuple[int, int]]
[docs] def extract_avalanches( r: Raster, dt: float, *, tstart: Optional[float] = None, tstop: Optional[float] = None, channels: Optional[Sequence[ChannelId]] = None, inclusive_stop: bool = False, size_definition: int = 1, ) -> AvalancheResult: """ Extract neuronal avalanches (sizes and lifetimes) from a raster. This follows the definitions in the provided references: - Bin time into windows of width Δt. - A bin is "active" if at least one electrode fires ≥1 spike in that bin; "blank" otherwise. - An avalanche is a maximal contiguous sequence of active bins bounded by blank bins. - Lifetime = number of consecutive active bins. - Size: (1) sum over bins of number of active electrodes in that bin (multiple activations counted) (2) number of distinct electrodes that were active at least once during the avalanche Parameters ---------- r Input raster. dt Bin width (Δt). Must be positive. tstart, tstop Optional analysis window. If None, inferred from the data in the selected channels. (Inference follows your Raster.bin_counts behavior.) channels Channels to include. If None, uses all channels in the raster. inclusive_stop Whether to include events at exactly tstop in binning window. size_definition 1 or 2, as above. Returns ------- AvalancheResult Extracted avalanche sizes, lifetimes, and supporting metadata. Raises ------ ValueError If `size_definition` is not 1 or 2. """ if size_definition not in (1, 2): raise ValueError(f"size_definition must be 1 or 2, got {size_definition!r}.") # Use the canonical binning primitive from Raster bin_edges, counts, ch_list = r.bin_counts( dt, tstart=tstart, tstop=tstop, channels=channels, inclusive_stop=inclusive_stop, ) # counts: shape (n_channels, n_bins) n_ch, n_bins = counts.shape # Per-channel activity mask per bin active_ch_bin = counts > 0 # bool, (n_ch, n_bins) active_bins = np.any(active_ch_bin, axis=0) # bool, (n_bins,) # Avalanche segments: contiguous True regions bounded by False. # Treat outside-window as blank by padding with False on both sides. padded = np.concatenate([[False], active_bins, [False]]).astype(bool, copy=False) transitions = np.flatnonzero(padded[1:] != padded[:-1]) # transitions come in pairs: start (False->True), stop (True->False), in padded coordinates # Convert to bin indices in [0, n_bins) intervals_bins: List[Tuple[int, int]] = [] for k in range(0, transitions.size, 2): start_p = int(transitions[k]) # index in padded where change occurs stop_p = int(transitions[k + 1]) start_bin = start_p # because padded has one leading element stop_bin = stop_p # exclusive # In terms of original active_bins indexing: [start_bin, stop_bin) intervals_bins.append((start_bin, stop_bin)) # Compute lifetimes and sizes lifetimes = np.zeros(len(intervals_bins), dtype=np.int64) sizes = np.zeros(len(intervals_bins), dtype=np.int64) for i, (b0, b1) in enumerate(intervals_bins): seg_len = b1 - b0 lifetimes[i] = seg_len seg = active_ch_bin[:, b0:b1] # bool (n_ch, seg_len) if size_definition == 1: # sum over bins of # active electrodes in bin # i.e., sum_{bin} sum_{ch} 1[ch active in bin] sizes[i] = int(np.sum(seg)) else: # number of distinct electrodes active at least once in avalanche sizes[i] = int(np.sum(np.any(seg, axis=1))) # Determine actual window used for metadata from edges tstart_used = float(bin_edges[0]) if bin_edges.size else 0.0 tstop_used = float(bin_edges[-1]) if bin_edges.size else 0.0 return AvalancheResult( sizes=sizes, lifetimes=lifetimes, dt=float(dt), tstart=tstart_used, tstop=tstop_used, channels=list(ch_list), size_definition=size_definition, bin_edges=bin_edges, active_bins=active_bins, intervals_bins=intervals_bins, )