Here is the heart of QEC made visible. One logical bit is stored across several data qubits (the circles). Between every pair sits a parity check that asks a single yes/no question: "do my two neighbors disagree?" — without revealing what either one actually is.
Click a data qubit to inject an error (a bit-flip). Watch the checks: an error makes exactly the checks on its two sides light up red. That pattern of lit checks is called the syndrome. Then press Decode & correct — the decoder reads only the syndrome, deduces which qubit flipped, and repairs it. Notice it never had to look at the data.
<p class="hint">One <b>logical bit</b> lives across these data qubits (circles). Between each pair sits a <b>parity check</b> (diamonds): it lights up when its two neighbors disagree — without revealing their values. Click a qubit to inject a bit-flip, then let the decoder read only the lit checks (the <b>syndrome</b>) and repair it.</p>
<div id="line" class="line"></div>
<div class="status" id="status">Click a qubit to inject an error.</div>
<div class="btns">
<button id="decode" type="button">Decode & correct</button>
<button id="random" type="button">Random error</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.line { display: flex; align-items: center; justify-content: center; gap: 0; margin: 1rem 0; flex-wrap: wrap; }
.qubit { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center;
justify-content: center; font: 700 16px ui-monospace, monospace; cursor: pointer;
background: #e8eef3; color: #1d3557; border: 2px solid #cdd9e3; transition: all .12s; user-select: none; }
.qubit:hover { background: #d7e2ec; }
.qubit.flipped { background: #e63946; border-color: #c92f3c; color: #fff; }
.check { width: 26px; height: 26px; margin: 0 -2px; transform: rotate(45deg);
background: #eef1f4; border: 2px solid #cdd9e3; border-radius: 4px; transition: all .12s; }
.check.lit { background: #ffb703; border-color: #fb8500; box-shadow: 0 0 8px #ffb70399; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; text-align: center; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center; }
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] ?? ''));
// A distance-d repetition (bit-flip) code: ONE logical bit spread over N data
// qubits. A bit-flip on qubit i is recorded in err[i]. Between qubits i and i+1
// there is a parity check (stabilizer) that fires iff its two neighbors DISAGREE,
// i.e. syndrome s[i] = err[i] XOR err[i+1]. Crucially the check reveals only this
// parity, never the data values themselves — that is what makes QEC legal.
// The decoder sees only the syndrome and must reconstruct the error. For the
// repetition code, lit checks come in pairs bounding a flipped segment; the
// minimum-weight fix flips the shorter side. Here we show the canonical case:
// a single flip lights exactly the two checks on its sides.
const N = 5; // data qubits
let err = new Array(N).fill(0);
const syndrome = () => { // length N-1
const s = [];
for (let i = 0; i < N - 1; i++) s.push(err[i] ^ err[i + 1]);
return s;
};
// Minimum-weight decoder for the repetition code: the lit checks partition the
// line into segments; flipping qubits between consecutive defects (and the
// shorter of the two boundary regions) reproduces the syndrome with fewest flips.
function decode(s) {
const defects = [];
for (let i = 0; i < s.length; i++) if (s[i]) defects.push(i); // check i sits between qubit i and i+1
const fix = new Array(N).fill(0);
// Pair up defects left-to-right; each adjacent pair (a,b) means qubits a+1..b flipped.
for (let k = 0; k + 1 < defects.length; k += 2) {
for (let q = defects[k] + 1; q <= defects[k + 1]; q++) fix[q] ^= 1;
}
// Odd leftover defect: the flipped run reaches a boundary; flip the shorter side.
if (defects.length % 2 === 1) {
const d = defects[defects.length - 1];
const leftLen = d + 1, rightLen = N - 1 - d;
if (leftLen <= rightLen) for (let q = 0; q <= d; q++) fix[q] ^= 1;
else for (let q = d + 1; q < N; q++) fix[q] ^= 1;
}
return fix;
}
function render() {
const line = document.getElementById('line'); line.innerHTML = '';
const s = syndrome();
for (let i = 0; i < N; i++) {
const q = document.createElement('div');
q.className = 'qubit' + (err[i] ? ' flipped' : '');
q.textContent = 'q' + i;
q.addEventListener('click', () => { err[i] ^= 1; render(); setStatus('Click a qubit to inject an error.', ''); });
line.appendChild(q);
if (i < N - 1) {
const c = document.createElement('div');
c.className = 'check' + (s[i] ? ' lit' : '');
line.appendChild(c);
}
}
}
function setStatus(t, cls) { const el = document.getElementById('status'); el.textContent = t; el.className = 'status ' + cls; }
function doDecode() {
const s = syndrome();
if (!s.some(x => x)) { setStatus('No syndrome lit — nothing to correct.', 'ok'); return; }
const fix = decode(s);
for (let i = 0; i < N; i++) err[i] ^= fix[i]; // apply correction
render();
const fixed = fix.map((f, i) => f ? 'q' + i : null).filter(Boolean);
const clean = syndrome().every(x => !x);
setStatus(clean ? fmt('✓ Syndrome decoded — flipped back %fixed%. Logical bit restored.', { fixed: fixed.join(', ') })
: '✗ Residual error remains.', clean ? 'ok' : 'bad');
console.log('syndrome', s, '-> correction', fix, '-> residual', syndrome());
}
function randomErr() {
err = err.map(() => Math.random() < 0.25 ? 1 : 0);
if (!err.some(x => x)) err[Math.floor(Math.random() * N)] = 1;
render(); setStatus('Errors injected. Read the syndrome, then decode.', '');
}
document.getElementById('decode').addEventListener('click', doDecode);
document.getElementById('random').addEventListener('click', randomErr);
document.getElementById('reset').addEventListener('click', () => { err = new Array(N).fill(0); render(); setStatus('Click a qubit to inject an error.', ''); });
render();
<p class="hint">One <b>logical bit</b> lives across these data qubits (circles). Between each pair sits a <b>parity check</b> (diamonds): it lights up when its two neighbors disagree — without revealing their values. Click a qubit to inject a bit-flip, then let the decoder read only the lit checks (the <b>syndrome</b>) and repair it.</p>
<div id="line" class="line"></div>
<div class="status" id="status">Click a qubit to inject an error.</div>
<div class="btns">
<button id="decode" type="button">Decode & correct</button>
<button id="random" type="button">Random error</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.line { display: flex; align-items: center; justify-content: center; gap: 0; margin: 1rem 0; flex-wrap: wrap; }
.qubit { width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center;
justify-content: center; font: 700 16px ui-monospace, monospace; cursor: pointer;
background: #e8eef3; color: #1d3557; border: 2px solid #cdd9e3; transition: all .12s; user-select: none; }
.qubit:hover { background: #d7e2ec; }
.qubit.flipped { background: #e63946; border-color: #c92f3c; color: #fff; }
.check { width: 26px; height: 26px; margin: 0 -2px; transform: rotate(45deg);
background: #eef1f4; border: 2px solid #cdd9e3; border-radius: 4px; transition: all .12s; }
.check.lit { background: #ffb703; border-color: #fb8500; box-shadow: 0 0 8px #ffb70399; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; text-align: center; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; justify-content: center; }
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] ?? ''));
// A distance-d repetition (bit-flip) code: ONE logical bit spread over N data
// qubits. A bit-flip on qubit i is recorded in err[i]. Between qubits i and i+1
// there is a parity check (stabilizer) that fires iff its two neighbors DISAGREE,
// i.e. syndrome s[i] = err[i] XOR err[i+1]. Crucially the check reveals only this
// parity, never the data values themselves — that is what makes QEC legal.
// The decoder sees only the syndrome and must reconstruct the error. For the
// repetition code, lit checks come in pairs bounding a flipped segment; the
// minimum-weight fix flips the shorter side. Here we show the canonical case:
// a single flip lights exactly the two checks on its sides.
const N = 5; // data qubits
let err = new Array(N).fill(0);
const syndrome = () => { // length N-1
const s = [];
for (let i = 0; i < N - 1; i++) s.push(err[i] ^ err[i + 1]);
return s;
};
// Minimum-weight decoder for the repetition code: the lit checks partition the
// line into segments; flipping qubits between consecutive defects (and the
// shorter of the two boundary regions) reproduces the syndrome with fewest flips.
function decode(s) {
const defects = [];
for (let i = 0; i < s.length; i++) if (s[i]) defects.push(i); // check i sits between qubit i and i+1
const fix = new Array(N).fill(0);
// Pair up defects left-to-right; each adjacent pair (a,b) means qubits a+1..b flipped.
for (let k = 0; k + 1 < defects.length; k += 2) {
for (let q = defects[k] + 1; q <= defects[k + 1]; q++) fix[q] ^= 1;
}
// Odd leftover defect: the flipped run reaches a boundary; flip the shorter side.
if (defects.length % 2 === 1) {
const d = defects[defects.length - 1];
const leftLen = d + 1, rightLen = N - 1 - d;
if (leftLen <= rightLen) for (let q = 0; q <= d; q++) fix[q] ^= 1;
else for (let q = d + 1; q < N; q++) fix[q] ^= 1;
}
return fix;
}
function render() {
const line = document.getElementById('line'); line.innerHTML = '';
const s = syndrome();
for (let i = 0; i < N; i++) {
const q = document.createElement('div');
q.className = 'qubit' + (err[i] ? ' flipped' : '');
q.textContent = 'q' + i;
q.addEventListener('click', () => { err[i] ^= 1; render(); setStatus('Click a qubit to inject an error.', ''); });
line.appendChild(q);
if (i < N - 1) {
const c = document.createElement('div');
c.className = 'check' + (s[i] ? ' lit' : '');
line.appendChild(c);
}
}
}
function setStatus(t, cls) { const el = document.getElementById('status'); el.textContent = t; el.className = 'status ' + cls; }
function doDecode() {
const s = syndrome();
if (!s.some(x => x)) { setStatus('No syndrome lit — nothing to correct.', 'ok'); return; }
const fix = decode(s);
for (let i = 0; i < N; i++) err[i] ^= fix[i]; // apply correction
render();
const fixed = fix.map((f, i) => f ? 'q' + i : null).filter(Boolean);
const clean = syndrome().every(x => !x);
setStatus(clean ? fmt('✓ Syndrome decoded — flipped back %fixed%. Logical bit restored.', { fixed: fixed.join(', ') })
: '✗ Residual error remains.', clean ? 'ok' : 'bad');
console.log('syndrome', s, '-> correction', fix, '-> residual', syndrome());
}
function randomErr() {
err = err.map(() => Math.random() < 0.25 ? 1 : 0);
if (!err.some(x => x)) err[Math.floor(Math.random() * N)] = 1;
render(); setStatus('Errors injected. Read the syndrome, then decode.', '');
}
document.getElementById('decode').addEventListener('click', doDecode);
document.getElementById('random').addEventListener('click', randomErr);
document.getElementById('reset').addEventListener('click', () => { err = new Array(N).fill(0); render(); setStatus('Click a qubit to inject an error.', ''); });
render();
This is the whole magic in miniature: a single error lights up its two bordering checks; a decoder turns that fingerprint back into a fix. The real surface code does exactly this on a 2D grid, correcting both bit-flips and phase-flips at once. For a sense of why the underlying physics is so costly to simulate, see quantum simulation.
Comments
Loading comments...