Skip to content

Commit a8856a0

Browse files
authored
Merge pull request #304 from CAMBI-tech/vep-calib
VEP calibration
2 parents 8ee2170 + 172bedf commit a8856a0

20 files changed

+1321
-470
lines changed

bcipy/display/demo/components/demo_layouts.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def demo_vep(win: visual.Window):
132132
layout = centered(width_pct=0.9, height_pct=0.9)
133133
layout.parent = win
134134

135-
box_config = BoxConfiguration(layout, num_boxes=4, height_pct=0.28)
135+
box_config = BoxConfiguration(layout, height_pct=0.28)
136136
size = box_config.box_size
137137
positions = box_config.positions
138138

+57-34
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1+
"""Demo VEP display"""
12
import logging
23
import sys
3-
4-
from psychopy import core
4+
from typing import Any, List
55

66
from bcipy.display import (InformationProperties, VEPStimuliProperties,
77
init_display_window)
88
from bcipy.display.components.layout import centered
99
from bcipy.display.components.task_bar import CalibrationTaskBar
10+
from bcipy.display.paradigm.vep.codes import DEFAULT_FLICKER_RATES
1011
from bcipy.display.paradigm.vep.display import VEPDisplay
1112
from bcipy.display.paradigm.vep.layout import BoxConfiguration
1213
from bcipy.helpers.clock import Clock
14+
from bcipy.helpers.system_utils import get_screen_info
1315

1416
root = logging.getLogger()
1517
root.setLevel(logging.DEBUG)
@@ -26,61 +28,82 @@
2628
info_text=['VEP Display Demo'],
2729
)
2830

29-
task_text = ['1/4', '2/4', '3/4', '4/4']
30-
num_boxes = 6
31-
31+
task_text = ['1/3', '2/3', '3/3']
32+
stim_screen = 0
3233
window_parameters = {
3334
'full_screen': False,
3435
'window_height': 700,
3536
'window_width': 700,
36-
'stim_screen': 1,
37+
'stim_screen': stim_screen,
3738
'background_color': 'black'
3839
}
3940
win = init_display_window(window_parameters)
4041
win.recordFrameIntervals = True
41-
frameRate = win.getActualFrameRate()
42+
frame_rate = win.getActualFrameRate()
43+
if not frame_rate:
44+
# Allow the demo to work using the configured rate.
45+
frame_rate = get_screen_info(stim_screen).rate
4246

43-
print(f'Monitor refresh rate: {frameRate} Hz')
47+
print(f'Monitor refresh rate: {frame_rate} Hz')
4448

45-
box_colors = ['#00FF80', '#FFFFB3', '#CB99FF', '#FB8072', '#80B1D3', '#FF8232']
46-
stim_color = [[color] for i, color in enumerate(box_colors) if i < num_boxes]
49+
stim_color = [
50+
'green', 'red', '#00FF80', '#FFFFB3', '#CB99FF', '#FB8072', '#80B1D3',
51+
'#FF8232'
52+
]
4753

4854
layout = centered(width_pct=0.95, height_pct=0.80)
49-
box_config = BoxConfiguration(layout, num_boxes=num_boxes)
55+
box_config = BoxConfiguration(layout, height_pct=0.30)
5056

5157
experiment_clock = Clock()
5258
len_stimuli = 10
53-
stimuli = VEPStimuliProperties(
54-
stim_color=stim_color,
59+
stim_props = VEPStimuliProperties(
60+
stim_font=font,
5561
stim_pos=box_config.positions,
5662
stim_height=0.1,
57-
stim_font=font,
58-
timing=(1, 0.5, 2, 4), # prompt, fixation, animation, stimuli
63+
timing=[4, 0.5, 4], # target, fixation, stimuli
64+
stim_color=stim_color,
65+
inquiry=[],
5966
stim_length=1, # how many times to stimuli
60-
)
61-
task_bar = CalibrationTaskBar(win,
62-
inquiry_count=4,
63-
current_index=0,
64-
font=font)
65-
vep = VEPDisplay(win, experiment_clock, stimuli, task_bar, info, box_config=box_config)
66-
timing = []
67+
animation_seconds=2.0)
68+
task_bar = CalibrationTaskBar(win, inquiry_count=3, current_index=0, font=font)
69+
vep = VEPDisplay(win,
70+
experiment_clock,
71+
stim_props,
72+
task_bar,
73+
info,
74+
box_config=box_config,
75+
flicker_rates=DEFAULT_FLICKER_RATES,
76+
should_prompt_target=True,
77+
frame_rate=frame_rate)
6778
wait_seconds = 2
6879

