Skip to content

Commit

Permalink
Merge pull request #353 from CAMBI-tech/matrix-inquiry-preview
Browse files Browse the repository at this point in the history
Matrix Inquiry Preview
  • Loading branch information
lawhead authored Oct 10, 2024
2 parents 8f38b36 + 963b6e6 commit 7508da5
Show file tree
Hide file tree
Showing 20 changed files with 929 additions and 414 deletions.
109 changes: 109 additions & 0 deletions bcipy/display/components/button_press_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Handles button press interactions"""
from abc import ABC, abstractmethod
from typing import List, Optional, Type

from psychopy import event
from psychopy.core import CountdownTimer

from bcipy.helpers.clock import Clock
from bcipy.helpers.task import get_key_press


class ButtonPressHandler(ABC):
"""Handles button press events."""

def __init__(self,
max_wait: float,
key_input: str,
clock: Optional[Clock] = None,
timer: Type[CountdownTimer] = CountdownTimer):
"""
Parameters
----------
wait_length - maximum number of seconds to wait for a key press
key_input - key that we are listening for.
clock - clock used to associate the key event with a timestamp
"""
self.max_wait = max_wait
self.key_input = key_input
self.clock = clock or Clock()
self.response: Optional[List] = None

self._timer: Optional[CountdownTimer] = None
self.make_timer = timer

@property
def response_label(self) -> Optional[str]:
"""Label for the latest button press"""
return self.response[0] if self.response else None

@property
def response_timestamp(self) -> Optional[float]:
"""Timestamp for the latest button response"""
return self.response[1] if self.response else None

def _reset(self) -> None:
"""Reset any existing events and timers."""
self._timer = self.make_timer(self.max_wait)
self.response = None
event.clearEvents(eventType='keyboard')
self._timer.reset()

def await_response(self) -> None:
"""Wait for a button response for a maximum number of seconds. Wait
period could end early if the class determines that some other
criteria have been met (such as an acceptable response)."""

self._reset()
while self._should_keep_waiting() and self._within_wait_period():
self._check_key_press()

def has_response(self) -> bool:
"""Whether a response has been provided"""
return self.response is not None

def _check_key_press(self) -> None:
"""Check for any key press events and set the latest as the response."""
self.response = get_key_press(
key_list=[self.key_input],
clock=self.clock,
)

def _within_wait_period(self) -> bool:
"""Check that we are within the allotted time for a response."""
return (self._timer is not None) and (self._timer.getTime() > 0)

def _should_keep_waiting(self) -> bool:
"""Check that we should keep waiting for responses."""
return not self.has_response()

@abstractmethod
def accept_result(self) -> bool:
"""Should the result of a button press be affirmative"""


class AcceptButtonPressHandler(ButtonPressHandler):
"""ButtonPressHandler where a matching button press indicates an affirmative result."""

def accept_result(self) -> bool:
"""Should the result of a button press be affirmative"""
return self.has_response()


class RejectButtonPressHandler(ButtonPressHandler):
"""ButtonPressHandler where a matching button press indicates a rejection."""

def accept_result(self) -> bool:
"""Should the result of a button press be affirmative"""
return not self.has_response()


class PreviewOnlyButtonPressHandler(ButtonPressHandler):
"""ButtonPressHandler that waits for the entire span of the configured max_wait."""

def _should_keep_waiting(self) -> bool:
return True

def accept_result(self) -> bool:
"""Should the result of a button press be affirmative"""
return True
9 changes: 8 additions & 1 deletion bcipy/display/demo/matrix/demo_calibration_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from bcipy.display import (InformationProperties, StimuliProperties,
init_display_window)
from bcipy.display.components.task_bar import CalibrationTaskBar
from bcipy.display.main import PreviewParams
from bcipy.display.paradigm.matrix.display import MatrixDisplay

info = InformationProperties(
Expand Down Expand Up @@ -34,11 +35,17 @@
win.recordFrameIntervals = False

task_bar = CalibrationTaskBar(win, inquiry_count=4, current_index=0, font='Arial')
preview_config = PreviewParams(show_preview_inquiry=True,
preview_inquiry_length=2,
preview_inquiry_key_input='return',
preview_inquiry_progress_method=2,
preview_inquiry_isi=1)
matrix_display = MatrixDisplay(win,
experiment_clock,
stim_properties,
task_bar=task_bar,
info=info)
info=info,
preview_config=preview_config)

time_target = 1
time_fixation = 0.5
Expand Down
10 changes: 8 additions & 2 deletions bcipy/display/demo/matrix/demo_copyphrase_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from bcipy.display import (InformationProperties, StimuliProperties,
init_display_window)
from bcipy.display.components.task_bar import CopyPhraseTaskBar
from bcipy.display.main import PreviewParams
from bcipy.display.paradigm.matrix.display import MatrixDisplay

font = "Overpass Mono"
Expand Down Expand Up @@ -72,13 +73,18 @@
spelled_text='COPY_PHA',
colors=['white', 'green'],
font=font)

preview_config = PreviewParams(show_preview_inquiry=True,
preview_inquiry_length=2,
preview_inquiry_key_input='return',
preview_inquiry_progress_method=0,
preview_inquiry_isi=1)
display = MatrixDisplay(win,
experiment_clock,
stim_properties,
task_bar,
info,
should_prompt_target=False)
should_prompt_target=False,
preview_config=preview_config)

counter = 0

Expand Down
17 changes: 13 additions & 4 deletions bcipy/display/demo/rsvp/demo_calibration_rsvp.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from psychopy import core

from bcipy.display import (InformationProperties, StimuliProperties,
init_display_window)
from bcipy.display.components.task_bar import CalibrationTaskBar
from bcipy.display.main import PreviewParams
from bcipy.display.paradigm.rsvp.mode.calibration import CalibrationDisplay
from bcipy.helpers.clock import Clock
from bcipy.display import InformationProperties, StimuliProperties, init_display_window
from bcipy.display.components.task_bar import CalibrationTaskBar

info = InformationProperties(
info_color=['White'],
Expand Down Expand Up @@ -44,7 +46,7 @@
'full_screen': False,
'window_height': 500,
'window_width': 500,
'stim_screen': 1,
'stim_screen': 0,
'background_color': 'black'
}
win = init_display_window(window_parameters)
Expand All @@ -70,13 +72,20 @@
inquiry_count=100,
current_index=0,
font='Arial')

preview_config = PreviewParams(show_preview_inquiry=True,
preview_inquiry_length=2,
preview_inquiry_key_input='return',
preview_inquiry_progress_method=0,
preview_inquiry_isi=1)
rsvp = CalibrationDisplay(
win,
clock,
experiment_clock,
stimuli,
task_bar,
info)
info,
preview_config=preview_config)


for idx_o in range(len(task_text)):
Expand Down
44 changes: 15 additions & 29 deletions bcipy/display/demo/rsvp/demo_copyphrase_rsvp.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,23 @@

from psychopy import core

from bcipy.display import (
init_display_window,
InformationProperties,
PreviewInquiryProperties,
StimuliProperties)
from bcipy.display import (InformationProperties, StimuliProperties,
init_display_window)
from bcipy.display.components.task_bar import CopyPhraseTaskBar
from bcipy.display.main import PreviewParams
from bcipy.display.paradigm.rsvp.mode.copy_phrase import CopyPhraseDisplay
from bcipy.helpers.clock import Clock

# Initialize Stimulus
is_txt_stim = True
show_preview_inquiry = True


# Inquiry preview
preview_inquiry_length = 5
preview_inquiry_key_input = 'space'
preview_inquiry_progress_method = 1 # press to accept ==1 wait to accept ==2
preview_inquiry_isi = 3
preview_config = PreviewParams(show_preview_inquiry=True,
preview_inquiry_length=5,
preview_inquiry_key_input='space',
preview_inquiry_progress_method=1,
preview_inquiry_isi=3)

info = InformationProperties(
info_color=['White'],
Expand Down Expand Up @@ -50,7 +49,7 @@
'full_screen': False,
'window_height': 500,
'window_width': 500,
'stim_screen': 1,
'stim_screen': 0,
'background_color': 'black'
}

Expand Down Expand Up @@ -86,24 +85,19 @@
clock = core.StaticPeriod(screenHz=frameRate)
experiment_clock = Clock()
task_bar = CopyPhraseTaskBar(win, task_text='COPY_PHRASE', font='Menlo')
preview_inquiry = PreviewInquiryProperties(
preview_only=True,
preview_inquiry_length=preview_inquiry_length,
preview_inquiry_key_input=preview_inquiry_key_input,
preview_inquiry_progress_method=preview_inquiry_progress_method,
preview_inquiry_isi=preview_inquiry_isi)

rsvp = CopyPhraseDisplay(
win,
clock,
experiment_clock,
stimuli,
task_bar,
info,
preview_inquiry=preview_inquiry)
preview_config=preview_config)

counter = 0

for idx_o in range(len(spelled_text)):
for idx_o, _symbol in enumerate(spelled_text):

rsvp.update_task_bar(text=spelled_text[idx_o])
rsvp.draw_static()
Expand All @@ -119,16 +113,8 @@

core.wait(inter_stim_buffer)

if show_preview_inquiry:
inquiry_timing, proceed = rsvp.preview_inquiry()
print(inquiry_timing)
if proceed:
inquiry_timing.extend(rsvp.do_inquiry())
else:
print('Rejected inquiry! Handle here')
inquiry_timing = rsvp.do_inquiry()
else:
inquiry_timing = rsvp.do_inquiry()
inquiry_timing = rsvp.do_inquiry()
print(inquiry_timing)

core.wait(inter_stim_buffer)
counter += 1
Expand Down
53 changes: 51 additions & 2 deletions bcipy/display/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# mypy: disable-error-code="assignment,empty-body"
from abc import ABC, abstractmethod
from enum import Enum
from logging import Logger
from typing import Any, List, Optional, Tuple, Union
from typing import Any, List, NamedTuple, Optional, Tuple, Type, Union

from psychopy import visual

from bcipy.display.components.button_press_handler import (
AcceptButtonPressHandler, ButtonPressHandler,
PreviewOnlyButtonPressHandler, RejectButtonPressHandler)
from bcipy.helpers.clock import Clock
from bcipy.helpers.system_utils import get_screen_info

Expand Down Expand Up @@ -64,7 +68,7 @@ def draw_static(self) -> None:
"""
...

