CASCADE: criticality avalanche spike cross-platform analysis detection engine, a multi-manufacturer MEA bash analysis pipeline.
The 10 matches
- [1] § 2 Materials and methods ↔ cascade/core/spikes.py, lines 37–88 · score 0.94 · refractory period violation, electrical noise, inter spike intervals, maximum firing rate, electrode filter, removes channels
- [2] § 2 Materials and methods ↔ cascade/core/avalanche.py, lines 1–28 · score 0.83 · Neuronal avalanche detection, empty bins, population activity, percentile threshold, inter, consecutive
- [3] § 2 Materials and methods ↔ cascade/run_analysis.py, lines 220–281 · score 0.83 · refractory period violation, electrical noise, inter spike intervals, electrode filter, removes channels, RPV
- [4] § 2 Materials and methods ↔ cascade/utils/io.py, lines 60–118 · score 0.82 · population activity percentile, power law, duration exponent, SCE, BR, DCC
- [5] § 3 Result ↔ cascade/run_analysis.py, lines 1–72 · score 0.65 · percentile threshold, firing rate, post, CL1, ae, MCS
- [6] § 2 Materials and methods ↔ cascade/run_analysis.py, lines 326–367 · score 0.63 · Pearson correlation, spatial entropy, correlation threshold, active electrodes, connectivity, consecutive
- [7] § 2 Materials and methods ↔ cascade/loaders/cl1_loader.py, lines 1–38 · score 0.62 · Cortical Lab, game recording, CL1, loaders, CASCADE, spike
- [8] § 2 Materials and methods ↔ cascade/core/bursts.py, lines 13–54 · score 0.58 · population spike train, Network bursts
- [9] § 2 Materials and methods ↔ cascade/run_analysis.py, lines 1–72 · score 0.58 · Cortical Lab, unified, CL1, bxr, MCS, Axion
- [10] § 2 Materials and methods ↔ cascade/core/connectivity.py, lines 1–8 · score 0.55 · Pearson correlation, Functional connectivity
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Python · 706 lines · 38 KB · Apache-2.0 · 4 matches
- #!/usr/bin/env python
- """
- run_analysis.py
- ---------------
- Unified MEA analysis pipeline.
- Supports 3Brain (.bxr), Maxwell (.h5), MCS (.h5), Cortical Labs (.h5),
- and Axion BioSystems (.csv) files.
- Adding a new manufacturer = write one new loader in loaders/ only.
- Usage examples
- --------------
- # 3Brain file:
- python run_analysis.py --input recording.bxr --manufacturer 3brain --output results/
- # Maxwell file:
- python run_analysis.py --input data.raw.h5 --manufacturer maxwell --output results/
- # CL1 file:
- python run_analysis.py --input recording.h5 --manufacturer cl --output results/
- # MCS file:
- python run_analysis.py --input recording.h5 --manufacturer mcs --output results/
- # Axion spike-list CSV:
- python run_analysis.py --input recording_spike_list.csv --manufacturer axion --output results/
- # With active-electrode filter and Habibollahi threshold:
- python run_analysis.py --input recording.bxr --manufacturer 3brain --ae 5 --perc 0.25 --output results/
- # With epoch comparison (pre vs post stimulation):
- python run_analysis.py --input recording.bxr --manufacturer 3brain --epochA 0,60 --epochB 120,60 --output results/
- Flags
- -----
- --input path to data file (auto-searched if omitted)
- --manufacturer 3brain | maxwell | cl | mcs | axion (required)
- --well well/stream name (default: first available)
- --start start of analysis window in seconds (default: 0)
- --dur duration of analysis window in seconds (default: full recording)
- --output output folder (default: mea_out)
- --bin bin size for firing rates in seconds (default: 1.0)
- --ae active-electrode filter: keep channels with >= X spikes/min (default: 0 = off)
- --max_rate noisy-electrode filter: remove channels with > X spikes/min (default: 0 = off)
- recommended: 1000 (≈ 17 Hz) for in-vitro MEA
- --max_rpv noisy-electrode filter: remove channels with > X% of ISIs < 2 ms (default: 0 = off)
- recommended: 1.0–2.0% for in-vitro MEA
- --ava_size minimum avalanche size in spikes (default: 2)
- --ava_dur minimum avalanche duration in bins (default: 1 — no duration filter)
- --perc percentile threshold for avalanche detection (default: 0.0 = off)
- 0.0 = any spike counts; 0.25 = Habibollahi 2022 default
- --aval_dt avalanche bin width in seconds (default: inferred from mean ISI)
- --burst_min_spikes min spikes in a run to count as a network burst (default: 5)
- --burst_max_gap max gap between consecutive spikes within a burst in seconds (default: 0.1)
- --burst_min upper xmin limit for size power-law fit (default: 12)
- --t_min upper xmin limit for duration power-law fit (default: 4)
- --conn_thr |r| threshold for connectivity edges (default: 0.3)
- --entropy_win window size for entropy and active-area curves in seconds (default: 10)
- --sc_Dmin shape collapse: min avalanche duration in bins (default: 4)
- --sc_Dmax shape collapse: max avalanche duration in bins (default: 20)
- --epochA epoch A window: 'start,duration' in seconds e.g. '0,60'
- --epochB epoch B window: 'start,duration' in seconds e.g. '120,60'
- --svg also save all figures as SVG (default: PNG only)
- --jpg also save all figures as JPG (default: PNG only)
- --verbose print progress messages
- """
- import argparse
- import glob
- import os
- import sys
- import numpy as np
- import pandas as pd
- # ── imports from this package ──────────────────────────────────────────────────
- from cascade.loaders.brainwave_loader import BrainwaveLoader
- from cascade.loaders.maxwell_loader import MaxwellLoader
- from cascade.loaders.cl1_loader import CL1Loader
- from cascade.loaders.mcs_loader import MCSLoader
- from cascade.loaders.axion_loader import AxionLoader
- from cascade.core.spikes import filter_active_electrodes, filter_noisy_electrodes, bin_spikes, compute_isi_metrics
- from cascade.core.bursts import compute_burst_metrics, compute_active_area
- from cascade.core.avalanche import infer_dt, compute_avalanches, counts_per_dt
- from cascade.core.criticality import compute_exponents, compute_branching_ratio, shape_collapse
- from cascade.core.connectivity import compute_connectivity, summarize_connectivity
- from cascade.core.entropy import compute_entropy
- from cascade.core.plots import (plot_raster, plot_rate_heatmap, plot_population_rate,
- plot_burst_timeline, plot_active_area_curve,
- plot_waveforms, plot_corr_heatmap, plot_degree_histogram,
- plot_avalanche_distributions, plot_loglog_linearization,
- plot_shape_collapse, plot_entropy_curve, plot_epoch_compare)
- from cascade.utils.common import say, banner, step, CYAN, GRN, YEL, RED
- from cascade.utils.io import write_csv, write_tsv, write_criticality_report, write_run_log, _Tee
- # ── manufacturer auto-detection ────────────────────────────────────────────────
- def _peek_h5_manufacturer(path: str) -> str:
- """Peek inside an HDF5 file to distinguish CL1 / MCS / Maxwell."""
- try:
- import h5py
- with h5py.File(path, "r") as h5:
- sid = h5.attrs.get("system_id", b"")
- if isinstance(sid, (bytes, np.bytes_)):
- sid = sid.decode("ascii", errors="replace")
- if str(sid).upper().startswith("CL1"):
- return "cl"
- if "McsHdf5ProtocolType" in h5.attrs:
- return "mcs"
- except Exception:
- pass
- return "maxwell"
- def detect_manufacturer(path: str) -> str:
- ext = os.path.splitext(path)[1].lower()
- if ext == ".bxr":
- return "3brain"
- if ext in (".h5", ".hdf5"):
- return _peek_h5_manufacturer(path)
- return "unknown"
- def get_loader(manufacturer: str):
- if manufacturer == "3brain":
- return BrainwaveLoader()
- if manufacturer == "maxwell":
- return MaxwellLoader()
- if manufacturer == "cl":
- return CL1Loader()
- if manufacturer == "mcs":
- return MCSLoader()
- if manufacturer == "axion":
- return AxionLoader()
- raise ValueError(f"Unknown manufacturer '{manufacturer}'. "
- f"Supported: 3brain, maxwell, cl, mcs, axion")
- # ── epoch comparison helper ────────────────────────────────────────────────────
- def _parse_epoch(s: str):
- try:
- a, b = s.split(",")
- return float(a), float(b)
- except Exception:
- return None
- def _epoch_stats(spike_times: dict, start: float, dur: float,
- all_channels: list) -> dict:
- end = start + dur
- sub = {ch: t[(t >= start) & (t < end)]
- for ch, t in spike_times.items()}
- all_t = np.concatenate(list(sub.values())) if sub else np.array([])
- pop_rate = 0.0
- if all_t.size > 0 and dur > 0:
- pop_rate = float(all_t.size / dur / max(len(all_channels), 1))
- n_active = sum(1 for t in sub.values() if t.size > 0)
- n_total = max(len(all_channels), 1)
- return dict(pop_rate=pop_rate,
- active_area=float(n_active / n_total))
- # ── main pipeline ──────────────────────────────────────────────────────────────
- def main():
- ap = argparse.ArgumentParser(
- prog="cascade",
- description=(
- "CASCADE — Criticality Avalanche Spike Cross-platform Analysis "
- "Detection Engine\n"
- "Unified MEA analysis pipeline supporting multiple manufacturers.\n\n"
- "Outputs: raster plot, rate heatmap, burst detection, active area,\n"
- " connectivity, neuronal avalanches, criticality metrics\n"
- " (DCC, branching ratio, shape collapse), entropy.\n\n"
- "Other CASCADE tools:\n"
- " cascade-info Quick recording inspector (duration, channels,\n"
- " active electrodes, time-window support)\n"
- " Usage: python scanner.py --help\n\n"
- " cascade-ltpltd LTP/LTD classification from firing-rate CSVs\n"
- " Usage: python ltpltd.py --help\n\n"
- "Example:\n"
- " cascade --input recording.bxr --output results/ --ae 5 --ava_size 2 --verbose"
- ),
- formatter_class=argparse.RawDescriptionHelpFormatter
- )
- ap.add_argument("--version", action="version", version="CASCADE v0.8.0")
- ap.add_argument("--test", action="store_true",
- help="Run the built-in self-test with synthetic data. "
- "No recording file required.")
- # ── input / output ─────────────────────────────────────────────────────────
- io_group = ap.add_argument_group("Input / Output")
- io_group.add_argument("--input",
- default=None,
- help="Path to the data file (.bxr or .h5). "
- "If omitted, CASCADE searches the current folder.")
- io_group.add_argument("--manufacturer",
- required=False, default=None,
- choices=["3brain", "maxwell", "cl", "mcs", "axion"],
- help="MEA manufacturer (required except with --test). "
- "3brain (.bxr), maxwell (.h5), cl (.h5), mcs (.h5), axion (.csv)")
- io_group.add_argument("--well",
- default=None,
- help="Well or stream name to analyse (e.g. Well_A1 for 3Brain, "
- "data0000 for Maxwell). Default: first available well.")
- io_group.add_argument("--output",
- default="mea_out",
- help="Output folder for all figures and data files. "
- "Created automatically if it does not exist. (default: mea_out)")
- io_group.add_argument("--svg",
- action="store_true",
- help="Also save all figures as SVG.")
- io_group.add_argument("--jpg",
- action="store_true",
- help="Also save all figures as JPG (300 dpi).")
- io_group.add_argument("--verbose",
- action="store_true",
- help="Print detailed progress messages to the terminal.")
- # ── recording window ───────────────────────────────────────────────────────
- win_group = ap.add_argument_group("Recording Window")
- win_group.add_argument("--start",
- type=float, default=0.0,
- help="Start of the analysis window in seconds. (default: 0)")
- win_group.add_argument("--dur",
- type=float, default=None,
- help="Duration of the analysis window in seconds. "
- "Default: full recording length.")
- # ── spike filtering ────────────────────────────────────────────────────────
- spike_group = ap.add_argument_group("Spike Filtering")
- spike_group.add_argument("--ae",
- type=float, default=0.0,
- help="Active electrode filter. Keep only channels that fired "
- "at least X spikes per minute. Set to 0 to keep all "
- "channels. (default: 0 = off)")
- spike_group.add_argument("--max_rate",
- type=float, default=0.0,
- help="Noisy-electrode filter: remove channels whose firing rate "
- "exceeds this value (spikes per minute). Electrodes with "
- "abnormally high rates are typically recording electrical "
- "noise rather than real neurons. Set to 0 to disable. "
- "Recommended starting value: 1000 (≈ 17 Hz). (default: 0 = off)")
- spike_group.add_argument("--max_rpv",
- type=float, default=0.0,
- help="Noisy-electrode filter: remove channels where more than "
- "this percentage of inter-spike intervals are shorter than "
- "2 ms (refractory period violation). Real neurons cannot "
- "fire faster than ~500 Hz; a high violation rate indicates "
- "electrical noise. Set to 0 to disable. "
- "Recommended starting value: 1.0 (1%%). (default: 0 = off)")
- spike_group.add_argument("--bin",
- type=float, default=1.0,
- help="Bin size in seconds for computing per-channel firing "
- "rates (used for heatmap and connectivity). (default: 1.0)")
- # ── avalanche detection ────────────────────────────────────────────────────
- ava_group = ap.add_argument_group("Avalanche Detection")
- ava_group.add_argument("--ava_size",
- type=int, default=2,
- help="Minimum avalanche size (total spikes). Avalanches with "
- "fewer than X spikes are discarded. (default: 2)")
- ava_group.add_argument("--ava_dur",
- type=int, default=1,
- help="Minimum avalanche duration (bins). Avalanches spanning "
- "fewer than X bins are discarded. Default is 1 (no "
- "duration filter) because single-bin events are valid "
- "avalanches — they represent synchronous bursts where "
- "many channels fire within one Δt window. (default: 1)")
- ava_group.add_argument("--aval_dt",
- type=float, default=None,
- help="Avalanche bin width in seconds. Default: automatically "
- "inferred from the mean inter-spike interval (ISI) of the "
- "population, clipped to 1–50 ms.")
- ava_group.add_argument("--perc",
- type=float, default=0.0,
- help="Percentile threshold for avalanche detection. The bin "
- "count must exceed this percentile of all bin counts to "
- "be included in an avalanche. 0.0 = any spike counts "
- "(recommended for clean in-vitro spike data). 0.25 = "
- "Habibollahi 2022 default for noisy recordings. (default: 0.0)")
- # ── criticality metrics ────────────────────────────────────────────────────
- crit_group = ap.add_argument_group("Criticality Metrics")
- crit_group.add_argument("--burst_min",
- type=int, default=12,
- help="Upper limit for the xmin search in the avalanche SIZE "
- "power-law fit. The algorithm tries xmin values from 1 "
- "to this number and picks the best one. Recommended: "
- "8–20. (default: 12)")
- crit_group.add_argument("--t_min",
- type=int, default=4,
- help="Upper limit for the xmin search in the avalanche "
- "DURATION power-law fit. Recommended: 3–6. (default: 4)")
- crit_group.add_argument("--pl_xmin_S",
- type=float, default=None,
- help="Manually override the xmin for the size power-law fit. "
- "Default: auto-detected. Use this if the automatic result "
- "looks wrong on your data.")
- crit_group.add_argument("--pl_xmin_D",
- type=float, default=None,
- help="Manually override the xmin for the duration power-law "
- "fit. Default: auto-detected.")
- crit_group.add_argument("--skip_rare_sizes",
- action="store_true",
- help="On the size log-log plot, exclude avalanche sizes that "
- "occurred only once in the recording. These rare points "
- "can distort the visual fit line. Does NOT affect the "
- "MLE exponent value.")
- crit_group.add_argument("--skip_rare_durations",
- action="store_true",
- help="Same as --skip_rare_sizes but for the duration log-log "
- "plot.")
- # ── shape collapse ─────────────────────────────────────────────────────────
- sc_group = ap.add_argument_group("Shape Collapse")
- sc_group.add_argument("--sc_Dmin",
- type=int, default=4,
- help="Minimum avalanche duration (in bins) to include in the "
- "shape collapse analysis. (default: 4)")
- sc_group.add_argument("--sc_Dmax",
- type=int, default=20,
- help="Maximum avalanche duration (in bins) to include in the "
- "shape collapse analysis. (default: 20)")
- # ── burst detection ────────────────────────────────────────────────────────
- burst_group = ap.add_argument_group("Burst Detection")
- burst_group.add_argument("--burst_min_spikes",
- type=int, default=5,
- help="Minimum number of spikes required for a run to be "
- "counted as a network burst. Lower values detect more, "
- "shorter bursts (MEAanalysis default: 5). "
- "Higher values require denser events. (default: 5)")
- burst_group.add_argument("--burst_max_gap",
- type=float, default=0.1,
- help="Maximum allowed gap between consecutive spikes (seconds) "
- "within a burst. Spikes further apart than this split the "
- "burst into two. (default: 0.1)")
- # ── connectivity ───────────────────────────────────────────────────────────
- conn_group = ap.add_argument_group("Connectivity")
- conn_group.add_argument("--conn_thr",
- type=float, default=0.3,
- help="Pearson correlation threshold for drawing a functional "
- "connectivity edge between two channels. Pairs with "
- "|r| >= this value are connected. (default: 0.3)")
- # ── entropy and active area ────────────────────────────────────────────────
- ea_group = ap.add_argument_group("Entropy and Active Area")
- ea_group.add_argument("--entropy_win",
- type=float, default=10.0,
- help="Window size in seconds for computing spatial entropy "
- "and active electrode fraction over time. (default: 10.0)")
- # ── epoch comparison ───────────────────────────────────────────────────────
- ep_group = ap.add_argument_group("Epoch Comparison (optional)")
- ep_group.add_argument("--epochA",
- default=None,
- help="First epoch window as 'start,duration' in seconds. "
- "Example: --epochA 0,60 for the first 60 seconds.")
- ep_group.add_argument("--epochB",
- default=None,
- help="Second epoch window as 'start,duration' in seconds. "
- "Example: --epochB 120,60 for 60 seconds starting at 2 min. "
- "Both --epochA and --epochB must be set to compare.")
- args = ap.parse_args()
- # ── self-test ──────────────────────────────────────────────────────────────
- if args.test:
- from cascade.tests.self_test import run_self_test
- sys.exit(run_self_test())
- if args.manufacturer is None:
- ap.error("--manufacturer is required. Choose from: 3brain, maxwell, cl, mcs, axion")
- svg = args.svg
- jpg = args.jpg
- v = args.verbose
- banner("CASCADE • Criticality Avalanche Spike Cross-platform Analysis Detection Engine • v0.8.0", enabled=v)
- # ── find file ──────────────────────────────────────────────────────────────
- path = args.input
- if not path:
- for pattern in ("*.bxr", "*.raw.h5", "*.h5"):
- hits = sorted(glob.glob(pattern))
- if hits:
- path = hits[0]
- break
- if not path:
- say("No data file found. Use --input <path>", "[FAIL]", RED)
- sys.exit(1)
- # ── load manufacturer ──────────────────────────────────────────────────────
- mfr = args.manufacturer
- loader = get_loader(mfr)
- say(f"File: {path}", "[DIR]", CYAN, enabled=v)
- say(f"Manufacturer: {loader.manufacturer_name()}", "[CHECK]", CYAN, enabled=v)
- # ── load data ──────────────────────────────────────────────────────────────
- step("Loading data", enabled=v)
- rec = loader.load(path,
- well = args.well,
- start_s = args.start,
- duration_s = args.dur)
- say(f"SR={rec.sampling_rate:.1f} Hz | "
- f"channels={rec.n_channels} | "
- f"duration={rec.duration:.1f} s | "
- f"spikes={rec.n_spikes():,}", "[OK]", GRN, enabled=v)
- os.makedirs(args.output, exist_ok=True)
- well_label = rec.metadata.get("well", "well")
- # ── active-electrode filter ────────────────────────────────────────────────
- step("Active-electrode filter", enabled=v)
- spike_times = filter_active_electrodes(rec.spike_times, args.ae, rec.duration)
- n_active = len(spike_times)
- say(f"Channels kept (>= {args.ae} spikes/min): {n_active}", "[DATA]", CYAN, enabled=v)
- if n_active == 0:
- print("[WARN] No active channels found after filtering. "
- "The recording may have very low firing activity, or --ae is set too high. "
- "Try lowering --ae or removing it entirely.")
- elif n_active == 1:
- print("[WARN] Only 1 active channel found. Connectivity analysis will be "
- "skipped (requires at least 2 channels). Avalanche and criticality metrics "
- "may be unreliable with a single electrode. Consider lowering --ae.")
- # ── noisy-electrode filter ─────────────────────────────────────────────────
- if args.max_rate > 0 or args.max_rpv > 0:
- step("Noisy-electrode filter", enabled=v)
- spike_times, n_removed, removed_ch = filter_noisy_electrodes(
- spike_times, rec.duration,
- max_rate_per_min=args.max_rate,
- max_rpv_pct=args.max_rpv,
- )
- say(f"Noisy channels removed: {n_removed} "
- f"(max_rate={args.max_rate} spk/min, max_rpv={args.max_rpv}%)",
- "[DATA]", CYAN, enabled=v)
- if n_removed > 0 and v:
- say(f"Removed channel IDs: {removed_ch[:20]}"
- f"{'…' if len(removed_ch) > 20 else ''}",
- "[DATA]", CYAN, enabled=v)
- n_active = len(spike_times)
- all_spikes = rec.all_spike_times() if (args.ae <= 0 and args.max_rate <= 0 and args.max_rpv <= 0) else \
- np.sort(np.concatenate(list(spike_times.values()))) \
- if spike_times else np.array([])
- # ── raster ────────────────────────────────────────────────────────────────
- step("Raster & rate heatmap", enabled=v)
- plot_raster(spike_times,
- f"{args.output}/raster_max_{well_label}.png",
- f"Spike raster — {well_label}", svg, jpg,
- n_channels=rec.n_channels)
- # active: remap electrode IDs to 0..N-1 so Y-axis max = number of active electrodes
- _sorted_chs = sorted(spike_times.keys())
- _ch_seq_map = {ch: i for i, ch in enumerate(_sorted_chs)}
- _st_active = {_ch_seq_map[ch]: times for ch, times in spike_times.items()}
- plot_raster(_st_active,
- f"{args.output}/raster_active_{well_label}.png",
- f"Spike raster (active) — {well_label}", svg, jpg)
- # ── firing rates ──────────────────────────────────────────────────────────
- rate_df = bin_spikes(spike_times, rec.duration, args.bin)
- write_tsv(rate_df, f"{args.output}/rates_{well_label}.tsv")
- plot_rate_heatmap(rate_df,
- f"{args.output}/rate_heatmap_max_{well_label}.png",
- f"Rate heatmap — {well_label} (bin={args.bin}s)", svg, jpg,
- n_channels=rec.n_channels)
- _rate_df_active = rate_df.copy()
- _rate_df_active["ch_idx"] = _rate_df_active["ch_idx"].map(_ch_seq_map)
- plot_rate_heatmap(_rate_df_active,
- f"{args.output}/rate_heatmap_active_{well_label}.png",
- f"Rate heatmap (active) — {well_label} (bin={args.bin}s)", svg, jpg)
- plot_population_rate(rate_df,
- f"{args.output}/population_rate_{well_label}.png",
- well_label, svg, jpg)
- # ── ISI metrics ────────────────────────────────────────────────────────────
- step("ISI metrics", enabled=v)
- isi_df = compute_isi_metrics(spike_times, rec.sampling_rate)
- write_tsv(isi_df, f"{args.output}/isi_metrics_{well_label}.tsv")
- # ── network bursts ─────────────────────────────────────────────────────────
- step("Network burst detection", enabled=v)
- burst_df, bsum = compute_burst_metrics(all_spikes,
- min_spikes=args.burst_min_spikes,
- max_gap_s=args.burst_max_gap)
- write_tsv(burst_df, f"{args.output}/bursts_{well_label}.tsv")
- plot_burst_timeline(burst_df, rec.duration,
- f"{args.output}/bursts_timeline_{well_label}.png",
- well_label, svg, jpg)
- say(f"Bursts: n={bsum['n_bursts']} "
- f"mean_dur={bsum['mean_duration_s']:.2f}s", "[DATA]", CYAN, enabled=v)
- if bsum["n_bursts"] == 0:
- print("[WARN] No network bursts detected. This is normal for low-activity "
- "recordings or recordings with sparse, uncorrelated firing.")
- # ── active area ────────────────────────────────────────────────────────────
- act_df = compute_active_area(spike_times, rec.duration, args.entropy_win)
- write_tsv(act_df, f"{args.output}/active_area_{well_label}.tsv")
- plot_active_area_curve(act_df,
- f"{args.output}/active_area_curve_{well_label}.png",
- well_label, svg, jpg)
- # ── waveforms (3Brain only) ────────────────────────────────────────────────
- if mfr == "3brain" and rec.metadata.get("has_waveforms"):
- step("Spike waveforms", enabled=v)
- waves = loader.load_waveforms(path,
- well = rec.metadata.get("well"),
- start_s = args.start,
- duration_s = rec.duration)
- if waves:
- plot_waveforms(waves,
- f"{args.output}/wave_templates_{well_label}.png",
- well_label, svg, jpg)
- # ── connectivity ───────────────────────────────────────────────────────────
- step("Connectivity (Pearson correlation)", enabled=v)
- if rate_df.shape[1] < 2:
- say("Skipped -- fewer than 2 active channels", "[WARN]", YEL, enabled=v)
- C, ch_ids = None, []
- else:
- C, ch_ids = compute_connectivity(rate_df, zscore=True)
- if C is not None:
- np.savetxt(f"{args.output}/corr_{well_label}.tsv", C, delimiter="\t")
- plot_corr_heatmap(C,
- f"{args.output}/corr_heatmap_max_{well_label}.png",
- args.conn_thr, well_label, svg, jpg,
- channel_ids=ch_ids,
- n_channels=rec.n_channels)
- plot_corr_heatmap(C,
- f"{args.output}/corr_heatmap_active_{well_label}.png",
- args.conn_thr, well_label, svg, jpg,
- channel_ids=ch_ids)
- deg_df, csum = summarize_connectivity(C, ch_ids, args.conn_thr)
- write_tsv(deg_df, f"{args.output}/degrees_{well_label}.tsv")
- plot_degree_histogram(deg_df,
- f"{args.output}/degree_hist_{well_label}.png",
- well_label, svg, jpg)
- say(f"Edges |r|>={args.conn_thr}: {csum['n_edges']} "
- f"density={csum['edge_density']:.4f}", "[DATA]", CYAN, enabled=v)
- # ── avalanches ─────────────────────────────────────────────────────────────
- step("Neuronal avalanches", enabled=v)
- dt = args.aval_dt if args.aval_dt is not None else infer_dt(all_spikes)
- say(f"Δt = {dt*1000:.2f} ms | "
- f"perc threshold = {args.perc*100:.0f}%", "[FAST]", CYAN, enabled=v)
- av_df = compute_avalanches(all_spikes, dt, perc=args.perc)
- # apply size/duration threshold
- av_df = av_df[
- (av_df["avalanche_size"] >= args.ava_size) &
- (av_df["duration_bins"] >= args.ava_dur)
- ].reset_index(drop=True)
- write_tsv(av_df, f"{args.output}/avalanches_{well_label}.tsv")
- plot_avalanche_distributions(av_df,
- f"{args.output}/avalanches_{well_label}",
- well_label, svg, jpg)
- say(f"Avalanches after threshold (size>={args.ava_size}, dur>={args.ava_dur}): {len(av_df)}", "[DATA]", CYAN, enabled=v)
- if av_df.empty:
- print("[WARN] No avalanches passed the size/duration threshold. "
- "Criticality metrics (tau, alpha, DCC, branching ratio) cannot be computed. "
- "Try lowering --ava_size (current: {}) or --ava_dur (current: {}) or --perc (current: {}).".format(
- args.ava_size, args.ava_dur, args.perc))
- elif len(av_df) < 50:
- print(f"[WARN] Only {len(av_df)} avalanches detected. Power-law fitting "
- "requires many events for reliable results -- interpret criticality metrics "
- "with caution. Consider lowering --ava_size, --ava_dur, or --perc.")
- # ── criticality metrics ────────────────────────────────────────────────────
- step("Criticality metrics (exponents, DCC, BR, shape collapse)", enabled=v)
- sizes = av_df["avalanche_size"].to_numpy() if not av_df.empty else np.array([])
- durations = av_df["duration_bins"].to_numpy() if not av_df.empty else np.array([])
- exp_metrics = compute_exponents(sizes, durations,
- burst_min=args.burst_min,
- t_min=args.t_min)
- # log–log plots with fitted lines
- # --pl_xmin_S/D override the auto-detected xmin for the visual fit line
- # --skip_rare_sizes/durations excludes single-occurrence points from the OLS line
- xmin_s = args.pl_xmin_S if args.pl_xmin_S is not None else exp_metrics.get("xmin_s")
- xmin_d = args.pl_xmin_D if args.pl_xmin_D is not None else exp_metrics.get("xmin_d")
- plot_loglog_linearization(
- sizes,
- f"{args.output}/loglog_size_{well_label}.png",
- f"Log–log size — {well_label}",
- tau=exp_metrics.get("tau"), xmin=xmin_s,
- exclude_singletons=args.skip_rare_sizes,
- save_svg=svg, save_jpg=jpg)
- plot_loglog_linearization(
- durations,
- f"{args.output}/loglog_duration_{well_label}.png",
- f"Log–log duration — {well_label}",
- tau=exp_metrics.get("alpha"), xmin=xmin_d,
- exclude_singletons=args.skip_rare_durations,
- save_svg=svg, save_jpg=jpg)
- # branching ratio
- counts, _ = counts_per_dt(all_spikes, dt)
- br, br_h, br_pts = compute_branching_ratio(counts)
- write_tsv(br_pts, f"{args.output}/branching_points_{well_label}.tsv")
- # shape collapse
- Dmin_eff = max(args.sc_Dmin, args.ava_dur)
- sc_beta, sce, sc_minerr, sc_details = shape_collapse(
- counts, exp_metrics.get("beta_pred", np.nan),
- Dmin=Dmin_eff, Dmax=args.sc_Dmax, min_size=args.ava_size)
- plot_shape_collapse(sc_details,
- f"{args.output}/shape_collapse_{well_label}.png",
- well_label, svg, jpg)
- # write text report
- crit_metrics = dict(
- well = well_label,
- dt = dt,
- ava_size_thr = args.ava_size,
- ava_dur_thr = args.ava_dur,
- perc = args.perc,
- tau = exp_metrics.get("tau", np.nan),
- xmin_s = exp_metrics.get("xmin_s", np.nan),
- alpha = exp_metrics.get("alpha", np.nan),
- xmin_d = exp_metrics.get("xmin_d", np.nan),
- beta_pred = exp_metrics.get("beta_pred", np.nan),
- beta_fit = exp_metrics.get("beta_fit", np.nan),
- dcc = exp_metrics.get("dcc", np.nan),
- br = br,
- br_intercept = br_h,
- sc_beta = sc_beta,
- sce = sce,
- sc_minerr = sc_minerr,
- )
- write_criticality_report(crit_metrics,
- f"{args.output}/criticality_report_{well_label}.txt")
- say(f"DCC={exp_metrics.get('dcc', float('nan')):.4f} "
- f"BR={br:.4f} SCE={sce:.4f}", "[CRIT]", CYAN, enabled=v)
- # ── entropy ────────────────────────────────────────────────────────────────
- step("Spatial entropy", enabled=v)
- ent_df = compute_entropy(spike_times, rec.duration, args.entropy_win)
- write_tsv(ent_df, f"{args.output}/entropy_{well_label}.tsv")
- plot_entropy_curve(ent_df,
- f"{args.output}/entropy_curve_{well_label}.png",
- well_label, svg, jpg)
- # ── epoch comparison (optional) ────────────────────────────────────────────
- if args.epochA and args.epochB:
- step("Epoch comparison", enabled=v)
- epA = _parse_epoch(args.epochA)
- epB = _parse_epoch(args.epochB)
- if epA and epB:
- stats = {
- "A": _epoch_stats(spike_times, epA[0], epA[1], rec.channel_ids),
- "B": _epoch_stats(spike_times, epB[0], epB[1], rec.channel_ids),
- }
- plot_epoch_compare(stats,
- f"{args.output}/epoch_compare_{well_label}.png",
- well_label, svg, jpg)
- say(f"Epoch A PopRate={stats['A']['pop_rate']:.3f} Hz "
- f"-> B PopRate={stats['B']['pop_rate']:.3f} Hz", "[DATA]", CYAN, enabled=v)
- else:
- say("Bad epoch format. Use 'start,dur' e.g. --epochA 0,60", "[WARN]", YEL)
- banner(f"ALL DONE • outputs in {args.output}", enabled=v)
- print(f"[OK] Done. Outputs saved in: {args.output}")
- def _run_with_log():
- import io as _io
- _cmd = "cascade " + " ".join(sys.argv[1:])
- _buf = _io.StringIO()
- _tee = _Tee(sys.stdout, _buf)
- sys.stdout = _tee
- _out = "mea_out"
- for _i, _arg in enumerate(sys.argv):
- if _arg == "--output" and _i + 1 < len(sys.argv):
- _out = sys.argv[_i + 1]
- break
- _status = "SUCCESS"
- _error = None
- try:
- main()
- except SystemExit:
- _status = "FAILED"
- raise
- except Exception as exc:
- _status = "FAILED"
- _error = str(exc)
- raise
- finally:
- sys.stdout = _tee._original
- write_run_log(_out, _cmd, _status, error=_error, output=_buf.getvalue())
- if __name__ == "__main__":
- _run_with_log()
run_analysis.py, under Apache-2.0 · at the source
Overview
- Center for Alternatives to Animal Testing, Korea Institute of Toxicology, Yuseong, Daejeon 34114, Republic of Korea
- Human and Environmental Toxicology, University of Science and Technology (UST), Yuseong, Daejeon 34113, Republic of Korea
- Department of Physiology, College of Medicine, Soonchunhyang University, Cheonan 31151, Republic of Korea
- Institute for Molecular Metabolism Innovation, Soonchunhyang University, Asan 31538, Republic of Korea
- Department of Biophysics, Institute of Quantum Biophysics, Sungkyunkwan University, Suwon 16419, Republic of Korea
- Department of MetaBioHealth, Sungkyunkwan University, Suwon 16419, Republic of Korea
- Department of Biopharmaceutical Convergence, Sungkyunkwan University, Suwon 16419, Republic of Korea
- Department of Physiology, School of Medicine, Pusan National University, Yangsan-si 50612, Republic of Korea
- Research Institute for Convergence of Biomedical Science and Technology, Pusan National University Yangsan Hospital, Yangsan-si 50612, Republic of Korea
- Medical Research Institute, School of Medicine, Pusan National University, Yangsan-si 50612, Republic of Korea
Abstract
Summary: Multi-electrode array (MEA) recordings are widely utilized to characterize neuronal network activity. However, currently each manufacturer distributes proprietary software that operates exclusively on its own file format. Since no cross-platform, open-source pipeline currently exists. Thus, we created CASCADE (Criticality Avalanche Spike Cross-platform Analysis Detection Engine), a Python-based command-line (bash) pipeline to analyze MEA recordings from 3Brain, Axion Biosystems, Cortical Labs, Maxwell Biosystems, and Multi-Channel Systems MEA devices. CASCADE measures the standard parameters such as: network, burst, firing rate frequency, and also measures criticality and avalanches. CASCADE criticality calculation was benchmarked across six MEA recordings from five manufacturers, resulting in comparable values because all recordings were analyzed using identical computational methods regardless of manufacturer file format. Thus, CASCADE provides a flexible and robust MEA analysis pipeline that can also analyze neuronal avalanches and criticality from multiple MEA devices from different manufacturers.
Availability and implementation: CASCADE is freely available at GitHub repository (https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Repository
Its files are read in the Code ↔ Paper reader above, with 10 matches between paragraphs and lines of code.
Zenodo 20840795
Availability: 1 check, the latest on 26 September 2026: the link answers (HTTP 200)
- 26 September 2026: the link answers (HTTP 200)
27 files
- cascade/
__init__.py — Python, 10 lines - cascade/
core/ — Python, 2 lines__init__.py - cascade/
core/ — Python, 151 lines, 1 matchavalanche.py - cascade/
core/ — Python, 137 lines, 1 matchbursts.py - cascade/
core/ — Python, 80 lines, 1 matchconnectivity.py - cascade/
core/ — Python, 332 linescriticality.py - cascade/
core/ — Python, 67 linesentropy.py - cascade/
core/ — Python, 386 linesplots.py - cascade/
core/ — Python, 188 lines, 1 matchspikes.py - cascade/
loaders/ — Python, 6 lines__init__.py - cascade/
loaders/ — Python, 188 linesaxion_loader.py - cascade/
loaders/ — Python, 47 linesbase_loader.py - cascade/
loaders/ — Python, 170 linesbrainwave_loader.py - cascade/
loaders/ — Python, 154 lines, 1 matchcl1_loader.py - cascade/
loaders/ — Python, 177 linesmaxwell_loader.py - cascade/
loaders/ — Python, 191 linesmcs_loader.py - cascade/
ltpltd.py — Python, 331 lines - cascade/
run_analysis.py — Python, 706 lines, 4 matches - cascade/
scanner.py — Python, 447 lines - cascade/
tests/ — Python, 1 line__init__.py - cascade/
tests/ — Python, 267 linesself_test.py - cascade/
utils/ — Python, 4 lines__init__.py - cascade/
utils/ — Python, 104 linescommon.py - cascade/
utils/ — Python, 222 linesinspect_file.py - cascade/
utils/ — Python, 162 lines, 1 matchio.py - LICENSE — License, 205 lines
- README.md — Text, 535 lines
Availability and implementation
CASCADE is freely available at GitHub repository (https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 25 scripts, each with its path and the digest of its content;
- 10 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
Datasets cited
- zenodo:19161615 — at Zenodo; found in “Data availability”
Data availability
CASCADE is freely available at GitHub repository (https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 27 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 6 authors, 7 MeSH terms, 5 funders, 17 references.
Cite
This paper
Avila, F., Kim, J., Park, J.-C., Kang, R., Lee, H., & Park, S.-H. (2026). CASCADE: criticality avalanche spike cross-platform analysis detection engine, a multi-manufacturer MEA bash analysis pipeline. Bioinformatics (Oxford, England), 42(8), btag570. https://
BibTeX
@article{avila2026cascad
author = {Avila, Forbes and Kim, Jin and Park, Jong-Chan and Kang, Rian and Lee, Hyunsu and Park, Sun-Hyun},
title = {{CASCADE: criticality avalanche spike cross-platform analysis detection engine, a multi-manufacturer MEA bash analysis pipeline}},
journal = {Bioinformatics (Oxford, England)},
year = {2026},
month = aug,
volume = {42},
number = {8},
pages = {btag570},
publisher = {Oxford University Press},
issn = {1367-4803},
doi = {10.1093/
url = {https://
pmid = {42525390},
pmcid = {PMC13478863}
}
RIS
TY - JOUR
AU - Avila, Forbes
AU - Kim, Jin
AU - Park, Jong-Chan
AU - Kang, Rian
AU - Lee, Hyunsu
AU - Park, Sun-Hyun
TI - CASCADE: criticality avalanche spike cross-platform analysis detection engine, a multi-manufacturer MEA bash analysis pipeline
T2 - Bioinformatics (Oxford, England)
J2 - Bioinformatics
PY - 2026
DA - 2026/
VL - 42
IS - 8
SP - btag570
SN - 1367-4803
PB - Oxford University Press
DO - 10.1093/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1093/
"type": "article-journal",
"title": "CASCADE: criticality avalanche spike cross-platform analysis detection engine, a multi-manufacturer MEA bash analysis pipeline",
"container-title": "Bioinformatics (Oxford, England)",
"author": [
{
"family": "Avila",
"given": "Forbes"
},
{
"family": "Kim",
"given": "Jin"
},
{
"family": "Park",
"given": "Jong-Chan"
},
{
"family": "Kang",
"given": "Rian"
},
{
"family": "Lee",
"given": "Hyunsu"
},
{
"family": "Park",
"given": "Sun-Hyun"
}
],
"container-title-short":
"volume": "42",
"issue": "8",
"page": "btag570",
"DOI": "10.1093/
"PMID": "42525390",
"PMCID": "PMC13478863",
"ISSN": "1367-4803",
"publisher": "Oxford University Press",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
8,
1
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1162/imag.a.1348 [code]
- The role of high-amplitude bursts of high-gamma activity in naturalistic speech and music listening.Journal: Imaging neuroscience (Cambridge, Mass.)In common: h5py, pandas, SciPy, 2 other tools, 3 references
- [2] doi:10.1016/j.isci.2026.117212 [code]
- Critical neuronal avalanches arise from excitation-inhibition balanced spontaneous activity.Journal: iScienceIn common: 5 references
- [3] doi:10.1016/j.isci.2026.115488 [code]
- An integrated &
lt;i& gt;i& lt;/ i& gt; & lt;i& gt;n vitro& lt;/ i& gt; platform and biophysical modeling approach for studying synaptic transmission in isolated neuronal pairs. Journal: iScienceIn common: h5py, pandas, SciPy, 2 other tools, 1 reference - [4] doi:10.1038/s41467-026-74466-2 [code]
- Neuromorphic hierarchical modular reservoirs.Journal: Nature communicationsIn common: pandas, SciPy, Matplotlib, 1 other tool, 2 references
- [5] doi:10.7554/elife.110588 [code]
- Opening the black box toward a modular approach to spike sorting.Journal: eLifeIn common: h5py, pandas, SciPy, 2 other tools, extracellular electrophysiology (units, LFP), methods / tools
- [6] doi:10.21769/bioprotoc.5708 [code]
- Measuring Electrophysiological Activity in Acute Brain Slices, Spheroids, and Organoids Using 3D High-Density Multielectrode Arrays.Journal: Bio-protocolIn common: h5py, pandas, SciPy, 2 other tools, extracellular electrophysiology (units, LFP), methods / tools
- [7] doi:10.1002/advs.202519893 [code]
- NeuroSuite for Long-Term Functional and Structural Studies of Air-Liquid Interface Cerebral Organoids.Journal: Advanced science (Weinheim, Baden-Wurttemberg, Germany)In common: pandas, SciPy, Matplotlib, 1 other tool, extracellular electrophysiology (units, LFP), 1 reference
- [8] doi:10.1038/s41531-026-01531-4 [code]
- Inconsistent subthalamic local field potential beta activity amid in- and antiphasic neuronal bursts.Journal: NPJ Parkinson's diseaseIn common: h5py, pandas, SciPy, 2 other tools, extracellular electrophysiology (units, LFP)
- [9] doi:10.1016/j.patter.2026.101590 [code]
- Density-based longitudinal neuron tracking in high-density electrophysiological recordings.Journal: Patterns (New York, N.Y.)In common: h5py, pandas, SciPy, 2 other tools, extracellular electrophysiology (units, LFP)
- [10] doi:10.1038/s41592-026-03076-z [code]
- Neuropixels Opto: combining high-resolution electrophysiology and optogenetics.Journal: Nature methodsIn common: h5py, pandas, SciPy, 2 other tools, extracellular electrophysiology (units, LFP)
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 25 scripts, and 10 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:828cc32adbe67d87…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[.
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
