<!-- Demo: arrowhead sparse matrix — fill-in with natural vs AMD-style reordering -->
<div class="controls">
<label class="toggle-label">
<span>Elimination order:</span>
<div class="toggle-wrap">
<button id="btn-natural" class="order-btn active" type="button">Natural order</button>
<button id="btn-reordered" class="order-btn" type="button">Reordered (AMD-style)</button>
</div>
</label>
</div>
<div class="stats-row">
<span class="stat-box" id="stat-nz">Original non-zeros: <strong id="val-nz">0</strong></span>
<span class="stat-box fill-stat" id="stat-fill">Fill-in added: <strong id="val-fill">0</strong></span>
<span class="stat-box total-stat" id="stat-total">Factor non-zeros: <strong id="val-total">0</strong></span>
</div>
<div class="matrix-wrap">
<div id="matrix-original" class="matrix-panel">
<div class="panel-title">Original matrix A</div>
<canvas id="canvas-orig" width="210" height="210"></canvas>
</div>
<div class="arrow">→</div>
<div id="matrix-factor" class="matrix-panel">
<div class="panel-title">Factor L (after elimination)</div>
<canvas id="canvas-factor" width="210" height="210"></canvas>
</div>
</div>
<p class="explanation" id="explanation">Natural order — the hub node is eliminated last, but every other pair of rows sharing the hub column creates fill. Expect heavy fill-in.</p>
<button id="btn-step" type="button">Step through fill-in</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
/* layout and visual styles for the fill-in demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.controls { margin-bottom: .6rem; }
.toggle-label { display: flex; align-items: center; gap: .6rem; font-size: .9rem; font-weight: 600; }
.toggle-wrap { display: flex; gap: 4px; }
.order-btn { font: 600 13px system-ui; padding: .3rem .7rem; border: 1.5px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 6px; cursor: pointer; }
.order-btn.active { background: #1d3557; color: #fff; }
.stats-row { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
.stat-box { font-size: .82rem; padding: .2rem .5rem; border-radius: 5px;
background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.fill-stat { background: #fff3e0; border-color: #ffb74d; color: #a04000; }
.total-stat { background: #e8f5e9; border-color: #81c784; color: #1b5e20; }
.matrix-wrap { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
.matrix-panel { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.panel-title { font-size: .82rem; font-weight: 700; color: #1d3557; text-align: center; }
canvas { border: 1px solid #cdd9e3; border-radius: 4px; display: block; }
.arrow { font-size: 1.6rem; color: #888; }
.explanation { font-size: .88rem; line-height: 1.5; color: #333; margin: .4rem 0; min-height: 2.5em; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; margin-right: .4rem; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// An arrowhead matrix: diagonal entries + one dense hub row and column.
// Natural order puts the hub first — eliminating it fills every row pair.
// Reordered: hub last — interior nodes are eliminated cheaply, hub touched once.
const N = 9; // matrix dimension (interior nodes + 1 hub)
// hub node index (last in natural order)
const HUB = N - 1;
// build sparsity pattern for the chosen ordering
function buildPattern(reordered) {
const pat = [];
for (let i = 0; i < N; i++) pat.push(new Array(N).fill(false));
// diagonal entries always present
for (let i = 0; i < N; i++) pat[i][i] = true;
if (!reordered) {
// natural order — hub at row/col 0, eliminated first (catastrophic fill)
for (let i = 1; i < N; i++) { pat[0][i] = true; pat[i][0] = true; }
} else {
// reordered (AMD-style) — hub at row/col N-1, eliminated last (no fill)
for (let i = 0; i < N - 1; i++) { pat[HUB][i] = true; pat[i][HUB] = true; }
}
return pat;
}
// simulate LU fill-in by symmetric Gaussian elimination (upper triangular propagation)
function computeFill(pat) {
const n = pat.length;
const L = pat.map(row => row.slice());
const fill = [];
for (let k = 0; k < n; k++) {
for (let i = k + 1; i < n; i++) {
if (L[i][k]) {
for (let j = k + 1; j < n; j++) {
if (L[k][j] && !L[i][j]) {
L[i][j] = true;
fill.push([i, j]);
}
}
}
}
}
return { L, fill };
}
// count non-zero entries in a pattern
function countNZ(pat) {
return pat.reduce((s, row) => s + row.filter(Boolean).length, 0);
}
let useReordered = false;
let animStep = 0;
let fillList = [];
let currentL = null;
let basePat = null;
const origCanvas = document.getElementById('canvas-orig');
const factCanvas = document.getElementById('canvas-factor');
const origCtx = origCanvas.getContext('2d');
const factCtx = factCanvas.getContext('2d');
const CELL = 21;
const PAD = 7;
// draw the matrix grid on a canvas
function drawMatrix(ctx, pat, fillSet, highlightStep) {
const n = pat.length;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = '#f6f8fa';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
const x = PAD + j * CELL;
const y = PAD + i * CELL;
const isFill = fillSet && fillSet.has(i + ',' + j);
const isHighlight = highlightStep && highlightStep.has(i + ',' + j);
if (isHighlight) {
ctx.fillStyle = '#ff7043';
} else if (isFill) {
ctx.fillStyle = '#ffb74d44';
} else if (pat[i][j]) {
ctx.fillStyle = '#1d3557';
} else {
ctx.fillStyle = '#e8eef3';
}
ctx.beginPath();
ctx.roundRect(x, y, CELL - 2, CELL - 2, 3);
ctx.fill();
}
}
}
function getFilledSet(upToStep) {
const s = new Set();
for (let i = 0; i <= upToStep && i < fillList.length; i++) {
s.add(fillList[i][0] + ',' + fillList[i][1]);
}
return s;
}
function updateStats(nzOrig, fillCount, totalNZ) {
document.getElementById('val-nz').textContent = nzOrig;
document.getElementById('val-fill').textContent = fillCount;
document.getElementById('val-total').textContent = totalNZ;
}
function updateExplanation() {
const fillCount = fillList.length;
if (!useReordered) {
document.getElementById('explanation').textContent =
i18nFmt('Natural order: %fill% fill entries added during elimination of a %n%×%n% arrowhead matrix.', { fill: fillCount, n: N });
} else {
document.getElementById('explanation').textContent =
i18nFmt('Reordered (AMD-style): %fill% fill entries added — interior nodes eliminated first, hub last.', { fill: fillCount, n: N });
}
}
function reset() {
basePat = buildPattern(useReordered);
const result = computeFill(basePat);
currentL = result.L;
fillList = result.fill;
animStep = -1;
const nzOrig = countNZ(basePat);
updateStats(nzOrig, 0, nzOrig);
updateExplanation();
drawMatrix(origCtx, basePat, null, null);
drawMatrix(factCtx, basePat, null, null);
document.getElementById('btn-step').textContent = 'Step through fill-in';
}
function stepAnimation() {
if (animStep >= fillList.length) return;
animStep++;
const filled = getFilledSet(animStep - 1);
const highlight = animStep > 0 && animStep <= fillList.length
? new Set([fillList[animStep - 1][0] + ',' + fillList[animStep - 1][1]])
: null;
drawMatrix(factCtx, currentL, filled, highlight);
updateStats(countNZ(basePat), Math.min(animStep, fillList.length), countNZ(currentL));
if (animStep >= fillList.length) {
document.getElementById('btn-step').textContent = 'Done — all fill shown';
drawMatrix(factCtx, currentL, filled, null);
}
}
document.getElementById('btn-natural').addEventListener('click', () => {
useReordered = false;
document.getElementById('btn-natural').classList.add('active');
document.getElementById('btn-reordered').classList.remove('active');
reset();
});
document.getElementById('btn-reordered').addEventListener('click', () => {
useReordered = true;
document.getElementById('btn-reordered').classList.add('active');
document.getElementById('btn-natural').classList.remove('active');
reset();
});
document.getElementById('btn-step').addEventListener('click', stepAnimation);
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
<!-- Demo: arrowhead sparse matrix — fill-in with natural vs AMD-style reordering -->
<div class="controls">
<label class="toggle-label">
<span>Elimination order:</span>
<div class="toggle-wrap">
<button id="btn-natural" class="order-btn active" type="button">Natural order</button>
<button id="btn-reordered" class="order-btn" type="button">Reordered (AMD-style)</button>
</div>
</label>
</div>
<div class="stats-row">
<span class="stat-box" id="stat-nz">Original non-zeros: <strong id="val-nz">0</strong></span>
<span class="stat-box fill-stat" id="stat-fill">Fill-in added: <strong id="val-fill">0</strong></span>
<span class="stat-box total-stat" id="stat-total">Factor non-zeros: <strong id="val-total">0</strong></span>
</div>
<div class="matrix-wrap">
<div id="matrix-original" class="matrix-panel">
<div class="panel-title">Original matrix A</div>
<canvas id="canvas-orig" width="210" height="210"></canvas>
</div>
<div class="arrow">→</div>
<div id="matrix-factor" class="matrix-panel">
<div class="panel-title">Factor L (after elimination)</div>
<canvas id="canvas-factor" width="210" height="210"></canvas>
</div>
</div>
<p class="explanation" id="explanation">Natural order — the hub node is eliminated last, but every other pair of rows sharing the hub column creates fill. Expect heavy fill-in.</p>
<button id="btn-step" type="button">Step through fill-in</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
/* layout and visual styles for the fill-in demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.controls { margin-bottom: .6rem; }
.toggle-label { display: flex; align-items: center; gap: .6rem; font-size: .9rem; font-weight: 600; }
.toggle-wrap { display: flex; gap: 4px; }
.order-btn { font: 600 13px system-ui; padding: .3rem .7rem; border: 1.5px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 6px; cursor: pointer; }
.order-btn.active { background: #1d3557; color: #fff; }
.stats-row { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
.stat-box { font-size: .82rem; padding: .2rem .5rem; border-radius: 5px;
background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.fill-stat { background: #fff3e0; border-color: #ffb74d; color: #a04000; }
.total-stat { background: #e8f5e9; border-color: #81c784; color: #1b5e20; }
.matrix-wrap { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
.matrix-panel { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.panel-title { font-size: .82rem; font-weight: 700; color: #1d3557; text-align: center; }
canvas { border: 1px solid #cdd9e3; border-radius: 4px; display: block; }
.arrow { font-size: 1.6rem; color: #888; }
.explanation { font-size: .88rem; line-height: 1.5; color: #333; margin: .4rem 0; min-height: 2.5em; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; margin-right: .4rem; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// An arrowhead matrix: diagonal entries + one dense hub row and column.
// Natural order puts the hub first — eliminating it fills every row pair.
// Reordered: hub last — interior nodes are eliminated cheaply, hub touched once.
const N = 9; // matrix dimension (interior nodes + 1 hub)
// hub node index (last in natural order)
const HUB = N - 1;
// build sparsity pattern for the chosen ordering
function buildPattern(reordered) {
const pat = [];
for (let i = 0; i < N; i++) pat.push(new Array(N).fill(false));
// diagonal entries always present
for (let i = 0; i < N; i++) pat[i][i] = true;
if (!reordered) {
// natural order — hub at row/col 0, eliminated first (catastrophic fill)
for (let i = 1; i < N; i++) { pat[0][i] = true; pat[i][0] = true; }
} else {
// reordered (AMD-style) — hub at row/col N-1, eliminated last (no fill)
for (let i = 0; i < N - 1; i++) { pat[HUB][i] = true; pat[i][HUB] = true; }
}
return pat;
}
// simulate LU fill-in by symmetric Gaussian elimination (upper triangular propagation)
function computeFill(pat) {
const n = pat.length;
const L = pat.map(row => row.slice());
const fill = [];
for (let k = 0; k < n; k++) {
for (let i = k + 1; i < n; i++) {
if (L[i][k]) {
for (let j = k + 1; j < n; j++) {
if (L[k][j] && !L[i][j]) {
L[i][j] = true;
fill.push([i, j]);
}
}
}
}
}
return { L, fill };
}
// count non-zero entries in a pattern
function countNZ(pat) {
return pat.reduce((s, row) => s + row.filter(Boolean).length, 0);
}
let useReordered = false;
let animStep = 0;
let fillList = [];
let currentL = null;
let basePat = null;
const origCanvas = document.getElementById('canvas-orig');
const factCanvas = document.getElementById('canvas-factor');
const origCtx = origCanvas.getContext('2d');
const factCtx = factCanvas.getContext('2d');
const CELL = 21;
const PAD = 7;
// draw the matrix grid on a canvas
function drawMatrix(ctx, pat, fillSet, highlightStep) {
const n = pat.length;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = '#f6f8fa';
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
const x = PAD + j * CELL;
const y = PAD + i * CELL;
const isFill = fillSet && fillSet.has(i + ',' + j);
const isHighlight = highlightStep && highlightStep.has(i + ',' + j);
if (isHighlight) {
ctx.fillStyle = '#ff7043';
} else if (isFill) {
ctx.fillStyle = '#ffb74d44';
} else if (pat[i][j]) {
ctx.fillStyle = '#1d3557';
} else {
ctx.fillStyle = '#e8eef3';
}
ctx.beginPath();
ctx.roundRect(x, y, CELL - 2, CELL - 2, 3);
ctx.fill();
}
}
}
function getFilledSet(upToStep) {
const s = new Set();
for (let i = 0; i <= upToStep && i < fillList.length; i++) {
s.add(fillList[i][0] + ',' + fillList[i][1]);
}
return s;
}
function updateStats(nzOrig, fillCount, totalNZ) {
document.getElementById('val-nz').textContent = nzOrig;
document.getElementById('val-fill').textContent = fillCount;
document.getElementById('val-total').textContent = totalNZ;
}
function updateExplanation() {
const fillCount = fillList.length;
if (!useReordered) {
document.getElementById('explanation').textContent =
i18nFmt('Natural order: %fill% fill entries added during elimination of a %n%×%n% arrowhead matrix.', { fill: fillCount, n: N });
} else {
document.getElementById('explanation').textContent =
i18nFmt('Reordered (AMD-style): %fill% fill entries added — interior nodes eliminated first, hub last.', { fill: fillCount, n: N });
}
}
function reset() {
basePat = buildPattern(useReordered);
const result = computeFill(basePat);
currentL = result.L;
fillList = result.fill;
animStep = -1;
const nzOrig = countNZ(basePat);
updateStats(nzOrig, 0, nzOrig);
updateExplanation();
drawMatrix(origCtx, basePat, null, null);
drawMatrix(factCtx, basePat, null, null);
document.getElementById('btn-step').textContent = 'Step through fill-in';
}
function stepAnimation() {
if (animStep >= fillList.length) return;
animStep++;
const filled = getFilledSet(animStep - 1);
const highlight = animStep > 0 && animStep <= fillList.length
? new Set([fillList[animStep - 1][0] + ',' + fillList[animStep - 1][1]])
: null;
drawMatrix(factCtx, currentL, filled, highlight);
updateStats(countNZ(basePat), Math.min(animStep, fillList.length), countNZ(currentL));
if (animStep >= fillList.length) {
document.getElementById('btn-step').textContent = 'Done — all fill shown';
drawMatrix(factCtx, currentL, filled, null);
}
}
document.getElementById('btn-natural').addEventListener('click', () => {
useReordered = false;
document.getElementById('btn-natural').classList.add('active');
document.getElementById('btn-reordered').classList.remove('active');
reset();
});
document.getElementById('btn-reordered').addEventListener('click', () => {
useReordered = true;
document.getElementById('btn-reordered').classList.add('active');
document.getElementById('btn-natural').classList.remove('active');
reset();
});
document.getElementById('btn-step').addEventListener('click', stepAnimation);
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
Comments
Loading comments...