Skip to contents

Every result returned by eigencore carries a certificate: a compact record of the residual, backward-error scale, orthogonality check, and tolerance used for that result. The certificate lets you inspect those checks without recomputing the residual. It does not certify spectral ordering or any property that its certificate_type does not name.

This vignette walks through the certificate’s fields, the three things they measure, and what to do when one of them fails.

Anatomy of a certificate

set.seed(1)
n <- 200
A <- crossprod(matrix(rnorm(n * n), n, n)) / n + diag(n)

fit  <- eig_partial(A, k = 5, target = largest())
cert <- fit$certificate
cert
#> eigencore certificate
#>   passed: TRUE 
#>   tolerance: 1e-08 
#>   type: residual_backward_error 
#>   norm bound: two_norm_lower_bound+identity_exact 
#>   norm source: ritz+identity 
#>   scale estimated: FALSE 
#>   max residual: 6.259907e-11 
#>   max backward error: 6.578934e-12 
#>   max orthogonality loss: 8.881784e-16 
#>   orthogonality tolerance: 1.490116e-08 
#>   orthogonality required: TRUE 
#>   target completeness: inertia_verified

The fields you act on, in order:

  • passed — overall verdict. TRUE means every returned pair satisfies the checks required by this certificate type. The backward-error scale is exact or a lower bound on the matrix 2-norm, never an estimate, so a pass is never optimistic (see “Norm bounds” below).
  • tolerance — the user-requested tolerance. Defaults to 1e-8.
  • max_residual — the worst absolute residual ||A v - lambda v|| (or ||A v - lambda B v|| for generalized problems) across the returned basis.
  • max_backward_error — the worst residual divided by the labelled scale. This is the number that has to be below tolerance for a pair to be considered converged.
  • max_orthogonality_loss — the worst entry of |V* V - I| (or |V* B V - I| in B-inner-product problems).
  • failed_indices — which pairs failed. Empty when passed = TRUE.

Three more fields explain how the verdict was reached rather than what it is: norm_bound_type says whether the ||A||_2 (and ||B||_2) value in the backward-error scale is exact or a lower bound, norm_source says where it came from, and norm_values holds the values used. scale_is_estimate is always FALSE for built-in certificates. certificate_type and notes carry provenance text for unusual states. ?certificate documents every field.

The max_* fields are summaries. The certificate also keeps the underlying per-pair vectors (cert$backward_error, cert$converged), so you can see at a glance whether the whole basis cleared the bar or just barely missed:

Stem plot of backward error for five eigenpairs on a log scale, all falling well below the dashed tolerance line.

Per-pair backward error for the five returned eigenpairs. The max_backward_error field is simply the height of the tallest stem; passed is TRUE because every stem clears the tolerance line.

Three things a certificate measures

1. Residual

For a Hermitian eigenproblem A v = lambda v, the absolute residual is ||A v_i - lambda_i v_i||. For an SVD, both the left and right relations matter, so the combined residual is

sqrt( ||A v - sigma u||^2 + ||A^T u - sigma v||^2 ).

For a generalized SPD problem A v = lambda B v, the residual is computed in the original coordinates: ||A v_i - lambda_i B v_i||. Eigencore never reports a residual computed in a transformed problem space without saying so in certificate_type.

2. Backward error

Absolute residuals can be misleading when the operator’s norm is large or small. Backward error is the residual divided by a scale that captures “how big could a perturbation of A (and B) be that exactly makes (lambda, v) an eigenpair?”. eigencore uses the standard normwise definition in the matrix 2-norm:

eta_i = ||r_i|| / ( (||A||_2 + |lambda_i| ||B||_2) ||v_i|| ),

and for a singular triplet eta_i = sqrt(||A v - sigma u||^2 + ||A^T u - sigma v||^2) / ||A||_2 (unit vectors). A pair “converges” when eta_i <= tol. The tolerance you pass to eig_partial(tol = ...) is this backward-error tolerance, not a residual tolerance.

||A||_2 is rarely known exactly, so the denominator uses a value that never exceeds it. A smaller denominator can only make the reported eta_i larger than the true backward error, so a certificate that passes would also pass with the exact norm. (Before eigencore 1.4 the scale was the Frobenius norm, which can be up to sqrt(rank) times larger than ||A||_2 and made the check correspondingly more lenient.)

3. Orthogonality

