<p class="hint">Toom-3 splits each number into 3 chunks, treats them as coefficients of a small polynomial, evaluates both polynomials at 5 points, multiplies the results pointwise, then interpolates back to the product. Enter two numbers and press <b>Run Toom-3</b> to watch every stage.</p>
<div class="inputs">
<label>Number A <input id="numA" type="text" inputmode="numeric" value="123456"></label>
<label>Number B <input id="numB" type="text" inputmode="numeric" value="654321"></label>
</div>
<div class="btns">
<button id="run" type="button">Run Toom-3</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="pipeline" id="pipeline"></div>
<div class="status" id="status">Press Run Toom-3 to see the pipeline.</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 .7rem; line-height: 1.45; }
.inputs { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .6rem; }
.inputs label { font-size: .85rem; font-weight: 600; color: #1d3557; display: flex; flex-direction: column; gap: .25rem; }
.inputs input { font: 600 15px ui-monospace, monospace; padding: .4rem .5rem; border: 1px solid #cdd9e3;
border-radius: 8px; width: 190px; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
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; }
.pipeline { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .6rem; }
.stage { border: 1px solid #cdd9e3; border-radius: 8px; padding: .5rem .7rem; background: #f6f8fa;
font-size: .82rem; line-height: 1.5; opacity: 0; transform: translateY(4px);
transition: opacity .25s ease, transform .25s ease; }
.stage.show { opacity: 1; transform: translateY(0); }
.stage .stage-title { font-weight: 700; color: #1d3557; display: block; margin-bottom: .2rem; }
.stage code { font: 600 .8rem ui-monospace, monospace; background: #e8eef3; padding: .05rem .3rem; border-radius: 4px; }
.row { display: flex; gap: .5rem; flex-wrap: wrap; }
.chip { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; border-radius: 6px;
padding: .15rem .45rem; font: 600 .78rem ui-monospace, monospace; }
.status { font-size: 1rem; font-weight: 600; margin: .3rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const numAEl = document.getElementById('numA');
const numBEl = document.getElementById('numB');
const pipelineEl = document.getElementById('pipeline');
const statusEl = document.getElementById('status');
// Split each number into 3 base-B chunks, least-significant chunk first.
function splitDigits(numStr, parts, chunkLen) {
const padded = numStr.padStart(parts * chunkLen, '0');
const chunks = [];
for (let i = 0; i < parts; i++) {
const start = padded.length - (i + 1) * chunkLen;
const end = padded.length - i * chunkLen;
chunks.push(BigInt(padded.slice(Math.max(0, start), end) || '0'));
}
return chunks; // [m0, m1, m2]: number = m0 + m1*B + m2*B^2
}
// Evaluate the 3-coefficient polynomial at x = 0, 1, -1, 2 and (implicitly) infinity.
function evalPoly([c0, c1, c2]) {
return {
p0: c0,
p1: c0 + c1 + c2,
pm1: c0 - c1 + c2,
p2: c0 + 2n * c1 + 4n * c2,
pinf: c2,
};
}
// Recover the 5 coefficients of the degree-4 product polynomial from its 5 sampled values.
function interpolate(r0, r1, rm1, r2, rinf) {
const c0 = r0;
const c4 = rinf;
const e = (r1 + rm1) / 2n;
const o = (r1 - rm1) / 2n;
const c2 = e - c0 - c4;
const c3 = (r2 - c0 - 2n * o - 4n * c2 - 16n * c4) / 6n;
const c1 = o - c3;
return [c0, c1, c2, c3, c4];
}
function addStage(title, bodyHtml) {
const d = document.createElement('div');
d.className = 'stage';
d.innerHTML = '<span class="stage-title">' + title + '</span>' + bodyHtml;
pipelineEl.appendChild(d);
requestAnimationFrame(() => requestAnimationFrame(() => d.classList.add('show')));
return d;
}
function chip(v) { return '<span class="chip">' + v + '</span>'; }
function setStatus(t, cls) { statusEl.textContent = t; statusEl.className = 'status ' + (cls || ''); }
function run() {
pipelineEl.innerHTML = '';
const aStr = (numAEl.value || '0').replace(/\D/g, '') || '0';
const bStr = (numBEl.value || '0').replace(/\D/g, '') || '0';
const chunkLen = Math.max(Math.ceil(aStr.length / 3), Math.ceil(bStr.length / 3), 1);
const B = 10n ** BigInt(chunkLen);
const a = splitDigits(aStr, 3, chunkLen);
const b = splitDigits(bStr, 3, chunkLen);
addStage('1. Split into chunks', i18nFmt('Each number becomes 3 chunks (least-significant first): A = %a0% + %a1%·B + %a2%·B², B = %b0% + %b1%·B + %b2%·B².', {
a: aStr, b: bStr,
a2: a[2], a1: a[1], a0: a[0],
b2: b[2], b1: b[1], b0: b[0],
}));
const pa = evalPoly(a);
const pb = evalPoly(b);
addStage('2. Evaluate both polynomials at 5 points', '<div>Instead of multiplying every chunk of A by every chunk of B, evaluate A(x) and B(x) at x = 0, 1, -1, 2 and ∞:</div>' +
'<div class="row">' + chip('A(0)=' + pa.p0) + chip('A(1)=' + pa.p1) + chip('A(-1)=' + pa.pm1) +
chip('A(2)=' + pa.p2) + chip('A(∞)=' + pa.pinf) + '</div>' +
'<div class="row">' + chip('B(0)=' + pb.p0) + chip('B(1)=' + pb.p1) + chip('B(-1)=' + pb.pm1) +
chip('B(2)=' + pb.p2) + chip('B(∞)=' + pb.pinf) + '</div>');
const r0 = pa.p0 * pb.p0;
const r1 = pa.p1 * pb.p1;
const rm1 = pa.pm1 * pb.pm1;
const r2 = pa.p2 * pb.p2;
const rinf = pa.pinf * pb.pinf;
addStage('3. Multiply pointwise (the 5 hard multiplications)', '<div>Multiply the matching values together — only 5 products, versus the 9 chunk-products schoolbook multiplication would need:</div>' +
'<div class="row">' + chip('R(0)=' + r0) + chip('R(1)=' + r1) + chip('R(-1)=' + rm1) +
chip('R(2)=' + r2) + chip('R(∞)=' + rinf) + '</div>');
const coeffs = interpolate(r0, r1, rm1, r2, rinf);
addStage('4. Interpolate', '<div>Solve a small fixed linear system to recover the 5 coefficients of the product polynomial from its 5 sampled values:</div>' +
'<div class="row">' + coeffs.map((c, i) => chip('c' + i + '=' + c)).join('') + '</div>');
let product = 0n;
for (let i = 0; i < coeffs.length; i++) product += coeffs[i] * (B ** BigInt(i));
addStage('5. Recombine', i18nFmt('Substitute the chunk base B back into the coefficients to get the final product: %product%', { product: product.toString() }));
const expected = BigInt(aStr || '0') * BigInt(bStr || '0');
if (product === expected) {
setStatus(i18nFmt('Correct! Toom-3 matched schoolbook multiplication using %toom% pointwise products instead of %schoolbook%.', { schoolbook: 9, toom: 5 }), 'ok');
} else {
setStatus('Something doesn’t add up — check the interpolation step.', 'bad');
}
}
document.getElementById('run').addEventListener('click', run);
document.getElementById('reset').addEventListener('click', () => {
numAEl.value = '123456';
numBEl.value = '654321';
pipelineEl.innerHTML = '';
setStatus('Press Run Toom-3 to see the pipeline.', '');
});
run();
<p class="hint">Toom-3 splits each number into 3 chunks, treats them as coefficients of a small polynomial, evaluates both polynomials at 5 points, multiplies the results pointwise, then interpolates back to the product. Enter two numbers and press <b>Run Toom-3</b> to watch every stage.</p>
<div class="inputs">
<label>Number A <input id="numA" type="text" inputmode="numeric" value="123456"></label>
<label>Number B <input id="numB" type="text" inputmode="numeric" value="654321"></label>
</div>
<div class="btns">
<button id="run" type="button">Run Toom-3</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="pipeline" id="pipeline"></div>
<div class="status" id="status">Press Run Toom-3 to see the pipeline.</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 .7rem; line-height: 1.45; }
.inputs { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .6rem; }
.inputs label { font-size: .85rem; font-weight: 600; color: #1d3557; display: flex; flex-direction: column; gap: .25rem; }
.inputs input { font: 600 15px ui-monospace, monospace; padding: .4rem .5rem; border: 1px solid #cdd9e3;
border-radius: 8px; width: 190px; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
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; }
.pipeline { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .6rem; }
.stage { border: 1px solid #cdd9e3; border-radius: 8px; padding: .5rem .7rem; background: #f6f8fa;
font-size: .82rem; line-height: 1.5; opacity: 0; transform: translateY(4px);
transition: opacity .25s ease, transform .25s ease; }
.stage.show { opacity: 1; transform: translateY(0); }
.stage .stage-title { font-weight: 700; color: #1d3557; display: block; margin-bottom: .2rem; }
.stage code { font: 600 .8rem ui-monospace, monospace; background: #e8eef3; padding: .05rem .3rem; border-radius: 4px; }
.row { display: flex; gap: .5rem; flex-wrap: wrap; }
.chip { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; border-radius: 6px;
padding: .15rem .45rem; font: 600 .78rem ui-monospace, monospace; }
.status { font-size: 1rem; font-weight: 600; margin: .3rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const numAEl = document.getElementById('numA');
const numBEl = document.getElementById('numB');
const pipelineEl = document.getElementById('pipeline');
const statusEl = document.getElementById('status');
// Split each number into 3 base-B chunks, least-significant chunk first.
function splitDigits(numStr, parts, chunkLen) {
const padded = numStr.padStart(parts * chunkLen, '0');
const chunks = [];
for (let i = 0; i < parts; i++) {
const start = padded.length - (i + 1) * chunkLen;
const end = padded.length - i * chunkLen;
chunks.push(BigInt(padded.slice(Math.max(0, start), end) || '0'));
}
return chunks; // [m0, m1, m2]: number = m0 + m1*B + m2*B^2
}
// Evaluate the 3-coefficient polynomial at x = 0, 1, -1, 2 and (implicitly) infinity.
function evalPoly([c0, c1, c2]) {
return {
p0: c0,
p1: c0 + c1 + c2,
pm1: c0 - c1 + c2,
p2: c0 + 2n * c1 + 4n * c2,
pinf: c2,
};
}
// Recover the 5 coefficients of the degree-4 product polynomial from its 5 sampled values.
function interpolate(r0, r1, rm1, r2, rinf) {
const c0 = r0;
const c4 = rinf;
const e = (r1 + rm1) / 2n;
const o = (r1 - rm1) / 2n;
const c2 = e - c0 - c4;
const c3 = (r2 - c0 - 2n * o - 4n * c2 - 16n * c4) / 6n;
const c1 = o - c3;
return [c0, c1, c2, c3, c4];
}
function addStage(title, bodyHtml) {
const d = document.createElement('div');
d.className = 'stage';
d.innerHTML = '<span class="stage-title">' + title + '</span>' + bodyHtml;
pipelineEl.appendChild(d);
requestAnimationFrame(() => requestAnimationFrame(() => d.classList.add('show')));
return d;
}
function chip(v) { return '<span class="chip">' + v + '</span>'; }
function setStatus(t, cls) { statusEl.textContent = t; statusEl.className = 'status ' + (cls || ''); }
function run() {
pipelineEl.innerHTML = '';
const aStr = (numAEl.value || '0').replace(/\D/g, '') || '0';
const bStr = (numBEl.value || '0').replace(/\D/g, '') || '0';
const chunkLen = Math.max(Math.ceil(aStr.length / 3), Math.ceil(bStr.length / 3), 1);
const B = 10n ** BigInt(chunkLen);
const a = splitDigits(aStr, 3, chunkLen);
const b = splitDigits(bStr, 3, chunkLen);
addStage('1. Split into chunks', i18nFmt('Each number becomes 3 chunks (least-significant first): A = %a0% + %a1%·B + %a2%·B², B = %b0% + %b1%·B + %b2%·B².', {
a: aStr, b: bStr,
a2: a[2], a1: a[1], a0: a[0],
b2: b[2], b1: b[1], b0: b[0],
}));
const pa = evalPoly(a);
const pb = evalPoly(b);
addStage('2. Evaluate both polynomials at 5 points', '<div>Instead of multiplying every chunk of A by every chunk of B, evaluate A(x) and B(x) at x = 0, 1, -1, 2 and ∞:</div>' +
'<div class="row">' + chip('A(0)=' + pa.p0) + chip('A(1)=' + pa.p1) + chip('A(-1)=' + pa.pm1) +
chip('A(2)=' + pa.p2) + chip('A(∞)=' + pa.pinf) + '</div>' +
'<div class="row">' + chip('B(0)=' + pb.p0) + chip('B(1)=' + pb.p1) + chip('B(-1)=' + pb.pm1) +
chip('B(2)=' + pb.p2) + chip('B(∞)=' + pb.pinf) + '</div>');
const r0 = pa.p0 * pb.p0;
const r1 = pa.p1 * pb.p1;
const rm1 = pa.pm1 * pb.pm1;
const r2 = pa.p2 * pb.p2;
const rinf = pa.pinf * pb.pinf;
addStage('3. Multiply pointwise (the 5 hard multiplications)', '<div>Multiply the matching values together — only 5 products, versus the 9 chunk-products schoolbook multiplication would need:</div>' +
'<div class="row">' + chip('R(0)=' + r0) + chip('R(1)=' + r1) + chip('R(-1)=' + rm1) +
chip('R(2)=' + r2) + chip('R(∞)=' + rinf) + '</div>');
const coeffs = interpolate(r0, r1, rm1, r2, rinf);
addStage('4. Interpolate', '<div>Solve a small fixed linear system to recover the 5 coefficients of the product polynomial from its 5 sampled values:</div>' +
'<div class="row">' + coeffs.map((c, i) => chip('c' + i + '=' + c)).join('') + '</div>');
let product = 0n;
for (let i = 0; i < coeffs.length; i++) product += coeffs[i] * (B ** BigInt(i));
addStage('5. Recombine', i18nFmt('Substitute the chunk base B back into the coefficients to get the final product: %product%', { product: product.toString() }));
const expected = BigInt(aStr || '0') * BigInt(bStr || '0');
if (product === expected) {
setStatus(i18nFmt('Correct! Toom-3 matched schoolbook multiplication using %toom% pointwise products instead of %schoolbook%.', { schoolbook: 9, toom: 5 }), 'ok');
} else {
setStatus('Something doesn’t add up — check the interpolation step.', 'bad');
}
}
document.getElementById('run').addEventListener('click', run);
document.getElementById('reset').addEventListener('click', () => {
numAEl.value = '123456';
numBEl.value = '654321';
pipelineEl.innerHTML = '';
setStatus('Press Run Toom-3 to see the pipeline.', '');
});
run();
Comments
Loading comments...