diff --git a/.gitignore b/.gitignore index 2e28cf8..b9ebf12 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,16 @@ /data/ +/summary_graphs/ +/each_session/ /project.avi /project.mp4 +# Conda environment +/betterbehavior/ + +# PyCharm +.idea/ +.iml +.xml + +# Python cache +__pycache__/ +*.pyc diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..f2186e2 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +user_info.py \ No newline at end of file diff --git a/.idea/desktop.ini b/.idea/desktop.ini deleted file mode 100644 index c8d646a..0000000 --- a/.idea/desktop.ini +++ /dev/null @@ -1,5 +0,0 @@ -[.ShellClassInfo] -InfoTip=This folder is shared online. -IconFile=C:\Program Files\Google\Drive\googledrivesync.exe -IconIndex=16 -IconResource=C:\Program Files\Google\Drive File Stream\90.0.3.0\GoogleDriveFS.exe,25 diff --git a/.idea/dictionaries/desktop.ini b/.idea/dictionaries/desktop.ini deleted file mode 100644 index c8d646a..0000000 --- a/.idea/dictionaries/desktop.ini +++ /dev/null @@ -1,5 +0,0 @@ -[.ShellClassInfo] -InfoTip=This folder is shared online. -IconFile=C:\Program Files\Google\Drive\googledrivesync.exe -IconIndex=16 -IconResource=C:\Program Files\Google\Drive File Stream\90.0.3.0\GoogleDriveFS.exe,25 diff --git a/.idea/discrete_choice.iml b/.idea/discrete_choice.iml index f941a9e..3ec477e 100644 --- a/.idea/discrete_choice.iml +++ b/.idea/discrete_choice.iml @@ -2,7 +2,7 @@ - + diff --git a/.idea/inspectionProfiles/desktop.ini b/.idea/inspectionProfiles/desktop.ini deleted file mode 100644 index c8d646a..0000000 --- a/.idea/inspectionProfiles/desktop.ini +++ /dev/null @@ -1,5 +0,0 @@ -[.ShellClassInfo] -InfoTip=This folder is shared online. -IconFile=C:\Program Files\Google\Drive\googledrivesync.exe -IconIndex=16 -IconResource=C:\Program Files\Google\Drive File Stream\90.0.3.0\GoogleDriveFS.exe,25 diff --git a/.idea/misc.xml b/.idea/misc.xml index bc867d9..3b054df 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,10 @@ + + - + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml index a708ec7..0fd93c1 100644 --- a/.idea/other.xml +++ b/.idea/other.xml @@ -2,5 +2,7 @@ \ No newline at end of file diff --git a/backend/add_2ndry_properties_to_pi_events.py b/backend/add_2ndry_properties_to_pi_events.py new file mode 100644 index 0000000..5ae1ac7 --- /dev/null +++ b/backend/add_2ndry_properties_to_pi_events.py @@ -0,0 +1,180 @@ +import numpy as np +import pandas as pd +from .get_interlocked_arrays import get_interlocked_arrays + + +def identify_event_order(pi_events, col_name_to_add, condition): + pi_events[col_name_to_add] = np.NaN + for tr in range(1, pi_events.total_trial + 1): + all_condition_bg = (pi_events.trial == tr) & condition & (pi_events.port == pi_events.loc[pi_events['key'] == 'background', 'port'].iloc[-1]) + all_condition_exp = (pi_events.trial == tr) & condition & (pi_events.port == pi_events.loc[pi_events['key'] == 'exp_decreasing', 'port'].iloc[-1]) + pi_events[col_name_to_add].loc[all_condition_bg] = range(1, len(pi_events.index[all_condition_bg]) + 1) + pi_events[col_name_to_add].loc[all_condition_exp] = range(1, len(pi_events.index[all_condition_exp]) + 1) + + +def get_valid_entry_exit(pi_events): + def update_trial_validity(valid_trials): + nonlocal exp_entries, exp_exits, bg_entries, bg_exits + exp_entries = exp_entries[exp_entries.trial.isin(valid_trials)] + exp_exits = exp_exits[exp_exits.trial.isin(valid_trials)] + bg_entries = bg_entries[bg_entries.trial.isin(valid_trials)] + bg_exits = bg_exits[bg_exits.trial.isin(valid_trials)] + + pi_events['is_valid_trial'] = True + reward_trials = pi_events[(pi_events.key == 'reward_initiate')].trial.to_numpy() + non_reward = ~pi_events.trial.isin(reward_trials) + bg_end_times = pi_events[(pi_events.key == 'LED') & (pi_events.port == pi_events.loc[pi_events['key'] == 'background', 'port'].iloc[-1]) & (pi_events.value == 1)] + + exp_entries = pi_events[(pi_events.key == 'head') & (pi_events.value == 1) & (pi_events.port == pi_events.loc[pi_events['key'] == 'exp_decreasing', 'port'].iloc[-1])] + exp_exits = pi_events[(pi_events.key == 'head') & (pi_events.value == 0) & (pi_events.port == pi_events.loc[pi_events['key'] == 'exp_decreasing', 'port'].iloc[-1])] + bg_entries = pi_events[(pi_events.key == 'trial') & (pi_events.value == 1)] + bg_exits = pi_events[(pi_events.key == 'head') & (pi_events.value == 0) & (pi_events.port == pi_events.loc[pi_events['key'] == 'background', 'port'].iloc[-1])] + + vid_bg_entries, vid_exp_exits = get_interlocked_arrays(bg_entries.index.to_numpy(), exp_exits.index.to_numpy(), + direction='widest') + + # find the latest bg_exits in between each pair of bg_entry and exp_exit + bgx_bgn = np.subtract.outer(bg_exits.index.to_numpy(), vid_bg_entries) + expx_bgx = np.subtract.outer(vid_exp_exits, bg_exits.index.to_numpy()) + vid_bg_exits = np.empty(np.shape(vid_bg_entries)) + for i in range(len(vid_bg_entries)): + interval_l = bgx_bgn[:, i] + interval_r = expx_bgx[i, :] + bgx_element_avail = np.where(np.multiply(interval_l > 0, interval_r > 0) > 0)[0] + if bgx_element_avail.size == 0: + vid_bg_exit_to_take = np.nan + else: + bgx_element_to_take = bgx_element_avail.max() + vid_bg_exit_to_take = bg_exits.index.to_numpy()[bgx_element_to_take] + vid_bg_exits[i] = vid_bg_exit_to_take + vid_bg_entries = np.delete(vid_bg_entries, np.isnan(vid_bg_exits)) + vid_exp_exits = np.delete(vid_exp_exits, np.isnan(vid_bg_exits)) + vid_bg_exits = np.delete(vid_bg_exits, np.isnan(vid_bg_exits)) + + # find the exp_entries in between each pair of bg_exit and exp_exit + expn_bgx = np.subtract.outer(exp_entries.index.to_numpy(), vid_bg_exits) + expx_expn = np.subtract.outer(vid_exp_exits, exp_entries.index.to_numpy()) + vid_exp_entries = np.empty(np.shape(vid_bg_exits)) + for i in range(len(vid_bg_exits)): + interval_l = expn_bgx[:, i] + interval_r = expx_expn[i, :] + expn_element_avail = np.where(np.multiply(interval_l > 0, interval_r > 0))[0] + if expn_element_avail.size == 0: + vid_exp_entry_to_take = np.nan + else: + expn_element_to_take = expn_element_avail.min() + vid_exp_entry_to_take = exp_entries.index.to_numpy()[expn_element_to_take] + vid_exp_entries[i] = vid_exp_entry_to_take + vid_bg_entries = np.delete(vid_bg_entries, np.isnan(vid_exp_entries)) + vid_bg_exits = np.delete(vid_bg_exits, np.isnan(vid_exp_entries)) + vid_exp_exits = np.delete(vid_exp_exits, np.isnan(vid_exp_entries)) + vid_exp_entries = np.delete(vid_exp_entries, np.isnan(vid_exp_entries)) + + pi_events['is_valid'] = False + pi_events.loc[vid_exp_entries, 'is_valid'] = True + pi_events.loc[vid_exp_exits, 'is_valid'] = True + pi_events.loc[vid_bg_entries, 'is_valid'] = True + pi_events.loc[vid_bg_exits, 'is_valid'] = True + # region only get the trials where there are both an exp_entry and an exp_exit + complete_trials = set(exp_entries.trial) & set(exp_exits.trial) & set(bg_entries.trial) & set(bg_exits.trial) + update_trial_validity(complete_trials) + # endregion + # region valid trial condition 1: toss trials with multiple re-entry into the exponential port + # # but skip this step when it's a single-reward task + # if (pi_events['task'].iloc[0] != 'single_reward'): + # is_minimal_reentry = exp_entries.groupby('trial').entry_order_in_trial.max() <= 2 + # single_entry_trials = is_minimal_reentry[is_minimal_reentry].index + # update_trial_validity(single_entry_trials) + # pi_events.is_valid_trial[~pi_events['trial'].isin(single_entry_trials)] = False + # endregion + # region valid trial condition 2: toss trials with background stay too long + bg_stay = bg_exits.groupby('trial').trial_time.max() + trial_phase = bg_exits.groupby('trial').phase.max() + good_for_long_block = (trial_phase == '0.4') & (bg_stay < 25) + good_for_short_block = (trial_phase == '0.8') & (bg_stay < 12.5) + good_bg_stay = good_for_long_block | good_for_short_block + good_bg_trials = good_bg_stay[good_bg_stay].index + update_trial_validity(good_bg_trials) + pi_events.loc[~pi_events['trial'].isin(good_bg_trials), 'is_valid_trial'] = False + for trial in trial_phase.index.to_list(): + pi_events.loc[pi_events.trial == trial, 'phase'] = trial_phase.loc[trial] + # endregion + valid_trials = np.unique(pi_events.trial[pi_events.is_valid_trial]) + bg_entries_idx = [bg_entries.groupby('trial').groups[i].max() for i in valid_trials] + bg_exits_idx = [bg_exits.groupby('trial').groups[i].max() for i in valid_trials] + exp_entries_idx = [exp_entries.groupby('trial').groups[i].max() for i in valid_trials] + exp_exits_idx = [exp_exits.groupby('trial').groups[i].max() for i in valid_trials] + bg_entries = bg_entries.session_time.to_numpy() + bg_exits = bg_exits.groupby('trial').session_time.max().to_numpy() + exp_entries = exp_entries.session_time.to_numpy() + exp_exits = exp_exits.session_time.to_numpy() + + return pi_events, valid_trials, exp_entries, exp_exits, bg_entries, bg_exits + + +def add_2ndry_properties_to_pi_events(pi_events): + pd.options.mode.chained_assignment = None # default='warn' + pi_events.total_trial = int(pi_events.trial.max()) + # region Identify the order of rewards, entries, and exits in each trial + identify_event_order(pi_events, 'reward_order_in_trial', + condition=(pi_events.key == 'reward') & (pi_events.value == 1)) + identify_event_order(pi_events, 'entry_order_in_trial', + condition=(pi_events.key == 'head') & (pi_events.value == 1)) + identify_event_order(pi_events, 'exit_order_in_trial', + condition=(pi_events.key == 'head') & (pi_events.value == 0)) + # endregion + + # region Identify the animal's first encounters of each reward + pi_events['is_1st_lick'] = 0 + pi_events['is_1st_encounter'] = 0 + reward_idx = pi_events.index[(pi_events.key == 'reward') & (pi_events.value == 1)] + for i in range(len(reward_idx)): + idx = reward_idx[i] + if i + 1 >= len(reward_idx): + next_idx = max(pi_events.index) + else: + next_idx = reward_idx[i + 1] + isafter = pi_events.index > idx + islick = pi_events.key == 'lick' + if pi_events[isafter & islick & (pi_events.value == 1)].empty: + print('') + else: + first_lick_idx = pi_events[isafter & islick & (pi_events.value == 1)].index[0] + pi_events.is_1st_lick[first_lick_idx] = 1 + + if pi_events[isafter & islick].empty: + print('') + elif (pi_events.value[isafter & islick].iloc[0] == 0) & (pi_events.index[isafter & islick].min() < next_idx): + pi_events.is_1st_encounter[idx] = 1 + elif pi_events.value[isafter & islick].iloc[0] == 1: + idx_to_change = pi_events[isafter & islick].index[0] + pi_events.is_1st_encounter[idx_to_change] = 1 + pi_events['is_1st_lick'] = pi_events['is_1st_lick'].astype(bool) + pi_events['is_1st_encounter'] = pi_events['is_1st_encounter'].astype(bool) + # endregion + pi_events, valid_trials, exp_entries, exp_exits, bg_entries, bg_exits = get_valid_entry_exit(pi_events) + + pi_events.reset_index(drop=True, inplace=True) + + # region Identify interval from last entry (time they are in the port) + pi_events['time_in_port'] = np.nan + condition_entering_bg = (pi_events['key'] == 'trial') & (pi_events['value'] == 1) + condition_exiting_bg = (pi_events['key'] == 'head') & (pi_events['value'] == 0) & (pi_events.port == pi_events.loc[pi_events['key'] == 'background', 'port'].iloc[-1]) + condition_entering_exp = (pi_events['key'] == 'head') & (pi_events['value'] == 1) & (pi_events.port == pi_events.loc[pi_events['key'] == 'exp_decreasing', 'port'].iloc[-1]) + condition_exiting_exp = (pi_events['key'] == 'head') & (pi_events['value'] == 0) & (pi_events.port == pi_events.loc[pi_events['key'] == 'exp_decreasing', 'port'].iloc[-1]) + for i in range(1, pi_events.total_trial+1): + is_in_trial = pi_events.trial == i + time_entering_bg = pi_events.loc[is_in_trial & pi_events.is_valid & condition_entering_bg, 'session_time'] + time_exiting_bg = pi_events.loc[is_in_trial & pi_events.is_valid & condition_exiting_bg, 'session_time'] + if (time_entering_bg.size > 0) & (time_exiting_bg.size > 0): + idx_list_bg = range(time_entering_bg.index[0], time_exiting_bg.index[0]+1) + time_in_bg = [pi_events.loc[idx, 'session_time'] - time_entering_bg.values[0] for idx in idx_list_bg] + pi_events.loc[idx_list_bg, 'time_in_port'] = time_in_bg + time_entering_exp = pi_events.loc[is_in_trial & pi_events.is_valid & condition_entering_exp, 'session_time'] + time_exiting_exp = pi_events.loc[is_in_trial & pi_events.is_valid & condition_exiting_exp, 'session_time'] + if (time_entering_exp.size > 0) & (time_exiting_exp.size > 0): + idx_list_exp = range(time_entering_exp.index[0], time_exiting_exp.index[0]+1) + time_in_exp = [pi_events.loc[idx, 'session_time'] - time_entering_exp.values[0] for idx in idx_list_exp] + pi_events.loc[idx_list_exp, 'time_in_port'] = time_in_exp + # endregion + return pi_events diff --git a/backend/get_interlocked_arrays.py b/backend/get_interlocked_arrays.py new file mode 100644 index 0000000..948c707 --- /dev/null +++ b/backend/get_interlocked_arrays.py @@ -0,0 +1,40 @@ +import numpy as np + +def get_interlocked_arrays(array_left, array_right, direction = 'widest'): + outer = np.subtract.outer(array_right, array_left) + arr_l_set = set() + arr_r_set = set() + if direction == 'widest': + # For each 'left' element, find the highest-indexed 'right' element that is smaller + arr_r_idx = [ + np.nan if (np.where(outer[:, col] < 0)[0].size == 0) else np.where(outer[:, col] < 0)[0].max() + for col in list(range(array_left.size))] + # For each 'right' element, find the lowest-indexed 'left' element it is smaller than + arr_l_idx = [ + np.nan if (np.where(outer[row, :] < 0)[0].size == 0) else np.where(outer[row, :] < 0)[0].min() + for row in list(range(array_right.size))] + arr_l_set.add(0) + arr_r_set.add(array_right.size - 1) + elif direction == 'narrowest': + # For each 'left' element, find the lowest-indexed 'right' element that is larger + arr_r_idx = [ + np.nan if (np.where(outer[:, col] > 0)[0].size == 0) else np.where(outer[:, col] > 0)[0].min() + for col in list(range(array_left.size))] + # For each 'right' element, find the highest-indexed 'left' element it is larger than + arr_l_idx = [ + np.nan if (np.where(outer[row, :] > 0)[0].size == 0) else np.where(outer[row, :] > 0)[0].max() + for row in list(range(array_right.size))] + else: + print(f'Direction can only be widest or narrowest.') + arr_l_set.update(x for x in arr_l_idx if x == x) + arr_r_set.update(x for x in arr_r_idx if x == x) + if len(arr_l_set) - len(arr_r_set) == 1: + arr_l_set.discard(max(arr_l_set)) + arr_l_idx = sorted(i for i in arr_l_set) + arr_r_idx = sorted(i for i in arr_r_set) + array_left = array_left[arr_l_idx] + array_right = array_right[arr_r_idx] + if array_left.size != array_right.size: + print('The sizes of arrays do not match! Debug the function get_interlocked_arrays().') + else: + return array_left, array_right \ No newline at end of file diff --git a/behavior_code/.gitignore b/behavior_code/.gitignore new file mode 100644 index 0000000..2e28cf8 --- /dev/null +++ b/behavior_code/.gitignore @@ -0,0 +1,3 @@ +/data/ +/project.avi +/project.mp4 diff --git a/behavior_code/.idea/.gitignore b/behavior_code/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/behavior_code/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/behavior_code/.idea/.name b/behavior_code/.idea/.name new file mode 100644 index 0000000..f2186e2 --- /dev/null +++ b/behavior_code/.idea/.name @@ -0,0 +1 @@ +user_info.py \ No newline at end of file diff --git a/behavior_code/.idea/deployment.xml b/behavior_code/.idea/deployment.xml new file mode 100644 index 0000000..894f2d0 --- /dev/null +++ b/behavior_code/.idea/deployment.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/behavior_code/.idea/dictionaries/Elissa.xml b/behavior_code/.idea/dictionaries/Elissa.xml new file mode 100644 index 0000000..bcbf3ab --- /dev/null +++ b/behavior_code/.idea/dictionaries/Elissa.xml @@ -0,0 +1,7 @@ + + + + gpio + + + \ No newline at end of file diff --git a/behavior_code/.idea/discrete_choice.iml b/behavior_code/.idea/discrete_choice.iml new file mode 100644 index 0000000..3ec477e --- /dev/null +++ b/behavior_code/.idea/discrete_choice.iml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/behavior_code/.idea/inspectionProfiles/profiles_settings.xml b/behavior_code/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/behavior_code/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/behavior_code/.idea/misc.xml b/behavior_code/.idea/misc.xml new file mode 100644 index 0000000..3b054df --- /dev/null +++ b/behavior_code/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/behavior_code/.idea/modules.xml b/behavior_code/.idea/modules.xml new file mode 100644 index 0000000..f08c6b1 --- /dev/null +++ b/behavior_code/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/behavior_code/.idea/other.xml b/behavior_code/.idea/other.xml new file mode 100644 index 0000000..0fd93c1 --- /dev/null +++ b/behavior_code/.idea/other.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/behavior_code/.idea/remote-mappings.xml b/behavior_code/.idea/remote-mappings.xml new file mode 100644 index 0000000..d7ead44 --- /dev/null +++ b/behavior_code/.idea/remote-mappings.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/behavior_code/.idea/sshConfigs.xml b/behavior_code/.idea/sshConfigs.xml new file mode 100644 index 0000000..efeb954 --- /dev/null +++ b/behavior_code/.idea/sshConfigs.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/behavior_code/.idea/vcs.xml b/behavior_code/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/behavior_code/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/behavior_code/LICENSE b/behavior_code/LICENSE new file mode 100644 index 0000000..096e8f4 --- /dev/null +++ b/behavior_code/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 esutlie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/behavior_code/README.md b/behavior_code/README.md new file mode 100644 index 0000000..7aadc2b --- /dev/null +++ b/behavior_code/README.md @@ -0,0 +1,40 @@ +

Behavior Task Control

+

+ Manage sensors and task logic for behavioral experiments. +

+ + + + +## About The Project + +This code repo developed by Elissa Sutlief is used by the Shuler lab to run behavioral tasks. Code is meant to be run on a raspberry pi wired with custom sensors embedded in a behavior box. Edit the logic within the task class to make a custom task. + + + +## Installation + +1. Clone the repo + ```sh + git clone https://github.com/esutlie/behavior_code.git + ``` +2. Set up remote interpreter on Raspberry Pi +3. Edit task logic to suit your own experiment + + + +## License + +Distributed under the MIT License. See `LICENSE.txt` for more information. + + + + +## Contact + +Elissa Sutlief - elissasutlief@gmail.com + +Project Link: [https://github.com/esutlie/behavior_code](https://github.com/esutlie/behavior_code) + +

(back to top)

