tfd_mv / tfb_mv classes for vector-valued functions f: R -> R^d - #233
Merged
Conversation
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 Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #233 +/- ##
==========================================
+ Coverage 85.58% 89.65% +4.06%
==========================================
Files 36 51 +15
Lines 4365 7211 +2846
==========================================
+ Hits 3736 6465 +2729
- Misses 629 746 +117 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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>
2 tasks
- 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.
…review) A curve finite at the subset-freezing iteration can still produce a non-finite objective later (e.g. alignment pushes it outside the evaluable range). mean() over the frozen subset then goes NA and the is.finite(obj) guard silently disables best-tracking and the worsening check for the rest of the run. Fix: obj averages over the currently-finite kept curves; the worsening comparison uses the pairwise-finite kept subset so both means cover the SAME curves within each comparison -- the only comparability the same-template check needs (na.rm on each side separately would reintroduce the changing-subset incomparability #265 removed). https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
- tf_depth.tf_mv returns a named numeric(0) for zero-length input (previously errored in the vapply/matrix reshaping) - depth_median_index treats non-finite depths as -Inf so a partially-NA curve can't poison the tie count; aborts informatively when no finite depth exists - median.tf_mv keeps its documented length-1 contract on empty (post-filter) input via vec_init, consistent with summary/fivenum https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
tf_mv verbs: quantile + points implemented, tf_invert permanently stubbed (#255 follow-up)
Multivariate depth: weighted componentwise MBD, joint median, tf_order (#273)
The test ran for the first time on CI (fdasrvf absent in the dev container, so it always skipped locally) and failed on a pure names-attribute mismatch: aligned_arclen carries curve names, the expected side was unname()d. Values agree. https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
The standalone Rd topic added in #297 was missing from _pkgdown.yml; pkgdown aborts on unlisted topics. https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
Fix wave-fallout CI red: tf_scales test names + median.tf_mv pkgdown index
R CMD check requires method arguments to match the generic's (x, arg, depth, na.rm, ...) in order, with method-specific extras after them. The weights argument added in #297 sat before na.rm, which failed 'checking S3 generic/method consistency' across the whole check matrix once the test red from #298 no longer masked it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M1QMfji5MpKJvzJYw5Kjb9
The #241 fix gave empty tfd prototypes the sentinel domain c(NA, NA), but every consumer still compared domains with NA-unsafe arithmetic, so vec_c()/vec_rbind()/c() involving tfd() crashed with "missing value where TRUE/FALSE needed" -- a regression vs 0.4.1 for any workflow binding onto an initially empty tfd column. This treats the sentinel as a wildcard everywhere: - get_larger_domain()/domain_contains()/assert_same_domains() defer to the known domain; ptype2/cast methods short-circuit on empty prototypes so no spurious irreg-casts or basis comparisons happen (mgcv would abort on empty grids). - tfb_spline/tfb_fpc/tf_mv empty prototypes now carry the same sentinel as tfd (was the old c(0, 0)), and validate_tf accepts it for prototypes. - new_tf_mv() unions component domains NA-safely; per review, widening a component's domain now warns for tfd components and aborts for tfb components (basis evaluation outside the fitted range extrapolates, i.e. fabricates values). - new_tfd() recognizes all-NA input arriving as NULL placeholders (tfd(list(NA_real_)) crashed on range(integer(0))). - Empty-input hardening in the same cluster: [.tf with matrix = TRUE returns a 0-row matrix instead of crashing in colnames<-, no spurious interpolate-warning for length-0 input, tf_mfpc_scores(m[0]) returns a 0 x npc matrix. New regression tests in test-empty-prototypes.R; existing tests updated to expect the new widening warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…component args Four review findings on the component-wise verb layer: - tf_fmax()/tf_fmin()/tf_fmedian() on tf_mv returned a flattened length-n*d vector with NA-padded names (curve-major interleaving of components); they now return the same n x d matrix as tf_fmean()/tf_fvar()/tf_fsd(). - Summary.tf_mv built one shared NA mask across all mv operands, which recycled masks across operands of different (recyclable) lengths and dropped the wrong curves; each operand is now completed with its own mask (mv_missing() removed as dead code). - The loud MFPC demotion that vec_arith/Math already performed is now centralized in map_components()/map2_components()/imap_components(), so tf_smooth()/tf_derive()/tf_zoom()/tf_integrate()/mean()/sd()/var()/ Summary on a tfb_mfpc no longer silently produce a tfb_mv whose components carry the unusable per-component scoring stub -- they warn once and return valid demoted tfb_fpc components instead. - tf_derive.tf_mv()/tf_integrate.tf_mv() now accept per-component `arg` lists via the existing tf_mv_component_arg() helper, consistent with tf_interpolate() and the fwise summaries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MFPCA (review findings on tfb-mfpc.R): - tfb_mfpc() and tfb_fpc() abort informatively on completely missing curves instead of crashing with "subscript out of bounds" in df_2_mat() (partially missing evaluations continue to use soft-impute SVD). - df_2_mat() keeps factor-id row alignment, so curves whose rows are all incomplete stay row-aligned instead of shifting every later curve up. - Re-scoring new data (tf_rebase()/vec_cast onto an MFPCA basis) maps completely missing curves to NA scores and NA entries -- previously the scoring silently zero-weighted them into ~0 scores; tf_mfpc_scores() returns NA rows for such entries instead of dropping them. - tfb_mfpc.list()/default() forward constructor arguments (arg, domain) to tfd_mv() explicitly; they used to stay in `...` and collide with the `arg` the univariate FPCA method already receives. Constructors (tfb-mv.R / tfd-mv.R): - tfb_mv() on an existing tfb_mv with an explicit `basis` re-fits instead of silently returning the old basis; a change of basis kind converts through tfd() (the univariate tfb re-fitters assume same-kind attributes and crash on cross-kind input). - tfb_mv.list()/default() partition `...` into constructor (arg, domain) vs basis arguments instead of leaking both into both calls. - tfd_mv.array() no longer collapses length-1 curve/arg margins (drop = TRUE lost the curve dimension of n x 1 slices). - "id" is now a reserved component name like "arg": a component named id silently collided with the curve-id column in long-format conversions (found via tidyfun's tf_unnest()). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… boundaries Three review findings on the registration layer: - tf_register() on tf_mv inputs whose components live on different grids failed the univariate arg assertion when re-evaluating the shared inverse warps; they are now represented on the sorted union of the component grids. - tf_register_shape() unconditionally overwrote a user-supplied template with the mean of the aligned curves; tf_template() now returns exactly the supplied template, consistent with tf_register(method = "srvf_mv"). - The srvf_mv/shape machinery accepted d = 1 and template-free n = 1 input and then crashed deep inside fdasrvf: single-component srvf_mv now delegates to univariate elastic registration, shape registration requires >= 2 components, and template-free single-curve registration is rejected with an explanation at the validator boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tf_arg() on a tf_mv keeps its univariate-matching polymorphic contract (shared numeric grid, per-curve list, or per-component list), but internal consumers no longer disambiguate the list shapes by comparing names -- which is ambiguous when the curve count equals the component count with colliding names. tf_mv_curve_grids() and tf_register() now recompute the layout from the components via mv_args_shared(). Also documents the deliberate deviation of tf's vec_cast() methods from the strict vctrs cast invariant: casts re-express in the target's representation but keep the source's arg-grid (decided in review -- the alternative silently coarsens data during dplyr-style type unification). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review findings on the geometry/calculus layer: - tf_arclength(method = "derive") always passed its default lower/upper (the full domain) down to tf_integrate(), defeating the per-curve default-limit rescue for irregular curves that do not span the domain -- every such curve silently returned NA. Limits are now only forwarded when the user supplied them. - Indefinite tf_integrate() on irregular tfd inserted user-supplied limits with strict comparisons, so a limit that float-mismatches a grid point (0.3 vs seq()'s 0.30000000000000004) created an (almost-)duplicate arg value and thus an invalid tfd that aborts on any later use. Limits (and polyline arc-length endpoints) now snap to (almost-)coinciding grid points via the grid's own resolution. - tfd_mv.array() dimension fix follow-up: preserve curve names, which the matrix() reshape dropped (caught by the shape-registration tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Bump to 0.5.0: the mv extension is the largest feature addition since 0.3, and tidyfun's Depends will pin tf (>= 0.5.0). - NEWS.md: document the pre-release-review contract changes (reserved component names, domain-widening warn/error policy, loud MFPC demotion, n x d freduce results, NA-curve aborts in fpc/mfpc) and bug fixes. - cran-comments.md: note the mlr3fda revdep finding (test asserting the pre-#241 all-NA collapse; maintainers have the fix staged upstream). - Reattach the tfbrackets roxygen block to its topic: the helper functions inserted above [.tf had captured it, so document() generated a spurious tf_bracket_i.Rd and deleted tfbrackets.Rd. - Document the new arg/domain formals of tfb_mv()/tfb_mfpc(); regenerate NAMESPACE (matpoints/points imports) and Rd files; Rbuildignore .codex. R CMD check: 0 errors, 0 warnings, 1 note (local html-tidy tooling only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- extract all_equal_to_first() helper; let mv_args_shared() accept precomputed per-component args - hoist tf_arg() extraction out of the per-curve loop in tf_mv_curve_grids() (was O(n^2 d) for irregular components) - tf_evaluations.tf_mv() delegates to tf_evaluate.tf_mv(), whose no-arg branch is the same contract (drops n*d throwaway bracket data.frames) - pass a shared numeric caller arg through tf_evaluate.tf_mv() as-is instead of expanding to a length-n list (keeps the univariate tfb single-matrix fast path) - assemble_mv_evals(): build each per-curve data.frame in one data_frame0() call instead of growing it column-by-column - tf_bracket_j(): vec_unique() the per-curve grids before the union sort - [.tf_mv: replace the per-component interpolate vector with a scalar -- components are constructor-guaranteed same-kind, so it was constant - tf_count.tf_mv: use is_tfb_mv() and drop the dead is.null(mat) branch - drop no-op names(comps) <- comp_names after purrr maps over the (always-named) components Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
map_components()/map2_components() already demote tfb_mfpc operands (with the same warn-once logic); forwarding the operation name via .op makes the per-method pre-demotion boilerplate redundant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- new_tf_mv(): reuse the already-computed all_tfb flag in the domain-widening abort instead of re-mapping is_tfb per component - tfd_mv.data.frame(): factor the thrice-repeated column-selector resolution into a local col_nm() helper - tfd_mv.tf_mv(): delegate to build_components() like the other constructor methods - distribute_dots(): delegate to tf_mv_component_arg(), which implements the identical per-component-list dispatch rule - drop no-op names(comps) <- comp_names after maps over components Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- tf_tangent.tf_mv(): compute the inverse speed directly from the single set of derivative evaluations instead of evaluating the derivative a second time through tf_norm() + re-evaluation of the speed tfd - tf_reparam_arclength(): emit the degenerate-curve warning once, use plain vec_c() (which performs the same ptype_common/cast internally) - arclength_polyline(): lengths() idiom, drop guards unreachable after tf_evaluate() filled the >= 2-point grids, reuse all_equal_to_first() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- format.tf_mv(): one vectorized paste() instead of an n x d scalar-indexing double loop - as.data.frame.tf_mv(unnest = TRUE): build the long/wide columns in one pass instead of rbind-ing n (or n x d) per-curve data.frames; the final (id, arg[, component]) sort makes the construction order irrelevant, so results are identical Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The freduce factory handled tf_mv via an is_tf_mv() branch inside its closures, while the adjacent tf_fmean/tf_fvar/tf_fsd use UseMethod() with explicit .tf_mv methods. Align with that pattern (and with the documented rule that tf_mv behaviour comes only from explicitly registered .tf_mv methods); results are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tail of new_tfb_fpc_demoted() duplicated the tfb_fpc assembly of new_tfb_fpc_shared() verbatim; delegate instead so the attribute contract lives in one place. Also drop a no-op names() reassignment after map2 over the named components. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- tf_register_srvf(): reuse srvf_mv_gamma_to_warps() for the fdasrvf gamma rescaling + endpoint pinning (#242 logic existed twice) - tf_warp.tfd()/tf_align.tfd(): extract the thrice-repeated domain-carrying re-evaluation (#266) into retfd_keep_domain() and hoist it out of tf_align's duplicated if/else tails - tf_register_shape(): reuse srvf_mv_validate_template() with a new scalar_only flag instead of restating its checks inline Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
validate_tfb_spline()/validate_tfb_fpc() shared ~50 identical lines of basis/arg/coefficient checks differing only in the class name shown in messages; factor them into validate_tfb_common(). Remove the dangling "Spec phrased this as ..." development note from validate_tf_mv(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- drop redundant names(comps) <- comp_names after purrr maps over the always-named components (calculus-mv, depth, rng) - tfd_numeric_op(): iterate evaluations in fixed order and swap operands only at the do.call, instead of swapping map2's inputs twice - mv-stubs.R: collapse a section header that only recorded where removed stubs went Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Components on differing argument grids can now be exported two ways: "union" (default, previous behavior) evaluates every component on each curve's union grid, interpolating inside the observed range -- the right pairing for trajectory-style consumers; "component" evaluates each component strictly on its own grid (or `arg`), fabricating nothing at args a component was not observed at (absent rows in the long schema, NA cells in the wide one) -- the faithful tabular export that tidyfun::tf_unnest() provides, which can now delegate here instead of maintaining a parallel outer-join implementation. Both settings agree for shared grids. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… vctrs docs
Sweep from the cran-submission checklist: replace non-ASCII characters
(em-dashes, arrows, approx signs, accented names) in code comments added on
this branch -- they pass R CMD check locally but the CRAN pretest builds the
PDF manual with LaTeX and chokes on non-ASCII in sources ("Matérn" in the
tf_rgp() docs stays: it shipped in 0.4.1's CRAN-built manual). Add the two
missing @examples (median.tf_mv, the vctrs methods page); datasets carry
\format as intended, urlchecker reports all URLs correct, and the code-policy
sweep (T/F literals, options/par hygiene, :::, tempdir) found nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 checkis clean.Design
Composition approach: a
tf_mvvector of lengthnbundlesdunivariatetfvectors (one per output dimension) as an attribute, with a customvec_proxy/vec_restorepair (data-frame-of-components proxy). Almost every method falls out of mapping the existing univariate code over thedcomponents — very few new numeric kernels. Bothtfdandtfbrepresentations 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 inattic/design/multivariate.md.Core classes & API
tfd_mv/tfb_mv(parenttf_mv); thetf_mv ⊂ tfinheritance contract is made explicit.c(), casting, ptype2, tibble columns,vec_ptype_abbr/vec_ptype_full.tf, list-of-matrices, 3-d[curve, arg, component]array, and long/wide data frames.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-valuedj") with acomponent=selector (incl. multi-component selection); matrix-index(curve, arg)pairs return one row per pair ×dcolumns.Math,Summary,mean/median/sd/var,==/!=.tfb_mvaccepts per-component basis specs via component-named list...args (e.g.k = list(x = 5, y = 12)).tf_rebase,tf_derive,tf_integrate(definite →n × dmatrix; indefinite →tf_mv),tf_smooth,tf_zoom.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.plot.tf_mvwith"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 accessorstf_aligned(),tf_inv_warps(),tf_template().tf_reparam_arclength(): constant-speed (parametrization-only) reparametrization.method = "cc": warp from a single 1-d reference signal (ref_component,"norm"for the pointwise Euclidean norm, or a customfunction(tf_mv) → tf); also"affine","landmark", and per-component"srvf".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.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 thefdasrvfpackage, in Suggests.)Multivariate FPCA —
tfb_mfpc()(new)tfb_mvwhose components aretfb_fpcobjects sharing identical scores, so reconstruction / printing / plotting work via the existing machinery."inverse_variance"(default),"snr","equal", or a numeric vector; separate univariate (uni_pve) and multivariate (pve/npc) truncation.tfd_mvdata is projected onto a fitted basis (joint re-scoring) viatf_rebase()/vec_cast().tf_mfpc_scores(),tf_mfpc_efunctions(); predicateis_tfb_mfpc().Geometry primitives
tf_norm,tf_speed,tf_inner,tf_distance,tf_tangent,tf_reparam_arclength, andtf_arclength(method = "polyline"default, or"derive"; definite and indefinite modes). Generalized to work on univariatetftoo.Vignette & docs
attic/vector-valued-functions.Rmd: two real-data case studies (tf::gait;dplyr::stormsas 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).converters-mv,tf_register_shape,tfb_mfpc, …); design doc and prototype history live underattic/.NEWS.mdupdated for 0.4.2.Status
R CMD check: 0 errors / 0 warnings / 0 notes (tf 0.4.2).srvf_mvand 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.devtools::check(),srvfregistration ontf_mv(now a first-class multivariate method + tests), a real-datagaitwalkthrough (the vignette), and the design-doc tone (now an internalattic/record with the user-facing vignette alongside).Future work (out of scope; tracked in
attic/design/multivariate.md)tfb_mv(one basis system across components, singlebasis_matrix+dcoefficient vectors);tf_curvature,tf_frenet,tf_rotate/tf_translate/tf_affine,tf_project,tf_is_closed,tf_self_intersection;R/*-mv.Rmethods into the existing per-topic files.🤖 Generated with Claude Code