diff --git a/built_in_tasks/bmimultitasks.py b/built_in_tasks/bmimultitasks.py index 5051e016..434a49d8 100644 --- a/built_in_tasks/bmimultitasks.py +++ b/built_in_tasks/bmimultitasks.py @@ -407,4 +407,161 @@ class BMIControlMultiDirectionConstraint(BMIControlMultiMixin, ScreenReachAngle) ''' Adds an additional constraint that the direction of travel must be within a certain angle ''' - pass \ No newline at end of file + pass + +class FixationBMIControlMulti(BMIControlMultiEyeConstrained): + blink_time_threshold = traits.Float(0.1, desc="The amount of time in seconds that the eyes can be closed before triggering a fixation break, measured by eye_diam=0") + cursor_out_of_bounds_penalty_time = traits.Float(0.5, desc="The length of the penalty for moving the cursor out of bounds, in seconds") + assist_level = (1, 1) + is_bmi_seed = True + + exclude_parent_traits = ['cursor_bounds','show_environment_plane', 'show_environment'] + static_states = ['wait', 'delay', 'reward', 'cursor_out_of_bounds_penalty', 'fixation_penalty', 'pause', 'delay_penalty','timeout_penalty', 'sync'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.most_recent_open_eye = 0 + self.cursor_bounds = (-100., 100., -100., 100., -100., 100.) + self.show_environment_plane = True + self.show_environment = False + + status = dict( + wait = dict(start_trial="target", start_pause="pause"), + target = dict(timeout="timeout_penalty", enter_target="hold", start_pause="pause", fixation_break="fixation_penalty", + cursor_out_of_bounds='cursor_out_of_bounds_penalty'), + hold = dict(leave_target="hold_penalty", hold_complete="delay", fixation_break="fixation_penalty", start_pause="pause"), + delay = dict(leave_target="delay_penalty", delay_complete="targ_transition", fixation_break="fixation_penalty", start_pause="pause"), + targ_transition = dict(trial_complete="reward", trial_abort="wait", trial_incomplete="target", + cursor_out_of_bounds='cursor_out_of_bounds_penalty', start_pause="pause"), + cursor_out_of_bounds_penalty = dict(cursor_out_of_bounds_end="wait", start_pause="pause", end_state=True), + timeout_penalty = dict(timeout_penalty_end="wait", start_pause="pause", end_state=True), + hold_penalty = dict(hold_penalty_end="wait", start_pause="pause", end_state=True), + delay_penalty = dict(delay_penalty_end="wait", start_pause="pause", end_state=True), + fixation_penalty = dict(fixation_penalty_end="wait", start_pause="pause", end_state=True), + reward = dict(reward_end="wait", start_pause="pause", stoppable=False, end_state=True), + pause = dict(end_pause="wait", end_state=True), + ) + + def _reset_cursor(self): + self.decoder.filt.state.mean = self.init_decoder_mean.copy() + self.plant.set_visibility(False) + + + def _test_cursor_out_of_bounds_end(self, time_in_state): + return time_in_state > self.cursor_out_of_bounds_penalty_time + + def _start_cursor_out_of_bounds_penalty(self): + self.sync_event('OUT_OF_BOUNDS_PENALTY') + self._reset_cursor() + # Hide targets + for target in self.targets: + target.hide() + target.reset() + + self._increment_tries() + self.penalty_index = 1 + + def _end_cursor_out_of_bounds_penalty(self): + self.sync_event('TRIAL_END') + #self.sync_event('TRIAL_END') + + + def _test_cursor_out_of_bounds(self, time_in_state): + width, height = self.environment_plane_size + x_pos, y_pos, z_pos = self.environment_plane_origin + x_bound = np.array([width/2, -width/2])+x_pos + y_bound = np.array([height/2, -height/2])+y_pos + + #traits.Tuple((-10., 10., -10., 10., -10., 10.), desc='(x min, x max, y min, y max, z min, z max)') + cur_pos = self.plant.get_endpoint_pos() + check_x = (cur_pos[0] >= x_bound[0]) or (cur_pos[0] <= x_bound[1]) + check_y = (cur_pos[1] >= y_bound[0]) or (cur_pos[1] <= y_bound[1]) + #check_z = (cur_pos[2] >= cursor_bounds[4]) or (cur_pos[2] <= cursor_bounds[5]) + + cursor_out_of_bounds = check_x or check_y# or ~check_z + return cursor_out_of_bounds + + def move_effector(self): + pass + + def _start_target(self): + self.plant.set_visibility(True) + #target capture + self.target_index += 1 + + #screen target capture: Modified + # Show target if it is hidden (this is the first target, or previous state was a penalty) + target = self.targets[self.target_index % 2] + if self.target_index == 0: + #target.move_to_position(self.targs[self.target_index]) + #target.show() + self.sync_event('TARGET_ON', self.gen_indices[self.target_index]) + self.target_location = self.targs[self.target_index] # save for BMILoop + + + #if self.target_index == 0: + # self.targets_eye[0].move_to_position(self.targs[self.target_index] - self.offset_cube) + # self.targets_eye[0].show() + + + def _start_fixation_penalty(self): + self._reset_cursor() + super()._start_fixation_penalty() + + def _start_wait(self): + super()._start_wait() + #self.plant_visible = False + self.plant.set_visibility(True) + + def _end_targ_transition(self): + self.plant.set_visibility(False) + super()._end_targ_transition() + + def _start_pause(self): + super()._start_pause() + self.plant.set_visibility(False) + + def _end_pause(self): + super()._end_pause() + # Reset on any target transition away from the last target + self.decoder.filt.state.mean = self.init_decoder_mean.copy() + self.hdf.sendMsg("reset") + + self.plant.set_visibility(True) + + def _test_fixation_break(self,time_in_state): + ''' + Triggers the fixation_penalty state when eye positions are outside fixation distance + Only apply this to the first hold and delay period + ''' + + eye_pos = self.calibrated_eye_pos + eye_d = np.linalg.norm(eye_pos - self.targs[self.target_index,[0,2]]) + + #Make flag that tracks the last non-zero eye diameter and check that it has occured in the last 100ms + #First logic check: Check to see if the eye is open. If open, reset flag to 0 + eye_within_fixation_buffer = (eye_d > self.target_radius + self.fixation_radius_buffer) + #eye_within_fixation_buffer_cursor = + if self.keyboard_control: + return eye_within_fixation_buffer + elif np.any(self.eye_diam != 0): + self.most_recent_open_eye = 0 + elif self.most_recent_open_eye == 0: #Additionally check if this if the first cycle of 'eyes closed'. If not the first cycle, check how long since the flag was set and trigger a fixatio nfailure if longer than 100ms. + self.most_recent_open_eye=self.get_time() + elif (self.get_time()-self.most_recent_open_eye) > self.blink_time_threshold: + self.most_recent_open_eye = 0 + return True + + #Finally check if the eye location is within the target + buffer + return eye_within_fixation_buffer + + def _test_start_trial(self, time_in_state): + #Check that the eye position is on the center target + #return True #super()._test_start_trial + eye_pos = self.calibrated_eye_pos + eye_d = np.linalg.norm(eye_pos - self.targs[0,[0,2]]) #target index is zero, this is only applyied during the wait period before the center target comes on + + blink = self.keyboard_control | np.any(self.eye_diam!=0) + + value = (eye_d < self.target_radius + self.fixation_radius_buffer) & blink + return value#(eye_d > self.target_radius + self.fixation_radius_buffer) \ No newline at end of file diff --git a/built_in_tasks/passivetasks.py b/built_in_tasks/passivetasks.py index 03a2d58e..e99745ce 100644 --- a/built_in_tasks/passivetasks.py +++ b/built_in_tasks/passivetasks.py @@ -24,7 +24,7 @@ from .target_graphics import * -from .bmimultitasks import BMIControlMultiEyeConstrained +from .bmimultitasks import FixationBMIControlMulti bmi_ssm_options = ['Endpt2D', 'Tentacle', 'Joint2L'] @@ -57,122 +57,11 @@ class TargetCaptureVisualFeedback(EndPostureFeedbackController, BMIControlMulti) def move_effector(self): pass -class TargetCaptureVisualFeedbackEyeConstrained(EndPostureFeedbackController, BMIControlMultiEyeConstrained): - blink_time_threshold = traits.Float(0.1, desc="The amount of time in seconds that the eyes can be closed before triggering a fixation break, measured by eye_diam=0") - assist_level = (1, 1) - is_bmi_seed = True - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.most_recent_open_eye = 0 - - status = dict( - wait = dict(start_trial="target", start_pause="pause"), - target = dict(timeout="timeout_penalty", enter_target="hold", start_pause="pause", fixation_break="fixation_penalty"), - hold = dict(leave_target="hold_penalty", hold_complete="delay", fixation_break="fixation_penalty", start_pause="pause"), - delay = dict(leave_target="delay_penalty", delay_complete="targ_transition", fixation_break="fixation_penalty", start_pause="pause"), - targ_transition = dict(trial_complete="reward", trial_abort="wait", trial_incomplete="target", start_pause="pause"), - timeout_penalty = dict(timeout_penalty_end="wait", start_pause="pause", end_state=True), - hold_penalty = dict(hold_penalty_end="wait", start_pause="pause", end_state=True), - delay_penalty = dict(delay_penalty_end="wait", start_pause="pause", end_state=True), - fixation_penalty = dict(fixation_penalty_end="wait", start_pause="pause", end_state=True), - reward = dict(reward_end="wait", start_pause="pause", stoppable=False, end_state=True), - pause = dict(end_pause="wait", end_state=True), - ) - def move_effector(self): - pass - - def _start_target(self): - self.plant.set_visibility(True) - #target capture - self.target_index += 1 - - #screen target capture: Modified - # Show target if it is hidden (this is the first target, or previous state was a penalty) - target = self.targets[self.target_index % 2] - if self.target_index == 0: - #target.move_to_position(self.targs[self.target_index]) - #target.show() - self.sync_event('TARGET_ON', self.gen_indices[self.target_index]) - self.target_location = self.targs[self.target_index] # save for BMILoop - - - #if self.target_index == 0: - # self.targets_eye[0].move_to_position(self.targs[self.target_index] - self.offset_cube) - # self.targets_eye[0].show() - - def _start_fixation_penalty(self): - self.plant.set_visibility(False) - super()._start_fixation_penalty() - self.decoder.filt.state.mean = self.init_decoder_mean.copy() - - def _start_wait(self): - super()._start_wait() - #self.plant_visible = False - self.plant.set_visibility(True) - - def _end_targ_transition(self): - self.plant.set_visibility(False) - super()._end_targ_transition() - - def _start_pause(self): - super()._start_pause() - self.plant.set_visibility(False) - - def _end_pause(self): - super()._end_pause() - # Reset on any target transition away from the last target - self.decoder.filt.state.mean = self.init_decoder_mean.copy() - self.hdf.sendMsg("reset") - - self.plant.set_visibility(True) - - def _test_fixation_break(self,time_in_state): - ''' - Triggers the fixation_penalty state when eye positions are outside fixation distance - Only apply this to the first hold and delay period - ''' - - eye_pos = self.calibrated_eye_pos - eye_d = np.linalg.norm(eye_pos - self.targs[self.target_index,[0,2]]) - - #Make flag that tracks the last non-zero eye diameter and check that it has occured in the last 100ms - #First logic check: Check to see if the eye is open. If open, reset flag to 0 - eye_within_fixation_buffer = (eye_d > self.target_radius + self.fixation_radius_buffer) - if self.keyboard_control: - return eye_within_fixation_buffer - elif np.any(self.eye_diam != 0): - self.most_recent_open_eye = 0 - elif self.most_recent_open_eye == 0: #Additionally check if this if the first cycle of 'eyes closed'. If not the first cycle, check how long since the flag was set and trigger a fixatio nfailure if longer than 100ms. - self.most_recent_open_eye=self.get_time() - elif (self.get_time()-self.most_recent_open_eye) > self.blink_time_threshold: - self.most_recent_open_eye = 0 - return True - - #Finally check if the eye location is within the target + buffer - return eye_within_fixation_buffer - - def _test_start_trial(self, time_in_state): - #Check that the eye position is on the center target - #return True #super()._test_start_trial - eye_pos = self.calibrated_eye_pos - eye_d = np.linalg.norm(eye_pos - self.targs[0,[0,2]]) #target index is zero, this is only applyied during the wait period before the center target comes on - - blink = self.keyboard_control | np.any(self.eye_diam!=0) - - value = (eye_d < self.target_radius + self.fixation_radius_buffer) & blink - return value#(eye_d > self.target_radius + self.fixation_radius_buffer) - - #def _end_targ_transition(self): - # super()._end_targ_transition() - # if self.reset == 1:# and ((self.target_index == self.chain_length - 1) or (self.target_index == -1)): - - # # Reset on any target transition away from the last target - # self.decoder.filt.state.mean = self.init_decoder_mean.copy() - # self.hdf.sendMsg("reset") - +class TargetCaptureVisualFeedbackEyeConstrained(EndPostureFeedbackController, FixationBMIControlMulti): + """Passive viewing version of the fixation BMI task""" + static_states = ['wait', 'delay', 'reward', 'cursor_out_of_bounds_penalty', 'fixation_penalty', 'pause', 'delay_penalty','timeout_penalty', 'sync'] class TargetCaptureVFB2DWindow(TargetCaptureVisualFeedback, WindowDispl2D): diff --git a/config/rig_defaults.py b/config/rig_defaults.py index 1efa8898..e4f92cba 100644 --- a/config/rig_defaults.py +++ b/config/rig_defaults.py @@ -26,6 +26,7 @@ TIMEOUT_PENALTY = 0x41, DELAY_PENALTY = 0x42, FIXATION_PENALTY = 0x43, + OUT_OF_BOUNDS_PENALTY = 0x44, OTHER_PENALTY = 0x4f, CURSOR_ENTER_TARGET = 0x50, CURSOR_LEAVE_TARGET = 0x60, diff --git a/db/tracker/json_param.py b/db/tracker/json_param.py index a048d442..1a0e1f78 100644 --- a/db/tracker/json_param.py +++ b/db/tracker/json_param.py @@ -82,6 +82,8 @@ def norm_trait(trait, value): Model = getattr(models, mdl_name) record = Model.objects.get(pk=value) value = record.get() + if value is None: + raise ValueError("Instance not loaded properly, likely a Decoder not found or not properly loaded. Check that /storage/bmi is properly mounted and that the specified decoder is there") # Otherwise, let's hope it's already an instance elif ttype == 'DataFile': # Similar to Instance traits, except we always know to use models.DataFile as the database table to look up the primary key diff --git a/features/ecube_features.py b/features/ecube_features.py index 9029019f..04122793 100644 --- a/features/ecube_features.py +++ b/features/ecube_features.py @@ -279,7 +279,7 @@ class EcubeFileBMI(EcubeFileData, CorticalBMI): ''' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - + self.cortical_channels = [int(ch) for ch in self.decoder.units[:,0]] # These get read by CorticalData when initializing the extractor