Iterative methods drift. After enough restarts the returned basis can lose orthogonality even when each per-pair residual is small. The certificate records max_abs(V* V - I) (or max_abs(V* B V - I)); its acceptance tolerance is max(tol, sqrt(eps)). A passing residual should be interpreted alongside the orthogonality field. If orthogonality loss exceeds its tolerance, clustered or repeated eigenvalues may be represented by nearly duplicate vectors even when individual residuals are small.

A certificate evaluates the pairs the solver returned. It does not prove that those pairs are the requested largest, smallest, or nearest eigenvalues. When target identity is consequential, inspect the method and target in the plan and compare against an independent oracle on a tractable instance.

Reading common certificate states

The single most useful habit is to picture the per-pair backward error against the tolerance line. A passing certificate is “all stems below the line”; a failing one is “at least one stem above it.” Here are the same ten eigenpairs of A, computed two ways:

# Largest eigenvalues are well separated -> easy, converges fast.
fit_pass <- eig_partial(A, k = 10, target = largest())

# Smallest eigenvalues are densely clustered near 1 -> a tight maxit (restart)
# budget leaves them short of tolerance.
fit_fail <- eig_partial(A, k = 10, target = smallest(), maxit = 4)

c(largest_passed  = fit_pass$certificate$passed,
  smallest_passed = fit_fail$certificate$passed)
#>  largest_passed smallest_passed 
#>            TRUE           FALSE
Two side-by-side stem plots of per-pair backward error. Left panel shows ten blue points below the tolerance line; right panel shows ten red points above the tolerance line.

Same matrix, same k, two targets. Left: the ten largest eigenpairs all clear the tolerance (blue, passed). Right: the ten smallest stall above it under a tight iteration budget (red, failed).

Clean: every box ticked

fit_pass$certificate$passed
#> [1] TRUE
fit_pass$certificate$norm_bound_type
#> [1] "two_norm_lower_bound+identity_exact"
fit_pass$certificate$scale_is_estimate
#> [1] FALSE

This is the easy case. Every residual is below tolerance and orthogonality is near machine precision. The scale is a lower bound on ||A||_2, here the largest computed Ritz value (norm_source is "ritz+identity"), which for a largest-eigenvalue target is essentially the exact norm.

Failed: residual too large

The right-hand panel above is a genuine failure. The smallest eigenvalues of A sit in a dense cluster just above 1, so they need many more iterations than the well-separated largest ones. With maxit = 4 the solver runs out of restart budget before all pairs converge, and the verdict flips. The failed_indices slot tells you which Ritz pairs missed:

fit_fail$certificate$passed
#> [1] FALSE
fit_fail$certificate$failed_indices
#>  [1]  1  2  3  4  5  6  7  8  9 10
fit_fail$certificate$max_backward_error
#> [1] 0.004174378

You do not have to guess how far off it was, or whether it was inching toward convergence. The solver records a convergence_history; plotting the worst backward error per restart shows the failed run plateauing above the line while a generous budget drives it underneath.

fit_ok <- eig_partial(A, k = 10, target = smallest(), maxit = 40)
fit_ok$certificate$passed
#> [1] TRUE
Line plot on a log scale of backward error versus restart number. The red maxit-4 curve plateaus above the tolerance line; the blue maxit-40 curve descends below it.

Worst backward error per restart for the ten smallest eigenpairs. A tight budget (red) stalls above the tolerance; a generous one (blue) drives the error under the line and the certificate passes.

What to do: increase maxit, raise tol, or — when you suspect a clustered spectrum — request a larger k (so the cluster is fully covered by the returned basis) and slice afterwards. Here, lifting maxit (the restart limit) from 4 to 40 is enough.

Norm bounds: exact or a lower bound

norm_bound_type says what kind of ||A||_2 value scaled the backward error, as A or A+B parts (B = I appears as identity_exact):

  • two_norm_exact — the exact spectral norm: diagonal matrices (max |d_i|), a full computed Hermitian spectrum (eig_full() and dense solves that compute every eigenvalue), or a user-asserted metadata$two_norm.
  • two_norm_lower_bound — a value that never exceeds ||A||_2.

norm_source names where the value came from:

  • diagonal, full_spectrum, metadata, identity — exact values.
  • column_norms — the largest column norm ||A e_j|| (dense, sparse, tridiagonal storage; no operator applies).
  • applied_vectors — the largest ||A x|| / ||x|| over the certified vectors, computed with the same operator applies as the residuals.
  • ritz — the residual-corrected value |lambda| ||B x|| / ||x|| - ||r|| / ||x|| when only residual norms are available.
  • frobenius_rank_bound — ||A||_F / sqrt(min(m, n)) when an operator only carries Frobenius metadata.
  • lanczos — a 24-step Lanczos estimate on A (Hermitian) or A^T A, verified as ||A y|| / ||y|| for its Ritz vector. It runs only when some pair fails with the cheaper bounds but could pass with a larger one, uses a fixed start vector (it never touches the R random-number stream), and is memoised per operator.

