OSCR

Cross-wavelet analysis allows obtaining high temporal and frequency resolution in heart rate synchrony analysis.

Code ↔ Paper

3 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 3 matches
  1. [1] § Principles of cross-wavelet power analysis ↔ CrossWaveletPower_RScript.Rmd, lines 161–230 · score 0.80 · imaginary part, mother wavelet, Morlet wavelet, daughter wavelet, sine waves, Wavelet transform
  2. [2] § Principles of cross-wavelet power analysis ↔ CrossWaveletPower_RScript.Rmd, lines 76–159 · score 0.64 · widely known Fourier, Fourier transform, frequency components, Wavelet transform, amplitudes, power
  3. [3] § Principles of cross-wavelet power analysis ↔ CrossWaveletPower_RScript.Rmd, lines 161–230 · score 0.57 · shifted version, Morlet wavelet, daughter wavelet, compressed

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

R Markdown · 663 lines · 23 KB · no license · 3 matches

  1. ---
  2. title: "R Script for the manuscript ''Cross-wavelet power analysis allows obtaining high temporal and spatial resolution in physiological synchrony analysis"
  3. output: html_document
  4. date: "2026-04-20"
  5. ---
  6. ```{r setup, include=FALSE}
  7. knitr::opts_chunk$set(echo = TRUE)
  8. ```
  9. ## Synchrony analysis using cross-wavelet power
  10. Simulated data to visualize different types of synchrony using cross-wavelet power plots, compared to cross-correlation analysis.
  11. ### Load libraries and set working directory:
  12. ```{r lib}
  13. setwd("/home/bernadette/Documents/Papers/Wavelet_paper")
  14. library(WaveletComp)
  15. library(ggplot2)
  16. library(SUSY)
  17. library(grid)
  18. set.seed(100)
  19. ```
  20. ### Different synchrony types
  21. Interpersonal synchrony is defined as the correspondence between individuals' trajectories,which can take on different forms: in-phase, anti-phase, and shifted synchrony are basic synchrony types.
  22. ```{r synctype_examples}
  23. # base time series
  24. ts1 <- sin(seq(1,20, length.out = 1000))
  25. # in-phase synchrony
  26. ts2 <- ts1-0.6
  27. # anti-phase synchrony
  28. ts3 <- -ts1-0.2
  29. # shifted synchrony (person 1 leading)
  30. ts4 <- cos(seq(1, 20, length.out = 1000))-0.2
  31. # shifted synchrony (person 1 following)
  32. ts5 <- -cos(seq(1, 20, length.out = 1000))-0.2
  33. # in-phase synchrony with higher-frequency signal
  34. ts6 <- sin(seq(1,50, length.out = 1000))
  35. ts7 <- ts6-0.6
  36. # change from synchronized to de-synchronized
  37. ts8 <- periodic.series(start.period = 330,
  38. length = 1000, phase = 54,
  39. end.period = 115)-0.6
  40. # data frame to store time series combinations
  41. combs_df <- data.frame(ts_a = c(rep("ts1", 4), "ts6", "ts1"),
  42. ts_b = c(paste("ts", 2:5, sep = ""),
  43. "ts7", "ts8"))
  44. df <- data.frame(index = 1:1000,
  45. ts1 = ts1, ts2 = ts2, ts3 = ts3,
  46. ts4 = ts4, ts5 = ts5,
  47. ts6 = ts6, ts7 = ts7,
  48. ts8 = ts8)
  49. # Plot all relevant combinations of time series
  50. par(mfrow = c(4,2))
  51. plt_list1 <- list()
  52. for (comb in 1:nrow(combs_df)){
  53. ts_a_comb <- combs_df$ts_a[comb]
  54. ts_b_comb <- combs_df$ts_b[comb]
  55. plot(df$index, df[, ts_a_comb],
  56. type = "l", xlab = "Time",
  57. ylab = "Marker", ylim = c(-2, 2),
  58. cex.lab=1.5, cex.axis=1.5, cex.main = 2, col = "darkgrey",
  59. lwd = 1.5, main = LETTERS[comb])
  60. lines(df$index, df[, ts_b_comb], col = "#00A9E0",
  61. lwd = 1.5)
  62. }
  63. ```
  64. ### Principles of cross-wavelet power
  65. To understand cross-wavelet power, we start with the principle behind wavelet transform. Wavelet transform is similar to the more widely-known Fourier transform. In both analyses, a signal is decomposed into its frequency components.
  66. ```{r fourier_example}
  67. # Define periodic time series with different frequencies
  68. x1 <- periodic.series(start.period = 80, length = 1000)
  69. x2 <- periodic.series(start.period = 30, length = 1000)
  70. x3 <- periodic.series(start.period = 100, length = 1000)
  71. x4 <- periodic.series(start.period = 10, length = 1000)
  72. # Add x1-x4
  73. x_add <- 3*x1 + 1.4*x2 + 0.7*x3 + 1*x4
  74. # Plot the underlying frequency components and the
  75. # added resulting time series
  76. layout(matrix(c(0,0,0,0,0,0,
  77. 0,1,0,2,0,5,
  78. 0,0,0,0,0,5,
  79. 0,3,0,4,0,5,
  80. 0,0,0,0,0,0),
  81. 5, 6, byrow=TRUE),
  82. heights = c(0.4,1.1,0.1,1.1,0.1),
  83. widths = c(0.05,0.5,0.05,0.5,0.05, 0.7))
  84. par(mar = c(2.5, 2.5, 1, 1), oma = c(0, 0, 0, 0))
  85. plot(1:1000, 3*x1, type = "l", col = "black", lwd = 1,
  86. main = "",
  87. ylab = "Amplitude", xlab = "Time")
  88. par(xpd = TRUE)
  89. mtext("A", side = 3, line = 1, adj = -0.1, col = "grey40",
  90. padj = 0.1, cex = 1.5)
  91. par(xpd = FALSE)
  92. par(mar = c(2.5, 2.5, 1, 1))
  93. plot(1:1000, 1.4*x2, type = "l", col = "black", lwd = 1,
  94. main = "", ylim = c(-3,3),
  95. ylab = "Amplitude", xlab = "Time")
  96. mtext("B", side = 3, line = 1, adj = -0.1, col = "grey40",
  97. padj = 0.1, cex = 1.5)
  98. par(xpd = FALSE)
  99. par(mar = c(2.5, 2.5, 1, 1))
  100. plot(1:1000, 0.7*x3, type = "l", col = "black", lwd = 1,
  101. main = "", ylim = c(-3,3),
  102. ylab = "Amplitude", xlab = "Time")
  103. par(mar = c(2.5, 2.5, 1, 1))
  104. par(xpd = TRUE)
  105. mtext("C", side = 3, line = 1, adj = -0.05, col = "grey40",
  106. padj = -0.1, cex = 1.5)
  107. par(xpd = FALSE)
  108. par(mar = c(2.5, 2.5, 1, 1))
  109. plot(1:1000, 1*x4, type = "l", col = "black", lwd = 1,
  110. main = "",
  111. ylab = "Amplitude", xlab = "Time",
  112. ylim = c(-3,3))
  113. par(mar = c(2.5, 2.5, 1, 1))
  114. par(xpd = TRUE)
  115. mtext("D", side = 3, line = 1, adj = -0.05, col = "grey40",
  116. padj = -0.1, cex = 1.5)
  117. par(xpd = FALSE)
  118. par(mar = c(2.5, 2.5, 1, 1))
  119. plot(1:1000, x_add, type = "l", col = "black", lwd = 1,
  120. main = "",
  121. ylab = "Amplitude", xlab = "Time")
  122. par(mar = c(2.5, 2.5, 1, 1))
  123. par(xpd = TRUE)
  124. mtext("E", side = 3, line = 1, adj = -0.05, col = "grey40",
  125. padj = -0.1, cex = 1.5)
  126. par(xpd = FALSE)
  127. # Demonstrate the decomposition into frequency bands
  128. # 1. using Fourier transform
  129. spec.pgram(x_add,
  130. taper = 0.1,
  131. log = "no",
  132. xlab = "Frequency (Hz)",
  133. ylab = "Power",
  134. main = "Periodogram", xlim = c(0,0.2))
  135. # -> the peaks correspond to the frequencies used in the data
  136. # 2. using wavelet transform
  137. x_add_df <- data.frame(x_add = x_add)
  138. wp_x_add <- analyze.wavelet(x_add_df, loess.span = 0,
  139. verbose = F)
  140. wt.image(wp_x_add)
  141. # -> the red lines correspond to the frequencies used in the data
  142. ```
  143. Instead of decomposing the signal into sine waves, wavelet transform decomposes the signal into daughter wavelets. Below are the Morlet wavelet and some exemplary daughter wavelets.
  144. ```{r wavelets}
  145. # Define a function to create the wavelet
  146. morlet_func <- function(t, omega0 = 6) {
  147. pi^(-1/4) * exp(1i * omega0 * t) * exp(-0.5 * t^2)
  148. }
  149. # Cycling frequency set to 6
  150. omega0 = 6
  151. # Time vector
  152. t <- seq(-5, 5, length.out = 500)
  153. # Morlet mother wavelet
  154. psi <- morlet_func(t)
  155. # Compressed daugther wavelet
  156. # Scaling factor 0.5
  157. s = 0.5
  158. psi_compr <- pi^(-1/4) *
  159. (1 / sqrt(s)) *
  160. exp(1i * 6 * (t / s)) *
  161. exp(-0.5 * (t / s)^2)
  162. # Shifted version
  163. t2 <- seq(-5, 10, length.out = 550)
  164. psi2 <- morlet_func(t2)
  165. psi_shftd <-
  166. pi^(-1/4) * exp(1i * omega0 * (t2-4)) * exp(-0.5 * (t2-4)^2)
  167. # Plot the different wavelets
  168. layout(matrix(c(0,0,0,0,0,
  169. 0,1,0,2,0,
  170. 0,1,0,0,0,
  171. 0,1,0,3,0,
  172. 0,0,0,0,0),
  173. 5, 5, byrow=TRUE),
  174. heights = c(0.4,1.1,0.1,1.1,0.1),
  175. widths = c(0.05,0.5,0.05,0.5,0.05))
  176. par(mar = c(2.5, 2.5, 1, 1), oma = c(0, 0, 0, 0))
  177. plot(t, Im(psi), type = "l", col = "grey", lwd = 2,
  178. main = "",
  179. ylab = "Amplitude", xlab = "Time",
  180. ylim = c(-1.5, 1.5))
  181. lines(t, Re(psi), col = "black", lwd = 2)
  182. legend("topright", legend = c("Real part", "Imaginary part"),
  183. col = c("black", "grey"), lty = 1, lwd = 2)
  184. par(xpd = TRUE)
  185. mtext("A", side = 3, line = 1, adj = -0.1, col = "grey40",
  186. padj = 0.1, cex = 2)
  187. par(xpd = FALSE)
  188. par(mar = c(2.5, 2.5, 1, 1))
  189. plot(t, Re(psi), type = "l", col = "black", lwd = 2,
  190. main = "",
  191. ylab = "Amplitude", xlab = "Time",
  192. ylim = c(-1.5, 1.5))
  193. lines(t, Re(psi_compr), col = "#00A9E0", lwd = 2)
  194. mtext("B", side = 3, line = 1, adj = -0.1, col = "grey40",
  195. padj = 0.1, cex = 2)
  196. par(xpd = FALSE)
  197. par(mar = c(2.5, 2.5, 1, 1))
  198. plot(t2, Re(psi2), type = "l", col = "black", lwd = 2,
  199. main = "",
  200. ylab = "Amplitude", xlab = "Time",
  201. xlim = c(-5, 9), ylim = c(-1.5, 1.5))
  202. lines(t2, Re(psi_shftd), col = "#00A9E0", lwd = 2)
  203. par(mar = c(2.5, 2.5, 1, 1))
  204. par(xpd = TRUE)
  205. mtext("C", side = 3, line = 1, adj = -0.05, col = "grey40",
  206. padj = -0.1, cex = 2)
  207. par(xpd = FALSE)
  208. ```
  209. Cross-wavelet power is the overlap between two wavelet-transformed signals.
  210. ```{r create_wt_to_cwp}
  211. # Add noise to each time series (newly generated every time)
  212. noise_func <- function(sd = 1){
  213. 0.2*rnorm(1000, sd = sd)
  214. }
  215. # Simulate time series with increasing and decreasing period, respectively
  216. ts1 <- periodic.series(start.period = 2, end.period = 64,
  217. length = 1000)+noise_func()
  218. ts2 <- periodic.series(start.period = 64, end.period = 2,
  219. length = 1000)+noise_func()
  220. # Combine into a data frame
  221. df1 <- data.frame(time = 1:1000, ts1 = ts1,
  222. ts2 = ts2)
  223. # Calculate wavelet transform of each time series and cross-wavelet power
  224. w1 <- analyze.coherency(df1, c("ts1", "ts2"), verbose = F)
  225. # Plot the raw and wavelet-transformed time series as well as the cross-wavelet power
  226. layout(matrix(c(0,0,0,0,0,
  227. 0,1,0,0,0,
  228. 0,2,0,5,0,
  229. 0,0,0,5,0,
  230. 0,3,0,5,0,
  231. 0,4,0,0,0,
  232. 0,0,0,0,0),
  233. 7, 5, byrow=TRUE),
  234. heights = c(0.1,1.1, 1.1, 0.1,1.1, 1.1,0.1),
  235. widths = c(0.05,0.5,0.05, 0.5,0.05,0.05))
  236. par(mar = c(2.5, 2.5, 1, 1), oma = c(0, 0, 0, 0))
  237. plot(ts1, type = "l", cex.axis = 1.5)
  238. par(xpd = TRUE)
  239. mtext("A", side = 3, line = 1, adj = -0.1, col = "grey40",
  240. padj = 1.6, cex = 2)
  241. par(xpd = FALSE)
  242. par(mar = c(2.5, 2.5, 1, 1))
  243. wt.image(w1, my.series = 1, color.key = "i", graphics.reset = FALSE)
  244. par(mar = c(2.5, 2.5, 1, 1))
  245. plot(ts2, type = "l", cex.axis = 1.5)
  246. par(xpd = TRUE)
  247. mtext("B", side = 3, line = 1, adj = -0.1, col = "grey40",
  248. padj = 1.6, cex = 2)
  249. par(xpd = FALSE)
  250. par(mar = c(2.5, 2.5, 1, 1))
  251. wt.image(w1, my.series = 2, color.key = "i", graphics.reset = FALSE)
  252. par(mar = c(2.5, 2.5, 1, 1))
  253. wc.image(w1, color.key = "i", graphics.reset = FALSE)
  254. grid.rect(x = 0.25, y = 0.74, width = 0.48, height = 0.48,
  255. gp = gpar(col = "grey40", lwd = 1.3, fill = NA))
  256. grid.rect(x = 0.25, y = 0.25, width = 0.48, height = 0.48,
  257. gp = gpar(col = "grey40", lwd = 1.3, fill = NA))
  258. grid.rect(x = 0.73, y = 0.53, width = 0.47, height = 0.55,
  259. gp = gpar(col = "grey40", lwd = 1.3, fill = NA))
  260. par(xpd = TRUE)
  261. mtext("C", side = 3, line = 1, adj = -0.05, col = "grey40",
  262. padj = -0.1, cex = 2)
  263. par(xpd = FALSE)
  264. ```
  265. ### Synchrony analysis of simulated data
  266. Next, we apply cross-wavelet power and cross-correlational analysis to simulated data.
  267. ```{r data_sim}
  268. # Simulate time series
  269. ts1 = periodic.series(start.period = 64,
  270. length = 1000)+noise_func()
  271. # TS2: in-phase synchrony
  272. ts2 = ts1-0.9+noise_func()
  273. # TS3: antiphase synchrony
  274. ts3 = -ts1-0.2+ noise_func()
  275. # TS4: shifted synchrony
  276. ts4 = periodic.series(start.period = 64, phase = -20,
  277. length = 1000)+noise_func()
  278. # TS5: non-synchronous time series
  279. ts5 = periodic.series(start.period = 15, phase = -20,
  280. length = 1000)+noise_func()
  281. # Store time series in data frame
  282. df <- data.frame(time = 1:1000, ts1 = ts1,
  283. ts2 = ts2, ts3 = ts3,
  284. ts4 = ts4, ts5 = ts5)
  285. ```
  286. Calculate cross-wavelet power for the simulated time series
  287. ```{r types_cwp}
  288. wc1 <- analyze.coherency(df, c("ts1", "ts2"), verbose = F,
  289. lowerPeriod = 8, upperPeriod = 128)
  290. wc.image(wc1, color.key = "i")
  291. wc2 <- analyze.coherency(df, c("ts1", "ts3"), verbose = F,
  292. lowerPeriod = 8, upperPeriod = 128)
  293. wc.image(wc2, color.key = "i")
  294. wc3 <- analyze.coherency(df, c("ts1", "ts4"), verbose = F,
  295. lowerPeriod = 8, upperPeriod = 128)
  296. wc.image(wc3, color.key = "i")
  297. wc4 <- analyze.coherency(df, c("ts1", "ts5"), verbose = F,
  298. lowerPeriod = 8, upperPeriod = 128)
  299. wc.image(wc4, color.key = "i")
  300. ```
  301. Comparison to SUSY:
  302. Customized SUSY function (only plot title is changed).
  303. ```{r plot_susy_function}
  304. plot_susy_custom <- function (x, type = c(4, 5), ...)
  305. {
  306. if (!inherits(x, "susy"))
  307. stop("'x' must be an object of class 'susy'")
  308. if (!is.numeric(type))
  309. stop("'type' argument must be numeric")
  310. type = as.integer(type)
  311. if (anyNA(type) || any(type > 5L) || any(type < 1L))
  312. stop("'type' values must be in range of 1 to 5")
  313. if (anyDuplicated(type))
  314. stop("'type' values must be unique")
  315. first.plot = TRUE
  316. plot1 = function(x) {
  317. a = x$data[[1L]]
  318. b = x$data[[2L]]
  319. variablenname1 = names(x$data)[1L]
  320. variablenname2 = names(x$data)[2L]
  321. size = x$params$size
  322. segment = x$params$segment
  323. anzahlPseudosProEpoche = x$params$anzahlPseudosProEpoche
  324. numberEpochen = x$params$numberEpochen
  325. maxlagHz = x$params$maxlagHz
  326. meanccorrReal = x$lagtimes2.data$meanccorrReal
  327. meanccorrPseudo = x$lagtimes2.data$meanccorrPseudo
  328. meanccorrRealZ = x$lagtimes2.data$meanccorrRealZ
  329. meanccorrPseudoZ = x$lagtimes2.data$meanccorrPseudoZ
  330. meanccorrRealZNotAbs = x$lagtimes2.data$meanccorrRealZNotAbs
  331. meanccorrPseudoZNotAbs = x$lagtimes2.data$meanccorrPseudoZNotAbs
  332. nReal = x$segment.data$nReal
  333. nPseudo = x$segment.data$nPseudo
  334. for (t in type) {
  335. if (!first.plot)
  336. dev.new()
  337. else first.plot <<- FALSE
  338. if (t == 1L) {
  339. min0 = min(meanccorrReal, meanccorrPseudo)
  340. max0 = max(meanccorrReal, meanccorrPseudo)
  341. title0 = ""
  342. plot(seq(from = -maxlagHz, to = maxlagHz), meanccorrReal,
  343. ylim = c(min0, max0 + 0.19 * (max0 - min0)),
  344. main = title0, xlab = "lag", ylab = "correlation",
  345. type = "l", col = "green", lwd = 4)
  346. lines(seq(from = -maxlagHz, to = maxlagHz), meanccorrPseudo,
  347. col = "red", lwd = 4)
  348. legend("topright", pch = c(3), col = c("green",
  349. "red"), legend = c("meanccorr", "meanccorr pseudo"))
  350. }
  351. else if (t == 2L) {
  352. title0 = ""
  353. plot(seq(from = 1, to = numberEpochen), nReal,
  354. ylim = c(min(nReal, nPseudo), max(nReal, nPseudo)),
  355. main = title0, xlab = "segment", ylab = "correlation",
  356. type = "l", col = "green")
  357. lines(seq(from = 1, to = numberEpochen), nPseudo,
  358. col = "red", lwd = 2)
  359. legend("topright", pch = c(3), col = c("green",
  360. "red"), legend = c("real", "pseudo"))
  361. }
  362. else if (t == 3L) {
  363. min0 = min(meanccorrRealZ, meanccorrPseudoZ)
  364. max0 = max(meanccorrRealZ, meanccorrPseudoZ)
  365. title0 = ""
  366. plot(seq(from = -maxlagHz, to = maxlagHz), meanccorrRealZ,
  367. ylim = c(min0, max0 + 0.19 * (max0 - min0)),
  368. main = title0, xlab = "lag", ylab = "correlation",
  369. type = "l", col = "green", lwd = 4)
  370. lines(seq(from = -maxlagHz, to = maxlagHz), meanccorrPseudoZ,
  371. col = "red", lwd = 4)
  372. legend("topright", pch = c(3), col = c("green",
  373. "red"), legend = c("Z(meanccorr)", "Z(meanccorr pseudo)"))
  374. }
  375. else if (t == 4L) {
  376. title0 = ""
  377. plot(seq(from = 1, to = size), a, ylim = c(min(a,
  378. b), max(a, b)), main = "", xlab = "Zeit",
  379. ylab = "Wert", type = "l", pch = 20, col = "green")
  380. points(seq(from = 1, to = size), b, type = "l",
  381. pch = 20, col = "red", lwd = 2)
  382. legend("topright", pch = c(20, 20), col = c("green",
  383. "red"), legend = c(variablenname1, variablenname2))
  384. }
  385. else if (t == 5L) {
  386. min0 = min(meanccorrRealZNotAbs, meanccorrPseudoZNotAbs)
  387. max0 = max(meanccorrRealZNotAbs, meanccorrPseudoZNotAbs)
  388. title0 = ""
  389. plot(seq(from = -maxlagHz, to = maxlagHz), meanccorrRealZNotAbs,
  390. ylim = c(min0, max0 + 0.19 * (max0 - min0)),
  391. main = title0, xlab = "lag", ylab = "correlation",
  392. type = "l", col = "green", lwd = 4)
  393. lines(seq(from = -maxlagHz, to = maxlagHz), meanccorrPseudoZNotAbs,
  394. col = "red", lwd = 4)
  395. legend("topright", pch = c(3), col = c("green",
  396. "red"), legend = c("real", #"Z(meanccorr not ABS)",
  397. "pseudo"))#"Z(meanccorr pseudo not ABS)"))
  398. }
  399. }
  400. }
  401. lapply(x, plot1)
  402. invisible()
  403. }
  404. ```
  405. Make data frame compatible with SUSY package:
  406. ```{r susy_example_1}
  407. susy_df1 <- data.frame(ts1 = ts1,
  408. ts2 = ts2,
  409. ts3 = ts3,
  410. ts4 = ts4,
  411. ts5 = ts5)
  412. ```
  413. Plot of simulated data with cross-wavelet power and cross-correlation outcomes:
  414. ```{r plot_cwp_susy_results}
  415. layout(matrix(c(0,0,0,0,0,0,0,
  416. 0,1,0,5,0,9,0,
  417. 0,2,0,6,0,10,0,
  418. 0,3,0,7,0,11,0,
  419. 0,4,0,8,0,12,0,
  420. 0,0,0,0,0,0,0),
  421. 6, 7, byrow=TRUE),
  422. heights = c(0.1,1.1, 1.1, 1.1,1.1,0.1),
  423. widths = c(0.05,0.5,0.05, 0.5,0.05,0.5,0.05))
  424. par(mar = c(2.5, 2.5, 1, 1), oma = c(0, 0, 0, 0))
  425. plot(ts1, type = "l",
  426. ylim = c(min(c(ts1, ts2)),
  427. max(c(ts1, ts2))))
  428. lines(ts2, col = "#00A9E0")
  429. par(xpd = TRUE)
  430. mtext("A", side = 3, line = 1, adj = -0.2, col = "grey40",
  431. padj = 1.6, cex = 1.6)
  432. par(xpd = FALSE)
  433. par(mar = c(2.5, 2.5, 1, 1))
  434. plot(ts1, type = "l",
  435. ylim = c(min(c(ts1, ts3)),
  436. max(c(ts1, ts3))))
  437. lines(ts3, col = "#00A9E0")
  438. par(xpd = TRUE)
  439. mtext("B", side = 3, line = 1, adj = -0.2, col = "grey40",
  440. padj = 1.6, cex = 1.6)
  441. par(xpd = FALSE)
  442. par(mar = c(2.5, 2.5, 1, 1))
  443. plot(ts1, type = "l",
  444. ylim = c(min(c(ts1, ts4)),
  445. max(c(ts1, ts4))))
  446. lines(ts4, col = "#00A9E0")
  447. par(xpd = TRUE)
  448. mtext("C", side = 3, line = 1, adj = -0.2, col = "grey40",
  449. padj = 1.6, cex = 1.6)
  450. par(xpd = FALSE)
  451. par(mar = c(2.5, 2.5, 1, 1))
  452. plot(ts1, type = "l",
  453. ylim = c(min(c(ts1, ts5)),
  454. max(c(ts1, ts5))))
  455. lines(ts5, col = "#00A9E0")
  456. par(xpd = TRUE)
  457. mtext("D", side = 3, line = 1, adj = -0.2, col = "grey40",
  458. padj = 1.6, cex = 1.6)
  459. par(xpd = FALSE)
  460. par(mar = c(2.5, 2.5, 1, 1))
  461. wc.image(wc1, color.key = "i", graphics.reset = FALSE)
  462. par(mar = c(2.5, 2.5, 1, 1))
  463. wc.image(wc2, color.key = "i", graphics.reset = FALSE)
  464. par(mar = c(2.5, 2.5, 1, 1))
  465. wc.image(wc3, color.key = "i", graphics.reset = FALSE)
  466. par(mar = c(2.5, 2.5, 1, 1))
  467. wc.image(wc4, color.key = "i", graphics.reset = FALSE)
  468. par(xpd = FALSE)
  469. par(mar = c(2.5, 2.5, 1, 1))
  470. plot_susy_custom(susy(susy_df1[, c(1,2)], segment = 60, Hz = 1, maxlag = 25), type = 5)
  471. par(mar = c(2.5, 2.5, 1, 1))
  472. plot_susy_custom(susy(susy_df1[, c(1,3)], segment = 60, Hz = 1, maxlag = 25), type = 5)
  473. par(mar = c(2.5, 2.5, 1, 1))
  474. plot_susy_custom(susy(susy_df1[, c(1,4)], segment = 60, Hz = 1, maxlag = 25), type = 5)
  475. par(mar = c(2.5, 2.5, 1, 1))
  476. plot_susy_custom(susy(susy_df1[, c(1,5)], segment = 60, Hz = 1, maxlag = 25), type = 5)
  477. ```
  478. Next, we consider simulated data containing changes in synchrony over time, specifically:
  479. - changes from non-synchronized to synchronized
  480. - changes from synchronized on 1 frequency band to synchronized on 2 frequency bands
  481. - changes from synchronized on some frequency bands to synchronized on other frequency bands
  482. ```{r cwp_temp}
  483. # define base TS
  484. tsb <- periodic.series(start.period = 64,
  485. length = 1000)+
  486. periodic.series(start.period = 16,
  487. length = 1000)+
  488. periodic.series(start.period = 4,
  489. length = 1000)+
  490. noise_func()
  491. # changes from non-synchronized to synchronized
  492. ts6 <- periodic.series(start.period = 30,
  493. end.period = 64,
  494. length = 1000)+
  495. periodic.series(start.period = 8,
  496. end.period = 16,
  497. length = 1000)+
  498. periodic.series(start.period = 2,
  499. end.period = 4,
  500. length = 1000)+
  501. noise_func()
  502. # changes from synchronized on 1 frequency band to synchronized on 2 frequency bands
  503. ts7 <- periodic.series(start.period = 64,
  504. length = 1000)+
  505. periodic.series(start.period = 8,
  506. length = 1000)+
  507. periodic.series(start.period = 2,
  508. end.period = 4,
  509. length = 1000)+
  510. noise_func()
  511. # changes from synchronized on some frequency bands to synchronized on other frequency bands
  512. # define other base time series
  513. tsb_2 <- periodic.series(start.period = 64,
  514. end.period = 16,
  515. length = 1000)+
  516. periodic.series(start.period = 2,
  517. end.period = 2,
  518. length = 1000)+
  519. noise_func()
  520. # define synchronized time series
  521. ts8 <- periodic.series(start.period = 64,
  522. end.period = 16,
  523. length = 1000)+
  524. periodic.series(start.period = 8,
  525. length = 1000)+
  526. periodic.series(start.period = 4,
  527. end.period = 4,
  528. length = 1000)+
  529. noise_func()
  530. # Store simulated data in data frame
  531. df_time <- data.frame(time = 1:1000,
  532. tsb = tsb,
  533. tsb_2 = tsb_2,
  534. ts6 = ts6,
  535. ts7 = ts7,
  536. ts8 = ts8)
  537. # Calculate cross-wavelet power
  538. wc5 <- analyze.coherency(df_time, c("tsb", "ts6"),
  539. verbose = F)
  540. wc6 <- analyze.coherency(df_time, c("tsb", "ts7"),
  541. verbose = F)
  542. wc7 <- analyze.coherency(df_time, c("tsb_2", "ts8"),
  543. verbose = F)
  544. ```
  545. ```{r ccf_examples}
  546. # Store the simulated data in a data frame suitable for analysis with SUSY
  547. susy_df <- data.frame(f1 = tsb,
  548. f2 = ts6,
  549. f1 = tsb,
  550. f3 = ts7,
  551. f4 = tsb_2,
  552. f5 = ts8)
  553. ```
  554. ```{r cwp_ccf_time_freq_plot}
  555. # Plot synchrony changes over time using cross-wavelet power and cross-corelation analyses
  556. layout(matrix(c(0,0,0,0,0,0,0,
  557. 0,1,0,4,0,7,0,
  558. 0,2,0,5,0,8,0,
  559. 0,3,0,6,0,9,0,
  560. 0,0,0,0,0,0,0),
  561. 5, 7, byrow=TRUE),
  562. heights = c(0.1,1.1, 1.1, 1.1,1.1,0.1),
  563. widths = c(0.05,0.5,0.05, 0.5,0.05,0.5, 0.05))
  564. par(mar = c(2.5, 2.5, 1, 1), oma = c(0, 0, 0, 0))
  565. plot(tsb, type = "l",
  566. ylim = c(min(c(tsb, ts6)),
  567. max(c(tsb, ts6))))
  568. lines(ts6, col = "#00A9E0")
  569. par(xpd = TRUE)
  570. mtext("A", side = 3, line = 1, adj = -0.2, col = "grey40",
  571. padj = 1.6, cex = 1.6)
  572. par(xpd = FALSE)
  573. par(mar = c(2.5, 2.5, 1, 1))
  574. plot(tsb, type = "l",
  575. ylim = c(min(c(tsb, ts7)),
  576. max(c(tsb, ts7))))
  577. lines(ts7, col = "#00A9E0")
  578. par(xpd = TRUE)
  579. mtext("B", side = 3, line = 1, adj = -0.2, col = "grey40",
  580. padj = 1.6, cex = 1.6)
  581. par(xpd = FALSE)
  582. par(mar = c(2.5, 2.5, 1, 1))
  583. plot(tsb_2, type = "l",
  584. ylim = c(min(c(tsb_2, ts8)),
  585. max(c(tsb_2, ts8))))
  586. lines(ts8, col = "#00A9E0")
  587. par(xpd = TRUE)
  588. mtext("C", side = 3, line = 1, adj = -0.2, col = "grey40",
  589. padj = 1.6, cex = 1.6)
  590. par(xpd = FALSE)
  591. par(mar = c(2.5, 2.5, 1, 1))
  592. wc.image(wc5, color.key = "i", graphics.reset = FALSE)
  593. par(mar = c(2.5, 2.5, 1, 1))
  594. wc.image(wc6, color.key = "i", graphics.reset = FALSE)
  595. par(mar = c(2.5, 2.5, 1, 1))
  596. wc.image(wc7, color.key = "i", graphics.reset = FALSE)
  597. par(xpd = FALSE)
  598. par(mar = c(2.5, 2.5, 1, 1))
  599. plot_susy_custom(susy(susy_df[, c(1,2)], segment = 60, Hz = 1,
  600. maxlag = 25),
  601. type = 2)
  602. par(mar = c(2.5, 2.5, 1, 1))
  603. plot_susy_custom(susy(susy_df[, c(3,4)], segment = 60, Hz = 1,
  604. maxlag = 25),
  605. type = 2)
  606. par(mar = c(2.5, 2.5, 1, 1))
  607. plot_susy_custom(susy(susy_df[, c(5,6)], segment = 60, Hz = 1,
  608. maxlag = 25),
  609. type = 2)
  610. ```

