Skip to contents

This vignette provides access to the registration workflows from tf and places them in the tidyfun site for convenience. It is aimed at users who need to understand templates, warps, and diagnostics rather than at casual end users exploring the core wrangling and plotting features.

Registration: Template, Warp, Domain

Registration aligns functions in time by reducing phase variation (timing differences - “horizontal” variability) while preserving amplitude variation (shape/height differences - “vertical” variability).

Use registration if:

  • Curves have broadly similar shapes that are shifted, stretched or compressed relative to each other
    • and your analysis does not care about these timing differences
    • or you want to analyze these timing differences separately from shape differences.
  • You can interpret both the target template and the resulting warps in domain terms.

Do not use registration as a (default) preprocessing step if:

  • Curves come from clearly different shape classes/clusters (see below).
  • Timing differences are meaningful for your question and/or cannot be viewed in isolation from shape differences.
  • The time deformation from registration creates implausible subject-specific timelines.

Core problem:

Registration models observed curves \(x_i(t)\) as time-deformed versions of a template \(m(s)\), where the time deformation is given by a monotone warping function \(h_i(s)\):

\[ x_i(t) \approx m(h_i^{-1}(t)) \]

Key interpretation:

  • \(h_i\) maps aligned/system time to observed time.
  • \(h_i^{-1}\) maps observed time to aligned time.
  • Slope of \(h_i^{-1}\) greater than 1 indicates local compression in observed time (events happen faster), slope less than 1 indicates local dilation (events happen slower).

Core workflow in tf:

# 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)

What Is the Template?

Template choice is the most important modeling choice in registration: every estimated warp is relative to that template.

Default template behavior in tf_register() / tf_estimate_warps():

Method Default template behavior How to override
srvf Karcher-type mean shape1 estimated iteratively by fdasrvf pass template = ...
cc arithmetic mean curve (estimated iteratively) pass template = ...
affine arithmetic mean curve pass template = ...
landmark column-wise mean of landmark locations pass template_landmarks = ...

Practical rules:

  1. If you need interpretable warpings relative to a known reference, supply an explicit template.

  2. Template and data need to be on the same domain and grid for srvf and cc.

  3. Always visualize raw curves and the template together before trusting warps.

  4. Under strong phase variation, the pointwise mean can be a poor template — see Unsuitable template in the Pitfalls section for an example and remedy.

What is a Warping Function?

tf_estimate_warps() returns forward warps \(h_i\) (aligned → observed time), while tf_inv_warps(reg) returns the inverse warps \(h_i^{-1}\) (observed → aligned time) that are directly used for alignment. These are the natural functions to inspect and plot — they show how each curve’s observed timepoints map to aligned “system” time.

  • Warping functions close to the identity line imply there is little phase correction.
  • Local slopes different from 1 imply local time dilation (\(\tfrac{d}{dt} h_i^{-1}(t) < 1\): \(s\) increases more slowly than \(t\)) or compression (\(\tfrac{d}{dt} h_i^{-1}(t) > 1\): \(s\) increases more quickly than \(t\)) of the observed function relative to the template function.
  • Large departures from identity or frequent slope changes and crossings of the identity line without clear alignment gain indicate over-warping risk.

Domain-Preserving vs Non-domain-preserving Warps

This distinction is fundamental for interpretation and edge behavior:

  • Domain-preserving: warps map domain endpoints to themselves. Registration using the srvf, cc, and landmark methods in tf is domain preserving.
  • Non-domain-preserving: warps can shift or scale outside (or into a subset of) the observed domain. Only available with the affine method in tf.

Implications:

  1. Non-domain-preserving warps can produce boundary NAs after tf_align() because parts of the aligned timeline have no observed support.
  2. Domain-preserving warps avoid these boundary losses but can still overfit timing.
  3. Method choice is therefore not only about alignment quality; it is also about what time transformation is scientifically admissible.

The figure below illustrates how warping functions compose with a template to produce shifted/deformed curves. Note that this shows the forward simulation view, \(m(h(s))\): applying a warp to generate an observed curve from the template. In practice, registration works in the inverse direction — estimating \(h_i^{-1}\) to map an observed curve back to system time (i.e., to align it to a template).

  • Note that the affine shift warp is NOT domain-preserving (it shrinks the domain down to [0.1, 0.9]), while the smooth and wiggly elastic warps ARE domain-preserving (they start at 0 and end at 1).
  • The flatter the warping function, the more it compresses time and the steeper the more it dilates the reference “system” time of the template.
  • Where the warping function \(h(s)\) is above the identity line, the template \(m(s)\) is ahead of the warped function \(x(t) = m(h(s))\) , where it is below, the template is behind the warped function.

