Skip to content

Commit

Permalink
Matrix timing validation (#213)
Browse files Browse the repository at this point in the history
  • Loading branch information
tab-cmd authored Apr 25, 2022
1 parent c865ba2 commit 6637816
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 19 deletions.
7 changes: 4 additions & 3 deletions bcipy/display/paradigm/matrix/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ def __init__(
self.max_grid_width = 0.7
self.stim_registry = {}

self.opacity = 0.15
self.start_opacity = 0.15
self.highlight_opacity = 1

# Trigger handling
self.first_run = True
Expand Down Expand Up @@ -161,7 +162,7 @@ def build_grid(self) -> None:
text_stim = visual.TextStim(
win=self.window,
text=sym,
opacity=self.opacity,
opacity=self.start_opacity,
pos=pos,
height=self.grid_stimuli_height)
self.stim_registry[sym] = text_stim
Expand Down Expand Up @@ -243,7 +244,7 @@ def animate_scp(self) -> List[float]:
self.draw_static()

# highlight a stimuli
self.stim_registry[sym].opacity = 1
self.stim_registry[sym].opacity = self.highlight_opacity
self.stim_registry[sym].draw()
# present stimuli and wait for self.stimuli_timing

Expand Down
15 changes: 15 additions & 0 deletions bcipy/helpers/stimuli.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ def list(cls):
return list(map(lambda c: c.value, cls))


class PhotoDiodeStimuli(Enum):
"""Photodiode Stimuli.
Enum to define unicode stimuli needed for testing system timing.
"""

EMPTY = '\u25A1' # box with a white border, no fill
SOLID = '\u25A0' # solid white box

@classmethod
def list(cls):
"""Returns all enum values as a list"""
return list(map(lambda c: c.value, cls))


class InquirySchedule(NamedTuple):
"""Schedule for the next inquiries to present, where each inquiry specifies
the stimulus, duration, and color information.
Expand Down
14 changes: 9 additions & 5 deletions bcipy/task/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@ Currently, these are the supported paradigms and modes:

> Calibration
> Alert Tone Calibration
> Inter-Inquiry Feedback Calibration
> Timing Verification Calibration
> Time Test Calibration
##### Mode: Copy Phrase

> Copy Phrase
### *Paradigm: Matrix*

##### Mode: Calibration

> Calibration
> Time Test Calibration

## Start Task
-------------
Expand Down
81 changes: 81 additions & 0 deletions bcipy/task/paradigm/matrix/timing_verification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from itertools import cycle
from bcipy.task import Task
from bcipy.task.paradigm.matrix.calibration import MatrixCalibrationTask
from bcipy.helpers.stimuli import PhotoDiodeStimuli


class MatrixTimingVerificationCalibration(Task):
"""Matrix Calibration Task.
This task is used for verifying display timing by alternating solid and empty boxes. These
stimuli can be used with a photodiode to ensure accurate presentations.
Input:
win (PsychoPy Display Object)
daq (Data Acquisition Object)
parameters (Dictionary)
file_save (String)
Output:
file_save (String)
"""
TASK_NAME = 'Matrix Timing Verification Task'

def __init__(self, win, daq, parameters, file_save):
super(MatrixTimingVerificationCalibration, self).__init__()
self._task = MatrixCalibrationTask(win, daq, parameters, file_save)
self.stimuli = PhotoDiodeStimuli.list()
self.create_testing_grid()
self._task.matrix.start_opacity = 0
self._task.matrix.grid_stimuli_height = 0.8
self._task.generate_stimuli = self.generate_stimuli

def create_testing_grid(self):
"""Create Testing Grid.
To test timing, we require the photodiode stimuli to flicker at the rate used in the execute method. The middle
of the existing grid is replaced with the necessary stimuli.
"""
mid = int(len(self._task.matrix.symbol_set) / 2)

for i, stim in enumerate(self.stimuli):
self._task.matrix.symbol_set[mid + i] = stim

def generate_stimuli(self):
"""Generates the inquiries to be presented.
Returns:
--------
tuple(
samples[list[list[str]]]: list of inquiries
timing(list[list[float]]): list of timings
color(list(list[str])): list of colors)
"""
samples, times, colors = [], [], []
stimuli = cycle(self.stimuli)

# alternate between solid and empty boxes
time_stim = self._task.timing
color_stim = self._task.color

inq_len = self._task.stim_length
inq_stim = [next(stimuli) for _ in range(inq_len)]
inq_times = [time_stim[0] for _ in range(inq_len)]
inq_colors = [color_stim[0] for _ in range(inq_len)]

for _ in range(self._task.stim_number):
samples.append(inq_stim)
times.append(inq_times)
colors.append(inq_colors)

return (samples, times, colors)

def execute(self):
self.logger.debug(f'Starting {self.name()}!')
self._task.execute()

@classmethod
def label(cls):
return MatrixTimingVerificationCalibration.TASK_NAME

def name(self):
return MatrixTimingVerificationCalibration.TASK_NAME
20 changes: 11 additions & 9 deletions bcipy/task/paradigm/rsvp/calibration/timing_verification.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from itertools import cycle
from bcipy.task import Task
from bcipy.task.paradigm.rsvp.calibration.calibration import RSVPCalibrationTask
from bcipy.helpers.stimuli import PhotoDiodeStimuli, get_fixation


class RSVPTimingVerificationCalibration(Task):
"""RSVP Calibration Task that for verifying timing.
"""RSVP Calibration Task.
This task is used for verifying display timing by alternating solid and empty boxes. These
stimuli can be used with a photodiode to ensure accurate presentations.
Input:
win (PsychoPy Display Object)
Expand All @@ -20,7 +24,8 @@ class RSVPTimingVerificationCalibration(Task):
def __init__(self, win, daq, parameters, file_save):
super(RSVPTimingVerificationCalibration, self).__init__()
parameters['stim_height'] = 0.8
parameters['stim_pos_y'] = 0.2
parameters['stim_pos_y'] = 0.0
self.stimuli = PhotoDiodeStimuli.list()
self._task = RSVPCalibrationTask(win, daq, parameters, file_save)
self._task.generate_stimuli = self.generate_stimuli

Expand All @@ -35,17 +40,14 @@ def generate_stimuli(self):
"""
samples, times, colors = [], [], []

solid_box = '\u25A0'
empty_box = '\u25A1'

target = 'x' # solid_box # 'X'
fixation = '\u25CB' # circle

# alternate between solid and empty boxes
letters = cycle([solid_box, empty_box])
letters = cycle(self.stimuli)
time_prompt, time_fixation, time_stim = self._task.timing
color_target, color_fixation, color_stim = self._task.color

target = next(letters)
fixation = get_fixation(is_txt=True)

inq_len = self._task.stim_length
inq_stim = [target, fixation, *[next(letters) for _ in range(inq_len)]]
inq_times = [time_prompt, time_fixation, *[time_stim for _ in range(inq_len)]]
Expand Down
8 changes: 6 additions & 2 deletions bcipy/task/start_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from bcipy.task.paradigm.rsvp.calibration.timing_verification import RSVPTimingVerificationCalibration

from bcipy.task.paradigm.matrix.calibration import MatrixCalibrationTask
from bcipy.task.paradigm.matrix.timing_verification import MatrixTimingVerificationCalibration

from bcipy.task import Task
from bcipy.task.exceptions import TaskRegistryException
Expand Down Expand Up @@ -40,12 +41,15 @@ def make_task(display_window, daq, task, parameters, file_save,
language_model, fake=fake)

if task is TaskType.RSVP_TIMING_VERIFICATION_CALIBRATION:
return RSVPTimingVerificationCalibration(display_window, daq,
parameters, file_save)
return RSVPTimingVerificationCalibration(display_window, daq, parameters, file_save)

if task is TaskType.MATRIX_CALIBRATION:
return MatrixCalibrationTask(
display_window, daq, parameters, file_save
)

if task is TaskType.MATRIX_TIMING_VERIFICATION_CALIBRATION:
return MatrixTimingVerificationCalibration(display_window, daq, parameters, file_save)
raise TaskRegistryException(
'The provided experiment type is not registered.')

Expand Down
1 change: 1 addition & 0 deletions bcipy/task/task_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class TaskType(Enum):
RSVP_COPY_PHRASE = 'RSVP Copy Phrase'
RSVP_TIMING_VERIFICATION_CALIBRATION = 'RSVP Time Test Calibration'
MATRIX_CALIBRATION = 'Matrix Calibration'
MATRIX_TIMING_VERIFICATION_CALIBRATION = 'Matrix Time Test Calibration'

def __new__(cls, *args, **kwds):
"""Autoincrements the value of each item added to the enum."""
Expand Down

0 comments on commit 6637816

Please sign in to comment.