Choose a preset or type your own coefficients and starting values (up to order 6, integers of any size). The explorer computes exact terms with big integers, lists every zero up to the n you ask for, and tries to recognize arithmetic progressions among them.
<div class="wrap">
<div class="row">
<label for="preset">Example</label>
<select id="preset">
<option value="fib">Fibonacci (one zero, at n = 0)</option>
<option value="trib">Tribonacci from 0, 0, 1</option>
<option value="ap">A rotating sequence (zeros every 4 steps)</option>
<option value="two">Two progressions (multiples of 3 or 4)</option>
<option value="sporadic">2^n − n^2 (two isolated zeros)</option>
<option value="custom">Your own recurrence</option>
</select>
</div>
<div class="formula" id="formula"></div>
<div class="row">
<label for="coef">Coefficients c1…ck</label>
<input id="coef" type="text" value="1, 1" spellcheck="false" aria-label="Coefficients of the recurrence, separated by commas">
</div>
<div class="row">
<label for="init">Initial values u(0)…u(k−1)</label>
<input id="init" type="text" value="0, 1" spellcheck="false" aria-label="Initial values, separated by commas">
</div>
<div class="row">
<label for="nmax">Compute up to n =</label>
<input id="nmax" type="number" min="10" max="400" value="120">
<button id="run" type="button">Compute</button>
</div>
<div class="err" id="err" role="alert"></div>
<div class="card">
<div class="h">First terms</div>
<div class="mono" id="terms"></div>
</div>
<div class="card">
<div class="h">Zeros found</div>
<div id="zeros"></div>
<div id="ap" class="ap"></div>
</div>
<div class="card">
<div class="h">Sign and size of each term</div>
<canvas id="plot" height="150" aria-label="Bar chart of the sign and base-10 logarithm of the magnitude of each term"></canvas>
<div class="legend"><span class="sw pos"></span>positive <span class="sw neg"></span>negative <span class="sw zero"></span>zero</div>
</div>
<div class="card">
<div class="h">Characteristic roots in the complex plane</div>
<canvas id="cplane" height="210" aria-label="Complex plane showing the roots of the characteristic polynomial"></canvas>
<div class="legend"><span class="dot dom"></span>dominant roots (largest modulus) <span class="dot oth"></span>other roots</div>
<div class="mono small" id="rootlist"></div>
</div>
<div class="card">
<div class="h">Zeros modulo small primes</div>
<div class="small">Reduce the sequence mod p. A true zero is always a zero mod every p, and mod p the sequence is eventually periodic. Bold 0 marks positions that vanish mod p, over one period.</div>
<div class="mono small" id="modtab"></div>
</div>
</div>
:root {
--bg: #ffffff; --fg: #1d2733; --muted: #5b6878; --card: #f2f5f9; --line: #d3dbe5;
--accent: #1d5fa8; --pos: #2a7fc4; --neg: #d9822b; --zero: #d23c4a; --on-accent: #ffffff;
--dom: #d23c4a; --oth: #6b7a8c;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #14181d; --fg: #e4eaf1; --muted: #9aa8b8; --card: #1d2430; --line: #333e4d;
--accent: #6cb0f0; --pos: #5aa9ec; --neg: #f0a24f; --zero: #ff6b78; --on-accent: #0b1117;
--dom: #ff6b78; --oth: #8fa0b4;
}
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.45 system-ui, sans-serif; }
.wrap { padding: 8px; max-width: 760px; margin: 0 auto; }
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 6px 0; }
label { font-weight: 600; min-width: 96px; }
select, input { font: inherit; color: var(--fg); background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 5px 8px; min-width: 0; flex: 1 1 140px; }
input[type=number] { flex: 0 0 90px; }
button { font: 600 14px system-ui, sans-serif; padding: 6px 14px; border: 0; border-radius: 6px; background: var(--accent); color: var(--on-accent); cursor: pointer; }
.formula { font-family: ui-monospace, Consolas, monospace; color: var(--muted); font-size: 13px; overflow-wrap: anywhere; }
.err { color: var(--zero); font-weight: 600; min-height: 1.2em; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; margin: 8px 0; }
.h { font-weight: 700; margin-bottom: 4px; }
.mono { font-family: ui-monospace, Consolas, monospace; overflow-wrap: anywhere; }
.small { font-size: 12.5px; color: var(--muted); }
canvas { width: 100%; display: block; background: var(--bg); border: 1px solid var(--line); border-radius: 6px; }
.legend { font-size: 12px; color: var(--muted); margin-top: 4px; display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; }
.sw { display: inline-block; width: 10px; height: 10px; margin-right: 3px; border-radius: 2px; }
.sw.pos { background: var(--pos); } .sw.neg { background: var(--neg); } .sw.zero { background: var(--zero); }
.dot { display: inline-block; width: 10px; height: 10px; margin-right: 3px; border-radius: 50%; }
.dot.dom { background: var(--dom); } .dot.oth { background: var(--oth); }
.zchip { display: inline-block; background: var(--zero); color: #fff; border-radius: 10px; padding: 0 7px; margin: 2px 3px 2px 0; font: 600 12px ui-monospace, monospace; }
.ap { margin-top: 6px; font-weight: 600; color: var(--accent); }
.bits { letter-spacing: 1px; }
.bits b { color: var(--zero); }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const PRESETS = {
fib: { c: '1, 1', i: '0, 1', n: 120 },
trib: { c: '1, 1, 1', i: '0, 0, 1', n: 120 },
ap: { c: '2, -2', i: '0, 1', n: 120 },
two: { c: '2, -2, 4, -4', i: '0, 1, 2, 0', n: 120 },
sporadic: { c: '5, -9, 7, -2', i: '1, 1, 0, -1', n: 120 },
custom: { c: '0, 0, 0, 0, 1, 1', i: '1, 0, 0, 0, 0, 3', n: 120 }
};
const PRIMES = [2, 3, 5, 7, 11, 13];
const $ = (id) => document.getElementById(id);
function parseList(text) {
const parts = text.split(/[\s,;]+/).filter((x) => x.length);
return parts.map((p) => {
if (!/^[+-]?\d+$/.test(p)) throw new Error('bad');
return BigInt(p);
});
}
function genTerms(c, init, N) {
const u = init.slice();
const k = c.length;
while (u.length <= N) {
let s = 0n;
for (let j = 0; j < k; j++) s += c[j] * u[u.length - 1 - j];
u.push(s);
}
return u.slice(0, N + 1);
}
function zeroIndices(u) {
const z = [];
for (let n = 0; n < u.length; n++) if (u[n] === 0n) z.push(n);
return z;
}
function log10Big(x) {
const s = (x < 0n ? -x : x).toString();
if (s.length <= 15) return Math.log10(Number(s) + 1);
return s.length - 1 + Math.log10(Number(s.slice(0, 15)) / 1e14);
}
function cmul(a, b) { return [a[0] * b[0] - a[1] * b[1], a[0] * b[1] + a[1] * b[0]]; }
function cadd(a, b) { return [a[0] + b[0], a[1] + b[1]]; }
function csub(a, b) { return [a[0] - b[0], a[1] - b[1]]; }
function cdiv(a, b) {
const d = b[0] * b[0] + b[1] * b[1];
return [(a[0] * b[0] + a[1] * b[1]) / d, (a[1] * b[0] - a[0] * b[1]) / d];
}
function charRoots(c) {
const k = c.length;
const a = [1];
for (let j = 0; j < k; j++) a.push(-Number(c[j]));
let R = 1;
for (let j = 1; j <= k; j++) R = Math.max(R, 1 + Math.abs(a[j]));
const z = [];
for (let j = 0; j < k; j++) {
const ang = 2 * Math.PI * j / k + 0.4;
const rad = R * 0.5;
z.push([rad * Math.cos(ang), rad * Math.sin(ang)]);
}
const ev = (x) => {
let r = [1, 0];
for (let j = 1; j <= k; j++) r = cadd(cmul(r, x), [a[j], 0]);
return r;
};
for (let it = 0; it < 2000; it++) {
let delta = 0;
for (let i = 0; i < k; i++) {
let den = [1, 0];
for (let j = 0; j < k; j++) if (j !== i) den = cmul(den, csub(z[i], z[j]));
if (den[0] === 0 && den[1] === 0) den = [1e-12, 0];
const step = cdiv(ev(z[i]), den);
z[i] = csub(z[i], step);
delta = Math.max(delta, Math.hypot(step[0], step[1]));
}
if (delta < 1e-13) break;
}
return z;
}
function findProgressions(zeros, N) {
const zset = new Set(zeros);
const covered = new Set();
const found = [];
for (let m = 1; m <= Math.floor(N / 3); m++) {
for (let r = 0; r < m; r++) {
const members = zeros.filter((n) => n % m === r);
if (members.length < 3) continue;
let start = -1;
for (let n0 = r; n0 <= N; n0 += m) {
let all = true;
for (let n = n0; n <= N; n += m) if (!zset.has(n)) { all = false; break; }
if (all) { start = n0; break; }
}
if (start < 0 || Math.floor((N - start) / m) < 2) continue;
let fresh = false;
for (let n = start; n <= N; n += m) if (!covered.has(n)) fresh = true;
if (!fresh) continue;
for (let n = start; n <= N; n += m) covered.add(n);
found.push({ m, r, start });
}
}
const isolated = zeros.filter((n) => !covered.has(n));
return { found, isolated };
}
function modPattern(c, init, p) {
const k = c.length;
const bp = BigInt(p);
const cm = c.map((x) => Number(((x % bp) + bp) % bp));
let st = [];
for (let i = 0; i < k; i++) st.push(Number(((init[i] % bp) + bp) % bp));
const seen = new Map();
const seq = [];
const CAP = 20000;
let n = 0;
while (n < CAP) {
const key = st.join(',');
if (seen.has(key)) {
const mu = seen.get(key);
return { mu, L: n - mu, seq: seq.slice(mu, n), full: true };
}
seen.set(key, n);
seq.push(st[0]);
let nx = 0;
for (let j = 0; j < k; j++) nx += cm[j] * st[k - 1 - j];
st = st.slice(1);
st.push(nx % p);
n++;
}
return { mu: 0, L: 0, seq: [], full: false };
}
function drawPlot(u, zeros) {
const cv = $('plot');
const w = cv.clientWidth || 600;
const h = 150;
const dpr = window.devicePixelRatio || 1;
cv.width = Math.floor(w * dpr);
cv.height = Math.floor(h * dpr);
const g = cv.getContext('2d');
g.setTransform(dpr, 0, 0, dpr, 0, 0);
const css = getComputedStyle(document.documentElement);
g.clearRect(0, 0, w, h);
const logs = u.map(log10Big);
const mx = Math.max(1, ...logs);
const mid = h / 2;
const bw = w / u.length;
for (let n = 0; n < u.length; n++) {
const x = n * bw;
if (u[n] === 0n) continue;
const hh = (logs[n] / mx) * (mid - 8);
g.fillStyle = css.getPropertyValue(u[n] > 0n ? '--pos' : '--neg');
if (u[n] > 0n) g.fillRect(x, mid - hh, Math.max(1, bw - 0.5), hh);
else g.fillRect(x, mid, Math.max(1, bw - 0.5), hh);
}
g.strokeStyle = css.getPropertyValue('--muted');
g.beginPath();
g.moveTo(0, mid);
g.lineTo(w, mid);
g.stroke();
g.fillStyle = css.getPropertyValue('--zero');
for (const n of zeros) {
g.beginPath();
g.arc(n * bw + bw / 2, mid, Math.max(2.5, Math.min(5, bw)), 0, 2 * Math.PI);
g.fill();
}
g.fillStyle = css.getPropertyValue('--muted');
g.font = '11px system-ui, sans-serif';
g.fillText('log10|u| ' + mx.toFixed(1), 4, 12);
g.fillText('n = 0 … ' + (u.length - 1), 4, h - 4);
}
function drawRoots(roots) {
const cv = $('cplane');
const w = cv.clientWidth || 600;
const h = 210;
const dpr = window.devicePixelRatio || 1;
cv.width = Math.floor(w * dpr);
cv.height = Math.floor(h * dpr);
const g = cv.getContext('2d');
g.setTransform(dpr, 0, 0, dpr, 0, 0);
const css = getComputedStyle(document.documentElement);
const mods = roots.map((z) => Math.hypot(z[0], z[1]));
const rmax = Math.max(...mods, 1e-9);
const scale = (Math.min(w / 2, h / 2) - 14) / (rmax * 1.1);
const cx = w / 2;
const cy = h / 2;
g.strokeStyle = css.getPropertyValue('--line');
g.beginPath();
g.moveTo(0, cy); g.lineTo(w, cy); g.moveTo(cx, 0); g.lineTo(cx, h);
g.stroke();
g.setLineDash([3, 3]);
g.beginPath();
g.arc(cx, cy, 1 * scale, 0, 2 * Math.PI);
g.stroke();
g.beginPath();
g.arc(cx, cy, rmax * scale, 0, 2 * Math.PI);
g.strokeStyle = css.getPropertyValue('--dom');
g.stroke();
g.setLineDash([]);
roots.forEach((z, i) => {
const dom = mods[i] >= rmax * (1 - 1e-3);
g.fillStyle = css.getPropertyValue(dom ? '--dom' : '--oth');
g.beginPath();
g.arc(cx + z[0] * scale, cy - z[1] * scale, dom ? 6 : 4.5, 0, 2 * Math.PI);
g.fill();
});
g.fillStyle = css.getPropertyValue('--muted');
g.font = '11px system-ui, sans-serif';
g.fillText('Re', w - 18, cy - 4);
g.fillText('Im', cx + 4, 12);
g.fillText('|z| = 1', cx + scale + 3, cy + 12);
}
function showFormula(c) {
const parts = c.map((x, j) => '(' + x + ')·u(n−' + (j + 1) + ')');
$('formula').textContent = 'u(n) = ' + parts.join(' + ');
}
function run() {
$('err').textContent = '';
let c, init;
try {
c = parseList($('coef').value);
init = parseList($('init').value);
} catch (e) {
$('err').textContent = "Enter integers separated by commas, for example: 1, 1";
return;
}
if (c.length < 1 || c.length > 6) { $('err').textContent = "The order must be between 1 and 6."; return; }
if (init.length !== c.length) { $('err').textContent = fmt("You need exactly %k% initial values, one per coefficient.", { k: c.length }); return; }
let N = parseInt($('nmax').value, 10);
if (!(N >= 10)) N = 10;
if (N > 400) N = 400;
$('nmax').value = N;
showFormula(c);
const u = genTerms(c, init, N);
const zeros = zeroIndices(u);
const head = u.slice(0, 14).map((x) => {
const s = x.toString();
return s.length > 14 ? s.slice(0, 6) + '…(' + s.length + ')' : s;
});
$('terms').textContent = head.join(', ') + ', …';
const zEl = $('zeros');
const apEl = $('ap');
zEl.textContent = '';
apEl.textContent = '';
if (zeros.length === 0) {
zEl.textContent = fmt("No zero among u(0) … u(%n%).", { n: N });
} else {
zEl.append(fmt("%z% zeros among u(0) … u(%n%):", { z: zeros.length, n: N }) + ' ');
zeros.slice(0, 60).forEach((n) => {
const s = document.createElement('span');
s.className = 'zchip';
s.textContent = n;
zEl.appendChild(s);
});
if (zeros.length > 60) zEl.append('…');
if (zeros.length === u.length) {
apEl.textContent = "Every term is zero.";
} else {
const { found, isolated } = findProgressions(zeros, N);
const lines = found.map((f) => fmt("Pattern: u(n) = 0 for every n ≡ %r% (mod %m%) from n = %s% on.", { r: f.r, m: f.m, s: f.start }));
if (isolated.length) lines.push(fmt("Isolated zeros: %list%", { list: isolated.slice(0, 20).join(', ') }));
apEl.innerHTML = lines.join('<br>');
}
}
drawPlot(u, zeros);
const roots = charRoots(c);
drawRoots(roots);
const mods = roots.map((z) => Math.hypot(z[0], z[1]));
const rmax = Math.max(...mods);
$('rootlist').innerHTML = roots.map((z, i) => {
const dom = mods[i] >= rmax * (1 - 1e-3);
const im = (z[1] < 0 ? '−' : '+') + Math.abs(z[1]).toFixed(4);
const t = z[0].toFixed(4) + ' ' + im + 'i |z|=' + mods[i].toFixed(4);
return dom ? '<b>' + t + '</b>' : t;
}).join('<br>');
const rows = [];
for (const p of PRIMES) {
const r = modPattern(c, init, p);
if (!r.full) { rows.push(fmt("mod %p%: period longer than the search limit", { p })); continue; }
const shown = r.seq.slice(0, 48).map((x) => (x === 0 ? '<b>0</b>' : '·')).join('');
const nz = r.seq.filter((x) => x === 0).length;
rows.push(fmt("mod %p%: period %L% (after %mu% preperiodic terms), %z% zero positions per period", { p, L: r.L, mu: r.mu, z: nz }) + '<br><span class="bits">' + shown + (r.L > 48 ? '…' : '') + '</span>');
}
$('modtab').innerHTML = rows.join('<br>');
}
function loadPreset() {
const p = PRESETS[$('preset').value];
$('coef').value = p.c;
$('init').value = p.i;
$('nmax').value = p.n;
run();
}
$('preset').addEventListener('change', loadPreset);
$('run').addEventListener('click', run);
$('coef').addEventListener('input', () => { $('preset').value = 'custom'; });
$('init').addEventListener('input', () => { $('preset').value = 'custom'; });
window.addEventListener('resize', run);
run();
<div class="wrap">
<div class="row">
<label for="preset">Example</label>
<select id="preset">
<option value="fib">Fibonacci (one zero, at n = 0)</option>
<option value="trib">Tribonacci from 0, 0, 1</option>
<option value="ap">A rotating sequence (zeros every 4 steps)</option>
<option value="two">Two progressions (multiples of 3 or 4)</option>
<option value="sporadic">2^n − n^2 (two isolated zeros)</option>
<option value="custom">Your own recurrence</option>
</select>
</div>
<div class="formula" id="formula"></div>
<div class="row">
<label for="coef">Coefficients c1…ck</label>
<input id="coef" type="text" value="1, 1" spellcheck="false" aria-label="Coefficients of the recurrence, separated by commas">
</div>
<div class="row">
<label for="init">Initial values u(0)…u(k−1)</label>
<input id="init" type="text" value="0, 1" spellcheck="false" aria-label="Initial values, separated by commas">
</div>
<div class="row">
<label for="nmax">Compute up to n =</label>
<input id="nmax" type="number" min="10" max="400" value="120">
<button id="run" type="button">Compute</button>
</div>
<div class="err" id="err" role="alert"></div>
<div class="card">
<div class="h">First terms</div>
<div class="mono" id="terms"></div>
</div>
<div class="card">
<div class="h">Zeros found</div>
<div id="zeros"></div>
<div id="ap" class="ap"></div>
</div>
<div class="card">
<div class="h">Sign and size of each term</div>
<canvas id="plot" height="150" aria-label="Bar chart of the sign and base-10 logarithm of the magnitude of each term"></canvas>
<div class="legend"><span class="sw pos"></span>positive <span class="sw neg"></span>negative <span class="sw zero"></span>zero</div>
</div>
<div class="card">
<div class="h">Characteristic roots in the complex plane</div>
<canvas id="cplane" height="210" aria-label="Complex plane showing the roots of the characteristic polynomial"></canvas>
<div class="legend"><span class="dot dom"></span>dominant roots (largest modulus) <span class="dot oth"></span>other roots</div>
<div class="mono small" id="rootlist"></div>
</div>
<div class="card">
<div class="h">Zeros modulo small primes</div>
<div class="small">Reduce the sequence mod p. A true zero is always a zero mod every p, and mod p the sequence is eventually periodic. Bold 0 marks positions that vanish mod p, over one period.</div>
<div class="mono small" id="modtab"></div>
</div>
</div>
:root {
--bg: #ffffff; --fg: #1d2733; --muted: #5b6878; --card: #f2f5f9; --line: #d3dbe5;
--accent: #1d5fa8; --pos: #2a7fc4; --neg: #d9822b; --zero: #d23c4a; --on-accent: #ffffff;
--dom: #d23c4a; --oth: #6b7a8c;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #14181d; --fg: #e4eaf1; --muted: #9aa8b8; --card: #1d2430; --line: #333e4d;
--accent: #6cb0f0; --pos: #5aa9ec; --neg: #f0a24f; --zero: #ff6b78; --on-accent: #0b1117;
--dom: #ff6b78; --oth: #8fa0b4;
}
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.45 system-ui, sans-serif; }
.wrap { padding: 8px; max-width: 760px; margin: 0 auto; }
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 6px 0; }
label { font-weight: 600; min-width: 96px; }
select, input { font: inherit; color: var(--fg); background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 5px 8px; min-width: 0; flex: 1 1 140px; }
input[type=number] { flex: 0 0 90px; }
button { font: 600 14px system-ui, sans-serif; padding: 6px 14px; border: 0; border-radius: 6px; background: var(--accent); color: var(--on-accent); cursor: pointer; }
.formula { font-family: ui-monospace, Consolas, monospace; color: var(--muted); font-size: 13px; overflow-wrap: anywhere; }
.err { color: var(--zero); font-weight: 600; min-height: 1.2em; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; margin: 8px 0; }
.h { font-weight: 700; margin-bottom: 4px; }
.mono { font-family: ui-monospace, Consolas, monospace; overflow-wrap: anywhere; }
.small { font-size: 12.5px; color: var(--muted); }
canvas { width: 100%; display: block; background: var(--bg); border: 1px solid var(--line); border-radius: 6px; }
.legend { font-size: 12px; color: var(--muted); margin-top: 4px; display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; }
.sw { display: inline-block; width: 10px; height: 10px; margin-right: 3px; border-radius: 2px; }
.sw.pos { background: var(--pos); } .sw.neg { background: var(--neg); } .sw.zero { background: var(--zero); }
.dot { display: inline-block; width: 10px; height: 10px; margin-right: 3px; border-radius: 50%; }
.dot.dom { background: var(--dom); } .dot.oth { background: var(--oth); }
.zchip { display: inline-block; background: var(--zero); color: #fff; border-radius: 10px; padding: 0 7px; margin: 2px 3px 2px 0; font: 600 12px ui-monospace, monospace; }
.ap { margin-top: 6px; font-weight: 600; color: var(--accent); }
.bits { letter-spacing: 1px; }
.bits b { color: var(--zero); }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const PRESETS = {
fib: { c: '1, 1', i: '0, 1', n: 120 },
trib: { c: '1, 1, 1', i: '0, 0, 1', n: 120 },
ap: { c: '2, -2', i: '0, 1', n: 120 },
two: { c: '2, -2, 4, -4', i: '0, 1, 2, 0', n: 120 },
sporadic: { c: '5, -9, 7, -2', i: '1, 1, 0, -1', n: 120 },
custom: { c: '0, 0, 0, 0, 1, 1', i: '1, 0, 0, 0, 0, 3', n: 120 }
};
const PRIMES = [2, 3, 5, 7, 11, 13];
const $ = (id) => document.getElementById(id);
function parseList(text) {
const parts = text.split(/[\s,;]+/).filter((x) => x.length);
return parts.map((p) => {
if (!/^[+-]?\d+$/.test(p)) throw new Error('bad');
return BigInt(p);
});
}
function genTerms(c, init, N) {
const u = init.slice();
const k = c.length;
while (u.length <= N) {
let s = 0n;
for (let j = 0; j < k; j++) s += c[j] * u[u.length - 1 - j];
u.push(s);
}
return u.slice(0, N + 1);
}
function zeroIndices(u) {
const z = [];
for (let n = 0; n < u.length; n++) if (u[n] === 0n) z.push(n);
return z;
}
function log10Big(x) {
const s = (x < 0n ? -x : x).toString();
if (s.length <= 15) return Math.log10(Number(s) + 1);
return s.length - 1 + Math.log10(Number(s.slice(0, 15)) / 1e14);
}
function cmul(a, b) { return [a[0] * b[0] - a[1] * b[1], a[0] * b[1] + a[1] * b[0]]; }
function cadd(a, b) { return [a[0] + b[0], a[1] + b[1]]; }
function csub(a, b) { return [a[0] - b[0], a[1] - b[1]]; }
function cdiv(a, b) {
const d = b[0] * b[0] + b[1] * b[1];
return [(a[0] * b[0] + a[1] * b[1]) / d, (a[1] * b[0] - a[0] * b[1]) / d];
}
function charRoots(c) {
const k = c.length;
const a = [1];
for (let j = 0; j < k; j++) a.push(-Number(c[j]));
let R = 1;
for (let j = 1; j <= k; j++) R = Math.max(R, 1 + Math.abs(a[j]));
const z = [];
for (let j = 0; j < k; j++) {
const ang = 2 * Math.PI * j / k + 0.4;
const rad = R * 0.5;
z.push([rad * Math.cos(ang), rad * Math.sin(ang)]);
}
const ev = (x) => {
let r = [1, 0];
for (let j = 1; j <= k; j++) r = cadd(cmul(r, x), [a[j], 0]);
return r;
};
for (let it = 0; it < 2000; it++) {
let delta = 0;
for (let i = 0; i < k; i++) {
let den = [1, 0];
for (let j = 0; j < k; j++) if (j !== i) den = cmul(den, csub(z[i], z[j]));
if (den[0] === 0 && den[1] === 0) den = [1e-12, 0];
const step = cdiv(ev(z[i]), den);
z[i] = csub(z[i], step);
delta = Math.max(delta, Math.hypot(step[0], step[1]));
}
if (delta < 1e-13) break;
}
return z;
}
function findProgressions(zeros, N) {
const zset = new Set(zeros);
const covered = new Set();
const found = [];
for (let m = 1; m <= Math.floor(N / 3); m++) {
for (let r = 0; r < m; r++) {
const members = zeros.filter((n) => n % m === r);
if (members.length < 3) continue;
let start = -1;
for (let n0 = r; n0 <= N; n0 += m) {
let all = true;
for (let n = n0; n <= N; n += m) if (!zset.has(n)) { all = false; break; }
if (all) { start = n0; break; }
}
if (start < 0 || Math.floor((N - start) / m) < 2) continue;
let fresh = false;
for (let n = start; n <= N; n += m) if (!covered.has(n)) fresh = true;
if (!fresh) continue;
for (let n = start; n <= N; n += m) covered.add(n);
found.push({ m, r, start });
}
}
const isolated = zeros.filter((n) => !covered.has(n));
return { found, isolated };
}
function modPattern(c, init, p) {
const k = c.length;
const bp = BigInt(p);
const cm = c.map((x) => Number(((x % bp) + bp) % bp));
let st = [];
for (let i = 0; i < k; i++) st.push(Number(((init[i] % bp) + bp) % bp));
const seen = new Map();
const seq = [];
const CAP = 20000;
let n = 0;
while (n < CAP) {
const key = st.join(',');
if (seen.has(key)) {
const mu = seen.get(key);
return { mu, L: n - mu, seq: seq.slice(mu, n), full: true };
}
seen.set(key, n);
seq.push(st[0]);
let nx = 0;
for (let j = 0; j < k; j++) nx += cm[j] * st[k - 1 - j];
st = st.slice(1);
st.push(nx % p);
n++;
}
return { mu: 0, L: 0, seq: [], full: false };
}
function drawPlot(u, zeros) {
const cv = $('plot');
const w = cv.clientWidth || 600;
const h = 150;
const dpr = window.devicePixelRatio || 1;
cv.width = Math.floor(w * dpr);
cv.height = Math.floor(h * dpr);
const g = cv.getContext('2d');
g.setTransform(dpr, 0, 0, dpr, 0, 0);
const css = getComputedStyle(document.documentElement);
g.clearRect(0, 0, w, h);
const logs = u.map(log10Big);
const mx = Math.max(1, ...logs);
const mid = h / 2;
const bw = w / u.length;
for (let n = 0; n < u.length; n++) {
const x = n * bw;
if (u[n] === 0n) continue;
const hh = (logs[n] / mx) * (mid - 8);
g.fillStyle = css.getPropertyValue(u[n] > 0n ? '--pos' : '--neg');
if (u[n] > 0n) g.fillRect(x, mid - hh, Math.max(1, bw - 0.5), hh);
else g.fillRect(x, mid, Math.max(1, bw - 0.5), hh);
}
g.strokeStyle = css.getPropertyValue('--muted');
g.beginPath();
g.moveTo(0, mid);
g.lineTo(w, mid);
g.stroke();
g.fillStyle = css.getPropertyValue('--zero');
for (const n of zeros) {
g.beginPath();
g.arc(n * bw + bw / 2, mid, Math.max(2.5, Math.min(5, bw)), 0, 2 * Math.PI);
g.fill();
}
g.fillStyle = css.getPropertyValue('--muted');
g.font = '11px system-ui, sans-serif';
g.fillText('log10|u| ' + mx.toFixed(1), 4, 12);
g.fillText('n = 0 … ' + (u.length - 1), 4, h - 4);
}
function drawRoots(roots) {
const cv = $('cplane');
const w = cv.clientWidth || 600;
const h = 210;
const dpr = window.devicePixelRatio || 1;
cv.width = Math.floor(w * dpr);
cv.height = Math.floor(h * dpr);
const g = cv.getContext('2d');
g.setTransform(dpr, 0, 0, dpr, 0, 0);
const css = getComputedStyle(document.documentElement);
const mods = roots.map((z) => Math.hypot(z[0], z[1]));
const rmax = Math.max(...mods, 1e-9);
const scale = (Math.min(w / 2, h / 2) - 14) / (rmax * 1.1);
const cx = w / 2;
const cy = h / 2;
g.strokeStyle = css.getPropertyValue('--line');
g.beginPath();
g.moveTo(0, cy); g.lineTo(w, cy); g.moveTo(cx, 0); g.lineTo(cx, h);
g.stroke();
g.setLineDash([3, 3]);
g.beginPath();
g.arc(cx, cy, 1 * scale, 0, 2 * Math.PI);
g.stroke();
g.beginPath();
g.arc(cx, cy, rmax * scale, 0, 2 * Math.PI);
g.strokeStyle = css.getPropertyValue('--dom');
g.stroke();
g.setLineDash([]);
roots.forEach((z, i) => {
const dom = mods[i] >= rmax * (1 - 1e-3);
g.fillStyle = css.getPropertyValue(dom ? '--dom' : '--oth');
g.beginPath();
g.arc(cx + z[0] * scale, cy - z[1] * scale, dom ? 6 : 4.5, 0, 2 * Math.PI);
g.fill();
});
g.fillStyle = css.getPropertyValue('--muted');
g.font = '11px system-ui, sans-serif';
g.fillText('Re', w - 18, cy - 4);
g.fillText('Im', cx + 4, 12);
g.fillText('|z| = 1', cx + scale + 3, cy + 12);
}
function showFormula(c) {
const parts = c.map((x, j) => '(' + x + ')·u(n−' + (j + 1) + ')');
$('formula').textContent = 'u(n) = ' + parts.join(' + ');
}
function run() {
$('err').textContent = '';
let c, init;
try {
c = parseList($('coef').value);
init = parseList($('init').value);
} catch (e) {
$('err').textContent = "Enter integers separated by commas, for example: 1, 1";
return;
}
if (c.length < 1 || c.length > 6) { $('err').textContent = "The order must be between 1 and 6."; return; }
if (init.length !== c.length) { $('err').textContent = fmt("You need exactly %k% initial values, one per coefficient.", { k: c.length }); return; }
let N = parseInt($('nmax').value, 10);
if (!(N >= 10)) N = 10;
if (N > 400) N = 400;
$('nmax').value = N;
showFormula(c);
const u = genTerms(c, init, N);
const zeros = zeroIndices(u);
const head = u.slice(0, 14).map((x) => {
const s = x.toString();
return s.length > 14 ? s.slice(0, 6) + '…(' + s.length + ')' : s;
});
$('terms').textContent = head.join(', ') + ', …';
const zEl = $('zeros');
const apEl = $('ap');
zEl.textContent = '';
apEl.textContent = '';
if (zeros.length === 0) {
zEl.textContent = fmt("No zero among u(0) … u(%n%).", { n: N });
} else {
zEl.append(fmt("%z% zeros among u(0) … u(%n%):", { z: zeros.length, n: N }) + ' ');
zeros.slice(0, 60).forEach((n) => {
const s = document.createElement('span');
s.className = 'zchip';
s.textContent = n;
zEl.appendChild(s);
});
if (zeros.length > 60) zEl.append('…');
if (zeros.length === u.length) {
apEl.textContent = "Every term is zero.";
} else {
const { found, isolated } = findProgressions(zeros, N);
const lines = found.map((f) => fmt("Pattern: u(n) = 0 for every n ≡ %r% (mod %m%) from n = %s% on.", { r: f.r, m: f.m, s: f.start }));
if (isolated.length) lines.push(fmt("Isolated zeros: %list%", { list: isolated.slice(0, 20).join(', ') }));
apEl.innerHTML = lines.join('<br>');
}
}
drawPlot(u, zeros);
const roots = charRoots(c);
drawRoots(roots);
const mods = roots.map((z) => Math.hypot(z[0], z[1]));
const rmax = Math.max(...mods);
$('rootlist').innerHTML = roots.map((z, i) => {
const dom = mods[i] >= rmax * (1 - 1e-3);
const im = (z[1] < 0 ? '−' : '+') + Math.abs(z[1]).toFixed(4);
const t = z[0].toFixed(4) + ' ' + im + 'i |z|=' + mods[i].toFixed(4);
return dom ? '<b>' + t + '</b>' : t;
}).join('<br>');
const rows = [];
for (const p of PRIMES) {
const r = modPattern(c, init, p);
if (!r.full) { rows.push(fmt("mod %p%: period longer than the search limit", { p })); continue; }
const shown = r.seq.slice(0, 48).map((x) => (x === 0 ? '<b>0</b>' : '·')).join('');
const nz = r.seq.filter((x) => x === 0).length;
rows.push(fmt("mod %p%: period %L% (after %mu% preperiodic terms), %z% zero positions per period", { p, L: r.L, mu: r.mu, z: nz }) + '<br><span class="bits">' + shown + (r.L > 48 ? '…' : '') + '</span>');
}
$('modtab').innerHTML = rows.join('<br>');
}
function loadPreset() {
const p = PRESETS[$('preset').value];
$('coef').value = p.c;
$('init').value = p.i;
$('nmax').value = p.n;
run();
}
$('preset').addEventListener('change', loadPreset);
$('run').addEventListener('click', run);
$('coef').addEventListener('input', () => { $('preset').value = 'custom'; });
$('init').addEventListener('input', () => { $('preset').value = 'custom'; });
window.addEventListener('resize', run);
run();
Comments
Loading comments...