## ----setup, include = FALSE---------------------------------------------------
knitr::opts_chunk$set(
  cache = TRUE,
  collapse = TRUE,
  comment = "#>",
  dpi = 72,
  fig.retina = 1
)
library(tf)
oldpar <- par(no.readonly = TRUE)
par(las = 1)
alpha_palette <- c(
  "#00000080", "#DF536B80", "#61D04F80", "#2297E680", "#28E2E580",
  "#CD0BBC80", "#F5C71080", "#9E9E9E80", "#00000080", "#DF536B80"
)
have_srvf <- rlang::is_installed("fdasrvf")
have_cc <- TRUE

## ----eval=FALSE, echo = TRUE, class.source = "fold-show"----------------------
# # One-shot registration (returns tf_registration object):
# reg <- tf_register(x, method = "...")
# tf_aligned(reg)   # registered/aligned curves
# tf_inv_warps(reg)     # estimated inverse warping functions (observed -> aligned time)
# tf_template(reg)  # template used
# summary(reg)      # alignment diagnostics
# plot(reg)          # 3-panel diagnostic plot
# 
# # Or step by step:
# warps <- tf_estimate_warps(x, method = "...")
# x_registered <- tf_align(x, warps)

## ----warp-illustration, echo = FALSE, fig.width = 6, fig.height = 6-----------
# Synthetic illustration of different warp types
s <- seq(0, 1, length.out = 201)

# Template: 
template_synth <- tfd(exp(-((s - 0.45)^2) / (2 * 0.08^2)) + cos(2*pi*s) + s, arg = s)

# Warp 1: affine shift + scale (not domain-preserving -- extends beyond [0,1])
h_shift <- tfd(0.8 * s + 0.1, arg = s)

# Warp 2: smooth diffeomorphism (domain-preserving)
h_smooth_raw <- s - 0.25 * sin(pi * s)
h_smooth <- tfd(
  (h_smooth_raw - h_smooth_raw[1]) / (h_smooth_raw[length(s)] - h_smooth_raw[1]),
  arg = s)

# Warp 3: wiggly diffeomorphism (domain-preserving)
h_wiggly_raw <- .1 * pbeta(s, .1, .1) + .5 * pbeta(s, 25, 25) + 
  .2 * pbeta(s, 4, 20) + .2 * pbeta(s, 100, 10)
h_wiggly <- tfd(h_wiggly_raw, arg = s)  

# Compose template with warps: m(h(s))
warped_shift <- tf_warp(template_synth, h_shift) |> suppressWarnings()
warped_smooth <- tf_warp(template_synth, h_smooth)
warped_wiggly <- tf_warp(template_synth, h_wiggly)

warp_cols <- c("#DF536B", "#2297E6", "#61D04F")

layout(matrix(1:9, 3, 3))
par(mar = c(4, 4, 3, 1))

warp_names <- c("Affine (shift & scale)", "Smooth elastic", "Wiggly elastic")
warps <- list(h_shift, h_smooth, h_wiggly)
warped <- list(warped_shift, warped_smooth, warped_wiggly)

# Column 1: blank, template, blank
plot.new()
plot(template_synth, lwd = 2.5,
     main = "Template m(s)", xlab = "s", ylab = "m(s)")
plot.new()

# Column 2: one warp per row
for (i in 1:3) {
  plot(s, s, type = "l", lty = 3, lwd = 1,
       main = paste0("h(s): ", warp_names[[i]]),
       xlab = "s", ylab = "h(s)", ylim = c(-0.05, 1.15))
  do.call(graphics::lines, list(warps[[i]], col = warp_cols[[i]], lwd = 2.5))
}

# Column 3: template (dashed) + warped curve per row
for (i in 1:3) {
  plot(template_synth, lwd = 2, lty = 3,
       main = paste0("x(t) = m(h(s)): ", warp_names[[i]]),
       xlab = "t", ylab = "x(t)")
  do.call(graphics::lines, list(warped[[i]], col = warp_cols[[i]], lwd = 2.5))
}

