Skip to content

Commit

Permalink
Add preview only mode (#177)
Browse files Browse the repository at this point in the history
  • Loading branch information
tab-cmd authored Oct 18, 2021
1 parent 4a87e8f commit 2511ae6
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 26 deletions.
20 changes: 15 additions & 5 deletions bcipy/display/rsvp/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,21 +175,23 @@ class PreviewInquiryProperties:

def __init__(
self,
preview_only: bool,
preview_inquiry_length: float,
preview_inquiry_progress_method: int,
preview_inquiry_key_input: str,
preview_inquiry_isi: float):
"""Initialize Inquiry Preview Parameters.
preview_inquiry_length(float): Length of time in seconds to present the inquiry preview
preview_inquiry_progress_method(int): Method of progression for inquiry preview. 1 == press to accept
inquiry 2 == press to skip inquiry
preview_inquiry_progress_method(int): Method of progression for inquiry preview.
0 == preview only; 1 == press to accept inquiry; 2 == press to skip inquiry.
preview_inquiry_key_input(str): Defines which key should be listened to for progressing
preview_inquiry_isi(float): Length of time after displaying the inquiry preview to display a blank screen
"""
self.preview_inquiry_length = preview_inquiry_length
self.preview_inquiry_key_input = preview_inquiry_key_input
self.press_to_accept = True if preview_inquiry_progress_method == 1 else False
self.preview_only = preview_only
self.preview_inquiry_isi = preview_inquiry_isi


Expand Down Expand Up @@ -403,6 +405,10 @@ def preview_inquiry(self) -> Tuple[List[float], bool]:
- A tuple containing the timing information and a boolean describing whether to present
the inquiry (True) or generate another (False).
"""
# self._preview_inquiry defaults to None on __init__, assert it is defined correctly
assert isinstance(self._preview_inquiry, PreviewInquiryProperties), (
'PreviewInquiryProperties are not set on this RSVPDisplay. '
'Add them as a preview_inquiry kwarg to use preview_inquiry().')
# construct the timing to return and generate the content for preview
timing = []
if self.first_run:
Expand All @@ -424,22 +430,26 @@ def preview_inquiry(self) -> Tuple[List[float], bool]:

timer = core.CountdownTimer(self._preview_inquiry.preview_inquiry_length)
response = False

while timer.getTime() > 0:
# wait for a key press event
response = get_key_press(
key_list=[self._preview_inquiry.preview_inquiry_key_input],
clock=self.experiment_clock,
)
if response:

# break if a response given unless this is preview only and wait the timer
if response and not self._preview_inquiry.preview_only:
break

# reset the screen
self.draw_static()
self.window.flip()
self.trigger_callback.reset()

core.wait(self._preview_inquiry.preview_inquiry_isi)

if self._preview_inquiry.preview_only:
return timing, True

# depending on whether or not press to accept, define what to return to the task
if response and self._preview_inquiry.press_to_accept:
timing.append(response)
Expand Down
65 changes: 62 additions & 3 deletions bcipy/display/tests/rsvp/test_rsvp_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,10 @@ def setUp(self):
self.stimuli = TEST_STIM
self.preview_inquiry_length = 0.1
self.preview_inquiry_isi = 0.1
self.preview_inquiry_progress_method = 1 # press to accept == 1 press to skip == 2
self.preview_inquiry_progress_method = 1 # preview only = 0; press to accept == 1; press to skip == 2
self.preview_inquiry_key_input = 'space'
self.preview_inquiry = PreviewInquiryProperties(
preview_only=False,
preview_inquiry_length=self.preview_inquiry_length,
preview_inquiry_isi=self.preview_inquiry_isi,
preview_inquiry_progress_method=self.preview_inquiry_progress_method,
Expand Down Expand Up @@ -233,7 +234,7 @@ def test_preview_inquiry_evoked_press_to_accept_not_pressed(self, get_key_press_
get_key_press_mock.return_value = None
response = self.rsvp.preview_inquiry()
# we expect the trigger callback to return none, the key press to return the time and key press message,
# The second item should be True as it is press to accept and a response was returned
# The second item should be False as it is press to accept and a response was returned
expected = ([None], False)
self.assertEqual(response, expected)

Expand All @@ -254,10 +255,68 @@ def test_preview_inquiry_evoked_press_to_skip_not_pressed(self, get_key_press_mo
get_key_press_mock.return_value = None
response = self.rsvp.preview_inquiry()
# we expect the trigger callback to return none, the key press to return the time and key press message,
# The second item should be False as it is press to skip and a response was returned
# The second item should be True as it is press to skip and a response was not returned
expected = ([None], True)
self.assertEqual(response, expected)

@patch('bcipy.display.rsvp.display.get_key_press')
def test_preview_inquiry_preview_only_response_registered(self, get_key_press_mock):
# set the progress method to press to skip
self.rsvp._preview_inquiry.press_to_accept = False
self.rsvp._preview_inquiry.preview_only = True
stim_mock = mock()
# mock the stimulus generation
when(self.rsvp)._generate_inquiry_preview().thenReturn(stim_mock)
when(stim_mock).draw().thenReturn()
when(self.rsvp).draw_static().thenReturn()
when(self.rsvp.window).flip().thenReturn()
when(self.rsvp)._trigger_pulse(any()).thenReturn([])

# skip the core wait for testing
when(psychopy.core).wait(self.preview_inquiry.preview_inquiry_isi).thenReturn()
# we return a key press value here to demonstrate, even if a response is returned by this method, it will not
# be used in the preview_inquiry response from our display.
key_timestamp = 1000
get_key_press_mock.return_value = [
f'bcipy_key_press_{self.preview_inquiry_key_input}', key_timestamp
]
response = self.rsvp.preview_inquiry()
# we expect the trigger callback to return none, no key press response even if returned,
# The second item should be True as it is preview only
expected = ([None], True)
self.assertEqual(response, expected)

@patch('bcipy.display.rsvp.display.get_key_press')
def test_preview_inquiry_preview_only_no_response(self, get_key_press_mock):
# set the progress method to press to skip
self.rsvp._preview_inquiry.press_to_accept = False
self.rsvp._preview_inquiry.preview_only = True
stim_mock = mock()
# mock the stimulus generation
when(self.rsvp)._generate_inquiry_preview().thenReturn(stim_mock)
when(stim_mock).draw().thenReturn()
when(self.rsvp).draw_static().thenReturn()
when(self.rsvp.window).flip().thenReturn()
when(self.rsvp)._trigger_pulse(any()).thenReturn([])

# skip the core wait for testing
when(psychopy.core).wait(self.preview_inquiry.preview_inquiry_isi).thenReturn()

get_key_press_mock.return_value = None
response = self.rsvp.preview_inquiry()
# we expect the trigger callback to return none, no key press response,
# The second item should be True as it is preview only
expected = ([None], True)
self.assertEqual(response, expected)

def test_error_thrown_when_calling_preview_inquiry_without_properties_set(self):
# If not defined using the kwarg preview_inquiry, this value is set to None
self.rsvp._preview_inquiry = None

# Assert when set to None, calling the method will result in an exception
with self.assertRaises(Exception):
self.rsvp.preview_inquiry()


if __name__ == '__main__':
unittest.main()
5 changes: 3 additions & 2 deletions bcipy/parameters/parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -804,11 +804,12 @@
"type": "bool"
},
"preview_inquiry_progress_method": {
"value": "1",
"value": "0",
"section": "bci_config",
"readableName": "Preview Inquiry Progression Method",
"helpTip": "If show_preview_inquiry true, this will determine how to proceed after a key hit. 1 = press to confirm 2 = press to skip to another inquiry",
"helpTip": "If show_preview_inquiry true, this will determine how to proceed after a key hit. 0 = preview only; 1 = press to confirm; 2 = press to skip to another inquiry",
"recommended_values": [
"0",
"1",
"2"
],
Expand Down
37 changes: 25 additions & 12 deletions bcipy/task/paradigm/rsvp/copy_phrase.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,27 @@ def __init__(self, win, daq, parameters, file_save, signal_model,
self.experiment_clock = Clock()
self.start_time = self.experiment_clock.getTime()

self.alp = alphabet(parameters)
self.alp = alphabet(self.parameters)

self.button_press_error_prob = 0.05
self.evidence_types = [EvidenceType.LM, EvidenceType.ERP]
if self.parameters['show_preview_inquiry']:
self.evidence_types.append(EvidenceType.BTN)
# set a preview_only parameter
self.parameters.add_entry(
'preview_only',
{
'value': True if self.parameters['preview_inquiry_progress_method'] == 0 else False,
'section': '',
'readableName': '',
'helpTip': '',
'recommended_values': '',
'type': 'bool'
}
)

self.rsvp = _init_copy_phrase_display(self.parameters, self.window,
self.daq, self.static_clock,
self.experiment_clock)
self.static_clock, self.experiment_clock)
self.file_save = file_save

self.trigger_save_location = f"{self.file_save}/{parameters['trigger_file_name']}"
Expand All @@ -121,12 +138,6 @@ def __init__(self, win, daq, parameters, file_save, signal_model,
self.language_model = language_model
self.signal_model = signal_model

# TODO: add a parameter for button_press_error_prob.
self.button_press_error_prob = 0.05
self.evidence_types = [EvidenceType.LM, EvidenceType.ERP]
if self.parameters['show_preview_inquiry']:
self.evidence_types.append(EvidenceType.BTN)

def setup(self) -> None:
"""Initialize/reset parameters used in the execute run loop."""

Expand Down Expand Up @@ -451,8 +462,9 @@ def compute_button_press_evidence(
tuple of (evidence type, evidence) or None if inquiry preview is
not enabled.
"""
if not self.parameters[
'show_preview_inquiry'] or not self.current_inquiry:
if not self.parameters['show_preview_inquiry'] \
or not self.current_inquiry \
or self.parameters['preview_only']:
return None
probs = compute_probs_after_preview(self.current_inquiry.stimuli[0],
self.alp,
Expand Down Expand Up @@ -623,9 +635,10 @@ def name(self) -> str:
return self.TASK_NAME


def _init_copy_phrase_display(parameters, win, daq, static_clock,
def _init_copy_phrase_display(parameters, win, static_clock,
experiment_clock):
preview_inquiry = PreviewInquiryProperties(
preview_only=parameters['preview_only'],
preview_inquiry_length=parameters['preview_inquiry_length'],
preview_inquiry_key_input=parameters['preview_inquiry_key_input'],
preview_inquiry_progress_method=parameters[
Expand Down
10 changes: 6 additions & 4 deletions bcipy/task/tests/paradigm/rsvp/test_copy_phrase.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@
from bcipy.acquisition.protocols.lsl.lsl_client import LslAcquisitionClient
from bcipy.acquisition.device_info import DeviceInfo
from bcipy.helpers.copy_phrase_wrapper import CopyPhraseWrapper
from bcipy.helpers.parameters import Parameters
from bcipy.task.paradigm.rsvp.copy_phrase import RSVPCopyPhraseTask
from bcipy.task.data import Session, EvidenceType
from bcipy.helpers.stimuli import InquirySchedule, StimuliOrder
from bcipy.helpers.stimuli import InquirySchedule


class TestCopyPhrase(unittest.TestCase):
"""Tests for Copy Phrase task."""

def setUp(self):
"""Override; set up the needed path for load functions."""
self.parameters = {
parameters = {
'backspace_always_shown': True,
'decision_threshold': 0.8,
'down_sampling_rate': 2,
Expand Down Expand Up @@ -66,7 +67,7 @@ def setUp(self):
'stim_height': 0.6,
'stim_length': 10,
'stim_number': 100,
'stim_order': StimuliOrder.RANDOM,
'stim_order': 'random',
'stim_pos_x': 0.0,
'stim_pos_y': 0.0,
'stim_space_char': '–',
Expand All @@ -89,6 +90,7 @@ def setUp(self):
'wait_screen_message': 'Press Space to start or Esc to exit',
'wait_screen_message_color': 'white'
}
self.parameters = Parameters.from_cast_values(**parameters)

self.win = mock({'size': [500, 500], 'units': 'height'})

Expand All @@ -112,7 +114,7 @@ def setUp(self):

self.display = mock()
when(bcipy.task.paradigm.rsvp.copy_phrase)._init_copy_phrase_display(
self.parameters, self.win, self.daq, any,
self.parameters, self.win, any,
any).thenReturn(self.display)

when(bcipy.task.paradigm.rsvp.copy_phrase)._init_copy_phrase_wrapper(
Expand Down

0 comments on commit 2511ae6

Please sign in to comment.