Example: Shift Registration with Explicit Template

This minimal example uses simple shift registration, i.e. the warping model assumes \(h_i(s) = s + b_i\) (no elastic deformation of the domain, simply shifts the functions’ arguments). We pass an explicit template to make the target shape transparent:

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"
)
#> Warning:  128 evaluations were `NA`
#>  Returning irregular <tfd>.
#>  128 evaluations were `NA`
#>  Returning irregular <tfd>.
reg_shift
#> <tf_registration>
#> Call: tf_register(x = pinch_small, type = "shift", template = template_affine, 
#>     method = "affine")
#> 10 curves on [0, 0.3]
#> Components: aligned, inv_warps, template, original data
summary(reg_shift)
#> tf_register(x = pinch_small, type = "shift", template = template_affine, 
#>     method = "affine")
#> 
#> 10 curve(s) on [0, 0.3]
#> 
#> Amplitude variance reduction: 97.1%
#> 
#> Inverse warp deviations from identity (relative to domain length):
#>     0%    10%    25%    50%    75%    90%   100% 
#> 0.0002 0.1027 0.1350 0.1875 0.2027 0.2090 0.2469 
#> 
#> Inverse warp slopes (1 = identity):
#>   overall range: [1, 1]
#>   per-curve slopes:
#>   0%  10%  25%  50%  75%  90% 100% 
#>    1    1    1    1    1    1    1 
#> 
#> Domain coverage loss after alignment (fraction of original range):
#>     0%    10%    25%    50%    75%    90%   100% 
#> 0.0067 0.0547 0.0733 0.0967 0.1067 0.1087 0.1267
pinch_inv_warp_shift <- tf_inv_warps(reg_shift)
pinch_small_reg <- tf_aligned(reg_shift)
pinch_template_shift <- tf_template(reg_shift)

Quick success checks:

  • The template is plausible for the population you are aligning.
  • Major features (e.g., peaks) are more aligned after registration.
  • Inverse warps are not excessively far from the identity line for this problem.
  • Boundary NAs are limited and scientifically acceptable.
  • Resulting timing shifts are domain-plausible.

Choosing a Registration Method

srvf (default) cc affine landmark
Use when Smooth, non-linear timing differences with shared shape family You want continuous-criterion alignment and can tune criterion Mostly shift/scale timing variability; need interpretable warps Reliable, repeated landmarks are available across curves
Avoid when Very noisy data, sparse grids, highly heterogeneous shapes Complex phase-amplitude interactions or severe irregularity Strong non-linear timing deformation Ambiguous/noisy landmarks or mismatched counts
Template handling Implicit Karcher-type mean unless template supplied Arithmetic mean unless template supplied Arithmetic mean unless template supplied Uses template_landmarks (default: column means)
Key arguments method = "srvf", lambda method = "cc", crit = 1/2, nbasis, lambda, conv, iterlim method = "affine", type, bounds method = "landmark", landmarks, template_landmarks
Input grid regular only regular only regular + irregular regular + irregular
Noise robustness Low (pre-smooth if noisy) High (most stable) Moderate Moderate; outlier-robust
Typical speed Fast (but O(n2) in grid) Moderate Fast Very fast
Typical failure signs Unstable warps, over-warping, inconsistent reruns Sensitive to criterion choice, weak alignment gains Residual misalignment of local features, boundary NAs under stronger shifts/scales Forced/broken alignments from bad landmarks
First fallback Pre-smooth inputs; try affine or landmark Change criterion, basis dimension and/or amount of penalization; try srvf or affine Set stricter/looser bounds; upgrade to srvf Check appropriateness of landmarks; use srvf or affine

The srvf and cc methods require regular grids (tfd_reg or tfb). The affine and landmark methods also accept irregular grids (tfd_irreg).

Evidence-Based Method Selection

Based on benchmarks across 15 data-generating processes, 3 noise levels, and 5 methods:

  1. Reliable landmarks available? → Use landmark. Fastest, simplest warps, and most robust to outlier contamination (warp MISE degrades only ~1.6× at 30% contamination vs 2.6–3.7× for iterative methods).
  2. Mostly shift/scale variation? → Use affine. Fast, interpretable, but produces boundary NAs.
  3. Noisy data? → Use cc (criterion 2) as a stable default — the most noise-robust method in our benchmarks. Or use srvf after pre-smoothing: tfb(x, k = 25) |> tf_register(method = "srvf").
  4. Suspected outlier contamination (>10%)? → Use landmark if possible. Otherwise, remove outliers before registration.
  5. Clean data, complex phase variation? → Use srvf. Best overall warp recovery on clean data with domain-preserving warps, but rankings shift with noise level and template shape.
  6. Always compare ≥2 methods and inspect diagnostics (see Diagnostics Workflow below). No single method dominates across all conditions.

