--- title: Get started with eigencore output: rmarkdown::html_vignette: toc: yes toc_depth: 2 css: albers.css includes: in_header: albers-header.html params: family: lapis preset: homage resource_files: - albers.css - albers.js - albers-header.html - fonts vignette: | %\VignetteIndexEntry{Get started with eigencore} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup-opts, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", message = FALSE, warning = FALSE, fig.width = 7, fig.height = 4.1, fig.align = "center", out.width = "92%", dpi = 100 ) ``` ```{r albers-classes, echo=FALSE, results='asis'} cat(sprintf( paste0( '' ), params$family, params$preset )) ``` ```{r helpers, include = FALSE} # Shared plotting helpers. Kept out of the reader's view: the reader sees # the API call and the picture, never the plotting boilerplate. ec_blue <- "#2166ac"; ec_grey <- "grey78" # A computed slice (blue) drawn on top of the full dense spectrum (grey). plot_spectrum <- function(all_vals, computed, ylab = "value", main = NULL) { s <- sort(all_vals, decreasing = TRUE) k <- length(computed) op <- par(mar = c(4, 4.6, if (is.null(main)) 1 else 2.4, 1)); on.exit(par(op)) plot(seq_along(s), s, pch = 19, cex = 0.4, col = ec_grey, bty = "n", xlab = "rank (largest to smallest)", ylab = ylab, main = main) points(seq_len(k), sort(computed, decreasing = TRUE), pch = 19, cex = 1.2, col = ec_blue) legend("topright", c("full spectrum (dense reference)", "computed by eigencore"), pch = 19, pt.cex = c(0.7, 1.2), col = c(ec_grey, ec_blue), bty = "n", cex = 0.9) } ``` A matrix too large to fully diagonalize still has a handful of eigenvalues or singular values you actually need — the top few directions of variance, the leading modes of a graph, the dominant singular triplets behind a low-rank approximation. eigencore computes exactly that partial answer, and every result carries a certificate: residuals, a backward-error bound, and a pass/fail verdict, so you don't have to recompute the check yourself to trust it. ```{r setup} library(eigencore) ``` ## Compute a certified partial eigendecomposition Build a symmetric matrix and ask for its five largest eigenvalues: ```{r make-A} set.seed(1) n <- 200 A <- crossprod(matrix(rnorm(n * n), n, n)) / n + diag(n) fit <- eig_partial(A, k = 5, target = largest()) fit ``` The certificate is not a side note — `fit$certificate$passed` is the answer to "can I trust these five numbers?" ```{r first-certificate} fit$certificate$passed ``` ```{r spectrum, echo = FALSE, fig.cap = "The five largest eigenvalues (blue) located within the full spectrum of A (grey). eigencore computes only the requested slice, then certifies it.", fig.alt = "Scatter plot of all 200 eigenvalues sorted from largest to smallest in grey, with the five largest highlighted in blue at the top-left."} all_vals <- eigen(A, symmetric = TRUE, only.values = TRUE)$values plot_spectrum(all_vals, fit$values, ylab = "eigenvalue") ``` With `n = 200` this computed 5 of 200 pairs. In a production problem with `n = 1e6`, storing all eigenvectors and computing the full spectrum is usually impractical; a certified partial result is often the useful result you can afford. ## Generalized SPD eigenproblem (`A v = lambda B v`) Pass a metric `B` to `eig_partial()` when the problem is `A v = lambda B v` rather than the standard `A v = lambda v`. ```{r generalized} B <- diag(seq(1, 5, length.out = n)) fit_gen <- eig_partial(A, k = 5, target = largest(), B = B, method = lobpcg(maxit = 200)) fit_gen ``` The absolute residual is `||A v - lambda B v||`. The certificate divides that quantity by `||A|| + |lambda| ||B||` (and the vector norm, when needed) to form the backward error used for convergence. Orthogonality is measured in the `B`-inner product where appropriate. See `vignette("generalized-eigenproblems")` for dense pencils, singular `B`, and the QZ decomposition. ## Partial SVD For rectangular problems use `svd_partial()`: ```{r svd} M <- matrix(rnorm(400 * 50), 400, 50) svd_fit <- svd_partial(M, rank = 5, target = largest()) svd_fit ``` The same mental model applies to singular values: you compute the leading few and leave the tail untouched. ```{r svd-scree, echo = FALSE, fig.cap = "The five leading singular values (blue) computed by eigencore, shown against the full singular-value spectrum of M (grey).", fig.alt = "Scatter plot of all 50 singular values of M sorted descending in grey, with the top five highlighted in blue."} all_sv <- svd(M, nu = 0, nv = 0)$d plot_spectrum(all_sv, svd_fit$d, ylab = "singular value") ``` The reported `method` identifies the path — for very small or near-square problems eigencore may use a dense LAPACK SVD fallback rather than running its iterative Golub-Kahan kernel. Either way the certificate covers both `||A v - sigma u||` and `||A^T u - sigma v||`. ## RSpectra-compatible workflow If your existing code uses `RSpectra::eigs_sym()`, you can call eigencore in the same shape — the return list extends RSpectra's by adding `certificate` and `diagnostics`: ```{r rspectra} res <- eigs_sym(A, k = 5, which = "LA") str(res, max.level = 1) ``` ```{r rspectra-cert} res$certificate ``` `eigs()`, `eigs_sym()`, and `svds()` accept the same `which` codes as `RSpectra` — `"LM"`, `"SM"`, `"LA"`, `"SA"`, `"LR"`, `"SR"`, `"LI"`, `"SI"`, and `"BE"`. ## Under the hood: operators, problems, and plans `eig_partial()` and `svd_partial()` cover the common case, but every call you've made in this vignette runs through the same four-stage architecture, and you can drop down to it directly when you want to inspect or reuse a piece of that pipeline. **1. Operators.** Any matrix is wrapped in a *block-native operator* the moment you hand it to a problem constructor. You can do this explicitly: ```{r as-operator} Aop <- as_operator(A) Aop ``` The operator carries dimensions, structure tags, and a flag indicating whether the underlying storage has a native kernel (in this case dense double — yes). `vignette("sparse-pca")` builds operators that center and scale a sparse matrix without densifying it — the same object type, doing more work per call. **2. Problems.** `eigen_problem()` and `svd_problem()` describe *what* to solve: the matrix, its structure (`hermitian()`, `general()`, ...), and the spectral target (`largest()`, `smallest()`, `nearest()`, ...). ```{r problem} P <- eigen_problem(A, structure = hermitian(), target = largest()) ``` **3. Plans.** `plan_solver()` reports the primary method selected from the problem's current structure, target, and policy, together with any allowed fallback. It is an inspection object, not an executable or frozen plan: certification can still trigger a recorded fallback while solving. ```{r plan} plan <- plan_solver(P, k = 5) plan ``` **4. Solve.** `solve()` on a problem is what `eig_partial()` calls internally. Reuse the problem object when you want to solve it again. Inspect the returned `method`, warnings, and restart diagnostics to see the route that actually ran. ```{r solve} same_fit <- solve(P, k = 5) isTRUE(all.equal(same_fit$values, fit$values)) ``` ## Where to go next - `vignette("sparse-pca")` — the flagship workflow: centering and scaling a sparse matrix without densifying it, and building matrix-free operators by hand. - `vignette("certificates")` is the deep dive on reading the numerical evidence — what each field means and what to do when a check fails. - `vignette("generalized-eigenproblems")` covers dense pencils, singular `B`, and the QZ decomposition. - Run `help(package = "eigencore")` to browse the installed help index. - `?certificate` documents the certificate fields in detail. - `?plan_solver` explains how operator structure, target, and method combine to choose a kernel.