Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/ndi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,20 @@
# Import epoch module (Phase 6)
# Import Phase 10: Cloud API client
# Import Phase 11: Schema validation
from . import calc, cloud, common, daq, epoch, file, session, time, util, validate, validators
from . import (
calc,
cloud,
common,
daq,
epoch,
file,
session,
setup,
time,
util,
validate,
validators,
)

# Import Phase 9: ndi_app framework and calculators
from .app import ndi_app
Expand Down Expand Up @@ -124,6 +137,7 @@ def version() -> tuple:
"ndi_calculator",
"calc",
"cloud",
"setup",
"util",
"validate",
"validators",
Expand Down
18 changes: 17 additions & 1 deletion src/ndi/class_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,25 @@ def _build_registry() -> dict[str, type]:
from .daq.reader.mfdaq.blackrock import ndi_daq_reader_mfdaq_blackrock
from .daq.reader.mfdaq.cedspike2 import ndi_daq_reader_mfdaq_cedspike2
from .daq.reader.mfdaq.intan import ndi_daq_reader_mfdaq_intan
from .daq.reader.mfdaq.ndr import ndi_daq_reader_mfdaq_ndr
from .daq.reader.mfdaq.spikegadgets import ndi_daq_reader_mfdaq_spikegadgets
from .daq.system import ndi_daq_system
from .element import ndi_element
from .file.navigator import ndi_file_navigator
from .file.navigator.epochdir import ndi_file_navigator_epochdir
from .probe import ndi_probe
from .probe.timeseries import ndi_probe_timeseries
from .probe.timeseries_mfdaq import ndi_probe_timeseries_mfdaq
from .probe.timeseries_stimulator import ndi_probe_timeseries_stimulator
from .setup.daq.reader.mfdaq.stimulus.nielsenvisintan import (
ndi_setup_daq_reader_mfdaq_stimulus_nielsenvisintan,
)
from .setup.daq.reader.mfdaq.stimulus.vhaudreybpod import (
ndi_setup_daq_reader_mfdaq_stimulus_VHAudreyBPod,
)
from .setup.daq.reader.mfdaq.stimulus.vhlabvisspike2 import (
ndi_setup_daq_reader_mfdaq_stimulus_vhlabvisspike2,
)

registry: dict[str, type] = {}

Expand All @@ -56,15 +67,20 @@ def _build_registry() -> dict[str, type]:
ndi_daq_reader_mfdaq_intan,
ndi_daq_reader_mfdaq_blackrock,
ndi_daq_reader_mfdaq_cedspike2,
ndi_daq_reader_mfdaq_ndr,
ndi_daq_reader_mfdaq_spikegadgets,
ndi_setup_daq_reader_mfdaq_stimulus_vhlabvisspike2,
ndi_setup_daq_reader_mfdaq_stimulus_nielsenvisintan,
ndi_setup_daq_reader_mfdaq_stimulus_VHAudreyBPod,
):
registry[cls.NDI_DAQREADER_CLASS] = cls

# DAQ system
registry[ndi_daq_system.NDI_DAQSYSTEM_CLASS] = ndi_daq_system

# File navigator
# File navigators
registry[ndi_file_navigator.NDI_FILENAVIGATOR_CLASS] = ndi_file_navigator
registry[ndi_file_navigator_epochdir.NDI_FILENAVIGATOR_CLASS] = ndi_file_navigator_epochdir

return registry

Expand Down
2 changes: 2 additions & 0 deletions src/ndi/daq/metadatareader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,11 @@ def __hash__(self) -> int:
# Import subclass readers
from .newstim_stims import ndi_daq_metadatareader_NewStimStims
from .nielsenlab_stims import ndi_daq_metadatareader_NielsenLabStims
from .vhaudreybpod_stims import ndi_daq_metadatareader_VHAudreyBPod

__all__ = [
"ndi_daq_metadatareader",
"ndi_daq_metadatareader_NewStimStims",
"ndi_daq_metadatareader_NielsenLabStims",
"ndi_daq_metadatareader_VHAudreyBPod",
]
107 changes: 107 additions & 0 deletions src/ndi/daq/metadatareader/vhaudreybpod_stims.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""
ndi.daq.metadatareader.VHAudreyBPod - VH Lab Audrey BPod stimulus metadata reader.

Reads stimulus parameters from BPod behavioral task summary log JSON files
used in the VH Lab taste experiments.

MATLAB equivalent: src/ndi/+ndi/+daq/+metadatareader/VHAudreyBPod.m
"""

from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any

from ..metadatareader import ndi_daq_metadatareader


class ndi_daq_metadatareader_VHAudreyBPod(ndi_daq_metadatareader):
"""
Metadata reader for VH Lab Audrey BPod stimulus systems.

Reads stimulus parameters from JSON summary log files produced by
BPod behavioral task systems.

