mwTensor is an R package for Multi-Way Component Analysis (MWCA). It provides solvers for decomposing single or multiple matrices and tensors with flexible choices of matrix factorization algorithms (SVD, NMF, ICA, CX, etc.).
Key functions:
| Function | Purpose |
|---|---|
MWCA |
Decompose a single tensor |
CoupledMWCA |
Decompose multiple coupled tensors simultaneously |
checkCoupledMWCA |
Validate parameters before optimization |
initCoupledMWCA |
Generate reproducible initial values |
refineFactor |
One-step decomposition of a factor (experimental) |
MWCAProgram |
Declarative factorization program representation |
MWCA applies a matrix factorization method to each mode
of a single tensor.
library(mwTensor)
# Create a 3-way tensor (10 x 12 x 8)
set.seed(123)
X <- array(runif(10 * 12 * 8), dim = c(10, 12, 8))
# Default parameters: SVD on each mode, lower dim = 2
params <- defaultMWCAParams(X)
params@algorithms # decomposition method per mode
#> [1] "mySVD" "mySVD" "mySVD"
params@dims # target lower dimension per mode
#> [1] 2 2 2out <- MWCA(params)
# Factor matrices: one per mode
# Each is dims[i] x dim(X)[i]
lapply(out@factors, dim)
#> [[1]]
#> [1] 2 10
#>
#> [[2]]
#> [1] 2 12
#>
#> [[3]]
#> [1] 2 8
# Core tensor
dim(out@core)
#> [1] 2 2 2
# Reconstruction error
out@rec_error
#> [1] 8.776407params2 <- new("MWCAParams",
X = X,
mask = NULL,
pseudocount = 1e-10,
algorithms = c("mySVD", "myNMF", "myICA"),
dims = c(3L, 4L, 2L),
transpose = FALSE,
viz = FALSE,
figdir = NULL)
out2 <- MWCA(params2)
lapply(out2@factors, dim)
#> [[1]]
#> [1] 3 10
#>
#> [[2]]
#> [1] 4 12
#>
#> [[3]]
#> [1] 2 8Available algorithms: mySVD, myALS_SVD,
myNMF, myICA, myCX.
CoupledMWCA decomposes multiple tensors that share
dimensions. Shared dimensions get common factor
matrices; unshared dimensions get specific factor
matrices.
Xs <- toyModel("coupled_CP_Easy")
# X1: 20x30 matrix, X2: 30x30x30 tensor, X3: 30x25 matrix
lapply(Xs, dim)
#> $X1
#> [1] 20 30
#>
#> $X2
#> [1] 30 30 30
#>
#> $X3
#> [1] 30 25The coupling structure:
I2 is shared between X1 and X2. I4 is shared between X2 and X3.
# common_model maps: block -> (mode_name -> factor_name)
common_model <- list(
X1 = list(I1 = "A1", I2 = "A2"),
X2 = list(I2 = "A2", I3 = "A3", I4 = "A4"),
X3 = list(I4 = "A4", I5 = "A5"))
# A2 appears in both X1 and X2 -> shared factor for dimension I2
# A4 appears in both X2 and X3 -> shared factor for dimension I4params <- defaultCoupledMWCAParams(Xs, common_model)
# Inspect defaults
params@common_algorithms # "mySVD" for all
#> $A1
#> [1] "mySVD"
#>
#> $A2
#> [1] "mySVD"
#>
#> $A3
#> [1] "mySVD"
#>
#> $A4
#> [1] "mySVD"
#>
#> $A5
#> [1] "mySVD"
params@common_dims # 2 for all
#> $A1
#> [1] 2
#>
#> $A2
#> [1] 2
#>
#> $A3
#> [1] 2
#>
#> $A4
#> [1] 2
#>
#> $A5
#> [1] 2
params@common_coretype # "Tucker"
#> [1] "Tucker"
out <- CoupledMWCA(params)
# Common factor matrices
lapply(out@common_factors, dim)
#> $A1
#> [1] 2 20
#>
#> $A2
#> [1] 2 30
#>
#> $A3
#> [1] 2 30
#>
#> $A4
#> [1] 2 30
#>
#> $A5
#> [1] 2 25
# Convergence
tail(out@rec_error)
#> 95 96 97 98 99 100
#> 7952.131 7952.131 7952.131 7952.131 7952.131 7952.131params@common_algorithms <- list(
A1 = "mySVD", A2 = "myNMF", A3 = "myNMF",
A4 = "mySVD", A5 = "myCX")
params@common_dims <- list(
A1 = 3, A2 = 3, A3 = 5, A4 = 4, A5 = 4)
params@common_iteration <- list(
A1 = 5, A2 = 5, A3 = 5, A4 = 5, A5 = 5)
out2 <- CoupledMWCA(params)
lapply(out2@common_factors, dim)
#> $A1
#> [1] 3 20
#>
#> $A2
#> [1] 3 30
#>
#> $A3
#> [1] 5 30
#>
#> $A4
#> [1] 4 30
#>
#> $A5
#> [1] 4 25Before running a potentially expensive optimization, validate the
parameter object. Unlike CoupledMWCA which stops on the
first error, checkCoupledMWCA collects all
errors and warnings.
# Introduce multiple errors
bad_params <- params
bad_params@common_algorithms$A1 <- "nonExistentAlgo"
bad_params@common_iteration$A2 <- 1.5 # must be integer
bad_params@common_coretype <- "TUCKER" # must be "Tucker" or "CP"
result <- checkCoupledMWCA(bad_params)
result$ok
#> [1] FALSE
result$errors
#> [1] "nonExistentAlgo is not defined in .GlovalEnv"
#> [2] "params@common_iteration must be specified as an integer vector"
#> [3] "params@common_coretype must be 'Tucker' or 'CP'"
result$summary
#> [1] "Validation failed: 3 error(s), 0 warning(s)"initCoupledMWCA generates initial factor matrices
without running optimization. This is useful for:
params_ok <- defaultCoupledMWCAParams(Xs, common_model)
# Random initialization with seed
init <- initCoupledMWCA(params_ok, seed = 42L)
init@init_policy
#> [1] "random"
lapply(init@common_factors, dim)
#> $A1
#> [1] 2 20
#>
#> $A2
#> [1] 2 30
#>
#> $A3
#> [1] 2 30
#>
#> $A4
#> [1] 2 30
#>
#> $A5
#> [1] 2 25A CoupledMWCAInit object can be passed directly to
CoupledMWCA. The pre-computed factors are used as the
starting point.
# Initialize, then optimize
init <- initCoupledMWCA(params_ok, seed = 42L, init_policy = "svd")
out <- CoupledMWCA(init)
# Same seed -> same result
out_a <- CoupledMWCA(initCoupledMWCA(params_ok, seed = 123L))
out_b <- CoupledMWCA(initCoupledMWCA(params_ok, seed = 123L))
identical(out_a@common_factors$A1, out_b@common_factors$A1)
#> [1] TRUErefineFactor applies a one-step matrix factorization to
a factor obtained from a previous decomposition. This is a
proof-of-concept for “decomposition of a decomposition.”
Given a factor matrix A (k x n), the refinement decomposes t(A) (n x k):
t(A) ~ U * V
where U is (n x dim) and V is (dim x k). This gives:
sub_factors = t(U): new lower-rank basis vectors (dim x
n)coef = t(V): coefficients (k x dim)A ~ coef %*% sub_factorsset.seed(42)
X <- matrix(runif(20 * 30), nrow = 20, ncol = 30)
params_mat <- defaultMWCAParams(X)
params_mat@dims <- c(5L, 5L)
fit <- MWCA(params_mat)
# Factor 1 is 5 x 20. Refine it down to dim=2.
ref <- refineFactor(fit, 1L, algorithm = "mySVD", dim = 2L)
dim(ref@sub_factors) # 2 x 20
#> [1] 2 20
dim(ref@coef) # 5 x 2
#> [1] 5 2
# Approximation quality
max(abs(ref@source_factor - ref@coef %*% ref@sub_factors))
#> [1] 0.4163489params_c <- defaultCoupledMWCAParams(Xs, common_model)
params_c@common_dims <- list(A1=3, A2=3, A3=3, A4=3, A5=3)
fit_c <- CoupledMWCA(params_c)
# Refine common factor A2 (3 x 30) with NMF
ref_A2 <- refineFactor(fit_c, "A2", algorithm = "myNMF", dim = 2L)
dim(ref_A2@sub_factors) # 2 x 30
#> [1] 2 30
dim(ref_A2@coef) # 3 x 2
#> [1] 3 2MWCAProgram provides a declarative way to describe
factorization problems, including one-step factor refinements. This is
an internal representation layer designed for future recursive
decomposition support.
prog <- MWCAProgram(
blocks = list(
X1 = MWCAProgramBlock(
modes = c("I1", "I2"),
factor_map = c(I1 = "A1", I2 = "A2")),
X2 = MWCAProgramBlock(
modes = c("I2", "I3", "I4"),
factor_map = c(I2 = "A2", I3 = "A3", I4 = "A4")),
X3 = MWCAProgramBlock(
modes = c("I4", "I5"),
factor_map = c(I4 = "A4", I5 = "A5"))),
factors = list(
A1 = MWCAProgramFactor(mode = "I1", dim = 3),
A2 = MWCAProgramFactor(mode = "I2", dim = 3),
A3 = MWCAProgramFactor(mode = "I3", dim = 5, algorithm = "myNMF"),
A4 = MWCAProgramFactor(mode = "I4", dim = 4),
A5 = MWCAProgramFactor(mode = "I5", dim = 4)))
print(prog)
#> MWCAProgram
#> Blocks: 3
#> Factors: 5
#> Refinements: 0
#> Block details:
#> X1 [common]: modes=I1,I2 -> factors={A1, A2}
#> X2 [common]: modes=I2,I3,I4 -> factors={A2, A3, A4}
#> X3 [common]: modes=I4,I5 -> factors={A4, A5}
#> Factor details:
#> A1 [common]: mode=I1, dim=3, status=decomposed, algo=mySVD
#> A2 [common]: mode=I2, dim=3, status=decomposed, algo=mySVD
#> A3 [common]: mode=I3, dim=5, status=decomposed, algo=myNMF
#> A4 [common]: mode=I4, dim=4, status=decomposed, algo=mySVD
#> A5 [common]: mode=I5, dim=4, status=decomposed, algo=mySVDprog_ref <- MWCAProgram(
blocks = prog$blocks,
factors = prog$factors,
refinements = list(
R1 = MWCAProgramRefinement(
source_factor = "A2",
algorithm = "mySVD",
dim = 2)))
result <- executeMWCAProgram(prog_ref, Xs)
is(result$fit, "CoupledMWCAResult")
#> [1] TRUE
length(result$refinements) # 1
#> [1] 1
dim(result$refinements$R1@sub_factors)
#> [1] 2 30prog_status <- MWCAProgram(
blocks = list(
X1 = MWCAProgramBlock(
modes = c("I1", "I2"),
factor_map = c(I1 = "A1", I2 = "A2")),
X2 = MWCAProgramBlock(
modes = c("I2", "I3"),
factor_map = c(I2 = "A2", I3 = "A3"))),
factors = list(
A1 = MWCAProgramFactor(mode = "I1", dim = 3, status = "decomposed"),
A2 = MWCAProgramFactor(mode = "I2", dim = 3, status = "fixed"),
A3 = MWCAProgramFactor(mode = "I3", dim = 3, status = "frozen")))
# decomposed: updated by solver
# fixed: initialized but not updated (fix=TRUE)
# frozen: identity-like, not decomposed (decomp=FALSE)
validateMWCAProgram(prog_status)$ok
#> [1] TRUEsessionInfo()
#> R version 4.6.0 (2026-04-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.4 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] mwTensor_1.2.2 rmarkdown_2.31
#>
#> loaded via a namespace (and not attached):
#> [1] dotCall64_1.2 gtable_0.3.6 ellipse_0.5.0
#> [4] spam_2.11-3 xfun_0.57 bslib_0.10.0
#> [7] ggplot2_4.0.3 tagcloud_0.7.0 ggrepel_0.9.8
#> [10] lattice_0.22-9 vctrs_0.7.3 tools_4.6.0
#> [13] generics_0.1.4 parallel_4.6.0 tibble_3.3.1
#> [16] rARPACK_0.11-0 pkgconfig_2.0.3 Matrix_1.7-5
#> [19] RColorBrewer_1.1-3 S7_0.2.2 mixOmics_6.37.0
#> [22] lifecycle_1.0.5 compiler_4.6.0 farver_2.1.2
#> [25] stringr_1.6.0 fields_17.3 iTensor_1.0.5
#> [28] codetools_0.2-20 misc3d_0.9-2 einsum_0.1.2
#> [31] htmltools_0.5.9 sys_3.4.3 buildtools_1.0.0
#> [34] maps_3.4.3 sass_0.4.10 yaml_2.3.12
#> [37] tidyr_1.3.2 pillar_1.11.1 jquerylib_0.1.4
#> [40] MASS_7.3-65 BiocParallel_1.47.0 cachem_1.1.0
#> [43] nlme_3.1-169 RSpectra_0.16-2 nnTensor_1.4.0
#> [46] tidyselect_1.2.1 digest_0.6.39 stringi_1.8.7
#> [49] purrr_1.2.2 dplyr_1.2.1 reshape2_1.4.5
#> [52] splines_4.6.0 maketools_1.3.2 fastmap_1.2.0
#> [55] grid_4.6.0 cli_3.6.6 rTensor_1.5.0
#> [58] ccTensor_1.0.3 magrittr_2.0.5 corpcor_1.6.10
#> [61] scales_1.4.0 plot3D_1.4.2 matrixStats_1.5.0
#> [64] Vicus_0.99.0 igraph_2.3.1 gridExtra_2.3
#> [67] RANN_2.6.2 evaluate_1.0.5 knitr_1.51
#> [70] tcltk_4.6.0 viridisLite_0.4.3 mgcv_1.9-4
#> [73] jointDiag_0.4 rlang_1.2.0 Rcpp_1.1.1-1.1
#> [76] glue_1.8.1 geigen_2.3 jsonlite_2.0.0
#> [79] plyr_1.8.9 R6_2.6.1