Because every source is a lower bound, matrix-free operators certify like any other operator. A linear_operator() with no norm metadata used to fall back to a stochastic Hutchinson estimate and withhold passed; it now certifies with the bound from its own vectors:

set.seed(2)
op <- linear_operator(
  dim = c(n, n),
  apply = function(X, alpha = 1, beta = 0, Y = NULL) {
    Z <- alpha * (A %*% X)
    if (is.null(Y) || beta == 0) Z else Z + beta * Y
  },
  apply_adjoint = function(X, alpha = 1, beta = 0, Y = NULL) {
    Z <- alpha * (A %*% X)
    if (is.null(Y) || beta == 0) Z else Z + beta * Y
  },
  structure = hermitian(),
  name = "matrix-free Hermitian wrapper"
)
# The matrix-free path runs the reference Lanczos solver, which needs a
# larger subspace than the default to resolve this clustered spectrum.
fit_mf <- eig_partial(op, k = 5, target = largest(),
                      method = lanczos(max_subspace = 100))
fit_mf$certificate$norm_bound_type
#> [1] "two_norm_lower_bound+identity_exact"
fit_mf$certificate$norm_source
#> [1] "applied_vectors+identity"
fit_mf$certificate$scale_is_estimate
#> [1] FALSE
fit_mf$certificate$passed
#> [1] TRUE

A lower bound can be weak — for example ||A x|| / ||x|| for the smallest eigenvalues of an operator with no other information. Then the reported backward error over-states the true one and a pair may fail although it is accurate; the Lanczos refinement exists to close that gap before a pair is reported as failed. What to do if a pass matters and the bound is still weak: supply metadata = list(two_norm = ...) when you know ||A||_2, or wrap a built-in dense / dgCMatrix / ddiMatrix matrix.

Generalized SPD: B-orthogonality matters

For A v = lambda B v, the certificate’s orthogonality field is in the B-inner product: max_abs(V* B V - I). For this certificate type, passed = TRUE requires both the residual threshold and B-orthogonality of the returned vectors.

set.seed(4)
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$certificate$norm_bound_type
#> [1] "two_norm_lower_bound+two_norm_lower_bound"
fit_gen$certificate$max_orthogonality_loss
#> [1] 2.442491e-15
fit_gen$certificate$passed
#> [1] TRUE

If a B-orthogonality value comes back near machine precision, the B-inner-product Cholesky-QR refinement inside the solver did its job. If it comes back loose (say, 1e-4), increase maxit or lower tol — orthogonality loss is usually the first thing to surface in ill-conditioned-B problems.

Target completeness: the right set, not just right pairs

Residuals prove that each returned pair is an eigenpair. They cannot prove that the returned set is the one you asked for. A single-vector Krylov method sees, in exact arithmetic, only one direction of each eigenspace, so it can miss a copy of a repeated eigenvalue: for the spectrum 9, 9, 7, 7, 7, 5, ... it may return 9, 9, 7, 7, 5, and every one of those five pairs certifies.

After a certified Hermitian Krylov solve with a largest(), smallest() or largest_magnitude() target (and, with completeness = "inertia", nearest()), eigencore therefore checks the returned set. With an explicit dense or sparse matrix it counts eigenvalues (the next section); for a matrix-free operator, or when a factorisation would cost more than the solve, it runs a short deflated complement probe: a few block Lanczos steps on the operator restricted to the orthogonal complement of the returned vectors (B-orthogonal for generalized problems), from a fixed-seed start that does not touch R’s random-number stream. Any Ritz value of that restricted operator that lies beyond the least-preferred returned value (by more than the residual and tolerance margin) proves a more-preferred eigenvalue is missing. The solver then repairs the result with a deflated complement solve, merges the two sets by a Rayleigh-Ritz step, re-certifies and checks again.