See Theoretical Background below for method-specific mathematical details.

Pitfalls and Likely Failure Modes

Structured shape variation

Issue: Registered curves still form distinct shape clusters.

Remedies: Stratify first; register within clusters; compare cluster-specific results.

Registration assumes a single shared template. If curves come from different shape families, a single registration will try to compromise and may distort curves from both groups.

Noise dominates phase signal

Issue: Warps become jagged, wiggly or highly variable across near-identical curves.

Remedies: Smooth inputs (more) first; reduce basis flexibility; compare before/after sensitivity.

In our benchmarks2, CC methods (method = "cc") were the most noise-robust, while SRVF degraded most sharply under noise — because SRSFs involve numerical derivatives that amplify observation noise. Pre-smoothing SRVF inputs with tfb(x, k = 25) before registration reduced warp error (measured as warp MISE) by 50–70% under moderate noise. Recipe: x_smooth <- tfb(x, k = 25); reg <- tf_register(x_smooth, method = "srvf").

Sparse or irregular grids

Issue: Registration quality may be affected strongly by interpolation grid choices.

Remedies: Re-evaluate on a fine, common grid; run sensitivity checks on grid density.

The srvf and cc methods require regular grids (tfd_reg); interpolate sparse or irregular data to a common regular grid first. The affine and landmark methods also accept irregular grids (tfd_irreg). Grid density can affect results — try at least two grid resolutions to check stability. SRVF performance varies substantially with grid resolution due to numerical differentiation: grid sizes around 100 points are a robust default. Finer grids (>200 points) can degrade SRVF warp recovery under noise by amplifying derivative artifacts (warp MISE roughly doubles at grid=201 vs 101 under moderate noise). CC, affine, and landmark methods are largely grid-insensitive.

Boundary and overlap artifacts

Issue: Large NA regions after unwarping, especially for aggressive affine shifts/scales.

Remedies: Inspect boundary behavior explicitly; narrow affine bounds via shift_range/scale_range.

This issue is specific to the affine method, which is the only non-domain-preserving method in tf. The srvf, cc, and landmark methods preserve domain endpoints by construction and do not produce boundary NAs.

Over-warping

Issue: Extreme timing distortions with little gain in feature alignment.

Remedies: Prefer simpler methods (affine), reduce flexibility, or avoid registration for that subset.

This is primarily a risk with flexible methods (srvf, cc). Affine warps are constrained by design and rarely over-warp. Comparing results across methods can help detect over-warping: if a flexible method’s warps are much more variable than an affine baseline without a clear improvement in alignment, over-warping may be occurring.

Lambda penalization

Issue: You want to constrain warp flexibility, but are unsure which lambda value to use.

Remedies: For cc, lambda in range 1e-4 to 0.01 is a reasonable starting point; higher values pull warps toward the identity, reducing over-warping at the cost of alignment precision. For srvf, lambda penalization has inconsistent effects across DGPs and noise levels in our benchmarks — prefer pre-smoothing inputs (e.g., tfb(x, k = 25)) over lambda tuning. Note that optimal lambda values are problem-specific; the ranges above are derived from oracle (ex-post) analysis and have not been validated via cross-validation.

Unsuitable template

Issue: Registration produces poor alignment or increases variability because the default template does not represent the common curve shape.

Remedies: Inspect the template; supply a robust alternative such as the MBD median.

The default template for affine and cc registration is the pointwise arithmetic mean (re-estimated iteratively). When phase variation is large — curve features are spread far apart in time — the pointwise mean gets smeared out and no longer resembles any individual curve. A robust alternative is to use the most central observed curve, e.g. the curve with the highest modified band depth (MBD, see tf_depth()).

# 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)
#> Warning:  126 evaluations were `NA`
#>  Returning irregular <tfd>.
#>  126 evaluations were `NA`
#>  Returning irregular <tfd>.
reg_median <- tf_register(bumps, method = "affine", type = "shift",
                          template = bumps_median)
