diff --git a/Experiments/Passive.py b/Experiments/Passive.py index 5624975b..d67add95 100644 --- a/Experiments/Passive.py +++ b/Experiments/Passive.py @@ -23,11 +23,13 @@ def entry(self): # updates stateMachine from Database entry - override for timi class Entry(Experiment): - def entry(self): - self.stim.prepare - def next(self): - return 'PreTrial' + if self.logger.setup_status in ['operational']: + return 'PreTrial' + elif self.is_stopped(): # if run out of conditions exit + return 'Exit' + else: + return 'Entry' class PreTrial(Experiment): diff --git a/Interfaces/Photodiode.py b/Interfaces/Photodiode.py new file mode 100644 index 00000000..4be40298 --- /dev/null +++ b/Interfaces/Photodiode.py @@ -0,0 +1,84 @@ +from core.Interface import * +import json, time +from serial import Serial +import threading +from queue import PriorityQueue + + +class Photodiode(Interface): + thread_end, msg_queue = threading.Event(), PriorityQueue(maxsize=1) + + def __init__(self, **kwargs): + super(Photodiode, self).__init__(**kwargs) + self.port = self.exp.logger.get(table='SetupConfiguration', key=self.exp.params, fields=['path'])[0] + self.baud = 115200 + self.timeout = .001 + self.offset = None + self.no_response = False + self.timeout_timer = time.time() + self.dataset = self.exp.logger.createDataset(dataset_name='fliptimes', + dataset_type=np.dtype([("phd_level", np.double), + ("tmst", np.double)])) + self.ser = Serial(self.port, baudrate=self.baud) + sleep(1) + self.thread_runner = threading.Thread(target=self._communicator) + self.thread_runner.start() + sleep(1) + print('logger timer:', self.exp.logger.logger_timer.elapsed_time()) + self.msg_queue.put(Message(type='offset', value=self.exp.logger.logger_timer.elapsed_time())) + + def cleanup(self): + self.thread_end.set() + self.ser.close() # Close the Serial connection + + def _communicator(self): + while not self.thread_end.is_set(): + if not self.msg_queue.empty(): + msg = self.msg_queue.get().dict() + self._write_msg(msg) # Send it + msg = self._read_msg() # Read the response + if msg is not None: + if msg['type'] == 'Level' and self.offset is not None: + print(msg['value'], msg['tmst']) + self.dataset.append('fliptimes', [msg['value'], msg['tmst']]) + elif msg['type'] == 'Offset': + self.offset = msg['value'] + print('offset:', self.offset) + + def _read_msg(self): + """Reads a line from the serial buffer, + decodes it and returns its contents as a dict.""" + now = time.time() + if (now - self.timeout_timer) > 3: + self.timeout_timer = time.time() + return None + elif self.ser.in_waiting == 0: # Nothing received + self.no_response = True + return None + incoming = self.ser.readline().decode("utf-8") + resp = None + self.no_response = False + self.timeout_timer = time.time() + try: + resp = json.loads(incoming) + except json.JSONDecodeError: + print("Error decoding JSON message!") + return resp + + def _write_msg(self, message=None): + """Sends a JSON-formatted command to the serial interface.""" + try: + json_msg = json.dumps(message) + self.ser.write(json_msg.encode("utf-8")) + except TypeError: + print("Unable to serialize message.") + + +@dataclass +class Message: + type: str = datafield(compare=False, default='') + value: int = datafield(compare=False, default=0) + + def dict(self): + return self.__dict__ + diff --git a/Interfaces/Wheel.py b/Interfaces/Wheel.py new file mode 100644 index 00000000..976f805a --- /dev/null +++ b/Interfaces/Wheel.py @@ -0,0 +1,84 @@ +from core.Interface import * +import json, time +from serial import Serial +import threading +from queue import PriorityQueue + + +class Wheel(Interface): + thread_end, msg_queue = threading.Event(), PriorityQueue(maxsize=1) + + def __init__(self, **kwargs): + super(Wheel, self).__init__(**kwargs) + self.port = self.logger.get(table='SetupConfiguration', key=self.exp.params, fields=['path'])[0] + self.baud = 115200 + self.timeout = .001 + self.offset = None + self.no_response = False + self.timeout_timer = time.time() + self.wheel_dataset = self.logger.createDataset(dataset_name='wheel', + dataset_type=np.dtype([("position", np.double), + ("tmst", np.double)])) + self.frame_dataset = self.logger.createDataset(dataset_name='frames', + dataset_type=np.dtype([("idx", np.double), + ("tmst", np.double)])) + self.ser = Serial(self.port, baudrate=self.baud) + sleep(1) + self.thread_runner = threading.Thread(target=self._communicator) + self.thread_runner.start() + sleep(1) + self.msg_queue.put(Message(type='offset', value=self.logger.logger_timer.elapsed_time())) + + def release(self): + self.thread_end.set() + self.ser.close() # Close the Serial connection + + def _communicator(self): + while not self.thread_end.is_set(): + if not self.msg_queue.empty(): + msg = self.msg_queue.get().dict() + self._write_msg(msg) # Send it + msg = self._read_msg() # Read the response + if msg is not None: + if msg['type'] == 'Position' and self.offset is not None: + self.wheel_dataset.append('wheel', [msg['value'], msg['tmst']]) + elif msg['type'] == 'Frame' and self.offset is not None: + self.frame_dataset.append('frames', [msg['value'], msg['tmst']]) + elif msg['type'] == 'Offset': + self.offset = msg['value'] + + def _read_msg(self): + """Reads a line from the serial buffer, + decodes it and returns its contents as a dict.""" + if self.ser.in_waiting == 0: # don't run faster than necessary + return None + + try: # read message + incoming = self.ser.readline().decode("utf-8") + except: # partial read of message, retry + return None + + try: # decode message + response = json.loads(incoming) + return response + except json.JSONDecodeError: + print("Error decoding JSON message!") + return None + + def _write_msg(self, message=None): + """Sends a JSON-formatted command to the serial interface.""" + try: + json_msg = json.dumps(message) + self.ser.write(json_msg.encode("utf-8")) + except TypeError: + print("Unable to serialize message.") + + +@dataclass +class Message: + type: str = datafield(compare=False, default='') + value: int = datafield(compare=False, default=0) + + def dict(self): + return self.__dict__ + diff --git a/Stimuli/PsychoBar.py b/Stimuli/PsychoBar.py new file mode 100644 index 00000000..49329675 --- /dev/null +++ b/Stimuli/PsychoBar.py @@ -0,0 +1,107 @@ +from core.Stimulus import * +from utils.helper_functions import flat2curve +from utils.PsychoPresenter import * + +@stimulus.schema +class Bar(Stimulus, dj.Manual): + definition = """ + # This class handles the presentation of area mapping Bar stimulus + -> StimCondition + --- + axis : enum('vertical','horizontal') + bar_width : float # degrees + bar_speed : float # degrees/sec + flash_speed : float # cycles/sec + grat_width : float # degrees + grat_freq : float + grid_width : float + grit_freq : float + style : enum('checkerboard', 'grating','none') + direction : float # 1 for UD LR, -1 for DU RL + flatness_correction : tinyint(1) # 1 correct for flatness of monitor, 0 do not + intertrial_duration : int + max_res : smallint + """ + + cond_tables = ['Bar'] + default_key = {'max_res' : 1000, + 'bar_width' : 4, # degrees + 'bar_speed' : 2, # degrees/sec + 'flash_speed' : 2, + 'grat_width' : 10, # degrees + 'grat_freq' : 1, + 'grid_width' : 10, + 'grit_freq' : .1, + 'style' : 'checkerboard', # checkerboard, grating + 'direction' : 1, # 1 for UD LR, -1 for DU RL + 'flatness_correction' : 1, + 'intertrial_duration' : 0} + + def setup(self): + self.Presenter = Presenter(self.logger, self.monitor, background_color=self.fill_colors.background, + photodiode='parity', rec_fliptimes=self.rec_fliptimes) + ymonsize = self.monitor.size * 2.54 / np.sqrt(1 + self.monitor.aspect ** 2) # cm Y monitor size + monSize = [ymonsize * self.monitor.aspect, ymonsize] + y_res = int(self.exp.params['max_res'] / self.monitor.aspect) + self.monRes = [self.exp.params['max_res'], int(y_res + np.ceil(y_res % 2))] + self.FoV = np.arctan(np.array(monSize) / 2 / self.monitor.distance) * 2 * 180 / np.pi # in degrees + self.FoV[1] = self.FoV[0] / self.monitor.aspect + + def prepare(self, curr_cond): + self.curr_cond = curr_cond + self.in_operation = True + self.curr_frame = 1 + + # initialize hor/ver gradients + caxis = 1 if self.curr_cond['axis'] == 'vertical' else 0 + Yspace = np.linspace(-self.FoV[1], self.FoV[1], self.monRes[1]) * self.curr_cond['direction'] + Xspace = np.linspace(-self.FoV[0], self.FoV[0], self.monRes[0]) * self.curr_cond['direction'] + self.cycles = dict() + [self.cycles[abs(caxis - 1)], self.cycles[caxis]] = np.meshgrid(-Yspace/2/self.curr_cond['bar_width'], + -Xspace/2/self.curr_cond['bar_width']) + if self.curr_cond['flatness_correction']: + I_c, self.transform = flat2curve(self.cycles[0], self.monitor.distance, + self.monitor.size, method='index', + center_x=self.monitor.center_x, + center_y=self.monitor.center_y) + self.BarOffset = -np.max(I_c) - 0.5 + deg_range = (np.ptp(I_c)+1)*self.curr_cond['bar_width'] + else: + deg_range = (self.FoV[caxis] + self.curr_cond['bar_width']) # in degrees + self.transform = lambda x: x + self.BarOffset = np.min(self.cycles[0]) - 0.5 + self.nbFrames = np.ceil( deg_range / self.curr_cond['bar_speed'] * self.monitor.fps) + self.BarOffsetCyclesPerFrame = deg_range / self.nbFrames / self.curr_cond['bar_width'] + + # compute fill parameters + if self.curr_cond['style'] == 'checkerboard': # create grid + [X, Y] = np.meshgrid(-Yspace / 2 / self.curr_cond['grid_width'], -Xspace / 2 / self.curr_cond['grid_width']) + VG1 = np.cos(2 * np.pi * X) > 0 # vertical grading + VG2 = np.cos((2 * np.pi * X) - np.pi) > 0 # vertical grading with pi offset + HG = np.cos(2 * np.pi * Y) > 0 # horizontal grading + Grid = VG1 * HG + VG2 * (1 - HG) # combine all + self.StimOffsetCyclesPerFrame = self.curr_cond['flash_speed'] / self.monitor.fps + self.generate = lambda x: abs(Grid - (np.cos(2 * np.pi * x) > 0)) + elif self.curr_cond['style'] == 'grating': + self.StimOffsetCyclesPerFrame = self.curr_cond['grat_freq'] / self.monitor.fps + self.generate = lambda x: np.cos(2 * np.pi * (self.cycles[1]*self.curr_cond['bar_width']/self.curr_cond['grat_width'] + x)) > 0 # vertical grading + elif self.curr_cond['style'] == 'none': + self.StimOffsetCyclesPerFrame = 1 + self.generate = lambda x: 1 + self.StimOffset = 0 # intialize offsets + + def present(self): + if self.curr_frame < self.nbFrames: + offset_cycles = self.cycles[0] + self.BarOffset + offset_cycles[np.logical_or(offset_cycles < -0.5, offset_cycles > .5)] = 0.5 # threshold grading to create a single bar + texture = np.int8((np.cos(offset_cycles * 2 * np.pi) > -1) * self.generate(self.StimOffset)*2) - 1 + new_surface = self.transform(np.tile(texture[:, :, np.newaxis], (1, 3))) + curr_image = psychopy.visual.ImageStim(self.win, new_surface) + curr_image.draw() + self.flip() + self.StimOffset += self.StimOffsetCyclesPerFrame + self.BarOffset += self.BarOffsetCyclesPerFrame + else: + self.in_operation = False + self.fill() + diff --git a/Stimuli/PsychoDot.py b/Stimuli/PsychoDot.py new file mode 100644 index 00000000..9829718f --- /dev/null +++ b/Stimuli/PsychoDot.py @@ -0,0 +1,59 @@ +from core.Stimulus import * +from utils.PsychoPresenter import * + +@stimulus.schema +class Dot(Stimulus, dj.Manual): + definition = """ + # This class handles the presentation of area mapping Bar stimulus + -> StimCondition + --- + bg_level : tinyblob # 0-255 + dot_level : tinyblob # 0-255 + dot_x : float # (fraction of monitor width, 0 for center, from -0.5 to 0.5) position of dot on x axis + dot_y : float # (fraction of monitor width, 0 for center) position of dot on y axis + dot_xsize : float # fraction of monitor width, width of dots + dot_ysize : float # fraction of monitor width, height of dots + dot_shape : enum('rect','oval') # shape of the dot + dot_time : float # (sec) time of each dot persists + """ + + cond_tables = ['Dot'] + required_fields = ['dot_x', 'dot_y', 'dot_xsize', 'dot_ysize', 'dot_time'] + default_key = {'bg_level' : 1, + 'dot_level' : 0, # degrees + 'dot_shape' : 'rect'} + + def setup(self): + self.Presenter = Presenter(self.logger, self.monitor, background_color=self.fill_colors.background, + photodiode='parity', rec_fliptimes=self.rec_fliptimes) + + def prepare(self, curr_cond): + self.curr_cond = curr_cond + self.fill_colors.background = self.curr_cond['bg_level'] + self.Presenter.fill(self.curr_cond['bg_level']) + self.rect = psychopy.visual.Rect(self.Presenter.win, + width=self.curr_cond['dot_xsize'], + height=self.curr_cond['dot_ysize'] * float(self.Presenter.window_ratio), + pos=[self.curr_cond['dot_x'], + self.curr_cond['dot_y'] * float(self.Presenter.window_ratio) ]) + + def start(self): + super().start() + self.rect.color = self.curr_cond['dot_level'] + self.rect.draw() + + def stop(self): + self.log_stop() + self.in_operation = False + + def present(self): + if self.timer.elapsed_time() > self.curr_cond['dot_time']*1000: + self.in_operation = False + + def exit(self): + self.Presenter.fill(self.fill_colors.background) + super().exit() + + + + diff --git a/Stimuli/PsychoGrating.py b/Stimuli/PsychoGrating.py new file mode 100644 index 00000000..95b840e6 --- /dev/null +++ b/Stimuli/PsychoGrating.py @@ -0,0 +1,81 @@ +from core.Stimulus import * +from utils.PsychoPresenter import * +from utils.helper_functions import iterable + + +@stimulus.schema +class PsychoGrating(Stimulus, dj.Manual): + definition = """ + # Definition of grating stimulus conditions + -> StimCondition + pos_x : float # relative to the origin at the center + --- + pos_y : float # relative to the origin at the center + tex="sin" : enum('sin','sqr','saw','tri') # Texture to use for the primary carrier + units="deg" : enum('deg','pix','cm') # Units to use when drawing + size : float # Size of the grating + mask="sqr" : enum('circle','sqr','gauss') # mask to control the shape of the grating + ori : float # Initial orientation of the shape in degrees about its origin + sf : float # cycles/deg + tf : float # cycles/sec + phase : float # initial phase in 0-1 + contrast : tinyint # 0-1 + warper : tinyint(1) # 1 correct for flatness of monitor, 0 do not + duration : smallint # grating duration (seconds) + """ + + cond_tables = ["PsychoGrating"] + required_fields = ['ori', 'sf', 'duration'] + default_key = { + "pos_x": 0, + "pos_y": 0, + "tex": "sin", + "units": " ", + "size": 100, + "mask": "gauss", + "tf": 0.5, + "phase": 0, + "contrast": 1, + "warper": 1} + + def setup(self): + """setup stimulation for presentation before experiment starts""" + self.Presenter = Presenter(self.logger, self.monitor, background_color=self.fill_colors.background, + photodiode='parity', rec_fliptimes=self.rec_fliptimes) + + def prepare(self, curr_cond, stim_period=''): + self.gratings = dict() + self.conds = dict() + self.curr_cond = curr_cond + for idx, pos_x in enumerate(iterable(self.curr_cond['pos_x'])): + # Create GratingStim object + cond = self._get_cond(idx) + self.conds[idx] = cond + self.gratings[idx] = psychopy.visual.GratingStim(self.Presenter.win, + pos=[cond['pos_x'], cond['pos_y']], + ori=cond['ori'], + sf=cond['sf'], + tex=cond['tex'], + units=cond['units'], + size=[cond['size'], cond['size']], + mask=cond['mask'], + phase=cond['phase'], + contrast=cond['contrast']) + + def present(self): + if self.timer.elapsed_time() > self.curr_cond['duration']: + self.in_operation = False + return + # Stimulus presentation ( + for idx, grat in enumerate(iterable(self.curr_cond['pos_x'])): + self.gratings[idx].phase += self.conds[idx]['tf'] / self.monitor.fps # assuming 60 Hz refresh rate + self.gratings[idx].draw() + self.Presenter.flip() + + def _get_cond(self, idx=0): + return {k: v if type(v) is int or type(v) is float or type(v) is str or type(v) is bool else v[idx] + for k, v in self.curr_cond.items()} + + def exit(self): + """exit stimulus stuff""" + self.Presenter.win.close() \ No newline at end of file diff --git a/Stimuli/PsychoMovies.py b/Stimuli/PsychoMovies.py new file mode 100644 index 00000000..4287e52d --- /dev/null +++ b/Stimuli/PsychoMovies.py @@ -0,0 +1,79 @@ +from core.Stimulus import * +from time import sleep +import io, os, imageio +from utils.PsychoPresenter import * + + +@stimulus.schema +class Movies(Stimulus, dj.Manual): + definition = """ + # movie clip conditions + -> StimCondition + --- + movie_name : char(8) # short movie title + clip_number : int # clip index + movie_duration : smallint # movie duration + skip_time : smallint # start time in clip + static_frame : smallint # static frame presentation + """ + + default_key = dict(movie_duration=10, skip_time=0, static_frame=False) + required_fields = ['movie_name', 'clip_number', 'movie_duration', 'skip_time', 'static_frame'] + cond_tables = ['Movies'] + + def init(self, exp): + super().init(exp) + self.path = self.logger.source_path + 'movies/' + + def setup(self): + self.Presenter = Presenter(self.logger, self.monitor, background_color=self.fill_colors.background, + photodiode='parity', rec_fliptimes=self.rec_fliptimes) + + def prepare(self, curr_cond, stim_period=''): + self.curr_cond = curr_cond + file_name = self.get_clip_info(self.curr_cond, 'Movie.Clip', 'file_name') + frame_rate, frame_height, frame_width = \ + self.get_clip_info(self.curr_cond, 'Movie', 'frame_rate', 'frame_height', 'frame_width') + video_aspect = frame_width[0]/frame_height[0] + size = [2, video_aspect*2] if self.monitor.aspect > video_aspect else [2*video_aspect, 2] + print('size: ', size) + self.vid = psychopy.visual.MovieStim(self.Presenter.win, self.path +file_name[0], + loop=False, # replay the video when it reaches the end + autoStart=True, + size=[2, 2], # set as `None` to use the native video size + units='norm') + + self.in_operation = True + self.timer.start() + + def present(self): + if self.timer.elapsed_time() < self.curr_cond['movie_duration']: + self.vid.draw() + self.Presenter.flip() + else: + self.in_operation = False + + def stop(self): + super().stop() + self.vid.stop() + + def get_clip_info(self, key, table, *fields): + return self.exp.logger.get(schema='stimulus', table=table, key=key, fields=fields) + + def make_conditions(self, conditions): + conditions = super().make_conditions(conditions) + + # store local copy of files + if not os.path.isdir(self.path): # create path if necessary + os.makedirs(self.path) + for cond in conditions: + file = self.exp.logger.get(schema='stimulus', table='Movie.Clip', key=cond, fields=('file_name',)) + print(file) + filename = self.path + file[0] + if not os.path.isfile(filename): + print('Saving %s' % filename) + clip = self.exp.logger.get(schema='stimulus', table='Movie.Clip', key=cond, fields=('clip',)) + clip[0].tofile(filename) + return conditions + + diff --git a/conf/bar_test.py b/conf/bar_test.py index d76bceee..c5313a5e 100644 --- a/conf/bar_test.py +++ b/conf/bar_test.py @@ -28,16 +28,17 @@ 'grit_freq' : 1, 'style' : 'checkerboard', # checkerboard, grating 'direction' : 1, # 1 for UD LR, -1 for DU RL - 'flatness_correction' : 1, + 'flatness_correction' : 0, 'intertrial_duration' : 0, } repeat_n = 10 +block = exp.Block(difficulty=0, next_up=0, next_down=0, trial_selection='fixed') conditions = [] for axis in ['horizontal', 'vertical']: for rep in range(0, repeat_n): - conditions += exp.make_conditions(stim_class=Bar(), conditions={**key, 'axis': axis}) + conditions += exp.make_conditions(stim_class=Bar(), conditions={**block.dict(),**key, 'axis': axis}) # run experiments diff --git a/conf/dot_test.py b/conf/dot_test.py index 3dbbfd3b..7a6e9a94 100644 --- a/conf/dot_test.py +++ b/conf/dot_test.py @@ -31,8 +31,9 @@ dot = Dot() dot.photodiode = False dot.rec_fliptimes = False +block = exp.Block(difficulty=0, next_up=0, next_down=0, trial_selection='fixed') for rep in range(0, repeat_n): - conditions += exp.make_conditions(stim_class=dot, conditions=key) + conditions += exp.make_conditions(stim_class=dot, conditions={**block.dict(), **key}) # randomize conditions random.seed(0) diff --git a/conf/grating_test.py b/conf/grating_test.py index 2d666d84..76677e99 100644 --- a/conf/grating_test.py +++ b/conf/grating_test.py @@ -1,7 +1,8 @@ # Orientation discrimination experiment -from Behaviors.MultiPort import * -from Experiments.MatchPort import * +from Experiments.Passive import * from Stimuli.Grating import * +from core.Behavior import * +from Interfaces.Photodiode import * # define session parameters session_params = { @@ -15,23 +16,23 @@ } exp = Experiment() -exp.setup(logger, MultiPort, session_params) +exp.setup(logger, Behavior, session_params) +#phd = Photodiode(exp=exp) +block = exp.Block(difficulty=0, next_up=0, next_down=0, trial_selection='fixed') # define stimulus conditions key = { 'contrast' : 100, 'spatial_freq' : .05, # cycles/deg 'square' : 0, # squarewave or Guassian - 'temporal_freq' : 0, # cycles/sec + 'temporal_freq' : 1, # cycles/sec 'flatness_correction': 1, # adjustment of spatiotemporal frequencies based on animal distance 'duration' : 5000, - 'difficulty' : 1, 'timeout_duration' : 1000, 'trial_duration' : 5000, 'intertrial_duration': 0, 'init_duration' : 0, 'delay_duration' : 0, - 'reward_amount' : 8 } repeat_n = 1 @@ -41,15 +42,16 @@ 2: 90} Grating_Stimuli = Grating() #if session_params['setup_conf_idx'] ==0 else GratingOld() +Grating_Stimuli.photodiode = 'parity' +Grating_Stimuli.rec_fliptimes = True Grating_Stimuli.fill_colors.ready = [] -block = exp.Block(difficulty=1, next_up=1, next_down=1, trial_selection='staircase', metric='dprime', stair_up=1, stair_down=0.5) for port in ports: - conditions += exp.make_conditions(stim_class=Grating_Stimuli, conditions={**block.dict(), - **key, + conditions += exp.make_conditions(stim_class=Grating_Stimuli, conditions={**block.dict(), **key, 'theta' : ports[port], 'reward_port' : port, 'response_port': port}) # run experiments exp.push_conditions(conditions) -exp.start() \ No newline at end of file +exp.start() +#phd.cleanup() \ No newline at end of file diff --git a/conf/movies_test.py b/conf/movies_test.py index f12b8608..9b187620 100644 --- a/conf/movies_test.py +++ b/conf/movies_test.py @@ -15,18 +15,19 @@ conditions = [] # define stimulus conditions -objects = ['MadMax'] +objects = ['madmax'] key = { - 'clip_number' : list(range(10, 40)), + 'clip_number' : [3,2,4], 'skip_time' : [0], - 'movie_duration' : 5000, + 'movie_duration' : 2000, 'static_frame' : False, 'intertrial_duration': 500, } +block = exp.Block(difficulty=0, next_up=0, next_down=0, trial_selection='fixed') for obj in objects: - conditions += exp.make_conditions(stim_class=Movies(), conditions={**key, 'movie_name': obj}) + conditions += exp.make_conditions(stim_class=Movies(), conditions={**block.dict(), **key, 'movie_name': obj}) random.seed(0) random.shuffle(conditions) diff --git a/conf/obj_test_passive.py b/conf/obj_test_passive.py index 1e2f4f8e..07d876c3 100644 --- a/conf/obj_test_passive.py +++ b/conf/obj_test_passive.py @@ -24,11 +24,11 @@ np.random.seed(0) obj_combs = [[3, 2]] times = 10 -reps = 1 +reps = 2 panda_obj = Panda() panda_obj.record() - +block = exp.Block(difficulty=0, next_up=0, next_down=0, trial_selection='fixed') for idx, obj_comb in enumerate(obj_combs): for irep in range(0, reps): pos_x_f = lambda x: interp(np.random.rand(x) - 0.5) @@ -39,7 +39,7 @@ yaw_f = lambda x: interp(np.random.rand(x)*10) dir1_f = lambda: np.array([0, -20, 0]) + np.random.randn(3)*30 dir2_f = lambda: np.array([180, -20, 0]) + np.random.randn(3)*30 - conditions += exp.make_conditions(stim_class=panda_obj, conditions={ + conditions += exp.make_conditions(stim_class=panda_obj, conditions={**block.dict(), 'background_color': (0.5, 0.5, 0.5), 'obj_dur': 3000, 'obj_id': [obj_comb], diff --git a/conf/psychograting_test.py b/conf/psychograting_test.py new file mode 100644 index 00000000..c8287d2a --- /dev/null +++ b/conf/psychograting_test.py @@ -0,0 +1,54 @@ +# Orientation discrimination experiment +from Experiments.Passive import * +from Stimuli.PsychoGrating import * +from core.Behavior import * +from Interfaces.Photodiode import * + +# define session parameters +session_params = { + 'trial_selection' : 'fixed', + 'setup_conf_idx' : 0, +} + +exp = Experiment() +exp.setup(logger, Behavior, session_params) +block = exp.Block(difficulty=0, next_up=0, next_down=0, trial_selection='fixed') + +# define stimulus conditions +key = { + 'contrast' : 1, + 'sf' : .2, # cycles/deg + 'duration' : 1000, + 'trial_duration' : 1000, + 'intertrial_duration': 0, + 'init_duration' : 0, + 'delay_duration' : 0, +} + +repeat_n = 1 +conditions = [] + +ports = {1: 0, + 2: 90} + +Grating_Stimuli = PsychoGrating() +Grating_Stimuli.fill_colors.ready = [] +Grating_Stimuli.fill_colors.background = [] + +Grating_Stimuli.fill_colors.set({'background': (0.5, 0.5, 0.5)}) + + +for port in ports: + conditions += exp.make_conditions(stim_class=Grating_Stimuli, conditions={**block.dict(), **key, + 'ori' : ports[port], + 'pos_x': [[-5, 5]], + 'size': 10}) + +conditions += exp.make_conditions(stim_class=Grating_Stimuli, conditions={**block.dict(), **key, + 'ori' : 0, + 'pos_x': 0, + 'size': 100}) + +# run experiments +exp.push_conditions(conditions) +exp.start() \ No newline at end of file diff --git a/core/Logger.py b/core/Logger.py index 47e8cfc6..5ceee396 100755 --- a/core/Logger.py +++ b/core/Logger.py @@ -592,6 +592,7 @@ def log_session(self, params: Dict[str, Any], log_protocol: bool = False) -> Non log_protocol (bool): Whether to log the protocol information. """ # Initializes session parameters and logs the session start. + self.logger_timer.start() # Start session time self._init_session_params(params) # Save the protocol file, name and the git_hash in the database. @@ -604,7 +605,7 @@ def log_session(self, params: Dict[str, Any], log_protocol: bool = False) -> Non # Init the informations(e.g. trial_id=0, session) in control table self._init_control_table(params) - self.logger_timer.start() # Start session time + def _init_session_params(self, params: Dict[str, Any]) -> None: """ @@ -630,7 +631,8 @@ def _init_session_params(self, params: Dict[str, Any]) -> None: # Creates a session key by merging trial key with session parameters. # TODO: Read the user name from the Control Table session_key = {**self.trial_key, **params, "setup": self.setup, - "user_name": params.get("user_name", "bot")} + "user_name": params.get("user_name", "bot"), + "logger_tmst": self.logger_timer.start_time} logging.info("session_key:\n%s", pprint.pformat(session_key)) # Logs the new session id to the database self.put(table="Session", tuple=session_key, priority=1, validate=True, block=True) @@ -1019,7 +1021,8 @@ def log_recording(self, rec_key): fields=["rec_idx"], ) rec_idx = 1 if not recs else max(recs) + 1 - self.log('Recording', data={**rec_key, 'rec_idx': rec_idx}, schema='recording') + self.log('Recording', data={**rec_key, 'rec_idx': rec_idx}, schema='recording', priority=1, block=True, + validate=True) def closeDatasets(self): """ diff --git a/core/Stimulus.py b/core/Stimulus.py index 997f91dd..a2d4d619 100755 --- a/core/Stimulus.py +++ b/core/Stimulus.py @@ -121,4 +121,4 @@ class Trial(dj.Part): -> StimCondition start_time : int # start time from session start (ms) end_time=NULL : int # end time from session start (ms) - """ + """ \ No newline at end of file diff --git a/utils/PsychoPresenter.py b/utils/PsychoPresenter.py new file mode 100644 index 00000000..6c57f2e8 --- /dev/null +++ b/utils/PsychoPresenter.py @@ -0,0 +1,108 @@ +import psychopy.visual +import psychopy.event +import psychopy.core +from psychopy.visual.windowwarp import Warper +import numpy as np +import pygame + + +class Presenter(): + + def __init__(self, logger, monitor, background_color=(0, 0, 0), photodiode=False, rec_fliptimes=False): + self.logger = logger + self.monitor = monitor + self.clock = pygame.time.Clock() + + # define the window + self.win = psychopy.visual.Window( + size=[monitor.resolution_x, monitor.resolution_y], + monitor='testMonitor', screen=monitor.screen_idx-1, + fullscr=monitor.fullscreen, useFBO=True, waitBlanking=True) + + # compensate for monitor flatness + if monitor.flatness_correction: + self.warper = Warper(self.win, + warp='spherical', + warpGridsize=64, + flipHorizontal=False, + flipVertical=False) + self.warper.dist_cm = 5 #self.monitor.distance + self.warper.warpGridsize = 64 + self.warper.changeProjection('spherical', eyepoint=(monitor.center_x+0.5, monitor.center_y+0.5)) + + # record fliptimes for syncing + self.rec_fliptimes = rec_fliptimes + if self.rec_fliptimes: + self.fliptimes_dataset = self.logger.createDataset(dataset_name='fliptimes', + dataset_type=np.dtype([("flip_idx", np.double), + ("tmst", np.double)])) + # initialize variables + self.flip_count = 0 + self.photodiode = photodiode + self.phd_size = 0.1 # default photodiode signal size in ratio of the X screen size + self.window_ratio = self.monitor.resolution_x / self.monitor.resolution_y + self._setup_photodiode(photodiode) + self.background_color = background_color + self.fill() + + def _setup_photodiode(self, photodiode=True): + if photodiode: + self.photodiode_rect = psychopy.visual.Rect(self.win, + units='norm', + width=self.phd_size, + height=self.phd_size * float(self.window_ratio), + pos=[-1 + self.phd_size / 2, + 1 - self.phd_size * float(self.window_ratio) / 2]) + if photodiode == 'parity': + # Encodes the flip even / dot in the flip amplitude + self.phd_f = lambda x: float(float(x // 2) == x / 2) + elif photodiode == 'flipcount': + # Encodes the flip count (n) in the flip amplitude. + # Every 32 sequential flips encode 32 21-bit flip numbers. + # Thus each n is a 21-bit flip number: FFFFFFFFFFFFFFFFCCCCP + # C = the position within F + # F = the current block of 32 flips + self.phd_f = lambda x: 0.5 * float(((x+1) & 1) * (2 - ((x+1) & (1 << (((np.int64(np.floor((x+1) / 2)) & 15) + 6) - 1)) != 0))) + else: + print(photodiode, ' method not implemented! Available methods: parity, flipcount') + + def render_image(self, image): + curr_image = psychopy.visual.ImageStim(self.win, image) + curr_image.draw() + self.flip() + + def set_background_color(self, color): + self.background_color = color + self.fill(color=color) + + def fill(self, color=False): + """stimulus hiding method""" + if not color: + color = self.background_color + if color: + self.win.color = color + self.flip() + + def flip(self): + self.flip_count += 1 + self._encode_photodiode() + self.win.flip() + if self.rec_fliptimes: + self.fliptimes_dataset.append('fliptimes', [self.flip_count, self.logger.logger_timer.elapsed_time()]) + self.tick() + + def tick(self): + self.clock.tick(self.monitor.fps) + + def _encode_photodiode(self): + """ Encodes the flip parity or flip number in the flip amplitude. + """ + if self.photodiode: + amp = self.phd_f(self.flip_count) + #self.warper.changeProjection(None) + self.photodiode_rect.color = [amp, amp, amp] + self.photodiode_rect.draw() + #self.warper.changeProjection('spherical', eyepoint=(self.monitor.center_x+0.5, self.monitor.center_y+0.5)) + + def quit(self): + self.win.close()