# Author: Nicolas Legrand <nicolas.legrand@cas.au.dk>
from typing import Optional, Tuple
import numpy as np
import pandas as pd
[docs]def run(
parameters: dict,
runTutorial: bool = True,
):
"""Run the entire task sequence.
Parameters
----------
parameters : dict
Task parameters.
tutorial : bool
If `True`, will present a tutorial with 10 training trial with feedback and 5
trials with confidence rating.
"""
from psychopy import core, visual
# Run tutorial
if runTutorial is True:
tutorial(parameters)
# Rest
if parameters["restPeriod"] is True:
rest(parameters, duration=parameters["restLength"])
for condition, duration, nTrial in zip(
parameters["conditions"],
parameters["times"],
range(0, len(parameters["conditions"])),
):
parameters["triggers"]["trialStart"] # Send trigger or None
nCount, confidence, confidenceRT = trial(
condition, duration, nTrial, parameters
)
parameters["triggers"]["trialStop"] # Send trigger or None
# Store results in a DataFrame
parameters["results_df"] = pd.concat(
[
parameters["results_df"],
pd.DataFrame(
{
"nTrial": [nTrial],
"Reported": [nCount],
"Condition": [condition],
"Duration": [duration],
"Confidence": [confidence],
"ConfidenceRT": [confidenceRT],
}
),
],
ignore_index=True,
)
# Save the results at each iteration
parameters["results_df"].to_csv(
parameters["resultPath"]
+ "/"
+ parameters["participant"]
+ parameters["session"]
+ ".txt",
index=False,
)
# Save results
parameters["results_df"].to_csv(
parameters["resultPath"]
+ "/"
+ parameters["participant"]
+ parameters["session"]
+ "_final.txt",
index=False,
)
# End of the task
end = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
pos=(0.0, 0.0),
text="You have completed the task. Thank you for your participation.",
)
end.draw()
parameters["win"].flip()
core.wait(3)
[docs]def trial(
condition: str,
duration: int,
nTrial: int,
parameters: dict,
) -> Tuple[Optional[int], Optional[float], Optional[float]]:
"""Run one trial.
Parameters
----------
condition : str
The trial condition, can be `"Rest"` or `"Count"`.
duration : int
The lenght of the recording (in seconds).
ntrial : int
Trial number.
parameters : dict
Task parameters.
Returns
-------
nCount : int
The number of heartbeat estimated by the participant.
confidence : int
The confidence in the estimation of the heartbeat provided by the
participant.
confidenceRT : float
The response time to provide confidence rating.
"""
from psychopy import core, event, visual
# Initialize default values
confidence, confidenceRT = None, None
nCounts: str = ""
# Ask the participant to press 'Space' (default) to start the trial
messageStart = visual.TextStim(
parameters["win"], height=parameters["textSize"], text="Press space to continue"
)
messageStart.draw()
parameters["win"].flip()
event.waitKeys(keyList=parameters["startKey"])
parameters["win"].flip()
parameters["oxiTask"].setup()
parameters["oxiTask"].read(duration=2)
# Show instructions
if condition == "Rest":
message = visual.TextStim(
parameters["win"],
text=parameters["texts"]["Rest"],
pos=(0.0, 0.2),
height=parameters["textSize"],
)
message.draw()
parameters["restLogo"].draw()
elif (condition == "Count") | (condition == "Training"):
message = visual.TextStim(
parameters["win"],
text=parameters["texts"]["Count"],
pos=(0.0, 0.2),
height=parameters["textSize"],
)
message.draw()
parameters["heartLogo"].draw()
parameters["win"].flip()
# Wait for a beat to start the task
parameters["oxiTask"].waitBeat()
core.wait(3)
# Sound signaling trial start
if (condition == "Count") | (condition == "Training"):
parameters["oxiTask"].readInWaiting()
# Add event marker
parameters["oxiTask"].channels["Channel_0"][-1] = 1
parameters["noteStart"].play()
parameters["triggers"]["listeningStart"]
core.wait(1)
# Record for a desired time length
parameters["oxiTask"].read(duration=duration - 1)
# Sound signaling trial stop
if (condition == "Count") | (condition == "Training"):
# Add event marker
parameters["oxiTask"].readInWaiting()
parameters["oxiTask"].channels["Channel_0"][-1] = 2
parameters["noteStop"].play()
parameters["triggers"]["listeningStop"]
core.wait(3)
parameters["oxiTask"].readInWaiting()
# Hide instructions
parameters["win"].flip()
# Save recording
parameters["oxiTask"].save(
parameters["resultPath"]
+ "/"
+ parameters["participant"]
+ str(nTrial)
+ "_"
+ str(nTrial)
)
###############################
# Record participant estimation
###############################
if (condition == "Count") | (condition == "Training"):
# Ask the participant to press 'Space' (default) to start the trial
messageCount = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
pos=(0, 0.2),
text=parameters["texts"]["nCount"],
)
messageCount.draw()
parameters["win"].flip()
parameters["triggers"]["decisionStart"] # Send trigger or None
nCounts = ""
while True:
# Record new key
key = event.waitKeys(
keyList=[
"escape",
"backspace",
"return",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
"num_1",
"num_2",
"num_3",
"num_4",
"num_5",
"num_6",
"num_7",
"num_8",
"num_9",
"num_0",
]
)
if key[0] == "escape":
keys = event.getKeys()
if "escape" in keys:
print("User abort")
parameters["win"].close()
core.quit()
if key[0] == "backspace":
if nCounts:
nCounts = nCounts[:-1]
elif key[0] == "return":
if not all(char.isdigit() for char in nCounts):
messageError = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
pos=(0, 0.2),
text="You should only provide numbers",
)
messageError.draw()
parameters["win"].flip()
core.wait(2)
elif nCounts == "":
messageError = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
pos=(0, 0.2),
text="You should provide numbers",
)
messageError.draw()
parameters["win"].flip()
core.wait(2)
else:
break
else:
if key:
nCounts += [s for s in key[0] if s.isdigit()][0]
# Show the text on the screen
recordedText = visual.TextStim(
parameters["win"], height=parameters["textSize"], text=nCounts
)
recordedText.draw()
messageCount.draw()
parameters["win"].flip()
parameters["triggers"]["decisionStop"] # Send trigger or None
##############
# Rating scale
##############
if parameters["rating"] is True:
markerStart = np.random.choice(
np.arange(parameters["confScale"][0], parameters["confScale"][1])
)
ratingScale = visual.RatingScale(
parameters["win"],
low=parameters["confScale"][0],
high=parameters["confScale"][1],
noMouse=True,
labels=parameters["labelsRating"],
acceptKeys="down",
markerStart=markerStart,
)
message = visual.TextStim(
parameters["win"],
text=parameters["texts"]["confidence"],
height=parameters["textSize"],
)
parameters["triggers"]["confidenceStart"]
while ratingScale.noResponse:
message.draw()
ratingScale.draw()
parameters["win"].flip()
confidence = ratingScale.getRating()
confidenceRT = ratingScale.getRT()
parameters["triggers"]["confidenceStop"]
finalCount = int(nCounts) if nCounts else None
return finalCount, confidence, confidenceRT
[docs]def tutorial(parameters: dict):
"""Run tutorial for the Heartbeat Counting Task.
Parameters
----------
parameters : dict
Task parameters.
win : `psychopy.visual.window` or None
The window in which to draw objects.
"""
from psychopy import event, visual
# Tutorial 1
messageStart = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text=parameters["texts"]["Tutorial1"],
)
messageStart.draw()
press = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text="Please press SPACE to continue",
pos=(0.0, -0.4),
)
press.draw()
parameters["win"].flip()
event.waitKeys(keyList=parameters["startKey"])
# Tutorial 2
messageStart = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
pos=(0.0, 0.2),
text=parameters["texts"]["Tutorial2"],
)
messageStart.draw()
parameters["heartLogo"].draw()
press = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text="Please press SPACE to continue",
pos=(0.0, -0.4),
)
press.draw()
parameters["win"].flip()
event.waitKeys(keyList=parameters["startKey"])
# Tutorial 3
if parameters["taskVersion"] == "Shandry":
messageStart = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
pos=(0.0, 0.2),
text=parameters["texts"]["Tutorial3"],
)
messageStart.draw()
parameters["restLogo"].draw()
press = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text="Please press SPACE to continue",
pos=(0.0, -0.4),
)
press.draw()
parameters["win"].flip()
event.waitKeys(keyList=parameters["startKey"])
# Tutorial 4
messageStart = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text=parameters["texts"]["Tutorial4"],
)
messageStart.draw()
press = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text="Please press SPACE to continue",
pos=(0.0, -0.4),
)
press.draw()
parameters["win"].flip()
event.waitKeys(keyList=parameters["startKey"])
# Tutorial 5
messageStart = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text=parameters["texts"]["Tutorial5"],
)
messageStart.draw()
press = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text="Please press SPACE to continue",
pos=(0.0, -0.4),
)
press.draw()
parameters["win"].flip()
event.waitKeys(keyList=parameters["startKey"])
# Tutorial 6
messageStart = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text=parameters["texts"]["Tutorial6"],
)
messageStart.draw()
press = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text="Please press SPACE to continue",
pos=(0.0, -0.4),
)
press.draw()
parameters["win"].flip()
event.waitKeys(keyList=parameters["startKey"])
# Tutorial 7
messageStart = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text=parameters["texts"]["Tutorial7"],
)
messageStart.draw()
press = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text="Please press SPACE to continue",
pos=(0.0, -0.4),
)
press.draw()
parameters["win"].flip()
event.waitKeys(keyList=parameters["startKey"])
# Tutorial 8
messageStart = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text=parameters["texts"]["Tutorial8"],
)
messageStart.draw()
press = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text="Please press SPACE to continue",
pos=(0.0, -0.4),
)
press.draw()
parameters["win"].flip()
event.waitKeys(keyList=parameters["startKey"])
# Practice trial
_ = trial("Count", 15, 0, parameters)
# Tutorial 9
messageStart = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text=parameters["texts"]["Tutorial9"],
)
messageStart.draw()
press = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
text="Please press SPACE to continue",
pos=(0.0, -0.4),
)
press.draw()
parameters["win"].flip()
event.waitKeys(keyList=parameters["startKey"])
[docs]def rest(parameters: dict, duration: float = 300.0):
"""Run a resting state period for heart rate variability before running the Heart
Beat Counting Task.
Parameters
----------
parameters : dict
Task parameters.
duration : float
Duration or the recording (seconds).
"""
from psychopy import visual
# Show the resting state instructions
messageStart = visual.TextStim(
parameters["win"],
height=parameters["textSize"],
pos=(0.0, 0.2),
text=("Calibrating... Please sit quietly" " until the end of the recording."),
)
messageStart.draw()
parameters["restLogo"].draw()
parameters["win"].flip()
# Record PPG signal
parameters["oxiTask"].setup()
parameters["oxiTask"].read(duration=duration)
# Save recording
parameters["oxiTask"].save(
parameters["resultPath"] + "/" + parameters["participant"] + "_Rest"
)