From 3741d0469bb4f7495c3ca093e38bf9c4cef23557 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Fri, 25 Oct 2024 15:44:38 -0700 Subject: [PATCH 01/42] adding functionality to alert user of percent of same or cross side lick interval under 100ms --- src/foraging_gui/Foraging.py | 8 +++++ src/foraging_gui/MyFunctions.py | 58 +++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 4544e952b..ea1656fef 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -2404,6 +2404,12 @@ def _LickSta(self): toolbar.setMaximumWidth(300) layout.addWidget(toolbar) layout.addWidget(PlotLick) + # add text labels to indicate lick interval percentages + self.same_side_lick_interval = QtWidgets.QLabel() + self.cross_side_lick_interval = QtWidgets.QLabel() + layout.addWidget(self.same_side_lick_interval) + layout.addWidget(self.cross_side_lick_interval) + self.LickSta_ToInitializeVisual=0 try: if hasattr(self, 'GeneratedTrials'): @@ -4055,6 +4061,8 @@ def _Start(self): GeneratedTrials._GenerateATrial(self.Channel4) # delete licks from the previous session GeneratedTrials._DeletePreviousLicks(self.Channel2) + GeneratedTrials.lick_interval_time.start() # start lick interval calculation + if self.Start.isChecked(): # if session log handler is not none, stop logging for previous session diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index 9cb4c9687..4e59d5e24 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -25,6 +25,9 @@ class GenerateTrials(): def __init__(self,win): self.win=win + self.B_LeftLickIntervalPercent = None # percentage of left lick intervals under 100ms + self.B_RightLickIntervalPercent = None # percentage of right lick intervals under 100ms + self.B_CrossSideIntervalPercent = None # percentage of cross side lick intervals under 100ms self.B_Bias = [0] # lick bias self.B_RewardFamilies=self.win.RewardFamilies self.B_CurrentTrialN=-1 # trial number starts from 0; Update when trial starts @@ -104,7 +107,10 @@ def __init__(self,win): self.Obj={} # get all of the training parameters of the current trial self._GetTrainingParameters(self.win) - + + # create timer to calculate lick intervals every 10 minutes + self.lick_interval_time = QtCore.QTimer(timeout=self.calculate_inter_lick_intervals, interval=600000) + def _GenerateATrial(self,Channel4): self.finish_select_par=0 if self.win.UpdateParameters==1: @@ -890,7 +896,55 @@ def _LickSta(self,Trials=None): self.Start_CoCue_LeftRightRatio=np.nan else: self.Start_CoCue_LeftRightRatio=np.array(sum(self.Start_GoCue_LeftLicks))/np.array(sum(self.Start_GoCue_RightLicks)) - + + def calculate_inter_lick_intervals(self): + """ + Calculate and categorize lick intervals + """ + + right = self.B_RightLickTime + left = self.B_LeftLickTime + + if right is not None and left is not None: + # calculate left interval and fraction + same_side_l = np.diff(left) + same_side_l_frac = len(same_side_l[~(same_side_l > 100)]) / len(same_side_l) + logging.info(f'Percentage of left lick intervals under 100 ms is {same_side_l_frac * 100}%.') + self.B_LeftLickIntervalPercent = same_side_l_frac * 100 + + # calculate left interval and fraction + same_side_r = np.diff(right) + same_side_r_frac = len(same_side_r[~(same_side_r > 100)]) / len(same_side_r) + logging.info(f'Percentage of right lick intervals under 100 ms is {same_side_r_frac * 100}%.') + self.B_RightLickIntervalPercent = same_side_r_frac * 100 + + if same_side_l_frac + same_side_r_frac >= .1: + self.win.same_side_lick_interval.setText(f'Percentage of same side lick intervals under 100 ms is ' + f'over 10%: {(same_side_r_frac+same_side_l_frac) * 100}%.') + else: + self.win.same_side_lick_interval.setText('') + + # calculate cross side interval and frac + dummy_array = np.ones(right.shape) # use array to tell lick side after merging + stacked_right = np.column_stack((dummy_array, right)) + stacked_left = np.column_stack((np.negative(dummy_array), left)) + merged_sorted = sorted(np.concatenate((stacked_right, stacked_left)), + key=lambda x: x[1]) # sort by second element + cross_sides = [] + for i in range(1, len(right)): + if merged_sorted[i - 1][0] == -merged_sorted[i][0]: # cross side lick + cross_sides.append(merged_sorted[i][1] - merged_sorted[i - 1][1]) + cross_sides = np.array(cross_sides) + cross_side_frac = len(cross_sides[~(cross_sides > 100)]) / len(cross_sides) + logging.info(f'Percentage of cross side lick intervals under 100 ms is {cross_side_frac * 100}%.') + self.B_CrossSideIntervalPercent = cross_side_frac * 100 + + if cross_side_frac >= .1: + self.win.cross_side_lick_interval.setText(f'Percentage of cross side lick intervals under 100 ms is ' + f'over 10%: {(same_side_r_frac+same_side_l_frac) * 100}%.') + else: + self.win.cross_side_lick_interval.setText('') + def _GetDoubleDipping(self,LicksIndex): '''get the number of double dipping. e.g. 0 1 0 will result in 2 double dipping''' DoubleDipping=np.sum(np.diff(LicksIndex)!=0) From 9eae80db9dc14e6a0c6e93db90a8ed7d31406ce8 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Fri, 25 Oct 2024 15:53:11 -0700 Subject: [PATCH 02/42] fixing len of 0 bug --- src/foraging_gui/Foraging.py | 3 +++ src/foraging_gui/MyFunctions.py | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index ea1656fef..aa99416c9 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -4001,6 +4001,9 @@ def _Start(self): b_bias_len = len(self.GeneratedTrials.B_Bias) self.GeneratedTrials.B_Bias += [last_bias]*((self.GeneratedTrials.B_CurrentTrialN+1)-b_bias_len) + # stop lick interval calculation + self.GeneratedTrials.lick_interval_time.stop() # stop lick interval calculation + if (self.StartANewSession == 1) and (self.ANewTrial == 0): # If we are starting a new session, we should wait for the last trial to finish self._StopCurrentSession() diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index 4e59d5e24..601286c62 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -904,17 +904,17 @@ def calculate_inter_lick_intervals(self): right = self.B_RightLickTime left = self.B_LeftLickTime - - if right is not None and left is not None: + threshold = .1 + if right is not None and left is not None and len(right) > 0 and len(left) > 0: # calculate left interval and fraction same_side_l = np.diff(left) - same_side_l_frac = len(same_side_l[~(same_side_l > 100)]) / len(same_side_l) + same_side_l_frac = len(same_side_l[~(same_side_l > threshold)]) / len(same_side_l) logging.info(f'Percentage of left lick intervals under 100 ms is {same_side_l_frac * 100}%.') self.B_LeftLickIntervalPercent = same_side_l_frac * 100 # calculate left interval and fraction same_side_r = np.diff(right) - same_side_r_frac = len(same_side_r[~(same_side_r > 100)]) / len(same_side_r) + same_side_r_frac = len(same_side_r[~(same_side_r > threshold)]) / len(same_side_r) logging.info(f'Percentage of right lick intervals under 100 ms is {same_side_r_frac * 100}%.') self.B_RightLickIntervalPercent = same_side_r_frac * 100 @@ -935,7 +935,7 @@ def calculate_inter_lick_intervals(self): if merged_sorted[i - 1][0] == -merged_sorted[i][0]: # cross side lick cross_sides.append(merged_sorted[i][1] - merged_sorted[i - 1][1]) cross_sides = np.array(cross_sides) - cross_side_frac = len(cross_sides[~(cross_sides > 100)]) / len(cross_sides) + cross_side_frac = len(cross_sides[~(cross_sides > threshold)]) / len(cross_sides) logging.info(f'Percentage of cross side lick intervals under 100 ms is {cross_side_frac * 100}%.') self.B_CrossSideIntervalPercent = cross_side_frac * 100 From 892ba771c4bf036c09f960a797a4c326a1b30808 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Mon, 28 Oct 2024 08:02:51 -0700 Subject: [PATCH 03/42] restart timer if continuing trial --- src/foraging_gui/Foraging.py | 3 ++- src/foraging_gui/MyFunctions.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index aa99416c9..f4c961c77 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -3844,6 +3844,8 @@ def _Start(self): self.Start.setChecked(False) logging.info('User declines continuation of session') return + self.GeneratedTrials.lick_interval_time.start() # restart lick interval calculation + # check experimenter name reply = QMessageBox.critical(self, 'Box {}, Start'.format(self.box_letter), @@ -4066,7 +4068,6 @@ def _Start(self): GeneratedTrials._DeletePreviousLicks(self.Channel2) GeneratedTrials.lick_interval_time.start() # start lick interval calculation - if self.Start.isChecked(): # if session log handler is not none, stop logging for previous session if self.session_log_handler is not None: diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index 601286c62..7eb8083a6 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -109,7 +109,8 @@ def __init__(self,win): self._GetTrainingParameters(self.win) # create timer to calculate lick intervals every 10 minutes - self.lick_interval_time = QtCore.QTimer(timeout=self.calculate_inter_lick_intervals, interval=600000) + #self.lick_interval_time = QtCore.QTimer(timeout=self.calculate_inter_lick_intervals, interval=600000) + self.lick_interval_time = QtCore.QTimer(timeout=self.calculate_inter_lick_intervals, interval=100) def _GenerateATrial(self,Channel4): self.finish_select_par=0 @@ -905,6 +906,7 @@ def calculate_inter_lick_intervals(self): right = self.B_RightLickTime left = self.B_LeftLickTime threshold = .1 + print(right, left, 'right and left lick time') if right is not None and left is not None and len(right) > 0 and len(left) > 0: # calculate left interval and fraction same_side_l = np.diff(left) From ccc88e73cf5a7f8308035021e96169519177beae Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 29 Oct 2024 08:17:05 -0700 Subject: [PATCH 04/42] fixing comments and formating --- src/foraging_gui/MyFunctions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index 7eb8083a6..b74c78617 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -906,15 +906,15 @@ def calculate_inter_lick_intervals(self): right = self.B_RightLickTime left = self.B_LeftLickTime threshold = .1 - print(right, left, 'right and left lick time') + if right is not None and left is not None and len(right) > 0 and len(left) > 0: - # calculate left interval and fraction + # calculate left interval and fraction same_side_l = np.diff(left) same_side_l_frac = len(same_side_l[~(same_side_l > threshold)]) / len(same_side_l) logging.info(f'Percentage of left lick intervals under 100 ms is {same_side_l_frac * 100}%.') self.B_LeftLickIntervalPercent = same_side_l_frac * 100 - # calculate left interval and fraction + # calculate right interval and fraction same_side_r = np.diff(right) same_side_r_frac = len(same_side_r[~(same_side_r > threshold)]) / len(same_side_r) logging.info(f'Percentage of right lick intervals under 100 ms is {same_side_r_frac * 100}%.') @@ -943,7 +943,7 @@ def calculate_inter_lick_intervals(self): if cross_side_frac >= .1: self.win.cross_side_lick_interval.setText(f'Percentage of cross side lick intervals under 100 ms is ' - f'over 10%: {(same_side_r_frac+same_side_l_frac) * 100}%.') + f'over 10%: {(same_side_r_frac+same_side_l_frac) * 100}%.') else: self.win.cross_side_lick_interval.setText('') From 6cb2c97f104beb462466f12553ea851543c7b12e Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 30 Oct 2024 11:53:27 -0700 Subject: [PATCH 05/42] bug fixes and simplified logic --- src/foraging_gui/MyFunctions.py | 42 ++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index b74c78617..becb47239 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -907,43 +907,51 @@ def calculate_inter_lick_intervals(self): left = self.B_LeftLickTime threshold = .1 - if right is not None and left is not None and len(right) > 0 and len(left) > 0: + same_side_l = np.diff(left) + same_side_r = np.diff(right) + if len(right) > 0: # calculate left interval and fraction - same_side_l = np.diff(left) - same_side_l_frac = len(same_side_l[~(same_side_l > threshold)]) / len(same_side_l) + same_side_l_frac = np.mean(same_side_l <= threshold) logging.info(f'Percentage of left lick intervals under 100 ms is {same_side_l_frac * 100}%.') self.B_LeftLickIntervalPercent = same_side_l_frac * 100 + elif len(left) > 0: # calculate right interval and fraction - same_side_r = np.diff(right) - same_side_r_frac = len(same_side_r[~(same_side_r > threshold)]) / len(same_side_r) + same_side_r_frac = np.mean(same_side_r <= threshold) logging.info(f'Percentage of right lick intervals under 100 ms is {same_side_r_frac * 100}%.') self.B_RightLickIntervalPercent = same_side_r_frac * 100 - if same_side_l_frac + same_side_r_frac >= .1: + elif len(right) > 0 and len(left) > 0: + # calculate same side lick interval and fraction for both right and left + same_side_combined = np.concat([same_side_l, same_side_r]) + same_side_frac = np.mean(same_side_combined <= threshold) + + if same_side_frac >= threshold: self.win.same_side_lick_interval.setText(f'Percentage of same side lick intervals under 100 ms is ' - f'over 10%: {(same_side_r_frac+same_side_l_frac) * 100}%.') + f'over 10%: {same_side_frac * 100}%.') else: self.win.same_side_lick_interval.setText('') # calculate cross side interval and frac - dummy_array = np.ones(right.shape) # use array to tell lick side after merging + dummy_array = np.ones(right.shape) # array used to assign lick direction + # 2d arrays pairing each time with a 1 (right) or -1 (left) stacked_right = np.column_stack((dummy_array, right)) stacked_left = np.column_stack((np.negative(dummy_array), left)) + # concatenate stacked_right and stacked_left then sort based on time element + # e.g. [[-1, 10], [1, 15], [-1, 20], [1, 25]...]. Ones added to assign lick side to times merged_sorted = sorted(np.concatenate((stacked_right, stacked_left)), - key=lambda x: x[1]) # sort by second element - cross_sides = [] - for i in range(1, len(right)): - if merged_sorted[i - 1][0] == -merged_sorted[i][0]: # cross side lick - cross_sides.append(merged_sorted[i][1] - merged_sorted[i - 1][1]) - cross_sides = np.array(cross_sides) - cross_side_frac = len(cross_sides[~(cross_sides > threshold)]) / len(cross_sides) + key=lambda x: x[1]) + + diffs = np.diff(merged_sorted[:, 0]) # take difference of 1 (right) or -1 (left) + # take difference of next index with previous at indices where directions are opposite + cross_sides = [merged_sorted[i + 1, 1] - merged_sorted[i, 1] for i in np.where(diffs != 0)] + cross_side_frac = np.mean(cross_sides <= threshold) logging.info(f'Percentage of cross side lick intervals under 100 ms is {cross_side_frac * 100}%.') self.B_CrossSideIntervalPercent = cross_side_frac * 100 - if cross_side_frac >= .1: + if cross_side_frac >= threshold: self.win.cross_side_lick_interval.setText(f'Percentage of cross side lick intervals under 100 ms is ' - f'over 10%: {(same_side_r_frac+same_side_l_frac) * 100}%.') + f'over 10%: {cross_side_frac * 100}%.') else: self.win.cross_side_lick_interval.setText('') From 35f04e44d8d02f89213033312ce591c9ac7fbbd4 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 30 Oct 2024 11:55:52 -0700 Subject: [PATCH 06/42] fixed if statements --- src/foraging_gui/MyFunctions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index becb47239..c5f588b7d 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -915,13 +915,13 @@ def calculate_inter_lick_intervals(self): logging.info(f'Percentage of left lick intervals under 100 ms is {same_side_l_frac * 100}%.') self.B_LeftLickIntervalPercent = same_side_l_frac * 100 - elif len(left) > 0: + if len(left) > 0: # calculate right interval and fraction same_side_r_frac = np.mean(same_side_r <= threshold) logging.info(f'Percentage of right lick intervals under 100 ms is {same_side_r_frac * 100}%.') self.B_RightLickIntervalPercent = same_side_r_frac * 100 - elif len(right) > 0 and len(left) > 0: + if len(right) > 0 and len(left) > 0: # calculate same side lick interval and fraction for both right and left same_side_combined = np.concat([same_side_l, same_side_r]) same_side_frac = np.mean(same_side_combined <= threshold) From defa09c3dae35048149b3d4a3faf851d6a0ce142 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 30 Oct 2024 12:00:26 -0700 Subject: [PATCH 07/42] small bug fixes --- src/foraging_gui/MyFunctions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index c5f588b7d..bc22c344c 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -923,7 +923,7 @@ def calculate_inter_lick_intervals(self): if len(right) > 0 and len(left) > 0: # calculate same side lick interval and fraction for both right and left - same_side_combined = np.concat([same_side_l, same_side_r]) + same_side_combined = np.concatenate([same_side_l, same_side_r]) same_side_frac = np.mean(same_side_combined <= threshold) if same_side_frac >= threshold: @@ -939,12 +939,12 @@ def calculate_inter_lick_intervals(self): stacked_left = np.column_stack((np.negative(dummy_array), left)) # concatenate stacked_right and stacked_left then sort based on time element # e.g. [[-1, 10], [1, 15], [-1, 20], [1, 25]...]. Ones added to assign lick side to times - merged_sorted = sorted(np.concatenate((stacked_right, stacked_left)), - key=lambda x: x[1]) + merged_sorted = np.array(sorted(np.concatenate((stacked_right, stacked_left)), + key=lambda x: x[1])) diffs = np.diff(merged_sorted[:, 0]) # take difference of 1 (right) or -1 (left) # take difference of next index with previous at indices where directions are opposite - cross_sides = [merged_sorted[i + 1, 1] - merged_sorted[i, 1] for i in np.where(diffs != 0)] + cross_sides = np.array([merged_sorted[i + 1, 1] - merged_sorted[i, 1] for i in np.where(diffs != 0)])[0] cross_side_frac = np.mean(cross_sides <= threshold) logging.info(f'Percentage of cross side lick intervals under 100 ms is {cross_side_frac * 100}%.') self.B_CrossSideIntervalPercent = cross_side_frac * 100 From c373aa452ede088a75ca661d8e59dd4c04ee1068 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 30 Oct 2024 14:05:25 -0700 Subject: [PATCH 08/42] added round and formatting --- src/foraging_gui/MyFunctions.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index bc22c344c..cc3865711 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -13,7 +13,7 @@ from serial.tools.list_ports import comports as list_comports from PyQt5 import QtWidgets from PyQt5 import QtCore - +from random import randint from foraging_gui.reward_schedules.uncoupled_block import UncoupledBlocks if PLATFORM == 'win32': @@ -109,8 +109,7 @@ def __init__(self,win): self._GetTrainingParameters(self.win) # create timer to calculate lick intervals every 10 minutes - #self.lick_interval_time = QtCore.QTimer(timeout=self.calculate_inter_lick_intervals, interval=600000) - self.lick_interval_time = QtCore.QTimer(timeout=self.calculate_inter_lick_intervals, interval=100) + self.lick_interval_time = QtCore.QTimer(timeout=self.calculate_inter_lick_intervals, interval=600000) def _GenerateATrial(self,Channel4): self.finish_select_par=0 @@ -911,24 +910,24 @@ def calculate_inter_lick_intervals(self): same_side_r = np.diff(right) if len(right) > 0: # calculate left interval and fraction - same_side_l_frac = np.mean(same_side_l <= threshold) + same_side_l_frac = round(np.mean(same_side_l <= threshold), 4) logging.info(f'Percentage of left lick intervals under 100 ms is {same_side_l_frac * 100}%.') self.B_LeftLickIntervalPercent = same_side_l_frac * 100 if len(left) > 0: # calculate right interval and fraction - same_side_r_frac = np.mean(same_side_r <= threshold) + same_side_r_frac = round(np.mean(same_side_r <= threshold), 4) logging.info(f'Percentage of right lick intervals under 100 ms is {same_side_r_frac * 100}%.') self.B_RightLickIntervalPercent = same_side_r_frac * 100 if len(right) > 0 and len(left) > 0: # calculate same side lick interval and fraction for both right and left same_side_combined = np.concatenate([same_side_l, same_side_r]) - same_side_frac = np.mean(same_side_combined <= threshold) - + same_side_frac = round(np.mean(same_side_combined <= threshold), 4) + logging.info(f'Percentage of right and left lick intervals under 100 ms is {same_side_frac * 100}%.') if same_side_frac >= threshold: self.win.same_side_lick_interval.setText(f'Percentage of same side lick intervals under 100 ms is ' - f'over 10%: {same_side_frac * 100}%.') + f'over 10%: {same_side_frac * 100:.2f}%.') else: self.win.same_side_lick_interval.setText('') @@ -945,8 +944,8 @@ def calculate_inter_lick_intervals(self): diffs = np.diff(merged_sorted[:, 0]) # take difference of 1 (right) or -1 (left) # take difference of next index with previous at indices where directions are opposite cross_sides = np.array([merged_sorted[i + 1, 1] - merged_sorted[i, 1] for i in np.where(diffs != 0)])[0] - cross_side_frac = np.mean(cross_sides <= threshold) - logging.info(f'Percentage of cross side lick intervals under 100 ms is {cross_side_frac * 100}%.') + cross_side_frac = round(np.mean(cross_sides <= threshold), 4) + logging.info(f'Percentage of cross side lick intervals under 100 ms is {cross_side_frac * 100:.2f}%.') self.B_CrossSideIntervalPercent = cross_side_frac * 100 if cross_side_frac >= threshold: From a90b3e5741171bee4a86b089b8e9ba15d910b94b Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 30 Oct 2024 14:39:15 -0700 Subject: [PATCH 09/42] update formatting --- src/foraging_gui/MyFunctions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index cc3865711..af6f25a7f 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -110,7 +110,7 @@ def __init__(self,win): # create timer to calculate lick intervals every 10 minutes self.lick_interval_time = QtCore.QTimer(timeout=self.calculate_inter_lick_intervals, interval=600000) - + def _GenerateATrial(self,Channel4): self.finish_select_par=0 if self.win.UpdateParameters==1: @@ -911,20 +911,20 @@ def calculate_inter_lick_intervals(self): if len(right) > 0: # calculate left interval and fraction same_side_l_frac = round(np.mean(same_side_l <= threshold), 4) - logging.info(f'Percentage of left lick intervals under 100 ms is {same_side_l_frac * 100}%.') + logging.info(f'Percentage of left lick intervals under 100 ms is {same_side_l_frac * 100:.2f}%.') self.B_LeftLickIntervalPercent = same_side_l_frac * 100 if len(left) > 0: # calculate right interval and fraction same_side_r_frac = round(np.mean(same_side_r <= threshold), 4) - logging.info(f'Percentage of right lick intervals under 100 ms is {same_side_r_frac * 100}%.') + logging.info(f'Percentage of right lick intervals under 100 ms is {same_side_r_frac * 100:.2f}%.') self.B_RightLickIntervalPercent = same_side_r_frac * 100 if len(right) > 0 and len(left) > 0: # calculate same side lick interval and fraction for both right and left same_side_combined = np.concatenate([same_side_l, same_side_r]) same_side_frac = round(np.mean(same_side_combined <= threshold), 4) - logging.info(f'Percentage of right and left lick intervals under 100 ms is {same_side_frac * 100}%.') + logging.info(f'Percentage of right and left lick intervals under 100 ms is {same_side_frac * 100:.2f}%.') if same_side_frac >= threshold: self.win.same_side_lick_interval.setText(f'Percentage of same side lick intervals under 100 ms is ' f'over 10%: {same_side_frac * 100:.2f}%.') @@ -950,7 +950,7 @@ def calculate_inter_lick_intervals(self): if cross_side_frac >= threshold: self.win.cross_side_lick_interval.setText(f'Percentage of cross side lick intervals under 100 ms is ' - f'over 10%: {cross_side_frac * 100}%.') + f'over 10%: {cross_side_frac * 100:.2f}%.') else: self.win.cross_side_lick_interval.setText('') From 861153513897e92b3b9fd27b75713fc8fc31594a Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 31 Oct 2024 08:41:01 -0700 Subject: [PATCH 10/42] use signal to trigger manifest creation when session created --- src/foraging_gui/Foraging.py | 57 ++++++++++++++++++---------- src/foraging_gui/GenerateMetadata.py | 51 ++----------------------- 2 files changed, 41 insertions(+), 67 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 836262425..33a076e23 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -45,6 +45,7 @@ from foraging_gui.GenerateMetadata import generate_metadata from foraging_gui.RigJsonBuilder import build_rig_json from aind_data_schema.core.session import Session +from aind_data_schema_models.modalities import Modality logger = logging.getLogger(__name__) logger.root.handlers.clear() # clear handlers so console output can be configured @@ -61,6 +62,7 @@ def default(self, obj): class Window(QMainWindow): Time = QtCore.pyqtSignal(int) # Photometry timer signal + sessionGenerated = QtCore.pyqtSignal(Session) # signal to indicate Session has been generated def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): logging.info('Creating Window') @@ -192,6 +194,9 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): # Initializes session log handler as None self.session_log_handler = None + # generate an upload manifest when a session has been produced + self.upload_manifest_slot = self.sessionGenerated.connect(self._generate_upload_manifest) + # show disk space self._show_disk_space() if not self.start_bonsai_ide: @@ -2713,18 +2718,19 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): Obj['meta_data_dialog'] = self.Metadata_dialog.meta_data # generate the metadata file generated_metadata=generate_metadata(Obj=Obj) + session = generated_metadata._session() + self.sessionGenerated.emit(session) # emit sessionGenerated + if BackupSave==0: text="Session metadata generated successfully: " + str(generated_metadata.session_metadata_success)+"\n"+\ - "Rig metadata generated successfully: " + str(generated_metadata.rig_metadata_success)+"\n"+\ - "Data description generated successfully: " + str(generated_metadata.data_description_success) + "Rig metadata generated successfully: " + str(generated_metadata.rig_metadata_success) self._manage_warning_labels(self.MetadataWarning,warning_text=text) Obj['generate_session_metadata_success']=generated_metadata.session_metadata_success Obj['generate_rig_metadata_success']=generated_metadata.rig_metadata_success - Obj['generate_data_description_success']=generated_metadata.data_description_success if save_clicked: # create water log result if weight after filled and uncheck save if self.BaseWeight.text() != '' and self.WeightAfter.text() != '' and self.ID.text() not in ['0','1','2','3','4','5','6','7','8','9','10']: - self._AddWaterLogResult(generated_metadata._session()) + self._AddWaterLogResult(session) self.bias_indicator.clear() # prepare for new session @@ -2735,7 +2741,6 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): # set to False if error occurs Obj['generate_session_metadata_success']=False Obj['generate_rig_metadata_success']=False - Obj['generate_data_description_success']=False # don't save the data if the load tag is 1 if self.load_tag==0: @@ -3873,6 +3878,7 @@ def _Start(self): self.Start.setChecked(False) logging.info('User declines continuation of session') return + # check experimenter name reply = QMessageBox.critical(self, 'Box {}, Start'.format(self.box_letter), @@ -3964,6 +3970,11 @@ def _Start(self): # disable metadata fields self._set_metadata_enabled(False) + # generate an upload manifest when a session has been produced if slot is not already connected + if self.upload_manifest_slot is None: + logging.debug('Connecting sessionGenerated to _generate_upload_manifest') + self.upload_manifest_slot = self.sessionGenerated.connect(self._generate_upload_manifest) + # Set IACUC protocol in metadata based on schedule protocol = self._GetInfoFromSchedule(mouse_id, 'Protocol') if protocol is not None: @@ -4229,13 +4240,6 @@ def _StartTrialLoop(self,GeneratedTrials,worker1,worker_save): last_trial_start = time.time() stall_iteration = 1 - # create manifest after first trial is completed - if GeneratedTrials.B_CurrentTrialN == 1: - if self.Settings['AutomaticUpload']: - self._generate_upload_manifest() # Generate the upload manifest file - else: - logging.info('Skipping Automatic Upload based on ForagingSettings.json') - # can start a new trial when we receive the trial end signal from Bonsai self.ANewTrial=0 GeneratedTrials.B_CurrentTrialN+=1 @@ -4640,13 +4644,20 @@ def _open_mouse_on_streamlit(self): '&session_plot_selected_draw_types=1.+Choice+history' ) - def _generate_upload_manifest(self): + def _generate_upload_manifest(self, session: Session): ''' Generates a manifest.yml file for triggering data copy to VAST and upload to aws + :param session: session to use to create upload manifest ''' + if self.ID.text() in ['0','1','2','3','4','5','6','7','8','9','10']: logging.info('Skipping upload manifest, because this is the test mouse') return + + if not self.Settings['AutomaticUpload']: + logging.info('Skipping Automatic Upload based on ForagingSettings.json') + return + try: if not hasattr(self, 'project_name'): self.project_name = 'Behavior Platform' @@ -4654,7 +4665,15 @@ def _generate_upload_manifest(self): schedule = self.acquisition_datetime.split('_')[0]+'_20-30-00' capsule_id = 'c089614a-347e-4696-b17e-86980bb782c1' mount = 'FIP' - + + + stream_modalities = session.data_streams[0].stream_modalities + modalities = {'behavior': [self.TrainingFolder.replace('\\', '/')]} + if Modality.FIB in stream_modalities: + modalities['fib']=[self.PhotometryFolder.replace('\\','/')] + if Modality.BEHAVIOR_VIDEOS in stream_modalities: + modalities['behavior-videos'] = [self.VideoFolder.replace('\\','/')] + date_format = "%Y-%m-%d_%H-%M-%S" # Define contents of manifest file contents = { @@ -4667,15 +4686,10 @@ def _generate_upload_manifest(self): 'destination': '//allen/aind/scratch/dynamic_foraging_rig_transfer', 's3_bucket':'private', 'processor_full_name': 'AIND Behavior Team', - 'modalities':{ - 'behavior':[self.TrainingFolder.replace('\\','/')], - 'behavior-videos':[self.VideoFolder.replace('\\','/')], - 'fib':[self.PhotometryFolder.replace('\\','/')] - }, + 'modalities':modalities, 'schemas':[ os.path.join(self.MetadataFolder,'session.json').replace('\\','/'), os.path.join(self.MetadataFolder,'rig.json').replace('\\','/'), - os.path.join(self.MetadataFolder, 'data_description.json').replace('\\', '/') ], 'schedule_time':datetime.strptime(schedule,date_format), 'project_name':self.project_name, @@ -4699,6 +4713,9 @@ def _generate_upload_manifest(self): 'Could not generate upload manifest. '+\ 'Please alert the mouse owner, and report on github.') + # disconnect slot to only create manifest once + logging.debug('Disconnecting sessionGenerated from _generate_upload_manifest') + self.upload_manifest_slot = self.sessionGenerated.disconnect(self.upload_manifest_slot) def start_gui_log_file(box_number): ''' diff --git a/src/foraging_gui/GenerateMetadata.py b/src/foraging_gui/GenerateMetadata.py index d183a346e..4dfb36244 100644 --- a/src/foraging_gui/GenerateMetadata.py +++ b/src/foraging_gui/GenerateMetadata.py @@ -13,13 +13,12 @@ from aind_data_schema.components.stimulus import AuditoryStimulation from aind_data_schema.components.devices import SpoutSide,Calibration from aind_data_schema_models.units import SizeUnit,FrequencyUnit,SoundIntensityUnit,PowerUnit -from aind_data_schema_models.modalities import Modality -from aind_data_schema.core.data_description import DataLevel, Funding, RawDataDescription +from aind_data_schema.core.data_description import Funding from aind_data_schema_models.organizations import Organization from aind_data_schema_models.modalities import Modality from aind_data_schema_models.platforms import Platform -from aind_data_schema_models.pid_names import PIDName, BaseName +from aind_data_schema_models.pid_names import PIDName from aind_data_schema.components.coordinates import RelativePosition, Translation3dTransform, Rotation3dTransform,Axis,AxisName @@ -68,7 +67,6 @@ def __init__(self, json_file=None, Obj=None, dialog_metadata_file=None,dialog_me self.session_metadata_success=False self.rig_metadata_success=False - self.data_description_success=False if Obj is None: self._set_metadata_logging() @@ -112,12 +110,10 @@ def __init__(self, json_file=None, Obj=None, dialog_metadata_file=None,dialog_me self._mapper() self._get_box_type() self._session() - if self.has_data_description: - self._session_description() + logging.info("Session metadata generated successfully: " + str(self.session_metadata_success)) logging.info("Rig metadata generated successfully: " + str(self.rig_metadata_success)) - logging.info("Data description generated successfully: " + str(self.data_description_success)) - + def _mapper(self): ''' Name mapping @@ -248,39 +244,6 @@ def _get_box_type(self): self.box_type = 'Ephys' else: self.box_type = 'Behavior' - - def _session_description(self): - ''' - Generate the session description to the MetadataFolder - ''' - if self.Obj['meta_data_dialog']['rig_metadata']=={}: - logging.info('rig metadata is empty!') - return - self._get_session_time() - if self.session_start_time == '' or self.session_end_time == '': - logging.info('session_start_time or session_end_time is empty!') - return - - self.orcid = BaseName(name="Open Researcher and Contributor ID", abbreviation="ORCID") - self._get_modality() - self._get_investigators() - self._get_funding_source() - self._get_platform() - - description= RawDataDescription( - data_level=DataLevel.RAW, - funding_source=self.funding_source, - investigators=self.investigators, - modality=self.modality, - project_name=self.Obj['meta_data_dialog']['session_metadata']['ProjectName'], - data_summary=self.Obj['meta_data_dialog']['session_metadata']['DataSummary'], - institution=Organization.AIND, - creation_time=self.session_start_time, - platform= self.platform, - subject_id=self.Obj['ID'], - ) - description.write_standard_file(output_directory=self.output_folder) - self.data_description_success=True def _get_funding_source(self): ''' @@ -479,12 +442,6 @@ def _handle_edge_cases(self): # Possible reason: 1) old version of the software. 2) the "Project Name and Funding Source v2.csv" is not provided. self._initialize_fields(dic=self.Obj['meta_data_dialog']['session_metadata'],keys=['ProjectName'],default_value='') - if self.Obj['meta_data_dialog']['session_metadata']['ProjectName']=='': - self.has_data_description = False - logging.info('No data description for session metadata') - else: - self.has_data_description = True - # Missing field 'B_AnimalResponseHistory' in the json file. # Possible reason: 1) the behavior data is not started in the session. # total_reward_consumed_in_session is the reward animal consumed in the session, not including the supplementary water. From a66a1f16c4ff6d754ffd5d050388e60f80775e4a Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 31 Oct 2024 09:19:16 -0700 Subject: [PATCH 11/42] correctly iterating through strams and modalities --- src/foraging_gui/Foraging.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 33a076e23..ad82d1736 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -4666,13 +4666,14 @@ def _generate_upload_manifest(self, session: Session): capsule_id = 'c089614a-347e-4696-b17e-86980bb782c1' mount = 'FIP' - - stream_modalities = session.data_streams[0].stream_modalities - modalities = {'behavior': [self.TrainingFolder.replace('\\', '/')]} - if Modality.FIB in stream_modalities: - modalities['fib']=[self.PhotometryFolder.replace('\\','/')] - if Modality.BEHAVIOR_VIDEOS in stream_modalities: - modalities['behavior-videos'] = [self.VideoFolder.replace('\\','/')] + modalities = {} + for stream in session.data_streams: + if Modality.BEHAVIOR in stream.stream_modalities: + modalities['behavior'] = [self.TrainingFolder.replace('\\', '/')] + elif Modality.FIB in stream.stream_modalities: + modalities['fib'] = [self.PhotometryFolder.replace('\\', '/')] + elif Modality.BEHAVIOR_VIDEOS in stream.stream_modalities: + modalities['behavior-videos'] = [self.VideoFolder.replace('\\', '/')] date_format = "%Y-%m-%d_%H-%M-%S" # Define contents of manifest file From 3310b8a7358dcd978cf983138d36e696b676457e Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Fri, 1 Nov 2024 11:04:03 -0700 Subject: [PATCH 12/42] changing logic to prevent empy stings in water calibrations --- src/foraging_gui/Calibration.ui | 8 ++++---- src/foraging_gui/Dialogs.py | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/foraging_gui/Calibration.ui b/src/foraging_gui/Calibration.ui index ce8a0a7ed..63d20360b 100644 --- a/src/foraging_gui/Calibration.ui +++ b/src/foraging_gui/Calibration.ui @@ -646,7 +646,7 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + 510 @@ -659,7 +659,7 @@ - + 510 @@ -790,7 +790,7 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + 360 @@ -803,7 +803,7 @@ - + 360 diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index fe2de30c1..b05411ae8 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -672,8 +672,8 @@ def _CalibrateLeftOne(self,repeat=False): valve_open_time=str(current_valve_opentime), valve_open_interval=str(self.params['Interval']), cycle=str(self.params['Cycle']), - total_water=float(self.WeightAfterLeft.text()), - tube_weight=float(self.WeightBeforeLeft.text()) + total_water=float(final_tube_weight), + tube_weight=float(before_weight) ) self._UpdateFigure() @@ -807,6 +807,7 @@ def _CalibrateRightOne(self,repeat=False): "Weight after (g): ", final_tube_weight, 0, 1000, 4) + print(final_tube_weight, ok) if not ok: self.Warning.setText('Please repeat measurement') self.WeightBeforeRight.setText('') @@ -823,8 +824,8 @@ def _CalibrateRightOne(self,repeat=False): valve_open_time=str(current_valve_opentime), valve_open_interval=str(self.params['Interval']), cycle=str(self.params['Cycle']), - total_water=float(self.WeightAfterRight.text()), - tube_weight=float(self.WeightBeforeRight.text()) + total_water=float(final_tube_weight), + tube_weight=float(before_weight) ) self._UpdateFigure() From aa36c4e729c95aa9bfa3846221b50d329e62dcac Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Mon, 4 Nov 2024 07:46:50 -0800 Subject: [PATCH 13/42] changing kernel size to 10 --- src/foraging_gui/Foraging.py | 2 +- src/foraging_gui/Visualization.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 48d86e20f..aaa178427 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -30,7 +30,7 @@ from pyOSC3.OSC3 import OSCStreamingClient import webbrowser -from StageWidget.main import get_stage_widget +#from StageWidget.main import get_stage_widget import foraging_gui import foraging_gui.rigcontrol as rigcontrol diff --git a/src/foraging_gui/Visualization.py b/src/foraging_gui/Visualization.py index c00a72a67..d64f256b5 100644 --- a/src/foraging_gui/Visualization.py +++ b/src/foraging_gui/Visualization.py @@ -129,7 +129,7 @@ def _PlotChoice(self): RightChoice_UnRewarded=np.where(np.logical_and(self.B_AnimalResponseHistory==1, self.B_RewardedHistory[1]==False)) # running average of choice - self.kernel_size=2 + self.kernel_size=10 ResponseHistoryT=self.B_AnimalResponseHistory.copy() ResponseHistoryT[ResponseHistoryT==2]=np.nan ResponseHistoryF=ResponseHistoryT.copy() From c7d9f539678fe350467ccf0acd19b72176f7be7f Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Mon, 4 Nov 2024 07:48:23 -0800 Subject: [PATCH 14/42] adding stage widget back in --- src/foraging_gui/Foraging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index aaa178427..48d86e20f 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -30,7 +30,7 @@ from pyOSC3.OSC3 import OSCStreamingClient import webbrowser -#from StageWidget.main import get_stage_widget +from StageWidget.main import get_stage_widget import foraging_gui import foraging_gui.rigcontrol as rigcontrol From b557dcf4fca4bf14922df9738cb5cb1648c66a97 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Mon, 4 Nov 2024 10:56:45 -0800 Subject: [PATCH 15/42] start of warning widget --- src/foraging_gui/Foraging.py | 2 +- src/foraging_gui/warning_widget.py | 65 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/foraging_gui/warning_widget.py diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 0b4711198..b2dbb80b3 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -30,7 +30,7 @@ from pyOSC3.OSC3 import OSCStreamingClient import webbrowser -from StageWidget.main import get_stage_widget +#from StageWidget.main import get_stage_widget import foraging_gui import foraging_gui.rigcontrol as rigcontrol diff --git a/src/foraging_gui/warning_widget.py b/src/foraging_gui/warning_widget.py new file mode 100644 index 000000000..2eb1a6412 --- /dev/null +++ b/src/foraging_gui/warning_widget.py @@ -0,0 +1,65 @@ +import logging +from multiprocessing import Queue +from logging.handlers import QueueHandler +from random import randint +from PyQt5.QtCore import QTimer +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QScrollArea +import sys + +class WarningWidget(QWidget): + """Widget that uses a logging QueueHandler to display log errors and warning""" + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + #self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") + + # create vertical layout + self.setLayout(QVBoxLayout()) + + # configure queue handler to write to queue + self.queue = Queue() + queue_handler = QueueHandler(self.queue) + queue_handler.setLevel(logging.WARNING) + logger.root.addHandler(queue_handler) + + # create QTimer to periodically check queue + self.check_timer = QTimer(timeout=self.check_warning_queue, interval=1000) + self.check_timer.start() + + def check_warning_queue(self): + """ + Check queue and update layout with latest warnings + """ + #print('while not que empty', self.queue.qsize(), self.queue.full()) + if not self.queue.empty(): + print(self.queue.get(block=False, timeout=1)) + # label = QLabel(str(self.queue.get().getMessage())) + # self.layout().insertWidget(0, label) + + + # prune layout if too many warnings + # if self.layout().count() == 30: + # widget = self.layout().itemAt(29).widget() + # self.layout().removeWidget(widget) + +if __name__ == '__main__': + app = QApplication(sys.argv) + + logger = logging.getLogger() + stream_handler = logging.StreamHandler() + stream_handler.setLevel(logger.root.level) + logger.root.addHandler(stream_handler) + + warn_widget = WarningWidget(logger) + + warnings = ['this is a warning', 'this is also a warning', 'this is a warning too', 'Warn warn warn', + 'are you warned yet?'] + + warning_timer = QTimer(timeout=lambda: logger.warning(warnings[randint(0, 5)]), interval=1000) + + warning_timer.start() + warn_widget.show() + + sys.exit(app.exec_()) \ No newline at end of file From 845c7d7bafc5d4113968c92699bdf1b9e7e32ccb Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Mon, 4 Nov 2024 11:13:44 -0800 Subject: [PATCH 16/42] only start bias calculation thread if previous calculation is done --- src/foraging_gui/Foraging.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 0b4711198..78685fd52 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -140,6 +140,7 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): self.bias_indicator = BiasIndicator(x_range=self.bias_n_size) # TODO: Where to store bias_threshold parameter? self.Settings? self.bias_indicator.biasValue.connect(self.bias_calculated) # update dashboard value self.bias_indicator.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + self.bias_thread = threading.Thread() # dummy thread # Set up more parameters self.FIP_started=False @@ -4267,13 +4268,17 @@ def _StartTrialLoop(self,GeneratedTrials,worker1,worker_save): n_trial_back = self.bias_n_size if l > self.bias_n_size else \ round(len(np.array(choice_history)[~np.isnan(choice_history)])*.6) # add data to bias_indicator - bias_thread = threading.Thread(target=self.bias_indicator.calculate_bias, - kwargs={'time_point': self.GeneratedTrials.B_TrialStartTime[-1], - 'choice_history': choice_history, - 'reward_history': np.array(any_reward).astype(int), - 'n_trial_back': n_trial_back, - 'cv': 2}) - bias_thread.start() + if not self.bias_thread.is_alive(): + logger.debug('Starting bias thread.') + self.bias_thread = threading.Thread(target=self.bias_indicator.calculate_bias, + kwargs={'time_point': self.GeneratedTrials.B_TrialStartTime[-1], + 'choice_history': choice_history, + 'reward_history': np.array(any_reward).astype(int), + 'n_trial_back': n_trial_back, + 'cv': 2}) + self.bias_thread.start() + else: + logger.debug('Skipping bias calculation as previous is still in progress. ') # save the data everytrial if GeneratedTrials.CurrentSimulation==True: From f187b7353ba5d158aeed2d9b96485449341eb88e Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 5 Nov 2024 08:09:23 -0800 Subject: [PATCH 17/42] replacing warning labels with log warnings --- src/foraging_gui/Dialogs.py | 37 ++-- src/foraging_gui/Foraging.py | 149 ++++----------- src/foraging_gui/ForagingGUI.ui | 292 ----------------------------- src/foraging_gui/MyFunctions.py | 73 +++----- src/foraging_gui/warning_widget.py | 29 ++- 5 files changed, 82 insertions(+), 498 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index b05411ae8..e7fce1e12 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -268,8 +268,6 @@ def _LaserColor(self,Numb): ItemsLaserPower=sorted(ItemsLaserPower) getattr(self, f"Laser{laser_tag}_power_{str(Numb)}").clear() getattr(self, f"Laser{laser_tag}_power_{str(Numb)}").addItems(ItemsLaserPower) - self.MainWindow.WarningLabel.setText('') - self.MainWindow.WarningLabel.setStyleSheet("color: gray;") else: no_calibration=True else: @@ -280,8 +278,7 @@ def _LaserColor(self,Numb): if no_calibration: for laser_tag in self.laser_tags: getattr(self, f"Laser{laser_tag}_power_{str(Numb)}").clear() - self.MainWindow.WarningLabel.setText('No calibration for this protocol identified!') - self.MainWindow.WarningLabel.setStyleSheet(self.MainWindow.default_warning_color) + logging.warning('No calibration for this protocol identified!') getattr(self, 'Location_' + str(Numb)).setEnabled(Label) getattr(self, 'Laser1_power_' + str(Numb)).setEnabled(Label) @@ -1370,8 +1367,7 @@ def _StartCamera(self): self.MainWindow.Channel.CameraControl(int(1)) time.sleep(5) self.camera_start_time = str(datetime.now()) - self.MainWindow.WarningLabelCamera.setText('Camera is on!') - self.MainWindow.WarningLabelCamera.setStyleSheet(self.MainWindow.default_warning_color) + logging.warning('Camera is on!') self.WarningLabelCameraOn.setText('Camera is on!') self.WarningLabelCameraOn.setStyleSheet(self.MainWindow.default_warning_color) self.WarningLabelLogging.setText('') @@ -1385,8 +1381,7 @@ def _StartCamera(self): self.MainWindow.Channel.CameraControl(int(2)) self.camera_stop_time = str(datetime.now()) time.sleep(5) - self.MainWindow.WarningLabelCamera.setText('Camera is off!') - self.MainWindow.WarningLabelCamera.setStyleSheet(self.MainWindow.default_warning_color) + logging.warning('Camera is off!') self.WarningLabelCameraOn.setText('Camera is off!') self.WarningLabelCameraOn.setStyleSheet(self.MainWindow.default_warning_color) self.WarningLabelLogging.setText('') @@ -1557,8 +1552,7 @@ def _ProduceWaveForm(self,Amplitude): # add ramping down if self.CLP_RampingDown>0: if self.CLP_RampingDown>self.CLP_CurrentDuration: - self.win.WarningLabel.setText('Ramping down is longer than the laser duration!') - self.win.WarningLabel.setStyleSheet(self.MainWindow.default_warning_color) + logging.warning('Ramping down is longer than the laser duration!') else: Constant=np.ones(int((self.CLP_CurrentDuration-self.CLP_RampingDown)*self.CLP_SampleFrequency)) RD=np.arange(1,0, -1/(np.shape(self.my_wave)[0]-np.shape(Constant)[0])) @@ -1567,15 +1561,13 @@ def _ProduceWaveForm(self,Amplitude): self.my_wave=np.append(self.my_wave,[0,0]) elif self.CLP_Protocol=='Pulse': if self.CLP_PulseDur=='NA': - self.win.WarningLabel.setText('Pulse duration is NA!') - self.win.WarningLabel.setStyleSheet(self.MainWindow.default_warning_color) + logging.warning('Pulse duration is NA!') else: self.CLP_PulseDur=float(self.CLP_PulseDur) PointsEachPulse=int(self.CLP_SampleFrequency*self.CLP_PulseDur) PulseIntervalPoints=int(1/self.CLP_Frequency*self.CLP_SampleFrequency-PointsEachPulse) if PulseIntervalPoints<0: - self.win.WarningLabel.setText('Pulse frequency and pulse duration are not compatible!') - self.win.WarningLabel.setStyleSheet(self.MainWindow.default_warning_color) + logging.warning('Pulse frequency and pulse duration are not compatible!') TotalPoints=int(self.CLP_SampleFrequency*self.CLP_CurrentDuration) PulseNumber=np.floor(self.CLP_CurrentDuration*self.CLP_Frequency) EachPulse=Amplitude*np.ones(PointsEachPulse) @@ -1587,8 +1579,7 @@ def _ProduceWaveForm(self,Amplitude): for i in range(int(PulseNumber-1)): self.my_wave=np.concatenate((self.my_wave, WaveFormEachCycle), axis=0) else: - self.win.WarningLabel.setText('Pulse number is less than 1!') - self.win.WarningLabel.setStyleSheet(self.MainWindow.default_warning_color) + logging.warning('Pulse number is less than 1!') return self.my_wave=np.concatenate((self.my_wave, EachPulse), axis=0) self.my_wave=np.concatenate((self.my_wave, np.zeros(TotalPoints-np.shape(self.my_wave)[0])), axis=0) @@ -1599,8 +1590,7 @@ def _ProduceWaveForm(self,Amplitude): if self.CLP_RampingDown>0: # add ramping down if self.CLP_RampingDown>self.CLP_CurrentDuration: - self.win.WarningLabel.setText('Ramping down is longer than the laser duration!') - self.win.WarningLabel.setStyleSheet(self.MainWindow.default_warning_color) + logging.warning('Ramping down is longer than the laser duration!') else: Constant=np.ones(int((self.CLP_CurrentDuration-self.CLP_RampingDown)*self.CLP_SampleFrequency)) RD=np.arange(1,0, -1/(np.shape(self.my_wave)[0]-np.shape(Constant)[0])) @@ -1608,8 +1598,7 @@ def _ProduceWaveForm(self,Amplitude): self.my_wave=self.my_wave*RampingDown self.my_wave=np.append(self.my_wave,[0,0]) else: - self.win.WarningLabel.setText('Unidentified optogenetics protocol!') - self.win.WarningLabel.setStyleSheet(self.MainWindow.default_warning_color) + logging.warning('Unidentified optogenetics protocol!') def _GetLaserAmplitude(self): '''the voltage amplitude dependens on Protocol, Laser Power, Laser color, and the stimulation locations<>''' @@ -1620,9 +1609,8 @@ def _GetLaserAmplitude(self): elif self.CLP_Location=='Both': self.CurrentLaserAmplitude=[self.CLP_InputVoltage,self.CLP_InputVoltage] else: - self.win.WarningLabel.setText('No stimulation location defined!') - self.win.WarningLabel.setStyleSheet(self.MainWindow.default_warning_color) - + logging.warning('No stimulation location defined!') + # get training parameters def _GetTrainingParameters(self,win): '''Get training parameters''' @@ -2042,7 +2030,8 @@ def _clear_metadata(self): def _removing_warning(self): '''remove the warning''' if self.RigMetadataFile.text()!='': - self.MainWindow._manage_warning_labels(self.MainWindow.MetadataWarning,warning_text='') + pass + #self.MainWindow._manage_warning_labels(self.MainWindow.MetadataWarning,warning_text='') def _load_metadata(self): '''load the metadata from a json file''' diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index b2dbb80b3..3a82c1e53 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -26,7 +26,7 @@ from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox, QSizePolicy from PyQt5.QtWidgets import QFileDialog,QVBoxLayout, QGridLayout from PyQt5 import QtWidgets,QtGui,QtCore, uic -from PyQt5.QtCore import QThreadPool,Qt,QThread +from PyQt5.QtCore import QThreadPool,Qt,QThread, QTimer from pyOSC3.OSC3 import OSCStreamingClient import webbrowser @@ -42,6 +42,7 @@ from foraging_gui.MyFunctions import GenerateTrials, Worker,TimerWorker, NewScaleSerialY, EphysRecording from foraging_gui.stage import Stage from foraging_gui.bias_indicator import BiasIndicator +from foraging_gui.warning_widget import WarningWidget from foraging_gui.GenerateMetadata import generate_metadata from foraging_gui.RigJsonBuilder import build_rig_json from aind_data_schema.core.session import Session @@ -197,6 +198,10 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): # generate an upload manifest when a session has been produced self.upload_manifest_slot = self.sessionGenerated.connect(self._generate_upload_manifest) + # create and add warning_widget + self.warning_widget = WarningWidget() + self.scrollArea_6.setWidget(self.warning_widget) + # show disk space self._show_disk_space() if not self.start_bonsai_ide: @@ -628,21 +633,16 @@ def _check_drop_frames(self,save_tag=1): elif len(os.listdir(video_folder)) == 0: # no video data saved. self.trigger_length=0 - self.WarningLabelCamera.setText('') - self.WarningLabelCamera.setStyleSheet(self.default_warning_color) self.to_check_drop_frames=0 return elif ('HighSpeedCamera' in self.SettingsBox) and (self.SettingsBox['HighSpeedCamera'] ==1): self.trigger_length=0 logging.error('Saved video data, but no camera trigger file found') - self.WarningLabelCamera.setText('No camera trigger file found!') - self.WarningLabelCamera.setStyleSheet(self.default_warning_color) + logging.warning('No camera trigger file found!') return else: logging.info('Saved video data, but not using high speed camera - skipping drop frame check') self.trigger_length=0 - self.WarningLabelCamera.setText('') - self.WarningLabelCamera.setStyleSheet(self.default_warning_color) self.to_check_drop_frames=0 return csv_files = [file for file in os.listdir(video_folder) if file.endswith(".csv")] @@ -662,11 +662,7 @@ def _check_drop_frames(self,save_tag=1): else: self.drop_frames_warning_text+=f"Correct: {avi_file} has {num_frames} frames and {self.trigger_length} triggers\n" self.frame_num[camera_name] = num_frames - self.WarningLabelCamera.setText(self.drop_frames_warning_text) - if self.drop_frames_tag: - self.WarningLabelCamera.setStyleSheet(self.default_warning_color) - else: - self.WarningLabelCamera.setStyleSheet("color: green;") + logging.warning(self.drop_frames_warning_text) # only check drop frames once each session self.to_check_drop_frames=0 @@ -942,8 +938,7 @@ def _ConnectBonsai(self): subprocess.Popen('title Box{}'.format(self.box_letter),shell=True) except Exception as e: logging.error(traceback.format_exc()) - self.WarningLabelInitializeBonsai.setText('Please open bonsai!') - self.WarningLabelInitializeBonsai.setStyleSheet(self.default_warning_color) + logging.warning('Please open bonsai!') self.InitializeBonsaiSuccessfully=0 def _ReconnectBonsai(self): @@ -1365,8 +1360,6 @@ def _InitializeBonsai(self): logging.info('Connected to already running Bonsai') logging.info('Bonsai started successfully') self.InitializeBonsaiSuccessfully=1 - self.WarningLabel.setText('') - self.WarningLabel.setStyleSheet(self.default_warning_color) return # Start Bonsai @@ -1389,17 +1382,13 @@ def _InitializeBonsai(self): # We could connect logging.info('Connected to Bonsai after {} seconds'.format(wait)) logging.info('Bonsai started successfully') - if self.WarningLabel.text() == 'Lost bonsai connection': - self.WarningLabel.setText('') - self.WarningLabel.setStyleSheet(self.default_warning_color) self.InitializeBonsaiSuccessfully=1 subprocess.Popen('title Box{}'.format(self.box_letter),shell=True) return # Could not connect and we timed out logging.info('Could not connect to bonsai with max wait time {} seconds'.format(max_wait)) - self.WarningLabel_2.setText('Started without bonsai connected!') - self.WarningLabel_2.setStyleSheet(self.default_warning_color) + logging.warning('Started without bonsai connected!') def _ConnectOSC(self): ''' @@ -1463,9 +1452,6 @@ def _ConnectOSC(self): self.Channel3.receive() while not self.Channel4.msgs.empty(): self.Channel4.receive() - self.WarningLabel_2.setText('') - self.WarningLabel_2.setStyleSheet("color: gray;") - self.WarningLabelInitializeBonsai.setText('') self.InitializeBonsaiSuccessfully=1 def _OpenBonsaiWorkflow(self,runworkflow=1): @@ -1522,9 +1508,8 @@ def _load_most_recent_rig_json(self,error_if_none=True): rig_json_path = '' if error_if_none: logging.error('Did not find any existing rig.json files') - self._manage_warning_labels(self.MetadataWarning,warning_text='No rig metadata found!') else: - logging.info('Did not find any existing rig.json files') + logging.info('Did not find any existing rig.json files') #FIXME: is this really the right message else: rig_json_path = os.path.join(self.Settings['rig_metadata_folder'],files[-1]) logging.info('Found existing rig.json: {}'.format(files[-1])) @@ -2463,20 +2448,14 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): "Do you want to save without weight or extra water information provided?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,QMessageBox.Yes) if response==QMessageBox.Yes: - self.WarningLabel.setText('Saving without weight or extra water!') - self.WarningLabel.setStyleSheet(self.default_warning_color) - logging.info('saving without weight or extra water') + logging.warning('Saving without weight or extra water!') pass elif response==QMessageBox.No: logging.info('saving declined by user') - self.WarningLabel.setText('') - self.WarningLabel.setStyleSheet(self.default_warning_color) self.Save.setChecked(False) # uncheck button return elif response==QMessageBox.Cancel: logging.info('saving canceled by user') - self.WarningLabel.setText('') - self.WarningLabel.setStyleSheet(self.default_warning_color) self.Save.setChecked(False) # uncheck button return # check if the laser power and target are entered @@ -2486,20 +2465,14 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): "Do you want to save without complete laser target or laser power calibration information provided?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,QMessageBox.Yes) if response==QMessageBox.Yes: - self.WarningLabel.setText('Saving without laser target or laser power!') - self.WarningLabel.setStyleSheet(self.default_warning_color) - logging.info('saving without laser target or laser power') + logging.warning('Saving without laser target or laser power!') pass elif response==QMessageBox.No: logging.info('saving declined by user') - self.WarningLabel.setText('') - self.WarningLabel.setStyleSheet(self.default_warning_color) self.Save.setChecked(False) # uncheck button return elif response==QMessageBox.Cancel: logging.info('saving canceled by user') - self.WarningLabel.setText('') - self.WarningLabel.setStyleSheet(self.default_warning_color) self.Save.setChecked(False) # uncheck button return @@ -2533,8 +2506,6 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): self.SaveFile=Names[0] if self.SaveFile == '': logging.info('empty file name') - self.WarningLabel.setText('') - self.WarningLabel.setStyleSheet(self.default_warning_color) self.Save.setChecked(False) # uncheck button return @@ -2724,10 +2695,9 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): short_file = self.SaveFile.split('\\')[-1] if self.load_tag==0: - self.WarningLabel.setText('Saved: {}'.format(short_file)) + logging.warning('Saved: {}'.format(short_file)) else: - self.WarningLabel.setText('Saving of loaded files is not allowed!') - self.WarningLabel.setStyleSheet(self.default_warning_color) + logging.warning('Saving of loaded files is not allowed!') self.SessionlistSpin.setEnabled(True) self.Sessionlist.setEnabled(True) @@ -3279,8 +3249,6 @@ def _StartFIP(self): if self.Teensy_COM == '': logging.warning('No Teensy COM configured for this box, cannot start FIP workflow') - self.TeensyWarning.setText('No Teensy COM for this box') - self.TeensyWarning.setStyleSheet(self.default_warning_color) msg = 'No Teensy COM configured for this box, cannot start FIP workflow' reply = QMessageBox.information(self, 'Box {}, StartFIP'.format(self.box_letter), msg, QMessageBox.Ok ) @@ -3288,8 +3256,6 @@ def _StartFIP(self): if self.FIP_workflow_path == "": logging.warning('No FIP workflow path defined in ForagingSettings.json') - self.TeensyWarning.setText('FIP workflow path not defined') - self.TeensyWarning.setStyleSheet(self.default_warning_color) msg = 'FIP workflow path not defined, cannot start FIP workflow' reply = QMessageBox.information(self, 'Box {}, StartFIP'.format(self.box_letter), msg, QMessageBox.Ok ) @@ -3330,8 +3296,6 @@ def _StartExcitation(self): if self.Teensy_COM == '': logging.warning('No Teensy COM configured for this box, cannot start excitation') - self.TeensyWarning.setText('No Teensy COM for this box') - self.TeensyWarning.setStyleSheet(self.default_warning_color) msg = 'No Teensy COM configured for this box, cannot start excitation' reply = QMessageBox.information(self, 'Box {}, StartExcitation'.format(self.box_letter), msg, QMessageBox.Ok ) @@ -3350,19 +3314,15 @@ def _StartExcitation(self): elif self.FIPMode.currentText() == "Axon": ser.write(b'e') ser.close() - self.TeensyWarning.setText('Started FIP excitation') - self.TeensyWarning.setStyleSheet(self.default_warning_color) + logging.warning('Started FIP excitation') except Exception as e: logging.error(traceback.format_exc()) - self.TeensyWarning.setText('Error: starting excitation!') - self.TeensyWarning.setStyleSheet(self.default_warning_color) + logging.warning('Error: starting excitation!') reply = QMessageBox.critical(self, 'Box {}, Start excitation:'.format(self.box_letter), 'error when starting excitation: {}'.format(e), QMessageBox.Ok) self.StartExcitation.setChecked(False) self.StartExcitation.setStyleSheet("background-color : none") return 0 else: - self.TeensyWarning.setText('') - self.TeensyWarning.setStyleSheet(self.default_warning_color) self.fiber_photometry_start_time = str(datetime.now()) else: @@ -3373,17 +3333,13 @@ def _StartExcitation(self): # Trigger Teensy with the above specified exp mode ser.write(b's') ser.close() - self.TeensyWarning.setText('Stopped FIP excitation') - self.TeensyWarning.setStyleSheet(self.default_warning_color) + logging.warning('Stopped FIP excitation') except Exception as e: logging.error(traceback.format_exc()) - self.TeensyWarning.setText('Error stopping excitation!') - self.TeensyWarning.setStyleSheet(self.default_warning_color) + logging.warning('Error stopping excitation!') reply = QMessageBox.critical(self, 'Box {}, Start excitation:'.format(self.box_letter), 'error when stopping excitation: {}'.format(e), QMessageBox.Ok) return 0 else: - self.TeensyWarning.setText('') - self.TeensyWarning.setStyleSheet(self.default_warning_color) self.fiber_photometry_end_time = str(datetime.now()) return 1 @@ -3392,8 +3348,6 @@ def _StartBleaching(self): if self.Teensy_COM == '': logging.warning('No Teensy COM configured for this box, cannot start bleaching') - self.TeensyWarning.setText('No Teensy COM for this box') - self.TeensyWarning.setStyleSheet(self.default_warning_color) msg = 'No Teensy COM configured for this box, cannot start bleaching' reply = QMessageBox.information(self, 'Box {}, StartBleaching'.format(self.box_letter), msg, QMessageBox.Ok ) @@ -3427,14 +3381,12 @@ def _StartBleaching(self): # Trigger Teensy with the above specified exp mode ser.write(b'd') ser.close() - self.TeensyWarning.setText('Start bleaching!') - self.TeensyWarning.setStyleSheet(self.default_warning_color) + logging.warning('Start bleaching!') except Exception as e: logging.error(traceback.format_exc()) # Alert user - self.TeensyWarning.setText('Error: start bleaching!') - self.TeensyWarning.setStyleSheet(self.default_warning_color) + logging.warning('Error: start bleaching!') reply = QMessageBox.critical(self, 'Box {}, Start bleaching:'.format(self.box_letter), 'Cannot start photobleaching: {}'.format(str(e)), QMessageBox.Ok) @@ -3461,12 +3413,9 @@ def _StartBleaching(self): # Trigger Teensy with the above specified exp mode ser.write(b's') ser.close() - self.TeensyWarning.setText('') - self.TeensyWarning.setStyleSheet(self.default_warning_color) except Exception as e: logging.error(traceback.format_exc()) - self.TeensyWarning.setText('Error: stop bleaching!') - self.TeensyWarning.setStyleSheet(self.default_warning_color) + logging.warning('Error: stop bleaching!') def _StopPhotometry(self,closing=False): ''' @@ -3487,8 +3436,6 @@ def _StopPhotometry(self,closing=False): logging.info('Photometry excitation stopped') finally: # Reset all GUI buttons - self.TeensyWarning.setText('') - self.TeensyWarning.setStyleSheet(self.default_warning_color) self.StartBleaching.setStyleSheet("background-color : none") self.StartExcitation.setStyleSheet("background-color : none") self.StartBleaching.setChecked(False) @@ -3534,8 +3481,7 @@ def _stop_logging(self): self.logging_type=-1 # logging has stopped except Exception as e: logging.warning('Bonsai connection is closed') - self.WarningLabel.setText('Lost bonsai connection') - self.WarningLabel.setStyleSheet(self.default_warning_color) + logging.warning('Lost bonsai connection') self.InitializeBonsaiSuccessfully=0 def _NewSession(self): @@ -3578,22 +3524,18 @@ def _NewSession(self): self.WeightAfter.setText('') # Reset GUI visuals - self.ManualWaterWarning.setText('') self.Save.setStyleSheet("color:black;background-color:None;") self.NewSession.setStyleSheet("background-color : green;") self.NewSession.setChecked(False) self.Start.setStyleSheet("background-color : none") self.Start.setChecked(False) self.Start.setDisabled(False) - self.WarningLabel.setText('') self.TotalWaterWarning.setText('') - self.WarningLabel_2.setText('') self._set_metadata_enabled(True) self._ConnectBonsai() if self.InitializeBonsaiSuccessfully == 0: - self.WarningLabel.setText('Lost bonsai connection') - self.WarningLabel.setStyleSheet(self.default_warning_color) + logging.warning('Lost bonsai connection') # Reset state variables self._StopPhotometry() # Make sure photoexcitation is stopped @@ -3645,13 +3587,10 @@ def _StopCurrentSession(self): stall_iteration = 1 stall_duration = 5*60 if self.ANewTrial==0: - self.WarningLabel.setText('Waiting for the finish of the last trial!') - self.WarningLabel.setStyleSheet(self.default_warning_color) + logging.warning('Waiting for the finish of the last trial!') while 1: QApplication.processEvents() if self.ANewTrial==1: - self.WarningLabel.setText('') - self.WarningLabel.setStyleSheet(self.default_warning_color) break elif (time.time() - start_time) > stall_duration*stall_iteration: elapsed_time = int(np.floor(stall_duration*stall_iteration/60)) @@ -3660,8 +3599,6 @@ def _StopCurrentSession(self): if reply == QMessageBox.Yes: logging.error('trial stalled {} minutes, user force stopped trials'.format(elapsed_time)) self.ANewTrial=1 - self.WarningLabel.setText('') - self.WarningLabel.setStyleSheet(self.default_warning_color) break else: stall_iteration+=1 @@ -3694,8 +3631,6 @@ def _thread_complete_timer(self): if not self.ignore_timer: self.finish_Timer=1 logging.info('Finished photometry baseline timer') - self.WarningLabelStop.setText('') - self.WarningLabelStop.setStyleSheet(self.default_warning_color) def _update_photometery_timer(self,time): ''' @@ -3706,8 +3641,7 @@ def _update_photometery_timer(self,time): if len(str(seconds)) == 1: seconds = '0{}'.format(seconds) if not self.ignore_timer: - self.WarningLabelStop.setText('Running photometry baseline: {}:{}'.format(minutes,seconds)) - self.WarningLabelStop.setStyleSheet(self.default_warning_color) + logging.warning('Running photometry baseline: {}:{}'.format(minutes,seconds)) def _set_metadata_enabled(self, enable: bool): '''Enable or disable metadata fields''' @@ -3777,9 +3711,7 @@ def _Start(self): self.Sessionlist.setEnabled(False) # Clear warnings - self.WarningLabelInitializeBonsai.setText('') self.NewSession.setDisabled(False) - self.WarningLabelCamera.setText('') # Toggle button colors if self.Start.isChecked(): logging.info('Start button pressed: starting trial loop') @@ -3926,8 +3858,6 @@ def _Start(self): self.Start.setStyleSheet("background-color : green;") self.NewSession.setStyleSheet("background-color : none") self.NewSession.setChecked(False) - self.WarningLabel.setText('') - self.WarningLabel.setStyleSheet("color: none;") # disable metadata fields self._set_metadata_enabled(False) @@ -3988,15 +3918,11 @@ def _Start(self): self.ignore_timer=True self.PhotometryRun=0 logging.info('canceling photometry baseline timer') - self.WarningLabelStop.setText('') - self.WarningLabelStop.setStyleSheet(self.default_warning_color) if hasattr(self, 'workertimer'): # Stop the worker, this has a 1 second delay before taking effect # so we set the text to get ignored as well self.workertimer._stop() - self.ManualWaterWarning.setText('') - # fill out GenerateTrials B_Bias last_bias = self.GeneratedTrials.B_Bias[-1] b_bias_len = len(self.GeneratedTrials.B_Bias) @@ -4008,9 +3934,6 @@ def _Start(self): # to see if we should start a new session if self.StartANewSession==1 and self.ANewTrial==1: # generate a new session id - self.WarningLabel.setText('') - self.WarningLabel.setStyleSheet("color: gray;") - self.WarmupWarning.setText('') self.ManualWaterVolume=[0,0] # start a new logging try: @@ -4027,8 +3950,7 @@ def _Start(self): except Exception as e: if 'ConnectionAbortedError' in str(e): logging.info('lost bonsai connection: restartlogging()') - self.WarningLabel.setText('Lost bonsai connection') - self.WarningLabel.setStyleSheet(self.default_warning_color) + logging.warning('Lost bonsai connection') self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") self.InitializeBonsaiSuccessfully=0 @@ -4139,8 +4061,7 @@ def _Start(self): self.workertimer_thread.start() self.Time.emit(int(np.floor(float(self.baselinetime.text())*60))) - self.WarningLabelStop.setText('Running photometry baseline') - self.WarningLabelStop.setStyleSheet(self.default_warning_color) + logging.warning('Running photometry baseline') self._StartTrialLoop(GeneratedTrials,worker1,worker_save) @@ -4222,8 +4143,7 @@ def _StartTrialLoop(self,GeneratedTrials,worker1,worker_save): except Exception as e: if 'ConnectionAbortedError' in str(e): logging.info('lost bonsai connection: InitiateATrial') - self.WarningLabel.setText('Lost bonsai connection') - self.WarningLabel.setStyleSheet(self.default_warning_color) + logging.warning('Lost bonsai connection') self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") self.InitializeBonsaiSuccessfully=0 @@ -4340,8 +4260,7 @@ def _StartTrialLoop(self,GeneratedTrials,worker1,worker_save): self.Start.setStyleSheet("background-color : none") # Give warning to user - self.WarningLabel.setText('Trials stalled, recheck bonsai connection.') - self.WarningLabel.setStyleSheet(self.default_warning_color) + logging.warning('Trials stalled, recheck bonsai connection.') break else: # User continues, wait another stall_duration and prompt again @@ -4448,8 +4367,8 @@ def _GiveLeft(self): self.Channel.LeftValue(float(self.TP_LeftValue)*1000) self.ManualWaterVolume[0]=self.ManualWaterVolume[0]+float(self.TP_GiveWaterL_volume)/1000 self._UpdateSuggestedWater() - self.ManualWaterWarning.setText('Give left manual water (ul): '+str(np.round(float(self.TP_GiveWaterL_volume),3))) - self.ManualWaterWarning.setStyleSheet(self.default_warning_color) + logger.warning('Give left manual water (ul): '+str(np.round(float(self.TP_GiveWaterL_volume),3))) + def _give_reserved_water(self,valve=None): '''give reserved water usually after the go cue''' @@ -4503,8 +4422,7 @@ def _GiveRight(self): self.Channel.RightValue(float(self.TP_RightValue)*1000) self.ManualWaterVolume[1]=self.ManualWaterVolume[1]+float(self.TP_GiveWaterR_volume)/1000 self._UpdateSuggestedWater() - self.ManualWaterWarning.setText('Give right manual water (ul): '+str(np.round(float(self.TP_GiveWaterR_volume),3))) - self.ManualWaterWarning.setStyleSheet(self.default_warning_color) + logger.warning('Give right manual water (ul): '+str(np.round(float(self.TP_GiveWaterR_volume),3))) def _toggle_save_color(self): '''toggle the color of the save button to mediumorchid''' @@ -4514,7 +4432,6 @@ def _toggle_save_color(self): def _PostWeightChange(self): self.unsaved_data=True self.Save.setStyleSheet("color: white;background-color : mediumorchid;") - self.WarningLabel.setText('') self._UpdateSuggestedWater() def _UpdateSuggestedWater(self,ManualWater=0): diff --git a/src/foraging_gui/ForagingGUI.ui b/src/foraging_gui/ForagingGUI.ui index 3da1b4ad6..1c7de1b22 100644 --- a/src/foraging_gui/ForagingGUI.ui +++ b/src/foraging_gui/ForagingGUI.ui @@ -597,298 +597,6 @@ true - - - - 0 - 0 - 178 - 247 - - - - - - - - 0 - 0 - - - - - 7 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - 7 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 7 - - - - - - - - - - - - 0 - 0 - - - - - 7 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 7 - - - - - - - - - - - - 7 - - - - - - - - - - - - 0 - 0 - - - - - 7 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - 7 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - 7 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - 0 - 40 - - - - - 7 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - 7 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index 7370188e9..e659341f8 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -163,7 +163,7 @@ def _GenerateATrial(self,Channel4): )) # Show msg - self.win.WarningLabel_uncoupled_task.setText(msg_uncoupled_block) + logging.warning(msg_uncoupled_block) # Append the (updated) current reward probability to the history self.B_RewardProHistory=np.append( @@ -283,7 +283,7 @@ def _CheckWarmUp(self): self.win.keyPressEvent() self.win.NextBlock.setChecked(True) self.win._NextBlock() - self.win.WarmupWarning.setText('Warm up is turned off') + logging.warning('Warm up is turned off') def _get_warmup_state(self): '''calculate the metrics related to the warm up and decide if we should turn on the warm up''' @@ -308,8 +308,7 @@ def _get_warmup_state(self): # turn on the warm up warmup=1 # show current metrics of the warm up - self.win.WarmupWarning.setText('Finish trial: '+str(round(finish_trial,2))+ '; Finish ratio: '+str(round(finish_ratio,2))+'; Choice ratio bias: '+str(round(abs(choice_ratio-0.5),2))) - self.win.WarmupWarning.setStyleSheet(self.win.default_warning_color) + logging.error('Finish trial: '+str(round(finish_trial,2))+ '; Finish ratio: '+str(round(finish_ratio,2))+'; Choice ratio bias: '+str(round(abs(choice_ratio-0.5),2))) return warmup def _CheckBaitPermitted(self): @@ -325,11 +324,7 @@ def _CheckBaitPermitted(self): else: self.BaitPermitted=True if self.BaitPermitted==False: - self.win.WarningLabelRewardN.setText('The active side has no reward due to consecutive \nselections('+str(MaxCLen)+')<'+self.TP_InitiallyInactiveN) - self.win.WarningLabelRewardN.setStyleSheet(self.win.default_warning_color) - else: - self.win.WarningLabelRewardN.setText('') - self.win.WarningLabelRewardN.setStyleSheet("color: gray;") + logging.warning('The active side has no reward due to consecutive \nselections('+str(MaxCLen)+')<'+self.TP_InitiallyInactiveN) def _GetMaximumConSelection(self): '''get the maximum consecutive selection of the active side of the current block''' @@ -1121,9 +1116,8 @@ def _CheckStop(self): stop=False # Update the warning label text/color - self.win.WarningLabelStop.setText(warning_label_text) - self.win.WarningLabelStop.setStyleSheet(warning_label_color) - + logging.warning(warning_label_text) + # If we should stop trials, uncheck the start button if stop: self.win.Start.setStyleSheet("background-color : none") @@ -1137,11 +1131,9 @@ def _CheckAutoWater(self): IgnoredN=int(self.TP_Ignored) if UnrewardedN<=0: self.CurrentAutoReward=1 - self.win.WarningLabelAutoWater.setText('Auto water because unrewarded trials exceed: '+self.TP_Unrewarded) - self.win.WarningLabelAutoWater.setStyleSheet(self.win.default_warning_color) + logging.warning('Auto water because unrewarded trials exceed: '+self.TP_Unrewarded) elif IgnoredN <=0: - self.win.WarningLabelAutoWater.setText('Auto water because ignored trials exceed: '+self.TP_Ignored) - self.win.WarningLabelAutoWater.setStyleSheet(self.win.default_warning_color) + logging.warning('Auto water because ignored trials exceed: '+self.TP_Ignored) self.CurrentAutoReward=1 else: if np.shape(self.B_AnimalResponseHistory)[0]>=IgnoredN or np.shape(self.B_RewardedHistory[0])[0]>=UnrewardedN: @@ -1152,12 +1144,10 @@ def _CheckAutoWater(self): B_RewardedHistory[i]=np.logical_or(self.B_RewardedHistory[i],self.B_AutoWaterTrial[i][Ind]) if np.all(self.B_AnimalResponseHistory[-IgnoredN:]==2) and np.shape(self.B_AnimalResponseHistory)[0]>=IgnoredN: self.CurrentAutoReward=1 - self.win.WarningLabelAutoWater.setText('Auto water because ignored trials exceed: '+self.TP_Ignored) - self.win.WarningLabelAutoWater.setStyleSheet(self.win.default_warning_color) + logging.warning('Auto water because ignored trials exceed: '+self.TP_Ignored) elif (np.all(B_RewardedHistory[0][-UnrewardedN:]==False) and np.all(B_RewardedHistory[1][-UnrewardedN:]==False) and np.shape(B_RewardedHistory[0])[0]>=UnrewardedN): self.CurrentAutoReward=1 - self.win.WarningLabelAutoWater.setText('Auto water because unrewarded trials exceed: '+self.TP_Unrewarded) - self.win.WarningLabelAutoWater.setStyleSheet(self.win.default_warning_color) + logging.warning('Auto water because unrewarded trials exceed: '+self.TP_Unrewarded) else: self.CurrentAutoReward=0 else: @@ -1192,8 +1182,7 @@ def _GetLaserWaveForm(self): # the duration is determined by CurrentITI, CurrentDelay, self.CLP_OffsetStart, self.CLP_OffsetEnd # only positive CLP_OffsetStart is allowed if self.CLP_OffsetStart<0: - self.win.WarningLabel.setText('Please set offset start to be positive!') - self.win.WarningLabel.setStyleSheet(self.win.default_warning_color) + logging.warning('Please set offset start to be positive!') # there is no delay for optogenetics trials self.CLP_CurrentDuration=self.CurrentITI-self.CLP_OffsetStart+self.CLP_OffsetEnd elif self.CLP_LaserStart=='Go cue' and self.CLP_LaserEnd=='Trial start': @@ -1227,14 +1216,12 @@ def _ProduceWaveForm(self,Amplitude): elif self.CLP_Protocol=='Pulse': if self.CLP_PulseDur=='NA': - self.win.WarningLabel.setText('Pulse duration is NA!') - self.win.WarningLabel.setStyleSheet(self.win.default_warning_color) + logging.warning('Pulse duration is NA!') self.CLP_PulseDur=0 self.my_wave=np.empty(0) self.opto_error_tag=1 elif self.CLP_Frequency=='': - self.win.WarningLabel.setText('Pulse frequency is NA!') - self.win.WarningLabel.setStyleSheet(self.win.default_warning_color) + logging.warning('Pulse frequency is NA!') self.CLP_Frequency=0 self.my_wave=np.empty(0) self.opto_error_tag=1 @@ -1243,8 +1230,7 @@ def _ProduceWaveForm(self,Amplitude): PointsEachPulse=int(self.CLP_SampleFrequency*self.CLP_PulseDur) PulseIntervalPoints=int(1/self.CLP_Frequency*self.CLP_SampleFrequency-PointsEachPulse) if PulseIntervalPoints<0: - self.win.WarningLabel.setText('Pulse frequency and pulse duration are not compatible!') - self.win.WarningLabel.setStyleSheet(self.win.default_warning_color) + logging.warning('Pulse frequency and pulse duration are not compatible!') TotalPoints=int(self.CLP_SampleFrequency*self.CLP_CurrentDuration) PulseNumber=np.floor(self.CLP_CurrentDuration*self.CLP_Frequency) EachPulse=Amplitude*np.ones(PointsEachPulse) @@ -1256,8 +1242,7 @@ def _ProduceWaveForm(self,Amplitude): for i in range(int(PulseNumber-1)): self.my_wave=np.concatenate((self.my_wave, WaveFormEachCycle), axis=0) else: - self.win.WarningLabel.setText('Pulse number is less than 1!') - self.win.WarningLabel.setStyleSheet(self.win.default_warning_color) + logging.warning('Pulse number is less than 1!') return self.my_wave=np.concatenate((self.my_wave, EachPulse), axis=0) self.my_wave=np.concatenate((self.my_wave, np.zeros(TotalPoints-np.shape(self.my_wave)[0])), axis=0) @@ -1273,8 +1258,7 @@ def _ProduceWaveForm(self,Amplitude): self._add_offset() self.my_wave=np.append(self.my_wave,[0,0]) else: - self.win.WarningLabel.setText('Unidentified optogenetics protocol!') - self.win.WarningLabel.setStyleSheet(self.win.default_warning_color) + logging.warning('Unidentified optogenetics protocol!') ''' # test @@ -1286,8 +1270,7 @@ def _get_ramping_down(self): '''Add ramping down to the waveform''' if self.CLP_RampingDown>0: if self.CLP_RampingDown>self.CLP_CurrentDuration: - self.win.WarningLabel.setText('Ramping down is longer than the laser duration!') - self.win.WarningLabel.setStyleSheet(self.win.default_warning_color) + logging.warning('Ramping down is longer than the laser duration!') else: Constant=np.ones(int((self.CLP_CurrentDuration-self.CLP_RampingDown)*self.CLP_SampleFrequency)) RD=np.arange(1,0, -1/(np.shape(self.my_wave)[0]-np.shape(Constant)[0])) @@ -1306,29 +1289,25 @@ def _GetLaserAmplitude(self): self.CurrentLaserAmplitude=[0,0] if self.CLP_Location=='Laser_1': if self.CLP_Laser1Power=='': - self.win.WarningLabel.setText('No amplitude for Laser_1 defined!') - self.win.WarningLabel.setStyleSheet(self.win.default_warning_color) + logging.warning('No amplitude for Laser_1 defined!') else: Laser1PowerAmp=eval(self.CLP_Laser1Power) self.CurrentLaserAmplitude=[Laser1PowerAmp[0],0] elif self.CLP_Location=='Laser_2': if self.CLP_Laser2Power=='': - self.win.WarningLabel.setText('No amplitude for Laser_2 defined!') - self.win.WarningLabel.setStyleSheet(self.win.default_warning_color) + logging.warning('No amplitude for Laser_2 defined!') else: Laser2PowerAmp=eval(self.CLP_Laser2Power) self.CurrentLaserAmplitude=[0,Laser2PowerAmp[0]] elif self.CLP_Location=='Both': if self.CLP_Laser1Power=='' or self.CLP_Location=='': - self.win.WarningLabel.setText('No amplitude for Laser_1 or Laser_2 laser defined!') - self.win.WarningLabel.setStyleSheet(self.win.default_warning_color) + logging.warning('No amplitude for Laser_1 or Laser_2 laser defined!') else: Laser1PowerAmp=eval(self.CLP_Laser1Power) Laser2PowerAmp=eval(self.CLP_Laser2Power) self.CurrentLaserAmplitude=[Laser1PowerAmp[0],Laser2PowerAmp[0]] else: - self.win.WarningLabel.setText('No stimulation location defined!') - self.win.WarningLabel.setStyleSheet(self.win.default_warning_color) + logging.warning('No stimulation location defined!') self.B_LaserAmplitude.append(self.CurrentLaserAmplitude) def _SelectOptogeneticsCondition(self): @@ -1390,9 +1369,6 @@ def _InitiateATrial(self,Channel1,Channel4): self.B_Baited= self.CurrentBait.copy() self.B_BaitHistory=np.append(self.B_BaitHistory, self.CurrentBait.reshape(2,1),axis=1) # determine auto water - if self.CurrentAutoReward==0: - self.win.WarningLabelAutoWater.setText('') - self.win.WarningLabelAutoWater.setStyleSheet("color: gray;") if self.CurrentAutoReward==1: self.CurrentAutoRewardTrial=[0,0] if self.TP_AutoWaterType=='Natural': @@ -1434,8 +1410,7 @@ def _InitiateATrial(self,Channel1,Channel4): Channel1.PassGoCue(int(0)) Channel1.PassRewardOutcome(int(1)) else: - self.win.WarningLabel.setText('Unindentified optogenetics start event!') - self.win.WarningLabel.setStyleSheet(self.win.default_warning_color) + logging.warning('Unindentified optogenetics start event!') # send the waveform size Channel1.Location1_Size(int(self.Location1_Size)) Channel1.Location2_Size(int(self.Location2_Size)) @@ -1657,11 +1632,9 @@ def _GetAnimalResponse(self,Channel1,Channel3,Channel4): # give reserved manual water if float(self.win.give_left_volume_reserved) > 0 or float(self.win.give_right_volume_reserved) > 0: # Set the text of a label or text widget to show the reserved volumes - self.win.ManualWaterWarning.setText( + logging.warning( f'Give reserved manual water (ul) left: {self.win.give_left_volume_reserved}; right: {self.win.give_right_volume_reserved}' ) - # Set the text color of the label or text widget to red - self.win.ManualWaterWarning.setStyleSheet(self.win.default_warning_color) # The manual water of two sides are given sequentially. Randomlizing the order to avoid bias. if np.random.random(1)<0.5: diff --git a/src/foraging_gui/warning_widget.py b/src/foraging_gui/warning_widget.py index 2eb1a6412..dbdf1ef16 100644 --- a/src/foraging_gui/warning_widget.py +++ b/src/foraging_gui/warning_widget.py @@ -1,5 +1,5 @@ import logging -from multiprocessing import Queue +from queue import Queue from logging.handlers import QueueHandler from random import randint from PyQt5.QtCore import QTimer @@ -13,7 +13,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - #self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") + self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") # create vertical layout self.setLayout(QVBoxLayout()) @@ -22,7 +22,7 @@ def __init__(self, *args, **kwargs): self.queue = Queue() queue_handler = QueueHandler(self.queue) queue_handler.setLevel(logging.WARNING) - logger.root.addHandler(queue_handler) + self.logger.root.addHandler(queue_handler) # create QTimer to periodically check queue self.check_timer = QTimer(timeout=self.check_warning_queue, interval=1000) @@ -30,19 +30,17 @@ def __init__(self, *args, **kwargs): def check_warning_queue(self): """ - Check queue and update layout with latest warnings + Check queue and update layout with the latest warnings """ - #print('while not que empty', self.queue.qsize(), self.queue.full()) - if not self.queue.empty(): - print(self.queue.get(block=False, timeout=1)) - # label = QLabel(str(self.queue.get().getMessage())) - # self.layout().insertWidget(0, label) + while not self.queue.empty(): + label = QLabel(str(self.queue.get().getMessage())) + self.layout().insertWidget(0, label) # prune layout if too many warnings - # if self.layout().count() == 30: - # widget = self.layout().itemAt(29).widget() - # self.layout().removeWidget(widget) + if self.layout().count() == 30: + widget = self.layout().itemAt(29).widget() + self.layout().removeWidget(widget) if __name__ == '__main__': app = QApplication(sys.argv) @@ -52,14 +50,13 @@ def check_warning_queue(self): stream_handler.setLevel(logger.root.level) logger.root.addHandler(stream_handler) - warn_widget = WarningWidget(logger) + warn_widget = WarningWidget() + warn_widget.show() warnings = ['this is a warning', 'this is also a warning', 'this is a warning too', 'Warn warn warn', 'are you warned yet?'] - warning_timer = QTimer(timeout=lambda: logger.warning(warnings[randint(0, 5)]), interval=1000) - + warning_timer = QTimer(timeout=lambda: logger.warning(warnings[randint(0, 4)]), interval=1000) warning_timer.start() - warn_widget.show() sys.exit(app.exec_()) \ No newline at end of file From c824cf21fd6c35d4bcb2f275b94544d6a755b9a4 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 5 Nov 2024 08:30:13 -0800 Subject: [PATCH 18/42] more label replacement --- src/foraging_gui/Foraging.py | 2 +- src/foraging_gui/warning_widget.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 3a82c1e53..ef9268234 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -2656,7 +2656,7 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): if BackupSave==0: text="Session metadata generated successfully: " + str(generated_metadata.session_metadata_success)+"\n"+\ "Rig metadata generated successfully: " + str(generated_metadata.rig_metadata_success) - self._manage_warning_labels(self.MetadataWarning,warning_text=text) + logging.warning(text) Obj['generate_session_metadata_success']=generated_metadata.session_metadata_success Obj['generate_rig_metadata_success']=generated_metadata.rig_metadata_success diff --git a/src/foraging_gui/warning_widget.py b/src/foraging_gui/warning_widget.py index dbdf1ef16..2e3501cfc 100644 --- a/src/foraging_gui/warning_widget.py +++ b/src/foraging_gui/warning_widget.py @@ -34,7 +34,8 @@ def check_warning_queue(self): """ while not self.queue.empty(): - label = QLabel(str(self.queue.get().getMessage())) + log = self.queue.get() + label = QLabel(str(log.getMessage())) self.layout().insertWidget(0, label) # prune layout if too many warnings From 5391ebd99c8f63d1165feb74defbd61016350714 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 5 Nov 2024 11:56:24 -0800 Subject: [PATCH 19/42] adding tags to all warningwidget log messages --- src/foraging_gui/Dialogs.py | 31 +++++++-------- src/foraging_gui/Foraging.py | 61 +++++++++++++++++------------- src/foraging_gui/MyFunctions.py | 59 ++++++++++++++++++----------- src/foraging_gui/warning_widget.py | 29 ++++++++++++-- 4 files changed, 110 insertions(+), 70 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index e7fce1e12..90aa0fbbf 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -278,7 +278,8 @@ def _LaserColor(self,Numb): if no_calibration: for laser_tag in self.laser_tags: getattr(self, f"Laser{laser_tag}_power_{str(Numb)}").clear() - logging.warning('No calibration for this protocol identified!') + logging.warning('No calibration for this protocol identified!', + extra={'tags': [self.MainWindow.warning_log_tag]}) getattr(self, 'Location_' + str(Numb)).setEnabled(Label) getattr(self, 'Laser1_power_' + str(Numb)).setEnabled(Label) @@ -1367,7 +1368,7 @@ def _StartCamera(self): self.MainWindow.Channel.CameraControl(int(1)) time.sleep(5) self.camera_start_time = str(datetime.now()) - logging.warning('Camera is on!') + logging.info('Camera is on!', extra={'tags': [self.MainWindow.warning_log_tag]}) self.WarningLabelCameraOn.setText('Camera is on!') self.WarningLabelCameraOn.setStyleSheet(self.MainWindow.default_warning_color) self.WarningLabelLogging.setText('') @@ -1381,7 +1382,7 @@ def _StartCamera(self): self.MainWindow.Channel.CameraControl(int(2)) self.camera_stop_time = str(datetime.now()) time.sleep(5) - logging.warning('Camera is off!') + logging.info('Camera is off!', extra={'tags': [self.MainWindow.warning_log_tag]}) self.WarningLabelCameraOn.setText('Camera is off!') self.WarningLabelCameraOn.setStyleSheet(self.MainWindow.default_warning_color) self.WarningLabelLogging.setText('') @@ -1552,7 +1553,8 @@ def _ProduceWaveForm(self,Amplitude): # add ramping down if self.CLP_RampingDown>0: if self.CLP_RampingDown>self.CLP_CurrentDuration: - logging.warning('Ramping down is longer than the laser duration!') + logging.warning('Ramping down is longer than the laser duration!', + extra={'tags': [self.MainWindow.warning_log_tag]}) else: Constant=np.ones(int((self.CLP_CurrentDuration-self.CLP_RampingDown)*self.CLP_SampleFrequency)) RD=np.arange(1,0, -1/(np.shape(self.my_wave)[0]-np.shape(Constant)[0])) @@ -1561,13 +1563,14 @@ def _ProduceWaveForm(self,Amplitude): self.my_wave=np.append(self.my_wave,[0,0]) elif self.CLP_Protocol=='Pulse': if self.CLP_PulseDur=='NA': - logging.warning('Pulse duration is NA!') + logging.warning('Pulse duration is NA!', extra={'tags': [self.MainWindow.warning_log_tag]}) else: self.CLP_PulseDur=float(self.CLP_PulseDur) PointsEachPulse=int(self.CLP_SampleFrequency*self.CLP_PulseDur) PulseIntervalPoints=int(1/self.CLP_Frequency*self.CLP_SampleFrequency-PointsEachPulse) if PulseIntervalPoints<0: - logging.warning('Pulse frequency and pulse duration are not compatible!') + logging.warning('Pulse frequency and pulse duration are not compatible!', + extra={'tags': [self.MainWindow.warning_log_tag]}) TotalPoints=int(self.CLP_SampleFrequency*self.CLP_CurrentDuration) PulseNumber=np.floor(self.CLP_CurrentDuration*self.CLP_Frequency) EachPulse=Amplitude*np.ones(PointsEachPulse) @@ -1579,7 +1582,7 @@ def _ProduceWaveForm(self,Amplitude): for i in range(int(PulseNumber-1)): self.my_wave=np.concatenate((self.my_wave, WaveFormEachCycle), axis=0) else: - logging.warning('Pulse number is less than 1!') + logging.warning('Pulse number is less than 1!', extra={'tags': [self.MainWindow.warning_log_tag]}) return self.my_wave=np.concatenate((self.my_wave, EachPulse), axis=0) self.my_wave=np.concatenate((self.my_wave, np.zeros(TotalPoints-np.shape(self.my_wave)[0])), axis=0) @@ -1590,7 +1593,8 @@ def _ProduceWaveForm(self,Amplitude): if self.CLP_RampingDown>0: # add ramping down if self.CLP_RampingDown>self.CLP_CurrentDuration: - logging.warning('Ramping down is longer than the laser duration!') + logging.warning('Ramping down is longer than the laser duration!', + extra={'tags': [self.MainWindow.warning_log_tag]}) else: Constant=np.ones(int((self.CLP_CurrentDuration-self.CLP_RampingDown)*self.CLP_SampleFrequency)) RD=np.arange(1,0, -1/(np.shape(self.my_wave)[0]-np.shape(Constant)[0])) @@ -1598,7 +1602,7 @@ def _ProduceWaveForm(self,Amplitude): self.my_wave=self.my_wave*RampingDown self.my_wave=np.append(self.my_wave,[0,0]) else: - logging.warning('Unidentified optogenetics protocol!') + logging.warning('Unidentified optogenetics protocol!', extra={'tags': [self.MainWindow.warning_log_tag]}) def _GetLaserAmplitude(self): '''the voltage amplitude dependens on Protocol, Laser Power, Laser color, and the stimulation locations<>''' @@ -1609,7 +1613,7 @@ def _GetLaserAmplitude(self): elif self.CLP_Location=='Both': self.CurrentLaserAmplitude=[self.CLP_InputVoltage,self.CLP_InputVoltage] else: - logging.warning('No stimulation location defined!') + logging.warning('No stimulation location defined!', extra={'tags': [self.MainWindow.warning_log_tag]}) # get training parameters def _GetTrainingParameters(self,win): @@ -1960,7 +1964,6 @@ def _connectSignalsSlots(self): self.ManipulatorZ.textChanged.connect(self._save_configuration) self.SaveMeta.clicked.connect(self._save_metadata) self.LoadMeta.clicked.connect(self._load_metadata) - self.RigMetadataFile.textChanged.connect(self._removing_warning) self.ClearMetadata.clicked.connect(self._clear_metadata) self.Stick_ArcAngle.textChanged.connect(self._save_configuration) self.Stick_ModuleAngle.textChanged.connect(self._save_configuration) @@ -2027,12 +2030,6 @@ def _clear_metadata(self): self.ExperimentDescription.clear() self._update_metadata() - def _removing_warning(self): - '''remove the warning''' - if self.RigMetadataFile.text()!='': - pass - #self.MainWindow._manage_warning_labels(self.MainWindow.MetadataWarning,warning_text='') - def _load_metadata(self): '''load the metadata from a json file''' metadata_dialog_file, _ = QFileDialog.getOpenFileName( diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index ef9268234..f588ad362 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -199,7 +199,8 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): self.upload_manifest_slot = self.sessionGenerated.connect(self._generate_upload_manifest) # create and add warning_widget - self.warning_widget = WarningWidget() + self.warning_log_tag = 'warning_widget' # TODO: How to set this or does it matter? + self.warning_widget = WarningWidget(log_tag=self.warning_log_tag) self.scrollArea_6.setWidget(self.warning_widget) # show disk space @@ -638,7 +639,7 @@ def _check_drop_frames(self,save_tag=1): elif ('HighSpeedCamera' in self.SettingsBox) and (self.SettingsBox['HighSpeedCamera'] ==1): self.trigger_length=0 logging.error('Saved video data, but no camera trigger file found') - logging.warning('No camera trigger file found!') + logging.info('No camera trigger file found!', extra={'tags': [self.warning_log_tag]}) return else: logging.info('Saved video data, but not using high speed camera - skipping drop frame check') @@ -662,7 +663,7 @@ def _check_drop_frames(self,save_tag=1): else: self.drop_frames_warning_text+=f"Correct: {avi_file} has {num_frames} frames and {self.trigger_length} triggers\n" self.frame_num[camera_name] = num_frames - logging.warning(self.drop_frames_warning_text) + logging.warning(self.drop_frames_warning_text, extra={'tags': [self.warning_log_tag]}) # only check drop frames once each session self.to_check_drop_frames=0 @@ -865,7 +866,8 @@ def _InitializeMotorStage(self): # If we can't find any stages, return if len(self.instances) == 0: - logging.warning('Could not find any instances of NewScale Stage') + logging.info('Could not find any instances of NewScale Stage', + extra={'tags': [self.warning_log_tag]}) self._no_stage() return @@ -938,7 +940,7 @@ def _ConnectBonsai(self): subprocess.Popen('title Box{}'.format(self.box_letter),shell=True) except Exception as e: logging.error(traceback.format_exc()) - logging.warning('Please open bonsai!') + logging.warning('Please open bonsai!', extra={'tags': [self.warning_log_tag]}) self.InitializeBonsaiSuccessfully=0 def _ReconnectBonsai(self): @@ -1191,7 +1193,8 @@ def _GetSettings(self): for key in defaults: if key not in self.Settings: self.Settings[key] = defaults[key] - logging.warning('Missing setting ({}), using default: {}'.format(key,self.Settings[key])) + logging.warning('Missing setting ({}), using default: {}'.format(key,self.Settings[key]), + extra={'tags': [self.warning_log_tag]}) if key in ['default_saveFolder','current_box']: logging.error('Missing setting ({}), is required'.format(key)) raise Exception('Missing setting ({}), is required'.format(key)) @@ -1388,7 +1391,7 @@ def _InitializeBonsai(self): # Could not connect and we timed out logging.info('Could not connect to bonsai with max wait time {} seconds'.format(max_wait)) - logging.warning('Started without bonsai connected!') + logging.warning('Started without bonsai connected!', extra={'tags': [self.warning_log_tag]}) def _ConnectOSC(self): ''' @@ -2448,7 +2451,7 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): "Do you want to save without weight or extra water information provided?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,QMessageBox.Yes) if response==QMessageBox.Yes: - logging.warning('Saving without weight or extra water!') + logging.warning('Saving without weight or extra water!', extra={'tags': [self.warning_log_tag]}) pass elif response==QMessageBox.No: logging.info('saving declined by user') @@ -2465,7 +2468,7 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): "Do you want to save without complete laser target or laser power calibration information provided?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,QMessageBox.Yes) if response==QMessageBox.Yes: - logging.warning('Saving without laser target or laser power!') + logging.warning('Saving without laser target or laser power!', extra={'tags': [self.warning_log_tag]}) pass elif response==QMessageBox.No: logging.info('saving declined by user') @@ -2656,7 +2659,7 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): if BackupSave==0: text="Session metadata generated successfully: " + str(generated_metadata.session_metadata_success)+"\n"+\ "Rig metadata generated successfully: " + str(generated_metadata.rig_metadata_success) - logging.warning(text) + logging.warning(text, extra={'tags': [self.warning_log_tag]}) Obj['generate_session_metadata_success']=generated_metadata.session_metadata_success Obj['generate_rig_metadata_success']=generated_metadata.rig_metadata_success @@ -2695,9 +2698,9 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): short_file = self.SaveFile.split('\\')[-1] if self.load_tag==0: - logging.warning('Saved: {}'.format(short_file)) + logging.warning('Saved: {}'.format(short_file), extra={'tags': [self.warning_log_tag]}) else: - logging.warning('Saving of loaded files is not allowed!') + logging.warning('Saving of loaded files is not allowed!', extra={'tags': [self.warning_log_tag]}) self.SessionlistSpin.setEnabled(True) self.Sessionlist.setEnabled(True) @@ -3314,10 +3317,10 @@ def _StartExcitation(self): elif self.FIPMode.currentText() == "Axon": ser.write(b'e') ser.close() - logging.warning('Started FIP excitation') + logging.info('Started FIP excitation', extra={'tags': [self.warning_log_tag]}) except Exception as e: logging.error(traceback.format_exc()) - logging.warning('Error: starting excitation!') + logging.warning('Error: starting excitation!', extra={'tags': [self.warning_log_tag]}) reply = QMessageBox.critical(self, 'Box {}, Start excitation:'.format(self.box_letter), 'error when starting excitation: {}'.format(e), QMessageBox.Ok) self.StartExcitation.setChecked(False) self.StartExcitation.setStyleSheet("background-color : none") @@ -3333,10 +3336,10 @@ def _StartExcitation(self): # Trigger Teensy with the above specified exp mode ser.write(b's') ser.close() - logging.warning('Stopped FIP excitation') + logging.info('Stopped FIP excitation', extra={'tags': [self.warning_log_tag]}) except Exception as e: logging.error(traceback.format_exc()) - logging.warning('Error stopping excitation!') + logging.warning('Error stopping excitation!', extra={'tags': [self.warning_log_tag]}) reply = QMessageBox.critical(self, 'Box {}, Start excitation:'.format(self.box_letter), 'error when stopping excitation: {}'.format(e), QMessageBox.Ok) return 0 else: @@ -3381,12 +3384,12 @@ def _StartBleaching(self): # Trigger Teensy with the above specified exp mode ser.write(b'd') ser.close() - logging.warning('Start bleaching!') + logging.info('Start bleaching!', extra={'tags': [self.warning_log_tag]}) except Exception as e: logging.error(traceback.format_exc()) # Alert user - logging.warning('Error: start bleaching!') + logging.warning('Error: start bleaching!', extra={'tags': [self.warning_log_tag]}) reply = QMessageBox.critical(self, 'Box {}, Start bleaching:'.format(self.box_letter), 'Cannot start photobleaching: {}'.format(str(e)), QMessageBox.Ok) @@ -3481,7 +3484,7 @@ def _stop_logging(self): self.logging_type=-1 # logging has stopped except Exception as e: logging.warning('Bonsai connection is closed') - logging.warning('Lost bonsai connection') + logging.warning('Lost bonsai connection', extra={'tags': [self.warning_log_tag]}) self.InitializeBonsaiSuccessfully=0 def _NewSession(self): @@ -3535,7 +3538,7 @@ def _NewSession(self): self._ConnectBonsai() if self.InitializeBonsaiSuccessfully == 0: - logging.warning('Lost bonsai connection') + logging.warning('Lost bonsai connection', extra={'tags': [self.warning_log_tag]}) # Reset state variables self._StopPhotometry() # Make sure photoexcitation is stopped @@ -3587,7 +3590,7 @@ def _StopCurrentSession(self): stall_iteration = 1 stall_duration = 5*60 if self.ANewTrial==0: - logging.warning('Waiting for the finish of the last trial!') + logging.warning('Waiting for the finish of the last trial!', extra={'tags': [self.warning_log_tag]}) while 1: QApplication.processEvents() if self.ANewTrial==1: @@ -3641,7 +3644,8 @@ def _update_photometery_timer(self,time): if len(str(seconds)) == 1: seconds = '0{}'.format(seconds) if not self.ignore_timer: - logging.warning('Running photometry baseline: {}:{}'.format(minutes,seconds)) + logging.info('Running photometry baseline: {}:{}'.format(minutes,seconds), + extra={'tags': [self.warning_log_tag]}) def _set_metadata_enabled(self, enable: bool): '''Enable or disable metadata fields''' @@ -3950,7 +3954,7 @@ def _Start(self): except Exception as e: if 'ConnectionAbortedError' in str(e): logging.info('lost bonsai connection: restartlogging()') - logging.warning('Lost bonsai connection') + logging.warning('Lost bonsai connection', extra={'tags': [self.warning_log_tag]}) self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") self.InitializeBonsaiSuccessfully=0 @@ -4061,7 +4065,7 @@ def _Start(self): self.workertimer_thread.start() self.Time.emit(int(np.floor(float(self.baselinetime.text())*60))) - logging.warning('Running photometry baseline') + logging.info('Running photometry baseline', extra={'tags': [self.warning_log_tag]}) self._StartTrialLoop(GeneratedTrials,worker1,worker_save) @@ -4260,7 +4264,8 @@ def _StartTrialLoop(self,GeneratedTrials,worker1,worker_save): self.Start.setStyleSheet("background-color : none") # Give warning to user - logging.warning('Trials stalled, recheck bonsai connection.') + logging.warning('Trials stalled, recheck bonsai connection.', + extra={'tags': [self.warning_log_tag]}) break else: # User continues, wait another stall_duration and prompt again @@ -4367,7 +4372,8 @@ def _GiveLeft(self): self.Channel.LeftValue(float(self.TP_LeftValue)*1000) self.ManualWaterVolume[0]=self.ManualWaterVolume[0]+float(self.TP_GiveWaterL_volume)/1000 self._UpdateSuggestedWater() - logger.warning('Give left manual water (ul): '+str(np.round(float(self.TP_GiveWaterL_volume),3))) + logger.info('Give left manual water (ul): '+str(np.round(float(self.TP_GiveWaterL_volume),3)), + extra={'tags': [self.warning_log_tag]}) def _give_reserved_water(self,valve=None): @@ -4422,7 +4428,8 @@ def _GiveRight(self): self.Channel.RightValue(float(self.TP_RightValue)*1000) self.ManualWaterVolume[1]=self.ManualWaterVolume[1]+float(self.TP_GiveWaterR_volume)/1000 self._UpdateSuggestedWater() - logger.warning('Give right manual water (ul): '+str(np.round(float(self.TP_GiveWaterR_volume),3))) + logger.info('Give right manual water (ul): '+str(np.round(float(self.TP_GiveWaterR_volume),3)), + extra={'tags': [self.warning_log_tag]}) def _toggle_save_color(self): '''toggle the color of the save button to mediumorchid''' diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index e659341f8..660b62308 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -163,7 +163,7 @@ def _GenerateATrial(self,Channel4): )) # Show msg - logging.warning(msg_uncoupled_block) + logging.warning(msg_uncoupled_block, extra={'tags': [self.win.warning_log_tag]}) # Append the (updated) current reward probability to the history self.B_RewardProHistory=np.append( @@ -283,7 +283,7 @@ def _CheckWarmUp(self): self.win.keyPressEvent() self.win.NextBlock.setChecked(True) self.win._NextBlock() - logging.warning('Warm up is turned off') + logging.info('Warm up is turned off', extra={'tags': [self.win.warning_log_tag]}) def _get_warmup_state(self): '''calculate the metrics related to the warm up and decide if we should turn on the warm up''' @@ -308,7 +308,9 @@ def _get_warmup_state(self): # turn on the warm up warmup=1 # show current metrics of the warm up - logging.error('Finish trial: '+str(round(finish_trial,2))+ '; Finish ratio: '+str(round(finish_ratio,2))+'; Choice ratio bias: '+str(round(abs(choice_ratio-0.5),2))) + logging.info('Finish trial: '+str(round(finish_trial,2))+ '; Finish ratio: '+str(round(finish_ratio,2))+ + '; Choice ratio bias: '+str(round(abs(choice_ratio-0.5),2)), + extra={'tags': [self.win.warning_log_tag]}) return warmup def _CheckBaitPermitted(self): @@ -324,7 +326,8 @@ def _CheckBaitPermitted(self): else: self.BaitPermitted=True if self.BaitPermitted==False: - logging.warning('The active side has no reward due to consecutive \nselections('+str(MaxCLen)+')<'+self.TP_InitiallyInactiveN) + logging.warning('The active side has no reward due to consecutive \nselections('+str(MaxCLen)+')<'+ + self.TP_InitiallyInactiveN, extra={'tags': [self.win.warning_log_tag]}) def _GetMaximumConSelection(self): '''get the maximum consecutive selection of the active side of the current block''' @@ -1116,7 +1119,7 @@ def _CheckStop(self): stop=False # Update the warning label text/color - logging.warning(warning_label_text) + logging.warning(warning_label_text, extra={'tags': [self.win.warning_log_tag]}) # If we should stop trials, uncheck the start button if stop: @@ -1131,9 +1134,11 @@ def _CheckAutoWater(self): IgnoredN=int(self.TP_Ignored) if UnrewardedN<=0: self.CurrentAutoReward=1 - logging.warning('Auto water because unrewarded trials exceed: '+self.TP_Unrewarded) + logging.warning('Auto water because unrewarded trials exceed: '+self.TP_Unrewarded, + extra={'tags': [self.win.warning_log_tag]}) elif IgnoredN <=0: - logging.warning('Auto water because ignored trials exceed: '+self.TP_Ignored) + logging.warning('Auto water because ignored trials exceed: '+self.TP_Ignored, + extra={'tags': [self.win.warning_log_tag]}) self.CurrentAutoReward=1 else: if np.shape(self.B_AnimalResponseHistory)[0]>=IgnoredN or np.shape(self.B_RewardedHistory[0])[0]>=UnrewardedN: @@ -1144,10 +1149,12 @@ def _CheckAutoWater(self): B_RewardedHistory[i]=np.logical_or(self.B_RewardedHistory[i],self.B_AutoWaterTrial[i][Ind]) if np.all(self.B_AnimalResponseHistory[-IgnoredN:]==2) and np.shape(self.B_AnimalResponseHistory)[0]>=IgnoredN: self.CurrentAutoReward=1 - logging.warning('Auto water because ignored trials exceed: '+self.TP_Ignored) + logging.warning('Auto water because ignored trials exceed: '+self.TP_Ignored, + extra={'tags': [self.win.warning_log_tag]}) elif (np.all(B_RewardedHistory[0][-UnrewardedN:]==False) and np.all(B_RewardedHistory[1][-UnrewardedN:]==False) and np.shape(B_RewardedHistory[0])[0]>=UnrewardedN): self.CurrentAutoReward=1 - logging.warning('Auto water because unrewarded trials exceed: '+self.TP_Unrewarded) + logging.warning('Auto water because unrewarded trials exceed: '+self.TP_Unrewarded, + extra={'tags': [self.win.warning_log_tag]}) else: self.CurrentAutoReward=0 else: @@ -1182,7 +1189,7 @@ def _GetLaserWaveForm(self): # the duration is determined by CurrentITI, CurrentDelay, self.CLP_OffsetStart, self.CLP_OffsetEnd # only positive CLP_OffsetStart is allowed if self.CLP_OffsetStart<0: - logging.warning('Please set offset start to be positive!') + logging.warning('Please set offset start to be positive!', extra={'tags': [self.win.warning_log_tag]}) # there is no delay for optogenetics trials self.CLP_CurrentDuration=self.CurrentITI-self.CLP_OffsetStart+self.CLP_OffsetEnd elif self.CLP_LaserStart=='Go cue' and self.CLP_LaserEnd=='Trial start': @@ -1216,12 +1223,12 @@ def _ProduceWaveForm(self,Amplitude): elif self.CLP_Protocol=='Pulse': if self.CLP_PulseDur=='NA': - logging.warning('Pulse duration is NA!') + logging.warning('Pulse duration is NA!', extra={'tags': [self.win.warning_log_tag]}) self.CLP_PulseDur=0 self.my_wave=np.empty(0) self.opto_error_tag=1 elif self.CLP_Frequency=='': - logging.warning('Pulse frequency is NA!') + logging.warning('Pulse frequency is NA!', extra={'tags': [self.win.warning_log_tag]}) self.CLP_Frequency=0 self.my_wave=np.empty(0) self.opto_error_tag=1 @@ -1230,7 +1237,8 @@ def _ProduceWaveForm(self,Amplitude): PointsEachPulse=int(self.CLP_SampleFrequency*self.CLP_PulseDur) PulseIntervalPoints=int(1/self.CLP_Frequency*self.CLP_SampleFrequency-PointsEachPulse) if PulseIntervalPoints<0: - logging.warning('Pulse frequency and pulse duration are not compatible!') + logging.warning('Pulse frequency and pulse duration are not compatible!', + extra={'tags': [self.win.warning_log_tag]}) TotalPoints=int(self.CLP_SampleFrequency*self.CLP_CurrentDuration) PulseNumber=np.floor(self.CLP_CurrentDuration*self.CLP_Frequency) EachPulse=Amplitude*np.ones(PointsEachPulse) @@ -1242,7 +1250,7 @@ def _ProduceWaveForm(self,Amplitude): for i in range(int(PulseNumber-1)): self.my_wave=np.concatenate((self.my_wave, WaveFormEachCycle), axis=0) else: - logging.warning('Pulse number is less than 1!') + logging.warning('Pulse number is less than 1!', extra={'tags': [self.win.warning_log_tag]}) return self.my_wave=np.concatenate((self.my_wave, EachPulse), axis=0) self.my_wave=np.concatenate((self.my_wave, np.zeros(TotalPoints-np.shape(self.my_wave)[0])), axis=0) @@ -1258,7 +1266,7 @@ def _ProduceWaveForm(self,Amplitude): self._add_offset() self.my_wave=np.append(self.my_wave,[0,0]) else: - logging.warning('Unidentified optogenetics protocol!') + logging.warning('Unidentified optogenetics protocol!', extra={'tags': [self.win.warning_log_tag]}) ''' # test @@ -1270,7 +1278,8 @@ def _get_ramping_down(self): '''Add ramping down to the waveform''' if self.CLP_RampingDown>0: if self.CLP_RampingDown>self.CLP_CurrentDuration: - logging.warning('Ramping down is longer than the laser duration!') + logging.warning('Ramping down is longer than the laser duration!', + extra={'tags': [self.win.warning_log_tag]}) else: Constant=np.ones(int((self.CLP_CurrentDuration-self.CLP_RampingDown)*self.CLP_SampleFrequency)) RD=np.arange(1,0, -1/(np.shape(self.my_wave)[0]-np.shape(Constant)[0])) @@ -1289,25 +1298,26 @@ def _GetLaserAmplitude(self): self.CurrentLaserAmplitude=[0,0] if self.CLP_Location=='Laser_1': if self.CLP_Laser1Power=='': - logging.warning('No amplitude for Laser_1 defined!') + logging.warning('No amplitude for Laser_1 defined!', extra={'tags': [self.win.warning_log_tag]}) else: Laser1PowerAmp=eval(self.CLP_Laser1Power) self.CurrentLaserAmplitude=[Laser1PowerAmp[0],0] elif self.CLP_Location=='Laser_2': if self.CLP_Laser2Power=='': - logging.warning('No amplitude for Laser_2 defined!') + logging.warning('No amplitude for Laser_2 defined!', extra={'tags': [self.win.warning_log_tag]}) else: Laser2PowerAmp=eval(self.CLP_Laser2Power) self.CurrentLaserAmplitude=[0,Laser2PowerAmp[0]] elif self.CLP_Location=='Both': if self.CLP_Laser1Power=='' or self.CLP_Location=='': - logging.warning('No amplitude for Laser_1 or Laser_2 laser defined!') + logging.warning('No amplitude for Laser_1 or Laser_2 laser defined!', + extra={'tags': [self.win.warning_log_tag]}) else: Laser1PowerAmp=eval(self.CLP_Laser1Power) Laser2PowerAmp=eval(self.CLP_Laser2Power) self.CurrentLaserAmplitude=[Laser1PowerAmp[0],Laser2PowerAmp[0]] else: - logging.warning('No stimulation location defined!') + logging.warning('No stimulation location defined!', extra={'tags': [self.win.warning_log_tag]}) self.B_LaserAmplitude.append(self.CurrentLaserAmplitude) def _SelectOptogeneticsCondition(self): @@ -1410,7 +1420,8 @@ def _InitiateATrial(self,Channel1,Channel4): Channel1.PassGoCue(int(0)) Channel1.PassRewardOutcome(int(1)) else: - logging.warning('Unindentified optogenetics start event!') + logging.warning('Unindentified optogenetics start event!', + extra={'tags': [self.win.warning_log_tag]}) # send the waveform size Channel1.Location1_Size(int(self.Location1_Size)) Channel1.Location2_Size(int(self.Location2_Size)) @@ -1632,8 +1643,10 @@ def _GetAnimalResponse(self,Channel1,Channel3,Channel4): # give reserved manual water if float(self.win.give_left_volume_reserved) > 0 or float(self.win.give_right_volume_reserved) > 0: # Set the text of a label or text widget to show the reserved volumes - logging.warning( - f'Give reserved manual water (ul) left: {self.win.give_left_volume_reserved}; right: {self.win.give_right_volume_reserved}' + logging.info( + f'Give reserved manual water (ul) left: {self.win.give_left_volume_reserved}; ' + f'right: {self.win.give_right_volume_reserved}', + extra={'tags': [self.win.warning_log_tag]} ) # The manual water of two sides are given sequentially. Randomlizing the order to avoid bias. diff --git a/src/foraging_gui/warning_widget.py b/src/foraging_gui/warning_widget.py index 2e3501cfc..474f63864 100644 --- a/src/foraging_gui/warning_widget.py +++ b/src/foraging_gui/warning_widget.py @@ -6,10 +6,15 @@ from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QScrollArea import sys + class WarningWidget(QWidget): """Widget that uses a logging QueueHandler to display log errors and warning""" - def __init__(self, *args, **kwargs): + def __init__(self, log_tag: str = 'warning_widget', log_level: str = 'INFO', *args, **kwargs): + """ + :param log_tag: log_tag to pass into filter + :param log_level: level for QueueHandler + """ super().__init__(*args, **kwargs) @@ -21,7 +26,8 @@ def __init__(self, *args, **kwargs): # configure queue handler to write to queue self.queue = Queue() queue_handler = QueueHandler(self.queue) - queue_handler.setLevel(logging.WARNING) + queue_handler.setLevel(log_level) + queue_handler.addFilter(WarningFilter(log_tag)) # add filter self.logger.root.addHandler(queue_handler) # create QTimer to periodically check queue @@ -43,6 +49,23 @@ def check_warning_queue(self): widget = self.layout().itemAt(29).widget() self.layout().removeWidget(widget) + +class WarningFilter(logging.Filter): + """ Log filter which logs messages with tags that contain keyword""" + + def __init__(self, keyword: str, *args, **kwargs): + """ + :param keyword: word that filter will look for in tags + """ + + self.keyword = keyword + super().__init__(*args, **kwargs) + + def filter(self, record): + """Returns True for a record that matches a log we want to keep.""" + return self.keyword in record.__dict__.get('tags', []) + + if __name__ == '__main__': app = QApplication(sys.argv) @@ -60,4 +83,4 @@ def check_warning_queue(self): warning_timer = QTimer(timeout=lambda: logger.warning(warnings[randint(0, 4)]), interval=1000) warning_timer.start() - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) From 7cd9179c84b6974eaf1189d35744df590f7217c7 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 5 Nov 2024 15:02:00 -0800 Subject: [PATCH 20/42] updating the Ephys ui file --- src/foraging_gui/Camera.ui | 112 --------- src/foraging_gui/Dialogs.py | 28 +-- src/foraging_gui/Foraging.py | 5 +- src/foraging_gui/ForagingGUI_Ephys.ui | 317 +------------------------- 4 files changed, 19 insertions(+), 443 deletions(-) diff --git a/src/foraging_gui/Camera.ui b/src/foraging_gui/Camera.ui index e485a4f61..05889c3f9 100644 --- a/src/foraging_gui/Camera.ui +++ b/src/foraging_gui/Camera.ui @@ -142,62 +142,6 @@ 10 - - - - 50 - 380 - 171 - 51 - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - 50 - 390 - 171 - 51 - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignCenter - - @@ -220,62 +164,6 @@ false - - - - 50 - 340 - 171 - 51 - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignCenter - - - - - - 50 - 350 - 171 - 51 - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignCenter - - diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index 90aa0fbbf..bd876d361 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -1292,11 +1292,9 @@ def _OpenSaveFolder(self): subprocess.Popen(['explorer', os.path.join(os.path.dirname(os.path.dirname(self.MainWindow.Ot_log_folder)),'behavior-videos')]) except Exception as e: logging.error(str(e)) - self.WarningLabelOpenSave.setText('No logging folder found!') - self.WarningLabelOpenSave.setStyleSheet(self.MainWindow.default_warning_color) + logging.warning('No logging folder found!', extra={'tags': self.MainWindow.warning_log_tag}) else: - self.WarningLabelOpenSave.setText('No logging folder found!') - self.WarningLabelOpenSave.setStyleSheet(self.MainWindow.default_warning_color) + logging.warning('No logging folder found!', extra={'tags': self.MainWindow.warning_log_tag}) def _start_preview(self): '''Start the camera preview''' @@ -1314,8 +1312,7 @@ def _start_preview(self): self.MainWindow.Channel.CameraControl(int(1)) self.StartPreview.setStyleSheet("background-color : green;") - self.WarningLabelCameraOn.setText('Camera is on') - self.WarningLabelCameraOn.setStyleSheet(self.MainWindow.default_warning_color) + logging.info('Camera is on', extra={'tags': self.MainWindow.warning_log_tag}) else: # enable the start recording button self.StartRecording.setEnabled(True) @@ -1325,8 +1322,7 @@ def _start_preview(self): self.MainWindow.Channel.StopCameraPreview(int(1)) self.StartPreview.setStyleSheet("background-color : none;") - self.WarningLabelCameraOn.setText('Camera is off') - self.WarningLabelCameraOn.setStyleSheet(self.MainWindow.default_warning_color) + logging.info('Camera is off', extra={'tags': self.MainWindow.warning_log_tag}) def _AutoControl(self): '''Trigger the camera during the start of a new behavior session''' @@ -1341,8 +1337,7 @@ def _StartCamera(self): return if self.StartRecording.isChecked(): self.StartRecording.setStyleSheet("background-color : green;") - self.WarningLabelCameraOn.setText('Camera is turning on') - self.WarningLabelCameraOn.setStyleSheet(self.MainWindow.default_warning_color) + logging.info('Camera is turning on', extra={'tags': self.MainWindow.warning_log_tag}) QApplication.processEvents() # untoggle the preview button if self.StartPreview.isChecked(): @@ -1369,25 +1364,14 @@ def _StartCamera(self): time.sleep(5) self.camera_start_time = str(datetime.now()) logging.info('Camera is on!', extra={'tags': [self.MainWindow.warning_log_tag]}) - self.WarningLabelCameraOn.setText('Camera is on!') - self.WarningLabelCameraOn.setStyleSheet(self.MainWindow.default_warning_color) - self.WarningLabelLogging.setText('') - self.WarningLabelLogging.setStyleSheet("color: None;") - self.WarningLabelOpenSave.setText('') else: self.StartRecording.setStyleSheet("background-color : none") - self.WarningLabelCameraOn.setText('Camera is turning off') - self.WarningLabelCameraOn.setStyleSheet(self.MainWindow.default_warning_color) + logging.info('Camera is turning off', extra={'tags': self.MainWindow.warning_log_tag}) QApplication.processEvents() self.MainWindow.Channel.CameraControl(int(2)) self.camera_stop_time = str(datetime.now()) time.sleep(5) logging.info('Camera is off!', extra={'tags': [self.MainWindow.warning_log_tag]}) - self.WarningLabelCameraOn.setText('Camera is off!') - self.WarningLabelCameraOn.setStyleSheet(self.MainWindow.default_warning_color) - self.WarningLabelLogging.setText('') - self.WarningLabelLogging.setStyleSheet("color: None;") - self.WarningLabelOpenSave.setText('') def is_file_in_use(file_path): '''check if the file is open''' diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index f588ad362..1d7ccb795 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -67,6 +67,8 @@ class Window(QMainWindow): def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): logging.info('Creating Window') + self.warning_log_tag = 'warning_widget' # TODO: How to set this or does it matter? + super().__init__(parent) # Process inputs @@ -199,7 +201,6 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): self.upload_manifest_slot = self.sessionGenerated.connect(self._generate_upload_manifest) # create and add warning_widget - self.warning_log_tag = 'warning_widget' # TODO: How to set this or does it matter? self.warning_widget = WarningWidget(log_tag=self.warning_log_tag) self.scrollArea_6.setWidget(self.warning_widget) @@ -2670,7 +2671,7 @@ def _Save(self,ForceSave=0,SaveAs=0,SaveContinue=0,BackupSave=0): except Exception as e: - self._manage_warning_labels(self.MetadataWarning,warning_text='Meta data is not saved!') + logging.warning('Meta data is not saved!', extra= {'tags': {self.warning_log_tag}}) logging.error('Error generating session metadata: '+str(e)) logging.error(traceback.format_exc()) # set to False if error occurs diff --git a/src/foraging_gui/ForagingGUI_Ephys.ui b/src/foraging_gui/ForagingGUI_Ephys.ui index 10090d5f6..a6f608c61 100644 --- a/src/foraging_gui/ForagingGUI_Ephys.ui +++ b/src/foraging_gui/ForagingGUI_Ephys.ui @@ -3188,7 +3188,7 @@ 1230 840 291 - 101 + 110 @@ -3200,202 +3200,15 @@ Warning - - - - 10 - 79 - 331 - 21 - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - 10 - 20 - 331 - 21 - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - 10 - 40 - 331 - 21 - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - 10 - 70 - 331 - 21 - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - 10 - 30 - 361 - 31 - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - 10 - 30 - 331 - 21 - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - 10 - 40 - 361 - 31 - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - + + + + + true + + + + @@ -3764,34 +3577,6 @@ the ghost in the shell - - - - 1240 - 860 - 361 - 71 - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - @@ -4007,34 +3792,6 @@ - - - - 1240 - 860 - 361 - 31 - - - - - 0 - 0 - - - - - - - Qt::AutoText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - @@ -4097,24 +3854,6 @@ Streamlit! - - - - 1240 - 870 - 261 - 16 - - - - - 7 - - - - - - @@ -4200,37 +3939,6 @@ - - - - 1240 - 870 - 261 - 16 - - - - - 7 - - - - - - - - - - 1550 - 850 - 301 - 91 - - - - - - true @@ -4718,22 +4426,17 @@ groupBox_2 label_74 Experimenter - WarningLabelCamera label_61 label_47 baselinetime StartExcitation StartBleaching - TeensyWarning AutoTrain label_auto_train_stage pushButton_streamlit - WarningLabel_uncoupled_task StartEphysRecording label_65 OpenEphysRecordingType - ManualWaterWarning - MetadataWarning DiskSpaceProgreeBar diskspace motor_stage_widget From 463f4ece77c4648b91b1ebcfca907df7904deab3 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 5 Nov 2024 15:02:54 -0800 Subject: [PATCH 21/42] add stage widget back in --- src/foraging_gui/Foraging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 1d7ccb795..bd57df5fa 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -30,7 +30,7 @@ from pyOSC3.OSC3 import OSCStreamingClient import webbrowser -#from StageWidget.main import get_stage_widget +from StageWidget.main import get_stage_widget import foraging_gui import foraging_gui.rigcontrol as rigcontrol From 0d3123355d157f90e38ef3eb24fa969d5e997878 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 5 Nov 2024 15:26:50 -0800 Subject: [PATCH 22/42] add in date --- src/foraging_gui/Foraging.py | 11 +++++++---- src/foraging_gui/warning_widget.py | 26 +++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index bd57df5fa..3ac57b65d 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -67,7 +67,10 @@ class Window(QMainWindow): def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): logging.info('Creating Window') + + # create warning widget self.warning_log_tag = 'warning_widget' # TODO: How to set this or does it matter? + self.warning_widget = WarningWidget(log_tag=self.warning_log_tag) super().__init__(parent) @@ -107,6 +110,10 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): # Load User interface self._LoadUI() + # add warning_widget to layout and set color + self.scrollArea_6.setWidget(self.warning_widget) + self.warning_widget.setTextColor(self.default_warning_color) + # set window title self.setWindowTitle(self.rig_name) logging.info('Setting Window title: {}'.format(self.rig_name)) @@ -200,10 +207,6 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): # generate an upload manifest when a session has been produced self.upload_manifest_slot = self.sessionGenerated.connect(self._generate_upload_manifest) - # create and add warning_widget - self.warning_widget = WarningWidget(log_tag=self.warning_log_tag) - self.scrollArea_6.setWidget(self.warning_widget) - # show disk space self._show_disk_space() if not self.start_bonsai_ide: diff --git a/src/foraging_gui/warning_widget.py b/src/foraging_gui/warning_widget.py index 474f63864..5ac87f657 100644 --- a/src/foraging_gui/warning_widget.py +++ b/src/foraging_gui/warning_widget.py @@ -10,16 +10,23 @@ class WarningWidget(QWidget): """Widget that uses a logging QueueHandler to display log errors and warning""" - def __init__(self, log_tag: str = 'warning_widget', log_level: str = 'INFO', *args, **kwargs): + def __init__(self, log_tag: str = 'warning_widget', + log_level: str = 'INFO', + text_color: str = 'black', + *args, **kwargs): """ :param log_tag: log_tag to pass into filter :param log_level: level for QueueHandler + :param text_color: color of warning messages """ super().__init__(*args, **kwargs) self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") + # set color for labels + self.text_color = text_color + # create vertical layout self.setLayout(QVBoxLayout()) @@ -28,13 +35,14 @@ def __init__(self, log_tag: str = 'warning_widget', log_level: str = 'INFO', *ar queue_handler = QueueHandler(self.queue) queue_handler.setLevel(log_level) queue_handler.addFilter(WarningFilter(log_tag)) # add filter + queue_handler.setFormatter(logging.Formatter(fmt='%(asctime)s: %(message)s', datefmt='%I:%M:%S %p')) self.logger.root.addHandler(queue_handler) # create QTimer to periodically check queue self.check_timer = QTimer(timeout=self.check_warning_queue, interval=1000) self.check_timer.start() - def check_warning_queue(self): + def check_warning_queue(self) -> None: """ Check queue and update layout with the latest warnings """ @@ -42,6 +50,7 @@ def check_warning_queue(self): while not self.queue.empty(): log = self.queue.get() label = QLabel(str(log.getMessage())) + label.setStyleSheet(f'"color: {self.text_color};"') self.layout().insertWidget(0, label) # prune layout if too many warnings @@ -49,6 +58,13 @@ def check_warning_queue(self): widget = self.layout().itemAt(29).widget() self.layout().removeWidget(widget) + def setTextColor(self, color: str) -> None: + """ + Set color of text + :param color: color to set text to + """ + + self.text_color = color class WarningFilter(logging.Filter): """ Log filter which logs messages with tags that contain keyword""" @@ -72,6 +88,9 @@ def filter(self, record): logger = logging.getLogger() stream_handler = logging.StreamHandler() stream_handler.setLevel(logger.root.level) + log_format = '%(asctime)s:%(levelname)s:%(module)s:%(filename)s:%(funcName)s:line %(lineno)d:%(message)s' + log_datefmt = '%I:%M:%S %p' + stream_handler.setFormatter(logging.Formatter(fmt=log_format, datefmt=log_datefmt)) logger.root.addHandler(stream_handler) warn_widget = WarningWidget() @@ -80,7 +99,8 @@ def filter(self, record): warnings = ['this is a warning', 'this is also a warning', 'this is a warning too', 'Warn warn warn', 'are you warned yet?'] - warning_timer = QTimer(timeout=lambda: logger.warning(warnings[randint(0, 4)]), interval=1000) + warning_timer = QTimer(timeout=lambda: logger.warning(warnings[randint(0, 4)], + extra={'tags': 'warning_widget'}), interval=1000) warning_timer.start() sys.exit(app.exec_()) From b7e31bb71fad4018726873d7bb018369ec70b8d0 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 5 Nov 2024 15:44:31 -0800 Subject: [PATCH 23/42] skipping empty messages --- src/foraging_gui/Foraging.py | 8 ++++---- src/foraging_gui/warning_widget.py | 21 +++++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 3ac57b65d..9febabe0b 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -70,7 +70,6 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): # create warning widget self.warning_log_tag = 'warning_widget' # TODO: How to set this or does it matter? - self.warning_widget = WarningWidget(log_tag=self.warning_log_tag) super().__init__(parent) @@ -111,8 +110,9 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): self._LoadUI() # add warning_widget to layout and set color + self.warning_widget = WarningWidget(log_tag=self.warning_log_tag, + text_color=self.default_warning_color) self.scrollArea_6.setWidget(self.warning_widget) - self.warning_widget.setTextColor(self.default_warning_color) # set window title self.setWindowTitle(self.rig_name) @@ -274,13 +274,13 @@ def _LoadUI(self): logging.info('Using ForagingGUI.ui interface') self.label_date.setText(str(date.today())) self.default_warning_color="color: purple;" - self.default_text_color='color: purple;' + self.default_text_color='purple' self.default_text_background_color='background-color: purple;' elif self.default_ui=='ForagingGUI_Ephys.ui': logging.info('Using ForagingGUI_Ephys.ui interface') self.Visualization.setTitle(str(date.today())) self.default_warning_color="color: red;" - self.default_text_color='color: red;' + self.default_text_color='red' self.default_text_background_color='background-color: red;' else: logging.info('Using ForagingGUI.ui interface') diff --git a/src/foraging_gui/warning_widget.py b/src/foraging_gui/warning_widget.py index 5ac87f657..e09097091 100644 --- a/src/foraging_gui/warning_widget.py +++ b/src/foraging_gui/warning_widget.py @@ -48,15 +48,16 @@ def check_warning_queue(self) -> None: """ while not self.queue.empty(): - log = self.queue.get() - label = QLabel(str(log.getMessage())) - label.setStyleSheet(f'"color: {self.text_color};"') - self.layout().insertWidget(0, label) + log = self.queue.get().getMessage() + if log[12:].strip(): # skip empty messages + label = QLabel(log) + label.setStyleSheet(f'"color: {self.text_color};"') + self.layout().insertWidget(0, label) - # prune layout if too many warnings - if self.layout().count() == 30: - widget = self.layout().itemAt(29).widget() - self.layout().removeWidget(widget) + # prune layout if too many warnings + if self.layout().count() == 30: + widget = self.layout().itemAt(29).widget() + self.layout().removeWidget(widget) def setTextColor(self, color: str) -> None: """ @@ -97,9 +98,9 @@ def filter(self, record): warn_widget.show() warnings = ['this is a warning', 'this is also a warning', 'this is a warning too', 'Warn warn warn', - 'are you warned yet?'] + 'are you warned yet?', ''] - warning_timer = QTimer(timeout=lambda: logger.warning(warnings[randint(0, 4)], + warning_timer = QTimer(timeout=lambda: logger.warning(warnings[randint(0, 5)], extra={'tags': 'warning_widget'}), interval=1000) warning_timer.start() From 7dea95b33d34b12dcdb231c3f79aba80d897aba5 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 5 Nov 2024 15:54:02 -0800 Subject: [PATCH 24/42] bug fixes --- src/foraging_gui/Foraging.py | 8 ++++---- src/foraging_gui/warning_widget.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 9febabe0b..59131a11b 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -273,14 +273,14 @@ def _LoadUI(self): if self.default_ui=='ForagingGUI.ui': logging.info('Using ForagingGUI.ui interface') self.label_date.setText(str(date.today())) - self.default_warning_color="color: purple;" - self.default_text_color='purple' + self.default_warning_color="purple" + self.default_text_color="color: purple;" self.default_text_background_color='background-color: purple;' elif self.default_ui=='ForagingGUI_Ephys.ui': logging.info('Using ForagingGUI_Ephys.ui interface') self.Visualization.setTitle(str(date.today())) - self.default_warning_color="color: red;" - self.default_text_color='red' + self.default_warning_color="red" + self.default_text_color="color: red;" self.default_text_background_color='background-color: red;' else: logging.info('Using ForagingGUI.ui interface') diff --git a/src/foraging_gui/warning_widget.py b/src/foraging_gui/warning_widget.py index e09097091..b7c39005e 100644 --- a/src/foraging_gui/warning_widget.py +++ b/src/foraging_gui/warning_widget.py @@ -51,7 +51,7 @@ def check_warning_queue(self) -> None: log = self.queue.get().getMessage() if log[12:].strip(): # skip empty messages label = QLabel(log) - label.setStyleSheet(f'"color: {self.text_color};"') + label.setStyleSheet(f'color: {self.text_color};') self.layout().insertWidget(0, label) # prune layout if too many warnings From e3125147fe26ff1d59530c91faf8c47b1ddab402 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 6 Nov 2024 08:22:01 -0800 Subject: [PATCH 25/42] sizing fix --- src/foraging_gui/warning_widget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/warning_widget.py b/src/foraging_gui/warning_widget.py index b7c39005e..c74aaa690 100644 --- a/src/foraging_gui/warning_widget.py +++ b/src/foraging_gui/warning_widget.py @@ -3,7 +3,7 @@ from logging.handlers import QueueHandler from random import randint from PyQt5.QtCore import QTimer -from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QScrollArea +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QSizePolicy import sys @@ -51,6 +51,7 @@ def check_warning_queue(self) -> None: log = self.queue.get().getMessage() if log[12:].strip(): # skip empty messages label = QLabel(log) + label.setWordWrap(True) label.setStyleSheet(f'color: {self.text_color};') self.layout().insertWidget(0, label) From 6f546b529842df0897a99c32017e73e4ae444a1a Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 6 Nov 2024 10:53:54 -0800 Subject: [PATCH 26/42] adding warnings per review --- src/foraging_gui/Foraging.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 59131a11b..590d235d7 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -3255,7 +3255,8 @@ def _StartFIP(self): self.StartFIP.setChecked(False) if self.Teensy_COM == '': - logging.warning('No Teensy COM configured for this box, cannot start FIP workflow') + logging.warning('No Teensy COM configured for this box, cannot start FIP workflow', + extra={'tags': [self.warning_log_tag]}) msg = 'No Teensy COM configured for this box, cannot start FIP workflow' reply = QMessageBox.information(self, 'Box {}, StartFIP'.format(self.box_letter), msg, QMessageBox.Ok ) @@ -3302,7 +3303,8 @@ def _StartFIP(self): def _StartExcitation(self): if self.Teensy_COM == '': - logging.warning('No Teensy COM configured for this box, cannot start excitation') + logging.warning('No Teensy COM configured for this box, cannot start excitation', + extra={'tags': [self.warning_log_tag]}) msg = 'No Teensy COM configured for this box, cannot start excitation' reply = QMessageBox.information(self, 'Box {}, StartExcitation'.format(self.box_letter), msg, QMessageBox.Ok ) @@ -3354,7 +3356,8 @@ def _StartExcitation(self): def _StartBleaching(self): if self.Teensy_COM == '': - logging.warning('No Teensy COM configured for this box, cannot start bleaching') + logging.warning('No Teensy COM configured for this box, cannot start bleaching', + extra={'tags': [self.warning_log_tag]}) msg = 'No Teensy COM configured for this box, cannot start bleaching' reply = QMessageBox.information(self, 'Box {}, StartBleaching'.format(self.box_letter), msg, QMessageBox.Ok ) @@ -4151,7 +4154,7 @@ def _StartTrialLoop(self,GeneratedTrials,worker1,worker_save): except Exception as e: if 'ConnectionAbortedError' in str(e): logging.info('lost bonsai connection: InitiateATrial') - logging.warning('Lost bonsai connection') + logging.warning('Lost bonsai connection', extra={'tags': [self.warning_log_tag]}) self.Start.setChecked(False) self.Start.setStyleSheet("background-color : none") self.InitializeBonsaiSuccessfully=0 From b81fe83063e7b0a780a1b37e8051ef4701937da7 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 6 Nov 2024 11:39:52 -0800 Subject: [PATCH 27/42] add photometery lable countdown to warning_widget --- src/foraging_gui/Foraging.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 590d235d7..140b0153c 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -24,13 +24,13 @@ from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar from scipy.io import savemat, loadmat from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox, QSizePolicy -from PyQt5.QtWidgets import QFileDialog,QVBoxLayout, QGridLayout +from PyQt5.QtWidgets import QFileDialog,QVBoxLayout, QGridLayout, QLabel from PyQt5 import QtWidgets,QtGui,QtCore, uic -from PyQt5.QtCore import QThreadPool,Qt,QThread, QTimer +from PyQt5.QtCore import QThreadPool,Qt,QThread from pyOSC3.OSC3 import OSCStreamingClient import webbrowser -from StageWidget.main import get_stage_widget +#from StageWidget.main import get_stage_widget import foraging_gui import foraging_gui.rigcontrol as rigcontrol @@ -1197,8 +1197,7 @@ def _GetSettings(self): for key in defaults: if key not in self.Settings: self.Settings[key] = defaults[key] - logging.warning('Missing setting ({}), using default: {}'.format(key,self.Settings[key]), - extra={'tags': [self.warning_log_tag]}) + logging.warning('Missing setting ({}), using default: {}'.format(key,self.Settings[key])) if key in ['default_saveFolder','current_box']: logging.error('Missing setting ({}), is required'.format(key)) raise Exception('Missing setting ({}), is required'.format(key)) @@ -3651,8 +3650,7 @@ def _update_photometery_timer(self,time): if len(str(seconds)) == 1: seconds = '0{}'.format(seconds) if not self.ignore_timer: - logging.info('Running photometry baseline: {}:{}'.format(minutes,seconds), - extra={'tags': [self.warning_log_tag]}) + self.photometry_timer_label.setText('Running photometry baseline: {}:{}'.format(minutes,seconds)) def _set_metadata_enabled(self, enable: bool): '''Enable or disable metadata fields''' @@ -4061,6 +4059,10 @@ def _Start(self): self.PhotometryRun=1 self.ignore_timer=False + # create label to display time remaining on photometry label and add to warning widget + self.photometry_timer_label = QLabel() + self.warning_widget.layout().insertWidget(0, self.photometry_timer_label) + # If we already created a workertimer and thread we can reuse them if not hasattr(self, 'workertimer'): self.workertimer = TimerWorker() From 99e7583daeade61435a09522c9327df59237ba27 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 6 Nov 2024 12:23:10 -0800 Subject: [PATCH 28/42] adding stage back in --- src/foraging_gui/Foraging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 140b0153c..04f960a50 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -30,7 +30,7 @@ from pyOSC3.OSC3 import OSCStreamingClient import webbrowser -#from StageWidget.main import get_stage_widget +from StageWidget.main import get_stage_widget import foraging_gui import foraging_gui.rigcontrol as rigcontrol From d1009e97877be7454d09cd9d7a7558baf370ff09 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 6 Nov 2024 12:29:17 -0800 Subject: [PATCH 29/42] minute test --- src/foraging_gui/MyFunctions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index 6600681d3..48cdb8828 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -109,7 +109,7 @@ def __init__(self,win): self._GetTrainingParameters(self.win) # create timer to calculate lick intervals every 10 minutes - self.lick_interval_time = QtCore.QTimer(timeout=self.calculate_inter_lick_intervals, interval=600000) + self.lick_interval_time = QtCore.QTimer(timeout=self.calculate_inter_lick_intervals, interval=60000) def _GenerateATrial(self,Channel4): self.finish_select_par=0 From b8f6935cc41f317a9adb81f7f4b870fbee0428c6 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 6 Nov 2024 12:40:18 -0800 Subject: [PATCH 30/42] creating different dummy arrays --- src/foraging_gui/MyFunctions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index 48cdb8828..fe4abbf50 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -933,10 +933,12 @@ def calculate_inter_lick_intervals(self): self.win.same_side_lick_interval.setText('') # calculate cross side interval and frac - dummy_array = np.ones(right.shape) # array used to assign lick direction + right_dummy = np.ones(right.shape) # array used to assign lick direction + left_dummy = np.negative(np.ones(left.shape)) + # 2d arrays pairing each time with a 1 (right) or -1 (left) - stacked_right = np.column_stack((dummy_array, right)) - stacked_left = np.column_stack((np.negative(dummy_array), left)) + stacked_right = np.column_stack((right_dummy, right)) + stacked_left = np.column_stack((left_dummy, left)) # concatenate stacked_right and stacked_left then sort based on time element # e.g. [[-1, 10], [1, 15], [-1, 20], [1, 25]...]. Ones added to assign lick side to times merged_sorted = np.array(sorted(np.concatenate((stacked_right, stacked_left)), From 091f79b8a13bd98047422c9768acb4d390aa0bbf Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 6 Nov 2024 12:50:05 -0800 Subject: [PATCH 31/42] setting timer back to 10 minutes --- src/foraging_gui/MyFunctions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index fe4abbf50..432ba49a9 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -109,7 +109,7 @@ def __init__(self,win): self._GetTrainingParameters(self.win) # create timer to calculate lick intervals every 10 minutes - self.lick_interval_time = QtCore.QTimer(timeout=self.calculate_inter_lick_intervals, interval=60000) + self.lick_interval_time = QtCore.QTimer(timeout=self.calculate_inter_lick_intervals, interval=600000) def _GenerateATrial(self,Channel4): self.finish_select_par=0 @@ -935,7 +935,7 @@ def calculate_inter_lick_intervals(self): # calculate cross side interval and frac right_dummy = np.ones(right.shape) # array used to assign lick direction left_dummy = np.negative(np.ones(left.shape)) - + # 2d arrays pairing each time with a 1 (right) or -1 (left) stacked_right = np.column_stack((right_dummy, right)) stacked_left = np.column_stack((left_dummy, left)) From 5eaa42255e1c6649d0285bf84ad1b455bed8d5d6 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 6 Nov 2024 14:19:03 -0800 Subject: [PATCH 32/42] adding in error messages to create issues if over --- src/foraging_gui/MyFunctions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/foraging_gui/MyFunctions.py b/src/foraging_gui/MyFunctions.py index 432ba49a9..efbbfbf87 100644 --- a/src/foraging_gui/MyFunctions.py +++ b/src/foraging_gui/MyFunctions.py @@ -929,6 +929,8 @@ def calculate_inter_lick_intervals(self): if same_side_frac >= threshold: self.win.same_side_lick_interval.setText(f'Percentage of same side lick intervals under 100 ms is ' f'over 10%: {same_side_frac * 100:.2f}%.') + logging.error(f'Percentage of same side lick intervals under 100 ms in Box {self.win.box_letter} ' + f'mouse {self.win.ID.text()} exceeded 10%') else: self.win.same_side_lick_interval.setText('') @@ -954,6 +956,8 @@ def calculate_inter_lick_intervals(self): if cross_side_frac >= threshold: self.win.cross_side_lick_interval.setText(f'Percentage of cross side lick intervals under 100 ms is ' f'over 10%: {cross_side_frac * 100:.2f}%.') + logging.error(f'Percentage of cross side lick intervals under 100 ms in Box {self.win.box_letter} ' + f'mouse {self.win.ID.text()} exceeded 10%') else: self.win.cross_side_lick_interval.setText('') From 5733da8479562da06fa83f5610b0bc7df3584045 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 7 Nov 2024 09:02:27 -0800 Subject: [PATCH 33/42] setting color for photometry countdown --- src/foraging_gui/Foraging.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 04f960a50..c70977ccd 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -4061,6 +4061,7 @@ def _Start(self): # create label to display time remaining on photometry label and add to warning widget self.photometry_timer_label = QLabel() + self.photometry_timer_label.setStyleSheet(f'color: {self.default_warning_color};') self.warning_widget.layout().insertWidget(0, self.photometry_timer_label) # If we already created a workertimer and thread we can reuse them From 04d78d5d63df7f1b08bf82a7169ffbcd3e59f563 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 7 Nov 2024 09:49:30 -0800 Subject: [PATCH 34/42] added rig warning --- src/foraging_gui/Foraging.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index c70977ccd..a825999a7 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -30,7 +30,7 @@ from pyOSC3.OSC3 import OSCStreamingClient import webbrowser -from StageWidget.main import get_stage_widget +#from StageWidget.main import get_stage_widget import foraging_gui import foraging_gui.rigcontrol as rigcontrol @@ -1514,6 +1514,7 @@ def _load_most_recent_rig_json(self,error_if_none=True): rig_json_path = '' if error_if_none: logging.error('Did not find any existing rig.json files') + logging.warning('No rig metadata found!', extra={'tags': self.warning_log_tag}) else: logging.info('Did not find any existing rig.json files') #FIXME: is this really the right message else: From 7df889d8e31a8568586cc9b24ca6ab74fc335c81 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 7 Nov 2024 11:38:00 -0800 Subject: [PATCH 35/42] adding stage widget back in --- src/foraging_gui/Foraging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index a825999a7..d9349c3bb 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -30,7 +30,7 @@ from pyOSC3.OSC3 import OSCStreamingClient import webbrowser -#from StageWidget.main import get_stage_widget +from StageWidget.main import get_stage_widget import foraging_gui import foraging_gui.rigcontrol as rigcontrol From b2da69130910a588f28bbe26707a07ba5f4002f1 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 7 Nov 2024 14:30:49 -0800 Subject: [PATCH 36/42] changing color of text based on level --- src/foraging_gui/Foraging.py | 2 +- src/foraging_gui/warning_widget.py | 83 +++++++++++++++++++++++++----- 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index d9349c3bb..47052f162 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -111,7 +111,7 @@ def __init__(self, parent=None,box_number=1,start_bonsai_ide=True): # add warning_widget to layout and set color self.warning_widget = WarningWidget(log_tag=self.warning_log_tag, - text_color=self.default_warning_color) + warning_color=self.default_warning_color) self.scrollArea_6.setWidget(self.warning_widget) # set window title diff --git a/src/foraging_gui/warning_widget.py b/src/foraging_gui/warning_widget.py index c74aaa690..d6eb08f13 100644 --- a/src/foraging_gui/warning_widget.py +++ b/src/foraging_gui/warning_widget.py @@ -11,13 +11,17 @@ class WarningWidget(QWidget): """Widget that uses a logging QueueHandler to display log errors and warning""" def __init__(self, log_tag: str = 'warning_widget', - log_level: str = 'INFO', - text_color: str = 'black', + log_level: str = logging.INFO, + warning_color: str = 'purple', + info_color: str = 'green', + error_color: str = 'orange', *args, **kwargs): """ :param log_tag: log_tag to pass into filter :param log_level: level for QueueHandler - :param text_color: color of warning messages + :param warning_color: color of warning messages + :param info_color: color of info messages + :param error_color: color of error messages """ super().__init__(*args, **kwargs) @@ -25,7 +29,9 @@ def __init__(self, log_tag: str = 'warning_widget', self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}") # set color for labels - self.text_color = text_color + self._warning_color = warning_color + self._info_color = info_color + self._error_color = error_color # create vertical layout self.setLayout(QVBoxLayout()) @@ -48,11 +54,18 @@ def check_warning_queue(self) -> None: """ while not self.queue.empty(): - log = self.queue.get().getMessage() - if log[12:].strip(): # skip empty messages - label = QLabel(log) + log = self.queue.get() + + if log.getMessage()[12:].strip(): # skip empty messages + label = QLabel(log.getMessage()) label.setWordWrap(True) - label.setStyleSheet(f'color: {self.text_color};') + print(log.levelno, logging.WARNING) + if log.levelno == logging.WARNING: + label.setStyleSheet(f'color: {self._warning_color};') + elif log.levelno == logging.ERROR: + label.setStyleSheet(f'color: {self._error_color};') + elif log.levelno == logging.INFO: + label.setStyleSheet(f'color: {self._info_color};') self.layout().insertWidget(0, label) # prune layout if too many warnings @@ -60,13 +73,49 @@ def check_warning_queue(self) -> None: widget = self.layout().itemAt(29).widget() self.layout().removeWidget(widget) - def setTextColor(self, color: str) -> None: + def setWarningColor(self, color: str) -> None: """ - Set color of text + Set color of warning labels :param color: color to set text to """ - self.text_color = color + self._warning_color = color + def warningColor(self) -> str: + """ + return color of warning labels + """ + + return self._warning_color + + def setInfoColor(self, color: str) -> None: + """ + Set color of info labels + :param color: color to set text to + """ + + self._info_color = color + + def infoColor(self) -> str: + """ + return color of info labels + """ + + return self._info_color + + def setErrorColor(self, color: str) -> None: + """ + Set color of error labels + :param color: color to set text to + """ + + self._error_color = color + + def errorColor(self) -> str: + """ + return color of error labels + """ + + return self._error_color class WarningFilter(logging.Filter): """ Log filter which logs messages with tags that contain keyword""" @@ -89,7 +138,7 @@ def filter(self, record): logger = logging.getLogger() stream_handler = logging.StreamHandler() - stream_handler.setLevel(logger.root.level) + stream_handler.setLevel('INFO') log_format = '%(asctime)s:%(levelname)s:%(module)s:%(filename)s:%(funcName)s:line %(lineno)d:%(message)s' log_datefmt = '%I:%M:%S %p' stream_handler.setFormatter(logging.Formatter(fmt=log_format, datefmt=log_datefmt)) @@ -100,9 +149,19 @@ def filter(self, record): warnings = ['this is a warning', 'this is also a warning', 'this is a warning too', 'Warn warn warn', 'are you warned yet?', ''] + infos = ['info dump', 'inspo info', 'too much information'] + errors = ['error 7', 'error 8', 'error 9'] warning_timer = QTimer(timeout=lambda: logger.warning(warnings[randint(0, 5)], extra={'tags': 'warning_widget'}), interval=1000) + + info_timer = QTimer(timeout=lambda: logger.info(infos[randint(0, 2)], + extra={'tags': 'warning_widget'}), interval=1000) + error_timer = QTimer(timeout=lambda: logger.error(errors[randint(0, 2)], + extra={'tags': 'warning_widget'}), interval=1000) + warning_timer.start() + info_timer.start() + error_timer.start() sys.exit(app.exec_()) From cf227ae66b6bd0c6b044c05e0655cdba8b1eb227 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 7 Nov 2024 15:03:25 -0800 Subject: [PATCH 37/42] color coding dropped frames --- src/foraging_gui/Foraging.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/foraging_gui/Foraging.py b/src/foraging_gui/Foraging.py index 47052f162..36ad011ba 100644 --- a/src/foraging_gui/Foraging.py +++ b/src/foraging_gui/Foraging.py @@ -667,7 +667,10 @@ def _check_drop_frames(self,save_tag=1): else: self.drop_frames_warning_text+=f"Correct: {avi_file} has {num_frames} frames and {self.trigger_length} triggers\n" self.frame_num[camera_name] = num_frames - logging.warning(self.drop_frames_warning_text, extra={'tags': [self.warning_log_tag]}) + if self.drop_frames_tag: + logging.warning(self.drop_frames_warning_text, extra={'tags': [self.warning_log_tag]}) + else: + logging.info(self.drop_frames_warning_text, extra={'tags': [self.warning_log_tag]}) # only check drop frames once each session self.to_check_drop_frames=0 From 737d24e5d6fb646a30a0ba5ebbd9113adc371c56 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Fri, 8 Nov 2024 09:09:11 -0800 Subject: [PATCH 38/42] adding camera textbox --- src/foraging_gui/Dialogs.py | 22 ++++++++++++++++++++-- src/foraging_gui/warning_widget.py | 1 - 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/foraging_gui/Dialogs.py b/src/foraging_gui/Dialogs.py index bd876d361..b1bd3e2c4 100644 --- a/src/foraging_gui/Dialogs.py +++ b/src/foraging_gui/Dialogs.py @@ -805,7 +805,6 @@ def _CalibrateRightOne(self,repeat=False): "Weight after (g): ", final_tube_weight, 0, 1000, 4) - print(final_tube_weight, ok) if not ok: self.Warning.setText('Please repeat measurement') self.WeightBeforeRight.setText('') @@ -1278,7 +1277,13 @@ def __init__(self, MainWindow, parent=None): self._connectSignalsSlots() self.camera_start_time='' self.camera_stop_time='' - + + self.info_label = QLabel(parent=self) + self.info_label.setStyleSheet(f'color: {self.MainWindow.default_warning_color};') + self.info_label.move(50, 350) + self.info_label.setFixedSize(171, 51) + self.info_label.setAlignment(Qt.AlignCenter) + def _connectSignalsSlots(self): self.StartRecording.toggled.connect(self._StartCamera) self.StartPreview.toggled.connect(self._start_preview) @@ -1287,14 +1292,20 @@ def _connectSignalsSlots(self): def _OpenSaveFolder(self): '''Open the log/save folder of the camera''' + + text = self.info_label.text() if hasattr(self.MainWindow,'Ot_log_folder'): try: subprocess.Popen(['explorer', os.path.join(os.path.dirname(os.path.dirname(self.MainWindow.Ot_log_folder)),'behavior-videos')]) except Exception as e: logging.error(str(e)) logging.warning('No logging folder found!', extra={'tags': self.MainWindow.warning_log_tag}) + if 'No logging folder found!' not in text: + self.info_label.setText(text + '\n No logging folder found!') else: logging.warning('No logging folder found!', extra={'tags': self.MainWindow.warning_log_tag}) + if 'No logging folder found!' not in text: + self.info_label.setText(text + '\n No logging folder found!') def _start_preview(self): '''Start the camera preview''' @@ -1313,6 +1324,8 @@ def _start_preview(self): self.StartPreview.setStyleSheet("background-color : green;") logging.info('Camera is on', extra={'tags': self.MainWindow.warning_log_tag}) + self.info_label.setText('Camera is on') + else: # enable the start recording button self.StartRecording.setEnabled(True) @@ -1323,6 +1336,7 @@ def _start_preview(self): self.StartPreview.setStyleSheet("background-color : none;") logging.info('Camera is off', extra={'tags': self.MainWindow.warning_log_tag}) + self.info_label.setText('Camera is off') def _AutoControl(self): '''Trigger the camera during the start of a new behavior session''' @@ -1338,6 +1352,7 @@ def _StartCamera(self): if self.StartRecording.isChecked(): self.StartRecording.setStyleSheet("background-color : green;") logging.info('Camera is turning on', extra={'tags': self.MainWindow.warning_log_tag}) + self.info_label.setText('Camera is turning on') QApplication.processEvents() # untoggle the preview button if self.StartPreview.isChecked(): @@ -1364,14 +1379,17 @@ def _StartCamera(self): time.sleep(5) self.camera_start_time = str(datetime.now()) logging.info('Camera is on!', extra={'tags': [self.MainWindow.warning_log_tag]}) + self.info_label.setText('Camera is on!') else: self.StartRecording.setStyleSheet("background-color : none") logging.info('Camera is turning off', extra={'tags': self.MainWindow.warning_log_tag}) + self.info_label.setText('Camera is turning off') QApplication.processEvents() self.MainWindow.Channel.CameraControl(int(2)) self.camera_stop_time = str(datetime.now()) time.sleep(5) logging.info('Camera is off!', extra={'tags': [self.MainWindow.warning_log_tag]}) + self.info_label.setText('Camera is off!') def is_file_in_use(file_path): '''check if the file is open''' diff --git a/src/foraging_gui/warning_widget.py b/src/foraging_gui/warning_widget.py index d6eb08f13..fca8d556d 100644 --- a/src/foraging_gui/warning_widget.py +++ b/src/foraging_gui/warning_widget.py @@ -59,7 +59,6 @@ def check_warning_queue(self) -> None: if log.getMessage()[12:].strip(): # skip empty messages label = QLabel(log.getMessage()) label.setWordWrap(True) - print(log.levelno, logging.WARNING) if log.levelno == logging.WARNING: label.setStyleSheet(f'color: {self._warning_color};') elif log.levelno == logging.ERROR: From 1e88353666fd4276fa14229ae72664e6b9ba28a3 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Fri, 8 Nov 2024 09:32:15 -0800 Subject: [PATCH 39/42] fixing example --- src/foraging_gui/warning_widget.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/foraging_gui/warning_widget.py b/src/foraging_gui/warning_widget.py index fca8d556d..45e402785 100644 --- a/src/foraging_gui/warning_widget.py +++ b/src/foraging_gui/warning_widget.py @@ -3,7 +3,7 @@ from logging.handlers import QueueHandler from random import randint from PyQt5.QtCore import QTimer -from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QSizePolicy +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel import sys @@ -135,6 +135,8 @@ def filter(self, record): if __name__ == '__main__': app = QApplication(sys.argv) + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger() stream_handler = logging.StreamHandler() stream_handler.setLevel('INFO') @@ -144,7 +146,6 @@ def filter(self, record): logger.root.addHandler(stream_handler) warn_widget = WarningWidget() - warn_widget.show() warnings = ['this is a warning', 'this is also a warning', 'this is a warning too', 'Warn warn warn', 'are you warned yet?', ''] From c10c591843cb416a91089d837e8185a0f7ef3638 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Fri, 8 Nov 2024 09:32:54 -0800 Subject: [PATCH 40/42] fixing example --- src/foraging_gui/warning_widget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/foraging_gui/warning_widget.py b/src/foraging_gui/warning_widget.py index 45e402785..c3fc1f734 100644 --- a/src/foraging_gui/warning_widget.py +++ b/src/foraging_gui/warning_widget.py @@ -146,6 +146,7 @@ def filter(self, record): logger.root.addHandler(stream_handler) warn_widget = WarningWidget() + warn_widget.show() warnings = ['this is a warning', 'this is also a warning', 'this is a warning too', 'Warn warn warn', 'are you warned yet?', ''] From 18ff76d0ae50d8161cf884f1d5316a8c1dc0f3fe Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Fri, 8 Nov 2024 09:38:21 -0800 Subject: [PATCH 41/42] adding scroll to example --- src/foraging_gui/warning_widget.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/foraging_gui/warning_widget.py b/src/foraging_gui/warning_widget.py index c3fc1f734..025276621 100644 --- a/src/foraging_gui/warning_widget.py +++ b/src/foraging_gui/warning_widget.py @@ -3,9 +3,9 @@ from logging.handlers import QueueHandler from random import randint from PyQt5.QtCore import QTimer -from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QScrollArea import sys - +from time import sleep class WarningWidget(QWidget): """Widget that uses a logging QueueHandler to display log errors and warning""" @@ -145,8 +145,12 @@ def filter(self, record): stream_handler.setFormatter(logging.Formatter(fmt=log_format, datefmt=log_datefmt)) logger.root.addHandler(stream_handler) - warn_widget = WarningWidget() - warn_widget.show() + scroll = QScrollArea() + warn_widget = WarningWidget(parent=scroll) + scroll.setWidget(warn_widget) + scroll.setWidgetResizable(True) + scroll.show() + warnings = ['this is a warning', 'this is also a warning', 'this is a warning too', 'Warn warn warn', 'are you warned yet?', ''] @@ -157,12 +161,15 @@ def filter(self, record): extra={'tags': 'warning_widget'}), interval=1000) info_timer = QTimer(timeout=lambda: logger.info(infos[randint(0, 2)], - extra={'tags': 'warning_widget'}), interval=1000) + extra={'tags': 'warning_widget'}), interval=1500) error_timer = QTimer(timeout=lambda: logger.error(errors[randint(0, 2)], - extra={'tags': 'warning_widget'}), interval=1000) + extra={'tags': 'warning_widget'}), interval=1750) warning_timer.start() + sleep(.5) info_timer.start() + sleep(1) error_timer.start() + sleep(1) sys.exit(app.exec_()) From 4f2e01a975c50fe38aec8f52ffb58a17b6875826 Mon Sep 17 00:00:00 2001 From: "jessy.liao" Date: Fri, 8 Nov 2024 13:33:39 -0800 Subject: [PATCH 42/42] Specify dev version of stagewidget for testing --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a4fcab84d..53fd5801f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "aind-data-schema==1.1.0", "aind-data-schema-models==0.5.6", "pydantic==2.9.2", - "stagewidget", + "stagewidget==1.0.4.dev0", "pyyaml", "pyqtgraph" diff --git a/requirements.txt b/requirements.txt index b79e46420..05c572008 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,4 +21,4 @@ aind-data-schema-models==0.5.6 pydantic==2.9.2 pyyaml pyqtgraph -stagewidget +stagewidget==1.0.4.dev0