From 5d73568fa24939cd9ea6ddbc8ef52ac92d76a4b6 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Mon, 17 Jun 2024 16:58:10 +0200 Subject: [PATCH 1/9] add custom behavior interface --- src/howe_lab_to_nwb/vu2024/__init__.py | 1 - .../vu2024/interfaces/__init__.py | 1 + .../interfaces/vu2024_behaviorinterface.py | 225 ++++++++++++++++++ .../vu2024/vu2024behaviorinterface.py | 27 --- 4 files changed, 226 insertions(+), 28 deletions(-) create mode 100644 src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py delete mode 100644 src/howe_lab_to_nwb/vu2024/vu2024behaviorinterface.py diff --git a/src/howe_lab_to_nwb/vu2024/__init__.py b/src/howe_lab_to_nwb/vu2024/__init__.py index 7d9d095..127bd84 100644 --- a/src/howe_lab_to_nwb/vu2024/__init__.py +++ b/src/howe_lab_to_nwb/vu2024/__init__.py @@ -1,2 +1 @@ -from .vu2024behaviorinterface import Vu2024BehaviorInterface from .vu2024nwbconverter import Vu2024NWBConverter diff --git a/src/howe_lab_to_nwb/vu2024/interfaces/__init__.py b/src/howe_lab_to_nwb/vu2024/interfaces/__init__.py index 3c99cfa..c917ba5 100644 --- a/src/howe_lab_to_nwb/vu2024/interfaces/__init__.py +++ b/src/howe_lab_to_nwb/vu2024/interfaces/__init__.py @@ -1,2 +1,3 @@ from .vu2024_fiberphotometryinterface import Vu2024FiberPhotometryInterface from .cxdimaginginterface import CxdImagingInterface +from .vu2024_behaviorinterface import Vu2024BehaviorInterface diff --git a/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py b/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py new file mode 100644 index 0000000..b614d08 --- /dev/null +++ b/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py @@ -0,0 +1,225 @@ +from typing import Optional + +import numpy as np +import pandas as pd +from neuroconv import BaseTemporalAlignmentInterface +from neuroconv.tools import get_module +from neuroconv.utils import FilePathType +from pymatreader import read_mat +from pynwb import NWBFile +from pynwb.behavior import SpatialSeries, CompassDirection, BehavioralTimeSeries +from pynwb.epoch import TimeIntervals + + +class Vu2024BehaviorInterface(BaseTemporalAlignmentInterface): + """ + Interface for reading the processed behavior data from .mat files from the Howe Lab. + """ + + display_name = "Vu2024Behavior" + associated_suffixes = (".mat",) + info = "Interface for behavior data (.mat) from the Howe Lab." + + def __init__( + self, + file_path: FilePathType, + verbose: bool = True, + ): + """ + DataInterface for reading the processed behavior data from .mat files from the Howe Lab. + + Parameters + ---------- + file_path : FilePathType + Path to the .mat file that contains the fiber photometry data. + verbose : bool, default: True + controls verbosity. + """ + + super().__init__(file_path=file_path, verbose=verbose) + self._timestamps = None + + def get_original_timestamps(self) -> np.ndarray: + filename = self.source_data["file_path"] + behavior_data = read_mat(filename=filename) + if "timestamp" not in behavior_data: + raise ValueError(f"Expected 'timestamp' is not in '{filename}'.") + timestamps = behavior_data["timestamp"] + + return timestamps + + def get_timestamps(self, stub_test: bool = False) -> np.ndarray: + timestamps = self._timestamps if self._timestamps is not None else self.get_original_timestamps() + if stub_test: + return timestamps[:6000] + return timestamps + + def set_aligned_timestamps(self, aligned_timestamps: np.ndarray) -> None: + self._timestamps = np.array(aligned_timestamps) + + def get_metadata(self): + metadata = super().get_metadata() + + metadata["Behavior"] = dict( + CompassDirection=dict( + SpatialSeries=dict( + name="SpatialSeries", + description="The yaw (rotational) velocity measured in degrees/s.", + reference_frame="unknown", + unit="degrees/s", + ), + ), + BehavioralTimeSeries=dict( + SpatialSeries=dict( + name="SpatialSeries", + description="Velocity for the roll and pitch (x, y) measured in m/s.", + reference_frame="unknown", + unit="m/s", + ), + ), + TimeIntervals=dict( + name="TimeIntervals", + description="The onset times of the events (licking, tone, light or reward delivery).", + ), + ) + + return metadata + + def add_velocity_signals(self, nwbfile: NWBFile, metadata: dict, stub_test: Optional[bool] = False) -> None: + """ + Add the velocity signals (yaw (z) velocity and the roll and pitch (x, y)) to the NWBFile. + The yaw velocity is measured in degrees/s and the roll and pitch velocities are measured in m/s. + + Parameters + ---------- + nwbfile : NWBFile + The NWBFile to which the velocity signals will be added. + metadata : dict + Metadata for the velocity signals. + stub_test : bool, optional + Whether to run a stub test, by default False. + """ + + end_frame = 6000 if stub_test else None + + behavior_metadata = metadata["Behavior"] + spatial_series_metadata = behavior_metadata["CompassDirection"]["SpatialSeries"] + + behavior_data = read_mat(filename=self.source_data["file_path"]) + timestamps = self.get_timestamps(stub_test=stub_test) + + compass_direction = CompassDirection(name="CompassDirection") + + compass_direction.create_spatial_series( + **spatial_series_metadata, + data=behavior_data["ballYaw"][:end_frame], + timestamps=timestamps, + ) + + behavior = get_module( + nwbfile, + name="behavior", + description="Contains the velocity signals from two optical mouse sensors (Logitech G203 mice with hard plastic shells removed).", + ) + behavior.add(compass_direction) + + behavioral_time_series_metadata = behavior_metadata["BehavioralTimeSeries"]["SpatialSeries"] + behavioral_time_series = BehavioralTimeSeries(name="BehavioralTimeSeries") + + data = np.column_stack((behavior_data["ballRoll"], behavior_data["ballPitch"])) + spatial_series = SpatialSeries( + **behavioral_time_series_metadata, + data=data[:end_frame], + timestamps=timestamps, + ) + behavioral_time_series.add_timeseries(spatial_series) + behavior.add(behavioral_time_series) + + def _get_start_end_times(self, binary_event_data: np.ndarray): + + timestamps = self.get_timestamps() + ones_indices = np.where(binary_event_data == 1)[0] + if not len(ones_indices): + return [], [] + + # Calculate the differences between consecutive indices + diff = np.diff(ones_indices) + + # Find where the difference is not 1 + ends = np.where(diff != 1)[0] + + # The start of an interval is one index after the end of the previous interval + starts = ends + 1 + + # Handle the case for the first interval + starts = np.insert(starts, 0, 0) + + # Handle the case for the last interval + ends = np.append(ends, len(ones_indices) - 1) + + # Return the start and end times of the intervals + start_times = timestamps[ones_indices[starts]] + end_times = timestamps[ones_indices[ends]] + + return start_times, end_times + + def add_binary_signals(self, nwbfile: NWBFile, metadata: dict): + behavior_data = read_mat(filename=self.source_data["file_path"]) + + events_metadata = metadata["Behavior"]["TimeIntervals"] + + event_name_mapping = dict( + lick="Lick", + reward="Reward", + stimulus_led="Light", + stimulus_sound="Tone", + ) + + event_dfs = [] + for event_name in ["lick", "reward", "stimulus_led", "stimulus_sound"]: + start_times, end_times = self._get_start_end_times(binary_event_data=behavior_data[event_name]) + if not len(start_times): + continue + + event_dfs.append( + pd.DataFrame( + { + "start_time": start_times, + "stop_time": end_times, + "event_type": event_name_mapping[event_name], + } + ) + ) + + if not len(event_dfs): + return + + events = TimeIntervals(**events_metadata) + events.add_column( + name="event_type", + description="The type of event (licking, light, tone or reward delivery).", + ) + + event_df = pd.concat(event_dfs) + event_df = event_df.sort_values(by="start_time") + event_df = event_df.reset_index(drop=True) + for row_ind, row in event_df.iterrows(): + events.add_interval( + start_time=row["start_time"], + stop_time=row["stop_time"], + event_type=row["event_type"], + id=row_ind, + ) + + behavior = get_module(nwbfile, name="behavior") + behavior.add(events) + + def add_to_nwbfile( + self, + nwbfile: NWBFile, + metadata: dict, + stub_test: Optional[bool] = False, + ) -> None: + + self.add_velocity_signals(nwbfile=nwbfile, metadata=metadata, stub_test=stub_test) + self.add_binary_signals(nwbfile=nwbfile, metadata=metadata) diff --git a/src/howe_lab_to_nwb/vu2024/vu2024behaviorinterface.py b/src/howe_lab_to_nwb/vu2024/vu2024behaviorinterface.py deleted file mode 100644 index 9daa990..0000000 --- a/src/howe_lab_to_nwb/vu2024/vu2024behaviorinterface.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Primary class for converting experiment-specific behavior.""" - -from pynwb.file import NWBFile - -from neuroconv.basedatainterface import BaseDataInterface -from neuroconv.utils import DeepDict - - -class Vu2024BehaviorInterface(BaseDataInterface): - """Behavior interface for vu2024 conversion""" - - keywords = ["behavior"] - - def __init__(self): - # This should load the data lazily and prepare variables you need - pass - - def get_metadata(self) -> DeepDict: - # Automatically retrieve as much metadata as possible from the source files available - 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 - - raise NotImplementedError() From d21bb9d8742ce6abd29e53615ffd48d8d4979379 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Mon, 17 Jun 2024 16:58:33 +0200 Subject: [PATCH 2/9] add to converter --- src/howe_lab_to_nwb/vu2024/vu2024nwbconverter.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/howe_lab_to_nwb/vu2024/vu2024nwbconverter.py b/src/howe_lab_to_nwb/vu2024/vu2024nwbconverter.py index aebda95..392278a 100644 --- a/src/howe_lab_to_nwb/vu2024/vu2024nwbconverter.py +++ b/src/howe_lab_to_nwb/vu2024/vu2024nwbconverter.py @@ -4,7 +4,11 @@ from neuroconv.datainterfaces import TiffImagingInterface from neuroconv.utils import DeepDict -from howe_lab_to_nwb.vu2024.interfaces import Vu2024FiberPhotometryInterface, CxdImagingInterface +from howe_lab_to_nwb.vu2024.interfaces import ( + CxdImagingInterface, + Vu2024FiberPhotometryInterface, + Vu2024BehaviorInterface, +) class Vu2024NWBConverter(NWBConverter): @@ -14,6 +18,7 @@ class Vu2024NWBConverter(NWBConverter): Imaging=CxdImagingInterface, ProcessedImaging=TiffImagingInterface, FiberPhotometry=Vu2024FiberPhotometryInterface, + Behavior=Vu2024BehaviorInterface, ) def get_metadata_schema(self) -> dict: From 207024fd93222dc83e321db445abfa872ab2ca37 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Mon, 17 Jun 2024 16:58:46 +0200 Subject: [PATCH 3/9] modify convert session script --- .../vu2024/vu2024_convert_single_wavelength_session.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/howe_lab_to_nwb/vu2024/vu2024_convert_single_wavelength_session.py b/src/howe_lab_to_nwb/vu2024/vu2024_convert_single_wavelength_session.py index 02af27b..e62b8e2 100644 --- a/src/howe_lab_to_nwb/vu2024/vu2024_convert_single_wavelength_session.py +++ b/src/howe_lab_to_nwb/vu2024/vu2024_convert_single_wavelength_session.py @@ -20,6 +20,7 @@ def single_wavelength_session_to_nwb( indicator: str, ttl_file_path: Union[str, Path], motion_corrected_imaging_file_path: Union[str, Path], + behavior_file_path: Union[str, Path], nwbfile_path: Union[str, Path], sampling_frequency: float = None, stub_test: bool = False, @@ -87,6 +88,10 @@ def single_wavelength_session_to_nwb( dict(FiberPhotometry=dict(stub_test=stub_test, fiber_locations_metadata=fiber_locations_metadata)) ) + # Add behavior + source_data.update(dict(Behavior=dict(file_path=str(behavior_file_path)))) + conversion_options.update(dict(Behavior=dict(stub_test=stub_test))) + converter = Vu2024NWBConverter(source_data=source_data) # Add datetime to conversion @@ -128,6 +133,7 @@ def single_wavelength_session_to_nwb( ttl_file_path = Path("/Volumes/t7-ssd/Howe/DL18/211110/GridDL-18_2021.11.10_16.12.31.mat") fiber_locations_file_path = Path("/Volumes/t7-ssd/Howe/DL18/DL18_fiber_locations.xlsx") motion_corrected_imaging_file_path = Path("/Volumes/t7-ssd/Howe/DL18/211110/Data00217_crop_MC.tif") + behavior_file_path = Path("/Volumes/t7-ssd/Howe/DL18/211110/GridDL-18_2021.11.10_16.12.31_ttlIn1_movie1.mat") # The sampling frequency of the raw imaging data must be provided when it cannot be extracted from the .cxd file sampling_frequency = None @@ -146,6 +152,7 @@ def single_wavelength_session_to_nwb( excitation_wavelength_in_nm=excitation_wavelength_in_nm, indicator=indicator, motion_corrected_imaging_file_path=motion_corrected_imaging_file_path, + behavior_file_path=behavior_file_path, nwbfile_path=nwbfile_path, sampling_frequency=sampling_frequency, stub_test=stub_test, From 074e8011908818db821a1f94c93139726142929e Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Tue, 18 Jun 2024 12:25:56 +0200 Subject: [PATCH 4/9] add velocity as timeseries --- .../interfaces/vu2024_behaviorinterface.py | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py b/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py index b614d08..7ae0c0c 100644 --- a/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py +++ b/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py @@ -6,8 +6,8 @@ from neuroconv.tools import get_module from neuroconv.utils import FilePathType from pymatreader import read_mat -from pynwb import NWBFile -from pynwb.behavior import SpatialSeries, CompassDirection, BehavioralTimeSeries +from pynwb import NWBFile, TimeSeries +from pynwb.behavior import CompassDirection from pynwb.epoch import TimeIntervals @@ -69,13 +69,10 @@ def get_metadata(self): unit="degrees/s", ), ), - BehavioralTimeSeries=dict( - SpatialSeries=dict( - name="SpatialSeries", - description="Velocity for the roll and pitch (x, y) measured in m/s.", - reference_frame="unknown", - unit="m/s", - ), + TimeSeries=dict( + name="Velocity", + description="Velocity for the roll and pitch (x, y) measured in m/s.", + unit="m/s", ), TimeIntervals=dict( name="TimeIntervals", @@ -123,17 +120,14 @@ def add_velocity_signals(self, nwbfile: NWBFile, metadata: dict, stub_test: Opti ) behavior.add(compass_direction) - behavioral_time_series_metadata = behavior_metadata["BehavioralTimeSeries"]["SpatialSeries"] - behavioral_time_series = BehavioralTimeSeries(name="BehavioralTimeSeries") - + velocity_metadata = behavior_metadata["TimeSeries"] data = np.column_stack((behavior_data["ballRoll"], behavior_data["ballPitch"])) - spatial_series = SpatialSeries( - **behavioral_time_series_metadata, + velocity_xy = TimeSeries( + **velocity_metadata, data=data[:end_frame], timestamps=timestamps, ) - behavioral_time_series.add_timeseries(spatial_series) - behavior.add(behavioral_time_series) + behavior.add(velocity_xy) def _get_start_end_times(self, binary_event_data: np.ndarray): From 1f6c7d1de9b6e0979549f529bd6be669b0d148b5 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Tue, 18 Jun 2024 15:44:14 +0200 Subject: [PATCH 5/9] define metadata schema for behavior interface --- .../interfaces/vu2024_behaviorinterface.py | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py b/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py index 7ae0c0c..47ee636 100644 --- a/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py +++ b/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py @@ -4,7 +4,7 @@ import pandas as pd from neuroconv import BaseTemporalAlignmentInterface from neuroconv.tools import get_module -from neuroconv.utils import FilePathType +from neuroconv.utils import FilePathType, get_base_schema from pymatreader import read_mat from pynwb import NWBFile, TimeSeries from pynwb.behavior import CompassDirection @@ -57,6 +57,48 @@ def get_timestamps(self, stub_test: bool = False) -> np.ndarray: def set_aligned_timestamps(self, aligned_timestamps: np.ndarray) -> None: self._timestamps = np.array(aligned_timestamps) + def get_metadata_schema(self) -> dict: + metadata_schema = super().get_metadata_schema() + metadata_schema["properties"]["Behavior"] = get_base_schema(tag="Behavior") + metadata_schema["properties"]["Behavior"].update( + required=["CompassDirection", "TimeSeries", "TimeIntervals"], + properties=dict( + CompassDirection=dict( + type="object", + required=["SpatialSeries"], + properties=dict( + SpatialSeries=dict( + type="object", + required=["name", "description", "unit"], + properties=dict( + name=dict(type="string"), + description=dict(type="string"), + unit=dict(type="string"), + ), + ), + ), + ), + TimeSeries=dict( + type="object", + required=["name", "description", "unit"], + properties=dict( + name=dict(type="string"), + description=dict(type="string"), + unit=dict(type="string"), + ), + ), + TimeIntervals=dict( + type="object", + required=["name", "description"], + properties=dict( + name=dict(type="string"), + description=dict(type="string"), + ), + ), + ), + ) + return metadata_schema + def get_metadata(self): metadata = super().get_metadata() From acd2a4e3953d301106db312e9aadea60a44d30e0 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Tue, 18 Jun 2024 15:46:01 +0200 Subject: [PATCH 6/9] add behavioral camera recording --- .../vu2024_convert_single_wavelength_session.py | 9 +++++++++ .../vu2024/vu2024_requirements.txt | 1 + src/howe_lab_to_nwb/vu2024/vu2024nwbconverter.py | 15 ++++++++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/howe_lab_to_nwb/vu2024/vu2024_convert_single_wavelength_session.py b/src/howe_lab_to_nwb/vu2024/vu2024_convert_single_wavelength_session.py index e62b8e2..fc8123f 100644 --- a/src/howe_lab_to_nwb/vu2024/vu2024_convert_single_wavelength_session.py +++ b/src/howe_lab_to_nwb/vu2024/vu2024_convert_single_wavelength_session.py @@ -22,6 +22,7 @@ def single_wavelength_session_to_nwb( motion_corrected_imaging_file_path: Union[str, Path], behavior_file_path: Union[str, Path], nwbfile_path: Union[str, Path], + behavior_avi_file_path: Union[str, Path] = None, sampling_frequency: float = None, stub_test: bool = False, ): @@ -92,6 +93,11 @@ def single_wavelength_session_to_nwb( source_data.update(dict(Behavior=dict(file_path=str(behavior_file_path)))) conversion_options.update(dict(Behavior=dict(stub_test=stub_test))) + # Add behavior camera recording + if behavior_avi_file_path is not None: + source_data.update(dict(Video=dict(file_paths=[str(behavior_avi_file_path)]))) + conversion_options.update(dict(Video=dict(stub_test=stub_test))) + converter = Vu2024NWBConverter(source_data=source_data) # Add datetime to conversion @@ -134,6 +140,8 @@ def single_wavelength_session_to_nwb( fiber_locations_file_path = Path("/Volumes/t7-ssd/Howe/DL18/DL18_fiber_locations.xlsx") motion_corrected_imaging_file_path = Path("/Volumes/t7-ssd/Howe/DL18/211110/Data00217_crop_MC.tif") behavior_file_path = Path("/Volumes/t7-ssd/Howe/DL18/211110/GridDL-18_2021.11.10_16.12.31_ttlIn1_movie1.mat") + # optional + behavior_camera_recording = Path("/Volumes/t7-ssd/Howe/DL18/211110/DL18-lick-11102021161113-0000.avi") # The sampling frequency of the raw imaging data must be provided when it cannot be extracted from the .cxd file sampling_frequency = None @@ -153,6 +161,7 @@ def single_wavelength_session_to_nwb( indicator=indicator, motion_corrected_imaging_file_path=motion_corrected_imaging_file_path, behavior_file_path=behavior_file_path, + behavior_avi_file_path=behavior_camera_recording, nwbfile_path=nwbfile_path, sampling_frequency=sampling_frequency, stub_test=stub_test, diff --git a/src/howe_lab_to_nwb/vu2024/vu2024_requirements.txt b/src/howe_lab_to_nwb/vu2024/vu2024_requirements.txt index e375cd5..1e8a883 100644 --- a/src/howe_lab_to_nwb/vu2024/vu2024_requirements.txt +++ b/src/howe_lab_to_nwb/vu2024/vu2024_requirements.txt @@ -2,3 +2,4 @@ pymatreader==0.0.32 ndx-fiber-photometry@git+https://github.com/catalystneuro/ndx-fiber-photometry.git@main openpyxl==3.1.2 aicsimageio>=4.14.0 +neuroconv[video] diff --git a/src/howe_lab_to_nwb/vu2024/vu2024nwbconverter.py b/src/howe_lab_to_nwb/vu2024/vu2024nwbconverter.py index 392278a..6c60823 100644 --- a/src/howe_lab_to_nwb/vu2024/vu2024nwbconverter.py +++ b/src/howe_lab_to_nwb/vu2024/vu2024nwbconverter.py @@ -1,8 +1,10 @@ """Primary NWBConverter class for this dataset.""" from neuroconv import NWBConverter -from neuroconv.datainterfaces import TiffImagingInterface +from neuroconv.datainterfaces import TiffImagingInterface, VideoInterface +from neuroconv.tools.signal_processing import get_rising_frames_from_ttl from neuroconv.utils import DeepDict +from pymatreader import read_mat from howe_lab_to_nwb.vu2024.interfaces import ( CxdImagingInterface, @@ -19,6 +21,7 @@ class Vu2024NWBConverter(NWBConverter): ProcessedImaging=TiffImagingInterface, FiberPhotometry=Vu2024FiberPhotometryInterface, Behavior=Vu2024BehaviorInterface, + Video=VideoInterface, ) def get_metadata_schema(self) -> dict: @@ -49,3 +52,13 @@ def temporally_align_data_interfaces(self): # The timestamps from the fiber photometry data is from the TTL signals fiber_photometry_timestamps = fiber_photometry.get_timestamps() imaging.set_aligned_timestamps(aligned_timestamps=fiber_photometry_timestamps) + + if "Video" in self.data_interface_objects: + video = self.data_interface_objects["Video"] + video_timestamps = video.get_timestamps() + ttl_file_path = fiber_photometry.source_data["ttl_file_path"] + ttl_data = read_mat(filename=ttl_file_path) + first_ttl_frame = get_rising_frames_from_ttl(trace=ttl_data["ttlIn3"])[0] + video.set_aligned_segment_starting_times( + aligned_segment_starting_times=[video_timestamps[0][first_ttl_frame]] + ) From f373860871732138f17adbfb53dcaf626cd7f9c7 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Tue, 18 Jun 2024 15:48:23 +0200 Subject: [PATCH 7/9] fill docstring --- .../vu2024_convert_single_wavelength_session.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/howe_lab_to_nwb/vu2024/vu2024_convert_single_wavelength_session.py b/src/howe_lab_to_nwb/vu2024/vu2024_convert_single_wavelength_session.py index fc8123f..1efe7fb 100644 --- a/src/howe_lab_to_nwb/vu2024/vu2024_convert_single_wavelength_session.py +++ b/src/howe_lab_to_nwb/vu2024/vu2024_convert_single_wavelength_session.py @@ -31,15 +31,31 @@ def single_wavelength_session_to_nwb( Parameters ---------- + raw_imaging_file_path : Union[str, Path] + The path to the .cxd file containing the raw imaging data. raw_fiber_photometry_file_path : Union[str, Path] The path to the .mat file containing the raw fiber photometry data. fiber_locations_file_path : Union[str, Path] The path to the .xlsx file containing the fiber locations. + excitation_wavelength_in_nm : int + The excitation wavelength in nm. + indicator : str + The name of the indicator used for the fiber photometry recording. ttl_file_path : Union[str, Path] The path to the .mat file containing the TTL signals. + motion_corrected_imaging_file_path : Union[str, Path] + The path to the .tif file containing the motion corrected imaging data. + behavior_file_path : Union[str, Path] + The path to the .mat file containing the processed behavior data. + nwbfile_path : Union[str, Path] + The path to the NWB file to be created. + behavior_avi_file_path : Union[str, Path], optional + The path to the .avi file containing the behavior camera recording. optional sampling_frequency : float, optional The sampling frequency of the data. If None, the sampling frequency will be read from the .cxd file. If missing from the file, the sampling frequency must be provided. + stub_test : bool, optional + Whether to run a stub test, by default False. """ raw_fiber_photometry_file_path = Path(raw_fiber_photometry_file_path) From dcfa7aa24e94a486a9c2c63159e43e8d9d57e2cf Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Fri, 28 Jun 2024 13:47:08 +0200 Subject: [PATCH 8/9] add all velocities as timeseries --- .../interfaces/vu2024_behaviorinterface.py | 111 +++++++++--------- 1 file changed, 57 insertions(+), 54 deletions(-) diff --git a/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py b/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py index 47ee636..be93859 100644 --- a/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py +++ b/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py @@ -4,10 +4,9 @@ import pandas as pd from neuroconv import BaseTemporalAlignmentInterface from neuroconv.tools import get_module -from neuroconv.utils import FilePathType, get_base_schema +from neuroconv.utils import FilePathType, get_base_schema, get_schema_from_hdmf_class from pymatreader import read_mat from pynwb import NWBFile, TimeSeries -from pynwb.behavior import CompassDirection from pynwb.epoch import TimeIntervals @@ -60,32 +59,14 @@ def set_aligned_timestamps(self, aligned_timestamps: np.ndarray) -> None: def get_metadata_schema(self) -> dict: metadata_schema = super().get_metadata_schema() metadata_schema["properties"]["Behavior"] = get_base_schema(tag="Behavior") + time_series_schema = get_schema_from_hdmf_class(TimeSeries) metadata_schema["properties"]["Behavior"].update( - required=["CompassDirection", "TimeSeries", "TimeIntervals"], + required=["TimeSeries", "TimeIntervals"], properties=dict( - CompassDirection=dict( - type="object", - required=["SpatialSeries"], - properties=dict( - SpatialSeries=dict( - type="object", - required=["name", "description", "unit"], - properties=dict( - name=dict(type="string"), - description=dict(type="string"), - unit=dict(type="string"), - ), - ), - ), - ), TimeSeries=dict( - type="object", - required=["name", "description", "unit"], - properties=dict( - name=dict(type="string"), - description=dict(type="string"), - unit=dict(type="string"), - ), + type="array", + minItems=1, + items=time_series_schema, ), TimeIntervals=dict( type="object", @@ -103,19 +84,18 @@ def get_metadata(self): metadata = super().get_metadata() metadata["Behavior"] = dict( - CompassDirection=dict( - SpatialSeries=dict( - name="SpatialSeries", - description="The yaw (rotational) velocity measured in degrees/s.", - reference_frame="unknown", - unit="degrees/s", + TimeSeries=[ + dict( + name="AngularVelocity", + description="The angular velocity from yaw (rotational) velocity converted to radians/s.", + unit="radians/s", ), - ), - TimeSeries=dict( - name="Velocity", - description="Velocity for the roll and pitch (x, y) measured in m/s.", - unit="m/s", - ), + dict( + name="Velocity", + description="Velocity for the roll and pitch (x, y) measured in m/s.", + unit="m/s", + ), + ], TimeIntervals=dict( name="TimeIntervals", description="The onset times of the events (licking, tone, light or reward delivery).", @@ -124,10 +104,17 @@ def get_metadata(self): return metadata - def add_velocity_signals(self, nwbfile: NWBFile, metadata: dict, stub_test: Optional[bool] = False) -> None: + def add_velocity_signals( + self, + nwbfile: NWBFile, + metadata: dict, + ball_diameter_in_meters: float = None, + stub_test: Optional[bool] = False, + ) -> None: """ Add the velocity signals (yaw (z) velocity and the roll and pitch (x, y)) to the NWBFile. - The yaw velocity is measured in degrees/s and the roll and pitch velocities are measured in m/s. + The yaw velocity is converted to radians/s using the calculation provided from the Howe Lab. + The roll and pitch velocities are in m/s. Parameters ---------- @@ -135,6 +122,10 @@ def add_velocity_signals(self, nwbfile: NWBFile, metadata: dict, stub_test: Opti The NWBFile to which the velocity signals will be added. metadata : dict Metadata for the velocity signals. + ball_diameter_in_meters : float, optional + The diameter of the ball in meters, optional. + Used to convert the yaw velocity from m/s to radians/s, when not specified default value is 0.2032. + Reference: https://zenodo.org/records/10272874 from ball2xy.m stub_test : bool, optional Whether to run a stub test, by default False. """ @@ -142,32 +133,38 @@ def add_velocity_signals(self, nwbfile: NWBFile, metadata: dict, stub_test: Opti end_frame = 6000 if stub_test else None behavior_metadata = metadata["Behavior"] - spatial_series_metadata = behavior_metadata["CompassDirection"]["SpatialSeries"] + time_series_metadata = behavior_metadata["TimeSeries"] behavior_data = read_mat(filename=self.source_data["file_path"]) timestamps = self.get_timestamps(stub_test=stub_test) - compass_direction = CompassDirection(name="CompassDirection") - - compass_direction.create_spatial_series( - **spatial_series_metadata, - data=behavior_data["ballYaw"][:end_frame], - timestamps=timestamps, - ) - behavior = get_module( nwbfile, name="behavior", description="Contains the velocity signals from two optical mouse sensors (Logitech G203 mice with hard plastic shells removed).", ) - behavior.add(compass_direction) - velocity_metadata = behavior_metadata["TimeSeries"] - data = np.column_stack((behavior_data["ballRoll"], behavior_data["ballPitch"])) + # ballYaw is in m/s convert to radians/s using their code + # https://zenodo.org/records/10272874 ball2xy.m + angular_velocity_metadata = time_series_metadata[0] + yaw_in_meters = behavior_data["ballYaw"][:end_frame] + ball_diameter_in_meters = ball_diameter_in_meters or 0.2032 + yaw_in_radians = yaw_in_meters / (np.pi * ball_diameter_in_meters) * 2 * np.pi + + velocity_z = TimeSeries( + **angular_velocity_metadata, + data=yaw_in_radians, + timestamps=timestamps, + ) + behavior.add(velocity_z) + + velocity_metadata = time_series_metadata[1] + velocity_in_meters = np.column_stack((behavior_data["ballRoll"], behavior_data["ballPitch"])) + velocity_in_meters = velocity_in_meters[:end_frame] velocity_xy = TimeSeries( **velocity_metadata, - data=data[:end_frame], - timestamps=timestamps, + data=velocity_in_meters, + timestamps=velocity_z, ) behavior.add(velocity_xy) @@ -255,7 +252,13 @@ def add_to_nwbfile( nwbfile: NWBFile, metadata: dict, stub_test: Optional[bool] = False, + ball_diameter_in_meters: Optional[float] = None, ) -> None: - self.add_velocity_signals(nwbfile=nwbfile, metadata=metadata, stub_test=stub_test) + self.add_velocity_signals( + nwbfile=nwbfile, + metadata=metadata, + stub_test=stub_test, + ball_diameter_in_meters=ball_diameter_in_meters, + ) self.add_binary_signals(nwbfile=nwbfile, metadata=metadata) From 7cc648ee63d57421bb7c5e99facf93d8ea88e576 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Fri, 28 Jun 2024 13:51:59 +0200 Subject: [PATCH 9/9] update docstring --- .../vu2024/interfaces/vu2024_behaviorinterface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py b/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py index be93859..dbb14a1 100644 --- a/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py +++ b/src/howe_lab_to_nwb/vu2024/interfaces/vu2024_behaviorinterface.py @@ -30,7 +30,8 @@ def __init__( Parameters ---------- file_path : FilePathType - Path to the .mat file that contains the fiber photometry data. + Path to the .mat file that contains the "binned" behavior data. + ("*ttlIn1_movie1.mat" files) verbose : bool, default: True controls verbosity. """