The demo below models a chain of L = 10 coupled blocks. Each block has an erasure probability ε (the noise level). The two boundary blocks (positions 0 and L-1) start fully decoded — they represent the pinned ends.
Set the noise level and click Step to advance the wave one position, or Decode all to watch it sweep the whole chain.
<!-- Chain of L coupled LDPC blocks; boundary blocks are pinned (decoded from the start) -->
<div class="controls">
<label>
<span>Noise ε:</span>
<input type="range" id="noiseSlider" min="0.30" max="0.55" step="0.01" value="0.45">
<span id="noiseVal">0.45</span>
</label>
<div class="btns">
<button id="btnStep" type="button">Step</button>
<button id="btnAll" type="button">Decode all</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
<div class="chain-wrap">
<div id="chain" class="chain"></div>
</div>
<div id="status" class="status"></div>
<div class="legend">
<span class="leg-item"><span class="leg-box decoded"></span> Decoded</span>
<span class="leg-item"><span class="leg-box decoding"></span> Decoding</span>
<span class="leg-item"><span class="leg-box stuck"></span> Stuck</span>
<span class="leg-item"><span class="leg-box undecoded"></span> Undecoded</span>
</div>
/* SC-LDPC chain visualization — bar height encodes erasure fraction */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; padding: 10px; }
.controls { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
.controls label { display: flex; align-items: center; gap: 8px; font-size: .9rem; }
.controls input[type=range] { flex: 1; max-width: 200px; }
.btns { display: flex; gap: 6px; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.chain-wrap { overflow-x: auto; padding-bottom: 4px; }
.chain { display: flex; gap: 4px; align-items: flex-end; min-width: max-content; margin: 0 auto; }
.block { display: flex; flex-direction: column; align-items: center; gap: 3px; width: 50px; }
.block-rect { width: 44px; border-radius: 7px; transition: background .35s, height .35s; }
.block-label { font-size: .7rem; color: #666; }
/* Block state color coding */
.decoded { background: #2a9d5c; }
.decoding { background: #e9c46a; }
.stuck { background: #e63946; }
.undecoded{ background: #c9ccd1; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.5em; margin: 8px 0 6px; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info{ color: #1d3557; }
.legend { display: flex; gap: 10px; flex-wrap: wrap; font-size: .78rem; color: #555; margin-top: 4px; }
.leg-item { display: flex; align-items: center; gap: 4px; }
.leg-box { display: inline-block; width: 14px; height: 14px; border-radius: 3px; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Spatially-Coupled LDPC demo: a chain of L=10 blocks.
// Boundary blocks (0 and L-1) are pinned (zero erasure).
// Each step propagates decoding inward using effective erasure from neighbors.
const L = 10; // number of blocks in the chain
const BP_THRESHOLD = 0.48; // BP threshold of the base LDPC ensemble
const MAP_THRESHOLD = 0.50;// MAP threshold = Shannon capacity limit
const BAR_MAX = 80; // max bar height in pixels (fully decoded)
const BAR_MIN = 16; // min bar height in pixels (high erasure)
let noise = 0.45;
// each block has a state:
// "undecoded" | "decoding" | "decoded" | "stuck"
let blockState = [];
// effective erasure fraction per block
let erasure = [];
let running = false;
function initChain() {
// start all interior blocks undecoded with erasure = noise
blockState = Array(L).fill('undecoded');
erasure = Array(L).fill(noise);
// pin the two boundary blocks — they represent the seed of the decoding wave
blockState[0] = 'decoded';
blockState[L - 1] = 'decoded';
erasure[0] = 0;
erasure[L - 1] = 0;
running = false;
renderChain();
setStatus('', '');
}
function renderChain() {
const chain = document.getElementById('chain');
chain.innerHTML = '';
for (let i = 0; i < L; i++) {
const wrap = document.createElement('div');
wrap.className = 'block';
const rect = document.createElement('div');
// bar height proportional to decoded fraction (1 - erasure)
const h = Math.round(BAR_MIN + (1 - erasure[i]) * (BAR_MAX - BAR_MIN));
rect.className = 'block-rect ' + blockState[i];
rect.style.height = h + 'px';
rect.title = fmt('Block %i% | ε=%v% | %v2%', { i: i, v: erasure[i].toFixed(3), v2: blockState[i] });
const lbl = document.createElement('div');
lbl.className = 'block-label';
lbl.textContent = i === 0 || i === L - 1 ? 'pin' : i;
wrap.appendChild(rect);
wrap.appendChild(lbl);
chain.appendChild(wrap);
}
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + cls;
}
// compute the effective erasure of block i under spatial coupling
function coupledErasure(i) {
if (blockState[i] === 'decoded') return 0;
// borrow information from left and right neighbors
const left = i > 0 ? erasure[i - 1] : 0;
const right = i < L - 1 ? erasure[i + 1] : 0;
// weighted average — coupling strength 0.3 per neighbor
return (erasure[i] + 0.3 * left + 0.3 * right) / 1.6;
}
// advance the wave by one iteration across all interior blocks
function stepWave() {
let changed = false;
const newState = [...blockState];
const newErasure = [...erasure];
for (let i = 1; i < L - 1; i++) {
if (blockState[i] === 'decoded') continue;
const eff = coupledErasure(i);
if (eff < BP_THRESHOLD) {
// effective erasure is below BP threshold: block can decode
newState[i] = 'decoding';
newErasure[i] = Math.max(0, eff * 0.25);
changed = true;
} else if (eff >= MAP_THRESHOLD) {
newState[i] = 'stuck';
}
}
// promote decoding blocks to decoded once erasure is negligible
for (let i = 1; i < L - 1; i++) {
if (newState[i] === 'decoding' && newErasure[i] < 0.05) {
newState[i] = 'decoded';
}
}
blockState = newState;
erasure = newErasure;
renderChain();
return changed;
}
function allDecoded() {
return blockState.every(s => s === 'decoded');
}
function anyStuck() {
return blockState.some(s => s === 'stuck');
}
function decodeAll() {
let iter = 0;
const maxIter = 60;
running = true;
document.getElementById('btnStep').disabled = true;
document.getElementById('btnAll').disabled = true;
function tick() {
if (!running || iter >= maxIter) {
running = false;
document.getElementById('btnStep').disabled = false;
document.getElementById('btnAll').disabled = false;
if (allDecoded()) {
setStatus('Wave decoded all blocks — threshold saturation achieved!', 'ok');
} else if (anyStuck()) {
setStatus('Wave stopped: noise above MAP threshold, some blocks stuck.', 'bad');
} else {
setStatus('Wave stalled — no further progress.', 'info');
}
return;
}
const changed = stepWave();
iter++;
if (allDecoded()) {
renderChain();
running = false;
document.getElementById('btnStep').disabled = false;
document.getElementById('btnAll').disabled = false;
setStatus('Wave decoded all blocks — threshold saturation achieved!', 'ok');
console.log('Decoded in', iter, 'iterations');
return;
}
if (!changed || anyStuck()) {
running = false;
document.getElementById('btnStep').disabled = false;
document.getElementById('btnAll').disabled = false;
setStatus(anyStuck() ? 'Wave stopped: noise above MAP threshold, some blocks stuck.' : 'Wave stalled — no further progress.', 'bad');
console.log('Wave failed after', iter);
return;
}
setTimeout(tick, 120);
}
tick();
}
// wire up UI controls
document.getElementById('noiseSlider').addEventListener('input', function () {
noise = parseFloat(this.value);
document.getElementById('noiseVal').textContent = noise.toFixed(2);
initChain();
});
document.getElementById('btnStep').addEventListener('click', function () {
if (allDecoded()) { setStatus('All blocks already decoded.', 'ok'); return; }
const changed = stepWave();
if (allDecoded()) setStatus('Wave decoded all blocks — threshold saturation achieved!', 'ok');
else if (anyStuck()) setStatus('Wave stopped: noise above MAP threshold, some blocks stuck.', 'bad');
else if (!changed) setStatus('Wave stalled — no further progress.', 'bad');
else setStatus('Wave is propagating…', 'info');
});
document.getElementById('btnAll').addEventListener('click', decodeAll);
document.getElementById('btnReset').addEventListener('click', initChain);
initChain();
<!-- Chain of L coupled LDPC blocks; boundary blocks are pinned (decoded from the start) -->
<div class="controls">
<label>
<span>Noise ε:</span>
<input type="range" id="noiseSlider" min="0.30" max="0.55" step="0.01" value="0.45">
<span id="noiseVal">0.45</span>
</label>
<div class="btns">
<button id="btnStep" type="button">Step</button>
<button id="btnAll" type="button">Decode all</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
</div>
<div class="chain-wrap">
<div id="chain" class="chain"></div>
</div>
<div id="status" class="status"></div>
<div class="legend">
<span class="leg-item"><span class="leg-box decoded"></span> Decoded</span>
<span class="leg-item"><span class="leg-box decoding"></span> Decoding</span>
<span class="leg-item"><span class="leg-box stuck"></span> Stuck</span>
<span class="leg-item"><span class="leg-box undecoded"></span> Undecoded</span>
</div>
/* SC-LDPC chain visualization — bar height encodes erasure fraction */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; padding: 10px; }
.controls { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
.controls label { display: flex; align-items: center; gap: 8px; font-size: .9rem; }
.controls input[type=range] { flex: 1; max-width: 200px; }
.btns { display: flex; gap: 6px; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.chain-wrap { overflow-x: auto; padding-bottom: 4px; }
.chain { display: flex; gap: 4px; align-items: flex-end; min-width: max-content; margin: 0 auto; }
.block { display: flex; flex-direction: column; align-items: center; gap: 3px; width: 50px; }
.block-rect { width: 44px; border-radius: 7px; transition: background .35s, height .35s; }
.block-label { font-size: .7rem; color: #666; }
/* Block state color coding */
.decoded { background: #2a9d5c; }
.decoding { background: #e9c46a; }
.stuck { background: #e63946; }
.undecoded{ background: #c9ccd1; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.5em; margin: 8px 0 6px; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info{ color: #1d3557; }
.legend { display: flex; gap: 10px; flex-wrap: wrap; font-size: .78rem; color: #555; margin-top: 4px; }
.leg-item { display: flex; align-items: center; gap: 4px; }
.leg-box { display: inline-block; width: 14px; height: 14px; border-radius: 3px; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Spatially-Coupled LDPC demo: a chain of L=10 blocks.
// Boundary blocks (0 and L-1) are pinned (zero erasure).
// Each step propagates decoding inward using effective erasure from neighbors.
const L = 10; // number of blocks in the chain
const BP_THRESHOLD = 0.48; // BP threshold of the base LDPC ensemble
const MAP_THRESHOLD = 0.50;// MAP threshold = Shannon capacity limit
const BAR_MAX = 80; // max bar height in pixels (fully decoded)
const BAR_MIN = 16; // min bar height in pixels (high erasure)
let noise = 0.45;
// each block has a state:
// "undecoded" | "decoding" | "decoded" | "stuck"
let blockState = [];
// effective erasure fraction per block
let erasure = [];
let running = false;
function initChain() {
// start all interior blocks undecoded with erasure = noise
blockState = Array(L).fill('undecoded');
erasure = Array(L).fill(noise);
// pin the two boundary blocks — they represent the seed of the decoding wave
blockState[0] = 'decoded';
blockState[L - 1] = 'decoded';
erasure[0] = 0;
erasure[L - 1] = 0;
running = false;
renderChain();
setStatus('', '');
}
function renderChain() {
const chain = document.getElementById('chain');
chain.innerHTML = '';
for (let i = 0; i < L; i++) {
const wrap = document.createElement('div');
wrap.className = 'block';
const rect = document.createElement('div');
// bar height proportional to decoded fraction (1 - erasure)
const h = Math.round(BAR_MIN + (1 - erasure[i]) * (BAR_MAX - BAR_MIN));
rect.className = 'block-rect ' + blockState[i];
rect.style.height = h + 'px';
rect.title = fmt('Block %i% | ε=%v% | %v2%', { i: i, v: erasure[i].toFixed(3), v2: blockState[i] });
const lbl = document.createElement('div');
lbl.className = 'block-label';
lbl.textContent = i === 0 || i === L - 1 ? 'pin' : i;
wrap.appendChild(rect);
wrap.appendChild(lbl);
chain.appendChild(wrap);
}
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + cls;
}
// compute the effective erasure of block i under spatial coupling
function coupledErasure(i) {
if (blockState[i] === 'decoded') return 0;
// borrow information from left and right neighbors
const left = i > 0 ? erasure[i - 1] : 0;
const right = i < L - 1 ? erasure[i + 1] : 0;
// weighted average — coupling strength 0.3 per neighbor
return (erasure[i] + 0.3 * left + 0.3 * right) / 1.6;
}
// advance the wave by one iteration across all interior blocks
function stepWave() {
let changed = false;
const newState = [...blockState];
const newErasure = [...erasure];
for (let i = 1; i < L - 1; i++) {
if (blockState[i] === 'decoded') continue;
const eff = coupledErasure(i);
if (eff < BP_THRESHOLD) {
// effective erasure is below BP threshold: block can decode
newState[i] = 'decoding';
newErasure[i] = Math.max(0, eff * 0.25);
changed = true;
} else if (eff >= MAP_THRESHOLD) {
newState[i] = 'stuck';
}
}
// promote decoding blocks to decoded once erasure is negligible
for (let i = 1; i < L - 1; i++) {
if (newState[i] === 'decoding' && newErasure[i] < 0.05) {
newState[i] = 'decoded';
}
}
blockState = newState;
erasure = newErasure;
renderChain();
return changed;
}
function allDecoded() {
return blockState.every(s => s === 'decoded');
}
function anyStuck() {
return blockState.some(s => s === 'stuck');
}
function decodeAll() {
let iter = 0;
const maxIter = 60;
running = true;
document.getElementById('btnStep').disabled = true;
document.getElementById('btnAll').disabled = true;
function tick() {
if (!running || iter >= maxIter) {
running = false;
document.getElementById('btnStep').disabled = false;
document.getElementById('btnAll').disabled = false;
if (allDecoded()) {
setStatus('Wave decoded all blocks — threshold saturation achieved!', 'ok');
} else if (anyStuck()) {
setStatus('Wave stopped: noise above MAP threshold, some blocks stuck.', 'bad');
} else {
setStatus('Wave stalled — no further progress.', 'info');
}
return;
}
const changed = stepWave();
iter++;
if (allDecoded()) {
renderChain();
running = false;
document.getElementById('btnStep').disabled = false;
document.getElementById('btnAll').disabled = false;
setStatus('Wave decoded all blocks — threshold saturation achieved!', 'ok');
console.log('Decoded in', iter, 'iterations');
return;
}
if (!changed || anyStuck()) {
running = false;
document.getElementById('btnStep').disabled = false;
document.getElementById('btnAll').disabled = false;
setStatus(anyStuck() ? 'Wave stopped: noise above MAP threshold, some blocks stuck.' : 'Wave stalled — no further progress.', 'bad');
console.log('Wave failed after', iter);
return;
}
setTimeout(tick, 120);
}
tick();
}
// wire up UI controls
document.getElementById('noiseSlider').addEventListener('input', function () {
noise = parseFloat(this.value);
document.getElementById('noiseVal').textContent = noise.toFixed(2);
initChain();
});
document.getElementById('btnStep').addEventListener('click', function () {
if (allDecoded()) { setStatus('All blocks already decoded.', 'ok'); return; }
const changed = stepWave();
if (allDecoded()) setStatus('Wave decoded all blocks — threshold saturation achieved!', 'ok');
else if (anyStuck()) setStatus('Wave stopped: noise above MAP threshold, some blocks stuck.', 'bad');
else if (!changed) setStatus('Wave stalled — no further progress.', 'bad');
else setStatus('Wave is propagating…', 'info');
});
document.getElementById('btnAll').addEventListener('click', decodeAll);
document.getElementById('btnReset').addEventListener('click', initChain);
initChain();
Notice: when the noise is below the BP threshold (roughly ε<0.48 for this ensemble), the wave always finishes. When it is above the BP threshold but below the MAP threshold (0.48<ε<0.5), an uncoupled code would fail — but the wave still propagates from the pinned boundary and decodes every block. That is threshold saturation in action.
Comments
Loading comments...