Example:
>>> reader = ndi_daq_metadatareader_VHAudreyBPod()
>>> params = reader.readmetadata(['triggers.tsv', 'summary_log.json'])
"""

SUMMARY_FILE_PATTERN = r"_summary_log\.json$"

def __init__(
self,
tsv_pattern: str = "",
identifier: str | None = None,
session: Any | None = None,
document: Any | None = None,
):
super().__init__(
tsv_pattern=tsv_pattern,
identifier=identifier,
session=session,
document=document,
)

def readmetadata(
self,
epochfiles: list[str],
) -> list[dict[str, Any]]:
"""
Read stimulus metadata from BPod summary log files.

Args:
epochfiles: List of file paths for the epoch

Returns:
List of parameter dictionaries
"""
# First try TSV-based reading from base class
if self._tab_separated_file_parameter:
try:
return super().readmetadata(epochfiles)
except (ValueError, FileNotFoundError):
pass

# Look for summary_log.json
summary_file = self._find_summary_file(epochfiles)
if summary_file is None:
return []

return self._read_summary_json(summary_file)

def _find_summary_file(self, epochfiles: list[str]) -> str | None:
"""Find the BPod summary log JSON file in epoch files."""
pattern = re.compile(self.SUMMARY_FILE_PATTERN, re.IGNORECASE)
for f in epochfiles:
if pattern.search(f):
return f
return None

@staticmethod
def _read_summary_json(filepath: str) -> list[dict[str, Any]]:
"""
Read stimulus parameters from a BPod summary log JSON file.

Args:
filepath: Path to the summary_log.json file

Returns:
List of parameter dicts
"""
if not Path(filepath).is_file():
return []

with open(filepath, encoding="utf-8") as f:
data = json.load(f)

if isinstance(data, list):
return data
if isinstance(data, dict):
return [data]
return []

def __repr__(self) -> str:
return f"ndi_daq_metadatareader_VHAudreyBPod(id='{self.id[:8]}...')"
2 changes: 2 additions & 0 deletions src/ndi/daq/reader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
ndi_daq_reader_mfdaq_blackrock,
ndi_daq_reader_mfdaq_cedspike2,
ndi_daq_reader_mfdaq_intan,
ndi_daq_reader_mfdaq_ndr,
ndi_daq_reader_mfdaq_spikegadgets,
)
from .spikeinterface_adapter import ndi_daq_reader_SpikeInterfaceReader
Expand All @@ -27,5 +28,6 @@
"ndi_daq_reader_mfdaq_intan",
"ndi_daq_reader_mfdaq_blackrock",
"ndi_daq_reader_mfdaq_cedspike2",
"ndi_daq_reader_mfdaq_ndr",
"ndi_daq_reader_mfdaq_spikegadgets",
]
2 changes: 2 additions & 0 deletions src/ndi/daq/reader/mfdaq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
from .blackrock import ndi_daq_reader_mfdaq_blackrock
from .cedspike2 import ndi_daq_reader_mfdaq_cedspike2
from .intan import ndi_daq_reader_mfdaq_intan
from .ndr import ndi_daq_reader_mfdaq_ndr
from .spikegadgets import ndi_daq_reader_mfdaq_spikegadgets

__all__ = [
"ndi_daq_reader_mfdaq_intan",
"ndi_daq_reader_mfdaq_blackrock",
"ndi_daq_reader_mfdaq_cedspike2",
"ndi_daq_reader_mfdaq_ndr",
"ndi_daq_reader_mfdaq_spikegadgets",
]
66 changes: 66 additions & 0 deletions src/ndi/daq/reader/mfdaq/ndr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
ndi.daq.reader.mfdaq.ndr - NDR (Neuroscience Data Reader) wrapper.

Thin wrapper around ndi_daq_reader_SpikeInterfaceReader for file formats
supported by the NDR library (e.g. Axon ABF files).

MATLAB equivalent: src/ndi/+ndi/+daq/+reader/+mfdaq/ndr.m
"""

from __future__ import annotations

import logging

from ...mfdaq import ChannelInfo, ndi_daq_reader_mfdaq

logger = logging.getLogger(__name__)


class ndi_daq_reader_mfdaq_ndr(ndi_daq_reader_mfdaq):
"""
Reader for data files handled by the NDR library.

Currently supports Axon ABF files via spikeinterface/neo.

File extensions: .abf
"""

NDI_DAQREADER_CLASS = "ndi.daq.reader.mfdaq.ndr"
FILE_EXTENSIONS = [".abf"]

def __init__(self, identifier=None, session=None, document=None):
super().__init__(identifier=identifier, session=session, document=document)
self._ndi_daqreader_class = self.NDI_DAQREADER_CLASS

def _get_si_reader(self):
try:
from ..spikeinterface_adapter import ndi_daq_reader_SpikeInterfaceReader

return ndi_daq_reader_SpikeInterfaceReader
except ImportError:
return None

