The demo below shows a 1-D problem: you specify an initial error profile across a fine grid, then watch what happens as a smoother (Gauss–Seidel relaxation) runs and as the multigrid V-cycle kicks in.
<!-- Multigrid error-smoothing demo: shows how a simple smoother kills -->
<p class="hint">Choose an initial error shape and click <b>Run smoother</b> to see Gauss–Seidel relaxation at work. High-frequency wiggles die quickly — but the smooth hump barely moves. Then click <b>Run V-cycle</b> to see multigrid crush the remaining error across all frequencies.</p>
<div class="controls">
<label>Initial error:
<select id="initMode">
<option value="low">Low-frequency (smooth hump)</option>
<option value="high">High-frequency (rapid wiggles)</option>
<option value="mixed" selected>Mixed (both)</option>
</select>
</label>
<label>Smoother sweeps:
<input id="sweepCount" type="number" min="1" max="30" value="5">
</label>
</div>
<canvas id="chart" width="560" height="200"></canvas>
<div class="legend">
<span class="dot dot-initial"></span> Initial
<span class="dot dot-smooth"></span> After smoother
<span class="dot dot-mg"></span> After V-cycle
</div>
<div class="status" id="status">Choose a profile, then run the smoother or V-cycle.</div>
<div class="btns">
<button id="btnSmooth" type="button">Run smoother</button>
<button id="btnMG" type="button">Run V-cycle</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
/* base layout and typography for the demo widget */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .6rem; font-size: .88rem; }
.controls label { display: flex; align-items: center; gap: .4rem; }
select, input[type=number] { font: inherit; border: 1px solid #cdd9e3; border-radius: 6px; padding: .2rem .4rem; background: #f7f9fb; }
input[type=number] { width: 56px; }
canvas { display: block; width: 100%; max-width: 560px; border: 1px solid #d0d7de; border-radius: 8px; background: #fff; }
.legend { display: flex; gap: 1.2rem; font-size: .82rem; margin: .4rem 0 .2rem; flex-wrap: wrap; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin-right: 3px; vertical-align: middle; }
.dot-initial { background: #adb1b8; }
.dot-smooth { background: #1d73b8; }
.dot-mg { background: #0a7d33; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.5em; margin: .3rem 0; }
.status.info { color: #1d3557; }
.status.ok { color: #0a7d33; }
.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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Multigrid error-smoothing demo: shows how a simple smoother kills
// high-frequency error fast but stalls on smooth (low-frequency) error, while a multigrid V-cycle kills both in one pass.
const N = 64; // fine grid with 64 points, boundary fixed at 0
// Build initial error profiles of different frequency content
function makeError(mode) {
const e = new Float64Array(N);
for (let i = 0; i < N; i++) {
const x = i / (N - 1);
if (mode === 'low') {
e[i] = Math.sin(Math.PI * x); // one sine arch — smooth error, very hard to kill by sweeping
} else if (mode === 'high') {
e[i] = Math.sin(24 * Math.PI * x); // 24 sine arches — high frequency, dies in a few sweeps
} else {
e[i] = Math.sin(Math.PI * x) + 0.4 * Math.sin(24 * Math.PI * x); // superposition: one smooth arch + high-frequency wiggles
}
}
return e;
}
// Gauss-Seidel smoother: replace each interior point with the average of its neighbors
function gaussSeidel(e, sweeps) {
const u = e.slice();
for (let s = 0; s < sweeps; s++) {
for (let i = 1; i < u.length - 1; i++) {
u[i] = 0.5 * (u[i - 1] + u[i + 1]); // simple averaging: kills high-freq fast, slow on low-freq
}
}
return u;
}
// Full-weighting restriction: map fine-grid values to half as many coarse-grid points
function restrictFW(fine) {
const nc = Math.floor(fine.length / 2);
const coarse = new Float64Array(nc);
for (let i = 1; i < nc - 1; i++) {
coarse[i] = 0.25 * fine[2*i-1] + 0.5 * fine[2*i] + 0.25 * fine[2*i+1]; // full-weighting stencil [1/4, 1/2, 1/4]
}
return coarse;
}
// Linear prolongation: interpolate coarse-grid values back onto the fine grid
function prolongate(coarse, nf) {
const fine = new Float64Array(nf);
const nc = coarse.length;
for (let i = 0; i < nc; i++) {
fine[2*i] += coarse[i];
if (2*i+1 < nf) fine[2*i+1] += 0.5 * (coarse[i] + (i+1<nc ? coarse[i+1] : 0)); // linear interpolation between coarse nodes
}
return fine;
}
// RMS norm of the error vector
function norm(v) {
let s = 0;
for (const x of v) s += x * x;
return Math.sqrt(s / v.length);
}
// V-cycle: recursively smooth on progressively coarser grids
// Low-freq error on fine grid looks like high-freq error on coarser grid, so GS kills it there
function vCycleSmooth(e, levels) {
if (e.length <= 4 || levels === 0) {
return gaussSeidel(e, 50); // base case: grid too small to recurse, smooth heavily
}
const presmoothed = gaussSeidel(e, 3); // pre-smoothing: kills high-frequency components on fine grid
const coarse = restrictFW(presmoothed); // transfer residual to coarser grid (half the points)
const coarseSmoothed = vCycleSmooth(coarse, levels - 1); // recursively smooth on coarser grid (kills what was low-freq on fine grid)
const back = prolongate(coarseSmoothed, e.length); // prolongate coarse correction back to fine grid
return gaussSeidel(back, 3); // post-smoothing: clean up interpolation artifacts
}
// Draw error profile curves on the canvas
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
function draw(initial, smoothed, mg) {
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
ctx.strokeStyle = '#e0e0e0'; // zero baseline
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, H/2); ctx.lineTo(W, H/2); ctx.stroke();
function drawLine(data, color, lw) {
ctx.strokeStyle = color; ctx.lineWidth = lw; ctx.beginPath();
data.forEach((v, i) => {
const x = (i / (data.length - 1)) * W;
const y = H/2 - v * (H * 0.42);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
}
drawLine(initial, '#adb1b8', 1.4); // initial error in gray
if (smoothed) drawLine(smoothed, '#1d73b8', 2); // after-smoother result in blue
if (mg) drawLine(mg, '#0a7d33', 2.2); // after-V-cycle result in green
}
let initial = makeError('mixed');
let smoothed = null, mg = null;
function reset() {
const mode = document.getElementById('initMode').value;
initial = makeError(mode);
smoothed = null; mg = null;
draw(initial, null, null);
document.getElementById('status').textContent = 'Choose a profile, then run the smoother or V-cycle.';
document.getElementById('status').className = 'status';
}
function runSmooth() {
const sweeps = Math.max(1, parseInt(document.getElementById('sweepCount').value, 10) || 5);
smoothed = gaussSeidel(initial, sweeps);
draw(initial, smoothed, mg);
const n0 = norm(initial), ns = norm(smoothed);
document.getElementById('status').textContent =
fmt('After %sweeps% sweep(s): RMS error %n0% → %ns%', { sweeps: sweeps, n0: n0.toFixed(4), ns: ns.toFixed(4) });
document.getElementById('status').className = 'status info';
}
function runMG() {
mg = vCycleSmooth(initial, 4); // 4 levels of coarsening: 64 -> 32 -> 16 -> 8 -> 4 points
draw(initial, smoothed, mg);
const n0 = norm(initial), nm = norm(mg);
document.getElementById('status').textContent =
fmt('After V-cycle: RMS error %n0% → %nm%', { n0: n0.toFixed(4), nm: nm.toFixed(4) });
document.getElementById('status').className = 'status ok';
}
document.getElementById('btnSmooth').addEventListener('click', runSmooth);
document.getElementById('btnMG').addEventListener('click', runMG);
document.getElementById('btnReset').addEventListener('click', reset);
document.getElementById('initMode').addEventListener('change', reset);
draw(initial, null, null);
<!-- Multigrid error-smoothing demo: shows how a simple smoother kills -->
<p class="hint">Choose an initial error shape and click <b>Run smoother</b> to see Gauss–Seidel relaxation at work. High-frequency wiggles die quickly — but the smooth hump barely moves. Then click <b>Run V-cycle</b> to see multigrid crush the remaining error across all frequencies.</p>
<div class="controls">
<label>Initial error:
<select id="initMode">
<option value="low">Low-frequency (smooth hump)</option>
<option value="high">High-frequency (rapid wiggles)</option>
<option value="mixed" selected>Mixed (both)</option>
</select>
</label>
<label>Smoother sweeps:
<input id="sweepCount" type="number" min="1" max="30" value="5">
</label>
</div>
<canvas id="chart" width="560" height="200"></canvas>
<div class="legend">
<span class="dot dot-initial"></span> Initial
<span class="dot dot-smooth"></span> After smoother
<span class="dot dot-mg"></span> After V-cycle
</div>
<div class="status" id="status">Choose a profile, then run the smoother or V-cycle.</div>
<div class="btns">
<button id="btnSmooth" type="button">Run smoother</button>
<button id="btnMG" type="button">Run V-cycle</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
/* base layout and typography for the demo widget */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .6rem; font-size: .88rem; }
.controls label { display: flex; align-items: center; gap: .4rem; }
select, input[type=number] { font: inherit; border: 1px solid #cdd9e3; border-radius: 6px; padding: .2rem .4rem; background: #f7f9fb; }
input[type=number] { width: 56px; }
canvas { display: block; width: 100%; max-width: 560px; border: 1px solid #d0d7de; border-radius: 8px; background: #fff; }
.legend { display: flex; gap: 1.2rem; font-size: .82rem; margin: .4rem 0 .2rem; flex-wrap: wrap; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin-right: 3px; vertical-align: middle; }
.dot-initial { background: #adb1b8; }
.dot-smooth { background: #1d73b8; }
.dot-mg { background: #0a7d33; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.5em; margin: .3rem 0; }
.status.info { color: #1d3557; }
.status.ok { color: #0a7d33; }
.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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Multigrid error-smoothing demo: shows how a simple smoother kills
// high-frequency error fast but stalls on smooth (low-frequency) error, while a multigrid V-cycle kills both in one pass.
const N = 64; // fine grid with 64 points, boundary fixed at 0
// Build initial error profiles of different frequency content
function makeError(mode) {
const e = new Float64Array(N);
for (let i = 0; i < N; i++) {
const x = i / (N - 1);
if (mode === 'low') {
e[i] = Math.sin(Math.PI * x); // one sine arch — smooth error, very hard to kill by sweeping
} else if (mode === 'high') {
e[i] = Math.sin(24 * Math.PI * x); // 24 sine arches — high frequency, dies in a few sweeps
} else {
e[i] = Math.sin(Math.PI * x) + 0.4 * Math.sin(24 * Math.PI * x); // superposition: one smooth arch + high-frequency wiggles
}
}
return e;
}
// Gauss-Seidel smoother: replace each interior point with the average of its neighbors
function gaussSeidel(e, sweeps) {
const u = e.slice();
for (let s = 0; s < sweeps; s++) {
for (let i = 1; i < u.length - 1; i++) {
u[i] = 0.5 * (u[i - 1] + u[i + 1]); // simple averaging: kills high-freq fast, slow on low-freq
}
}
return u;
}
// Full-weighting restriction: map fine-grid values to half as many coarse-grid points
function restrictFW(fine) {
const nc = Math.floor(fine.length / 2);
const coarse = new Float64Array(nc);
for (let i = 1; i < nc - 1; i++) {
coarse[i] = 0.25 * fine[2*i-1] + 0.5 * fine[2*i] + 0.25 * fine[2*i+1]; // full-weighting stencil [1/4, 1/2, 1/4]
}
return coarse;
}
// Linear prolongation: interpolate coarse-grid values back onto the fine grid
function prolongate(coarse, nf) {
const fine = new Float64Array(nf);
const nc = coarse.length;
for (let i = 0; i < nc; i++) {
fine[2*i] += coarse[i];
if (2*i+1 < nf) fine[2*i+1] += 0.5 * (coarse[i] + (i+1<nc ? coarse[i+1] : 0)); // linear interpolation between coarse nodes
}
return fine;
}
// RMS norm of the error vector
function norm(v) {
let s = 0;
for (const x of v) s += x * x;
return Math.sqrt(s / v.length);
}
// V-cycle: recursively smooth on progressively coarser grids
// Low-freq error on fine grid looks like high-freq error on coarser grid, so GS kills it there
function vCycleSmooth(e, levels) {
if (e.length <= 4 || levels === 0) {
return gaussSeidel(e, 50); // base case: grid too small to recurse, smooth heavily
}
const presmoothed = gaussSeidel(e, 3); // pre-smoothing: kills high-frequency components on fine grid
const coarse = restrictFW(presmoothed); // transfer residual to coarser grid (half the points)
const coarseSmoothed = vCycleSmooth(coarse, levels - 1); // recursively smooth on coarser grid (kills what was low-freq on fine grid)
const back = prolongate(coarseSmoothed, e.length); // prolongate coarse correction back to fine grid
return gaussSeidel(back, 3); // post-smoothing: clean up interpolation artifacts
}
// Draw error profile curves on the canvas
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
function draw(initial, smoothed, mg) {
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
ctx.strokeStyle = '#e0e0e0'; // zero baseline
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, H/2); ctx.lineTo(W, H/2); ctx.stroke();
function drawLine(data, color, lw) {
ctx.strokeStyle = color; ctx.lineWidth = lw; ctx.beginPath();
data.forEach((v, i) => {
const x = (i / (data.length - 1)) * W;
const y = H/2 - v * (H * 0.42);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
}
drawLine(initial, '#adb1b8', 1.4); // initial error in gray
if (smoothed) drawLine(smoothed, '#1d73b8', 2); // after-smoother result in blue
if (mg) drawLine(mg, '#0a7d33', 2.2); // after-V-cycle result in green
}
let initial = makeError('mixed');
let smoothed = null, mg = null;
function reset() {
const mode = document.getElementById('initMode').value;
initial = makeError(mode);
smoothed = null; mg = null;
draw(initial, null, null);
document.getElementById('status').textContent = 'Choose a profile, then run the smoother or V-cycle.';
document.getElementById('status').className = 'status';
}
function runSmooth() {
const sweeps = Math.max(1, parseInt(document.getElementById('sweepCount').value, 10) || 5);
smoothed = gaussSeidel(initial, sweeps);
draw(initial, smoothed, mg);
const n0 = norm(initial), ns = norm(smoothed);
document.getElementById('status').textContent =
fmt('After %sweeps% sweep(s): RMS error %n0% → %ns%', { sweeps: sweeps, n0: n0.toFixed(4), ns: ns.toFixed(4) });
document.getElementById('status').className = 'status info';
}
function runMG() {
mg = vCycleSmooth(initial, 4); // 4 levels of coarsening: 64 -> 32 -> 16 -> 8 -> 4 points
draw(initial, smoothed, mg);
const n0 = norm(initial), nm = norm(mg);
document.getElementById('status').textContent =
fmt('After V-cycle: RMS error %n0% → %nm%', { n0: n0.toFixed(4), nm: nm.toFixed(4) });
document.getElementById('status').className = 'status ok';
}
document.getElementById('btnSmooth').addEventListener('click', runSmooth);
document.getElementById('btnMG').addEventListener('click', runMG);
document.getElementById('btnReset').addEventListener('click', reset);
document.getElementById('initMode').addEventListener('change', reset);
draw(initial, null, null);
Notice that one sweep of the smoother kills the sharp, high-frequency wiggles almost immediately — but the broad, smooth hump barely moves. Press Run V-cycle and watch the multigrid cycle transfer that surviving error to a coarser grid, correct it there in a fraction of the work, and inject the correction back. The total error collapses where sweeping alone would crawl.
Comments
Loading comments...