CrossWaveletPower_RScript.Rmd at commit 3b960c7, no license · at the source

Overview

Authors: Bernadette F. Denk1,2, Stella Wienhold1,2, Nina Volkmer1,2, Nikolaus F. Troje3, Maria Meier1,4, Jens C. Pruessner1,2
  1. Department of Psychology, University of Konstanz, Konstanz, Germany
  2. Centre for the Advanced Study of Collective Behaviour, University of Konstanz, Konstanz, Germany
  3. Department of Biology, Centre for Vision Research, York University, Toronto, ON, Canada
  4. Department of Child and Adolescent Psychiatry/Psychotherapy, University of Ulm, Ulm, Germany
Institutions: University of Konstanz (Germany); York University (Canada); University of York (United Kingdom); Universität Ulm (Germany)
Journal: Frontiers in network physiology, volume 6, article 1869004
Dates: received 29 April 2026; accepted 8 June 2026; published online 8 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.3389/fnetp.2026.1869004 · PMID 42488130 · PMCID PMC13389767 · OpenAlex W7167712128
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: systems (subfield)
Methods: Connectivity, Machine learning, Preprocessing, Spectral & time-frequency
Keywords: autonomic nervous system, cross-correlation, cross-wavelet power, interpersonal synchrony, network physiology, social interaction, synchrony methods, time-frequency analysis
Journal subjects: Technology and Code
Topic: Action Observation and Synchronization (Social Psychology, Psychology), according to OpenAlex
Citations: not cited yet (Europe PMC); 55 references in the paper

