OSCR

A High-Throughput Live Imaging Platform to Investigate Circuit-Dependent Regulation of Circadian Rhythms in Brain Tissue.

Code ↔ Paper

8 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 8 matches · 1 of them tie a paragraph to a whole file, not to given lines: a weak match, whose lines are not tinted
  1. [1] § Methods › Data Analysis › Network Analysis ↔ R/processing_foo.R, lines 807–852 · score 0.80 · n_iterations, community detection, trace correlation, Leiden, isolate, igraph
  2. [2] § Methods › Data Analysis › Period Estimation ↔ R/processing_foo.R, lines 525–566 · score 0.66 · minpack.lm, nls.lm, offset, model, amplitude, phase
  3. [3] § Methods › Data Analysis › Network Analysis ↔ R/ClockCyteR.spatial-package.R, the whole file · a weak match · score 0.66 · convex hull, Leiden, deleted, igraph, segregation, weighting
  4. [4] § Methods › Statistical Analysis ↔ R/plotting_foo.R, lines 1555–1625 · score 0.60 · Shapiro Wilk normality, ANOVA, ggplot2, vectors
  5. [5] § Methods › Data Analysis › ClockCyteR ↔ R/processing_foo.R, lines 200–237 · score 0.60 · relative amplitude error, preprocess, RAE, NLLS, detrending, export
  6. [6] § Results › BMAL1 Deletion Impairs Intercluster Network Organization and Period Coherence of Axonal Ca2+ in the SCN ↔ R/processing_foo.R, lines 855–926 · score 0.53 · cluster phase variance, spatial segregation, strength, correlating, coherence, network
  7. [7] § Results › Enabling High‐Throughput Detection and Analysis of Circadian Rhythms in SCN Tissues ↔ R/processing_foo.R, lines 477–523 · score 0.53 · raw traces, FFT NLLS, smoothing, signal, amplitude
  8. [8] § Results › Spatiotemporal Characterization of Intracellular vs. Axonally‐Enriched Ca2+ Reporters in SCN Slices ↔ R/processing_foo.R, lines 1714–1791 · score 0.50 · circular variance, circadian phases, peak, Rayleigh, amplitude, traces

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 · 1,791 lines · 56 KB · other · 6 matches

  1. # collection of functions to process data
  2. # function to align phases to a standard
  3. #' Align a vector of phases to a target Circadian Time
  4. #'
  5. #' @param data Numeric vector of phases in radians.
  6. #' @param CT Numeric; target Circadian Time in hours. Defaults to \code{6}.
  7. #' @param ... Currently unused.
  8. #'
  9. #' @return Numeric vector of aligned phases in radians.
  10. #' @keywords internal
  11. align_phase <- function(data, CT = 6, ...) {
  12. phases = data
  13. mean_phase = mean(phases, na.rm = TRUE)
  14. CT = CT
  15. radCT = (CT / 12) * pi
  16. difference = mean_phase - radCT
  17. if (radCT >= mean_phase) {
  18. phases_aligned = phases + difference
  19. } else {
  20. phases_aligned = phases - difference
  21. }
  22. return(phases_aligned)
  23. }
  24. # Function to calculate AUC using the trapz function
  25. #' Calculate the area under a fluorescence trace
  26. #'
  27. #' @param y Numeric vector of fluorescence values.
  28. #' @param normalize Logical; if \code{TRUE}, divides the AUC by the number of
  29. #' time points. Defaults to \code{TRUE}.
  30. #'
  31. #' @return Numeric; the (optionally normalised) trapezoidal AUC.
  32. #' @keywords internal
  33. calculate_auc <- function(y, normalize = TRUE) {
  34. # Trapezoidal method
  35. time <- seq(0, length.out = length(y), by = 0.5)
  36. auc_trapz <- trapz(time, y)
  37. if (normalize) {
  38. auc_trapz <- auc_trapz / length(time)
  39. }
  40. return(AUC1 = auc_trapz)
  41. }
  42. #' Compute circular statistics from a period table
  43. #'
  44. #' @param period_table Data frame of period-analysis results; must contain a
  45. #' \code{phase_rad} column.
  46. #'
  47. #' @return A named list with elements \code{mean_phase}, \code{RT} (Rayleigh
  48. #' test result), \code{vectorLength}, \code{circStats} (Rayleigh p-value),
  49. #' and \code{phase_var} (angular variance).
  50. #' @keywords internal
  51. circular_stats <- function(period_table) {
  52. period_table_clean <- period_table[complete.cases(period_table),]
  53. phases <- period_table_clean$phase_rad
  54. # Compute and plot mean direction
  55. # Calculate the mean circular statistic
  56. suppressWarnings(
  57. meanTest <- circular::mean.circular(phases,
  58. Rotation = "counter",
  59. na.rm = TRUE))
  60. # Perform a Rayleigh test on RCaMP
  61. suppressWarnings(
  62. RT <- rayleigh.test(phases))
  63. # Calculate the vector length statistic
  64. vectorLength <- RT$statistic[1]
  65. # Get the p-value from the Rayleigh test
  66. circStats <- RT$p.value[1]
  67. # Round the p-value to 3 decimal places
  68. try(circStats <- round(circStats, digits = 3),
  69. silent = TRUE,
  70. circStats <- NA
  71. )
  72. # Angular variance
  73. variance <- circular::angular.variance(phases, na.rm = TRUE)
  74. list(mean_phase = meanTest,
  75. RT = RT,
  76. vectorLength = vectorLength,
  77. circStats = circStats,
  78. phase_var = variance)
  79. }
  80. #' Composite Interaction Score (CI) between two variables
  81. #'
  82. #' @param tbl1 Data frame containing the first variable and an \code{ID}
  83. #' column.
  84. #' @param var1 Character; column name of the first variable in \code{tbl1}.
  85. #' @param tbl2 Data frame containing the second variable.
  86. #' @param var2 Character; column name of the second variable in \code{tbl2}.
  87. #' @param weight Logical; if \code{TRUE}, weights the score by
  88. #' \code{1 - |var1 - var2|}. Defaults to \code{FALSE}.
  89. #' @param merge Logical; reserved for future use. Defaults to \code{FALSE}.
  90. #'
  91. #' @return A data frame with columns \code{ID} and \code{CI_score}.
  92. #' @keywords internal
  93. CI_score <- function(tbl1, var1, tbl2, var2, weight = FALSE, merge = FALSE) {
  94. # TODO add NA vals filtering
  95. score <- sqrt(tbl1[var1] * tbl2[var2])
  96. if (weight) {
  97. weight = 1 - abs(tbl1[var1] - tbl2[var2])
  98. score <- score * data$weight
  99. }
  100. CI_results <- data.frame(ID = tbl1$ID, CI_score <- score) %>%
  101. `colnames<-`(c("ID", "CI_score"))
  102. return(CI_results)
  103. }
  104. #function to compute spatial segregation index
  105. #' Compute a spatial segregation index across clusters
  106. #'
  107. #' @param node_data Data frame of node metadata with columns \code{cluster},
  108. #' \code{X}, and \code{Y}.
  109. #' @param min_cluster_size Integer; clusters with fewer cells than this are
  110. #' excluded before computing the index. Defaults to \code{3}.
  111. #'
  112. #' @return A named list with elements \code{global_index} (numeric) and
  113. #' \code{overlap_matrix} (square matrix of pairwise cluster overlaps).
  114. #' @keywords internal
  115. compute_spatial_segregation <- function(node_data, min_cluster_size = 3) {
  116. node_data <- node_data[which(node_data$cluster != "NULL"), ]
  117. if(nrow(node_data) > 0) {
  118. node_data$cluster <- droplevels(node_data$cluster)
  119. }
  120. # Filter out small clusters if needed
  121. cluster_counts <- table(node_data$cluster)
  122. keep_clusters <- names(cluster_counts[cluster_counts >= min_cluster_size])
  123. node_data <- node_data[node_data$cluster %in% keep_clusters, ]
  124. if (length(unique(node_data$cluster)) < 2) {
  125. warning("Less than 2 clusters remaining after filtering")
  126. return(list(global_index = NA, overlap_matrix = NULL))
  127. }
  128. # Convert to sf
  129. points_sf <- st_as_sf(node_data, coords = c("X", "Y"))
  130. # Compute convex hulls
  131. hulls <- points_sf |>
  132. group_by(cluster) |>
  133. summarise(geometry = st_combine(geometry), .groups = "drop") |>
  134. mutate(geometry = st_convex_hull(geometry))
  135. n <- nrow(hulls)
  136. overlap_matrix <- matrix(
  137. NA,
  138. nrow = n,
  139. ncol = n,
  140. dimnames = list(hulls$cluster, hulls$cluster)
  141. )
  142. # Pairwise segregation
  143. for (i in 1:(n - 1)) {
  144. for (j in (i + 1):n) {
  145. inter <- st_intersection(
  146. st_geometry(hulls[i, ]),
  147. st_geometry(hulls[j, ])
  148. )
  149. if (length(inter) > 0) {
  150. area_inter <- st_area(inter)
  151. area_union <- st_area(st_union(
  152. st_geometry(hulls[i, ]),
  153. st_geometry(hulls[j, ]))
  154. )
  155. segregation <- 1 - (as.numeric(area_inter) / as.numeric(area_union))
  156. } else {
  157. segregation <- 1
  158. }
  159. overlap_matrix[i, j] <- segregation
  160. overlap_matrix[j, i] <- segregation
  161. }
  162. }
  163. # Cluster sizes for weighting
  164. cluster_sizes <- table(node_data$cluster)
  165. weights <- outer(cluster_sizes, cluster_sizes)
  166. weights <- weights[upper.tri(weights)]
  167. # Global weighted segregation
  168. seg_values <- overlap_matrix[upper.tri(overlap_matrix, diag = FALSE)]
  169. global_segregation <- weighted.mean(seg_values, weights, na.rm = TRUE)
  170. return(list(
  171. global_index = global_segregation,
  172. overlap_matrix = overlap_matrix
  173. ))
  174. }
  175. # foo to analyse TS and make circular plot
  176. #' Run FFT-NLLS period analysis on a fluorescence trace matrix
  177. #'
  178. #' @param df Numeric matrix or data frame of fluorescence traces (cells ×
  179. #' frames), or a long-format table if \code{prep_tbl = FALSE}.
  180. #' @param excludeNC Logical; if \code{TRUE}, excludes non-circadian cells
  181. #' from the returned table. Defaults to \code{FALSE}.
  182. #' @param top Numeric; upper period bound in hours. Defaults to \code{30}.
  183. #' @param bottom Numeric; lower period bound in hours. Defaults to
  184. #' \code{18}.
  185. #' @param save.trace Logical; whether to store individual fitted traces.
  186. #' Defaults to \code{FALSE}.
  187. #' @param rm.start Integer; number of initial time points to discard before
  188. #' analysis. Defaults to \code{0}.
  189. #' @param prep_tbl Logical; if \code{TRUE}, calls \code{prep_table()} to
  190. #' reshape \code{df} before analysis. Defaults to \code{TRUE}.
  191. #' @param preprocess Logical; if \code{TRUE}, smooths and detrends traces
  192. #' during table preparation. Defaults to \code{TRUE}.
  193. #' @param transp Logical; if \code{TRUE}, transposes \code{df} during table
  194. #' preparation. Defaults to \code{TRUE}.
  195. #' @param add_t Logical; if \code{TRUE}, adds a time column during table
  196. #' preparation. Defaults to \code{TRUE}.
  197. #' @param time_res Numeric; temporal resolution in hours per frame. Defaults
  198. #' to \code{0.5}.
  199. #' @param smooth Logical; if \code{TRUE}, uses the smoothed trace for
  200. #' analysis. Defaults to \code{TRUE}.
  201. #' @param invert Logical; if \code{TRUE}, inverts the fluorescence signal
  202. #' before analysis. Defaults to \code{FALSE}.
  203. #' @param filterRAE Numeric threshold for the Relative Amplitude Error;
  204. #' cells with RAE above this value are excluded. Pass \code{NULL} to
  205. #' disable filtering. Defaults to \code{NULL}.
  206. #' @param ... Currently unused.
  207. #'
  208. #' @return A named list with elements \code{period_table} (data frame of
  209. #' per-cell results), \code{period_table_unfiltered}, and optionally
  210. #' \code{traces} and \code{fit_traces}.
  211. #' @export
  212. computePeriod <- function(
  213. df,
  214. excludeNC = FALSE,
  215. top = 30,
  216. bottom = 18,
  217. save.trace = FALSE,
  218. rm.start = 0,
  219. prep_tbl = TRUE,
  220. preprocess = TRUE,
  221. transp = TRUE,
  222. add_t = TRUE,
  223. time_res = 0.5,
  224. smooth = TRUE,
  225. invert = FALSE,
  226. filterRAE = NULL,
  227. ...
  228. ) {
  229. #prepare table
  230. if (prep_tbl) {
  231. suppressWarnings(
  232. data_df <- prep_table(df, rm.start, preprocess, transp, add_t, time_res)
  233. )
  234. } else {
  235. data_df <- df
  236. }
  237. # analyse period and store result in list
  238. unique_ids = unique(data_df$ID)
  239. results_list <- list()
  240. traces_list <- list()
  241. fit_trace_list <- list()
  242. smooth_trace_list <- list()
  243. plot_list <- list()
  244. message(" |--- Computing period...")
  245. cli::cli_progress_bar("Processing", total = length(unique_ids))
  246. #pb <- txtProgressBar(min = 2, max = length(unique_ids), style = 3)
  247. for (i in seq_len(length(unique_ids))) {
  248. # Filter data for the current ID
  249. ID = unique_ids[i]
  250. toget = which(data_df$ID == ID)
  251. data_ID <- data_df[toget, ]
  252. # perform linear detrending
  253. fit <- lm(data_ID$value ~ data_ID$t)
  254. detrended_data <- residuals(fit)
  255. # if required, invert the data
  256. if (invert == TRUE) {
  257. detrended_data = detrended_data * -1
  258. }
  259. if (length(detrended_data == length(data_ID$values))) {
  260. data_ID$value <- detrended_data
  261. }
  262. # smooth data with loess
  263. smooth_param <- loess(data_ID$value ~ data_ID$t, span = 0.08)
  264. data_ID$smooth <- smooth_param$fitted
  265. # Estimate period and other parameters
  266. result <- FFT_NLLS_analyse(
  267. data_ID,
  268. minPer = bottom,
  269. maxPer = top,
  270. smooth = smooth,
  271. invert = invert
  272. )
  273. # Add the ID to the result
  274. result$ID <- as.integer(ID)
  275. # add the detrended trace to a list for export
  276. traces_list[[ID]] <- detrended_data
  277. fit_trace_list[[ID]] <- result$fitted_trace
  278. smooth_trace_list[[ID]] <- data_ID$smooth
  279. # subset results list to omit fitted trace
  280. result$fitted_trace <- NULL
  281. # Append the result to the results list
  282. results_list[[ID]] <- result
  283. # setTxtProgressBar(pb, i)
  284. # cli::cli_inform("Processing ID {i}")
  285. cli::cli_progress_update()
  286. }
  287. # close(pb)
  288. cli::cli_progress_done()
  289. message(" |--- Completed")
  290. # merge all period data and traces into tables
  291. period_table = do.call(rbind, lapply(results_list, as.data.frame))
  292. traces_table = do.call(cbind, lapply(traces_list, as.data.frame)) %>%
  293. t() %>%
  294. `rownames<-`(names(traces_list))
  295. fit_table = do.call(cbind, lapply(fit_trace_list, as.data.frame)) %>%
  296. t() %>%
  297. `rownames<-`(names(fit_trace_list))
  298. smooth_table = do.call(cbind, lapply(smooth_trace_list, as.data.frame)) %>%
  299. t() %>%
  300. `rownames<-`(names(smooth_trace_list))
  301. colnames(fit_table) = colnames(traces_table)
  302. colnames(smooth_table) = colnames(traces_table)
  303. # count total traces
  304. period_counts <- sum(!is.na(period_table$phase_h))
  305. # clean table of all values outside of limits and count valid traces
  306. period_tbl_allvals <- period_table
  307. if (excludeNC == TRUE) {
  308. topvals <- which(period_table$period >= top)
  309. bottomvals <- which(period_table$period <= bottom)
  310. exclude = c(topvals, bottomvals)
  311. period_table[exclude, ] <- NA
  312. period_tbl_allvals$keep[exclude] <- 0
  313. #print(paste("Values >", top, "h | <", bottom, "h removed", sep = ""))
  314. }
  315. if (!is.null(filterRAE)) {
  316. badvals <- which(period_table$RAE >= filterRAE)
  317. period_table[badvals, ] <- NA
  318. period_tbl_allvals$keep[badvals] <- 0
  319. #print(paste("RAE vals exceeding ", filterRAE, " removed", sep = ""))
  320. }
  321. non_na_count <- sum(!is.na(period_table$phase_h))
  322. # clean table of NA values
  323. period_table <- period_table[complete.cases(period_table),]
  324. # add a column for normalized phase values
  325. period_table$phase_norm <- normalize_phase(period_table$phase_rad)
  326. # round all digits in the table
  327. period_table <- period_table %>%
  328. dplyr::mutate(dplyr::across(.cols = -all_of("ID"), ~ round(., 2)))
  329. # list to return results and traces of period analysis
  330. return_list = list(
  331. traces = traces_table,
  332. fit_traces = fit_table,
  333. smooth_traces = smooth_table,
  334. period_table = period_table,
  335. period_table_unfiltered = period_tbl_allvals
  336. )
  337. return(return_list)
  338. }
  339. # function to generate a coherence analysis
  340. #' Compute local spatial coherence for a period-analysis variable
  341. #'
  342. #' @param period_table Data frame of period-analysis results; must contain an
  343. #' \code{ID} column and the column named by \code{variable}.
  344. #' @param grid_coord Data frame of grid coordinates with three columns: ID,
  345. #' X, Y.
  346. #' @param variable Character; name of the column in \code{period_table} to
  347. #' use as the coherence metric.
  348. #' @param radius Numeric; neighbourhood radius in micrometres. Defaults to
  349. #' \code{100}.
  350. #' @param threshold Numeric; minimum coherence value to retain a cell.
  351. #' Defaults to \code{0.1}.
  352. #' @param merge Logical; if \code{TRUE}, merges coherence results back into
  353. #' \code{period_table}. Defaults to \code{TRUE}.
  354. #' @param ... Currently unused.
  355. #'
  356. #' @return A data frame of per-cell coherence scores merged with the input
  357. #' \code{period_table}.
  358. #' @keywords internal
  359. coherence_analysis <- function(
  360. period_table,
  361. grid_coord,
  362. variable,
  363. radius = 100,
  364. threshold = 0.1,
  365. merge = TRUE,
  366. ...
  367. ) {
  368. plot_data <- grid_coord %>% `colnames<-`(c("ID", "X", "Y"))
  369. # Merge tables to include period data
  370. merged_data <- left_join(plot_data, period_table, by = "ID") %>%
  371. .[!is.na(.$keep), ]
  372. # Choose the variable to visualize (e.g., period, amplitude, error)
  373. variable <- variable
  374. # list all cells within radius value
  375. coherence_results <- lapply(seq_len(nrow(merged_data)), function(i) {
  376. cell_id <- merged_data$ID[i]
  377. x_coord <- merged_data$X[i]
  378. y_coord <- merged_data$Y[i]
  379. distances <- sqrt(
  380. (merged_data$X - x_coord)^2 +
  381. (merged_data$Y - y_coord)^2
  382. )
  383. var_value <- merged_data[[variable]][i]
  384. nearby_mask <- distances <= radius & merged_data$ID != cell_id
  385. # Get periods of nearby cells
  386. nearby_var <- merged_data[[variable]][nearby_mask]
  387. if (length(nearby_var) == 0) return(NA) # No nearby cells
  388. # Calculate the absolute differences in periods
  389. var_diffs <- abs(nearby_var - var_value)
  390. # Count how many differences are within the threshold
  391. coherence_count <- sum(var_diffs <= threshold)
  392. # calculate the ratio between the coherence count and the number of nearby cells
  393. if (length(nearby_var) > 0) {
  394. coherence_ratio <- coherence_count / length(nearby_var)
  395. } else {
  396. coherence_ratio <- NA # No nearby cells
  397. }
  398. # return a table containing the ID and coherence ratio
  399. results <- data.frame(ID = cell_id, coherence_ratio = coherence_ratio)
  400. return(results)
  401. }
  402. )
  403. coherence_results <- do.call(rbind, coherence_results)
  404. # safety check to avoid returning an incompatible output
  405. if(!identical(colnames(coherence_results), c("ID", "coherence_ratio"))) {
  406. coherence_results <- data.frame(ID = NA,
  407. coherence_ratio = NA)
  408. }
  409. if (merge) {
  410. # Merge coherence results with the original period table
  411. coherence_results <- left_join(coherence_results, period_table, by = "ID")
  412. }
  413. return(coherence_results)
  414. }
  415. # perform period analysis
  416. #' Estimate period, phase, and amplitude using FFT initialisation and NLLS fitting
  417. #'
  418. #' @param data Data frame with columns \code{t} (time in hours), \code{value}
  419. #' (raw fluorescence), and \code{smooth} (smoothed fluorescence).
  420. #' @param minPer Numeric; lower period bound in hours. Defaults to \code{16}.
  421. #' @param maxPer Numeric; upper period bound in hours. Defaults to \code{32}.
  422. #' @param smooth Logical; if \code{TRUE}, fits the smoothed trace; if
  423. #' \code{FALSE}, fits the raw trace. Defaults to \code{TRUE}.
  424. #' @param invert Logical; if \code{TRUE}, inverts the signal before fitting.
  425. #' Defaults to \code{FALSE}.
  426. #'
  427. #' @return A named list with fitted \code{period}, \code{phase},
  428. #' \code{amplitude}, \code{RAE}, and \code{fitted_trace}.
  429. #' @keywords internal
  430. FFT_NLLS_analyse <- function(
  431. data,
  432. minPer = 16,
  433. maxPer = 32,
  434. smooth = TRUE,
  435. invert = FALSE
  436. ) {
  437. if (smooth) {
  438. vals <- data$smooth
  439. } else {
  440. vals <- data$value
  441. }
  442. # Perform FFT
  443. n <- length(vals)
  444. dt <- mean(diff(data$t))
  445. fft_result <- stats::fft(vals)
  446. frequencies <- seq(0, 1 / dt, length.out = n)
  447. # Identify the dominant frequency (excluding the zero frequency)
  448. dominant_frequency <- frequencies[
  449. which.max(base::Mod(fft_result)[2:(n / 2)]) + 1
  450. ]
  451. initial_period <- 1 / dominant_frequency
  452. # Define the sinusoidal model function
  453. sinusoidal_model <- function(params, t) {
  454. amplitude <- params[1]
  455. period <- params[2]
  456. phase <- params[3]
  457. offset <- params[4]
  458. return(amplitude * sin(2 * pi * t / period + phase) + offset)
  459. }
  460. # Define the residual function for nonlinear least squares
  461. residuals <- function(params, t, y) {
  462. return(y - sinusoidal_model(params, t))
  463. }
  464. # Initial parameter estimates
  465. initial_amplitude <- (max(vals) - min(vals)) / 2
  466. initial_phase <- 0
  467. initial_offset <- mean(vals)
  468. initial_params <- c(
  469. initial_amplitude,
  470. initial_period,
  471. initial_phase,
  472. initial_offset
  473. )
  474. # Perform nonlinear least squares fitting
  475. suppressWarnings(
  476. fit <- minpack.lm::nls.lm(
  477. par = initial_params,
  478. fn = residuals,
  479. t = data$t,
  480. y = vals,
  481. lower = c(-Inf, minPer, -Inf, -Inf),
  482. upper = c(Inf, maxPer, Inf, Inf)
  483. )
  484. )
  485. # Extract fitted parameters
  486. fitted_params <- fit$par
  487. fitted_amplitude <- fitted_params[1]
  488. fitted_period <- fitted_params[2]
  489. fitted_phase <- fitted_params[3]
  490. fitted_offset <- fitted_params[4]
  491. # Ensure the amplitude is positive
  492. if (fitted_amplitude < 0) {
  493. fitted_amplitude <- abs(fitted_amplitude)
  494. fitted_params[1] <- fitted_amplitude
  495. fitted_phase <- fitted_phase + pi # Adjust the phase by 180 degrees (π radians)
  496. fitted_params[3] <- fitted_phase
  497. }
  498. # save fitted phase before manipulation
  499. fitted_phase_original <- fitted_phase
  500. if (invert) {
  501. fitted_phase <- fitted_phase #+ pi
  502. }
  503. # Wrap the phase into the 0-2pi radians range and mirror it
  504. fitted_phase <- (2 * pi - fitted_phase) %% (2 * pi)
  505. if (fitted_phase < 0) {
  506. fitted_phase <- (2 * pi - fitted_phase) + 2 * pi
  507. }
  508. # Convert phase to time units (hours)
  509. fitted_phase <- circular::as.circular(
  510. fitted_phase,
  511. type = "angles",
  512. units = "radians",
  513. rotation = "clock",
  514. template = "none",
  515. modulo = "asis",
  516. zero = 0
  517. )
  518. phase_circadian <- circular::conversion.circular(
  519. fitted_phase,
  520. units = "hours"
  521. )
  522. phase_absolute <- phase_circadian * (fitted_period / 24)
  523. # Peak-based Phase Calculation #
  524. # Find peaks using the raw data
  525. peaks <- pracma::findpeaks(vals, nups = 5, ndowns = 5)
  526. if (!is.null(peaks)) {
  527. peak_times <- data$t[peaks[, 2]] # Get peak times from peak indices
  528. # First peak
  529. first_peak_time <- min(peak_times)
  530. first_peak_phase <- (first_peak_time %% fitted_period) / fitted_period * 24 # Convert to circadian hours
  531. # Last peak
  532. last_peak_time <- max(peak_times)
  533. last_peak_phase <- (last_peak_time %% fitted_period) / fitted_period * 24
  534. # Mean phase
  535. mean_peak_phase <- mean((peak_times %% fitted_period) / fitted_period * 24)
  536. } else {
  537. first_peak_phase <- NA
  538. last_peak_phase <- NA
  539. mean_peak_phase <- NA
  540. }
  541. # Compute residual error
  542. fitted_values <- sinusoidal_model(fitted_params, data$t)
  543. residual_error <- sqrt(mean((vals - fitted_values)^2))
  544. # Calculate R-squared (GOF)
  545. ss_total <- sum((vals - mean(vals))^2)
  546. ss_res <- sum((vals - fitted_values)^2)
  547. r_squared <- 1 - (ss_res / ss_total)
  548. # Compute standard deviation of residuals
  549. residual_std <- sqrt(
  550. sum((vals - fitted_values)^2) / (length(vals) - length(fitted_params))
  551. )
  552. # Compute RAE
  553. RAE <- residual_std / fitted_amplitude
  554. # Return the results
  555. result <- list(
  556. keep = TRUE,
  557. period = fitted_period,
  558. amplitude = fitted_amplitude,
  559. FFT_phase = fitted_phase_original,
  560. phase_h = phase_absolute,
  561. phase_rad = fitted_phase,
  562. phase_circ = phase_circadian,
  563. first_peak_phase_h = first_peak_phase,
  564. last_peak_phase_h = last_peak_phase,
  565. mean_peak_phase_h = mean_peak_phase,
  566. offset = fitted_offset,
  567. error = residual_error,
  568. GOF = r_squared,
  569. RAE = RAE,
  570. fitted_trace = fitted_values
  571. )
  572. return(result)
  573. }
  574. #' Run period analysis on the mean fluorescence trace of a channel
  575. #'
  576. #' @param channel_ctx List; channel context object containing elements
  577. #' \code{grid_vals} (numeric matrix, cells × frames) and \code{invert}
  578. #' (logical).
  579. #' @param filename Character; file identifier passed to \code{computePeriod()}.
  580. #' @param time_res Numeric; temporal resolution in hours per frame.
  581. #'
  582. #' @return A one-row data frame of period-analysis results for the mean trace.
  583. #' @keywords internal
  584. mean_trace_period <- function(channel_ctx, filename, time_res) {
  585. mean_trace <- colMeans(channel_ctx$grid_vals, na.rm = TRUE)
  586. matrix_onetrace <- matrix(rep(mean_trace, each = 2), nrow = 2)
  587. colnames(matrix_onetrace) <- colnames(channel_ctx$grid_vals)
  588. rownames(matrix_onetrace) <- seq(1, nrow(matrix_onetrace))
  589. suppressMessages(
  590. period_mean_res <- computePeriod(
  591. matrix_onetrace,
  592. filename,
  593. excludeNC = TRUE,
  594. top = 36,
  595. bottom = 16,
  596. time_res = time_res,
  597. invert = channel_ctx$invert,
  598. filterRAE = 0.90)
  599. )
  600. results_mean_trace <- period_mean_res$period_table[1, ]
  601. return(results_mean_trace)
  602. }
  603. #' Compute mutual information between two normalised variables
  604. #'
  605. #' @param var1 Numeric vector of values in \code{[0, 1]}.
  606. #' @param var2 Numeric vector of values in \code{[0, 1]}, same length as
  607. #' \code{var1}.
  608. #' @param n_bins Integer; number of bins for discretisation. Defaults to
  609. #' \code{10}.
  610. #' @param normalize Logical; if \code{TRUE}, normalises MI by the joint
  611. #' entropy. Defaults to \code{FALSE}.
  612. #'
  613. #' @return Numeric; the mutual information value, or \code{NA} if any input
  614. #' values are \code{NA}.
  615. #' @keywords internal
  616. mutual_information <- function(var1, var2, n_bins = 10, normalize = FALSE) {
  617. x = var1
  618. y = var2
  619. # Check if inputs are valid
  620. if (length(x) != length(y)) {
  621. stop("x and y must be the same length")
  622. }
  623. if (any(x < 0 | x > 1, na.rm = TRUE) || any(y < 0 | y > 1, na.rm = TRUE)) {
  624. stop("Inputs must be normalized to [0,1]")
  625. }
  626. if (any(is.na(x) | is.na(y))) return(mi = NA)
  627. # Discretize
  628. x_bin <- cut(
  629. x,
  630. breaks = seq(0, 1, length.out = n_bins + 1),
  631. include.lowest = TRUE,
  632. labels = FALSE
  633. )
  634. y_bin <- cut(
  635. y,
  636. breaks = seq(0, 1, length.out = n_bins + 1),
  637. include.lowest = TRUE,
  638. labels = FALSE
  639. )
  640. # Build joint frequency table
  641. joint_table <- table(x_bin, y_bin)
  642. joint_probs <- joint_table / sum(joint_table)
  643. # Marginal probabilities
  644. px <- rowSums(joint_probs)
  645. py <- colSums(joint_probs)
  646. # Compute mutual information
  647. mi <- 0
  648. for (i in seq_along(px)) {
  649. for (j in seq_along(py)) {
  650. pxy <- joint_probs[i, j]
  651. if (pxy > 0 && px[i] > 0 && py[j] > 0) {
  652. mi <- mi + pxy * log2(pxy / (px[i] * py[j]))
  653. }
  654. }
  655. }
  656. # Optional normalization: normalized MI in [0, 1]
  657. if (normalize) {
  658. h_x <- -sum(px[px > 0] * log2(px[px > 0]))
  659. h_y <- -sum(py[py > 0] * log2(py[py > 0]))
  660. mi <- mi / min(h_x, h_y) # or (h_x + h_y)/2 for symmetric NMI
  661. }
  662. return(mi)
  663. }
  664. #' Build and cluster a cell correlation network
  665. #'
  666. #' @param filename Character; file identifier used in plot filenames.
  667. #' @param interval_name Character; interval label used in plot filenames.
  668. #' @param cell_traces Numeric matrix of fluorescence traces (cells × frames);
  669. #' row names must be cell IDs.
  670. #' @param grid_coord Data frame of grid coordinates with columns \code{ID},
  671. #' \code{X}, and \code{Y}.
  672. #' @param period_table Data frame of period-analysis results containing an
  673. #' \code{ID} column.
  674. #' @param network_dir Character; directory for saving network output files.
  675. #' @param ... Optional: \code{plot_matrix} (logical, save correlation
  676. #' heatmap); \code{filter_nonrhythmic} (logical, remove NA-weight edges).
  677. #'
  678. #' @return A named list with elements \code{igraph_obj}, \code{node_data},
  679. #' and \code{network_vars}.
  680. #'
  681. #' @export
  682. network_analysis <- function(filename,
  683. interval_name,
  684. cell_traces,
  685. grid_coord,
  686. period_table,
  687. network_dir,
  688. ...
  689. ) {
  690. args <- list(...)
  691. plot_matrix <- args$plot_matrix
  692. plot_matrix <- ifelse(is.null(plot_matrix), FALSE, plot_matrix)
  693. filter_nonrhythmic <- args$filter_nonrhythmic
  694. filter_nonrhythmic <- ifelse(is.null(filter_nonrhythmic),
  695. TRUE,
  696. filter_nonrhythmic)
  697. # remove rows containing NA values
  698. period_table <- period_table[complete.cases(period_table), ]
  699. cell_names <-data.frame("del" = 0 ,"ID" = as.integer(rownames(cell_traces)))
  700. colnames(grid_coord) <- c("ID", "X", "Y")
  701. rownames(grid_coord) <- grid_coord$ID
  702. cell_coords <- left_join(cell_names, grid_coord, by = "ID") %>% select(-del)
  703. cell_nodes <- right_join(period_table, cell_coords, by = "ID")
  704. # # Now merge with period_table
  705. cell_nodes <- cell_nodes[, c("ID", setdiff(names(cell_nodes), "ID"))]
  706. names(cell_nodes)[1] <- "name"
  707. cell_nodes$name <- as.numeric(cell_nodes$name)
  708. # Reorder cell_nodes to match cell_traces row order:
  709. # TODO add the matching as a test
  710. trace_ids <- as.integer(rownames(cell_traces))
  711. cell_nodes <- cell_nodes[match(trace_ids, cell_nodes$name), ]
  712. # correlate traces
  713. edges_corr <- traces_correlation(method = "combined",
  714. traces = cell_traces,
  715. filename = filename,
  716. interval_name = interval_name,
  717. network_dir = network_dir,
  718. grid_coord = grid_coord,
  719. cell_nodes = cell_nodes,
  720. filter_nonrhythmic = TRUE,
  721. plot_matrix = plot_matrix
  722. )
  723. # create igraph
  724. g <- igraph::graph_from_data_frame(d = edges_corr,
  725. vertices = cell_nodes,
  726. directed = FALSE)
  727. # Calculate node metrics
  728. node_metrics_tbl <- node_metrics(g)
  729. node_data <- left_join(cell_nodes, node_metrics_tbl, "name")
  730. # Local clustering coefficient for each node
  731. clust_coef <- transitivity(g, type = "localundirected", isolates = "zero")
  732. # TODO create table with cluster summary coefficients
  733. # Global clustering coefficient
  734. global_clust <- transitivity(g, type = "global")
  735. # Community detection using Louvain method (good for weighted graphs)
  736. # TODO add additional methods for community detection (i.e. Kmeans)
  737. comm <- cluster_leiden(g,
  738. weights = E(g)$weight,
  739. n_iterations = 200,
  740. resolution = 0.75,
  741. objective_function = "modularity"
  742. )
  743. # Membership vector (which cluster each node belongs to)
  744. membership_vec <- membership(comm)
  745. # Modularity score (how strong the community structure is)
  746. modularity_score <- modularity(g, membership_vec, weights = E(g)$weight)
  747. # add back to the node data
  748. node_data$cluster <- as.integer(membership_vec)
  749. node_data$clust_coef <- clust_coef
  750. # returns g, node_data and mean_phase_per_cluster
  751. ordered_clusters <- order_clusters(graph_obj = g,
  752. node_data = node_data,
  753. cluster_order = "phase",
  754. network_dir = network_dir)
  755. g <- ordered_clusters$g
  756. node_data <- ordered_clusters$node_data
  757. color_map <- assign_cluster_colors(node_data) # TODO find function
  758. # create 3D plot representing amplitude, phase coherence (stored in node_data)
  759. # and cluster size (stored in cluster_size)
  760. cluster_summary_original <- summarise_cluster_data(node_data)
  761. cluster_summary_original <- left_join(cluster_summary_original,
  762. color_map,
  763. by = c("cluster")
  764. )
  765. segregation_ind <- compute_spatial_segregation(node_data)
  766. # Filter edges based on module
  767. g_filtered <- filter_edges(g)
  768. # Prepare network layout
  769. layout_list <- network_plot_layout(g_filtered, cell_nodes, color_map)
  770. # calculate cluster statistics
  771. cluster_stats <- cluster_stats(node_data)
  772. # correlate mean cluster phase and mean cluster edges strength
  773. summary_nodes <- node_data %>%
  774. group_by(cluster) %>%
  775. summarise(
  776. size = n(),
  777. mean_phase = as.numeric(mean(phase_norm, na.rm = TRUE)),
  778. mean_strength = mean(strength, na.rm = TRUE),
  779. mean_degree = mean(degree, na.rm = TRUE),
  780. mean_clust_coef = mean(clust_coef, na.rm = TRUE)
  781. )
  782. network_vars_list = list(global_clust = global_clust,
  783. modularity_score = modularity_score,
  784. node_metrics = node_metrics_tbl,
  785. cluster_df = cluster_summary_original,
  786. summary_nodes = summary_nodes,
  787. mean_cluster_phases = cluster_stats$mean_cluster_phases,
  788. variance_cluster_phases = cluster_stats$variance_cluster_phases,
  789. cluster_phases_variance = cluster_stats$cluster_phases_variance,
  790. segregation_idx = segregation_ind
  791. )
  792. network_return_list <- list(node_data = node_data,
  793. igraph = g,
  794. network_vars = network_vars_list
  795. )
  796. return(network_return_list)
  797. }
  798. #' Normalise phases to hours centred around zero
  799. #'
  800. #' @param angles Numeric vector or circular object of phase values in radians.
  801. #'
  802. #' @return Numeric vector of phase values in hours, centred around zero in
  803. #' the range \code{[-12, 12]}.
  804. #' @keywords internal
  805. normalize_phase <- function(angles) {
  806. if(!(length(angles)>0)) return(angles_in_hours <- numeric(0))
  807. # Ensure input is a circular object
  808. angles_circ <- circular(
  809. angles,
  810. type = "angles",
  811. units = "radians",
  812. modulo = "asis"
  813. )
  814. # Compute circular mean
  815. mean_angle <- mean.circular(angles_circ, na.rm = TRUE)
  816. # Normalize angles to be centered around 0 in the [-pi, pi] range
  817. normalized_angles <- (angles - as.numeric(mean_angle) + pi) %% (2 * pi) - pi
  818. # Convert radians to hours (scale π to 12)
  819. angles_in_hours <- normalized_angles * (12 / pi)
  820. return(angles_in_hours)
  821. }
  822. # prepare data into long table format
  823. #' Reshape and optionally preprocess a trace matrix into long format
  824. #'
  825. #' @param data Numeric matrix of fluorescence traces (cells × frames), with
  826. #' an optional leading ID column.
  827. #' @param rm.start Integer; number of initial time points to remove. Defaults
  828. #' to \code{0}.
  829. #' @param preprocess Logical; if \code{TRUE}, calls \code{preprocess_data()}
  830. #' to smooth and detrend. Defaults to \code{TRUE}.
  831. #' @param transp Logical; if \code{TRUE}, transposes \code{data} before
  832. #' processing. Defaults to \code{TRUE}.
  833. #' @param add_t Logical; if \code{TRUE}, adds a time column based on
  834. #' \code{time_res}. Defaults to \code{TRUE}.
  835. #' @param time_res Numeric; temporal resolution in hours per frame.
  836. #'
  837. #' @return A long-format \code{data.table} with columns \code{ID}, \code{t},
  838. #' \code{value}, and \code{smooth}.
  839. #' @keywords internal
  840. prep_table <- function(
  841. data,
  842. rm.start = 0,
  843. preprocess = TRUE,
  844. transp = TRUE,
  845. add_t = TRUE,
  846. time_res
  847. ) {
  848. # re-transpose table
  849. if (transp) {
  850. data = t(data)
  851. }
  852. # remove initial data
  853. if (rm.start != 0) {
  854. print("removing data")
  855. data = data[-c(seq(1, rm.start)), ]
  856. }
  857. # add time column
  858. if (add_t) {
  859. t = seq(0, length.out = length(data[, 1]), by = time_res)
  860. data = cbind(t, data)
  861. }
  862. # perform outliers removal, smoothening and detrending
  863. if (preprocess) {
  864. data_clean <- preprocess_data(data, grade = 3)
  865. } else {
  866. data_clean <- as.data.frame(data)
  867. }
  868. #make it into long table format
  869. data_long <- tidyr::pivot_longer(
  870. data_clean,
  871. cols = colnames(data_clean)[-1],
  872. names_to = "ID"
  873. ) %>%
  874. data.table::as.data.table(key = "ID")
  875. return(data_long)
  876. }
  877. # function to remove outliers, smooth and detrend
  878. #' Remove outliers, smooth, and detrend fluorescence traces
  879. #'
  880. #' @param data Numeric matrix of fluorescence traces with a leading time
  881. #' column (time × cells).
  882. #' @param grade Integer; polynomial degree used for detrending.
  883. #' @param mode Character; smoothing method — currently only
  884. #' \code{"mov_avg"} (moving average) is supported. Defaults to
  885. #' \code{"mov_avg"}.
  886. #' @param parallel Logical; reserved for future parallelisation. Defaults to
  887. #' \code{FALSE}.
  888. #'
  889. #' @return A numeric matrix of the same dimensions as \code{data} with
  890. #' smoothed and detrended traces.
  891. #' @export
  892. preprocess_data <- function(data, grade, mode = "mov_avg", parallel = FALSE) {
  893. # load table
  894. data_to_clear <- data
  895. # create table to store smoothened data
  896. n <- nrow(data)
  897. p <- ncol(data)
  898. clear_mat <- matrix(NA_real_, n, p)
  899. clear_mat[,1] <- data[,1]
  900. # get time values
  901. x_vals <- data[, 1]
  902. t_index <- seq_len(nrow(data))
  903. #message(" |--- Preprocessing data...")
  904. # pb <- txtProgressBar(
  905. # min = 2,
  906. # max = length(colnames(data_to_clear)),
  907. # style = 3
  908. # )
  909. pad <- 2
  910. # for cycle to go through each column and smooth the data
  911. # TODO implement parallelisation
  912. for (i in 2:p) {
  913. timeserie <- data_to_clear[, i]
  914. # create vector for cleaned data
  915. cleaned_ts <- timeserie
  916. switch (mode,
  917. "mov_avg" = {
  918. trend <- stats::lm(timeserie ~ poly(t_index, grade))
  919. detrended <- resid(trend)
  920. # outliers detection and removal
  921. sigma <- mad(detrended, constant = 1)
  922. outliers <- abs(detrended) > 2 * sigma
  923. # delete outliers from data
  924. detrended[outliers] <- NA
  925. # interpolate missing values
  926. interpolated_ts <- imputeTS::na_interpolation(detrended, option = "spline")
  927. # # perform smoothening of the data
  928. # fit <- smooth.spline(t_index, interpolated_ts, spar = 0.6)
  929. # smoothed_values <- predict(fit, t_index)$y
  930. # add edges padding
  931. x_pad <- c(
  932. interpolated_ts[1:pad],
  933. interpolated_ts,
  934. interpolated_ts[(length(interpolated_ts) - pad + 1):length(interpolated_ts)]
  935. )
  936. # moving average method
  937. averaged_pad <- stats::filter(
  938. x_pad,
  939. rep(1 / 5, 5),
  940. sides = 2
  941. )
  942. # remove edges
  943. averaged_ts <- averaged_pad[(pad + 1):(length(averaged_pad) - pad)]
  944. # add smoothened data to matrix
  945. clear_mat[, i] <- averaged_ts
  946. },
  947. "loess" = {
  948. # perform first loess approximation
  949. smoothed_series <- loess(timeserie ~ t_index, span = 0.08)
  950. smoothed_values <- predict(
  951. smoothed_series,
  952. newdata = data.frame(x = x_vals)
  953. )
  954. # calculate outliers based on distance from loess curve and sd
  955. stdev_ts <- sd(smoothed_values)
  956. outliers <- which(abs(smoothed_values - timeserie) > stdev_ts * 0.6)
  957. # delete outliers from data
  958. cleaned_ts[outliers] <- NA
  959. # perform second loess approximation on clean data
  960. smoothed_clean_series <- loess(
  961. cleaned_ts ~ seq_along(cleaned_ts),
  962. span = 0.08
  963. )
  964. smoothed_clean_values <- predict(
  965. smoothed_clean_series,
  966. newdata = data.frame(x = x_vals)
  967. )
  968. # interpolate missing values before detrending
  969. smoothed_clean_values <- imputeTS::na_interpolation(
  970. smoothed_clean_values,
  971. option = "spline"
  972. )
  973. # add detrending step
  974. smoothed_clean_detr_values <- astsa::detrend(smoothed_clean_values, grade)
  975. # add smoothened data to matrix
  976. clear_mat[, i] <- smoothed_clean_detr_values
  977. # update progressbar
  978. #setTxtProgressBar(pb, i)
  979. }
  980. )
  981. }
  982. #close(pb)
  983. #message(" |--- Completed")
  984. clear_data <- as.data.frame(clear_mat)
  985. # re add colnames
  986. colnames(clear_data) <- colnames(data)
  987. # return new table
  988. return(clear_data)
  989. }
  990. # function to align traces to the mean phase of the group
  991. #' Circularly shift traces so their phases align to the group mean
  992. #'
  993. #' @param traces_table Numeric matrix of fluorescence traces (cells × frames).
  994. #' @param period_table Data frame of period-analysis results with a
  995. #' \code{phase_rad} column.
  996. #' @param remove_start Integer; frames to discard from the start after
  997. #' shifting. Defaults to \code{0}.
  998. #' @param remove_end Integer; frames to discard from the end after shifting.
  999. #' Defaults to \code{0}.
  1000. #' @param align_to Numeric; target phase in hours; if \code{NA}, uses the
  1001. #' circular mean of the group. Defaults to \code{NA}.
  1002. #' @param debug Logical; if \code{TRUE}, enables debugging output. Defaults
  1003. #' to \code{FALSE}.
  1004. #'
  1005. #' @return A numeric matrix of phase-aligned traces, same dimensions as
  1006. #' \code{traces_table} (minus any removed frames).
  1007. #' @keywords internal
  1008. phase_align_trace <- function(
  1009. traces_table,
  1010. period_table,
  1011. remove_start = 0,
  1012. remove_end = 0,
  1013. align_to = NA,
  1014. debug = FALSE
  1015. ) {
  1016. # TODO check align phase value
  1017. # transform rad values in plus minus Pi
  1018. phases = circular::minusPiPlusPi(circular(
  1019. period_tbl$phase_rad,
  1020. units = "rad"
  1021. ))
  1022. if (is.na(align_to)) {
  1023. align_phase = circular::mean.circular(phases, na.rm = TRUE)
  1024. } else {
  1025. align_phase = circular::as.circular(
  1026. (align_to / 12) * pi,
  1027. type = "angles",
  1028. units = "radians",
  1029. rotation = "clock",
  1030. template = "none",
  1031. modulo = "asis",
  1032. zero = 0
  1033. )
  1034. }
  1035. ph_diff = align_phase - phases
  1036. adjust = circular::minusPiPlusPi(circular(ph_diff, units = "rad"))
  1037. adjust_frames = round(
  1038. circular::conversion.circular(adjust, units = "hours") * 2,
  1039. 0
  1040. )
  1041. # initialize new table to store vecs
  1042. traces_aligned <- replace(traces_table, all(), NA)
  1043. for (i in 1:dim(traces_table)[1]) {
  1044. # get vector
  1045. trace = traces_table[i, ]
  1046. adjust_fct = adjust_frames[i]
  1047. if (is.na(adjust_fct)) {
  1048. clean_trace = rep(NA, dim(traces_table)[2])
  1049. } else if (adjust_fct == 0) {
  1050. clean_trace = trace
  1051. } else if (adjust_fct < 0) {
  1052. clean_trace = c(
  1053. rep_len(NA, abs(adjust_fct)),
  1054. trace[1:(length(trace) - abs(adjust_fct))]
  1055. )
  1056. } else if (adjust_fct > 0) {
  1057. clean_trace = c(
  1058. trace[(abs(adjust_fct) + 1):length(trace)],
  1059. rep_len(NA, abs(adjust_fct))
  1060. )
  1061. }
  1062. traces_aligned[i, ] = clean_trace
  1063. if (debug) {
  1064. # browser()
  1065. plot(x = 0:(dim(traces_table)[2] - 1), y = trace, col = "red", type = "l")
  1066. lines(
  1067. x = 0:(dim(traces_table)[2] - 1),
  1068. y = clean_trace,
  1069. col = "blue",
  1070. type = "l"
  1071. )
  1072. }
  1073. }
  1074. # trim start and end of table
  1075. # evaluate if all have been shifted in one direction
  1076. # min_shift = min(adjust_frames, na.rm = TRUE)
  1077. # max_shift = max(adjust_frames, na.rm = TRUE)
  1078. # shift = min_shift*max_shift
  1079. # if(shift > 0){
  1080. # # case when all the shift happens in the same direction (need to cut only from one of the two ends)
  1081. # # TODO complete trimming in another function
  1082. # } else if(shift < 0){
  1083. # # case when shift happens in both direction (you can use the min-max rune simply)
  1084. # traces_aligned_trim = traces_aligned[ , (max(adjust_frames, na.rm = TRUE)+1):(dim(traces_aligned)[2]+min(adjust_frames, na.rm = TRUE))]
  1085. # }
  1086. #traces_aligned_trim = traces_aligned[ , (max(adjust_frames, na.rm = TRUE)+1):(dim(traces_aligned)[2]+min(adjust_frames, na.rm = TRUE))]
  1087. return(traces_aligned)
  1088. }
  1089. ranges_copy <- function(params) {
  1090. # TODO remove if and make function proper
  1091. if(!is.null(params$plotting$ranges)){
  1092. message("\nLoading plot ranges from previous experiment")
  1093. old_params_path <- file.path(previous_exp, "summary_stats", "plot_limits.rds")
  1094. old_params <- readRDS(old_params_path)
  1095. params$plotting$ranges <- old_params$plotting$ranges
  1096. '
  1097. # assign all period ranges from previous experiment into params
  1098. if(Channel2_an){
  1099. period_range_ch2 <- plot_ranges$period_range_ch2
  1100. amplitude_range_ch2 <- plot_ranges$amplitude_range_ch2
  1101. RAE_range_ch2 <- plot_ranges$RAE_range_ch2
  1102. AUC_range_ch2 <- plot_ranges$AUC_range_ch2
  1103. phase_y_lims_Ch2 <- plot_ranges$phase_y_lims_Ch2
  1104. }
  1105. if(Channel1_an & Channel2_an){
  1106. phase_y_lims_Chx <- plot_ranges$phase_y_lims_Chx
  1107. }
  1108. '
  1109. Y_range_loaded = TRUE
  1110. }
  1111. }
  1112. #' Compute per-variable display ranges across all files and intervals
  1113. #'
  1114. #' @param params List of analysis parameters produced by \code{make_params()};
  1115. #' must contain \code{channels} and \code{time$intervals}.
  1116. #' @param file_rows Data frame of file metadata; must contain columns
  1117. #' \code{file_id}, \code{file_path}, and \code{folder_path}.
  1118. #'
  1119. #' @return A named list (by channel) of per-variable min/max ranges used
  1120. #' to set consistent colour scales across plots.
  1121. #' @export
  1122. ranges_calculation <- function(params, file_rows) {
  1123. channel_ranges <- purrr::map(names(params$channels), function(ch_id) {
  1124. if (!params$channels[[ch_id]]$enabled) return(NULL)
  1125. # get all period tables
  1126. # TODO create a save function for parameters and retrieve params from path
  1127. # TODO make these two into functions
  1128. period_tbl_list <- unlist(
  1129. purrr::pmap(
  1130. file_rows,
  1131. function(file_id, file_path, folder_path,...) {
  1132. file_row <- list(file_id = file_id,
  1133. file_path = file_path,
  1134. folder_path = folder_path)
  1135. # Create a list for each interval
  1136. interval_tables <- setNames(
  1137. lapply(names(params$time$intervals), function(int_name) {
  1138. period_tbl_path <- file.path(file_row$folder_path, "rds",
  1139. paste0(file_row$file_id, "_",
  1140. int_name, "_",
  1141. ch_id, "_period_tbl_clean.rds"))
  1142. if(file.exists(period_tbl_path)) {
  1143. readRDS(period_tbl_path)
  1144. } else {
  1145. warning("File not found: ", period_tbl_path)
  1146. NULL
  1147. }
  1148. }),
  1149. paste0(file_id, "_", names(params$time$intervals))
  1150. )
  1151. return(interval_tables)
  1152. }),
  1153. recursive = FALSE
  1154. )
  1155. # Apply the same pattern to auc_tbl_list:
  1156. auc_tbl_list <- unlist(
  1157. purrr::pmap(
  1158. file_rows,
  1159. function(file_id, file_path, folder_path,...) {
  1160. file_row <- list(file_id = file_id,
  1161. file_path = file_path,
  1162. folder_path = folder_path)
  1163. interval_tables <- setNames(
  1164. lapply(names(params$time$intervals), function(int_name) {
  1165. auc_tbl_path <- file.path(file_row$folder_path, "rds",
  1166. paste0(file_row$file_id, "_",
  1167. int_name, "_",
  1168. ch_id, "_auc_results.rds"))
  1169. if(file.exists(auc_tbl_path)) {
  1170. readRDS(auc_tbl_path)
  1171. } else {
  1172. warning("File not found: ", auc_tbl_path)
  1173. NULL
  1174. }
  1175. }),
  1176. paste0(file_id, "_", names(params$time$intervals))
  1177. )
  1178. return(interval_tables)
  1179. }),
  1180. recursive = FALSE
  1181. )
  1182. # period_tbl_list <- setNames( # TODO make these two into functions
  1183. # purrr::pmap(
  1184. # file_rows,
  1185. # function(file_id, file_path, folder_path, ...) {
  1186. #
  1187. # file_row <- list(file_id = file_id,
  1188. # file_path = file_path,
  1189. # folder_path = folder_path)
  1190. #
  1191. # # generate period_tbl path
  1192. # # TODO fix case with multiple intervals, where it should gather period
  1193. # # tables from all intervals analyzed
  1194. # browser()
  1195. # period_tbl_path <- file.path(file_row$folder_path, "rds",
  1196. # paste0(file_row$file_id, "_",
  1197. # names(params$time$intervals), "_",
  1198. # ch_id, "_period_tbl_clean.rds"
  1199. # ))
  1200. # period_tbl <- readRDS(period_tbl_path)
  1201. #
  1202. # return(period_tbl)
  1203. # }),
  1204. # file_rows$file_id)
  1205. #
  1206. # auc_tbl_list <- setNames(
  1207. # purrr::pmap(
  1208. # file_rows,
  1209. # function(file_id, file_path, folder_path, ...) {
  1210. #
  1211. # file_row <- list(file_id = file_id,
  1212. # file_path = file_path,
  1213. # folder_path = folder_path)
  1214. #
  1215. # # generate auc_tbl path
  1216. # # TODO fix case with multiple intervals, where it should gather period
  1217. # # tables from all intervals analyzed
  1218. # auc_tbl_path <- file.path(file_row$folder_path, "rds",
  1219. # paste0(file_row$file_id, "_",
  1220. # names(params$time$intervals), "_",
  1221. # ch_id, "_auc_results.rds"
  1222. # ))
  1223. # auc_tbl <- readRDS(auc_tbl_path)
  1224. #
  1225. # return(auc_tbl)
  1226. # }),
  1227. # file_rows$file_id)
  1228. '
  1229. period_tbl_allpaths_Ch1 = readRDS(
  1230. file.path(wd, "summary_stats",
  1231. "period_tbl_allpaths_Ch1.rds"))
  1232. Y_range_loaded = FALSE
  1233. # load all period tables into a list
  1234. period_tbl_list_Ch1 <- setNames(lapply(seq_along(period_tbl_allpaths_Ch1),
  1235. function(i) {
  1236. readRDS(period_tbl_allpaths_Ch1[i])
  1237. }),
  1238. filenames)
  1239. '
  1240. # In each object of the list period_tbl_list, calculate the min, max
  1241. # and sd for the required variables
  1242. period_ranges <- lapply(period_tbl_list, function(tbl) {
  1243. if(!nrow(tbl) > 0) return(NULL)
  1244. data.frame(
  1245. period_min = min(tbl$period, na.rm = TRUE),
  1246. period_max = max(tbl$period, na.rm = TRUE),
  1247. period_median = median(tbl$period, na.rm = TRUE),
  1248. period_sd = safe_sd(tbl$period, na.rm = TRUE),
  1249. phase_distr_max = density(tbl$phase_norm, bw = 5) |>
  1250. (\(d) max(d$y))(),
  1251. amplitude_min = min(tbl$amplitude, na.rm = TRUE),
  1252. amplitude_max = max(tbl$amplitude, na.rm = TRUE),
  1253. amplitude_median = median(tbl$amplitude, na.rm = TRUE),
  1254. amplitude_sd = safe_sd(tbl$amplitude, na.rm = TRUE),
  1255. error_min = min(tbl$error, na.rm = TRUE),
  1256. error_max = max(tbl$error, na.rm = TRUE),
  1257. error_median = median(tbl$error, na.rm = TRUE),
  1258. error_sd = safe_sd(tbl$error, na.rm = TRUE),
  1259. RAE_min = min(tbl$RAE, na.rm = TRUE),
  1260. RAE_max = max(tbl$RAE, na.rm = TRUE),
  1261. RAE_median = median(tbl$RAE, na.rm = TRUE),
  1262. RAE_sd = safe_sd(tbl$RAE, na.rm = TRUE)
  1263. )
  1264. })
  1265. # collate all into a table
  1266. period_tbl_ranges_df <- do.call(rbind, period_ranges)
  1267. # if AUC gets computed, get all AUC results
  1268. AUC_ranges <- lapply(auc_tbl_list, function(vec) {
  1269. if(!nrow(vec) > 0) return(NULL)
  1270. data.frame(
  1271. AUC_min = min(vec[,2], na.rm = TRUE),
  1272. AUC_max = max(vec[,2], na.rm = TRUE),
  1273. AUC_median = median(vec[,2], na.rm = TRUE),
  1274. AUC_sd = sd(vec[,2], na.rm = TRUE)
  1275. )
  1276. })
  1277. # collate all into a table
  1278. AUC_tbl_ranges_df <- do.call(rbind, AUC_ranges)
  1279. # determine ranges
  1280. period_range <-
  1281. c(
  1282. round(
  1283. (mean(period_tbl_ranges_df$period_min, na.rm = TRUE)
  1284. - (mean(period_tbl_ranges_df$period_sd, na.rm = TRUE))),
  1285. 1),
  1286. round(
  1287. (mean(period_tbl_ranges_df$period_max, na.rm = TRUE)
  1288. + (mean(period_tbl_ranges_df$period_sd, na.rm = TRUE))),
  1289. 1)
  1290. )
  1291. amplitude_range <-
  1292. c(
  1293. round(
  1294. (mean(period_tbl_ranges_df$amplitude_min, na.rm = TRUE)
  1295. -(mean(period_tbl_ranges_df$amplitude_sd, na.rm = TRUE)/2)),
  1296. 0),
  1297. round(
  1298. (median(period_tbl_ranges_df$amplitude_max, na.rm = TRUE)
  1299. + (mean(period_tbl_ranges_df$amplitude_sd, na.rm = TRUE)/2)),
  1300. 0)
  1301. )
  1302. RAE_range <-
  1303. c(
  1304. round(
  1305. (min(period_tbl_ranges_df$RAE_min, na.rm = TRUE)
  1306. -(mean(period_tbl_ranges_df$RAE_sd, na.rm = TRUE)/2)),
  1307. 1),
  1308. round(
  1309. (max(period_tbl_ranges_df$RAE_max, na.rm = TRUE)
  1310. + (mean(period_tbl_ranges_df$RAE_sd, na.rm = TRUE)/2)),
  1311. 1)
  1312. )
  1313. error_range <-
  1314. c(
  1315. round(
  1316. (min(period_tbl_ranges_df$error_min, na.rm = TRUE)
  1317. -(mean(period_tbl_ranges_df$error_sd, na.rm = TRUE)/2)),
  1318. 1),
  1319. round(
  1320. (max(period_tbl_ranges_df$error_max, na.rm = TRUE)
  1321. + (mean(period_tbl_ranges_df$error_sd, na.rm = TRUE)/2)),
  1322. 1)
  1323. )
  1324. AUC_range <-
  1325. c(
  1326. round(
  1327. (min(AUC_tbl_ranges_df$AUC_min, na.rm = TRUE)
  1328. - (mean(AUC_tbl_ranges_df$AUC_sd, na.rm = TRUE))),
  1329. 0),
  1330. round(
  1331. (median(AUC_tbl_ranges_df$AUC_max, na.rm = TRUE)
  1332. + (mean(AUC_tbl_ranges_df$AUC_sd, na.rm = TRUE))),
  1333. 0)
  1334. )
  1335. phase_distr_range <-
  1336. c(0,
  1337. (round(
  1338. max(period_tbl_ranges_df$phase_distr_max, na.rm = TRUE),
  1339. 2)*1.3)
  1340. )
  1341. # assign to list to be added to params
  1342. channel_ranges <- list(
  1343. period_range = period_range,
  1344. amplitude_range = amplitude_range,
  1345. phase_distr_range = phase_distr_range,
  1346. RAE_range = RAE_range,
  1347. error_range = error_range,
  1348. AUC_range = AUC_range
  1349. )
  1350. return(channel_ranges)
  1351. }) # end of purrr::walk
  1352. channel_ranges <- setNames(channel_ranges, names(params$channels))
  1353. return(channel_ranges)
  1354. }
  1355. # create a similarity scores between two tables that share a key variable
  1356. #' Compute a pairwise similarity score between two variables
  1357. #'
  1358. #' @param tbl1 Data frame containing \code{var1} and \code{key}.
  1359. #' @param tbl2 Data frame containing \code{var2} and \code{key}.
  1360. #' @param var1 Character; column name of the first variable in \code{tbl1}.
  1361. #' @param var2 Character; column name of the second variable in \code{tbl2}.
  1362. #' @param key Character; name of the shared ID column used to join the
  1363. #' tables. Defaults to \code{"ID"}.
  1364. #'
  1365. #' @return A data frame with the \code{key} column and a \code{similarity}
  1366. #' column (\code{1 - |var1 - var2|}).
  1367. #' @keywords internal
  1368. similarity_score <- function(tbl1, tbl2, var1, var2, key = "ID") {
  1369. # browser()
  1370. # Ensure both inputs are data frames
  1371. stopifnot(is.data.frame(tbl1), is.data.frame(tbl2))
  1372. # keyvar = !!sym(key)
  1373. # merge the tables by key
  1374. merged_data <- left_join(tbl1, tbl2, by = key) %>%
  1375. `colnames<-`(c(key, var1, var2))
  1376. # Extract the vectors
  1377. x <- merged_data[[var1]]
  1378. y <- merged_data[[var2]]
  1379. # # Check for values outside 0–1
  1380. # if (any(x < 0 | x > 1, na.rm = TRUE) || any(y < 0 | y > 1, na.rm = TRUE)) {
  1381. # warning("One or both variables have values outside [0,1].")
  1382. # }
  1383. #
  1384. # # Remove NA pairs
  1385. # valid <- complete.cases(x, y)
  1386. # if (!all(valid)) {
  1387. # warning(sprintf("Removing %d incomplete rows (NA)", sum(!valid)))
  1388. # x <- x[valid]
  1389. # y <- y[valid]
  1390. # }
  1391. # Compute similarity score
  1392. similarity <- 1 - abs(merged_data[[var1]] - merged_data[[var2]])
  1393. merged_data$similarity <- similarity
  1394. #remove var1 and var2 columns
  1395. merged_data <- merged_data %>% select(-!!sym(var1), -!!sym(var2))
  1396. return(merged_data)
  1397. }
  1398. #' Summarise period analysis results into a single-row table
  1399. #'
  1400. #' @param period_res Named list as returned by \code{computePeriod()}; must
  1401. #' contain \code{period_table} and \code{period_table_unfiltered}.
  1402. #' @param circ_stats Named list as returned by \code{circular_stats()};
  1403. #' must contain \code{vectorLength} and \code{circStats}.
  1404. #' @param Ch_rep Character; channel label used as a column prefix. Defaults
  1405. #' to \code{"Ch"}.
  1406. #' @param coherence Logical; if \code{TRUE}, includes coherence metrics in
  1407. #' the summary. Defaults to \code{FALSE}.
  1408. #'
  1409. #' @return A one-row data frame of summary statistics for the analysis.
  1410. #' @keywords internal
  1411. summarizePeriod <- function(
  1412. period_res,
  1413. circ_stats,
  1414. # filename = "sample",
  1415. # interval_name = "Int",
  1416. Ch_rep = "Ch",
  1417. coherence = FALSE
  1418. ) {
  1419. period_table <- period_res$period_table
  1420. # TODO add check that
  1421. # trace stats
  1422. period_counts <- length(period_table$phase_h)
  1423. non_na_count <- sum(!is.na(period_res$period_table_unfiltered$phase_h))
  1424. if(non_na_count > 0) {
  1425. # period stats
  1426. period_var = stats::var(period_table$period, na.rm = TRUE) |> round(3)
  1427. period_mean = mean(period_table$period, na.rm = TRUE) |> round(2)
  1428. # phase stats
  1429. circ_phase_mean_rad = circular::mean.circular(
  1430. circular(
  1431. period_table$phase_rad,
  1432. units = "rad",
  1433. rotation = "clock",
  1434. template = "none",
  1435. modulo = "asis",
  1436. zero = 0
  1437. ),
  1438. na.rm = TRUE
  1439. ) |> round(3)
  1440. circ_phase_mean = circular::mean.circular(
  1441. circular(
  1442. period_table$phase_circ,
  1443. units = "hour",
  1444. rotation = "clock",
  1445. template = "none",
  1446. modulo = "asis",
  1447. zero = 0
  1448. ),
  1449. na.rm = TRUE
  1450. ) |> round(2)
  1451. phase_rad_var = circular::angular.variance(
  1452. circular(
  1453. period_table$phase_rad,
  1454. units = "radians",
  1455. rotation = "clock",
  1456. template = "none",
  1457. modulo = "asis",
  1458. zero = 0
  1459. ),
  1460. na.rm = TRUE
  1461. ) |> round(3)
  1462. phase_var = circular::angular.variance(
  1463. circular(
  1464. period_table$phase_circ,
  1465. units = "radians",
  1466. rotation = "clock",
  1467. template = "none",
  1468. modulo = "asis",
  1469. zero = 0
  1470. ),
  1471. na.rm = TRUE
  1472. ) |> round(3)
  1473. ' Deprecated
  1474. # first peak phase
  1475. first_peak_phase_mean = circular::mean.circular(
  1476. circular(
  1477. period_table$first_peak_phase_h,
  1478. units = "hour",
  1479. rotation = "clock",
  1480. template = "none",
  1481. modulo = "asis",
  1482. zero = 0
  1483. ),
  1484. na.rm = TRUE
  1485. )
  1486. first_phase_var = circular::angular.variance(
  1487. circular(
  1488. period_table$first_peak_phase_h,
  1489. units = "hour",
  1490. rotation = "clock",
  1491. template = "none",
  1492. modulo = "asis",
  1493. zero = 0
  1494. ),
  1495. na.rm = TRUE
  1496. )
  1497. '
  1498. # other stats
  1499. vector_length <- ifelse(!is.null(circ_stats$vectorLength),
  1500. round(circ_stats$vectorLength, 3),
  1501. NA)
  1502. amplitude_mean <- mean(abs(period_table$amplitude), na.rm = TRUE) |> round(2)
  1503. amplitude_var <- stats::var(period_table$amplitude, na.rm = TRUE) |> round(3)
  1504. RAE_mean <- mean(period_table$RAE, na.rm = TRUE) |> round(2)
  1505. RAE_var <- stats::var(period_table$RAE, na.rm = TRUE) |> round(2)
  1506. error_mean <- mean(period_table$error, na.rm = TRUE) |> round(2)
  1507. } else {
  1508. period_var = NA
  1509. period_mean = NA
  1510. circ_phase_mean_rad = NA
  1511. phase_rad_var = NA
  1512. vector_length = NA
  1513. circ_phase_mean = NA
  1514. phase_var = NA
  1515. amplitude_mean = NA
  1516. amplitude_var = NA
  1517. RAE_mean = NA
  1518. RAE_var = NA
  1519. error_mean = NA
  1520. }
  1521. # return values outside
  1522. outreturn_df <- data.frame(
  1523. # filename = as.factor(filename),
  1524. # interval = as.factor(interval_name),
  1525. channel_label = as.factor(Ch_rep), # channel label
  1526. trace_no = non_na_count, # number of rhythmic cells
  1527. trace_tot = period_counts, # number of cells sampled
  1528. period_var = period_var, # variance of period values
  1529. period_mean = period_mean, # mean of period values in hours
  1530. phase_rad_mean = circ_phase_mean_rad, # mean of period values in radians
  1531. phase_rad_var = phase_rad_var, # circular variance of phases in radians
  1532. vector_length = vector_length, # vector length of rayleigh plot
  1533. circ_phase_mean = circ_phase_mean, # circular mean of circadian phase values in hours
  1534. circ_phase_var = phase_var, # circular variance of circadian phases
  1535. # first_peak_phase_mean = first_peak_phase_mean,
  1536. # first_phase_var = first_phase_var,
  1537. amplitude_mean = amplitude_mean,
  1538. amplitude_var = amplitude_var,
  1539. RAE_mean = RAE_mean,
  1540. RAE_var = RAE_var,
  1541. error_mean = error_mean
  1542. )
  1543. if(coherence){
  1544. # spatial coherence stats
  1545. #period
  1546. period_crn_var = stats::var(period_table$period_crn, na.rm = TRUE) |>
  1547. round(3)
  1548. period_crn_mean = mean(period_table$period_crn, na.rm = TRUE) |>
  1549. round(3)
  1550. #phase
  1551. phase_crn_var = stats::var(period_table$phase_crn, na.rm = TRUE) |>
  1552. round(3)
  1553. phase_crn_mean = mean(period_table$phase_crn, na.rm = TRUE) |>
  1554. round(3)
  1555. #amplitude
  1556. amplitude_crn_var = stats::var(period_table$amp_crn, na.rm = TRUE) |>
  1557. round(3)
  1558. amplitude_crn_mean = mean(period_table$amp_crn, na.rm = TRUE) |>
  1559. round(3)
  1560. # add to table
  1561. outreturn_df <- cbind(outreturn_df,
  1562. period_crn_mean, period_crn_var,
  1563. phase_crn_mean, phase_crn_var,
  1564. amplitude_crn_mean, amplitude_crn_var)
  1565. }
  1566. return(outreturn_df)
  1567. }