#> Warning:  120 evaluations were `NA`
#>  Returning irregular <tfd>.
#> Warning:  120 evaluations were `NA`
#>  Returning irregular <tfd>.

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")

The pointwise mean (top row, dashed) is smeared and not representative of any individual curve’s shape. Registration toward it produces poor alignment. The MBD median (bottom row, dashed red) is the most central observed curve — it has the correct shape, and registration aligns the peaks well. When you expect strong phase variation, inspect the default template and consider supplying a suitable custom template like template = median(x).

Practical Diagnostics Workflow

After any registration run, use summary() and plot() for a quick assessment, then dig deeper as needed:

  1. summary(reg): Check amplitude variance reduction (should be positive), aggregated warp deviations (how far from identity?), min and max warp slopes (any extreme local distortions?), and domain coverage loss (for affine warps).
  2. plot(reg): Three-panel view of original curves + template, inverse warping functions + identity line, and aligned curves + template. Check that features are more aligned and warps are plausible.
  3. Quantify alignment of specific features you care about (e.g., spread of peak locations before vs after).
  4. Compare methods: Re-run with at least one alternative method. If results disagree strongly, treat conclusions as low confidence.
  5. Check stability: Re-run after modest smoothing or grid density change.

We demonstrate this workflow below using the pinch data.

Step 1: Register and inspect summary

x <- pinch[1:10]

# Register with affine shift warps:
reg_aff <- tf_register(x, method = "affine", type = "shift_scale")
#> Warning:  48 evaluations were `NA`
#>  Returning irregular <tfd>.
#> Warning:  46 evaluations were `NA`
#>  Returning irregular <tfd>.
#>  46 evaluations were `NA`
#>  Returning irregular <tfd>.

# summary() gives a quick quantitative overview:
summary(reg_aff)
#> tf_register(x = x, type = "shift_scale", method = "affine")
#> 
#> 10 curve(s) on [0, 0.3]
#> 
#> Amplitude variance reduction: 97.9%
#> 
#> Inverse warp deviations from identity (relative to domain length):
#>     0%    10%    25%    50%    75%    90%   100% 
#> 0.0483 0.0989 0.1222 0.1478 0.1725 0.1855 0.2374 
#> 
#> Inverse warp slopes (1 = identity):
#>   overall range: [1.088, 1.287]
#>   per-curve slopes:
#>    0%   10%   25%   50%   75%   90%  100% 
#> 1.088 1.099 1.116 1.135 1.227 1.265 1.287 
#> 
#> Domain coverage loss after alignment (fraction of original range):
#>     0%    10%    25%    50%    75%    90%   100% 
#> 0.0000 0.0000 0.0000 0.0200 0.0567 0.0640 0.1000

Key things to check in the summary:

  • Amplitude variance reduction near 100% — registration captures most of the variability. A negative value would mean registration made things worse (see the Unsuitable template section above for an example of when this can happen).
  • Warp deviations are moderate (well below 0.5, in most cases) — the aggregated timing corrections per function are not too extreme.
  • Warp slopes: For non-linear methods, slopes that veer far from 1 indicate strong local time dilations or compressions.
  • Domain coverage loss shows how much of the original domain is lost per curve after alignment — relevant only for affine (non-domain-preserving) warps.

Step 2: Visual inspection via plot()

# plot() provides a 3-panel diagnostic:
plot(reg_aff)

Check that (1) the template (dashed) looks representative of the original curve shapes, (2) inverse warping functions are not too far from the identity and have no extremely flat or steep segments, and (3) aligned curves show better feature alignment to the template.

Step 3: Quantify alignment of specific features

For this example:

# 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))
)
#>                 metric       value
#> 1       sd_peak_before 0.028031728
#> 2 sd_peak_after_affine 0.002796824

Step 4: Compare with an alternative method

# Compare with SRVF (non-linear warps):
reg_srvf <- tf_register(x, method = "srvf")

# Side-by-side summaries:
summary(reg_srvf)
#> tf_register(x = x, method = "srvf")
#> 
#> 10 curve(s) on [0, 0.3]
#> 
#> Amplitude variance reduction: 96.7%
#> 
#> Inverse warp deviations from identity (relative to domain length):
#>     0%    10%    25%    50%    75%    90%   100% 
#> 0.0430 0.0745 0.0852 0.1143 0.1240 0.1362 0.1594 
#> 
#> Inverse warp slopes (1 = identity):
#>   overall range: [0.143, 7]
#>   per-curve min slopes:
#>    0%   10%   25%   50%   75%   90%  100% 
#> 0.143 0.143 0.167 0.183 0.200 0.205 0.250 
#>   per-curve max slopes:
#>   0%  10%  25%  50%  75%  90% 100% 
#>  3.0  3.9  4.0  4.5  5.0  5.2  7.0
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))
)
#>                 metric       value
#> 1       sd_peak_before 0.028031728
#> 2 sd_peak_after_affine 0.002796824
#> 3   sd_peak_after_srvf 0.001349897
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)