80+
inquiries: List[List[Any]] = [[
81+
'U', '+', ['C', 'M', 'S'], ['D', 'P', 'X', '_'], ['L', 'U', 'Y'],
82+
['E', 'K', 'O'], ['<', 'A', 'F', 'H', 'I', 'J', 'N', 'Q', 'R', 'V', 'Z'],
83+
['B', 'G', 'T', 'W']
84+
],
85+
[
86+
'D', '+', ['O', 'X'], ['D'], ['P', 'U'],
87+
['<', 'B', 'E', 'G', 'H', 'J', 'K', 'L', 'R', 'T'],
88+
['A', 'C', 'F', 'I', 'M', 'N', 'Q', 'V', 'Y', '_'],
89+
['S', 'W', 'Z']
90+
],
91+
[
92+
'S', '+', ['A', 'J', 'K', 'T', 'V', 'W'], ['S'], ['_'],
93+
['E', 'G', 'M', 'R'],
94+
[
95+
'<', 'B', 'C', 'D', 'H', 'I', 'L', 'N', 'O', 'P', 'Q',
96+
'U', 'X', 'Z'
97+
], ['F', 'Y']
98+
]]
99+
100+
timing = []
69101
# loop over the text and colors, present the stimuli and record the timing
70-
for txt in task_text:
102+
for i, txt in enumerate(task_text):
71103
vep.update_task_bar(txt)
72-
if num_boxes == 4:
73-
stim = [['A', 'B'], ['Z'], ['P'], ['R', 'W']]
74-
if num_boxes == 6:
75-
stim = [['A'], ['B'], ['Z', 'X'], ['P', 'I'], ['R'], ['W', 'C']]
76-
vep.schedule_to(stimuli=stim)
104+
inq = inquiries[i]
105+
vep.schedule_to(stimuli=inq)
77106
timing += vep.do_inquiry()
78107

79-
# show the wait screen, this will only happen once
80-
while wait_seconds > 0:
81-
wait_seconds -= 1
82-
vep.wait_screen(f"Waiting for {wait_seconds}s", color='white')
83-
core.wait(1)
84-
85108
print(timing)
86109
win.close()

bcipy/display/main.py

+12-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# mypy: disable-error-code="assignment,empty-body"
22
from abc import ABC, abstractmethod
33
from logging import Logger
4-
from typing import Optional, List, Tuple, Union
4+
from typing import Any, List, Optional, Tuple, Union
55

66
from psychopy import visual
77

@@ -33,7 +33,7 @@ def do_inquiry(self) -> List[float]:
3333
...
3434

