from typing import Dict, Union
from .stimuli import ActionStimulus, BaseStimulus
try:
from psychopy.visual import ImageStim, TextStim, Window
from psychopy.visual.rect import Rect
except ModuleNotFoundError:
from .psychopy_stubs import Window, TextStim, ImageStim, Rect
import numpy as np
from ..utils import check_seed
from .default_images import generate_stimulus_properties, make_card_stimulus
from .logger import ExperimentLogger, SimulationLogger
[docs]
class ActionStimulusTooEarly(ActionStimulus):
"""
Stimulus class, for when actions are required by the participants.
"""
[docs]
def __init__(
self,
duration: float,
key_dict: Dict = {"left": 0, "right": 1},
name: str = "response",
timeout_action: int = None,
name_timeout="response-time-out",
name_tooearly="response-too-early",
text_tooearly={
"text": "Don't press too early!",
"pos": (0, -150),
"height": 28,
"color": "white",
},
):
"""
Setting up the action object.
Parameters
----------
duration : float
Duration of the response window.
key_dict : dict, optional
Dictionary to map keyboard responses to actions recognized by the environment, by default {"left": 0, "right": 1}
name : str, optional
name of the object, will be used for logging, by default None
timeout_action : int, optional
Behavior of the object if the response window times out, making it possible that no response is also a distinc action, by default None
"""
super().__init__(
name=name,
duration=duration,
key_dict=key_dict,
timeout_action=timeout_action,
name_timeout=name_timeout,
)
self.name_tooearly = name_tooearly
self.text_tooearly = text_tooearly
def _setup(self, win: Window = None, **kwargs):
"""
Does not need a special setup, including the function, to make easy looping possible.
Parameters
----------
win : Window, optional
The psychopy window object that is used for displaying stimuli, by default None
"""
self.text_stim = TextStim(win=win, **self.text_tooearly)
[docs]
def display(
self, win: Window, logger: ExperimentLogger, info: Dict, **kwargs
) -> Union[int, str]:
"""
Calls the stimulus object. In this case waiting for a specific response,
returning it and logging the process. Flipping the window in the end.
Parameters
----------
win : Window
The psychopy window object that is used for displaying stimuli.
logger : ExperimentLogger
The logger associated with the experiment.
Returns
-------
Union[int, str]
The response issued by the participant, interpretable by the environment.
"""
response_window_onset = logger.get_time()
response = logger.key_strokes(win, keyList=self.key_list)
if response:
RTE = response[1] - response_window_onset
logger.log_event(
{
"event_type": self.name_tooearly,
"response_button": response[0],
"response_time": RTE,
},
onset=response_window_onset,
)
self.text_stim.draw()
else:
RTE = False
# clearing buffer
logger.key_strokes(win)
response = self._response_handling(
win, logger, response_window_onset, info=info
)
if response is not None:
response_key, remaining = response
if RTE:
response_key = self.timeout_action
self.text_stim.draw()
else:
win.flip()
return response_key, remaining
else:
return response
[docs]
class ConditionBasedDisplay(BaseStimulus):
[docs]
def __init__(
self,
duration,
name=None,
image_position=(0, 0),
image_shift=350,
with_action=False,
image_map={
1: make_card_stimulus(generate_stimulus_properties(1)),
2: make_card_stimulus(generate_stimulus_properties(2)),
3: make_card_stimulus(generate_stimulus_properties(3)),
4: make_card_stimulus(generate_stimulus_properties(4)),
5: make_card_stimulus(generate_stimulus_properties(5)),
},
):
super().__init__(name=name, duration=duration)
self.image_position = image_position
self.image_map = image_map
self.image_shift = image_shift
self.with_action = with_action
def _setup(self, win, **kwargs):
self.image_dict = {}
for kk in self.image_map.keys():
if isinstance(self.image_map[kk], str):
self.image_dict[kk] = ImageStim(win=win, image=self.image_map[kk])
else:
self.image_dict[kk] = ImageStim(
win,
image=self.image_map[kk],
size=(self.image_map[kk].shape[1], self.image_map[kk].shape[0]),
)
[docs]
def display(self, win, logger, condition, action=None, **kwargs):
state1 = condition[0][list(condition[0].keys())[0]]
state2 = condition[0][list(condition[0].keys())[1]]
logger.key_strokes(win)
stim_onset = logger.get_time()
if state1 is not None:
imgA = self.image_dict[state1]
imgA.pos = (-self.image_shift, self.image_position[1])
imgA.setOpacity(1.0)
else:
imgA = None
if state2 is not None:
imgB = self.image_dict[state2]
imgB.pos = (self.image_shift, self.image_position[1])
imgB.setOpacity(1.0)
else:
imgB = None
if self.with_action:
if action == list(condition[0].keys())[0]:
feedback = Rect(
win=win,
width=imgA.size[0],
height=imgA.size[1],
lineColor="white",
lineWidth=7,
pos=imgA.pos,
)
if imgB is not None:
imgB.setOpacity(0.25)
else:
feedback = Rect(
win=win,
width=imgB.size[0],
height=imgB.size[1],
lineColor="white",
lineWidth=7,
pos=imgB.pos,
)
if imgA is not None:
imgA.setOpacity(0.25)
feedback.draw()
if imgA is not None:
imgA.draw()
if imgB is not None:
imgB.draw()
win.flip()
logger.wait(win, self.duration, stim_onset)
logger.log_event(
{"event_type": self.name, "expected_duration": self.duration},
onset=stim_onset,
)
return None
[docs]
class TwoStimuliWithResponseAndSelection(ActionStimulus):
[docs]
def __init__(
self,
duration: float,
key_dict: Dict = {"left": 0, "right": 1},
name: str = "response",
name_phase1: str = None,
name_phase2: str = None,
duration_phase1: float = 0.0,
duration_phase2: float = 0.0,
timeout_action: int = None,
name_timeout="response-time-out",
positions=((0, 0), (0, 0)),
images=[
make_card_stimulus(generate_stimulus_properties(12)),
make_card_stimulus(generate_stimulus_properties(23)),
],
flip_probability=0.5,
seed=111,
):
super().__init__(
name=name,
duration=duration,
key_dict=key_dict,
timeout_action=timeout_action,
name_timeout=name_timeout,
)
self.images = images
self.positions = positions
self.name_phase1 = name_phase1
self.name_phase2 = name_phase2
self.duration_phase1 = duration_phase1
self.duration_phase2 = duration_phase2
self.flip_probability = flip_probability
self.seed = check_seed(seed)
def _setup(self, win, **kwargs):
self.image_class = []
for img, pos in zip(self.images, self.positions):
if isinstance(img, str):
self.image_class.append(ImageStim(win=win, image=img, pos=pos))
else:
self.image_class.append(
ImageStim(
win, image=img, size=(img.shape[1], img.shape[0]), pos=pos
)
)
[docs]
def display(self, win, logger, info, **kwargs):
if self.seed.random() < self.flip_probability:
flip = True
flip_key_dict = {key: 1 - value for key, value in self.key_dict.items()}
else:
flip_key_dict = self.key_dict
flip = False
self._draw_stimulus(
win,
logger,
action=None,
name=self.name_phase1,
duration=self.duration_phase1,
flip=flip,
)
logger.key_strokes(win)
response_window_onset = logger.get_time()
response = self._response_handling(
win, logger, response_window_onset, key_dict=flip_key_dict, info=info
)
if response is not None:
self._draw_stimulus(
win,
logger,
action=response[0],
name=self.name_phase2,
duration=self.duration_phase2,
flip=flip,
)
return response
[docs]
def simulate(
self,
win: Window,
logger=SimulationLogger,
key: str = None,
rt: float = None,
info: Dict = None,
**kwargs,
):
stim_onset = logger.get_time()
logger.wait(win=None, time=self.duration_phase1, start=stim_onset)
logger.log_event(
{"event_type": self.name_phase1, "expected_duration": self.duration_phase1},
onset=stim_onset,
)
response_window_onset = logger.get_time()
response_key, remaining = self._simulate_response(
logger, key, rt, response_window_onset=response_window_onset, info=info
)
stim_onset = logger.get_time()
logger.wait(win=None, time=self.duration_phase2, start=stim_onset)
logger.log_event(
{"event_type": self.name_phase2, "expected_duration": self.duration_phase2},
onset=stim_onset,
)
return response_key, remaining
def _draw_stimulus(self, win, logger, action, name, duration, flip=False):
stim_onset = logger.get_time()
# Reset image positions
for im, po in zip(self.image_class, self.positions):
im.setPos(po)
imgA = self.image_class[0]
imgA.setOpacity(1.0)
imgB = self.image_class[1]
imgB.setOpacity(1.0)
if flip:
posA = imgA.pos
posB = imgB.pos
imgA.setPos(posB)
imgB.setPos(posA)
if action == 0:
feedback = Rect(
win=win,
width=imgA.size[0],
height=imgA.size[1],
lineColor="white",
lineWidth=7,
pos=imgA.pos,
)
imgB.setOpacity(0.25)
feedback.draw()
elif action == 1:
feedback = Rect(
win=win,
width=imgB.size[0],
height=imgB.size[1],
lineColor="white",
lineWidth=7,
pos=imgB.pos,
)
imgA.setOpacity(0.25)
feedback.draw()
imgA.draw()
imgB.draw()
for img in self.image_class[2:]:
img.draw()
win.flip()
logger.wait(win, duration, stim_onset)
logger.log_event(
{"event_type": name, "expected_duration": duration, "misc": f"flip-{flip}"},
onset=stim_onset,
)
[docs]
class TextWithBorder(BaseStimulus):
[docs]
def __init__(
self,
text,
condition_text,
duration=1.0,
width=250,
height=350,
lineWidth=3,
lineColor="white",
fillColor="grey",
textColor="white",
target="reward",
font_height=150,
position=(0, 0),
name=None,
):
super().__init__(name=name, duration=duration)
self.height = height
self.width = width
self.lineWidth = lineWidth
self.lineColor = lineColor
self.fillColor = fillColor
self.textColor = textColor
self.text = text
self.target = target
self.condition_text = condition_text
self.font_height = font_height
[docs]
def setup(self, win, **kwargs):
self.textStim = TextStim(
win=win,
name=self.name + "_text",
text=self.text,
color=self.textColor,
height=self.font_height,
)
self.rectStim = Rect(
win=win,
width=self.width,
height=self.height,
fillColor=self.fillColor,
lineWidth=self.lineWidth,
lineColor=self.lineColor,
name=self.name + "_rect",
)
[docs]
def display(self, win, logger, reward, action, **kwargs):
if self.target == "action":
reward = action
logger.key_strokes(win)
stim_onset = logger.get_time()
card = np.random.choice(self.condition_text[reward])
display_text = self.text.format(card)
self.textStim.setText(display_text)
self.rectStim.autoDraw = True
self.textStim.autoDraw = True
win.flip()
self.rectStim.autoDraw = False
self.textStim.autoDraw = False
logger.wait(win, self.duration, stim_onset)
logger.log_event(
{"event_type": self.name, "expected_duration": self.duration},
onset=stim_onset,
)
[docs]
class StimuliWithResponse(ActionStimulus):
[docs]
def __init__(
self,
duration: float,
key_dict: Dict = {"left": 0, "right": 1},
name: str = "response",
target_name: str = None,
target_duration: float = 0.0,
timeout_action: int = None,
name_timeout="response-time-out",
positions=((0, 0), (0, 0)),
images=[
make_card_stimulus(generate_stimulus_properties(12)),
make_card_stimulus(generate_stimulus_properties(23)),
],
flip_probability=0.5,
flip_dir: str = "horiz",
seed=111,
):
super().__init__(
name=name,
duration=duration,
key_dict=key_dict,
timeout_action=timeout_action,
name_timeout=name_timeout,
)
self.images = images
self.positions = positions
self.target_name = target_name
self.target_duration = target_duration
self.flip_probability = flip_probability
self.flip_dir = flip_dir
self.rng = check_seed(seed)
def _setup(self, win, **kwargs):
self.imageStims = []
for img, pos in zip(self.images, self.positions):
if isinstance(img, str):
self.imageStims.append(ImageStim(win=win, image=img, pos=pos))
else:
self.imageStims.append(
ImageStim(
win, image=img, size=(img.shape[1], img.shape[0]), pos=pos
)
)
[docs]
def display(self, win, logger, info, **kwargs):
logger.key_strokes(win)
response_window_onset = logger.get_time()
self._draw_stimulus(
win,
logger,
)
win.flip()
response = self._response_handling(
win, logger, response_window_onset, key_dict=self.key_dict, info=info
)
return response
[docs]
def simulate(
self,
win: Window,
logger=SimulationLogger,
key: str = None,
rt: float = None,
info: Dict = None,
**kwargs,
):
stim_onset = logger.get_time()
logger.wait(win=None, time=self.target_duration, start=stim_onset)
logger.log_event(
{"event_type": self.target_name, "expected_duration": self.target_duration},
onset=stim_onset,
)
response = self._simulate_response(
logger,
key,
rt,
response_window_onset=stim_onset,
info=info,
)
return response
def _draw_stimulus(self, win, logger, flip=False):
stim_onset = logger.get_time()
flip = self.rng.random() < self.flip_probability
# So that images are drawn on top
for ii in self.imageStims:
ii.autoDraw = True
if self.flip_dir == "vert":
ii.flip = [flip, False]
elif self.flip_dir == "horiz":
ii.flip = [False, flip]
win.flip()
for ii in self.imageStims:
ii.autoDraw = False
logger.wait(win, self.target_duration, stim_onset, wait_no_keys=True)
logger.log_event(
{
"event_type": self.target_name,
"expected_duration": self.target_duration,
"misc": f"flip-{flip}",
},
onset=stim_onset,
)