d <- c(9, 9, 7, 7, 7, seq(5, 0.1, length.out = 55))
set.seed(1001)
p <- sample(60)
S <- Matrix::sparseMatrix(i = p, j = p, x = d)
bare <- eig_partial(S, k = 5, method = lanczos(completeness = "none"), seed = 1)
sort(values(bare), decreasing = TRUE)
#> [1] 9 9 7 7 5
bare$certificate$passed
#> [1] FALSE
fit_c <- eig_partial(S, k = 5, method = lanczos(completeness = "probe"), seed = 1)
sort(values(fit_c), decreasing = TRUE)
#> [1] 9 9 7 7 7
fit_c$certificate$target_completeness
#> [1] "repaired"
fit_c$certificate$completeness[c("steps", "operator_columns", "rounds")]
#> $steps
#> [1] 10
#> 
#> $operator_columns
#> [1] 134
#> 
#> $rounds
#> [1] 1

The probe is cheap (by default at most 8 steps of block size 2, set by the options eigencore.completeness_probe_steps and eigencore.completeness_probe_block). It is a probabilistic check: it can prove a set incomplete, not complete; an intruder whose Ritz value a short run does not resolve goes unnoticed.

Counting certificates

For a Hermitian matrix (or a pencil with symmetric positive definite B) that is available explicitly, the number of eigenvalues below any shift t is the number of negative pivots of an LDLTLDL^T factorisation of A - t B (Sylvester’s law of inertia). eigen_count() exposes that count: dense matrices use LAPACK’s Bunch-Kaufman dsytrf, sparse ones CHOLMOD’s LDLTLDL^T with a fill-reducing ordering, tridiagonal ones a Sturm sequence.

set.seed(2)
Q <- qr.Q(qr(matrix(rnorm(60 * 60), 60)))
A7 <- Q %*% (d * t(Q))
A7 <- (A7 + t(A7)) / 2   # dense, same spectrum as S
eigen_count(S, 6)        # S is diagonal: an exact comparison
#> <eigencore_inertia> diagonal  
#>   sigma: 6 
#>   below: 55  zero: 0  above: 5  (n = 60 )
#>   reliable: TRUE  min pivot / scale: 0.0667  pivot growth: 0.393
eigen_count(A7, 6)
#> <eigencore_inertia> dense_bunch_kaufman  
#>   sigma: 6 
#>   below: 55  zero: 0  above: 5  (n = 60 )
#>   reliable: TRUE  min pivot / scale: 0.0443  pivot growth: 3.31
eigen_count(A7, 7)
#> <eigencore_inertia> dense_bunch_kaufman  
#>   sigma: 7  zero band: sigma -/+ 2.76e-11 
#>   below: 55  zero: 3  above: 2  (n = 60 )
#>   reliable: TRUE  min pivot / scale: 2.27e-11  pivot growth: 1 
#>   note: sigma was within the numerical zero band of the factorisation; counts use sigma -/+ 2.76e-11 and 'zero' counts the eigenvalues inside that band

