Skip to content

fix(svd): projection deflation for constrained factors (no repeated factors) - #84

Merged
zdebruine merged 1 commit into
mainfrom
fix/deflation-projection-for-constrained
Oct 19, 2026
Merged

zdebruine merged 1 commit into
mainfrom
fix/deflation-projection-for-constrained

Conversation

@zdebruine

Copy link
Copy Markdown
Owner

fix(svd): projection deflation for constrained factors (no repeated factors)

Problem

Follow-up to #83. With nonneg/L1/upper_bound, the deflation solver still used Hotelling deflation (subtracting each fitted rank-1 term). Constrained factors are not singular vectors, so the residual keeps energy along earlier factors and later factors converge to near-copies of them. #83 removed the Gram-Schmidt step that had been masking this (by breaking non-negativity), which exposed it at larger k.

pbmc3k (all 13,714 genes × 2,638 cells, log-normalized, k = 16, nonneg = c(TRUE, FALSE)): the 16 returned loading vectors spanned only 8 distinct directions (factors 5–16 pairwise |cos| ≥ 0.99).

Change

When either side has projection constraints, use generalized (projection) deflation, as in nsprcomp:
R_k = P_u A P_v, P = I − QQ' over an orthonormal basis of earlier constrained factors (two-pass projection). Both deflated products (R v, R' u) go through one helper used at all nine former spmv + deflation_correct call sites; post-factor Gram-Schmidt is skipped in this mode. Unconstrained SVD/PCA keeps the Hotelling path unchanged.

Verification

pbmc3k, k = 16, 2 threads, vs CRAN nsprcomp 0.5.1.2 on the same data:

distinct directions max |cos| between factors negative loadings best-fit rel. residual
before (main) 8 0.99 0 0.9500
this PR 16 0.43 0 0.9478
nsprcomp 16 0.48 0 0.9480

(unconstrained rank-16 PCA floor: 0.9435)

  • Simulated k = 3 checks from fix(svd): keep nonneg/L1 constraints on deflation factors after the first #83 unchanged: 0 negatives, cumulative adjusted variance 1.001–1.009× nsprcomp.
  • Unconstrained deflation SVD unchanged (max |U'U − I| 1.4e-7, singular values match base::svd).
  • New test (k = 10, distinct factors): fails on current main (max |cos| 1.000), passes here (0.233).
  • Full test suite: 940 tests, 0 failures, 0 errors (485 skipped: GPU / optional packages).

Not covered


Generated by Claude Code

…actors)

With nonneg/L1/upper_bound constraints, deflation subtracted each fitted
rank-1 term (Hotelling deflation). Constrained factors are not singular
vectors, so the residual kept energy along earlier factors and later
factors converged to near-copies of them. On pbmc3k (all 13,714 genes,
k = 16, nonneg loadings) the 16 returned factors spanned only 8 distinct
directions (max |cos| between factors 0.99).

When either side is constrained, use generalized (projection) deflation,
as in nsprcomp: R_k = P_u A P_v, with P = I - QQ' over an orthonormal
basis of the earlier constrained factors. Both deflated products go
through one helper; post-factor Gram-Schmidt is skipped in this mode.
Unconstrained SVD/PCA keeps the Hotelling path unchanged.

pbmc3k, k = 16: 16 distinct directions (max |cos| 0.43), 0 negative
loadings, best-fit relative residual 0.9478 vs 0.9480 for nsprcomp.
New test fails on the previous code (max |cos| 1.000) and passes here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014zFkHdSUAE1RSwGtTqz9Fi
@zdebruine
zdebruine merged commit 49df8da into main Oct 19, 2026
0 of 6 checks passed
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.

1 participant