Heart Rate Discrimination task#

The Heart Rate Discrimination task (HRD) [Legrand et al., 2022] measures judgements of one’s own heart rate. Participants attend to their heartbeat, listen to a sequence of tones, and judge whether the tones were faster or slower than their heart.

Across trials, these responses define a psychometric function. Its threshold estimates bias in perceived heart rate, and its slope describes the precision of those judgements. Optional confidence ratings allow researchers to examine how confidence relates to performance. Measuring cardiac interoception explains the interpretation and limits of these measures.

What a trial looks like#

A trial begins with a fixation cross for 0.25 s. The participant then attends to their heartbeat for 5 s while the task records the pulse and estimates heart rate. A sequence of tones follows at the measured rate plus a signed difference, Alpha, in beats per minute.

The participant has 5 s to judge whether the tones were faster or slower than their heart, using the mouse buttons or the up and down arrow keys. They then rate their confidence on a 0 to 100 slider with the mouse, or in ten steps with the keyboard. The rating period lasts between 0.5 and 5 s.

By default, half the trials form an exteroceptive control condition. The participant listens for 5 s to a reference tone sequence at a random rate between 40 and 100 BPM, then judges whether a second sequence is faster or slower. The response procedure and confidence rating are the same as in the interoceptive condition.

Panel a, the task's screens for one interoceptive and one exteroceptive trial, fixation 0.25 s, listening 5 s, decision up to 5 s and confidence 0.5 to 5 s. Panel b, the Psi staircase over 60 trials in each condition for one participant, with each stimulus marked by the answer given and the threshold estimate converging. Panel c, the psychometric functions at the final estimates, interoceptive alpha -3.2 and sigma 9.2, exteroceptive alpha +2.4 and sigma 4.8.

(a) The screens for one trial in each condition, with the default durations; a 0.5 s fixation precedes the decision. (b) The Psi staircase for one participant from the Visceral Mind Project, showing each trial’s ΔBPM and response, along with the threshold estimate and its 95 percent interval. (c) Psychometric functions drawn from the staircase’s final online estimates, with the threshold (α) at 50 percent “faster” responses. The tutorials explain how to fit psychometric functions to the trial data for analysis.#

Each condition has a separate Psi staircase [Kontsevich and Tyler, 1999]. On each trial, it chooses Alpha between -50.5 and 50.5 ΔBPM to obtain information about the participant’s threshold and slope.

If the participant does not respond in time, the task records DecisionProvided as false and leaves the staircase unchanged. By default, the missed trial returns to the end of the queue (onMissedTrial="represent"), with at most three presentations.

With runTutorial=True, the session begins with instructions and practice. Participants first complete five trials per condition with feedback at a difference of 20 BPM (nFeedback), then eight per condition with confidence ratings and no feedback (nConfidence).

The main task contains 120 trials by default, 60 per condition, with a break every 20 trials. The four example sessions in the documentation took 20 to 22 minutes without the tutorial, or about 10 s per trial. At that pace, the 26 practice trials add about 5 minutes, plus time for the instructions.

What you need#

  • A pulse oximeter on a USB port (the Nonin 3012LP Xpod by default), or an ECG or PPG device streaming over Lab Streaming Layer. Recording devices covers both and how to check the oximeter before a session.

  • Headphones or speakers.

  • A mouse or a keyboard.

Running it#

The repository ships a wrapper for the oximeter, wrappers/hrd.py, and one for LSL, wrappers/hrd_lsl.py. Copy the one you need next to your data folder and run it:

python hrd.py

A dialog asks for:

Field

Default

What it sets

participant

Participant

The participant identifier

session

HRD

The session identifier

Serial port

COM5

The port the oximeter is on. python -m cardioception.check_device --list lists the ports

Setup

behavioral

behavioral reads the oximeter; test replays a recorded pulse, for checking a script without a device

Device

mouse

mouse or keyboard for the answers and the rating

Language

english

The instructions

The LSL wrapper asks for the stream name, the channel label and the signal type (ecg or ppg) instead of the port and setup.

To run the task from a Python script:

from cardioception.HRD.parameters import getParameters
from cardioception.HRD.task import run

parameters = getParameters(
    participant="Subject_01", session="HRD", serialPort="COM3",
    setup="behavioral", nTrials=120, exteroception=True)
run(parameters, confidenceRating=True, runTutorial=True)
parameters["win"].close()

With an LSL device, pass a recorder instead of a port and close it at the end:

from cardioception.devices import LSLRecorder

recorder = LSLRecorder(name="PLUX", channel="ECG2", signal_type="ecg")
parameters = getParameters(participant="Subject_01", session="HRD", recorder=recorder)
run(parameters, confidenceRating=True, runTutorial=True)
parameters["win"].close()
recorder.close()

Escape ends the session; what has been collected is saved.

The main settings#

The following arguments to getParameters control the session:

Argument

Default

Meaning

nTrials

120

Trials in all. With exteroception=True half are interoceptive and half exteroceptive

exteroception

True

Include the exteroceptive control condition

device

"mouse"

"mouse" or "keyboard"

language

"english"

"english", "danish", "danish_children", "french" or "german". The wrappers offer a subset

confidenceScale

by device

A cardioception.scales.ConfidenceScale: 0 to 100 for the mouse, ten steps for the keyboard. The end labels follow the language

catchTrials

0.0

Share of trials at a fixed difference instead of the staircase’s, cycling through -30, +10, -20, +20, -10 and +30 BPM, to control the range of stimuli. They do not update the staircase. 0.2 if used