+ diff --git a/behavior_code/cam_test.py b/behavior_code/cam_test.py new file mode 100644 index 0000000..c95f492 --- /dev/null +++ b/behavior_code/cam_test.py @@ -0,0 +1,170 @@ +import pypylon.pylon as py +import matplotlib.pyplot as plt +import numpy as np +import time +import os + + +# definition of event handler class +class TriggeredImage(py.ImageEventHandler): + def __init__(self): + super().__init__() + self.grab_times = [] + + def OnImageGrabbed(self, camera, grabResult): + self.grab_times.append(grabResult.TimeStamp) + + +def main(): + img = py.PylonImage() + + camera = py.InstantCamera(py.TlFactory.GetInstance().CreateFirstDevice()) + print("Using device ", camera.GetDeviceInfo().GetModelName()) + + camera.Open() + camera.UserSetSelector = "Default" + camera.UserSetLoad.Execute() + + camera.LineSelector = "Line4" + camera.LineMode = "Input" + camera.TriggerSelector = "FrameStart" + camera.TriggerSource = "Line4" + camera.TriggerMode = "On" + + camera.ExposureTime.SetValue(50000) + camera.StartGrabbing(py.GrabStrategy_OneByOne) + last_frame_time = 0 + root = os.path.join('C:\\', 'video_data') + path = os.path.join(root, 'test') + if not os.path.isdir(path): + os.mkdir(path) + frame_number = 0 + need_new = True + + try: + while True: + if camera.GetGrabResultWaitObject().Wait(0): + grab = camera.RetrieveResult(0, py.TimeoutHandling_Return) + img.AttachGrabResultBuffer(grab) + print(f'test frame = {frame_number}') + if need_new: + path = os.path.join(root, time.strftime("%Y-%m-%d_%H-%M-%S")) + os.mkdir(path) + frame_number = 0 + need_new = False + ipo = py.ImagePersistenceOptions() + ipo.SetQuality(50) + timestamp = str(grab.TimeStamp) + zeros = ['0'] * (20 - len(timestamp)) + timestamp = ''.join(zeros) + timestamp + img.Save(py.ImageFileFormat_Jpeg, os.path.join(path, timestamp + '.jpeg'), ipo) + + last_frame_time = time.time() + frame_number += 1 + if time.time() - 5 > last_frame_time and not need_new: + need_new = True + + except: + camera.StopGrabbing() + camera.close() + + +def check_input(): + # open the camera + tlf = py.TlFactory.GetInstance() + cam = py.InstantCamera(tlf.CreateFirstDevice()) + print("Using device ", cam.GetDeviceInfo().GetModelName()) + + cam.Open() + # enable the chunk that + # samples all IO lines on every FrameStart + cam.ChunkModeActive = True + cam.ChunkSelector = "LineStatusAll" + cam.ChunkEnable = True + + # set max speed + cam.Height = cam.Height.Min + cam.Width = cam.Width.Min + cam.ExposureTime = cam.ExposureTime.Min + + # limit to 1khz + cam.AcquisitionFrameRateEnable = True + cam.AcquisitionFrameRate = 1000 + + print(cam.ResultingFrameRate.Value) + cam.StartGrabbingMax(1000) + + io_res = [] + while cam.IsGrabbing(): + with cam.RetrieveResult(1000) as res: + time_stamp = res.TimeStamp + io_res.append((time_stamp, res.ChunkLineStatusAll.Value)) + + cam.StopGrabbing() + + # simple logic analyzer :-) + + # convert to numpy array + io_array = np.array(io_res) + # extract first column timestamps + x_vals = io_array[:, 0] + # start with first timestamp as '0' + x_vals -= x_vals[0] + + # extract second column io values + y_vals = io_array[:, 1] + # for each bit plot the graph + for bit in range(8): + logic_level = ((y_vals & (1 << bit)) != 0) * 0.8 + bit + # plot in seconds + plt.plot(x_vals / 1e9, logic_level, label=bit) + + plt.xlabel("time [s]") + plt.ylabel("IO_LINE [#]") + plt.legend() + plt.show() + + # This next bit should grab on of the images + # get clean powerup state + cam.UserSetSelector = "Default" + cam.UserSetLoad.Execute() + + cam.LineSelector = "Line4" + cam.LineMode = "Input" + cam.TriggerSelector = "FrameStart" + cam.TriggerSource = "Line4" + cam.TriggerMode = "On" + print(cam.TriggerActivation.Value) + res = cam.GrabOne(py.waitForever) + + # https://github.com/basler/pypylon-samples/blob/c3e323c07b0e0efaf59a85685d35ff36056d2ef9/notebooks/USB_hardware_trigger_and_chunks.ipynb + # create event handler instance + image_timestamps = TriggeredImage() + + # register handler + # remove all other handlers + cam.RegisterImageEventHandler(image_timestamps, + py.RegistrationMode_ReplaceAll, + py.Cleanup_None) + + # start grabbing with background loop + cam.StartGrabbingMax(100, py.GrabStrategy_LatestImages, py.GrabLoop_ProvidedByInstantCamera) + # wait ... or do something relevant + while cam.IsGrabbing(): + time.sleep(0.1) + # stop grabbing + cam.StopGrabbing() + np.diff(image_timestamps.grab_times) + frame_delta_s = np.diff(np.array(image_timestamps.grab_times)) / 1.e9 + plt.plot(frame_delta_s, ".") + plt.axhline(np.mean(frame_delta_s)) + plt.show() + + plt.hist(frame_delta_s - np.mean(frame_delta_s), bins=100) + plt.xticks(rotation=45) + plt.show() + cam.Close() + + +if __name__ == '__main__': + check_input() diff --git a/behavior_code/camera.py b/behavior_code/camera.py new file mode 100644 index 0000000..8adaa78 --- /dev/null +++ b/behavior_code/camera.py @@ -0,0 +1,70 @@ +import pypylon.pylon as py +import time +import os +from pypylon._genicam import RuntimeException + + +def main(): + img = py.PylonImage() + while True: + tlf = py.TlFactory.GetInstance() + devices = tlf.EnumerateDevices() + if devices: + camera = py.InstantCamera(py.TlFactory.GetInstance().CreateFirstDevice()) + print("Using device ", camera.GetDeviceInfo().GetModelName()) + + camera.Open() + camera.UserSetSelector = "Default" + camera.UserSetLoad.Execute() + + camera.LineSelector = "Line4" + camera.LineMode = "Input" + camera.TriggerSelector = "FrameStart" + camera.TriggerSource = "Line4" + camera.TriggerMode = "On" + + camera.ExposureTime.SetValue(50000) + camera.StartGrabbing(py.GrabStrategy_OneByOne) + last_frame_time = 0 + root = os.path.join('C:\\', 'video_data') + path = os.path.join(root, 'test') + if not os.path.isdir(path): + os.mkdir(path) + frame_number = 0 + need_new = True + + try: + while True: + if camera.GetGrabResultWaitObject().Wait(0): + camera.ExposureTime.SetValue(50000) # We dont actually need to set this again, its just so if + # the camera is unplugged it will raise a RuntimeExcception. Then the code will chill until + # the camera is plugged back in + grab = camera.RetrieveResult(0, py.TimeoutHandling_Return) + img.AttachGrabResultBuffer(grab) + print(f'test frame = {frame_number}') + if need_new: + path = os.path.join(root, time.strftime("%Y-%m-%d_%H-%M-%S")) + os.mkdir(path) + frame_number = 0 + need_new = False + ipo = py.ImagePersistenceOptions() + ipo.SetQuality(50) + timestamp = str(grab.TimeStamp) + zeros = ['0'] * (20 - len(timestamp)) + timestamp = ''.join(zeros) + timestamp + img.Save(py.ImageFileFormat_Jpeg, os.path.join(path, timestamp + '.jpeg'), ipo) + + last_frame_time = time.time() + frame_number += 1 + if time.time() - 5 > last_frame_time and not need_new: + need_new = True + except RuntimeException: + print('camera disconnected (probably)') + except KeyboardInterrupt: + camera.StopGrabbing() + camera.close() + else: + time.sleep(1) + +if __name__ == '__main__': + main() diff --git a/behavior_code/data_conversion/__init__.py b/behavior_code/data_conversion/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/behavior_code/data_conversion/conversion.py b/behavior_code/data_conversion/conversion.py new file mode 100644 index 0000000..4fbb3eb --- /dev/null +++ b/behavior_code/data_conversion/conversion.py @@ -0,0 +1,136 @@ +""" +This script is designed to convert my (Elissa Sutlief's) behavior files into the new standard format the shuler lab +will use to make their behavior files easy to convert to the nwb format. Others should modify this script to convert +their own data files into the standard lab format, then use the nwb conversion script designed by catalyst neuro to +make their files compatible with dandy. + +The standard format consists of a csv file with the first line containing column headers and every other line containing +event data. This is paired with meta data json file. + +The folder structure of the original data : +data\ + ES031\ + data_YYYY-MM-DD_HH-mm-ss.txt + data_YYYY-MM-DD_HH-mm-ss.txt + data_YYYY-MM-DD_HH-mm-ss.txt + ES032\ + data_YYYY-MM-DD_HH-mm-ss.txt + data_YYYY-MM-DD_HH-mm-ss.txt + data_YYYY-MM-DD_HH-mm-ss.txt + +The folder structure of the converted data: +data_conversion\ + conversion.py --> This script. Edit and run to convert data to standard format + paths.py --> A script with the data paths that should be inside the same folder as this one +data_standard\ + mouse_data.json --> meta data for each mouse, to be added to each data file + task_data.json --> meta data for each task, to be added to each data file + ES031\ + ES031_YYYY-MM-DD_HH-mm-ss\ + data.csv + meta_data.json + ES031_YYYY-MM-DD_HH-mm-ss\ + data.csv + meta_data.json + ES031_YYYY-MM-DD_HH-mm-ss\ + data.csv + meta_data.json + ES032\ + ES032_YYYY-MM-DD_HH-mm-ss\ + data.csv + meta_data.json + ES032_YYYY-MM-DD_HH-mm-ss\ + data.csv + meta_data.json + ES032_YYYY-MM-DD_HH-mm-ss\ + data.csv + meta_data.json + +""" + +from paths import get_paths +import os +from os import walk +import pandas as pd +import numpy as np +from csv import reader +import json + + +def save_json(path, var): + json_object = json.dumps(var, indent=4) + with open(path, "w") as outfile: + outfile.write(json_object) + + +def load_json(path): + with open(path, 'r') as openfile: + var = json.load(openfile) + return var + + +def convert_data(regen=False): + paths = get_paths() + mouse_data = load_json(os.path.join(paths['converted_data'], 'mouse_data.json')) + task_data = load_json(os.path.join(paths['converted_data'], 'task_data.json')) + for root, dirs, filenames in walk(paths['original_data']): + if len(dirs) == 0 and os.path.basename(root)[:2] == 'ES': + mouse = os.path.basename(root) + for f in filenames: + if f == 'desktop.ini': + continue + dest = os.path.join(paths['converted_data'], mouse, f'{mouse}_{f[5:24]}') + if not regen and os.path.exists(dest): + continue + path = os.path.join(root, f) + data = pd.read_csv(path, na_values=['None'], skiprows=3) + + with open(path, 'r') as file: + r = reader(file) + info_keys = next(r) + info_values = next(r) + starts = np.where([True if s[0] == '{' else False for s in info_values])[0][::-1] + ends = np.where([True if s[-1] == '}' else False for s in info_values])[0][::-1] + for i in range(len(starts)): + info_values = info_values[:starts[i]] + [ + ",".join(info_values[starts[i]:ends[i] + 1])] + info_values[ + ends[i] + 1:] + info = dict(zip(info_keys, info_values)) + if 'port2duration' in info.keys(): + info['port2_duration'] = info['port2duration'] + + mouse_info = mouse_data[mouse] + task_info = task_data[info['task']] + session_meta_data = { + 'session': { + 'date': info['date'], + 'time': info['time'], + 'experimenter': 'Elissa Sutlief', + 'box': 0 if 'box' not in info.keys() else info['box'] + }, + 'mouse': mouse_info, + 'task': task_info + } + for p in [1, 2]: + if f'port{p}_info' in info.keys(): + port_dict = eval(info[f'port{p}_info'].replace('<', '\'').replace('>', '\'')) + if port_dict['distribution'][:8] == 'function': + port_dict['distribution'] = 'exp_decreasing' + session_meta_data['task'][f'port{p}'] = port_dict + else: + session_meta_data['task'][f'port{p}'] = { + 'distribution': info[f'port{p}_distribution'], + 'cumulative': info[f'port{p}_cumulative'], + 'peak': info[f'port{p}_peak'], + 'duration': info[f'port{p}_duration'], + 'port_num': p, + } + + if not os.path.exists(dest): + os.makedirs(dest) + save_json(os.path.join(dest, 'meta_data.json'), session_meta_data) + data.to_csv(os.path.join(dest, 'data.csv')) + + +if __name__ == '__main__': + convert_data() diff --git a/behavior_code/data_conversion/paths.py b/behavior_code/data_conversion/paths.py new file mode 100644 index 0000000..e8334cd --- /dev/null +++ b/behavior_code/data_conversion/paths.py @@ -0,0 +1,8 @@ +import os + + +def get_paths(): + return { + 'original_data': os.path.join(os.path.dirname(os.getcwd()), 'data'), + 'converted_data': os.path.join(os.path.dirname(os.getcwd()), 'data_standard') + } diff --git a/behavior_code/data_conversion/save_meta_data.py b/behavior_code/data_conversion/save_meta_data.py new file mode 100644 index 0000000..8b7f712 --- /dev/null +++ b/behavior_code/data_conversion/save_meta_data.py @@ -0,0 +1,49 @@ +import json +import os.path +import csv +from paths import get_paths + +def save_json(path, var): + json_object = json.dumps(var, indent=4) + with open(path, "w") as outfile: + outfile.write(json_object) + + +def load_json(path): + with open(path, 'r') as openfile: + var = json.load(openfile) + return var + + +def save_paths(): + paths = {} + + +def edit_mouse_data(): + path = os.path.join(os.getcwd(), '../data_standard/mouse_data.json') + mouse_data = load_json(path) + for key in mouse_data.keys(): + mouse_data[key]['species'] = 'mouse' + save_json(path, mouse_data) + + +def save_task_data(): + task_data = { + 'multi_reward': { + 'name': 'multi_reward', + 'description': 'a give up task with multiple rewards given in the exponential port.', + 'keys': { + 'head': 'IR sensor detecting when the mouse pokes its head into the port', + 'lick': 'IR sensor detecting when the mouse licks the lick spout', + }, + }, + } + path = os.path.join(os.getcwd(), '../data_standard/task_data.json') + save_json(path, task_data) + + +if __name__ == '__main__': + # save_paths() + # save_mouse_data() + # save_task_data() + edit_mouse_data() diff --git a/behavior_code/gui_functions.py b/behavior_code/gui_functions.py new file mode 100644 index 0000000..d42bc07 --- /dev/null +++ b/behavior_code/gui_functions.py @@ -0,0 +1,61 @@ +from support_classes import * +from time import sleep +from timescapes import * +import RPi.GPIO as GPIO +import random +import pickle + + +def run_behavior(mouse): + print(f"running behavior for {mouse}") + + +def calibrate(port): + print(f'calibrating port {port}') + with open('stand_alone/durations.pkl', 'rb') as f: + durations = pickle.load(f) + GPIO.setmode(GPIO.BCM) + port_object = Port(port, dist_info='filler', duration=durations[port]) + for _ in range(100): + port_object.sol_on() + sleep(port_object.base_duration) + port_object.sol_off() + sleep(.1) + + +def increase(port): + with open('stand_alone/durations.pkl', 'rb') as f: + durations = pickle.load(f) + durations[port] += .0005 + print(f'increasing port {port} to {durations[port]}') + with open('stand_alone/durations.pkl', 'wb') as f: + pickle.dump(durations, f) + + +def decrease(port): + with open('stand_alone/durations.pkl', 'rb') as f: + durations = pickle.load(f) + durations[port] -= .0005 + print(f'decreasing port {port} to {durations[port]}') + with open('stand_alone/durations.pkl', 'wb') as f: + pickle.dump(durations, f) + + +def reset(): + print('resetting task') + + +def stop(): + print('stopping task') + + +def party(): + print('partying') + +# +# if __name__ == '__main__': +# durations = {1: .01, +# 2: .01, +# 3: .01} +# with open('durations.pkl', 'wb') as f: +# pickle.dump(durations, f) diff --git a/behavior_code/progress_tracker.py b/behavior_code/progress_tracker.py new file mode 100644 index 0000000..f41c021 --- /dev/null +++ b/behavior_code/progress_tracker.py @@ -0,0 +1,110 @@ +import os +from tkinter import * +import time +from os import walk +import pandas as pd +from csv import DictReader, reader +import numpy as np +from datetime import date +from upload_to_pi import reset_time, ping_host +from user_info import get_user_info +import shutil + +info_dict = get_user_info() +initials = info_dict['initials'] +pi_names = info_dict['pi_names'] + + +def get_today_filepaths(days_back=0): + file_paths = [] + for root, dirs, filenames in walk(os.path.join(os.getcwd(), 'data')): + if len(dirs) == 0 and os.path.basename(root)[:2] == initials: + mouse = os.path.basename(root) + for f in filenames: + if f == 'desktop.ini': + continue + file_date = date(int(f[5:9]), int(f[10:12]), int(f[13:15])) + dif = date.today() - file_date + if dif.days <= days_back: + # if f[5:15] == time.strftime("%Y-%m-%d"): + file_paths.append(os.path.join(mouse, f)) + return file_paths + + +def gen_data(file_paths): + d = {} + for f in file_paths: + if f[:5] == 'mouse': + print('stop') + mouse_name = f[:5] + file_name = f[6:] + path = os.path.join(os.getcwd(), 'data', f) + data = pd.read_csv(path, na_values=['None'], skiprows=3) + with open(path, 'r') as file: + r = reader(file) + info_keys = next(r) + info_values = next(r) + starts = np.where([True if s[0] == '{' else False for s in info_values])[0][::-1] + ends = np.where([True if s[-1] == '}' else False for s in info_values])[0][::-1] + for i in range(len(starts)): + info_values = info_values[:starts[i]] + [",".join(info_values[starts[i]:ends[i] + 1])] + info_values[ + ends[i] + 1:] + info = dict(zip(info_keys, info_values)) + num_reward = len(data[(data.key == 'reward') & (data.value == 1)]) + mouse = os.path.dirname(f) + reward_string = f'{num_reward}({info["box"][-1]})' + + half_session_path = os.path.join(os.getcwd(), 'data', 'half_sessions', file_name) + if data.session_time.max() < 800: + print(f'moving {file_name} to half sessions, session time: {data.session_time.max():.2f} seconds') + shutil.move(path, half_session_path) + continue + if num_reward == 0: + ans = input(f'remove zero reward file? (y/n)\n{path}\n???') + if ans == 'y': + half_session_path = os.path.join(os.getcwd(), 'data', 'half_sessions', file_name) + shutil.move(path, half_session_path) + else: + if mouse in d.keys(): + d[mouse].append(reward_string) + else: + d[mouse] = [reward_string] + return d + + +class App(Frame): + def __init__(self, master=None): + Frame.__init__(self, master) + self.master = master + self.label = Label(text="", fg="Black", font=("Helvetica", 18)) + self.label.place(x=40, y=50) + self.host_names = pi_names + self.host_status = [True] * 3 + + data = gen_data(get_today_filepaths()) + txt = '\n'.join([f'{key}: {", ".join(str(d) for d in data[key])}' for key in data]) + self.label.configure(text=txt) + + def update(self): + data = gen_data(get_today_filepaths()) + txt = '\n'.join([f'{key}: {", ".join(str(d) for d in data[key])}' for key in data]) + self.label.configure(text=txt) + new_host_status = [ping_host(name) for name in self.host_names] + for i, (a, b) in enumerate(zip(new_host_status, self.host_status)): + if a and not b: + reset_time(self.host_names[i]) + self.host_status = new_host_status + self.after(10000, self.update) + + +def run_gui(): + root = Tk() + app = App(root) + root.wm_title("Tracker") + root.geometry("300x400") + root.after(10000, app.update) + root.mainloop() + + +if __name__ == '__main__': + run_gui() diff --git a/behavior_code/simple_plots.py b/behavior_code/simple_plots.py new file mode 100644 index 0000000..7e4d132 --- /dev/null +++ b/behavior_code/simple_plots.py @@ -0,0 +1,629 @@ +from datetime import date +import os +from tkinter import * +import time +from os import walk +import pandas as pd +from csv import DictReader, reader +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns +from matplotlib.collections import PatchCollection +from matplotlib.patches import Rectangle +from labellines import labelLine, labelLines +from user_info import get_user_info +import shutil + +info_dict = get_user_info() +initials = info_dict['initials'] +start_date = info_dict['start_date'] + + +def get_today_filepaths(days_back=0): + file_paths = [] + for root, dirs, filenames in walk(os.path.join(os.getcwd(), 'data')): + if len(dirs) == 0 and os.path.basename(root)[:2] == initials: + mouse = os.path.basename(root) + for f in filenames: + if f == 'desktop.ini': + continue + file_date = date(int(f[5:9]), int(f[10:12]), int(f[13:15])) + dif = date.today() - file_date + if dif.days <= days_back: + # if f[5:15] == time.strftime("%Y-%m-%d"): + file_paths.append(os.path.join(mouse, f)) + return file_paths + + +def min_dif(a, b, tolerance=0, return_index=False, rev=False): + if type(a) == pd.core.series.Series: + a = a.values + if type(b) == pd.core.series.Series: + b = b.values + if rev: + outer = -1 * np.subtract.outer(a, b) + outer[outer <= tolerance] = np.nan + else: + outer = np.subtract.outer(b, a) + outer[outer <= tolerance] = np.nan + # noinspection PyBroadException + mins = np.nanmin(outer, axis=0) + + if return_index: + index = np.nanargmin(outer, axis=0) + return index, mins + return mins + + +def read_pi_meta(pi_dir): + with open(pi_dir, 'r') as file: # Read meta data from first two lines into a dictionary + line1 = file.readline()[:-1] + line2 = file.readline()[:-1] + pieces = line2.split(',') + if '{' in line2: + curly_start = np.where(np.array([p[0] for p in pieces]) == '{')[0] + curly_end = np.where(np.array([p[-1] for p in pieces]) == '}')[0] + pieces_list = [] + sub_piece = [] + for i in range(len(pieces)): + if curly_start[0] <= i <= curly_end[0] or curly_start[1] <= i <= curly_end[1]: + sub_piece.append(pieces[i]) + else: + pieces_list.append(pieces[i]) + if i in curly_end: + string = ','.join(sub_piece) + try: + s, e = string.index('<'), string.index('>') + string = string[:s] + "'exp_decreasing'" + string[e + 1:] + except Exception as e: + pass + pieces_list.append(eval(string)) + sub_piece = [] + else: + pieces_list = line2.split(',') + info = dict(zip(line1.split(','), pieces_list)) + return info + + +def gen_data(file_paths, select_mouse=None, return_info=False): + d = {} + for f in file_paths: + mouse = os.path.dirname(f) + if select_mouse is not None and mouse not in select_mouse: + continue + + path = os.path.join(os.getcwd(), 'data', f) + if return_info: + data = read_pi_meta(path) + # if data['box'] == 'elissapi0': + # session = pd.read_csv(path, na_values=['None'], skiprows=3) + # session_summary(data_reduction(session), mouse) + # ans = input(f'remove broken file? (y/n)\n{path}\n???') + # if ans == 'y': + # file_name = f[6:] + # half_session_path = os.path.join(os.getcwd(), 'data', 'half_sessions', file_name) + # shutil.move(path, half_session_path) + else: + data = pd.read_csv(path, na_values=['None'], skiprows=3) + try: + data = data_reduction(data) + except ValueError: + file_name = f[6:] + half_session_path = os.path.join(os.getcwd(), 'data', 'half_sessions', file_name) + if data.session_time.max() < 800: + print(f'moving {f} to half sessions, session time: {data.session_time.max():.2f} seconds') + shutil.move(path, half_session_path) + else: + ans = input(f'remove broken file? (y/n)\n{path}\n???') + if ans == 'y': + shutil.move(path, half_session_path) + continue + if mouse in d.keys(): + d[mouse].append(data) + else: + d[mouse] = [data] + return d + + +def remove(df, key, tolerance, port): + on_times = df[(df.key == key) & (df.value == 1) & (df.port == port)].session_time.to_numpy() + off_times = df[(df.key == key) & (df.value == 0) & (df.port == port)].session_time.to_numpy() + if (on_times.size > 0) & (off_times.size > 0): + forward = min_dif(on_times, off_times) + forward_off = min_dif(on_times, off_times, rev=True) + forward[np.isnan(forward)] = tolerance + forward_off[np.isnan(forward_off)] = tolerance + on_times = on_times[forward >= tolerance] + off_times = off_times[forward_off >= tolerance] + + back = min_dif(off_times, on_times, rev=True) + back_off = min_dif(off_times, on_times) + back[np.isnan(back)] = tolerance + back_off[np.isnan(back_off)] = tolerance + on_times = on_times[back >= tolerance] + off_times = off_times[back_off >= tolerance] + + df = df[((df.key != key) | (df.value != 1) | (df.port != port)) | (df.session_time.isin(on_times))] + df = df[((df.key != key) | (df.value != 0) | (df.port != port)) | (df.session_time.isin(off_times))] + return df + + +def data_reduction(df, lick_tol=.01, head_tol=.2): + df = df[df.key != 'camera'] + df = df[df.phase != 'setup'] + df = remove(df, 'head', head_tol, port=1) + df = remove(df, 'head', head_tol, port=2) + df = remove(df, 'lick', lick_tol, port=1) + df = remove(df, 'lick', lick_tol, port=2) + return df + + +def consumption_time(df): + bg_end_times = df[(df.key == 'LED') & (df.port == 2) & (df.value == 1)] + exp_entries = df[(df.key == 'head') & (df.port == 1) & (df.value == 1)] + dif = min_dif(bg_end_times.session_time, exp_entries.session_time) + bg_consumption = dif[~np.isnan(dif)] + if df.task.iloc[10] != 'single_reward': + consumption_df = pd.DataFrame() + consumption_df['consumption time'] = bg_consumption + consumption_df['port'] = ['bg'] * len(bg_consumption) + return consumption_df + + exp_end_times = df[(df.key == 'LED') & (df.port == 1) & (df.value == 1)] + bg_entries = df[(df.key == 'head') & (df.port == 2) & (df.value == 1)] + dif = min_dif(exp_end_times.session_time, bg_entries.session_time) + exp_consumption = dif[~np.isnan(dif)] + consumption_df = pd.DataFrame() + consumption_df['consumption time'] = np.concatenate([bg_consumption, exp_consumption]) + consumption_df['port'] = ['bg'] * len(bg_consumption) + ['exp'] * len(exp_consumption) + return consumption_df + + +def block_leave_times(df): + reward_trials = df[(df.key == 'reward_initiate')].trial.to_numpy() + non_reward = ~df.trial.isin(reward_trials) + bg_end_times = df[(df.key == 'LED') & (df.port == 2) & (df.value == 1) & non_reward] + exp_entries = df[(df.key == 'head') & (df.value == 1) & (df.port == 1) & non_reward] + exp_exits = df[(df.key == 'head') & (df.value == 0) & (df.port == 1) & non_reward] + bg_end_times = bg_end_times[bg_end_times.session_time < exp_entries.session_time.max()] + ind, dif = min_dif(bg_end_times.session_time, exp_entries.session_time, return_index=True) + exp_entries = exp_entries.iloc[np.unique(ind)] + exp_entries = exp_entries.groupby('trial').session_time.max() + exp_exits = exp_exits.groupby('trial').session_time.max() + valid_trials = np.intersect1d(exp_exits.index.values, exp_entries.index.values) + valid_trials = np.intersect1d(valid_trials, bg_end_times.trial.values) + exp_exits = exp_exits.loc[valid_trials] + exp_entries = exp_entries.loc[valid_trials] + if len(exp_exits.to_numpy()) != len(exp_entries.to_numpy()): + print() + leave_times = exp_exits.to_numpy() - exp_entries.to_numpy() + + trial_blocks = bg_end_times[bg_end_times.trial.isin(exp_entries.index.values)].phase.to_numpy() + block_leaves_df = pd.DataFrame() + block_leaves_df['leave time'] = leave_times + block_leaves_df['block'] = trial_blocks + return block_leaves_df + + +def get_entry_exit(df, trial): + is_trial = df.trial == trial + start = df.value == 1 + end = df.value == 0 + port1 = df.port == 1 + port2 = df.port == 2 + + trial_start = df[is_trial & start & (df.key == 'trial')].session_time.values[0] + trial_middle = df[is_trial & end & (df.key == 'LED') & port2].session_time.values[0] + trial_end = df[is_trial & end & (df.key == 'trial')].session_time.values[0] + + bg_entries = df[is_trial & port2 & start & (df.key == 'head')].session_time.to_numpy() + bg_exits = df[is_trial & port2 & end & (df.key == 'head')].session_time.to_numpy() + + if len(bg_entries) == 0 or len(bg_exits) == 0 or bg_entries[0] > bg_exits[0]: + bg_entries = np.concatenate([[trial_start], bg_entries]) + if trial_end - bg_entries[-1] < .1: + bg_entries = bg_entries[:-1] + if len(bg_exits) == 0 or bg_entries[-1] > bg_exits[-1]: + bg_exits = np.concatenate([bg_exits, [trial_middle]]) + + exp_entries = df[is_trial & port1 & start & (df.key == 'head') & + (df.session_time > trial_middle)].session_time.to_numpy() + exp_exits = df[is_trial & port1 & end & (df.key == 'head') & + (df.session_time > trial_middle)].session_time.to_numpy() + + if not (len(exp_entries) == 0 and len(exp_exits) == 0): + if len(exp_entries) == 0: + exp_entries = np.concatenate([[trial_middle], exp_entries]) + if len(exp_exits) == 0: + exp_exits = np.concatenate([exp_exits, [trial_end]]) + + if exp_entries[0] > exp_exits[0]: + exp_entries = np.concatenate([[trial_middle], exp_entries]) + if exp_entries[-1] > exp_exits[-1]: + exp_exits = np.concatenate([exp_exits, [trial_end]]) + + early_exp_entries = df[is_trial & port1 & start & (df.key == 'head') & + (df.session_time < trial_middle)].session_time.to_numpy() + early_exp_exits = df[is_trial & port1 & end & (df.key == 'head') & + (df.session_time < trial_middle)].session_time.to_numpy() + + if not (len(early_exp_entries) == 0 and len(early_exp_exits) == 0): + if len(early_exp_entries) == 0: + early_exp_entries = np.concatenate([[trial_start], early_exp_entries]) + if len(early_exp_exits) == 0: + early_exp_exits = np.concatenate([early_exp_exits, [trial_middle]]) + + if early_exp_entries[0] > early_exp_exits[0]: + early_exp_entries = np.concatenate([[trial_start], early_exp_entries]) + if early_exp_entries[-1] > early_exp_exits[-1]: + early_exp_exits = np.concatenate([early_exp_exits, [trial_middle]]) + + if len(bg_entries) != len(bg_exits): + print() + if len(exp_entries) != len(exp_exits): + print() + if len(early_exp_entries) != len(early_exp_exits): + print() + + return bg_entries, bg_exits, exp_entries, exp_exits, early_exp_entries, early_exp_exits + + +def percent_engaged(df): + travel_time = .5 + blocks = df.phase.unique() + blocks.sort() + time_engaged = [] + block_time = [] + block_rewards = [] + for block in blocks: + engaged = [] + all_time = [] + rewards = [] + block_trials = df[(df.value == 0) & (df.key == 'trial') & (df.phase == block)].trial + for trial in block_trials: + bg_entries, bg_exits, exp_entries, exp_exits, _, _ = get_entry_exit(df, trial) + is_trial = df.trial == trial + start = df.value == 1 + end = df.value == 0 + # port1 = df.port == 1 + # port2 = df.port == 2 + + # + trial_start = df[is_trial & start & (df.key == 'trial')].session_time.values[0] + # trial_middle = df[is_trial & start & (df.key == 'LED') & port2].session_time.values[0] + trial_end = df[is_trial & end & (df.key == 'trial')].session_time.values[0] + # + # bg_entries = df[is_trial & port2 & start & (df.key == 'head')].session_time.to_numpy() + # bg_exits = df[is_trial & port2 & end & (df.key == 'head')].session_time.to_numpy() + # + # if len(bg_entries) == 0 or bg_entries[0] > bg_exits[0]: + # bg_entries = np.concatenate([[trial_start], bg_entries]) + # if trial_end - bg_entries[-1] < .1: + # bg_entries = bg_entries[:-1] + # if len(bg_exits) == 0 or bg_entries[-1] > bg_exits[-1]: + # bg_entries = np.concatenate([bg_exits, [trial_middle]]) + # + # if not (len(bg_entries) == len(bg_exits) and np.all(bg_exits - bg_entries > 0)): + # print('stop') + # bg_engaged = sum(bg_exits - bg_entries) + # + # exp_entries = df[is_trial & port1 & start & (df.key == 'head') & + # (df.session_time > trial_middle)].session_time.to_numpy() + # exp_exits = df[is_trial & port1 & end & (df.key == 'head') & + # (df.session_time > trial_middle)].session_time.to_numpy() + # + # if len(exp_entries) == 0 and len(exp_exits) == 0: + # exp_engaged = 0 + # else: + # if len(exp_entries) == 0: + # exp_entries = np.concatenate([[trial_middle], exp_entries]) + # if len(exp_exits) == 0: + # exp_exits = np.concatenate([exp_exits, [trial_end]]) + # + # if exp_entries[0] > exp_exits[0]: + # exp_entries = np.concatenate([[trial_middle], exp_entries]) + # if exp_entries[-1] > exp_exits[-1]: + # exp_exits = np.concatenate([exp_exits, [trial_end]]) + # exp_engaged = sum(exp_exits - exp_entries) + # + # # if not len(exp_entries) == len(exp_exits) and len(exp_entries): + # # print('stop') + # # if len(exp_entries): + + if len(exp_entries): + exp_engaged = sum(exp_exits - exp_entries) + else: + exp_engaged = 0 + bg_engaged = sum(bg_exits - bg_entries) + + all_time.append(trial_end - trial_start) + engaged.append(bg_engaged + exp_engaged) + rewards.append(len(df[is_trial & start & (df.key == 'reward')])) + + time_engaged.append(sum(engaged) + travel_time * 2 * len(block_trials)) + block_time.append(sum(all_time)) + block_rewards.append(sum(rewards)) + engaged_df = pd.DataFrame() + engaged_df['percent engaged'] = np.array(time_engaged) / np.array(block_time) + engaged_df['block'] = blocks + engaged_df['time engaged'] = time_engaged + engaged_df['rewards earned'] = block_rewards + engaged_df['reward rate'] = np.array(block_rewards) / np.array(time_engaged) + return engaged_df + + +def reentry_index(df): + is_bg_exit = (df.port == 2) & (df.key == 'head') & (df.value == 0) + is_slow_block = df.groupby('trial').phase.agg(pd.Series.mode) == '0.4' + is_fast_block = df.groupby('trial').phase.agg(pd.Series.mode) == '0.8' + num_ideal_bg_entry_slow = len(np.unique(df.trial.dropna())[is_slow_block]) + num_bg_entry_slow = len(df.index[is_bg_exit & df.trial.isin( + np.unique(df.trial.dropna())[is_slow_block])]) + num_ideal_bg_entry_fast = len(np.unique(df.trial.dropna())[is_fast_block]) + num_bg_entry_fast = len(df.index[is_bg_exit & df.trial.isin( + np.unique(df.trial.dropna())[is_fast_block])]) + + if num_ideal_bg_entry_slow != 0: + reentry_index_slow = num_bg_entry_slow / num_ideal_bg_entry_slow + else: + reentry_index_slow = np.nan + if num_ideal_bg_entry_fast != 0: + reentry_index_fast = num_bg_entry_fast / num_ideal_bg_entry_fast + else: + reentry_index_fast = np.nan + reentry_df = pd.DataFrame() + reentry_df['block'] = ['0.4', '0.8'] + reentry_df['bg_reentry_index'] = [reentry_index_slow, reentry_index_fast] + return reentry_df + + +def add_h_lines(data=None, x=None, y=None, hue=None, ax=None, palette=None, estimator='mean'): + days_back = 10 + palette = sns.color_palette(palette) + for i, hue_key in enumerate(data[hue].unique()): + df = data[data[hue] == hue_key] + if df[x].max() > days_back: + if estimator == 'median': + hue_mean = df[(df[x] > df[x].max() - days_back)][y].median() + else: + hue_mean = df[(df[x] > df[x].max() - days_back)][y].mean() + ax.hlines(hue_mean, df[x].max() - days_back, df[x].max(), palette[i], alpha=.5) + + +def merge_old_trials(session): + print() + return session + + +def simple_plots(select_mouse=None): + plot_single_mouse_plots=True + if select_mouse is None: + dif = date.today() - start_date + data = gen_data(get_today_filepaths(days_back=dif.days), select_mouse=select_mouse) + info = gen_data(get_today_filepaths(days_back=dif.days), select_mouse=select_mouse, return_info=True) + else: + data = gen_data(get_today_filepaths(days_back=1000), select_mouse=select_mouse) + info = gen_data(get_today_filepaths(days_back=1000), select_mouse=select_mouse, return_info=True) + block_leaves_last10 = pd.DataFrame() + for mouse in data.keys(): + if select_mouse is not None and mouse not in select_mouse: + continue + engaged = pd.DataFrame() + consumption = pd.DataFrame() + block_leaves = pd.DataFrame() + reentry = pd.DataFrame() + for i, session in enumerate(data[mouse]): + if info[mouse][i]['task'] == 'cued_forgo_forced': + continue + try: + session = merge_old_trials(session) + + engaged_df = percent_engaged(session) + engaged_df['day'] = [i] * len(engaged_df) + engaged = pd.concat([engaged, engaged_df]) + + consumption_df = consumption_time(session) + consumption_df['day'] = [i] * len(consumption_df) + consumption = pd.concat([consumption, consumption_df]) + + block_leaves_df = block_leave_times(session) + block_leaves_df['day'] = [i] * len(block_leaves_df) + block_leaves = pd.concat([block_leaves, block_leaves_df]) + + reentry_df = reentry_index(session) + reentry_df['day'] = [i] * len(reentry_df) + reentry = pd.concat([reentry, reentry_df]) + except Exception as e: + raise e + + engaged.sort_values('block', inplace=True) + block_leaves.sort_values('block', inplace=True) + if plot_single_mouse_plots: + fig, axes = plt.subplots(3, 2, figsize=[11, 12], layout="constrained") + sns.lineplot(data=block_leaves.reset_index(), x='day', y='leave time', hue='block', ax=axes[0, 0], + palette='Set2') + add_h_lines(data=block_leaves.reset_index(), x='day', y='leave time', hue='block', ax=axes[0, 0], + palette='Set2') + sns.lineplot(data=consumption.reset_index(), x='day', y='consumption time', hue='port', ax=axes[0, 1], + palette='Set1', estimator=np.median) + add_h_lines(data=consumption.reset_index(), x='day', y='consumption time', hue='port', ax=axes[0, 1], + palette='Set1', estimator='median') + sns.lineplot(data=engaged.reset_index(), x='day', y='reward rate', hue='block', ax=axes[1, 0], + palette='Set2') + add_h_lines(data=engaged.reset_index(), x='day', y='reward rate', hue='block', ax=axes[1, 0], + palette='Set2') + sns.lineplot(data=engaged.reset_index(), x='day', y='percent engaged', hue='block', ax=axes[1, 1], + palette='Set2') + add_h_lines(data=engaged.reset_index(), x='day', y='percent engaged', hue='block', ax=axes[1, 1], + palette='Set2') + sns.lineplot(data=reentry.reset_index(), x='day', y='bg_reentry_index', hue='block', ax=axes[2, 0], + palette='Set2') + add_h_lines(data=reentry.reset_index(), x='day', y='bg_reentry_index', hue='block', ax=axes[2, 0], + palette='Set2') + + axes[0, 0].set_title('Leave Time by Block') + axes[0, 1].set_title('Consumption Time by Port') + axes[1, 0].set_title('Reward Rate by Block') + axes[1, 1].set_title('Percent Time Engaged by Block') + axes[2, 0].set_title('Background Reentry Index') + + + axes[0, 0].set_ylim([0, 20]) + axes[0, 1].set_ylim([0, 20]) + axes[1, 0].set_ylim([0, .65]) + axes[1, 1].set_ylim([0, 1]) + axes[2, 0].set_ylim([0.98, 3]) + plt.suptitle(mouse, fontsize=20) + plt.show() + + block_leaves_last10_df = block_leaves[(block_leaves.day >= block_leaves.day.max() - 10)].groupby('block')[ + 'leave time'].mean().reset_index() + block_leaves_last10_df['animal'] = mouse + block_leaves_last10 = pd.concat([block_leaves_last10, block_leaves_last10_df]) + + fig, axes = plt.subplots(1, 1, figsize=[5, 10]) + sns.boxplot(data=block_leaves_last10.reset_index(), x='block', y='leave time') + for mouse in data.keys(): + plt.plot([-0.1, 0.9], block_leaves_last10[block_leaves_last10.animal == mouse]['leave time'], 'o-', + color='darkgray', label=mouse[-3:]) + labelLines(plt.gca().get_lines(), align=True, zorder=2.5, fontsize=7, xvals=(0.3, 0.8)) + plt.ylim([0, 14.5]) + fig.show() + +def single_session(select_mouse=None, num_back=1): + if select_mouse is None: + dif = date.today() - start_date + data = gen_data(get_today_filepaths(days_back=dif.days), select_mouse=select_mouse) + info = gen_data(get_today_filepaths(days_back=dif.days), select_mouse=select_mouse, return_info=True) + else: + data = gen_data(get_today_filepaths(days_back=1000), select_mouse=select_mouse) + info = gen_data(get_today_filepaths(days_back=1000), select_mouse=select_mouse, return_info=True) + for mouse in data.keys(): + if select_mouse is not None and mouse not in select_mouse: + continue + for i in range(1, num_back + 1): + last_session = data[mouse][-i] + last_info = info[mouse][-i] + session_summary(last_session, mouse, last_info) + + +def session_summary(data, mouse, info): + fig, [ax1, ax2] = plt.subplots(1, 2, figsize=[10, 10]) + port_palette = sns.color_palette('Set1') + block_palette = sns.color_palette('Set2') + start = data.value == 1 + end = data.value == 0 + head = data.key == 'head' + lick = data.key == 'lick' + reward = data.key == 'reward' + port1 = data.port == 1 + port2 = data.port == 2 + max_trial = data.trial.max() + + bg_rectangles = [] + exp_rectangles_in_bg = [] + exp_rectangles = [] + block1_rectangles = [] + block2_rectangles = [] + bg_reward_events = [] + exp_reward_events = [] + bg_lick_events = [] + exp_lick_events = [] + bg_lengths = [] + exp_lengths = [] + trial_blocks = data.groupby(['trial'])['phase'].agg(pd.Series.mode) + blocks = data.phase.unique() + blocks.sort() + for trial in data.trial.unique(): + if np.isnan(trial): + continue + is_trial = data.trial == trial + try: + trial_start = data[is_trial & start & (data.key == 'trial')].session_time.values[0] + trial_middle = data[is_trial & end & (data.key == 'LED') & port2].session_time.values[0] + trial_end = data[is_trial & end & (data.key == 'trial')].session_time.values[0] + except IndexError: + continue + + bg_rewards = data[is_trial & start & port2 & reward].session_time.values + exp_rewards = data[is_trial & start & port1 & reward].session_time.values + bg_licks = data[is_trial & start & lick & (data.session_time < trial_middle)].session_time.values + exp_licks = data[is_trial & start & lick & (data.session_time > trial_middle)].session_time.values + + bg_lengths.append(trial_middle - trial_start) + exp_lengths.append(trial_end - trial_middle) + + bg_entries, bg_exits, exp_entries, exp_exits, early_exp_entries, early_exp_exits = get_entry_exit(data, trial) + bg_intervals = list(zip(bg_entries, bg_exits)) + exp_intervals = list(zip(exp_entries, exp_exits)) + early_exp_intervals = list(zip(early_exp_entries, early_exp_exits)) + for [s, e] in bg_intervals: + bg_rectangles.append(Rectangle((s - trial_start, trial), e - s, .7)) + for [s, e] in early_exp_intervals: + exp_rectangles_in_bg.append(Rectangle((s - trial_start, trial), e - s, .7)) + for [s, e] in exp_intervals: + exp_rectangles.append(Rectangle((s - trial_middle, trial), e - s, .7)) + if np.where(blocks == trial_blocks.loc[trial])[0][0] == 0: + block1_rectangles.append(Rectangle((0, trial), 100, 1)) + else: + block2_rectangles.append(Rectangle((0, trial), 100, 1)) + bg_reward_events.append(bg_rewards - trial_start) + exp_reward_events.append(exp_rewards - trial_middle) + bg_lick_events.append(bg_licks - trial_start) + exp_lick_events.append(exp_licks - trial_middle) + + alpha = .5 + pc_b1 = PatchCollection(block1_rectangles, facecolors=block_palette[0], alpha=alpha) + pc_b2 = PatchCollection(block2_rectangles, facecolors=block_palette[1], alpha=alpha) + ax1.add_collection(pc_b1) + ax1.add_collection(pc_b2) + pc_b12 = PatchCollection(block1_rectangles, facecolors=block_palette[0], alpha=alpha) + pc_b22 = PatchCollection(block2_rectangles, facecolors=block_palette[1], alpha=alpha) + ax2.add_collection(pc_b12) + ax2.add_collection(pc_b22) + + pc_bg = PatchCollection(bg_rectangles, edgecolor=port_palette[0], facecolor='w', alpha=1) + ax1.add_collection(pc_bg) + + pc_exp_bg = PatchCollection(exp_rectangles_in_bg, edgecolor=port_palette[1], facecolor='w', alpha=1) + ax1.add_collection(pc_exp_bg) + + pc_exp = PatchCollection(exp_rectangles, edgecolor=port_palette[1], facecolor='w', alpha=1) + ax2.add_collection(pc_exp) + + offsets = np.array(list(range(len(bg_reward_events)))) + 1.4 + ax1.eventplot(bg_reward_events, color='purple', linelengths=.62, lineoffsets=offsets) + offsets = np.array(list(range(len(exp_reward_events)))) + 1.4 + ax2.eventplot(exp_reward_events, color='purple', linelengths=.62, lineoffsets=offsets) + + light = [.8, .7, .8] + dark = [.2, .2, .2] + offsets = np.array(list(range(len(bg_lick_events)))) + 1.4 + ax1.eventplot(bg_lick_events, color=light, linelengths=.25, lineoffsets=offsets) + offsets = np.array(list(range(len(exp_lick_events)))) + 1.4 + ax2.eventplot(exp_lick_events, color=light, linelengths=.25, lineoffsets=offsets) + + session_summary_axis_settings([ax1, ax2], max_trial) + plt.suptitle(f'{mouse}: {info["date"]} {info["time"]}') + plt.show() + + +def session_summary_axis_settings(axes, max_trial): + for ax in axes: + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + ax.spines['left'].set_visible(False) + ax.spines['bottom'].set_visible(True) + ax.get_yaxis().set_visible(False) + ax.set_ylim([-1, max_trial + 1]) + ax.set_xlim([0, 20]) + ax.invert_yaxis() + ax.set_ylabel('Trial') + ax.set_xlabel('Time (sec)') + + +if __name__ == '__main__': + mice = ['SZ050', 'SZ051', 'SZ052', 'SZ053', 'SZ054', 'SZ055', 'SZ056', 'SZ057', 'SZ058', 'SZ059'] + # mice = ['SZ036', 'SZ042', 'SZ044', 'SZ048'] + # single_session(mice) + simple_plots(mice) \ No newline at end of file diff --git a/behavior_code/stand_alone/check_sound.py b/behavior_code/stand_alone/check_sound.py new file mode 100644 index 0000000..60837cf --- /dev/null +++ b/behavior_code/stand_alone/check_sound.py @@ -0,0 +1,16 @@ +import pygame +import time + + +def check_sound(): + print('playing tone') + pygame.mixer.init() + tone = pygame.mixer.Sound('end_tone.wav') + tone.set_volume(.5) + pygame.mixer.Sound.play(tone, fade_ms=500) + time.sleep(5) + print('played tone') + + +if __name__ == '__main__': + check_sound() diff --git a/behavior_code/stand_alone/durations.pkl b/behavior_code/stand_alone/durations.pkl new file mode 100644 index 0000000..7ff68c9 Binary files /dev/null and b/behavior_code/stand_alone/durations.pkl differ diff --git a/behavior_code/stand_alone/end_tone.wav b/behavior_code/stand_alone/end_tone.wav new file mode 100644 index 0000000..0cb0374 Binary files /dev/null and b/behavior_code/stand_alone/end_tone.wav differ diff --git a/behavior_code/stand_alone/gui.py b/behavior_code/stand_alone/gui.py new file mode 100644 index 0000000..cd559e0 --- /dev/null +++ b/behavior_code/stand_alone/gui.py @@ -0,0 +1,164 @@ +import numpy as np +import tkinter as tk +import tkinter.font as font +import tkinter.ttk as ttk +from functools import partial +import RPi.GPIO as GPIO +from support_classes import * +import pickle +from time import sleep +from main import * +import pexpect +from user_settings import get_user_info + +info_dict = get_user_info() + +# scp -r C:\Users\Elissa\GoogleDrive\Code\Python\behavior_code\stand_alone pi@elissapi0:\home\pi +# scp C:\Users\Elissa\GoogleDrive\Code\Python\behavior_code\stand_alone\scp_rescue.py pi@elissapi1:\home\pi\behavior + +# scp -r "C:\Users\Shichen\OneDrive - Johns Hopkins\ShulerLab\behavior_code\stand_alone" pi@elissapi1:\home\pi\behavior1 +pastel_colors = ['#ffffcc', '#99ccff', '#cc99ff', '#ff99cc', '#ffcc99', '#ffffcc', '#99ffcc', '#ccffff', '#ccccff', + '#ffccff', '#ffcccc', '#D3D3D3', '#f0a207'] + + +class Gui: + def __init__(self): + self.root = tk.Tk() + self.root.geometry('2000x700') + self.root.title('BehaviorGui') + + with open('durations.pkl', 'rb') as f: + self.durations = pickle.load(f) + self.calibration_text = {1: tk.StringVar(), + 2: tk.StringVar()} + self.calibration_text[1].set(f'Port 1: {self.durations[1] * 1000}ms ') + self.calibration_text[2].set(f'Port 2: {self.durations[2] * 1000}ms') + myFont = font.Font(size=30) + mouse_rows = len(info_dict['mouse_buttons']) + self.mouse_assignments = info_dict['mouse_assignments'] + tasks = { + 'single_reward': single_reward, + 'cued_forgo': cued_forgo + } + for key in self.mouse_assignments.keys(): + self.mouse_assignments[key] = tasks[self.mouse_assignments[key]] + # self.mouse_assignments = { + # 'ES036': single_reward, + # 'ES037': single_reward, + # 'ES038': single_reward, + # 'ES039': cued_forgo, + # 'ES040': cued_forgo, + # 'testmouse': cued_forgo, + # } + buttons = np.array( + [*info_dict['mouse_buttons'], + ['check_scp', 'check_ir', 'testmouse'], + ['-0.25ms', self.calibration_text[1], '+0.25ms'], + ['-0.25ms', self.calibration_text[2], '+0.25ms']]) + mouse_functions = np.array( + [[partial(self.run_behavior, buttons[i, j]) for j in range(buttons.shape[1])] for i in range(mouse_rows)]) + control_func = np.array([[self.reset, self.check_ir, partial(self.run_behavior, 'testmouse')], + [partial(self.decrease, 1), partial(self.calibrate, 1), partial(self.increase, 1)], + [partial(self.decrease, 2), partial(self.calibrate, 2), partial(self.increase, 2)]]) + button_colors = [*info_dict['button_colors'], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ] + functions = np.concatenate([mouse_functions, control_func]) + self.button_list = [] + for i in range(buttons.shape[0]): + self.root.rowconfigure(i, weight=1, minsize=50) + for j in range(buttons.shape[1]): + self.root.columnconfigure(i, weight=1, minsize=75) + frame = tk.Frame( + master=self.root, + # relief=tk.RAISED, + borderwidth=1 + ) + frame.grid(row=i, column=j, sticky="nsew") + if i in [mouse_rows + 1, mouse_rows + 2] and j == 1: + button = tk.Button( + textvariable=buttons[i, j], + font=myFont, + width=60, + height=7, + bg=pastel_colors[button_colors[i][j]], + fg="black", + master=frame, + command=functions[i, j]) + else: + button = tk.Button( + text=buttons[i, j], + font=myFont, + width=60, + height=7, + bg=pastel_colors[button_colors[i][j]], + fg="black", + master=frame, + command=functions[i, j]) + + button.pack(fill=tk.BOTH, expand=True) + self.button_list.append(button) + self.root.mainloop() + + def run_behavior(self, mouse): + task = self.mouse_assignments[mouse] + print(f"running {task} for {mouse}") + main(mouse, task, forgo=False, forced_trials=True) + + def calibrate(self, port): + print(f'calibrating port {port}') + GPIO.setmode(GPIO.BCM) + port_object = Port(port, dist_info='filler', duration=self.durations[port]) + for _ in range(100): + port_object.sol_on() + sleep(port_object.base_duration) + port_object.sol_off() + sleep(.1) + + def increase(self, port): + self.durations[port] = np.around(self.durations[port] + .00025, decimals=6) + self.calibration_text[port].set(f'Port {port}: {self.durations[port] * 1000}ms') + print(f'increasing port {port} to {self.durations[port]}') + with open('durations.pkl', 'wb') as f: + pickle.dump(self.durations, f) + + def decrease(self, port): + self.durations[port] = np.around(self.durations[port] - .00025, decimals=6) + self.calibration_text[port].set(f'Port {port}: {self.durations[port] * 1000}ms') + print(f'decreasing port {port} to {self.durations[port]}') + with open('durations.pkl', 'wb') as f: + pickle.dump(self.durations, f) + + def reset(self): + session = Session('testmouse') + session.start_time = time.time() + session.log('test_line') + session.smooth_finish = True + session.end() + # GPIO.setmode(GPIO.BCM) + # ports = [Port(1, None, duration=.01), Port(2, None, duration=.01)] + # GPIO.cleanup() + # print('box reset') + + def check_ir(self): + GPIO.setmode(GPIO.BCM) + ports = [Port(1, None, duration=.01), Port(2, None, duration=.01)] + start = time.time() + while time.time() - start < 20: + for port in ports: + for change, event in zip([port.head_status_change(), port.lick_status_change()], ['head', 'lick']): + if change: + action = 'in' if change == 1 else 'out' + print(f'Port {port.name} {event} {action}') + GPIO.cleanup() + print('Done') + + +def run_gui(): + app = Gui() + + +if __name__ == '__main__': + run_gui() diff --git a/behavior_code/stand_alone/main.py b/behavior_code/stand_alone/main.py new file mode 100644 index 0000000..4815760 --- /dev/null +++ b/behavior_code/stand_alone/main.py @@ -0,0 +1,154 @@ +from support_classes import * +from tasks import * +from timescapes import * +import random + + +def give_up(session, reward_level, peak_time, session_length): + print(reward_level) + print(peak_time) + print(session_length) + exp_dist = {'distribution': lin_over_ex, + 'cumulative': reward_level, + 'peak_time': peak_time} + port_1 = Port(1, dist_info=exp_dist) + port_2 = Port(2, dist_info=exp_dist) + ports = [port_1, port_2] + task1 = Task(session, name='give_up', structure=give_up_task, ports=ports, maximum=session_length, limit='time') + session.start([task1]) + + +def give_up_forgo(session, reward_level, starting_prob, session_length, fixed_times): + print(reward_level) + print(starting_prob) + print(session_length) + exp_dist = {'distribution': exp_decreasing, + 'cumulative': reward_level, + 'staring_probability': starting_prob} + background_dist = {'distribution': fixed_single, + 'delays': fixed_times} + dists = [exp_dist, background_dist] + # dists = [background_dist, exp_dist] + port_1 = Port(1, dist_info=dists[0]) + port_2 = Port(2, dist_info=dists[1]) + task1 = Task(session, name='give_up_forgo', structure=give_up_forgo_task, ports=[port_1, port_2], + maximum=session_length, limit='time') + session.start([task1]) + + +def cued_forgo(session, reward_level, starting_prob, session_length, forgo=False, forced_trials=True): + print(reward_level) + print(starting_prob) + print(session_length) + task_structure = cued_forgo_task + + if forgo: + task_name = 'cued_forgo' + print('cued forgo') + else: + task_name = 'cued_no_forgo' + print('cued bg without forgo option') + + if forced_trials: + task_name = task_name + '_forced' + print('forced trials included') + + rates = [.4, .8, .4, .8, .4, .8] + if np.random.random() > .5: + rates.reverse() + background_dist = {'distribution': 'background', + 'rates': rates, + 'duration': 5, + 'port_num': 2} + print(background_dist['rates']) + exp_dist = {'distribution': exp_decreasing, + 'cumulative': reward_level, + 'starting_probability': starting_prob, + 'port_num': 1} + ports = {'exp': Port(exp_dist['port_num'], dist_info=exp_dist), + 'background': Port(background_dist['port_num'], dist_info=background_dist)} + task1 = Task(session, name=task_name, structure=task_structure, ports=ports, + maximum=session_length, limit='time', forgo=forgo, forced_trials=forced_trials) + session.start([task1]) + + +def single_reward(session, reward_level, starting_prob, session_length, forgo=False, forced_trials=True): + reward_level = 0.5994974874371859 # cumulative for and 8 reward version + starting_prob = 0.1301005025125628 + print(reward_level) + print(starting_prob) + print(session_length) + task_structure = single_reward_task + + task_name = 'single_reward' + + rates = [.4, .8, .4, .8, .4, .8] + if np.random.random() > .5: + rates.reverse() + background_dist = {'distribution': 'background', + 'rates': rates, + 'duration': 5, + 'port_num': 2} + print(background_dist['rates']) + exp_dist = {'distribution': exp_decreasing, + 'cumulative': reward_level, + 'starting_probability': starting_prob, + 'port_num': 1} + ports = {'exp': Port(exp_dist['port_num'], dist_info=exp_dist), + 'background': Port(background_dist['port_num'], dist_info=background_dist)} + task1 = Task(session, name=task_name, structure=task_structure, ports=ports, + maximum=session_length, limit='time') + session.start([task1]) + +def give_up_blocked(session, reward_level, starting_prob, session_length, forgo=True, forced_trials=False): + task_structure = give_up_blocked_task + task_name = 'give_up_blocked' + + blocks = ['hi_hi', 'hi_lo', 'lo_hi', 'lo_lo'] + # order = [1, 0, 2, 3] + # blocks = [blocks[i] for i in order] + random.shuffle(blocks) + print(blocks) + exp_dist = {'distribution': exp_decreasing, + 'blocks': blocks, + 'cumulative': 4, + 'starting': 1, + 'hi': 1, + 'lo': .8} + ports = {'right': Port(1, dist_info=exp_dist), + 'left': Port(2, dist_info=exp_dist)} + task1 = Task(session, name=task_name, structure=task_structure, ports=ports, + maximum=session_length, limit='time', forgo=forgo, forced_trials=forced_trials) + session.start([task1]) + + +def main(mouse, to_run, forgo=False, forced_trials=False): + cumulative = 8 + start_prob = 1 + session_time = 18 + mouse_settings = { + 'testmouse': [cumulative, start_prob, session_time], + 'default': [cumulative, start_prob, session_time], # reward level, starting prop, session time, [intervals]. + } + + session = Session(mouse) # Start a new session for the mouse + + try: + if mouse not in mouse_settings.keys(): + to_run(session, *mouse_settings['default'], forgo=forgo, forced_trials=forced_trials) # Run the task + else: + to_run(session, *mouse_settings[mouse], forgo=forgo, forced_trials=forced_trials) # Run the task + session.smooth_finish = True + print('smooth finish') + except KeyboardInterrupt: # Catch if the task is stopped via ctrl-C or the stop button + session.halted = True + finally: + session.end() + + +if __name__ == "__main__": + # main('testmouse', cued_forgo, forgo=False, forced_trials=True) + # main('ES024', cued_forgo, forgo=False, forced_trials=True) + main('ES030', cued_forgo, forgo=False, forced_trials=True) + # main('testmouse', give_up_blocked) + diff --git a/behavior_code/stand_alone/manual_sol_save.py b/behavior_code/stand_alone/manual_sol_save.py new file mode 100644 index 0000000..33210a4 --- /dev/null +++ b/behavior_code/stand_alone/manual_sol_save.py @@ -0,0 +1,86 @@ +import pickle +import RPi.GPIO as GPIO +from support_classes import * +from timescapes import * +from time import sleep +import smbus +import numpy as np + +DEVICE = 0x20 # Device address (A0-A2) +SETUP_A = 0x00 # Pin direction register +SETUP_B = 0x01 # Pin direction register +INPUT_A = 0x12 # Register for inputs on A +INPUT_B = 0x13 # Register for inputs on B +OUTPUT_A = 0x14 # Register for outputs on A +OUTPUT_B = 0x15 # Register for outputs on B + + +def led_test(): + GPIO.setmode(GPIO.BCM) + GPIO.setup(16, GPIO.OUT) + GPIO.setup(20, GPIO.OUT) + GPIO.setup(21, GPIO.OUT) + + GPIO.output(20, GPIO.HIGH) + sleep(5) + GPIO.output(20, GPIO.LOW) + sleep(2) + GPIO.output(21, GPIO.HIGH) + sleep(5) + GPIO.output(21, GPIO.LOW) + GPIO.cleanup() + + +def extra_gpio_test(): + expander = Expander() + for i in range(2): + for j in range(8): + print(i) + print(j) + expander.on(i, j) + sleep(1) + expander.off(i, j) + + +def save_change(): + with open('durations.pkl', 'wb') as f: + durations = { + 1: 0.0165, + 2: 0.01 + } + pickle.dump(durations, f) + + +def test_sol(repeats=100, interval=.1): + with open('durations.pkl', 'rb') as f: + durations = pickle.load(f) + GPIO.setmode(GPIO.BCM) + ports = [] + exp_dist = {'distribution': exp_decreasing, + 'cumulative': 5, + 'staring_probability': 1} + background_dist = {'distribution': 'background', + 'rates': [.4, .8, .4, .8, .4, .8], + 'duration': 5} + port_dict = { + 'exp': Port(1, dist_info=exp_dist, duration=durations[1]), + 'background': Port(2, dist_info=background_dist, duration=durations[2]) + } + ports = port_dict.values() + try: + for _ in range(repeats): + for port in ports: + port.sol_on() + sleep(port.base_duration) + port.sol_off() + sleep(interval) + finally: + for port in ports: + port.sol_off() + + +if __name__ == '__main__': + save_change() + test_sol() + # led_test() + # extra_gpio_test() diff --git a/behavior_code/stand_alone/scp_rescue.py b/behavior_code/stand_alone/scp_rescue.py new file mode 100644 index 0000000..1489755 --- /dev/null +++ b/behavior_code/stand_alone/scp_rescue.py @@ -0,0 +1,53 @@ +from support_classes import scp, ssh +import os +from user_settings import get_user_info + +info_dict = get_user_info() + + +def get_filepaths(): + file_paths = [] + local_dir = os.path.join(os.getcwd(), 'data') + for root, dirs, filenames in os.walk(local_dir): + if len(dirs) == 0 and os.path.basename(root)[:2] == info_dict['initials']: + mouse = os.path.basename(root) + for f in filenames: + file_paths.append(os.path.join(local_dir, mouse, f)) + return file_paths + + +def scp_rescue(gen_cmd=False): + file_paths = get_filepaths() + dest_path = os.path.join(info_dict['desktop_user_root'], info_dict['desktop_save_path']) + + for file in file_paths: + path_parts = file.split(os.sep) + file_mouse_path = os.path.join(*path_parts[-2:]) + full_dest_path = os.path.join(dest_path, path_parts[-2]) + file_name = os.path.join(*path_parts[-1:]) + os.chdir(os.path.join(*path_parts[-3:-1])) + ssh_path = os.path.join(dest_path, path_parts[-2]) + os.system('sudo chmod o-w ' + file_name) + mkdir_command = 'if not exist "%s" mkdir "%s"' % ( + ssh_path.replace('/', '\\'), ssh_path.replace('/', '\\')) + + ssh(info_dict['desktop_ip'], mkdir_command, info_dict['desktop_user'], + info_dict['desktop_password']) + + res = scp(info_dict['desktop_ip'], file_name, full_dest_path, info_dict['desktop_user'], + info_dict['desktop_password'], cmd=gen_cmd) + + os.chdir(os.path.join(os.getcwd(), '..', '..')) + if gen_cmd: + print(res) + else: + if res == 0: + print('\nSuccessful file transfer to "%s"\nDeleting local file from pi.' + % os.path.join(dest_path, file_mouse_path)) + os.remove(file) + else: + print('\nFile transfer failed with exit code "%s"\n' % str(res)) + + +if __name__ == '__main__': + scp_rescue() diff --git a/behavior_code/stand_alone/support_classes.py b/behavior_code/stand_alone/support_classes.py new file mode 100644 index 0000000..d5845bd --- /dev/null +++ b/behavior_code/stand_alone/support_classes.py @@ -0,0 +1,532 @@ +import RPi.GPIO as GPIO +import time +import datetime +import os +from timescapes import * +import pexpect +import pickle +import tempfile +import smbus +import numpy as np +from pygame import mixer +from user_settings import get_user_info + +info_dict = get_user_info() + +class Error(Exception): + """Base class for other exceptions""" + pass + + +class PortNumberError(Error): + """Raised when there are the wrong number of ports passed""" + pass + + +class TaskNameError(Error): + """Raised when the task name is not recognized""" + pass + + +class MouseSettingsError(Error): + """Raised when settings aren't defined for the mouse""" + pass + + +global_num_trials = 200 +global_max_time = 30 # minutes + + +def test(i): + print('test ' + str(i)) + + +def ssh(host, cmd, user, password, timeout=30, bg_run=False): + """SSH'es to a host using the supplied credentials and executes a command. + Throws an exception if the command doesn't return 0. + bgrun: run command in the background""" + + options = '-q -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -oPubkeyAuthentication=no' + if bg_run: + options += ' -f' + + ssh_cmd = 'ssh %s@%s %s \'%s\'' % (user, host, options, cmd) + print(ssh_cmd) + child = pexpect.spawnu(ssh_cmd, timeout=timeout) + child.expect(['[Pp]assword: ']) + child.sendline(password) + child.expect(pexpect.EOF) + child.close() + + +def scp(host, filename, destination, user, password, timeout=30, bg_run=False, recursive=False, cmd=False): + """Scp's to a host using the supplied credentials and executes a command. + Throws an exception if the command doesn't return 0. + bgrun: run command in the background""" + + options = '-q -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -oPubkeyAuthentication=no' + if recursive: + options += ' -r' + if bg_run: + options += ' -f' + scp_cmd = 'scp %s %s %s@%s:\'"%s"\'' % (options, filename, user, host, os.path.join(destination, filename)) + print(scp_cmd) + if cmd: + return scp_cmd + child = pexpect.spawnu(scp_cmd, timeout=timeout) # spawnu for Python 3 + child.expect(['[Pp]assword: ']) + child.sendline(password) + child.expect(pexpect.EOF) + child.close() + if child.exitstatus == 1: # if it doesn't work, try again with one fewer set of quotation marks + print('scp didn\'t work the first time so we are trying again with one fewer set of parentheses') + scp_cmd2 = 'scp %s %s %s@%s:"%s"' % (options, filename, user, host, os.path.join(destination, filename)) + print(scp_cmd2) + if cmd: + return scp_cmd2 + child = pexpect.spawnu(scp_cmd2, timeout=timeout) # spawnu for Python 3 + child.expect(['[Pp]assword: ']) + child.sendline(password) + child.expect(pexpect.EOF) + child.close() + if child.exitstatus == 1: + 'still didn\'t work :(' + elif child.exitstatus == 0: + print('worked!') + return child.exitstatus + + +def sync_stream(self): + pin_map = {'session': 25, + 'trial': 8, + 'head1': 7, + 'head2': 16, + 'lick1': 20, + 'lick2': 21, + 'sol1': 5, + 'sol2': 6, + 'led1': 6, + 'led2': 6 + } + for val in pin_map.values(): + GPIO.setup(val, GPIO.OUT) + GPIO.output(val, GPIO.LOW) + + +def perform(task): + try: + task.initialize() + task.structure(task) + except KeyboardInterrupt: + task.interrupted() + raise KeyboardInterrupt + task.end() + + +class Session: + def __init__(self, mouse): + self.mouse = mouse + # self.ip = '10.16.79.143' + # self.user = 'Elissa' + # self.ssh_path = 'GoogleDrive/Code/Python/behavior_code/data/' + self.mouse + # self.data_send_path = 'C:/Users/Elissa/' + self.ssh_path + '/' + self.ip = info_dict['desktop_ip'] + self.user = info_dict['desktop_user'] + self.ssh_path = os.path.join(info_dict['desktop_save_path'], self.mouse) + self.data_send_path = os.path.join(info_dict['desktop_user_root'], self.ssh_path) + # error with 0: re.compile('[Pp]assword: ') means you need to update the ip address. open command prompt and + # type ipconfig/all then press enter. Find the ip address starting with 10 and update it here + self.host_name = os.uname()[1] + self.password = info_dict['desktop_password'] + self.data_write_path = "/data/" + self.mouse + self.datetime = time.strftime("%Y-%m-%d_%H-%M-%S") + self.filename = "data_" + self.datetime + ".txt" + + self.halted = False + self.smooth_finish = False + GPIO.setmode(GPIO.BCM) + print(os.getcwd()) + os.system('sudo -u pi mkdir -p ' + os.getcwd() + self.data_write_path) + os.chdir(os.getcwd() + self.data_write_path) + os.system('sudo touch ' + self.filename) + os.system('sudo chmod o+w ' + self.filename) + self.f = open(self.filename, 'w') + self.start_time = None + self.sync_pins = {'LED1': 25, + 'LED2': 8, + 'trial': 7, + 'head1': 16, + 'head2': 20, + 'lick1': 21, + 'lick2': 5, + 'reward1': 6, + 'reward2': 13} + self.camera_pin = 10 + GPIO.setup(self.camera_pin, GPIO.OUT) + GPIO.output(self.camera_pin, GPIO.LOW) + + def start(self, task_list): + for val in self.sync_pins.values(): + GPIO.setup(val, GPIO.OUT) + GPIO.output(val, GPIO.LOW) + + info_fields = 'mouse,date,time,task,port1_info,port2_info,box' + data_fields = 'session_time,task,task_time,trial,trial_time,phase,port,value,key' + + self.f.write(info_fields + '\n') + for task in task_list: + info = [self.mouse, self.datetime[0:10], self.datetime[11:19], task.name] + for port in task.ports: + info = info + [str(port.dist_info)] + info.append(self.host_name) + info_string = ','.join(info) + self.f.write(info_string + '\n') + self.f.write('\n'.join(['# Data', data_fields, ''])) + + self.start_time = time.time() + self.log('nan,nan,nan,nan,setup,nan,1,session') + for task in task_list: + perform(task) + + def log(self, string): # Adds session time stamp to beginning of string and logs it + session_time = time.time() - self.start_time + new_line = str(session_time) + ',' + string + '\n' + # print(new_line) + self.f.write(new_line) + + def end(self): + self.log('nan,nan,nan,nan,setup,nan,0,session') + self.f.close() + os.system('sudo chmod o-w ' + self.filename) + mkdir_command = 'if not exist "%s" mkdir "%s"' % ( + self.ssh_path.replace('/', '\\'), self.ssh_path.replace('/', '\\')) + ssh(self.ip, mkdir_command, self.user, self.password) + res = scp(self.ip, self.filename, self.data_send_path, self.user, self.password) + if not res: + print('\nSuccessful file transfer to "%s"\nDeleting local file from pi.' % self.data_send_path) + os.remove(self.filename) + else: + print('connection back to desktop timed out') + GPIO.cleanup() + os.chdir(os.path.join(os.getcwd(), '..', '..')) + print('\nFile closed and clean up complete') + if self.halted: + print('Session stopped early') + elif self.smooth_finish: + print('Session ran smoothly to the end') + else: + print('Session ended due to an error:\n') + mixer.init() + tone = mixer.Sound('end_tone.wav') + tone.set_volume(.5) + mixer.Sound.play(tone, fade_ms=500) + time.sleep(5) + + +class Port: + def __init__(self, name, dist_info, duration=None): + if duration: + self.base_duration = duration + else: + durations_path = os.path.join(os.getcwd(), '..', '..', 'durations.pkl') + with open(durations_path, 'rb') as f: + durations = pickle.load(f) + self.base_duration = durations[name] + + pins = {2: [4, 27, 17, 9], + 1: [18, 24, 23, 11]} + self.name = name + [self.led_pin, self.ir_head_pin, self.ir_lick_pin, self.sol_pin] = pins[name] + # self.led_pin = led_pin + # self.ir_head_pin = ir_head_pin + # self.ir_lick_pin = ir_lick_pin + # self.sol_pin = sol_pin + GPIO.setup(self.led_pin, GPIO.OUT) + GPIO.output(self.led_pin, GPIO.LOW) + GPIO.setup(self.sol_pin, GPIO.OUT) + GPIO.output(self.sol_pin, GPIO.LOW) + GPIO.setup(self.ir_head_pin, GPIO.IN) + GPIO.setup(self.ir_lick_pin, GPIO.IN) + print(f'Port {self.name} head status: {GPIO.input(self.ir_head_pin)}') + print(f'Port {self.name} lick status: {GPIO.input(self.ir_lick_pin)}') + self.head_status = 0 + self.lick_status = 0 + self.sol = False + self.led = False + self.dist_info = dist_info + # self.distribution = distribution + # self.dist_args = dist_args + self.sol_opened_time = None + self.led_on_time = None + self.led_duration = 1 + print(str(name) + ' dur=' + str(self.base_duration)) + self.available = False + self.licked = True + self.led_stay = False + self.lick_start_time = time.time() + + def sol_on(self): + GPIO.output(self.sol_pin, GPIO.HIGH) + self.sol = True + self.sol_opened_time = time.time() + return time.time() + + def sol_off(self): + GPIO.output(self.sol_pin, GPIO.LOW) + self.sol = False + return time.time() + + def sol_cleanup(self): + if self.sol and self.sol_opened_time + self.base_duration < time.time(): + duration = time.time() - self.sol_opened_time + GPIO.output(self.sol_pin, GPIO.LOW) + self.sol = False + return duration + return False + + def led_cleanup(self): + if self.led and self.led_on_time + self.led_duration < time.time() and not self.led_stay: + duration = time.time() - self.led_on_time + GPIO.output(self.led_pin, GPIO.LOW) + self.led = False + return duration + return False + + def led_on(self): + GPIO.output(self.led_pin, GPIO.HIGH) + self.led_on_time = time.time() + self.led = True + print('led on') + return time.time() + + def led_off(self): + GPIO.output(self.led_pin, GPIO.LOW) + self.led = False + print('led off') + return time.time() + + def head_status_change(self): + change = GPIO.input(self.ir_head_pin) - self.head_status + self.head_status += change + return change + + def lick_status_change(self): + change = GPIO.input(self.ir_lick_pin) - self.lick_status + self.lick_status += change + if change == 1: + self.lick_start_time = time.time() + elif change == -1: + print(f'lick time: {time.time() - self.lick_start_time:.3f}') + return change + + +class Task: + def __init__(self, session, name='blank', structure=None, ports=None, limit='trials', + maximum=None, forgo=True, forced_trials=False): + print('Starting task: %s' % name) + self.structure = structure + self.port_dict = ports + self.ports = ports.values() + # self.ports = initialize_ports(ports, distributions) + self.session = session + self.name = name + self.limit = limit + if limit == 'trials': + self.num_trials = maximum if maximum else global_num_trials + self.max_time = None + elif limit == 'time': + self.max_time = maximum * 60 if maximum else global_max_time * 60 + self.num_trials = None + self.trial_number = 'nan' + self.phase = 'setup' + self.task_start_time = None + self.trial_start_time = None + self.last_video_start = None + self.reward_count = 0 + self.last_report = 0 + self.report_interval = 5 # Seconds + self.forgo = forgo + self.forced_trials = forced_trials + self.frame_rate = 25 # frames per second + self.frame_interval = 1 / self.frame_rate + self.last_frame = time.time() + self.cam_high = False + self.early_stop = False + + def initialize(self): + self.task_start_time = time.time() + for port in self.ports: + port.head_status = GPIO.input(port.ir_head_pin) + port.lick_status = GPIO.input(port.ir_lick_pin) + + def start(self): + self.task_start_time = time.time() + self.log('nan', 1, 'task') + self.trial_start_time = time.time() + self.trial_number = 0 + # self.check_video() + + def end(self): + self.phase = 'setup' + self.trial_number = 'nan' + self.sol_cleanup() + self.log('nan', 0, 'task') + + def interrupted(self): + self.log('nan', 0, 'task_interrupted') + + def check_number_of_ports(self, num): + if num != len(self.ports): + raise PortNumberError('\n This task needs %i ports, but %i were initialized.' % (num, len(self.ports))) + + def log(self, port_name, start, key): # Adds task name and timestamp, trial name and timestamp, and phase + task_timestamp = time.time() - self.task_start_time + if self.trial_number == 'nan': + trial_timestamp = 'nan' + else: + trial_timestamp = time.time() - self.trial_start_time + new_string = ','.join( + [self.name, str(task_timestamp), + str(self.trial_number), str(trial_timestamp), + str(self.phase), str(port_name), str(start), key]) + if key in ['trial']: + if start: + GPIO.output(self.session.sync_pins[key], GPIO.HIGH) + else: + GPIO.output(self.session.sync_pins[key], GPIO.LOW) + elif key in ['head', 'lick', 'LED', 'reward']: + if key == 'led': + print(self.session.sync_pins[key + str(port_name)]) + if start: + GPIO.output(self.session.sync_pins[key + str(port_name)], GPIO.HIGH) + if key == 'LED1': + GPIO.output(self.session.sync_pins['trial'], GPIO.LOW) + else: + GPIO.output(self.session.sync_pins[key + str(port_name)], GPIO.LOW) + + # elif key == 'sol': + # GPIO.output(self.session.sync_pins['sol' + str(port_name)], GPIO.HIGH) + # elif key == 'sol_duration': + # GPIO.output(self.session.sync_pins['sol' + str(port_name)], GPIO.LOW) + self.session.log(new_string) + + def start_trial(self, port_name='nan'): + self.trial_start_time = time.time() + self.log(port_name, 1, 'trial') + current_time = (time.time() - self.task_start_time) / 60 + print('trial %i start at %f' % (self.trial_number, current_time)) + # print('time: %f' % current_time) + + def end_trial(self, port_name='nan'): + self.log(port_name, 0, 'trial') + self.trial_number += 1 + + def next_trial(self, end_port_name='nan', start_port_name='nan'): + self.end_trial(port_name=end_port_name) + if self.condition(): + self.start_trial(port_name=start_port_name) + + def sol_cleanup(self): + for port in self.ports: + closed = port.sol_cleanup() + if closed: + self.log(port.name, 0, 'reward') + + def led_cleanup(self): + for port in self.ports: + if port.available == port.led: + if port.available: + port.led_off() + self.log(port.name, 0, 'LED') + else: + port.led_on() + self.log(port.name, 1, 'LED') + + def check_buttons(self, button_pad): + button_presses = button_pad.presses() + if 1 in button_presses[0:len(self.ports)]: + index = button_presses.index(1) + self.ports[index].sol_on() + time.sleep(self.ports[index].base_duration) + self.ports[index].sol_off() + print('manual reward delivered in port ' + str(index)) + self.log(self.ports[index].name, 1, 'manual_reward') + + def condition(self): + if self.limit == 'trials': + conditional = self.trial_number <= self.num_trials + elif self.limit == 'time': + conditional = time.time() - self.task_start_time < self.max_time + else: + conditional = False + if self.early_stop: + conditional = False + return conditional + + # def check_video(self): + # minutes_per = 5 + # if not self.last_video_start or time.time() - self.last_video_start > minutes_per * 60: + # print('video recording started') + # self.last_video_start = time.time() + # os.system( + # 'ssh Elissa@10.194.169.93 \'Anaconda3\\Scripts\\activate open_cv && cd PycharmProjects\\open_cv_test && curl -X POST -H "Content-Type: application/json" -d "{\\"mouse\\":\\"' + self.session.mouse + '\\"}" localhost:8000/check_start\'') + # self.log('nan', 1, 'video') + + def check_time(self): + if (time.time() - self.last_report) > self.report_interval: + task_time = time.time() - self.task_start_time + print('%i rewards in %s' % ( + int(self.reward_count), str(datetime.timedelta(seconds=task_time))[2:7])) + self.last_report = time.time() + if (time.time() - self.last_frame) > self.frame_interval: # If the square wave period has passed, go high + GPIO.output(self.session.camera_pin, GPIO.HIGH) + self.last_frame = time.time() + self.cam_high = True + self.log('nan', 1, 'camera') + # If half the period has passed and it's high, go low + if (time.time() - self.last_frame) > self.frame_interval / 2 and self.cam_high: + GPIO.output(self.session.camera_pin, GPIO.LOW) + self.cam_high = False + self.log('nan', 0, 'camera') + + +class Expander: + def __init__(self, input_pins_a=None, input_pins_b=None): + self.bus = smbus.SMBus(1) + self.DEVICE = 0x20 # Device address (A0-A2) + self.SETUP_REGISTER = [0x00, 0x01] # Pin direction register + self.INPUT_REGISTER = [0x12, 0x13] # Register for inputs on A + self.OUTPUT_REGISTER = [0x14, 0x15] # Register for inputs on A + self.output_pin_status = np.zeros([2, 8]) + self.input_pin_status = np.zeros([2, 8]) + self.input_pins = [input_pins_a, input_pins_b] + for side in [0, 1]: + self.bus.write_byte_data(self.DEVICE, self.SETUP_REGISTER[side], self.to_hex(self.input_pins[side])) + + def on(self, side, pin): + self.output_pin_status[side, pin] = 1 + self.refresh_pins() + + def off(self, side, pin): + self.output_pin_status[side, pin] = 0 + self.refresh_pins() + + def refresh_pins(self): + for side in [0, 1]: + self.bus.write_byte_data(self.DEVICE, self.OUTPUT_REGISTER[side], + self.to_hex(np.where(self.output_pin_status[side]))) + input_status = self.bus.read_byte_data(self.DEVICE, self.INPUT_REGISTER[side]) + print(input_status) + + def to_hex(self, num_list=None): + if not num_list: + return 0 + else: + return np.sum([2 ** num for num in num_list]) + + +if __name__ == '__main__': + expander = Expander() + expander.on(0, [2, 3, 6]) + expander.output_pin_status[1, :-3] = 1 + expander.refresh_pins() diff --git a/behavior_code/stand_alone/tasks.py b/behavior_code/stand_alone/tasks.py new file mode 100644 index 0000000..29a0779 --- /dev/null +++ b/behavior_code/stand_alone/tasks.py @@ -0,0 +1,808 @@ +import random +import time +from timescapes import * +import tkinter as tk +import tkinter.font as font +from threading import Thread +import datetime + + +def give_up_task(task_shell, step_size=.1): + num_ports = 2 # The number of ports used in the task, do not change + task_shell.check_number_of_ports(num_ports) + + current_port = None + previous_reward_check = 0 + licked = True + + # This loops until all the trials are complete + while task_shell.condition(): + task_shell.sol_cleanup() + task_shell.check_time() + + # This controls the task flow as the mouse moves in and out of ports + for port in task_shell.ports: + for change, event in zip([port.head_status_change(), port.lick_status_change()], ['head', 'lick']): + if change == 1: + if event == 'head': + task_shell.phase = 'consume' + print(str(time.time() - task_shell.task_start_time) + ' port ' + str(port.name) + ' entry') + if not current_port: + task_shell.trial_number = 1 + task_shell.start_trial() + elif port.name != current_port: + task_shell.next_trial(end_port_name=current_port, start_port_name=port.name) + if not task_shell.condition(): + licked = False + break + previous_reward_check = 0 + licked = True + current_port = port.name + if event == 'lick': + licked = True + task_shell.log(port.name, 1, event) + elif change == -1: + if event == 'head': + task_shell.phase = 'transit' + task_shell.log(port.name, 0, event) + + # This controls reward delivery + for port in task_shell.ports: + if port.head_status == 1 and licked: + trial_time = time.time() - task_shell.trial_start_time + if trial_time > previous_reward_check + step_size: + previous_reward_check = trial_time + density_function = port.dist_info['distribution'] + prob = density_function(trial_time, port.dist_info['cumulative'], + port.dist_info['peak_time']) * step_size + task_shell.log(port.name, prob, 'probability') + print_string = 'port ' + str(port.name) + ' P(reward) = ' + str(prob) + if prob > random.random(): + port.sol_on() + licked = False + task_shell.log(port.name, 1, 'reward') + print(print_string + ' (rewarded)') + task_shell.reward_count += 1 + + +def give_up_forgo_task(task_shell, step_size=.1): + num_ports = 2 # The number of ports used in the task, do not change + task_shell.check_number_of_ports(num_ports) + + current_port = None + previous_reward_check = 0 + licked = True + fixed_port_rewarded = False + + # This loops until all the trials are complete + while task_shell.condition(): + task_shell.sol_cleanup() + task_shell.check_time() + + # This controls the task flow as the mouse moves in and out of ports + for port in task_shell.ports: + for change, event in zip([port.head_status_change(), port.lick_status_change()], ['head', 'lick']): + if change == 1: + if event == 'head': + task_shell.phase = 'consume' + print(str(time.time() - task_shell.task_start_time) + ' port ' + str(port.name) + ' entry') + if not current_port: + task_shell.trial_number = 1 + task_shell.start_trial(port_name=port.name) + elif port.name != current_port: + task_shell.next_trial(end_port_name=current_port, start_port_name=port.name) + if not task_shell.condition(): + licked = False + break + previous_reward_check = 0 + licked = True + fixed_port_rewarded = False + current_port = port.name + if event == 'lick': + licked = True + # print('lick start') + task_shell.log(port.name, 1, event) + elif change == -1: + if event == 'head': + task_shell.phase = 'transit' + # if event == 'lick': + # print('lick stop') + task_shell.log(port.name, 0, event) + + # This controls reward delivery + for port in task_shell.ports: + if port.name == current_port: + if port.distribution == fixed_single: + if port.head_status == 1: + phase = int((time.time() - task_shell.task_start_time) // + (task_shell.max_time / len(port.dist_args))) + if not task_shell.condition(): + break + wait_time = port.dist_args[phase] + trial_time = time.time() - task_shell.trial_start_time + if trial_time > wait_time and not fixed_port_rewarded: + port.sol_on() + task_shell.log(port.name, 1, 'reward') + print('trial: %i port: %i fixed reward at %f seconds' % ( + task_shell.trial_number, port.name, trial_time)) + fixed_port_rewarded = True + task_shell.reward_count += 1 + elif port.distribution == exp_decreasing: + if port.head_status == 1 and licked: + trial_time = time.time() - task_shell.trial_start_time + if trial_time > previous_reward_check + step_size: + previous_reward_check = trial_time + prob = port.get_prob(trial_time) * step_size + task_shell.log(port.name, prob, 'probability') + print_string = 'port ' + str(port.name) + ' P(reward) = ' + str(prob) + if prob > random.random(): + port.sol_on() + licked = False + task_shell.log(port.name, 1, 'reward') + print(print_string + ' (rewarded)') + task_shell.reward_count += 1 + + +# def training_cued_forgo_task(task_shell, step_size=.1): +# num_ports = 2 # The number of ports used in the task, do not change +# task_shell.check_number_of_ports(num_ports) +# # travel_time_limit = 1 +# +# start = False +# background_start_time = None +# background_time = 0 +# background_rewards = 0 +# exp_start_time = None +# exp_available = False +# exp_taken = False +# bin_num = 0 +# background_available = True +# +# # This loops until all the trials are complete +# while task_shell.condition(): +# task_shell.sol_cleanup() +# task_shell.led_cleanup() +# task_shell.check_time() # print out the current time and number of trials and rewards +# +# # This controls the task flow as the mouse moves in and out of ports +# for port in task_shell.ports: +# for change, event in zip([port.head_status_change(), port.lick_status_change()], ['head', 'lick']): +# if change == 1: # beam break +# task_shell.log(port.name, 1, event) # Log the event +# if event == 'head': +# if port.dist_info['distribution'] == exp_decreasing and exp_available: +# exp_available = False +# exp_taken = True +# exp_start_time = time.time() +# bin_num = 0 +# print('took exp option') +# elif port.dist_info['distribution'] == 'background': +# background_start_time = time.time() +# if not start: +# task_shell.trial_number = 1 +# task_shell.start_trial(port_name=port.name) +# start = True +# trial_start_time = time.time() +# print('starting task...') +# if exp_taken: +# exp_taken = False +# background_available = True +# background_start_time = time.time() +# trial_start_time = time.time() +# background_time = 0 +# background_rewards = 0 +# print('returned to background') +# elif event == 'lick': +# port.licked = True +# elif change == -1: # beam un-break +# task_shell.log(port.name, 0, event) # Log the event +# if port.dist_info['distribution'] == 'background' and event == 'head': +# background_time += time.time() - background_start_time +# +# # This controls reward delivery +# if start: +# # if exp_available and (time.time() - task_shell.trial_start_time) > travel_time_limit: +# # exp_available = False +# # print('skipped exp option') +# for port in task_shell.ports: +# if port.dist_info['distribution'] == 'background' and port.head_status == 1 and background_available: +# block = int((time.time() - task_shell.task_start_time) // +# (task_shell.max_time / len(port.dist_info['rates']))) +# if block >= len(port.dist_info['rates']): +# block = len(port.dist_info['rates']) - 1 +# interval = 1 / port.dist_info['rates'][block] +# if (background_time + time.time() - background_start_time) // interval > background_rewards: +# if port.licked: +# port.sol_on() +# task_shell.log(port.name, 1, 'reward') +# task_shell.reward_count += 1 +# print( +# f'background reward delivered: {background_time + time.time() - background_start_time} {1}') +# port.licked = False +# else: +# print( +# f'background reward missed: {background_time + time.time() - background_start_time}') +# task_shell.log(port.name, 1, 'missed_reward') +# background_rewards += 1 +# if background_time + time.time() - background_start_time > port.dist_info['duration']: +# print('exp option available') +# port.led_on() +# task_shell.log(port.name, 1, 'LED') +# exp_available = True +# background_time = 0 +# background_rewards = 0 +# background_start_time = time.time() +# trial_start_time = time.time() +# background_available = False +# task_shell.next_trial(end_port_name=port.name, start_port_name=port.name) +# if port.dist_info['distribution'] == exp_decreasing: +# if port.head_status == 1 and port.licked and exp_taken: +# if (time.time() - exp_start_time) // step_size > bin_num: +# bin_num += 1 +# density_function = port.dist_info['distribution'] +# prob = density_function(time.time() - exp_start_time, +# cumulative=port.dist_info['cumulative'], +# starting=port.dist_info['starting_probability']) * step_size +# task_shell.log(port.name, prob, 'probability') +# # print_string = 'port ' + str(port.name) + ' P(reward) = ' + str(prob) +# print_string = f'port {port.name} P(reward) = {prob}' +# print(random.random()) +# if prob > random.random(): +# port.sol_on() +# port.licked = False +# task_shell.log(port.name, 1, 'reward') +# print(print_string + ' (rewarded)') +# task_shell.reward_count += 1 + + +def check_rate(task_shell, port): + block = int((time.time() - task_shell.task_start_time) // + (task_shell.max_time / len(port.dist_info['rates']))) + block = len(port.dist_info['rates']) - 1 if block >= len(port.dist_info['rates']) else block + return port.dist_info['rates'][block] + + +def cued_forgo_task(task_shell, step_size=.1): + t1 = Thread(target=stop_button, args=[task_shell]) + t1.start() + + forgo = task_shell.forgo + forced_trials = task_shell.forced_trials + num_ports = 2 # The number of ports used in the task, do not change + task_shell.check_number_of_ports(num_ports) + travel_time_limit = 5 + + start = False + background_start_time = time.time() + background_time = 0 + background_rewards = 0 + exp_start_time = None + trial_start_time = None + exp_available = False + exp_taken = False + bin_num = 0 + background_available = True + forced = False + choice = 'free' + current_time = time.time() + cycle_count = 10000 + cycle_time = np.zeros([cycle_count]) + cycle_num = 0 + cycle_timer = time.time() + + for port in task_shell.ports: + if port.dist_info['distribution'] == 'background': + task_shell.phase = check_rate(task_shell, port) + rates = np.unique(port.dist_info['rates']) + rates.sort() + + # This loops until all the trials are complete + while task_shell.condition(): + for port in task_shell.ports: + if port.dist_info['distribution'] == exp_decreasing: + port.available = exp_available or exp_taken + elif port.dist_info['distribution'] == 'background': + port.available = background_available or exp_taken + + task_shell.sol_cleanup() + task_shell.led_cleanup() + task_shell.check_time() # print out the current time and number of trials and rewards + + # This controls the task flow as the mouse moves in and out of ports + for port in task_shell.ports: + for change, event in zip([port.head_status_change(), port.lick_status_change()], ['head', 'lick']): + if change == 1: # beam break + if start: + task_shell.log(port.name, 1, event) # Log the event + if event == 'head': + if port.dist_info['distribution'] == exp_decreasing and exp_available: + print(choice) + task_shell.log(port.name, 1, choice) + exp_available = False + exp_taken = True + forced = False + exp_start_time = time.time() + bin_num = 0 + task_shell.port_dict['background'].led_stay = False + task_shell.led_cleanup() + elif port.dist_info['distribution'] == 'background': + background_start_time = time.time() + if not start: + task_shell.start() + task_shell.trial_number = 1 + task_shell.start_trial(port_name=port.name) + start = True + trial_start_time = time.time() + task_shell.log(port.name, 1, event) # Log the event + print('starting task...') + if exp_taken: + task_shell.next_trial(end_port_name=1, start_port_name=2) + background_available = True + exp_taken = False + background_start_time = time.time() + trial_start_time = time.time() + background_time = 0 + background_rewards = 0 + task_shell.phase = check_rate(task_shell, port) + print('returned to background') + elif event == 'lick': + port.licked = True + elif change == -1: # beam un-break + if start: + task_shell.log(port.name, 0, event) # Log the event + if port.dist_info['distribution'] == 'background' and event == 'head': + background_time += time.time() - background_start_time + + # This controls reward delivery + if start: + for port in task_shell.ports: + if port.dist_info['distribution'] == 'background' and port.head_status == 1 and background_available: + current_time = time.time() + # block = int((current_time - task_shell.task_start_time) // + # (task_shell.max_time / len(port.dist_info['rates']))) + # if block >= len(port.dist_info['rates']): + # block = len(port.dist_info['rates']) - 1 + interval = 1 / task_shell.phase + if (background_time + current_time - background_start_time) // interval > background_rewards: + if port.licked or port.lick_status: + port.sol_on() + task_shell.log(port.name, 1, 'reward') + task_shell.reward_count += 1 + print( + f'background reward delivered: {background_time + time.time() - background_start_time}' + f' / {time.time() - trial_start_time}') + if not port.licked and port.lick_status: + print('Reward delivered because lick has been on since the previous reward.') + port.licked = False + else: + print(f'background reward missed: {background_time + time.time() - background_start_time}' + f' / {time.time() - trial_start_time}') + task_shell.log(port.name, 1, 'missed_reward') + background_rewards += 1 + if background_time + current_time - background_start_time > port.dist_info['duration']: + # port.led_on() + # task_shell.log(port.name, 1, 'LED') + background_time = 0 + background_rewards = 0 + background_start_time = time.time() + trial_start_time = time.time() + if forgo: + task_shell.phase = check_rate(task_shell, port) + exp_available = True + if forced: + task_shell.log(port.name, 1, 'forgo') + task_shell.next_trial(end_port_name=port.name, start_port_name=port.name) + if forced_trials: + if forced: + background_available = False + task_shell.log(port.name, 1, 'forced_switch') + print('forced switch') + # port.led_stay = True + choice = 'forced' + else: + task_shell.log(port.name, 1, 'free_choice') + print('free choice') + forced = True + choice = 'free' + if not forgo: + if task_shell.phase == rates[1] or forced: + print('exp option available') + exp_available = True + background_available = False + task_shell.log(port.name, 1, 'forced_switch') + print('forced switch') + choice = 'forced' + else: + forced = True + + if port.dist_info['distribution'] == exp_decreasing: + if exp_taken and (time.time() - exp_start_time) // step_size > bin_num: + bin_num += 1 + if port.head_status == 1 and (port.licked or port.lick_status): + density_function = port.dist_info['distribution'] + prob = density_function(time.time() - exp_start_time, + cumulative=port.dist_info['cumulative'], + starting=port.dist_info['starting_probability']) * step_size + task_shell.log(port.name, prob, 'probability') + # print_string = 'port ' + str(port.name) + ' P(reward) = ' + str(prob) + print_string = f'port {port.name} P(reward) = {prob}' + # print(random.random()) + if prob > random.random(): + port.sol_on() + port.licked = False + task_shell.log(port.name, 1, 'reward') + print(print_string + ' (rewarded)') + task_shell.reward_count += 1 + # cycle_time[cycle_num] = time.time() - cycle_timer + # cycle_num += 1 + # if cycle_num == cycle_count: + # print( + # f' cycle mean: {np.mean(cycle_time)}, cycle max: {np.max(cycle_time)}, cycle min: {np.min(cycle_time)}') + # cycle_num = 0 + # cycle_timer = time.time() + t1.join() + + +def single_reward_task(task_shell, step_size=.1): + t1 = Thread(target=stop_button, args=[task_shell]) + t1.start() + + num_ports = 2 # The number of ports used in the task, do not change + task_shell.check_number_of_ports(num_ports) + + started = False + background_start_time = time.time() + background_time = 0 + background_rewards = 0 + reward_time = 0 + exp_start_time = None + trial_start_time = None + exp_available = False + exp_taken = False + exp_reward = False + exp_reward_number = 0 + exp_reward_target = 8 + exp_last_reward = time.time() + background_available = True + background_taken = False + choice = 'free' + starting = 0 + cumulative = 0 + + for port in task_shell.ports: + if port.dist_info['distribution'] == 'background': + task_shell.phase = check_rate(task_shell, port) + rates = np.unique(port.dist_info['rates']) + rates.sort() + + # This loops until all the trials are complete + while task_shell.condition(): + for port in task_shell.ports: + if port.dist_info['distribution'] == exp_decreasing: + port.available = exp_available or exp_taken or exp_reward + starting = port.dist_info['starting_probability'] + cumulative = port.dist_info['cumulative'] + elif port.dist_info['distribution'] == 'background': + port.available = background_available or background_taken + + task_shell.sol_cleanup() + task_shell.led_cleanup() + task_shell.check_time() # print out the current time and number of trials and rewards + + # This controls the task flow as the mouse moves in and out of ports + for port in task_shell.ports: + for change, event in zip([port.head_status_change(), port.lick_status_change()], ['head', 'lick']): + if change == 1: # beam break + if started: + task_shell.log(port.name, 1, event) # Log the event + if event == 'head': + if port.dist_info['distribution'] == exp_decreasing and exp_available: + print(choice) + task_shell.log(port.name, 1, choice) + exp_available = False + exp_taken = True + background_available = True + exp_start_time = time.time() + task_shell.port_dict['background'].led_stay = False + task_shell.led_cleanup() + elif port.dist_info['distribution'] == 'background': + background_start_time = time.time() + if not started: + task_shell.start() + task_shell.trial_number = 1 + task_shell.start_trial(port_name=port.name) + started = True + background_taken = True + background_available = False + r = np.random.random() + reward_time = exp_decreasing(r, cumulative=cumulative, starting=starting, draw=True) + + trial_start_time = time.time() + task_shell.log(port.name, 1, event) # Log the event + print('starting task...') + task_shell.log(port.name, reward_time, 'reward_time') # Log the event + + if background_available: + task_shell.next_trial(end_port_name=1, start_port_name=2) + background_available = False + background_taken = True + exp_taken = False + exp_reward = False + exp_reward_number = 0 + r = np.random.random() + reward_time = exp_decreasing(r, cumulative=cumulative, starting=starting, draw=True) + + task_shell.log(port.name, reward_time, 'reward_time') # Log the event + background_start_time = time.time() + trial_start_time = time.time() + background_time = 0 + background_rewards = 0 + task_shell.phase = check_rate(task_shell, port) + print('returned to background') + elif event == 'lick': + port.licked = True + elif change == -1: # beam un-break + if started: + task_shell.log(port.name, 0, event) # Log the event + if port.dist_info['distribution'] == 'background' and event == 'head': + background_time += time.time() - background_start_time + + # This controls reward delivery + if started: + for port in task_shell.ports: + if port.dist_info['distribution'] == 'background' and port.head_status == 1 and background_taken: + current_time = time.time() + interval = 1 / task_shell.phase + num_rewards = 4 + if (background_time + current_time - background_start_time) // interval > background_rewards: + background_rewards += 1 + if port.licked or port.lick_status: + port.sol_on() + task_shell.log(port.name, 1, 'reward') + task_shell.reward_count += 1 + print( + f'background reward delivered: {background_time + time.time() - background_start_time}' + f' / {time.time() - trial_start_time}') + if not port.licked and port.lick_status: + print('Reward delivered because lick has been on since the previous reward.') + port.licked = False + else: + print(f'background reward missed: {background_time + time.time() - background_start_time}' + f' / {time.time() - trial_start_time}') + task_shell.log(port.name, 1, 'missed_reward') + if background_rewards >= num_rewards: + print('exp option available') + background_time = 0 + background_rewards = 0 + background_start_time = time.time() + exp_available = True + background_taken = False + task_shell.log(port.name, 1, 'forced_switch') + print('forced switch') + choice = 'forced' + + if port.dist_info['distribution'] == exp_decreasing: + if exp_taken and ( + time.time() - exp_start_time) > reward_time and port.head_status == 1 and port.licked: + exp_taken = False + exp_reward = True + task_shell.log(port.name, 1, 'reward_initiate') + if exp_reward and port.head_status == 1 and (time.time() - exp_last_reward) > .1: + port.sol_on() + task_shell.log(port.name, 1, 'reward') + print('reward delivered at ' + str(time.time() - exp_start_time) + ' seconds') + task_shell.reward_count += 1 + exp_last_reward = time.time() + exp_reward_number += 1 + if exp_reward_number >= exp_reward_target: + exp_reward = False + t1.join() + + +def check_block(task_shell, port): + block = int((time.time() - task_shell.task_start_time) // + (task_shell.max_time / len(port.dist_info['blocks']))) + block = len(port.dist_info['blocks']) - 1 if block >= len(port.dist_info['blocks']) else block + return port.dist_info['blocks'][block] + + +def check_params(phase, port): + cumulative = port.dist_info['cumulative'] + starting = port.dist_info['starting'] + hi = port.dist_info['hi'] + lo = port.dist_info['lo'] + values = { + 'lo_lo': [lo, lo], + 'lo_hi': [lo, hi], + 'hi_lo': [hi, lo], + 'hi_hi': [hi, hi] + } + multiplier = values[phase][port.name - 1] + return cumulative * multiplier, starting * multiplier + + +def give_up_blocked_task(task_shell, step_size=.1): + num_ports = 2 # The number of ports used in the task, do not change + task_shell.check_number_of_ports(num_ports) + + current_port = None + previous_reward_check = 0 + licked = True + + # This loops until all the trials are complete + while task_shell.condition(): + task_shell.sol_cleanup() + task_shell.check_time() + + # This controls the task flow as the mouse moves in and out of ports + for port in task_shell.ports: + for change, event in zip([port.head_status_change(), port.lick_status_change()], ['head', 'lick']): + if change == 1: + if event == 'head': + print(str(time.time() - task_shell.task_start_time) + ' port ' + str(port.name) + ' entry') + if not current_port: + task_shell.trial_number = 1 + task_shell.phase = check_block(task_shell, port) + task_shell.start_trial(port_name=port.name) + elif port.name != current_port: # check only when entering a new trial + task_shell.end_trial(port_name=current_port) + new_phase = check_block(task_shell, port) + if task_shell.phase != new_phase: + print(f'{task_shell.phase} -> {new_phase}') + task_shell.phase = new_phase + if task_shell.condition(): + task_shell.start_trial(port_name=port.name) + if not task_shell.condition(): + licked = False + break + previous_reward_check = 0 + licked = True + current_port = port.name + if event == 'lick': + licked = True + task_shell.log(port.name, 1, event) + elif change == -1: + task_shell.log(port.name, 0, event) + + # This controls reward delivery + for port in task_shell.ports: + if port.head_status == 1 and licked: + trial_time = time.time() - task_shell.trial_start_time + if trial_time > previous_reward_check + step_size: + previous_reward_check = trial_time + density_function = port.dist_info['distribution'] + cumulative, starting = check_params(task_shell.phase, port) + prob = density_function(trial_time, cumulative=cumulative, starting=starting) * step_size + task_shell.log(port.name, prob, 'probability') + print_string = 'port ' + str(port.name) + ' P(reward) = ' + str(prob) + if prob > random.random(): + port.sol_on() + licked = False + task_shell.log(port.name, 1, 'reward') + print(print_string + ' (rewarded)') + task_shell.reward_count += 1 + + +def generic_task(task_shell, step_size=.1): + num_ports = 2 # The number of ports used in the task, do not change + task_shell.check_number_of_ports(num_ports) + + current_port = None + + # This loops until all the trials are complete + while task_shell.condition(): + task_shell.sol_cleanup( + [port.base_duration for port in task_shell.ports]) # close open solenoids if their duration is passed + task_shell.check_time() # print out the current time and number of trials and rewards + + # This controls the task flow as the mouse moves in and out of ports + for port in task_shell.ports: + for change, event in zip([port.head_status_change(), port.lick_status_change()], ['head', 'lick']): + if change == 1: # beam break + if event == 'head': # head entry + if not current_port: # Initialize the first trial + task_shell.trial_number = 1 + task_shell.start_trial(port_name=port.name) + elif port.name != current_port: # If port switch, reset for new trial + task_shell.next_trial(end_port_name=current_port, start_port_name=port.name) + current_port = port.name # set the current port + if event == 'lick': # lick start + pass + task_shell.log(port.name, 1, event) # Log the event + elif change == -1: # beam un-break + if event == 'head': # head exit + pass + if event == 'lick': # lick stop + pass + task_shell.log(port.name, 0, event) # Log the event + + reward_condition = False # Place holder for writing in a condition using some distribution + + # This controls reward delivery + for port in task_shell.ports: + if port.name == current_port: + if port.head_status == 1: + if reward_condition: + port.sol_on() + task_shell.log(port.name, 1, 'reward') + + +def example_task(session_manager): + state_variable1 = True + state_variable2 = 0 + start_time = time.time() # Grabs the time at the beginning of the task + max_time = 20 * 60 # Session capped at 20 minutes + max_trials = 100 # Session capped at 100 trials + trial_number = 0 + while trial_number < max_trials and time.time() - start_time < max_time: + session_manager.clean_up_function1() # Something to check on every loop. (ex. solenoid duration) + sensor_change1 = session_manager.check_sensor1() # Returns 1 for sensor break, -1 for unbreak, 0 for no change + sensor_change2 = session_manager.check_sensor2() + + if sensor_change1 == 1: + state_variable1 = True + session_manager.log(f'{time.time() - start_time}, {sensor_change1}, sensor1') # log the sensor change + elif sensor_change1 == -1: + session_manager.log(f'{time.time() - start_time}, {sensor_change1}, sensor1') # log the sensor change + + if sensor_change2 == 1: + state_variable2 += 1 + session_manager.log(f'{time.time() - start_time}, {sensor_change2}, sensor2') # log the sensor change + elif sensor_change2 == -1: + session_manager.log(f'{time.time() - start_time}, {sensor_change2}, sensor2') # log the sensor change + + if state_variable1 and state_variable2 > 5: + trial_number += 1 + session_manager.log(f'{time.time() - start_time}, {1}, trial') # log the trial start + state_variable2 = 0 + session_manager.solenoid.on() + session_manager.log(f'{time.time() - start_time}, {1}, reward') # log the reward delivery + + +def stop_button(task_shell): + app = StopButton(task_shell) + + +class StopButton: + def __init__(self, task_shell): + self.root = tk.Tk() + self.root.geometry("600x500+300+300") + self.root.title(task_shell.session.mouse) + self.task_shell = task_shell + self.button = tk.Button( + master=self.root, + text='Stop Task', + width=50, + height=10, + bg="white", + fg="black", + font=("Arial", 25), + command=self.stop) + self.name_label = tk.Label(self.root, text=f'{task_shell.session.mouse}', + width=50, height=2, font=("Arial", 40)) + self.label = tk.Label(self.root, text=f'Time Remaining: ' + f'{str(datetime.timedelta(seconds=task_shell.max_time))[2:7]}\n' + f'Rewards Collected: 0', + width=50, height=4, font=("Arial", 25)) + self.name_label.pack() + self.label.pack() + self.button.pack() + self._job = self.root.after(1000, self.check_continue) + self.root.mainloop() + + def stop(self): + self.task_shell.early_stop = True + self.task_shell.session.halted = True + + def check_continue(self): + if self.task_shell.limit == 'time': + remaining = self.task_shell.max_time - (time.time() - self.task_shell.task_start_time) + self.label['text'] = f'Time Remaining: {str(datetime.timedelta(seconds=remaining))[2:7]}\n' \ + f'Rewards Collected: {self.task_shell.reward_count}' + if not self.task_shell.condition(): + if self._job is not None: + self.root.after_cancel(self._job) + self._job = None + self.root.destroy() + else: + self._job = self.root.after(1000, self.check_continue) diff --git a/behavior_code/stand_alone/timescapes.py b/behavior_code/stand_alone/timescapes.py new file mode 100644 index 0000000..31b3082 --- /dev/null +++ b/behavior_code/stand_alone/timescapes.py @@ -0,0 +1,42 @@ +import numpy as np +# import matplotlib.pyplot as plt + + +# This is the new one. It's a decreasing exponential multiplied by a straight line. +def lin_over_ex(x, cumulative=10, x_peak=1): + b = 1 / x_peak + a = b ** 2 * cumulative + density = (a * x) / np.exp(b * x) + return density + + +# This is the simple exponential decreasing +def exp_decreasing(x, cumulative=10., starting=1., draw=False): + a = starting + b = a / cumulative + if not draw: + density = a / np.exp(b * x) + return density + chosen_time = np.log(cumulative / (cumulative - x)) / b + return chosen_time + + +def fixed_single(x, wait_time): + if x > wait_time: + reward = 1 + else: + reward = 0 + return reward + + +# if __name__ == '__main__': +# samples = 100000 +# x = np.random.random(samples) +# c = 0.5994974874371859 +# times = exp_decreasing(x, cumulative=c, starting=0.1301005025125628, draw=True) +# plt.hist(times, 50) +# x2 = np.linspace(0, 30) +# probs = exp_decreasing(x2, cumulative=c, starting=0.1301005025125628, draw=False) +# plt.plot(x2, probs*samples) +# plt.title(f'{np.sum(~np.isnan(times)) / samples * 100}% of samples give reward ({c * 100:.2f}% expected)') +# plt.show() diff --git a/behavior_code/stand_alone/user_info_generic.py b/behavior_code/stand_alone/user_info_generic.py new file mode 100644 index 0000000..d83eaca --- /dev/null +++ b/behavior_code/stand_alone/user_info_generic.py @@ -0,0 +1,78 @@ +import os + +""" +Copy this file to a new one with the name 'user_settings.py' in the same folder location as this one. Replace all BOLD_CASE +variables with your own. GOOD LUCK! +""" + + +def get_user_info(): + info_dict = { + 'initials': 'USER_INITIALS', + 'mouse_buttons': [['MOUSE_NAME', 'MOUSE_NAME', 'MOUSE_NAME'], + ['MOUSE_NAME', 'MOUSE_NAME', 'MOUSE_NAME'], + ['MOUSE_NAME', 'MOUSE_NAME', 'MOUSE_NAME']], + 'button_colors': [[1,1,1], + [2,2,2], + [3,3,3]], + 'mouse_assignments': { + 'MOUSE_NAME': 'TASK_NAME', + 'testmouse': 'TASK_NAME', + }, + 'mouse_colors': { + 'MOUSE_NAME': 1, + 'testmouse': 1, + }, + + 'desktop_ip': 'PUT_IP_ADDRESS_HERE', + 'desktop_user': 'PUT_USERNAME_HERE', + 'desktop_password': 'PUT_PASSWORD_HERE', + 'desktop_user_root': os.path.join('C:/', 'PATH', 'TO', 'ROOT'), + 'desktop_save_path': os.path.join('PATH', 'TO', 'DATA', 'FOLDER'), + } + return info_dict + +import os + + +def get_user_info(): + info_dict = { + 'initials': 'ES', + 'mouse_buttons': [['ES041', 'ES042', 'ES037'], + ['ES043', 'ES044', 'ES039'], + ['ES045', 'ES046', 'ES047']], + 'mouse_assignments': { + 'ES036': 'single_reward', + 'ES037': 'single_reward', + 'ES039': 'cued_forgo', + 'ES040': 'cued_forgo', + 'ES041': 'single_reward', + 'ES042': 'single_reward', + 'ES043': 'single_reward', + 'ES044': 'single_reward', + 'ES045': 'cued_forgo', + 'ES046': 'cued_forgo', + 'ES047': 'cued_forgo', + 'testmouse': 'cued_forgo', + }, + 'mouse_colors': { + 'ES036': 1, + 'ES037': 1, + 'ES039': 2, + 'ES040': 2, + 'ES041': 3, + 'ES042': 3, + 'ES043': 3, + 'ES044': 3, + 'ES045': 4, + 'ES046': 4, + 'ES047': 4, + 'testmouse': 1, + }, + 'desktop_ip': '10.16.79.143', + 'desktop_user': 'Elissa', + 'desktop_password': 'shuler', + 'desktop_user_root': os.path.join('C:/', 'Users', 'Elissa'), + 'desktop_save_path': os.path.join('GoogleDrive', 'Code', 'Python', 'behavior_code', 'data'), + } + return info_dict diff --git a/behavior_code/stand_alone/user_settings.py b/behavior_code/stand_alone/user_settings.py new file mode 100644 index 0000000..d1814ba --- /dev/null +++ b/behavior_code/stand_alone/user_settings.py @@ -0,0 +1,49 @@ +import os + + +def get_user_info(): + info_dict = { + 'initials': 'SZ', + 'mouse_buttons': [['SZ050', 'SZ051', 'SZ052'], + ['SZ053', 'SZ054', 'SZ055'], + ['SZ056', 'SZ057', 'SZ058'], + ['SZ059', 'cf_testmouse', 'sr_testmouse']], + 'button_colors': [[12, 12, 12], + [12, 12, 2], + [2, 2, 2], + [2, 3, 3]], + 'mouse_assignments': { + 'SZ050': 'cued_forgo', + 'SZ051': 'cued_forgo', + 'SZ052': 'cued_forgo', + 'SZ053': 'single_reward', + 'SZ054': 'single_reward', + 'SZ055': 'cued_forgo', + 'SZ056': 'cued_forgo', + 'SZ057': 'cued_forgo', + 'SZ058': 'single_reward', + 'SZ059': 'single_reward', + 'sr_testmouse': 'single_reward', + 'cf_testmouse': 'cued_forgo' + }, + 'mouse_colors': { + 'SZ050': 12, + 'SZ051': 12, + 'SZ052': 12, + 'SZ053': 12, + 'SZ054': 12, + 'SZ055': 2, + 'SZ056': 2, + 'SZ057': 2, + 'SZ058': 2, + 'SZ059': 2, + 'sr_testmouse': 3, + 'cf_testmouse': 3 + }, + 'desktop_ip': '10.16.80.130', + 'desktop_user': 'Shichen', + 'desktop_password': 'shuler_914WBSB', + 'desktop_user_root': os.path.join('C:/', 'Users', 'Shichen'), + 'desktop_save_path': os.path.join('OneDrive - Johns Hopkins', 'ShulerLab', 'behavior_code', 'data'), + } + return info_dict \ No newline at end of file diff --git a/behavior_code/upload_to_pi.py b/behavior_code/upload_to_pi.py new file mode 100644 index 0000000..3a98b8d --- /dev/null +++ b/behavior_code/upload_to_pi.py @@ -0,0 +1,66 @@ +import os +import paramiko +from datetime import datetime +from user_info import get_user_info + + +def upload_to_pi(pi_host_name, durations=False): + local_path = os.path.join(os.getcwd(), "stand_alone") + pi_user_name = 'pi' + remote_path = '/home/pi/behavior' + password = 'shuler' + # command = f'scp -r {os.path.join(local_path, "stand_alone")} {pi_user_name}@{pi_host_name}:{remote_path}' + ssh = paramiko.SSHClient() + ssh.load_host_keys(os.path.expanduser(os.path.join("~", ".ssh", "known_hosts"))) + ssh.connect(pi_host_name, username=pi_user_name, password=password) + sftp = ssh.open_sftp() + try: + sftp.chdir(remote_path) # Test if remote_path exists + except IOError: + sftp.mkdir(remote_path) # Create remote_path + sftp.chdir(remote_path) + + [_, _, files] = list(os.walk(local_path))[0] + for f in files: + if f != 'desktop.ini' and (f != 'durations.pkl' or durations): + print(pi_host_name + ' ' + '/'.join([remote_path, f])) + sftp.put(os.path.join(local_path, f), '/'.join([remote_path, f])) + sftp.close() + ssh.close() + + +def reset_time(pi_host_name): + now = datetime.now() + t = now.strftime('%Y-%m-%d %H:%M:%S') + pi_user_name = 'pi' + password = 'shuler' + ssh = paramiko.SSHClient() + ssh.load_host_keys(os.path.expanduser(os.path.join("~", ".ssh", "known_hosts"))) + ssh.connect(pi_host_name, username=pi_user_name, password=password) + stdin, stdout, stderr = ssh.exec_command(f"sudo date -s '{t}'") + cmd_output = stdout.read() + print(f'set {pi_host_name} time to {cmd_output}') + ssh.close() + + +def ping_host(pi_host_name): + pi_user_name = 'pi' + password = 'shuler' + ssh = paramiko.SSHClient() + ssh.load_host_keys(os.path.expanduser(os.path.join("~", ".ssh", "known_hosts"))) + try: + ssh.connect(pi_host_name, username=pi_user_name, password=password, timeout=1) + ssh.close() + return True + except Exception as e: + ssh.close() + print(f'{pi_host_name} ping failed', end=': ') + print(e) + return False + + +if __name__ == '__main__': + info_dict = get_user_info() + for name in info_dict['pi_names']: + upload_to_pi(name, durations=False) + reset_time(name) diff --git a/behavior_code/user_info.py b/behavior_code/user_info.py new file mode 100644 index 0000000..0678972 --- /dev/null +++ b/behavior_code/user_info.py @@ -0,0 +1,11 @@ +from datetime import date + + +def get_user_info(): + info_dict = { + 'initials': 'SZ', + 'pi_names': ['elissapi1', 'shichenpi2', 'shichenpi3'], + # 'pi_names': ['elissapi1'], + 'start_date': date(2024, 5, 20) # For the current cohort, for the sake of simple_plots.py + } + return info_dict diff --git a/behavior_code/video_maker.py b/behavior_code/video_maker.py new file mode 100644 index 0000000..4eae979 --- /dev/null +++ b/behavior_code/video_maker.py @@ -0,0 +1,24 @@ +import cv2 +import numpy as np +import glob +import os + +def make_vid(): + img_array = [] + path = os.path.join('C:\\', 'video_data', '2022-09-04_10-57-42') + print(path + '\\*.jpeg') + print(os.path.isdir(path)) + for filename in glob.glob(path + '\\*.jpeg'): + img = cv2.imread(filename) + height, width, layers = img.shape + size = (width, height) + img_array.append(img) + + out = cv2.VideoWriter('project.avi', cv2.VideoWriter_fourcc(*'DIVX'), 15, size) + + for i in range(len(img_array)): + out.write(img_array[i]) + out.release() + +if __name__ == '__main__': + make_vid() diff --git a/behavior_code/weights_gui.py b/behavior_code/weights_gui.py new file mode 100644 index 0000000..e44d005 --- /dev/null +++ b/behavior_code/weights_gui.py @@ -0,0 +1,463 @@ +import os +from tkinter import * +import time +from os import walk +import pandas as pd +from csv import DictReader, reader +import numpy as np +from datetime import date +import datetime +from upload_to_pi import reset_time, ping_host +from user_info import get_user_info +import shutil +import json +import tkinter as tk +import tkinter.font as font +from tkinter import ttk +from functools import partial +from openpyxl import load_workbook +import fnmatch +import serial + +pastel_colors = ['#ffffcc', '#99ccff', '#cc99ff', '#ff99cc', '#ffcc99', '#ffffcc', '#99ffcc', '#ccffff', '#ccccff', + '#ffccff', '#ffcccc', '#D3D3D3'] +str_fmt = "%Y-%m-%d" + + +def get_today_string(): + return datetime.datetime.today().strftime(str_fmt) + + +def date_from_string(date_string): + return datetime.datetime.strptime(date_string, str_fmt) + + +def string_from_date(datetime_obj): + return datetime_obj.strftime(str_fmt) + + +def get_active_mice(): + return [ + ['ES037'], + ['ES039'], + ['ES041', 'ES042', 'ES043', 'ES044'], + ['ES045', 'ES046', 'ES047'], + ['ES048', 'ES049', 'ES050'], + ] + + +def load_log(): + with open('weight_log.json', 'r') as file: + data = json.load(file) + return data + + +def save_log(log): + log = dict(sorted(log.items())) + with open('weight_log.json', 'w') as file: + json.dump(log, file, indent=2) + + +def make_log(): + save_log({}) + + +def save_excel_to_log(): + log = load_log() + wb = load_workbook('log.xlsx', read_only=True, keep_links=False) + page_names = wb.sheetnames + for i, page in enumerate(page_names): + sheet = wb.worksheets[i].values + sheet = pd.DataFrame(sheet) + columns = sheet.iloc[0] # grab the first row for the header + sheet = sheet[1:] # take the data less the header row + sheet.columns = columns + columns = [str(val) for val in columns.tolist()] + if 'date' in sheet.keys(): + sheet['date'] = [d_time.strftime(str_fmt) if d_time is not None else None for d_time in sheet['date']] + if fnmatch.fnmatch(page, 'ES***-**'): + mice = fnmatch.filter(columns, 'ES***') + for mouse in mice: + mouse_weights = sheet[['date', mouse]].copy() + mouse_weights = mouse_weights.dropna() + mouse_weights[mouse] = [[val] for val in mouse_weights[mouse]] + mouse_dict = dict(zip(mouse_weights['date'], mouse_weights[mouse])) + if mouse in log.keys(): + for key in log[mouse].keys(): + if key in mouse_dict.keys(): + log[mouse][key] = np.unique(np.array(log[mouse][key] + mouse_dict[key])).tolist() + for key in mouse_dict.keys(): + if key not in log[mouse].keys(): + log[mouse][key] = mouse_dict[key] + else: + log[mouse] = mouse_dict + elif fnmatch.fnmatch(page, 'ES***'): + mouse = page + if 'weight' not in sheet.keys(): + print() + mouse_weights = sheet[['date', 'weight']].copy() + mouse_weights = mouse_weights.dropna() + mouse_weights['weight'] = [[val] for val in mouse_weights['weight']] + mouse_dict = dict(zip(mouse_weights['date'], mouse_weights['weight'])) + if mouse in log.keys(): + for key in log[mouse].keys(): + if key in mouse_dict.keys(): + log[mouse][key] = np.unique(np.array(log[mouse][key] + mouse_dict[key])).tolist() + for key in mouse_dict.keys(): + if key not in log[mouse].keys(): + log[mouse][key] = mouse_dict[key] + else: + log[mouse] = mouse_dict + save_log(log) + + +class Scale: + def __init__(self): + self.port = 'COM3' + self.BAUD = 9600 + self.PARITY = serial.PARITY_ODD + self.STOP_BITS = serial.STOPBITS_ONE + self.BYTE_SIZE = serial.SIXBITS + + def cleanse(self): + try: + temp = serial.Serial(self.port, self.BAUD, self.BYTE_SIZE, self.PARITY, self.STOP_BITS) + # temp = serial.Serial(port, baud, bytesize, parity, stopbits) + + # Flush both input/output buffer. + temp.reset_input_buffer() + temp.reset_output_buffer() + + temp.close() + except: + print('Unable to open/clean COM port') + return + + def get_weight(self, ser): + line = '' + ser.reset_input_buffer() + purge = True + while True: + if ser.in_waiting > 0: + x = ser.read(1).decode() + if len(x) and x not in [' ', '\'']: + line += x + if line[-1:] == '\n': + if purge: + line = '' + purge = False + else: + w = float(line[:-1]) + break + return w + + def weigh_one(self, check_tare=True): + self.cleanse() + zeroed = False + weight_history = [] + with serial.Serial(self.port, self.BAUD, self.BYTE_SIZE, self.PARITY, self.STOP_BITS) as ser: + while True: + tic = time.time() + w = self.get_weight(ser) + + if not zeroed and -.2 < w < .2 and check_tare: + weight_history.append(w) + if len(weight_history) > 8 and np.std(np.array(weight_history)) < .1 and abs( + np.mean(np.array(weight_history))) < .1: + zeroed = True + weight_history = [] + if len(weight_history) > 10: + print(f'{np.mean(np.array(weight_history)):.2f} +- {np.std(np.array(weight_history)):.2f} != 0') + weight_history = weight_history[-10:] + + if not zeroed and not check_tare and w < 0: + zeroed = True + + if zeroed and 15 < w < 45: + weight_history.append(w) + if len(weight_history) > 25 and np.std(np.array(weight_history)) < .5: + weight = np.median(np.array(weight_history)) + print(f'final weight: {weight}') + break + if len(weight_history) > 30: + print(f'{np.mean(np.array(weight_history)):.2f} + or - {np.std(np.array(weight_history)):.2f}') + weight_history = weight_history[-30:] + t_remaining = .1 - (time.time() - tic) + if t_remaining > 0: + time.sleep(t_remaining) + # print(f'loop time: {time.time()-tic:.2f}') + + return weight + + +def test_scale2(): + port = 'COM3' + BAUD = 9600 + PARITY = serial.PARITY_ODD + BYTESIZE = serial.SIXBITS + STOPBITS = serial.STOPBITS_ONE + try: + temp = serial.Serial(port, BAUD, BYTESIZE, PARITY, STOPBITS) + # temp = serial.Serial(port, baud, bytesize, parity, stopbits) + + # Flush both input/output buffer. + temp.reset_input_buffer() + temp.reset_output_buffer() + + temp.close() + except: + print('Unable to open/clean ' + port + ':' + str(BAUD) + ',' + str(PARITY)) + return + + t = time.time() + line = '' + with serial.Serial(port, BAUD, BYTESIZE, PARITY, STOPBITS, timeout=.5) as ser: + while time.time() < t + 60: + if ser.in_waiting > 0: + x = ser.read(1).decode() + if len(x) and x not in [' ', '\'']: + line += x + if line[-1:] == '\n': + print(line, end='') + # print(x, end='') + # char_num += 1 + else: + time.sleep(.1) + + +def test_scale(): + baud_rates = [9600] + # baud_rates = [110, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000] + parities = [serial.PARITY_ODD, serial.PARITY_EVEN] + characters = [''.join([chr(i), chr(13)]) for i in list(range(97, 123))] + # characters = list(range(65, 91)) + list(range(97, 123)) + # characters = [''.join([chr(i), chr(13)]) for i in characters] + [chr(i) for i in characters] + # characters = ['weight\r'] + # bytesizes = [serial.EIGHTBITS] + stop_bits = [serial.STOPBITS_ONE] + bytesizes = [serial.SEVENBITS] + # bytesizes = [serial.FIVEBITS, serial.SIXBITS, serial.SEVENBITS, serial.EIGHTBITS] + # stop_bits = [serial.STOPBITS_ONE, serial.STOPBITS_TWO, serial.STOPBITS_ONE_POINT_FIVE] + port = 'COM3' + # port = '/dev/ttyUSB0' + # baudrate = 9600 + # parity = serial.PARITY_NONE + # stopbits = serial.STOPBITS_ONE + # bytesize = serial.EIGHTBITS + for BAUD in baud_rates: + for PARITY in parities: + for BYTESIZE in bytesizes: + for STOPBITS in stop_bits: + print(f'baud: {BAUD}, parity: {PARITY}, bytesize: {BYTESIZE}, stopbits: {STOPBITS}: ', end='') + success = False + try: + temp = serial.Serial(port, BAUD, BYTESIZE, PARITY, STOPBITS) + # temp = serial.Serial(port, baud, bytesize, parity, stopbits) + + # Flush both input/output buffer. + temp.reset_input_buffer() + temp.reset_output_buffer() + + temp.close() + except: + print('Unable to open/clean ' + port + ':' + str(BAUD) + ',' + str(PARITY)) + return + + # You are using Ctrl+C to stop the program. Using *with* ensures + # that the serial port is closed when you exit the program. + with serial.Serial(port, BAUD, BYTESIZE, PARITY, STOPBITS, timeout=.5) as ser: + if ser.in_waiting > 0: + x = ser.read(10) + print(f'no input: {x}') + success = True + for char in characters: + # ser.write(char.encode()) + time.sleep(.1) + # print(char) + if ser.in_waiting > 0: + x = ser.read(10) + print(f'{char[0]}: {x}') + success = True + if success: + print(f'baud: {BAUD}, parity: {PARITY}, bytesize: {BYTESIZE}, stopbits: {STOPBITS}: success!') + else: + print('unsuccessful') + + # for _ in range(1000): + # # Only read data if there are bytes already waiting in the buffer. + # if ser.in_waiting <= 0: + # time.sleep(0.1) + # continue + # + # # We got bytes, read them. + # x = ser.readline() + # print(x) + # time.sleep(1) + + +def make_button(frame, f, text, color, button_font): + return tk.Button( + text=text, + font=button_font, + width=10, + height=5, + bg=color, + fg="black", + master=frame, + command=f) + + +def make_display(frame): + return Label(frame, text="", fg="Black", font=("Helvetica", 18)) + + +class App: + def __init__(self, master=None): + self.root = tk.Tk() + self.root.geometry('400x700') + self.root.title('Weights Gui') + self.font = font.Font(size=15) + self.log = load_log() + + # Frame.__init__(self, master) + # self.master = master + self.mice = get_active_mice() + self.mouse_names = [item for sublist in self.mice for item in sublist] + self.button_list = [] + self.functions = [partial(self.record_weight, i) for i in range(len(self.mouse_names))] + self.root.rowconfigure(0, weight=1, minsize=50) + self.root.columnconfigure(0, weight=1, minsize=75) + button_i = 1 + for i, cage in enumerate(self.mice): + for j, mouse in enumerate(cage): + self.root.rowconfigure(button_i, weight=1, minsize=50) + frame = tk.Frame(master=self.root, borderwidth=1) + frame.grid(row=button_i, column=0, sticky="nsew") + button = make_button(frame, self.functions[button_i - 1], mouse, pastel_colors[i], self.font) + button.pack(fill=tk.BOTH, expand=True) + self.button_list.append(button) + button_i += 1 + self.root.rowconfigure(button_i, weight=1, minsize=50) + frame = tk.Frame(master=self.root, borderwidth=1) + frame.grid(row=button_i, column=0, sticky="nsew") + button = make_button(frame, self.weigh_all, 'Weigh All', 'white', self.font) + button.pack(fill=tk.BOTH, expand=True) + + self.weight_label_list = [] + self.root.columnconfigure(1, weight=1, minsize=75) + for i in range(len(self.mouse_names)): + frame = tk.Frame(master=self.root, borderwidth=1) + frame.grid(row=i + 1, column=1, sticky="nsew") + label = make_display(frame) + label.pack(fill=tk.BOTH, expand=True) + today = self.get_today(self.mouse_names[i]) + recent = self.get_recent(self.mouse_names[i]) + if today is not None: + label.configure(text=f'{today:.1f}g', fg='black') + elif recent is not None: + label.configure(text=f'{recent:.1f}g', fg='gray') + self.weight_label_list.append(label) + + self.percent_label_list = [] + self.root.columnconfigure(2, weight=1, minsize=75) + for i in range(len(self.mouse_names)): + frame = tk.Frame(master=self.root, borderwidth=1) + frame.grid(row=i + 1, column=2, sticky="nsew") + label = make_display(frame) + label.pack(fill=tk.BOTH, expand=True) + percent = self.get_percent(self.mouse_names[i]) + if percent is not None: + if percent > 85: + label.configure(text=f'{percent:.1f}%', fg='black') + elif percent <= 85 and percent >= 80: + label.configure(text=f'{percent:.1f}%', fg='orange') + elif percent < 80: + label.configure(text=f'{percent:.1f}%', fg='red') + self.percent_label_list.append(label) + headers = ['Mouse', 'Weight', 'Percent'] + for i, header in enumerate(headers): + frame = tk.Frame(master=self.root, borderwidth=1) + frame.grid(row=0, column=i, sticky="nsew") + label = make_display(frame) + label.pack(fill=tk.BOTH, expand=True) + label.configure(text=header, fg='black') + + self.root.mainloop() + + def record_weight(self, button_i, check_tare=True): + today = get_today_string() + print(self.mouse_names[button_i]) + scale = Scale() + weight = scale.weigh_one(check_tare=check_tare) + if today in self.log[self.mouse_names[button_i]].keys(): + self.log[self.mouse_names[button_i]][today].append(weight) + else: + self.log[self.mouse_names[button_i]][today] = [weight] + save_log(self.log) + self.update_display(button_i) + + def weigh_all(self): + k = 0 + for i, cage in enumerate(self.mice): + for _ in cage: + self.button_list[k].configure(bg='blue') + self.button_list[0].update() + self.record_weight(k, check_tare=k == 0) + self.button_list[k].configure(bg=pastel_colors[i]) + self.button_list[0].update() + k += 1 + + def update_display(self, i): + today = self.get_today(self.mouse_names[i]) + self.weight_label_list[i].configure(text=f'{today:.1f}g', fg='black') + + percent = self.get_percent(self.mouse_names[i]) + if percent is not None: + if percent > 85: + self.percent_label_list[i].configure(text=f'{percent:.1f}%', fg='black') + elif percent <= 85 and percent >= 80: + self.percent_label_list[i].configure(text=f'{percent:.1f}%', fg='orange') + elif percent < 80: + self.percent_label_list[i].configure(text=f'{percent:.1f}%', fg='red') + + def get_today(self, mouse): + today = get_today_string() + if today in self.log[mouse].keys(): + return self.log[mouse][today][-1] + else: + return None + + def get_recent(self, mouse): + if len(self.log[mouse].keys()): + keys = list(self.log[mouse].keys()) + keys.sort() + return self.log[mouse][keys[-1]][-1] + else: + return None + + def get_percent(self, mouse): + if len(self.log[mouse].keys()): + keys = list(self.log[mouse].keys()) + keys.sort() + current = self.log[mouse][keys[-1]][-1] + max_weight = np.max([val for sublist in self.log[mouse].values() for val in sublist]) + return current / max_weight * 100 + else: + return None + + +# def run_gui(): +# root = Tk() +# app = App(root) +# root.wm_title("Weights Gui") +# root.geometry("600x400") +# root.mainloop() + +def run_gui(): + app = App() + + +if __name__ == '__main__': + run_gui() + # save_excel_to_log() + # test_scale2() diff --git a/each_session/SZ036/SZ036_2024-01-13_20-50-39.png b/each_session/SZ036/SZ036_2024-01-13_20-50-39.png new file mode 100644 index 0000000..6918e67 Binary files /dev/null and b/each_session/SZ036/SZ036_2024-01-13_20-50-39.png differ diff --git a/each_session/SZ036/SZ036_2024-01-14_20-55-53.png b/each_session/SZ036/SZ036_2024-01-14_20-55-53.png new file mode 100644 index 0000000..88d7b15 Binary files /dev/null and b/each_session/SZ036/SZ036_2024-01-14_20-55-53.png differ diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..7cabd99 Binary files /dev/null and b/environment.yml differ diff --git a/simple_plots.py b/simple_plots.py index de1dbb3..9d322ee 100644 --- a/simple_plots.py +++ b/simple_plots.py @@ -10,30 +10,56 @@ import seaborn as sns from matplotlib.collections import PatchCollection from matplotlib.patches import Rectangle +from labellines import labelLine, labelLines from user_info import get_user_info +from backend.add_2ndry_properties_to_pi_events import add_2ndry_properties_to_pi_events import shutil +from datetime import datetime +import matplotlib as mpl +from lifelines import KaplanMeierFitter +from lifelines.statistics import logrank_test +mpl.rcParams['figure.dpi'] = 300 info_dict = get_user_info() initials = info_dict['initials'] start_date = info_dict['start_date'] +data_dir = os.path.join('C:\\', 'Users', 'shich', 'OneDrive - Johns Hopkins', 'ShulerLab', 'behavior_code', 'data') def get_today_filepaths(days_back=0): file_paths = [] - for root, dirs, filenames in walk(os.path.join(os.getcwd(), 'data')): - if len(dirs) == 0 and os.path.basename(root)[:2] == initials: + for root, dirs, filenames in walk(data_dir): + if len(dirs) == 0 and os.path.basename(root)[:2] in initials: mouse = os.path.basename(root) for f in filenames: if f == 'desktop.ini': continue - file_date = date(int(f[5:9]), int(f[10:12]), int(f[13:15])) - dif = date.today() - file_date + file_date = datetime.strptime(f[5:-4], '%Y-%m-%d_%H-%M-%S') + # file_date = date(int(f[5:9]), int(f[10:12]), int(f[13:15])) + dif = datetime.today() - file_date if dif.days <= days_back: # if f[5:15] == time.strftime("%Y-%m-%d"): file_paths.append(os.path.join(mouse, f)) return file_paths +def get_dateranged_filepaths(start_date, end_date): + print(f"Start date: {start_date}") + print(f"End date: {end_date}") + file_paths = [] + for root, dirs, filenames in walk(data_dir): + if len(dirs) == 0 and os.path.basename(root)[:2] in initials: + mouse = os.path.basename(root) + for f in filenames: + if f == 'desktop.ini': + continue + file_date = datetime.strptime(f[5:-4], '%Y-%m-%d_%H-%M-%S').date() + # file_date = date(int(f[5:9]), int(f[10:12]), int(f[13:15])) + if start_date <= file_date <= end_date: + file_paths.append(os.path.join(mouse, f)) + return file_paths + + def min_dif(a, b, tolerance=0, return_index=False, rev=False): if type(a) == pd.core.series.Series: a = a.values @@ -91,24 +117,39 @@ def gen_data(file_paths, select_mouse=None, return_info=False): if select_mouse is not None and mouse not in select_mouse: continue - path = os.path.join(os.getcwd(), 'data', f) + path = os.path.join(data_dir, f) + meta_data = read_pi_meta(path) + if return_info: - data = read_pi_meta(path) - # if data['box'] == 'elissapi0': - # session = pd.read_csv(path, na_values=['None'], skiprows=3) - # session_summary(data_reduction(session), mouse) - # ans = input(f'remove broken file? (y/n)\n{path}\n???') - # if ans == 'y': - # file_name = f[6:] - # half_session_path = os.path.join(os.getcwd(), 'data', 'half_sessions', file_name) - # shutil.move(path, half_session_path) + data = meta_data else: - data = pd.read_csv(path, na_values=['None'], skiprows=3) + try: + data = pd.read_csv(path, na_values=['None'], skiprows=3) + except pd.errors.EmptyDataError: + print(f'empty file at {path}') + continue try: data = data_reduction(data) + + # port info as a row + port_info_row = [{ + 'key': meta_data['port1_info']['distribution'], + 'port': meta_data['port1_info']['port_num'] + # 'phase': last_row_phase, + }, + {'key': meta_data['port2_info']['distribution'], + 'port': meta_data['port2_info']['port_num'] + # 'phase': last_row_phase, + } + ] + + # Added port info at the end of the dataframe + portinfo_rows_df = pd.DataFrame(port_info_row).reindex(columns=data.columns) + data = pd.concat([data, portinfo_rows_df], ignore_index=True) + except ValueError: file_name = f[6:] - half_session_path = os.path.join(os.getcwd(), 'data', 'half_sessions', file_name) + half_session_path = os.path.join(data_dir, 'half_sessions', file_name) if data.session_time.max() < 800: print(f'moving {f} to half sessions, session time: {data.session_time.max():.2f} seconds') shutil.move(path, half_session_path) @@ -117,6 +158,7 @@ def gen_data(file_paths, select_mouse=None, return_info=False): if ans == 'y': shutil.move(path, half_session_path) continue + if mouse in d.keys(): d[mouse].append(data) else: @@ -158,9 +200,13 @@ def data_reduction(df, lick_tol=.01, head_tol=.2): def consumption_time(df): - bg_end_times = df[(df.key == 'LED') & (df.port == 2) & (df.value == 1)] - exp_entries = df[(df.key == 'head') & (df.port == 1) & (df.value == 1)] + bgportassignment = df.loc[df['key'] == 'background', 'port'].iloc[-1] + expportassignment = df.loc[df['key'] == 'exp_decreasing', 'port'].iloc[-1] + bg_end_times = df[(df.key == 'LED') & (df.port == bgportassignment) & (df.value == 1)] + exp_entries = df[(df.key == 'head') & (df.port == expportassignment) & (df.value == 1)] + dif = min_dif(bg_end_times.session_time, exp_entries.session_time) + bg_consumption = dif[~np.isnan(dif)] if df.task.iloc[10] != 'single_reward': consumption_df = pd.DataFrame() @@ -168,8 +214,8 @@ def consumption_time(df): consumption_df['port'] = ['bg'] * len(bg_consumption) return consumption_df - exp_end_times = df[(df.key == 'LED') & (df.port == 1) & (df.value == 1)] - bg_entries = df[(df.key == 'head') & (df.port == 2) & (df.value == 1)] + exp_end_times = df[(df.key == 'LED') & (df.port == expportassignment) & (df.value == 1)] + bg_entries = df[(df.key == 'head') & (df.port == bgportassignment) & (df.value == 1)] dif = min_dif(exp_end_times.session_time, bg_entries.session_time) exp_consumption = dif[~np.isnan(dif)] consumption_df = pd.DataFrame() @@ -178,12 +224,60 @@ def consumption_time(df): return consumption_df +def calculate_premature_leave(df, threshold=1.0): # trials with premature leave + bgportassignment = df.loc[df['key'] == 'background', 'port'].iloc[-1] + premature_leave_trials = 0 # Counter for trials with premature leave + blocks = df.phase.dropna().unique() + blocks.sort() + results = [] + for block in blocks: + block_data = df[df.phase == block] + block_total_trials = len(block_data.trial.unique()) + for trial in block_data.trial.unique(): + if pd.isna(trial): + continue # Skip invalid trials + trial_data = df[df.trial == trial] # Filter data for the current trial + bg_on_times = trial_data[(trial_data['key'] == 'trial') & + (trial_data['value'] == 1)].session_time.values + if len(bg_on_times) == 0: + continue + bg_start = bg_on_times[0] + bg_off_times = trial_data[(trial_data['key'] == 'LED') & + (trial_data['port'] == bgportassignment) & + (trial_data['value'] == 1)].session_time.values + if len(bg_off_times) == 0: # Check if no BG ON recorded + continue + bg_end = bg_off_times[0] + bg_head_out_times = trial_data[ + (trial_data['key'] == 'head') & # Extract head-out and head-in times while BG port is active + (trial_data['port'] == bgportassignment) & + (trial_data['value'] == 0) & + (trial_data['session_time'] >= bg_start) & + (trial_data['session_time'] < bg_end)].session_time.values + bg_head_in_times = trial_data[(trial_data['key'] == 'head') & + (trial_data['port'] == bgportassignment) & + (trial_data['value'] == 1) & + (trial_data['session_time'] >= bg_start) & + (trial_data['session_time'] < bg_end)].session_time.values + for head_out in bg_head_out_times: # Check for premature leave: head-out without a valid head-in within threshold + if not any((head_in > head_out) and (head_in - head_out) <= threshold for head_in in bg_head_in_times): + premature_leave_trials += 1 # Flag this trial as a premature leave + break # Stop checking further head-outs in this trial + premature_leave_rate = (premature_leave_trials / block_total_trials) + results.append({"block": block, "premature leave numbers": premature_leave_trials, + "premature leave rate": premature_leave_rate}) + premature_leave_df = pd.DataFrame(results) + return premature_leave_df + + def block_leave_times(df): + bgportassignment = df.loc[df['key'] == 'background', 'port'].iloc[-1] + expportassignment = df.loc[df['key'] == 'exp_decreasing', 'port'].iloc[-1] reward_trials = df[(df.key == 'reward_initiate')].trial.to_numpy() non_reward = ~df.trial.isin(reward_trials) - bg_end_times = df[(df.key == 'LED') & (df.port == 2) & (df.value == 1) & non_reward] - exp_entries = df[(df.key == 'head') & (df.value == 1) & (df.port == 1) & non_reward] - exp_exits = df[(df.key == 'head') & (df.value == 0) & (df.port == 1) & non_reward] + bg_end_times = df[(df.key == 'LED') & (df.port == bgportassignment) & (df.value == 1) & non_reward] + exp_entries = df[(df.key == 'head') & (df.value == 1) & (df.port == expportassignment) & non_reward] + exp_exits = df[(df.key == 'head') & (df.value == 0) & (df.port == expportassignment) & non_reward] bg_end_times = bg_end_times[bg_end_times.session_time < exp_entries.session_time.max()] ind, dif = min_dif(bg_end_times.session_time, exp_entries.session_time, return_index=True) exp_entries = exp_entries.iloc[np.unique(ind)] @@ -193,8 +287,14 @@ def block_leave_times(df): valid_trials = np.intersect1d(valid_trials, bg_end_times.trial.values) exp_exits = exp_exits.loc[valid_trials] exp_entries = exp_entries.loc[valid_trials] + if len(exp_exits.to_numpy()) != len(exp_entries.to_numpy()): - print() + print( + f"Mismatch in exp_entries and exp_exits. " + f"exp_entries: {exp_entries}, exp_exits: {exp_exits}. " + "Using clean_entries_exits to resolve." + ) + exp_entries, exp_exits = clean_entries_exits(exp_entries, exp_exits) leave_times = exp_exits.to_numpy() - exp_entries.to_numpy() trial_blocks = bg_end_times[bg_end_times.trial.isin(exp_entries.index.values)].phase.to_numpy() @@ -208,15 +308,17 @@ def get_entry_exit(df, trial): is_trial = df.trial == trial start = df.value == 1 end = df.value == 0 - port1 = df.port == 1 - port2 = df.port == 2 + bgport = df.port == df.loc[df['key'] == 'background', 'port'].iloc[-1] + expport = df.port == df.loc[df['key'] == 'exp_decreasing', 'port'].iloc[-1] + # port1 = df.port == 1 + # port2 = df.port == 2 trial_start = df[is_trial & start & (df.key == 'trial')].session_time.values[0] - trial_middle = df[is_trial & end & (df.key == 'LED') & port2].session_time.values[0] + trial_middle = df[is_trial & end & (df.key == 'LED') & bgport].session_time.values[0] # head in to EXP, bg LED off trial_end = df[is_trial & end & (df.key == 'trial')].session_time.values[0] - bg_entries = df[is_trial & port2 & start & (df.key == 'head')].session_time.to_numpy() - bg_exits = df[is_trial & port2 & end & (df.key == 'head')].session_time.to_numpy() + bg_entries = df[is_trial & bgport & start & (df.key == 'head')].session_time.to_numpy() + bg_exits = df[is_trial & bgport & end & (df.key == 'head')].session_time.to_numpy() if len(bg_entries) == 0 or len(bg_exits) == 0 or bg_entries[0] > bg_exits[0]: bg_entries = np.concatenate([[trial_start], bg_entries]) @@ -225,15 +327,15 @@ def get_entry_exit(df, trial): if len(bg_exits) == 0 or bg_entries[-1] > bg_exits[-1]: bg_exits = np.concatenate([bg_exits, [trial_middle]]) - exp_entries = df[is_trial & port1 & start & (df.key == 'head') & + exp_entries = df[is_trial & expport & start & (df.key == 'head') & (df.session_time > trial_middle)].session_time.to_numpy() - exp_exits = df[is_trial & port1 & end & (df.key == 'head') & + exp_exits = df[is_trial & expport & end & (df.key == 'head') & (df.session_time > trial_middle)].session_time.to_numpy() if not (len(exp_entries) == 0 and len(exp_exits) == 0): - if len(exp_entries) == 0: + if len(exp_entries) == 0: # only exp out exp_entries = np.concatenate([[trial_middle], exp_entries]) - if len(exp_exits) == 0: + if len(exp_exits) == 0: # only exp in exp_exits = np.concatenate([exp_exits, [trial_end]]) if exp_entries[0] > exp_exits[0]: @@ -241,12 +343,12 @@ def get_entry_exit(df, trial): if exp_entries[-1] > exp_exits[-1]: exp_exits = np.concatenate([exp_exits, [trial_end]]) - early_exp_entries = df[is_trial & port1 & start & (df.key == 'head') & + early_exp_entries = df[is_trial & expport & start & (df.key == 'head') & (df.session_time < trial_middle)].session_time.to_numpy() - early_exp_exits = df[is_trial & port1 & end & (df.key == 'head') & + early_exp_exits = df[is_trial & expport & end & (df.key == 'head') & (df.session_time < trial_middle)].session_time.to_numpy() - if not (len(early_exp_entries) == 0 and len(early_exp_exits) == 0): + if not (len(early_exp_entries) == 0 and len(early_exp_exits) == 0): # any early exp in/out if len(early_exp_entries) == 0: early_exp_entries = np.concatenate([[trial_start], early_exp_entries]) if len(early_exp_exits) == 0: @@ -264,109 +366,258 @@ def get_entry_exit(df, trial): if len(early_exp_entries) != len(early_exp_exits): print() + if len(exp_entries): + if len(exp_exits) != len(exp_entries): + print( + f"Mismatch in exp_entries and exp_exits in trial {trial}. " + f"exp_entries: {exp_entries}, exp_exits: {exp_exits}. " + "Using clean_entries_exits to resolve." + ) + exp_entries, exp_exits = clean_entries_exits(exp_entries, exp_exits) + + if len(bg_entries) != len(bg_exits): + print( + f"Mismatch in bg_entries and bg_exits in trial {trial}. " + f"bg_entries: {bg_entries}, bg_exits: {bg_exits}. " + "Using clean_entries_exits to resolve." + ) + bg_entries, bg_exits = clean_entries_exits(bg_entries, bg_exits) + return bg_entries, bg_exits, exp_entries, exp_exits, early_exp_entries, early_exp_exits +def clean_entries_exits(entries, exits): + """ + Cleans mismatched entries and exits such that each entry is paired with the nearest valid exit. + """ + valid_entries = [] + valid_exits = [] + e_idx, x_idx = 0, 0 + + while e_idx < len(entries) and x_idx < len(exits): + if entries[e_idx] < exits[x_idx]: # Valid entry-exit pair + valid_entries.append(entries[e_idx]) + valid_exits.append(exits[x_idx]) + e_idx += 1 + x_idx += 1 # Move to the next entry and exit + else: + x_idx += 1 # Skip unmatched exits + + return valid_entries, valid_exits + +def get_bools(events): + head = events.key == 'head' + trial = events.key == 'trial' + cue = events.key == 'LED' + reward = events.key == 'reward' + lick = events.key == 'lick' + off = events.value == 0 + on = events.value == 1 + port1 = events.port == events.loc[events['key'] == 'exp_decreasing', 'port'].iloc[-1] + port2 = events.port == events.loc[events['key'] == 'background', 'port'].iloc[-1] + valid_head = events.is_valid + return [head, trial, cue, reward, lick, off, on, port1, port2, valid_head] + +def construct_trial_df(pi_events): + pi_events = add_2ndry_properties_to_pi_events(pi_events) + [head, trial, cue, reward, lick, off, on, port1, port2, valid_head] = get_bools(pi_events) + bg_entries = pi_events.loc[trial & on & valid_head, 'session_time'].to_list() + bg_exits = pi_events.loc[port2 & head & off & valid_head, 'session_time'].to_list() + exp_entries = pi_events.loc[port1 & head & on & valid_head, 'session_time'].to_list() + exp_exits = pi_events.loc[port1 & head & off & valid_head, 'session_time'].to_list() + trials = pi_events.loc[port2 & head & off & valid_head, 'trial'].to_list() + phase = pi_events.loc[port2 & head & off & valid_head, 'phase'].to_list() + rewards = [[] for _ in range(len(trials))] + licks = [[] for _ in range(len(trials))] + excess_bg_exits = [[] for _ in range(len(trials))] + excess_exp_entries = [[] for _ in range(len(trials))] + excess_exp_exits = [[] for _ in range(len(trials))] + for i, trial_id in enumerate(trials): + is_in_trial = pi_events['trial'] == trial_id + rewards[i] = pi_events.loc[reward & on & is_in_trial, 'session_time'].to_list() + licks[i] = pi_events.loc[lick & on & is_in_trial, 'session_time'].to_list() + excess_bg_exits[i] = pi_events.loc[ + port2 & head & off & is_in_trial & ~valid_head, 'session_time'].to_list() + excess_exp_entries[i] = pi_events.loc[ + port1 & head & on & is_in_trial & ~valid_head, 'session_time'].to_list() + excess_exp_exits[i] = pi_events.loc[ + port1 & head & off & is_in_trial & ~valid_head, 'session_time'].to_list() + trial_df = pd.DataFrame( + {'trial': trials, 'phase': phase, + 'rewards': rewards, 'licks': licks, + 'bg_entry': bg_entries, 'bg_exit': bg_exits, + 'exp_entry': exp_entries, 'exp_exit': exp_exits, + 'excess_bg_exits': excess_bg_exits, + 'excess_exp_exits': excess_exp_exits, + 'excess_exp_entries': excess_exp_entries + }) + return trial_df + +def plot_kaplan_meier(trial_df, session_info): + animal_id = session_info['mouse'] + session_id = f"{session_info['date']}_{session_info['time']}" + title = f"{animal_id}:{session_id} KM Survival Curves" + + trial_df['leave_time'] = trial_df['exp_exit'] - trial_df['exp_entry'] + trial_df['event_observed'] = 1 + + kmf = KaplanMeierFitter() + fig, ax = plt.subplots() + color_palette = sns.color_palette("Set2") + groups = { + '0.8': {'label': 'high', 'color': color_palette[1]}, + '0.4': {'label': 'low', 'color': color_palette[0]} + } + for phase, settings in groups.items(): + mask = trial_df['phase'] == phase + kmf.fit( + durations=trial_df.loc[mask, 'leave_time'], + event_observed=trial_df.loc[mask, 'event_observed'], + label=settings['label'] + ) + kmf.plot_survival_function(ax=ax, c=settings['color']) + plt.title(title) + plt.xlabel('Time from Entry (sec)') + plt.ylabel('Stay Probability') + plt.grid(True) + base_save_folder = save_folder = "C:\\Users\\shich\\OneDrive - Johns Hopkins\\ShulerLab\\behavior_code\\each_session" + save_folder = os.path.join(base_save_folder, animal_id) + os.makedirs(save_folder, exist_ok=True) + filename = f'{animal_id}_{session_id}_KM_survival.png' + save_path = os.path.join(save_folder, filename) + plt.savefig(save_path, dpi=300, bbox_inches='tight') + print(f"Graph saved to: {save_path}") + plt.close(fig) + # plt.show() + +def perform_log_rank_test(trial_df): + trial_df['leave_time'] = trial_df['exp_exit'] - trial_df['exp_entry'] + trial_df['event_observed'] = 1 + # log-rank test + high_mask = trial_df['phase'] == '0.8' + low_mask = trial_df['phase'] == '0.4' + results = logrank_test( + durations_A=trial_df.loc[high_mask, 'leave_time'], + event_observed_A=trial_df.loc[high_mask, 'event_observed'], + durations_B=trial_df.loc[low_mask, 'leave_time'], + event_observed_B=trial_df.loc[low_mask, 'event_observed'] + ) + return results + def percent_engaged(df): - travel_time = .5 - blocks = df.phase.unique() - blocks.sort() - time_engaged = [] - block_time = [] - block_rewards = [] - for block in blocks: - engaged = [] - all_time = [] - rewards = [] - block_trials = df[(df.value == 0) & (df.key == 'trial') & (df.phase == block)].trial - for trial in block_trials: - bg_entries, bg_exits, exp_entries, exp_exits, _, _ = get_entry_exit(df, trial) - is_trial = df.trial == trial - start = df.value == 1 - end = df.value == 0 - # port1 = df.port == 1 - # port2 = df.port == 2 - - # - trial_start = df[is_trial & start & (df.key == 'trial')].session_time.values[0] - # trial_middle = df[is_trial & start & (df.key == 'LED') & port2].session_time.values[0] - trial_end = df[is_trial & end & (df.key == 'trial')].session_time.values[0] - # - # bg_entries = df[is_trial & port2 & start & (df.key == 'head')].session_time.to_numpy() - # bg_exits = df[is_trial & port2 & end & (df.key == 'head')].session_time.to_numpy() - # - # if len(bg_entries) == 0 or bg_entries[0] > bg_exits[0]: - # bg_entries = np.concatenate([[trial_start], bg_entries]) - # if trial_end - bg_entries[-1] < .1: - # bg_entries = bg_entries[:-1] - # if len(bg_exits) == 0 or bg_entries[-1] > bg_exits[-1]: - # bg_entries = np.concatenate([bg_exits, [trial_middle]]) - # - # if not (len(bg_entries) == len(bg_exits) and np.all(bg_exits - bg_entries > 0)): - # print('stop') - # bg_engaged = sum(bg_exits - bg_entries) - # - # exp_entries = df[is_trial & port1 & start & (df.key == 'head') & - # (df.session_time > trial_middle)].session_time.to_numpy() - # exp_exits = df[is_trial & port1 & end & (df.key == 'head') & - # (df.session_time > trial_middle)].session_time.to_numpy() - # - # if len(exp_entries) == 0 and len(exp_exits) == 0: - # exp_engaged = 0 - # else: - # if len(exp_entries) == 0: - # exp_entries = np.concatenate([[trial_middle], exp_entries]) - # if len(exp_exits) == 0: - # exp_exits = np.concatenate([exp_exits, [trial_end]]) - # - # if exp_entries[0] > exp_exits[0]: - # exp_entries = np.concatenate([[trial_middle], exp_entries]) - # if exp_entries[-1] > exp_exits[-1]: - # exp_exits = np.concatenate([exp_exits, [trial_end]]) - # exp_engaged = sum(exp_exits - exp_entries) - # - # # if not len(exp_entries) == len(exp_exits) and len(exp_entries): - # # print('stop') - # # if len(exp_entries): - - if len(exp_entries): - exp_engaged = sum(exp_exits - exp_entries) - else: - exp_engaged = 0 - bg_engaged = sum(bg_exits - bg_entries) + try: + travel_time = .5 + blocks = df.phase.dropna().unique() + blocks.sort() + time_engaged = [] + block_time = [] + block_rewards = [] + + for block in blocks: + engaged = [] + all_time = [] + rewards = [] + block_trials = df[(df.value == 0) & (df.key == 'trial') & (df.phase == block)].trial + for trial in block_trials: + bg_entries, bg_exits, exp_entries, exp_exits, _, _ = get_entry_exit(df, trial) + is_trial = df.trial == trial + start = df.value == 1 + end = df.value == 0 + # port1 = df.port == 1 + # port2 = df.port == 2 + + # + trial_start = df[is_trial & start & (df.key == 'trial')].session_time.values[0] + # trial_middle = df[is_trial & start & (df.key == 'LED') & port2].session_time.values[0] + trial_end = df[is_trial & end & (df.key == 'trial')].session_time.values[0] + # + # bg_entries = df[is_trial & port2 & start & (df.key == 'head')].session_time.to_numpy() + # bg_exits = df[is_trial & port2 & end & (df.key == 'head')].session_time.to_numpy() + # + # if len(bg_entries) == 0 or bg_entries[0] > bg_exits[0]: + # bg_entries = np.concatenate([[trial_start], bg_entries]) + # if trial_end - bg_entries[-1] < .1: + # bg_entries = bg_entries[:-1] + # if len(bg_exits) == 0 or bg_entries[-1] > bg_exits[-1]: + # bg_entries = np.concatenate([bg_exits, [trial_middle]]) + # + # if not (len(bg_entries) == len(bg_exits) and np.all(bg_exits - bg_entries > 0)): + # print('stop') + # bg_engaged = sum(bg_exits - bg_entries) + # + # exp_entries = df[is_trial & port1 & start & (df.key == 'head') & + # (df.session_time > trial_middle)].session_time.to_numpy() + # exp_exits = df[is_trial & port1 & end & (df.key == 'head') & + # (df.session_time > trial_middle)].session_time.to_numpy() + # + # if len(exp_entries) == 0 and len(exp_exits) == 0: + # exp_engaged = 0 + # else: + # if len(exp_entries) == 0: + # exp_entries = np.concatenate([[trial_middle], exp_entries]) + # if len(exp_exits) == 0: + # exp_exits = np.concatenate([exp_exits, [trial_end]]) + # + # if exp_entries[0] > exp_exits[0]: + # exp_entries = np.concatenate([[trial_middle], exp_entries]) + # if exp_entries[-1] > exp_exits[-1]: + # exp_exits = np.concatenate([exp_exits, [trial_end]]) + # exp_engaged = sum(exp_exits - exp_entries) + # + # # if not len(exp_entries) == len(exp_exits) and len(exp_entries): + # # print('stop') + # # if len(exp_entries): + + if len(exp_entries): + exp_engaged = sum([exit - entry for entry, exit in zip(exp_entries, exp_exits)]) + else: + exp_engaged = 0 + + bg_engaged = sum([exit - entry for entry, exit in zip(bg_entries, bg_exits)]) - all_time.append(trial_end - trial_start) - engaged.append(bg_engaged + exp_engaged) - rewards.append(len(df[is_trial & start & (df.key == 'reward')])) + all_time.append(trial_end - trial_start) + engaged.append(bg_engaged + exp_engaged) + rewards.append(len(df[is_trial & start & (df.key == 'reward')])) - time_engaged.append(sum(engaged) + travel_time * 2 * len(block_trials)) - block_time.append(sum(all_time)) - block_rewards.append(sum(rewards)) - engaged_df = pd.DataFrame() - engaged_df['percent engaged'] = np.array(time_engaged) / np.array(block_time) - engaged_df['block'] = blocks - engaged_df['time engaged'] = time_engaged - engaged_df['rewards earned'] = block_rewards - engaged_df['reward rate'] = np.array(block_rewards) / np.array(time_engaged) - return engaged_df + time_engaged.append(sum(engaged) + travel_time * 2 * len(block_trials)) + block_time.append(sum(all_time)) + block_rewards.append(sum(rewards)) + engaged_df = pd.DataFrame() + engaged_df['percent engaged'] = np.array(time_engaged) / np.array(block_time) + engaged_df['block'] = blocks + engaged_df['time engaged'] = time_engaged + engaged_df['rewards earned'] = block_rewards + engaged_df['reward rate'] = np.array(block_rewards) / np.array(time_engaged) + + return engaged_df + except Exception as e: + print("Error in function.") + raise def reentry_index(df): - is_bg_exit = (df.port == 2) & (df.key == 'head') & (df.value == 0) - is_slow_block = df.groupby('trial').phase.agg(pd.Series.mode) == '0.4' - is_fast_block = df.groupby('trial').phase.agg(pd.Series.mode) == '0.8' - num_ideal_bg_entry_slow = len(np.unique(df.trial.dropna())[is_slow_block]) - num_bg_entry_slow = len(df.index[is_bg_exit & df.trial.isin( - np.unique(df.trial.dropna())[is_slow_block])]) - num_ideal_bg_entry_fast = len(np.unique(df.trial.dropna())[is_fast_block]) - num_bg_entry_fast = len(df.index[is_bg_exit & df.trial.isin( - np.unique(df.trial.dropna())[is_fast_block])]) - - reentry_index_slow = num_bg_entry_slow / num_ideal_bg_entry_slow - reentry_index_fast = num_bg_entry_fast / num_ideal_bg_entry_fast + bgportassignment = df.loc[df['key'] == 'background', 'port'].iloc[-1] + expportassignment = df.loc[df['key'] == 'exp_decreasing', 'port'].iloc[-1] + is_bg_exit = (df.port == bgportassignment) & (df.key == 'head') & (df.value == 0) + phase_mode = df.groupby('trial').phase.agg( + lambda s: s.iloc[-1] if (s.value_counts().get('0.4', 0) == 1 and s.value_counts().get('0.8', 0) == 1) + else pd.Series.mode(s).iloc[-1] + ) + + is_low_block = (phase_mode == '0.4') + is_high_block = (phase_mode == '0.8') + num_ideal_bg_entry_low = len(np.unique(df.trial.dropna())[is_low_block]) # gets number of low block trials + num_bg_entry_low = len(df.index[is_bg_exit & df.trial.isin( + np.unique(df.trial.dropna())[is_low_block])]) + num_ideal_bg_entry_high = len(np.unique(df.trial.dropna())[is_high_block]) # gets number of high block trials + num_bg_entry_high = len(df.index[is_bg_exit & df.trial.isin( + np.unique(df.trial.dropna())[is_high_block])]) + + reentry_index_low = num_bg_entry_low / num_ideal_bg_entry_low if num_ideal_bg_entry_low > 0 else 0 + reentry_index_high = num_bg_entry_high / num_ideal_bg_entry_high if num_ideal_bg_entry_high > 0 else 0 reentry_df = pd.DataFrame() reentry_df['block'] = ['0.4', '0.8'] - reentry_df['bg_reentry_index'] = [reentry_index_slow, reentry_index_fast] + reentry_df['bg_reentry_index'] = [reentry_index_low, reentry_index_high] return reentry_df @@ -388,15 +639,74 @@ def merge_old_trials(session): return session -def simple_plots(select_mouse=None): +def simple_plots(select_mouse=None, date_selected_by='days_back', **kwargs): + """ + Retrieves filepaths based on the method specified by date_selected_by. + + Args: + date_selected_by (str): Method to select files. + 'days_back' - uses 'days_back' kwarg. + 'range' - uses 'start_date' and 'end_date' kwargs. + **kwargs: Arbitrary keyword arguments. + Expected for 'days_back': days_back (int) + Expected for 'range': start_date (datetime.date or str 'YYYY-MM-DD'), + end_date (datetime.date or str 'YYYY-MM-DD') + + Returns: + list: A list of filepaths, or None if an error occurs. + """ plot_single_mouse_plots = True + save_folder = "C:\\Users\\shich\\OneDrive - Johns Hopkins\\ShulerLab\\behavior_code\\summary_graphs" + if select_mouse is None: dif = date.today() - start_date data = gen_data(get_today_filepaths(days_back=dif.days), select_mouse=select_mouse) info = gen_data(get_today_filepaths(days_back=dif.days), select_mouse=select_mouse, return_info=True) else: - data = gen_data(get_today_filepaths(days_back=1000), select_mouse=select_mouse) - info = gen_data(get_today_filepaths(days_back=1000), select_mouse=select_mouse, return_info=True) + if date_selected_by == 'days_back': + data = gen_data(get_today_filepaths(days_back=1000), select_mouse=select_mouse) + info = gen_data(get_today_filepaths(days_back=1000), select_mouse=select_mouse, return_info=True) + elif date_selected_by == 'range': + start_date_input = kwargs.get('start_date') + end_date_input = kwargs.get('end_date') + if start_date_input is None or end_date_input is None: + print("Error: 'start_date' and 'end_date' arguments are required when date_selected_by='range'.") + return None + try: + # Convert string dates to datetime.date objects if they are strings + if isinstance(start_date_input, str): + start_date_obj = datetime.strptime(start_date_input, '%Y-%m-%d').date() + elif isinstance(start_date_input, datetime): # if it's a datetime object + start_date_obj = start_date_input.date() + elif hasattr(start_date_input, 'date'): # for pandas Timestamp + start_date_obj = start_date_input.date() + else: # assume it's already a date object + start_date_obj = start_date_input + if isinstance(end_date_input, str): + end_date_obj = datetime.strptime(end_date_input, '%Y-%m-%d').date() + elif isinstance(end_date_input, datetime): + end_date_obj = end_date_input.date() + elif hasattr(end_date_input, 'date'): + end_date_obj = end_date_input.date() + else: + end_date_obj = end_date_input + if start_date_obj > end_date_obj: + print("Error: start_date cannot be after end_date.") + data = gen_data(get_dateranged_filepaths(start_date_obj, end_date_obj), select_mouse=select_mouse) + info = gen_data(get_dateranged_filepaths(start_date_obj, end_date_obj), select_mouse=select_mouse, + return_info=True) + except ValueError as ve: + print( + f"Error processing date for 'range' selection: {ve}. Ensure dates are 'YYYY-MM-DD' strings or date objects.") + return None + except Exception as e: + print(f"An error occurred in get_dateranged_filepaths: {e}") + return None + else: + print(f"Error: Unknown value for date_selected_by: '{date_selected_by}'. " + "Allowed values are 'days_back' or 'range'.") + return None + block_leaves_last10 = pd.DataFrame() for mouse in data.keys(): if select_mouse is not None and mouse not in select_mouse: @@ -405,74 +715,114 @@ def simple_plots(select_mouse=None): consumption = pd.DataFrame() block_leaves = pd.DataFrame() reentry = pd.DataFrame() + premature_leave = pd.DataFrame() + for i, session in enumerate(data[mouse]): + print(mouse, ' ', i) if info[mouse][i]['task'] == 'cued_forgo_forced': continue try: session = merge_old_trials(session) engaged_df = percent_engaged(session) - engaged_df['day'] = [i] * len(engaged_df) + engaged_df['session'] = [i] * len(engaged_df) engaged = pd.concat([engaged, engaged_df]) consumption_df = consumption_time(session) - consumption_df['day'] = [i] * len(consumption_df) + consumption_df['session'] = [i] * len(consumption_df) consumption = pd.concat([consumption, consumption_df]) block_leaves_df = block_leave_times(session) - block_leaves_df['day'] = [i] * len(block_leaves_df) + block_leaves_df['session'] = [i] * len(block_leaves_df) block_leaves = pd.concat([block_leaves, block_leaves_df]) reentry_df = reentry_index(session) - reentry_df['day'] = [i] * len(reentry_df) + reentry_df['session'] = [i] * len(reentry_df) reentry = pd.concat([reentry, reentry_df]) + + premature_leave_df = calculate_premature_leave(session) + premature_leave_df['session'] = [i] * len(premature_leave_df) + premature_leave = pd.concat([premature_leave, premature_leave_df]) + except Exception as e: - raise e + print(f"Error processing session {i} for mouse {mouse}: {e}") + raise engaged.sort_values('block', inplace=True) block_leaves.sort_values('block', inplace=True) if plot_single_mouse_plots: - fig, axes = plt.subplots(2, 2, figsize=[11, 8], layout="constrained") - sns.lineplot(data=block_leaves.reset_index(), x='day', y='leave time', hue='block', ax=axes[0, 0], + fig, axes = plt.subplots(3, 2, figsize=[11, 8], layout="constrained") + sns.lineplot(data=block_leaves.reset_index(), x='session', y='leave time', hue='block', style='block', + markers=True, ax=axes[0, 0], palette='Set2') - add_h_lines(data=block_leaves.reset_index(), x='day', y='leave time', hue='block', ax=axes[0, 0], + add_h_lines(data=block_leaves.reset_index(), x='session', y='leave time', hue='block', ax=axes[0, 0], palette='Set2') - sns.lineplot(data=consumption.reset_index(), x='day', y='consumption time', hue='port', ax=axes[0, 1], + sns.lineplot(data=consumption.reset_index(), x='session', y='consumption time', hue='port', style='port', + markers=True, ax=axes[1, 0], palette='Set1', estimator=np.median) - add_h_lines(data=consumption.reset_index(), x='day', y='consumption time', hue='port', ax=axes[0, 1], + add_h_lines(data=consumption.reset_index(), x='session', y='consumption time', hue='port', ax=axes[1, 0], palette='Set1', estimator='median') - sns.lineplot(data=engaged.reset_index(), x='day', y='reward rate', hue='block', ax=axes[1, 0], + sns.lineplot(data=engaged.reset_index(), x='session', y='reward rate', hue='block', style='block', + markers=True, ax=axes[1, 1], + palette='Set2') + add_h_lines(data=engaged.reset_index(), x='session', y='reward rate', hue='block', ax=axes[1, 1], + palette='Set2') + sns.lineplot(data=engaged.reset_index(), x='session', y='percent engaged', hue='block', style='block', + markers=True, ax=axes[2, 0], palette='Set2') - add_h_lines(data=engaged.reset_index(), x='day', y='reward rate', hue='block', ax=axes[1, 0], + add_h_lines(data=engaged.reset_index(), x='session', y='percent engaged', hue='block', ax=axes[2, 0], palette='Set2') - sns.lineplot(data=engaged.reset_index(), x='day', y='percent engaged', hue='block', ax=axes[1, 1], + # sns.lineplot(data=premature_leave.reset_index(), x='session', y='premature leave rate', hue='block', + # style='block', markers=True, ax=axes[2, 1], + # palette='Set2') + # add_h_lines(data=premature_leave.reset_index(), x='session', y='premature leave rate', hue='block', + # ax=axes[2, 1], + # palette='Set2') + sns.lineplot(data=reentry.reset_index(), x='session', y='bg_reentry_index', hue='block', ax=axes[2, 1], palette='Set2') - add_h_lines(data=engaged.reset_index(), x='day', y='percent engaged', hue='block', ax=axes[1, 1], + add_h_lines(data=reentry.reset_index(), x='session', y='bg_reentry_index', hue='block', ax=axes[2, 1], palette='Set2') + # axes[2, 0].axhline(y=0.2, color='red', linestyle='--', linewidth=1.5, label='Threshold = 0.2') + # # Add legend to show the line label + # axes[2, 0].legend() + axes[0, 0].set_title('Leave Time by Block') - axes[0, 1].set_title('Consumption Time by Port') - axes[1, 0].set_title('Reward Rate by Block') - axes[1, 1].set_title('Percent Time Engaged by Block') + axes[1, 0].set_title('Consumption Time by Port') + axes[1, 1].set_title('Reward Rate by Block') + axes[2, 0].set_title('Percent Time Engaged by Block') + # axes[2, 1].set_title('Premature leave from BG port by Block') + axes[2, 1].set_title('Background Reentry Index') axes[0, 0].set_ylim([0, 20]) - axes[0, 1].set_ylim([0, 20]) - axes[1, 0].set_ylim([0, .65]) - axes[1, 1].set_ylim([0, 1]) + axes[1, 0].set_ylim([0, 20]) + axes[1, 1].set_ylim([0, .65]) + axes[2, 0].set_ylim([0, 1]) + # axes[2, 1].set_ylim([0, 1]) + axes[2, 1].set_ylim([0.98, 3]) plt.suptitle(mouse, fontsize=20) + os.makedirs(save_folder, exist_ok=True) + # Construct the filename + filename = f'{mouse}_session_summary.png' + save_path = os.path.join(save_folder, filename) + plt.savefig(save_path, dpi=300, bbox_inches='tight') + print(f"Graph saved to: {save_path}") plt.show() - block_leaves_last10_df = block_leaves[(block_leaves.day >= block_leaves.day.max() - 10)].groupby('block')[ - 'leave time'].mean().reset_index() + block_leaves_last10_df = \ + block_leaves[(block_leaves.session >= block_leaves.session.max() - 10)].groupby('block')[ + 'leave time'].mean().reset_index() block_leaves_last10_df['animal'] = mouse block_leaves_last10 = pd.concat([block_leaves_last10, block_leaves_last10_df]) - fig, axes = plt.subplots(1, 1) + fig, axes = plt.subplots(1, 1, figsize=[5, 10]) sns.boxplot(data=block_leaves_last10.reset_index(), x='block', y='leave time') for mouse in data.keys(): plt.plot([-0.1, 0.9], block_leaves_last10[block_leaves_last10.animal == mouse]['leave time'], 'o-', - color='darkgray') - plt.show() + color='darkgray', label=mouse[-3:]) + labelLines(plt.gca().get_lines(), align=True, zorder=2.5, fontsize=7, xvals=(0.3, 0.8)) + plt.ylim([0, 14.5]) + fig.show() def single_session(select_mouse=None, num_back=2): @@ -489,10 +839,18 @@ def single_session(select_mouse=None, num_back=2): for i in range(1, num_back + 1): last_session = data[mouse][-i] last_info = info[mouse][-i] - session_summary(last_session, mouse, last_info) + trial_df = construct_trial_df(last_session) + if len(trial_df) <= 10: + continue + plot_kaplan_meier(trial_df, last_info) + # perform_log_rank_test(trial_df) + # session_summary(last_session, mouse, last_info) def session_summary(data, mouse, info): + base_save_folder = save_folder = "C:\\Users\\shich\\OneDrive - Johns Hopkins\\ShulerLab\\behavior_code\\each_session" + save_folder = os.path.join(base_save_folder, mouse) + os.makedirs(save_folder, exist_ok=True) fig, [ax1, ax2] = plt.subplots(1, 2, figsize=[10, 10]) port_palette = sns.color_palette('Set1') block_palette = sns.color_palette('Set2') @@ -501,8 +859,8 @@ def session_summary(data, mouse, info): head = data.key == 'head' lick = data.key == 'lick' reward = data.key == 'reward' - port1 = data.port == 1 - port2 = data.port == 2 + bgport = data.port == data.loc[data['key'] == 'background', 'port'].iloc[-1] + expport = data.port == data.loc[data['key'] == 'exp_decreasing', 'port'].iloc[-1] max_trial = data.trial.max() bg_rectangles = [] @@ -517,7 +875,7 @@ def session_summary(data, mouse, info): bg_lengths = [] exp_lengths = [] trial_blocks = data.groupby(['trial'])['phase'].agg(pd.Series.mode) - blocks = data.phase.unique() + blocks = data.phase.dropna().unique() blocks.sort() for trial in data.trial.unique(): if np.isnan(trial): @@ -525,13 +883,13 @@ def session_summary(data, mouse, info): is_trial = data.trial == trial try: trial_start = data[is_trial & start & (data.key == 'trial')].session_time.values[0] - trial_middle = data[is_trial & end & (data.key == 'LED') & port2].session_time.values[0] + trial_middle = data[is_trial & end & (data.key == 'LED') & bgport].session_time.values[0] trial_end = data[is_trial & end & (data.key == 'trial')].session_time.values[0] except IndexError: continue - bg_rewards = data[is_trial & start & port2 & reward].session_time.values - exp_rewards = data[is_trial & start & port1 & reward].session_time.values + bg_rewards = data[is_trial & start & bgport & reward].session_time.values + exp_rewards = data[is_trial & start & expport & reward].session_time.values bg_licks = data[is_trial & start & lick & (data.session_time < trial_middle)].session_time.values exp_licks = data[is_trial & start & lick & (data.session_time > trial_middle)].session_time.values @@ -548,10 +906,13 @@ def session_summary(data, mouse, info): exp_rectangles_in_bg.append(Rectangle((s - trial_start, trial), e - s, .7)) for [s, e] in exp_intervals: exp_rectangles.append(Rectangle((s - trial_middle, trial), e - s, .7)) - if np.where(blocks == trial_blocks.loc[trial])[0][0] == 0: + + block_value = trial_blocks.loc[trial] + if block_value == "0.4": block1_rectangles.append(Rectangle((0, trial), 100, 1)) else: block2_rectangles.append(Rectangle((0, trial), 100, 1)) + bg_reward_events.append(bg_rewards - trial_start) exp_reward_events.append(exp_rewards - trial_middle) bg_lick_events.append(bg_licks - trial_start) @@ -590,6 +951,14 @@ def session_summary(data, mouse, info): session_summary_axis_settings([ax1, ax2], max_trial) plt.suptitle(f'{mouse}: {info["date"]} {info["time"]}') + + # Construct the filename + filename = f'{mouse}_{info["date"]}_{info["time"]}.png' + save_path = os.path.join(save_folder, filename) + + # Save the plot + plt.savefig(save_path, dpi=300, bbox_inches='tight') + print(f"Graph saved to: {save_path}") plt.show() @@ -608,10 +977,14 @@ def session_summary_axis_settings(axes, max_trial): if __name__ == '__main__': - # mice = ['ES057', 'ES058', 'ES059', 'ES060', 'ES061', 'ES062'] - # mice = ['ES045', 'ES046', 'ES047', 'ES051', 'ES052', 'ES053', 'ES057', 'ES060', 'ES061', 'ES062'] - # mice = ['ES058', 'ES059', 'ES045', 'ES047'] - mice = ['ES057', 'ES046'] - # mice = ['ES051', 'ES052', 'ES053', 'ES060', 'ES061', 'ES062'] - simple_plots(mice) - # single_session(mice) + # mice = ['SZ050', 'SZ051', 'SZ052', 'SZ053', 'SZ054', 'SZ055', 'SZ056', 'SZ057', 'SZ058', 'SZ059'] + # mice = ['SZ055', 'SZ056', 'SZ057', 'SZ058', 'SZ059'] + # mice = ['SZ036','SZ037','SZ038','SZ039','SZ041','SZ042','SZ043','SZ050','SZ051','SZ052','SZ055'] # all multi-reward mice + # mice = ['SZ044', 'SZ045', 'SZ046', 'SZ047', 'SZ048', 'SZ053', 'SZ054', 'SZ058', 'SZ059'] # all single-reward mice + # mice = ['SZ036', 'SZ037', 'SZ038', 'SZ039', 'SZ042', 'SZ043', 'RK007', 'RK008', 'RK009', 'RK010'] + # mice = ['SZ036', 'SZ037', 'SZ038', 'SZ039', 'SZ042', 'SZ043'] + # single_session(mice, num_back=90) + # simple_plots(mice, date_selected_by='days_back') + mice = ['RK007', 'RK008', 'RK009', 'RK010'] + # single_session(mice, num_back=34) + simple_plots(mice, date_selected_by='range', start_date='2025-05-01', end_date='2025-05-24') diff --git a/stand_alone/durations.pkl b/stand_alone/durations.pkl new file mode 100644 index 0000000..7ff68c9 Binary files /dev/null and b/stand_alone/durations.pkl differ diff --git a/stand_alone/gui.py b/stand_alone/gui.py index 5127c4e..162a00c 100644 --- a/stand_alone/gui.py +++ b/stand_alone/gui.py @@ -19,7 +19,7 @@ # scp -r "C:\Users\Shichen\OneDrive - Johns Hopkins\ShulerLab\behavior_code\stand_alone" pi@elissapi1:\home\pi\behavior1 pastel_colors = ['#ffffcc', '#99ccff', '#cc99ff', '#ff99cc', '#ffcc99', '#ffffcc', '#99ffcc', '#ccffff', '#ccccff', - '#ffccff', '#ffcccc', '#D3D3D3'] + '#ffccff', '#ffcccc', '#D3D3D3', '#f0a207'] class Gui: diff --git a/stand_alone/support_classes.py b/stand_alone/support_classes.py index b5ad92a..9446ced 100644 --- a/stand_alone/support_classes.py +++ b/stand_alone/support_classes.py @@ -234,8 +234,8 @@ def __init__(self, name, dist_info, duration=None): durations = pickle.load(f) self.base_duration = durations[name] - pins = {1: [4, 27, 17, 9], - 2: [18, 24, 23, 11]} + pins = {2: [4, 27, 17, 9], + 1: [18, 24, 23, 11]} self.name = name [self.led_pin, self.ir_head_pin, self.ir_lick_pin, self.sol_pin] = pins[name] # self.led_pin = led_pin diff --git a/stand_alone/user_settings.py b/stand_alone/user_settings.py new file mode 100644 index 0000000..d1814ba --- /dev/null +++ b/stand_alone/user_settings.py @@ -0,0 +1,49 @@ +import os + + +def get_user_info(): + info_dict = { + 'initials': 'SZ', + 'mouse_buttons': [['SZ050', 'SZ051', 'SZ052'], + ['SZ053', 'SZ054', 'SZ055'], + ['SZ056', 'SZ057', 'SZ058'], + ['SZ059', 'cf_testmouse', 'sr_testmouse']], + 'button_colors': [[12, 12, 12], + [12, 12, 2], + [2, 2, 2], + [2, 3, 3]], + 'mouse_assignments': { + 'SZ050': 'cued_forgo', + 'SZ051': 'cued_forgo', + 'SZ052': 'cued_forgo', + 'SZ053': 'single_reward', + 'SZ054': 'single_reward', + 'SZ055': 'cued_forgo', + 'SZ056': 'cued_forgo', + 'SZ057': 'cued_forgo', + 'SZ058': 'single_reward', + 'SZ059': 'single_reward', + 'sr_testmouse': 'single_reward', + 'cf_testmouse': 'cued_forgo' + }, + 'mouse_colors': { + 'SZ050': 12, + 'SZ051': 12, + 'SZ052': 12, + 'SZ053': 12, + 'SZ054': 12, + 'SZ055': 2, + 'SZ056': 2, + 'SZ057': 2, + 'SZ058': 2, + 'SZ059': 2, + 'sr_testmouse': 3, + 'cf_testmouse': 3 + }, + 'desktop_ip': '10.16.80.130', + 'desktop_user': 'Shichen', + 'desktop_password': 'shuler_914WBSB', + 'desktop_user_root': os.path.join('C:/', 'Users', 'Shichen'), + 'desktop_save_path': os.path.join('OneDrive - Johns Hopkins', 'ShulerLab', 'behavior_code', 'data'), + } + return info_dict \ No newline at end of file diff --git a/stand_alone/user_settings_generic.py b/stand_alone/user_settings_generic.py index fb41112..d83eaca 100644 --- a/stand_alone/user_settings_generic.py +++ b/stand_alone/user_settings_generic.py @@ -12,6 +12,9 @@ def get_user_info(): 'mouse_buttons': [['MOUSE_NAME', 'MOUSE_NAME', 'MOUSE_NAME'], ['MOUSE_NAME', 'MOUSE_NAME', 'MOUSE_NAME'], ['MOUSE_NAME', 'MOUSE_NAME', 'MOUSE_NAME']], + 'button_colors': [[1,1,1], + [2,2,2], + [3,3,3]], 'mouse_assignments': { 'MOUSE_NAME': 'TASK_NAME', 'testmouse': 'TASK_NAME', @@ -20,6 +23,7 @@ def get_user_info(): 'MOUSE_NAME': 1, 'testmouse': 1, }, + 'desktop_ip': 'PUT_IP_ADDRESS_HERE', 'desktop_user': 'PUT_USERNAME_HERE', 'desktop_password': 'PUT_PASSWORD_HERE', @@ -27,3 +31,48 @@ def get_user_info(): 'desktop_save_path': os.path.join('PATH', 'TO', 'DATA', 'FOLDER'), } return info_dict + +import os + + +def get_user_info(): + info_dict = { + 'initials': 'ES', + 'mouse_buttons': [['ES041', 'ES042', 'ES037'], + ['ES043', 'ES044', 'ES039'], + ['ES045', 'ES046', 'ES047']], + 'mouse_assignments': { + 'ES036': 'single_reward', + 'ES037': 'single_reward', + 'ES039': 'cued_forgo', + 'ES040': 'cued_forgo', + 'ES041': 'single_reward', + 'ES042': 'single_reward', + 'ES043': 'single_reward', + 'ES044': 'single_reward', + 'ES045': 'cued_forgo', + 'ES046': 'cued_forgo', + 'ES047': 'cued_forgo', + 'testmouse': 'cued_forgo', + }, + 'mouse_colors': { + 'ES036': 1, + 'ES037': 1, + 'ES039': 2, + 'ES040': 2, + 'ES041': 3, + 'ES042': 3, + 'ES043': 3, + 'ES044': 3, + 'ES045': 4, + 'ES046': 4, + 'ES047': 4, + 'testmouse': 1, + }, + 'desktop_ip': '10.16.79.143', + 'desktop_user': 'Elissa', + 'desktop_password': 'shuler', + 'desktop_user_root': os.path.join('C:/', 'Users', 'Elissa'), + 'desktop_save_path': os.path.join('GoogleDrive', 'Code', 'Python', 'behavior_code', 'data'), + } + return info_dict diff --git a/summary_graphs/RK001-006_block_comparison.png b/summary_graphs/RK001-006_block_comparison.png new file mode 100644 index 0000000..0e0574b Binary files /dev/null and b/summary_graphs/RK001-006_block_comparison.png differ diff --git a/summary_graphs/RK001_session_summary.png b/summary_graphs/RK001_session_summary.png new file mode 100644 index 0000000..acf2dcc Binary files /dev/null and b/summary_graphs/RK001_session_summary.png differ diff --git a/summary_graphs/RK003_session_summary.png b/summary_graphs/RK003_session_summary.png new file mode 100644 index 0000000..d073544 Binary files /dev/null and b/summary_graphs/RK003_session_summary.png differ diff --git a/summary_graphs/RK005_session_summary.png b/summary_graphs/RK005_session_summary.png new file mode 100644 index 0000000..677a36c Binary files /dev/null and b/summary_graphs/RK005_session_summary.png differ diff --git a/summary_graphs/RK006_session_summary.png b/summary_graphs/RK006_session_summary.png new file mode 100644 index 0000000..09b13b6 Binary files /dev/null and b/summary_graphs/RK006_session_summary.png differ diff --git a/summary_graphs/RK007_session_summary.png b/summary_graphs/RK007_session_summary.png new file mode 100644 index 0000000..87f93af Binary files /dev/null and b/summary_graphs/RK007_session_summary.png differ diff --git a/summary_graphs/RK008_session_summary.png b/summary_graphs/RK008_session_summary.png new file mode 100644 index 0000000..ded5ddc Binary files /dev/null and b/summary_graphs/RK008_session_summary.png differ diff --git a/summary_graphs/RK009_session_summary.png b/summary_graphs/RK009_session_summary.png new file mode 100644 index 0000000..2bdabde Binary files /dev/null and b/summary_graphs/RK009_session_summary.png differ diff --git a/summary_graphs/RK010_session_summary.png b/summary_graphs/RK010_session_summary.png new file mode 100644 index 0000000..85899a0 Binary files /dev/null and b/summary_graphs/RK010_session_summary.png differ diff --git a/summary_graphs/SZ036-043_block_comparison.png b/summary_graphs/SZ036-043_block_comparison.png new file mode 100644 index 0000000..44339b7 Binary files /dev/null and b/summary_graphs/SZ036-043_block_comparison.png differ diff --git a/summary_graphs/SZ036_session_summary.png b/summary_graphs/SZ036_session_summary.png new file mode 100644 index 0000000..3aae914 Binary files /dev/null and b/summary_graphs/SZ036_session_summary.png differ diff --git a/summary_graphs/SZ037_session_summary.png b/summary_graphs/SZ037_session_summary.png new file mode 100644 index 0000000..7632ae1 Binary files /dev/null and b/summary_graphs/SZ037_session_summary.png differ diff --git a/summary_graphs/SZ038_session_summary.png b/summary_graphs/SZ038_session_summary.png new file mode 100644 index 0000000..0689cf6 Binary files /dev/null and b/summary_graphs/SZ038_session_summary.png differ diff --git a/summary_graphs/SZ039_session_summary.png b/summary_graphs/SZ039_session_summary.png new file mode 100644 index 0000000..2c6e417 Binary files /dev/null and b/summary_graphs/SZ039_session_summary.png differ diff --git a/summary_graphs/SZ042_session_summary.png b/summary_graphs/SZ042_session_summary.png new file mode 100644 index 0000000..b5e2c0c Binary files /dev/null and b/summary_graphs/SZ042_session_summary.png differ diff --git a/summary_graphs/SZ043_session_summary.png b/summary_graphs/SZ043_session_summary.png new file mode 100644 index 0000000..805a74d Binary files /dev/null and b/summary_graphs/SZ043_session_summary.png differ diff --git a/summary_graphs/halfway_trained/RK007_session_summary.png b/summary_graphs/halfway_trained/RK007_session_summary.png new file mode 100644 index 0000000..53d23c4 Binary files /dev/null and b/summary_graphs/halfway_trained/RK007_session_summary.png differ diff --git a/summary_graphs/halfway_trained/RK008_session_summary.png b/summary_graphs/halfway_trained/RK008_session_summary.png new file mode 100644 index 0000000..92505a6 Binary files /dev/null and b/summary_graphs/halfway_trained/RK008_session_summary.png differ diff --git a/summary_graphs/halfway_trained/RK009_session_summary.png b/summary_graphs/halfway_trained/RK009_session_summary.png new file mode 100644 index 0000000..6142e4d Binary files /dev/null and b/summary_graphs/halfway_trained/RK009_session_summary.png differ diff --git a/summary_graphs/halfway_trained/RK010_session_summary.png b/summary_graphs/halfway_trained/RK010_session_summary.png new file mode 100644 index 0000000..a0d4e44 Binary files /dev/null and b/summary_graphs/halfway_trained/RK010_session_summary.png differ diff --git a/summary_graphs/summary_box_plot.png b/summary_graphs/summary_box_plot.png new file mode 100644 index 0000000..be24685 Binary files /dev/null and b/summary_graphs/summary_box_plot.png differ diff --git a/user_info.py b/user_info.py index e11feb9..f823474 100644 --- a/user_info.py +++ b/user_info.py @@ -3,9 +3,9 @@ def get_user_info(): info_dict = { - 'initials': 'ES', - 'pi_names': ['elissapi0', 'elissapi2', 'elissapi4'], + 'initials': ['SZ', 'RK'], + 'pi_names': ['elissapi1', 'shichenpi2', 'shichenpi3'], # 'pi_names': ['elissapi1'], - 'start_date': date(2023, 10, 11) # For the current cohort, for the sake of simple_plots.py + 'start_date': date(2024, 5, 20) # For the current cohort, for the sake of simple_plots.py } return info_dict