def getchannelsepoch(self, epochfiles: list[str]) -> list[ChannelInfo]:
SI = self._get_si_reader()
if SI is None:
return []
try:
return SI().getchannelsepoch(epochfiles)
except Exception as exc:
logger.warning("ndi_daq_reader_mfdaq_ndr.getchannelsepoch failed: %s", exc)
return []

def readchannels_epochsamples(self, channeltype, channel, epochfiles, s0, s1):
SI = self._get_si_reader()
if SI is None:
raise ImportError("spikeinterface required for reading NDR data")
return SI().readchannels_epochsamples(channeltype, channel, epochfiles, s0, s1)

def samplerate(self, epochfiles, channeltype, channel):
SI = self._get_si_reader()
if SI is None:
raise ImportError("spikeinterface required for reading NDR data")
return SI().samplerate(epochfiles, channeltype, channel)

def __repr__(self):
return f"ndi_daq_reader_mfdaq_ndr(id={self.id[:8]}...)"
42 changes: 30 additions & 12 deletions src/ndi/daq/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,25 +209,43 @@ def _load_from_document(self, session: Any, document: Any) -> None:
from ..class_registry import get_class

ReaderCls = get_class(reader_class_name)
if ReaderCls is not None:
try:
self._daqreader = ReaderCls(session=session, document=reader_doc)
except Exception as exc:
logger.warning(
"Could not reconstruct DAQ reader %s: %s", reader_class_name, exc
)
else:
logger.debug("Unknown DAQ reader class: %s", reader_class_name)
if ReaderCls is None:
raise ValueError(
f"Unknown DAQ reader class: {reader_class_name!r}. "
f"Register it in ndi.class_registry."
)
try:
self._daqreader = ReaderCls(session=session, document=reader_doc)
except Exception as exc:
raise RuntimeError(
f"Could not reconstruct DAQ reader {reader_class_name!r}: {exc}"
) from exc

# Reconstruct file navigator from its document
self._filenavigator = None
if len(nav_docs) == 1:
from ..file.navigator import ndi_file_navigator
nav_doc = nav_docs[0]
nav_class_name = ""
nav_props = nav_doc.document_properties
if isinstance(nav_props, dict):
nav_class_name = nav_props.get("filenavigator", {}).get(
"ndi_filenavigator_class", ""
)

from ..class_registry import get_class as get_nav_class

NavCls = get_nav_class(nav_class_name)
if NavCls is None:
raise ValueError(
f"Unknown file navigator class: {nav_class_name!r}. "
f"Register it in ndi.class_registry."
)
try:
self._filenavigator = ndi_file_navigator(session=session, document=nav_docs[0])
self._filenavigator = NavCls(session=session, document=nav_doc)
except Exception as exc:
logger.warning("Could not reconstruct file navigator: %s", exc)
raise RuntimeError(
f"Could not reconstruct file navigator {nav_class_name!r}: {exc}"
) from exc

@property
def name(self) -> str:
Expand Down
2 changes: 2 additions & 0 deletions src/ndi/file/navigator/epochdir.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class ndi_file_navigator_epochdir(ndi_file_navigator):
>>> # trial_002/file.rhd -> epoch 2
"""

NDI_FILENAVIGATOR_CLASS = "ndi.file.navigator.epochdir"

def epochid(
self,
epoch_number: int,
Expand Down
15 changes: 15 additions & 0 deletions src/ndi/ndi_common/daq_systems/vhlab/vhtaste_bpod.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"Name": "vhtaste_bpod",
"DaqSystemClass": "ndi.daq.system.mfdaq",
"DaqReaderClass": "ndi.setup.daq.reader.mfdaq.stimulus.VHAudreyBPod",
"MetadataReaderClass": "ndi.daq.metadatareader.VHAudreyBPod",
"EpochProbeMapClass": "ndi.setup.epoch.epochprobemap_daqsystem_vhlab",
"FileParameters": [
"#_stimulus_triggers_log.tsv",
"#.*_summary_log.json"
],
"DaqReaderFileParameters": [],
"MetadataReaderFileParameters": "(.*)\\.json",
"EpochProbeMapFileParameters": "#_stimulus_triggers_log.tsv",
"HasEpochDirectories": false
}
17 changes: 17 additions & 0 deletions src/ndi/ndi_common/daq_systems/vhlab/vhtaste_sync.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"Name": "vhtaste_sync",
"DaqSystemClass": "ndi.daq.system.mfdaq",
"DaqReaderClass": "ndi.daq.reader.mfdaq.intan",
"MetadataReaderClass": [],
"EpochProbeMapClass": "ndi.setup.epoch.epochprobemap_daqsystem_vhlab",
"FileParameters": [
"reference.txt",
".*\\.rhd\\>",
"vhintan_channelgrouping.txt",
"vhtaste_sync.txt"
],
"DaqReaderFileParameters": [],
"MetadataReaderFileParameters": [],
"EpochProbeMapFileParameters": "vhtaste_sync.txt",
"HasEpochDirectories": true
}
Loading
Loading