## ----quickstart, class.source = "fold-show"-----------------------------------
pinch_small <- pinch[1:10]
template_affine <- pinch[7] |> tf_smooth(f= .2)

reg_shift <- tf_register(
  pinch_small,
  method = "affine",
  template = template_affine,
  type = "shift"
)
reg_shift
summary(reg_shift)
pinch_inv_warp_shift <- tf_inv_warps(reg_shift)
pinch_small_reg <- tf_aligned(reg_shift)
pinch_template_shift <- tf_template(reg_shift)

## ----quickstart-plot, echo = FALSE, class.source = "fold-show", fig.width = 6, fig.height = 3----
# plot(reg_shift) gives a compact diagnostic; below reuses the extracted
# components for a customized version of the same view:
layout(t(1:3))
plot(
  pinch_small,
  main = "Original Curves",
  xlab = "Observed Time t [sec]",
  ylab = "Force [N]",
  lwd = 1.5,
  col = alpha_palette
)
lines(pinch_template_shift, lwd = 2, lty = 3, col = rgb(0,0,.5))
plot(
  pinch_inv_warp_shift,
  points = FALSE,
  main = "Inverse Warping Functions",
  xlab = "Observed Time t [sec]",
  ylab = "Registered Time s [sec]",
  lwd = 1.5,
  col = alpha_palette
)
abline(0, 1, lty = 3)
plot(
  pinch_small_reg,
  main = "Registered Curves",
  xlab = "Registered Time s [sec]",
  ylab = "Force [N]",
  lwd = 1.5,
  col = alpha_palette, 
  points = FALSE
)
lines(pinch_template_shift, lwd = 2, lty = 3, col = rgb(0,0,.5))

## ----template-choice, fig.width = 6, fig.height = 4---------------------------
# Gaussian bumps with large shifts:
s <- seq(-4, 6, length.out = 201)
mus <- c(-2, -1, 0, 1, 2)
bumps <- tfd(t(sapply(mus, \(mu) dnorm(s, mu, sd = 0.5))), arg = s)

# Pointwise mean is smeared -- not a good template:
bumps_mean <- mean(bumps)
# MBD median picks the most central observed curve:
bumps_median <- median(bumps, depth = "MBD")

# Register with default (mean) vs MBD median template:
reg_mean <- tf_register(bumps, method = "affine", type = "shift",
                        template = bumps_mean)
reg_median <- tf_register(bumps, method = "affine", type = "shift",
                          template = bumps_median)

layout(matrix(1:6, 2, 3, byrow = TRUE))
plot(bumps, col = alpha_palette, lwd = 1.5,
     main = "Original + mean template")
lines(bumps_mean, lwd = 3, lty = 2)
plot(tf_inv_warps(reg_mean), col = alpha_palette, lwd = 1.5, points = FALSE,
     main = "Inverse warps (mean template)",
     xlab = "Observed time t", ylab = "Aligned time s")
abline(0, 1, lty = 3)
plot(tf_aligned(reg_mean), col = alpha_palette, lwd = 1.5, points = FALSE,
     main = "Aligned to mean")
lines(bumps_mean, lwd = 3, lty = 2)


plot(bumps, col = alpha_palette, lwd = 1.5,
     main = "Original + MBD median template")
lines(bumps_median, lwd = 3, lty = 2, col = "red3")
plot(tf_inv_warps(reg_median), col = alpha_palette, lwd = 1.5, points = FALSE,
     main = "Inverse warps (median template)",
     xlab = "Observed time t", ylab = "Aligned time s")
abline(0, 1, lty = 3)
plot(tf_aligned(reg_median), col = alpha_palette, lwd = 1.5, points = FALSE,
     main = "Aligned to MBD median")
lines(bumps_median, lwd = 3, lty = 2, col = "red3")

## ----diagnostics, class.source = "fold-show"----------------------------------
x <- pinch[1:10]

