Source code for cardioception.HRD.parameters

# 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

from cardioception.HRD.languages import danish, danish_children, english, french


[docs]def getParameters( participant: str = "SubjectTest", session: str = "001", serialPort: str = "COM3", setup: str = "behavioral", stairType: str = "psi", exteroception: bool = True, catchTrials: float = 0.0, nTrials: int = 120, device: str = "mouse", screenNb: int = 0, fullscr: bool = True, nBreaking: int = 20, resultPath: Optional[str] = None, language: str = "english", systole_kw: dict = {}, ): """Create Heart Rate Discrimination task parameters. Many task parameters, aesthetics, and options are controlled by the parameters dictonary defined herein. These are intended to provide flexibility and modularity to task. In many cases, unique versions of the task (e.g., with or without confidence ratings or choice feedback) can be created simply by changing these parameters, with no further interaction with the underlying task code. Parameters ---------- device : str Select how the participant provide responses. Can be `'mouse'` or `'keyboard'`. exteroception : bool If `True`, the task will include an exteroceptive (half of the trials). fullscr : bool If `True`, activate full screen mode. language : str The language used for the instruction. Can be `"english"`, `"danish"` or `"danish_children"` (a slightly simplified danish version), or `"french"`. nBreaking : int Number of trials to run before the break. nStaircase : int Number of staircase to use per condition (exteroceptive and interoceptive). nTrials : int The number of trials to run (UpDown and psi staircase). .. note:: This number indicates the total number of trials that will be presented during the experiment. If `nTrials=50` and `exteroception=False`, the task contains 50 interoceptive trials. If `nTrials=50` and `exteroception=True`, the task contains 25 interoceptive trials and 25 exteroceptive trials. participant : str Subject ID. Default is 'Participant'. catchTrials : float Ratio of Psi trials allocated to extreme values (+20 or -20 bpm with some jitter) to control for range of stimuli presented. Default to `0.0` (no catch trials). If not `0.0`, recomended value is `0.2`. resultPath : str | None Where to save the results. screenNb : int Screen number. Used to parametrize py:func:`psychopy.visual.Window`. Defaults 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. `"ehavioral"` will record through a Nonin pulse oximeter and `"test"` will use pre-recorded pulse time series (for testing only). stairType : str Staircase type. Can be "psi" or "updown". Default set to "psi". systole_kw : dict Additional keyword arguments for :py:class:`systole.recorder.Oxmeter`. Attributes ---------- allowedKeys : list of str The possible response keys. confScale : list The range of the confidence rating scale. device : str The device used for response and rating scale. Can be `"keyboard"` or `"mouse"`. HRcutOff : list Cut off for extreme heart rate values during recording. ExteroCondition : bool If `True`, the task includes an exteroceptive (half of the trials). isi : tuple Range of the inter-stimulus interval (seconds). Should be in the form of (low, high). At each trial the value is generated using a uniform distribution between these two values. Default is set to `(0.25, 0.25)` so the value is fixed at `0.25`. labelsRating : list The labels of the confidence rating scale. lambdaExtero : np.ndarray (3d) Posterior estimate of the psychophysics function parameters (slope and threshold) across trials for the exteroceptive condition. lambdaIntero : np.ndarray (3d) Posterior estimate of the psychophysics function parameters (slope and threshold) across trials for the interoceptive condition. listenLogo, heartLogo : Psychopy visual instance Image used for the inference and recording phases, respectively. maxRatingTime : float The maximum time for a confidence rating (in seconds). minRatingTime : float The minimum time before a rating can be provided during the confidence rating (in seconds). monitor : str The monitor used to present the task (Psychopy parameter). nBreaking : int Number of trials to run before the break. nConfidence : int The number of trial with feedback during the tutorial phase (no feedback). nFeedback : int The number of trial with feedback during the tutorial phase (no confidence rating). nFinger : str or None The finger number ("1", "2", "3", "4" or "5") where the participant decided to place the pulse oximeter (if relevant). nTrials : int The number of trials to run (UpDown and psi staircase). .. note:: This number indicates the total number of trials that will be presented during the experiment. If `nTrials=50` and `exteroception=False`, the task contains 50 interoceptive trials. If `nTrials=50` and `exteroception=True`, the task contains 25 interoceptive trials and 25 exteroceptive trials. participant : str Subject ID. Default is 'Participant'. path : str The task working directory. resultPath : str | None Where to save the results. serial : PySerial instance The serial port used to record the PPG activity. screenNb : int The screen number (Psychopy parameter). Default set to 0. signal_df : pandas.DataFrame instance Dataframe where the pulse signal recorded during the interoception condition will be stored. stairCase : dict The staircase instances for 'psi' and 'UpDown'. Each entry contain dictionary for 'Intero' and 'Extero conditions' (if relevant). staircaseType : 1d array-like Vector indexing stairce type (`'UpDown'`, `'psi'`, `'psiCatchTrial'`). startKey : str The key to press to start the task and go to next steps. respMax : float The maximum time for decision (in seconds). results : str The result directory. session : int Session number. Default to '001'. setup : str The context of recording. Can be `'behavioral'` or `'test'`. texts : dict Long text elements. textSize : float Scalling parameter for 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"` win : `psychopy.visual.window` The window in which to draw objects. Notes ----- When using the `behavioral` setup, triggers will be sent to the PPG recording. The trigger channel is coding for different events during the task as follows: - Trial start: 1 - recording trigger: 2 - sound trigger : 3 - rating trigger: 4 - end trigger: 5 All these events, except trial start, have also their time stamps encoded in the behavioral results data frame. """ from psychopy import data, event, visual parameters: Dict[str, Any] = {} parameters["ExteroCondition"] = exteroception parameters["device"] = device if parameters["device"] == "keyboard": parameters["confScale"] = [1, 7] parameters["labelsRating"] = ["Guess", "Certain"] parameters["screenNb"] = screenNb parameters["monitor"] = "testMonitor" parameters["nFeedback"] = 5 parameters["nConfidence"] = 8 parameters["respMax"] = 5 parameters["minRatingTime"] = 0.5 parameters["maxRatingTime"] = 5 parameters["isi"] = (0.25, 0.25) parameters["startKey"] = "space" parameters["allowedKeys"] = ["up", "down"] parameters["nTrials"] = nTrials parameters["nBreaking"] = nBreaking parameters["lambdaIntero"] = [] # Save the history of lambda values parameters["lambdaExtero"] = [] # Save the history of lambda values parameters["nFinger"] = None parameters["signal_df"] = pd.DataFrame([]) # Physiological recording parameters["results_df"] = pd.DataFrame([]) # Behavioral results # 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"] = None # Create Results directory if not already exists if not os.path.exists(parameters["resultPath"]): os.makedirs(parameters["resultPath"]) # Store posterior in a dictionary parameters["staircaisePosteriors"] = {} parameters["staircaisePosteriors"]["Intero"] = [] if exteroception is True: parameters["staircaisePosteriors"]["Extero"] = [] nCatch = int(parameters["nTrials"] * catchTrials) nStaircase = parameters["nTrials"] - nCatch # Vector encoding the staircase type if stairType == "psi": sc = np.array(["psi"] * nStaircase) elif stairType == "updown": sc = np.array(["updown"] * nStaircase) else: raise ValueError("stairType should be 'psi' or 'updown'") # Create and randomize condition vectors separately for each staircase if exteroception is True: # Create a modality vector containing nTrials/2 Intero and Extero conditions parameters["Modality"] = np.hstack( [np.array(["Extero", "Intero"] * int(parameters["nTrials"] / 2))] ) elif exteroception is False: # Create a modality vector containing nTrials/2 Intero and Extero conditions parameters["Modality"] = np.array(["Intero"] * int(parameters["nTrials"])) else: raise ValueError("exteroception should be a boolean") # Vector encoding the type of trial (psi, up/down or catch) parameters["staircaseType"] = np.hstack( [ sc, np.array(["CatchTrial"] * int((parameters["nTrials"] * catchTrials))), ] ) # Shuffle all trials shuffler = np.random.permutation(parameters["nTrials"]) parameters["Modality"] = parameters["Modality"][shuffler] parameters["staircaseType"] = parameters["staircaseType"][shuffler] # Default parameters for the basic staircase are set here. Please see # PsychoPy Staircase Handler Documentation for full options. By default, # the task implements a staircase using Psi method. # If UpDown is selected, 1 or 2 interleaved staircases are used (see # options in parameters dictionary), one is initalized 'high' and the other # 'low'. parameters["stairCase"] = {} if stairType == "updown": conditions = [ { "label": "low", "startVal": -40.5, "nUp": 1, "nDown": 1, "stepSizes": [20, 12, 12, 7, 4, 3, 2, 1], "stepType": "lin", "minVal": -40.5, "maxVal": 40.5, }, { "label": "high", "startVal": 40.5, "nUp": 1, "nDown": 1, "stepSizes": [20, 12, 12, 7, 4, 3, 2, 1], "stepType": "lin", "minVal": -40.5, "maxVal": 40.5, }, ] parameters["stairCase"]["Intero"] = data.MultiStairHandler( conditions=conditions, nTrials=parameters["nTrials"] ) elif stairType == "psi": parameters["stairCase"]["Intero"] = data.PsiHandler( nTrials=nTrials, intensRange=[-50.5, 50.5], alphaRange=[-50.5, 50.5], betaRange=[0.1, 25], intensPrecision=1, alphaPrecision=1, betaPrecision=0.1, delta=0.02, stepType="lin", expectedMin=0, ) if exteroception is True: if stairType == "updown": conditions = [ { "label": "low", "startVal": -40.5, "nUp": 1, "nDown": 1, "stepSizes": [20, 12, 12, 7, 4, 3, 2, 1], "stepType": "lin", "minVal": -40.5, "maxVal": 40.5, }, { "label": "high", "startVal": 40.5, "nUp": 1, "nDown": 1, "stepSizes": [20, 12, 12, 7, 4, 3, 2, 1], "stepType": "lin", "minVal": -40.5, "maxVal": 40.5, }, ] parameters["stairCase"]["Extero"] = data.MultiStairHandler( conditions=conditions, nTrials=parameters["nTrials"] ) elif stairType == "psi": parameters["stairCase"]["Extero"] = data.PsiHandler( nTrials=nTrials, intensRange=[-50.5, 50.5], alphaRange=[-50.5, 50.5], betaRange=[0.1, 25], intensPrecision=1, alphaPrecision=1, betaPrecision=0.1, delta=0.02, stepType="lin", expectedMin=0, ) parameters["setup"] = setup 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) ############## # Load texts # ############## if language == "english": parameters["texts"] = english( device=device, setup=setup, exteroception=exteroception ) elif language == "danish": parameters["texts"] = danish( device=device, setup=setup, exteroception=exteroception ) elif language == "danish_children": parameters["texts"] = danish_children( device=device, setup=setup, exteroception=exteroception ) elif language == "french": parameters["texts"] = french( device=device, setup=setup, exteroception=exteroception ) # Open window if parameters["setup"] == "test": fullscr = False parameters["win"] = visual.Window( monitor=parameters["monitor"], screen=parameters["screenNb"], fullscr=fullscr, units="height", ) parameters["win"].mouseVisible = False ############### # Image loading ############### if parameters["setup"] in ["test", "behavioral"]: parameters["pulseSchema"] = visual.ImageStim( win=parameters["win"], units="height", image=pkg_resources.resource_filename(__name__, "Images/pulseOximeter.png"), pos=(0.0, 0.0), ) parameters["pulseSchema"].size *= 0.2 parameters["handSchema"] = visual.ImageStim( win=parameters["win"], units="height", image=pkg_resources.resource_filename(__name__, "Images/hand.png"), pos=(0.0, -0.08), ) parameters["handSchema"].size *= 0.15 parameters["listenLogo"] = visual.ImageStim( win=parameters["win"], units="height", image=pkg_resources.resource_filename(__name__, "Images/listen.png"), pos=(0.0, 0.0), ) parameters["listenLogo"].size *= 0.08 parameters["heartLogo"] = visual.ImageStim( win=parameters["win"], units="height", image=pkg_resources.resource_filename(__name__, "Images/heartbeat.png"), pos=(0.0, 0.0), ) parameters["heartLogo"].size *= 0.04 parameters["textSize"] = 0.04 parameters["HRcutOff"] = [40, 120] if parameters["device"] == "keyboard": parameters["confScale"] = [1, 10] elif parameters["device"] == "mouse": parameters["myMouse"] = event.Mouse() return parameters