processing_foo.R at commit 0c2d463, under other · at the source

Overview

Authors: Marco Ferrari1,2, Natalie Ness1,2, Julieta Acosta1,2, Marco Brancaccio1,2
  1. UK Dementia Research Institute at Imperial College London London United Kingdom
  2. Department of Brain Science Imperial College London London United Kingdom
Institutions: UK Dementia Research Institute (United Kingdom); Imperial College London (United Kingdom)
Journal: Advanced science (Weinheim, Baden-Wurttemberg, Germany), volume 13, issue 40, article e75427
Dates: received 3 February 2026; accepted 11 April 2026; published online 28 April 2026; in print July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1002/advs.75427 · PMID 42048014 · PMCID PMC13335426 · OpenAlex W7158352060
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: mouse (organism)
Methods: Statistics, Connectivity, Preprocessing, Spectral & time-frequency, fMRI & imaging
Keywords: brain tissue, circadian rhythms, live‐imaging
MeSH: Brain*, Circadian Rhythm*, Neurons*, Suprachiasmatic Nucleus*, Animals, ARNTL Transcription Factors, Astrocytes, Calcium, Mice (* major topic)
Topic: Circadian rhythm and melatonin (Endocrine and Autonomic Systems, Neuroscience), according to OpenAlex
Funding: UK Dementia Research Institute (UKDRI‐5007); Michael Uren Foundation
Citations: not cited yet (Europe PMC); 62 references in the paper
Research resources: RRID:IMSR_JAX:006852

