From b34286669e7e35a512126eecf203567911c45a3f Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Tue, 8 Oct 2024 13:13:30 -0600 Subject: [PATCH] add freezing behavior interface --- pyproject.toml | 2 +- .../assets/experimental_setup.excalidraw | 200 +++++++++--------- .../zaki_2024/behaviorinterface.py | 63 ++++-- .../zaki_2024/zaki_2024_notes.md | 21 +- 4 files changed, 162 insertions(+), 124 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c0181f5..9fdf298 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ include = ["*"] [tool.black] line-length = 120 -target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] +target-version = ['py39', 'py310', 'py311', 'py312'] include = '\.pyi?$' extend-exclude = ''' /( diff --git a/src/cai_lab_to_nwb/zaki_2024/assets/experimental_setup.excalidraw b/src/cai_lab_to_nwb/zaki_2024/assets/experimental_setup.excalidraw index 987adbe..2bce307 100644 --- a/src/cai_lab_to_nwb/zaki_2024/assets/experimental_setup.excalidraw +++ b/src/cai_lab_to_nwb/zaki_2024/assets/experimental_setup.excalidraw @@ -5,8 +5,8 @@ "elements": [ { "type": "text", - "version": 115, - "versionNonce": 444498512, + "version": 116, + "versionNonce": 1625403132, "isDeleted": false, "id": "L-aY7li9S6byMi9dRpI0g", "fillStyle": "solid", @@ -26,7 +26,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109247, + "updated": 1728393699195, "link": null, "locked": false, "fontSize": 20, @@ -111,8 +111,8 @@ }, { "type": "text", - "version": 181, - "versionNonce": 532198576, + "version": 182, + "versionNonce": 1519505092, "isDeleted": false, "id": "5skludqUfD0GwbR4KnOgE", "fillStyle": "solid", @@ -132,7 +132,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109247, + "updated": 1728393699196, "link": null, "locked": false, "fontSize": 20, @@ -217,8 +217,8 @@ }, { "type": "text", - "version": 214, - "versionNonce": 597943376, + "version": 215, + "versionNonce": 726298492, "isDeleted": false, "id": "xwJEPjW4IczAvod45gqWw", "fillStyle": "solid", @@ -238,7 +238,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109247, + "updated": 1728393699196, "link": null, "locked": false, "fontSize": 20, @@ -253,8 +253,8 @@ }, { "type": "text", - "version": 236, - "versionNonce": 1144473776, + "version": 237, + "versionNonce": 1483953732, "isDeleted": false, "id": "EMIlW1zYnf5zJmKH25xk7", "fillStyle": "solid", @@ -274,7 +274,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109247, + "updated": 1728393699197, "link": null, "locked": false, "fontSize": 20, @@ -289,8 +289,8 @@ }, { "type": "text", - "version": 245, - "versionNonce": 876796496, + "version": 246, + "versionNonce": 466034684, "isDeleted": false, "id": "kwlpwOZPvqUt2M6P7dWmp", "fillStyle": "solid", @@ -310,7 +310,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109247, + "updated": 1728393699197, "link": null, "locked": false, "fontSize": 20, @@ -1445,8 +1445,8 @@ }, { "type": "text", - "version": 76, - "versionNonce": 881223344, + "version": 77, + "versionNonce": 1069018564, "isDeleted": false, "id": "gPC4A3gIuTwcj_3GOA9Hy", "fillStyle": "solid", @@ -1466,7 +1466,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109248, + "updated": 1728393699198, "link": null, "locked": false, "fontSize": 20, @@ -1481,8 +1481,8 @@ }, { "type": "text", - "version": 130, - "versionNonce": 175938640, + "version": 131, + "versionNonce": 710525052, "isDeleted": false, "id": "YUgHMyD9DSCM5Y-BeBs0N", "fillStyle": "solid", @@ -1502,7 +1502,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109248, + "updated": 1728393699198, "link": null, "locked": false, "fontSize": 20, @@ -1517,8 +1517,8 @@ }, { "type": "text", - "version": 100, - "versionNonce": 1583747248, + "version": 101, + "versionNonce": 293442884, "isDeleted": false, "id": "h2z4qkZL4niP7z-OkJqNJ", "fillStyle": "solid", @@ -1538,7 +1538,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109249, + "updated": 1728393699198, "link": null, "locked": false, "fontSize": 20, @@ -1553,8 +1553,8 @@ }, { "type": "text", - "version": 46, - "versionNonce": 793023056, + "version": 47, + "versionNonce": 2126795004, "isDeleted": false, "id": "rIvjvyeT7VnWjccCzKmkw", "fillStyle": "solid", @@ -1574,7 +1574,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109249, + "updated": 1728393699199, "link": null, "locked": false, "fontSize": 20, @@ -1589,8 +1589,8 @@ }, { "type": "text", - "version": 70, - "versionNonce": 1119910576, + "version": 71, + "versionNonce": 2065845444, "isDeleted": false, "id": "1fNKXjICemKUtJaIrE8kK", "fillStyle": "solid", @@ -1610,7 +1610,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109249, + "updated": 1728393699199, "link": null, "locked": false, "fontSize": 20, @@ -1625,8 +1625,8 @@ }, { "type": "text", - "version": 184, - "versionNonce": 597542992, + "version": 185, + "versionNonce": 2022232444, "isDeleted": false, "id": "AgsUL343nkP_YeK0xO6lR", "fillStyle": "solid", @@ -1646,7 +1646,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109249, + "updated": 1728393699200, "link": null, "locked": false, "fontSize": 20, @@ -1661,8 +1661,8 @@ }, { "type": "text", - "version": 61, - "versionNonce": 1144890544, + "version": 62, + "versionNonce": 1313084484, "isDeleted": false, "id": "kOodhWII0ZHRllxW-VVyU", "fillStyle": "solid", @@ -1682,7 +1682,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109249, + "updated": 1728393699200, "link": null, "locked": false, "fontSize": 20, @@ -1697,8 +1697,8 @@ }, { "type": "text", - "version": 200, - "versionNonce": 1667251792, + "version": 201, + "versionNonce": 921256444, "isDeleted": false, "id": "yVDlE4w1dC7oAQXrBm1Ji", "fillStyle": "solid", @@ -1718,7 +1718,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109249, + "updated": 1728393699200, "link": null, "locked": false, "fontSize": 20, @@ -1733,8 +1733,8 @@ }, { "type": "text", - "version": 77, - "versionNonce": 814059184, + "version": 78, + "versionNonce": 2025909188, "isDeleted": false, "id": "5zUAGm74Zkxw6EnGiT7LM", "fillStyle": "solid", @@ -1754,7 +1754,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726851109249, + "updated": 1728393699201, "link": null, "locked": false, "fontSize": 20, @@ -1835,7 +1835,7 @@ "containerId": "tvKoPR8PTQt8QL8Uo5oB6", "originalText": "Ophys", "lineHeight": 1.25, - "baseline": 18 + "baseline": 19 }, { "type": "rectangle", @@ -1909,66 +1909,66 @@ "containerId": "CGhC3QzHie5PeaZ9SqCzY", "originalText": "Ophys", "lineHeight": 1.25, - "baseline": 18 + "baseline": 19 }, { - "id": "hTKFkUsoeSb91JPHBBE3d", "type": "text", - "x": 14.820414244974074, - "y": 903.027045085445, - "width": 1664.0625, - "height": 24, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ebfbee", + "version": 264, + "versionNonce": 1348900432, + "isDeleted": false, + "id": "hTKFkUsoeSb91JPHBBE3d", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 14.820414244974074, + "y": 903.027045085445, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ebfbee", + "width": 1664.0625, + "height": 24, + "seed": 982023856, "groupIds": [], "frameId": null, "roundness": null, - "seed": 982023856, - "version": 264, - "versionNonce": 1348900432, - "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1726851109249, "link": null, "locked": false, - "text": "When the animal is taken out of the vivarium then the EEG is sort of meaningless, signal strength data in the edf reader should indicate this.", "fontSize": 20, "fontFamily": 3, + "text": "When the animal is taken out of the vivarium then the EEG is sort of meaningless, signal strength data in the edf reader should indicate this.", "textAlign": "left", "verticalAlign": "top", - "baseline": 18, "containerId": null, "originalText": "When the animal is taken out of the vivarium then the EEG is sort of meaningless, signal strength data in the edf reader should indicate this.", - "lineHeight": 1.2 + "lineHeight": 1.2, + "baseline": 18 }, { - "id": "cEPATEz6uaUihnMDzjGnh", "type": "text", - "x": 1423.8608307898594, - "y": 475.64302011287236, - "width": 644.53125, - "height": 48, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ebfbee", + "version": 308, + "versionNonce": 1593859664, + "isDeleted": false, + "id": "cEPATEz6uaUihnMDzjGnh", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1423.8608307898594, + "y": 475.64302011287236, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ebfbee", + "width": 644.53125, + "height": 48, + "seed": 95078576, "groupIds": [], "frameId": null, "roundness": null, - "seed": 95078576, - "version": 308, - "versionNonce": 1593859664, - "isDeleted": false, "boundElements": [ { "id": "R-BsHhEycXPGmVRWi8Q3b", @@ -1978,55 +1978,44 @@ "updated": 1726851152071, "link": null, "locked": false, - "text": "We should build the pipeline assuming those are present\nThey are not present in the two sesions that we got.", "fontSize": 20, "fontFamily": 3, + "text": "We should build the pipeline assuming those are present\nThey are not present in the two sesions that we got.", "textAlign": "left", "verticalAlign": "top", - "baseline": 42, "containerId": null, "originalText": "We should build the pipeline assuming those are present\nThey are not present in the two sesions that we got.", - "lineHeight": 1.2 + "lineHeight": 1.2, + "baseline": 42 }, { - "id": "R-BsHhEycXPGmVRWi8Q3b", "type": "arrow", - "x": 1410.752729314161, - "y": 500.6511148735924, - "width": 81.99122307121911, - "height": 0.6787182339436981, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ebfbee", + "version": 391, + "versionNonce": 1535679664, + "isDeleted": false, + "id": "R-BsHhEycXPGmVRWi8Q3b", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1410.752729314161, + "y": 500.6511148735924, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ebfbee", + "width": 81.99122307121911, + "height": 0.6787182339436981, + "seed": 220011184, "groupIds": [], "frameId": null, "roundness": { "type": 2 }, - "seed": 220011184, - "version": 391, - "versionNonce": 1535679664, - "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1726851156839, "link": null, "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -81.99122307121911, - 0.6787182339436981 - ] - ], - "lastCommittedPoint": null, "startBinding": { "elementId": "cEPATEz6uaUihnMDzjGnh", "focus": 0.06630149545474452, @@ -2037,8 +2026,19 @@ "focus": 0.013187886470180023, "gap": 10.397794997601977 }, + "lastCommittedPoint": null, "startArrowhead": null, - "endArrowhead": "arrow" + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -81.99122307121911, + 0.6787182339436981 + ] + ] } ], "appState": { diff --git a/src/cai_lab_to_nwb/zaki_2024/behaviorinterface.py b/src/cai_lab_to_nwb/zaki_2024/behaviorinterface.py index a716c9b..ca7398b 100644 --- a/src/cai_lab_to_nwb/zaki_2024/behaviorinterface.py +++ b/src/cai_lab_to_nwb/zaki_2024/behaviorinterface.py @@ -1,42 +1,67 @@ """Primary class for converting experiment-specific behavior.""" + from pynwb.file import NWBFile from neuroconv.basedatainterface import BaseDataInterface from neuroconv.datainterfaces import VideoInterface from neuroconv.utils import DeepDict from pydantic import FilePath -from pynwb.epoch import TimeIntervals +from typing import Optional + class FreezingBehaviorInterface(BaseDataInterface): """Adds intervals of freezing behavior interface.""" keywords = ["behavior"] - - def __init__(self, file_path: FilePath, verbose: bool = False): + + def __init__(self, file_path: FilePath, video_sampling_frequency: float, verbose: bool = False): # This should load the data lazily and prepare variables you need - + self.file_path = file_path self.verbose = verbose - - + self.video_sampling_frequency = video_sampling_frequency 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 + def add_to_nwbfile(self, nwbfile: NWBFile, metadata: Optional[dict] = None): - sampling_frequency = 30.0 - import pandas as pd - freezing_behavior_df = pd.read_csv(self.file_path, usecols=['Frame', 'Motion', 'Freezing']) - - start_time = freezing_behavior_df['Frame'][freezing_behavior_df['Freezing'] == 1].index.values/sampling_frequency - - TimeIntervals(name="Freezing", description="Intervals of freezing behavior", - start_time=freezing_behavior_df['Frame'][freezing_behavior_df['Freezing'] == 1].index.values/sampling_frequency, - stop_time=freezing_behavior_df['Frame'][freezing_behavior_df['Freezing'] == 0].index.values/sampling_frequency) - + import pandas as pd + from ndx_events import Events + + freezing_behavior_df = pd.read_csv(self.file_path) + + # Extract parameters, those values are unique per run + file = freezing_behavior_df["File"].unique()[0] + motion_cutoff = freezing_behavior_df["MotionCutoff"].unique()[0] + freeze_threshold = freezing_behavior_df["FreezeThresh"].unique()[0] + min_freeze_duration = freezing_behavior_df["MinFreezeDuration"].unique()[0] + + # Extract timestamps of the freezing events + # From the discussion wih the author, the freezing events are the frames where the freezing behavior is 100 + freezing_frames = freezing_behavior_df[freezing_behavior_df["Freezing"] == 100]["Frame"].values + timestamps = freezing_frames / self.video_sampling_frequency + + events_description = ( + f"Freezing Events Calculated with EzTrack software for file {file} " + f"with motion cutoff {motion_cutoff}, freeze threshold {freeze_threshold} " + f"and min freeze duration {min_freeze_duration}" + ) + + events = Events( + name="FreezingEvents", + description=events_description, + timestamps=timestamps, + ) + if "behavior" not in nwbfile.processing: + behavior_module = nwbfile.create_processing_module( + name="behavior", description="Contains behavior data" + ) + else: + behavior_module = nwbfile.processing["behavior"] + + behavior_module.add(events) diff --git a/src/cai_lab_to_nwb/zaki_2024/zaki_2024_notes.md b/src/cai_lab_to_nwb/zaki_2024/zaki_2024_notes.md index 933ba46..ea623ec 100644 --- a/src/cai_lab_to_nwb/zaki_2024/zaki_2024_notes.md +++ b/src/cai_lab_to_nwb/zaki_2024/zaki_2024_notes.md @@ -733,9 +733,7 @@ But that does not match the structure of the files discussed above. Also, check out here: https://github.com/catalystneuro/roiextractors/issues/356 -The difference in folder organization is a version number: -- -- +**The difference in folder organization is a version number** V4 just enumerates them in the same folder. This is because this is more general to add arbitrary behavioral cameras (you don't need the behavior) @@ -807,7 +805,7 @@ https://minian.readthedocs.io/en/stable/ ## Freezing Behavior and Video -I think this data was extracted with the ezTrack package: +This data was extracted from the the ezTrack package: This is how the data looks like: ![freeze_data](./assets/freeze_data_frame.png) @@ -844,7 +842,22 @@ https://youtu.be/BKgh-XcZhIM?t=1905 ## Sleep +The data looks like this: +| Index | Frame | SleepState | +|-------|-------|------------| +| 0 | 0 | wake | +| 1 | 1 | wake | +| 2 | 2 | wake | +| 3 | 3 | wake | +| 4 | 4 | wake | +| 5 | 5 | wake | + + +The possible sleep states are: +* wake +* quite wake +* sws