From 967f81e6216800bdd38866b643f3d6f221de7bd9 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Mon, 2 Dec 2024 18:22:17 +0100 Subject: [PATCH 1/5] Add time_shift to schierek_embargo_2024_processedbehaviorinterface --- ...embargo_2024_processedbehaviorinterface.py | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/constantinople_lab_to_nwb/schierek_embargo_2024/interfaces/schierek_embargo_2024_processedbehaviorinterface.py b/src/constantinople_lab_to_nwb/schierek_embargo_2024/interfaces/schierek_embargo_2024_processedbehaviorinterface.py index 2513549..af1399c 100644 --- a/src/constantinople_lab_to_nwb/schierek_embargo_2024/interfaces/schierek_embargo_2024_processedbehaviorinterface.py +++ b/src/constantinople_lab_to_nwb/schierek_embargo_2024/interfaces/schierek_embargo_2024_processedbehaviorinterface.py @@ -111,6 +111,13 @@ def add_to_nwbfile( if column_name_mapping is not None: columns_to_add = [column for column in column_name_mapping.keys() if column in data.keys()] + assert ( + self._center_port_column_name in data + ), f"'{self._center_port_column_name}' column must be present in the data to align the trials." + center_port_onset_times = [center_port_times[0] for center_port_times in data[self._center_port_column_name]] + center_port_offset_times = [center_port_times[1] for center_port_times in data[self._center_port_column_name]] + + time_shift = 0.0 if nwbfile.trials is None: assert trial_start_times is not None, "'trial_start_times' must be provided if trials table is not added." assert trial_stop_times is not None, "'trial_stop_times' must be provided if trials table is not added." @@ -126,6 +133,8 @@ def add_to_nwbfile( trial_start_times = trial_start_times[:num_trials] trial_stop_times = trial_stop_times[:num_trials] + time_shift = trial_start_times[0] - center_port_onset_times[0] + assert ( len(trial_start_times) == num_trials ), f"Length of 'trial_start_times' ({len(trial_start_times)}) must match the number of trials ({num_trials})." @@ -140,19 +149,17 @@ def add_to_nwbfile( check_ragged=False, ) - # break it into onset and offset time columns - if self._center_port_column_name in columns_to_add: - columns_to_add.remove(self._center_port_column_name) - trials_table.add_column( - name="center_poke_onset_time", - description="The time of center port LED on for each trial.", - data=[center_poke_times[0] for center_poke_times in data[self._center_port_column_name]], - ) - trials_table.add_column( - name="center_poke_offset_time", - description="The time of center port LED off for each trial.", - data=[center_poke_times[1] for center_poke_times in data[self._center_port_column_name]], - ) + # break 'Cled' into onset and offset time columns + trials_table.add_column( + name="center_port_onset_time", + description="The time of center port LED on for each trial.", + data=center_port_onset_times + time_shift, + ) + trials_table.add_column( + name="center_port_offset_time", + description="The time of center port LED off for each trial.", + data=center_port_offset_times + time_shift, + ) for column_name in columns_to_add: name = column_name_mapping.get(column_name, column_name) if column_name_mapping is not None else column_name From 88a529a3947e1566e626f31acfc310317b65d9c3 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Mon, 2 Dec 2024 18:26:37 +0100 Subject: [PATCH 2/5] Add reward and opt out times as separate columns --- ...embargo_2024_processedbehaviorinterface.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/constantinople_lab_to_nwb/schierek_embargo_2024/interfaces/schierek_embargo_2024_processedbehaviorinterface.py b/src/constantinople_lab_to_nwb/schierek_embargo_2024/interfaces/schierek_embargo_2024_processedbehaviorinterface.py index af1399c..bfb9944 100644 --- a/src/constantinople_lab_to_nwb/schierek_embargo_2024/interfaces/schierek_embargo_2024_processedbehaviorinterface.py +++ b/src/constantinople_lab_to_nwb/schierek_embargo_2024/interfaces/schierek_embargo_2024_processedbehaviorinterface.py @@ -161,6 +161,96 @@ def add_to_nwbfile( data=center_port_offset_times + time_shift, ) + side_port_columns = ["Cled", "Lled", "Rled", "l_opt", "r_opt"] + missing_columns = [col for col in side_port_columns if col not in data] + if missing_columns: + raise ValueError(f"Missing required columns in data: {', '.join(missing_columns)}") + + # During the delay between the center light turning off and the reward arriving, the side light turns on. + # The side light turns off when the reward is available, then stays off until the animal collects the reward. + # When the animal nose pokes to collect the reward, the light flashes on/off. + reward_side_light_onset_times = [] + reward_side_light_offset_times = [] + reward_side_light_flash_onset_times = [] + reward_side_light_flash_offset_times = [] + + opt_out_side_light_onset_times = [] + opt_out_side_light_offset_times = [] + opt_out_reward_port_turns_off = [] + opt_out_reward_port_light_turns_off = [] + + for i in range(num_trials): + rewarded_side = data["RewardedSide"][i] + if rewarded_side == "Left": + side_port_column_name = "Lled" + # the opt-out port is the opposite of the rewarded side + opt_out_port_column_name = "r_opt" + elif rewarded_side == "Right": + side_port_column_name = "Rled" + opt_out_port_column_name = "l_opt" + else: + raise ValueError(f"Invalid rewarded side '{rewarded_side}'.") + + reward_side_light_onset_times.append(data[side_port_column_name][i][0]) + reward_side_light_offset_times.append(data[side_port_column_name][i][1]) + reward_side_light_flash_onset_times.append(data[side_port_column_name][i][2]) + reward_side_light_flash_offset_times.append(data[side_port_column_name][i][3]) + + opt_out_side_light_onset_times.append(data[opt_out_port_column_name][i][0]) + opt_out_side_light_offset_times.append(data[opt_out_port_column_name][i][1]) + opt_out_reward_port_turns_off.append(data[side_port_column_name][i][3]) + opt_out_reward_port_light_turns_off.append(data[opt_out_port_column_name][i][3]) + + trials_table.add_column( + name="rewarded_port_onset_time", + description="The time of reward port light on for each trial. During the delay between the center light turning off and the reward arriving, the side light turns on.", + data=reward_side_light_onset_times + time_shift, + ) + + trials_table.add_column( + name="rewarded_port_offset_time", + description="The time of reward port light off for each trial. The side light turns off when the reward is available, then stays off until the animal collects the reward.", + data=reward_side_light_offset_times + time_shift, + ) + + trials_table.add_column( + name="rewarded_port_flash_onset_time", + description="The time of reward port light flash on for each trial. When the animal nose pokes to collect the reward, the light flashes on/off.", + data=reward_side_light_flash_onset_times + time_shift, + ) + + trials_table.add_column( + name="rewarded_port_flash_offset_time", + description="The time of reward port light flash off for each trial. When the animal nose pokes to collect the reward, the light flashes on/off.", + data=reward_side_light_flash_offset_times + time_shift, + ) + + trials_table.add_column( + name="opt_out_port_onset_time", + description=f"The time of side light turns on when the animal opts out by poking into the port opposite to the rewarded side.", + data=opt_out_side_light_onset_times + time_shift, + ) + + trials_table.add_column( + name="opt_out_port_offset_time", + description=f"The time of side light turns off when the animal opts out by poking into the port opposite to the rewarded side.", + data=opt_out_side_light_offset_times + time_shift, + ) + + trials_table.add_column( + name=f"opt_out_reward_port_offset_time", + description="The time of rewarded port turns off when the animal opts out by poking into the port opposite to the rewarded side.", + data=opt_out_reward_port_turns_off + time_shift, + ) + + trials_table.add_column( + name=f"opt_out_reward_port_light_offset_time", + description="The time of rewarded port light turns off when the animal opts out by poking into the port opposite to the rewarded side.", + data=opt_out_reward_port_light_turns_off + time_shift, + ) + + # filter columns to add, these columns were added separately + columns_to_add = [column for column in columns_to_add if column not in side_port_columns] for column_name in columns_to_add: name = column_name_mapping.get(column_name, column_name) if column_name_mapping is not None else column_name description = ( From a50328b4c05fc05847f48714d66095c539bf0fec Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Tue, 3 Dec 2024 14:26:44 +0100 Subject: [PATCH 3/5] explicitly assign session_start_time in the converter metadata to the Bpod's start time from the RawBehavior data interface. --- .../schierek_embargo_2024_nwbconverter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/constantinople_lab_to_nwb/schierek_embargo_2024/schierek_embargo_2024_nwbconverter.py b/src/constantinople_lab_to_nwb/schierek_embargo_2024/schierek_embargo_2024_nwbconverter.py index 32a807a..221e60b 100644 --- a/src/constantinople_lab_to_nwb/schierek_embargo_2024/schierek_embargo_2024_nwbconverter.py +++ b/src/constantinople_lab_to_nwb/schierek_embargo_2024/schierek_embargo_2024_nwbconverter.py @@ -115,6 +115,10 @@ def __init__( def get_metadata(self): metadata = super().get_metadata() + # Explicity set session_start_time to Bpod start time + session_start_time = self.data_interface_objects["RawBehavior"].get_metadata()["NWBFile"]["session_start_time"] + metadata["NWBFile"].update(session_start_time=session_start_time) + if "Electrodes" not in metadata["Ecephys"]: metadata["Ecephys"]["Electrodes"] = [] From b4deb1a0be4d703e2aae7d198a2bcd7ec07b62d2 Mon Sep 17 00:00:00 2001 From: weiglszonja Date: Tue, 3 Dec 2024 14:29:37 +0100 Subject: [PATCH 4/5] update time alignment notes --- .../schierek_embargo_2024_notes.md | 70 ++++++++++++++++--- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/src/constantinople_lab_to_nwb/schierek_embargo_2024/schierek_embargo_2024_notes.md b/src/constantinople_lab_to_nwb/schierek_embargo_2024/schierek_embargo_2024_notes.md index bcfacf2..d658608 100644 --- a/src/constantinople_lab_to_nwb/schierek_embargo_2024/schierek_embargo_2024_notes.md +++ b/src/constantinople_lab_to_nwb/schierek_embargo_2024/schierek_embargo_2024_notes.md @@ -129,27 +129,77 @@ column_descriptions = dict( ) ``` -### Temporal alignment +### Session start time -Align TTL signals to Raw Bpod trial times: -Compute the time shift from raw Bpod trial start times to the aligned center port timestamps. -The aligned center port times can be accessed from the processed behavior data using the `"Cled"` field. +The session start time is the reference time for all timestamps in the NWB file. We are using `session_start_time` from the Bpod output. (The start time of the session in the Bpod data can be accessed from the "Info" struct, with "SessionDate" and "SessionStartTime_UTC" fields.) + +### Bpod trial start time + +We are extracting the trial start times from the Bpod output using the "TrialStartTimestamp" field. ```python -from ndx_structured_behavior.utils import loadmat +from pymatreader import read_mat -bpod_data = loadmat("path/to/bpod_session.mat")["SessionData"] # should contain "SessionData" named struct -S_struct_data = loadmat("path/to/processed_behavior.mat")["S"] # should contain "S" named struct +bpod_data = read_mat("raw_Bpod/J076/DataFiles/J076_RWTautowait2_20231212_145250.mat")["SessionData"] # should contain "SessionData" named struct # The trial start times from the Bpod data bpod_trial_start_times = bpod_data['TrialStartTimestamp'] +bpod_trial_start_times[:7] +>>> [19.988, 39.7154, 43.6313, 46.8732, 59.4011, 77.7451, 79.4653] +``` + +### NIDAQ trial start time + +The aligned trial start times can be accessed from the processed behavior data using the `"Cled"` field. + +```python +from pymatreader import read_mat + +S_struct_data = read_mat("J076_2023-12-12.mat")["S"] # should contain "S" named struct # "Cled" field contains the aligned onset and offset times for each trial [2 x ntrials] -center_port_aligned_onset_times = [center_port_times[0] for center_port_times in S_struct_data["Cled"]] -time_shift = bpod_trial_start_times[0] - center_port_aligned_onset_times[0] +center_port_onset_times = [center_port_times[0] for center_port_times in S_struct_data["Cled"]] +center_port_offset_times = [center_port_times[1] for center_port_times in S_struct_data["Cled"]] + +center_port_onset_times[:7] +>>> [48.57017236918037, 68.2978722016674, 72.2138230625031, 75.45578122765313, 87.98392024937102, 106.3281781420765, 108.04842315623304] +``` + +## Alignment + +We are aligning the starting time of the recording and sorting interfaces to the Bpod interface. + +We are computing the time shift from the Bpod trial start time to the NIDAQ trial start time. + +```python +time_shift = bpod_trial_start_times[0] - center_port_onset_times[0] +>>> -28.58217236918037 +``` + +We are applying this time_shift to the timestamps for the raw recording as: + +```python +from neuroconv.datainterfaces import OpenEphysRecordingInterface +recording_folder_path = "J076_2023-12-12_14-52-04/Record Node 117" +ap_stream_name = "Record Node 117#Neuropix-PXI-119.ProbeA-AP" +recording_interface = OpenEphysRecordingInterface(recording_folder_path, ap_stream_name) + +unaligned_timestamps = recording_interface.get_timestamps() +unaligned_timestamps[:7] +>>> [29.74, 29.74003333, 29.74006667, 29.7401, 29.74013333, 29.74016667, 29.7402] + +aligned_timestamps = unaligned_timestamps + time_shift +>>> [1.15782763, 1.15786096, 1.1578943 , 1.15792763, 1.15796096, 1.1579943 , 1.15802763] ``` -We are using this computed time shift to shift the ephys timestamps. +1) When the time shift is negative and the first aligned timestamp of the recording trace is negative: +- shift back bpod (from every column that has a timestamp they have to be shifted back) +- shift back session start time +- don't have to move recording nor the center_port_onset_times and center_port_offset_times +2) When the time shift is negative and the first aligned timestamp of the recording trace is positive +- we move the recording, center_port_onset_times and center_port_offset_times backward +3) When time shift is positive +- we move the recording, center_port_onset_times and center_port_offset_times forward ### Mapping to NWB From ed6e26d2bc38214074491049c6ef9407a75b424b Mon Sep 17 00:00:00 2001 From: Szonja Weigl Date: Thu, 5 Dec 2024 09:51:59 +0100 Subject: [PATCH 5/5] Update src/constantinople_lab_to_nwb/schierek_embargo_2024/schierek_embargo_2024_notes.md --- .../schierek_embargo_2024/schierek_embargo_2024_notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/constantinople_lab_to_nwb/schierek_embargo_2024/schierek_embargo_2024_notes.md b/src/constantinople_lab_to_nwb/schierek_embargo_2024/schierek_embargo_2024_notes.md index d658608..afcbc1c 100644 --- a/src/constantinople_lab_to_nwb/schierek_embargo_2024/schierek_embargo_2024_notes.md +++ b/src/constantinople_lab_to_nwb/schierek_embargo_2024/schierek_embargo_2024_notes.md @@ -171,6 +171,9 @@ We are aligning the starting time of the recording and sorting interfaces to the We are computing the time shift from the Bpod trial start time to the NIDAQ trial start time. +From a conceptual point of view, the trial start and the central port onset are equivalent: a trial starts when the first time the central port is on. + + ```python time_shift = bpod_trial_start_times[0] - center_port_onset_times[0] >>> -28.58217236918037