If different methods disagree strongly and diagnostics are unstable, treat conclusions as low confidence until you resolve data representation and model-choice sensitivity.

Theoretical Background 101

Problem is to find an optimal deformation of the function’s domain:

\[ \text{Find } h_i(s) \text{ so that } d\left(x_i(h_i(s)), m(s)\right) \to \min \]

  • \(x_i(t)\): observed curve for subject \(i\) at observed times \(t\).
  • \(m(s)\): the “template” function you register against.
  • \(h_i(s)\): warping function mapping aligned time \(s\) (“system time”) to observed time \(t\) (“wallclock time”) for the \(i\)-th curve. These are returned by tf_estimate_warps() (called internally by tf_register()), and registration methods differ in the representations and constraints for these (see below).
  • \(h_i^{-1}(t)\): inverse warping (observed time \(\to\) system time) used by tf_align() to align observed curves, can be inspected/extracted via tf_inv_warps(<tf_registration>).
  • \(d(\cdot, \cdot)\): distance or dissimilarity to minimize (e.g., \(L_2\) distance of derivatives \(\int (x'_i(h_i(s))h'_i(s) - m'(s))^2 ds)\)).

Key references: Marron et al. (2015) provide an overview of amplitude and phase variation in FDA. Ramsay & Silverman (2005, Functional Data Analysis, Ch. 7–8) cover landmark and continuous registration. Srivastava & Klassen (2016, Functional and Shape Data Analysis) develop the elastic/SRVF framework in depth.

SRVF Framework

Represents each curve via its square root velocity function (SRVF), \(q(t) = \dot{x}(t) / \sqrt{|\dot{x}(t)|}\), and aligns curves by minimizing \(L_2\) distances between (aligned) SRVFs. This corresponds to minimizing an elastic distance metric between functions modulo reparameterization (i.e. under “warping”) and transforms the non-linear alignment problem into a simpler optimization on a Hilbert sphere3. See Srivastava et al. (2011) and Tucker et al. (2013) for the fdasrvf implementation.

  • Optimization: Dynamic programming over the space of diffeomorphisms4 (via fdasrvf).
  • Template: Karcher mean on the shape manifold (iterative; see footnote in the template table above), or user-supplied.
  • Domain-preserving: Yes — warps satisfy \(h(t_\min) = t_\min\) and \(h(t_\max) = t_\max\).
  • Key arguments: method = "srvf". Pass template to override the Karcher mean. Control warp flexibility via lambda (default is 0 for unrestricted/unpenalized warps) and penalty_method ("roughness" (default), "geodesic", or "norm").
  • Strengths: On clean or low-noise data, SRVF often achieves the best warp recovery for complex phase variation. Makes no assumptions about warping function shape (except domain preservation and monotonicity). Rankings shift with noise level and template shape, so always compare with at least one alternative.
  • Weaknesses: Sensitive to noise because SRSFs involve numerical derivatives that amplify observation noise. Pre-smoothing inputs (e.g., tfb(x, k = 25)) before registration reduces warp error (measured as warp MISE) by 50–70% under moderate noise. Also grid-sensitive: avoid grids >200 points on noisy data (see Sparse or irregular grids). Shows the most variable performance under template estimation compared to other methods.

CC (Continuous Registration Criterion)

Estimates smooth monotone warping functions by maximizing the integrated cross-correlation between the derivative of each registered curve and the first eigenfunction of the derivatives’ covariance, which represents the dominant common shape of the sample (criterion crit=2, the default) or by minimizing the squared differences between aligned function and template (criterion crit=1, not recommended). In current tf, this is implemented as a tf-native dense-grid optimizer with monotone spline warps rather than the older fda::register.fd() backend. See Ramsay & Li (1998, JRSS-B) and Ramsay & Silverman (2005, Functional Data Analysis, Ch. 8).

  • Optimization: tf-native dense-grid optimization of a continuous registration criterion with monotone spline warps.
  • Template: Arithmetic mean, re-estimated over max_iter iterations (default 3), or user-supplied.
  • Domain-preserving: Yes.
  • Key arguments: method = "cc". Use crit = 2 (default) to maximize the proportion of variance explained by the first eigenfunction of the registered sample, or crit = 1 to minimize integrated squared differences. Control warp flexibility via B-spline basis dimension of the warping functions nbasis (default is 6), their wiggliness via penalty parameter lambda (default is 0 for no penalization), and optimizer tolerances via conv and iterlim. In our experience, crit = 1 without penalization tends to be considerably less reliable than crit = 2 or penalized variants — the unpenalized L2 criterion can produce strongly distorted warps.
  • Strengths: The most noise-robust and grid-insensitive method in our benchmarks. Tends to be the most stable method under template estimation. Benefits from penalization (lambda > 0) in most conditions.
  • Weaknesses: Results are sensitive to the criterion choice and the spline basis configuration.

Affine (Shift / Scale)

Models warps as linear functions \(h(s) = a \cdot s + b\), with three sub-types:

  • type = "shift" (default): \(a = 1\), only horizontal translation (\(b\) free).
  • type = "scale": only uniform time scaling (\(a\) free, \(b\) derived from centering).
  • type = "shift_scale": both parameters free.

Each curve is aligned independently via bounded L-BFGS-B optimization of the L2 distance to the template. See Ramsay & Silverman (2005, Ch. 7) and Wang & Gasser (1997) for context on shift/scale alignment models.

  • Template: Arithmetic mean, or user-supplied.
  • Domain-preserving: No — warps can shift or scale outside the observed domain, producing boundary NAs after tf_align().
  • Input grid: Supports both regular and irregular grids (tfd_reg and tfd_irreg).
  • Key arguments: method = "affine", type, shift_range and scale_range to set upper and lower limits for time shifts \(b_i\) and time scales \(a_i\).
  • Strengths: Very fast (typically an order of magnitude or more faster than SRVF), highly interpretable parameters.
  • Weaknesses: Cannot capture non-linear timing deformation. Results are often shortened functions with substantial leading and/or trailing NAs (under strong shifts/scales).

Landmark

Constructs piecewise linear warps by mapping user-specified landmark positions to target positions. No continuous optimization is performed — the warp is fully determined by the landmark correspondence. See Kneip & Gasser (1992, Annals of Statistics) and Ramsay & Silverman (2005, Ch. 7).

  • Template: Defined by column-wise means of the landmark time points matrix, or user-supplied template_landmarks.
  • Domain-preserving: Yes — domain endpoints are used as boundary anchors.
  • Input grid: Supports both regular and irregular grids (tfd_reg and tfd_irreg).
  • Key arguments: method = "landmark", landmarks (required: \(n \times k\) matrix of landmark positions), template_landmarks (optional target positions).
  • Strengths: Fast and deterministic. In our benchmarks, landmark registration was most robust to outlier contamination: warp MISE degraded ~1.6× at 30% contamination, compared to 2.6–3.7× for iterative methods.
  • Weaknesses: Requires the ability to identify reliable, consistent landmarks across all curves. Fragile when features are ambiguous and often too simplistic if curves have few identifiable features (e.g., only one peak per curve).
  • Landmark detection: tf_landmarks_extrema() can automatically detect local maxima, minima, or zero crossings and cluster them across curves - usually better to pre-smooth noisy inputs with tf_smooth() or tfb before calling this function. Results will typically be better for user-defined landmarks that are based on domain knowledge.

Worked Examples

Pinch Data: Cross-method Comparison

pinch_small <- pinch[1:10]

reg_aff <- tf_register(pinch_small, method = "affine", type = "shift_scale")
#> Warning:  48 evaluations were `NA`
#>  Returning irregular <tfd>.
#> Warning:  46 evaluations were `NA`
#>  Returning irregular <tfd>.
#>  46 evaluations were `NA`
#>  Returning irregular <tfd>.
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()
#>       [,1]  [,2]  [,3]  [,4]
#> [1,] 0.028 0.160 0.222 0.284
#> [2,] 0.040 0.136 0.226 0.284
#> [3,] 0.036 0.160 0.226 0.282
#> [4,] 0.044 0.144 0.236 0.284
#> [5,] 0.040 0.126 0.228 0.284
#> [6,] 0.038 0.164 0.224 0.284
#     ... 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())
#>        [,1]
#>  [1,] 0.082
#>  [2,] 0.144
#>  [3,] 0.082
#>  [4,] 0.144
#>  [5,] 0.090
#>  [6,] 0.080
#>  [7,] 0.114
#>  [8,] 0.134
#>  [9,] 0.140
#> [10,] 0.090
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()
#>      start         end
#> [1,] 0.042 0.082 0.148
#> [2,] 0.102 0.144 0.206
#> [3,] 0.046 0.082 0.160
#> [4,] 0.112 0.144 0.214
#> [5,] 0.056 0.090 0.154
#> [6,] 0.046 0.080 0.148
reg_lm2 <- tf_register(pinch_small, method = "landmark", landmarks = pinch_landmarks)
inv_warp_lm2 <- tf_inv_warps(reg_lm2)
layout(t(matrix(1:12, 4, 3)))
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))

Note that affine registration with shift+scale warps produces some boundary NAs because the warps are not domain-preserving, but it does a good job aligning the peaks. Landmark registration with only the peak locations does a decent job aligning the peaks without producing NAs, but it does not align the start and end of the curves well. Landmark registration with start + peak + end landmarks does a good job aligning all three features without producing NAs.

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)
#> Iterative registration stopped after 5 of 10 iterations: alignment worsened
#> (objective 0.5409 > 0.5296).
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)
#> Iterative registration stopped after 18 of 20 iterations: alignment worsened
#> (objective 0.438 > 0.4374).
inv_warp_cc_pen <- tf_inv_warps(reg_cc_pen)

