highspeed-analysis-oddball.Rmd 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152
  1. ## Decoding on slow trials
  2. ### Initialization
  3. We load the data and relevant functions:
  4. ```{r, warning=FALSE, message=FALSE, echo=TRUE}
  5. # find the path to the root of this project:
  6. if (!requireNamespace("here")) install.packages("here")
  7. if ( basename(here::here()) == "highspeed" ) {
  8. path_root = here::here("highspeed-analysis")
  9. } else {
  10. path_root = here::here()
  11. }
  12. # create a list of participants to exclude based on behavioral performance:
  13. sub_exclude <- c("sub-24", "sub-31", "sub-37", "sub-40")
  14. ```
  15. The next step is only evaluated during manual execution:
  16. ```{r, eval=FALSE}
  17. # source all relevant functions from the setup R script:
  18. source(file.path(path_root, "code", "highspeed-analysis-setup.R"))
  19. ```
  20. ### Decoding accuracy
  21. #### Mean decoding accuracy
  22. We calculate the mean decoding accuracy:
  23. ```{r, results="hold", echo=TRUE}
  24. calc_class_stim_acc <- function(data, mask_label) {
  25. # calculate the mean decoding accuracy for each participant:
  26. dt_odd_peak = data %>%
  27. # select data for the oddball peak decoding
  28. # leave out the redundant "other" class predictions:
  29. filter(test_set == "test-odd_peak" & class != "other" & mask == mask_label) %>%
  30. # filter out excluded participants:
  31. filter(!(id %in% sub_exclude)) %>%
  32. # only check classifier of stimulus presented on a given trial:
  33. filter(class == stim) %>%
  34. # calculate the decoding accuracy by comparing true and predicted label:
  35. mutate(acc = 0) %>%
  36. transform(acc = ifelse(pred_label == stim, 1, acc)) %>%
  37. setDT(.) %>%
  38. # calculate average decoding accuracy for every participant and classification:
  39. .[, by = .(classification, id), .(
  40. mean_accuracy = mean(acc) * 100,
  41. num_trials = .N
  42. )] %>%
  43. verify(all(num_trials <= 480)) %>%
  44. # verify if the number of participants matches:
  45. verify(.[classification == "ovr", .(num_subs = .N)]$num_subs == 36)
  46. return(dt_odd_peak)
  47. }
  48. ```
  49. ```{r, results="hold", echo=TRUE}
  50. calc_max_prob_acc <- function(data, mask_label) {
  51. dt_odd_peak <- data %>%
  52. # select data for the oddball peak decoding
  53. # leave out the redundant "other" class predictions:
  54. filter(test_set == "test-odd_peak" & class != "other" & mask == mask_label) %>%
  55. # filter out excluded participants:
  56. filter(!(id %in% sub_exclude)) %>%
  57. # calculate the decoding accuracy by comparing true and predicted label:
  58. setDT(.) %>%
  59. # calculate average decoding accuracy for every participant and classification:
  60. .[, by = .(classification, id, trial, stim), .(
  61. max_prob_label = class[which.max(probability)],
  62. num_classes = .N
  63. )] %>%
  64. verify(num_classes == 5) %>%
  65. .[, "acc" := ifelse(stim == max_prob_label, 1, 0)] %>%
  66. .[, by = .(classification, id), .(
  67. mean_accuracy = mean(acc) * 100,
  68. num_trials = .N
  69. )] %>%
  70. verify(num_trials <= 480)
  71. return(dt_odd_peak)
  72. }
  73. ```
  74. We print the results for the decoding accuracy:
  75. ```{r, results="hold", warning=FALSE, message=FALSE, echo=TRUE}
  76. decoding_accuracy <- function(accuracy, alt_label){
  77. # print average decoding accuracy:
  78. print(sprintf("decoding accuracy: m = %0.2f %%", mean(accuracy)))
  79. # print sd decoding accuracy:
  80. print(sprintf("decoding accuracy: sd = %0.2f %%", sd(accuracy)))
  81. # print range of decoding accuracy:
  82. print(sprintf("decoding accuracy: range = %s %%",
  83. paste(round(range(accuracy),0), collapse = "-")))
  84. # define decoding accuracy baseline (should equal 20%):
  85. acc_baseline = 100/5
  86. print(sprintf("chance baseline = %d %%", acc_baseline))
  87. # perform effect size (cohen`s d)
  88. cohens_d = (mean(accuracy) - acc_baseline) / sd(accuracy)
  89. print(sprintf("effect size (cohen's d) = %0.2f", cohens_d))
  90. # perform shapiro-wilk test to test for normality of the data
  91. print(shapiro.test(accuracy))
  92. # perform one-sided one-sample t-test against baseline:
  93. results = t.test(accuracy, mu = acc_baseline, alternative = alt_label)
  94. print(results)
  95. # perform one-sample wilcoxon test in case of non-normality of the data:
  96. print(wilcox.test(accuracy, mu = acc_baseline, alternative = alt_label))
  97. }
  98. ```
  99. ```{r, results="hold", warning=FALSE, message=FALSE, echo=TRUE}
  100. #dt_odd_peak <- calc_class_stim_acc(data = dt_pred, mask_label = "cv")
  101. #dt_odd_peak_hpc <- calc_class_stim_acc(data = dt_pred, mask_label = "cv_hpc")
  102. dt_odd_peak <- calc_max_prob_acc(data = dt_pred, mask_label = "cv")
  103. dt_odd_peak_hpc <- calc_max_prob_acc(data = dt_pred, mask_label = "cv_hpc")
  104. decoding_accuracy(
  105. accuracy = subset(dt_odd_peak, classification == "ovr")$mean_acc,
  106. alt_label = "greater")
  107. decoding_accuracy(
  108. accuracy = subset(dt_odd_peak_hpc, classification == "ovr")$mean_accuracy,
  109. alt_label = "two.sided")
  110. ```
  111. #### Figure 2a / S2a
  112. We plot the mean decoding accuracy in occipito-temporal and hippocampal data:
  113. ```{r, echo=TRUE, class.source=NULL, fig.width=1, fig.height=3}
  114. plot_odd_peak <- function(data) {
  115. plot.odd = ggplot(data = data, aes(
  116. x = "mean_acc", y = as.numeric(mean_accuracy))) +
  117. geom_bar(stat = "summary", fun = "mean", fill = "lightgray") +
  118. geom_dotplot(binaxis = "y", stackdir = "center", stackratio = 0.5,
  119. color = "black", fill = "lightgray", alpha = 0.5,
  120. inherit.aes = TRUE, binwidth = 2) +
  121. geom_errorbar(stat = "summary", fun.data = "mean_se", width = 0.0, color = "black") +
  122. ylab("Accuracy (%)") + xlab("Condition") +
  123. geom_hline(aes(yintercept = 20), linetype = "dashed", color = "black") +
  124. scale_y_continuous(labels = label_fill(seq(0, 100, 20), mod = 1),
  125. breaks = seq(0, 100, 20)) +
  126. coord_capped_cart(left = "both", expand = TRUE, ylim = c(0, 100)) +
  127. theme(axis.ticks.x = element_blank(), axis.line.x = element_blank()) +
  128. theme(axis.title.x = element_blank(), axis.text.x = element_blank()) +
  129. annotate("text", x = 1, y = 15, label = "Chance", color = "black",
  130. size = rel(2.5), family = "Helvetica", fontface = "plain") +
  131. theme(panel.border = element_blank(), axis.line = element_line()) +
  132. theme(axis.line = element_line(colour = "black"),
  133. panel.grid.major = element_blank(),
  134. panel.grid.minor = element_blank(),
  135. panel.border = element_blank(),
  136. panel.background = element_blank())
  137. return(plot.odd)
  138. }
  139. fig_odd_peak <- plot_odd_peak(subset(dt_odd_peak, classification == "ovr"))
  140. fig_odd_peak_hpc <- plot_odd_peak(subset(dt_odd_peak_hpc, classification == "ovr"))
  141. fig_odd_peak; fig_odd_peak_hpc
  142. ```
  143. ```{r, include=FALSE, eval=FALSE, echo=FALSE}
  144. ggsave(filename = "highspeed_plot_decoding_average_decoding.pdf",
  145. plot = fig_odd_peak, device = cairo_pdf, path = path_figures, scale = 1,
  146. dpi = "retina", height = 5, width = 2)
  147. ```
  148. #### Source Data File Fig. 2a
  149. ```{r, echo=TRUE}
  150. subset(dt_odd_peak, classification == "ovr") %>%
  151. select(-num_trials, -classification) %>%
  152. write.csv(., file = file.path(path_sourcedata, "source_data_figure_2a.csv"),
  153. row.names = FALSE)
  154. ```
  155. #### Source Data File Fig. S2a
  156. ```{r, echo=TRUE}
  157. subset(dt_odd_peak_hpc, classification == "ovr") %>%
  158. select(-num_trials, -classification) %>%
  159. write.csv(., file = file.path(path_sourcedata, "source_data_figure_s2a.csv"),
  160. row.names = FALSE)
  161. ```
  162. ### Fold-wise decoding accuracy
  163. We calculate the mean decoding accuracy for each of the eight folds of the cross-validation procedure:
  164. ```{r, results="hold", echo=TRUE}
  165. dt_odd_peak_run = dt_pred %>%
  166. # select data for the oddball peak decoding
  167. # leave out the redundant "other" class predictions:
  168. filter(test_set == "test-odd_peak" & class != "other" & mask == "cv") %>%
  169. # filter out excluded participants:
  170. filter(!(id %in% sub_exclude)) %>%
  171. setDT(.) %>%
  172. # calculate average decoding accuracy for every participant and classification:
  173. .[, by = .(classification, id, run_study, trial, stim), .(
  174. max_prob_label = class[which.max(probability)],
  175. num_classes = .N
  176. )] %>%
  177. # check if there are five classifier predictions per trial:
  178. verify(num_classes == 5) %>%
  179. # determine accuracy if label with highest probability matches stimulus:
  180. .[, "accuracy" := ifelse(stim == max_prob_label, 1, 0)]
  181. # print results table:
  182. rmarkdown::paged_table(dt_odd_peak_run)
  183. ```
  184. ```{r, results="hold", echo=TRUE}
  185. # calculate average decoding accuracy for every participant and classification:
  186. dt_odd_peak_run_mean <- dt_odd_peak_run %>%
  187. .[, by = .(classification, id, run_study), .(
  188. mean_accuracy = mean(accuracy) * 100,
  189. num_trials = length(unique(trial))
  190. )] %>%
  191. # verify if the number of participants matches for every classification and run:
  192. verify(.[, by = .(classification, run_study),
  193. .(num_subs = .N)]$num_subs == 36) %>%
  194. setorder(., classification, id, run_study)
  195. # print the table:
  196. rmarkdown::paged_table(dt_odd_peak_run_mean)
  197. ```
  198. ```{r, echo=FALSE, class.source=NULL}
  199. fig_odd_run = ggplot(data = subset(dt_odd_peak_run_mean, classification == "ovr"), aes(
  200. x = run_study, y = as.numeric(mean_accuracy))) +
  201. geom_bar(stat = "summary", fun = "mean", fill = "lightgray", color = "black") +
  202. geom_dotplot(aes(color = "black", fill = id),
  203. binaxis = "y", stackdir = "center", stackratio = 0.5, alpha = 0.5,
  204. inherit.aes = TRUE, binwidth = 2) +
  205. geom_line(aes(group = id, color = id)) +
  206. geom_line(aes(group = 1), stat = "summary", fun = "mean", color = "black") +
  207. geom_errorbar(stat = "summary", fun.data = "mean_se", width = 0.0, color = "black") +
  208. ylab("Accuracy (%)") + xlab("Task run") +
  209. geom_hline(aes(yintercept = 20), linetype = "dashed", color = "black") +
  210. scale_y_continuous(labels = label_fill(seq(0, 100, 20), mod = 1), breaks = seq(0, 100, 20)) +
  211. coord_capped_cart(left = "both", expand = TRUE, ylim = c(0, 100)) +
  212. annotate("text", x = 8, y = 16, label = "Chance", color = "black",
  213. size = rel(2.5), family = "Helvetica", fontface = "plain") +
  214. theme(legend.position = "none") +
  215. theme(panel.border = element_blank()) +
  216. theme(axis.text = element_text(color = "black")) +
  217. theme(axis.ticks = element_line(color = "black")) +
  218. theme(axis.line.y = element_line(colour = "black"),
  219. axis.ticks.x = element_blank(),
  220. panel.grid.major = element_blank(),
  221. panel.grid.minor = element_blank(),
  222. panel.border = element_blank(),
  223. panel.background = element_blank())
  224. fig_odd_run
  225. ```
  226. ```{r, include=FALSE, eval=FALSE, echo=FALSE}
  227. ggsave(filename = "highspeed_plot_decoding_average_decoding_run.pdf",
  228. plot = fig_odd_run, device = cairo_pdf, path = path_figures, scale = 1,
  229. dpi = "retina", height = 3, width = 5)
  230. ```
  231. ```{r, results="hold"}
  232. lme_odd_peak_run <- lmerTest::lmer(
  233. mean_accuracy ~ run_study + (1 | id), control = lcctrl,
  234. data = subset(dt_odd_peak_run_mean, classification == "ovr")
  235. )
  236. summary(lme_odd_peak_run)
  237. anova(lme_odd_peak_run)
  238. emmeans_results = emmeans(lme_odd_peak_run, list(pairwise ~ run_study))
  239. emmeans_results
  240. ```
  241. ## Single-trial decoding time courses
  242. We calculate the multivariate decoding time courses on single slow trials:
  243. ```{r, echo=TRUE}
  244. dt_odd_long_sub = dt_pred %>%
  245. # select data for the oddball long decoding
  246. # leave out the redundant "other" class predictions:
  247. filter(test_set == "test-odd_long" & class != "other" & mask == "cv") %>%
  248. # filter out excluded participants:
  249. filter(!(id %in% sub_exclude)) %>% setDT(.) %>%
  250. # average across stimuli and runs for each participant
  251. # create an index for the true stimulus presented on each trial:
  252. .[, by = .(classification, id, seq_tr, class, stim), .(
  253. num = .N,
  254. mean_prob = mean(probability * 100),
  255. current_stim = as.numeric(class == unique(stim))
  256. )]
  257. dt_odd_long_mean = dt_odd_long_sub %>%
  258. # average across participants:
  259. .[, by = .(classification, seq_tr, class, stim, current_stim), .(
  260. mean_prob = mean(mean_prob),
  261. num_subs = .N,
  262. sem_upper = (mean(mean_prob) + (sd(mean_prob)/sqrt(.N))),
  263. sem_lower = (mean(mean_prob) - (sd(mean_prob)/sqrt(.N)))
  264. )] %>%
  265. # create an additional variable that expresses time in seconds:
  266. mutate(time = (seq_tr - 1) * 1.25) %>%
  267. # check if the number of participants is correct:
  268. verify(all(num_subs == 36))
  269. ```
  270. #### Figure 2b
  271. We plot the single-trial multi-variate decoding time courses on slow trials:
  272. ```{r}
  273. plot.odd.long = ggplot(data = subset(dt_odd_long_mean, classification == "ovr"), aes(
  274. x = as.factor(seq_tr), y = as.numeric(mean_prob))) +
  275. facet_wrap(~ as.factor(stim)) +
  276. geom_line(data = subset(dt_odd_long_sub, classification == "ovr"), aes(
  277. group = as.factor(interaction(id, class)), color = fct_rev(as.factor(current_stim))), alpha = 0.0) +
  278. geom_line(data = subset(dt_odd_long_sub, classification == "ovr" & current_stim == 0), aes(
  279. group = as.factor(interaction(id, class)), color = fct_rev(as.factor(current_stim))), alpha = 0.3) +
  280. geom_line(data = subset(dt_odd_long_sub, classification == "ovr" & current_stim == 1), aes(
  281. group = as.factor(interaction(id, class)), color = fct_rev(as.factor(current_stim))), alpha = 0.3) +
  282. geom_ribbon(
  283. data = subset(dt_odd_long_mean, classification == "ovr"),
  284. aes(ymin = sem_lower, ymax = sem_upper, fill = fct_rev(as.factor(current_stim)),
  285. group = as.factor(class)), alpha = 0.0, color = NA) +
  286. geom_line(
  287. data = subset(dt_odd_long_mean, classification == "ovr", alpha = 0),
  288. aes(color = fct_rev(as.factor(current_stim)), group = as.factor(class))) +
  289. geom_ribbon(
  290. data = subset(dt_odd_long_mean, classification == "ovr" & current_stim == 1),
  291. aes(ymin = sem_lower, ymax = sem_upper, fill = fct_rev(as.factor(current_stim)),
  292. group = as.factor(class)), alpha = 0.3, color = NA) +
  293. geom_line(
  294. data = subset(dt_odd_long_mean, classification == "ovr" & current_stim == 1),
  295. aes(color = fct_rev(as.factor(current_stim)), group = as.factor(class))) +
  296. ylab("Probability (%)") + xlab("Time from stimulus onset (TRs)") +
  297. scale_fill_manual(name = "Classified class", values = c("black", "lightgray"), labels = c("true", "other")) +
  298. scale_color_manual(name = "Classified class", values = c("black", "lightgray"), labels = c("true", "other")) +
  299. coord_capped_cart(left = "both", bottom = "both", expand = TRUE, ylim = c(0, 80)) +
  300. theme(legend.position = c(1, 0), legend.justification = c(1, 0)) +
  301. #theme(legend.position = c(0.95, 0.8), legend.justification = c(1, 0)) +
  302. #theme(legend.title = element_text(size = 8), legend.text = element_text(size = 8)) +
  303. #theme(legend.key.size = unit(0.7, "line")) +
  304. scale_x_discrete(labels = label_fill(seq(0, 7, 1), mod = 1), breaks = seq(0, 7, 1)) +
  305. guides(color = guide_legend(ncol = 2), fill = guide_legend(ncol = 2)) +
  306. theme(strip.text.x = element_text(margin = margin(b = 2, t = 2))) +
  307. theme(panel.border = element_blank(), axis.line = element_line()) +
  308. theme(axis.line = element_line(colour = "black"),
  309. panel.grid.major = element_blank(),
  310. panel.grid.minor = element_blank(),
  311. panel.border = element_blank(),
  312. panel.background = element_blank())
  313. #theme(strip.text.x = element_blank())
  314. plot.odd.long
  315. ```
  316. ```{r, include=FALSE, eval=FALSE, echo=FALSE}
  317. ggsave(filename = "highspeed_plot_decoding_single_trial_activation.pdf",
  318. plot = plot.odd.long, device = cairo_pdf, path = path_figures, scale = 1,
  319. dpi = "retina", width = 3.5, height = 3)
  320. ```
  321. #### Source Data File Fig. 2b
  322. ```{r, echo=TRUE}
  323. subset(dt_odd_long_sub, classification == "ovr") %>%
  324. select(-num, -classification) %>%
  325. write.csv(., file = file.path(path_sourcedata, "source_data_figure_2b.csv"),
  326. row.names = FALSE)
  327. ```
  328. We compare the mean classification probability of the current stimulus
  329. versus the mean of all other stimuli for each TR:
  330. ```{r}
  331. dt_odd_long_mean_stat = dt_pred %>%
  332. # select data for the oddball long decoding
  333. # leave out the redundant "other" class predictions:
  334. filter(test_set == "test-odd_long" & class != "other" & mask == "cv") %>%
  335. setDT(.) %>%
  336. # filter out excluded participants:
  337. filter(!(id %in% sub_exclude)) %>% setDT(.) %>%
  338. # average across stimuli and runs for each participant
  339. # create a new variable that indicated whether stimulus is current or not:
  340. .[, by = .(classification, id, seq_tr, class, stim), .(
  341. num_stim = .N,
  342. mean_prob = mean(probability * 100),
  343. stim_type = ifelse(class == unique(stim), "current", "other")
  344. )] %>%
  345. verify(num_stim <= 96) %>%
  346. # average across stimulus type:
  347. .[, by = .(classification, id, seq_tr, stim, stim_type), .(
  348. num_class = .N,
  349. mean_prob = mean(mean_prob)
  350. )] %>%
  351. verify(num_class %in% c(1, 4)) %>%
  352. select(., -num_class) %>%
  353. # turn into wide format:
  354. spread(key = stim_type, value = mean_prob) %>% setDT(.) %>%
  355. # calculate a paired t-test at every TR for each unique stimulus to
  356. # compare probability of the current vs. the mean other class probabilities:
  357. .[, by = .(classification, seq_tr, stim), {
  358. results = t.test(current, other, paired = TRUE, alternative = "two.sided")
  359. list(
  360. tvalue = results$statistic,
  361. df = results$parameter,
  362. pvalue = results$p.value,
  363. cohens_d = (mean(current) - mean(other)) / sd(current - other),
  364. num_subs = .N,
  365. mean_current = mean(current),
  366. mean_other = mean(other),
  367. pvalue_round = round_pvalues(results$p.value)
  368. )
  369. }] %>%
  370. # ajdust the p-value for multiple comparisons using Bonferroni correction:
  371. .[, by = .(classification), ":=" (
  372. num_comp = .N,
  373. pvalue_adjust = p.adjust(pvalue, method = "bonferroni", n = .N)
  374. )] %>%
  375. # check if correction was done for 5 (stimuli) by 7 (TRs) = 35 comparisons:
  376. verify(all(num_comp == 5 * 7)) %>%
  377. # verify if number of participants matches:
  378. verify(all(num_subs == 36)) %>%
  379. # check if degrees-of-freedom = n - 1
  380. verify(all(df == num_subs - 1)) %>%
  381. # round the bonferroni-adjusted p-values for clarity:
  382. mutate(pvalue_adjust_round = round_pvalues(pvalue_adjust)) %>%
  383. # sort the datatable according to classification, stimulus and TR:
  384. setorder(., classification, stim, seq_tr) %>%
  385. # only look at the fourth TR where probabilities are expected to peak
  386. filter(seq_tr == 4 & classification == "ovr")
  387. # print the data table:
  388. rmarkdown::paged_table(dt_odd_long_mean_stat)
  389. ```
  390. ```{r, echo=FALSE, class.source = NULL}
  391. plot_grid(fig_odd_peak, plot.odd.long, labels = c('a', 'b'), ncol = 2,
  392. rel_widths = c(1.5, 5), hjust = c(0, 0), label_fontface = "bold")
  393. ```
  394. ```{r, include=FALSE, eval=FALSE, echo=FALSE}
  395. ggsave(filename = "highspeed_plot_decoding_oddball.pdf",
  396. plot = last_plot(), device = cairo_pdf, path = path_figures, scale = 1,
  397. dpi = "retina", width = 6.5, height = 3.5)
  398. ggsave(filename = "wittkuhn_schuck_figure_s2.pdf",
  399. plot = last_plot(), device = cairo_pdf, path = path_figures, scale = 1,
  400. dpi = "retina", width = 6.5, height = 3.5)
  401. ```
  402. ## Response functions
  403. ### Define response functions
  404. We define the sine wave response function:
  405. ```{r}
  406. sine_truncated <- function(params, time) {
  407. if (!is.list(params)) {
  408. params = as.list(params)
  409. names(params) = c('frequency', 'amplitude', 'shift', 'baseline')
  410. }
  411. y = rep(0, 13)
  412. y = params$amp/2 * sin(2*pi*params$freq*time - 2*pi*params$freq*params$shift - 0.5*pi) + params$baseline + params$amp/2
  413. # flatten response function after one cycle:
  414. y[time < (params$shift)] = params$baseline
  415. y[time > (params$shift + 1/params$freq)] = params$baseline
  416. return(y)
  417. }
  418. ```
  419. We define a function to evaluate during optimization:
  420. ```{r}
  421. sine_truncated_eval = function(params, time, data) {
  422. y = sine_truncated(params, time)
  423. SSE = sum((data - y)^2)
  424. return(SSE)
  425. }
  426. ```
  427. ### Mean parameters
  428. We calculate the multivariate decoding time courses for each stimulus class:
  429. ```{r}
  430. # calculate mean probability for every stimulus for every TR:
  431. dt_odd_long_mean_class = dt_pred %>%
  432. # filter for oddball long predictions and only select ovr classifier:
  433. filter(test_set == "test-odd_long" & class != "other" &
  434. classification == "ovr" & mask == "cv") %>%
  435. # filter out excluded participants:
  436. filter(!(id %in% sub_exclude)) %>% setDT(.) %>%
  437. # select only predictions for the class currently shown on any given trial:
  438. filter(class == stim) %>% setDT(.) %>%
  439. # average the probability across trials
  440. .[, by = .(id, classification, stim, seq_tr), .(
  441. mean_probability = mean(probability, na.rm = TRUE)
  442. )]
  443. ```
  444. We fit the truncated sine wave response function to data from every participant:
  445. ```{r}
  446. # set optimization parameters:
  447. opts = list("algorithm" = "NLOPT_LN_COBYLA", "xtol_rel" = 1.0e-8, "maxeval" = 1.0e+5)
  448. default_params = c(0.2, 0.6, 0, 0.1)
  449. lower_bounds = c(0.01, 0.1, 0, 0)
  450. upper_bounds = c(0.5, 1, 5, 0.3)
  451. time = 0:6
  452. ```
  453. ```{r, results="hold"}
  454. dt_odd_long_fit = dt_odd_long_mean_class %>%
  455. # fit the truncated sine wave to probabilities of every decoding timecourse:
  456. .[, by = .(id, classification, stim), {
  457. res <- nloptr(
  458. x0 = default_params, eval_f = sine_truncated_eval, time = time,
  459. data = mean_probability, lb = lower_bounds, ub = upper_bounds, opts = opts)
  460. list(
  461. num_trs = .N,
  462. frequency = res$solution[1],
  463. wavelength = 1/res$solution[1],
  464. amplitude = res$solution[2],
  465. shift = res$solution[3],
  466. baseline = res$solution[4],
  467. params = list(res$solution)
  468. )}] %>%
  469. verify(all(num_trs == length(time)))
  470. # print the mean model parameters:
  471. summary(dt_odd_long_fit[, c("frequency", "wavelength", "amplitude", "shift", "baseline")])
  472. # calculate the mean parameters across all participants to be used later:
  473. mean_parameters = dt_odd_long_fit %>%
  474. # average the fitted parameters across participants:
  475. .[, by = .(classification), .(
  476. mean_freq = mean(frequency),
  477. mean_wavelength = mean(wavelength),
  478. mean_amplitude = mean(amplitude),
  479. mean_shift = mean(shift),
  480. mean_baseline = mean(baseline)
  481. )]
  482. ```
  483. We fit the sine response function for every participant using individual parameters:
  484. ```{r}
  485. dt_odd_long_fit_single = dt_odd_long_fit %>%
  486. # calculate a sine-wave respone function timecourse for every participant:
  487. .[, by = .(id, classification, stim), .(
  488. csine = sine_truncated(params = unlist(params), seq(0, 6, 0.1))
  489. )] %>%
  490. # add a counter that is used for plotting below:
  491. .[, by = .(classification, id, stim), ":=" (
  492. t = seq(0, 6, 0.1))]
  493. ```
  494. #### Figure S4a
  495. We plot the single sine wave fits for three example participants (supplement):
  496. ```{r, echo=FALSE}
  497. # set seed to reproduce random id sampling:
  498. set.seed(18)
  499. num_sub_select = 3
  500. # select random example participants:
  501. select_id = sample(x = unique(dt_odd_long_fit_single$id), size = num_sub_select)
  502. fig_s1 = ggplot(data = subset(dt_odd_long_fit_single, id %in% select_id),
  503. aes(x = t, y = csine * 100)) +
  504. geom_point(aes(color = "Model"), alpha = 0.5) +
  505. geom_line(data = subset(dt_odd_long_mean_class, id %in% select_id),
  506. aes(x = (seq_tr - 1), y = mean_probability * 100, color = "Data")) +
  507. geom_point(data = subset(dt_odd_long_mean_class, id %in% select_id),
  508. aes(x = (seq_tr - 1), y = mean_probability * 100, color = "Data")) +
  509. scale_colour_manual(name = "", values = c("gray", "black")) +
  510. facet_grid(vars(as.factor(id)), vars(as.factor(stim))) +
  511. coord_capped_cart(left = "both", bottom = "both") +
  512. xlab("Time from stimulus onset (in TRs)") + ylab("Probability (%)") +
  513. scale_x_continuous(labels = label_fill(seq(1, 7, 1), mod = 1), breaks = seq(0, 6, 1)) +
  514. theme(legend.position = "top", legend.direction = "horizontal",
  515. legend.justification = "center", legend.margin = margin(0, 0 ,0, 0),
  516. legend.box.margin = margin(t = 0, r = 0, b = -10, l = 0)) +
  517. theme(strip.text = element_text(margin = margin(b = 2, t = 2, r = 2, l = 2))) +
  518. theme(axis.title.x = element_blank()) +
  519. theme(axis.line = element_line(colour = "black"),
  520. panel.grid.major = element_blank(),
  521. panel.grid.minor = element_blank(),
  522. panel.border = element_blank(),
  523. panel.background = element_blank())
  524. fig_s1
  525. ```
  526. ```{r, include=FALSE, eval=FALSE, echo=FALSE}
  527. ggsave(filename = "highsspeed_plot_decoding_oddball_single_sine_fits.pdf",
  528. plot = last_plot(), device = cairo_pdf, path = path_figures, scale = 1,
  529. dpi = "retina", width = 6, height = 4)
  530. ```
  531. #### Source Data File Fig. S4a
  532. ```{r, echo=TRUE}
  533. dt_odd_long_mean_class %>%
  534. select(-classification) %>%
  535. write.csv(., file = file.path(path_sourcedata, "source_data_figure_s4a.csv"),
  536. row.names = FALSE)
  537. ```
  538. We calculate the mean shape of the sine wave response function across participants:
  539. ```{r, echo=TRUE}
  540. dt_odd_long_fit_mean = dt_odd_long_fit %>%
  541. # average the fitting parameters across participants:
  542. .[, by = .(classification, stim), .(
  543. num_subs = .N,
  544. mean_freq = mean(frequency),
  545. mean_amplitude = mean(amplitude),
  546. mean_shift = mean(shift),
  547. mean_baseline = mean(baseline)
  548. )] %>% verify(all(num_subs == 36)) %>%
  549. .[, by = .(classification, stim), .(
  550. csine = sine_truncated(params = c(mean_freq, mean_amplitude, mean_shift, mean_baseline),
  551. seq(0, 6, 0.1)))] %>%
  552. .[, by = .(classification, stim), ":=" (
  553. t = seq(0, 6, 0.1))]
  554. ```
  555. #### Figure S4b
  556. ```{r, echo=TRUE}
  557. fig_s2 = ggplot(data = dt_odd_long_mean_class) +
  558. geom_line(data = dt_odd_long_mean_class,
  559. aes(x = (seq_tr - 1), y = mean_probability * 100, group = as.factor(id),
  560. color = "Data"), alpha = 0.3) +
  561. geom_line(data = dt_odd_long_fit_mean, aes(x = t, y = csine * 100, color = "Model"),
  562. size = 1) +
  563. facet_wrap(~ as.factor(stim), nrow = 1) +
  564. coord_capped_cart(left = "both", bottom = "both", ylim = c(0, 80)) +
  565. xlab("Time from stimulus onset (in TRs)") + ylab("Probability (%)") +
  566. scale_colour_manual(name = "", values = c("gray", "black"), guide = FALSE) +
  567. scale_x_continuous(labels = label_fill(seq(1, 7, 1), mod = 1), breaks = seq(0, 6, 1)) +
  568. #theme(plot.margin = unit(c(t = 1, r = 1, b = 1, l = 1), "pt")) +
  569. theme(legend.position = "bottom", legend.direction = "horizontal",
  570. legend.justification = "center", legend.margin = margin(0, 0, 0, 0),
  571. legend.box.margin = margin(t = -10, r = 0, b = 0, l = 0)) +
  572. theme(strip.text = element_text(margin = margin(b = 2, t = 2, r = 2, l = 2))) +
  573. theme(axis.line = element_line(colour = "black"),
  574. panel.grid.major = element_blank(),
  575. panel.grid.minor = element_blank(),
  576. panel.border = element_blank(),
  577. panel.background = element_blank())
  578. fig_s2
  579. ```
  580. ```{r, include=FALSE, eval=FALSE, echo=FALSE}
  581. ggsave(filename = "highsspeed_plot_decoding_oddball_mean_sine_fits.pdf",
  582. plot = last_plot(), device = cairo_pdf, path = path_figures, scale = 1,
  583. dpi = "retina", width = 5, height = 3)
  584. ```
  585. #### Source Data File Fig. S4b
  586. ```{r, echo=TRUE}
  587. dt_odd_long_mean_class %>%
  588. select(-classification) %>%
  589. write.csv(., file = file.path(path_sourcedata, "source_data_figure_s4b.csv"),
  590. row.names = FALSE)
  591. ```
  592. #### Figure S4
  593. ```{r}
  594. plot_grid(fig_s1, fig_s2, nrow = 2, ncol = 1, hjust = c(0, 0),
  595. rel_heights = c(4, 2), labels = c("a", "b"), label_fontface = "bold")
  596. ```
  597. ```{r, include=FALSE, eval=FALSE, echo=FALSE}
  598. ggsave(filename = "highsspeed_plot_decoding_oddball_supplement.pdf",
  599. plot = last_plot(), device = cairo_pdf, path = path_figures, scale = 1,
  600. dpi = "retina", width = 7, height = 7)
  601. ggsave(filename = "wittkuhn_schuck_figure_s4.pdf",
  602. plot = last_plot(), device = cairo_pdf, path = path_figures, scale = 1,
  603. dpi = "retina", width = 7, height = 7)
  604. ```
  605. We evaluate the response function for two time-shifted events:
  606. ```{r}
  607. # horizontal phase shift (in TRs) that will be added to the second sine wave:
  608. add_shift = 0.5
  609. time = seq(0, 8, 0.1)
  610. dt_odd_long_fit_shift = dt_odd_long_fit %>%
  611. # average the fitted parameters across participants:
  612. .[, by = .(classification), .(
  613. mean_freq = mean(frequency),
  614. mean_amplitude = mean(amplitude),
  615. mean_shift = mean(shift),
  616. mean_baseline = mean(baseline)
  617. )] %>%
  618. # add variable that adds shift (in TRs) for the second sine wave:
  619. mutate(mean_shift_shifted = mean_shift + add_shift) %>%
  620. mutate(amp = mean_amplitude * sin(add_shift * mean_freq * pi)) %>%
  621. mutate(freq = mean_freq / (add_shift * mean_freq + 1)) %>% setDT(.) %>%
  622. # calculate first and second response filter time-courses
  623. .[, by = .(classification), .(
  624. first = sine_truncated(params = c(mean_freq, mean_amplitude, mean_shift, mean_baseline), time),
  625. second = sine_truncated(params = c(mean_freq, mean_amplitude, mean_shift_shifted, mean_baseline), time),
  626. cosine = amp * sin(2 * pi * time * freq - 2 * pi * freq * mean_shift),
  627. t = time
  628. )] %>%
  629. # shift the cosine wave by 0.6 (only for illustrational purposes):
  630. mutate(cosine = cosine - 0.6) %>% setDT(.) %>%
  631. # signify the tails of the sine wave (that will otherwise be flattened):
  632. .[time < mean_parameters$mean_shift, ":=" (
  633. cosine_tails = cosine,
  634. cosine = NA)] %>%
  635. .[time > (mean_parameters$mean_shift + add_shift + 1/mean_parameters$mean_freq), ":=" (
  636. cosine_tails = cosine,
  637. cosine = -NA)] %>%
  638. # calculate the difference between the first and second wave
  639. # adjust by 0.2 for illustrational purposes only:
  640. mutate(difference = first - second - 0.2) %>%
  641. gather(key = "event", value = "probability", -classification, -t)
  642. ```
  643. We calculate the mean probability timecourses for the true stimulus class across participants:
  644. ```{r, ech0=TRUE}
  645. # calculate the mean decoding time course across all classes and participants:
  646. dt_odd_long_mean = dt_pred %>%
  647. # filter for oddball multivariate data and one-versus-rest classification only:
  648. filter(test_set == "test-odd_long" & class != "other" &
  649. classification == "ovr" & mask == "cv") %>%
  650. # filter out excluded participants:
  651. filter(!(id %in% sub_exclude)) %>%
  652. # filter for all trials where the predicted class matches the true stimulus:
  653. filter(class == stim) %>% setDT(.) %>%
  654. # calculate the mean decoding time courses for each participant:
  655. .[, by = .(id, classification, stim, seq_tr), .(
  656. mean_probability = mean(probability, na.rm = TRUE) * 100)] %>%
  657. # average mean decoding time courses across participants:
  658. .[, by = .(classification, stim, seq_tr), .(
  659. mean_probability = mean(mean_probability),
  660. num_subs = .N,
  661. sem_upper = mean(mean_probability) + (sd(mean_probability)/sqrt(.N)),
  662. sem_lower = mean(mean_probability) - (sd(mean_probability)/sqrt(.N))
  663. )] %>%
  664. verify(all(num_subs == 36))
  665. ```
  666. #### Figure 2c
  667. ```{r, echo=FALSE}
  668. fig_a = ggplot(data = dt_odd_long_mean, aes(
  669. x = (seq_tr - 1), y = mean_probability, group = stim)) +
  670. geom_ribbon(aes(ymin = sem_lower, ymax = sem_upper), alpha = 0.1, color = NA) +
  671. geom_line(mapping = aes(color = "Data"), alpha = 1) +
  672. geom_point(mapping = aes(color = "Data"), alpha = 1, pch = 16) +
  673. geom_line(data = subset(dt_odd_long_fit_shift, event == "first"),
  674. aes(x = t, y = probability * 100, color = "Model"),
  675. size = 2, inherit.aes = FALSE) +
  676. coord_capped_cart(left = "both", bottom = "both", ylim = c(0, 100),
  677. xlim = c(0, 6), expand = TRUE) +
  678. xlab("Time (TRs)") + ylab("Probability (%)") +
  679. scale_colour_manual(name = "", values = c("gray", "black")) +
  680. scale_fill_manual(name = "", values = c("gray", "black")) +
  681. scale_x_continuous(labels = label_fill(seq(1,7,1), mod = 1), breaks = seq(0,6,1)) +
  682. scale_y_continuous(labels = label_fill(seq(0, 80, 20), mod = 1), breaks = seq(0, 80, 20)) +
  683. theme(legend.position = "top", legend.direction = "horizontal",
  684. legend.justification = "center", legend.margin = margin(0, 0 ,0, 0),
  685. legend.box.margin = margin(t = 0, r = 0, b = -60, l = 0)) +
  686. guides(color = guide_legend(ncol = 2)) +
  687. # add the shift parameter:
  688. geom_segment(aes(x = 0, xend = mean_parameters$mean_shift, yend = 65, y = 65),
  689. color = "darkgray", linetype = "dashed") +
  690. annotate("text", x = mean_parameters$mean_shift/2, y = 70,
  691. label = paste("italic(d)"), color = "darkgray", hjust = 0.5, parse = TRUE) +
  692. # add the wavelength parameter:
  693. geom_segment(aes(x = mean_parameters$mean_shift, xend = mean_parameters$mean_shift + 1/mean_parameters$mean_freq,
  694. yend = 70, y = 70), color = "darkgray", linetype = "dashed") +
  695. annotate("text", x = mean_parameters$mean_shift + 1/(2 * mean_parameters$mean_freq), y = 75,
  696. label = paste("italic(lambda)"), color = "darkgray", hjust = 0.5, parse = TRUE) +
  697. # add the amplitude parameter:
  698. geom_segment(aes(x = mean_parameters$mean_shift + 1/(2 * mean_parameters$mean_freq),
  699. xend = mean_parameters$mean_shift + 1/(2 * mean_parameters$mean_freq),
  700. y = mean_parameters$mean_baseline * 100,
  701. yend = (mean_parameters$mean_baseline + mean_parameters$mean_amplitude) * 100),
  702. color = "darkgray", linetype = "dashed") +
  703. annotate("text", x = mean_parameters$mean_shift + 1/(2 * mean_parameters$mean_freq) - 0.25,
  704. y = (mean_parameters$mean_baseline + mean_parameters$mean_amplitude / 2) * 100,
  705. label = paste("italic(A)"), color = "darkgray", parse = TRUE) +
  706. # add the baseline parameter:
  707. geom_segment(aes(x = mean_parameters$mean_shift + 1/(2 * mean_parameters$mean_freq) - 0.2,
  708. xend = mean_parameters$mean_shift + 1/(2 * mean_parameters$mean_freq) - 0.2,
  709. y = 0, yend = mean_parameters$mean_baseline * 100),
  710. color = "darkgray", linetype = "dashed") +
  711. annotate("text", x = mean_parameters$mean_shift + 1/(2 * mean_parameters$mean_freq) - 0.25 - 0.2,
  712. y = mean_parameters$mean_baseline / 2 * 100,
  713. label = paste("italic(b)"), color = "darkgray", parse = TRUE) +
  714. annotate("text", x = 6, y = 0, label = "1 TR = 1.25 s",
  715. hjust = 1, size = rel(2)) +
  716. theme(axis.line = element_line(colour = "black"),
  717. panel.grid.major = element_blank(),
  718. panel.grid.minor = element_blank(),
  719. panel.border = element_blank(),
  720. panel.background = element_blank())
  721. fig_a
  722. ```
  723. ```{r, include=FALSE, eval=FALSE, echo=FALSE}
  724. ggsave(filename = "highspeed_plot_decoding_oddball_sine_illustration_fig1.pdf",
  725. plot = last_plot(), device = cairo_pdf, path = path_figures, scale = 1,
  726. dpi = "retina", width = 3, height = 3.1)
  727. ```
  728. #### Source Data File Fig. 2c
  729. ```{r, echo=TRUE}
  730. dt_odd_long_mean %>%
  731. select(-classification, -num_subs) %>%
  732. write.csv(., file = file.path(path_sourcedata, "source_data_figure_2c.csv"),
  733. row.names = FALSE)
  734. ```
  735. ### Response model and difference dynamics
  736. ```{r, echo=TRUE}
  737. # get the average horizontal shift of the first sine wave:
  738. t_first = mean(dt_odd_long_fit$shift)
  739. # calculate the shift of the second sine wave (shift + half a wavelength)
  740. t_second = mean(dt_odd_long_fit$shift) + add_shift + 1/mean(dt_odd_long_fit$freq)
  741. # calculate the time point of cross over between the two sine functions:
  742. t_crossover = 0.5/mean(dt_odd_long_fit$freq) + mean(dt_odd_long_fit$shift) + add_shift/2
  743. # create a vector of time steps:
  744. time = seq(0, 8, 0.1)
  745. max_prob = max(dt_odd_long_fit_shift$probability[dt_odd_long_fit_shift$event == "difference"], na.rm = TRUE)
  746. t_max_diff = dt_odd_long_fit_shift$t[dt_odd_long_fit_shift$probability == max_prob & !is.na(dt_odd_long_fit_shift$probability)]
  747. a = dt_odd_long_fit_shift$probability[dt_odd_long_fit_shift$t == t_max_diff & dt_odd_long_fit_shift$event == "first"]
  748. ```
  749. #### Figure 2d
  750. ```{r, echo=TRUE}
  751. # select colors used for plotting:
  752. colors = c("darkgray", "darkgray", "black", "dodgerblue", "red", "black")
  753. # plot sine wave difference illustration:
  754. fig_b = ggplot(data = dt_odd_long_fit_shift, aes(
  755. x = time, y = probability * 100, group = event, color = event)) +
  756. # create rectangles to indicate the forward and backward phase:
  757. annotate("rect", xmin = t_first, xmax = t_crossover, ymin = -80, ymax = 95,
  758. alpha = 0.05, fill = "dodgerblue") +
  759. annotate("rect", xmin = t_crossover, xmax = t_second, ymin = -80, ymax = 95,
  760. alpha = 0.05, fill = "red") +
  761. # plot the onset of the first event:
  762. geom_vline(xintercept = t_first, color = "gray", linetype = "dashed") +
  763. # plot the offset of the second event (d + lambda + s in manuscript text):
  764. geom_vline(xintercept = t_second, color = "gray", linetype = "dashed") +
  765. # plot the crossover (d + 0.5 * (lambda + s) in manuscript text)
  766. geom_vline(xintercept = t_crossover, color = "gray", linetype = "dashed") +
  767. # plot the horizontal line for the difference time course:
  768. geom_hline(yintercept = -20, color = "gray") +
  769. # plot the horizontal line for the cosine wave:
  770. geom_hline(yintercept = -60, color = "gray") +
  771. # plot the first and second event time courses:
  772. geom_line(aes(x = t, linetype = fct_rev(event)), na.rm = TRUE) +
  773. coord_capped_cart(left = "both", bottom = "both", ylim = c(-80, 85)) +
  774. xlab("Time (in TRs)") + ylab("Probability (a.u.)") +
  775. scale_colour_manual(name = "Serial event", values = colors) +
  776. scale_linetype_manual(values = c(rep("solid", 3), "dashed", "solid")) +
  777. scale_x_continuous(labels = c(label_fill(seq(1,7,1), mod = 1), rep("", 2)),
  778. breaks = seq(0,8,1)) +
  779. theme(axis.text.y = element_blank(), axis.ticks.y = element_line(colour = "white"),
  780. axis.line.y = element_line(colour = "white")) +
  781. theme(legend.position = "none") +
  782. annotate("label", x = 0.5, y = 45, label = "~1^'st'~event",
  783. color = "dodgerblue", parse = TRUE, size = 3) +
  784. annotate("label", x = 0.5, y = 30, label = "~2^'nd'~event",
  785. color = "red", parse = TRUE, size = 3) +
  786. annotate("label", x = 0.5, y = -7, label = "Difference",
  787. color = "black", size = 3) +
  788. annotate("label", x = 0.5, y = -35, label = "Difference\n(no flattening)",
  789. color = "darkgray", size = 3) +
  790. # add annotation for forward period:
  791. geom_segment(aes(x = t_first, xend = t_crossover, yend = 85, y = 85,
  792. colour = "segment"), color = "dodgerblue") +
  793. annotate("label", x = t_first + (t_crossover - t_first)/2, y = 85,
  794. label = "Forward period",
  795. color = "dodgerblue", fill = "white", hjust = 0.5) +
  796. # add annotations for backward period:
  797. geom_segment(aes(x = t_crossover, xend = t_second, y = 85, yend = 85,
  798. colour = "segment"), color = "red") +
  799. annotate("label", x = t_crossover + (t_second - t_crossover)/2, y = 85,
  800. label = "Backward period",
  801. color = "red", fill = "white", hjust = 0.5) +
  802. # add annotation for early forward phase
  803. geom_segment(aes(x = t_first, xend = t_first + (t_crossover - t_first)/2, yend = 70, y = 70,
  804. colour = "segment"), color = "gray") +
  805. annotate("label", x = (t_first + (t_crossover - t_first)/4),
  806. y = 70, label = "early",
  807. color = "gray", fill = "white", hjust = 0.5) +
  808. # add annotation for late forward phase
  809. geom_segment(aes(x = t_first + (t_crossover - t_first)/2, xend = t_crossover,
  810. yend = 68, y = 68, colour = "segment"), color = "gray") +
  811. annotate("label", x = (t_first + (t_crossover - t_first)/4 * 3),
  812. y = 68, label = "late",
  813. color = "gray", fill = "white", hjust = 0.5) +
  814. # add annotation for early backward phase
  815. geom_segment(aes(x = t_crossover, xend = t_crossover + (t_second - t_crossover)/2,
  816. yend = 70, y = 70, colour = "segment"), color = "gray") +
  817. annotate("label", x = (t_crossover + (t_second - t_crossover)/4),
  818. y = 70, label = "early", color = "gray", fill = "white", hjust = 0.5) +
  819. # add annotation for late backward phase
  820. geom_segment(aes(x = t_crossover + (t_second - t_crossover)/2, xend = t_second,
  821. yend = 68, y = 68, colour = "segment"), color = "gray") +
  822. annotate("label", x = (t_crossover + (t_second - t_crossover)/4 * 3),
  823. y = 68, label = "late", color = "gray", fill = "white", hjust = 0.5) +
  824. # add annotation for delta:
  825. geom_segment(aes(x = t_first + (t_crossover - t_first)/2,
  826. xend = t_first + (t_crossover - t_first)/2 + add_shift,
  827. y = 35 , yend = 35, colour = "segment"), color = "darkgray", linetype = "dotted") +
  828. annotate("text", x = t_first + (t_crossover - t_first)/2 - 0.5 + add_shift / 2, y = 36,
  829. label = "delta", color = "darkgray", hjust = 0.5, parse = TRUE) +
  830. # add annotation for TRs:
  831. annotate("text", x = 8, y = -80, label = "1 TR = 1.25 s",
  832. hjust = 1, size = rel(2)) +
  833. theme(axis.line.x = element_line(colour = "black"),
  834. panel.grid.major = element_blank(),
  835. panel.grid.minor = element_blank(),
  836. panel.border = element_blank(),
  837. panel.background = element_blank())
  838. fig_b
  839. ```
  840. ```{r, include=FALSE, eval=FALSE, echo=FALSE}
  841. ggsave(filename = "highspeed_plot_decoding_oddball_sine_illustration_figb.pdf",
  842. plot = last_plot(), device = cairo_pdf, path = path_figures, scale = 1,
  843. dpi = "retina", width = 5.5, height = 4.5)
  844. ```
  845. #### Source Data File Fig. 2d
  846. ```{r, echo=TRUE}
  847. dt_odd_long_fit_shift %>%
  848. select(-classification) %>%
  849. write.csv(., file = file.path(path_sourcedata, "source_data_figure_2d.csv"),
  850. row.names = FALSE)
  851. ```
  852. ```{r, echo = TRUE}
  853. ampfun = function(s, A) {A*cos(s*mean(dt_odd_long_fit$freq)*pi - 0.5*pi)}
  854. cs = seq(0, 0.5/mean(dt_odd_long_fit$freq), 0.01)
  855. ba = data.table(cs = cs, diff = ampfun(cs, 1))
  856. fig_c = ggplot(data = ba, aes(x = cs, y = diff)) +
  857. geom_line() +
  858. xlab("Time-shift (in TRs)") + ylab("Max. diff. in amplitude") +
  859. coord_capped_cart(left = "both", bottom = "both", xlim = c(0,3)) +
  860. scale_x_continuous(labels = c("0", rep("", 2), "Resp. peak"), breaks = seq(0,3)) +
  861. scale_y_continuous(labels = c("0", rep("", 3), "Max")) +
  862. #theme(plot.margin = unit(c(t = 10, r = 30, b = 1, l = 1), "pt")) +
  863. theme(axis.text.y = element_text(angle = 90, hjust = 0.5)) +
  864. theme(axis.line = element_line(colour = "black"),
  865. panel.grid.major = element_blank(),
  866. panel.grid.minor = element_blank(),
  867. panel.border = element_blank(),
  868. panel.background = element_blank())
  869. fig_c
  870. ```
  871. ```{r, include=FALSE, eval=FALSE, echo=FALSE}
  872. ggsave(filename = "highspeed_plot_decoding_oddball_sine_illustration_fig3.pdf",
  873. plot = last_plot(), device = cairo_pdf, path = path_figures, scale = 1,
  874. dpi = "retina", width = 3, height = 2.5)
  875. ```
  876. We plot a combination of the previous plots:
  877. ```{r, echo=TRUE, fig.width = 7, fig.height = 3}
  878. plot_grid(fig_a, fig_b, ncol = 2, hjust = c(0,0),
  879. labels = c("c", "d", "e"), rel_widths = c(2, 4),
  880. label_fontface = "bold", label_size = 14)
  881. ```
  882. ```{r, include=FALSE, eval=FALSE, echo=FALSE}
  883. ggsave(filename = "highspeed_plot_decoding_oddball_sine_illustration.pdf",
  884. plot = last_plot(), device = cairo_pdf, path = path_figures, scale = 1,
  885. dpi = "retina", width = 7, height = 3)
  886. ```
  887. ### Forward and backward period for different speeds
  888. Here, we calculate the expected forward and backward periods for two events
  889. separated by different speed levels (different values for delta).
  890. ```{r}
  891. calc_periods = function(speed, stim_dur = 0.1, num_stim = 5, tr = 1.25){
  892. # mean wavelength based on fitted models, in TRs, round to two digits:
  893. lambda = round(mean_parameters$mean_wavelength, 2)
  894. # mean phase-shift based on fitted models, in TRs, round to two digits:
  895. phase_shift = round(mean_parameters$mean_shift, 2)
  896. # delta, in sec (time shift depending on speed)
  897. delta_sec = stim_dur * (num_stim - 1) + (num_stim - 1) * speed
  898. # delta, in trs:
  899. delta_tr = delta_sec / tr
  900. # end of forward period, in TRs:
  901. fwd_end = 0.5 * (lambda + delta_tr) + phase_shift
  902. # end of the backward period, in TRs:
  903. bwd_end = lambda + phase_shift + delta_tr
  904. # forward period, in TRs:
  905. forward = list(seq(round(phase_shift) + 1, round(fwd_end - 0.5) + 1))
  906. # backward period, in TRs:
  907. backward = list(seq(round(fwd_end + 0.5) + 1, round(bwd_end) + 1))
  908. # entire relevant trial period, in rounded TRs:
  909. trial_period = list(seq(round(phase_shift) + 1, round(lambda + phase_shift + delta_tr) + 1))
  910. # save results as a data table and return results:
  911. dt = data.table(speed, lambda, delta_sec, delta_tr, fwd_end, bwd_end, forward, backward, trial_period)
  912. return(dt)
  913. }
  914. # get the relevant timeperiods for each sequence speed condition:
  915. speeds = c(0.032, 0.064, 0.128, 0.512, 2.048)
  916. dt_periods = do.call(rbind, lapply(speeds, calc_periods))
  917. rmarkdown::paged_table(dt_periods)
  918. ```
  919. We save the periods of interest for different speeds which will be used for the analysis of sequence and repetition trials:
  920. ```{r}
  921. save(dt_periods, file = file.path(path_root, 'data', "tmp", "dt_periods.Rdata"))
  922. ```
  923. ### Probability differences at different speeds
  924. We plot the probability differences at different speeds:
  925. ```{r}
  926. time = seq(0, 12, 0.1)
  927. speeds = c(0.032, 0.064, 0.128, 0.512, 2.048)
  928. stim_dur = 0.1
  929. dt_odd_seq_sim = dt_odd_long_fit %>% setDT(.) %>%
  930. # average the fitted parameters across stimuli for each participant:
  931. .[, by = .(classification, id), .(
  932. mean_freq = mean(frequency),
  933. mean_amplitude = mean(amplitude),
  934. mean_shift = mean(shift),
  935. mean_baseline = mean(baseline)
  936. )] %>%
  937. # repeat rows once for each speed condition (5 repetitions in total):
  938. .[rep(seq(1, nrow(.)), length(speeds))] %>%
  939. # add the different speed conditions for every participant:
  940. .[, by = .(classification, id), ":=" (speed = speeds, num_events = 1)] %>%
  941. # repeat rows once for each hypothetical event (here, 2 events):
  942. .[rep(seq(1, nrow(.)), num_events)] %>%
  943. # add a counter for the number of events:
  944. .[, by = .(classification, id, speed), ":=" (event = 5)] %>%
  945. # calculate a speed-specific shift for each event:
  946. .[, by = .(classification, id, speed, event), {
  947. shift = (speed * (event - 1) + (event - 1) * stim_dur)/1.25
  948. amp = mean_amplitude * sin(shift * 0.5 * mean_freq * pi)
  949. freq = (1/(1/mean_freq + shift))
  950. c_d = amp * sin(2 * pi * time * freq - 2 * pi * freq * mean_shift)
  951. # flatten the tails of the response function
  952. cidx = time < mean_shift | time > (mean_shift + (1 / freq))
  953. c_d[cidx] = 0
  954. # save the response function:
  955. list(time = time, probability = c_d)
  956. }] %>%
  957. filter(classification == "ovr") %>% setDT(.)
  958. dt_odd_seq_sim_diff = dt_odd_seq_sim %>%
  959. .[, by = .(classification, speed, time), .(
  960. num_subs = .N,
  961. mean_difference = mean(probability),
  962. sem_upper = mean(probability) + (sd(probability)/sqrt(.N)),
  963. sem_lower = mean(probability) - (sd(probability)/sqrt(.N))
  964. )] %>% verify(all(num_subs == 36))
  965. ```
  966. ```{r}
  967. save(dt_odd_seq_sim_diff, file = file.path(
  968. path_root, "data", "tmp", "dt_odd_seq_sim_diff.Rdata"))
  969. save(dt_odd_seq_sim, file = file.path(
  970. path_root, "data", "tmp", "dt_odd_seq_sim.Rdata"))
  971. ```
  972. #### Figure 2e
  973. ```{r}
  974. fig_seq_sim_diff = ggplot(data = dt_odd_seq_sim_diff, mapping = aes(
  975. x = time, y = as.numeric(mean_difference), color = as.factor(speed * 1000),
  976. fill = as.factor(speed * 1000))) +
  977. geom_hline(aes(yintercept = 0), linetype = "solid", color = "gray") +
  978. geom_ribbon(aes(ymin = sem_lower, ymax = sem_upper), alpha = 0.5, color = NA) +
  979. geom_line() +
  980. theme(legend.position = "top", legend.direction = "horizontal",
  981. legend.justification = "center", legend.margin = margin(0, 0, 0, 0),
  982. legend.box.margin = margin(t = 0, r = 0, b = -5, l = 0)) +
  983. xlab("Time from sequence onset (TRs)") + ylab("Probability difference") +
  984. scale_colour_viridis(name = "Speed (ms)", discrete = TRUE, option = "cividis") +
  985. scale_fill_viridis(name = "Speed (ms)", discrete = TRUE, option = "cividis") +
  986. coord_capped_cart(left = "both", bottom = "both", expand = TRUE,
  987. ylim = c(-0.4, 0.4), xlim = c(0, 12)) +
  988. scale_x_continuous(labels = label_fill(seq(1, 13, 1), mod = 4), breaks = seq(0, 12, 1)) +
  989. guides(color = guide_legend(nrow = 1)) +
  990. geom_segment(aes(x = 0, xend = 0, y = 0.01, yend = 0.4),
  991. arrow = arrow(length = unit(5, "pt")), color = "darkgray") +
  992. geom_segment(aes(x = 0, xend = 0, y = -0.01, yend = -0.4),
  993. arrow = arrow(length = unit(5, "pt")), color = "darkgray") +
  994. annotate(geom = "text", x = 0.4, y = 0.2, label = "Forward order",
  995. color = "darkgray", angle = 90, size = 3) +
  996. annotate(geom = "text", x = 0.4, y = -0.2, label = "Backward order",
  997. color = "darkgray", angle = 90, size = 3) +
  998. annotate("text", x = 12, y = -0.4, label = "1 TR = 1.25 s",
  999. hjust = 1, size = rel(2)) +
  1000. theme(axis.line = element_line(colour = "black"),
  1001. panel.grid.major = element_blank(),
  1002. panel.grid.minor = element_blank(),
  1003. panel.border = element_blank(),
  1004. panel.background = element_blank())
  1005. fig_seq_sim_diff
  1006. ```
  1007. ```{r, include=FALSE, eval=FALSE, echo=FALSE}
  1008. ggsave(filename = "highspeed_plot_decoding_oddball_sequence_predictions.pdf",
  1009. plot = last_plot(), device = cairo_pdf, path = path_figures, scale = 1,
  1010. dpi = "retina", width = 5.5, height = 3)
  1011. ```
  1012. #### Source Data File Fig. 2e
  1013. ```{r, echo=TRUE}
  1014. dt_odd_seq_sim_diff %>%
  1015. select(-num_subs, -classification) %>%
  1016. write.csv(., file = file.path(path_sourcedata, "source_data_figure_2e.csv"),
  1017. row.names = FALSE)
  1018. ```
  1019. ### Figure 2
  1020. ```{r, echo=TRUE, fig.width=8, fig.height=3}
  1021. upper = plot_grid(fig_odd_peak, plot.odd.long, fig_a, labels = c("a", "b", "c"), ncol = 3,
  1022. rel_widths = c(0.8, 3.5, 2), label_fontface = "bold", hjust = c(0, 0))
  1023. lower = plot_grid(fig_b, fig_seq_sim_diff, labels = c("d", "e"), nrow = 1,
  1024. label_fontface = "bold", hjust = c(0, 0), rel_widths = c(0.45, 0.53))
  1025. plot_grid(upper, lower, nrow = 2, ncol = 1, rel_heights = c(2.5, 3))
  1026. ```
  1027. ```{r}
  1028. ggsave(filename = "highspeed_plot_decoding_oddball_data.pdf",
  1029. plot = last_plot(), device = cairo_pdf, path = path_figures, scale = 1,
  1030. dpi = "retina", width = 10, height = 6.5)
  1031. ggsave(filename = "wittkuhn_schuck_figure_2.pdf",
  1032. plot = last_plot(), device = cairo_pdf, path = path_figures, scale = 1,
  1033. dpi = "retina", width = 10, height = 6.5)
  1034. ```