The last call lands exactly on the triple eigenvalue 7: the factorisation reports a (numerically) zero pivot, so the shift is moved to 7 -/+ delta, zero counts the eigenvalues inside that band, and reliable says whether every count came from a trustworthy factorisation. CHOLMOD’s sparse LDLTLDL^T does not pivot, so each count carries its backward-error bound (diagnostics$backward_bound, from || |L| |D| |L'| ||) and pivot growth; an unreliable count is perturbed, falls back to a pivoted dense factorisation for moderate n, or is reported with reliable = FALSE, never silently trusted.

The completeness certificate turns two such counts into a proof. By Kahan’s theorem every returned value θi\theta_i has its own (distinct) eigenvalue within ρ=∥R∥F/σmin(X)\rho = \|R\|_F / \sigma_{\min}(X) of it, where R holds the certificate’s residuals. For a largest() target with returned edge θmin\theta_{\min}, if exactly k eigenvalues exceed θmin−ρ−m\theta_{\min} - \rho - m (the margin m exceeds the factorisation’s backward error), those k eigenvalues are the returned ones and the set is complete. If at least k eigenvalues exceed θmin+ρ+m\theta_{\min} + \rho + m, a value is provably missing.

fit_i <- eig_partial(S, k = 5, method = lanczos(), seed = 1)
sort(values(fit_i), decreasing = TRUE)
#> [1] 9 9 7 7 7
fit_i$certificate$target_completeness
#> [1] "inertia_verified"
fit_i$certificate$completeness[c("inertia_method", "rho", "count_upper",
                                 "factorizations", "repaired")]
#> $inertia_method
#> [1] "diagonal"
#> 
#> $rho
#> [1] 3.382558e-12
#> 
#> $count_upper
#> [1] 5
#> 
#> $factorizations
#> [1] 3
#> 
#> $repaired
#> [1] TRUE

Here the first Krylov answer missed a copy of 7; the count proved it, the repair recovered it, and the repaired set was then counted complete.

certificate$target_completeness is one of:

  • "inertia_verified": the count proves the returned set is the requested one (completeness$repaired records whether a repair was needed);
  • "inertia_failed": the count proves a more-preferred eigenvalue is missing and the repair budget (options(eigencore.completeness_max_rounds = 3)) did not fix it; passed is then FALSE although every residual passes (residual_passed keeps that verdict, target_passed is FALSE);
  • "inertia_inconclusive": the k-th and (k+1)-th eigenvalues are closer than the residual uncertainty (for example k cuts through a multiple eigenvalue), so the target edge is not separated; no claim is made either way and passed is unaffected;
  • "probed": the probe found nothing beyond the returned edge;
  • "repaired": the probe found a missing value and the repaired set then probed clean;
  • "failed": a probe intruder remained after the repair budget; passed is FALSE;
  • "exact": a full-spectrum route (dense LAPACK, tridiagonal, analytic grid Laplacian) selected from every eigenvalue, so the set is complete by construction;
  • "inconclusive" (nonsymmetric probe): the probe could not reach a verdict (it did not converge, or the returned vectors are linearly dependent, as for a defective or repeated eigenvalue a single-vector Krylov method cannot resolve); passed is FALSE;
  • "not_checked": no check applied (both-ends targets, complex matrix-free nonsymmetric operators, a residual certificate that already failed, certify = FALSE, or completeness = "none").

Nonsymmetric problems

Dense LAPACK routes (dgeev, zgeev, QZ for pencils) select from the full spectrum and report "exact". Krylov-Schur (Arnoldi) results, including eigs(), shift-invert (probed on (A−σI)−1(A - \sigma I)^{-1}), matrix-free operators and diagonal-B sparse pencils (probed on B−1AB^{-1}A), are probed: with QQ an orthonormal basis of the returned vectors, T=QTAQT = Q^T A Q and R=AQ−QTR = AQ - QT, the eigenvalues missing from the returned set are those of the compression (I−QQT)A(I−QQT)(I - QQ^T) A (I - QQ^T) when R=0R = 0. A deflated Krylov-Schur run on that compression (fixed-seed start) looks for an eigenvalue more preferred than the returned edge by more than the margin max⁡(tol|λ|,2∥R∥F)\max(\mathrm{tol}\,|\lambda|, 2\|R\|_F) plus its own Ritz residual; one found is merged in by Rayleigh-Ritz on the enlarged subspace ("repaired"), otherwise the result is "failed". A clean probe ("probed") is evidence, not proof, and the margin covers the normal (or well-conditioned) case only: for a non-normal matrix an eigenvalue can move by up to its condition number times ∥R∥\|R\| (a Jordan block of size m by ∥R∥1/m\|R\|^{1/m}), which the probe does not bound. Small complements (64 or fewer dimensions) are spanned completely.

The check is chosen by lanczos(completeness = ) or options(eigencore.target_completeness = ): "auto" (the default) counts edge targets when the matrix is explicit and the predicted factorisation time (from CHOLMOD’s symbolic analysis, or n3/3n^3/3 flops for dense) is at most max(0.5 s, solve time) (options eigencore.completeness_inertia_seconds and eigencore.completeness_inertia_ratio), and probes otherwise; "inertia" always counts when it can; "probe" always probes; "none" switches the check off. certificate$completeness$inertia_gate records why the count was skipped. Matrices whose factors fill catastrophically (large random sparse graphs) are left to the probe; banded, grid and other mesh-like sparse matrices factor cheaply and are counted.

Every eigenvalue in an interval

The same counts drive the interval(a, b) target: eig_partial() counts the eigenvalues in the closed interval [a, b] first (so k can be left out), solves for exactly that many, and certifies the set with the count. For a sparse matrix the interval is split into slices by further counts (“spectrum slicing”); each slice is solved by shift-invert Lanczos at its centre and the slices are merged.

L1 <- Matrix::bandSparse(30, k = c(0, 1),
                         diagonals = list(rep(2, 30), rep(-1, 29)),
                         symmetric = TRUE)
L2 <- kronecker(L1, Matrix::Diagonal(30)) + kronecker(Matrix::Diagonal(30), L1)
op <- options(eigencore.interval_dense_limit = 0)  # show the sparse route
fit_iv <- eig_partial(L2, target = interval(1, 1.5))
options(op)
length(values(fit_iv))
#> [1] 41
fit_iv$certificate$target_completeness
#> [1] "inertia_verified"
fit_iv$interval[c("count", "slices", "factorizations")]
#> $count
#> [1] 41
#> 
#> $slices
#> [1] 1
#> 
#> $factorizations
#> [1] 3

The interval is closed: an eigenvalue numerically on an end point (inside the zero band of the factorisation there) counts as inside. With residual bound ρ\rho from Kahan’s theorem, if all m returned values lie inside [a,b][a, b] by more than ρ\rho, they are provably the m eigenvalues of the interval ("inertia_verified"). A value within ρ\rho of an end point is listed in completeness$boundary_ambiguous; one recount on the interval widened by ρ\rho settles it (the same count proves the set; a larger one means an eigenvalue sits just outside within the residual bound and the result is "inertia_inconclusive"). A returned count different from the interval count is "inertia_failed" and passed = FALSE. Dense matrices use LAPACK’s dsyevr with a value range instead, which selects the eigenvalues by the same kind of Sturm count on the tridiagonal form ("exact").

Comparing eigencore certificates to RSpectra diagnostics

RSpectra::eigs_sym() returns nconv and niter but does not return residuals, backward errors, or an orthogonality measure. The eigencore shim (eigencore::eigs_sym()) returns the same RSpectra-shaped list with two added fields:

res <- eigs_sym(A, k = 5, which = "LA")
names(res)
#> [1] "values"      "vectors"     "nconv"       "niter"       "nops"       
#> [6] "certificate" "diagnostics"
res$certificate
#> eigencore certificate
#>   passed: TRUE 
#>   tolerance: 1e-08 
#>   type: residual_backward_error 
#>   norm bound: two_norm_lower_bound+identity_exact 
#>   norm source: ritz+identity 
#>   scale estimated: FALSE 
#>   max residual: 2.308902e-09 
#>   max backward error: 2.304029e-10 
#>   max orthogonality loss: 8.881784e-16 
#>   orthogonality tolerance: 1.490116e-08 
#>   orthogonality required: TRUE 
#>   target completeness: inertia_verified

Code already written against RSpectra::eigs_sym() ignores certificate and diagnostics silently; new code can opt in to certified results without changing call sites.

Cheat sheet

You see What it means What to do
passed = TRUE All checks required by this certificate type passed; the 2-norm scale was exact or a lower bound, so the backward error is not under-stated. Use the values/vectors within the stated certificate scope.
passed = FALSE, failed_indices non-empty Some pairs hit maxit before converging. Increase maxit, raise tol, or request a wider k.
passed = FALSE, norm_bound_type is two_norm_lower_bound, residuals tiny The norm lower bound may be weak (e.g. smallest targets of a matrix-free operator). Supply metadata$two_norm if ||A||_2 is known, or use a built-in matrix class.
max_orthogonality_loss near sqrt(eps) but residuals tiny Iterative drift; clustered eigenvalues at risk of duplicates. Increase maxit; check whether there are repeated eigenvalues.
failed_indices includes the first returned pairs Some leading returned pairs exceed the tolerance. Inspect convergence_history; increase maxit if the errors are still declining.
passed = FALSE, residual_passed = TRUE, target_completeness = "inertia_failed" An eigenvalue count proves a more-preferred eigenvalue is missing from the returned set. Use a block method with a block at least the multiplicity (lanczos(block = )), raise eigencore.completeness_max_rounds, or a dense solve.
target_completeness = "inertia_inconclusive" The target edge cuts through a cluster (closer than the residual bound); every returned pair is still certified. Request a k that does not split the cluster, or tighten tol.
passed = FALSE, residual_passed = TRUE, target_completeness = "failed" Every pair is accurate, but a more-preferred eigenvalue (e.g. a missed copy of a repeated value) lies outside the returned set. Use a block method with a block at least the multiplicity (lanczos(block = )), raise eigencore.completeness_max_rounds, or a dense solve.
failed_indices includes the last returned pairs Some trailing returned pairs exceed the tolerance. Inspect the spectral gap; a wider k may help when the target boundary cuts through a cluster.

The certificate records which numerical checks were run, the scale used for each backward error, and whether those checks passed. Use those fields together with the solver plan and the requirements of your downstream analysis.

Where to go next