SRVF and CC both use non-linear warps and tend to be able to align more complex phase variation than the affine method, but their warps are more complex and potentially harder to interpret, and registration can go off the rails more easily (as we can see here with the unpenalized cc registration plotted below, check the huge maximal warp slope and its poor amplitude variance reduction…) – using summary(<tf_registration>) for quick quantitative diagnostics:

summary(reg_cc_unpen)
#> tf_register(x = pinch_small, nbasis = 10, crit = 1, method = "cc", 
#>     max_iter = 10)
#> 
#> 10 curve(s) on [0, 0.3]
#> 
#> Amplitude variance reduction: 98.7%
#> 
#> Inverse warp deviations from identity (relative to domain length):
#>     0%    10%    25%    50%    75%    90%   100% 
#> 0.1143 0.1418 0.1530 0.1664 0.1936 0.1965 0.1968 
#> 
#> Inverse warp slopes (1 = identity):
#>   overall range: [0.282, 4.434]
#>   per-curve min slopes:
#>    0%   10%   25%   50%   75%   90%  100% 
#> 0.282 0.315 0.362 0.432 0.452 0.460 0.471 
#>   per-curve max slopes:
#>    0%   10%   25%   50%   75%   90%  100% 
#> 2.350 2.366 2.549 3.006 3.377 3.904 4.434
# ... ouch! max slope almost 100 and only 40% amplitude variance reduction ....

