<div class="controls">
<label>Matrix:
<select id="matrixSelect">
<option value="asym">A = [[3,1],[1,2]] — ratio 3/2</option>
<option value="slow">A = [[2,1],[0,1.9]] — close eigenvalues</option>
<option value="big">A = [[4,2],[1,3]] — ratio 4/3</option>
</select>
</label>
<button id="stepBtn" type="button">Step</button>
<button id="runBtn" type="button">Run</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="320" height="320"></canvas>
<div id="info" class="info">
<span id="iterLabel">Iteration: 0</span>
<span id="ratioLabel">Rayleigh quotient: —</span>
<span id="convLabel"></span>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .7rem; }
label { font-size: .88rem; color: #444; }
select { font-size: .88rem; padding: .3rem .5rem; border: 1px solid #ccc; border-radius: 6px; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border: 1px solid #dde3ea; border-radius: 10px; background: #f8fafc; }
.info { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: .55rem; font-size: .85rem; color: #444; }
#convLabel { color: #0a7d33; font-weight: 600; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Power iteration: v_{k+1} = A * v_k / ||A * v_k||
// Converges to the dominant eigenvector at rate |lambda2/lambda1| per step.
const MATRICES = {
asym: { A: [[3,1],[1,2]], label: '[[3,1],[1,2]]' },
slow: { A: [[2,1],[0,1.9]], label: '[[2,1],[0,1.9]]' },
big: { A: [[4,2],[1,3]], label: '[[4,2],[1,3]]' }
};
// Multiply 2x2 matrix by 2-vector
function matvec(A, v) {
return [
A[0][0]*v[0] + A[0][1]*v[1],
A[1][0]*v[0] + A[1][1]*v[1]
];
}
function norm(v) { return Math.sqrt(v[0]*v[0] + v[1]*v[1]); }
function normalize(v) { const n = norm(v); return [v[0]/n, v[1]/n]; }
function dot(a, b) { return a[0]*b[0] + a[1]*b[1]; }
// Rayleigh quotient: v^T A v / v^T v (v is unit vector so denominator = 1)
function rayleigh(A, v) {
const Av = matvec(A, v);
return dot(v, Av);
}
// Analytical dominant eigenvector for 2x2 via power of imagination:
// We compute it via many power iterations internally (100 steps).
function trueDominant(A) {
let v = [1, 0.5];
v = normalize(v);
for (let i = 0; i < 200; i++) {
const w = matvec(A, v);
const n = norm(w);
if (n < 1e-12) break;
v = [w[0]/n, w[1]/n];
}
return v;
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const CX = W/2, CY = H/2;
const SCALE = 110; // pixels per unit
let A, dominant, v, iteration, animId, converged;
function getMatrix() {
const key = document.getElementById('matrixSelect').value;
return MATRICES[key].A;
}
function reset() {
if (animId) { clearInterval(animId); animId = null; }
A = getMatrix();
dominant = trueDominant(A);
// start vector at 45 degrees, somewhat away from dominant
v = normalize([1, 0.3]);
iteration = 0;
converged = false;
document.getElementById('convLabel').textContent = '';
updateInfo();
draw();
}
function step() {
if (converged) return;
const w = matvec(A, v);
const n = norm(w);
const vNew = [w[0]/n, w[1]/n];
const change = norm([vNew[0]-v[0], vNew[1]-v[1]]);
// Also check flipped direction (eigenvector sign is arbitrary)
const changeFlip = norm([vNew[0]+v[0], vNew[1]+v[1]]);
const minChange = Math.min(change, changeFlip);
v = vNew;
iteration++;
if (minChange < 1e-6 && iteration > 2) {
converged = true;
document.getElementById('convLabel').textContent =
fmt('✓ Converged after %iteration% iterations', { iteration: iteration });
if (animId) { clearInterval(animId); animId = null; }
}
updateInfo();
draw();
}
function updateInfo() {
document.getElementById('iterLabel').textContent = fmt('Iteration: %iteration%', { iteration: iteration });
if (iteration === 0) {
document.getElementById('ratioLabel').textContent = 'Rayleigh quotient: —';
} else {
const rq = rayleigh(A, v);
document.getElementById('ratioLabel').textContent =
fmt('Rayleigh quotient: %rq%', { rq: rq.toFixed(4) });
}
}
function drawArrow(x1, y1, x2, y2, color, dashed) {
ctx.save();
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = dashed ? 1.5 : 2.5;
if (dashed) ctx.setLineDash([6, 4]);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.setLineDash([]);
// Arrowhead
const angle = Math.atan2(y2 - y1, x2 - x1);
const ah = 10;
ctx.beginPath();
ctx.moveTo(x2, y2);
ctx.lineTo(x2 - ah*Math.cos(angle-0.35), y2 - ah*Math.sin(angle-0.35));
ctx.lineTo(x2 - ah*Math.cos(angle+0.35), y2 - ah*Math.sin(angle+0.35));
ctx.closePath();
ctx.fill();
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, W, H);
// Grid
ctx.save();
ctx.strokeStyle = '#e0e7ef';
ctx.lineWidth = 1;
for (let x = 0; x <= W; x += W/8) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y <= H; y += H/8) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
ctx.restore();
// Axes
ctx.save();
ctx.strokeStyle = '#94a3b8';
ctx.lineWidth = 1.2;
ctx.beginPath(); ctx.moveTo(0, CY); ctx.lineTo(W, CY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(CX, 0); ctx.lineTo(CX, H); ctx.stroke();
ctx.restore();
// Dominant eigenvector (red dashed, both directions)
const dScale = SCALE * 1.3;
drawArrow(CX, CY,
CX + dominant[0]*dScale, CY - dominant[1]*dScale,
'#e63946', true);
drawArrow(CX, CY,
CX - dominant[0]*dScale, CY + dominant[1]*dScale,
'#e63946', true);
// Current vector (blue solid)
drawArrow(CX, CY,
CX + v[0]*SCALE, CY - v[1]*SCALE,
'#1d3557', false);
// Angle label
const angle = Math.abs(Math.acos(Math.min(1, Math.abs(dot(v, dominant)))));
ctx.save();
ctx.fillStyle = '#555';
ctx.font = '12px system-ui';
ctx.fillText(fmt('∠ to eigenvector: %v%°', { v: (angle * 180/Math.PI).toFixed(2) }), 8, 20);
ctx.restore();
// Legend
ctx.save();
ctx.font = '12px system-ui';
// Blue
ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.moveTo(8, H-30); ctx.lineTo(35, H-30); ctx.stroke();
ctx.fillStyle = '#1d3557';
ctx.fillText('current vector', 40, H-26);
// Red dashed
ctx.save();
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 1.5;
ctx.setLineDash([6,4]);
ctx.beginPath(); ctx.moveTo(8, H-10); ctx.lineTo(35, H-10); ctx.stroke();
ctx.restore();
ctx.fillStyle = '#e63946';
ctx.fillText('dominant eigenvector', 40, H-6);
ctx.restore();
}
document.getElementById('matrixSelect').addEventListener('change', reset);
document.getElementById('stepBtn').addEventListener('click', step);
document.getElementById('runBtn').addEventListener('click', function() {
if (animId) { clearInterval(animId); animId = null; return; }
if (converged) { reset(); }
animId = setInterval(function() { step(); if (converged) { clearInterval(animId); animId = null; } }, 120);
});
document.getElementById('resetBtn').addEventListener('click', reset);
reset();
<div class="controls">
<label>Matrix:
<select id="matrixSelect">
<option value="asym">A = [[3,1],[1,2]] — ratio 3/2</option>
<option value="slow">A = [[2,1],[0,1.9]] — close eigenvalues</option>
<option value="big">A = [[4,2],[1,3]] — ratio 4/3</option>
</select>
</label>
<button id="stepBtn" type="button">Step</button>
<button id="runBtn" type="button">Run</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="320" height="320"></canvas>
<div id="info" class="info">
<span id="iterLabel">Iteration: 0</span>
<span id="ratioLabel">Rayleigh quotient: —</span>
<span id="convLabel"></span>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .7rem; }
label { font-size: .88rem; color: #444; }
select { font-size: .88rem; padding: .3rem .5rem; border: 1px solid #ccc; border-radius: 6px; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border: 1px solid #dde3ea; border-radius: 10px; background: #f8fafc; }
.info { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: .55rem; font-size: .85rem; color: #444; }
#convLabel { color: #0a7d33; font-weight: 600; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Power iteration: v_{k+1} = A * v_k / ||A * v_k||
// Converges to the dominant eigenvector at rate |lambda2/lambda1| per step.
const MATRICES = {
asym: { A: [[3,1],[1,2]], label: '[[3,1],[1,2]]' },
slow: { A: [[2,1],[0,1.9]], label: '[[2,1],[0,1.9]]' },
big: { A: [[4,2],[1,3]], label: '[[4,2],[1,3]]' }
};
// Multiply 2x2 matrix by 2-vector
function matvec(A, v) {
return [
A[0][0]*v[0] + A[0][1]*v[1],
A[1][0]*v[0] + A[1][1]*v[1]
];
}
function norm(v) { return Math.sqrt(v[0]*v[0] + v[1]*v[1]); }
function normalize(v) { const n = norm(v); return [v[0]/n, v[1]/n]; }
function dot(a, b) { return a[0]*b[0] + a[1]*b[1]; }
// Rayleigh quotient: v^T A v / v^T v (v is unit vector so denominator = 1)
function rayleigh(A, v) {
const Av = matvec(A, v);
return dot(v, Av);
}
// Analytical dominant eigenvector for 2x2 via power of imagination:
// We compute it via many power iterations internally (100 steps).
function trueDominant(A) {
let v = [1, 0.5];
v = normalize(v);
for (let i = 0; i < 200; i++) {
const w = matvec(A, v);
const n = norm(w);
if (n < 1e-12) break;
v = [w[0]/n, w[1]/n];
}
return v;
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const CX = W/2, CY = H/2;
const SCALE = 110; // pixels per unit
let A, dominant, v, iteration, animId, converged;
function getMatrix() {
const key = document.getElementById('matrixSelect').value;
return MATRICES[key].A;
}
function reset() {
if (animId) { clearInterval(animId); animId = null; }
A = getMatrix();
dominant = trueDominant(A);
// start vector at 45 degrees, somewhat away from dominant
v = normalize([1, 0.3]);
iteration = 0;
converged = false;
document.getElementById('convLabel').textContent = '';
updateInfo();
draw();
}
function step() {
if (converged) return;
const w = matvec(A, v);
const n = norm(w);
const vNew = [w[0]/n, w[1]/n];
const change = norm([vNew[0]-v[0], vNew[1]-v[1]]);
// Also check flipped direction (eigenvector sign is arbitrary)
const changeFlip = norm([vNew[0]+v[0], vNew[1]+v[1]]);
const minChange = Math.min(change, changeFlip);
v = vNew;
iteration++;
if (minChange < 1e-6 && iteration > 2) {
converged = true;
document.getElementById('convLabel').textContent =
fmt('✓ Converged after %iteration% iterations', { iteration: iteration });
if (animId) { clearInterval(animId); animId = null; }
}
updateInfo();
draw();
}
function updateInfo() {
document.getElementById('iterLabel').textContent = fmt('Iteration: %iteration%', { iteration: iteration });
if (iteration === 0) {
document.getElementById('ratioLabel').textContent = 'Rayleigh quotient: —';
} else {
const rq = rayleigh(A, v);
document.getElementById('ratioLabel').textContent =
fmt('Rayleigh quotient: %rq%', { rq: rq.toFixed(4) });
}
}
function drawArrow(x1, y1, x2, y2, color, dashed) {
ctx.save();
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = dashed ? 1.5 : 2.5;
if (dashed) ctx.setLineDash([6, 4]);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.setLineDash([]);
// Arrowhead
const angle = Math.atan2(y2 - y1, x2 - x1);
const ah = 10;
ctx.beginPath();
ctx.moveTo(x2, y2);
ctx.lineTo(x2 - ah*Math.cos(angle-0.35), y2 - ah*Math.sin(angle-0.35));
ctx.lineTo(x2 - ah*Math.cos(angle+0.35), y2 - ah*Math.sin(angle+0.35));
ctx.closePath();
ctx.fill();
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, W, H);
// Grid
ctx.save();
ctx.strokeStyle = '#e0e7ef';
ctx.lineWidth = 1;
for (let x = 0; x <= W; x += W/8) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
}
for (let y = 0; y <= H; y += H/8) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
}
ctx.restore();
// Axes
ctx.save();
ctx.strokeStyle = '#94a3b8';
ctx.lineWidth = 1.2;
ctx.beginPath(); ctx.moveTo(0, CY); ctx.lineTo(W, CY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(CX, 0); ctx.lineTo(CX, H); ctx.stroke();
ctx.restore();
// Dominant eigenvector (red dashed, both directions)
const dScale = SCALE * 1.3;
drawArrow(CX, CY,
CX + dominant[0]*dScale, CY - dominant[1]*dScale,
'#e63946', true);
drawArrow(CX, CY,
CX - dominant[0]*dScale, CY + dominant[1]*dScale,
'#e63946', true);
// Current vector (blue solid)
drawArrow(CX, CY,
CX + v[0]*SCALE, CY - v[1]*SCALE,
'#1d3557', false);
// Angle label
const angle = Math.abs(Math.acos(Math.min(1, Math.abs(dot(v, dominant)))));
ctx.save();
ctx.fillStyle = '#555';
ctx.font = '12px system-ui';
ctx.fillText(fmt('∠ to eigenvector: %v%°', { v: (angle * 180/Math.PI).toFixed(2) }), 8, 20);
ctx.restore();
// Legend
ctx.save();
ctx.font = '12px system-ui';
// Blue
ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.moveTo(8, H-30); ctx.lineTo(35, H-30); ctx.stroke();
ctx.fillStyle = '#1d3557';
ctx.fillText('current vector', 40, H-26);
// Red dashed
ctx.save();
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 1.5;
ctx.setLineDash([6,4]);
ctx.beginPath(); ctx.moveTo(8, H-10); ctx.lineTo(35, H-10); ctx.stroke();
ctx.restore();
ctx.fillStyle = '#e63946';
ctx.fillText('dominant eigenvector', 40, H-6);
ctx.restore();
}
document.getElementById('matrixSelect').addEventListener('change', reset);
document.getElementById('stepBtn').addEventListener('click', step);
document.getElementById('runBtn').addEventListener('click', function() {
if (animId) { clearInterval(animId); animId = null; return; }
if (converged) { reset(); }
animId = setInterval(function() { step(); if (converged) { clearInterval(animId); animId = null; } }, 120);
});
document.getElementById('resetBtn').addEventListener('click', reset);
reset();
Watch how quickly the blue arrow locks onto the red one — even when you start far away. The ratio shown is the Rayleigh quotient, an estimate of the dominant eigenvalue that also converges to the true value.
Comments
Loading comments...