Abstract

Interpersonal synchrony, the temporal correspondence of repeated behavioral, physiological, or neural measures between individuals, has been analyzed using a variety of different methods. However, the analysis method needs to be carefully chosen to ensure an adequate interpretation of the results. Here, we suggest using cross-wavelet power for interpersonal synchrony analysis due to several advantages. We demonstrate the proposed method using the example of heart rate synchrony. In cross-wavelet power analysis, synchrony is determined per frequency band and time point, allowing for both fine-grained frequency and temporal resolution. We argue that applying this approach to analyze synchrony in heart rate data provides additional information about underlying processes that may influence overall heart rate alignment between individuals. We describe the principles of cross-wavelet power analysis and compare the method to the frequently used cross-correlational approach using simulated and real data. Cross-correlation is a linear measure of the similarity between time series, which includes the quantification of leader-follower relationships. We illustrate different implications for which data series are considered to be synchronous and describe the advantages and drawbacks of using cross-wavelet analysis across various possible synchronization scenarios. The main advantage of cross-wavelet power is its high time- and frequency resolution, whereas cross-correlation is more suitable when researchers aim to differentiate between synchrony types. Finally, we provide recommendations for implementing cross-wavelet power analysis, including R code to facilitate the application to one’s own data.

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 3 matches between paragraphs and lines of code.

