diff --git a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_behaviorinterface.py b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_behaviorinterface.py index 5623b41..6dcf29e 100644 --- a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_behaviorinterface.py +++ b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_behaviorinterface.py @@ -1,25 +1,115 @@ """Primary class for converting experiment-specific behavior.""" from pynwb.file import NWBFile +from pydantic import FilePath +import numpy as np +from h5py import File +from hdmf.common.table import DynamicTableRegion +from pynwb.behavior import BehavioralTimeSeries, TimeSeries +from ndx_events import EventTypesTable, EventsTable, Task, TimestampVectorData from neuroconv.basedatainterface import BaseDataInterface from neuroconv.utils import DeepDict +from neuroconv.tools import nwb_helpers + class Schneider2024BehaviorInterface(BaseDataInterface): """Behavior interface for schneider_2024 conversion""" keywords = ["behavior"] - - def __init__(self): - # This should load the data lazily and prepare variables you need - pass + + def __init__(self, file_path: FilePath): + super().__init__(file_path=file_path) def get_metadata(self) -> DeepDict: # Automatically retrieve as much metadata as possible from the source files available - metadata = super().get_metadata() + metadata = super().get_metadata() return metadata def add_to_nwbfile(self, nwbfile: NWBFile, metadata: dict): - # All the custom code to add the data the nwbfile + # Read Data + file_path = self.source_data["file_path"] + with File(file_path, "r") as file: + behavioral_time_series, name_to_times, name_to_values = [], dict(), dict() + for time_series_dict in metadata["Behavior"]["TimeSeries"]: + name = time_series_dict["name"] + timestamps = np.array(file["continuous"][name]["time"]).squeeze() + data = np.array(file["continuous"][name]["value"]).squeeze() + time_series = TimeSeries( + name=name, + timestamps=timestamps, + data=data, + unit="a.u.", + description=time_series_dict["description"], + ) + behavioral_time_series.append(time_series) + for event_dict in metadata["Behavior"]["Events"]: + name = event_dict["name"] + times = np.array(file["events"][name]["time"]).squeeze() + name_to_times[name] = times + for event_dict in metadata["Behavior"]["ValuedEvents"]: + name = event_dict["name"] + times = np.array(file["events"][name]["time"]).squeeze() + values = np.array(file["events"][name]["value"]).squeeze() + name_to_times[name] = times + name_to_values[name] = values + + # Add Data to NWBFile + behavior_module = nwb_helpers.get_module( + nwbfile=nwbfile, + name="behavior", + description="Behavioral data from the experiment.", + ) + + # Add BehavioralTimeSeries + behavioral_time_series = BehavioralTimeSeries( + time_series=behavioral_time_series, + name="behavioral_time_series", + ) + behavior_module.add(behavioral_time_series) + + # Add Events + event_types_table = EventTypesTable(name="event_types", description="Metadata about event types.") + event_type_name_to_row = dict() + i = 0 + for event_dict in metadata["Behavior"]["Events"]: + event_type_name_to_row[event_dict["name"]] = i + event_types_table.add_row( + event_name=event_dict["name"], + event_type_description=event_dict["description"], + ) + i += 1 + for event_dict in metadata["Behavior"]["ValuedEvents"]: + event_type_name_to_row[event_dict["name"]] = i + event_types_table.add_row( + event_name=event_dict["name"], + event_type_description=event_dict["description"], + ) + i += 1 + events_table = EventsTable( + name="events_table", + description="Metadata about events.", + target_tables={"event_type": event_types_table}, + ) + for event_dict in metadata["Behavior"]["Events"]: + event_times = name_to_times[event_dict["name"]] + event_type = event_type_name_to_row[event_dict["name"]] + for event_time in event_times: + events_table.add_row(timestamp=event_time, event_type=event_type) + valued_events_table = EventsTable( + name="valued_events_table", + description="Metadata about valued events.", + target_tables={"event_type": event_types_table}, + ) + valued_events_table.add_column(name="value", description="Value of the event.") + for event_dict in metadata["Behavior"]["ValuedEvents"]: + event_times = name_to_times[event_dict["name"]] + event_values = name_to_values[event_dict["name"]] + event_type = event_type_name_to_row[event_dict["name"]] + for event_time, event_value in zip(event_times, event_values): + valued_events_table.add_row(timestamp=event_time, event_type=event_type, value=event_value) + behavior_module.add(events_table) + behavior_module.add(valued_events_table) - raise NotImplementedError() + task = Task(event_types=event_types_table) + nwbfile.add_lab_meta_data(task) diff --git a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_convert_session.py b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_convert_session.py index b8686c4..ede8e74 100644 --- a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_convert_session.py +++ b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_convert_session.py @@ -18,6 +18,7 @@ def session_to_nwb(data_dir_path: Union[str, Path], output_dir_path: Union[str, output_dir_path = Path(output_dir_path) recording_folder_path = data_dir_path / "Raw Ephys" / "m69_2023-10-31_17-24-15_Day1_A1" sorting_folder_path = data_dir_path / "Processed Ephys" / "m69_2023-10-31_17-24-15_Day1_A1" + behavior_file_path = data_dir_path / "Behavior" / "m69_231031" / "raw_m69_231031_001.mat" if stub_test: output_dir_path = output_dir_path / "nwb_stub" recording_folder_path = recording_folder_path.with_name(recording_folder_path.name + "_stubbed") @@ -38,9 +39,9 @@ def session_to_nwb(data_dir_path: Union[str, Path], output_dir_path: Union[str, source_data.update(dict(Sorting=dict(folder_path=sorting_folder_path))) conversion_options.update(dict(Sorting=dict())) - # # Add Behavior - # source_data.update(dict(Behavior=dict())) - # conversion_options.update(dict(Behavior=dict())) + # Add Behavior + source_data.update(dict(Behavior=dict(file_path=behavior_file_path))) + conversion_options.update(dict(Behavior=dict())) converter = Schneider2024NWBConverter(source_data=source_data) diff --git a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_metadata.yaml b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_metadata.yaml index a6d749c..d85b00f 100644 --- a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_metadata.yaml +++ b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_metadata.yaml @@ -30,27 +30,27 @@ Ecephys: ElectricalSeries: - name: ElectricalSeries description: Recording of AC neural responses in mice performing this behavioral task will utilize dense 128-channel recording probes (Masmanidis Lab). These recording probes span a depth ~1mm, allowing for sampling of all layers of cortex. Electrophysiology data will be recorded using OpenEphys Acquisition Board v2.4 and associated OpenEphys GUI software. - # UnitProperties: - # - name: n_spikes - # description: Number of spikes recorded from each unit. - # - name: fr - # description: Average firing rate of each unit. - # - name: depth - # description: Estimated depth of each unit in micrometers. - # - name: Amplitude - # description: Per-template amplitudes, computed as the L2 norm of the template. - # - name: ContamPct - # description: Contamination rate for each template, computed as fraction of refractory period violations relative to expectation based on a Poisson process. - # - name: KSLabel - # description: Label indicating whether each template is 'mua' (multi-unit activity) or 'good' (refractory). - # - name: original_cluster_id - # description: Original cluster ID assigned by Kilosort. - # - name: amp - # description: For every template, the maximum amplitude of the template waveforms across all channels. - # - name: ch - # description: The channel label of the best channel, as defined by the user. - # - name: sh - # description: The shank label of the best channel. + +Behavior: + TimeSeries: + - name: encoder + description: Sampled values for entire duration of experiment for lever pressing/treadmill behavior read from a quadrature encoder. + - name: lick + description: Samples values for entire duration of experiment for voltage signal readout from an infrared/capacitive) lickometer sensor. + Events: + - name: target + description: Time at which the target zone is entered during a press. + - name: targetOUT + description: Time at which the target zone is overshot during a press. + - name: toneIN + description: Time at which target entry tone is played. + - name: toneOUT + description: Time at which target exit tone is played. + - name: valve + description: Times at which solenoid valve opens to deliver water after a correct trial. + ValuedEvents: + - name: tuningTones + description: Times at which tuning tones are played to an animal after a behavioral experiment during ephys recording sessions. Sorting: units_description: Neural spikes will be sorted offline using Kilosort 2.5 and Phy2 software and manually curated to ensure precise spike time acquisition. diff --git a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_notes.md b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_notes.md index 5ae3cf7..d09b9fb 100644 --- a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_notes.md +++ b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_notes.md @@ -1,9 +1,15 @@ # Notes concerning the schneider_2024 conversion +## Behavior +- opto events is just [0, 1] like empty variables (lick events), and opto_time in 'push' is all NaNs. Where is the actual opto stim times recorded? +- TuningTones has a values field with integers, what does this correspond to? +- 'push' is basically some kind of trials table, but need descriptions for variables ex. ITI_respect? + ## Data Requests - Mice sexes - Remaining data for Grant's project - More detailed position info for recording probe - Subfield of auditory cortex: A1? A2? AAF? etc. - stereotactic coordinates of the whole probe -- \ No newline at end of file +- Detailed description of the behavioral paradigm +- Description of lickometer and lever/treadmill quadrature encoder. diff --git a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_nwbconverter.py b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_nwbconverter.py index 5806bc5..4996d62 100644 --- a/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_nwbconverter.py +++ b/src/schneider_lab_to_nwb/schneider_2024/schneider_2024_nwbconverter.py @@ -14,4 +14,5 @@ class Schneider2024NWBConverter(NWBConverter): data_interface_classes = dict( Recording=OpenEphysRecordingInterface, Sorting=PhySortingInterface, + Behavior=Schneider2024BehaviorInterface, )