<p class="hint">Pick a number <b>N</b> to factor. The sieve tests values of <em>x</em>
near √N, checks whether <em>x</em>² mod N factors completely over the factor base,
and collects those <b>smooth relations</b> into a binary matrix. Once enough rows
accumulate, Gaussian elimination over 𝔽₂ finds a dependency that yields a factor.</p>
<div class="controls">
<label>N = <select id="nsel">
<option value="77">77 = ?</option>
<option value="143">143 = ?</option>
<option value="323" selected>323 = ?</option>
<option value="667">667 = ?</option>
<option value="1147">1147 = ?</option>
</select></label>
<button id="sieve" type="button">Run Sieve</button>
<button id="solve" type="button" disabled>Solve Matrix</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="fb-row" class="fb-row"></div>
<div id="matrix-wrap" class="matrix-wrap"></div>
<div class="status" id="status">Choose N and click Run Sieve.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin-bottom: .6rem; }
label { font-size: .9rem; }
select { font-size: .9rem; padding: .2rem .4rem; border-radius: 6px; border: 1px solid #adb1b8; }
button { font: 600 13px system-ui; padding: .4rem .8rem; 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; }
.fb-row { font-size: .8rem; color: #555; margin-bottom: .4rem; min-height: 1.2em; }
.matrix-wrap { overflow-x: auto; margin-bottom: .5rem; }
table { border-collapse: collapse; font-size: .82rem; font-family: ui-monospace, monospace; }
th { background: #1d3557; color: #fff; padding: 3px 7px; text-align: center; white-space: nowrap; }
td { padding: 3px 7px; text-align: center; border-bottom: 1px solid #e0e4e8; }
tr:nth-child(even) td { background: #f5f7f9; }
td.one { color: #e63946; font-weight: 700; }
td.dep { background: #fff3cd !important; }
.status { font-size: .95rem; font-weight: 600; margin-top: .3rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Quadratic Sieve demo: collect smooth relations, build the binary exponent
matrix (mod 2), then use Gaussian elimination over F_2 to find a linear
dependency. That dependency gives x^2 ≡ y^2 (mod N) => factor via gcd. */
function gcd(a, b) { while (b) { [a, b] = [b, a % b]; } return a; }
// Small primes up to 30 as factor base
const PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29];
// Trial-divide v completely over factor base; return exponent vector or null
function smoothDecompose(v, primes) {
if (v <= 0) return null;
const exps = new Array(primes.length).fill(0);
let rem = v;
for (let i = 0; i < primes.length; i++) {
while (rem % primes[i] === 0) { rem = rem / primes[i]; exps[i]++; }
}
return rem === 1 ? exps : null;
}
// Gaussian elimination over F_2; returns list of dependency rows (indices)
function f2Elimination(matrix) {
const rows = matrix.map(r => r.slice());
const n = rows.length, m = rows[0].length;
const pivot = new Array(m).fill(-1);
const rowOf = []; // rowOf[col] = pivot row index
for (let col = 0; col < m; col++) {
let found = -1;
for (let row = 0; row < n; row++) {
if (rows[row][col] === 1 && pivot.indexOf(row) === -1 && rowOf.indexOf(row) === -1) {
found = row; break;
}
}
if (found === -1) continue;
rowOf[col] = found;
pivot[found] = col;
for (let row = 0; row < n; row++) {
if (row !== found && rows[row][col] === 1) {
for (let c = 0; c < m; c++) rows[row][c] ^= rows[found][c];
}
}
}
// Rows not chosen as pivots are linear dependencies
const deps = [];
for (let row = 0; row < n; row++) {
if (pivot.indexOf(row) === -1) deps.push(row);
}
return deps;
}
let N = 323, relations = [], factorBase = [], solveEnabled = false;
function reset() {
relations = [];
solveEnabled = false;
document.getElementById('solve').disabled = true;
document.getElementById('matrix-wrap').innerHTML = '';
document.getElementById('fb-row').textContent = '';
setStatus(`Choose N and click Run Sieve.`, '');
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + cls;
}
function renderMatrix() {
if (relations.length === 0) { document.getElementById('matrix-wrap').innerHTML = ''; return; }
const fb = factorBase;
let html = '<table><thead><tr><th>x</th><th>x²mod N</th>';
fb.forEach(p => { html += '<th>' + p + '</th>'; });
html += '</tr></thead><tbody>';
relations.forEach(rel => {
html += '<tr><td>' + rel.x + '</td><td>' + rel.val + '</td>';
rel.exps.forEach(e => {
const bit = e % 2;
html += '<td class="' + (bit ? 'one' : '') + '">' + bit + '</td>';
});
html += '</tr>';
});
html += '</tbody></table>';
document.getElementById('matrix-wrap').innerHTML = html;
}
function runSieve() {
N = parseInt(document.getElementById('nsel').value);
factorBase = PRIMES.filter(p => p < Math.ceil(Math.sqrt(N)) + 5);
document.getElementById('fb-row').textContent =
fmt(`Factor base: {%factorBase%} (primes ≤ %v%)`, { factorBase: factorBase.join(', '), v: factorBase[factorBase.length-1] });
relations = [];
const root = Math.floor(Math.sqrt(N));
const needed = factorBase.length + 2; // a few extra for dependencies
let found = 0, tried = 0;
for (let x = root; x <= root + 200 && found < needed; x++) {
const val = (x * x) % N;
tried++;
const exps = smoothDecompose(val, factorBase);
if (exps) {
relations.push({ x, val, exps });
found++;
}
}
// Also try negative side: x from root down
for (let x = root - 1; x >= 2 && relations.length < needed + 2; x--) {
const val = (x * x) % N;
tried++;
const exps = smoothDecompose(val, factorBase);
if (exps) {
if (!relations.find(r => r.x === x)) relations.push({ x, val, exps });
}
}
relations.sort((a, b) => a.x - b.x);
renderMatrix();
if (relations.length >= factorBase.length + 1) {
document.getElementById('solve').disabled = false;
setStatus(
fmt(`Collected %length% smooth relations (need ≥ %length2%%v% ). Ready to solve!`, { length: relations.length, length2: factorBase.length, v: 1 }), 'info'
);
} else {
setStatus(
fmt(`Only %length% smooth relations found — not enough. Try a larger N.`, { length: relations.length }), 'bad'
);
}
}
function solveMatrix() {
if (relations.length === 0) return;
const fb = factorBase;
// Build exponent matrix mod 2
const matrix = relations.map(r => r.exps.map(e => e % 2));
const deps = f2Elimination(matrix);
if (deps.length === 0) {
setStatus(`No dependency found in this set of relations — run sieve again for more.`, 'bad');
return;
}
// Highlight dependency rows
const depIdx = deps[0];
const rows = document.querySelectorAll('#matrix-wrap tbody tr');
relations.forEach((_, i) => {
if (rows[i]) rows[i].querySelectorAll('td').forEach(td => td.classList.remove('dep'));
});
// For each dependency, attempt to extract a factor
let factored = false;
for (const depRow of deps) {
/* The dependency is a zero row in the eliminated matrix, meaning the
XOR of some subset of original rows is zero — find that subset. */
// We just use the single relation row directly for the toy demo.
const rel = relations[depRow];
const x = rel.x;
/* y^2 = product of x^2 mod N for subset; in our toy, subset is just {rel}
so y = x, and x^2 mod N = rel.val. We need gcd(x - sqrt(rel.val), N).
For a real factor we need a nontrivial congruence; try gcd(x-1, N) as proxy.
More correctly: combine all rows in subset. */
// Build subset from the dependency (rows that contribute to zero row):
// For simplicity, iterate over pairs of relations and check x1^2*x2^2 ≡ square.
for (let i = 0; i < relations.length && !factored; i++) {
for (let j = i + 1; j < relations.length && !factored; j++) {
const ri = relations[i], rj = relations[j];
// Combined exponents
const combined = ri.exps.map((e, k) => e + rj.exps[k]);
const allEven = combined.every(e => e % 2 === 0);
if (allEven) {
// x = ri.x * rj.x mod N; y = product of p^(exp/2)
const xVal = (ri.x * rj.x) % N;
let yVal = 1;
combined.forEach((e, k) => { for (let t = 0; t < e / 2; t++) yVal *= fb[k]; });
yVal = yVal % N;
const diff = Math.abs(xVal - yVal);
const f = gcd(diff, N);
if (f > 1 && f < N) {
if (rows[i]) rows[i].querySelectorAll('td').forEach(td => td.classList.add('dep'));
if (rows[j]) rows[j].querySelectorAll('td').forEach(td => td.classList.add('dep'));
setStatus(
fmt(`Dependency: rows for x= x=%x% and x= x=%x2% → %xVal%² ≡ %yVal%² (mod %N%) → gcd(%diff%, %N%) = %f% ✓ So %N% = %f% × %v%`, { x: ri.x, x2: rj.x, xVal: xVal, yVal: yVal, N: N, diff: diff, f: f, v: N / f }), 'ok'
);
factored = true;
}
}
}
}
if (factored) break;
}
if (!factored) {
// fallback: show what we found via gcd of differences
// Try single-row perfect square
let fallbackMsg = 'Matrix solved but all gcd values were trivial (1 or N). ';
for (const rel of relations) {
const sqRoot = Math.round(Math.sqrt(rel.val));
if (sqRoot * sqRoot === rel.val) {
const f = gcd(Math.abs(rel.x - sqRoot), N);
if (f > 1 && f < N) {
setStatus(
rel.x + '² ≡ ' + sqRoot + '² (mod ' + N + ')' +
' → gcd(' + Math.abs(rel.x - sqRoot) + ', ' + N + ') = ' + f +
' ✓ ' + N + ' = ' + f + ' × ' + (N / f), 'ok'
);
factored = true;
break;
}
}
}
if (!factored) setStatus(fmt(`%fallbackMsg%Try running the sieve again for more relations.`, { fallbackMsg: fallbackMsg }), 'bad');
}
}
document.getElementById('sieve').addEventListener('click', runSieve);
document.getElementById('solve').addEventListener('click', solveMatrix);
document.getElementById('reset').addEventListener('click', reset);
document.getElementById('nsel').addEventListener('change', reset);
reset();
<p class="hint">Pick a number <b>N</b> to factor. The sieve tests values of <em>x</em>
near √N, checks whether <em>x</em>² mod N factors completely over the factor base,
and collects those <b>smooth relations</b> into a binary matrix. Once enough rows
accumulate, Gaussian elimination over 𝔽₂ finds a dependency that yields a factor.</p>
<div class="controls">
<label>N = <select id="nsel">
<option value="77">77 = ?</option>
<option value="143">143 = ?</option>
<option value="323" selected>323 = ?</option>
<option value="667">667 = ?</option>
<option value="1147">1147 = ?</option>
</select></label>
<button id="sieve" type="button">Run Sieve</button>
<button id="solve" type="button" disabled>Solve Matrix</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="fb-row" class="fb-row"></div>
<div id="matrix-wrap" class="matrix-wrap"></div>
<div class="status" id="status">Choose N and click Run Sieve.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin-bottom: .6rem; }
label { font-size: .9rem; }
select { font-size: .9rem; padding: .2rem .4rem; border-radius: 6px; border: 1px solid #adb1b8; }
button { font: 600 13px system-ui; padding: .4rem .8rem; 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; }
.fb-row { font-size: .8rem; color: #555; margin-bottom: .4rem; min-height: 1.2em; }
.matrix-wrap { overflow-x: auto; margin-bottom: .5rem; }
table { border-collapse: collapse; font-size: .82rem; font-family: ui-monospace, monospace; }
th { background: #1d3557; color: #fff; padding: 3px 7px; text-align: center; white-space: nowrap; }
td { padding: 3px 7px; text-align: center; border-bottom: 1px solid #e0e4e8; }
tr:nth-child(even) td { background: #f5f7f9; }
td.one { color: #e63946; font-weight: 700; }
td.dep { background: #fff3cd !important; }
.status { font-size: .95rem; font-weight: 600; margin-top: .3rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Quadratic Sieve demo: collect smooth relations, build the binary exponent
matrix (mod 2), then use Gaussian elimination over F_2 to find a linear
dependency. That dependency gives x^2 ≡ y^2 (mod N) => factor via gcd. */
function gcd(a, b) { while (b) { [a, b] = [b, a % b]; } return a; }
// Small primes up to 30 as factor base
const PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29];
// Trial-divide v completely over factor base; return exponent vector or null
function smoothDecompose(v, primes) {
if (v <= 0) return null;
const exps = new Array(primes.length).fill(0);
let rem = v;
for (let i = 0; i < primes.length; i++) {
while (rem % primes[i] === 0) { rem = rem / primes[i]; exps[i]++; }
}
return rem === 1 ? exps : null;
}
// Gaussian elimination over F_2; returns list of dependency rows (indices)
function f2Elimination(matrix) {
const rows = matrix.map(r => r.slice());
const n = rows.length, m = rows[0].length;
const pivot = new Array(m).fill(-1);
const rowOf = []; // rowOf[col] = pivot row index
for (let col = 0; col < m; col++) {
let found = -1;
for (let row = 0; row < n; row++) {
if (rows[row][col] === 1 && pivot.indexOf(row) === -1 && rowOf.indexOf(row) === -1) {
found = row; break;
}
}
if (found === -1) continue;
rowOf[col] = found;
pivot[found] = col;
for (let row = 0; row < n; row++) {
if (row !== found && rows[row][col] === 1) {
for (let c = 0; c < m; c++) rows[row][c] ^= rows[found][c];
}
}
}
// Rows not chosen as pivots are linear dependencies
const deps = [];
for (let row = 0; row < n; row++) {
if (pivot.indexOf(row) === -1) deps.push(row);
}
return deps;
}
let N = 323, relations = [], factorBase = [], solveEnabled = false;
function reset() {
relations = [];
solveEnabled = false;
document.getElementById('solve').disabled = true;
document.getElementById('matrix-wrap').innerHTML = '';
document.getElementById('fb-row').textContent = '';
setStatus(`Choose N and click Run Sieve.`, '');
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + cls;
}
function renderMatrix() {
if (relations.length === 0) { document.getElementById('matrix-wrap').innerHTML = ''; return; }
const fb = factorBase;
let html = '<table><thead><tr><th>x</th><th>x²mod N</th>';
fb.forEach(p => { html += '<th>' + p + '</th>'; });
html += '</tr></thead><tbody>';
relations.forEach(rel => {
html += '<tr><td>' + rel.x + '</td><td>' + rel.val + '</td>';
rel.exps.forEach(e => {
const bit = e % 2;
html += '<td class="' + (bit ? 'one' : '') + '">' + bit + '</td>';
});
html += '</tr>';
});
html += '</tbody></table>';
document.getElementById('matrix-wrap').innerHTML = html;
}
function runSieve() {
N = parseInt(document.getElementById('nsel').value);
factorBase = PRIMES.filter(p => p < Math.ceil(Math.sqrt(N)) + 5);
document.getElementById('fb-row').textContent =
fmt(`Factor base: {%factorBase%} (primes ≤ %v%)`, { factorBase: factorBase.join(', '), v: factorBase[factorBase.length-1] });
relations = [];
const root = Math.floor(Math.sqrt(N));
const needed = factorBase.length + 2; // a few extra for dependencies
let found = 0, tried = 0;
for (let x = root; x <= root + 200 && found < needed; x++) {
const val = (x * x) % N;
tried++;
const exps = smoothDecompose(val, factorBase);
if (exps) {
relations.push({ x, val, exps });
found++;
}
}
// Also try negative side: x from root down
for (let x = root - 1; x >= 2 && relations.length < needed + 2; x--) {
const val = (x * x) % N;
tried++;
const exps = smoothDecompose(val, factorBase);
if (exps) {
if (!relations.find(r => r.x === x)) relations.push({ x, val, exps });
}
}
relations.sort((a, b) => a.x - b.x);
renderMatrix();
if (relations.length >= factorBase.length + 1) {
document.getElementById('solve').disabled = false;
setStatus(
fmt(`Collected %length% smooth relations (need ≥ %length2%%v% ). Ready to solve!`, { length: relations.length, length2: factorBase.length, v: 1 }), 'info'
);
} else {
setStatus(
fmt(`Only %length% smooth relations found — not enough. Try a larger N.`, { length: relations.length }), 'bad'
);
}
}
function solveMatrix() {
if (relations.length === 0) return;
const fb = factorBase;
// Build exponent matrix mod 2
const matrix = relations.map(r => r.exps.map(e => e % 2));
const deps = f2Elimination(matrix);
if (deps.length === 0) {
setStatus(`No dependency found in this set of relations — run sieve again for more.`, 'bad');
return;
}
// Highlight dependency rows
const depIdx = deps[0];
const rows = document.querySelectorAll('#matrix-wrap tbody tr');
relations.forEach((_, i) => {
if (rows[i]) rows[i].querySelectorAll('td').forEach(td => td.classList.remove('dep'));
});
// For each dependency, attempt to extract a factor
let factored = false;
for (const depRow of deps) {
/* The dependency is a zero row in the eliminated matrix, meaning the
XOR of some subset of original rows is zero — find that subset. */
// We just use the single relation row directly for the toy demo.
const rel = relations[depRow];
const x = rel.x;
/* y^2 = product of x^2 mod N for subset; in our toy, subset is just {rel}
so y = x, and x^2 mod N = rel.val. We need gcd(x - sqrt(rel.val), N).
For a real factor we need a nontrivial congruence; try gcd(x-1, N) as proxy.
More correctly: combine all rows in subset. */
// Build subset from the dependency (rows that contribute to zero row):
// For simplicity, iterate over pairs of relations and check x1^2*x2^2 ≡ square.
for (let i = 0; i < relations.length && !factored; i++) {
for (let j = i + 1; j < relations.length && !factored; j++) {
const ri = relations[i], rj = relations[j];
// Combined exponents
const combined = ri.exps.map((e, k) => e + rj.exps[k]);
const allEven = combined.every(e => e % 2 === 0);
if (allEven) {
// x = ri.x * rj.x mod N; y = product of p^(exp/2)
const xVal = (ri.x * rj.x) % N;
let yVal = 1;
combined.forEach((e, k) => { for (let t = 0; t < e / 2; t++) yVal *= fb[k]; });
yVal = yVal % N;
const diff = Math.abs(xVal - yVal);
const f = gcd(diff, N);
if (f > 1 && f < N) {
if (rows[i]) rows[i].querySelectorAll('td').forEach(td => td.classList.add('dep'));
if (rows[j]) rows[j].querySelectorAll('td').forEach(td => td.classList.add('dep'));
setStatus(
fmt(`Dependency: rows for x= x=%x% and x= x=%x2% → %xVal%² ≡ %yVal%² (mod %N%) → gcd(%diff%, %N%) = %f% ✓ So %N% = %f% × %v%`, { x: ri.x, x2: rj.x, xVal: xVal, yVal: yVal, N: N, diff: diff, f: f, v: N / f }), 'ok'
);
factored = true;
}
}
}
}
if (factored) break;
}
if (!factored) {
// fallback: show what we found via gcd of differences
// Try single-row perfect square
let fallbackMsg = 'Matrix solved but all gcd values were trivial (1 or N). ';
for (const rel of relations) {
const sqRoot = Math.round(Math.sqrt(rel.val));
if (sqRoot * sqRoot === rel.val) {
const f = gcd(Math.abs(rel.x - sqRoot), N);
if (f > 1 && f < N) {
setStatus(
rel.x + '² ≡ ' + sqRoot + '² (mod ' + N + ')' +
' → gcd(' + Math.abs(rel.x - sqRoot) + ', ' + N + ') = ' + f +
' ✓ ' + N + ' = ' + f + ' × ' + (N / f), 'ok'
);
factored = true;
break;
}
}
}
if (!factored) setStatus(fmt(`%fallbackMsg%Try running the sieve again for more relations.`, { fallbackMsg: fallbackMsg }), 'bad');
}
}
document.getElementById('sieve').addEventListener('click', runSieve);
document.getElementById('solve').addEventListener('click', solveMatrix);
document.getElementById('reset').addEventListener('click', reset);
document.getElementById('nsel').addEventListener('change', reset);
reset();
Comments
Loading comments...