def preview_inquiry(self) -> List[float]:
def preview_inquiry(self, *args, **kwargs) -> List[float]:
"""Preview Inquiry.
Display an inquiry or instruction beforehand to the user. This should be called before do_inquiry.
Expand Down Expand Up @@ -238,6 +242,13 @@ def build_info_text(self, window: visual.Window) -> List[visual.TextStim]:
return self.text_stim


class ButtonPressMode(Enum):
"""Represents the possible meanings for a button press (when using an Inquiry Preview.)"""
NOTHING = 0
ACCEPT = 1
REJECT = 2


class PreviewInquiryProperties:
""""Preview Inquiry Properties.
An encapsulation of properties relevant to preview_inquiry() operation.
Expand Down Expand Up @@ -269,6 +280,44 @@ def __init__(
self.preview_inquiry_isi = preview_inquiry_isi


class PreviewParams(NamedTuple):
"""Parameters relevant for the Inquiry Preview functionality.
Create from an existing Parameters instance using:
>>> parameters.instantiate(PreviewParams)
"""
show_preview_inquiry: bool
preview_inquiry_length: float
preview_inquiry_key_input: str
preview_inquiry_progress_method: int
preview_inquiry_isi: float

@property
def button_press_mode(self):
"""Mode indicated by the inquiry progress method."""
return ButtonPressMode(self.preview_inquiry_progress_method)


def get_button_handler_class(
mode: ButtonPressMode) -> Type[ButtonPressHandler]:
"""Get the appropriate handler constructor for the given button press mode."""
mapping = {
ButtonPressMode.NOTHING: PreviewOnlyButtonPressHandler,
ButtonPressMode.ACCEPT: AcceptButtonPressHandler,
ButtonPressMode.REJECT: RejectButtonPressHandler
}
return mapping[mode]


def init_preview_button_handler(params: PreviewParams,
experiment_clock: Clock) -> ButtonPressHandler:
""""Returns a button press handler for inquiry preview."""
make_handler = get_button_handler_class(params.button_press_mode)
return make_handler(max_wait=params.preview_inquiry_length,
key_input=params.preview_inquiry_key_input,
clock=experiment_clock)


class VEPStimuliProperties(StimuliProperties):

def __init__(self,
Expand Down
Loading

0 comments on commit 7508da5

Please sign in to comment.