onMissedTrial

"represent"

Re-present a trial with no answer ("represent", up to maxRepresentations, 3) or move on ("skip")

nBreaking

20

Trials between breaks

screenNb

0

The screen the task window opens on

resultPath

data

Root of the run directories

overwrite

False

Allow writing into a run directory that already holds results

seed

drawn

Seeds the session’s random generator; recorded in the manifest

stairType

"psi"

The only staircase. The nUp/nDown staircase was removed in 1.0.0, so "updown" raises; the changelog explains why

To change trial timing or staircase bounds, pass a TaskConfig object as config=:

from cardioception.HRD.config import TaskConfig

parameters = getParameters(..., config=TaskConfig(respMax=8, nFeedback=10))

Field

Default

Meaning

listeningDuration

5.0 s

The listening window in both conditions

respMax

5.0 s

Time to answer

minRatingTime, maxRatingTime

0.5, 5.0 s

Bounds on the confidence rating

HRcutOff

40 to 120 BPM

A heart rate outside this range is treated as artefact and the window retaken, up to maxHeartRateAttempts (10) times

exteroBPMRange

40 to 100 by 0.5

The rates the exteroceptive reference is drawn from

psiPreset

"legacy"

The Psi settings for both staircases, below

intensRange, alphaRange, betaRange

From the preset

The stimuli the staircase may present, and the thresholds and slopes it considers, in ΔBPM. A value set here applies to both conditions

delta

From the preset

The lapse rate, held fixed

psiPreset chooses between two sets of Psi settings. "legacy" is the setting of the original paper and of every earlier release: the same uniform ranges in both conditions. "courtin" narrows the ranges separately for each condition, to what the hierarchical refit of the Visceral Mind Project data found plausible, and halves the lapse rate [Courtin et al., 2026].

Stimuli

Threshold α

Slope σ

Lapse

"legacy", both conditions

-50.5 to 50.5

-50.5 to 50.5

0.1 to 25

0.02

"courtin", interoceptive

-50.5 to 50.5

-42 to 26

3.2 to 29

0.01

"courtin", exteroceptive

-50.5 to 50.5

-9 to 9

2.5 to 15.2

0.01

PsychoPy’s Psi method takes only a uniform prior, so the narrower ranges concentrate it, and the staircase spends fewer trials on implausible values. The ranges are also hard bounds: a participant whose threshold lies outside them is estimated at the edge. Sessions run with "legacy" remain directly comparable with published data. The settings each staircase used are written to manifest.json.

parameters = getParameters(..., config=TaskConfig(psiPreset="courtin"))

The staircase is response-coded, so it converges on the point of subjective equality between faster and slower, which is the participant’s bias, not on a target level of accuracy. That is what the task is designed to measure, but it has consequences for any measure scored against the true heart rate; see the metacognition tutorial. Tone sequences exist for 15 to 199 BPM, so a rate outside that range is clamped and the staircase is told the clamped value.

What it saves#

A session writes to data/sub-<participant>/ses-<session>/run-<YYYYMMDD-HHMMSS>/, under resultPath when it is given. Every file name carries the three identifiers.

File

Contents

*_final.txt

One row per trial

*_signal.txt

The pulse recorded in every interoceptive listening window, with the trial number and each sample’s time

*_ppg-<n>.txt

The oximeter recording, saved at every break and at the end, with the trigger codes in Channel_0

*_posterior-Intero.npy, *_posterior-Extero.npy

The staircase posterior after each trial

manifest.json

The settings, the seed and the version, written before the first trial

*_behaviour.txt, *_parameters.pickle, session.log

The rows written after every trial, the parameters at the end, and the session’s log

The main columns in *_final.txt are:

Column

Meaning

Modality

Intero or Extero

listenBPM, responseBPM, Alpha

The reference rate, the rate of the tones, and their difference in ΔBPM

Decision

More or Less

ResponseCorrect

Whether the decision matched the sign of Alpha

Confidence

The rating on the scale shown. ConfidenceUnit holds it on 0 to 1

DecisionProvided, RatingProvided

Whether the answer and the rating were given in time

EstimatedThreshold, EstimatedSlope

The staircase’s own running estimates. EstimatedSlope is the standard deviation of the psychometric function, so a larger value means less precise judgements

listenBPM is the rate over the listening window, 60000 / mean(IBI). Before 1.0 it was the mean of the per-beat rates, about 0.33 BPM higher on real PPG, so sessions run with 1.0 are not directly comparable to published ones on this column; the old value is kept as listenBPM_arithmetic.

The inspecting data tutorial describes every column.

Reading the results#

EstimatedThreshold and EstimatedSlope are a by-product of the staircase. Plot them to check that a session converged. They are not an analysis and must not be used as a dependent variable in a group-level test.

For analysis, model the trial-level responses (Decision) as a function of stimulus intensity (Alpha) and condition (Modality). The Hierarchical Interoception toolbox [Courtin et al., 2026] provides the models and priors. The tutorials explain data inspection, psychometric modelling, and the separate analysis of confidence ratings.

Reliability#

In the validation study, 190 participants ran the task a second time, about seven weeks after the first session. The test-retest correlation was r = 0.51 for the threshold and r = 0.10 for the slope [Legrand et al., 2022].

Where to go next#

  • Measuring cardiac interoception: why the HRD measures beliefs about heart rate, and what you may conclude from it.

  • Tutorials: inspecting a session, fitting psychometric functions, hierarchical models and metacognition.

  • API: every argument of getParameters and every field of TaskConfig.