Abstract

Circadian function in multicellular organisms arises from coordinated interactions amongst diverse cellular tissue populations. Existing approaches for long‐term imaging of within‐tissue circadian regulation remain low‐throughput, highly specialized, and largely inaccessible. Here, we developed ClockCyte, a high‐content fluorescent live‐imaging platform that enables continuous monitoring of circadian rhythms in up to 144 brain tissue samples. Using the mouse suprachiasmatic nucleus as a model, ClockCyte captures the differential circadian tissue regulation of neurons and astrocytes. We further identified a previously uncharacterized oscillatory circadian compartment in axonal calcium, showing highly homogeneous activity, opposed to waves of intracellular neuronal calcium. By deleting Bmal1 in neurons, we reveal the network underpinnings connecting clock gene expression to network‐wide axonal regulation. The discovery of distinct circadian properties of axonal calcium and their disruption by Bmal1 ablation highlights the potential to reveal new principles of intra‐tissue network‐level circadian organization. More broadly, this approach will enable systematic explorations of how cell‐type‐specific and compartmentalized subcellular rhythms contribute to brain physiology.

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

cabaJr/ClockCyteR.spatial

License: other
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 0c2d463886c3398bb6e55d8211cc4e60c7f08a55, 1 May 2026
Languages: R (19)
Size: 141 files, 19 scripts
Software Heritage: not archived
Found in: the text, “Spatiotemporal Circadian Activity Analysis–Clock”
Holds: README, license file, environment (DESCRIPTION), tests, continuous integration, documentation, 4 notebooks
Not found: CITATION.cff
Tools: tidyverse (10 files), ggplot2 (3 files), data.table (2 files), igraph (2 files), ggpubr (1 file), Plotly (1 file), rstatix (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
21 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;
  • 19 scripts, each with its path and the digest of its content;
  • 8 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 ClockCyteR and ClockCyteR.spatial packages are currently deposited on GitHub repositories (https://github.com/cabaJr/ClockCyteR) (https://github.com/cabaJr/ClockCyteR.spatial). The raw data used in the generation of the figures are available upon request.

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

Recorded: type, language, journal, volume, issue, pages, dates, 4 authors, 3 keywords, 9 MeSH terms, 2 funders, 53 references, 1 RRID.

Cite

This paper

Ferrari, M., Ness, N., Acosta, J., & Brancaccio, M. (2026). A High-Throughput Live Imaging Platform to Investigate Circuit-Dependent Regulation of Circadian Rhythms in Brain Tissue. Advanced science (Weinheim, Baden-Wurttemberg, Germany), 13(40), e75427. https://doi.org/10.1002/advs.75427

BibTeX

@article{ferrari2026high,
author = {Ferrari, Marco and Ness, Natalie and Acosta, Julieta and Brancaccio, Marco},
title = {{A High-Throughput Live Imaging Platform to Investigate Circuit-Dependent Regulation of Circadian Rhythms in Brain Tissue}},
journal = {Advanced science (Weinheim, Baden-Wurttemberg, Germany)},
year = {2026},
month = apr,
volume = {13},
number = {40},
pages = {e75427},
publisher = {Wiley},
issn = {2198-3844},
doi = {10.1002/advs.75427},
url = {https://doi.org/10.1002/advs.75427},
pmid = {42048014},
pmcid = {PMC13335426}
}

RIS

TY - JOUR
AU - Ferrari, Marco
AU - Ness, Natalie
AU - Acosta, Julieta
AU - Brancaccio, Marco
TI - A High-Throughput Live Imaging Platform to Investigate Circuit-Dependent Regulation of Circadian Rhythms in Brain Tissue
T2 - Advanced science (Weinheim, Baden-Wurttemberg, Germany)
J2 - Adv Sci (Weinh)
PY - 2026
DA - 2026/04/28
VL - 13
IS - 40
SP - e75427
SN - 2198-3844
PB - Wiley
DO - 10.1002/advs.75427
UR - https://doi.org/10.1002/advs.75427
LA - en
ER -

CSL-JSON

{
"id": "10.1002/advs.75427",
"type": "article-journal",
"title": "A High-Throughput Live Imaging Platform to Investigate Circuit-Dependent Regulation of Circadian Rhythms in Brain Tissue",
"container-title": "Advanced science (Weinheim, Baden-Wurttemberg, Germany)",
"author": [
{
"family": "Ferrari",
"given": "Marco"
},
{
"family": "Ness",
"given": "Natalie"
},
{
"family": "Acosta",
"given": "Julieta"
},
{
"family": "Brancaccio",
"given": "Marco"
}
],
"container-title-short": "Adv Sci (Weinh)",
"volume": "13",
"issue": "40",
"page": "e75427",
"DOI": "10.1002/advs.75427",
"PMID": "42048014",
"PMCID": "PMC13335426",
"ISSN": "2198-3844",
"publisher": "Wiley",
"URL": "https://doi.org/10.1002/advs.75427",
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
28
]
]
}
}

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

