Skip to content

tfb_mfpc invariants are not defended: mf + 1, mf$x <-, c(mf[1:4], mf[5:8]) #257

@fabian-s

Description

@fabian-s

Three independent integrity gaps in the multivariate FPC representation. The MFPCA math itself is correct (verified to 5.6e-15) — these are about preserving the joint-fit metadata under user operations.

1. Arithmetic explodes with an unhelpful internal error

R/tfb-mfpc.R:379-384. mf + 1 first warns "potentially lossy cast" then aborts via the per-component scoring stub: "Can't score data onto a single component…" — a baffling message for a user who typed + 1.

2. Component replacement silently destroys the joint basis

R/accessors-mv.R:110-137. mf$x <- some_other_tfb succeeds, is_tfb_mfpc() then returns FALSE, the joint spec is silently discarded.

3. Concatenation of slices of the same fit drops the spec

R/vctrs-mv.R:24-42. c(mf[1:4], mf[5:8]) of pieces of the identical fit loses the spec. The slice-keeps / concat-drops asymmetry is documented in a code comment but never to users. Any dplyr::bind_rows/summarize round-trip silently downgrades the column.

Fix

  • For Math/Ops on tfb_mfpc: intercept early with a clear cli_warn ("demoting to per-component tfb_fpc representation; rescore with tf_rebase for joint MFPC arithmetic"); actually demote (drop the joint spec attributes) and continue.
  • For $<- on tfb_mfpc: warn that replacement demotes; demote and continue.
  • For tf_mv_ptype2 on tfb_mfpc: keep the spec when all inputs carry identical() specs (i.e. they came from the same fit). Document the "different specs → demote" rule.

Reproduce

library(tf)
set.seed(1)
mf <- tfb_mfpc(tfd_mv(list(x = tf_rgp(20), y = tf_rgp(20))), pve = 0.95)
mf + 1               # baffling error
mf2 <- mf
mf2$x <- mf$x        # succeeds — but is_tfb_mfpc(mf2) is now FALSE, no warning
is_tfb_mfpc(c(mf[1:4], mf[5:8]))  # FALSE — same fit, lost spec

Regression test (sketch)

test_that("tfb_mfpc protects its joint spec", {
  set.seed(1)
  mf <- tfb_mfpc(tfd_mv(list(x = tf_rgp(20), y = tf_rgp(20))), pve = 0.95)
  # 1. Arithmetic demotes with a clear warning
  expect_warning(out <- mf + 1, "demot|mfpc|joint")
  expect_false(is_tfb_mfpc(out))
  # 2. $<- demotes with a clear warning
  expect_warning({mf2 <- mf; mf2$x <- mf$x}, "demot|mfpc|joint")
  expect_false(is_tfb_mfpc(mf2))
  # 3. c() of slices of the same fit preserves the spec
  expect_true(is_tfb_mfpc(c(mf[1:4], mf[5:8])))
})

Found in the June-2026 ground-up review.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions