Skip to contents

eigencore computes the top-k singular triplets or eigenpairs of a large sparse or structured matrix in R — the computation behind PCA on big sparse data, spectral embeddings, LSA, and low-rank approximation. It also certifies real symmetric positive-semidefinite (PSD) geometry, including singular forms whose null spaces must remain explicit.

eigencore focuses on three things:

  1. Every result is checked. Each call returns residuals, a backward-error bound, orthogonality loss, and a single passed/failed flag. The backward error uses the standard 2-norm definition with a denominator that never exceeds ||A||_2, so a certificate can only over-state the error.
  2. Centering and scaling without densifying. Explicitly centering a sparse matrix for PCA can require a dense copy. eigencore solves the centered (or scaled, or composed) problem as an operator, without forming that copy.
  3. PSD actions match their evidence. Complete dense and diagonal factors expose roots, pseudoinverses, projectors, and image reduction. Structural sparse Gram and Laplacian factors expose only what their construction proves, and unsupported requests fail before dense fallback.

Supported structured problems run through fast native kernels. The Benchmarks section compares them with RSpectra, irlba and base R using stored, reproducible results with independent accuracy checks.

Installation

# install.packages("pak")
pak::pak("bbuchsbaum/eigencore")

Quick start

The top 10 singular triplets of a 100,000 × 500 sparse matrix — the core computation in sparse PCA and LSA:

library(eigencore)
library(Matrix)

set.seed(2)
A <- as(rsparsematrix(100000, 500, density = 0.002), "dgCMatrix")

fit <- svd_partial(A, rank = 10, target = largest())
fit
#> Partial SVD
#>   requested rank: 10
#>   converged rank: 10
#>   method: native certified Gram SVD special case
#>   target: largest
#>   max residual: 1.736519e-14
#>   max backward error: 1.007435e-15
#>   max orthogonality loss: 1.332268e-15
#>   norm bound: two_norm_lower_bound
#>   scale estimated: FALSE
#>   certificate: passed

The printout names the kernel that ran, gives the worst residual, backward error, and orthogonality loss across the returned triplets, and shows the certificate passed. The backward error divides by a lower bound on ||A||_2 (two_norm_lower_bound), so it can only over-state the true normwise backward error. This problem uses the native certified Gram path; see Benchmarks for reproducible timings.

The ten largest singular values highlighted in blue against the full 500-point singular spectrum of A in grey.

Certificates

An iterative solver can stop early, miss a cluster, or lose orthogonality and still return plausible-looking numbers. eigencore makes validation part of the returned result: it checks both singular relations (||A v - sigma u|| and ||A^T u - sigma v||) and exposes the evidence:

fit$certificate
#> eigencore certificate
#>   passed: TRUE
#>   tolerance: 1e-08
#>   type: residual_backward_error
#>   norm bound: two_norm_lower_bound
#>   norm source: applied_vectors
#>   scale estimated: FALSE
#>   max residual: 1.736519e-14
#>   max backward error: 1.007435e-15
#>   max orthogonality loss: 1.332268e-15
#>   orthogonality tolerance: 1.490116e-08
#>   orthogonality required: TRUE
#>   target completeness: exact

The backward error is the standard normwise one, max residual / ||A||_2 for an SVD and ||A x - lambda x|| / ((||A||_2 + |lambda|) ||x||) for an eigenpair. The exact ||A||_2 is rarely available, so eigencore divides by a value that never exceeds it: the exact norm where it is cheap (diagonal matrices, a full computed spectrum), otherwise the largest of the column norms, the ||A x|| / ||x|| ratios of the returned vectors, and, only when that could change the verdict, a short deterministic Lanczos estimate. A smaller denominator makes the reported backward error larger, so passed is never optimistic. norm_bound_type says whether the scale was exact or a lower bound, and norm_source says where it came from.

This also covers operators with no cheap norm at all, such as a double-centered sparse matrix, which eigencore never forms explicitly:

cen <- svd_partial(center(A, rows = TRUE, columns = TRUE), rank = 5,
                    target = largest())

cen$certificate$passed
#> [1] TRUE
cen$certificate$norm_bound_type
#> [1] "two_norm_lower_bound"
cen$certificate$norm_source
#> [1] "applied_vectors"

If a pair’s residual is not small relative to this scale, the certificate fails and failed_indices names the offending pairs; the explicit flag lets downstream code act on that without inferring it from solver convergence.

Certified singular PSD geometry

A singular PSD form is a seminorm on the original coordinates and a genuine metric only on its image, or on the quotient by its null space. Certify the form once, inspect its numerical rank, and reduce data explicitly before an algorithm that requires an inner product:

L_metric <- matrix(c(
  1, 0, 1,
  0, 1, 1
), 2, 3, byrow = TRUE)
K <- crossprod(L_metric)

K_factor <- psd_factor(K)
c(rank = psd_rank(K_factor), nullity = psd_nullity(K_factor))
#>    rank nullity
#>       2       1

X_metric <- matrix(c(
  1, 2, 3,
  3, 2, 1
), 3, 2)
X_image <- psd_reduce(K_factor, X_metric)

stopifnot(isTRUE(all.equal(
  crossprod(X_image),
  psd_gram(K_factor, X_metric),
  tolerance = 1e-12
)))

psd_capabilities(K_factor) is the runtime manifest. Identity, diagonal, dense spectral, and dense Gram factors have complete numerical paths. Sparse Gram and graph-Laplacian constructors preserve sparse state and intentionally withhold roots, projectors, and numerical rank unless their evidence supports those actions. Generic sparse matrices and opaque callbacks are not promoted to certified factors from storage or metadata alone.

psd_apply(K_factor, b, "pseudoinverse") is defined for every finite b, but psd_solve(K_factor, b) is stricter: it rejects a right-hand side with a null component because the original equation K x = b has no solution. See vignette("psd-geometry") for the complete capability table, tolerance and repair semantics, sparse constructors, block primitives, persistence, and the explicit reduction path for singular generalized eigenproblems.

Center and scale without densifying

A dense centered copy of A would occupy 400 MB; the sparse original is a few MB. center() gives you the centered map as an operator, and the solver works through it directly:

A_centered <- center(A, columns = TRUE)        # a 100000 x 500 operator, not a matrix
svd_partial(A_centered, rank = 5, target = largest())$d
#> [1] 17.23701 16.65319 16.60961 16.48647 16.44760

Build operators with linear_operator(), combine them with compose(), crossprod_operator(), scale_cols(), center(), and friends. The planner picks the kernel from the structure. plan_solver() returns an executable, frozen record: solve(plan) runs that inspected route, while solve(problem) creates a fresh plan under current policy. Results report both the planned and actual runtime methods when a certification fallback is needed:

plan <- plan_solver(svd_problem(A_centered, target = largest()), rank = 5)
plan$method
#> [1] "native certified implicit Gram SVD (thick-restart Lanczos)"

Use work(result) for cross-solver accounting. It keeps forward, adjoint, metric, preconditioner, and certification calls and columns separate; result$matvecs remains the route-specific compatibility field.

For repeated standard Hermitian Lanczos solves, opt into reusable state through an executable plan. The retained object is an acceleration hint, not a saved certificate: every solve still applies and certifies the current operator.

workflow_matrix <- diag(seq(60, 1))
workflow_plan <- plan_solver(
  eigen_problem(workflow_matrix, target = largest()),
  k = 3,
  method = lanczos(block = 3, max_subspace = 24),
  tol = 1e-8
)
first_workflow <- solve(workflow_plan, retain_state = "same_operator")
second_workflow <- solve(
  workflow_plan,
  restart_state = restart_state(first_workflow, retention = "same_operator"),
  reuse = "same_operator"
)
stopifnot(
  certificate(second_workflow)$passed,
  second_workflow$state_transition$method_state_used
)

reuse = "auto" keeps only the public basis after a coordinate-compatible operator revision; reuse = "basis_only" always ignores method payloads. Unsupported SVD, generalized, transformed, Arnoldi, LOBPCG, and dense-fallback receiving routes fail rather than silently running cold.

Smallest eigenvalues of a symmetric operator

The same interface handles symmetric eigenproblems. Here is a sparse second-difference operator (a 1-D graph Laplacian) of size 20,000, asking for its smallest eigenvalues — the hard end of the spectrum for iterative solvers:

n <- 20000
L <- bandSparse(n, n, k = c(-1, 0, 1),
                diagonals = list(rep(-1, n - 1), rep(2, n), rep(-1, n - 1)))
L <- as(L, "dgCMatrix")

eig <- eig_partial(L, k = 8, target = smallest())
eig
#> Partial eigen decomposition
#>   requested: 8
#>   converged: 8
#>   method: native tridiagonal Hermitian shift-invert (factorized Lanczos)
#>   target: smallest
#>   restart: native_tridiagonal_shift_invert_lanczos
#>   locked: 0
#>   max residual: 3.41212e-10
#>   max backward error: 1.392991e-10
#>   max orthogonality loss: 6.661338e-16
#>   norm bound: two_norm_lower_bound+identity_exact
#>   scale estimated: FALSE
#>   certificate: passed

The planner selects a native tridiagonal shift-invert path and the resulting certificate passes. The exact spectrum is known in closed form, so the answer can also be checked directly:

The eight smallest eigenvalues highlighted in blue at the bottom of the full analytic spectrum of the 20,000-point 1-D Laplacian shown in grey.

Benchmarks

The table is drawn from stored results of the benchmark suite (inst/benchmarks/results/, profile standard), not timed when this README is built. Times are medians over repeated calls; eigencore’s include building its certificate, the other packages do no certification. Every method’s output is checked independently by the suite (2-norm backward error, error against a trusted reference, and whether the wanted eigenvalues were actually returned); † marks a result that missed part of the wanted set. RSpectra and irlba are single-threaded here; “n/a” means the method does not apply or the dense problem was too large.

Problem eigencore (1 thr) eigencore (4 thr) RSpectra irlba base R eigencore / RSpectra certified
sparse symmetric 20k, largest, k = 10 569 ms 620 ms 291 ms - n/a 1.96 yes
sparse symmetric 20k, smallest, k = 10 580 ms 667 ms 312 ms - n/a 1.86 yes
sparse nonsymmetric 20k, largest modulus, k = 6 7.55 s 7.05 s 2.80 s† - n/a 2.69 yes
sparse SVD 50k × 2k, k = 20 335 ms 349 ms 201 ms 2.04 s n/a 1.67 yes
sparse SVD 50k × 20k, k = 20 2.33 s 1.45 s 1.06 s 5.20 s n/a 2.19 yes
centred sparse PCA 50k × 1k, k = 10 511 ms 341 ms 533 ms 982 ms n/a 0.96 yes
dense symmetric 1500, largest, k = 10 198 ms 142 ms 105 ms - 934 ms 1.89 yes
full dense eigen 1500 (eig_full vs eigen) 1.38 s 773 ms - - 925 ms - yes
2-D Laplacian 10k, nearest 4.01, k = 6 116 ms 146 ms 56 ms - n/a 2.07 yes
generalized FEM pencil 2.5k, smallest, k = 6 334 ms 380 ms 645 ms - 10.6 s 0.52 yes
1-D Laplacian 20k, smallest, k = 8 61 ms 68 ms 9.97 s† - n/a 0.01 yes

2026-10-15; Intel(R) Xeon(R) Processor @ 2.80GHz, 4 logical cores; R 4.3.3; OpenBLAS; eigencore 2.0.0 (e560f8ad61); RSpectra 0.16.1, irlba 2.3.5.1; median of 5 reps; load average 1.0 at start, 2.0 at end; run 20261015-9d7130c3-standard.

Results depend on the processor, BLAS/LAPACK, thread count, machine load and the matrices themselves; these are random and model matrices measured on one 4-core cloud machine with nothing else running (load average recorded below). At this size several sparse cases are 10-25% slower with 4 threads than with 1 (the work per parallel region is small); the larger SVD, PCA and dense cases gain 1.4-2.4x. eigencore shrinks its OpenMP team when parallel regions stop paying off, so a busy machine degrades it gracefully instead of by an order of magnitude. The benchmarks article shows every case and method, operator-application counts, accuracy, scaling curves and the limits of the comparison. Reproduce or add your machine with

R CMD INSTALL --preclean --no-docs -l .rlib .
Rscript inst/benchmarks/run-suite.R --lib=.rlib --profile=standard --threads=1,4

When to use what

Use eigencore for tall or wide sparse SVD (PCA-shaped problems), the smallest eigenvalues of banded or structured symmetric operators (certified, with automatic planner selection), centered or scaled or composed operators, dense generalized eigen/QZ/GSVD compatibility work through eig_full(), generalized_schur(), and generalized_svd(), and workflows where explicit certificate metadata is useful.

For workloads outside those structured paths, benchmark the candidate packages on your own matrices and hardware rather than assuming any implementation will be fastest. Side-by-side evaluation is straightforward because eigencore ships RSpectra-compatible wrappers with the same arguments:

res <- eigs_sym(L, k = 8, which = "SA")
res$values
#> [1] 1.578979e-06 1.208906e-06 8.881755e-07 6.167886e-07 3.947447e-07
#> [6] 2.220439e-07 9.868617e-08 2.467154e-08
res$certificate$passed
#> [1] TRUE

eigs(), eigs_sym(), and svds() accept the same which codes as RSpectra ("LM", "SM", "LA", "SA", "LR", "SR", "LI", "SI", "BE") and additionally return a certificate.

Learning more

vignette("eigencore", package = "eigencore") is the guided tour. vignette("certificates", package = "eigencore") explains how to read the numerical evidence and what to do when a check fails.

Status

eigencore 1.3.0 is the current release. Its additive API includes certified real-double PSD factors, image-space reduction, strict singular solves, metric block primitives, dense Gram factors, and non-densifying structural sparse Gram and graph-Laplacian paths. The generalized-eigen B/metric= surface remains SPD-only; singular forms use explicit image reduction. The exported API is frozen by snapshot tests, and breaking changes follow semantic versioning. Unsupported solver or PSD capability families remain visibly unavailable rather than silently falling back.

License

MIT © Bradley Buchsbaum.