# Register with affine shift warps:
reg_aff <- tf_register(x, method = "affine", type = "shift_scale")

# summary() gives a quick quantitative overview:
summary(reg_aff)

## ----diagnostics-plot, fig.width = 6, fig.height = 3, class.source = "fold-show"----
# plot() provides a 3-panel diagnostic:
plot(reg_aff)

## ----diagnostics-features, class.source = "fold-show"-------------------------
# Are global peak locations more concentrated after alignment?
peak_before <- tf_where(x, value == max(value)) |> as.numeric()
peak_after_aff <- tf_where(tf_aligned(reg_aff), value == max(value)) |> as.numeric()

data.frame(
  metric = c("sd_peak_before", "sd_peak_after_affine"),
  value = c(sd(peak_before), sd(peak_after_aff))
)

## ----diagnostics-optional-srvf, eval = have_srvf, include = have_srvf, class.source = "fold-show"----
# Compare with SRVF (non-linear warps):
reg_srvf <- tf_register(x, method = "srvf")

# Side-by-side summaries:
summary(reg_srvf)

## ----diagnostics-optional-srvf-compare, eval = have_srvf, include = have_srvf, class.source = "fold-show"----
peak_after_srvf <- tf_where(tf_aligned(reg_srvf), value == max(value)) |> as.numeric()

data.frame(
  metric = c("sd_peak_before", "sd_peak_after_affine", "sd_peak_after_srvf"),
  value = c(sd(peak_before), sd(peak_after_aff), sd(peak_after_srvf))
)

## ----diagnostics-optional-srvf-plot, eval = have_srvf, include = have_srvf, fig.width = 6, fig.height = 4----
layout(matrix(1:6, 2, 3, byrow = TRUE))
# Affine:
plot(x, main = "Original", col = alpha_palette, lwd = 1.5)
lines(tf_template(reg_aff), lwd = 2, lty = 2)
plot(tf_inv_warps(reg_aff), main = "Affine Inverse Warps",
     col = alpha_palette, lwd = 1.5,  points = FALSE,
     xlab = "Observed time t", ylab = "Aligned time s")
abline(0, 1, lty = 3)
plot(tf_aligned(reg_aff), main = "Affine Aligned",
     col = alpha_palette, lwd = 1.5, points = FALSE)
lines(tf_template(reg_aff), lwd = 2, lty = 2)
# SRVF:
plot(x, main = "Original", col = alpha_palette, lwd = 1.5)
lines(tf_template(reg_srvf), lwd = 2, lty = 2)
plot(tf_inv_warps(reg_srvf), main = "SRVF Inverse Warps",
     col = alpha_palette, lwd = 1.5,   points = FALSE,
     xlab = "Observed time t", ylab = "Aligned time s")
abline(0, 1, lty = 3)
plot(tf_aligned(reg_srvf), main = "SRVF Aligned",
     col = alpha_palette, lwd = 1.5, points = FALSE)
lines(tf_template(reg_srvf), lwd = 2, lty = 2)

## ----pinch-compare, class.source = "fold-show"--------------------------------
pinch_small <- pinch[1:10]

reg_aff <- tf_register(pinch_small, method = "affine", type = "shift_scale")
inv_warp_aff <- tf_inv_warps(reg_aff)

# tf_landmarks_extrema needs smoothed inputs,
#     otherwise it tends to detect lots of spurious features:
pinch_small |> tf_landmarks_extrema(which = "max") |> head()
#     ... so in this case, we simply use the global maximum for each curve:
(peak_locs <- pinch_small |> tf_where(value == max(value)) |> unlist() |> as.matrix())
reg_lm <- tf_register(pinch_small, method = "landmark", landmarks = peak_locs)
inv_warp_lm <- tf_inv_warps(reg_lm)

