<p class="hint">Starting system: <b>x² - y</b> and <b>xy - 1</b> (lex order, x > y). Each step forms the S-polynomial of a pending pair and reduces it against the current basis. A nonzero remainder joins the basis; a zero remainder means that pair is already settled.</p>
<div class="basis-wrap">
<div class="basis-title">Current basis</div>
<div id="basis" class="basis"></div>
</div>
<div class="pairs-wrap">
<div class="basis-title">Pending S-polynomial pairs</div>
<div id="pairs" class="pairs"></div>
</div>
<div class="status" id="status">Ready. Press Step to form the first S-polynomial.</div>
<div class="btns">
<button id="step" type="button">Step</button>
<button id="run" type="button">Run to completion</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 .7rem; line-height: 1.45; }
.basis-title { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
color: #55606b; margin: .5rem 0 .35rem; }
.basis, .pairs { display: flex; flex-wrap: wrap; gap: .4rem; min-height: 2.4rem; }
.poly { font: 600 14px ui-monospace, monospace; padding: .4rem .65rem; border-radius: 8px;
background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.poly.new { background: #d6f0dc; border-color: #9bd6ad; color: #0a6b2c; }
.pair { font: 600 13px ui-monospace, monospace; padding: .3rem .55rem; border-radius: 8px;
background: #fff; border: 1px dashed #adb8c2; color: #55606b; }
.pair.active { background: #fff4d6; border-color: #e0b23a; border-style: solid; color: #8a6100; }
.status { font-size: .95rem; font-weight: 600; margin: .6rem 0; min-height: 2.6em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.zero { color: #55606b; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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; }
button:disabled { opacity: .5; cursor: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Polynomial representation and term order used throughout this demo.
// Monomial order: lexicographic with x > y. A term is {c, x, y} meaning c * x^x * y^y.
function sortPoly(p) {
return p.slice().sort((a, b) => (a.x !== b.x ? b.x - a.x : b.y - a.y)).filter(t => t.c !== 0);
}
function lead(p) { return p[0]; }
function mulTerm(p, t) { return p.map(term => ({ c: term.c * t.c, x: term.x + t.x, y: term.y + t.y })); }
function addPoly(p, q) {
const map = new Map();
for (const t of p.concat(q)) {
const k = t.x + ',' + t.y;
map.set(k, (map.get(k) || 0) + t.c);
}
const out = [];
for (const [k, c] of map) {
if (Math.abs(c) < 1e-9) continue;
const [x, y] = k.split(',').map(Number);
out.push({ c, x, y });
}
return sortPoly(out);
}
function subPoly(p, q) { return addPoly(p, q.map(t => ({ c: -t.c, x: t.x, y: t.y }))); }
const isZero = (p) => p.length === 0;
// Divide f by the current basis G, returning the part that cannot be cancelled by any leading term.
function reduce(f, G) {
let p = sortPoly(f.slice());
let r = [];
while (!isZero(p)) {
const lt = lead(p);
let divided = false;
for (const g of G) {
const lg = lead(g);
if (lt.x >= lg.x && lt.y >= lg.y) {
const factor = { c: lt.c / lg.c, x: lt.x - lg.x, y: lt.y - lg.y };
p = subPoly(p, mulTerm(g, factor));
divided = true;
break;
}
}
if (!divided) { r = addPoly(r, [lt]); p = p.slice(1); }
}
return sortPoly(r);
}
// Build the S-polynomial: the combination of f and g designed to cancel their leading terms.
function sPoly(f, g) {
const lf = lead(f), lg = lead(g);
const l = { x: Math.max(lf.x, lg.x), y: Math.max(lf.y, lg.y) };
const tf = { c: 1 / lf.c, x: l.x - lf.x, y: l.y - lf.y };
const tg = { c: 1 / lg.c, x: l.x - lg.x, y: l.y - lg.y };
return subPoly(mulTerm(f, tf), mulTerm(g, tg));
}
function polyToStr(p) {
if (isZero(p)) return '0';
return sortPoly(p).map((t, i) => {
const mag = Math.abs(t.c);
const coef = mag === 1 && (t.x || t.y) ? '' : String(mag);
const prefix = i === 0 ? (t.c < 0 ? '-' : '') : (t.c < 0 ? '- ' : '+ ');
let s = prefix + coef;
if (t.x) s += 'x' + (t.x > 1 ? '^' + t.x : '');
if (t.y) s += 'y' + (t.y > 1 ? '^' + t.y : '');
return s;
}).join(' ');
}
// The two starting polynomials: x^2 - y and xy - 1.
const F = [
[{ c: 1, x: 2, y: 0 }, { c: -1, x: 0, y: 1 }], // x^2 - y
[{ c: 1, x: 1, y: 1 }, { c: -1, x: 0, y: 0 }], // xy - 1
];
let G, pairs, lastNew, finished;
function setupInitial() {
G = F.map(sortPoly);
pairs = [];
for (let i = 0; i < G.length; i++)
for (let j = i + 1; j < G.length; j++)
pairs.push([i, j]);
lastNew = -1;
finished = false;
}
function render() {
const b = document.getElementById('basis');
b.innerHTML = '';
G.forEach((g, i) => {
const d = document.createElement('span');
d.className = 'poly' + (i === lastNew ? ' new' : '');
d.textContent = 'g' + i + ' = ' + polyToStr(g);
b.appendChild(d);
});
const pw = document.getElementById('pairs');
pw.innerHTML = '';
if (pairs.length === 0) {
const span = document.createElement('span');
span.className = 'pair';
span.textContent = 'none — basis is stable';
pw.appendChild(span);
} else {
pairs.forEach(([i, j]) => {
const span = document.createElement('span');
span.className = 'pair';
span.textContent = 'S(g' + i + ', g' + j + ')';
pw.appendChild(span);
});
}
document.getElementById('step').disabled = finished;
document.getElementById('run').disabled = finished;
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
// Process exactly one pending pair: form its S-polynomial, reduce it, and grow the basis if needed.
function step() {
if (finished) return;
if (pairs.length === 0) {
finished = true;
setStatus(i18nFmt('Done: every S-polynomial reduces to 0. Final basis has %n% polynomials — this is a Gröbner basis.', { n: G.length }), 'ok');
render();
return;
}
const [i, j] = pairs.shift();
const s = sPoly(G[i], G[j]);
const r = reduce(s, G);
lastNew = -1;
if (isZero(r)) {
setStatus(i18nFmt('S(g%i%, g%j%) reduces to 0 — no new information from this pair.', { i: i, j: j }), 'zero');
} else {
G.push(r);
const newIndex = G.length - 1;
for (let k = 0; k < newIndex; k++) pairs.push([k, newIndex]);
lastNew = newIndex;
setStatus(i18nFmt('S(g%i%, g%j%) reduces to %poly% (nonzero) — added as g%idx%.', { i: i, j: j, poly: polyToStr(r), idx: newIndex }), 'ok');
}
if (pairs.length === 0) {
finished = true;
}
render();
}
function runToCompletion() {
let guard = 0;
while (!finished && guard < 500) { step(); guard++; }
}
document.getElementById('step').addEventListener('click', step);
document.getElementById('run').addEventListener('click', runToCompletion);
document.getElementById('reset').addEventListener('click', () => {
setupInitial();
setStatus('Ready. Press Step to form the first S-polynomial.', '');
render();
});
setupInitial();
render();
<p class="hint">Starting system: <b>x² - y</b> and <b>xy - 1</b> (lex order, x > y). Each step forms the S-polynomial of a pending pair and reduces it against the current basis. A nonzero remainder joins the basis; a zero remainder means that pair is already settled.</p>
<div class="basis-wrap">
<div class="basis-title">Current basis</div>
<div id="basis" class="basis"></div>
</div>
<div class="pairs-wrap">
<div class="basis-title">Pending S-polynomial pairs</div>
<div id="pairs" class="pairs"></div>
</div>
<div class="status" id="status">Ready. Press Step to form the first S-polynomial.</div>
<div class="btns">
<button id="step" type="button">Step</button>
<button id="run" type="button">Run to completion</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 .7rem; line-height: 1.45; }
.basis-title { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
color: #55606b; margin: .5rem 0 .35rem; }
.basis, .pairs { display: flex; flex-wrap: wrap; gap: .4rem; min-height: 2.4rem; }
.poly { font: 600 14px ui-monospace, monospace; padding: .4rem .65rem; border-radius: 8px;
background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.poly.new { background: #d6f0dc; border-color: #9bd6ad; color: #0a6b2c; }
.pair { font: 600 13px ui-monospace, monospace; padding: .3rem .55rem; border-radius: 8px;
background: #fff; border: 1px dashed #adb8c2; color: #55606b; }
.pair.active { background: #fff4d6; border-color: #e0b23a; border-style: solid; color: #8a6100; }
.status { font-size: .95rem; font-weight: 600; margin: .6rem 0; min-height: 2.6em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.zero { color: #55606b; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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; }
button:disabled { opacity: .5; cursor: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Polynomial representation and term order used throughout this demo.
// Monomial order: lexicographic with x > y. A term is {c, x, y} meaning c * x^x * y^y.
function sortPoly(p) {
return p.slice().sort((a, b) => (a.x !== b.x ? b.x - a.x : b.y - a.y)).filter(t => t.c !== 0);
}
function lead(p) { return p[0]; }
function mulTerm(p, t) { return p.map(term => ({ c: term.c * t.c, x: term.x + t.x, y: term.y + t.y })); }
function addPoly(p, q) {
const map = new Map();
for (const t of p.concat(q)) {
const k = t.x + ',' + t.y;
map.set(k, (map.get(k) || 0) + t.c);
}
const out = [];
for (const [k, c] of map) {
if (Math.abs(c) < 1e-9) continue;
const [x, y] = k.split(',').map(Number);
out.push({ c, x, y });
}
return sortPoly(out);
}
function subPoly(p, q) { return addPoly(p, q.map(t => ({ c: -t.c, x: t.x, y: t.y }))); }
const isZero = (p) => p.length === 0;
// Divide f by the current basis G, returning the part that cannot be cancelled by any leading term.
function reduce(f, G) {
let p = sortPoly(f.slice());
let r = [];
while (!isZero(p)) {
const lt = lead(p);
let divided = false;
for (const g of G) {
const lg = lead(g);
if (lt.x >= lg.x && lt.y >= lg.y) {
const factor = { c: lt.c / lg.c, x: lt.x - lg.x, y: lt.y - lg.y };
p = subPoly(p, mulTerm(g, factor));
divided = true;
break;
}
}
if (!divided) { r = addPoly(r, [lt]); p = p.slice(1); }
}
return sortPoly(r);
}
// Build the S-polynomial: the combination of f and g designed to cancel their leading terms.
function sPoly(f, g) {
const lf = lead(f), lg = lead(g);
const l = { x: Math.max(lf.x, lg.x), y: Math.max(lf.y, lg.y) };
const tf = { c: 1 / lf.c, x: l.x - lf.x, y: l.y - lf.y };
const tg = { c: 1 / lg.c, x: l.x - lg.x, y: l.y - lg.y };
return subPoly(mulTerm(f, tf), mulTerm(g, tg));
}
function polyToStr(p) {
if (isZero(p)) return '0';
return sortPoly(p).map((t, i) => {
const mag = Math.abs(t.c);
const coef = mag === 1 && (t.x || t.y) ? '' : String(mag);
const prefix = i === 0 ? (t.c < 0 ? '-' : '') : (t.c < 0 ? '- ' : '+ ');
let s = prefix + coef;
if (t.x) s += 'x' + (t.x > 1 ? '^' + t.x : '');
if (t.y) s += 'y' + (t.y > 1 ? '^' + t.y : '');
return s;
}).join(' ');
}
// The two starting polynomials: x^2 - y and xy - 1.
const F = [
[{ c: 1, x: 2, y: 0 }, { c: -1, x: 0, y: 1 }], // x^2 - y
[{ c: 1, x: 1, y: 1 }, { c: -1, x: 0, y: 0 }], // xy - 1
];
let G, pairs, lastNew, finished;
function setupInitial() {
G = F.map(sortPoly);
pairs = [];
for (let i = 0; i < G.length; i++)
for (let j = i + 1; j < G.length; j++)
pairs.push([i, j]);
lastNew = -1;
finished = false;
}
function render() {
const b = document.getElementById('basis');
b.innerHTML = '';
G.forEach((g, i) => {
const d = document.createElement('span');
d.className = 'poly' + (i === lastNew ? ' new' : '');
d.textContent = 'g' + i + ' = ' + polyToStr(g);
b.appendChild(d);
});
const pw = document.getElementById('pairs');
pw.innerHTML = '';
if (pairs.length === 0) {
const span = document.createElement('span');
span.className = 'pair';
span.textContent = 'none — basis is stable';
pw.appendChild(span);
} else {
pairs.forEach(([i, j]) => {
const span = document.createElement('span');
span.className = 'pair';
span.textContent = 'S(g' + i + ', g' + j + ')';
pw.appendChild(span);
});
}
document.getElementById('step').disabled = finished;
document.getElementById('run').disabled = finished;
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
// Process exactly one pending pair: form its S-polynomial, reduce it, and grow the basis if needed.
function step() {
if (finished) return;
if (pairs.length === 0) {
finished = true;
setStatus(i18nFmt('Done: every S-polynomial reduces to 0. Final basis has %n% polynomials — this is a Gröbner basis.', { n: G.length }), 'ok');
render();
return;
}
const [i, j] = pairs.shift();
const s = sPoly(G[i], G[j]);
const r = reduce(s, G);
lastNew = -1;
if (isZero(r)) {
setStatus(i18nFmt('S(g%i%, g%j%) reduces to 0 — no new information from this pair.', { i: i, j: j }), 'zero');
} else {
G.push(r);
const newIndex = G.length - 1;
for (let k = 0; k < newIndex; k++) pairs.push([k, newIndex]);
lastNew = newIndex;
setStatus(i18nFmt('S(g%i%, g%j%) reduces to %poly% (nonzero) — added as g%idx%.', { i: i, j: j, poly: polyToStr(r), idx: newIndex }), 'ok');
}
if (pairs.length === 0) {
finished = true;
}
render();
}
function runToCompletion() {
let guard = 0;
while (!finished && guard < 500) { step(); guard++; }
}
document.getElementById('step').addEventListener('click', step);
document.getElementById('run').addEventListener('click', runToCompletion);
document.getElementById('reset').addEventListener('click', () => {
setupInitial();
setStatus('Ready. Press Step to form the first S-polynomial.', '');
render();
});
setupInitial();
render();
Comments
Loading comments...