diff --git a/src/fox_lab_to_nwb/__init__.py b/src/fox_lab_to_nwb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fox_lab_to_nwb/behavior.py b/src/fox_lab_to_nwb/behavior.py new file mode 100644 index 0000000..9ac5721 --- /dev/null +++ b/src/fox_lab_to_nwb/behavior.py @@ -0,0 +1,107 @@ +from pathlib import Path +from typing import Optional + +from neuroconv import BaseDataInterface +from pydantic import FilePath +from pynwb import NWBFile +from pynwb import TimeSeries +from pymatreader import read_mat + + +class BehaviorInterface(BaseDataInterface): + def __init__(self, file_path: FilePath): + self.file_path = Path(file_path) + + def add_to_nwbfile( + self, + nwbfile: NWBFile, + metadata: Optional[dict] = None, + ) -> None: + + mat = read_mat(self.file_path) + rec_struct = mat["rec"] + daq_struct = rec_struct["daq"] + + timestamps = daq_struct["tstamps"] + + # Extracted from the matlab manually + # neither scipy or pymatreader can read string characters from the .mat file + channel_names = [ + "CamSync", + "CamTrigger", + "OptoTrigger", + "LWingBeatAmp", + "RWingBeatAmp", + "WingBeatFreq", + "LHutchen", + "RHutchen", + "PTrigger", + ] + + # TODO: figure how how to store this + # Synchronization signals + cam_sync = daq_struct["data"][:, 0] + cam_trigger = daq_struct["data"][:, 1] + opto_trigger = daq_struct["data"][:, 2] + ptrigger = daq_struct["data"][:, 8] + + # Behavior signals + left_wing_beat_amplitude = daq_struct["data"][:, 3] + right_wing_beat_amplitude = daq_struct["data"][:, 4] + wing_beat_frequency = daq_struct["data"][:, 5] + + unit = "tbd" + description = "tbd" + left_wing_beat_amplitude_time_series = TimeSeries( + name="LeftWingBeatAmplitudeTimeSeries", + data=left_wing_beat_amplitude, + unit=unit, + timestamps=timestamps, + description=description, + ) + + description = "tbd" + right_wing_beat_amplitude_time_series = TimeSeries( + name="RightWingBeatAmplitudeTimeSeries", + data=right_wing_beat_amplitude, + unit=unit, + timestamps=timestamps, + description=description, + ) + + description = "tbd" + unit = "Hz" # TODO: Figure this out, the values in the plot are around 3 but should be higher for flies + wing_beat_frequency_time_series = TimeSeries( + name="WingBeatFrequencyTimeSeries", + data=wing_beat_frequency, + unit=unit, + timestamps=timestamps, + description=description, + ) + + nwbfile.add_acquisition(left_wing_beat_amplitude_time_series) + nwbfile.add_acquisition(right_wing_beat_amplitude_time_series) + nwbfile.add_acquisition(wing_beat_frequency_time_series) + + # TODO: Ask Ben if this nesting makes sense? probably not + # time_series = [left_wing_beat_amplitude_time_series, right_wing_beat_amplitude_time_series, wing_beat_frequency_time_series] + # behavioral_time_series_container = BehavioralTimeSeries(name="BehavioralTimeSeries", time_series=time_series) + # nwbfile.add_acquisition(behavioral_time_series_container) + + # Not clear what are those signals, haltere? + lhutchen = daq_struct["data"][:, 6] + rhutchen = daq_struct["data"][:, 7] + + unit = "tbd" + description = "tbd" + lhutchen_time_series = TimeSeries( + name="LHutchenTimeSeries", data=lhutchen, unit=unit, timestamps=timestamps, description=description + ) + + description = "tbd" + rhutchen_time_series = TimeSeries( + name="RHutchenTimeSeries", data=rhutchen, unit=unit, timestamps=timestamps, description=description + ) + + nwbfile.add_acquisition(lhutchen_time_series) + nwbfile.add_acquisition(rhutchen_time_series) diff --git a/src/fox_lab_to_nwb/conversion.py b/src/fox_lab_to_nwb/conversion.py new file mode 100644 index 0000000..5fa3d35 --- /dev/null +++ b/src/fox_lab_to_nwb/conversion.py @@ -0,0 +1,73 @@ +from pathlib import Path +import time +from datetime import datetime +from typing import Optional +from zoneinfo import ZoneInfo +from neuroconv.utils import dict_deep_update, load_dict_from_file + +from neuroconv import ConverterPipe +from fox_lab_to_nwb.behavior import BehaviorInterface + + +def run_trial_conversion(trial_data_folder: Path, output_dir_path: Optional[Path] = None, verbose: bool = True): + + if verbose: + start_time = time.time() + + if output_dir_path is None: + output_dir_path = Path.home() / "conversion_nwb" + + # session_id = f"{subject}_{session_date}_{session_time}" + session_id = "session" + nwbfile_path = output_dir_path / f"{session_id}.nwb" + + file_path = trial_data_folder / "Tshx18D07_240124_115923_f3_r1.fly2" + assert file_path.exists(), f"File {file_path} does not exist" + interface = BehaviorInterface(file_path=file_path) + + converter = ConverterPipe(data_interfaces={"behavior": interface}) + + + # Add datetime to conversion + metadata = converter.get_metadata() + session_start_time = datetime.now().astimezone(ZoneInfo("America/New_York")) + metadata["NWBFile"]["session_start_time"] = session_start_time + + # Update default metadata with the editable in the corresponding yaml file + editable_metadata_path = Path(__file__).parent / "metadata.yaml" + editable_metadata = load_dict_from_file(editable_metadata_path) + metadata = dict_deep_update(metadata, editable_metadata) + + subject_metadata = metadata["Subject"] + subject = "subject" + subject_metadata["subject_id"] = f"{subject}" + + # Run conversion, this adds the basic data to the NWBFile + conversion_options = {} + converter.run_conversion( + metadata=metadata, + nwbfile_path=nwbfile_path, + conversion_options=conversion_options, + overwrite=True, + ) + + if verbose: + stop_time = time.time() + conversion_time_seconds = stop_time - start_time + if conversion_time_seconds <= 60 * 3: + print(f"Conversion took {conversion_time_seconds:.2f} seconds") + elif conversion_time_seconds <= 60 * 60: + print(f"Conversion took {conversion_time_seconds / 60:.2f} minutes") + else: + print(f"Conversion took {conversion_time_seconds / 60 / 60:.2f} hours") + + +if __name__ == "__main__": + + verbose = True + data_path = Path("/home/heberto/cohen_project/Sample data/Fox Lab") + assert data_path.exists(), f"Folder {data_path} does not exist" + trial_data_folder = data_path / "Tshx18D07_240124_115923_f3_r1" + assert trial_data_folder.exists(), f"Folder {trial_data_folder} does not exist" + + run_trial_conversion(trial_data_folder=trial_data_folder, verbose=verbose) diff --git a/src/fox_lab_to_nwb/conversion_notes.md b/src/fox_lab_to_nwb/conversion_notes.md index 36e8c73..d3c8b64 100644 --- a/src/fox_lab_to_nwb/conversion_notes.md +++ b/src/fox_lab_to_nwb/conversion_notes.md @@ -29,10 +29,11 @@ Channel names: - LWingBeatAmp - RWingBeatAmp - WingBeatFreq -- LHutchen +- LHutchen # What does Hutchen mean? Could be halteres? - RHutchen - PTrigger +These can't be extracted with pyton from matlab because they are strings. Are they always the same order? ## Cameras ### Fastec diff --git a/src/fox_lab_to_nwb/metadata.yaml b/src/fox_lab_to_nwb/metadata.yaml new file mode 100644 index 0000000..c215ad3 --- /dev/null +++ b/src/fox_lab_to_nwb/metadata.yaml @@ -0,0 +1,20 @@ +NWBFile: + keywords: + - Fly + - Wingbeat + - Behavior + related_publications: + - https://doi.org/great_publication or link to APA or MLA citation of the publication + session_description: + A rich text description of the experiment. Can also just be the abstract of the publication. + experiment_description: 'Task: Rapid serial visual presentation (RSVP).' + institution: Case Western Reserve University + lab: Fox + experimenter: + - Streets, Amy + - Lea, Kriss + surgery: TBD +Subject: + sex: M + species: Drosophila melanogaster # TODO: Figure this one out + description: fly