3535
@abstractmethod
36-
def wait_screen(self) -> None:
36+
def wait_screen(self, *args, **kwargs) -> None:
3737
"""Wait Screen.
3838
3939
Define what happens on the screen when a user pauses a session.
@@ -265,15 +265,15 @@ def __init__(
265265

266266
class VEPStimuliProperties(StimuliProperties):
267267

268-
def __init__(
269-
self,
270-
stim_font: str,
271-
stim_pos: List[Tuple[float, float]],
272-
stim_height: float,
273-
timing: Optional[Tuple[float, float, float]] = None,
274-
stim_color: Optional[List[List[str]]] = None,
275-
inquiry: Optional[List[List[str]]] = None,
276-
stim_length: int = 1):
268+
def __init__(self,
269+
stim_font: str,
270+
stim_pos: List[Tuple[float, float]],
271+
stim_height: float,
272+
timing: List[float],
273+
stim_color: List[str],
274+
inquiry: List[List[Any]],
275+
stim_length: int = 1,
276+
animation_seconds: float = 1.0):
277277
"""Initialize VEP Stimuli Parameters.
278278
stim_color(List[str]): Ordered list of colors to apply to VEP stimuli
279279
stim_font(str): Font to apply to all VEP stimuli
@@ -293,6 +293,7 @@ def __init__(
293293
# dynamic properties, must be a a list of lists where each list is a different box
294294
self.stim_colors = stim_color
295295
self.stim_inquiry = inquiry
296+
self.animation_seconds = animation_seconds
296297

297298
def build_init_stimuli(self, window: visual.Window) -> None:
298299
""""Build Initial Stimuli."""

bcipy/display/paradigm/vep/codes.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Functions related to VEP flash rates"""
2+
import logging
3+
from typing import List
4+
5+
import numpy as np
6+
7+
from bcipy.helpers.exceptions import BciPyCoreException
8+
9+
log = logging.getLogger(__name__)
10+
11+
# These rates work for a 60hz display
12+
DEFAULT_FLICKER_RATES = [4, 5, 6, 10, 12, 15]
13+
14+
15+
def create_vep_codes(length=32, count=4) -> List[List[int]]:
16+
"""Create a list of random VEP codes.
17+
18+
length - how many bits in each code. This should be greater than or equal to the refresh rate
19+
if using these to flicker. For example, if the refresh rate is 60Hz, then the length should
20+
be at least 60.
21+
count - how many codes to generate, each will be unique.
22+
"""
23+
np.random.seed(1)
24+
return [np.random.randint(2, size=length) for _ in range(count)]
25+
26+
27+
def ssvep_to_code(refresh_rate: int = 60, flicker_rate: int = 10) -> List[int]:
28+
"""Convert SSVEP to Code.
29+
30+
Converts a SSVEP (steady state visual evoked potential; ex. 10 Hz) to a code (0,1)
31+
given the refresh rate of the monitor (Hz) provided and a desired flicker rate (Hz).
32+
33+
TODO: https://www.pivotaltracker.com/story/show/186522657
34+
Consider an additional parameter for the number of seconds.
35+
36+
Parameters:
37+
-----------
38+
refresh_rate: int, refresh rate of the monitor (Hz)
39+
flicker_rate: int, desired flicker rate (Hz)
40+
Returns:
41+
--------
42+
list of 0s and 1s that represent the code for the SSVEP on the monitor.
43+
"""
44+
if flicker_rate > refresh_rate:
45+
raise BciPyCoreException(
46+
'flicker rate cannot be greater than refresh rate')
47+
if flicker_rate <= 1:
48+
raise BciPyCoreException('flicker rate must be greater than 1')
49+
50+
# get the number of frames per flicker
51+
length_flicker = refresh_rate / flicker_rate
52+
53+
if length_flicker.is_integer():
54+
length_flicker = int(length_flicker)
55+
else:
56+
err_message = f'flicker rate={flicker_rate} is not an integer multiple of refresh rate={refresh_rate}'
57+
log.exception(err_message)
58+
raise BciPyCoreException(err_message)
59+
60+
# start the first frames as off (0) for length of flicker;
61+
# it will then toggle on (1)/ off (0) for length of flicker until all frames are filled for refresh rate.
62+
t = 0
63+
codes = []
64+
for _ in range(flicker_rate):
65+
codes += [t] * length_flicker
66+
t = 1 - t
67+
68+
return codes
69+
70+
71+
def round_refresh_rate(rate: float) -> int:
72+
"""Round the given display rate to the nearest 10s value.
73+
74+
>>> round_refresh_rate(59.12)
75+
60
76+
>>> round_refresh_rate(61.538)
77+
60
78+
>>> round_refresh_rate(121.23)
79+
120
80+
"""
81+
return int(round(rate, -1))

0 commit comments

Comments
 (0)