Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions Experiments/Passive.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
84 changes: 84 additions & 0 deletions Interfaces/Photodiode.py
Original file line number Diff line number Diff line change
@@ -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__

84 changes: 84 additions & 0 deletions Interfaces/Wheel.py
Original file line number Diff line number Diff line change
@@ -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__

107 changes: 107 additions & 0 deletions Stimuli/PsychoBar.py
Original file line number Diff line number Diff line change
@@ -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()

59 changes: 59 additions & 0 deletions Stimuli/PsychoDot.py
Original file line number Diff line number Diff line change
@@ -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()




Loading