# ... but using more than one peak location as the only landmark will get us
# better alignment here - use times where curve first & last exceeds 3 as well:
pinch_landmarks  <- cbind(
  start = pinch_small |> tf_where(value > 3, return = "first") |> unlist(),
  peak  = peak_locs,
  end   = pinch_small |> tf_where(value > 3, return = "last") |> unlist()
)
pinch_landmarks |> head()
reg_lm2 <- tf_register(pinch_small, method = "landmark", landmarks = pinch_landmarks)
inv_warp_lm2 <- tf_inv_warps(reg_lm2)

## ----pinch-compare-plot, fig.width = 6, fig.height = 6------------------------
layout(t(matrix(1:12, 4, 3)))
par(cex.main = 0.8)
plot.new()
plot(inv_warp_aff, main = "Affine Inverse Warps", ylab = "",
     points = FALSE, col = alpha_palette, lwd = 1.5)
abline(0, 1, lty = 3)
plot(tf_aligned(reg_aff), main = "Affine Registered",
     col = alpha_palette, lwd = 1.5, points = FALSE)
plot(tf_aligned(reg_aff), main = "Affine Registered",
     type = "lasagna", col = hcl.colors(12, rev = TRUE))

plot(pinch_small, main = "Original",
     col = alpha_palette, lwd = 1.5)
plot(inv_warp_lm, main = "Landmark Inverse Warps \n (Peak only)", ylab = "",
     points = FALSE, col = alpha_palette, lwd = 1.5)
abline(0, 1, lty = 3)
plot(tf_aligned(reg_lm), main = "Landmark Registered\n (Peak only)", col = alpha_palette, lwd = 1.5)
plot(tf_aligned(reg_lm), main = "Landmark Registered\n (Peak only)", type = "lasagna", col = hcl.colors(12, rev = TRUE))

plot.new()
plot(inv_warp_lm2, main = "Landmark Inverse Warps \n (Start + Peak + End)", ylab = "",
     points = FALSE, col = alpha_palette, lwd = 1.5)
abline(0, 1, lty = 3)
plot(tf_aligned(reg_lm2), main = "Landmark Registered\n (Start + Peak + End)", col = alpha_palette, lwd = 1.5)
plot(tf_aligned(reg_lm2), main = "Landmark Registered\n (Start + Peak + End)", type = "lasagna", col = hcl.colors(12, rev = TRUE))

## ----pinch-compare-srvf-cc, eval = have_srvf && have_cc, class.source = "fold-show"----
reg_srvf <- tf_register(pinch_small, method = "srvf")
inv_warp_srvf <- tf_inv_warps(reg_srvf)
reg_cc_unpen <- tf_register(pinch_small, method = "cc", max_iter = 10, nbasis = 10, crit = 1)
inv_warp_cc_unpen <- tf_inv_warps(reg_cc_unpen)
reg_cc_pen <- tf_register(pinch_small, method = "cc", lambda = 1e-4, max_iter = 20)
inv_warp_cc_pen <- tf_inv_warps(reg_cc_pen)

## ----pinch-compare-srvf-cc-summary, eval = have_srvf && have_cc, class.source = "fold-show"----
summary(reg_cc_unpen)
# ... ouch! max slope almost 100 and only 40% amplitude variance reduction ....

summary(reg_srvf)
summary(reg_cc_pen)

## ----pinch-compare-srvf-cc-plot, eval = have_srvf && have_cc, fig.width = 6, fig.height = 6----
layout(t(matrix(1:12, 4, 3)))
par(cex.main = 0.8)
plot.new()
plot(inv_warp_srvf, main = "SRVF Inverse Warps", ylab = "",
     points = FALSE, col = alpha_palette, lwd = 1.5)
abline(0, 1, lty = 3)
plot(tf_aligned(reg_srvf), main = "SRVF Registered",
     col = alpha_palette, lwd = 1.5, points = FALSE)
plot(tf_aligned(reg_srvf), main = "SRVF Registered",
     type = "lasagna", col = hcl.colors(12, rev = TRUE))

plot(pinch_small, main = "Original",
     col = alpha_palette, lwd = 1.5)
