Skip to content

tfd_mv / tfb_mv classes for vector-valued functions f: R -> R^d#233

Open
fabian-s wants to merge 171 commits into
mainfrom
claude/jolly-planck-nvXkZ
Open

tfd_mv / tfb_mv classes for vector-valued functions f: R -> R^d#233
fabian-s wants to merge 171 commits into
mainfrom
claude/jolly-planck-nvXkZ

Conversation

@fabian-s

@fabian-s fabian-s commented May 28, 2026

Copy link
Copy Markdown
Contributor

Implementation of vector-valued functional data — curves into $\mathbb{R}^d$, e.g. movement trajectories — addressing #18 ("multivariate evaluations (curves)") and #27 (movement data, à la Joo et al. 2019). What began as a prototype is now a fairly complete feature: regular + irregular sampling, the full vctrs/tidyverse surface, registration up to elastic shape space, multivariate FPCA, and a worked vignette. Package version bumped to 0.4.2; R CMD check is clean.

Design

Composition approach: a tf_mv vector of length n bundles d univariate tf vectors (one per output dimension) as an attribute, with a custom vec_proxy / vec_restore pair (data-frame-of-components proxy). Almost every method falls out of mapping the existing univariate code over the d components — very few new numeric kernels. Both tfd and tfb representations work, regular + irregular sampling work, and per-component differing arg grids are accommodated. The three candidate designs (composition vs. matrix-valued evaluations vs. long/stacked) are compared in attic/design/multivariate.md.

