Source code for meanap.pipeline.spreadsheet
"""Read the recording list spreadsheet, mirroring ``pipelineReadCSV.m``."""
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
import numpy as np
import pandas as pd
[docs]
@dataclass
class RecordingInfo:
filename: str
div: float
group: str
ground: str | None = None
[docs]
def parse_spreadsheet_range(range_str: str) -> tuple[int, int]:
"""Parse a range like ``'A2:A3'`` or ``'2:1000'`` into 1-indexed (start_line, end_line).
Line numbers count the header as line 1, matching MATLAB's ``DataLines``/
``csvRange`` convention (e.g. ``[2, 3]`` reads the first two data rows
after the header).
"""
match = re.match(r"^[A-Za-z]*(\d+)\s*:\s*[A-Za-z]*(\d+)$", range_str.strip())
if not match:
raise ValueError(f"Invalid spreadsheet range: {range_str!r}")
return int(match.group(1)), int(match.group(2))
[docs]
def read_recording_csv(path: str | Path, spreadsheet_range: str) -> list[RecordingInfo]:
"""Read the recording list CSV.
Expects columns: Recording Filename, DIV group, Genotype, [Ground].
"""
start_line, end_line = parse_spreadsheet_range(spreadsheet_range)
df = pd.read_csv(path, header=0)
start_idx = max(start_line - 2, 0)
end_idx = min(end_line - 1, len(df))
subset = df.iloc[start_idx:end_idx]
has_ground = subset.shape[1] >= 4
recordings = []
for _, row in subset.iterrows():
recordings.append(RecordingInfo(
filename=str(row.iloc[0]),
div=float(row.iloc[1]),
group=str(row.iloc[2]),
ground=str(row.iloc[3]) if has_ground else None,
))
return recordings
[docs]
def parse_ground_electrodes(ground: str | None) -> set[int] | None:
"""Parse a recording's ``Ground`` spreadsheet value (comma-separated
channel IDs) into a set of ints, port of ``groundSpikeTimes.m``'s
electrode-list parsing. Returns ``None`` if there's nothing to ground —
including pandas turning an empty cell into the string ``"nan"``, which
``read_recording_csv`` doesn't special-case (it just calls ``str()`` on
whatever pandas gives it).
"""
if ground is None:
return None
ground = ground.strip()
if not ground or ground.lower() == "nan":
return None
return {int(float(x)) for x in ground.split(",") if x.strip()}
[docs]
def ground_spike_times_dict(
spike_times_dict: dict[int, np.ndarray],
channels: np.ndarray,
ground_electrodes: set[int] | None,
) -> dict[int, np.ndarray]:
"""Zero out spike times for channels listed in ``ground_electrodes``
(matched by channel ID/name — MATLAB's default
``Params.electrodesToGroundPerRecordingUseName = 1`` behavior, the only
mode this port supports), port of ``groundSpikeTimes.m``.
``spike_times_dict`` maps 0-indexed channel *position* (matching
``channels[i]``'s position) to that channel's spike times for a single
already-selected detection method — the shape this is called with in
``step2.py``/``step3.py``/``step4.py``.
"""
if not ground_electrodes:
return spike_times_dict
grounded_idx = {i for i, ch in enumerate(channels) if int(ch) in ground_electrodes}
if not grounded_idx:
return spike_times_dict
return {
ch: (np.array([]) if ch in grounded_idx else times)
for ch, times in spike_times_dict.items()
}