Type a sequence of non-negative integers separated by spaces and watch Golomb-Rice encode each one. Adjust the divisor m (a power of two gives Rice coding) and see how the bit counts change.
<p class="hint">Encode non-negative integers with Golomb-Rice and compare against standard 8-bit fixed-width codes. Adjust <b>m</b> (divisor) to tune performance. Powers of 2 give Rice coding — fast on hardware.</p>
<div class="controls">
<label>Numbers: <input id="nums" type="text" value="0 1 0 2 1 0 3 0 1 0 2 5 0 1" /></label>
<label>Divisor m: <input id="mval" type="number" min="1" max="64" value="4" /></label>
<button id="encode" type="button">Encode</button>
<button id="preset1" type="button" class="ghost">Audio residuals</button>
<button id="preset2" type="button" class="ghost">Large numbers</button>
</div>
<div id="output"></div>
<div id="summary"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.4; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .8rem; }
label { display: flex; align-items: center; gap: .3rem; font-size: .85rem; }
input[type=text] { width: 200px; padding: .3rem .5rem; border: 1px solid #bbb; border-radius: 6px; font-size: .85rem; }
input[type=number] { width: 60px; padding: .3rem .5rem; border: 1px solid #bbb; border-radius: 6px; font-size: .85rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
table { border-collapse: collapse; width: 100%; margin: .5rem 0; font-size: .82rem; }
th { background: #e8eef3; color: #1d3557; text-align: left; padding: .35rem .5rem; border-bottom: 2px solid #b0c4d8; }
td { padding: .28rem .5rem; border-bottom: 1px solid #e0e8f0; font-family: ui-monospace, monospace; vertical-align: middle; }
tr:hover td { background: #f3f7fb; }
.bits-g { color: #0a6ea0; font-weight: 600; }
.bits-f { color: #888; }
.bar-wrap { display: flex; align-items: center; gap: 3px; }
.bar { height: 9px; border-radius: 2px; display: inline-block; min-width: 2px; }
.bar-g { background: #0a6ea0; }
.bar-f { background: #ccc; }
#summary { margin-top: .6rem; padding: .55rem .8rem; background: #e8f4ec; border-radius: 8px; font-size: .88rem; line-height: 1.5; }
#summary strong { color: #0a7d33; }
#summary.worse { background: #fde8e8; }
#summary.worse strong { color: #c92f3c; }
.err { color: #c92f3c; font-size: .9rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Golomb-Rice coding: encode non-negative integers with divisor m.
// For m = power-of-2 this is Rice coding (remainder uses exactly log2(m) bits).
//
// Encoding n with divisor m:
// q = floor(n / m) -> unary code: q ones followed by a zero (q+1 bits)
// r = n mod m -> truncated binary code
//
// Truncated binary for r with modulus m:
// k = floor(log2(m)), threshold = 2^(k+1) - m
// if r < threshold: use k bits
// else: use k+1 bits
//
// Fixed-width baseline: 8 bits per number (standard PCM/genomic integer size)
const FIXED_BITS = 8; // standard fixed-width comparison
function golombBits(n, m) {
if (n < 0 || !Number.isInteger(n) || m < 1 || !Number.isInteger(m)) return null;
const q = Math.floor(n / m);
const r = n % m;
const k = Math.floor(Math.log2(m));
const threshold = (1 << (k + 1)) - m;
const rBits = r < threshold ? k : k + 1;
const unaryBits = q + 1;
return {
q, r, rBits, unaryBits, total: unaryBits + rBits,
unary: '1'.repeat(q) + '0',
rem: r.toString(2).padStart(rBits, '0')
};
}
function encode() {
const raw = document.getElementById('nums').value.trim();
const m = parseInt(document.getElementById('mval').value, 10);
const out = document.getElementById('output');
const sumEl = document.getElementById('summary');
if (!raw || isNaN(m) || m < 1) {
out.innerHTML = '<p class="err">Enter numbers and a valid m ≥ 1.</p>';
sumEl.innerHTML = ''; return;
}
const nums = raw.split(/\s+/).map(s => parseInt(s, 10));
if (nums.some(isNaN) || nums.some(n => n < 0)) {
out.innerHTML = '<p class="err">All numbers must be non-negative integers.</p>';
sumEl.innerHTML = ''; return;
}
let totalG = 0, totalF = 0;
const scale = 2.8;
let rows = '';
for (const n of nums) {
const g = golombBits(n, m);
if (!g) { out.innerHTML = fmt(`<p class="err">Encoding error for n=%n%</p>`, { n: n }); return; }
totalG += g.total;
totalF += FIXED_BITS;
rows += fmt(`<tr>
<td>%n%</td>
<td class="bits-g">%unary% | %rem%</td>
<td class="bits-g">%total%</td>
<td class="bits-f">%FIXED_BITS%</td>
<td><div class="bar-wrap">
<span class="bar bar-g" style="width:%round%px" title="Golomb: %total%b"></span>
<span class="bar bar-f" style="width:%round2%px" title="Fixed: %FIXED_BITS%b"></span>
</div></td>
</tr>`, { n: n, unary: g.unary, rem: g.rem, total: g.total, FIXED_BITS: FIXED_BITS, round: Math.round(g.total*scale), round2: Math.round(FIXED_BITS*scale) });
}
out.innerHTML = fmt(`<table>
<thead><tr><th>n</th><th>Golomb (unary | remainder)</th><th>Golomb bits</th><th>Fixed-8 bits</th><th>Visual comparison</th></tr></thead>
<tbody>%rows%</tbody>
</table>`, { rows: rows });
const saved = totalF - totalG;
const pct = ((saved / totalF) * 100).toFixed(1);
const better = saved > 0;
sumEl.className = better ? '' : 'worse';
sumEl.innerHTML = fmt(`<strong>%v%</strong>
— Golomb total: <b>%totalG% bits</b> | Fixed-8 total: <b>%totalF% bits</b> (%length% numbers × %FIXED_BITS% bits each).
%v2%`, { v: better
? 'Golomb-Rice saves ' + saved + ' bits (' + pct + `% smaller than fixed-width)`
: 'Fixed-width uses ' + Math.abs(saved) + ' fewer bits', totalG: totalG, totalF: totalF, length: nums.length, FIXED_BITS: FIXED_BITS, v2: better
? `Data clusters near zero → geometric assumption holds → Golomb wins.`
: `Numbers are large/spread out → fixed-width is more efficient here.` });
// Verification output for node testing
console.log('VERIFY', JSON.stringify({ nums, m, totalG, totalF, saved, pct }));
}
document.getElementById('encode').addEventListener('click', encode);
document.getElementById('preset1').addEventListener('click', () => {
document.getElementById('nums').value = '0 1 0 2 1 0 3 0 1 0 2 5 0 1';
document.getElementById('mval').value = '4';
encode();
});
document.getElementById('preset2').addEventListener('click', () => {
document.getElementById('nums').value = '100 200 150 180 220 90 210 170 140 190';
document.getElementById('mval').value = '4';
encode();
});
encode();
<p class="hint">Encode non-negative integers with Golomb-Rice and compare against standard 8-bit fixed-width codes. Adjust <b>m</b> (divisor) to tune performance. Powers of 2 give Rice coding — fast on hardware.</p>
<div class="controls">
<label>Numbers: <input id="nums" type="text" value="0 1 0 2 1 0 3 0 1 0 2 5 0 1" /></label>
<label>Divisor m: <input id="mval" type="number" min="1" max="64" value="4" /></label>
<button id="encode" type="button">Encode</button>
<button id="preset1" type="button" class="ghost">Audio residuals</button>
<button id="preset2" type="button" class="ghost">Large numbers</button>
</div>
<div id="output"></div>
<div id="summary"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.4; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: .8rem; }
label { display: flex; align-items: center; gap: .3rem; font-size: .85rem; }
input[type=text] { width: 200px; padding: .3rem .5rem; border: 1px solid #bbb; border-radius: 6px; font-size: .85rem; }
input[type=number] { width: 60px; padding: .3rem .5rem; border: 1px solid #bbb; border-radius: 6px; font-size: .85rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
table { border-collapse: collapse; width: 100%; margin: .5rem 0; font-size: .82rem; }
th { background: #e8eef3; color: #1d3557; text-align: left; padding: .35rem .5rem; border-bottom: 2px solid #b0c4d8; }
td { padding: .28rem .5rem; border-bottom: 1px solid #e0e8f0; font-family: ui-monospace, monospace; vertical-align: middle; }
tr:hover td { background: #f3f7fb; }
.bits-g { color: #0a6ea0; font-weight: 600; }
.bits-f { color: #888; }
.bar-wrap { display: flex; align-items: center; gap: 3px; }
.bar { height: 9px; border-radius: 2px; display: inline-block; min-width: 2px; }
.bar-g { background: #0a6ea0; }
.bar-f { background: #ccc; }
#summary { margin-top: .6rem; padding: .55rem .8rem; background: #e8f4ec; border-radius: 8px; font-size: .88rem; line-height: 1.5; }
#summary strong { color: #0a7d33; }
#summary.worse { background: #fde8e8; }
#summary.worse strong { color: #c92f3c; }
.err { color: #c92f3c; font-size: .9rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Golomb-Rice coding: encode non-negative integers with divisor m.
// For m = power-of-2 this is Rice coding (remainder uses exactly log2(m) bits).
//
// Encoding n with divisor m:
// q = floor(n / m) -> unary code: q ones followed by a zero (q+1 bits)
// r = n mod m -> truncated binary code
//
// Truncated binary for r with modulus m:
// k = floor(log2(m)), threshold = 2^(k+1) - m
// if r < threshold: use k bits
// else: use k+1 bits
//
// Fixed-width baseline: 8 bits per number (standard PCM/genomic integer size)
const FIXED_BITS = 8; // standard fixed-width comparison
function golombBits(n, m) {
if (n < 0 || !Number.isInteger(n) || m < 1 || !Number.isInteger(m)) return null;
const q = Math.floor(n / m);
const r = n % m;
const k = Math.floor(Math.log2(m));
const threshold = (1 << (k + 1)) - m;
const rBits = r < threshold ? k : k + 1;
const unaryBits = q + 1;
return {
q, r, rBits, unaryBits, total: unaryBits + rBits,
unary: '1'.repeat(q) + '0',
rem: r.toString(2).padStart(rBits, '0')
};
}
function encode() {
const raw = document.getElementById('nums').value.trim();
const m = parseInt(document.getElementById('mval').value, 10);
const out = document.getElementById('output');
const sumEl = document.getElementById('summary');
if (!raw || isNaN(m) || m < 1) {
out.innerHTML = '<p class="err">Enter numbers and a valid m ≥ 1.</p>';
sumEl.innerHTML = ''; return;
}
const nums = raw.split(/\s+/).map(s => parseInt(s, 10));
if (nums.some(isNaN) || nums.some(n => n < 0)) {
out.innerHTML = '<p class="err">All numbers must be non-negative integers.</p>';
sumEl.innerHTML = ''; return;
}
let totalG = 0, totalF = 0;
const scale = 2.8;
let rows = '';
for (const n of nums) {
const g = golombBits(n, m);
if (!g) { out.innerHTML = fmt(`<p class="err">Encoding error for n=%n%</p>`, { n: n }); return; }
totalG += g.total;
totalF += FIXED_BITS;
rows += fmt(`<tr>
<td>%n%</td>
<td class="bits-g">%unary% | %rem%</td>
<td class="bits-g">%total%</td>
<td class="bits-f">%FIXED_BITS%</td>
<td><div class="bar-wrap">
<span class="bar bar-g" style="width:%round%px" title="Golomb: %total%b"></span>
<span class="bar bar-f" style="width:%round2%px" title="Fixed: %FIXED_BITS%b"></span>
</div></td>
</tr>`, { n: n, unary: g.unary, rem: g.rem, total: g.total, FIXED_BITS: FIXED_BITS, round: Math.round(g.total*scale), round2: Math.round(FIXED_BITS*scale) });
}
out.innerHTML = fmt(`<table>
<thead><tr><th>n</th><th>Golomb (unary | remainder)</th><th>Golomb bits</th><th>Fixed-8 bits</th><th>Visual comparison</th></tr></thead>
<tbody>%rows%</tbody>
</table>`, { rows: rows });
const saved = totalF - totalG;
const pct = ((saved / totalF) * 100).toFixed(1);
const better = saved > 0;
sumEl.className = better ? '' : 'worse';
sumEl.innerHTML = fmt(`<strong>%v%</strong>
— Golomb total: <b>%totalG% bits</b> | Fixed-8 total: <b>%totalF% bits</b> (%length% numbers × %FIXED_BITS% bits each).
%v2%`, { v: better
? 'Golomb-Rice saves ' + saved + ' bits (' + pct + `% smaller than fixed-width)`
: 'Fixed-width uses ' + Math.abs(saved) + ' fewer bits', totalG: totalG, totalF: totalF, length: nums.length, FIXED_BITS: FIXED_BITS, v2: better
? `Data clusters near zero → geometric assumption holds → Golomb wins.`
: `Numbers are large/spread out → fixed-width is more efficient here.` });
// Verification output for node testing
console.log('VERIFY', JSON.stringify({ nums, m, totalG, totalF, saved, pct }));
}
document.getElementById('encode').addEventListener('click', encode);
document.getElementById('preset1').addEventListener('click', () => {
document.getElementById('nums').value = '0 1 0 2 1 0 3 0 1 0 2 5 0 1';
document.getElementById('mval').value = '4';
encode();
});
document.getElementById('preset2').addEventListener('click', () => {
document.getElementById('nums').value = '100 200 150 180 220 90 210 170 140 190';
document.getElementById('mval').value = '4';
encode();
});
encode();
Notice the asymmetry: small numbers near zero cost only 1–2 bits; large numbers cost proportionally more. With fixed-width 8-bit codes the total is always 8 × count bits. The savings only appear when your data is concentrated near zero — exactly the geometric-distribution assumption Golomb proved to be tight.
Comments
Loading comments...