Source code for meanap.pipeline.io

"""I/O helpers for MEA-NAP pipeline files.

Handles HDF5/v7.3 .mat files produced by the Axion MEA system and by
the MATLAB MEA-NAP pipeline.  ``scipy.io.loadmat`` cannot read v7.3 files,
so we use ``h5py`` throughout.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

import h5py
import numpy as np


# ── Raw recording files ───────────────────────────────────────────────────────

[docs] def load_raw_recording(path: str | Path) -> tuple[np.ndarray, np.ndarray, float]: """Load a raw MEA recording .mat file (HDF5/v7.3 format). Returns ------- dat : (n_samples, n_channels) float32 array — raw voltage traces channels : (n_channels,) int array — channel IDs fs : float — sampling frequency in Hz """ with h5py.File(path, "r") as f: dat = f["dat"][()].T.astype(np.float32) # (n_channels, n_samples) → (n_samples, n_channels) channels = f["channels"][()].flatten().astype(int) fs = float(f["fs"][()].flatten()[0]) return dat, channels, fs
# ── Spike detection output files ──────────────────────────────────────────────
[docs] def load_spike_times_mat(path: str | Path) -> dict[int, dict[str, np.ndarray]]: """Read spike times from a MEA-NAP ``_spikes.mat`` (HDF5/v7.3) file. Returns ------- spike_times : dict[channel_index, dict[method, times_in_seconds]] ``channel_index`` is 0-based. ``method`` is e.g. ``'bior1p5'``, ``'thr4'``, ``'thr5'``. """ result: dict[int, dict[str, np.ndarray]] = {} with h5py.File(path, "r") as f: st = f["spikeTimes"] n_channels = st.shape[0] for ch_idx in range(n_channels): ref = st[ch_idx, 0] group = f[ref] if isinstance(group, h5py.Group): result[ch_idx] = { k: group[k][()].flatten() for k in group.keys() } else: result[ch_idx] = {"default": group[()].flatten()} return result
[docs] def save_spike_times_npz( path: str | Path, spike_times: dict[int, dict[str, np.ndarray]], channels: np.ndarray, fs: float, params: dict[str, Any] | None = None, ) -> None: """Save spike detection results to a ``.npz`` file. Saved arrays ------------ ``channels`` — channel IDs ``fs`` — sampling frequency ``spike_times_{ch}_{method}`` — spike times in seconds for each channel/method Also saves a text file ``{stem}_params.txt`` alongside if ``params`` given. """ path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) arrays: dict[str, Any] = { "channels": channels, "fs": np.array([fs]), } for ch_idx, methods in spike_times.items(): for method, times in methods.items(): arrays[f"spike_times_{ch_idx}_{method}"] = times np.savez(path, **arrays) if params is not None: params_path = path.with_name(path.stem + "_params.txt") with open(params_path, "w") as fh: for k, v in params.items(): fh.write(f"{k}: {v}\n")
[docs] def load_spike_times_npz(path: str | Path) -> dict[int, dict[str, np.ndarray]]: """Load spike times saved by ``save_spike_times_npz``.""" data = np.load(path) result: dict[int, dict[str, np.ndarray]] = {} prefix = "spike_times_" for key in data.files: if not key.startswith(prefix): continue rest = key[len(prefix):] parts = rest.split("_", 1) if len(parts) != 2: continue ch_idx = int(parts[0]) method = parts[1] if ch_idx not in result: result[ch_idx] = {} result[ch_idx][method] = data[key] return result