The demo below shows a small expander-code codeword (8 bits). Each bit on the left is connected to two check nodes on the right. A check node is satisfied when the XOR (parity) of its connected bits is zero; otherwise it is unsatisfied (shown in red).
<!-- Expander codes demo HTML layout. -->
<p class="hint">Each circle (bit) connects to 3 square check nodes. A check node shows <b>✓</b> (green) when the XOR of its bits is 0, or <b>✗</b> (red) otherwise. Corrupt one bit, then decode using the bit-flipping algorithm.</p>
<div class="layout">
<div class="col" id="bit-col"><!-- Bit column (left side, circles) --></div>
<svg id="edges-svg" class="edges-svg" aria-hidden="true"></svg>
<div class="col" id="check-col"><!-- Check column (right side, squares) --></div>
</div>
<div class="status" id="status">Codeword is valid. Click Corrupt to introduce an error.</div>
<div class="btns">
<button id="corrupt" type="button">Corrupt (1 bit)</button>
<button id="decode" type="button">Decode (bit-flip)</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="legend">
<span class="legend-ok">● satisfied check</span>
<span class="legend-err">● failed check / corrupt bit</span>
<span class="legend-flip">● bit being flipped</span>
</div>
/* Expander codes demo styles. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.layout { display: flex; align-items: center; gap: 0; min-height: 320px; position: relative; }
.col { display: flex; flex-direction: column; gap: 6px; z-index: 1; }
#bit-col { margin-right: 6px; }
#check-col { margin-left: 6px; }
.edges-svg { flex: 1; height: 320px; overflow: visible; }
.node { width: 40px; height: 40px; display: flex; align-items: center;
justify-content: center; font: 700 14px ui-monospace, monospace;
border: 2px solid #adb1b8; background: #e0e4ea;
transition: background .25s, border-color .25s; user-select: none; }
.bit-node { border-radius: 50%; }
.check-node { border-radius: 6px; }
.corrupted { background: #e63946; border-color: #c92f3c; color: #fff; }
.flipped-now { background: #ff9f1c; border-color: #e0890f; color: #fff; }
.check-ok { background: #d4edda; border-color: #74c489; color: #0a7d33; font-size: 18px; }
.check-fail { background: #f8d7da; border-color: #e63946; color: #c92f3c; font-size: 18px; }
.edge { stroke: #c8cdd6; stroke-width: 1.4; transition: stroke .25s; }
.edge.active { stroke: #e63946; stroke-width: 2.2; }
.edge.flipping { stroke: #ff9f1c; stroke-width: 2.2; }
.status { font-size: .92rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
.legend { display: flex; gap: 1rem; font-size: .78rem; color: #555; flex-wrap: wrap; }
.legend-ok { color: #0a7d33; }
.legend-err { color: #c92f3c; }
.legend-flip { color: #e0890f; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Expander codes demo: Sipser-Spielman bit-flipping decoder.
// Codeword: 10 bits, 5 checks, each bit has degree 3 (connects to 3 checks).
// Decoder: each round flip the bit with the most unsatisfied checks (greedy bit-flip).
// Valid codeword (all 5 check parities = 0).
// H is a (5x10) LDPC parity-check matrix with 3 ones per column and 6 ones per row.
const CODEWORD = [1, 0, 1, 1, 0, 1, 0, 0, 0, 0];
const NUM_BITS = 10;
const NUM_CHECKS = 5;
// Bipartite edge list: [bit_index, check_index]. Each bit has degree 3.
// This sparse regular structure is what makes the code an expander code.
const EDGES = [
[0,0],[0,1],[0,2],
[1,0],[1,1],[1,3],
[2,0],[2,2],[2,4],
[3,1],[3,2],[3,3],
[4,1],[4,3],[4,4],
[5,2],[5,3],[5,4],
[6,0],[6,2],[6,3],
[7,0],[7,3],[7,4],
[8,1],[8,2],[8,4],
[9,0],[9,1],[9,4]
];
let bits = CODEWORD.slice(); // mutable current state of the 10 bits
let animating = false;
// Returns XOR of all bits in check c. 0 means the check is satisfied.
function checkValue(b, c) {
let xor = 0;
EDGES.forEach(([bi, ch]) => { if (ch === c) xor ^= b[bi]; });
return xor;
}
// Count of unsatisfied checks incident to bit i.
function unsatCount(b, i) {
let unsat = 0;
EDGES.forEach(([bit, ch]) => { if (bit === i && checkValue(b, ch) !== 0) unsat++; });
return unsat;
}
// Render the board. highlightFlip = bit index currently being flipped, or null.
function render(highlightFlip) {
const bitCol = document.getElementById('bit-col');
const checkCol = document.getElementById('check-col');
const svg = document.getElementById('edges-svg');
bitCol.innerHTML = '';
checkCol.innerHTML = '';
svg.innerHTML = '';
// Draw bit circles.
for (let i = 0; i < NUM_BITS; i++) {
const d = document.createElement('div');
d.className = 'node bit-node';
d.id = 'bit-' + i;
d.textContent = bits[i];
if (bits[i] !== CODEWORD[i]) d.classList.add('corrupted');
if (highlightFlip === i) d.classList.add('flipped-now');
bitCol.appendChild(d);
}
// Draw check squares.
for (let c = 0; c < NUM_CHECKS; c++) {
const d = document.createElement('div');
d.className = 'node check-node';
d.id = 'check-' + c;
const sat = checkValue(bits, c) === 0;
d.classList.add(sat ? 'check-ok' : 'check-fail');
d.textContent = sat ? '✓' : '✗';
d.title = 'C' + c + ' ' + (sat ? 'satisfied' : 'unsatisfied');
checkCol.appendChild(d);
}
// Draw edges (red=failing check, orange=bit being flipped).
requestAnimationFrame(() => {
const svgRect = svg.getBoundingClientRect();
EDGES.forEach(([b, c]) => {
const bEl = document.getElementById('bit-' + b);
const cEl = document.getElementById('check-' + c);
if (!bEl || !cEl) return;
const br = bEl.getBoundingClientRect();
const cr = cEl.getBoundingClientRect();
const x1 = br.right - svgRect.left;
const y1 = br.top + br.height / 2 - svgRect.top;
const x2 = cr.left - svgRect.left;
const y2 = cr.top + cr.height / 2 - svgRect.top;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
const sat = checkValue(bits, c) === 0;
const isFlipEdge = (highlightFlip === b);
line.className.baseVal = 'edge' + (isFlipEdge ? ' flipping' : (sat ? '' : ' active'));
svg.appendChild(line);
});
});
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
// Flip exactly 1 random bit to introduce an error.
function corrupt() {
if (animating) return;
bits = CODEWORD.slice();
// Choose a random bit position and flip it.
const pos = Math.floor(Math.random() * NUM_BITS);
bits[pos] ^= 1;
render(null);
setStatus(fmt('Bit %pos% corrupted. Click Decode to fix it.', { pos: pos }), 'bad');
}
// Run the greedy bit-flipping decoder step by step with animation.
function decode() {
if (animating) return;
const allSatAlready = Array.from({length: NUM_CHECKS}, (_, c) => checkValue(bits, c) === 0).every(Boolean);
if (allSatAlready) { setStatus('Codeword is already valid — nothing to decode.', 'ok'); return; }
animating = true;
document.getElementById('corrupt').disabled = true;
document.getElementById('decode').disabled = true;
let round = 0;
const MAX_ROUNDS = 15;
function step() {
const allSat = Array.from({length: NUM_CHECKS}, (_, c) => checkValue(bits, c) === 0).every(Boolean);
if (allSat) {
render(null);
setStatus('✓ All checks satisfied — codeword recovered!', 'ok');
animating = false;
document.getElementById('corrupt').disabled = false;
document.getElementById('decode').disabled = false;
return;
}
if (round >= MAX_ROUNDS) {
setStatus('Max rounds reached. Could not converge.', 'bad');
animating = false;
document.getElementById('corrupt').disabled = false;
document.getElementById('decode').disabled = false;
return;
}
// Greedy rule: flip the bit with the highest number of unsatisfied checks.
// For a single error, the corrupt bit has all 3 checks unsatisfied; others have at most 1.
let maxUnsat = 0, bestBit = -1;
for (let i = 0; i < NUM_BITS; i++) {
const u = unsatCount(bits, i);
if (u > maxUnsat) { maxUnsat = u; bestBit = i; }
}
if (bestBit < 0 || maxUnsat === 0) {
setStatus('No bit to flip — decoder stuck.', 'bad');
animating = false;
document.getElementById('corrupt').disabled = false;
document.getElementById('decode').disabled = false;
return;
}
render(bestBit);
setStatus(fmt('Round %round%%v%: flipping bit %bestBit% (%maxUnsat% unsatisfied checks).', { round: round, v: 1, bestBit: bestBit, maxUnsat: maxUnsat }), 'info');
setTimeout(() => {
bits[bestBit] ^= 1;
round++;
step();
}, 700);
}
step();
}
document.getElementById('corrupt').addEventListener('click', corrupt);
document.getElementById('decode').addEventListener('click', decode);
document.getElementById('reset').addEventListener('click', () => {
if (animating) return;
bits = CODEWORD.slice();
render(null);
setStatus('Codeword is valid. Click Corrupt to introduce an error.', '');
});
render(null);
<!-- Expander codes demo HTML layout. -->
<p class="hint">Each circle (bit) connects to 3 square check nodes. A check node shows <b>✓</b> (green) when the XOR of its bits is 0, or <b>✗</b> (red) otherwise. Corrupt one bit, then decode using the bit-flipping algorithm.</p>
<div class="layout">
<div class="col" id="bit-col"><!-- Bit column (left side, circles) --></div>
<svg id="edges-svg" class="edges-svg" aria-hidden="true"></svg>
<div class="col" id="check-col"><!-- Check column (right side, squares) --></div>
</div>
<div class="status" id="status">Codeword is valid. Click Corrupt to introduce an error.</div>
<div class="btns">
<button id="corrupt" type="button">Corrupt (1 bit)</button>
<button id="decode" type="button">Decode (bit-flip)</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="legend">
<span class="legend-ok">● satisfied check</span>
<span class="legend-err">● failed check / corrupt bit</span>
<span class="legend-flip">● bit being flipped</span>
</div>
/* Expander codes demo styles. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.layout { display: flex; align-items: center; gap: 0; min-height: 320px; position: relative; }
.col { display: flex; flex-direction: column; gap: 6px; z-index: 1; }
#bit-col { margin-right: 6px; }
#check-col { margin-left: 6px; }
.edges-svg { flex: 1; height: 320px; overflow: visible; }
.node { width: 40px; height: 40px; display: flex; align-items: center;
justify-content: center; font: 700 14px ui-monospace, monospace;
border: 2px solid #adb1b8; background: #e0e4ea;
transition: background .25s, border-color .25s; user-select: none; }
.bit-node { border-radius: 50%; }
.check-node { border-radius: 6px; }
.corrupted { background: #e63946; border-color: #c92f3c; color: #fff; }
.flipped-now { background: #ff9f1c; border-color: #e0890f; color: #fff; }
.check-ok { background: #d4edda; border-color: #74c489; color: #0a7d33; font-size: 18px; }
.check-fail { background: #f8d7da; border-color: #e63946; color: #c92f3c; font-size: 18px; }
.edge { stroke: #c8cdd6; stroke-width: 1.4; transition: stroke .25s; }
.edge.active { stroke: #e63946; stroke-width: 2.2; }
.edge.flipping { stroke: #ff9f1c; stroke-width: 2.2; }
.status { font-size: .92rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
.legend { display: flex; gap: 1rem; font-size: .78rem; color: #555; flex-wrap: wrap; }
.legend-ok { color: #0a7d33; }
.legend-err { color: #c92f3c; }
.legend-flip { color: #e0890f; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Expander codes demo: Sipser-Spielman bit-flipping decoder.
// Codeword: 10 bits, 5 checks, each bit has degree 3 (connects to 3 checks).
// Decoder: each round flip the bit with the most unsatisfied checks (greedy bit-flip).
// Valid codeword (all 5 check parities = 0).
// H is a (5x10) LDPC parity-check matrix with 3 ones per column and 6 ones per row.
const CODEWORD = [1, 0, 1, 1, 0, 1, 0, 0, 0, 0];
const NUM_BITS = 10;
const NUM_CHECKS = 5;
// Bipartite edge list: [bit_index, check_index]. Each bit has degree 3.
// This sparse regular structure is what makes the code an expander code.
const EDGES = [
[0,0],[0,1],[0,2],
[1,0],[1,1],[1,3],
[2,0],[2,2],[2,4],
[3,1],[3,2],[3,3],
[4,1],[4,3],[4,4],
[5,2],[5,3],[5,4],
[6,0],[6,2],[6,3],
[7,0],[7,3],[7,4],
[8,1],[8,2],[8,4],
[9,0],[9,1],[9,4]
];
let bits = CODEWORD.slice(); // mutable current state of the 10 bits
let animating = false;
// Returns XOR of all bits in check c. 0 means the check is satisfied.
function checkValue(b, c) {
let xor = 0;
EDGES.forEach(([bi, ch]) => { if (ch === c) xor ^= b[bi]; });
return xor;
}
// Count of unsatisfied checks incident to bit i.
function unsatCount(b, i) {
let unsat = 0;
EDGES.forEach(([bit, ch]) => { if (bit === i && checkValue(b, ch) !== 0) unsat++; });
return unsat;
}
// Render the board. highlightFlip = bit index currently being flipped, or null.
function render(highlightFlip) {
const bitCol = document.getElementById('bit-col');
const checkCol = document.getElementById('check-col');
const svg = document.getElementById('edges-svg');
bitCol.innerHTML = '';
checkCol.innerHTML = '';
svg.innerHTML = '';
// Draw bit circles.
for (let i = 0; i < NUM_BITS; i++) {
const d = document.createElement('div');
d.className = 'node bit-node';
d.id = 'bit-' + i;
d.textContent = bits[i];
if (bits[i] !== CODEWORD[i]) d.classList.add('corrupted');
if (highlightFlip === i) d.classList.add('flipped-now');
bitCol.appendChild(d);
}
// Draw check squares.
for (let c = 0; c < NUM_CHECKS; c++) {
const d = document.createElement('div');
d.className = 'node check-node';
d.id = 'check-' + c;
const sat = checkValue(bits, c) === 0;
d.classList.add(sat ? 'check-ok' : 'check-fail');
d.textContent = sat ? '✓' : '✗';
d.title = 'C' + c + ' ' + (sat ? 'satisfied' : 'unsatisfied');
checkCol.appendChild(d);
}
// Draw edges (red=failing check, orange=bit being flipped).
requestAnimationFrame(() => {
const svgRect = svg.getBoundingClientRect();
EDGES.forEach(([b, c]) => {
const bEl = document.getElementById('bit-' + b);
const cEl = document.getElementById('check-' + c);
if (!bEl || !cEl) return;
const br = bEl.getBoundingClientRect();
const cr = cEl.getBoundingClientRect();
const x1 = br.right - svgRect.left;
const y1 = br.top + br.height / 2 - svgRect.top;
const x2 = cr.left - svgRect.left;
const y2 = cr.top + cr.height / 2 - svgRect.top;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
const sat = checkValue(bits, c) === 0;
const isFlipEdge = (highlightFlip === b);
line.className.baseVal = 'edge' + (isFlipEdge ? ' flipping' : (sat ? '' : ' active'));
svg.appendChild(line);
});
});
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
// Flip exactly 1 random bit to introduce an error.
function corrupt() {
if (animating) return;
bits = CODEWORD.slice();
// Choose a random bit position and flip it.
const pos = Math.floor(Math.random() * NUM_BITS);
bits[pos] ^= 1;
render(null);
setStatus(fmt('Bit %pos% corrupted. Click Decode to fix it.', { pos: pos }), 'bad');
}
// Run the greedy bit-flipping decoder step by step with animation.
function decode() {
if (animating) return;
const allSatAlready = Array.from({length: NUM_CHECKS}, (_, c) => checkValue(bits, c) === 0).every(Boolean);
if (allSatAlready) { setStatus('Codeword is already valid — nothing to decode.', 'ok'); return; }
animating = true;
document.getElementById('corrupt').disabled = true;
document.getElementById('decode').disabled = true;
let round = 0;
const MAX_ROUNDS = 15;
function step() {
const allSat = Array.from({length: NUM_CHECKS}, (_, c) => checkValue(bits, c) === 0).every(Boolean);
if (allSat) {
render(null);
setStatus('✓ All checks satisfied — codeword recovered!', 'ok');
animating = false;
document.getElementById('corrupt').disabled = false;
document.getElementById('decode').disabled = false;
return;
}
if (round >= MAX_ROUNDS) {
setStatus('Max rounds reached. Could not converge.', 'bad');
animating = false;
document.getElementById('corrupt').disabled = false;
document.getElementById('decode').disabled = false;
return;
}
// Greedy rule: flip the bit with the highest number of unsatisfied checks.
// For a single error, the corrupt bit has all 3 checks unsatisfied; others have at most 1.
let maxUnsat = 0, bestBit = -1;
for (let i = 0; i < NUM_BITS; i++) {
const u = unsatCount(bits, i);
if (u > maxUnsat) { maxUnsat = u; bestBit = i; }
}
if (bestBit < 0 || maxUnsat === 0) {
setStatus('No bit to flip — decoder stuck.', 'bad');
animating = false;
document.getElementById('corrupt').disabled = false;
document.getElementById('decode').disabled = false;
return;
}
render(bestBit);
setStatus(fmt('Round %round%%v%: flipping bit %bestBit% (%maxUnsat% unsatisfied checks).', { round: round, v: 1, bestBit: bestBit, maxUnsat: maxUnsat }), 'info');
setTimeout(() => {
bits[bestBit] ^= 1;
round++;
step();
}, 700);
}
step();
}
document.getElementById('corrupt').addEventListener('click', corrupt);
document.getElementById('decode').addEventListener('click', decode);
document.getElementById('reset').addEventListener('click', () => {
if (animating) return;
bits = CODEWORD.slice();
render(null);
setStatus('Codeword is valid. Click Corrupt to introduce an error.', '');
});
render(null);
Comments
Loading comments...