Introduction

Every square matrix describes a transformation: stretch, shrink, rotate, shear. Hidden inside any symmetric matrix — one that equals its own transpose — are special directions called eigenvectors along which the transformation acts as a pure stretch. The stretch factors are the eigenvalues, and knowing them unlocks the geometry of the whole transformation.

Finding eigenvalues is one of the central tasks of numerical computing. Physicists need them for quantum energy levels, engineers for vibration modes, data scientists for dimensionality reduction. The naive approach — solving the characteristic polynomial det⁡(A−λI)=0\det(A - \lambda I) = 0 — becomes numerically unstable and computationally brutal for large matrices.

In 1846, the mathematician Carl Gustav Jacob Jacobi described a beautifully simple alternative: apply a sequence of plane rotations, each one annihilating a single off-diagonal entry, until the matrix is diagonal. When the dust settles, the diagonal entries are the eigenvalues. The algorithm is still in active use today, nearly 180 years later.

Try It: Watch Off-Diagonals Shrink

Below is a symmetric 3×33 \times 3 matrix. Each Step button applies one Jacobi rotation, targeting the largest off-diagonal entry and annihilating it. Watch the off-diagonal values shrink toward zero while the diagonal converges to the eigenvalues.

<!-- Demo layout — matrix table + stats + controls -->
<p class="hint">Each cell shows one entry of a symmetric 3×3 matrix. Red cells are the largest off-diagonal entry — the next rotation will zero it. Click <b>Step</b> to apply one Jacobi rotation, or <b>Sweep</b> to zero all off-diagonal entries in one pass. The diagonal converges to the eigenvalues.</p>
<div class="matrix-wrap">
  <div class="matrix-label">Current matrix A</div>
  <table id="matrix" class="matrix"></table>
</div>
<div class="stats-row">
  <span class="stat-item"><span class="stat-label">Off-diagonal norm:</span> <span id="off-norm" class="stat-val">—</span></span>
  <span class="stat-item"><span class="stat-label">Steps:</span> <span id="step-count" class="stat-val">0</span></span>
</div>
<div class="status" id="status">Press Step or Sweep to begin.</div>
<div class="btns">
  <button id="btn-step" type="button">Step</button>
  <button id="btn-sweep" type="button">Sweep</button>
  <button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Matrix cells are color-coded — diagonal green, largest off-diagonal red, others light blue-grey */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.5; }