plot(inv_warp_cc_unpen, main = "CC Inverse Warps (unpen.)", ylab = "",
     points = FALSE, col = alpha_palette, lwd = 1.5)
abline(0, 1, lty = 3)
plot(tf_aligned(reg_cc_unpen), main = "CC Registered (unpen.)",
     col = alpha_palette, lwd = 1.5, points = FALSE)
plot(tf_aligned(reg_cc_unpen), main = "CC Registered (unpen.)",
     type = "lasagna", col = hcl.colors(12, rev = TRUE))

plot.new()
plot(inv_warp_cc_pen, main = expression("CC Inverse Warps (" * lambda * " = 1e-4)"),
     ylab = "", points = FALSE, col = alpha_palette, lwd = 1.5)
abline(0, 1, lty = 3)
plot(tf_aligned(reg_cc_pen), main = expression("CC Registered (" * lambda * " = 1e-4)"),
     col = alpha_palette, lwd = 1.5, points = FALSE)
plot(tf_aligned(reg_cc_pen), main = expression("CC Registered (" * lambda * " = 1e-4)"),
     type = "lasagna", col = hcl.colors(12, rev = TRUE))

## ----growth-prep, class.source = "fold-show"----------------------------------
growth <- tf::growth |> dplyr::filter(gender == "female")

# Raw velocity via finite differences -- noisy, only 30 midpoints from 31 measurements:
growth$raw_vel <- tf_derive(growth$height)

# Smooth velocity via spline representation on a much denser grid, then derive analytically:
growth$smooth_vel <- tfb(
  growth$height,
  k = 15,
  bs = "tp",
  arg = seq(1, 18, l = 80),
  global = TRUE
  # family = gaussian(link = "log") # ensures positivity of velocity
) |>
  tf_derive()

## ----growth-raw-vs-smooth, eval = have_srvf, include = have_srvf, class.source = "fold-show"----
# SRVF on raw (noisy) velocity:
reg_raw_obj <- tf_register(growth$raw_vel, method = "srvf")
inv_warp_raw <- tf_inv_warps(reg_raw_obj)
reg_raw <- tf_aligned(reg_raw_obj)

# SRVF on smooth velocity:
reg_smooth_obj <- tf_register(growth$smooth_vel, method = "srvf")
inv_warp_smooth <- tf_inv_warps(reg_smooth_obj)
reg_smooth <- tf_aligned(reg_smooth_obj)

## ----growth-raw-vs-smooth-plot, eval = have_srvf, include = have_srvf, fig.width = 6, fig.height = 4----
reg_brks <- range(c(tf_evaluations(reg_raw), tf_evaluations(reg_smooth))) |>
  (\(x) seq(x[1], x[2], l = 13))()

layout(t(matrix(1:8, 4, 2)))
par(cex.main = 0.8)

plot(growth$raw_vel, main = "Raw Velocity (finite diff.)",
     xlab = "Age [years]", ylab = "cm/year", lwd = 0.8, ylim = c(0, 30))
plot(inv_warp_raw, points = FALSE, main = "SRVF Inverse Warps (raw)",
     xlab = "Chronological Age", ylab = "Registered Age", lwd = 0.8)
abline(0, 1, lty = 3)
plot(reg_raw, main = "SRVF Registered (raw)",
     xlab = "Registered Age [years]", ylab = "cm/year", lwd = 0.8, ylim = c(0, 30))
plot(reg_raw, main = "SRVF Registered (raw)",
     type = "lasagna", col = hcl.colors(12, rev = TRUE), breaks = reg_brks)

plot(growth$smooth_vel, main = "Smooth Velocity (tfb deriv.)",
     xlab = "Age [years]", ylab = "cm/year", lwd = 0.8, ylim = c(0, 30))
plot(inv_warp_smooth, points = FALSE, main = "SRVF Inverse Warps (smooth)",
     xlab = "Chronological Age", ylab = "Registered Age", lwd = 0.8)
