<p class="hint">Fix the Gröbner basis G = {x - y, 2y^2 - 1} (in lex order, x > y). Pick a starting polynomial and reduce it one step at a time: each step cancels the leading term using whichever basis element’s leading term divides it.</p>
<div class="row">
<label for="target">Polynomial:</label>
<select id="target">
<option value="0">x^2 + xy - 1</option>
<option value="1">x^2 - y</option>
<option value="2">x^3</option>
</select>
<button id="step" type="button">Step</button>
<button id="run" type="button">Run to the end</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="basis" id="basis"></div>
<div class="current" id="current"></div>
<div class="log" id="log"></div>
<div class="status" id="status">Press Step to divide the leading term.</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; }
.row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
label { font-size: .85rem; font-weight: 600; color: #1d3557; }
select { font: 600 14px ui-monospace, monospace; padding: .35rem .5rem; border-radius: 6px; border: 1px solid #adb1b8; }
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: .45; cursor: default; }
.basis { font: 600 14px ui-monospace, monospace; background: #e8eef3; border: 1px solid #cdd9e3;
border-radius: 8px; padding: .5rem .7rem; margin-bottom: .6rem; color: #1d3557; }
.current { font: 700 17px ui-monospace, monospace; background: #fff; border: 2px solid #1d3557;
border-radius: 8px; padding: .6rem .8rem; margin-bottom: .6rem; min-height: 1.6em; }
.log { font: 13px ui-monospace, monospace; color: #444; max-height: 150px; overflow-y: auto;
border-left: 3px solid #cdd9e3; padding-left: .6rem; margin-bottom: .5rem; }
.log div { margin-bottom: .3rem; }
.status { font-size: 1rem; font-weight: 600; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.zero { color: #0a7d33; }
.status.nonzero { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Fixed 2-variable Gröbner basis reduction demo (lex order, x > y).
// Compare exponent of x first, then exponent of y — higher wins.
function cmpLex(a, b) { if (a.ex !== b.ex) return b.ex - a.ex; return b.ey - a.ey; }
function normalize(terms) {
const map = new Map();
for (const t of terms) {
const key = t.ex + ',' + t.ey;
map.set(key, (map.get(key) || 0) + t.c);
}
const out = [];
for (const [key, c] of map) {
if (Math.abs(c) > 1e-9) {
const [ex, ey] = key.split(',').map(Number);
out.push({ c, ex, ey });
}
}
out.sort(cmpLex);
return out;
}
// G = { x - y , 2y^2 - 1 }, the Groebner basis of the ideal (x^2+y^2-1, x-y).
const BASIS = [
normalize([{ c: 1, ex: 1, ey: 0 }, { c: -1, ex: 0, ey: 1 }]), // x - y
normalize([{ c: 2, ex: 0, ey: 2 }, { c: -1, ex: 0, ey: 0 }]), // 2y^2 - 1
];
const BASIS_LABELS = ['x - y', '2y^2 - 1'];
const TARGETS = [
normalize([{ c: 1, ex: 2, ey: 0 }, { c: 1, ex: 1, ey: 1 }, { c: -1, ex: 0, ey: 0 }]), // x^2+xy-1
normalize([{ c: 1, ex: 2, ey: 0 }, { c: -1, ex: 0, ey: 1 }]), // x^2-y
normalize([{ c: 1, ex: 3, ey: 0 }]), // x^3
];
// A basis leading monomial lm divides a term’s leading monomial lt iff every exponent of lm is <= the matching exponent of lt.
function divides(lt, lm) { return lt.ex >= lm.ex && lt.ey >= lm.ey; }
function subtractScaled(poly, other, factor) {
const scaled = other.map(t => ({ c: t.c * factor.c, ex: t.ex + factor.ex, ey: t.ey + factor.ey }));
return normalize(poly.concat(scaled.map(t => ({ c: -t.c, ex: t.ex, ey: t.ey }))));
}
// Try each basis element in order; use the first whose leading term divides the current leading term.
function reduceOneStep(poly) {
const lt = poly[0];
for (let i = 0; i < BASIS.length; i++) {
const lm = BASIS[i][0];
if (divides(lt, lm)) {
const factor = { c: lt.c / lm.c, ex: lt.ex - lm.ex, ey: lt.ey - lm.ey };
return { next: subtractScaled(poly, BASIS[i], factor), usedIndex: i, factor };
}
}
return null;
}
function fmtNum(c) {
const r = Math.round(c * 1000) / 1000;
return Number.isInteger(r) ? String(r) : r.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
}
function fmtMono(ex, ey) {
const parts = [];
if (ex === 1) parts.push('x'); else if (ex > 1) parts.push('x^' + ex);
if (ey === 1) parts.push('y'); else if (ey > 1) parts.push('y^' + ey);
return parts.join('');
}
function polyStr(p) {
if (!p || p.length === 0) return '0';
return p.map((t, i) => {
const mono = fmtMono(t.ex, t.ey);
const abs = fmtNum(Math.abs(t.c));
const coeff = (mono && abs === '1') ? '' : abs;
const sign = t.c < 0 ? '-' : (i === 0 ? '' : '+');
const sep = (i === 0) ? '' : ' ';
return sep + sign + (i > 0 ? ' ' : '') + coeff + mono;
}).join('');
}
function fmtFactor(f) {
const mono = fmtMono(f.ex, f.ey);
const c = fmtNum(f.c);
if (!mono) return c;
return (c === '1' ? '' : c) + mono;
}
let current = null, done = false, stepCount = 0;
function renderBasis() {
document.getElementById('basis').textContent =
i18nFmt('G = { %g1% , %g2% }', { g1: BASIS_LABELS[0], g2: BASIS_LABELS[1] });
}
function renderCurrent() {
document.getElementById('current').textContent = polyStr(current);
}
function log(msg) {
const l = document.getElementById('log');
const d = document.createElement('div');
d.textContent = msg;
l.appendChild(d);
l.scrollTop = l.scrollHeight;
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + cls;
}
function loadTarget() {
const idx = Number(document.getElementById('target').value);
current = TARGETS[idx].map(t => ({ ...t }));
done = false; stepCount = 0;
document.getElementById('log').innerHTML = '';
renderCurrent();
setStatus('Press Step to divide the leading term.', '');
document.getElementById('step').disabled = false;
document.getElementById('run').disabled = false;
}
// Perform exactly one division step and log what happened.
function step() {
if (done || current.length === 0) return;
const r = reduceOneStep(current);
if (!r) {
done = true;
finish();
return;
}
stepCount++;
log(i18nFmt('Step %n%: subtract (%factor%)·(basis element %gi%) from %before% → %after%', {
n: stepCount, factor: fmtFactor(r.factor), gi: (r.usedIndex + 1),
before: polyStr(current), after: polyStr(r.next),
}));
current = r.next;
renderCurrent();
if (current.length === 0) { done = true; finish(); }
}
function finish() {
document.getElementById('step').disabled = true;
document.getElementById('run').disabled = true;
if (current.length === 0) setStatus(i18nFmt('Reduced to 0 after %n% step(s) — this polynomial belongs to the ideal generated by G.', { n: stepCount }), 'zero');
else setStatus(i18nFmt('Normal form: %r% after %n% step(s) — no basis leading term divides what’s left, so the division stops here.', { r: polyStr(current), n: stepCount }), 'nonzero');
}
function runAll() {
let guard = 0;
while (!done && guard < 100) { step(); guard++; }
}
document.getElementById('target').addEventListener('change', loadTarget);
document.getElementById('step').addEventListener('click', step);
document.getElementById('run').addEventListener('click', runAll);
document.getElementById('reset').addEventListener('click', loadTarget);
renderBasis();
loadTarget();
<p class="hint">Fix the Gröbner basis G = {x - y, 2y^2 - 1} (in lex order, x > y). Pick a starting polynomial and reduce it one step at a time: each step cancels the leading term using whichever basis element’s leading term divides it.</p>
<div class="row">
<label for="target">Polynomial:</label>
<select id="target">
<option value="0">x^2 + xy - 1</option>
<option value="1">x^2 - y</option>
<option value="2">x^3</option>
</select>
<button id="step" type="button">Step</button>
<button id="run" type="button">Run to the end</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="basis" id="basis"></div>
<div class="current" id="current"></div>
<div class="log" id="log"></div>
<div class="status" id="status">Press Step to divide the leading term.</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; }
.row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
label { font-size: .85rem; font-weight: 600; color: #1d3557; }
select { font: 600 14px ui-monospace, monospace; padding: .35rem .5rem; border-radius: 6px; border: 1px solid #adb1b8; }
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: .45; cursor: default; }
.basis { font: 600 14px ui-monospace, monospace; background: #e8eef3; border: 1px solid #cdd9e3;
border-radius: 8px; padding: .5rem .7rem; margin-bottom: .6rem; color: #1d3557; }
.current { font: 700 17px ui-monospace, monospace; background: #fff; border: 2px solid #1d3557;
border-radius: 8px; padding: .6rem .8rem; margin-bottom: .6rem; min-height: 1.6em; }
.log { font: 13px ui-monospace, monospace; color: #444; max-height: 150px; overflow-y: auto;
border-left: 3px solid #cdd9e3; padding-left: .6rem; margin-bottom: .5rem; }
.log div { margin-bottom: .3rem; }
.status { font-size: 1rem; font-weight: 600; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.zero { color: #0a7d33; }
.status.nonzero { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Fixed 2-variable Gröbner basis reduction demo (lex order, x > y).
// Compare exponent of x first, then exponent of y — higher wins.
function cmpLex(a, b) { if (a.ex !== b.ex) return b.ex - a.ex; return b.ey - a.ey; }
function normalize(terms) {
const map = new Map();
for (const t of terms) {
const key = t.ex + ',' + t.ey;
map.set(key, (map.get(key) || 0) + t.c);
}
const out = [];
for (const [key, c] of map) {
if (Math.abs(c) > 1e-9) {
const [ex, ey] = key.split(',').map(Number);
out.push({ c, ex, ey });
}
}
out.sort(cmpLex);
return out;
}
// G = { x - y , 2y^2 - 1 }, the Groebner basis of the ideal (x^2+y^2-1, x-y).
const BASIS = [
normalize([{ c: 1, ex: 1, ey: 0 }, { c: -1, ex: 0, ey: 1 }]), // x - y
normalize([{ c: 2, ex: 0, ey: 2 }, { c: -1, ex: 0, ey: 0 }]), // 2y^2 - 1
];
const BASIS_LABELS = ['x - y', '2y^2 - 1'];
const TARGETS = [
normalize([{ c: 1, ex: 2, ey: 0 }, { c: 1, ex: 1, ey: 1 }, { c: -1, ex: 0, ey: 0 }]), // x^2+xy-1
normalize([{ c: 1, ex: 2, ey: 0 }, { c: -1, ex: 0, ey: 1 }]), // x^2-y
normalize([{ c: 1, ex: 3, ey: 0 }]), // x^3
];
// A basis leading monomial lm divides a term’s leading monomial lt iff every exponent of lm is <= the matching exponent of lt.
function divides(lt, lm) { return lt.ex >= lm.ex && lt.ey >= lm.ey; }
function subtractScaled(poly, other, factor) {
const scaled = other.map(t => ({ c: t.c * factor.c, ex: t.ex + factor.ex, ey: t.ey + factor.ey }));
return normalize(poly.concat(scaled.map(t => ({ c: -t.c, ex: t.ex, ey: t.ey }))));
}
// Try each basis element in order; use the first whose leading term divides the current leading term.
function reduceOneStep(poly) {
const lt = poly[0];
for (let i = 0; i < BASIS.length; i++) {
const lm = BASIS[i][0];
if (divides(lt, lm)) {
const factor = { c: lt.c / lm.c, ex: lt.ex - lm.ex, ey: lt.ey - lm.ey };
return { next: subtractScaled(poly, BASIS[i], factor), usedIndex: i, factor };
}
}
return null;
}
function fmtNum(c) {
const r = Math.round(c * 1000) / 1000;
return Number.isInteger(r) ? String(r) : r.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
}
function fmtMono(ex, ey) {
const parts = [];
if (ex === 1) parts.push('x'); else if (ex > 1) parts.push('x^' + ex);
if (ey === 1) parts.push('y'); else if (ey > 1) parts.push('y^' + ey);
return parts.join('');
}
function polyStr(p) {
if (!p || p.length === 0) return '0';
return p.map((t, i) => {
const mono = fmtMono(t.ex, t.ey);
const abs = fmtNum(Math.abs(t.c));
const coeff = (mono && abs === '1') ? '' : abs;
const sign = t.c < 0 ? '-' : (i === 0 ? '' : '+');
const sep = (i === 0) ? '' : ' ';
return sep + sign + (i > 0 ? ' ' : '') + coeff + mono;
}).join('');
}
function fmtFactor(f) {
const mono = fmtMono(f.ex, f.ey);
const c = fmtNum(f.c);
if (!mono) return c;
return (c === '1' ? '' : c) + mono;
}
let current = null, done = false, stepCount = 0;
function renderBasis() {
document.getElementById('basis').textContent =
i18nFmt('G = { %g1% , %g2% }', { g1: BASIS_LABELS[0], g2: BASIS_LABELS[1] });
}
function renderCurrent() {
document.getElementById('current').textContent = polyStr(current);
}
function log(msg) {
const l = document.getElementById('log');
const d = document.createElement('div');
d.textContent = msg;
l.appendChild(d);
l.scrollTop = l.scrollHeight;
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + cls;
}
function loadTarget() {
const idx = Number(document.getElementById('target').value);
current = TARGETS[idx].map(t => ({ ...t }));
done = false; stepCount = 0;
document.getElementById('log').innerHTML = '';
renderCurrent();
setStatus('Press Step to divide the leading term.', '');
document.getElementById('step').disabled = false;
document.getElementById('run').disabled = false;
}
// Perform exactly one division step and log what happened.
function step() {
if (done || current.length === 0) return;
const r = reduceOneStep(current);
if (!r) {
done = true;
finish();
return;
}
stepCount++;
log(i18nFmt('Step %n%: subtract (%factor%)·(basis element %gi%) from %before% → %after%', {
n: stepCount, factor: fmtFactor(r.factor), gi: (r.usedIndex + 1),
before: polyStr(current), after: polyStr(r.next),
}));
current = r.next;
renderCurrent();
if (current.length === 0) { done = true; finish(); }
}
function finish() {
document.getElementById('step').disabled = true;
document.getElementById('run').disabled = true;
if (current.length === 0) setStatus(i18nFmt('Reduced to 0 after %n% step(s) — this polynomial belongs to the ideal generated by G.', { n: stepCount }), 'zero');
else setStatus(i18nFmt('Normal form: %r% after %n% step(s) — no basis leading term divides what’s left, so the division stops here.', { r: polyStr(current), n: stepCount }), 'nonzero');
}
function runAll() {
let guard = 0;
while (!done && guard < 100) { step(); guard++; }
}
document.getElementById('target').addEventListener('change', loadTarget);
document.getElementById('step').addEventListener('click', step);
document.getElementById('run').addEventListener('click', runAll);
document.getElementById('reset').addEventListener('click', loadTarget);
renderBasis();
loadTarget();
Comments
Loading comments...