gitlab.inf.uni-konstanz.de/bernadette.denk/cwp_for_sync_analysis

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 3b960c7d8262fd4498ff2ffe6bdb1a134ca750a6, 20 April 2026
Languages: R (1)
Size: 2 files, 1 script
Software Heritage: not archived
Found in: “Data availability statement”
Holds: README, 1 notebook
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: ggplot2 (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
2 files

The paper's code and data availability statement is in the Data section.

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;
  • 1 script, each with its path and the digest of its content;
  • 3 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

The original contributions presented in the study are included in the article/Supplementary Material. The code for data simulation and analysis is available under https://gitlab.inf.uni-konstanz.de/bernadette.denk/cwp_for_sync_analysis.

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 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 6 authors, 8 keywords, 1 funder, 49 references.

Cite

This paper

Denk, B. F., Wienhold, S., Volkmer, N., Troje, N. F., Meier, M., & Pruessner, J. C. (2026). Cross-wavelet analysis allows obtaining high temporal and frequency resolution in heart rate synchrony analysis. Frontiers in network physiology, 6, 1869004. https://doi.org/10.3389/fnetp.2026.1869004

BibTeX

@article{denk2026cross,
author = {Denk, Bernadette F. and Wienhold, Stella and Volkmer, Nina and Troje, Nikolaus F. and Meier, Maria and Pruessner, Jens C.},
title = {{Cross-wavelet analysis allows obtaining high temporal and frequency resolution in heart rate synchrony analysis}},
journal = {Frontiers in network physiology},
year = {2026},
month = jul,
volume = {6},
pages = {1869004},
publisher = {Frontiers Media SA},
issn = {2674-0109},
doi = {10.3389/fnetp.2026.1869004},
url = {https://doi.org/10.3389/fnetp.2026.1869004},
pmid = {42488130},
pmcid = {PMC13389767}
}

RIS

TY - JOUR
AU - Denk, Bernadette F.
AU - Wienhold, Stella
AU - Volkmer, Nina
AU - Troje, Nikolaus F.
AU - Meier, Maria
AU - Pruessner, Jens C.
TI - Cross-wavelet analysis allows obtaining high temporal and frequency resolution in heart rate synchrony analysis
T2 - Frontiers in network physiology
J2 - Front Netw Physiol
PY - 2026
DA - 2026/07/08
VL - 6
SP - 1869004
SN - 2674-0109
PB - Frontiers Media SA
DO - 10.3389/fnetp.2026.1869004
UR - https://doi.org/10.3389/fnetp.2026.1869004
LA - en
ER -

CSL-JSON

{
"id": "10.3389/fnetp.2026.1869004",
"type": "article-journal",
"title": "Cross-wavelet analysis allows obtaining high temporal and frequency resolution in heart rate synchrony analysis",
"container-title": "Frontiers in network physiology",
"author": [
{
"family": "Denk",
"given": "Bernadette F."
},
{
"family": "Wienhold",
"given": "Stella"
},
{
"family": "Volkmer",
"given": "Nina"
},
{
"family": "Troje",
"given": "Nikolaus F."
},
{
"family": "Meier",
"given": "Maria"
},
{
"family": "Pruessner",
"given": "Jens C."
}
],
"container-title-short": "Front Netw Physiol",
"volume": "6",
"page": "1869004",
"DOI": "10.3389/fnetp.2026.1869004",
"PMID": "42488130",
"PMCID": "PMC13389767",
"ISSN": "2674-0109",
"publisher": "Frontiers Media SA",
"URL": "https://doi.org/10.3389/fnetp.2026.1869004",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
8
]
]
}
}

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.3758/s13428-026-03060-7 [code]
Synchronizing brains and hearts: A practical guide for caregiver-child fNIRS-ECG multimodal hyperscanning.
Journal: Behavior research methods
In common: 7 references
[2] doi:10.1111/psyp.70271 [code]
Disentangling Respiratory Phase-Dependent and Phase-Independent Components of Anticipatory Cardiac Deceleration.
Journal: Psychophysiology
In common: 3 references
[3] doi:10.1038/s41398-026-04333-7
Cardiac vagal activity during in-vivo threat exposure is associated with within-session inhibition of fear and avoidance.
Journal: Translational psychiatry
In common: 3 references
[4] doi:10.1093/nc/niag009 [code]
Autonomic indicators of self-transcendence: insights from the numadelic VR paradigm.
Journal: Neuroscience of consciousness
In common: 3 references
[5] doi:10.3758/s13415-026-01442-0 [code]
Empathy for pain persists across live two-way video interactions and viewing of prerecorded videos.
Journal: Cognitive, affective & behavioral neuroscience
In common: ggplot2, 2 references
[6] doi:10.1016/j.isci.2026.116936 [code]
Beyond neural oscillations: Stress-related aperiodic activity and aperiodic-oscillatory spectral covariation.
Journal: iScience
In common: ggplot2, systems, 1 reference
[7] doi:10.3389/fnsys.2026.1880737
Autonomic-salience stability as a candidate Gate for awake low-dose ketamine: a systems neuroscience framework with a clinical anchor.
Journal: Frontiers in systems neuroscience
In common: 2 references
[8] doi:10.1038/s42003-026-10156-5 [code]
Brain-heart interactions in late-onset major depressive disorder revealed by multimodal HRV-driven fMRI.
Journal: Communications biology
In common: 2 references
[9] doi:10.7554/elife.107088 [code]
Development of auditory and spontaneous movement responses to music over the first postnatal year.
Journal: eLife
In common: ggplot2, 1 reference
[10] doi:10.1093/nc/niag029 [code]
A data-driven approach to identifying and evaluating connectivity-based neural correlates of conscious visual perception.
Journal: Neuroscience of consciousness
In common: ggplot2, 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.