abline(0, 1, lty = 3)
plot(reg_smooth, main = "SRVF Registered (smooth)",
     xlab = "Registered Age [years]", ylab = "cm/year", lwd = 0.8, ylim = c(0, 30))
plot(reg_smooth, main = "SRVF Registered (smooth)",
     type = "lasagna", col = hcl.colors(12, rev = TRUE), breaks = reg_brks)

## ----growth-penalization, eval = have_srvf, include = have_srvf, class.source = "fold-show"----
# Penalized SRVF on the raw velocity:
reg_raw_pen_obj <- tf_register(growth$raw_vel, method = "srvf", lambda = 0.1)
inv_warp_raw_pen <- tf_inv_warps(reg_raw_pen_obj)
reg_raw_pen <- tf_aligned(reg_raw_pen_obj)

## ----growth-penalization-plot, eval = have_srvf, include = have_srvf, fig.width = 6, fig.height = 3----
layout(t(matrix(1:4, 4, 1)))
par(cex.main = 0.8)
plot(growth$raw_vel, main = "Raw Velocity",
     xlab = "Age [years]", ylab = "cm/year", lwd = 0.8)
plot(inv_warp_raw_pen, points = FALSE,
     main = expression("Penalized Warps (raw, " * lambda * " = 0.1)"),
     xlab = "Chronological Age", ylab = "Registered Age", lwd = 0.8)
abline(0, 1, lty = 3)
plot(reg_raw_pen,
     main = expression("Penalized Registered (raw, " * lambda * " = 0.1)"),
     xlab = "Registered Age [years]", ylab = "cm/year", lwd = 0.8)
plot(reg_raw_pen, main = "Penalized Registered (raw)",
     type = "lasagna", col = hcl.colors(12, rev = TRUE), breaks = reg_brks)

## ----growth-landmarks, class.source = "fold-show"-----------------------------
# End of rapid infant growth: 
#    less than 2/3 of max early childhood growth (1-3) velocity before age 5
growth_slows <- growth$smooth_vel |> tf_zoom(begin = 1, end = 5) |> 
  tf_where(value < 0.66 * max(value[1:10]), return = "first") |> unlist()

# Pubertal peak: maximum velocity in age 8--17
growth_peaks <- growth$smooth_vel |> tf_zoom(begin = 8, end = 17) |> 
  tf_where(value == max(value)) |> unlist()

# Pre-pubertal trough: minimum velocity between age 5 and 1 year before the peak
growth_troughs <- growth$smooth_vel |> tf_zoom(begin = 5, end = growth_peaks - 1) |> 
  tf_where(value == min(value)) |> unlist() 

# Build landmark matrix and register:
growth_lm <- cbind(slowdown = growth_slows, trough = growth_troughs, peak = growth_peaks)
reg_lm_obj <- tf_register(growth$smooth_vel, method = "landmark", landmarks = growth_lm)
inv_warp_lm <- tf_inv_warps(reg_lm_obj)
reg_lm <- tf_aligned(reg_lm_obj)

## ----growth-landmarks-plot, fig.width = 6, fig.height = 3---------------------
if (!exists("reg_brks")) {
  reg_brks <- range(c(tf_evaluations(growth$smooth_vel), tf_evaluations(reg_lm))) |>
    (\(x) seq(x[1], x[2], l = 13))()
}
layout(t(1:4))
plot(growth$smooth_vel, main = "Smooth Velocity",
     xlab = "Age [years]", ylab = "cm/year", lwd = 0.8)
plot(inv_warp_lm, points = FALSE, main = "Landmark Inverse Warps",
     xlab = "Chronological Age", ylab = "Registered Age", lwd = 0.8)
abline(0, 1, lty = 3)
plot(reg_lm, main = "Landmark Registered",
     xlab = "Registered Age [years]", ylab = "cm/year", lwd = 0.8)
plot(reg_lm, main = "Landmark Registered",
     type = "lasagna", col = hcl.colors(12, rev = TRUE), breaks = reg_brks)

## ----restore-par, include = FALSE---------------------------------------------
par(oldpar)

