Below is a symmetric 3×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.');
<!-- 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.');
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.
Comments
Loading comments...