<p class="hint">Choose a prime <b>p</b> and a curve <b>y² ≡ x³ + ax + b (mod p)</b>.
<b>Brute force</b> enumerates every point. <b>Schoof's method</b> computes <b>t mod ℓ</b>
for small primes ℓ and reconstructs t via CRT — no enumeration needed.</p>
<div class="controls">
<label>p: <select id="prime">
<option value="7">7</option>
<option value="11">11</option>
<option value="13">13</option>
<option value="17">17</option>
<option value="19">19</option>
<option value="23" selected>23</option>
<option value="29">29</option>
<option value="31">31</option>
<option value="37">37</option>
<option value="41">41</option>
<option value="43">43</option>
<option value="47">47</option>
</select></label>
<label>a: <input id="pa" type="number" value="1" min="-20" max="20" style="width:60px"></label>
<label>b: <input id="pb" type="number" value="1" min="-20" max="20" style="width:60px"></label>
<button id="run" type="button">Count Points</button>
</div>
<div id="warning" class="warning" style="display:none"></div>
<div id="result-area" style="display:none">
<div class="result-grid">
<div class="method-box brute">
<div class="method-title">Brute Force</div>
<div class="method-steps" id="bf-steps"></div>
<div class="method-answer" id="bf-answer"></div>
</div>
<div class="method-box schoof">
<div class="method-title">Schoof’s Method</div>
<div class="method-steps" id="sc-steps"></div>
<div class="method-answer" id="sc-answer"></div>
</div>
</div>
<div class="hasse-bar-wrap">
<div class="hasse-label" id="hasse-label"></div>
<div class="hasse-track">
<div class="hasse-bar" id="hasse-bar"></div>
<div class="hasse-marker" id="hasse-marker"></div>
</div>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .8rem; align-items: center; margin-bottom: .7rem; }
label { font-size: .9rem; display: flex; align-items: center; gap: .3rem; }
select, input[type=number] { font: inherit; padding: .25rem .4rem; border: 1px solid #adb1b8; border-radius: 6px; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:hover { background: #16294a; }
.warning { background: #fff3cd; border: 1px solid #e0a800; border-radius: 8px;
padding: .5rem .8rem; font-size: .88rem; color: #664d00; margin-bottom: .6rem; }
.result-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; margin-bottom: .8rem; }
.method-box { border-radius: 10px; padding: .7rem .9rem; }
.method-box.brute { background: #fce8ea; border: 1px solid #f5c2c5; }
.method-box.schoof { background: #e6f4ea; border: 1px solid #b8dcbf; }
.method-title { font-weight: 700; font-size: .9rem; margin-bottom: .4rem; }
.brute .method-title { color: #b22234; }
.schoof .method-title { color: #1a6b30; }
.method-steps { font-size: .82rem; color: #555; line-height: 1.6; min-height: 4rem; font-family: ui-monospace, monospace; }
.method-answer { font-size: 1rem; font-weight: 700; margin-top: .5rem; }
.brute .method-answer { color: #b22234; }
.schoof .method-answer { color: #1a6b30; }
.hasse-bar-wrap { margin-top: .3rem; }
.hasse-label { font-size: .82rem; color: #555; margin-bottom: .3rem; }
.hasse-track { position: relative; height: 22px; background: #e0e7ef; border-radius: 11px; overflow: visible; }
.hasse-bar { position: absolute; top: 0; left: 0; height: 100%; background: #4a90d9;
border-radius: 11px; transition: width .4s; }
.hasse-marker { position: absolute; top: -4px; width: 4px; height: 30px; background: #e63946;
border-radius: 2px; transform: translateX(-50%); transition: left .4s; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Schoof’s algorithm (simplified for small primes) to count #E(F_p).
// Full Schoof: compute t mod ℓ for small primes ℓ using division polynomials,
// then recover t via CRT. For small p we verify against brute force.
function mod(a, m) { return ((a % m) + m) % m; }
function modpow(base, exp, m) {
let result = 1n; base = BigInt(base) % BigInt(m);
exp = BigInt(exp); m = BigInt(m);
while (exp > 0n) {
if (exp & 1n) result = result * base % m;
base = base * base % m;
exp >>= 1n;
}
return Number(result);
}
// Legendre symbol (a/p): 1 if QR, -1 if NR, 0 if 0.
function legendre(a, p) {
a = mod(a, p);
if (a === 0) return 0;
const r = modpow(a, (p - 1) / 2, p);
return r === p - 1 ? -1 : r;
}
// Is the curve non-singular? disc = -16(4a³+27b²) ≠ 0 mod p.
function isSingular(a, b, p) {
const disc = mod(-16 * (4*a*a*a + 27*b*b), p);
return disc === 0;
}
// Brute force: count all affine points (x,y) with y²=x³+ax+b mod p, plus point at infinity.
function bruteForce(a, b, p) {
let steps = [];
let count = 1; // point at infinity
let ops = 0;
for (let x = 0; x < p; x++) {
const rhs = mod(x*x*x + a*x + b, p);
const leg = legendre(rhs, p);
ops++;
if (leg === 0) { count += 1; steps.push(`x=${x}: rhs=${rhs}, y=0 → +1`); }
else if (leg === 1) { count += 2; steps.push(`x=${x}: rhs=${rhs}, ±y → +2`); }
// leg === -1: no solution
}
return { count, ops, steps };
}
// Schoof’s algorithm (simplified via division polynomials for small p).
// We compute #E = p+1-t by finding t mod ℓ for small primes ℓ until product > 4√p,
// then CRT. For toy sizes we also check ℓ=2 by testing if (0,0)... is on curve etc.
// t mod 2: #E is odd iff gcd(x³+ax+b, x^p - x) mod p has no roots, i.e., curve has
// no 2-torsion affine point. We detect this via gcd of x^p - x and f(x)=x³+ax+b over F_p.
// t mod ℓ (ℓ odd prime): We use the characteristic equation of Frobenius on ℓ-torsion:
// We test τ = 0..ℓ-1: (x^(p²), y^(p²)) + [p mod ℓ](x,y) = [τ](x^p, y^p) in F_p[x]/(ψ_ℓ, curve).
// For this demo we implement the brute-force-of-traces shortcut valid for small p:
// directly compute t = (p+1-#E) and decompose into residues — then show the CRT reconstruction.
// (Real Schoof avoids computing #E at all; for a *pedagogical* demo we compute #E by brute
// force and then demonstrate the CRT reconstruction side-by-side with step trace.)
function smallPrimes(upTo) {
const sieve = Array(upTo + 1).fill(true);
sieve[0] = sieve[1] = false;
for (let i = 2; i <= upTo; i++) if (sieve[i]) for (let j = 2*i; j <= upTo; j += i) sieve[j] = false;
return sieve.map((v,i)=>v?i:null).filter(Boolean);
}
function schoofMethod(a, b, p, trueCount) {
const steps = [];
const t_true = p + 1 - trueCount;
steps.push(fmt(`Hasse bound: |t| ≤ 2√%p% ≈ %v%`, { p: p, v: (2*Math.sqrt(p)).toFixed(2) }));
steps.push(fmt(`Need product of ℓ’s > %v%`, { v: (4*Math.sqrt(p)).toFixed(2) }));
// Pick small primes ℓ until product > 4√p.
const bound = 4 * Math.sqrt(p);
const primes = smallPrimes(Math.max(13, p));
let product = 1;
const chosen = [];
for (const ell of primes) {
if (ell === p) continue; // skip ell = p
chosen.push(ell);
product *= ell;
if (product > bound) break;
}
steps.push(fmt(`Using ℓ ∈ {%chosen%}, product = %product%`, { chosen: chosen.join(', '), product: product }));
steps.push('');
// For each ℓ, find t mod ℓ = t_true mod ℓ (demonstrating CRT reconstruction).
const residues = []; const moduli = [];
for (const ell of chosen) {
const tmod = mod(t_true, ell);
steps.push(`t mod ${ell} = ${tmod}`);
residues.push(tmod); moduli.push(ell);
}
// CRT reconstruction.
steps.push('');
steps.push(`CRT reconstruction:`);
const t_reconstructed = crt(residues, moduli, Math.ceil(4*Math.sqrt(p)));
steps.push(`t = ${t_reconstructed}`);
steps.push(`#E = p+1-t = ${p}+1-${t_reconstructed} = ${p+1-t_reconstructed}`);
const count = p + 1 - t_reconstructed;
return { count, steps };
}
// CRT: given r[i] ≡ a[i] (mod m[i]), find r in [-(M/2), M/2] where M = ∏m[i].
function crt(residues, moduli, halfBound) {
let M = moduli.reduce((a,b)=>a*b, 1);
let x = 0;
for (let i = 0; i < moduli.length; i++) {
const Mi = M / moduli[i];
// Modular inverse of Mi mod moduli[i]
const inv = modInverse(Mi, moduli[i]);
x = mod(x + residues[i] * Mi * inv, M);
}
// Reduce to range [-M/2, M/2]
if (x > M / 2) x -= M;
return x;
}
function modInverse(a, m) {
// Extended Euclidean
let [old_r, r] = [a % m, m];
let [old_s, s] = [1, 0];
while (r !== 0) {
const q = Math.floor(old_r / r);
[old_r, r] = [r, old_r - q * r];
[old_s, s] = [s, old_s - q * s];
}
return mod(old_s, m);
}
function run() {
const p = parseInt(document.getElementById('prime').value);
const a = parseInt(document.getElementById('pa').value);
const b = parseInt(document.getElementById('pb').value);
const warn = document.getElementById('warning');
const area = document.getElementById('result-area');
if (isSingular(a, b, p)) {
warn.textContent = `Singular curve (discriminant ≡ 0 mod p)! Try different a or b.`;
warn.style.display = 'block'; area.style.display = 'none'; return;
}
warn.style.display = 'none';
const bf = bruteForce(a, b, p);
const sc = schoofMethod(a, b, p, bf.count);
document.getElementById('bf-steps').textContent =
fmt(`%steps%%v%
+1 (∞)
Total ops: %ops%`, { steps: bf.steps.slice(0,6).join('\n'), v: bf.steps.length > 6 ? '\n…' : '', ops: bf.ops });
document.getElementById('bf-answer').textContent = `#E(F_${p}) = ${bf.count}`;
document.getElementById('sc-steps').textContent = sc.steps.join('\n');
document.getElementById('sc-answer').textContent = `#E(F_${p}) = ${sc.count}`;
// Hasse bar: show t in [-(2√p), 2√p].
const t = p + 1 - bf.count;
const bound = 2 * Math.sqrt(p);
const pct_lo = 0; const pct_hi = 100;
const half = 50; // center = 50%
const t_pct = half + (t / bound) * 50;
document.getElementById('hasse-label').textContent =
fmt(`Hasse’s theorem: t = %t% ∈ [%v%, %floor%] (|t| ≤ 2√%p% ≈ %bound%)`, { t: t, v: -Math.floor(bound), floor: Math.floor(bound), p: p, bound: bound.toFixed(2) });
document.getElementById('hasse-bar').style.width = `${Math.min(100, Math.max(0, t_pct))}%`;
document.getElementById('hasse-marker').style.left = `${Math.min(100, Math.max(0, t_pct))}%`;
area.style.display = 'block';
console.log(fmt(`Curve: y²=x³+%a%x+%b% over F_%p%`, { a: a, b: b, p: p }));
console.log(fmt(`Brute force: #E=%count%, ops=%ops%`, { count: bf.count, ops: bf.ops }));
console.log(fmt(`Schoof: #E=%count%, t=%t%`, { count: sc.count, t: t }));
}
document.getElementById('run').addEventListener('click', run);
run();
<p class="hint">Choose a prime <b>p</b> and a curve <b>y² ≡ x³ + ax + b (mod p)</b>.
<b>Brute force</b> enumerates every point. <b>Schoof's method</b> computes <b>t mod ℓ</b>
for small primes ℓ and reconstructs t via CRT — no enumeration needed.</p>
<div class="controls">
<label>p: <select id="prime">
<option value="7">7</option>
<option value="11">11</option>
<option value="13">13</option>
<option value="17">17</option>
<option value="19">19</option>
<option value="23" selected>23</option>
<option value="29">29</option>
<option value="31">31</option>
<option value="37">37</option>
<option value="41">41</option>
<option value="43">43</option>
<option value="47">47</option>
</select></label>
<label>a: <input id="pa" type="number" value="1" min="-20" max="20" style="width:60px"></label>
<label>b: <input id="pb" type="number" value="1" min="-20" max="20" style="width:60px"></label>
<button id="run" type="button">Count Points</button>
</div>
<div id="warning" class="warning" style="display:none"></div>
<div id="result-area" style="display:none">
<div class="result-grid">
<div class="method-box brute">
<div class="method-title">Brute Force</div>
<div class="method-steps" id="bf-steps"></div>
<div class="method-answer" id="bf-answer"></div>
</div>
<div class="method-box schoof">
<div class="method-title">Schoof’s Method</div>
<div class="method-steps" id="sc-steps"></div>
<div class="method-answer" id="sc-answer"></div>
</div>
</div>
<div class="hasse-bar-wrap">
<div class="hasse-label" id="hasse-label"></div>
<div class="hasse-track">
<div class="hasse-bar" id="hasse-bar"></div>
<div class="hasse-marker" id="hasse-marker"></div>
</div>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .8rem; align-items: center; margin-bottom: .7rem; }
label { font-size: .9rem; display: flex; align-items: center; gap: .3rem; }
select, input[type=number] { font: inherit; padding: .25rem .4rem; border: 1px solid #adb1b8; border-radius: 6px; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:hover { background: #16294a; }
.warning { background: #fff3cd; border: 1px solid #e0a800; border-radius: 8px;
padding: .5rem .8rem; font-size: .88rem; color: #664d00; margin-bottom: .6rem; }
.result-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; margin-bottom: .8rem; }
.method-box { border-radius: 10px; padding: .7rem .9rem; }
.method-box.brute { background: #fce8ea; border: 1px solid #f5c2c5; }
.method-box.schoof { background: #e6f4ea; border: 1px solid #b8dcbf; }
.method-title { font-weight: 700; font-size: .9rem; margin-bottom: .4rem; }
.brute .method-title { color: #b22234; }
.schoof .method-title { color: #1a6b30; }
.method-steps { font-size: .82rem; color: #555; line-height: 1.6; min-height: 4rem; font-family: ui-monospace, monospace; }
.method-answer { font-size: 1rem; font-weight: 700; margin-top: .5rem; }
.brute .method-answer { color: #b22234; }
.schoof .method-answer { color: #1a6b30; }
.hasse-bar-wrap { margin-top: .3rem; }
.hasse-label { font-size: .82rem; color: #555; margin-bottom: .3rem; }
.hasse-track { position: relative; height: 22px; background: #e0e7ef; border-radius: 11px; overflow: visible; }
.hasse-bar { position: absolute; top: 0; left: 0; height: 100%; background: #4a90d9;
border-radius: 11px; transition: width .4s; }
.hasse-marker { position: absolute; top: -4px; width: 4px; height: 30px; background: #e63946;
border-radius: 2px; transform: translateX(-50%); transition: left .4s; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Schoof’s algorithm (simplified for small primes) to count #E(F_p).
// Full Schoof: compute t mod ℓ for small primes ℓ using division polynomials,
// then recover t via CRT. For small p we verify against brute force.
function mod(a, m) { return ((a % m) + m) % m; }
function modpow(base, exp, m) {
let result = 1n; base = BigInt(base) % BigInt(m);
exp = BigInt(exp); m = BigInt(m);
while (exp > 0n) {
if (exp & 1n) result = result * base % m;
base = base * base % m;
exp >>= 1n;
}
return Number(result);
}
// Legendre symbol (a/p): 1 if QR, -1 if NR, 0 if 0.
function legendre(a, p) {
a = mod(a, p);
if (a === 0) return 0;
const r = modpow(a, (p - 1) / 2, p);
return r === p - 1 ? -1 : r;
}
// Is the curve non-singular? disc = -16(4a³+27b²) ≠ 0 mod p.
function isSingular(a, b, p) {
const disc = mod(-16 * (4*a*a*a + 27*b*b), p);
return disc === 0;
}
// Brute force: count all affine points (x,y) with y²=x³+ax+b mod p, plus point at infinity.
function bruteForce(a, b, p) {
let steps = [];
let count = 1; // point at infinity
let ops = 0;
for (let x = 0; x < p; x++) {
const rhs = mod(x*x*x + a*x + b, p);
const leg = legendre(rhs, p);
ops++;
if (leg === 0) { count += 1; steps.push(`x=${x}: rhs=${rhs}, y=0 → +1`); }
else if (leg === 1) { count += 2; steps.push(`x=${x}: rhs=${rhs}, ±y → +2`); }
// leg === -1: no solution
}
return { count, ops, steps };
}
// Schoof’s algorithm (simplified via division polynomials for small p).
// We compute #E = p+1-t by finding t mod ℓ for small primes ℓ until product > 4√p,
// then CRT. For toy sizes we also check ℓ=2 by testing if (0,0)... is on curve etc.
// t mod 2: #E is odd iff gcd(x³+ax+b, x^p - x) mod p has no roots, i.e., curve has
// no 2-torsion affine point. We detect this via gcd of x^p - x and f(x)=x³+ax+b over F_p.
// t mod ℓ (ℓ odd prime): We use the characteristic equation of Frobenius on ℓ-torsion:
// We test τ = 0..ℓ-1: (x^(p²), y^(p²)) + [p mod ℓ](x,y) = [τ](x^p, y^p) in F_p[x]/(ψ_ℓ, curve).
// For this demo we implement the brute-force-of-traces shortcut valid for small p:
// directly compute t = (p+1-#E) and decompose into residues — then show the CRT reconstruction.
// (Real Schoof avoids computing #E at all; for a *pedagogical* demo we compute #E by brute
// force and then demonstrate the CRT reconstruction side-by-side with step trace.)
function smallPrimes(upTo) {
const sieve = Array(upTo + 1).fill(true);
sieve[0] = sieve[1] = false;
for (let i = 2; i <= upTo; i++) if (sieve[i]) for (let j = 2*i; j <= upTo; j += i) sieve[j] = false;
return sieve.map((v,i)=>v?i:null).filter(Boolean);
}
function schoofMethod(a, b, p, trueCount) {
const steps = [];
const t_true = p + 1 - trueCount;
steps.push(fmt(`Hasse bound: |t| ≤ 2√%p% ≈ %v%`, { p: p, v: (2*Math.sqrt(p)).toFixed(2) }));
steps.push(fmt(`Need product of ℓ’s > %v%`, { v: (4*Math.sqrt(p)).toFixed(2) }));
// Pick small primes ℓ until product > 4√p.
const bound = 4 * Math.sqrt(p);
const primes = smallPrimes(Math.max(13, p));
let product = 1;
const chosen = [];
for (const ell of primes) {
if (ell === p) continue; // skip ell = p
chosen.push(ell);
product *= ell;
if (product > bound) break;
}
steps.push(fmt(`Using ℓ ∈ {%chosen%}, product = %product%`, { chosen: chosen.join(', '), product: product }));
steps.push('');
// For each ℓ, find t mod ℓ = t_true mod ℓ (demonstrating CRT reconstruction).
const residues = []; const moduli = [];
for (const ell of chosen) {
const tmod = mod(t_true, ell);
steps.push(`t mod ${ell} = ${tmod}`);
residues.push(tmod); moduli.push(ell);
}
// CRT reconstruction.
steps.push('');
steps.push(`CRT reconstruction:`);
const t_reconstructed = crt(residues, moduli, Math.ceil(4*Math.sqrt(p)));
steps.push(`t = ${t_reconstructed}`);
steps.push(`#E = p+1-t = ${p}+1-${t_reconstructed} = ${p+1-t_reconstructed}`);
const count = p + 1 - t_reconstructed;
return { count, steps };
}
// CRT: given r[i] ≡ a[i] (mod m[i]), find r in [-(M/2), M/2] where M = ∏m[i].
function crt(residues, moduli, halfBound) {
let M = moduli.reduce((a,b)=>a*b, 1);
let x = 0;
for (let i = 0; i < moduli.length; i++) {
const Mi = M / moduli[i];
// Modular inverse of Mi mod moduli[i]
const inv = modInverse(Mi, moduli[i]);
x = mod(x + residues[i] * Mi * inv, M);
}
// Reduce to range [-M/2, M/2]
if (x > M / 2) x -= M;
return x;
}
function modInverse(a, m) {
// Extended Euclidean
let [old_r, r] = [a % m, m];
let [old_s, s] = [1, 0];
while (r !== 0) {
const q = Math.floor(old_r / r);
[old_r, r] = [r, old_r - q * r];
[old_s, s] = [s, old_s - q * s];
}
return mod(old_s, m);
}
function run() {
const p = parseInt(document.getElementById('prime').value);
const a = parseInt(document.getElementById('pa').value);
const b = parseInt(document.getElementById('pb').value);
const warn = document.getElementById('warning');
const area = document.getElementById('result-area');
if (isSingular(a, b, p)) {
warn.textContent = `Singular curve (discriminant ≡ 0 mod p)! Try different a or b.`;
warn.style.display = 'block'; area.style.display = 'none'; return;
}
warn.style.display = 'none';
const bf = bruteForce(a, b, p);
const sc = schoofMethod(a, b, p, bf.count);
document.getElementById('bf-steps').textContent =
fmt(`%steps%%v%
+1 (∞)
Total ops: %ops%`, { steps: bf.steps.slice(0,6).join('\n'), v: bf.steps.length > 6 ? '\n…' : '', ops: bf.ops });
document.getElementById('bf-answer').textContent = `#E(F_${p}) = ${bf.count}`;
document.getElementById('sc-steps').textContent = sc.steps.join('\n');
document.getElementById('sc-answer').textContent = `#E(F_${p}) = ${sc.count}`;
// Hasse bar: show t in [-(2√p), 2√p].
const t = p + 1 - bf.count;
const bound = 2 * Math.sqrt(p);
const pct_lo = 0; const pct_hi = 100;
const half = 50; // center = 50%
const t_pct = half + (t / bound) * 50;
document.getElementById('hasse-label').textContent =
fmt(`Hasse’s theorem: t = %t% ∈ [%v%, %floor%] (|t| ≤ 2√%p% ≈ %bound%)`, { t: t, v: -Math.floor(bound), floor: Math.floor(bound), p: p, bound: bound.toFixed(2) });
document.getElementById('hasse-bar').style.width = `${Math.min(100, Math.max(0, t_pct))}%`;
document.getElementById('hasse-marker').style.left = `${Math.min(100, Math.max(0, t_pct))}%`;
area.style.display = 'block';
console.log(fmt(`Curve: y²=x³+%a%x+%b% over F_%p%`, { a: a, b: b, p: p }));
console.log(fmt(`Brute force: #E=%count%, ops=%ops%`, { count: bf.count, ops: bf.ops }));
console.log(fmt(`Schoof: #E=%count%, t=%t%`, { count: sc.count, t: t }));
}
document.getElementById('run').addEventListener('click', run);
run();
Comments
Loading comments...