.matrix-wrap { margin: .4rem 0 .6rem; }
.matrix-label { font-size: .8rem; font-weight: 600; color: #5a7088; margin-bottom: .3rem; text-transform: uppercase; letter-spacing: .04em; }
.matrix { border-collapse: separate; border-spacing: 4px; }
.matrix td { width: 56px; height: 40px; text-align: center; font: 600 13px ui-monospace, monospace;
             border-radius: 6px; border: 1px solid #cdd9e3; transition: background .3s, color .3s; }
.matrix td.diag { background: #e0eedb; color: #1a5c2a; border-color: #a8d4a0; }
.matrix td.off-hi { background: #fde8e8; color: #a01010; border-color: #f5b3b3; }
.matrix td.off { background: #f0f4f8; color: #334; border-color: #cdd9e3; }
.stats-row { display: flex; gap: 1.2rem; margin: .3rem 0 .5rem; flex-wrap: wrap; }
.stat-item { font-size: .88rem; }
.stat-label { color: #555; }
.stat-val { font-weight: 700; color: #1d3557; }
.status { font-size: .95rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; color: #333; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d5f9e; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: not-allowed; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Jacobi eigenvalue algorithm: iteratively rotate a real symmetric matrix
// to zero its off-diagonal entries, revealing eigenvalues on the diagonal.

const N = 3;
let A = [];   // current working matrix (copy updated each step)
let steps = 0;
const EPS = 1e-9; // tolerance for convergence and near-zero denominators

// Generate a random symmetric matrix with entries in [-4, 4] for diagonal and [-3, 3] off-diagonal
function randomSymmetric() {
  const M = [];
  for (let i = 0; i < N; i++) {
    M[i] = [];
    for (let j = 0; j < N; j++) M[i][j] = 0;
  }
  for (let i = 0; i < N; i++) {
    M[i][i] = Math.round((Math.random() * 8 - 4) * 10) / 10;
    for (let j = i + 1; j < N; j++) {
      const v = Math.round((Math.random() * 6 - 3) * 10) / 10;
      M[i][j] = v;
      M[j][i] = v;
    }
  }
  return M;
}

// Frobenius norm of off-diagonal part — measures how far we are from diagonal
function offNorm(M) {
  let s = 0;
  for (let i = 0; i < N; i++)
    for (let j = 0; j < N; j++)
      if (i !== j) s += M[i][j] * M[i][j];
  return Math.sqrt(s);
}

// Find the off-diagonal entry with largest absolute value — the next rotation target
function findPivot(M) {
  let p = 0, q = 1, best = 0;
  for (let i = 0; i < N; i++)
    for (let j = i + 1; j < N; j++)
      if (Math.abs(M[i][j]) > best) { best = Math.abs(M[i][j]); p = i; q = j; }
  return { p, q };
}

// Apply one Jacobi rotation to zero entry (p,q); return updated matrix or false if already zero
function jacobiStep(M) {
  const { p, q } = findPivot(M);
  if (Math.abs(M[p][q]) < EPS) return false; // already zero, nothing to do
  const diff = M[q][q] - M[p][p];
  let theta;
  if (Math.abs(M[p][q]) < EPS * Math.abs(diff)) {
    theta = M[p][q] / diff;
  } else {
    const tau = diff / (2 * M[p][q]);
    theta = tau >= 0
      ? 1 / (tau + Math.sqrt(1 + tau * tau))
      : 1 / (tau - Math.sqrt(1 + tau * tau));
  }
  const c = 1 / Math.sqrt(1 + theta * theta);
  const s = theta * c;
  // Update all rows/columns affected by the (p,q) rotation
  const newM = M.map(r => r.slice());
  for (let r = 0; r < N; r++) {
    if (r === p || r === q) continue;
    newM[r][p] = c * M[r][p] - s * M[r][q];
    newM[p][r] = newM[r][p];
    newM[r][q] = s * M[r][p] + c * M[r][q];
    newM[q][r] = newM[r][q];
  }
  newM[p][p] = c * c * M[p][p] - 2 * s * c * M[p][q] + s * s * M[q][q];
  newM[q][q] = s * s * M[p][p] + 2 * s * c * M[p][q] + c * c * M[q][q];
  newM[p][q] = 0;
  newM[q][p] = 0;
  return newM;
}

// Render the matrix table with color coding and update stats
function renderMatrix(M) {
  const tbl = document.getElementById('matrix');
  tbl.innerHTML = '';
  const { p, q } = findPivot(M);
  for (let i = 0; i < N; i++) {
    const tr = document.createElement('tr');
    for (let j = 0; j < N; j++) {
      const td = document.createElement('td');
      td.textContent = M[i][j].toFixed(3);
      if (i === j) td.className = 'diag';
      else if (i === p && j === q || i === q && j === p) td.className = 'off-hi';
      else td.className = 'off';
      tr.appendChild(td);
    }
    tbl.appendChild(tr);
  }
  const norm = offNorm(M);
  document.getElementById('off-norm').textContent = norm.toFixed(5);
  document.getElementById('step-count').textContent = steps;
}

function setStatus(t, cls) {
  const el = document.getElementById('status');
  el.textContent = t;
  el.className = 'status ' + (cls || '');
}

function isConverged(M) { return offNorm(M) < 1e-6; }

// Step button — apply one rotation and refresh display
document.getElementById('btn-step').addEventListener('click', () => {
  if (isConverged(A)) { setStatus('Converged! Diagonal entries are the eigenvalues.', 'ok'); return; }
  const next = jacobiStep(A);
  if (!next) { setStatus('Converged! Diagonal entries are the eigenvalues.', 'ok'); return; }
  A = next;
  steps++;
  renderMatrix(A);
  if (isConverged(A)) setStatus('Converged! Diagonal entries are the eigenvalues.', 'ok');
  else setStatus(i18nFmt('Applied %n% rotation(s) — off-diagonals still shrinking.', { n: steps }), 'info');
});

// Sweep button — apply n*(n-1)/2 rotations (one full sweep) and refresh
document.getElementById('btn-sweep').addEventListener('click', () => {
  const sweepSize = (N * (N - 1)) / 2;
  let did = 0;
  for (let k = 0; k < sweepSize; k++) {
    if (isConverged(A)) break;
    const next = jacobiStep(A);
    if (!next) break;
    A = next;
    steps++;
    did++;
  }
  renderMatrix(A);
  if (isConverged(A)) setStatus('Converged! Diagonal entries are the eigenvalues.', 'ok');
  else setStatus(i18nFmt('Completed a sweep of %n% rotation(s).', { n: did }), 'info');
});

// Reset button — generate a new random symmetric matrix
document.getElementById('btn-reset').addEventListener('click', () => {
  A = randomSymmetric();
  steps = 0;
  renderMatrix(A);
  setStatus('Press Step or Sweep to begin.');
});

// Initialize with a random matrix on page load
A = randomSymmetric();
renderMatrix(A);
setStatus('Press Step or Sweep to begin.');
Result
Console

After enough steps, the matrix is essentially diagonal. The three diagonal entries are the eigenvalues — the stretch factors of the original transformation along its principal axes. Press Reset to start over with a fresh random matrix.

The Real Complexity

How does the Jacobi algorithm behave as a computation?

  • One rotation, one entry zeroed. A Jacobi rotation is a 2×22 \times 2 rotation embedded in an n×nn \times n identity matrix. Multiplying AA on both sides takes O(n)O(n) work and drives one off-diagonal entry to exactly zero — but may slightly disturb others already zeroed.
  • A sweep costs O(n2)O(n^2) rotations, O(n3)O(n^3) arithmetic. One complete sweep through all n(n−1)2\frac{n(n-1)}{2} off-diagonal positions is called a sweep, and costs O(n3)O(n^3) floating-point operations — the same order as a matrix multiplication.
  • Convergence is quadratic (and proven). Jacobi proved in 1846 that the sum of squares of off-diagonal entries decreases by a constant factor each sweep. The method always converges for symmetric real matrices. Once the off-diagonal norm falls below a threshold ε\varepsilon, the diagonal entries approximate eigenvalues to within O(ε)O(\varepsilon).
  • Modern alternatives exist. For large nn, the QR algorithm (1961, Francis and Kublanovskaya) typically runs faster in practice. But Jacobi remains the preferred choice when high accuracy is needed for small dense matrices, and its parallel variants excel on modern hardware.

The key theoretical point: the sum S=∑i≠jaij2S = \sum_{i \neq j} a_{ij}^{2} after zeroing entry (p,q)(p, q) satisfies S′≤S⋅(1−2n(n−1))S' \leq S \cdot \bigl(1 - \tfrac{2}{n(n-1)}\bigr), guaranteeing geometric convergence. Dimensionality reduction via PCA relies on exactly this kind of eigensolver.

Where It Matters

Finding eigenvalues of symmetric matrices is one of the most common operations in quantitative science, and Jacobi's rotations underpin many of those computations:

  • Quantum mechanics: energy levels of a physical system are eigenvalues of its Hamiltonian matrix. Small systems are still diagonalized by Jacobi methods.
  • Structural engineering: the natural vibration frequencies of a bridge or aircraft wing are eigenvalues of a stiffness-mass matrix. Identifying them prevents resonance disasters.
  • Principal Component Analysis (PCA): the directions of maximum variance in a dataset are eigenvectors of its covariance matrix. Jacobi-style sweeps are used when the matrix is small and accuracy matters.
  • Computer graphics: inertia tensors, stress tensors, and deformation gradients are all symmetric matrices whose eigenvectors define principal axes for physical simulation.
  • Signal processing: the signal and noise subspaces in MUSIC and ESPRIT algorithms for direction-of-arrival estimation are found by eigendecomposition of covariance matrices.

Any time a problem reduces to "find the principal axes of this symmetric object," you are one Jacobi sweep away from the answer. The algorithm's beauty lies in its directness: it does not approximate eigenvalues — it becomes them.

Conclusion

The Jacobi eigenvalue algorithm is a rare thing in computing: a method that is simultaneously simple, provably correct, and still useful after nearly two centuries. It asks a deceptively modest question — "what if I just rotate away the biggest off-diagonal entry, over and over?" — and the answer is that the matrix diagonalizes, revealing its eigenvalues one rotation at a time.

From quantum energy levels to the principal components of your data, eigenvalues of symmetric matrices are everywhere. The next time a physicist solves a Hamiltonian or a data scientist runs PCA, somewhere in the machinery is the same geometric insight Jacobi had in 1846: rotate the coordinate system until the transformation has nowhere left to hide.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/jacobi-eigenvalue/Content licensed under CC BY-NC 4.0.