# tf 0.5.0

## Internal

* `tf_rgp()` no longer depends on `mvtnorm` or `pracma`. GP draws are now
  generated by a direct eigen-based sampler (the symmetric square root
  `V D^{1/2} V^T` of the covariance) that reproduces the previous
  `mvtnorm::rmvnorm(method = "eigen")` draws bit-for-bit under `set.seed()` and
  handles rank-deficient kernels (e.g. squared-exp with zero nugget) correctly.
  `tf_smooth.tfd(method = "savgol")` now inlines a small Savitzky–Golay filter
  in place of the `pracma::savgol()` dependency.

## Bug fixes

* `tf_evaluate()` no longer returns values at the wrong positions when the
  requested arg contains duplicates (#236).
* `tf_integrate(f, definite = FALSE)` for irregular `tfd` with n > 1 no longer
  crashes; the antiderivative's per-curve grids are kept as a list (#237).
* `Math.tfd()` / `Math.tfb()` now forward `...` to the underlying op so
  `round(x, digits)`, `log(x, base)`, `signif(x, digits)`, etc. honor their
  extra arguments instead of silently dropping them (#246).
* `tf_integrate()` on irregular `tfd` no longer silently returns `NA` under
  default limits; for irregular input the defaults are now each curve's own
  observed arg range (i.e., the range of its `tf_arg()` values). Pass explicit
  `lower` / `upper` (or an extrapolating evaluator) to override (#253).

## Vector-valued functional data

This release introduces first-class support for vector-valued (multivariate)
functional data -- functions whose codomain is `R^d` -- alongside the existing
univariate `tfd`/`tfb` classes.

* `tfd_mv()` and `tfb_mv()`: new `vctrs`-based S3 classes for vector-valued
  functional data, holding several component functions per observation on a
  shared domain. Constructors accept named lists of `tfd`/`tfb` vectors or
  list-columns of matrices.
* `tfb_mfpc()` implements multivariate functional principal component analysis
  (Happ & Greven, 2018) for `tf_mv` data: a single set of scalar scores per
  curve shared across all components, with vector-valued eigenfunctions.
  Component weighting is configurable (`"inverse_variance"` default, `"snr"`,
  `"equal"`, or user-supplied). New data can be projected onto a fitted basis
  via `tf_rebase()` / `vec_cast()`. Accessors `tf_mfpc_scores()`,
  `tf_mfpc_efunctions()` and the predicate `is_tfb_mfpc()`.
* Multivariate registration: `tf_register()` gains `method = "srvf_mv"` for
  jointly aligning the components of `tf_mv` curves via the multivariate SRVF
  framework, and `tf_register_shape()` provides elastic shape registration
  (rotation/translation/scale-invariant) via `fdasrvf`.
* New geometry verbs for `tf_mv` (and where meaningful for univariate `tf`):
  `tf_norm()`, `tf_inner()`, `tf_tangent()`, `tf_arclength()`.
* Component accessors (`tf_ncomp()`, `tf_components()`, `tf_component()`),
  `tf_split()` / `tf_combine()` extensions and `[`/`[[` methods for extracting,
  replacing and recombining components.
* Function-wise scalar summaries `tf_fmean()`, `tf_fvar()` and `tf_fsd()`
  return component-wise matrices for `tf_mv` objects.
* `tf_interpolate()`, `tf_fwise()`, `tf_sparsify()` and `tf_jiggle()` now have
  component-wise `tf_mv` methods; `tf_sparsify()` and `tf_jiggle()` keep shared
  component grids by default via `same_arg = TRUE`.
* `tf_where()` / `tf_anywhere()` work on `tf_mv` objects with *joint*
  conditions across components, referenced by name (e.g.
  `tf_where(f, x > 0 & y < 1)`); there is no `value` column for `tf_mv` input.
  Components must share a common grid, or `arg` must be supplied explicitly.
* `quantile()` works on `tf_mv` objects, returning the *component-wise*
  pointwise quantiles: a `tf_mv` with one curve per requested probability in
  `probs`, exactly as `quantile.tf()` does per component (`na.rm` / `probs` /
  `type` are forwarded).
* `points()` works on `tf_mv` objects, mirroring `lines.tf_mv()`: in
  `"trajectory"` mode (`d == 2`) it overlays the paired `(x(t), y(t))` points
  with per-curve graphical-parameter recycling; in `"facet"` mode it overlays
  each component onto the current device.
* `tf_depth()` gains a `tf_mv` method: a weighted componentwise aggregation of
  the (centrality) univariate depths, with a `weights` argument mirroring
  `tfb_mfpc()` (`"equal"` default, `"inverse_variance"`, or a numeric vector).
  The up-down ordering index `"MHI"` has no canonical vector-valued analogue and
  aborts with a pointer to `tf_order()` (#273).
* `median()`, `summary()` and `fivenum()` on `tf_mv` now select *observed*
  vector-valued curves via a single joint-depth index (one curve across all
  components), fixing the previous component-wise "chimera" where each component
  could come from a different observation. Pointwise entries (mean, min/max, the
  central-half range) remain component-wise (#273).
* New generic `tf_order()` with a `tf_mv` method: `tf_order(f, by = "norm")`
  (or `by = <component name>`) orders vector-valued curves by an explicit
  scalar reduction. `sort()`, `rank()` and `xtfrm()` remain permanently
  undefined for `tf_mv` -- there is no canonical total order on `R^d` -- so
  base `sort()`/`order()`/`rank()` fail fast rather than fabricate an order
  (#273).

### Contract change

* `is_tf()` now returns `TRUE` for `tf_mv` as well as univariate `tfd`/`tfb`.
  Code that branched on `is_tf()` to mean "univariate `tf`" should switch to
  the new predicate `is_tf_1d()`.
* `"arg"` and `"id"` are reserved and cannot be used as component names --
  they collide with the grid and curve-id columns of long-format conversions.
* Constructing a `tf_mv` from components with differing domains now *warns*
  when widening `tfd` components to the union domain (evaluations outside the
  observed range are `NA`) and *aborts* for `tfb` components, where basis
  evaluation outside the fitted range would extrapolate, i.e. fabricate
  values.
* Component-wise operations on a `tfb_mfpc` (`tf_smooth()`, `tf_derive()`,
  `tf_zoom()`, `tf_integrate()`, `mean()`/`sd()`/`var()`, `Summary` group
  generics) now consistently demote to valid per-component `tfb_fpc`
  representations with a one-time warning, like arithmetic already did --
  previously they silently produced objects that abort on later use.
* `tf_fmax()`, `tf_fmin()` and `tf_fmedian()` on `tf_mv` return an `n x d`
  curves-x-components matrix like `tf_fmean()`/`tf_fsd()` (previously: a
  misnamed interleaved vector).
* `tfb_fpc()` and `tfb_mfpc()` abort informatively when the input contains
  completely missing curves (partially missing evaluations continue to be
  handled by the soft-impute SVD); re-scoring new data with `NA` curves via
  `tf_rebase()` / `vec_cast()` yields `NA` scores and `NA` entries.

### Tabular export of components on differing grids

* `as.data.frame.tf_mv(unnest = TRUE)` gains a `grids` argument controlling
  where components are evaluated when they live on different argument grids:
  `"union"` (default, previous behavior) evaluates every component on each
  curve's union grid, interpolating inside the observed range; `"component"`
  evaluates each component strictly on its own grid (or on `arg`), so no
  values are fabricated where a component was not observed -- the faithful
  export used by `tidyfun::tf_unnest()`. For shared grids both agree.

## More bug fixes (pre-release review)

* Empty prototypes (`tfd()`, `tfb()`, `tfd_mv(list())` and friends) carry the
  sentinel domain `c(NA, NA)` and now combine cleanly with populated vectors
  in all `vctrs` paths (`vec_c()`, `vec_rbind()`, `c()`, casts) -- binding
  rows onto an initially empty `tf` column works again.
* `tfb_mv()` on an existing `tfb_mv` with an explicit `basis` argument re-fits
  in the requested basis instead of silently returning the old one; changing
  the basis kind converts through `tfd()`.
* `tfb_mv()`/`tfb_mfpc()` on raw lists/arrays route constructor arguments
  (`arg`, `domain`) and basis arguments to the right place instead of
  forwarding everything everywhere.
* `tfd_mv()` on 3-d arrays keeps length-1 curve/arg margins and curve names.
* `tf_derive()` and `tf_integrate()` on `tf_mv` accept per-component `arg`
  lists, consistent with `tf_interpolate()`.
* `Summary` group generics on `tf_mv` no longer recycle `NA` masks across
  operands of different lengths.
* `tf_arclength()`: `method = "polyline"` honors `lower`/`upper` without
  creating near-duplicate vertices, and `method = "derive"` no longer
  NA-poisons irregular curves that do not span the full domain.
* Indefinite `tf_integrate()` on irregular `tfd` snaps user-supplied limits
  to (almost-)coinciding grid points instead of constructing invalid objects.
* `tf_register()` works for `tf_mv` inputs whose components live on different
  grids; `tf_register_shape()` returns a user-supplied `template` unchanged
  from `tf_template()`; single-component `srvf_mv` delegates to univariate
  elastic registration and template-free single-curve registration is
  rejected with an explanation instead of crashing inside `fdasrvf`.

# tf 0.4.1

## Bug fixes

* replace blanket `checkmate` namespace imports with selective imports to avoid
  new R-devel warnings from upstream `rlang` exports (`check_string()` and
  `check_data_frame()`)

# tf 0.4.0

## New features

* `tf_register()` & co implement registration methods (SRVF, affine, landmark,
  continuous-criterion) for aligning functions and analyzing phase variability.
  Returns a new `tf_registration` class with accessors `tf_aligned()`,
  `tf_inv_warps()`, `tf_template()`.
* `tf_depth()` now implements five functional depth methods: MBD (default), MHI,
  FM (Fraiman-Muniz), FSD (Functional Spatial Depth), and RPD (Regularized
  Projection Depth). MHI ranks functions from lowest (0) to highest (1); all
  other depths are scaled from 0 (most extreme) to 1 (most central).
  MBD is now scaled to [0, 1] instead of [0, 0.5].
* `tf_split()` / `tf_combine()` for splitting functions into sub-domain
  fragments and re-combining them
* `tf_rgp()` gains a `"brownian_bridge"` covariance option and can generate
  irregular data via the new `irreg` argument
* `tfb_spline()` now converts `fda::fd` and `fda::fdSmooth` objects directly
* `mgcv`-style Fourier basis constructor for use in `tfb_spline()`
* sparklines in `print()` / `format()` for all `tf` subtypes
* `tf_where()` now defaults to `arg = tf_arg(x)`, consistent with other functions
* `as.matrix()` for irregular `tfd` now always interpolates to a common grid

## Bug fixes

* `tf_derive()` for `tfd` objects now uses second-order accurate finite
  differences, preserving the original domain and grid (#59)
* major overhaul of NA handling: NA entries are now consistently represented as
  `NULL` internally; fixes for `tf_arg()`, `tf_evaluate()`, `tf_smooth()`,
  `tfd()` conversion, `is.na()`, printing, and arithmetic with NA entries
* fix `summary.tf()` returning wrong central range
* fix `tfd` domain enforcement for length-1 data (#139)
* fix printing for empty `tfb` objects and all-NA vectors

## Other changes

* `tf_derive` and `tf_integrate` can now handle `tfb` with non-identity link functions
  by falling back to `tfd`-calculus methods (and returning `tfd` objects).
* `tfd_irreg` arithmetic now operates on intersection of arg-values instead
  of failing when arg-values are not identical
* `tfb` arithmetic fix
* dropped `glue` dependency in favour of `cli` / `sprintf`; 
  replaced `stopifnot()` with informative `cli::cli_abort()` throughout
* use `vctrs::vec_arith()` for group generics, `vctrs` utils in `[.tf`
* various internal performance improvements
* use `air` formatter for all R code

# tf 0.3.4

* bug fix: normalize `tf_crosscov` correctly

# tf 0.3.3

* `tf_rebase` behaves more consistently on (irregular) data with different lengths
* avoid CRAN issues by streamlining `tfb_fpc` example and skipping `tf_rebase` tests
