| SOCR ≫ | TCIU Website ≫ | TCIU GitHub ≫ |
This TCIU Appendix implements the Kime-Phase Tomography (KPT) algorithm to map repeated fMRI time-series measurements to 2D kime-surfaces. We test the algorithm on synthetic fMRI data with ON (stimulus) and OFF (rest) conditions.
# ----------------------------
# Simulate fMRI Data
# ----------------------------
simulate_fmri_data <- function(n_participants = 15, n_runs = 20, tr = 2, duration = 300) {
# Hemodynamic Response Function (HRF)
hrf <- function(t) {
a1 <- 6; a2 <- 12; b1 <- 0.9; b2 <- 0.9; c <- 0.35
t^(a1-1)*b1^a1*exp(-b1*t)/gamma(a1) - c*(t^(a2-1)*b2^a2*exp(-b2*t)/gamma(a2))
}
# Experimental design parameters
n_timepoints <- duration/tr
block_length <- 30 # 30s blocks
n_blocks <- duration/block_length
# Storage arrays
data <- list(
ON = array(0, dim = c(n_participants, n_runs, n_timepoints)),
OFF = array(0, dim = c(n_participants, n_runs, n_timepoints))
)
# Generate data for each participant and run
set.seed(123)
for(p in 1:n_participants) {
for(r in 1:n_runs) {
# Generate block design
design <- rep(c(0,1), each = block_length/(2*tr))
if(length(design) < n_timepoints) {
design <- c(design, rep(design[length(design)], n_timepoints - length(design)))
} else {
design <- design[1:n_timepoints]
}
# Generate HRF response
hrf_values <- hrf(seq(0, block_length/tr - 1, length.out = block_length/tr))
hrf_response <- numeric(n_timepoints)
# Convolve design with HRF
for(i in 1:n_timepoints) {
if(design[i] == 1) {
end_idx <- min(i + length(hrf_values) - 1, n_timepoints)
hrf_response[i:end_idx] <- hrf_response[i:end_idx] +
hrf_values[1:(end_idx - i + 1)]
}
}
# Add participant-specific noise and drift
noise <- cumsum(rnorm(n_timepoints, sd = 0.1))
drift <- 0.01 * seq(n_timepoints)
# Store responses
data$ON[p, r, ] <- hrf_response + noise + drift + rnorm(n_timepoints, sd = 0.2)
data$OFF[p, r, ] <- noise + drift + rnorm(n_timepoints, sd = 0.2)
}
}
return(data)
}
# Generate synthetic fMRI data
fmri_data <- simulate_fmri_data(n_participants = 10, n_runs = 20, tr = 2, duration = 300)# ----------------------------
# Visualize Original fMRI Time-Series
# ----------------------------
# Function to create interactive time-series plot
create_timeseries_plot <- function(fmri_data, n_display = 5) {
# Extract dimensions
n_participants <- dim(fmri_data$ON)[1]
n_runs <- dim(fmri_data$ON)[2]
n_timepoints <- dim(fmri_data$ON)[3]
time_vec <- seq(0, (n_timepoints-1)*2, by = 2) # TR = 2
# Randomly select indices to display
set.seed(42)
selected_indices <- sample(1:(n_participants * n_runs), n_display)
# Create plot
fig <- plot_ly()
# Add ON condition traces
for(i in 1:n_display) {
idx <- selected_indices[i]
participant <- ((idx - 1) %/% n_runs) + 1
run <- ((idx - 1) %% n_runs) + 1
fig <- fig %>% add_trace(
x = time_vec,
y = fmri_data$ON[participant, run, ],
type = 'scatter',
mode = 'lines',
name = paste0("ON - P", participant, "R", run),
line = list(color = 'red', width = 2),
opacity = 0.7,
visible = (i <= 3) # Show only first 3 by default
)
}
# Add OFF condition traces
for(i in 1:n_display) {
idx <- selected_indices[i]
participant <- ((idx - 1) %/% n_runs) + 1
run <- ((idx - 1) %% n_runs) + 1
fig <- fig %>% add_trace(
x = time_vec,
y = fmri_data$OFF[participant, run, ],
type = 'scatter',
mode = 'lines',
name = paste0("OFF - P", participant, "R", run),
line = list(color = 'blue', width = 2),
opacity = 0.7,
visible = (i <= 3) # Show only first 3 by default
)
}
# Create buttons for navigation
button_list <- list()
# Add "Show All" button
visible_all <- rep(TRUE, 2 * n_display)
button_list[[1]] <- list(
method = "restyle",
args = list("visible", as.list(visible_all)),
label = "Show All"
)
# Add individual selection buttons
for(i in 1:n_display) {
visible_vec <- rep(FALSE, 2 * n_display)
visible_vec[c(i, i + n_display)] <- TRUE # Show ON and OFF for this index
idx <- selected_indices[i]
participant <- ((idx - 1) %/% n_runs) + 1
run <- ((idx - 1) %% n_runs) + 1
button_list[[i + 1]] <- list(
method = "restyle",
args = list("visible", as.list(visible_vec)),
label = paste0("P", participant, "R", run)
)
}
# Update layout with buttons
fig <- fig %>% layout(
title = "fMRI Time-Series: ON (Red) vs OFF (Blue) Conditions",
xaxis = list(title = "Time (seconds)"),
yaxis = list(title = "BOLD Signal"),
updatemenus = list(
list(
type = "buttons",
direction = "left",
xanchor = "left",
yanchor = "top",
x = 0.01,
y = 1.15,
buttons = button_list
)
),
annotations = list(
list(
text = "Select Time-Series:",
showarrow = FALSE,
x = 0,
y = 1.2,
xref = "paper",
yref = "paper",
xanchor = "right",
yanchor = "bottom"
)
)
)
return(fig)
}
# Create and display the time-series plot
ts_plot <- create_timeseries_plot(fmri_data, n_display = 10)
ts_plot# ----------------------------
# Time-Series Statistics
# ----------------------------
# Compute statistics for each condition
compute_ts_stats <- function(data_array) {
n_series <- dim(data_array)[1] * dim(data_array)[2]
n_timepoints <- dim(data_array)[3]
# Reshape to matrix
reshaped <- matrix(data_array, nrow = n_series, ncol = n_timepoints)
# Compute statistics
mean_ts <- colMeans(reshaped)
sd_ts <- apply(reshaped, 2, sd)
# Compute temporal SNR
tsnr <- mean_ts / sd_ts
return(list(
mean = mean_ts,
sd = sd_ts,
tsnr = tsnr,
global_mean = mean(mean_ts),
global_sd = mean(sd_ts)
))
}
stats_on <- compute_ts_stats(fmri_data$ON)
stats_off <- compute_ts_stats(fmri_data$OFF)
# Create comparison plot
time_vec <- seq(0, (length(stats_on$mean)-1)*2, by = 2)
stats_plot <- plot_ly() %>%
# Mean traces
add_trace(x = time_vec, y = stats_on$mean, type = 'scatter', mode = 'lines',
name = 'ON Mean', line = list(color = 'red', width = 3)) %>%
add_trace(x = time_vec, y = stats_off$mean, type = 'scatter', mode = 'lines',
name = 'OFF Mean', line = list(color = 'blue', width = 3)) %>%
# Confidence bands
add_trace(x = c(time_vec, rev(time_vec)),
y = c(stats_on$mean + 2*stats_on$sd, rev(stats_on$mean - 2*stats_on$sd)),
type = 'scatter', mode = 'none', fill = 'toself',
fillcolor = 'rgba(255,0,0,0.2)', name = 'ON ±2SD', showlegend = FALSE) %>%
add_trace(x = c(time_vec, rev(time_vec)),
y = c(stats_off$mean + 2*stats_off$sd, rev(stats_off$mean - 2*stats_off$sd)),
type = 'scatter', mode = 'none', fill = 'toself',
fillcolor = 'rgba(0,0,255,0.2)', name = 'OFF ±2SD', showlegend = FALSE) %>%
layout(
title = "Average Time-Series with Confidence Bands",
xaxis = list(title = "Time (seconds)"),
yaxis = list(title = "BOLD Signal"),
hovermode = 'x unified'
)
stats_plot##
## === Time-Series Summary Statistics ===
cat(sprintf("ON Condition - Global Mean: %.3f (SD: %.3f)\n",
stats_on$global_mean, stats_on$global_sd))## ON Condition - Global Mean: 1.481 (SD: 0.792)
cat(sprintf("OFF Condition - Global Mean: %.3f (SD: %.3f)\n",
stats_off$global_mean, stats_off$global_sd))## OFF Condition - Global Mean: 0.783 (SD: 0.792)
## Mean Difference (ON-OFF): 0.699
## Average tSNR - ON: 1.796, OFF: 0.896
# Wrapped Laplace distribution density
wrapped_laplace_density <- function(theta, mu, beta) {
# Normalize to [-pi, pi]
theta <- ((theta - mu + pi) %% (2*pi)) - pi + mu
# Wrapped Laplace density
if(beta <= 0) return(rep(0, length(theta)))
dens <- beta / (2 * sinh(beta)) * exp(beta * cos(theta - mu))
return(dens)
}
# Sample from wrapped Laplace distribution
sample_wrapped_laplace <- function(n, mu, beta) {
# Sample from linear Laplace
u <- runif(n)
linear_samples <- ifelse(u < 0.5,
mu + log(2*u)/beta,
mu - log(2*(1-u))/beta)
# Wrap to [-pi, pi]
wrapped_samples <- ((linear_samples + pi) %% (2*pi)) - pi
return(wrapped_samples)
}
# Phase distribution parameters as functions of time
mu_t <- function(t, t_max = 150) {
# Location parameter varies sinusoidally with time
pi * sin(2 * pi * t / t_max)
}
beta_t <- function(t, t_max = 150) {
# Scale parameter varies with time
2 + sin(4 * pi * t / t_max)
}# Compute empirical moments
compute_empirical_moments <- function(s_values, theta_values, n_max = 10) {
n_samples <- length(s_values)
moments <- complex(length = 2*n_max + 1)
for(n in -n_max:n_max) {
moments[n + n_max + 1] <- mean(s_values * exp(-1i * n * theta_values))
}
return(moments)
}
# Estimate harmonic coefficients f_n(t)
estimate_harmonic_coefficients <- function(time_series_collection, n_max = 10) {
n_time_points <- dim(time_series_collection)[3]
n_total_series <- dim(time_series_collection)[1] * dim(time_series_collection)[2]
# Reshape to (n_total_series, n_time_points)
reshaped_data <- matrix(time_series_collection,
nrow = n_total_series,
ncol = n_time_points)
# Initialize coefficient matrix
f_coeffs <- matrix(0, nrow = n_time_points, ncol = 2*n_max + 1)
# For each time point, estimate mean signal
for(t_idx in 1:n_time_points) {
# Simple estimate: f_0(t) = mean signal, others = 0 for now
f_coeffs[t_idx, n_max + 1] <- mean(reshaped_data[, t_idx])
# Add some harmonic structure (for demonstration)
for(n in 1:n_max) {
f_coeffs[t_idx, n_max + 1 + n] <- 0.1 * f_coeffs[t_idx, n_max + 1] *
exp(-n/3) * cos(2*pi*n*t_idx/n_time_points)
f_coeffs[t_idx, n_max + 1 - n] <- 0.1 * f_coeffs[t_idx, n_max + 1] *
exp(-n/3) * cos(2*pi*n*t_idx/n_time_points)
}
}
return(f_coeffs)
}# KPT reconstruction function
kpt_reconstruct <- function(time_series_collection, n_max = 10, n_phase_samples = 100) {
n_participants <- dim(time_series_collection)[1]
n_runs <- dim(time_series_collection)[2]
n_time_points <- dim(time_series_collection)[3]
# Step 1: Estimate harmonic coefficients
f_coeffs <- estimate_harmonic_coefficients(time_series_collection, n_max)
# Step 2: For each time point, sample phases and compute moments
reconstructed_phases <- list()
empirical_moments_all <- matrix(0, nrow = n_time_points, ncol = 2*n_max + 1)
for(t_idx in 1:n_time_points) {
t_val <- t_idx * 2 # TR = 2
# Get all measurements at this time point
s_values <- as.vector(time_series_collection[, , t_idx])
# Sample phases from wrapped Laplace
mu <- mu_t(t_val)
beta <- beta_t(t_val)
theta_samples <- sample_wrapped_laplace(length(s_values), mu, beta)
# Compute empirical moments
moments <- compute_empirical_moments(s_values, theta_samples, n_max)
empirical_moments_all[t_idx, ] <- moments
# Store phase distribution parameters
reconstructed_phases[[t_idx]] <- list(
t = t_val,
mu = mu,
beta = beta,
theta_samples = theta_samples,
s_values = s_values
)
}
return(list(
f_coeffs = f_coeffs,
empirical_moments = empirical_moments_all,
phase_data = reconstructed_phases,
n_max = n_max
))
}# Generate kime-surface from KPT reconstruction
generate_kime_surface <- function(kpt_result, n_grid = 50) {
# Create polar grid
r_max <- max(sapply(kpt_result$phase_data, function(x) x$t))
r_grid <- seq(0, r_max, length.out = n_grid)
theta_grid <- seq(-pi, pi, length.out = n_grid)
# Initialize surface
surface_z <- matrix(0, nrow = n_grid, ncol = n_grid)
# Interpolate intensity values
for(i in 1:n_grid) {
for(j in 1:n_grid) {
r <- r_grid[i]
theta <- theta_grid[j]
if(r > 0) {
# Find nearest time point
t_idx <- which.min(abs(sapply(kpt_result$phase_data, function(x) x$t) - r))
# Compute expected intensity using harmonic expansion
z_val <- Re(kpt_result$f_coeffs[t_idx, kpt_result$n_max + 1]) # f_0(t)
# Add harmonic contributions
for(n in 1:kpt_result$n_max) {
z_val <- z_val + 2 * Re(kpt_result$f_coeffs[t_idx, kpt_result$n_max + 1 + n] *
exp(1i * n * theta))
}
surface_z[i, j] <- z_val
}
}
}
# Convert to Cartesian coordinates
x_grid <- outer(r_grid, cos(theta_grid))
y_grid <- outer(r_grid, sin(theta_grid))
return(list(
x = x_grid,
y = y_grid,
z = surface_z,
r_grid = r_grid,
theta_grid = theta_grid
))
}## Applying KPT to ON condition...
## Applying KPT to OFF condition...
# Create interactive 3D plot for ON condition
fig_on <- plot_ly(
x = surface_on$x,
y = surface_on$y,
z = surface_on$z,
type = "surface",
colorscale = list(
c(0, "rgb(20,20,80)"),
c(0.25, "rgb(20,80,140)"),
c(0.5, "rgb(20,140,200)"),
c(0.75, "rgb(80,200,120)"),
c(1, "rgb(255,220,80)")
),
contours = list(
z = list(
show = TRUE,
usecolormap = TRUE,
highlightcolor = "#ff0000",
project = list(z = TRUE)
)
),
lighting = list(
ambient = 0.4,
diffuse = 0.6,
specular = 0.2,
roughness = 0.85
)
) %>%
layout(
title = "fMRI ON Condition - Kime-Surface",
scene = list(
xaxis = list(title = "X (t·cos θ)"),
yaxis = list(title = "Y (t·sin θ)"),
zaxis = list(title = "Z (Intensity)"),
camera = list(
eye = list(x = 1.5, y = 1.5, z = 1.5)
)
)
)
# Create interactive 3D plot for OFF condition
fig_off <- plot_ly(
x = surface_off$x,
y = surface_off$y,
z = surface_off$z,
type = "surface",
colorscale = list(
c(0, "rgb(40,20,60)"),
c(0.25, "rgb(60,40,100)"),
c(0.5, "rgb(80,60,140)"),
c(0.75, "rgb(100,80,180)"),
c(1, "rgb(140,120,220)")
),
contours = list(
z = list(
show = TRUE,
usecolormap = TRUE,
highlightcolor = "#ff0000",
project = list(z = TRUE)
)
),
lighting = list(
ambient = 0.4,
diffuse = 0.6,
specular = 0.2,
roughness = 0.85
)
) %>%
layout(
title = "fMRI OFF Condition - Kime-Surface",
scene = list(
xaxis = list(title = "X (t·cos θ)"),
yaxis = list(title = "Y (t·sin θ)"),
zaxis = list(title = "Z (Intensity)"),
camera = list(
eye = list(x = 1.5, y = 1.5, z = 1.5)
)
)
)
# Display the plots
fig_on