# Author: Nicolas Legrand <nicolas.legrand@cas.au.dk>
import os
from typing import Any, Dict, Optional
import numpy as np
import pandas as pd
import pkg_resources # type: ignore
import serial
from systole import serialSim
from systole.recording import Oximeter
[docs]def getParameters(
participant: str = "Participant",
session: str = "001",
serialPort: str = "COM3",
taskVersion: str = "Garfinkel",
setup: str = "behavioral",
screenNb: int = 0,
fullscr: bool = True,
resultPath: Optional[str] = None,
systole_kw: dict = {},
) -> Dict:
"""Create Heartbeat Counting task parameters.
Parameters
----------
participant : str
Subject ID. Default is 'exteroStairCase'.
resultPath : str or None
Where to save the results.
screenNb : int
Screen number. Used to parametrize py:func:`psychopy.visual.Window`.
Default is set to 0.
serialPort: str
The USB port where the pulse oximeter is plugged. Should be written as a string
e.g. `"COM3"` for USB ports on Windows.
session : int
Session number. Default to '001'.
setup : str
Context of oximeter recording. `"behavioral"` will record through a Nonin
pulse oximeter, `"test"` will use pre-recorded pulse time series (for testing
only).
systole_kw : dict
Additional keyword arguments for :py:class:`systole.recorder.Oxmeter`.
taskVersion : str or None
Task version to run. Can be 'Garfinkel', 'Shandry', 'test' or None.
Attributes
----------
conditions : 1d array-like of str
The conditions. Can be 'Rest', 'Training' or 'Count'.
confScale : list
The range of the confidence rating scale.
heartLogo : `psychopy.visual.ImageStim`
Image presented during resting conditions.
labelsRating : list
The labels of the confidence rating scale.
noteStart : psychopy.sound.Sound instance
The sound that will be played when trial starts.
noteStop : psychopy.sound.Sound instance
The sound that will be played when trial ends.
path : str
The task working directory.
randomize : bool
If `True` (default), will randomize the order of the conditions. If
taskVersion is not None, will use the default task parameter instead.
rating : bool
If `True` (default), will add a rating scale after the evaluation.
restLength : int
The length of the resting period (seconds). Default is 300 seconds.
restLogo : `psychopy.visual.ImageStim`
Image presented during resting conditions.
restPeriod : bool
If `True`, a resting period will be proposed before the task.
resultPath : str
The subject result directory.
screenNb : int
The screen number (Psychopy parameter). Default set to 0.
serial : `serial.Serial`
The serial port used to record the PPG activity.
startKey : str
The key to press to start the task and go to next steps.
taskVersion : str or None
Task version to run. Can be 'Garfinkel', 'Shandry', 'test' or None.
texts : dict
Dictionary containing the texts to be presented.
textSize : float
Text size.
triggers : dict
Dictionary {str, callable or None}. The function will be executed
before the corresponding trial sequence. The default values are
`None` (no trigger sent).
* `"trialStart"`
* `"trialStop"`
* `"listeningStart"`
* `"listeningStop"`
* `"decisionStart"`
* `"decisionStop"`
* `"confidenceStart"`
* `"confidenceStop"`
times : 1d array-like of int
Length of trials, in seconds.
win : `psychopy.visual.window`
The window in which to draw objects.
"""
from psychopy import sound, visual
parameters: Dict[str, Any] = {}
parameters["restPeriod"] = True
parameters["restLength"] = 30
parameters["randomize"] = True
parameters["startKey"] = "space"
parameters["rating"] = True
parameters["confScale"] = [1, 7]
parameters["labelsRating"] = ["Guess", "Certain"]
parameters["taskVersion"] = taskVersion
parameters["results_df"] = pd.DataFrame({})
parameters["setup"] = setup
# Initialize triggers dictionary with None
# Some or all can later be overwrited with callable
# sending the information needed.
parameters["triggers"] = {
"trialStart": None,
"trialStop": None,
"listeningStart": None,
"listeningStop": None,
"decisionStart": None,
"decisionStop": None,
"confidenceStart": None,
"confidenceStop": None,
}
# Experimental design - can choose between a version based on recent
# papers from Sarah Garfinkel's group, or the classic Schandry approach.
# The primary difference ebtween the two is the order of trials and the
# use of resting periods between trials.
if parameters["taskVersion"] == "Garfinkel":
parameters["times"] = np.array([25, 30, 35, 40, 45, 50])
np.random.shuffle(parameters["times"])
parameters["conditions"] = [
"Count",
"Count",
"Count",
"Count",
"Count",
"Count",
]
elif parameters["taskVersion"] == "Schandry":
parameters["times"] = np.array([60, 25, 30, 35, 30, 45])
parameters["conditions"] = ["Rest", "Count", "Rest", "Count", "Rest", "Count"]
elif parameters["taskVersion"] == "test":
parameters["times"] = np.array([5, 5])
parameters["conditions"] = ["Rest", "Count"]
else:
raise ValueError("Invalid task condition")
# Set default path /Results/ 'Subject ID' /
parameters["participant"] = participant
parameters["session"] = session
parameters["path"] = os.getcwd()
if resultPath is None:
parameters["resultPath"] = parameters["path"] + "/data/" + participant + session
else:
parameters["resultPath"] = resultPath
# Create Results directory of not already exists
if not os.path.exists(parameters["resultPath"]):
os.makedirs(parameters["resultPath"])
# Set note played at trial start
parameters["noteStart"] = sound.Sound(
pkg_resources.resource_filename("cardioception.HBC", "Sounds/start.wav")
)
parameters["noteStop"] = sound.Sound(
pkg_resources.resource_filename("cardioception.HBC", "Sounds/stop.wav")
)
# Open window
if parameters["setup"] == "test":
fullscr = False
parameters["win"] = visual.Window(screen=screenNb, fullscr=fullscr, units="height")
parameters["win"].mouseVisible = False
parameters["restLogo"] = visual.ImageStim(
win=parameters["win"],
units="height",
image=pkg_resources.resource_filename(__name__, "Images/rest.png"),
pos=(0.0, -0.2),
)
parameters["restLogo"].size *= 0.15
parameters["heartLogo"] = visual.ImageStim(
win=parameters["win"],
units="height",
image=pkg_resources.resource_filename(__name__, "Images/heartbeat.png"),
pos=(0.0, -0.2),
)
parameters["heartLogo"].size *= 0.05
if setup == "behavioral":
# PPG recording
port = serial.Serial(serialPort)
parameters["oxiTask"] = Oximeter(
serial=port, sfreq=75, add_channels=1, **systole_kw
)
parameters["oxiTask"].setup().read(duration=1)
elif setup == "test":
# Use pre-recorded pulse time series for testing
port = serialSim()
parameters["oxiTask"] = Oximeter(
serial=port, sfreq=75, add_channels=1, **systole_kw
)
parameters["oxiTask"].setup().read(duration=1)
#######
# Texts
#######
# Task instructions
parameters["texts"] = dict()
parameters["texts"]["Rest"] = "Please sit quietly until the next session"
parameters["texts"]["Count"] = (
"After you hear START, try to count your heartbeats"
" by concentrating on your body feelings."
" Stop counting when you hear STOP"
)
parameters["texts"]["Training"] = (
"After you hear START, try to count your heartbeats"
" by concentrating on your body feelings"
" Stop counting when you hear STOP"
)
parameters["texts"]["nCount"] = (
"How many heartbeats did you count?"
" Write a number and press ENTER to validate."
)
parameters["texts"]["confidence"] = (
"How confident are you about your count?"
"Use the RIGHT/LEFT keys to select and the DOWN key to confirm"
)
# Tutorial instructions
parameters["texts"]["Tutorial1"] = (
"During this experiment, we will ask you to silently"
" count your heartbeats for different intervals of time."
)
parameters["texts"]["Tutorial2"] = (
'When you see this "heart" icon, you will silently count your'
" heartbeats by focusing on your body sensations."
)
parameters["texts"]["Tutorial3"] = (
'Sometime, you will also encounter this "rest" icon.'
" In this case your task will just be to sit quietly until the next"
" session."
)
parameters["texts"]["Tutorial4"] = (
"The beginning and the end of the task will be signalled when you hear"
" the words 'START'' and 'STOP'. While counting your heartbeats, you"
" may close your eyes if you find that helpful. Please keep your hand"
" still during the counting period, to avoid interfering with"
" the heartbeat recording."
)
parameters["texts"]["Tutorial5"] = (
"After the counting part of the task, you will be asked to report the"
" exact number of heartbeats you felt during the interval between"
" 'START' and 'STOP'. Please do not try to estimate the number of"
" heartbeats, but instead only report the heartbeats you actually felt"
" during the interval. You will input your response using the number"
" pad and press return when done. You can also correct your response"
" using backspace."
)
parameters["texts"]["Tutorial6"] = (
"Once you have made your response, you will estimate your subjective"
" feeling of confidence in how accurate your count was"
" for that interval. A large number here means that you are totally"
" certain you counted the exact number of heartbeats that occured,"
" and a small number means that you are totally uncertain or felt that"
" you were guessing about the"
" number of heartbeats. You should use the RIGHT and LEFT"
" key to select your response and the DOWN key to confirm."
)
parameters["texts"]["Tutorial7"] = (
"Before the main task begins there is a short resting period of"
" several minutes, during which we will calibrate the heartbeat"
" recording. During this period, please sit quietly with your"
" hands still to avoid interfering with the calibration."
" Afterwards, the counting task will begin, and will take about"
" 6 minutes in total."
)
parameters["texts"]["Tutorial8"] = (
"You will now complete a short practice task."
" Please ask the experimenter if you have any questions before"
" continuing to the main experiment."
)
parameters["texts"]["Tutorial9"] = (
"Good job! If you have any question, ask the experimenter now,"
" otherwise press SPACE to continue to the experiment."
)
parameters["textSize"] = 0.04
return parameters