summary(reg_srvf)
#> tf_register(x = pinch_small, method = "srvf")
#> 
#> 10 curve(s) on [0, 0.3]
#> 
#> Amplitude variance reduction: 96.7%
#> 
#> Inverse warp deviations from identity (relative to domain length):
#>     0%    10%    25%    50%    75%    90%   100% 
#> 0.0430 0.0745 0.0852 0.1143 0.1240 0.1362 0.1594 
#> 
#> Inverse warp slopes (1 = identity):
#>   overall range: [0.143, 7]
#>   per-curve min slopes:
#>    0%   10%   25%   50%   75%   90%  100% 
#> 0.143 0.143 0.167 0.183 0.200 0.205 0.250 
#>   per-curve max slopes:
#>   0%  10%  25%  50%  75%  90% 100% 
#>  3.0  3.9  4.0  4.5  5.0  5.2  7.0
summary(reg_cc_pen)
#> tf_register(x = pinch_small, lambda = 1e-04, method = "cc", max_iter = 20)
#> 
#> 10 curve(s) on [0, 0.3]
#> 
#> Amplitude variance reduction: 97.1%
#> 
#> Inverse warp deviations from identity (relative to domain length):
#>     0%    10%    25%    50%    75%    90%   100% 
#> 0.0192 0.0586 0.0812 0.1152 0.1535 0.1709 0.1947 
#> 
#> Inverse warp slopes (1 = identity):
#>   overall range: [0.508, 1.862]
#>   per-curve min slopes:
#>    0%   10%   25%   50%   75%   90%  100% 
#> 0.508 0.550 0.592 0.782 0.823 0.855 0.878 
#>   per-curve max slopes:
#>    0%   10%   25%   50%   75%   90%  100% 
#> 1.053 1.206 1.234 1.572 1.671 1.700 1.862
layout(t(matrix(1:12, 4, 3)))

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 Data: Deeper Real-data Workflow

