The Correlation Engine's eigenvalue panel needs an eigendecomposition of an N×N symmetric real matrix where N goes up to 250. The obvious move is to pull mathjs (or numjs, or eigen.js, or one of the WebAssembly-backed BLAS wrappers) and call eig(matrix). We rejected that and wrote a ~150-line cyclic Jacobi routine in plain TypeScript instead. This post explains the trade-off, because it's the second time we've made this call (the first was in correlation.ts itself, where we hand-wrote Pearson rather than depending on mathjs's stats module).
The argument for a dependency is real: battle-tested numerical code, almost certainly faster on large matrices, almost certainly more numerically stable on edge cases the hand-rolled version hasn't seen. The argument against, in this specific context, is bundle-size and surface-area discipline. mathjs is ~600KB minified-and-gzipped including the LU/QR/eigen routines; we use exactly one routine (symmetric-real eigendecomposition) on exactly one input shape (correlation matrices in [-1, 1] with a unit diagonal). Pulling 600KB of bundle for one routine on one shape is the wrong trade — the module already runs server-side so client bundle isn't load-bearing for users, but it IS load-bearing for our own cold-start latency, and pulling mathjs server-side adds module-resolution cost on every Lambda-style cold boot.
Cyclic Jacobi is the right algorithm for this shape. The matrix is symmetric real (correlation matrices always are), small enough that O(N²) per sweep and O(N³ log N) total isn't dominant on N ≤ 250 (we measure ~50ms on N=100, ~400ms on N=250 in plain Node), and the convergence behaviour is provably good — quadratic near the solution, with a tolerance-based exit. Around 50 sweeps is enough for any well-conditioned symmetric matrix; we cap at 50 and surface the converged flag in the result so a pathological input is visible, not silent.
Two stability tweaks the textbook algorithm omits. First, we sanitise the input matrix before decomposition: any null cells (which the correlation-engine produces when a pair shares fewer than three observations after a regime mask) become zero off-diagonal and one on-diagonal — the principle is 'no information about the relationship,' which mathematically reads as zero correlation. We also clip out-of-range entries to [-1, 1] (Pearson can drift slightly past ±1 from floating-point accumulation) and symmetrise the matrix as a defensive step against null-fill asymmetry. Jacobi requires perfect symmetry to converge cleanly; small floating-point asymmetry can cause the algorithm to oscillate. Second, after convergence we clip eigenvalues to [0, ∞) before computing variance shares and entropy. Symmetric correlation matrices have non-negative eigenvalues in theory, but Jacobi can leave residuals like −1e-14 that would crash the log in the entropy calculation. The clip is rounding, not truth-correction.
Vortex Legacy
Vortex Research Suite modules produce quantitative diagnostic assessments only. They do not constitute investment advice, price prediction, or buy/sell recommendations.
We tested it. Eight unit cases: identity gives N uniform eigenvalues, ENB equals N exactly. The 2×2 [[1, r], [r, 1]] case has known eigenvalues 1+r and 1-r — checked at r = 0.6. Null cells handled correctly. ENB collapses toward 1 as the matrix saturates at high r. Topology check: the layout-derived property test asserts that high-correlation pairs end up closer than low-correlation pairs after force-directed layout converges. The implementation passes all eight; the test suite is in tests/unit/correlation-engine-eigen.test.ts.
The stance is the same as in correlation.ts: hand-roll when the math is small enough to read in one sitting, the input shape is narrow, and the dependency would be heavier than the algorithm. mathjs is the right call for projects that need ten matrix operations on six input shapes. For one operation on one shape, the bundle isn't worth it.