11 Univariate GLM: Relative time
We want to run a GLM with the relative time of other events as a parametric modulator for activity evoked by picture presentations in the picture viewing task. We will use the already-preprocessed data and use some FSL tools to implement the GLM
11.1 Copy FEAT-folder from Preprocessing to First-Level GLM directory
First, set up the folder we are working in. Make sure it’s empty when we start and then copy the preprocessed data there because we don’t need to repeat the preprocessing.
$level1glm_dir <- here("data", "mri", "processed", "glm_rel_time" ) dirs
# if the directory is not empty delete everything for a fresh start
if(is_empty(dir(path = dirs$level1glm_dir)) | !length(dir(path = dirs$level1glm_dir)) == 0) {
# delete the entire directory and create it again
# unlink(dirs$level1glm_dir, recursive = TRUE)
dir.create(dirs$level1glm_dir)
}
# copy preprocessing FEATs to directory (~45 mins)
if(length(dir(path = dirs$level1glm_dir)) == 0) {
<- Sys.time()
start_time print('Copying preprocessed data to first level folder')
for (i_sub in subjects){
# copy the preprocessed data
dir.create(file.path(dirs$level1glm_dir, sprintf("VIRTEM_P%s", i_sub)), recursive = TRUE)
<- list.dirs(file.path(dirs$feat_dir, sprintf("VIRTEM_P%s", i_sub)), recursive = FALSE)
to_copy <- grep("RSA_02", to_copy, value = TRUE)
to_copy file.copy(from = to_copy, to = file.path(dirs$level1glm_dir, sprintf("VIRTEM_P%s", i_sub)), recursive = TRUE)
}
<- Sys.time()
end_time -start_time
end_time }
11.2 Prepare GLM
Compute relative time
First, we need to calculate, for each event, the mean relative time of events at the same sequence position in the other sequences. As a difference to the main behavioral analyses, we use the absolute relative time of other events.
# load data from CSV
<- file.path(dirs$data4analysis, "behavioral_data.txt")
fname <- cols_only(
col_types_list sub_id = col_factor(),
day = col_factor(),
event = col_factor(),
pic = col_integer(),
virtual_time = col_double(),
real_time = col_double(),
memory_time = col_double(),
memory_order = col_double(),
sorted_day = col_integer()
)<- as_tibble(read_csv(fname, col_types = col_types_list))
beh_data
head(beh_data)
# quantify the deviation in virtual time for each event relative to other events
# at the same sequence position
$rel_time_other_events<-NA
beh_data
for (i_day in 1:n_days){
for (i_event in 1:n_events_day){
# find the events at this sequence position from all four sequences
<- beh_data[beh_data$event == i_event,]
curr_events
# find the average virtual time of the other events at this sequence position
<- curr_events %>%
avg_vir_time_other_events filter(day != i_day) %>%
summarise(avg_vir_time = mean(virtual_time)) %>%
select(avg_vir_time) %>%
as.numeric(.)
# for the given event, store the relative time of the other events,
# as the absolute difference in virtual time (positive values mean other events happen later)
<- beh_data$day==i_day & beh_data$event==i_event
event_idx $rel_time_other_events[event_idx] <- abs(avg_vir_time_other_events - beh_data$virtual_time[event_idx])
beh_data
}
}
# center the relative time
%>%
beh_data mutate(rel_time_other_events = scale(rel_time_other_events, center = TRUE, scale = FALSE))
Write custom EV files for FSL FEAT
As a first step, let’s write the EVs to file. For that we load the log from the post-learning picture viewing, split it into the ten blocks of each of those runs, and reference the onset times of the event image presentations to the first volume of the scanning runs. Then we write out the custom EV files:
- picture presentations
- relative time parametric modulator for picture presentations
- catch trial picture presentations
#Write custom EV files for FSL FEAT
for (i_sub in subjects){
# define the directory we want to use
<- file.path(dirs$level1glm_dir, "feat_evs", i_sub)
feat_in_dir if (!dir.exists(feat_in_dir)){dir.create(feat_in_dir, recursive = TRUE)}
for (i_run in 2:n_runs){
# load the logfile for this run (pre or post)
<- file.path(dirs$pvt_log_dir, sprintf('P%s_%svirtem.txt', i_sub, runs[i_run]))
log_fn <- read.table(log_fn)
log colnames(log) <- c("pic", "fix_start", "pic_start", "volume", "response", "RT", "trial_end")
# split the log file into the 10 blocks
<- split(log, rep(1:10, each = 21))
log_split
# reference volume numbers to the first volume of that block
<- lapply(log_split, function(x){x$volume - x$volume[1]})
vol_block <- mapply(cbind, log_split, "vol_block"=vol_block, SIMPLIFY=FALSE)
log_split
# reference picture starts to 0 for each block
<- lapply(log_split, function(x){((x$vol_block)*2270)/1000})
pic_start_block <- mapply(cbind, log_split, "pic_start_block"=pic_start_block, SIMPLIFY=FALSE)
log_split
for (i_block in 1:10){
# write picture onset EV file
<- file.path(feat_in_dir, sprintf("%s_%s_%02d_%s.txt",
fn "pic_on"))
i_sub, runs[i_run], i_block, %>%
log_split[[i_block]] filter(pic != 21) %>%
select(pic_start_block) %>%
mutate(duration = 2.5,
weight = 1) %>%
write.table(., file = fn, row.names = FALSE, col.names = FALSE, sep = "\t")
# write parametric regressor for picture onsets to EV file based on relative time of other events
# get the relative times for pictures (sorted 1-20)
<- beh_data %>% filter(sub_id == as.numeric(i_sub)) %>%
rel_times arrange(pic) %>%
pull(rel_time_other_events)
<- file.path(feat_in_dir, sprintf("%s_%s_%02d_%s.txt",
fn "rel_time"))
i_sub, runs[i_run], i_block, %>%
log_split[[i_block]] filter(pic != 21) %>%
arrange(pic) %>%
mutate(rel_time = rel_times) %>%
arrange(pic_start_block) %>%
select(pic_start_block, rel_time) %>%
mutate(duration = 2.5) %>%
relocate(duration, .before = "rel_time") %>%
write.table(., file = fn, row.names = FALSE, col.names = FALSE, sep = "\t")
# write catch picture onset EV file
<- file.path(feat_in_dir, sprintf("%s_%s_%02d_%s.txt",
fn "catch_pic_on"))
i_sub, runs[i_run], i_block, %>%
log_split[[i_block]] filter(pic == 21) %>%
select(pic_start_block) %>%
mutate(duration = 2.5,
weight = 1) %>%
write.table(., file = fn, row.names = FALSE, col.names = FALSE, sep = "\t")
# write button press EV file
<- file.path(feat_in_dir, sprintf("%s_%s_%02d_%s.txt",
fn "button_press"))
i_sub, runs[i_run], i_block, %>%
log_split[[i_block]] filter(response == 1) %>%
mutate(press = pic_start_block + RT/1000,
duration = 0,
weight = 1) %>%
select(press, duration, weight) %>%
write.table(., file = fn, row.names = FALSE, col.names = FALSE, sep = "\t")
}
} }
GLM Design files
Now that we have the relevant EV files, let’s set up the GLM using the tools from FSL FEAT. We create a configuration file for each block of each participant.
<- '
dsgn # FEAT version number
set fmri(version) 6.00
# Are we in MELODIC?
set fmri(inmelodic) 0
# Analysis level
# 1 : First-level analysis
# 2 : Higher-level analysis
set fmri(level) 1
# Which stages to run
# 0 : No first-level analysis (registration and/or group stats only)
# 7 : Full first-level analysis
# 1 : Pre-processing
# 2 : Statistics
set fmri(analysis) 2
# Use relative filenames
set fmri(relative_yn) 0
# Balloon help
set fmri(help_yn) 1
# Run Featwatcher
set fmri(featwatcher_yn) 0
# Cleanup first-level standard-space images
set fmri(sscleanup_yn) 0
# Output directory
set fmri(outputdir) ""
# TR(s)
set fmri(tr) 2.270000
# Total volumes
set fmri(npts) @n_vols@
# Delete volumes
set fmri(ndelete) 0
# Perfusion tag/control order
set fmri(tagfirst) 1
# Number of first-level analyses
set fmri(multiple) 1
# Higher-level input type
# 1 : Inputs are lower-level FEAT directories
# 2 : Inputs are cope images from FEAT directories
set fmri(inputtype) 1
# Carry out pre-stats processing?
set fmri(filtering_yn) 0
# Brain/background threshold, %
set fmri(brain_thresh) 10
# Critical z for design efficiency calculation
set fmri(critical_z) 5.3
# Noise level
set fmri(noise) 0.66
# Noise AR(1)
set fmri(noisear) 0.34
# Motion correction
# 0 : None
# 1 : MCFLIRT
set fmri(mc) 1
# Spin-history (currently obsolete)
set fmri(sh_yn) 0
# B0 fieldmap unwarping?
set fmri(regunwarp_yn) 0
# EPI dwell time (ms)
set fmri(dwell) 0.235
# EPI TE (ms)
set fmri(te) 24
# % Signal loss threshold
set fmri(signallossthresh) 10
# Unwarp direction
set fmri(unwarp_dir) y-
# Slice timing correction
# 0 : None
# 1 : Regular up (0, 1, 2, 3, ...)
# 2 : Regular down
# 3 : Use slice order file
# 4 : Use slice timings file
# 5 : Interleaved (0, 2, 4 ... 1, 3, 5 ... )
set fmri(st) 0
# Slice timings file
set fmri(st_file) ""
# BET brain extraction
set fmri(bet_yn) 1
# Spatial smoothing FWHM (mm)
set fmri(smooth) 0
# Intensity normalization
set fmri(norm_yn) 0
# Perfusion subtraction
set fmri(perfsub_yn) 0
# Highpass temporal filtering
set fmri(temphp_yn) 1
# Lowpass temporal filtering
set fmri(templp_yn) 0
# MELODIC ICA data exploration
set fmri(melodic_yn) 0
# Carry out main stats?
set fmri(stats_yn) 1
# Carry out prewhitening?
set fmri(prewhiten_yn) 1
# Add motion parameters to model
# 0 : No
# 1 : Yes
set fmri(motionevs) 0
set fmri(motionevsbeta) ""
set fmri(scriptevsbeta) ""
# Robust outlier detection in FLAME?
set fmri(robust_yn) 0
# Higher-level modelling
# 3 : Fixed effects
# 0 : Mixed Effects: Simple OLS
# 2 : Mixed Effects: FLAME 1
# 1 : Mixed Effects: FLAME 1+2
set fmri(mixed_yn) 2
# Number of EVs
set fmri(evs_orig) 3
set fmri(evs_real) 6
set fmri(evs_vox) 0
# Number of contrasts
set fmri(ncon_orig) 1
set fmri(ncon_real) 1
# Number of F-tests
set fmri(nftests_orig) 0
set fmri(nftests_real) 0
# Add constant column to design matrix? (obsolete)
set fmri(constcol) 0
# Carry out post-stats steps?
set fmri(poststats_yn) 0
# Pre-threshold masking?
set fmri(threshmask) ""
# Thresholding
# 0 : None
# 1 : Uncorrected
# 2 : Voxel
# 3 : Cluster
set fmri(thresh) 3
# P threshold
set fmri(prob_thresh) 0.05
# Z threshold
set fmri(z_thresh) 2.3
# Z min/max for colour rendering
# 0 : Use actual Z min/max
# 1 : Use preset Z min/max
set fmri(zdisplay) 0
# Z min in colour rendering
set fmri(zmin) 2
# Z max in colour rendering
set fmri(zmax) 8
# Colour rendering type
# 0 : Solid blobs
# 1 : Transparent blobs
set fmri(rendertype) 1
# Background image for higher-level stats overlays
# 1 : Mean highres
# 2 : First highres
# 3 : Mean functional
# 4 : First functional
# 5 : Standard space template
set fmri(bgimage) 1
# Create time series plots
set fmri(tsplot_yn) 1
# Registration to initial structural
set fmri(reginitial_highres_yn) 0
# Search space for registration to initial structural
# 0 : No search
# 90 : Normal search
# 180 : Full search
set fmri(reginitial_highres_search) 90
# Degrees of Freedom for registration to initial structural
set fmri(reginitial_highres_dof) 6
# Registration to main structural
set fmri(reghighres_yn) 0
# Search space for registration to main structural
# 0 : No search
# 90 : Normal search
# 180 : Full search
set fmri(reghighres_search) 90
# Degrees of Freedom for registration to main structural
set fmri(reghighres_dof) BBR
# Registration to standard image?
set fmri(regstandard_yn) 1
# Use alternate reference images?
set fmri(alternateReference_yn) 0
# Standard image
set fmri(regstandard) "/afs/cbs/software/fsl/6.0.3/ubuntu-bionic-amd64/data/standard/MNI152_T1_1mm_brain"
# Search space for registration to standard space
# 0 : No search
# 90 : Normal search
# 180 : Full search
set fmri(regstandard_search) 90
# Degrees of Freedom for registration to standard space
set fmri(regstandard_dof) 12
# Do nonlinear registration from structural to standard space?
set fmri(regstandard_nonlinear_yn) 0
# Control nonlinear warp field resolution
set fmri(regstandard_nonlinear_warpres) 10
# High pass filter cutoff
set fmri(paradigm_hp) 100
# Total voxels
set fmri(totalVoxels) 366912000
# Number of lower-level copes feeding into higher-level analysis
set fmri(ncopeinputs) 0
# 4D AVW data or FEAT directory (1)
set feat_files(1) "@level1glm_dir@/VIRTEM_P@sub_ID@/RSA_@run_no@_Block@block_no@.feat"
# Add confound EVs text file
set fmri(confoundevs) 1
# Confound EVs text file for analysis 1
set confoundev_files(1) "@level1glm_dir@/VIRTEM_P@sub_ID@/RSA_@run_no@_Block@block_no@.feat/mc/prefiltered_func_data_mcf.par"
# Subjects structural image for analysis 1
set highres_files(1) "/home/memspa/lordeu/Projects/virtem/mri/raw/Nifti/VIRTEM_P@sub_ID@/VIRTEM_P@sub_ID@_t1_co_brain"
# EV 1 title
set fmri(evtitle1) "pic_onset"
# Basic waveform shape (EV 1)
# 0 : Square
# 1 : Sinusoid
# 2 : Custom (1 entry per volume)
# 3 : Custom (3 column format)
# 4 : Interaction
# 10 : Empty (all zeros)
set fmri(shape1) 3
# Convolution (EV 1)
# 0 : None
# 1 : Gaussian
# 2 : Gamma
# 3 : Double-Gamma HRF
# 4 : Gamma basis functions
# 5 : Sine basis functions
# 6 : FIR basis functions
set fmri(convolve1) 2
# Convolve phase (EV 1)
set fmri(convolve_phase1) 0
# Apply temporal filtering (EV 1)
set fmri(tempfilt_yn1) 1
# Add temporal derivative (EV 1)
set fmri(deriv_yn1) 1
# Custom EV file (EV 1)
set fmri(custom1) "@level1glm_dir@/feat_evs/@sub_ID@/@sub_ID@_@run_name@_@block_no@_pic_on.txt"
# Gamma sigma (EV 1)
set fmri(gammasigma1) 3
# Gamma delay (EV 1)
set fmri(gammadelay1) 6
# Orthogonalise EV 1 wrt EV 0
set fmri(ortho1.0) 0
# Orthogonalise EV 1 wrt EV 1
set fmri(ortho1.1) 0
# Orthogonalise EV 1 wrt EV 2
set fmri(ortho1.2) 0
# Orthogonalise EV 1 wrt EV 3
set fmri(ortho1.3) 0
# EV 2 title
set fmri(evtitle2) "rel_time_other"
# Basic waveform shape (EV 2)
# 0 : Square
# 1 : Sinusoid
# 2 : Custom (1 entry per volume)
# 3 : Custom (3 column format)
# 4 : Interaction
# 10 : Empty (all zeros)
set fmri(shape2) 3
# Convolution (EV 2)
# 0 : None
# 1 : Gaussian
# 2 : Gamma
# 3 : Double-Gamma HRF
# 4 : Gamma basis functions
# 5 : Sine basis functions
# 6 : FIR basis functions
set fmri(convolve2) 2
# Convolve phase (EV 2)
set fmri(convolve_phase2) 0
# Apply temporal filtering (EV 2)
set fmri(tempfilt_yn2) 1
# Add temporal derivative (EV 2)
set fmri(deriv_yn2) 1
# Custom EV file (EV 2)
set fmri(custom2) "@level1glm_dir@/feat_evs/@sub_ID@/@sub_ID@_@run_name@_@block_no@_rel_time.txt"
# Gamma sigma (EV 2)
set fmri(gammasigma2) 3
# Gamma delay (EV 2)
set fmri(gammadelay2) 6
# Orthogonalise EV 2 wrt EV 0
set fmri(ortho2.0) 0
# Orthogonalise EV 2 wrt EV 1
set fmri(ortho2.1) 0
# Orthogonalise EV 2 wrt EV 2
set fmri(ortho2.2) 0
# Orthogonalise EV 2 wrt EV 3
set fmri(ortho2.3) 0
# EV 3 title
set fmri(evtitle3) "catch_pic_on"
# Basic waveform shape (EV 3)
# 0 : Square
# 1 : Sinusoid
# 2 : Custom (1 entry per volume)
# 3 : Custom (3 column format)
# 4 : Interaction
# 10 : Empty (all zeros)
set fmri(shape3) 3
# Convolution (EV 3)
# 0 : None
# 1 : Gaussian
# 2 : Gamma
# 3 : Double-Gamma HRF
# 4 : Gamma basis functions
# 5 : Sine basis functions
# 6 : FIR basis functions
set fmri(convolve3) 2
# Convolve phase (EV 3)
set fmri(convolve_phase3) 0
# Apply temporal filtering (EV 3)
set fmri(tempfilt_yn3) 1
# Add temporal derivative (EV 3)
set fmri(deriv_yn3) 1
# Custom EV file (EV 3)
set fmri(custom3) "@level1glm_dir@/feat_evs/@sub_ID@/@sub_ID@_@run_name@_@block_no@_catch_pic_on.txt"
# Gamma sigma (EV 3)
set fmri(gammasigma3) 3
# Gamma delay (EV 3)
set fmri(gammadelay3) 6
# Orthogonalise EV 3 wrt EV 0
set fmri(ortho3.0) 0
# Orthogonalise EV 3 wrt EV 1
set fmri(ortho3.1) 0
# Orthogonalise EV 3 wrt EV 2
set fmri(ortho3.2) 0
# Orthogonalise EV 3 wrt EV 3
set fmri(ortho3.3) 0
# Contrast & F-tests mode
# real : control real EVs
# orig : control original EVs
set fmri(con_mode_old) orig
set fmri(con_mode) orig
# Display images for contrast_real 1
set fmri(conpic_real.1) 1
# Title for contrast_real 1
set fmri(conname_real.1) ""
# Real contrast_real vector 1 element 1
set fmri(con_real1.1) 0.0
# Real contrast_real vector 1 element 2
set fmri(con_real1.2) 0
# Real contrast_real vector 1 element 3
set fmri(con_real1.3) 1.0
# Real contrast_real vector 1 element 4
set fmri(con_real1.4) 0
# Real contrast_real vector 1 element 5
set fmri(con_real1.5) 0
# Real contrast_real vector 1 element 6
set fmri(con_real1.6) 0
# Display images for contrast_orig 1
set fmri(conpic_orig.1) 1
# Title for contrast_orig 1
set fmri(conname_orig.1) ""
# Real contrast_orig vector 1 element 1
set fmri(con_orig1.1) 0.0
# Real contrast_orig vector 1 element 2
set fmri(con_orig1.2) 1.0
# Real contrast_orig vector 1 element 3
set fmri(con_orig1.3) 0
# Contrast masking - use >0 instead of thresholding?
set fmri(conmask_zerothresh_yn) 0
# Do contrast masking at all?
set fmri(conmask1_1) 0
##########################################################
# Now options that do not appear in the GUI
# Alternative (to BETting) mask image
set fmri(alternative_mask) ""
# Initial structural space registration initialisation transform
set fmri(init_initial_highres) ""
# Structural space registration initialisation transform
set fmri(init_highres) ""
# Standard space registration initialisation transform
set fmri(init_standard) ""
# For full FEAT analysis: overwrite existing .feat output dir?
set fmri(overwrite_yn) 0
'
# folder for fsf files and copy template there
<- file.path(dirs$level1glm_dir, "fsf")
fsf_dir if(!dir.exists(fsf_dir)){dir.create(fsf_dir)}
#file.copy(from = here("virtem_code", "unused_do_not_delete", "feat_level1glm_relTimeOther.fsf"),
# to = file.path(fsf_dir, "feat_level1glm_template.fsf"))
# set up data frame
<- tibble(sub_id = subjects %>% rep(each = n_blocks))
df $run_name <- "post" #runs %>% rep(each = 10) %>% rep(n_subs)
df$run_no <- 2 #c(1,2) %>% rep(each = 10) %>% rep(n_subs)
df$block <- c(1:10) %>% rep(n_subs) # c(1:10) %>% rep(n_runs) %>% rep(n_subs)
df$fn <- file.path(fsf_dir, sprintf("%s_%s_%02d.fsf", df$sub_id, df$run_name, df$block))
df$in_dir <- dirs$level1glm_dir
df$filtered_func <- file.path(dirs$level1glm_dir, sprintf("VIRTEM_P%s", df$sub_id),
dfsprintf("RSA_%02d_Block%02d.feat", df$run_no, df$block),
"filtered_func_data.nii.gz")
$fsf_dir <- fsf_dir
df
# define function to substitute placeholders
<- function(df = df[1,], fsf_lines = dsgn){
replace_fsf
# read in the template file
#fn <- file.path(df$fsf_dir, "feat_level1glm_template.fsf")
#fsf_lines <- read_delim(fn, delim = "\r",
# col_names = FALSE, col_types = cols(col_character()))
# replace the path
<- mgsub(pattern = "@level1glm_dir@",
fsf_lines replacement = df$in_dir, string = fsf_lines)
# replace the subject ID
<- mgsub(pattern = "@sub_ID@",
fsf_lines replacement = df$sub_id, string = fsf_lines)
# replace character run name
<- mgsub(pattern = "@run_name@",
fsf_lines replacement = df$run_name, string = fsf_lines)
<- mgsub(pattern = "@run_no@",
fsf_lines replacement = str_pad(as.character(df$run_no),
width = 2, side = "left", pad = "0"),
string = fsf_lines)
# replace block number
<- mgsub(pattern = "@block_no@",
fsf_lines replacement = str_pad(as.character(df$block),
width = 2, side = "left", pad = "0"),
string = fsf_lines)
# replace the number of volumes in 4D-nifti based on Header info
# min throws a warning because min is called with no data, but much faster
<- readNIfTI2(df$filtered_func, read_data = FALSE)
nii <- mgsub(pattern = "@n_vols@",
fsf_lines replacement = nii@dim_[5], string = fsf_lines)
# write to file
<- file(df$fn)
con writeLines(fsf_lines, con)
close(con)
}
# create FSF info for each row of the data frame (i.e. each block of each subject)
for (i in 1:nrow(df)){replace_fsf(df = df[i,], fsf_lines = dsgn)}
Prepare confound EV
We want to include the motion parameters from FEAT as confound regressors. Thus, we copy them from the mc subfolder of the FEAT directory and save them using just one space as a delimiter so they can be used with feat_model.
for (i in 1:nrow(df)){
<- file.path(df$in_dir[i], sprintf("VIRTEM_P%s",df$sub_id[i]),
mcpar_fn sprintf("RSA_%02d_Block%02d.feat", df$run_no[i], df$block[i]),
"mc", "prefiltered_func_data_mcf.par")
<- file.path(df$in_dir[i], sprintf("VIRTEM_P%s",df$sub_id[i]),
conf_ev_fn sprintf("RSA_%02d_Block%02d.feat", df$run_no[i], df$block[i]), "mcf.txt")
# paste to the correct file using only one space as delimiter
system(sprintf("paste -d ' ' %s > %s", mcpar_fn, conf_ev_fn))
}
Create design matrix
Next, we call feat_model to create the design matrix and contrast file needed to run the GLM from the fsf-configuration.
for (i in 1:nrow(df)){
<- file.path(df$in_dir[i], sprintf("VIRTEM_P%s",df$sub_id[i]),
fsf_fn sprintf("RSA_%02d_Block%02d.feat", df$run_no[i], df$block[i]),
"design.fsf")
<- file.path(df$in_dir[i], sprintf("VIRTEM_P%s",df$sub_id[i]),
conf_ev_fn sprintf("RSA_%02d_Block%02d.feat", df$run_no[i], df$block[i]), "mcf.txt")
# copy fsf to Feat folder
file.copy(from = df$fn[i],
to = file.path(df$in_dir[i], sprintf("VIRTEM_P%s",df$sub_id[i]),
sprintf("RSA_%02d_Block%02d.feat", df$run_no[i], df$block[i]),
"design.fsf"),
overwrite = TRUE)
# run feat_model design to create files for film_gls
system(sprintf("feat_model %s %s", tools::file_path_sans_ext(fsf_fn), conf_ev_fn))
}
11.3 Run 1st-level GLM on HTCondor
Finally, we can run the GLM. For that, we create a small shell script that has the film_gls command and that deletes the residual file after finishing. This is used as the executable for a task list that specifies the specific files to use for each block of each participant.
# folder for condor output
<- here("htc_logs", "rel_time_glm_level1")
htc_dir if(!exists(htc_dir)){dir.create(htc_dir, recursive = TRUE)}
# write shell script that runs the GLM and removes the residuals afterwards
<- file.path(htc_dir, "rel_time_glm_level1")
fn_sh <- file(fn_sh)
con open(con, "w")
writeLines("#!/bin/bash
/usr/share/fsl/5.0/bin/film_gls --in=$1 --rn=$2 --pd=$3 --thr=1000.0 --sa --ms=5 --con=$4
rm $5
", con)
close(con)
system(sprintf("chmod +x %s",fn_sh))
# write the submit file
<- file.path(htc_dir, "rel_time_glm_level1_tasklist.txt")
fn <- file(fn)
con open(con, "w")
writeLines(c(
"universe = vanilla",
sprintf("executable = %s", fn_sh),
"request_memory = 5000",
"request_cpus = 1",
"getenv = True",
"notification = error"
),con)
=0
cfor (i in 1:nrow(df)){
<- file.path(df$in_dir[i], sprintf("VIRTEM_P%s",df$sub_id[i]),
stat_pth sprintf("RSA_%02d_Block%02d.feat", df$run_no[i], df$block[i]), "stats")
<- file.path(stat_pth, "zstat1.nii.gz")
chk_fn
if (!file.exists(chk_fn)){
if(dir.exists(stat_pth)){unlink(stat_pth, recursive = TRUE)}
writeLines(c(
sprintf("arguments = %s %s %s %s %s",
$filtered_func[i],
df
stat_pth,file.path(df$in_dir[i], sprintf("VIRTEM_P%s",df$sub_id[i]),
sprintf("RSA_%02d_Block%02d.feat", df$run_no[i], df$block[i]),
"design.mat"),
file.path(df$in_dir[i], sprintf("VIRTEM_P%s",df$sub_id[i]),
sprintf("RSA_%02d_Block%02d.feat", df$run_no[i], df$block[i]),
"design.con"),
file.path(stat_pth, "res4d.nii.gz")
),sprintf("output = %s",
file.path(htc_dir, sprintf("%s_%s_block%d.out",
$sub_id[i], df$run_name[i], df$block[i]))),
dfsprintf("error = %s",
file.path(htc_dir, sprintf("%s_%s_block%d.err",
$sub_id[i], df$run_name[i], df$block[i]))),
dfsprintf("log = %s",
file.path(htc_dir, sprintf("%s_%s_block%d.log",
$sub_id[i], df$run_name[i], df$block[i]))),
dfsprintf("queue")
),con)=c+1
c
}
}close(con)
# submit to cluster
<- system(paste("condor_submit", fn), intern = TRUE)
batch_id
# find batch ID and wait
<- regmatches(batch_id[2], gregexpr("[[:digit:]]+", batch_id[2]))[[1]][2]
batch_id sprintf("submitted jobs (ID = %s) for 1st level GLM for parametric modulation by relative time. Time to wait...", batch_id)
pause_until_batch_done(batch_id = batch_id, wait_interval = 300)
11.4 Group-level
Move first-level images to MNI space
The first level output of each block is in each participant’s common functional space. For group-level stats we need to register the outputs to MNI (1mm) space.
# name of transformation matrix file to move from functional space to MNI 1mm
<- file.path(dirs$level1glm_dir, sprintf("VIRTEM_P%s", rep(subjects, each = n_blocks)),
func2standard sprintf("RSA_%02d_Block%02d.feat", 2, rep(1:n_blocks, n_subs)),
"reg", "example_func2standard.mat")
# parametric modulation result in whole-brain functional space
<- file.path(df$in_dir[i], sprintf("VIRTEM_P%s", rep(subjects, each = n_blocks)),
in_nii_fn sprintf("RSA_%02d_Block%02d.feat", 2, rep(1:n_blocks, n_subs)),
"stats", "zstat1.nii.gz")
# output folder & file name in MNI space
invisible(lapply(file.path(dirs$level1glm_dir, "mni", subjects),
function(x) if(!dir.exists(x)) dir.create(x, recursive=TRUE)))
<- file.path(dirs$level1glm_dir, "mni", rep(subjects, each = n_blocks),
out_nii_fn sprintf("%s_post_block%02d_rel_time_parametric_modulation.nii.gz",
rep(subjects, each = n_blocks), rep(1:n_blocks, n_subs)))
# apply FSL flirt to move from whole-brain functional space to 1mm MNI space
invisible(mapply(flirt_apply,
infile = in_nii_fn,
reffile = mni_fname("1"),
initmat = func2standard,
outfile = out_nii_fn,
verbose = FALSE, retimg = FALSE))
Merge first-level images
For each subject, we merge the results files from each block.
for (i_sub in subjects){
# input files
<- file.path(dirs$level1glm_dir, "mni", i_sub,
in_files sprintf("%s_post_block%02d_rel_time_parametric_modulation.nii.gz",
1:n_blocks))
i_sub,
# output file name
<- file.path(dirs$level1glm_dir, "mni", i_sub,
fn_4d sprintf("%s_post_rel_time_parametric_modulation_merged.nii.gz", i_sub))
# concatenate the images
fslmerge(infiles = in_files, direction = "t", outfile = fn_4d, retimg = FALSE, verbose = FALSE)
}
Smooth first-level images
We smooth the first level images for each participant.
# what smoothing to apply
= 3 # in mm
FWHM = FWHM/2.3548 # fslmaths needs sigma not FWHM of kernel
sigma
# field of view mask
<- file.path(dirs$mask_dir, "fov", "fov_mask_mni.nii.gz")
fov_mask
# open the task list
<- file.path(htc_dir, "smooth_rel_time_tasklist.txt")
fn <- file(fn)
con open(con, "w")
# file name of 4D output file
<- file.path(dirs$level1glm_dir, "mni", subjects,
fn_4d sprintf("%s_post_rel_time_parametric_modulation_merged.nii.gz", subjects))
# name of smoothed output file
<- file.path(dirs$level1glm_dir, "mni", subjects,
fn_4d_smooth sprintf("%s_post_rel_time_parametric_modulation_merged_smoothed.nii.gz", subjects))
# write smoothing command to file
writeLines(sprintf("fslmaths %s -s %f -mas %s %s",
fn_4d, sigma, fov_mask, fn_4d_smooth), con)close(con)
# submit to cluster
<- sprintf("fsl_sub -T 30 -t %s -l %s -M bellmund@cbs.mpg.de -N smooth_rel_time", fn, htc_dir)
cmd <- system(cmd, intern = TRUE)
batch_id
pause_until_batch_done(batch_id = batch_id, wait_interval = 20)
Average first-level images
for (i_sub in subjects){
fslmaths(file = file.path(dirs$level1glm_dir, "mni", i_sub,
sprintf("%s_post_rel_time_parametric_modulation_merged_smoothed.nii.gz", i_sub)),
outfile = file.path(dirs$level1glm_dir, "mni", i_sub,
sprintf("%s_post_rel_time_parametric_modulation_merged_smoothed_mean.nii.gz", i_sub)),
retimg = FALSE,
opts = "-Tmean",
verbose = FALSE
) }
Now that we have the mean parametric modulation effect for each participant, we concatenate the images across participants.
# file names of files to merge
<- file.path(dirs$level1glm_dir, "mni", subjects,
in_files sprintf("%s_post_rel_time_parametric_modulation_merged_smoothed_mean.nii.gz", subjects))
# file name of 4D output file
<- file.path(dirs$level1glm_dir, "mni", "4D_rel_time_parametric_modulation.nii.gz")
fn_4d
# concatenate the images
fslmerge(infiles = in_files, direction = "t", outfile = fn_4d, retimg = FALSE, verbose = FALSE)
aHPC ROI analysis
# load 4D file with data of each participant
<- file.path(dirs$level1glm_dir, "mni", "4D_rel_time_parametric_modulation.nii.gz")
in_fn <- readNIfTI2(in_fn)
in_nii
# load ROI mask and binarize it and linearize
<- aHPC_fn <- here("data", "mri", "rois", "aHPC_lr",
aHPC_fn sprintf("%s_group_prob_mni1mm.nii.gz", "aHPC_lr"))
<- readNIfTI2(aHPC_fn)
aHPC_nii >0.99] <- 1
aHPC_nii[aHPC_nii !=1 ] <- 0
aHPC_nii[aHPC_nii <- as.logical(c(aHPC_nii))
aHPC_lin
<- tibble(pmod = rep(0,n_subs))
aHPC_rel_time for (i_sub in 1:n_subs){
# get this subject's data from aHPC
<- in_nii[,,,i_sub]
curr_nii <- c(curr_nii)
curr_dat <- curr_dat[aHPC_lin]
curr_dat
# store mean across aHPC voxels
$pmod[i_sub] <- mean(curr_dat)
aHPC_rel_time
}
# across subject statistics
<- paired_t_perm_jb (aHPC_rel_time$pmod, n_perm = n_perm) stats
Run FSL Randomise
Now we are ready to run FSL Randomise.
# open the task list
<- file.path(htc_dir, "glm_rel_time_randomise_tasklist.txt")
fn <- file(fn)
con open(con, "w")
# masks to use
<- file.path(dirs$mask_dir, "gray_matter", "gray_matter_mask.nii.gz")
gm_mask_fn <- file.path(dirs$mask_dir, "svc", "svc_mask.nii.gz")
svc_mask_fn
# 4D input image to run randomise on
<- file.path(dirs$level1glm_dir, "mni", "4D_rel_time_parametric_modulation.nii.gz")
in_fn
# output file name for FOV
<- file.path(dirs$level1glm_dir, "mni", "randomise_rel_time_parametric_modulation_fov")
out_fn
# define randomise command for full FOV and write to file
writeLines(sprintf("randomise -i %s -o %s -1 -T --uncorrp -m %s -n 5000",
in_fn, out_fn, gm_mask_fn),con)
# output file name for SVC
<- file.path(dirs$level1glm_dir, "mni", "randomise_rel_time_parametric_modulation_svc")
out_fn
# define randomise command for small-volume correction and and write to file
writeLines(sprintf("randomise -i %s -o %s -1 -T --uncorrp -m %s -n 5000",
in_fn, out_fn, svc_mask_fn),con)close(con)
# submit to cluster
<- sprintf("fsl_sub -T 300 -t %s -l %s -M bellmund@cbs.mpg.de -N randomise_rel_time", fn, htc_dir)
cmd <- system(cmd, intern = TRUE)
batch_id pause_until_batch_done(batch_id = batch_id, wait_interval = 300)
Results in aHPC and alEC
Let’s first have a look at the results in our a priori regions of interest, the aHPC and the alEC. For this we have run randomise using the small volume correction mask that includes these areas. We create a figure of the results at a liberal threshold.
# 1 mm MNI template as background image
<- file.path(dirs$data4analysis, "mni1mm_masks", "MNI152_T1_1mm_brain.nii.gz")
mni_fn <- readNIfTI2(mni_fn)
mni_nii
# load FOV mask and binarize it
<- file.path(dirs$data4analysis, "mni1mm_masks", "fov_mask_mni.nii.gz")
fov_fn <- readNIfTI2(fov_fn)
fov_nii >0] <- 1
fov_nii[fov_nii !=1 ] <- 0
fov_nii[fov_nii
# make a mask for the brain area outside our FOV
<- (fov_nii == 0) & (mni_nii>0)
out_fov <- scales::rescale(mni_nii[out_fov], from=range(mni_nii[out_fov]), to=c(6000, 8207))
mni_nii[out_fov] == 0] <- NA
mni_nii[mni_nii
# load image with uncorrected p-values in SVC
<- file.path(dirs$level1glm_dir, "mni", "randomise_rel_time_parametric_modulation_svc_tfce_p_tstat1.nii.gz")
uncorrpsvc_fn <- readNIfTI2(uncorrpsvc_fn)
uncorrpsvc_nii
# load image with corrected p-values in SVC
<- file.path(dirs$level1glm_dir, "mni", "randomise_rel_time_parametric_modulation_svc_tfce_corrp_tstat1.nii.gz")
corrpsvc_fn <- readNIfTI2(corrpsvc_fn)
corrpsvc_nii
# t-values to plot (SVC)
<- file.path(dirs$level1glm_dir, "mni", "randomise_rel_time_parametric_modulation_svc_tstat1.nii.gz")
tvals_fn <- readNIfTI2(tvals_fn)
t_nii
# find coordinates of voxel with minimum p-value (image is 1-p) and get a data frame for labeling the plots
<- which(t_nii == max(t_nii), arr.ind = TRUE)
coords #coords <- mni2vox(coords)
# print results
sprintf("peak voxel of relative time parametric modulation in SVC at MNI x=%d, y=%d, z=%d, t=%.2f, puncorr=%.3f, pcorr=%.3f",
vox2mni(coords)[1], vox2mni(coords)[2], vox2mni(coords)[3], t_nii[coords], 1-uncorrpsvc_nii[coords], 1-corrpsvc_nii[coords])
## [1] "peak voxel of relative time parametric modulation in SVC at MNI x=22, y=-13, z=-16, t=3.93, puncorr=0.001, pcorr=0.173"
<- data.frame(col_ind = sprintf("%s=%d",c("x","y","z"), vox2mni(coords)),
label_df label = sprintf("%s=%d",c("x","y","z"), vox2mni(coords)))
# get ggplot object for template as background
<-getggTemplate(col_template=rev(RColorBrewer::brewer.pal(8, 'Greys')),brains=mni_nii,
ggTemplateall_brain_and_time_inds_one=TRUE,
mar=c(1,2,3), mar_ind=coords, row_ind = c(1,1,1),
col_ind=sprintf("%s=%d",c("x","y","z"),vox2mni(coords)), center_coords=TRUE)
# threshold t-values for plotting
<- 0.05
thresh < 1-thresh] <- NA
t_nii[uncorrpsvc_nii
# get data frame for the t-values
<- getBrainFrame(brains=t_nii, mar=c(1,2,3),
t_df col_ind = sprintf("%s=%d",c("x","y","z"),vox2mni(coords)),
mar_ind=coords, mask=NULL, center_coords=TRUE)
<- ggTemplate +
fig_rel_time geom_tile(data=t_df, aes(x=row,y=col,fill=value))+
facet_wrap(~col_ind, nrow=1, ncol=3, strip.position = "top", scales = "free") +
::scale_fill_scico(palette = "lajolla",
scicobegin = 0.3, end=0.9, direction = -1,
name = element_text("t"),
limits = round(range(t_df$value), digits=2),
breaks = round(range(t_df$value), digits=2)) +
#ggtitle(label = "\ndifferent\nsequence") +
theme_cowplot(line_size = NA) +
theme(strip.background = element_blank(), strip.text.x = element_blank(),
plot.title = element_text(hjust = 0.5),
text = element_text(size=10, family=font2use), axis.text = element_text(size=8, family=font2use),
legend.text=element_text(size=8, family=font2use), legend.title = element_text(size=8, family=font2use),
legend.key.width = unit(0.015, "npc"),
legend.key.height = unit(0.03, "npc"),
legend.position = "right", legend.justification = c(0.5, 0),
aspect.ratio = 1)+
guides(fill = guide_colorbar(
direction = "vertical",
title.position = "top",
title.hjust = 0,
label.position = "right"))+
xlab(element_blank()) + ylab(element_blank()) +
coord_cartesian(clip = "off") +
geom_text(data = label_df, x=0, y=-Inf, aes(label = label),
size = 8/.pt, family=font2use, vjust=1) +
theme_no_ticks()
<- here("figures", "letter_glm_rel_time_aHPC_svc")
fn ggsave(paste0(fn, ".pdf"), plot=fig_rel_time, units = "cm",
width = 17.4, height = 6, dpi = "retina", device = cairo_pdf)
ggsave(paste0(fn, ".png"), plot=fig_rel_time, units = "cm",
width = 17.4, height = 6, dpi = "retina", device = "png")
print(fig_rel_time)
FOV results
We check how many voxels survive at an uncorrected p-threshold of 0.001 in our FOV. No clusters are visible at an uncorrected threshold of p<0.001.
# load image with uncorrected p-values in FOV
<- file.path(dirs$level1glm_dir, "mni", "randomise_rel_time_parametric_modulation_fov_tfce_p_tstat1.nii.gz")
uncorrpfov_fn <- readNIfTI2(uncorrpfov_fn)
uncorrpfov_nii
<- 0.001
thresh if(sum(c(uncorrpfov_nii)>(1-thresh)) == 0){
# print to screen
cat("no significant voxels in parametric modulation analysis at p<",
sprintf("%.3f (uncorrected)",thresh))
}
## no significant voxels in parametric modulation analysis at p< 0.001 (uncorrected)