--- title: "Time Complexity, Inferential uncertainty and Spacekime Analytics" subtitle: "Appendix 03: Kime-Phase Tomography (KPT V.4: KPT-GEM & KPT-FFT): fMRI and Double-Slit Validation" author: "SOCR Team" date: "`r format(Sys.time(),'%m/%d/%Y')`" output: html_document: theme: spacelab highlight: tango includes: before_body: TCIU_header.html toc: true number_sections: true toc_depth: 3 toc_float: collapsed: false smooth_scroll: true code_folding: hide pandoc_args: ["+RTS", "-K1024m", "-RTS"] word_document: toc: true toc_depth: '3' header-includes: - \usepackage{amsmath} - \usepackage{amssymb} - \usepackage{physics} - \usepackage{mathrsfs} - \usepackage{bm} --- This [TCIU Appendix](https://tciu.predictive.space/) offers a self-contained implementation of the kime phase tompgraphy algorithms, *KPT-GEM* (expectation maximization) and *KPT-FFT (FFT alternating spectral)* for estimating the phase distribution and reconstructing the kime-surface representations from observed repeated measurement data from longitudinal processes. The KPT algorihtm is validated using two simulations; *double-slit physics experiment* and *fMRI ON vs. OFF neuroscience experiment*. We report quyantitative performance metrics (summary tables) and provide qualitative results as surface/heatmap overlays contrasting for the *true* vs. *estimated* phase distributions and kime-surfaces. The key steps of the KPT algorithm include E-step, Wiener-Sobolev filtering, simplex projection, ans reality constraints, subject to normalization, operator conventions. ```{r setup, include=FALSE} knitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE) suppressPackageStartupMessages({ library(ggplot2) library(dplyr) library(tidyr) library(Matrix) library(plotly) library(knitr) library(purrr) }) set.seed(2025) ``` # Overview & Conventions This notebook demonstrates *KPT-GEM* and *KPT-FFT* on two simulations: - *Double-slit* physics experiment (latent phase drives interference). - *fMRI ON vs. OFF* event-related design (latent phase modulates BOLD fMRI neuroscience study). We use the circular phase normalization $\int_0^{2\pi}\varphi_t(\theta)\,\frac{d\theta}{2\pi}=1$. On a uniform grid of length $L_\theta$, this is equivalent to the *row-mean equals 1* constraint. The spectral updates use the [Wiener-Sobolev filter](https://doi.org/10.1016/S0022-1236(03)00236-2). $$\widehat{\Phi}(\omega,t)=\frac{\overline{\widehat F(\omega,t)}}{|\widehat F(\omega,t)|^2+\lambda\,\Lambda_p(\omega)}\,\widehat M(\omega,t),\quad \Lambda_p(\omega)=(2\sin(\omega/2))^{2p},$$ with simplex projection in $\theta$ (nonnegativity + unit mass). *Algorithmic parity.* KPT-FFT is an alternating spectral method (phase-step *and* surface-step) implemented with `fft()` for the DFT/IDFT. KPT-GEM is the same penalized tomography specialized to updating $\varphi_t$ given a stabilized surface model $\mathcal S$ (practical when a robust $\mathcal S$ proxy is available from the data, e.g., ON-OFF construction). # Utilities (Fourier, projection, metrics, visuals) We start by defining the core utility functions. ```{r utilities} # --- In Utilities chunk (right before defining plotting helpers) --- if (exists("plot_phase_overlay", inherits = TRUE)) { rm(plot_phase_overlay) } # Dimension-checked overlay (unique name to avoid conflicts) kpt_plot_phase_overlay <- function(theta, phi_true_row, phi_est_row, phi_est_row2=NULL, t_lab = "", title = "") { n_theta <- length(theta) stopifnot(length(phi_true_row) == n_theta, length(phi_est_row) == n_theta) if (is.null(phi_est_row2)) { df <- tibble::tibble( theta = rep(theta, 2L), # length = 2*n density = c(phi_true_row, phi_est_row), # length = 2*n kind = rep(c("True", "Estimated"), each = n_theta) # length = 2*n ) } else { df <- tibble::tibble( theta = rep(theta, 3L), # length = 3*n density = c(phi_true_row, phi_est_row, phi_est_row2), # length = 3*n kind = rep(c("True", "FFT-Estimated", "GEM-Estimated"), each = n_theta) # length = 3*n ) } ggplot2::ggplot(df, ggplot2::aes(theta, density, color = kind)) + ggplot2::geom_line(linewidth = 0.9) + ggplot2::labs(title = title, subtitle = t_lab, x = expression(theta), y = expression(varphi(t,theta))) + ggplot2::theme_minimal() } # ---- Grids & helpers ---- theta_grid <- function(L) seq(0, 2*pi, length.out = L + 1L)[1L:L] make_nvec <- function(J) seq.int(-J, J, by = 1L) # Work with uniform grid on [0,2π) wrap_0_2pi <- function(x) (x %% (2*pi)) # Posterior softmax (row-wise, stable) softmax_rows <- function(Lmat) { m <- apply(Lmat, 1L, max) W <- exp(Lmat - m) W / pmax(rowSums(W), .Machine$double.eps) } # Enforce probability simplex per row on uniform theta grid: # nonnegativity + ∫ φ dθ/(2π)=1 <=> row mean equals 1 row_project_simplex <- function(P, eps = 1e-12) { P <- pmax(P, 0) m <- rowMeans(P) m[m < eps] <- 1 sweep(P, 1L, m, `/`) } # ---- FFT-friendly mapping between coefficients (n=-J..J) and DFT order (m=0..N-1) ---- coeffs_to_dft_order <- function(x_n, nvec) { N <- length(nvec); stopifnot(N == length(x_n)) x_m <- complex(length = N) for (i in seq_along(nvec)) { n <- nvec[i] m <- (n + N) %% N # map -J..J -> 0..N-1 x_m[m + 1L] <- x_n[i] } x_m } dft_order_to_coeffs <- function(x_m, nvec) { N <- length(nvec); stopifnot(N == length(x_m)) x_n <- complex(length = N) for (i in seq_along(nvec)) { n <- nvec[i] m <- (n + N) %% N x_n[i] <- x_m[m + 1L] } x_n } # Evaluate generating function on unit circle via FFT coeffs_to_Xomega_fft <- function(x_n, nvec) { x_m <- coeffs_to_dft_order(x_n, nvec) fft(x_m, inverse = FALSE) # no 1/N scaling } # Invert: X(ω) -> coefficients in n=-J..J via IFFT Xomega_to_coeffs_fft <- function(X_w, nvec) { N <- length(nvec); stopifnot(N == length(X_w)) x_m <- fft(X_w, inverse = TRUE) / N dft_order_to_coeffs(x_m, nvec) } # Synthesize S(t,θ) from f_n(t) on θ-grid surface_from_coeffs <- function(f_n_row, nvec, theta) { # S(t,θ) = Re(Σ_n f_n e^{i n θ}) E <- exp(1i * outer(nvec, theta)) as.vector(Re(drop(f_n_row %*% E))) } # Reality constraint on Fourier coeffs: f_-n = Conj(f_n) enforce_reality_coeffs <- function(f_n, nvec) { N <- length(nvec); J <- (N - 1L) / 2L # indices: n=0..J and negative mates for (j in 1:J) { pos <- which(nvec == j) neg <- which(nvec == -j) avg <- 0.5 * (f_n[pos] + Conj(f_n[neg])) f_n[pos] <- avg f_n[neg] <- Conj(avg) } # n=0 coefficient should be real f_n[nvec==0] <- Re(f_n[nvec==0]) f_n } # ---- Phase metrics per time (KL, JSD, Hellinger, TV) ---- row_normalize_phi <- function(P, eps = 1e-12) { P <- pmax(P, eps); m <- rowMeans(P); m[m < eps] <- 1; sweep(P, 1, m, `/`) } phase_metrics_df <- function(phi_true, phi_est, theta, time, base2 = TRUE) { P <- row_normalize_phi(phi_true); Q <- row_normalize_phi(phi_est) K <- nrow(P); L <- ncol(P); stopifnot(K == nrow(Q), L == ncol(Q)) logb <- if (base2) function(x) log(x, base = 2) else log tibble( time = time, KL_true_est = double(K), KL_est_true = double(K), JSD_bits = double(K), Hellinger = double(K), TV = double(K) ) %>% mutate( KL_true_est = rowMeans(P * logb(P / pmax(Q, 1e-12))), KL_est_true = rowMeans(Q * logb(Q / pmax(P, 1e-12))), { M <- 0.5 * (P + Q) JSD_bits <- 0.5 * rowMeans(P * logb(P / pmax(M, 1e-12))) + 0.5 * rowMeans(Q * logb(Q / pmax(M, 1e-12))) Hellinger <- sqrt(0.5 * rowMeans((sqrt(P) - sqrt(Q))^2)) TV <- 0.5 * rowMeans(abs(P - Q)) data.frame(JSD_bits, Hellinger, TV) } ) } # ---- Surface L2 (cone-weighted) ---- surface_L2_cone <- function(S_true, S_est, time, theta) { stopifnot(all(dim(S_true) == dim(S_est))) K <- nrow(S_true); L <- ncol(S_true) dtheta <- 2*pi / L dt <- numeric(K) if (K == 1) dt[] <- 1 else { dt[1] <- (time[2]-time[1])/2; dt[K] <- (time[K]-time[K-1])/2 if (K > 2) dt[2:(K-1)] <- (time[3:K]-time[1:(K-2)])/2 } w_t <- time diff2 <- (S_true - S_est)^2 per_t_L2_sq <- rowSums(diff2) * dtheta * w_t total_L2 <- sqrt(sum(per_t_L2_sq * dt)) true_norm_sq <- rowSums(S_true^2) * dtheta * w_t rel_L2 <- total_L2 / max(sqrt(sum(true_norm_sq * dt)), .Machine$double.eps) list(total_L2 = total_L2, rel_L2 = rel_L2, per_time_L2 = sqrt(rowSums(diff2) * dtheta * w_t)) } # ---- Visualization helpers ---- plot_phase_overlay <- function(theta, phi_true_row, phi_est_row, t_lab = "", title = "") { df <- tibble( theta = rep(theta, 2), density = c(phi_true_row, phi_est_row), kind = rep(c("True","Estimated"), each = length(theta)) ) ggplot(df, aes(theta, density, color = kind)) + geom_line(linewidth = 0.9) + labs(title = title, subtitle = t_lab, x = expression(theta), y = expression(varphi(t,theta))) + theme_minimal() } # plot_surface_3d <- function(time, theta, S, title = "Surface", opacity=0.9, colorscale='Viridis') { # plot_ly(x = time, y = theta, z = S, type = "surface", opacity=opacity, # colorscale=colorscale, name=title) %>% # layout(title = title, # scene = list(xaxis = list(title = "t"), # yaxis = list(title = "θ"), # expression(theta)), # zaxis = list(title = "Intensity"))) # } plot_surface_3d <- function(time, theta, S, title = "Surface", opacity=0.9, colorscale='Viridis', colorbar_title = "Intensity", showscale = TRUE) { plot_ly(x = time, y = theta, z = S, type = "surface", opacity=opacity, colorscale=colorscale, colorbar = list(title = colorbar_title), showscale = showscale) %>% layout(title = title, scene = list(xaxis = list(title = "t"), yaxis = list(title = "θ"), zaxis = list(title = colorbar_title))) } ``` We need to avoid potential phase-periodicity inconsistencies or phase-shift ambiguity (anchoring) between phase domains $[-\pi,\pi)$ and $[0,2\pi)$. As KPT recovers $M=F\Phi$, if $\Phi(\cdot,t)$ is rotated by $\alpha(t)$ and $F(\cdot,t)$ is counter-rotated by $-\alpha(t)$, then the product doesn’t change. Without an anchoring rule, $\hat{\varphi}^t(\theta)$ can drift by a constant rotation $\theta \to \theta + \alpha$ making the performance-validation plots shifted (not necessarily by $\pi$, just by some $\alpha$. This is expected and harmless unless we compare the estimated phases to to the fixed (gold-standard) ground truth phases. # KPT Algorithms All methematical, computaitonal, and statistical details are provided in the [KPT paper](https://www.overleaf.com/project/68a72229f8e907137629d964). ## KPT-GEM (penalized tomography with stabilized surface) This KPT algorithmic variant (*KPT-GEM*, generalized expectation maximization estimation) updates $\widehat{\varphi}_t$ via Wiener-Sobolev filtering given a stable proxy $\mathcal S(t,\theta)$ (from the data), then projects to the simplex. ### Kime-Phase Tomography generalized EM algorithm (KPT-GEM) Algorithm **Inputs:** $\{y_{j,k}\}$, harmonics $n\in\{-J,\ldots,J\}$, tolerance $\varepsilon$, $(\lambda_\Phi,p_\Phi)$, $(\lambda_F,p_F)$ **Initialize:** $\widehat\varphi^{(0)}_{t_k}\equiv \tfrac{1}{2\pi}$; initialize $\widehat f^{(0)}_n(t_k)$ (e.g., from cross‑sectional averages); set iteration $\ell=0$ **Repeat:** 1. **E-step:** compute $\xi^{(\ell)}_{j,k}(n)$ and $\widehat m^{(\ell)}_n(t_k)=\tfrac{1}{N}\sum_j y_{j,k}\xi^{(\ell)}_{j,k}(n)$ 2. **$\Phi$-step (phase update):** For each $t_k$, FFT in $n$ to get $\widehat M^{(\ell)}(\omega,t_k)$ and $\widehat F^{(\ell)}(\omega,t_k)$; set $$\widehat\Phi^{(\ell+1)}(\omega,t_k)=\frac{\overline{\widehat F^{(\ell)}(\omega,t_k)}}{|\widehat F^{(\ell)}(\omega,t_k)|^2+\lambda_\Phi\,\Lambda_{p_\Phi}(\omega)}\,\widehat M^{(\ell)}(\omega,t_k).$$ IFFT in $\omega$ and project: $\widehat\varphi^{(\ell+1)}_{t_k}\leftarrow \Pi_{\mathcal P}\big[\mathcal{F}^{-1}\widehat\Phi^{(\ell+1)}(\cdot,t_k)\big]$. Optionally smooth in $t$. 3. **$F$-step (surface update):** For each $t_k$, recompute $\widehat\Phi^{(\ell+1)}$ and set $$\widehat F^{(\ell+1)}(\omega,t_k)=\frac{\overline{\widehat \Phi^{(\ell+1)}(\omega,t_k)}}{|\widehat \Phi^{(\ell+1)}(\omega,t_k)|^2+\lambda_F\,\Lambda_{p_F}(\omega)}\,\widehat M^{(\ell)}(\omega,t_k).$$ IFFT in $\omega$ to obtain $\widehat f^{(\ell+1)}_n(t_k)$; enforce $f_{-n}=\overline{f_n}$. Optionally smooth in $t$. 4. **Synthesis:** $\widehat{\mathcal S}^{(\ell+1)}(t_k,\theta)=\sum_{n=-J}^J \widehat f^{(\ell+1)}_n(t_k)e^{in\theta}$ 5. **Convergence check:** stop when $\max_k\|\widehat\varphi^{(\ell+1)}_{t_k}-\widehat\varphi^{(\ell)}_{t_k}\|_{L^2}<\varepsilon$ and $\max_{k,n}|\widehat f^{(\ell+1)}_n(t_k)-\widehat f^{(\ell)}_n(t_k)|<\varepsilon$ **Until** converged ```{r kpt-gem} build_S_from_data <- function(Y, time, theta, alpha2 = 0.2, smooth_spar = 0.6, offset_frac = 0.1) { # Robust ON-OFF style proxy: baseline + amplitude * (cos θ + α2 cos 2θ) K <- ncol(Y); L <- length(theta) mu_k <- apply(Y, 2L, mean) A_k <- apply(Y, 2L, sd) mu_s <- stats::smooth.spline(time, mu_k, spar = smooth_spar)$y A_s <- stats::smooth.spline(time, A_k, spar = smooth_spar)$y off <- offset_frac * median(abs(A_s) + 1e-8) cos1 <- cos(outer(rep(1, K), theta)) cos2 <- cos(2 * outer(rep(1, K), theta)) S <- mu_s %o% rep(1, L) + (A_s + off) %o% rep(1, L) * cos1 + (alpha2 * A_s) %o% rep(1, L) * cos2 S } kpt_gem <- function(Y, time, J = 12, L_theta = 256, lambda = 1e-2, p = 1, max_iter = 30, tol = 1e-3, sigma = NULL, verbose = TRUE) { I <- nrow(Y); K <- ncol(Y) theta <- theta_grid(L_theta); nvec <- make_nvec(J); Nn <- length(nvec) if (is.null(sigma)) sigma <- 0.5 * stats::mad(as.numeric(Y), center = 0) + 1e-6 Sgrid <- build_S_from_data(Y, seq_len(K), theta) # proxy surface on time index (unit steps) # Precompute f_n(t_k) by IFFT across θ for each time (DFT by matrix for clarity) f_n <- matrix(0+0i, nrow = K, ncol = Nn) for (k in 1:K) { Eneg <- exp(-1i * outer(theta, nvec)) # [Lθ x (2J+1)] f_n[k, ] <- as.vector((t(Sgrid[k, ]) %*% Eneg) / L_theta) f_n[k, ] <- enforce_reality_coeffs(f_n[k, ], nvec) } phi_grid <- matrix(1.0, nrow = K, ncol = L_theta) Lambda_p <- (2 * sin(pi * (0:(Nn-1))/Nn))^(2*p) # Λ_p(ω) with ω=2πk/Nn obj <- numeric(max_iter) for (it in 1:max_iter) { # E-step: posterior weights & mixed moments m_hat <- matrix(0+0i, nrow = K, ncol = Nn) for (k in 1:K) { prior_k <- phi_grid[k, ] Srow <- matrix(Sgrid[k, ], nrow = I, ncol = L_theta, byrow = TRUE) LogL <- - ( (Y[, k] - Srow)^2 ) / (2 * sigma^2) LogW <- sweep(LogL, 2L, log(pmax(prior_k, 1e-12)), `+`) W <- softmax_rows(LogW) # [I x Lθ] Eneg <- exp(-1i * outer(theta, nvec)) # [Lθ x (2J+1)] xi <- W %*% Eneg # [I x (2J+1)] m_hat[k, ] <- colMeans(xi * Y[, k]) } # M-step: spectral deconvolution per time using FFT evaluation on unit circle phi_hat_n <- matrix(0+0i, nrow = K, ncol = Nn) for (k in 1:K) { F_w <- coeffs_to_Xomega_fft(f_n[k, ], nvec) M_w <- coeffs_to_Xomega_fft(m_hat[k, ], nvec) denom <- (Mod(F_w)^2) + lambda * Lambda_p + 1e-10 Phi_w <- Conj(F_w) * M_w / denom phi_hat_n[k, ] <- Xomega_to_coeffs_fft(Phi_w, nvec) } # Synthesize φ and project phi_new <- matrix(0, nrow = K, ncol = L_theta) Epos <- exp(1i * outer(nvec, theta)) for (k in 1:K) { phi_row <- Re(drop(t(Epos) %*% phi_hat_n[k, ])) phi_new[k, ] <- phi_row } phi_new <- row_project_simplex(phi_new) diff_norm <- sqrt(mean((phi_new - phi_grid)^2)) phi_grid <- phi_new obj[it] <- mean(Mod(m_hat)^2) if (verbose) message(sprintf("[GEM] iter %02d: Δ=%.3e", it, diff_norm)) if (diff_norm < tol) { obj <- obj[1:it]; break } } list(varphi_theta = phi_grid, phi_hat_n = phi_hat_n, f_n = f_n, Sgrid = Sgrid, theta = theta, nvec = nvec, omega = 2*pi*(0:(Nn-1))/Nn, time = seq_len(K), lambda = lambda, p = p, sigma = sigma, objective = obj) } ``` ## KPT-FFT (alternating phase & surface updates) This is the fully alternating spectral algorithm consistent with the $\Phi$-step / $F$-step updates $$\Phi \leftarrow \frac{\overline F}{|F|^2+\lambda_\Phi\Lambda_{p_\Phi}}\,M,\qquad F \leftarrow \frac{\overline \Phi}{|\Phi|^2+\lambda_F\Lambda_{p_F}}\,M,$$ with IFFT to coefficients and synthesis after each step (plus reality constraint for $f_n$). ## Algorithm: Fast Kime‑Phase Tomography via FFT (KPT-FFT) **Input:** $\{y_{j,k}\}$, $J$, $(\lambda_\Phi,p_\Phi)$, $(\lambda_F,p_F)$, tolerance $\epsilon$ **Init:** $\widehat\varphi^{(0)}_{t_k}\equiv\tfrac{1}{2\pi}$; initialize $\widehat f^{(0)}_n(t_k)$ **Repeat:** 1. **E-step (vectorized):** compute $\xi^{(\ell)}_{j,k}(n)$ and $\widehat m^{(\ell)}_n(t_k)$ 2. **FFT:** $\widehat M^{(\ell)}(\omega,t_k)\leftarrow \mathrm{FFT}_n\{\widehat m^{(\ell)}_n(t_k)\}$ 3. **Phase update:** $$\widehat\Phi^{(\ell+1)}(\omega,t_k)\leftarrow \dfrac{\overline{\widehat F^{(\ell)}(\omega,t_k)}}{|\widehat F^{(\ell)}(\omega,t_k)|^2+\lambda_\Phi\Lambda_{p_\Phi}(\omega)}\,\widehat M^{(\ell)}(\omega,t_k)$$ 4. **IFFT & projection:** $\widehat\varphi^{(\ell+1)}_{t_k}\leftarrow \Pi_{\mathcal P}[\mathrm{IFFT}_\omega\{\widehat\Phi^{(\ell+1)}(\omega,t_k)\}]$ 5. **Surface update:** $$\widehat F^{(\ell+1)}(\omega,t_k)\leftarrow \dfrac{\overline{\widehat \Phi^{(\ell+1)}(\omega,t_k)}}{|\widehat \Phi^{(\ell+1)}(\omega,t_k)|^2+\lambda_F\Lambda_{p_F}(\omega)}\,\widehat M^{(\ell)}(\omega,t_k)$$ 6. **IFFT & reality:** $\widehat f^{(\ell+1)}_n(t_k)\leftarrow \mathrm{IFFT}_\omega\left\{\widehat F^{(\ell+1)}(\omega,t_k)\right\}$ and enforce $f_{-n}=\overline{f_n}$ **Until** $\|\widehat\varphi^{(\ell+1)}-\widehat\varphi^{(\ell)}\|_{L^2}<\epsilon$ and $\max_{k,n}|\widehat f^{(\ell+1)}_n(t_k)-\widehat f^{(\ell)}_n(t_k)|<\epsilon$ **Return** $\widehat\varphi_t$ and $\widehat{\mathcal S}(t,\theta)$. ```{r kpt-fft} kpt_fft <- function(Y, time, J = 12, L_theta = 256, lambda_phi = 1e-2, p_phi = 1, lambda_F = 1e-2, p_F = 1, max_iter = 30, tol = 1e-3, sigma = NULL, verbose = TRUE) { I <- nrow(Y); K <- ncol(Y) theta <- theta_grid(L_theta); nvec <- make_nvec(J); Nn <- length(nvec) omega <- 2*pi*(0:(Nn-1))/Nn if (is.null(sigma)) sigma <- 0.5 * stats::mad(as.numeric(Y), center = 0) + 1e-6 # Initialize: uniform φ, simple S proxy phi_grid <- matrix(1.0, nrow = K, ncol = L_theta) Sgrid <- build_S_from_data(Y, seq_len(K), theta) f_n <- matrix(0+0i, nrow = K, ncol = Nn) for (k in 1:K) { Eneg <- exp(-1i * outer(theta, nvec)) f_n[k, ] <- as.vector((t(Sgrid[k, ]) %*% Eneg) / L_theta) f_n[k, ] <- enforce_reality_coeffs(f_n[k, ], nvec) } Lambda_phi <- (2 * sin(0.5 * omega))^(2*p_phi) Lambda_F <- (2 * sin(0.5 * omega))^(2*p_F) obj <- numeric(max_iter) for (it in 1:max_iter) { # ---- E-step (posterior stats & mixed moments) ---- m_hat <- matrix(0+0i, nrow = K, ncol = Nn) for (k in 1:K) { prior_k <- phi_grid[k, ] Srow <- matrix(Sgrid[k, ], nrow = I, ncol = L_theta, byrow = TRUE) LogL <- - ( (Y[, k] - Srow)^2 ) / (2 * sigma^2) LogW <- sweep(LogL, 2L, log(pmax(prior_k, 1e-12)), `+`) W <- softmax_rows(LogW) Eneg <- exp(-1i * outer(theta, nvec)) xi <- W %*% Eneg m_hat[k, ] <- colMeans(xi * Y[, k]) } # ---- Φ-step (phase update in spectral domain via FFT) ---- phi_hat_n <- matrix(0+0i, nrow = K, ncol = Nn) for (k in 1:K) { F_w <- coeffs_to_Xomega_fft(f_n[k, ], nvec) M_w <- coeffs_to_Xomega_fft(m_hat[k, ], nvec) denom <- (Mod(F_w)^2) + lambda_phi * Lambda_phi + 1e-10 Phi_w <- Conj(F_w) * M_w / denom phi_hat_n[k, ] <- Xomega_to_coeffs_fft(Phi_w, nvec) } # Synthesize φ(θ) & project phi_new <- matrix(0, nrow = K, ncol = L_theta) Epos <- exp(1i * outer(nvec, theta)) for (k in 1:K) { phi_row <- Re(drop(t(Epos) %*% phi_hat_n[k, ])) phi_new[k, ] <- phi_row } phi_new <- row_project_simplex(phi_new) # ---- F-step (surface update in spectral domain via FFT) ---- f_n_new <- matrix(0+0i, nrow = K, ncol = Nn) for (k in 1:K) { Phi_w <- coeffs_to_Xomega_fft(phi_hat_n[k, ], nvec) M_w <- coeffs_to_Xomega_fft(m_hat[k, ], nvec) denom <- (Mod(Phi_w)^2) + lambda_F * Lambda_F + 1e-10 F_w <- Conj(Phi_w) * M_w / denom f_n_new[k, ] <- enforce_reality_coeffs(Xomega_to_coeffs_fft(F_w, nvec), nvec) } # Synthesize S(t,θ) S_new <- matrix(0, nrow = K, ncol = L_theta) for (k in 1:K) S_new[k, ] <- surface_from_coeffs(f_n_new[k, ], nvec, theta) # ---- Convergence & update ---- diff_phi <- sqrt(mean((phi_new - phi_grid)^2)) diff_f <- sqrt(mean(Mod(f_n_new - f_n)^2)) if (verbose) message(sprintf("[FFT] iter %02d: Δφ=%.3e, Δf=%.3e", it, diff_phi, diff_f)) phi_grid <- phi_new; f_n <- f_n_new; Sgrid <- S_new obj[it] <- mean(Mod(m_hat)^2) if (max(diff_phi, diff_f) < tol) { obj <- obj[1:it]; break } } list(varphi_theta = phi_grid, phi_hat_n = phi_hat_n, f_n = f_n, Sgrid = Sgrid, theta = theta, nvec = nvec, omega = omega, time = seq_len(K), lambda_phi = lambda_phi, p_phi = p_phi, lambda_F = lambda_F, p_F = p_F, sigma = sigma, objective = obj) } ``` # Simulation 1: Double-Slit Physics Experiment We simulate an interference-like kime-surface $$\mathcal S(t,\theta)=B(t)+A_1(t)\cos(\kappa t+\theta)+A_2(t)\cos(2(\kappa t+\theta)+\phi_2),$$ e.g., $\mu(t) + A(t)\cos(\theta) + 0.25A(t)\cos(2\theta)$, with a time-varying *mixture of von Mises* phase laws (two lobes drifting over time), then generate repeated measurements $$\underbrace{y_{j,k}}_{observed}=\overbrace{\mathcal S(t_k,\underbrace{\Theta_j(t_k)}_{intrinsic\\ phase\ domain\\ stochasticity})}^{kimesurface} +\underbrace{\varepsilon_{j,k}}_{extrinsic\ range\\ stochasticity}.$$ ```{r sim-double-slit} # von Mises pdf on grid (no external deps) dvonmises <- function(theta, mu, kappa) { (1/(2*pi* besselI(kappa, 0))) * exp(kappa * cos(theta - mu)) } simulate_double_slit <- function(I = 256, K = 256, L_theta = 256, kappa_phase = 6, noise_sd = 0.20, kappa_wave = 12, seed = 42) { set.seed(seed) theta <- theta_grid(L_theta) t <- seq(0, 1, length.out = K) # Surface components B <- 0.2 * sin(2*pi*t) # small baseline A1 <- 0.8 * exp(-20*(t-0.5)^2) # Gaussian envelope A2 <- 0.3 * exp(-20*(t-0.5)^2) phi2 <- pi/6 S_true <- sapply(t, function(tt) B[which.min(abs(t-tt))] + A1[which.min(abs(t-tt))] * cos(kappa_wave*tt + theta) + A2[which.min(abs(t-tt))] * cos(2*(kappa_wave*tt + theta) + phi2) ) %>% t() # Time-varying von Mises mixture over theta (two drifting lobes) mu1 <- (2*pi*(0.2 + 0.6*t)) %% (2*pi) mu2 <- (2*pi*(0.9 - 0.6*t)) %% (2*pi) w <- 0.5 + 0.4*sin(2*pi*t) # mixture weight phi_true <- matrix(0, nrow = K, ncol = L_theta) for (k in 1:K) { p <- w[k]*dvonmises(theta, mu1[k], kappa_phase) + (1-w[k])*dvonmises(theta, mu2[k], kappa_phase) phi_true[k, ] <- p / mean(p) # mean == 1 (∫dθ/(2π)=1) } # Draw Θ_jk by discrete sampling on theta grid according to phi_true[k,] sample_theta <- function(p_row) { sample(theta, size = I, replace = TRUE, prob = pmax(p_row, 1e-12)) } Y <- matrix(NA_real_, nrow = I, ncol = K) for (k in 1:K) { thetas <- sample_theta(phi_true[k, ]) mu_k <- sapply(thetas, function(th) S_true[k, which.min(abs(theta - th))]) Y[, k] <- mu_k + rnorm(I, 0, noise_sd) } list(Y = Y, time = t, theta = theta, phi_true = phi_true, S_true = S_true) } ds <- simulate_double_slit() str(ds, 1) ``` ## Testing KPT-GEM and KPT-FFT on Double-Slit Experiment ```{r run-ds} # KPT-GEM (surface stabilized from data) fit_ds_gem <- kpt_gem(ds$Y, time = ds$time, J = 16, L_theta = length(ds$theta), lambda = 2e-2, p = 1, max_iter = 30, tol = 5e-4, verbose = TRUE) # KPT-FFT (alternating) fit_ds_fft <- kpt_fft(ds$Y, time = ds$time, J = 16, L_theta = length(ds$theta), lambda_phi = 2e-2, p_phi = 1, lambda_F = 2e-2, p_F = 1, max_iter = 30, tol = 5e-4, verbose = TRUE) ``` ## Double-Slit Results: Quantitative & Qualitative Evaluation ```{r eval-ds} # Phase metrics per time pm_gem <- phase_metrics_df(ds$phi_true, fit_ds_gem$varphi_theta, ds$theta, ds$time) pm_fft <- phase_metrics_df(ds$phi_true, fit_ds_fft$varphi_theta, ds$theta, ds$time) # Surface L2 under cone measure S_gem <- fit_ds_gem$Sgrid S_fft <- fit_ds_fft$Sgrid m_gem <- surface_L2_cone(ds$S_true, S_gem, ds$time, ds$theta) m_fft <- surface_L2_cone(ds$S_true, S_fft, ds$time, ds$theta) summary_ds <- tibble( Algorithm = c("KPT-GEM", "KPT-FFT"), Mean_JSD_bits = c(mean(pm_gem$JSD_bits), mean(pm_fft$JSD_bits)), Mean_Hellinger = c(mean(pm_gem$Hellinger), mean(pm_fft$Hellinger)), Mean_TV = c(mean(pm_gem$TV), mean(pm_fft$TV)), Rel_L2_Surface= c(m_gem$rel_L2, m_fft$rel_L2) ) kable(summary_ds, digits = 4, caption = "Double-Slit: Summary metrics: True Kimesurface vs. KPT-GEM and KPT-FFT") # Additional debugging cat("Length check:\n") cat("ds$theta:", length(ds$theta), "\n") cat("fit_ds_fft$theta:", length(fit_ds_fft$theta), "\n") cat("ds$phi_true row:", length(ds$phi_true[1,]), "\n") cat("fit_ds_fft$varphi_theta row:", length(fit_ds_fft$varphi_theta[1,]), "\n") # Check if they're the same all.equal(length(ds$theta), length(fit_ds_fft$theta)) all.equal(length(ds$phi_true[1,]), length(fit_ds_fft$varphi_theta[1,])) # Overlays at representative times pick_t <- unique(round(seq(1, length(ds$time), length.out = 4))) # plots <- lapply(pick_t, function(kk) { # kpt_plot_phase_overlay( # ds$theta, # ds$phi_true[kk, ], # fit_ds_fft$varphi_theta[kk, ], # t_lab = paste("t =", round(ds$time[kk], 3)), # title = "Double-Slit: Phase (True vs. KPT-FFT)" # ) # }) plots <- lapply(pick_t, function(kk) { kpt_plot_phase_overlay( ds$theta, ds$phi_true[kk, ], fit_ds_fft$varphi_theta[kk, ], fit_ds_gem$varphi_theta[kk, ], t_lab = paste("t =", round(ds$time[kk], 3)), title = "Double-Slit: Phases (True vs. KPT-Estimated)" ) }) plots[[1]]; plots[[2]]; plots[[3]]; plots[[4]] ### Plot heatmap of tru and KPT recoverd phases # ds$phi_true, fit_ds_gem$varphi_theta, ds$theta, ds$time) # pm_fft <- phase_metrics_df(ds$phi_true, fit_ds_fft$varphi_theta, ds$theta, ds$time hm_phase_true <- plot_ly(x=ds$time, y=ds$theta, z=ds$phi_true, type="heatmap", name="DS: True Phase", opacity=0.9, colorscale='Viridis', colorbar = list(title = "True Phase")) hm_phase_gem <- plot_ly(x=ds$time, y=ds$theta, z=fit_ds_gem$varphi_theta, type="heatmap", name="DS: KPT-GEM Phase", opacity=0.9, colorscale='Viridis', colorbar = list(title = "KPT-GEM Phase")) hm_phase_fft <- plot_ly(x=ds$time, y=ds$theta, z=fit_ds_fft$varphi_theta, type="heatmap", name="DS: KPT-FFT Phase", opacity=0.9, colorscale='Viridis', colorbar = list(title = "KPT-FFT Phase")) # Display one interactively (others available in object) # p_fft subplot(hm_phase_true, hm_phase_gem, hm_phase_fft, nrows = 3) %>% layout(title="Double-Slit Experiment: Heatmaps of True, KPT-GEM & KPT-FFT Phases") ### FIG. 3. (Double-Slit Experiment) Phase metrics (KL, symmetric JSD in bits, Hellinger, TV) # summarizing the performance of the KPT-GEM to recover ϕt at each time. JSD’s bit scale is an # information-theoretic distance that is easy to compare across runs pm_gem_plot <- plot_ly(pm_gem, x = ~time) %>% add_trace(y = ~KL_true_est, type = 'scatter', mode = 'lines', name = 'KL(true||est)', line = list(color = '#1f77b4', width = 2)) %>% add_trace(y = ~KL_est_true, type = 'scatter', mode = 'lines', name = 'KL(est||true)', line = list(color = '#ff7f0e', width = 2)) %>% add_trace(y = ~JSD_bits, type = 'scatter', mode = 'lines', name = 'JSD (bits)', line = list(color = '#2ca02c', width = 2)) %>% add_trace(y = ~Hellinger, type = 'scatter', mode = 'lines', name = 'Hellinger', line = list(color = '#d62728', width = 2)) %>% add_trace(y = ~TV, type = 'scatter', mode = 'lines', name = 'Total Variation', line = list(color = '#9467bd', width = 2)) %>% layout(title = list(text = "KPT-GEM: Distribution Distance Metrics Over Time", x = 0.5, xanchor = 'center'), xaxis = list(title = "Time", gridcolor = '#e0e0e0'), yaxis = list(title = "Distance Metric Value", gridcolor = '#e0e0e0'), plot_bgcolor = 'white', paper_bgcolor = 'white', legend = list(x = 0.02, y = 0.98, bgcolor = 'rgba(255,255,255,0.9)', bordercolor = '#CCCCCC', borderwidth = 1)) pm_gem_plot ``` ```{r ds-surfaces-3d} # Create plots p_true <- plot_surface_3d(ds$time, ds$theta, ds$S_true, "True Surface", opacity=0.95, colorscale='Viridis', colorbar_title = "True Surface") p_gem <- plot_surface_3d(ds$time, ds$theta, S_gem, "KPT-GEM Surface", opacity=0.4, colorscale='Blues', colorbar_title = "KPT-GEM Surface") p_fft <- plot_surface_3d(ds$time, ds$theta, S_fft, "KPT-FFT Surface", opacity=0.1, colorscale='Reds', colorbar_title = "KPT-FFT Surface") subplot(p_true, p_gem, p_fft) %>% layout(title = "Double-Slit: True Kimesurface vs. KPT-GEM vs. KPT-FFT") # p_true <- plot_surface_3d(ds$time, ds$theta, ds$S_true, "DS: True Surface", # opacity=0.9, colorscale='Viridis') # p_gem <- plot_surface_3d(ds$time, ds$theta, S_gem, "DS: KPT-GEM Surface", # opacity=0.7, colorscale='red') # p_fft <- plot_surface_3d(ds$time, ds$theta, S_fft, "DS: KPT-FFT Surface", # opacity=0.5, colorscale='blue') # # Display one interactively (others available in object) # # p_fft # # subplot(p_true, p_gem, p_fft) %>% layout(title="Double-Slit: True Kimesurface vs. KPT-GEM vs. KPT-FFT") #### Heatmaps hm_p_true <- plot_ly(x=ds$time, y=ds$theta, z=ds$S_true, type="heatmap", name="DS: True Surface", opacity=0.9, colorscale='Viridis', colorbar = list(title = "True Surface")) hm_p_gem <- plot_ly(x=ds$time, y=ds$theta, z=S_gem, type="heatmap", name="DS: KPT-GEM Surface", opacity=0.9, colorscale='Viridis', colorbar = list(title = "KPT-GEM Surface")) hm_p_fft <- plot_ly(x=ds$time, y=ds$theta, z=S_fft, type="heatmap", name="DS: KPT-FFT Surface", opacity=0.9, colorscale='Viridis', colorbar = list(title = "KPT-FFT Surface")) # Display one interactively (others available in object) # p_fft subplot(hm_p_true, hm_p_gem, hm_p_fft, nrows = 3) %>% layout(title="Double-Slit Experiment: Heatmaps of True Kimesurface, KPT-GEM & KPT-FFT") ## 3 3D scenes with kime-surfaces # Create individual plots with distinct scene names # Create the three surfaces using your original approach p_true <- plot_ly(x = ds$time, y = ds$theta, z = ds$S_true, type = "surface", scene = 'scene1', name = "True Surface", colorscale = 'Viridis', opacity = 0.95) %>% layout(title = "DS: True Surface") p_gem <- plot_ly(x = ds$time, y = ds$theta, z = S_gem, type = "surface", scene = 'scene2', name = "KPT-GEM Surface", colorscale = 'Blues', opacity = 0.4) %>% layout(title = "DS: KPT-GEM Surface") p_fft <- plot_ly(x = ds$time, y = ds$theta, z = S_fft, type = "surface", scene = 'scene3', name = "KPT-FFT Surface", colorscale = 'Reds', opacity = 0.1) %>% layout(title = "DS: KPT-FFT Surface") # Scene titles/annotations annotations <- list( list(showarrow = FALSE, text = 'True Surface', xref = 'paper', yref = 'paper', x = 0.15, y = 0.95, xanchor = 'center', yanchor = 'bottom', font = list(size = 14)), list(showarrow = FALSE, text = 'KPT-GEM Surface', xref = 'paper', yref = 'paper', x = 0.5, y = 0.95, xanchor = 'center', yanchor = 'bottom', font = list(size = 14)), list(showarrow = FALSE, text = 'KPT-FFT Surface', xref = 'paper', yref = 'paper', x = 0.85, y = 0.95, xanchor = 'center', yanchor = 'bottom', font = list(size = 14)) ) # Combine the plots with explicit domain assignments combined_plot <- subplot(p_true, p_gem, p_fft) %>% layout( title = "DS: True Kimesurface vs. KPT-GEM vs. KPT-FFT", scene = list( domain = list(x = c(0, 0.33), y = c(0, 1)), aspectmode = "cube", camera = list(eye = list(x = 1.5, y = 1.5, z = 1.5)) ), scene2 = list( domain = list(x = c(0.33, 0.66), y = c(0, 1)), aspectmode = "cube", camera = list(eye = list(x = 1.5, y = 1.5, z = 1.5)) ), scene3 = list( domain = list(x = c(0.66, 1), y = c(0, 1)), aspectmode = "cube", camera = list(eye = list(x = 1.5, y = 1.5, z = 1.5)) ), annotations = annotations, margin = list(l = 0, r = 0, b = 0, t = 50) ) # Add the same camera synchronization JavaScript from your working example combined_plot <- combined_plot %>% htmlwidgets::onRender( "function(x, el) { x.on('plotly_relayout', function(d) { const camera = Object.keys(d).filter((key) => /\\.camera$/.test(key)); if (camera.length) { const scenes = Object.keys(x.layout).filter((key) => /^scene\\d*/.test(key)); const new_layout = {}; scenes.forEach(key => { new_layout[key] = {...x.layout[key], camera: {...d[camera]}}; }); Plotly.relayout(x, new_layout); } }); }") combined_plot ``` # Simulation 2: fMRI ON vs. OFF We generate an ON/OFF event design, produce an ON-OFF signal per $subject \times run$, and impose a phase process that is *more concentrated* during $ON$ (stimulus) blocks (latent synchrony). ```{r sim-fmri} simulate_fmri_onoff <- function(P = 10, R = 20, TR = 2, duration = 512, # duration = 512 = 2 * 256 L_theta = 256, noise_sd = 0.20, kappa_on = 5, kappa_off = 0.5, seed = 77) { set.seed(seed) K <- as.integer(duration / TR); t <- seq(0, by = TR, length.out = K) # Block design: 30s ON/OFF block_len <- 30; on_len <- block_len/(2*TR); design <- rep(c(0,1), each = on_len) design <- rep_len(design, K) # HRF-like shape (simple) hrf <- function(x) (x^5 * exp(-x)) / gamma(6) - 0.35 * (x^7 * exp(-x)) / gamma(8) h <- hrf(seq(0, 12, length.out = on_len)) A <- stats::filter(rep_len(design, K), filter = h, method = "convolution", sides = 1) A <- as.numeric(A); A[is.na(A)] <- 0; A <- scales::rescale(A, to = c(0.2, 1.0)) mu <- 0.05 * sin(2*pi*t/max(t)) # Observations per subject×run: Y = (ON - OFF) proxy style I <- P * R ON <- array(0, dim = c(P, R, K)) OFF <- array(0, dim = c(P, R, K)) for (p in 1:P) for (r in 1:R) { OFF[p, r, ] <- mu + rnorm(K, 0, noise_sd) ON[p, r, ] <- mu + A + rnorm(K, 0, noise_sd) } Y <- sapply(1:K, function(k) as.numeric(ON[, , k] - OFF[, , k])) # [I x K] # Construct S_true(t,θ) ~ baseline + A(t)*cos(θ) + 0.25 A(t) cos(2θ) # vs. S(t,\theta)=B(t)+A_1(t)\cos(\kappa t+\theta)+A_2(t)\cos(2(\kappa t+\theta)+\phi_2) theta <- theta_grid(L_theta) S_true <- sapply(1:K, function(k) mu[k] + A[k]*cos(theta) + 0.25*A[k]*cos(2*theta)) %>% t() # Phase law more concentrated in ON than OFF phi_true <- matrix(0, nrow = K, ncol = L_theta) for (k in 1:K) { kap <- if (design[k] == 1) kappa_on else kappa_off p <- (1/(2*pi*besselI(kap, 0))) * exp(kap * cos(theta - 0)) # μ=0 phi_true[k, ] <- p / mean(p) } list(Y = Y, time = t, theta = theta, phi_true = phi_true, S_true = S_true) } fm <- simulate_fmri_onoff() str(fm, 1) ``` ## Run KPT-GEM and KPT-FFT on fMRI data ```{r run-fmri} fit_fm_gem <- kpt_gem(fm$Y, time = fm$time, J = 12, L_theta = length(fm$theta), lambda = 1e-2, p = 1, max_iter = 25, tol = 8e-4, verbose = TRUE) fit_fm_fft <- kpt_fft(fm$Y, time = fm$time, J = 12, L_theta = length(fm$theta), lambda_phi = 1e-2, p_phi = 1, lambda_F = 1e-2, p_F = 1, max_iter = 25, tol = 8e-4, verbose = TRUE) ``` ## fMRI: Quantitative & Qualitative Evaluation ```{r eval-fmri} pm_gem <- phase_metrics_df(fm$phi_true, fit_fm_gem$varphi_theta, fm$theta, fm$time) pm_fft <- phase_metrics_df(fm$phi_true, fit_fm_fft$varphi_theta, fm$theta, fm$time) S_gem <- fit_fm_gem$Sgrid S_fft <- fit_fm_fft$Sgrid m_gem <- surface_L2_cone(fm$S_true, S_gem, fm$time, fm$theta) m_fft <- surface_L2_cone(fm$S_true, S_fft, fm$time, fm$theta) summary_fmri <- tibble( Algorithm = c("KPT-GEM", "KPT-FFT"), Mean_JSD_bits = c(mean(pm_gem$JSD_bits), mean(pm_fft$JSD_bits)), Mean_Hellinger = c(mean(pm_gem$Hellinger), mean(pm_fft$Hellinger)), Mean_TV = c(mean(pm_gem$TV), mean(pm_fft$TV)), Rel_L2_Surface= c(m_gem$rel_L2, m_fft$rel_L2) ) kable(summary_fmri, digits = 4, caption = "fMRI ON/OFF: Summary metrics: True Kimesurface vs. KPT-GEM vs. KPT-FFT") # Phase overlays (sample ON and OFF times) idx_on <- which(diff(sign(diff(fm$time)))==0)[1]; idx_on <- ifelse(is.na(idx_on), 30, idx_on) idx_off <- max(1, idx_on - 10) # p_on <- plot_phase_overlay(fm$theta, fm$phi_true[idx_on, ], fit_fm_fft$varphi_theta[idx_on, ], # t_lab = sprintf("ON t=%.0fs", fm$time[idx_on]), # title = "fMRI: Phase (True vs. KPT-FFT)") # p_off <- plot_phase_overlay(fm$theta, fm$phi_true[idx_off, ], fit_fm_fft$varphi_theta[idx_off, ], # t_lab = sprintf("OFF t=%.0fs", fm$time[idx_off]), # title = "fMRI: Phase (True vs. KPT-FFT)") # p_on p_on <- kpt_plot_phase_overlay(fm$theta, fm$phi_true[idx_on, ], fit_fm_fft$varphi_theta[idx_on, ], fit_ds_gem$varphi_theta[idx_on, ], t_lab = sprintf("ON t=%.0fs", fm$time[idx_on]), title = "fMRI: Phase (True vs. KPT-FFT)") p_off <- kpt_plot_phase_overlay(fm$theta, fm$phi_true[idx_off, ], fit_fm_fft$varphi_theta[idx_off, ], fit_ds_gem$varphi_theta[idx_on, ], t_lab = sprintf("OFF t=%.0fs", fm$time[idx_off]), title = "fMRI: Phase (True vs. KPT-FFT)") p_on p_off # # Dimension-checked overlay (unique name to avoid conflicts) # kpt_plot_phase_overlay <- function(theta, phi_true_row, phi_est_row, t_lab = "", title = "") { # n_theta <- length(theta) # stopifnot(length(phi_true_row) == n_theta, length(phi_est_row) == n_theta) # # df <- tibble::tibble( # theta = rep(theta, 2L), # length = 2*n # density = c(phi_true_row, phi_est_row), # length = 2*n # kind = rep(c("True", "Estimated"), each = n_theta) # length = 2*n # ) # # ggplot2::ggplot(df, ggplot2::aes(theta, density, color = kind)) + # ggplot2::geom_line(linewidth = 0.9) + # ggplot2::labs(title = title, subtitle = t_lab, # x = expression(theta), y = expression(varphi(t,theta))) + # ggplot2::theme_minimal() # } # Additional debugging cat("fMRI: Length check:\n") cat("fm$theta:", length(fm$theta), "\n") cat("fit_fm_fft$theta:", length(fit_fm_fft$theta), "\n") cat("fm$phi_true row:", length(fm$phi_true[1,]), "\n") cat("fit_fm_fft$varphi_theta row:", length(fit_fm_fft$varphi_theta[1,]), "\n") # Check if they're the same all.equal(length(fm$theta), length(fit_fm_fft$theta)) all.equal(length(fm$phi_true[1,]), length(fit_fm_fft$varphi_theta[1,])) # Overlays at representative times # pick_t <- unique(round(seq(1, length(fm$time), length.out = 4))) # plots <- lapply(pick_t, function(kk) { # kpt_plot_phase_overlay( # fm$theta, # fm$phi_true[kk, ], # fit_fm_fft$varphi_theta[kk, ], # fit_fm_fft$varphi_theta[kk, ], # t_lab = paste("t =", round(fm$time[kk], 3)), # title = "fMRI: Phase (True vs. KPT-FFT)" # ) # }) # plots[[1]]; plots[[2]]; plots[[3]]; plots[[4]] pick_t <- unique(round(seq(1, length(fm$time), length.out = 10))) plots <- lapply(pick_t, function(kk) { kpt_plot_phase_overlay( ds$theta, ds$phi_true[kk, ], fit_ds_fft$varphi_theta[kk, ], fit_ds_gem$varphi_theta[kk, ], t_lab = paste("t =", round(ds$time[kk], 3)), title = "fMRI: Phases (True vs. KPT-Estimated)" ) }) plots[[1]]; plots[[2]]; plots[[3]]; plots[[4]]; plots[[10]] ``` ```{r fmri-surfaces-3d} p_true <- plot_surface_3d(fm$time, fm$theta, fm$S_true, "FMRI: True Surface", opacity=0.95, colorscale='Viridis', colorbar_title = "True Surface") p_gem <- plot_surface_3d(fm$time, fm$theta, S_gem, "FMRI: KPT-GEM Surface", opacity=0.4, colorscale='Blues', colorbar_title = "KPT-GEM Surface") p_fft <- plot_surface_3d(fm$time, fm$theta, S_fft, "FMRI: KPT-FFT Surface", opacity=0.1, colorscale='Reds', colorbar_title = "KPT-FFT Surface") subplot(p_true, p_gem, p_fft) %>% layout(title = "fMRI: True Kimesurface vs. KPT-GEM vs. KPT-FFT") #### Heatmaps hm_p_true <- plot_ly(x=fm$time, y=fm$theta, z=fm$S_true, type="heatmap", name="FMRI: True Surface", opacity=0.9, colorscale='Viridis', colorbar = list(title = "True Surface")) hm_p_gem <- plot_ly(x=fm$time, y=fm$theta, z=S_gem, type="heatmap", name="FMRI: KPT-GEM Surface", opacity=0.9, colorscale='Viridis', colorbar = list(title = "KPT-GEM Surface")) hm_p_fft <- plot_ly(x=fm$time, y=fm$theta, z=S_fft, type="heatmap", name="FMRI: KPT-FFT Surface", opacity=0.9, colorscale='Viridis', colorbar = list(title = "KPT-FFT Surface")) # Display one interactively (others available in object) # p_fft subplot(hm_p_true, hm_p_gem, hm_p_fft, nrows = 3) %>% layout(title="fMRI Experiment: Heatmaps of True Kimesurface, KPT-GEM & KPT-FFT") ## 3 3D scenes with kime-surfaces # Create individual plots with distinct scene names # Create the three surfaces using your original approach p_true <- plot_ly(x = fm$time, y = fm$theta, z = fm$S_true, type = "surface", scene = 'scene1', name = "True Surface", colorscale = 'Viridis', opacity = 0.95) %>% layout(title = "FMRI: True Surface") p_gem <- plot_ly(x = fm$time, y = fm$theta, z = S_gem, type = "surface", scene = 'scene2', name = "KPT-GEM Surface", colorscale = 'Blues', opacity = 0.4) %>% layout(title = "FMRI: KPT-GEM Surface") p_fft <- plot_ly(x = fm$time, y = fm$theta, z = S_fft, type = "surface", scene = 'scene3', name = "KPT-FFT Surface", colorscale = 'Reds', opacity = 0.1) %>% layout(title = "FMRI: KPT-FFT Surface") # Scene titles/annotations annotations <- list( list(showarrow = FALSE, text = 'True Surface', xref = 'paper', yref = 'paper', x = 0.15, y = 0.95, xanchor = 'center', yanchor = 'bottom', font = list(size = 14)), list(showarrow = FALSE, text = 'KPT-GEM Surface', xref = 'paper', yref = 'paper', x = 0.5, y = 0.95, xanchor = 'center', yanchor = 'bottom', font = list(size = 14)), list(showarrow = FALSE, text = 'KPT-FFT Surface', xref = 'paper', yref = 'paper', x = 0.85, y = 0.95, xanchor = 'center', yanchor = 'bottom', font = list(size = 14)) ) # Combine the plots with explicit domain assignments combined_plot <- subplot(p_true, p_gem, p_fft) %>% layout( title = "fMRI: True Kimesurface vs. KPT-GEM vs. KPT-FFT", scene = list( domain = list(x = c(0, 0.33), y = c(0, 1)), aspectmode = "cube", camera = list(eye = list(x = 1.5, y = 1.5, z = 1.5)) ), scene2 = list( domain = list(x = c(0.33, 0.66), y = c(0, 1)), aspectmode = "cube", camera = list(eye = list(x = 1.5, y = 1.5, z = 1.5)) ), scene3 = list( domain = list(x = c(0.66, 1), y = c(0, 1)), aspectmode = "cube", camera = list(eye = list(x = 1.5, y = 1.5, z = 1.5)) ), annotations = annotations, margin = list(l = 0, r = 0, b = 0, t = 50) ) # Add the same camera synchronization JavaScript from your working example combined_plot <- combined_plot %>% htmlwidgets::onRender( "function(x, el) { x.on('plotly_relayout', function(d) { const camera = Object.keys(d).filter((key) => /\\.camera$/.test(key)); if (camera.length) { const scenes = Object.keys(x.layout).filter((key) => /^scene\\d*/.test(key)); const new_layout = {}; scenes.forEach(key => { new_layout[key] = {...x.layout[key], camera: {...d[camera]}}; }); Plotly.relayout(x, new_layout); } }); }") combined_plot ``` # Consolidated Performance Tables ```{r tables} bind_rows( summary_ds %>% mutate(Experiment = "Double-Slit"), summary_fmri%>% mutate(Experiment = "fMRI ON/OFF") ) %>% relocate(Experiment) %>% arrange(Experiment, Algorithm) %>% kable(digits = 4, caption = "Consolidated metrics across experiments") ``` # Notes on Reproducibility & Alignment with Paper - *Normalization & geometry.* Phase integrals use $d\theta/(2\pi)$; row-mean-1 enforcement ensures densities integrate to $1$ on a uniform grid. All kime-surface comparisons use the *cone-weighted* $t\,dt\,d\theta$ $L^2$ norm. - *E-step statistics.* We use the unified *posterior Fourier expectations* $\xi_{i,k}(n)$ (row-wise softmax), eliminating notation drift and matching the paper’s algorithms. - *Spectral steps.* Both KPT-GEM and KPT-FFT algorithms implement the *Wiener–Sobolev* filter; KPT-FFT alternates $\Phi$- and $F$-updates, while KPT-GEM uses a stabilized surface proxy for robustness. The *reality constraint* $f_{-n}=\overline{f_n}$ is enforced in both algorithms. -*Anchoring.* A global phase-shift ambiguity is inherent; visualizations here keep the canonical grid (anchoring options can be added if needed).