Core classes & API

  • New classes tfd_mv / tfb_mv (parent tf_mv); the tf_mv ⊂ tf inheritance contract is made explicit.
  • Full vctrs integration: subset, c(), casting, ptype2, tibble columns, vec_ptype_abbr / vec_ptype_full.
  • Constructors from list-of-tf, list-of-matrices, 3-d [curve, arg, component] array, and long/wide data frames.
  • Accessors: tf_ncomp, tf_components, tf_component(<-), $ / $<-, tf_arg, tf_evaluations, tf_count, is.na.
  • [ returns a [curve, arg, component] array (classes for surfaces / images #18's "array-valued j") with a component= selector (incl. multi-component selection); matrix-index (curve, arg) pairs return one row per pair × d columns.
  • Component-wise arithmetic, Math, Summary, mean / median / sd / var, == / !=.
  • tfb_mv accepts per-component basis specs via component-named list ... args (e.g. k = list(x = 5, y = 12)).
  • Delegated verbs: tf_rebase, tf_derive, tf_integrate (definite → n × d matrix; indefinite → tf_mv), tf_smooth, tf_zoom.
  • Interop: as.matrix[curve, arg, component] array; as.data.frame(unnest = TRUE) full-outer-joins on (id, arg) (mixed reg/irreg components handled); long/wide schemas.
  • Plotting: plot.tf_mv with "facet" (one panel per component) or "trajectory" (d == 2); print/format header reports per-component value ranges.

Registration & alignment — a 4-rung ladder

A single shared time-warp is estimated per curve and applied jointly to all components, via tf_register() / tf_estimate_warps() / tf_warp() / tf_align() with accessors tf_aligned(), tf_inv_warps(), tf_template().

  • Rung 1 — tf_reparam_arclength(): constant-speed (parametrization-only) reparametrization.
  • Rung 2 — method = "cc": warp from a single 1-d reference signal (ref_component, "norm" for the pointwise Euclidean norm, or a custom function(tf_mv) → tf); also "affine", "landmark", and per-component "srvf".
  • Rung 3 — method = "srvf_mv": true multivariate elastic (Fisher–Rao / SRVF) registration that aligns the joint (hip, knee, …) trajectory using all components at once, with the multivariate Karcher-mean template.
  • Rung 4 — tf_register_shape(): full elastic shape registration — warp + rotation + scale — landing in a centered, normalized shape space; tf_rotations() / tf_scales() expose the estimated rotations and template-relative scale factors.

(srvf / srvf_mv / shape registration use the fdasrvf package, in Suggests.)

Multivariate FPCA — tfb_mfpc() (new)

  • Happ & Greven (2018) MFPCA: run the univariate FPCA per component, then combine the per-component scores into a single shared score vector per curve with vector-valued eigenfunctions $\Psi_m: \mathbb{R} \to \mathbb{R}^d$. The result is an ordinary tfb_mv whose components are tfb_fpc objects sharing identical scores, so reconstruction / printing / plotting work via the existing machinery.
  • Component weighting: "inverse_variance" (default), "snr", "equal", or a numeric vector; separate univariate (uni_pve) and multivariate (pve / npc) truncation.
  • New tfd_mv data is projected onto a fitted basis (joint re-scoring) via tf_rebase() / vec_cast().
  • Accessors tf_mfpc_scores(), tf_mfpc_efunctions(); predicate is_tfb_mfpc().

Geometry primitives

tf_norm, tf_speed, tf_inner, tf_distance, tf_tangent, tf_reparam_arclength, and tf_arclength (method = "polyline" default, or "derive"; definite and indefinite modes). Generalized to work on univariate tf too.

Vignette & docs

  • attic/vector-valued-functions.Rmd: two real-data case studies (tf::gait; dplyr::storms as 4-d (long, lat, wind, pres)), the 4-rung alignment ladder with a shape-space quotient demo, and FPC + MFPCA sections — with a proper literature bibliography (attic/references.bib).
  • pkgdown reference index updated (converters-mv, tf_register_shape, tfb_mfpc, …); design doc and prototype history live under attic/.
  • NEWS.md updated for 0.4.2.

Status

  • R CMD check: 0 errors / 0 warnings / 0 notes (tf 0.4.2).
  • Full test suite passes (0 failures), covering mv classes, vctrs/ptype/cast, verbs, geometry, registration (incl. srvf_mv and shape), MFPCA, and tibble/dplyr/tidyr interop. Relevant files: test-tfd-mv, test-tfb-mv, test-mv-vctrs, test-mv-methods, test-mv-verbs, test-mv-edge, test-mv-geom, test-mv-contract, test-mv-tidyverse, test-register-mv-srvf, test-mfpc.
  • All of the original pre-merge TODOs are resolved: end-to-end devtools::check(), srvf registration on tf_mv (now a first-class multivariate method + tests), a real-data gait walkthrough (the vignette), and the design-doc tone (now an internal attic/ record with the user-facing vignette alongside).

Future work (out of scope; tracked in attic/design/multivariate.md)

  • shared-basis tfb_mv (one basis system across components, single basis_matrix + d coefficient vectors);
  • further geometry: tf_curvature, tf_frenet, tf_rotate / tf_translate / tf_affine, tf_project, tf_is_closed, tf_self_intersection;
  • dispersing the remaining R/*-mv.R methods into the existing per-topic files.

🤖 Generated with Claude Code

claude added 15 commits May 27, 2026 16:52
Introduces a prototype representation for multivariate-output functional
data (curves into R^d, e.g. movement trajectories), addressing issues
#18 and #27. Uses a composition design: a tf_mv vector bundles d
univariate tf vectors (one per output dimension) and delegates all
numeric work to the existing univariate machinery, so both tfd and tfb
representations and regular/irregular sampling are supported with no new
numeric kernels.

- new classes tfd_mv / tfb_mv (parent tf_mv) built on vctrs::new_vctr
- custom vec_proxy/vec_restore (data-frame-of-components proxy) plus
  component-wise vec_ptype2/vec_cast for full vctrs compatibility
  (subset, c(), casting, tibble columns)
- constructors from lists of tf vectors / matrices, 3-d arrays and long
  data.frames; accessors tf_ncomp/tf_components/tf_component + $ sugar
- component-wise arithmetic, Math/Summary, mean/median/sd/var, ==/!=
- [ evaluation returns a [curve, arg, component] array (issue #18's
  array-valued j) with a component= selector; facet and trajectory plots
- design/multivariate.md compares the candidate approaches
- tests for construction, vctrs, brackets and methods

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Adds component-wise tf_mv methods for the remaining univariate verbs:
tf_rebase (so tfd_mv<->tfb_mv conversion via tf_rebase works), tf_derive,
tf_integrate (definite -> n x d matrix; indefinite -> tfd_mv), tf_smooth
and tf_zoom.

Registration is handled specially: a vector-valued curve shares one time
axis, so tf_estimate_warps.tf_mv estimates a single warp per curve from a
univariate registration signal (default: the first component; ref_component
can select another component, "norm" for the pointwise Euclidean norm, or a
custom function) and tf_warp.tf_mv / tf_align.tf_mv apply that shared warp
to every component. tf_register then composes unchanged, yielding a
tf_registration whose registered/template are tf_mv and whose warps are
univariate. Adds tests in test-mv-verbs.R and notes in design/multivariate.md.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
The previous all.equal comparison across component args was implicitly
detecting "all components are regular with the same shared grid"; check
that directly via is_irreg() instead. Same observable behaviour, clearer
intent.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Two refinements to how tf_mv accommodates irregular data:

1. new_tf_mv no longer rejects components with differing domains. By
   default it takes the union as the mv domain and widens each
   component to match (warnings about the widening are suppressed; the
   widening is intentional). Users can supply an explicit `domain` to
   tfd_mv() as long as it contains every component's observed range.
   This fixes the common case where independent irregular sampling
   yields components whose auto-derived domains differ by floating-
   point amounts.

2. tf_arg.tf_mv now collapses to a single per-curve list (length n)
   when every component is irregular AND the per-curve args agree
   across components -- the canonical "movement data with irregular
   timestamps" shape, where reporting two redundant copies was
   misleading. Per-component shapes still emerge for genuinely
   differing arg structures.

Tests cover the auto-union, user-supplied-domain, out-of-range-domain
and arg-collapse cases.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Adds an "Irregularity cases" table to design/multivariate.md covering the
four qualitatively different shapes a tf_mv can have (fully regular;
per-curve shared across components; per-component grid; per-(curve,
component) grids), what tf_arg() and tf_evaluations() return in each, and
explicitly acknowledges the storage redundancy in case 1 as the cost of
the composition design. Also updates the internal-layout description to
reflect that new_tf_mv() unions differing component domains by default
rather than rejecting them, and refreshes the files list.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Captures four follow-up items in design/multivariate.md:
- convenience verbs to regularize tf_mv args across components or entries,
- shared-basis tfb_mv,
- multivariate FPCA (MFPCA) as a first-class tfb_mv subclass,
- a proper vignette with real-data case studies (e.g. gait).

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Notes the dispersal target for each *.tf_mv method (e.g. [.tf_mv into
brackets.R, registration methods into register.R, calculus methods into
calculus.R, etc.) once the feature stabilizes, while keeping the core
constructors and shared mv helpers together.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
- tfb_mv.list short-circuits the empty-list case explicitly so the returned
  prototype is tfb_mv rather than tfd_mv (the all(map_lgl(empty, is_tf))
  check is vacuously TRUE, which previously routed empty input into
  new_tf_mv() with the default tfd_mv class).
- tfb_mv.list for non-tf input forwards ... to tfd_mv() only (not also to
  the subsequent tfb_mv.tf_mv() call), so user-supplied arg/domain are
  consumed once.
- New test-mv-edge.R covers gaps surfaced by covr::package_coverage:
  empty prototype, n=1 / d=1, NA-curve propagation through ops/subset,
  Summary group generic (sum/min/max), var/sd, unary minus and the
  incompatible-op error path, tfb_mv.list (all-tf and non-tf branches),
  c(tfb_mv, tfb_mv) and ptype_abbr/full for tfb_mv, tfd_mv re-evaluation
  on a new grid, tf_rebase with an mv basis_from, tf_evaluate direct
  call, as.matrix(arg=) and as.data.frame both modes, ref_component =
  "norm" registration, tf_component<- adding components and length-
  mismatch rejection.

mv code coverage: 63.9% -> 83.1% (the remainder is print/plot/format
visual code). Full suite 1387/1387, zero regressions.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Replace "R^d" in the print header with the actual per-component
evaluation ranges joined by " x " (e.g.
"tfd_mv<d=2>[4] (x, y): [0, 1] -> [-2.19, 1.75] x [-8.51, 10.93]"),
matching how the univariate print.tf header shows the range of f.
Empty d=0 prototype keeps "R^0".

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
…nents

Previously the unnest path assumed every component had the same long-form
(id, arg) rows and assigned them side-by-side; that failed with a row-count
mismatch when mixing tfd_reg + tfd_irreg components (or any two components
with different arg structures). Build each component's long data.frame
independently and merge() them with all = TRUE on (id, arg); components
without an observation at a given (id, arg) get NA in their column. For
already-aligned components the result is the same shape as before.

Adds a "mixed regular/irregular components work across the API" test
exercising construction, accessors, subset, c(), arithmetic, and the
joined as.data.frame.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Previously tfb_mv(f, k = 10) shared a single ... across every component
(same k, bs, sp, etc. for all dimensions); users wanting different specs
had to pre-build each component with tfb() and wrap with tfb_mv.list().
Now any ... argument that is a list named by component names is
distributed per-component, while everything else stays shared:

  tfb_mv(f, k = list(x = 5, y = 15), bs = "tp")

fits component x with k = 5 and component y with k = 15, both with
bs = "tp". A list whose names do not match the component names is
treated as a shared argument value (back-compatible).

This is "per-component basis spec, independent fits" -- distinct from
the still-TODO "one basis shared across components" item.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
For f: [a,b] -> R^d the arc length is integral_a^b ||f'(t)|| dt. The
implementation is pure composition of existing verbs -- tf_derive.tf_mv for
per-component differentiation, sqrt(Reduce("+", map(., ^2))) for the
pointwise Euclidean norm of the derivative, then tf_integrate -- so no new
numeric kernels.

Signature mirrors tf_integrate (arg, lower, upper, definite, ...):
  definite = TRUE  (default) -> numeric vector of total lengths per curve
  definite = FALSE           -> univariate tfd giving the cumulative
                                arc length s(t) = integral_a^t ||f'(u)|| du

Tests in test-mv-verbs.R: unit-circle total length (~ 2*pi), vectorised
batch (k-loop -> 2*pi*k), partial integration via lower/upper, definite
vs indefinite mode, and a 3-d helix (2*pi*sqrt(1 + c^2)).

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Picks up the per-component-basis-spec @details I added to tfb_mv and adds
tf_arclength() to the @family tf_mv-class cross-reference block on the
four sibling man pages -- both should have been committed alongside the
corresponding R changes; the stop hook caught the omission.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Three changes:

1. Add tier-1 geometric helpers in R/mv-methods.R (all are 1-3 lines of
   composition over existing univariate Ops/Math + tf_derive/tf_warp):
     tf_norm(f)     -- pointwise ||f(t)||  as univariate tfd
     tf_speed(f)    -- pointwise ||f'(t)|| as univariate tfd
     tf_inner(f, g) -- pointwise <f, g>    as univariate tfd
     tf_distance(f, g) -- pointwise ||f - g|| as univariate tfd
     tf_tangent(f)     -- unit tangent f' / ||f'|| as tf_mv
     tf_reparam_arclength(f) -- re-parametrize curve at constant speed
   Also refactors mv_registration_signal's "norm" branch to use tf_norm.

2. tf_arclength now defaults to a polyline (sum-of-segments) method
   rather than the derive+integrate composition. Polyline computes the
   sum of Euclidean lengths of segments between consecutive sample
   points in R^d, evaluating each component on each curve's grid (the
   union across components/curves when those differ). This avoids the
   compounding error of numerical differentiation followed by
   quadrature on raw tfd_mv data; the derive method is kept available
   via method = "derive" for analytic (tfb) settings or custom
   tf_integrate forwarding. New tests confirm polyline beats derive on
   the unit-circle benchmark.

3. design/multivariate.md gains a tier-2/tier-3 TODO list including
   tf_curvature, tf_frenet, tf_rotate/translate/affine, tf_project,
   tf_is_closed, tf_self_intersection, tf_align_rigid, and
   tf_landmarks_extrema.tf_mv.

Tests (test-mv-geom.R, test-mv-verbs.R additions): tf_norm on a
constant (3,4)->5 vector, tf_speed = tf_norm o tf_derive, tf_inner
dot-product identity, tf_distance(f,f) = 0, unit-circle tangent has
unit norm, tf_reparam_arclength of f(t) = (t^2, 0) gives g(0.5) =
(0.5, 0) at speed 1, polyline vs derive accuracy comparison.

Full suite 1435/1435.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
tf_mv columns work end-to-end in the tidyverse pipeline via the existing
vctrs proxy/restore plumbing -- this commit just locks that in with
asserted tests rather than relying on smoke checks.

Covers: tibble column construction & printing, dplyr::filter (incl. with
a tf_mv-derived predicate), mutate (scalar reductions like tf_arclength,
tfd reductions like tf_speed, in-place tfd_mv transforms like 2*path),
summarize (mean(path) -> length-1 tfd_mv), group_by + summarize, arrange,
slice, bind_rows, left_join, pull, distinct, tidyr::nest / unnest round
trip, and rowwise mutate.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.49086% with 342 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.51%. Comparing base (2b4eeb5) to head (75121b7).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
R/assertions.R 64.87% 98 Missing ⚠️
R/register-mv.R 87.61% 40 Missing ⚠️
R/tfb-mfpc.R 88.09% 40 Missing ⚠️
R/geometry-mv.R 89.09% 24 Missing ⚠️
R/tfd-mv.R 86.75% 20 Missing ⚠️
R/registration-class.R 86.71% 17 Missing ⚠️
R/print-format-mv.R 78.87% 15 Missing ⚠️
R/register.R 48.14% 14 Missing ⚠️
R/tfd-class.R 84.84% 10 Missing ⚠️
R/fwise.R 82.97% 8 Missing ⚠️
... and 16 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #233      +/-   ##
==========================================
+ Coverage   85.58%   88.51%   +2.92%     
==========================================
  Files          36       51      +15     
  Lines        4365     6876    +2511     
==========================================
+ Hits         3736     6086    +2350     
- Misses        629      790     +161     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

claude and others added 10 commits May 28, 2026 15:17
Two roxygen mismatches caused R CMD check (and thus pkgdown) to fail on
every CI runner:

1. tf_rebase.tf_mv had `@rdname tf_mv-methods`, which appended its
   \usage line (with formals `object, basis_from, arg, ...`) to the
   tf_mv-methods Rd page; those args were undocumented there.
   Move the explanatory @details block onto the tf_ncomp roxygen and
   leave tf_rebase.tf_mv with a bare @export so it stays attached to
   its own generic's Rd.

2. The @param list declared `f, x` but no \usage line on the Rd uses
   `x` (the `$` accessor is exported separately and not aliased to
   this page). Drop `x` from @param.

Also tidies the @details to remove the contradiction between "by
default from the pointwise Euclidean norm" and the immediately
following "the registration signal is, by default, the first
component" -- only the latter is true.

R CMD check now reports only environment-induced messages (locale
warning, blocked CRAN, missing fda/fdasrvf/refund Suggests, and the
pre-existing `fdasrvf` Rd xref NOTE). Full suite 1474/1474.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
pkgdown evaluates `_pkgdown.yml` `contents:` entries as R expressions, so
a topic name with a hyphen (`tf_mv-methods`) gets parsed as the
subtraction `tf_mv - methods` and the build aborts. Renames the topic
to `tf_mv_methods` (underscore) -- four `@rdname` directives in
R/mv-methods.R, one `_pkgdown.yml` entry, and the generated Rd file
(now man/tf_mv_methods.Rd, the stale hyphenated one is removed).

Two more pkgdown errors surfaced from the topic-vs-alias mismatch on
the first roxygen block of a multi-function topic: the Rd's \name and
the implicit \alias both default to the first @export'ed object, so
`tf_mv_methods` and `tf_geom` weren't registered as aliases. Adds an
explicit `@name tf_mv_methods` / `@name tf_geom` to each topic's first
block so the topic name resolves as an alias.

Adds `tf_geom` and `tf_arclength` to the "Vector-valued functional
data" section of `_pkgdown.yml` so all new reference pages are
included in the site index (pkgdown errors out on missing topics).

Local pkgdown::build_reference() now completes without errors; full
test suite 1474/1474.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
…check

R-release / R-devel R CMD check flags `pkg::fn` references where `pkg`
is not declared in DESCRIPTION Imports or in NAMESPACE importFrom
("'::' or ':::' imports not declared from:"). plot.tf_mv and
lines.tf_mv use graphics::par, graphics::lines, and grDevices::n2mfrow;
only graphics::lines was implicitly imported (older code paths).
Add explicit @importFrom directives on the lines.tf_mv roxygen block
so roxygen2 emits the required NAMESPACE entries.

Reproduced and fixed locally: R CMD check now reports only the
environment-induced WARNING (locale) and the two pre-existing NOTEs
(fdasrvf Rd xref, missing CRAN Suggests in the sandbox).

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Two more R CMD check "'::' or ':::' imports not declared from:"
warnings:

1. test-mv-tidyverse.R uses tibble::tibble and tidyr::nest/unnest.
   R CMD check scans test files regardless of skip_if_not_installed(),
   so the suggesting packages need to be declared. Add tibble and
   tidyr to DESCRIPTION Suggests.

2. plot.tf_mv's roxygen had a [plot.tfd][tf::plot.tf] cross-reference
   that self-qualifies the host package; R CMD check treats that as
   an undeclared self-import. Use [plot.tf()] instead -- pkgdown and
   help() both resolve it cleanly.

Local R CMD check is clean modulo the environment-induced WARNING
(locale) and the pre-existing NOTEs (fdasrvf Rd xref, missing CRAN
Suggests in the sandbox). Full suite still 1474/1474.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Address review findings and add missing functionality for the
vector-valued (tfd_mv / tfb_mv) classes:

- Fix Reduce-based ops on zero-component objects: ==, tf_norm, tf_inner
  now return zero-length results instead of NULL / erroring.
- tf_reparam_arclength leaves zero-length (constant) curves unchanged
  with a clear warning instead of producing NaN warps.
- tf_count(tfb_mv) aborts with an informative message.
- Trajectory plots: recycle per-curve graphical params (col/lty/lwd via
  matlines), default to "trajectory" for d == 2, and evaluate components
  on a common grid so mixed / irregular grids no longer error.
- Add [<-.tf_mv (component-wise replacement; supports NA assignment and
  casting) and names<-.tf_mv (curve names round-trip through subset / c()).
- print.tf_mv reports per-component gridpoints + interpolator (tfd) or
  basis spec (tfb), collapsing when components agree.
- tfd_mv docs: drop GitHub-issue references, add examples for the list,
  matrix, array and data.frame constructors.
- Regression tests for all of the above.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make tf_norm/tf_inner/tf_tangent S3 generics so the pointwise geometric
primitives also work on univariate tfd/tfb (norm = |f|, inner = f*g,
tangent = f'/|f'|), with .default methods that emit informative cli
errors. tf_speed/tf_distance generalize for free via the now-generic
tf_norm. tf_mv inherits from "tf", so the .tf_mv methods stay selected
for vector-valued input.

Add input validation to user-facing tf_mv functions:
- assert_tf_mv() helper
- check_component_index() for tf_component()/tf_component<-(), rejecting
  out-of-range, fractional, multi-element, NA and logical selectors;
  fixes a crash where a length>1 character selector hit `&&` coercion
- max_iter/tol checks in tf_estimate_warps.tf_mv()
- lower<=upper / finite limits in tf_arclength.tf_mv()
- explicit non-tf_mv rejection in tf_inner.tf_mv()

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
devtools::document() re-rendered all help topics with the locally
installed roxygen2 8.0.0 (Config/roxygen2/version bumped from 7.3.3),
which also reflows existing man pages to the newer link syntax. Fix the
tf_component<- multi-length-selector test to match the checkmate
"length 1" assertion message.

R CMD check: 0 errors | 0 warnings | 0 notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude added 3 commits May 31, 2026 10:34
- New pkgdown article walks through tfd_mv/tfb_mv end-to-end on the
  built-in gait data and on Atlantic hurricane tracks from dplyr::storms,
  covering construction, accessors, plotting (facet + trajectory),
  arithmetic/summaries, geometric primitives, basis fitting, and dplyr
  integration.
- tf_arclength.tf_mv: per-curve clamp to the intersection of [lower,
  upper] with each curve's observed argument range, so irregular curves
  that don't span the global domain return their actual path length
  instead of erroring on NA paired evaluations. Fix cli plural marker on
  the now-defensive abort path. Regression test added.
- plot.tf_mv (trajectory mode): honour user-supplied xlab/ylab via
  modifyList (prev: "matched by multiple actual arguments"); accept an
  alpha argument and apply it via grDevices::adjustcolor, matching
  plot.tf semantics.
- Wire the article into _pkgdown.yml and ignore built HTML/artefacts
  under vignettes/articles/. Add knitr/rmarkdown to Suggests for
  building.
pkgdown's vignette index keys nest articles under `articles/<slug>`, so
the bare `vector-valued-functions` entry in `_pkgdown.yml#articles` did
not match any known topic and broke `navbar_articles()` during the
site build.
Article (vignettes/articles/vector-valued-functions.Rmd)
- Reframed around concrete analytical questions instead of an API tour.
- Gait: pointwise mean+/-sd envelope; min/max arc-length subjects;
  variance-share / RMSE for an FPC basis; phase alignment via
  tf_register(method = "cc", ref_component = "hip"); unit-speed
  reparameterization via tf_reparam_arclength.
- Storms: project (long, lat) into per-storm local-km coordinates so
  tf_arclength reports kilometres and tf_speed reports km/h (deg/h
  artefactually overweights northward motion via the cos(lat) shrink
  of a longitude degree). Map by peak Saffir-Simpson category;
  path-length boxplot vs intensity; forward-speed time courses split
  TS/TD vs Cat 4+; tfb_mv smoothing of the longest 6 tracks.

R/mv-geom.R
- tf_reparam_arclength: out[good] <- tf_warp(...) failed with a vctrs
  ptype mismatch when tf_warp upgraded tfd_reg -> tfd_irreg.  Build
  the output by ptype-common vec_c() of warped + untouched curves
  followed by an index reordering instead of in-place subassign.

R/mv-plot.R
- plot.tf_mv(type = "facet"): prefer mfrow = c(1, d) for d <= 3 (the
  typical "small multiples in a row" layout fits standard figure widths
  without "figure margins too large"); fall back to n2mfrow for larger d.
fabian-s and others added 30 commits June 11, 2026 11:42
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Fix: drop unresolvable \link{} targets in tf_mv_unimplemented docs (urgent, CI red)
Hygiene B: code cleanup — globals, nocov, cli, internalize (#260)
PR #281 made prep_plotting_arg internal and deleted its Rd topic, but
left the reference to it in _pkgdown.yml — pkgdown's reference-index
build aborts with 'must be a known topic name or alias' (the topic no
longer exists).

Fixes the pkgdown CI failure introduced by #281.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Hygiene C: dedup backlog — NA helper, cum dispatcher, factories (#260)
Fix: drop orphan prep_plotting_arg entry from _pkgdown.yml (urgent, pkgdown CI red)
Split exported tf_landmarks_extrema from internal helpers. The shared
@Rdname plus @Keywords internal previously caused R CMD check WARNING
because non-exported detect_landmarks/cluster_landmarks/build_landmark_matrix
appeared in \usage. Marked helpers @nord.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Without an explicit @name on the first-collated block (vctrs-mv.R),
roxygen named the Rd \name{vec_ptype2.tfd_mv.tfd_mv}, breaking
reference/vctrs.html. Pin @name vctrs on the first collated block.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
- Correct gait reference: data is in fda, not datasets (and the dataset
  is 39 children, not 39 boys).
- Fix growth gender description: factor levels are female/male, not
  boy/girl.
- Note in @Format that height/knee_angle/hip_angle are tfd columns.
- Fix grammar typo "Data is also include" -> "Data is also included".

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Replace "as usual" / "dots" placeholders with proper one-line param
descriptions; they were rendering literally in man/tfmethods.Rd.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
"an tf object" -> "a tf object". Replace the cryptic "for the x-axis...!"
@param y description with a clear note that y is the evaluation grid
passed through as arg, not y-axis values.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
These topics previously shipped without examples, which is unhelpful
for discovery and breaks the rendered "Examples" sections on pkgdown.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
coef.tfb cleared attributes via attributes(object) <- NULL, which also
stripped any user-set names. Save and restore names so coef(x) follows
the same naming contract as the rest of the methods. Add a regression
test in tests/testthat/test-names.R.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
After 78a256b, coef.tfb returns a named list. Those names propagated
through evaluate.R into per-element results, producing named numeric
vectors in tf_evaluate output. test-evaluator.R:66 caught it: unlist()
of the concatenated single-element results disambiguated duplicate
inner names to "1.1","2.2","3.3", breaking equality with the bulk
call.

Strip names at the two internal call sites (the pmap branch and the
length(arg)==1 cbind branch) so internal computation is name-agnostic
while coef(x) still preserves names at the user level.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
The docs state empty strings are replaced with "NA" before
deduplication, but the substitution only ran for inputs that were
already character vectors, so factor or other inputs coercing to ""
kept empty names. Move the substitution after as.character() so the
documented behavior holds for all inputs, and add a regression test
for factor input.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Replace the single use of mvtnorm::rmvnorm in tf_rgp() with a direct
Cholesky factorization. Falls back to a small diagonal jitter when the
covariance is numerically singular (e.g. squared-exp kernel with zero
nugget), matching what mvtnorm's eigen-based default did implicitly.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Replace the single use of pracma::savgol in R/smooth.R with an inline
re-implementation (~15 lines) based on Savitzky & Golay (1964): build
the centered Vandermonde-style design matrix, take its SVD pseudoinverse
(matching pracma::pinv's tolerance), then linearly convolve and trim
fc samples from each end to recover the input length -- identical
end-effect handling to pracma::savgol.

Filter coefficients are cached per (fl, forder, dorder) so repeated
tf_smooth() calls reuse them. Numerical output is bit-identical to
pracma::savgol across the tested combinations.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
The previous chol()+jitter sampler added full-rank noise to near-singular
GP kernels, changing the effective rank of `tf_rgp()` draws compared to
the original `mvtnorm::rmvnorm(method = "eigen")` implementation. This
broke `test-tfb-fpc.R:75`, where the default `pve` cutoff began dropping
components that previously contributed to reconstruction.

Replace with a direct eigen-based sampler using the symmetric square root
`V D^{1/2} V^T` of the covariance. Negative eigenvalues from numerical
noise are clamped to zero, so rank-deficient kernels are sampled in their
effective subspace. Draws now match the prior `mvtnorm`-based output
bit-for-bit under `set.seed()`.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
The project pins roxygen via `Config/roxygen2/version: 8.0.0`;
`devtools::document()` had injected a duplicate `RoxygenNote: 7.3.1`.

NEWS: document the `tf_rgp()` sampler change and the drop of `mvtnorm`
and `pracma` from Imports.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Address copilot review on #283:
- Validate fl as integer scalar (rejects fl = 2.5), forder/dorder as
  non-negative integers, and require dorder <= forder and fl > forder
  (prevents indexing error in .savgol_coefs())
- Qualify convolve() as stats::convolve() to avoid R CMD check note
- Fix stale chol() comment in test-rgp.R (sampler is eigen-based)

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
PR #278 (mfpc invariants) introduced new_tfb_fpc_demoted() which calls
mfpc_quad_weights(arg). PR #282 (hygiene dedup) consolidated the four
quadrature-weight implementations into a single trapezoid_weights()
helper and removed mfpc_quad_weights. The two PRs were merged without
detecting the semantic conflict, so all mfpc demote paths now abort
with 'could not find function mfpc_quad_weights'.

One-line fix: call trapezoid_weights() (same math, just the new name).

Fixes 9 test failures in test-mfpc.R covering arithmetic on demoted
tfb_mfpc, $<- demote, and chained Math/Ops.

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Fix: use trapezoid_weights() in new_tfb_fpc_demoted (urgent, CI red)
Hygiene D: drop mvtnorm and pracma (#260)
PR #280 added an @examples block to man/fpc_wsvd.Rd that calls
fpc_wsvd(data, ...) at the user level, but the generic itself was
never exported -- only its .matrix and .data.frame methods carried
@export tags (registered via S3method() in NAMESPACE). R CMD check
--as-cran runs the examples in a fresh session that only sees
exported symbols, so 'could not find function fpc_wsvd' aborts the
check with 2 ERRORs.

One-line @export on the generic + matching export(fpc_wsvd) in
NAMESPACE (hand-added; no document() to avoid the roxygen drift
churn on ~40 Rd files).

https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Fix: export fpc_wsvd generic (urgent, CI red — 2 ERRORs in R CMD check)
- tf_fwise(), tf_fmean(), tf_fvar(), tf_fsd() gain tf_mv methods
  returning component-wise (n x d) matrices
- tf_interpolate(), tf_sparsify(), tf_jiggle() gain component-wise
  tf_mv methods; same_arg = TRUE keeps shared component grids
- [.tf_mv supports multi-component subsets via component = c(...)
- tfb_mfpc: preserve/reorder the mfpc spec across component renaming
- export prep_plotting_arg() as a developer tool; landmark docs
  reorganized under tf_landmarks_extrema

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Joint conditions across components, referenced by name
(e.g. tf_where(f, x > 0 & y < 1)); per-component use remains available
via f[, component = ...]. Closes part of the tf_mv verb gap (#255).

- using `value` in a condition aborts with classed error
  tf_mv_where_value and a hint listing the component names
- components on different grids abort with tf_mv_incommensurate_args
  (interpolate first, or supply a single numeric `arg`); the
  per-component list returned by tf_arg() on such objects is rejected
  since it would be misread as per-curve grids
- "arg" is now a reserved component name: it would silently shadow the
  grid column in evaluation data.frames
- shared workhorse tf_where_impl() no longer uses subset(), whose NSE
  would let component names capture local variables; logical indexing
  preserves subset()'s recycling and NA semantics
- return = "range" on zero-length input now returns an empty
  begin/end data.frame instead of erroring

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants