OSCR

Task-Evoked Functional Activation and Coupling With CSF Flow Detected in the Human Brain With Ultrashort Echo Time fMRI at 7 T.

Code ↔ Paper

2 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 2 matches
  1. [1] § Methods › Imaging Data Processing › Physiological Data Processing ↔ CreatePhysioPredictors_BIDS.py, lines 1–38 · score 0.84 · physiological signals, respiration volume, breathing rate, respiration signals, heart rate, imported
  2. [2] § Methods › Data Acquisition ↔ CreatePhysioPredictors_BIDS.py, lines 1–38 · score 0.71 · physiological signals, functional scanning, breathing rate, heart rate, belt, protocol

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 1,857 lines · 90 KB · MIT · 2 matches

  1. # -*- coding: utf-8 -*-
  2. """
  3. CREATE PHYSIOLOGICAL NOISE PREDICTORS
  4. This script can be used to:
  5. 1. Import *.json and *.tsv.gz physiological recordings that follow BIDS standard as well as the corresponding FMR (generated with BV 21.4 or newer)
  6. 2. Preprocess the cardiac signal derived from a PPU (peripheral pulse unit) and the respiratory signal derived from a breathing belt
  7. 3. Perform peak detection on the preprocessed physiological signals and extract different physiological variables (e.g. heart rate, breathing rate, respiratory volume time, etc.)
  8. 4. Extract volume- (or slice-) based fMRI triggers from the *.json and *.tsv.gz physiological recording files
  9. 5. Extract fMRI aquisition parameters from the FMR and create volume-based physiological predictors
  10. 6. Save SDM files with the filtered physiological signals
  11. 7. Save RETROICOR SDM predictors based on the method proposed by Glover:
  12. Glover, G. H., Li, T.-Q., & Ress, D. (2000). Image-based method for retrospective correction of physiological motion effects in fMRI: RETROICOR.
  13. Magnetic Resonance in Medicine, 44(1), 162-167.
  14. 8. Create physiological noise predictors saved in single sdm files, including:
  15. - (shifted) heart rate (HR),
  16. - HR convolved with the cardiac response function (CRF),
  17. - breathing rate (BR),
  18. - respiratory flow (RF),
  19. - the (shifted) envelope of the respiratory signal (ENV),
  20. - the (shifted) respiration variation (RV),
  21. - (shifted) respiration volume per time (RVT),
  22. - RV convolved with the respiratory response function (RRF)
  23. - RVT convolved with the RRF
  24. 9. if specified by the user, compute Pearson correlations of the task design (specified in a SDM file) with the derived physiological regressors
  25. 10. if specified by the user, plot stimulation protocol (PRT) together with computed heart rate (HR) and/or breathing rate (BR) and compute the mean HR and BR per event in the PRT
  26. Resulting files will be saved in a directory called 'PhysioOut', which is created in the same folder as the specified FMR file:
  27. 11. save PNG figures showing the cardiac and respiratory signal, the identified peaks, the functional scan time and some derived physiological noise regressors
  28. 12. save input, output and processing parameters in JSON files (*_InputParameters.json, *_OutputParameters.json) in the 'PhysioOut' folder
  29. 13. save the resulting physiological noise regressors as SDM files and as *_PhysiologicalNoiseRegressors.tsv
  30. 14. save the task x noise correlation matrix with the corresponding p-values as *_CorrelationMatrixNoiseTaskRegressors.tsv
  31. 15. save a PNG file of the plotted stimulation protocol together with HR and/or BR
  32. """
  33. __author__ = "Judith Eck"
  34. __version__ = "0.1.0"
  35. __date__ = "29-11-2022"
  36. __name__ = "CreatePhysioPredictors_BIDS.py"
  37. # =============================================================================
  38. # Import required packages
  39. # =============================================================================
  40. import numpy as np
  41. import scipy.stats as stats
  42. import scipy.signal as signal
  43. import scipy.interpolate as interpolate
  44. from scipy.ndimage.filters import uniform_filter1d
  45. import pandas as pd
  46. import copy
  47. # needed for warning messages outside of BV
  48. from PyQt5.QtWidgets import QMessageBox, QFileDialog, QApplication
  49. # needed for warning message in BV
  50. # from PythonQt.QtGui import QmessageBox
  51. import matplotlib.pyplot as plt
  52. import matplotlib as mpl
  53. from matplotlib.patches import Rectangle, Patch
  54. import seaborn as sns
  55. import json
  56. import os.path
  57. from datetime import datetime
  58. import sys
  59. from brainvoyagertools import sdm, prt
  60. # this sets the Physio, FMR, SDM, PRT file names, if this script is called
  61. # via the batch processing script
  62. if len(sys.argv) > 2:
  63. physio_json_name = sys.argv[1]
  64. fmr_file_name = sys.argv[2]
  65. sdm_task_file_name = sys.argv[3]
  66. prt_task_file_name = sys.argv[4]
  67. plotdisp = sys.argv[5]
  68. # change default plotting properties
  69. mpl.rcParams['figure.figsize'] = (10, 6) # set figure size
  70. mpl.rcParams['figure.dpi'] = 150
  71. mpl.rcParams['lines.linewidth'] = 0.5
  72. mpl.rcParams['legend.fontsize'] = 'small'
  73. mpl.rcParams['axes.labelsize'] = 'small'
  74. mpl.rcParams['xtick.labelsize'] = 'small'
  75. mpl.rcParams['ytick.labelsize'] = 'small'
  76. mpl.rcParams['axes.titlesize'] = 'medium'
  77. mpl.rcParams['legend.frameon'] = False
  78. mpl.rcParams['legend.framealpha'] = 1
  79. mpl.rcParams['legend.fontsize'] = 'small'
  80. mpl.rcParams['lines.markersize'] = 2
  81. mpl.rcParams['legend.loc'] = 'upper center'
  82. app = QApplication(sys.argv)
  83. # =============================================================================
  84. # User-specified parameters
  85. # =============================================================================
  86. # specifies if cardiac respiratory raw data are saved together in a
  87. # single JSON/TSV file
  88. # if only one of these measures exist please set this to TRUE
  89. physio_1file = True
  90. # cutoff frequencies in Hz for zero-phase second-order bandpass butterworth
  91. cardiac_low = 0.5
  92. cardiac_high = 8
  93. # Definition of RETROICOR model, effects of cardiac and respiratory cycles
  94. # estimated as a linear combination of sinusoidal signals, default order of
  95. # correction terms (3c4r1i) based on:
  96. # Harvey, A. K., Pattinson, K. T. S., Brooks, J. C. W., Mayhew, S. D., Jenkinson, M., & Wise, R. G. (2008).
  97. # Brainstem functional magnetic resonance imaging: Disentangling signal from physiological noise.
  98. # Journal of Magnetic Resonance Imaging, 28(6), 1337–1344. https://doi.org/10.1002/jmri.21623
  99. # for alternative numbers (2c2r0i) see:
  100. # # Power, J. D., Plitt, M., Laumann, T. O., & Martin, A. (2017). Sources and implications of whole-brain fMRI
  101. # signals in humans. NeuroImage, 146, 609-625. https://doi.org/10.1016/J.NEUROIMAGE.2016.09.038
  102. order_cardiac = 2 # number of cardiac harmonics (based on brainstem imaging), change order if needed
  103. order_resp = 2 # number of respiratory harmonics (based on brainstem imaging), change order if needed
  104. order_cardresp = 0 # number of multiplicative harmonics (based on brainstem imaging)
  105. # Outlier detection in identified heartbeats based on percentage rule
  106. # Only used to let the user decide between pulse peak detection approaches
  107. # e.g. Forcolin, F., Buendia, R., Candefjord, S., Karlsson, J., Sjöqvist, B. A., & Anund, A. (2018). Comparison of
  108. # outlier heartbeat identification and spectral transformation strategies for deriving heart rate variability indices
  109. # for drivers at different stages of sleepiness. Https://Doi.Org/10.1080/15389588.2017.1393073, 19, S112–S119. https://doi.org/10.1080/15389588.2017.1393073
  110. # if outliers are detected within the heartbeats, the user can decide to try an alternative peak detection for the cardiac signal
  111. # inter-beat-intervals (IBIs) that differ by more than 30 percent (outlier_cardiac_threshold) from the mean of
  112. # their 5 neighboring IBIs (nIBIs) are considered outliers
  113. outlier_cardiac_threshold = 30 # specified in percentage
  114. nIBIs = 5 # number of neighboring IBIs taken into account
  115. # Minimum interval between heartbeats in seconds, normal values vary
  116. # from 0.40 to 0.90 seconds (values depend on age, gender, health, level of training, physical and emotional state)
  117. # this value is used for the alternative peak detection method only
  118. min_hbi = 0.60
  119. # Values for outlier removal in HR, based on Kassinopoulos, M., & Mitsis, G. D., 2019.
  120. # used to remove outliers in the RESULTING Heartrate (HR) signal if there are sudden changes in HR due to noise.
  121. # It needs to be visually inspected whether these sudden changes are indeed noise or true sudden changes in the PPU signal
  122. hr_filloutliers_window = 25 # given in seconds
  123. # outliers are defined as elements more than "hr_filloutliers_threshold"
  124. # median absolute deviations (MAD) from the median
  125. hr_filloutliers_threshold = 10 # normal range between 3-20
  126. # Shifting of heartate (HR) regressor based on Shmueli et al., 2007
  127. hr_shifts = np.arange(0, 25, 2) # 0:2:24 seconds, or -12:6:12 seconds (when referring to Biancardi et al., 2009)
  128. # if no temporal shifts are required use np.arange(0,1)
  129. # Values for outlier removal in respiratory signal, based on Power et al., 2020
  130. # window and threshold to eliminate spike artifacts
  131. resp_filloutliers_window = 0.25 # given in seconds
  132. # outliers are defined as elements more than "resp_filloutliers_threshold"
  133. # median absolute deviations (MAD) from the median
  134. resp_filloutliers_threshold = 3 # default is 3
  135. # Shifting of (respiration volume time (RVT) regressor based on Jo et al., 2010
  136. rvt_shifts = np.arange(0, 21, 5) # 0:5:20 seconds, or -24:6:18 seconds (when referring to Biancardi et al., 2009)
  137. # if no temporal shifts required use np.arange(0,1)
  138. # Shifting of respiration variation (RV) regressor based on:
  139. # Power, J. D., Plitt, M., Laumann, T. O., & Martin, A. (2017). Sources and implications of whole-brain fMRI
  140. # signals in humans. NeuroImage, 146, 609-625. https://doi.org/10.1016/J.NEUROIMAGE.2016.09.038
  141. rv_shifts = np.arange(-7, 8, 7) # -7:7:7 seconds
  142. # if no temporal shifts required use np.arange(0,1)
  143. # Shifting of the respiratory envelope (ENV) regressor, similar to RV:
  144. env_shifts = np.arange(-7, 8, 7) # -7:7:7 seconds
  145. # if no temporal shifts required use np.arange(0,1)
  146. # Minimum correlation value to be shown in heatmap of task-noise correlations
  147. min_pvalue = 0.05
  148. # =============================================================================
  149. # Define some simple warning messages for the user
  150. # =============================================================================
  151. def showdialog_info(string_physio):
  152. '''
  153. messagebox used to inform user about missing physiological measures for the available functional run
  154. string_physio: unavailable physiological measure, e.g. "cardiac" or "respiratory"
  155. '''
  156. msg = QMessageBox()
  157. msg.setIcon(QMessageBox.Information)
  158. msg.setText("No " + string_physio + " data available for this functional run")
  159. msg.setWindowTitle("Information about unavailable physiological measure")
  160. msg.setStandardButtons(QMessageBox.Ok)
  161. msg.exec()
  162. def showdialog_peakdetect():
  163. '''
  164. messagebox used to inform user about potential problems with the applied pulse peak detection approach
  165. '''
  166. msg = QMessageBox()
  167. msg.setIcon(QMessageBox.Warning)
  168. msg.setText("There have been outliers detected in the calculated heart rate. \n\nPlease check the identified pulse peaks in the cardiac signal of the next figure to rule out a sub-optimal peak-detection.")
  169. msg.setWindowTitle("Check identified pulse peaks")
  170. msg.setStandardButtons(QMessageBox.Ok)
  171. msg.exec()
  172. def userinput_peakdetect():
  173. '''
  174. messagebox to ask user for a potential switch to an alternative peak detection method
  175. '''
  176. msg = QMessageBox()
  177. msg.setIcon(QMessageBox.Question)
  178. msg.setText("Would you like to use an alternative peak detection approach?")
  179. msg.setWindowTitle("Change of Peak Detection Approach")
  180. msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
  181. retval = msg.exec()
  182. if retval == QMessageBox.Yes:
  183. new = True
  184. else:
  185. new = False
  186. return(new)
  187. def showdialog_triggererr():
  188. '''
  189. show an error message to the user if not for every functional volume in the fmr
  190. a trigger has been saved
  191. '''
  192. msg = QMessageBox()
  193. msg.setIcon(QMessageBox.Critical)
  194. msg.setText("There is not for every recorded volume a trigger saved in the Physio TSV file!")
  195. msg.setWindowTitle("Recorded Scan Trigger Error")
  196. msg.setStandardButtons(QMessageBox.Ok)
  197. msg.exec()
  198. # =============================================================================
  199. # Define some simple functions for the script
  200. # =============================================================================
  201. def shift_preds(a, b, hz):
  202. '''
  203. Shifting predictors in steps of seconds
  204. Parameters:
  205. a: arrary_like
  206. Array containing the predictor to be shifted
  207. b: arry_like
  208. shifts in the form of an array of int32 containing the temporal shifts in seconds
  209. e.g. [-5 0 5 10 15]
  210. sampling: int
  211. sampling rate of the signal in Hz
  212. '''
  213. a_shiftedfuncs = np.zeros((len(b), len(a)))
  214. for i in range(len(b)):
  215. shift = int(np.round(abs(b[i]*hz)))
  216. if b[i] < 0:
  217. a_shiftedfuncs[i, 0:-shift] = a[shift::]
  218. a_shiftedfuncs[i, -shift::] = np.mean(a)
  219. elif b[i] > 0:
  220. a_shiftedfuncs[i, 0:shift] = np.mean(a)
  221. a_shiftedfuncs[i, shift::] = a[0:-shift]
  222. else:
  223. a_shiftedfuncs[i, :] = a
  224. a_final = np.transpose(a_shiftedfuncs)
  225. return(a_final)
  226. # copied from https://stackoverflow.com/a/46940319
  227. # define hampel filter
  228. def hampel(vals_orig, k=7, t0=3):
  229. '''
  230. vals: pandas series of values from which to remove outliers
  231. k: size of window (including the sample; 7 is equal to 3 on
  232. either side of value)
  233. '''
  234. # Make copy so original not edited
  235. vals = vals_orig.copy()
  236. # Hampel Filter
  237. L = 1.4826
  238. rolling_median = vals.rolling(k).median()
  239. difference = np.abs(rolling_median-vals)
  240. median_abs_deviation = difference.rolling(k).median()
  241. threshold = t0 * L * median_abs_deviation
  242. outlier_idx = difference > threshold
  243. vals[outlier_idx] = np.nan
  244. return(vals)
  245. # =============================================================================
  246. # Create Dictionary for Input and Output Parameters
  247. # =============================================================================
  248. # Input Parameters
  249. physio_input_parameters = {
  250. "CurrentTime": datetime.now().strftime("%d-%m-%Y, %H:%M:%S"),
  251. "Scriptname": __name__,
  252. "Scriptversion": __version__,
  253. "Scriptdate": __date__,
  254. "ScriptParameters": {
  255. "CardiacBandpassFilterCutOff": str(cardiac_low) + "-" + str(cardiac_high),
  256. "RetroicorCardiacTerm": order_cardiac,
  257. "RetroicorRespiratoryTerm": order_resp,
  258. "RetroicorInteractionTerm": order_cardresp,
  259. "HeartRateOutlierWindowSec": hr_filloutliers_window,
  260. "HeartRateOutlierThresholdMAD": hr_filloutliers_threshold,
  261. "PulsePeakDetectionBased": [],
  262. "MinimumHeartBeatIntervalSec": min_hbi,
  263. "HeartBeatInterval%OutlierThreshold": outlier_cardiac_threshold,
  264. "HeartBeatInterval%OutlierWindowSize": nIBIs,
  265. "ShiftsOfHeartRateRegressorSec": list(hr_shifts),
  266. "RespiratoryOutlierWindowSec": resp_filloutliers_window,
  267. "RespiratoryOutlierThresholdMAD": resp_filloutliers_threshold,
  268. "ShiftsOfRVTRegressorSec": list(rvt_shifts),
  269. "ShiftsOfRVRegressorSec": list(rv_shifts),
  270. "ShiftsOfENVRegressorSec": list(env_shifts),
  271. },
  272. "PhysioJsonFile": {
  273. "Name": [],
  274. "SamplingFrequency": [],
  275. "StartTime": [],
  276. "PhysioColumnHeaders": [],
  277. "PhysioData": [],
  278. "NumberOfStartScanTriggersSaved": [],
  279. "IndicesScanTriggers": [],
  280. "IndicesVolumeTriggers": [],
  281. "PhysioTimeSec": []
  282. }
  283. }
  284. # Dictionary for Output Parameters
  285. physio_output_parameters = {
  286. "CurrentTime": datetime.now().strftime("%d-%m-%Y, %H:%M:%S"),
  287. "Scriptname": __name__,
  288. "Scriptversion": __version__,
  289. "Scriptdate": __date__,
  290. "NoiseRegressors": []}
  291. # =============================================================================
  292. # Read in all neccessary files
  293. # =============================================================================
  294. # LOAD PHYSIO DATA FROM BIDS-COMPATIBLE FILES
  295. if physio_1file:
  296. physio_loop = 1
  297. else:
  298. physio_loop = 2
  299. for pl in range(physio_loop):
  300. # read json
  301. # physio_json_name = brainvoyager.choose_file(
  302. # 'Select the JSON File(s) of the Physiorecordings of a Single Run',
  303. # '*.json'
  304. # )
  305. if not 'physio_json_name' in locals() or pl == 1:
  306. physio_json_name, _ = QFileDialog.getOpenFileName(None, 'Select Physio JSON File', os.getcwd(), 'JSON Files (*.json)')
  307. physio_input_parameters['PhysioJsonFile']['Name'].append(physio_json_name)
  308. with open(physio_json_name) as _json_file:
  309. _temp = json.load(_json_file)
  310. physio_input_parameters['PhysioJsonFile']['SamplingFrequency'].append(_temp['SamplingFrequency'])
  311. physio_input_parameters['PhysioJsonFile']['StartTime'].append(_temp['StartTime'])
  312. physio_input_parameters['PhysioJsonFile']['PhysioColumnHeaders'].append (_temp['Columns'])
  313. del [_temp, _json_file]
  314. # Save the index of the pulse and respiratory data within the dictionary
  315. test_card = [i for i, s in enumerate(physio_input_parameters['PhysioJsonFile']['PhysioColumnHeaders'][pl]) if 'card' in s.lower()]
  316. if np.size(test_card):
  317. pulse_col_dict = [pl, test_card[0]]
  318. test_resp = [i for i, s in enumerate(physio_input_parameters['PhysioJsonFile']['PhysioColumnHeaders'][pl]) if 'resp' in s.lower()]
  319. if np.size(test_resp):
  320. resp_col_dict = [pl, test_resp[0]]
  321. del (test_card, test_resp)
  322. # read tsv
  323. physio_tsv_name = physio_json_name.rsplit('.', 1)[0] + '.tsv.gz'
  324. temp_data = np.genfromtxt(fname=physio_tsv_name, delimiter='\t')
  325. physio_input_parameters['PhysioJsonFile']['PhysioData'].append(temp_data)
  326. _temp = np.diff(np.append(0, temp_data[:, -1]), n=1)
  327. physio_input_parameters['PhysioJsonFile']['NumberOfStartScanTriggersSaved'].append(int((_temp == 1).sum()))
  328. # find MRI trigger locations in Physio Data
  329. physio_input_parameters['PhysioJsonFile']["IndicesScanTriggers"].append(np.squeeze(np.nonzero(temp_data[:, -1])))
  330. # sometimes (e.g for the CMRR Sequence) there are 1s written for the entire
  331. # time a slice acquisition is on and not just for the start of a slice/volume,
  332. # by finding all indices in the last physio_data column where the data
  333. # trace changes from 0 to 1, it is possible identify just the beginning of the
  334. # slice/volume acquisition
  335. physio_input_parameters['PhysioJsonFile']["IndicesVolumeTriggers"].append(np.squeeze(np.nonzero(
  336. np.diff(
  337. np.insert(temp_data[:, -1], 0, 0)
  338. )
  339. == 1)))
  340. del _temp
  341. # save time stamps of the physio data in seconds
  342. # with 0 being the start of the functional run
  343. physio_input_parameters['PhysioJsonFile']["PhysioTimeSec"].append(np.linspace(
  344. list(physio_input_parameters['PhysioJsonFile']['StartTime'])[-1],
  345. list(physio_input_parameters['PhysioJsonFile']['StartTime'])[-1] + (1/list(physio_input_parameters['PhysioJsonFile']['SamplingFrequency'])[-1]) * (temp_data.shape[0]-1),
  346. temp_data.shape[0]
  347. ))
  348. del temp_data
  349. # LOAD FMR TO EXTRACT AND COMPARE TIMING INFORMATION
  350. # fmr_file_name = brainvoyager.choose_file(
  351. # 'Select the correspding FMR file', '*.fmr'
  352. # )
  353. if not 'fmr_file_name' in locals():
  354. fmr_file_name, _ = QFileDialog.getOpenFileName(None, 'Select the Corresponding FMR File', physio_json_name.rsplit('/', 1)[0], 'FMR Files (*.fmr)')
  355. with open(fmr_file_name) as _file:
  356. _fmr = _file.readlines()
  357. _fmr = [line.strip().replace(' ', '') for line in _fmr]
  358. fmr_name = ''.join([line for line in _fmr if "Prefix:" in line]).split(':')[1]
  359. fmr_name = fmr_name.replace('"', '')
  360. fmr_time_repeat = float(
  361. ''.join([line for line in _fmr if 'TR:' in line]).split(':')[1]
  362. )
  363. fmr_no_volumes = int(
  364. ''.join([line for line in _fmr if 'NrOfVolumes:' in line]).split(':')[1]
  365. )
  366. fmr_no_slices = int(
  367. ''.join([line for line in _fmr if 'NrOfSlices:' in line]).split(':')[1]
  368. )
  369. fmr_sli_table_size = int(
  370. ''.join([line for line in _fmr if 'SliceTimingTableSize:' in line]
  371. ).split(':')[1])
  372. _index = [n for n, line in enumerate(_fmr) if 'SliceTimingTableSize:' in line]
  373. if int(_fmr[_index[0]].split(':')[-1]) == 0:
  374. fmr_slice_times = np.zeros(fmr_no_slices)
  375. _temp = []
  376. else:
  377. _temp = np.array(_fmr[_index[0]+1: _index[0]+1+fmr_no_slices])
  378. fmr_slice_table = _temp.astype(float)
  379. fmr_slice_times = np.unique(fmr_slice_table)
  380. del [_temp, _index, _file, _fmr]
  381. physio_input_parameters["ScanningParameters"] = {
  382. 'FmrName': fmr_file_name, 'NoSlices': fmr_no_slices,
  383. 'NoVolumes': fmr_no_volumes, 'RepetitionTimeSec': fmr_time_repeat/1000,
  384. 'MBfactor': int(fmr_no_slices/len(fmr_slice_times)),
  385. 'UniqueSliceAcquisitionTimesSec': list(fmr_slice_times/1000)
  386. }
  387. # LAOD TASK DESIGN MATRIX (SDM)
  388. # TASK SDM: in case you would like to correlate the resulting physiological
  389. # measures with your task predictors, select the task SDM file
  390. # sdm_task_file_name = brainvoyager.choose_file(
  391. # "Select the SDM file of your task design if you would like to
  392. # correlate your predictors with the physiological measures,
  393. # if not click Cancel", "*.sdm"
  394. # )
  395. if not 'sdm_task_file_name' in locals():
  396. sdm_task_file_name, _ = QFileDialog.getOpenFileName(None, 'Select the Corresponding Task Design', physio_json_name.rsplit('/', 1)[0], 'SDM Files (*.sdm)')
  397. if sdm_task_file_name.endswith('.sdm'):
  398. with open(sdm_task_file_name) as _file:
  399. lines = _file.readlines()
  400. sdm_task_no_predictors = int(
  401. ''.join([line for line in lines if "NrOfPredictors:" in line]
  402. ).split(":")[1])
  403. sdm_task_no_volumes = int(
  404. ''.join([line for line in lines if "NrOfDataPoints:" in line]
  405. ).split(":")[1])
  406. sdm_task_incl_constant = bool(
  407. ''.join([line for line in lines if "IncludesConstant:" in line]
  408. ).split(":")[1])
  409. sdm_task_first_confound = int(
  410. ''.join([line for line in lines if "FirstConfoundPredictor:" in line]
  411. ).split(":")[1])
  412. for counter, line in enumerate(lines):
  413. if line.startswith('FirstConfoundPredictor:'):
  414. break
  415. sdm_task_colours = lines[counter+2].strip().split(' ')
  416. sdm_task_name = lines[counter+3].strip().strip('"').split('" "')
  417. t_data = []
  418. colwidth = int(len(lines[counter + 4]) / sdm_task_no_predictors) # taking the length of the first data line and divide by the number of predictors to compute the width of each predictor column
  419. for line in lines[counter + 4:]:
  420. t_data.append([float(line[i:i+colwidth]) for i in range(0, len(line), colwidth)if line[i]!="\n"])
  421. sdm_task_data = np.array(t_data)
  422. del (counter, t_data)
  423. physio_input_parameters['TaskDesign'] = {
  424. 'SdmName': sdm_task_file_name, 'NumberPredictors': sdm_task_no_predictors,
  425. 'NumberVolumes': sdm_task_no_volumes, 'PredictorNames': sdm_task_name}
  426. # LOAD Stimulation Protocol (PRT)
  427. # PRT: Extract the mean heart- and respiratory rate per
  428. # condition and per event in the physio structure
  429. # prt_task_file_name = brainvoyager.choose_file(
  430. # "Select the stimulation protocol if you would like to
  431. # compute the mean heart- and/or respiratory rate per condition,
  432. # if not click Cancel", "*.prt"
  433. # )
  434. if not 'prt_task_file_name' in locals():
  435. prt_task_file_name, _ = QFileDialog.getOpenFileName(None, 'Select the Corresponding Stimulation Protocol', physio_json_name.rsplit('/', 1)[0], 'PRT Files (*.prt)')
  436. if prt_task_file_name.endswith('.prt'):
  437. # load PRT
  438. protocol = prt.StimulationProtocol(load=prt_task_file_name)
  439. # save some important parameters to the input dictionary
  440. physio_input_parameters['StimulationProtocol'] = {
  441. 'PrtName': prt_task_file_name, 'NumberConditions': len(protocol.conditions),
  442. 'ResolutionOfTime': protocol.time_units, 'ConditionNames': list(set(protocol.event_names))}
  443. # save also some of these parameters for reference to the output dict
  444. physio_output_parameters['StimulationProtocol'] = {
  445. "PrtName": prt_task_file_name,
  446. "ConditionNames": [protocol.conditions[cond].name for cond in range(len(protocol.conditions))]}
  447. # if PRT resolution = Volumes, convert condition onsets and offsets to
  448. # extract the heart- and/or breathing rates in these intervals
  449. if protocol.time_units == "Volumes":
  450. protocol.convert_to_msec(fmr_time_repeat)
  451. # =============================================================================
  452. # Define Output Variables
  453. # =============================================================================
  454. # create output Physio folder in subfolder of FMR (if it does not exist)
  455. physio_out_path = fmr_file_name.rsplit("/", 1)[0] + '/PhysioOut'
  456. if not os.path.isdir(physio_out_path):
  457. os.mkdir(physio_out_path)
  458. # define a counter to keep track of the number of noise models created
  459. noise_model_no = 0
  460. physio_regressors_matrix = np.zeros((fmr_no_volumes, 1))
  461. physio_regressors_names = []
  462. # =============================================================================
  463. # Start the Processing
  464. # =============================================================================
  465. # =============================================================================
  466. # Pulse Data
  467. # =============================================================================
  468. physio_output_parameters['CardiacOutput'] = {}
  469. # if there is no pulse data provide feedback to the user
  470. if 'pulse_col_dict' not in locals():
  471. physio_output_parameters['CardiacOutput'].update({"Error": "No cardiac data provided for this functional run"})
  472. if len(sys.argv) == 1:
  473. showdialog_info('cardiac')
  474. else:
  475. print("No cardiac data provided for this functional run\n")
  476. else:
  477. # reorganize data for easier use
  478. physio_hz = physio_input_parameters['PhysioJsonFile']['SamplingFrequency'][pulse_col_dict[0]]
  479. physio_nyq = physio_hz/2
  480. physio_data = physio_input_parameters['PhysioJsonFile']['PhysioData'][pulse_col_dict[0]]
  481. physio_triggers_sum = physio_input_parameters['PhysioJsonFile']['NumberOfStartScanTriggersSaved'][pulse_col_dict[0]]
  482. physio_triggers_ind = physio_input_parameters['PhysioJsonFile']['IndicesScanTriggers'][pulse_col_dict[0]]
  483. physio_triggers_startslice_ind = physio_input_parameters['PhysioJsonFile']['IndicesVolumeTriggers'][pulse_col_dict[0]]
  484. physio_time = physio_input_parameters['PhysioJsonFile']['PhysioTimeSec'][pulse_col_dict[0]]
  485. physio_hz_10 = 10
  486. physio_ts_10 = 1/physio_hz_10 # sampling steps for 10Hz signal
  487. # time vector for 10Hz
  488. physio_time_10 = np.arange(physio_time[0], physio_time[-1], physio_ts_10)
  489. # extract z-transformed cardiac signal
  490. cardiac = stats.zscore(np.squeeze(physio_data[:, pulse_col_dict[1]]))
  491. # check whether there are any missing values in the cardiac signal
  492. assert ~np.sum(np.isnan(cardiac)), 'Nan values in the cardiac signal'
  493. # __________________________________________________________________________
  494. # 4.1. SYSTOLIC PEAK DETECTION based on:
  495. # Elgendi M, Norton I, Brearley M, Abbott D, Schuurmans D. Systolic
  496. # peak detection in acceleration photoplethysmograms measured from
  497. # emergency responders in tropical conditions. PLoS ONE. 2013;8(10):76585.
  498. # doi: 10.1371/journal.pone.0076585
  499. # Three-Stage method to get indices of systolic peaks:
  500. # 1. Preprocessing (bandpass filtering and squaring)
  501. # 2. Feature extraction (generating potential blocks using 2 moving averages)
  502. # 3. Classification (thresholding)
  503. physio_input_parameters["ScriptParameters"]["PulsePeakDetectionBased"] = "Elgendi M, Norton I, Brearley M, Abbott D, Schuurmans D. Systolic peak detection in acceleration photoplethysmograms measured from emergency responders in tropical conditions. PLoS ONE. 2013;8(10):76585."
  504. # 1. PREPROCESSING: zero-phase second-order Butterworth filter
  505. # removing baseline wander and high frequencies not
  506. # contributing to systolic peaks
  507. # order = 2, normalized cut-off frequency between 0 & 1 (1 = nyquist freq)
  508. b, a = signal.butter(
  509. 2, [cardiac_low/physio_nyq, cardiac_high/physio_nyq], btype='band')
  510. # changed method from pad (in Matlab) to gust to improve the filtering at the end of the signal
  511. cardiac_filt = signal.filtfilt(b, a, cardiac, method='gust') # method='pad' is also possible
  512. del b, a
  513. cardiac_filt_zscore = stats.zscore(cardiac_filt)
  514. cardiac_filt[cardiac_filt < 0] = 0 # clip to ouput signal > 0
  515. # squaring signal emphasizing large differences from systolic wave and
  516. # suppressing small differences from diastolic wave and noise
  517. cardiac_filt = cardiac_filt**2
  518. # 2. FEATURE EXTRACTION: Blocks of interest are generated using
  519. # two moving averages marking systolic and heartbeat areas
  520. w1 = 0.111 # sec (window size of one systolic peak duration)
  521. w1_norm = round(w1/(1/physio_hz)) # n data points in physio for w1
  522. w2 = 0.667 # in sec (window size of one beat duration)
  523. w2_norm = round(w2/(1/physio_hz)) # n data points in physio vector for w2
  524. # 1st moving average - emphasizing the systolic peak area in the signal
  525. ma_peak = uniform_filter1d(cardiac_filt, w1_norm, mode='reflect')
  526. # 2nd moving average - emphasizing the beat area to be used as a threshold
  527. # for the first moving average
  528. ma_beat = uniform_filter1d(cardiac_filt, w2_norm, mode='reflect')
  529. # plt.plot(ma_beat2)
  530. # 3. TRHESHOLDING: equation determining offset level 'beta' is based
  531. # on a brute force search
  532. beta = 0.02 # offset level
  533. # thresholding, MAbeat + a small offset
  534. thr1 = ma_beat + beta*np.mean(cardiac_filt)
  535. # generate block variable with indices that contain possible peaks in
  536. # cardiac_filt (1 = true), (0 = false)
  537. blocks = ma_peak > thr1
  538. blocks = blocks.astype(int)
  539. # get onsets, offset and durations of blocks of interest
  540. onset = np.flatnonzero(np.diff(blocks) == 1) + 1
  541. offset = np.flatnonzero(np.diff(blocks) == -1)
  542. # if the onset of the first peak was not recorded
  543. if onset[1] > offset[1]:
  544. onset = np.insert(onset, 0, 1)
  545. # if the offset of the last peak was not recorded
  546. if onset[-1] > offset[-1]:
  547. offset = np.append(offset, len(cardiac))
  548. duration = offset - onset+1
  549. # get the indices of the pulse peaks
  550. peaks_ind = []
  551. counter = 0
  552. for elem in duration:
  553. if elem >= w1_norm:
  554. ind = np.argmax(cardiac_filt_zscore[onset[counter]: offset[counter]])
  555. ind = ind + onset[counter]
  556. peaks_ind = np.append(peaks_ind, ind)
  557. del ind
  558. counter = counter + 1
  559. peaks_ind = peaks_ind.astype(int)
  560. # save indices (within the data vector) of identified peaks
  561. physio_ppg_peaks_ind = peaks_ind
  562. # save timings of the peaks in seconds
  563. physio_ppg_peaks_time = physio_time[peaks_ind]
  564. # get peaks within the functional scan
  565. peaks_ind_run = peaks_ind[
  566. (peaks_ind >= physio_triggers_ind[0])
  567. &
  568. (peaks_ind <= physio_triggers_ind[-1])]
  569. del (w1, w1_norm, w2, w2_norm, beta, blocks, thr1, onset, offset, duration,
  570. ma_beat, ma_peak, counter, elem)
  571. # ____________________________________________________________________________
  572. # 4.2. HEART RATE and HEART RATE VARIABILITY to cross-check and possibly
  573. # choose different peak-detection
  574. hr = 60/np.diff(physio_ppg_peaks_time)
  575. # physio_time is sampled in middle of two consecutive peaks
  576. hr_time = np.diff(physio_ppg_peaks_time) / 2 + physio_ppg_peaks_time[0:-1]
  577. hr_time = np.insert(hr_time, 0, physio_time[0])
  578. hr_time = np.append(hr_time, physio_time[-1])
  579. f = interpolate.interp1d(hr_time, np.block([hr[0], hr, hr[-1]]))
  580. # interpolate heart rate values to original sampling time
  581. hr_raw_fs = f(physio_time)
  582. # perform outlier correction similar to this function in Matlab:
  583. # HR_filloutl_Fs = filloutliers(HR_raw_Fs,'linear','movmedian',
  584. # round(hr_filloutliers_window*physio_Hz),'ThresholdFactor',
  585. # hr_filloutliers_threshold)
  586. # here performed in two steps:
  587. df = pd.DataFrame({'hr_raw_fs': hr_raw_fs})
  588. # 1. outlier detection: outliers are defined as elements more than 3
  589. # MAD from the median. The scaled MAD is defined
  590. # as c*median(abs(A-median(A))), (see Matlab)
  591. # c = -1/(2**0.5*special.erfcinv(3/2)) = 1.4826
  592. # apply hampel filter
  593. df['hr_raw_fs_outlier'] = hampel(df['hr_raw_fs'],
  594. k=int(hr_filloutliers_window * physio_hz),
  595. t0=hr_filloutliers_threshold)
  596. # 2. filling outliers using linear interpolation
  597. df['hr_raw_fs_filloutlier'] = df['hr_raw_fs_outlier'].interpolate(
  598. method='linear', limit_direction="both")
  599. hr_raw_fs_filloutlier = df['hr_raw_fs_filloutlier'].to_numpy()
  600. del df
  601. # compute heartbeat interval (hbi) in seconds
  602. physio_hbi = np.diff(physio_ppg_peaks_time)
  603. # compute Heart Rate Varibility (measure of the autonomic nervous activity) as RMSDD (root mean square of successive differences between hearbeats in ms)
  604. # e.g. van den Berg, M. E., Rijnbeek, P. R., Niemeijer, M. N., Hofman, A., Herpen, G. van, Bots, M. L., Hillege, H., Swenne, C. A., Eijgelsheim, M., Stricker,
  605. # B. H., & Kors, J. A. (2018). Normal values of corrected heart-rate variability in 10-second electrocardiograms for all ages. Frontiers in Physiology, 9(APR), 424.
  606. # https://doi.org/10.3389/FPHYS.2018.00424/BIBTEX
  607. physio_hbi_rmssd = np.sqrt(
  608. np.mean((physio_hbi[1::] - physio_hbi[0:-1])**2)) * 1000
  609. # identify possible outliers in identified heart beats (only used to decide between peak detection approaches)
  610. df = pd.DataFrame({'physio_hbi': physio_hbi})
  611. # apply rolling mean
  612. df['mean_physio_hbi'] = df['physio_hbi'].rolling(window=nIBIs, min_periods=1, center=True).mean()
  613. mean_physio_hbi = df['mean_physio_hbi'].to_numpy()
  614. hbi_outlier = np.logical_or((physio_hbi > mean_physio_hbi + mean_physio_hbi/100*outlier_cardiac_threshold), (physio_hbi < mean_physio_hbi - mean_physio_hbi/100*outlier_cardiac_threshold))
  615. del (df, mean_physio_hbi)
  616. # PLOT HR and cardiac signal
  617. fig_cardiac = plt.figure('Cardiac', constrained_layout=True)
  618. ax_card_raw = fig_cardiac.add_subplot(211)
  619. plot_raw, = ax_card_raw.plot(
  620. physio_time, cardiac, zorder=1)
  621. plot_filt, = ax_card_raw.plot(physio_time, cardiac_filt_zscore,
  622. color='cyan', zorder=4)
  623. plot_startscan, = ax_card_raw.plot([physio_time[physio_triggers_ind[0]], physio_time[physio_triggers_ind[0]]], np.squeeze([
  624. ax_card_raw.get_ylim()]), color='red', zorder=2)
  625. plot_endscan, = ax_card_raw.plot([physio_time[physio_triggers_ind[-1]], physio_time[physio_triggers_ind[-1]]],
  626. np.squeeze([ax_card_raw.get_ylim()]), color='red', zorder=3)
  627. plot_peaks, = ax_card_raw.plot(
  628. physio_ppg_peaks_time, cardiac_filt_zscore[peaks_ind], 'g.', zorder=5)
  629. ax_card_raw.set_title('Cardiac raw signal')
  630. ax_card_raw.set_xlabel('Time (s)')
  631. ax_card_raw.set_ylabel('Normalized amplitude')
  632. ax_card_raw.legend([plot_startscan, plot_raw, plot_filt, plot_peaks], ['run length', 'cardiac raw data', 'filtered cardiac data', 'peaks'],
  633. ncol=4)
  634. ax_card_hr = fig_cardiac.add_subplot(212)
  635. plot_hr, = ax_card_hr.plot(physio_time, hr_raw_fs)
  636. ax_card_hr.set_xlabel('Time (s)')
  637. ax_card_hr.set_ylabel('Heart beats per minutes')
  638. a = int(np.round(np.mean(hr_raw_fs_filloutlier)))
  639. b = int(np.round(np.std(hr_raw_fs_filloutlier)))
  640. c = int(np.round(physio_hbi_rmssd))
  641. ax_card_hr.set_title(
  642. 'Heart rate ({0} +/- {1} bpm), heart rate variability as RMSSD ({2} ms)'.format(a, b, c))
  643. # if outliers have been detected in the identified inter-beat-intervals (heart rate), let user decide to switch the peak
  644. # detection method
  645. if (hbi_outlier.sum() == 0 and len(sys.argv) == 1) or (hbi_outlier.sum() == 0 and len(sys.argv) > 1 and plotdisp.lower() == 'true' ):
  646. plt.show()
  647. elif (hbi_outlier.sum() > 0 and len(sys.argv) == 1) or (hbi_outlier.sum() > 0 and len(sys.argv) > 1 and plotdisp.lower() == 'true' ):
  648. showdialog_peakdetect()
  649. plt.show()
  650. new = userinput_peakdetect()
  651. if new:
  652. physio_input_parameters["ScriptParameters"]["PulsePeakDetectionBased"] = "Kassinopoulos, M., & Mitsis, G. D. (2019). Identification of physiological response functions to correct for fluctuations in resting-state fMRI related to heart rate and respiration. NeuroImage, 202, 116150. https://doi.org/10.1016/j.neuroimage.2019.116150"
  653. # peak detection based on Kassinopoulos et al., 2019
  654. min_peak = int(np.round(min_hbi*physio_hz)) # minimum number of datapoints in between heartbeats
  655. peaks_ind, _ = signal.find_peaks(cardiac_filt_zscore, distance=min_peak, prominence=0.4)
  656. # get peaks within the functional scan
  657. peaks_ind_run = peaks_ind[
  658. (peaks_ind >= physio_triggers_ind[0])
  659. &
  660. (peaks_ind <= physio_triggers_ind[-1])]
  661. physio_ppg_peaks_ind = peaks_ind
  662. physio_ppg_peaks_time = physio_time[peaks_ind]
  663. hr = 60/np.diff(physio_ppg_peaks_time)
  664. # physio_time is sampled in middle of two consecutive peaks
  665. hr_time = np.diff(physio_ppg_peaks_time) / 2 + physio_ppg_peaks_time[0:-1]
  666. hr_time = np.insert(hr_time, 0, physio_time[0])
  667. hr_time = np.append(hr_time, physio_time[-1])
  668. f = interpolate.interp1d(hr_time, np.block([hr[0], hr, hr[-1]]))
  669. # interpolate heart rate values to original sampling time
  670. hr_raw_fs = f(physio_time)
  671. df = pd.DataFrame({'hr_raw_fs': hr_raw_fs})
  672. # apply hampel filter
  673. df['hr_raw_fs_outlier'] = hampel(df['hr_raw_fs'],
  674. k=int(hr_filloutliers_window * physio_hz),
  675. t0=hr_filloutliers_threshold)
  676. # 2. filling outliers using linear interpolation
  677. df['hr_raw_fs_filloutlier'] = df['hr_raw_fs_outlier'].interpolate(
  678. method='linear', limit_direction="both")
  679. hr_raw_fs_filloutlier = df['hr_raw_fs_filloutlier'].to_numpy()
  680. del df
  681. # delete old heartbeat interval and its corresponding measures and compute new heartbeat interval (hbi) in seconds
  682. del (physio_hbi, physio_hbi_rmssd, hbi_outlier)
  683. physio_hbi = np.diff(physio_ppg_peaks_time)
  684. # calculate RMSSD based on all identified heartbeats
  685. physio_hbi_rmssd = np.sqrt(
  686. np.mean((physio_hbi[1::] - physio_hbi[0:-1])**2)) * 1000
  687. # identify possible outliers in newly computed heartbeat signal
  688. df = pd.DataFrame({'physio_hbi': physio_hbi})
  689. # apply rolling mean
  690. df['mean_physio_hbi'] = df['physio_hbi'].rolling(window=nIBIs, min_periods=1, center=True).mean()
  691. mean_physio_hbi = df['mean_physio_hbi'].to_numpy()
  692. hbi_outlier = np.logical_or((physio_hbi > mean_physio_hbi + mean_physio_hbi/100*outlier_cardiac_threshold), (physio_hbi < mean_physio_hbi - mean_physio_hbi/100*outlier_cardiac_threshold))
  693. del (df, mean_physio_hbi)
  694. del(new)
  695. else:
  696. plt.close()
  697. # ____________________________________________________________________________
  698. # 4.3. CARDIAC PHASE: compute based on systolic peaks
  699. cardiac_phase = np.zeros(len(cardiac))
  700. for ind in range(len(cardiac)):
  701. if ind < peaks_ind[0] or ind >= peaks_ind[-1]:
  702. cardiac_phase[ind] = float('NaN')
  703. else:
  704. prev_peak = np.argwhere(peaks_ind <= ind)[-1]
  705. t1 = peaks_ind[prev_peak]
  706. t2 = peaks_ind[prev_peak+1]
  707. # phase coded between 0 and 2pi (see Glover et al., 2000)
  708. cardiac_phase[ind] = 2*np.pi*(ind - t1)/(t2-t1)
  709. del (t1, t2)
  710. del ind
  711. # ____________________________________________________________________________
  712. # 4.4. HEART RATE computation based on Kassinopoulos et al., 2019
  713. f = interpolate.interp1d(physio_time, hr_raw_fs_filloutlier)
  714. hr_10 = f(physio_time_10)
  715. physio_hr_mean = np.mean(hr_raw_fs_filloutlier)
  716. physio_hr_std = np.std(hr_raw_fs_filloutlier)
  717. # generate shifted versions of the cleaned HR signal
  718. # (hr_raw_fs_filloutlier) in the original sampling rate
  719. hr_final = shift_preds(hr_raw_fs_filloutlier, hr_shifts, physio_hz)
  720. # ____________________________________________________________________________
  721. # 4.5. Plotting Cardiac Measures
  722. fig_cardiac_final = plt.figure('Cardiac', constrained_layout=True)
  723. ax_card_raw = fig_cardiac_final.add_subplot(211)
  724. plot_raw, = ax_card_raw.plot(
  725. physio_time, cardiac, zorder=1)
  726. plot_filt, = ax_card_raw.plot(physio_time, cardiac_filt_zscore,
  727. color='cyan', zorder=4)
  728. plot_startscan, = ax_card_raw.plot([physio_time[physio_triggers_ind[0]], physio_time[physio_triggers_ind[0]]], np.squeeze([
  729. ax_card_raw.get_ylim()]), color='red', zorder=2)
  730. plot_endscan, = ax_card_raw.plot([physio_time[physio_triggers_ind[-1]], physio_time[physio_triggers_ind[-1]]],
  731. np.squeeze([ax_card_raw.get_ylim()]), color='red', zorder=3)
  732. plot_peaks, = ax_card_raw.plot(
  733. physio_time[peaks_ind], cardiac_filt_zscore[peaks_ind], 'r.', zorder=5)
  734. plot_peaks_run, = ax_card_raw.plot(
  735. physio_time[peaks_ind_run], cardiac_filt_zscore[peaks_ind_run], 'g.', zorder=6)
  736. y = ax_card_raw.get_ylim()
  737. ax_card_raw.set_ylim(y[0], y[1]+2)
  738. ax_card_raw.set_title('Cardiac signal')
  739. ax_card_raw.set_xlabel('Time (s)')
  740. ax_card_raw.set_ylabel('Normalized amplitude')
  741. ax_card_raw.legend([plot_startscan, plot_raw, plot_filt, plot_peaks, plot_peaks_run], ['run length', 'cardiac raw data', 'filtered cardiac data', 'peaks', 'peaks in run'],
  742. ncol=5)
  743. ax_card_hr = fig_cardiac_final.add_subplot(212)
  744. plot_hr, = ax_card_hr.plot(physio_time, hr_raw_fs)
  745. plot_hr_cor, = ax_card_hr.plot(physio_time_10, hr_10)
  746. y = ax_card_hr.get_ylim()
  747. ax_card_hr.set_ylim(y[0], y[1]+10)
  748. ax_card_hr.set_xlabel('Time (s)')
  749. ax_card_hr.set_ylabel('Heart beats per minute')
  750. a = int(np.round(np.mean(hr_raw_fs_filloutlier)))
  751. b = int(np.round(np.std(hr_raw_fs_filloutlier)))
  752. c = int(np.round(physio_hbi_rmssd))
  753. ax_card_hr.set_title(
  754. 'Heart rate ({0} +/- {1} bpm), heart rate variability as RMSSD ({2} ms)'.format(a, b, c))
  755. ax_card_hr.legend([plot_hr, plot_hr_cor], ['raw heart rate', 'heart rate corrected for outliers'],
  756. ncol=2)
  757. fig_cardiac_final.savefig((physio_out_path + '/' + fmr_name + '_cardiac.png'), dpi=600, format='png')
  758. if (len(sys.argv) == 1) or (len(sys.argv) > 1 and plotdisp.lower() == 'true'):
  759. plt.show()
  760. del(a, b, c, y)
  761. # ____________________________________________________________________________
  762. # 4.6. HR*CRF: Apply standard PRF model (cardiac response function, CRF)
  763. # as defined by: Chang, C., Cunningham, J. P., & Glover, G. H. (2009).
  764. # Influence of heart rate on the BOLD signal: the cardiac response function.
  765. # NeuroImage, 44(3), 857?869. https://doi.org/10.1016/j.neuroimage.2008.09.029
  766. # code based on Kassinopoulos et al., 2019
  767. # time vector for impulse response
  768. t_ir = np.linspace(0, 60, physio_hz_10*60+1)
  769. # cardiac response function as defined by Chang et al., 2009
  770. crf = 0.6*t_ir**2.7 *np.exp(-t_ir/1.6)-(16/(np.sqrt(2*np.pi*9)))* np.exp(-(t_ir-12)**2/18)
  771. crf = crf/max(crf)
  772. del t_ir
  773. # smoothing HR data with 6 seconds
  774. hr_10_sm = uniform_filter1d(hr_10, int(6*physio_hz_10), mode='reflect')
  775. # in order to avoid the convolution with the zero-padded edges of the data
  776. # vectors, the approach by the Tapas PhysIO-toolbox is used, using as
  777. # padding value the mean of the HR
  778. temp = np.mean(hr_10_sm)
  779. hr_pad = np.concatenate((temp * np.ones(len(crf)-1), hr_10_sm))
  780. # create HR*CRF
  781. hr_conv = np.convolve(hr_pad, crf, 'valid')
  782. del(temp, hr_pad)
  783. # ____________________________________________________________________________
  784. # 4.7 VOLUME-BASED REGRESSORS: sample volume-based values of all
  785. # calculated cardiac measures and save as SDM files
  786. # Sample volume-based cardiac phase, hr, hr*crf
  787. # get trigger indices for 10 Hz signal
  788. time_trigger = physio_time[physio_triggers_startslice_ind]
  789. # get matrix with diff values of size time_triggers x physio_time_10
  790. temp = np.absolute(time_trigger - physio_time_10[:, np.newaxis])
  791. # indices of volume values for 10 Hz signal
  792. trig_ind_time10 = np.argmin(temp, axis=0)
  793. del(temp, time_trigger)
  794. temp = int(physio_triggers_sum/fmr_no_volumes)
  795. # not for every recorded volume a trigger saved in the TSV file
  796. if temp < 1:
  797. showdialog_triggererr()
  798. # only volume triggers saved in physio_data
  799. elif temp == 1:
  800. # filtered and z-scored cardiac values, sampled at volume times
  801. cardiac_filt_vol = cardiac_filt_zscore[physio_triggers_startslice_ind]
  802. # cardiac phase values, sampled at volume times
  803. cardiac_phase_vol = cardiac_phase[physio_triggers_startslice_ind]
  804. # heart rate values, sampled at volume times
  805. hr_vol = hr_final[physio_triggers_startslice_ind]
  806. # heart rate values convolved with the cardiac response function,
  807. # sampled at volume times
  808. hr_conv_vol = hr_conv[trig_ind_time10]
  809. # slice triggers saved in physio_data:
  810. elif temp > 1:
  811. # sampled at the start of each volume
  812. cardiac_filt_vol = cardiac_filt_zscore[physio_triggers_startslice_ind[0::temp]]
  813. cardiac_phase_vol = cardiac_phase[physio_triggers_startslice_ind[0::temp]]
  814. # cardiac_phase_vol = cardiac_phase[physio_triggers_startslice_ind[round(temp/2)-1::temp]] # sample the middle of the volume
  815. hr_vol = hr_final[physio_triggers_startslice_ind[0::temp]]
  816. hr_conv_vol = hr_conv[trig_ind_time10[0::temp]]
  817. del(temp)
  818. # Rescale and Detrend predictors
  819. hr_vol = hr_vol/np.amax(hr_vol, axis=0)
  820. hr_conv_vol = signal.detrend(hr_conv_vol)
  821. hr_conv_vol = hr_conv_vol / max(hr_conv_vol)
  822. # Fit Xth order fourier series to estimate cardiac phase
  823. dm_phs_c = np.zeros((fmr_no_volumes, order_cardiac*2))
  824. if order_cardiac > 0:
  825. for i in range(order_cardiac):
  826. dm_phs_c[:, (i*2)] = np.cos((i+1)*cardiac_phase_vol)
  827. dm_phs_c[:, (i*2)+1] = np.sin((i+1)*cardiac_phase_vol)
  828. del(i)
  829. # SAVE CARDIAC SDM FILES
  830. # Save Cardiac RETROICOR Predictors
  831. sdm_cp = sdm.DesignMatrix()
  832. colour_temp = np.linspace(225, 30, order_cardiac*2)
  833. counter = 1
  834. for i in range(0, order_cardiac*2, 2):
  835. sdm_cp.add_predictor(sdm.Predictor('Cardiac_Cos' + str(counter), dm_phs_c[:,i], colour=[int(colour_temp[i]), 0, 0]))
  836. sdm_cp.add_predictor(sdm.Predictor('Cardiac_Sin' + str(counter), dm_phs_c[:,i+1], colour=[int(colour_temp[i+1]), 0, 0]))
  837. counter = counter + 1
  838. del(i, counter, colour_temp)
  839. physio_regressors_names.extend(sdm_cp.names)
  840. sdm_cp.add_constant()
  841. sdm_cp.save(physio_out_path + '/' + fmr_name + '_cardiac_RETROICOR.sdm')
  842. physio_regressors_matrix = np.append(physio_regressors_matrix, dm_phs_c, axis=1)
  843. noise_model_no = noise_model_no + order_cardiac*2
  844. temp = np.sum(np.isnan(sdm_cp.data))
  845. if temp > 0:
  846. print('NaNs in cardiac RETROICOR predictors \n')
  847. physio_output_parameters['CardiacOutput'].update({
  848. 'Error_RETROICOR': 'NaNs in cardiac RETROICOR predictors'
  849. })
  850. del(temp)
  851. # Save Filtered and Z-transformed Cardiac Signal
  852. sdm_cfilt = sdm.DesignMatrix()
  853. sdm_cfilt.add_predictor(sdm.Predictor((
  854. physio_input_parameters['PhysioJsonFile']['PhysioColumnHeaders'][pulse_col_dict[0]][pulse_col_dict[1]] +
  855. '_BP' + str(cardiac_low) + '-' + str(cardiac_high) + 'Hz_zscore'),
  856. cardiac_filt_vol, colour=[np.random.randint(30,225), 0, 0]))
  857. physio_regressors_names.extend(sdm_cfilt.names)
  858. sdm_cfilt.add_constant()
  859. sdm_cfilt.save(physio_out_path + '/' + fmr_name + '_cardiac_BP' + str(cardiac_low) + '-' + str(cardiac_high) + 'Hz_z.sdm')
  860. physio_regressors_matrix = np.append(physio_regressors_matrix, np.reshape(cardiac_filt_vol,(fmr_no_volumes,1)), axis=1)
  861. noise_model_no = noise_model_no + 1
  862. # Save Shifted HR Predictors
  863. sdm_hr = sdm.DesignMatrix()
  864. colour_temp = np.linspace(225, 30, len(hr_shifts))
  865. for i in range(len(hr_shifts)):
  866. sdm_hr.add_predictor(sdm.Predictor('HR_shift_' + str(hr_shifts[i]) + 'sec', hr_vol[:,i], colour=[int(colour_temp[i]), 0, 0]))
  867. del(i, colour_temp)
  868. physio_regressors_names.extend(sdm_hr.names)
  869. sdm_hr.add_constant()
  870. sdm_hr.save(physio_out_path + '/' + fmr_name + '_HR.sdm')
  871. physio_regressors_matrix = np.append(physio_regressors_matrix, hr_vol, axis=1)
  872. noise_model_no = noise_model_no + len(hr_shifts)
  873. # Save HR*CRF Predictor
  874. sdm_crf = sdm.DesignMatrix()
  875. sdm_crf.add_predictor(sdm.Predictor('HR*CRF', hr_conv_vol, colour=[np.random.randint(30,225), 0, 0]))
  876. physio_regressors_names.extend(sdm_crf.names)
  877. sdm_crf.add_constant()
  878. sdm_crf.save(physio_out_path + '/' + fmr_name + '_HRCRF.sdm')
  879. physio_regressors_matrix = np.append(physio_regressors_matrix, np.reshape(hr_conv_vol,(fmr_no_volumes,1)), axis=1)
  880. noise_model_no = noise_model_no + 1
  881. # Fill the physio_output_parameters dict
  882. physio_output_parameters['CardiacOutput'].update({
  883. "HbiRmssd": round(physio_hbi_rmssd, 2), "HeartRateAverage(BPM)": round(physio_hr_mean,2),
  884. "HeartRateStd(BPM)": round(physio_hr_std, 2), "PulsePeaksInRun": len(peaks_ind_run), "HbiOutliersCount": int(hbi_outlier.sum())
  885. })
  886. # If a PRT was loaded, sample the heart rate in ms resolution
  887. if prt_task_file_name.endswith('.prt'):
  888. physio_time_hr_ms = np.arange(0, physio_time[-1], 1/1000)
  889. f = interpolate.interp1d(physio_time, hr_raw_fs_filloutlier)
  890. hr_ms = f(physio_time_hr_ms)
  891. del(f, peaks_ind, peaks_ind_run, physio_hz, physio_nyq,
  892. physio_data, physio_triggers_ind, physio_triggers_sum,
  893. physio_triggers_startslice_ind, physio_time, physio_time_10,
  894. physio_hz_10, physio_ts_10)
  895. # =============================================================================
  896. # Respiratory Data
  897. # =============================================================================
  898. physio_output_parameters['RespiratoryOutput'] = {}
  899. # if there is no respiratory data provide feedback to the user
  900. if 'resp_col_dict' not in locals():
  901. physio_output_parameters['RespiratoryOutput'].update({"Error": "No respiratory data provided for this functional run"})
  902. if len(sys.argv) == 1:
  903. showdialog_info('respiratory')
  904. else:
  905. print("No respiratory data provided for this functional run")
  906. else:
  907. # reorganize data for easier use
  908. physio_hz = physio_input_parameters['PhysioJsonFile']['SamplingFrequency'][resp_col_dict[0]]
  909. physio_data = physio_input_parameters['PhysioJsonFile']['PhysioData'][resp_col_dict[0]]
  910. physio_triggers_sum = physio_input_parameters['PhysioJsonFile']['NumberOfStartScanTriggersSaved'][resp_col_dict[0]]
  911. physio_triggers_ind = physio_input_parameters['PhysioJsonFile']['IndicesScanTriggers'][resp_col_dict[0]]
  912. physio_triggers_startslice_ind = physio_input_parameters['PhysioJsonFile']['IndicesVolumeTriggers'][resp_col_dict[0]]
  913. physio_time = physio_input_parameters['PhysioJsonFile']['PhysioTimeSec'][resp_col_dict[0]]
  914. physio_hz_10 = 10
  915. physio_ts_10 = 1/physio_hz_10 # sampling steps for 10Hz signal
  916. # time vector for 10Hz
  917. physio_time_10 = np.arange(physio_time[0], physio_time[-1], physio_ts_10)
  918. # extract z-transformed respiratory signal
  919. resp = stats.zscore(np.squeeze(physio_data[:, resp_col_dict[1]]))
  920. # check whether there are any missing values in the respiratory signal
  921. assert ~np.sum(np.isnan(resp)), 'Nan values in the respiratory signal'
  922. # __________________________________________________________________________
  923. # 5.1. PREPROCESSING of respiratory signal
  924. # detrend data (adapted from Kassinopoulos et al, 2019)
  925. resp = signal.detrend(resp)
  926. # outlier replacement and filtering adpated from Power, J. D., Lynch, C. J.,
  927. # Dubin, M. J., Silver, B. M., Martin, A., & Jones, R. M. (2020).
  928. # Characteristics of respiratory measures in young adults scanned at rest,
  929. # including systematic changes and missed deep breaths. NeuroImage, 204,
  930. # 116234. https://doi.org/10.1016/j.neuroimage.2019.116234
  931. # outlier replacement filter to eliminate spurious spike artifacts
  932. df = pd.DataFrame({'resp': resp})
  933. # 1. apply hampel filter
  934. df['resp_outlier'] = hampel(
  935. df['resp'], k=int(resp_filloutliers_window * physio_hz),
  936. t0=resp_filloutliers_threshold)
  937. # 2. filling outliers using linear interpolation
  938. df['resp_filloutlier'] = df['resp_outlier'].interpolate(
  939. method='linear', limit_direction="both")
  940. resp_filloutlier = df['resp_filloutlier'].to_numpy()
  941. del df
  942. # blurring, using a 1 second window to aid peak detection
  943. resp_filt = signal.savgol_filter(
  944. resp_filloutlier, window_length=int((np.ceil(physio_hz) // 2 * 2 + 1)),
  945. polyorder=2, mode='interp')
  946. # __________________________________________________________________________
  947. # 5.2. RESPIRATORY PHASE computation
  948. # respiratory phase needs to be calculated taking not only the times of peak
  949. # inspiration into account (peak location), but also the amplitude of
  950. # inspiration, since the depth of breathing also influences the amount of
  951. # head motion
  952. # 1. amplitude of respiratory signal from the pneumatic belt, is normalized
  953. # to the range (0, Rmax), i.e. find max and min amplitudes and normalize
  954. # amplitude
  955. resp_norm = (resp_filt-min(resp_filt)) / (max(resp_filt)-min(resp_filt))
  956. # 2. Calculate the histogram from the number of occurrences of specific
  957. # respiratory amplitudes in bins 1:100 and the bth bin is accordingly
  958. # centered at bRmax/100
  959. resp_hist, _ = np.histogram(resp_norm, 100)
  960. # 3. Calculate running integral of the histogram, creating an equalized
  961. # transfer function between the breathing amplitude and respiratory phase,
  962. # where end-expiration is assigned a phase of 0 and peak inspiration has
  963. # phase of +/-pi. While inhaling the phase spans 0 to pi and during
  964. # expiration the phase is negated.
  965. resp_transfer_func = np.insert(
  966. (np.cumsum(resp_hist) / np.sum(resp_hist)), 0, 0)
  967. kern_size = int(round(physio_hz - 1))
  968. # smoothed version for taking derivative
  969. resp_smooth = np.convolve(resp_norm, np.ones(kern_size), 'same')
  970. # derivative dR/dt
  971. resp_diff = np.append(np.diff(resp_smooth), 0)
  972. # for phase calculation +pi was added -> so the adapted range is from 0
  973. # to 2pi (as defined in the Tapas PhysIO toolbox), for plotting use
  974. # resp_phase - pi to get the original range defined by Glover
  975. indices = np.round(resp_norm * 100).astype(int)
  976. resp_phase = np.pi * np.take(resp_transfer_func,indices) * np.sign(resp_diff) + np.pi
  977. del(indices, kern_size, resp_transfer_func)
  978. # __________________________________________________________________________
  979. # 5.3. BREATHING RATE (BR), RESPIRATION VARIATION (RV), RESPIRATION VOLUME
  980. # PER TIME (RVT), RESPIRATORY FLOW (RF), WINDOWED ENVELOPE OVER THE
  981. # RESPIRATORY TRACE (ENV)
  982. # peak detection adapted from Power, J. D., Lynch, C. J., Dubin, M. J., Silver,
  983. # B. M., Martin, A., & Jones, R. M. (2020). Characteristics of respiratory
  984. # measures in young adults scanned at rest, including systematic changes and
  985. # missed deep breaths. NeuroImage, 204, 116234.
  986. # https://doi.org/10.1016/j.neuroimage.2019.116234
  987. # RVT, BR and RF computation adapted from Kassinopoulos et al., 2019
  988. # RV as defined by: Chang, C., & Glover, G. H. (2009). Relationship between
  989. # respiration, end-tidal CO2, and BOLD signals in resting-state fMRI.
  990. # NeuroImage, 47(4), 1381-1393.
  991. # https://doi.org/10.1016/j.neuroimage.2009.04.048
  992. # RV = standard deviation of the respiratory signal over 6 seconds
  993. # computation based on Power et al., 2020
  994. # ENV: windowed envelope of the respiratory signal over a 10-s window
  995. # adapted from Power et al., 2020
  996. # z-scoring of filtered respiratory data
  997. resp_filtz = stats.zscore(resp_filt)
  998. f = interpolate.interp1d(physio_time, resp_filtz)
  999. resp_10 = f(physio_time_10)
  1000. # calculate windowed envelope of the respiratory signal over a 10-s window
  1001. def envelope_rms(a, window_size):
  1002. a2 = np.power(a, 2)
  1003. window = np.ones(window_size)/float(window_size)
  1004. return np.sqrt(np.convolve(a2, window, 'same'))
  1005. env = envelope_rms(resp_filtz, int(physio_hz*10))
  1006. # generate shifted versions of the respiration envelope
  1007. env_final = shift_preds(env, env_shifts, physio_hz)
  1008. # RV: calculate respiration variation (RV) over 6 seconds window
  1009. df = pd.DataFrame({'resp_filtz': resp_filtz})
  1010. # apply rolling standard deviation
  1011. df['rv'] = df['resp_filtz'].rolling(window=int(physio_hz*6), min_periods=1, center=True).std()
  1012. rv = df['rv'].to_numpy()
  1013. del df
  1014. # generate shifted versions of the respiration variation
  1015. rv_final = shift_preds(rv, rv_shifts, physio_hz)
  1016. # PEAKS: find peaks and troughs in respiratory signal to calculate RVT and BR
  1017. # minpeakdistance = 1.8 sec, presumes breaths occur more than 1.8 s apart
  1018. resp_peaks_ind, _ = signal.find_peaks(
  1019. resp_filtz, distance=physio_hz*1.8, prominence=0.5)
  1020. resp_peaks = resp_filtz[resp_peaks_ind]
  1021. resp_troughs_ind, _ = signal.find_peaks(
  1022. -resp_filtz, distance=physio_hz*1.8, prominence=0.5)
  1023. resp_troughs = resp_filtz[resp_troughs_ind]
  1024. temp = (resp_peaks_ind >= physio_triggers_ind[0]) * (resp_peaks_ind <= physio_triggers_ind[-1])
  1025. resp_peaks_ind_run = resp_peaks_ind[temp]
  1026. del(temp)
  1027. temp = (resp_troughs_ind >= physio_triggers_ind[0]) * (resp_troughs_ind <= physio_triggers_ind[-1])
  1028. resp_troughs_ind_run = resp_troughs_ind[temp]
  1029. del(temp)
  1030. physio_resp_peaks_ind = resp_peaks_ind
  1031. physio_resp_peaks_time = physio_time[resp_peaks_ind]
  1032. temp_time_peaks = np.concatenate(([physio_time[0]], physio_time[resp_peaks_ind], [physio_time[-1]]))
  1033. temp_peaks = np.concatenate(([resp_peaks[0]], resp_peaks, [resp_peaks[-1]]))
  1034. f = interpolate.interp1d(temp_time_peaks, temp_peaks)
  1035. resp_up_10 = f(physio_time_10)
  1036. del(temp_time_peaks, temp_peaks, f)
  1037. temp_time_troughs = np.concatenate(([physio_time[0]], physio_time[resp_troughs_ind], [physio_time[-1]]))
  1038. temp_troughs = np.concatenate(([resp_troughs[0]], resp_troughs, [resp_troughs[-1]]))
  1039. f = interpolate.interp1d(temp_time_troughs, temp_troughs)
  1040. resp_low_10 = f(physio_time_10)
  1041. del(temp_time_troughs, temp_troughs, f)
  1042. # BR: calculate breathing rate (BR)
  1043. br = 60 / np.diff(physio_time[resp_peaks_ind])
  1044. time_br = np.concatenate(([physio_time[0]], (np.diff(physio_time[resp_peaks_ind])) /2 + physio_time[resp_peaks_ind[:-1]], [physio_time[-1]]))
  1045. f = interpolate.interp1d(time_br, np.concatenate(([br[0]], br, [br[-1]])))
  1046. br_10 = f(physio_time_10)
  1047. del(time_br)
  1048. physio_br_mean = np.mean(br_10)
  1049. physio_br_std = np.std(br_10)
  1050. # RVT: calculate respiratory volume per time (RVT), i.e. change in breath
  1051. # amplitude over one breath cycle
  1052. rvt = (resp_up_10 - resp_low_10) * br_10
  1053. # generate shifted versions of the cleaned RVT signal at 10 Hz
  1054. rvt_final = shift_preds(rvt, rvt_shifts, physio_hz_10)
  1055. # RF: calculate respiratory flow (RF)
  1056. # using a moving average window of 1.5 s to avoid spike artifacts
  1057. # might be not even necessary as the original respiratory data has been
  1058. # smoothed already (resp_filt)
  1059. resp_s = uniform_filter1d(resp_10, int(1.5*physio_hz_10), mode='reflect')
  1060. rf = np.diff(resp_s)
  1061. rf = np.insert(rf, 0, 0)
  1062. rf = rf**2
  1063. del(resp_s)
  1064. # ____________________________________________________________________________
  1065. # 5.4. Plotting Respiratory Measures
  1066. fig_respiratory = plt.figure('Respiratory', figsize=(10, 8), constrained_layout=True)
  1067. ax_resp_hist = fig_respiratory.add_subplot(5, 1, 1)
  1068. ax_resp_hist.hist(resp_norm, bins=100, edgecolor='b')
  1069. ax_resp_hist.set_title('Histogram of detrended, outlier-corrected, smoothed and amplitude-normalized respiration')
  1070. ax_resp_raw = fig_respiratory.add_subplot(5, 1, 2)
  1071. plot_raw, = ax_resp_raw.plot(
  1072. physio_time, resp, zorder=1)
  1073. plot_filt, = ax_resp_raw.plot(
  1074. physio_time, resp_filtz, color='cyan', zorder=4)
  1075. plot_startscan, = ax_resp_raw.plot([physio_time[physio_triggers_ind[0]], physio_time[physio_triggers_ind[0]]], np.squeeze([
  1076. ax_resp_raw.get_ylim()]), color='red', zorder=2)
  1077. plot_endscan, = ax_resp_raw.plot([physio_time[physio_triggers_ind[-1]], physio_time[physio_triggers_ind[-1]]],
  1078. np.squeeze([ax_resp_raw.get_ylim()]), color='red', zorder=3)
  1079. plot_peaks, = ax_resp_raw.plot(
  1080. physio_time[resp_peaks_ind], resp_filtz[resp_peaks_ind], 'r.', zorder=5)
  1081. plot_peaks_run, = ax_resp_raw.plot(
  1082. physio_time[resp_peaks_ind_run], resp_filtz[resp_peaks_ind_run], 'g.', zorder=7)
  1083. plot_troughs, = ax_resp_raw.plot(
  1084. physio_time[resp_troughs_ind], resp_filtz[resp_troughs_ind], 'r.', zorder=6)
  1085. plot_troughs_run, = ax_resp_raw.plot(
  1086. physio_time[resp_troughs_ind_run], resp_filtz[resp_troughs_ind_run], 'g.', zorder=8)
  1087. ax_resp_raw.plot(physio_time_10, resp_up_10)
  1088. ax_resp_raw.plot(physio_time_10, resp_low_10)
  1089. y = ax_resp_raw.get_ylim()
  1090. ax_resp_raw.set_ylim(y[0], y[1]+2)
  1091. ax_resp_raw.set_title('Respiratory signal')
  1092. ax_resp_raw.set_xlabel('Time (s)')
  1093. ax_resp_raw.set_ylabel('Normalized amplitude')
  1094. ax_resp_raw.legend([plot_startscan, plot_raw, plot_filt, plot_peaks, plot_peaks_run], ['run length', 'respiratory raw data', 'filtered respiratory data', 'peaks/troughs', 'peaks/troughs in run'],
  1095. ncol=5)
  1096. ax_resp_br = fig_respiratory.add_subplot(5, 1, 3)
  1097. plot_br, = ax_resp_br.plot(physio_time_10, br_10)
  1098. ax_resp_br.set_title(
  1099. 'Breathing rate (BR): {0} +/- {1} rpm'.format(np.round(physio_br_mean, decimals=1), np.round(physio_br_std, decimals=1)))
  1100. ax_resp_br.set_xlabel('Time (s)')
  1101. ax_resp_br.set_ylabel('Rpm') # Respirations per Minute (rpm)
  1102. ax_resp_rvt_env = fig_respiratory.add_subplot(5, 1, 4)
  1103. plot_resp, = ax_resp_rvt_env.plot(physio_time, resp_filtz)
  1104. plot_rvt, = ax_resp_rvt_env.plot(physio_time_10, stats.zscore(rvt), linewidth=1)
  1105. plot_env, = ax_resp_rvt_env.plot(physio_time, stats.zscore(env), linewidth=1)
  1106. plot_rv, = ax_resp_rvt_env.plot(physio_time, stats.zscore(rv), linewidth=1, color='y')
  1107. y = ax_resp_rvt_env.get_ylim()
  1108. ax_resp_rvt_env.set_ylim(y[0], y[1]+2)
  1109. ax_resp_rvt_env.set_ylabel('Normalized amplitude')
  1110. ax_resp_rvt_env.set_xlabel('time(s)')
  1111. ax_resp_rvt_env.set_title('Respiration volume per time (RVT), windowed Envelope over respiration signal (ENV) and Respiration variation (RV)')
  1112. ax_resp_rvt_env.legend([plot_resp, plot_env, plot_rvt, plot_rv], ['filtered respiratory data', 'ENV', 'RVT', 'RV'],
  1113. ncol=4)
  1114. ax_resp_rf = fig_respiratory.add_subplot(5, 1, 5)
  1115. ax_resp_rf.plot(physio_time_10, rf, 'g')
  1116. ax_resp_rf.set_title('Respiratory flow (RF)')
  1117. ax_resp_rf.set_ylabel('RF (a.u.)')
  1118. ax_resp_rf.set_xlabel('Time (s)')
  1119. fig_respiratory.savefig((physio_out_path + '/' + fmr_name + '_resp.png'), dpi=600, format='png')
  1120. if (len(sys.argv) == 1) or (len(sys.argv) > 1 and plotdisp.lower() == 'true'):
  1121. plt.show()
  1122. del(y)
  1123. # __________________________________________________________________________
  1124. # 5.5. RVT*RRF, RV*RRF: Apply standard RRF model (respiratory response function, RRF)
  1125. # as defined by: Birn, R. M., Smith, M. A., Jones, T. B., & Bandettini, P. A. (2008).
  1126. # The respiration response function: The temporal dynamics of fMRI signal fluctuations
  1127. # related to changes in respiration. NeuroImage, 40(2), 644-654.
  1128. # https://doi.org/10.1016/j.neuroimage.2007.11.059
  1129. # code based on Kassinopoulos et al., 2019
  1130. # downsample respiration variation to 10 Hz before convolution with RRF
  1131. f = interpolate.interp1d(physio_time, rv)
  1132. rv_10 = f(physio_time_10)
  1133. del(f)
  1134. # time vector for impulse response (sampled at 10 Hz)
  1135. t_ir = np.linspace(0, 60, physio_hz_10*60+1)
  1136. # respiratory response function as defined by Birn et al., 2008
  1137. # (sampled at 10 Hz)
  1138. rrf = 0.6*t_ir**2.1*np.exp(-t_ir/1.6)-0.0023*t_ir**3.54*np.exp(-t_ir/4.25)
  1139. # normalize by max = 1
  1140. rrf = rrf/max(rrf)
  1141. del(t_ir)
  1142. # in order to avoid the convolution with the zero-padded edges of the data
  1143. # vectors, the approach by the Tapas PhysIO-toolbox is used, using as
  1144. # padding value the mean of the RVT and RV
  1145. temp_rvt = np.mean(rvt)
  1146. rvt_pad = np.concatenate((temp_rvt * np.ones(len(rrf)-1), rvt))
  1147. temp_rv = np.mean(rv_10)
  1148. rv_pad = np.concatenate((temp_rv * np.ones(len(rrf)-1), rv_10))
  1149. # create RVT*RRF, RV*RRF
  1150. rvt_conv = np.convolve(rvt_pad, rrf, 'valid')
  1151. rv_conv = np.convolve(rv_pad, rrf, 'valid')
  1152. del(temp_rvt, temp_rv, rvt_pad, rv_pad)
  1153. # ____________________________________________________________________________
  1154. # 5.6 VOLUME-BASED REGRESSORS: sample volume-based values of all
  1155. # calculated respiratory measures and save as SDM files
  1156. # Sample volume-based filtered respiratory signal, respiratory phase,
  1157. # BR, RVT, RF, ENV, RVT*RRF
  1158. # get trigger indices for 10 Hz signal
  1159. time_trigger = physio_time[physio_triggers_startslice_ind]
  1160. # get matrix with diff values of size time_triggers x physio_time_10
  1161. temp = np.absolute(time_trigger - physio_time_10[:, np.newaxis])
  1162. # indices of volume values for 10 Hz signal
  1163. trig_ind_time10 = np.argmin(temp, axis=0)
  1164. del(temp, time_trigger)
  1165. temp = int(physio_triggers_sum/fmr_no_volumes)
  1166. # not for every recorded volume a trigger saved in the TSV file
  1167. if temp < 1:
  1168. showdialog_triggererr()
  1169. # only volume triggers saved in physio_data
  1170. elif temp == 1:
  1171. # filtered and z-scored respiratory values, sampled at volume times
  1172. resp_filt_vol = resp_filtz[physio_triggers_startslice_ind]
  1173. # respiratory phase values, sampled at volume times
  1174. resp_phase_vol = resp_phase[physio_triggers_startslice_ind]
  1175. # breathing rate values, sampled at volume times
  1176. br_vol = br_10[trig_ind_time10]
  1177. # respiration volume per time values, sampled at TR
  1178. rvt_vol = rvt_final[trig_ind_time10,:]
  1179. # windowed envelope values of respiratory signal, sampled at TR
  1180. env_vol = env_final[physio_triggers_startslice_ind,:]
  1181. # respiration variation, sampled at TR
  1182. rv_vol = rv_final[physio_triggers_startslice_ind,:]
  1183. # respiratory flow values, sampled at TR
  1184. rf_vol = rf[trig_ind_time10]
  1185. # respiration volume per time values convolved with the respiratory
  1186. # response function, sampled at volume times (rvt*rrf)
  1187. rvt_conv_vol = rvt_conv[trig_ind_time10]
  1188. # respiration variation convolved with the respiratory
  1189. # response function, sampled at volume times (rv*rrf)
  1190. rv_conv_vol = rv_conv[trig_ind_time10]
  1191. # slice triggers saved in physio_data:
  1192. elif temp > 1:
  1193. # sampled at the start of each volume
  1194. resp_filt_vol = resp_filtz[physio_triggers_startslice_ind[0::temp]]
  1195. resp_phase_vol = resp_phase[physio_triggers_startslice_ind[0::temp]]
  1196. # resp_phase_vol = resp_phase[physio_triggers_startslice_ind[round(temp/2)-1::temp]] # sample the middle of the volume
  1197. br_vol = br_10[trig_ind_time10[0::temp]]
  1198. rvt_vol = rvt_final[trig_ind_time10[0::temp],:]
  1199. env_vol = env_final[physio_triggers_startslice_ind[0::temp],:]
  1200. rv_vol = rv_final[physio_triggers_startslice_ind[0::temp],:]
  1201. rf_vol = rf[trig_ind_time10[0::temp]]
  1202. rvt_conv_vol = rvt_conv[trig_ind_time10[0::temp]]
  1203. rv_conv_vol = rv_conv[trig_ind_time10[0::temp]]
  1204. del(temp)
  1205. # Rescale and Detrend predictors
  1206. rvt_vol = rvt_vol/np.amax(rvt_vol, axis=0)
  1207. rv_vol = rv_vol/np.amax(rv_vol, axis=0)
  1208. env_vol = env_vol / np.amax(env_vol, axis=0)
  1209. br_vol = br_vol / max(br_vol)
  1210. rf_vol = rf_vol / max(rf_vol)
  1211. rvt_conv_vol = signal.detrend(rvt_conv_vol)
  1212. rvt_conv_vol = rvt_conv_vol / max(rvt_conv_vol)
  1213. rv_conv_vol = signal.detrend(rv_conv_vol)
  1214. rv_conv_vol = rv_conv_vol / max(rv_conv_vol)
  1215. # Fit Xth order fourier series to estimate respiratory phase
  1216. dm_phs_r = np.zeros((fmr_no_volumes, order_resp*2))
  1217. if order_resp > 0:
  1218. for i in range(order_resp):
  1219. dm_phs_r[:, (i*2)] = np.cos((i+1)*resp_phase_vol)
  1220. dm_phs_r[:, (i*2)+1] = np.sin((i+1)*resp_phase_vol)
  1221. del(i)
  1222. # SAVE RESPIRATORY SDM FILES
  1223. # Save Respiratory RETROICOR Predictors
  1224. sdm_rp = sdm.DesignMatrix()
  1225. colour_temp = np.linspace(225, 30, order_resp*2)
  1226. counter = 1
  1227. for i in range(0, order_resp*2, 2):
  1228. sdm_rp.add_predictor(sdm.Predictor('Resp_Cos' + str(counter), dm_phs_r[:,i], colour=[0, 0, int(colour_temp[i])]))
  1229. sdm_rp.add_predictor(sdm.Predictor('Resp_Sin' + str(counter), dm_phs_r[:,i+1], colour=[0, 0, int(colour_temp[i+1])]))
  1230. counter = counter + 1
  1231. del(i, counter, colour_temp)
  1232. physio_regressors_names.extend(sdm_rp.names)
  1233. sdm_rp.add_constant()
  1234. sdm_rp.save(physio_out_path + '/' + fmr_name + '_resp_RETROICOR.sdm')
  1235. physio_regressors_matrix = np.append(physio_regressors_matrix, dm_phs_r, axis=1)
  1236. noise_model_no = noise_model_no + order_resp*2
  1237. temp = np.sum(np.isnan(sdm_rp.data))
  1238. if temp > 0:
  1239. print('NaNs in respiratory RETROICOR predictors \n')
  1240. physio_output_parameters['RespiratoryOutput'].update({
  1241. 'Error_RETROICOR': 'NaNs in repiratory RETROICOR predictors'
  1242. })
  1243. del(temp)
  1244. # Save Filtered and Z-transformed Respiratory Signal
  1245. sdm_rfilt = sdm.DesignMatrix()
  1246. sdm_rfilt.add_predictor(sdm.Predictor((
  1247. physio_input_parameters['PhysioJsonFile']['PhysioColumnHeaders'][resp_col_dict[0]][resp_col_dict[1]]
  1248. + '_filt_zscore'), resp_filt_vol, colour=[0, 0, np.random.randint(30,225)]))
  1249. physio_regressors_names.extend(sdm_rfilt.names)
  1250. sdm_rfilt.add_constant()
  1251. sdm_rfilt.save(physio_out_path + '/' + fmr_name + '_resp_filtz.sdm')
  1252. physio_regressors_matrix = np.append(physio_regressors_matrix, np.reshape(resp_filt_vol,(fmr_no_volumes,1)), axis=1)
  1253. noise_model_no = noise_model_no + 1
  1254. # Save BR predictor
  1255. sdm_br = sdm.DesignMatrix()
  1256. sdm_br.add_predictor(sdm.Predictor('BreathingRate', br_vol, colour=[0, 0, np.random.randint(30,225)]))
  1257. physio_regressors_names.extend(sdm_br.names)
  1258. sdm_br.add_constant()
  1259. sdm_br.save(physio_out_path + '/' + fmr_name + '_BR.sdm')
  1260. physio_regressors_matrix = np.append(physio_regressors_matrix, np.reshape(br_vol,(fmr_no_volumes,1)), axis=1)
  1261. noise_model_no = noise_model_no + 1
  1262. # Save RF predictor
  1263. sdm_rf = sdm.DesignMatrix()
  1264. sdm_rf.add_predictor(sdm.Predictor('RespiratoryFlow', rf_vol, colour=[0, 0, np.random.randint(30,225)]))
  1265. physio_regressors_names.extend(sdm_rf.names)
  1266. sdm_rf.add_constant()
  1267. sdm_rf.save(physio_out_path + '/' + fmr_name + '_RF.sdm')
  1268. physio_regressors_matrix = np.append(physio_regressors_matrix, np.reshape(rf_vol,(fmr_no_volumes,1)), axis=1)
  1269. noise_model_no = noise_model_no + 1
  1270. # Save Shifted ENV Predictors
  1271. sdm_env = sdm.DesignMatrix()
  1272. colour_temp = np.linspace(225, 30, len(env_shifts))
  1273. for i in range(len(env_shifts)):
  1274. sdm_env.add_predictor(sdm.Predictor('ENV_shift_' + str(env_shifts[i]) + 'sec', env_vol[:,i], colour=[0, 0, int(colour_temp[i])]))
  1275. del(i, colour_temp)
  1276. physio_regressors_names.extend(sdm_env.names)
  1277. sdm_env.add_constant()
  1278. sdm_env.save(physio_out_path + '/' + fmr_name + '_ENV.sdm')
  1279. physio_regressors_matrix = np.append(physio_regressors_matrix, env_vol, axis=1)
  1280. noise_model_no = noise_model_no + len(env_shifts)
  1281. # Save Shifted RV Predictors
  1282. sdm_rv = sdm.DesignMatrix()
  1283. colour_temp = np.linspace(225, 30, len(rv_shifts))
  1284. for i in range(len(rv_shifts)):
  1285. sdm_rv.add_predictor(sdm.Predictor('RV_shift_' + str(rv_shifts[i]) + 'sec', rv_vol[:,i], colour=[0, 0, int(colour_temp[i])]))
  1286. del(i, colour_temp)
  1287. physio_regressors_names.extend(sdm_rv.names)
  1288. sdm_rv.add_constant()
  1289. sdm_rv.save(physio_out_path + '/' + fmr_name + '_RV.sdm')
  1290. physio_regressors_matrix = np.append(physio_regressors_matrix, rv_vol, axis=1)
  1291. noise_model_no = noise_model_no + len(rv_shifts)
  1292. # Save Shifted RVT Predictors
  1293. sdm_rvt = sdm.DesignMatrix()
  1294. colour_temp = np.linspace(225, 30, len(rvt_shifts))
  1295. for i in range(len(rvt_shifts)):
  1296. sdm_rvt.add_predictor(sdm.Predictor('RVT_shift_' + str(rvt_shifts[i]) + 'sec', rvt_vol[:,i], colour=[0, 0, int(colour_temp[i])]))
  1297. del(i, colour_temp)
  1298. physio_regressors_names.extend(sdm_rvt.names)
  1299. sdm_rvt.add_constant()
  1300. sdm_rvt.save(physio_out_path + '/' + fmr_name + '_RVT.sdm')
  1301. physio_regressors_matrix = np.append(physio_regressors_matrix, rvt_vol, axis=1)
  1302. noise_model_no = noise_model_no + len(rvt_shifts)
  1303. # Save RVT*RRF Predictor
  1304. sdm_rvtrrf = sdm.DesignMatrix()
  1305. sdm_rvtrrf.add_predictor(sdm.Predictor('RVT*RRF', rvt_conv_vol, colour=[0, 0, np.random.randint(30,225)]))
  1306. physio_regressors_names.extend(sdm_rvtrrf.names)
  1307. sdm_rvtrrf.add_constant()
  1308. sdm_rvtrrf.save(physio_out_path + '/' + fmr_name + '_RVTRRF.sdm')
  1309. physio_regressors_matrix = np.append(physio_regressors_matrix, np.reshape(rvt_conv_vol,(fmr_no_volumes,1)), axis=1)
  1310. noise_model_no = noise_model_no + 1
  1311. # Save RV*RRF Predictor
  1312. sdm_rvrrf = sdm.DesignMatrix()
  1313. sdm_rvrrf.add_predictor(sdm.Predictor('RV*RRF', rv_conv_vol, colour=[0, 0, np.random.randint(30,225)]))
  1314. physio_regressors_names.extend(sdm_rvrrf.names)
  1315. sdm_rvrrf.add_constant()
  1316. sdm_rvrrf.save(physio_out_path + '/' + fmr_name + '_RVRRF.sdm')
  1317. physio_regressors_matrix = np.append(physio_regressors_matrix, np.reshape(rv_conv_vol,(fmr_no_volumes,1)), axis=1)
  1318. noise_model_no = noise_model_no + 1
  1319. # Fill the physio_output_parameters dict
  1320. physio_output_parameters['RespiratoryOutput'].update({
  1321. "RespiratoryRateAverage(BPM)": round(physio_br_mean,2),
  1322. "RespiratoryRateStd(BPM)": round(physio_br_std, 2), "RespiratoryPeaksInRun": len(resp_peaks_ind_run)})
  1323. # If a PRT was loaded, sample the respiratory rate in ms resolution
  1324. if prt_task_file_name.endswith('.prt'):
  1325. physio_time_br_ms = np.arange(0, physio_time_10[-1], 1/1000)
  1326. f = interpolate.interp1d(physio_time_10, br_10)
  1327. br_ms = f(physio_time_br_ms)
  1328. del(physio_hz, physio_data, physio_triggers_ind, physio_triggers_sum,
  1329. physio_triggers_startslice_ind, physio_time)
  1330. # ____________________________________________________________________________
  1331. # 6. If Both Cardiac and Respiratory Signal are available merge cardiac and
  1332. # respiratory RETROICOR SDMs,
  1333. # if a multplicative term has been specified by the user, include it in the
  1334. # final RETROICOR SDM
  1335. # see Harvey et al., 2008:
  1336. # interaction between cardiac and respiratory processes, giving rise to
  1337. # amplitude modulation of the cardiac signal by the respiratory waveform
  1338. # (1. heart rate varies with respiration - known as respiratory sinus
  1339. # arrhythmia, 2. venous return to the heart is facilitated during
  1340. # inspiration - known as intrathoracic pump)
  1341. # see also: Kasper, L., Bollmann, S., Diaconescu, A. O., Hutton, C.,
  1342. # Heinzle, J., Iglesias, S., Stephan, K. E. (2017). The PhysIO Toolbox for
  1343. # Modeling Physiological Noise in fMRI Data. Journal of Neuroscience
  1344. # Methods, 276, 56-72. https://doi.org/10.1016/J.JNEUMETH.2016.10.019
  1345. # Merge and Save Complete RETROICOR Model
  1346. if 'pulse_col_dict' in locals() and 'resp_col_dict' in locals() and order_cardiac > 0:
  1347. sdm_crp = copy.deepcopy(sdm_cp)
  1348. colour_temp = np.linspace(225, 30, order_resp*2)
  1349. counter = 1
  1350. for i in range(0, order_resp*2, 2):
  1351. sdm_crp.add_predictor(sdm.Predictor('Resp_Cos' + str(counter), dm_phs_r[:,i], colour=[0, 0, int(colour_temp[i])]))
  1352. sdm_crp.add_predictor(sdm.Predictor('Resp_Sin' + str(counter), dm_phs_r[:,i+1], colour=[0, 0, int(colour_temp[i+1])]))
  1353. counter = counter + 1
  1354. del(i, counter, colour_temp)
  1355. # add multiplicative term to RETROICOR Model if specified by the user
  1356. # a first order interaction model results in 4 predictors
  1357. if order_cardresp > 0:
  1358. dm_phs_cr = np.zeros((fmr_no_volumes, order_cardresp*4))
  1359. for i in range(order_cardresp):
  1360. dm_phs_cr[:, (i*4)] = np.cos((i+1)*cardiac_phase_vol + (i+1)*resp_phase_vol)
  1361. dm_phs_cr[:, (i*4)+1] = np.sin((i+1)*cardiac_phase_vol + (i+1)*resp_phase_vol)
  1362. dm_phs_cr[:, (i*4)+2] = np.cos((i+1)*cardiac_phase_vol - (i+1)*resp_phase_vol)
  1363. dm_phs_cr[:, (i*4)+3] = np.sin((i+1)*cardiac_phase_vol - (i+1)*resp_phase_vol)
  1364. del(i)
  1365. colour_temp = np.linspace(225, 30, order_cardresp*4)
  1366. for i in range(0, order_cardresp*4, 4):
  1367. sdm_crp.add_predictor(sdm.Predictor('CardiacResp_' + str(i+1), dm_phs_cr[:,i], colour=[0, int(colour_temp[i]), 0]))
  1368. sdm_crp.add_predictor(sdm.Predictor('CardiacResp_' + str(i+2), dm_phs_cr[:,i+1], colour=[0, int(colour_temp[i+1]), 0]))
  1369. sdm_crp.add_predictor(sdm.Predictor('CardiacResp_' + str(i+3), dm_phs_cr[:,i+2], colour=[0, int(colour_temp[i+2]), 0]))
  1370. sdm_crp.add_predictor(sdm.Predictor('CardiacResp_' + str(i+4), dm_phs_cr[:,i+3], colour=[0, int(colour_temp[i+3]), 0]))
  1371. del(i, colour_temp)
  1372. physio_regressors_names.extend(sdm_crp.names[-(order_cardresp*4)-1:-1])
  1373. physio_regressors_matrix = np.append(physio_regressors_matrix, dm_phs_cr, axis=1)
  1374. noise_model_no = noise_model_no + order_cardresp*4
  1375. sdm_crp.save(physio_out_path + '/' + fmr_name + '_cardiacresp_RETROICOR.sdm')
  1376. physio_regressors_matrix = np.delete(physio_regressors_matrix, 0, 1)
  1377. # ____________________________________________________________________________
  1378. # 7. Correlate the resulting physiological measures with the task predictors
  1379. # specified in the SDM file
  1380. # if the user has specified a task design matrix, Pearson correlations
  1381. # between all task predictors and all physio predictors will be calculated
  1382. # and saved
  1383. if sdm_task_file_name.endswith('.sdm'):
  1384. # calcualte Pearson correlation coefficients and corresponding p-values
  1385. # in physio_task_noise_corr_matrix and physio_task_noise_corrpval_matrix
  1386. # remove all constant predictors, as these are not meaningful for correlation computation
  1387. task_names = np.array(sdm_task_name)
  1388. task_data = sdm_task_data
  1389. constant_task_columns = sdm_task_data == sdm_task_data[0,:]
  1390. mask_constants = ~constant_task_columns.all(0)
  1391. task_names = task_names[mask_constants].tolist()
  1392. task_data = task_data[:,mask_constants]
  1393. constantpreds = np.array(sdm_task_name)[constant_task_columns.all(0)].tolist()
  1394. # if a constant task predictor exists, add a note to the _OutputParameters.json file
  1395. if sdm_task_no_predictors > np.sum(mask_constants):
  1396. constantpreds = np.array(sdm_task_name)[constant_task_columns.all(0)].tolist()
  1397. if sdm_task_incl_constant:
  1398. constantpreds.remove('Constant')
  1399. if len(constantpreds) > 0:
  1400. print('Constant Task Regressors Excluded for Task x Physio Correlation: ', constantpreds, ' \n')
  1401. physio_output_parameters.update({
  1402. 'Constant SDM Task Regressors Detected': constantpreds
  1403. })
  1404. physio_task_noise_corr_matrix = np.zeros((len(physio_regressors_names), len(task_names)))
  1405. physio_task_noise_corrpval_matrix = np.zeros((len(physio_regressors_names), len(task_names)))
  1406. for n in range(len(physio_regressors_names)):
  1407. for t in range(len(task_names)):
  1408. # if there are NaNs in the created predictors, perform the correlation without the affected volumes
  1409. if np.sum(np.isnan(physio_regressors_matrix[:,n])) > 0:
  1410. ind_nan = np.argwhere(np.isnan(physio_regressors_matrix[:,n]))
  1411. x = np.delete(physio_regressors_matrix[:,n], ind_nan)
  1412. y = np.delete(task_data[:,t], ind_nan)
  1413. corr_p = stats.pearsonr(x,y)
  1414. del(x,y,ind_nan)
  1415. else:
  1416. corr_p = stats.pearsonr(physio_regressors_matrix[:,n], task_data[:,t])
  1417. physio_task_noise_corr_matrix[n,t] = corr_p[0]
  1418. physio_task_noise_corrpval_matrix[n,t] = corr_p[1]
  1419. del(corr_p)
  1420. df_corr = pd.DataFrame(physio_task_noise_corr_matrix, columns = ["correlation_" + item for item in task_names], index = physio_regressors_names)
  1421. df_pval = pd.DataFrame(physio_task_noise_corrpval_matrix, columns = ["pval_" + item for item in task_names], index = physio_regressors_names)
  1422. # Save resulting correlation matrix and corresponding p-values in tsv file
  1423. df_temp = pd.concat([df_corr, df_pval], axis=1)
  1424. df_temp.to_csv(physio_out_path + '/' + fmr_name + '_CorrelationMatrixNoiseTaskRegressors.tsv', sep="\t")
  1425. # Plot the the Pearson correlation coefficients in a heatmap
  1426. fig_tasknoisecorr = plt.figure('Task-Noise-Correlation', figsize=(10, 8))
  1427. temp = np.ma.masked_outside(physio_task_noise_corrpval_matrix, -min_pvalue, min_pvalue)
  1428. mask_sig = np.ma.getmaskarray(temp)
  1429. temp = np.ma.masked_inside(physio_task_noise_corrpval_matrix, -min_pvalue, min_pvalue)
  1430. mask_nonsig = np.ma.getmaskarray(temp)
  1431. del(temp)
  1432. ax = sns.heatmap(
  1433. physio_task_noise_corr_matrix,
  1434. vmin=np.min(physio_task_noise_corr_matrix),
  1435. vmax=np.max(physio_task_noise_corr_matrix),
  1436. center=0,
  1437. linewidth=0.3,
  1438. linecolor='black',
  1439. mask=mask_sig,
  1440. cmap='coolwarm',
  1441. annot=True,
  1442. annot_kws={'size':6, 'color':'black', 'fontweight':'bold'}) # sns.diverging_palette(20, 220, n=200))
  1443. ax2 = ax.twinx()
  1444. sns.heatmap(
  1445. np.round(physio_task_noise_corr_matrix, decimals=2),
  1446. center=0,
  1447. linewidth=0.3,
  1448. linecolor='black',
  1449. mask=mask_nonsig,
  1450. cmap=mpl.colors.ListedColormap(['white']),
  1451. annot=True,
  1452. cbar=False,
  1453. alpha=1,
  1454. ax = ax2,
  1455. xticklabels=False,
  1456. yticklabels=False,
  1457. annot_kws={'size':6, 'color':'black'}) # sns.diverging_palette(20, 220, n=200))
  1458. ax.set_xticks(np.arange(len(task_names)), labels=task_names, fontsize = 'xx-small')
  1459. ax.set_yticks(np.arange(len(physio_regressors_names)), labels=physio_regressors_names, fontsize = 'xx-small')
  1460. ax.set_title("Pearson Correlation Values of Task and Noise Regressors, with p < {0} ".format(min_pvalue))
  1461. # Rotate the tick labels and set their alignment.
  1462. plt.setp(ax.get_xticklabels(), rotation=45, ha="left")
  1463. plt.setp(ax.get_yticklabels(), rotation=360, va="top")
  1464. fig_tasknoisecorr.tight_layout()
  1465. fig_tasknoisecorr.savefig((physio_out_path + '/' + fmr_name + '_TaskNoiseCorr.png'), dpi=600, format='png')
  1466. if (len(sys.argv) == 1) or (len(sys.argv) > 1 and plotdisp.lower() == 'true'):
  1467. plt.show()
  1468. # ____________________________________________________________________________
  1469. # 8. Plot and calculate the heart and respiratory rate in relation to the
  1470. # stimulation protocol,
  1471. # if the user has specified a stimulation protocol, the PRT will be plotted
  1472. # and the mean heart- and/or respiratory rate per condition is saved
  1473. if prt_task_file_name.endswith('.prt'):
  1474. # Compute heart rate per event and condition if cardiac data exists
  1475. if 'pulse_col_dict' in locals():
  1476. physio_output_parameters['StimulationProtocol']['Cardiac'] = {}
  1477. for cond in range(len(protocol.conditions)):
  1478. physio_output_parameters['StimulationProtocol']['Cardiac'].update({('Mean_HeartRate_PerEvent ' + protocol.conditions[cond].name): []})
  1479. for event in range(np.size(protocol.conditions[cond].data,0)):
  1480. temp = np.mean(hr_ms[(physio_time_hr_ms*1000 >= protocol.conditions[cond].data[event,0]) & (physio_time_hr_ms*1000 <= protocol.conditions[cond].data[event,1])])
  1481. physio_output_parameters['StimulationProtocol']['Cardiac'][('Mean_HeartRate_PerEvent ' + protocol.conditions[cond].name)].append(np.round(temp,decimals=2))
  1482. del(temp)
  1483. temp_cond = np.mean(physio_output_parameters['StimulationProtocol']['Cardiac'][('Mean_HeartRate_PerEvent ' + protocol.conditions[cond].name)])
  1484. physio_output_parameters['StimulationProtocol']['Cardiac'].update({('Mean_HeartRate ' + protocol.conditions[cond].name): np.round(temp_cond,decimals=2)})
  1485. del(temp_cond)
  1486. # Compute breathing rate per event and condition if respiratory data exists
  1487. if 'resp_col_dict' in locals():
  1488. physio_output_parameters['StimulationProtocol']['Respiratory'] = {}
  1489. for cond in range(len(protocol.conditions)):
  1490. physio_output_parameters['StimulationProtocol']['Respiratory'].update({('Mean_BreathingRate_PerEvent ' + protocol.conditions[cond].name): []})
  1491. for event in range(np.size(protocol.conditions[cond].data,0)):
  1492. temp = np.mean(br_ms[(physio_time_br_ms*1000 >= protocol.conditions[cond].data[event,0]) & (physio_time_br_ms*1000 <= protocol.conditions[cond].data[event,1])])
  1493. physio_output_parameters['StimulationProtocol']['Respiratory'][('Mean_BreathingRate_PerEvent ' + protocol.conditions[cond].name)].append(np.round(temp,decimals=2))
  1494. del(temp)
  1495. temp_cond = np.mean(physio_output_parameters['StimulationProtocol']['Respiratory'][('Mean_BreathingRate_PerEvent ' + protocol.conditions[cond].name)])
  1496. physio_output_parameters['StimulationProtocol']['Respiratory'].update({('Mean_BreathingRate ' + protocol.conditions[cond].name): np.round(temp_cond,decimals=2)})
  1497. del(temp_cond)
  1498. ### PLOTTING PRT
  1499. fig_prt = plt.figure('Stimulation-Protocol', figsize=(10, 8))
  1500. ax_prt = fig_prt.add_subplot(111)
  1501. # set the limits to the scan duration
  1502. ax_prt.set_xlim(0, fmr_no_volumes*fmr_time_repeat/1000)
  1503. ax_prt.set_ylim(0, 2)
  1504. ax_prt.set_title('Stimulation Protocol: '+ fmr_name)
  1505. counter = 0
  1506. # plot heart rate, rescaled to arbitrary range
  1507. if 'pulse_col_dict' in locals():
  1508. hr_ms_norm = (((1.95-1.05)*(hr_ms-min(hr_ms))) / (max(hr_ms) - min(hr_ms))) + 1.05
  1509. plot_hr, = ax_prt.plot(physio_time_hr_ms, hr_ms_norm, linewidth=1, color='red', label='heart rate')
  1510. counter = counter + 1
  1511. # plot breathing rate, rescaled to arbitrary range
  1512. if "resp_col_dict" in locals():
  1513. br_ms_norm = (((0.95-0.05)*(br_ms-min(br_ms))) / (max(br_ms) - min(br_ms))) + 0.05
  1514. plot_br, = ax_prt.plot(physio_time_br_ms, br_ms_norm, linewidth=1, color='blue', label='respiratory rate')
  1515. counter = counter + 1
  1516. # get current handles of heart and/or respiratory rate
  1517. handles, labels = ax_prt.get_legend_handles_labels()
  1518. # add vertical separation between respiratory and heart rate
  1519. ax_prt.add_patch(Rectangle((0,1),fmr_no_volumes*fmr_time_repeat/1000,0.0005,
  1520. facecolor = 'black', edgecolor = 'black',
  1521. alpha=1))
  1522. # add text to indicate heart rate and breathing rate box
  1523. ax_prt.text(((fmr_no_volumes*fmr_time_repeat/1000)/2), 1.02, 'Cardiac', horizontalalignment ='center', fontsize=10)
  1524. ax_prt.text(((fmr_no_volumes*fmr_time_repeat/1000)/2), 0.02, 'Respiratory', horizontalalignment ='center', fontsize=10)
  1525. # plot the single PRT events
  1526. for cond in range(len(protocol.conditions)):
  1527. for event in range(np.size(protocol.conditions[cond].data,0)):
  1528. ax_prt.add_patch(Rectangle((protocol.conditions[cond].data[event,0]/1000,0),
  1529. (protocol.conditions[cond].data[event,1] - protocol.conditions[cond].data[event,0])/1000, 2,
  1530. facecolor = np.array(protocol.conditions[cond].colour)/255,
  1531. edgecolor = 'none',
  1532. alpha=0.3))
  1533. # create handles of the PRT conditions for the legend
  1534. prt_handles = [Patch(color= np.array(protocol.conditions[cond].colour)/255, label=protocol.conditions[cond].name) for cond in range(len(protocol.conditions))]
  1535. # add condition handles to original plot handles
  1536. handles = handles + prt_handles
  1537. ax_prt.set_yticks([])
  1538. ax_prt.set_ylabel('Signal Variation')
  1539. ax_prt.set_xlabel('Time (s)')
  1540. fig_prt.legend(handles= handles, ncol=len(protocol.conditions)+counter, loc=8)
  1541. fig_prt.savefig((physio_out_path + '/' + fmr_name + '_PRT_PhysioRate.png'), dpi=600, format='png')
  1542. if (len(sys.argv) == 1) or (len(sys.argv) > 1 and plotdisp.lower() == 'true'):
  1543. plt.show()
  1544. ### SAVING PHYSIO-PARAMETER FILES
  1545. # int32 is not JSON serializable -> convert to int
  1546. physio_input_parameters["ScriptParameters"]["ShiftsOfHeartRateRegressorSec"] = [int(item) for item in physio_input_parameters["ScriptParameters"]["ShiftsOfHeartRateRegressorSec"]]
  1547. physio_input_parameters["ScriptParameters"]["ShiftsOfRVTRegressorSec"] = [int(item) for item in physio_input_parameters["ScriptParameters"]["ShiftsOfRVTRegressorSec"]]
  1548. physio_input_parameters["ScriptParameters"]["ShiftsOfRVRegressorSec"] = [int(item) for item in physio_input_parameters["ScriptParameters"]["ShiftsOfRVRegressorSec"]]
  1549. physio_input_parameters["ScriptParameters"]["ShiftsOfENVRegressorSec"] = [int(item) for item in physio_input_parameters["ScriptParameters"]["ShiftsOfENVRegressorSec"]]
  1550. # remove the ndarrays from physio_input_parameters to save it as a json file
  1551. keys = ['IndicesScanTriggers', 'IndicesVolumeTriggers', 'PhysioData', 'PhysioTimeSec']
  1552. for key in keys:
  1553. physio_input_parameters['PhysioJsonFile'].pop(key, None)
  1554. # Save the physio_input_parameters as json file
  1555. with open((physio_out_path + '/' + fmr_name + '_PhysioProcessing_InputParameters.json'), 'w') as write_file:
  1556. json.dump(physio_input_parameters, write_file, indent=4)
  1557. physio_output_parameters['NoiseRegressors'] = physio_regressors_names
  1558. # Save the physio_output_parameters as json file
  1559. with open((physio_out_path + '/' + fmr_name + '_PhysioProcessing_OutputParameters.json'), 'w') as write_file:
  1560. json.dump(physio_output_parameters, write_file, indent=4)
  1561. # Save all created noise regressors in a single file for external use
  1562. df = pd.DataFrame(physio_regressors_matrix)
  1563. df.columns = physio_regressors_names
  1564. df.to_csv(physio_out_path + '/' + fmr_name + '_PhysiologicalNoiseRegressors.tsv', sep="\t")
  1565. # Restore the default plotting parameters after script is finished
  1566. mpl.rcParams.update(mpl.rcParamsDefault)

CreatePhysioPredictors_BIDS.py at commit 234eaa9, under MIT · at the source

Overview

Authors: Sara Ponticorvo1, Ekaterina Paasonen2,3, Pavel Filip4, Douglas L. Rothman5, Juha S. Valjakka2, Olli Gröhn2, Shalom Michaeli1, Silvia Mangia1
  1. Department of Radiology, Center for Magnetic Resonance Research University of Minnesota Minneapolis Minnesota USA
  2. A. I. Virtanen Institute for Molecular Sciences University of Eastern Finland Kuopio Finland
  3. Neurocenter Kuopio University Hospital Kuopio Finland
  4. General University Hospital, Charles University Prague Czech Republic
  5. Department of Radiology and Biomedical Imaging, Magnetic Resonance Research Center Yale University New Haven Connecticut USA
Journal: NMR in biomedicine, volume 39, issue 8, article e70342
Dates: received 2 December 2025; accepted 8 June 2026; published online 21 June 2026; in print August 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1002/nbm.70342 · PMID 42324687 · PMCID PMC13284447 · OpenAlex W7165549946
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: fMRI (modality), human (organism), stroke (population), cognitive (subfield)
Methods: Connectivity, Statistics, Spectral & time-frequency, fMRI & imaging, Smoothing, state filtering, decompositions, Physiology & signal measures
Keywords: CSF, fMRI, glymphatic system, high‐field, inflow, neurofluids, ultrashort echo time, zero echo time
MeSH: Brain*, Cerebrospinal Fluid*, Magnetic Resonance Imaging*, Adult, Brain Mapping, Cerebrovascular Circulation, Female, Humans, Male, Time Factors, Young Adult (* major topic)
Topic: Functional Brain Connectivity Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: NIH (P41 EB027061)
Citations: not cited yet (Europe PMC); 67 references in the paper

Abstract

Ultrashort echo time (UTE) and zero echo time (ZTE) techniques have been shown to generate robust functional MRI (fMRI) contrast of hemodynamic origin from the inflow of unperturbed spins into the imaging volume under local radiofrequency transmission. As such, they are ideally suited not only to map the dynamics of cerebral blood flow (CBF) during intense neuronal activity but also the dynamics of cerebrospinal fluid (CSF). The goal of this work was to pilot the use of UTE‐fMRI for human studies of brain activation and neurofluid dynamics. We thus conducted fMRI studies at 7 T with a transmit/receive head coil using a slab‐selective UTE sequence on 13 human participants during a visual task. Spatio‐temporally matched gradient‐echo echo planar imaging (GE‐EPI) data were also acquired for qualitative comparison purposes, and respiratory and cardiac signals were measured to quantify multiple physiological metrics. Functional brain activations were analyzed using a general linear model at single subject and group levels. Moreover, time‐courses from the whole cortex, carotid arteries, and fourth ventricle were correlated between each other, and physiological contributions to these ROI signals were evaluated with a linear mixed model and a relative importance computation. Robust and reproducible functional activations were detected with UTE‐fMRI in the visual cortex across participants. Although the functional contrast‐to‐noise ratio was higher with GE‐EPI than with UTE, the temporal signal‐to‐noise ratio was lower, and group‐level statistical power and activation patterns of UTE maps were similar to those obtained with GE‐EPI. In addition, the UTE task‐evoked signal in CSF was negatively correlated with those in the whole cortex and in the carotid arteries and was primarily driven by the stimulus paradigm. We conclude that UTE‐fMRI can be used not only for functional studies of the human brain but also for assessing the relationship between hemodynamic and CSF signals, which can help elucidate brain homeostatic processes.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 2 matches between paragraphs and lines of code.

BrainInnovation/physiocorr

License: MIT
State: the link answers, verified on 26 September 2026
Evidence: files inventoried
Commit: 234eaa957ae622a6956a1d298828ec8fa1bc73c1, 29 November 2022
Languages: Python (3)
Size: 5 files, 3 scripts
Software Heritage: not archived
Found in: the text, “Physiological Data Processing”
Holds: README, license file
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (3 files), Matplotlib (1 file), pandas (1 file), SciPy (1 file), seaborn (1 file)
Availability: 1 check, the latest on 26 September 2026: the link answers
  • 26 September 2026: the link answers
5 files

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 3 scripts, each with its path and the digest of its content;
  • 2 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data Availability Statement

Unprocessed data are not publicly available due to the sensitive nature of the dataset. However, data will be provided at a reasonable request addressed to the corresponding author.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 3, 28 September 2026

  • Publisher: n/a → Wiley

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 8 authors, 8 keywords, 11 MeSH terms, 1 funder, 62 references.

Cite

This paper

Ponticorvo, S., Paasonen, E., Filip, P., Rothman, D. L., Valjakka, J. S., Gröhn, O., Michaeli, S., & Mangia, S. (2026). Task-Evoked Functional Activation and Coupling With CSF Flow Detected in the Human Brain With Ultrashort Echo Time fMRI at 7 T. NMR in biomedicine, 39(8), e70342. https://doi.org/10.1002/nbm.70342

BibTeX

@article{ponticorvo2026task,
author = {Ponticorvo, Sara and Paasonen, Ekaterina and Filip, Pavel and Rothman, Douglas L. and Valjakka, Juha S. and Gröhn, Olli and Michaeli, Shalom and Mangia, Silvia},
title = {{Task-Evoked Functional Activation and Coupling With CSF Flow Detected in the Human Brain With Ultrashort Echo Time fMRI at 7 T}},
journal = {NMR in biomedicine},
year = {2026},
month = aug,
volume = {39},
number = {8},
pages = {e70342},
publisher = {Wiley},
issn = {0952-3480},
doi = {10.1002/nbm.70342},
url = {https://doi.org/10.1002/nbm.70342},
pmid = {42324687},
pmcid = {PMC13284447}
}

RIS

TY - JOUR
AU - Ponticorvo, Sara
AU - Paasonen, Ekaterina
AU - Filip, Pavel
AU - Rothman, Douglas L.
AU - Valjakka, Juha S.
AU - Gröhn, Olli
AU - Michaeli, Shalom
AU - Mangia, Silvia
TI - Task-Evoked Functional Activation and Coupling With CSF Flow Detected in the Human Brain With Ultrashort Echo Time fMRI at 7 T
T2 - NMR in biomedicine
J2 - NMR Biomed
PY - 2026
DA - 2026/08/01
VL - 39
IS - 8
SP - e70342
SN - 0952-3480
PB - Wiley
DO - 10.1002/nbm.70342
UR - https://doi.org/10.1002/nbm.70342
LA - en
ER -

CSL-JSON

{
"id": "10.1002/nbm.70342",
"type": "article-journal",
"title": "Task-Evoked Functional Activation and Coupling With CSF Flow Detected in the Human Brain With Ultrashort Echo Time fMRI at 7 T",
"container-title": "NMR in biomedicine",
"author": [
{
"family": "Ponticorvo",
"given": "Sara"
},
{
"family": "Paasonen",
"given": "Ekaterina"
},
{
"family": "Filip",
"given": "Pavel"
},
{
"family": "Rothman",
"given": "Douglas L."
},
{
"family": "Valjakka",
"given": "Juha S."
},
{
"family": "Gröhn",
"given": "Olli"
},
{
"family": "Michaeli",
"given": "Shalom"
},
{
"family": "Mangia",
"given": "Silvia"
}
],
"container-title-short": "NMR Biomed",
"volume": "39",
"issue": "8",
"page": "e70342",
"DOI": "10.1002/nbm.70342",
"PMID": "42324687",
"PMCID": "PMC13284447",
"ISSN": "0952-3480",
"publisher": "Wiley",
"URL": "https://doi.org/10.1002/nbm.70342",
"language": "en",
"issued": {
"date-parts": [
[
2026,
8,
1
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1002/mrm.70438 [code]
Quantifying Cardiac, Respiratory, and Low Frequency Components of CSF Motion From fMRI Inflow Effects.
Journal: Magnetic resonance in medicine
In common: fMRI, 7 references
[2] doi:10.1038/s41398-026-04157-5 [code]
Association of glymphatic function with 40-Hz neural oscillations, systemic metabolic markers, and cognitive performance in healthy aging adults: An EEG and MRI study.
Journal: Translational psychiatry
In common: seaborn, pandas, SciPy, 2 other tools, fMRI, 4 references
[3] doi:10.1186/s12888-026-08304-6
Transcranial direct current stimulation improves reduced global BOLD-CSF coupling in patients with insomnia disorder and comorbid anxiety: a resting-state functional MRI study.
Journal: BMC psychiatry
In common: fMRI, 6 references
[4] doi:10.1162/netn.a.547 [code]
An evaluation of the efficacy of single-echo and multi-echo fMRI denoising strategies.
Journal: Network neuroscience (Cambridge, Mass.)
In common: seaborn, pandas, SciPy, 2 other tools, fMRI, 2 references
[5] doi:10.1162/netn.a.570 [code]
Higher-order statistics for constructing centered edge functional connectivity.
Journal: Network neuroscience (Cambridge, Mass.)
In common: seaborn, pandas, SciPy, 2 other tools, fMRI, 2 references
[6] doi:10.1038/s41467-026-76306-9 [code]
Non-invasive characterization of perivascular subarachnoid spaces.
Journal: Nature communications
In common: SciPy, Matplotlib, NumPy, 3 references
[7] doi:10.1073/pnas.2517059123 [code]
Stretch and flow at the gliovascular interface: High-fidelity modeling of astrocyte endfeet.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: seaborn, pandas, SciPy, 2 other tools, 2 references
[8] doi:10.21203/rs.3.rs-9326213/v1 [code]
Multi-task fMRI outperforms resting-state fMRI for revealing task-invariant organization of the human brain
Journal: Research Square (preprint)
In common: seaborn, pandas, SciPy, 2 other tools, fMRI, cognitive, 1 reference
[9] doi:10.1371/journal.pbio.3003684 [code]
The retrieval of previously learned motor memories is facilitated by the reinstatement of default mode network manifold structures.
Journal: PLoS biology
In common: seaborn, pandas, SciPy, 2 other tools, fMRI, cognitive, 1 reference
[10] doi:10.64898/2026.03.09.710558 [code]
Multi-task fMRI outperforms resting-state fMRI for revealing task-invariant organization of the human brain
Journal: bioRxiv (preprint)
In common: seaborn, pandas, SciPy, 2 other tools, fMRI, cognitive, 1 reference

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.