The demo below shows a random 4×4 matrix. At each step one Householder reflection zeroes out everything below the diagonal in the current column. Click Next step to apply each reflection and watch the matrix march toward upper-triangular form.
<!-- Householder QR demo: matrix display + step button -->
<p class="hint">Each click applies one Householder reflection, zeroing out the column below the diagonal. After N-1 steps the matrix is upper-triangular (R).</p>
<div class="matrix-area">
<div class="mat-label">Matrix A (current state)</div>
<table id="matA" class="mat"></table>
</div>
<div class="status" id="status">Press "Next step" to apply the first reflection.</div>
<div class="btns">
<button id="btnStep" type="button">Next step</button>
<button id="btnReset" type="button" class="ghost">New matrix</button>
</div>
<div class="step-info" id="stepInfo"></div>
/* Styles for the Householder QR demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.matrix-area { margin: .5rem 0; }
.mat-label { font-size: .8rem; font-weight: 700; color: #1d3557; margin-bottom: .2rem; letter-spacing: .04em; }
.mat { border-collapse: separate; border-spacing: 3px; }
.mat td {
width: 54px; height: 36px; text-align: right; padding: 0 6px;
font: 600 12px ui-monospace, monospace; border-radius: 5px;
transition: background .35s, color .35s;
}
.cell-normal { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.cell-zero { background: #d4edda; color: #0a7d33; border: 1px solid #b8dbc3; }
.cell-pivot { background: #fff3cd; color: #856404; border: 1px solid #ffe08a; }
.cell-active { background: #f8d7da; color: #842029; border: 1px solid #f5c2c7; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 14px system-ui; 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: .4; cursor: not-allowed; }
.step-info { font-size: .82rem; color: #555; min-height: 1.2em; line-height: 1.5; }
// Householder QR factorization — step-by-step demo
// At each step k we build a Householder reflector H_k that zeroes out A[k+1..n, k].
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Matrix dimension
const N = 4;
// Generate a random matrix with entries in [-5, 5]
function randMatrix() {
const A = [];
for (let i = 0; i < N; i++) {
A.push([]);
for (let j = 0; j < N; j++) {
A[i].push(Math.round((Math.random() * 10 - 5) * 10) / 10);
}
}
return A;
}
// Deep-copy a matrix
function copyM(A) { return A.map(r => r.slice()); }
// Euclidean norm of a vector
function norm2(v) { return Math.sqrt(v.reduce((s, x) => s + x * x, 0)); }
// Apply one Householder step to column k of matrix A
// Returns the updated matrix with zeros below the diagonal in column k.
function householderStep(A, k) {
const m = A.length;
// Extract the subcolumn from row k downward
const x = [];
for (let i = k; i < m; i++) x.push(A[i][k]);
const nrm = norm2(x);
if (nrm < 1e-12) return copyM(A); // column already zero, nothing to do
// Build the Householder vector u = x + sign(x[0])*||x||*e1
const u = x.slice();
u[0] += (x[0] >= 0 ? 1 : -1) * nrm;
const nu = norm2(u);
if (nu < 1e-15) return copyM(A);
const un = u.map(v => v / nu);
// Apply H = I - 2*u*u^T to the submatrix from column k onward
const B = copyM(A);
for (let j = k; j < m; j++) {
// dot product of u with each column of the submatrix
let dot = 0;
for (let i = 0; i < x.length; i++) dot += un[i] * B[i + k][j];
for (let i = 0; i < x.length; i++) B[i + k][j] -= 2 * un[i] * dot;
}
// Force exact zeros below diagonal to avoid floating-point drift
for (let i = k + 1; i < m; i++) B[i][k] = 0;
return B;
}
let origA, curA, step;
function init() {
origA = randMatrix();
curA = copyM(origA);
step = 0;
document.getElementById('btnStep').disabled = false;
render(curA, -1);
setStatus('Press "Next step" to apply the first reflection.', 'info');
document.getElementById('stepInfo').textContent = '';
}
// Format a number for display: treat tiny values as zero
function fmtNum(v) {
if (Math.abs(v) < 5e-10) return '0';
return v.toFixed(2);
}
function render(A, activeCol) {
const tbl = document.getElementById('matA');
tbl.innerHTML = '';
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 = fmtNum(A[i][j]);
if (j === activeCol && i > activeCol) {
td.className = 'cell-active'; // entries being zeroed this step
} else if (j < activeCol && i > j) {
td.className = 'cell-zero'; // already-zeroed entries from previous steps
} else if (i === j && j === activeCol) {
td.className = 'cell-pivot'; // current pivot diagonal entry
} else {
td.className = 'cell-normal';
}
tr.appendChild(td);
}
tbl.appendChild(tr);
}
}
function setStatus(t, cls) {
const el = document.getElementById('status');
el.textContent = t;
el.className = 'status ' + cls;
}
document.getElementById('btnStep').addEventListener('click', () => {
if (step >= N - 1) return;
const next = householderStep(curA, step);
curA = next;
render(curA, step);
step++;
if (step < N - 1) {
setStatus(i18nFmt('Step %k% complete. Column %k% is now upper-triangular.', { k: step }), 'info');
document.getElementById('stepInfo').textContent =
i18nFmt('Next: reflection will zero out column %k%.', { k: step + 1 });
} else {
setStatus('Done! The matrix is now upper-triangular (R).', 'ok');
document.getElementById('btnStep').disabled = true;
document.getElementById('stepInfo').textContent = 'All reflections applied. The accumulated reflectors give Q^T.';
render(curA, -1);
}
});
document.getElementById('btnReset').addEventListener('click', init);
init();
<!-- Householder QR demo: matrix display + step button -->
<p class="hint">Each click applies one Householder reflection, zeroing out the column below the diagonal. After N-1 steps the matrix is upper-triangular (R).</p>
<div class="matrix-area">
<div class="mat-label">Matrix A (current state)</div>
<table id="matA" class="mat"></table>
</div>
<div class="status" id="status">Press "Next step" to apply the first reflection.</div>
<div class="btns">
<button id="btnStep" type="button">Next step</button>
<button id="btnReset" type="button" class="ghost">New matrix</button>
</div>
<div class="step-info" id="stepInfo"></div>
/* Styles for the Householder QR demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.matrix-area { margin: .5rem 0; }
.mat-label { font-size: .8rem; font-weight: 700; color: #1d3557; margin-bottom: .2rem; letter-spacing: .04em; }
.mat { border-collapse: separate; border-spacing: 3px; }
.mat td {
width: 54px; height: 36px; text-align: right; padding: 0 6px;
font: 600 12px ui-monospace, monospace; border-radius: 5px;
transition: background .35s, color .35s;
}
.cell-normal { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.cell-zero { background: #d4edda; color: #0a7d33; border: 1px solid #b8dbc3; }
.cell-pivot { background: #fff3cd; color: #856404; border: 1px solid #ffe08a; }
.cell-active { background: #f8d7da; color: #842029; border: 1px solid #f5c2c7; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 14px system-ui; 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: .4; cursor: not-allowed; }
.step-info { font-size: .82rem; color: #555; min-height: 1.2em; line-height: 1.5; }
// Householder QR factorization — step-by-step demo
// At each step k we build a Householder reflector H_k that zeroes out A[k+1..n, k].
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Matrix dimension
const N = 4;
// Generate a random matrix with entries in [-5, 5]
function randMatrix() {
const A = [];
for (let i = 0; i < N; i++) {
A.push([]);
for (let j = 0; j < N; j++) {
A[i].push(Math.round((Math.random() * 10 - 5) * 10) / 10);
}
}
return A;
}
// Deep-copy a matrix
function copyM(A) { return A.map(r => r.slice()); }
// Euclidean norm of a vector
function norm2(v) { return Math.sqrt(v.reduce((s, x) => s + x * x, 0)); }
// Apply one Householder step to column k of matrix A
// Returns the updated matrix with zeros below the diagonal in column k.
function householderStep(A, k) {
const m = A.length;
// Extract the subcolumn from row k downward
const x = [];
for (let i = k; i < m; i++) x.push(A[i][k]);
const nrm = norm2(x);
if (nrm < 1e-12) return copyM(A); // column already zero, nothing to do
// Build the Householder vector u = x + sign(x[0])*||x||*e1
const u = x.slice();
u[0] += (x[0] >= 0 ? 1 : -1) * nrm;
const nu = norm2(u);
if (nu < 1e-15) return copyM(A);
const un = u.map(v => v / nu);
// Apply H = I - 2*u*u^T to the submatrix from column k onward
const B = copyM(A);
for (let j = k; j < m; j++) {
// dot product of u with each column of the submatrix
let dot = 0;
for (let i = 0; i < x.length; i++) dot += un[i] * B[i + k][j];
for (let i = 0; i < x.length; i++) B[i + k][j] -= 2 * un[i] * dot;
}
// Force exact zeros below diagonal to avoid floating-point drift
for (let i = k + 1; i < m; i++) B[i][k] = 0;
return B;
}
let origA, curA, step;
function init() {
origA = randMatrix();
curA = copyM(origA);
step = 0;
document.getElementById('btnStep').disabled = false;
render(curA, -1);
setStatus('Press "Next step" to apply the first reflection.', 'info');
document.getElementById('stepInfo').textContent = '';
}
// Format a number for display: treat tiny values as zero
function fmtNum(v) {
if (Math.abs(v) < 5e-10) return '0';
return v.toFixed(2);
}
function render(A, activeCol) {
const tbl = document.getElementById('matA');
tbl.innerHTML = '';
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 = fmtNum(A[i][j]);
if (j === activeCol && i > activeCol) {
td.className = 'cell-active'; // entries being zeroed this step
} else if (j < activeCol && i > j) {
td.className = 'cell-zero'; // already-zeroed entries from previous steps
} else if (i === j && j === activeCol) {
td.className = 'cell-pivot'; // current pivot diagonal entry
} else {
td.className = 'cell-normal';
}
tr.appendChild(td);
}
tbl.appendChild(tr);
}
}
function setStatus(t, cls) {
const el = document.getElementById('status');
el.textContent = t;
el.className = 'status ' + cls;
}
document.getElementById('btnStep').addEventListener('click', () => {
if (step >= N - 1) return;
const next = householderStep(curA, step);
curA = next;
render(curA, step);
step++;
if (step < N - 1) {
setStatus(i18nFmt('Step %k% complete. Column %k% is now upper-triangular.', { k: step }), 'info');
document.getElementById('stepInfo').textContent =
i18nFmt('Next: reflection will zero out column %k%.', { k: step + 1 });
} else {
setStatus('Done! The matrix is now upper-triangular (R).', 'ok');
document.getElementById('btnStep').disabled = true;
document.getElementById('stepInfo').textContent = 'All reflections applied. The accumulated reflectors give Q^T.';
render(curA, -1);
}
});
document.getElementById('btnReset').addEventListener('click', init);
init();
Notice that after step k the entries below the diagonal in column k are exactly zero (up to floating-point rounding). The matrix never needs to store Q explicitly — the reflection vectors are enough to reconstruct it later. By the final step the matrix has become R, and the accumulated reflections give QT.
Comments
Loading comments...