Similar papers

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

[1] doi:10.1002/imt2.70163 [code]
Spatial multi-omics unveils sphingolipid metabolic reprogramming within the retinal pathological niche.
Journal: iMeta
In common: rstatix, igraph, Plotly, 4 other tools, mouse
[2] doi:10.1186/s13059-026-04177-w [code]
Genomic sequence evolution underlying human neocortical interareal diversification.
Journal: Genome biology
In common: rstatix, igraph, Plotly, 4 other tools, mouse
[3] doi:10.1093/bioinformatics/btag592 [code]
Network-based stratification of allele-specific expression reveals patient subgroups in Huntington's disease.
Journal: Bioinformatics (Oxford, England)
In common: rstatix, igraph, Plotly, 4 other tools
[4] doi:10.1038/s41467-026-74753-y [code]
A human-specific microRNA controls the timing of excitatory synaptogenesis.
Journal: Nature communications
In common: rstatix, igraph, Plotly, 4 other tools
[5] doi:10.1073/pnas.2609132123 [code]
A human lysosomal storage disorder toolkit for decoding proteome landscapes in cortical-like and dopaminergic-like induced neurons.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: rstatix, igraph, Plotly, 4 other tools
[6] doi:10.1126/sciadv.aeg3223 [code]
The extreme diversity of retinal amacrine cells has deep evolutionary roots.
Journal: Science advances
In common: rstatix, igraph, Plotly, 3 other tools, 1 reference
[7] doi:10.1371/journal.pcbi.1014573 [code]
Cell-type-specific m1A dynamics are associated with microglial phenotypic transition and neuronal metabolic adaptation during spinal cord injury.
Journal: PLoS computational biology
In common: rstatix, igraph, ggpubr, 3 other tools, mouse
[8] doi:10.1038/s41586-026-10755-6 [code]
An intrinsic cytoskeletal oscillator establishes neuronal polarity.
Journal: Nature
In common: rstatix, Plotly, ggpubr, 3 other tools, mouse
[9] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: igraph, Plotly, ggpubr, 3 other tools, mouse
[10] doi:10.1073/pnas.2523130123 [code]
FABP7 controls radial glial scaffold stability during human cortical development.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: igraph, Plotly, ggpubr, 3 other tools, mouse

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.