The Berkeley growth data contains height measurements for 39 boys and 54 girls aged 1–18. Growth velocity curves (first derivatives of height) show a prominent pubertal growth spurt whose timing varies substantially between individuals — a natural target for registration. We use the subset of girls5 from this dataset to illustrate how data representation, penalization, and landmark choice affect registration quality.

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()
#> Using global smoothing parameter `sp = 0.008` estimated on subsample of curves.
#> Percentage of input data variability preserved in basis representation
#> (per functional observation, approximate):
#> Min. 1st Qu.  Median Mean 3rd Qu.  Max.
#> 100 100 100 100 100 100

Data representation matters

The raw finite-difference velocity estimates are jagged (30 irregularly spaced points from the original measurement grid). SRVF registration on such noisy inputs tends to produce wiggly warps that chase noise rather than genuine phase variation. Converting to a smooth representation first gives SRVF cleaner input and much better results.

# 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)
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)))

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)

The top row shows extreme, biologically implausible warps (i.e. mapping chronological ages 5-8 to “registered” ages < 2) with local wiggles that are more likely to reflect noise in the raw velocity estimates rather than genuine timing differences. The bottom row, using smoother velocity curves, produces simpler and less extreme warps that capture the pubertal timing variation — the main feature of interest.

Penalization

Even on noisy inputs, SRVF penalization (lambda > 0) can help suppress over-warping. Higher lambda pulls warps toward the identity (no warping), trading less strict alignment for smoother and more subtle warps.

# 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)
layout(t(matrix(1:4, 4, 1)))

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)

Penalization smooths out the worst warp artifacts, and the result here looks more reasonable than the result for the (oversmoothed) tfb-inputs above. That said, in most cases the best strategy is likely to be to (carefully!) pre-smooth, and then to use penalization only if the warps still look too flexible.

Landmark registration

Landmark registration avoids continuous optimization entirely — it constructs piecewise linear warps from a (user-specified) correspondence of curve features. For growth velocity, natural landmarks are the end of rapid infant growth (velocity drops a lot below initial velocity), pre-pubertal trough (minimum velocity before the puberty growth spurt), the pubertal peak (maximum velocity during puberty). We identify these by searching for the minimum in age 5–(peak-1) and the maximum in age 8–17 on each smooth velocity curve.

# 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() 
#>  `tf_zoom()` was called with varying start or end points -- converting to
#>   <tfd>.

# 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)
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)

The piecewise linear warps align the pubertal peaks well, with the pubertal trough and infant growth landmarks anchoring the earlier phases. Compared to SRVF, the warps are simpler and fully interpretable — each segment directly corresponds to a physiological period (early childhood, late childhood, pubertal acceleration, post-peak deceleration). The trade-off is that landmark registration cannot adapt between landmarks (alignment between successive landmarks depends entirely on those two anchor points) and that landmark definition is often somewhat arbitrary (c.f. definition of “end of rapid infant growth” above…).

References

  • Kneip, A., & Gasser, T. (1992). Statistical tools to analyze data representing a sample of curves. Annals of Statistics, 20(3), 1266–1305. doi:10.1214/aos/1176348794
  • Marron, J.S., Ramsay, J.O., Sangalli, L.M., & Srivastava, A. (2015). Functional data analysis of amplitude and phase variation. Statistical Science, 30(4), 468–484. doi:10.1214/15-EJS1030
  • Ramsay, J.O., & Li, X. (1998). Curve registration. Journal of the Royal Statistical Society: Series B, 60(2), 351–363. doi:10.1111/1467-9868.00129
  • Ramsay, J.O., & Silverman, B.W. (2005). Functional Data Analysis (2nd ed.). Springer.
  • Srivastava, A., & Klassen, E.P. (2016). Functional and Shape Data Analysis. Springer.
  • Srivastava, A., Wu, W., Kurtek, S., Klassen, E., & Marron, J.S. (2011). Registration of functional data using Fisher-Rao metric. arXiv:1103.3817
  • Tucker, J.D., Wu, W., & Srivastava, A. (2013). Generative models for functional data using phase and amplitude separation. Computational Statistics & Data Analysis, 61, 50–66. doi:10.1016/j.csda.2012.12.001
  • Wang, K., & Gasser, T. (1997). Alignment of curves by dynamic time warping. Annals of Statistics, 25(3), 1251–1276. doi:10.1214/aos/1069362747