Tutorial 3: Regressors from ecg data

This example shows how to use niphlem for one subject from one of our particular projects. These data consist of one acquisition of resting-state that has been previously preprocesses using fMRIPrep and physiological acquired through a Siemens multiband protocol. For the sake of simplicity in this example, we are going to concentrate only on cardiac signal reprsented by the ECG.

In addition, we are going to show the maps generated by the niphlem generated regressors using the library nilearn (https://nilearn.github.io/)

[2]:
# IMPORTS
import numpy as np
import pandas as pd

from nilearn import image, plotting
from nilearn.glm.first_level import FirstLevelModel

from niphlem.input_data import (load_cmrr_info, load_cmrr_data)

Let’s start definining our data

[3]:
# Our preprocessed BOLD image
run_img = image.load_img("./data/demo/sub-06_ses-04_task-resting_run-01_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz")
# The repetition time of the scanner
t_r = 1.5
# The frame times, used to define the regressors at the precise times
n_vols = run_img.shape[-1]
frame_times = np.arange(n_vols)*t_r
# Our file with the confounder regressors as a Dataframe. It will be used later to extract the motion parameters
conf_df = pd.read_csv("./data/demo/sub-06_ses-04_task-resting_run-01_desc-confounds_regressors.tsv", sep="\t")

# Our physiological data, the info log file and the ECG log file
info_log = "./data/demo/physio/Physio_20210322_140315_89a222d1-4c24-4caf-a898-f06c6bfd2342_Info.log"
ecg_log = "./data/demo/physio/Physio_20210322_140315_89a222d1-4c24-4caf-a898-f06c6bfd2342_ECG.log"

Load data

The first thing that we need is to load the info file, which will give us the times of the scanner. We can load this kind of the data using the function niphlem.input_data.load_cmrr_info. The only thing that we have to do is to pass the info log file to this function, and it will return the time traces and a dictionary with some meta information which is at the beginning of the file.

[4]:
time_traces, meta_info = load_cmrr_info(info_log)
[5]:
meta_info
[5]:
{'uuid': '89a222d1-4c24-4caf-a898-f06c6bfd2342',
 'scan_date': '20210322_140315',
 'log_version': 'EJA_1',
 'n_slices': 68,
 'n_vols': 353,
 'n_echoes': 1,
 'init_physio': 20241969,
 'end_physio': 20457989,
 'init_scan': 20246200,
 'end_scan': 20457978,
 'repetition_time': 600.0}

Now that we have the time traces, we are going to load our ECG data by passing to the function niphlem.input_data.load_cmrr_data its log file and the meta information dictionary that we’ve just generated. This dictionary is required to be able to synchronize the physiological signal with the scanner times. We also specify the type of signal, as a new entry to an updated dictionary will be added, containing meta information of this ECG signal.

[6]:
ecg_signal, meta_info = load_cmrr_data(ecg_log, info_dict=meta_info, sig_type="cardiac")

Let’s have a look into a portion of this signal, corresponding to 2 s

[7]:
fig, ax =plt.subplots(figsize=(7,5))
plt.plot(ecg_signal[:,0], label="electrode 1")
plt.plot(1000 + ecg_signal[:,1], label="electrode 2")
plt.plot(2000 + ecg_signal[:,2], label="electrode 3")
plt.plot(3000 + ecg_signal[:,3], label="electrode 4")
plt.legend(prop={'size':12})
plt.xlim([0,800])
pass
../_images/tutorials_tutorial3_12_0.png

Let’s also have a look at the updated information dictionary.

[8]:
meta_info
[8]:
{'uuid': '89a222d1-4c24-4caf-a898-f06c6bfd2342',
 'scan_date': '20210322_140315',
 'log_version': 'EJA_1',
 'n_slices': 68,
 'n_vols': 353,
 'n_echoes': 1,
 'init_physio': 20241969,
 'end_physio': 20457989,
 'init_scan': 20246200,
 'end_scan': 20457978,
 'repetition_time': 600.0,
 'cardiac': {'n_channels': 4, 'sample_rate': 1}}

Retroicor regressors

Let’s start the generation of regressors for the Retroicor Model. The class for generating these regressors is found as niphlem.models.RetroicorPhysio.

[9]:
from niphlem.models import RetroicorPhysio

Let’s have a look at the documentation of the class

[10]:
print(RetroicorPhysio.__doc__)

     Physiological regressors using Retroicor.

    Parameters
    ----------
    physio_rate : float
        Sampling rate for the recording in Hz.
        This is needed for filtering to define the nyquist frequency.
    t_r : float
        Repetition time for the scanner (the usual T_R) in secs.
    delta: float
        minimum separation (in physio recording units) between
        events in signal to be considered peaks
    peak_rise: float
        relative height with respect to the 20th tallest events in signal
        to consider events as peak.
    order: int or array-like of shape (n_orders,)
        Fourier expansion for phases. If int, the fourier expansion is
        performed to that order, starting from 1. If an array is provided,
        each element will multiply the phases.
    transform : {"demean", "zscore", "abs"}, optional
        Transform data before filtering. The default is "demean".
    high_pass : float, optional
        High-pass filtering frequency (in Hz). The default is None.
    low_pass : float, optional
        Low-pass filtering frequency (in Hz). The default is None.
    columns : List or array of n_channels elements, "mean" or None, optional
        It describes how to handle input signal channels. If a list, it will
        weight each channel and take the dot product. If "mean",
        the average across the channels. If None, it will consider each
        channel separately. The default is None.
    peak_correct : bool, optional
        Whether to apply an automatic Grubbs' test for peak outlier
        correction. The default is True.
    n_jobs : int, optional
        Number of jobs to consider in parallel. The default is 1.

We have three required arguments (physio_rate, t_r, delta) and a bunch of optional ones. In our case physio_rate is 400 Hz, t_r is 1.5 s and delta=200, the minimu separation between peaks and that corresponds to 200, i.e. 2 peaks per second or at most 120 peaks per minute.

Before the computing any regressors, we are going to take the mean signal across all the for electrodes and then filter the signal between 0.5 and 3.0 Hz.

Finally, retroicor regressors of order 2 will be generated.

[11]:
retro_ecg = RetroicorPhysio(physio_rate=400, # Physiological rate (400 Hz)
                            t_r=1.5, # Scan rate (1.5 s)
                            delta=200,
                            peak_rise=0.5,
                            columns="mean", # mean across the four electrodes
                            low_pass=2.0, # keep frequencies below this
                            high_pass=0.5, # keep frequencies above this
                            order=2 # order 2 of the retroicor expansion
                           )

Once we have defined this object, we generate the regressors using the method compute_regressors, which is comon to all the models

[12]:
retroicor_regressors = retro_ecg.compute_regressors(signal=ecg_signal, time_scan=frame_times)

Let’s see the a part of these regressors

[13]:
fig, ax = plt.subplots(figsize=(7,5))
plt.plot(frame_times, retroicor_regressors[:,0], label="sin1")
plt.plot(frame_times, 3 + retroicor_regressors[:,1], label="cos1")
plt.plot(frame_times, 6 + retroicor_regressors[:,2], label="sin2")
plt.plot(frame_times, 9 + retroicor_regressors[:,3], label="cos2")
plt.xlim([0,100])
plt.xlabel("time (s)", size=25)
plt.legend(ncol=2, prop={'size':12})
plt.tick_params(labelsize=20)
plt.yticks([])
pass
../_images/tutorials_tutorial3_25_0.png

Let’s build now a design matrix that contains these regressors in addition to the motion parameters and an intercept.

[14]:
dm_retroicor = np.column_stack((np.ones(len(frame_times)), # Intercept
                                conf_df.filter(regex="rot|trans").to_numpy(), # Motion parameters
                                retroicor_regressors # ECG regressors
                               ))
dm_retroicor = pd.DataFrame(dm_retroicor,
                            columns=["intercept",
                                     "trans_x","trans_y","trans_z","rot_x","rot_y","rot_z",
                                     "sin1", "cos1", "sin2", "cos2"
                                      ]
                     )
dm_retroicor.index=frame_times
dm_retroicor.head()
[14]:
intercept trans_x trans_y trans_z rot_x rot_y rot_z sin1 cos1 sin2 cos2
0.0 1.0 0.031165 -0.003878 -0.027213 -0.001164 0.000187 0.000000 0.000000 1.000000 0.000000 1.000000
1.5 1.0 0.021195 -0.071983 -0.047930 0.000039 0.000260 0.000000 -0.989701 0.143149 -0.283349 -0.959017
3.0 1.0 0.017855 -0.041552 -0.036992 0.000027 0.000076 -0.000260 0.145010 -0.989430 -0.286955 0.957944
4.5 1.0 0.013670 0.072409 -0.036982 -0.000340 0.000188 -0.000202 0.550055 0.835128 0.918733 0.394879
6.0 1.0 0.009882 -0.046013 -0.040077 0.000352 0.000367 -0.000228 -0.999079 0.042905 -0.085731 -0.996318
[15]:
fig, ax = plt.subplots()
ax.pcolormesh(dm_retroicor)
ax.set_xticks(0.5 + np.arange(dm_retroicor.shape[1]))
ax.set_xticklabels(dm_retroicor.columns, rotation=45, size=15)
pass
../_images/tutorials_tutorial3_28_0.png

Now fit this design matrix to our BOLD image

[16]:
first_level = FirstLevelModel(t_r=t_r, drift_model=None, signal_scaling=False, smoothing_fwhm=6)
first_level.fit(run_imgs=run_img, design_matrices=dm_retroicor)
retroicor_map = first_level.compute_contrast("sin1+cos1+sin2+cos2", stat_type="F", output_type="z_score")

And plot this map in a glass brain fashion

[17]:
plotting.plot_glass_brain(retroicor_map, colorbar=True, threshold=1.96)
[17]:
<nilearn.plotting.displays.OrthoProjector at 0x7fd644d7ea00>
../_images/tutorials_tutorial3_32_1.png

Variation model’s regressors

Let’s continue now with the generation of regressors using the Variation Model. The class for generating these regressors is found as niphlem.models.HVPhysio.

[18]:
from niphlem.models import HVPhysio

Let’s have a look at the documentation of the class

[19]:
print(HVPhysio.__doc__)

     Physiological regressors for variations in heart rate.

    Parameters
    ----------
    physio_rate : float
        Sampling rate for the recording in Hz.
        This is needed for filtering to define the nyquist frequency.
    t_r : float
        Repetition time for the scanner (the usual T_R) in secs.
    delta: float
        minimum separation (in physio recording units) between
        events in signal to be considered peaks
    peak_rise: float
        relative height with respect to the 20th tallest events in signal
        to consider events as peak.
    time_window : float
        Time window (in secs) around the T_R from which computing variations
        (time differences between signal events ). The default is 6 secs.
    transform : {"demean", "zscore", "abs"}, optional
        Transform data before filtering. The default is "demean".
    high_pass : float, optional
        High-pass filtering frequency (in Hz). The default is None.
    low_pass : float, optional
        Low-pass filtering frequency (in Hz).  The default is None.
    columns : List or array of n_channels elements, "mean" or None, optional
        It describes how to handle input signal channels. If a list, it will
        weight each channel and take the dot product. If "mean",
        the average across the channels. If None, it will consider each
        channel separately. The default is None.
    peak_correct : bool, optional
        Whether to apply an automatic Grubbs' test for peak outlier
        correction. The default is True.
    n_jobs : int, optional
        Number of jobs to consider in parallel. The default is 1.

[20]:
variaton_ecg = HVPhysio(physio_rate=400, t_r=t_r, delta=200, columns="mean", time_window=4.5,
                        low_pass=2.0, high_pass=0.5, peak_rise=0.5
                       )
[21]:
variation_regressors = variaton_ecg.compute_regressors(signal=ecg_signal, time_scan=frame_times)
[22]:
fig, ax = plt.subplots(figsize=(7,5))
plt.plot(frame_times, variation_regressors[:,0], label="ecg_var")
plt.legend()
plt.xlim([0,100])
plt.xlabel("time (s)", size=25)
plt.legend(prop={'size':12})
plt.tick_params(labelsize=20)
pass
../_images/tutorials_tutorial3_40_0.png
[23]:
dm_variation = np.column_stack((np.ones(len(frame_times)), # Intercept
                                conf_df.filter(regex="rot|trans").to_numpy(), # Motion parameters
                                variation_regressors # ECG regressors
                         ))

dm_variation = pd.DataFrame(dm_variation,
                            columns=["intercept",
                                     "trans_x","trans_y","trans_z","rot_x","rot_y","rot_z",
                                     "ecg_var"
                                    ]
                     )
dm_variation.index=frame_times
dm_variation.head()
[23]:
intercept trans_x trans_y trans_z rot_x rot_y rot_z ecg_var
0.0 1.0 0.031165 -0.003878 -0.027213 -0.001164 0.000187 0.000000 -0.000677
1.5 1.0 0.021195 -0.071983 -0.047930 0.000039 0.000260 0.000000 0.661642
3.0 1.0 0.017855 -0.041552 -0.036992 0.000027 0.000076 -0.000260 1.652585
4.5 1.0 0.013670 0.072409 -0.036982 -0.000340 0.000188 -0.000202 1.194417
6.0 1.0 0.009882 -0.046013 -0.040077 0.000352 0.000367 -0.000228 -1.499796
[24]:
fig, ax = plt.subplots()
ax.pcolormesh(dm_variation)
ax.set_xticks(0.5 + np.arange(dm_variation.shape[1]))
ax.set_xticklabels(dm_variation.columns, rotation=45, size=15)
pass
../_images/tutorials_tutorial3_42_0.png

Again, let’s fit this design matrix to our BOLD data and show the pattern obtained

[25]:
first_level = FirstLevelModel(t_r=t_r, drift_model=None, signal_scaling=False, smoothing_fwhm=6)
first_level.fit(run_imgs=run_img, design_matrices=dm_variation)
variation_map = first_level.compute_contrast("ecg_var", stat_type="F", output_type="z_score")
[26]:
plotting.plot_glass_brain(variation_map, colorbar=True, threshold=1.96)
[26]:
<nilearn.plotting.displays.OrthoProjector at 0x7fd640c25730>
../_images/tutorials_tutorial3_45_1.png