eyeling 1.15.11 → 1.15.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +3 -3
  2. package/follows-from/artifacts/ackermann.html +678 -0
  3. package/follows-from/artifacts/auroracare.html +1297 -0
  4. package/follows-from/artifacts/bike-trip.html +752 -0
  5. package/follows-from/artifacts/binomial-theorem.html +631 -0
  6. package/follows-from/artifacts/bmi.html +511 -0
  7. package/follows-from/artifacts/building-performance.html +750 -0
  8. package/follows-from/artifacts/clinical-care.html +726 -0
  9. package/follows-from/artifacts/collatz.html +403 -0
  10. package/follows-from/artifacts/complex.html +321 -0
  11. package/follows-from/artifacts/control-system.html +482 -0
  12. package/follows-from/artifacts/delfour.html +849 -0
  13. package/follows-from/artifacts/earthquake-epicenter.html +982 -0
  14. package/follows-from/artifacts/eco-route.html +662 -0
  15. package/follows-from/artifacts/euclid-infinitude.html +564 -0
  16. package/follows-from/artifacts/euler-identity.html +667 -0
  17. package/follows-from/artifacts/exoplanet-transit.html +1000 -0
  18. package/follows-from/artifacts/faltings-theorem.html +1046 -0
  19. package/follows-from/artifacts/fibonacci.html +299 -0
  20. package/follows-from/artifacts/fundamental-theorem-arithmetic.html +398 -0
  21. package/follows-from/artifacts/godel-numbering.html +743 -0
  22. package/follows-from/artifacts/gps-bike.html +759 -0
  23. package/follows-from/artifacts/gps-clinical-bench.html +792 -0
  24. package/follows-from/artifacts/graph-french.html +449 -0
  25. package/follows-from/artifacts/grass-molecular.html +592 -0
  26. package/follows-from/artifacts/group-theory.html +740 -0
  27. package/follows-from/artifacts/health-info.html +833 -0
  28. package/follows-from/artifacts/kaprekar-constant.html +576 -0
  29. package/follows-from/artifacts/lee.html +805 -0
  30. package/follows-from/artifacts/linked-lists.html +502 -0
  31. package/follows-from/artifacts/lldm.html +612 -0
  32. package/follows-from/artifacts/matrix-multiplication.html +502 -0
  33. package/follows-from/artifacts/matrix.html +651 -0
  34. package/follows-from/artifacts/newton-raphson.html +944 -0
  35. package/follows-from/artifacts/peano-factorial.html +456 -0
  36. package/follows-from/artifacts/pi.html +363 -0
  37. package/follows-from/artifacts/polynomial.html +646 -0
  38. package/follows-from/artifacts/prime.html +366 -0
  39. package/follows-from/artifacts/pythagorean-theorem.html +468 -0
  40. package/follows-from/artifacts/rest-path.html +469 -0
  41. package/follows-from/artifacts/roots-of-unity.html +363 -0
  42. package/follows-from/artifacts/turing.html +409 -0
  43. package/follows-from/artifacts/wind-turbines.html +726 -0
  44. package/follows-from/index.html +549 -0
  45. package/follows-from/library/index.md +22 -0
  46. package/follows-from/logo.svg +12 -0
  47. package/follows-from/manifesto.md +48 -0
  48. package/follows-from/method/index.md +30 -0
  49. package/follows-from/path/index.md +20 -0
  50. package/package.json +4 -3
@@ -0,0 +1,646 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Polynomial roots (Durand–Kerner)</title>
7
+ <style>
8
+ :root {
9
+ --fg: #101014;
10
+ --bg: #ffffff;
11
+ --muted: #666;
12
+ --accent: #2563eb;
13
+ --chip: #eef2ff;
14
+ --ok: #16a34a;
15
+ --bad: #dc2626;
16
+ --warn: #ca8a04;
17
+ --card: color-mix(in srgb, var(--accent) 4%, transparent);
18
+ }
19
+ @media (prefers-color-scheme: dark) {
20
+ :root {
21
+ --fg: #eaeaf0;
22
+ --bg: #0b0b10;
23
+ --muted: #a0a0b0;
24
+ --accent: #60a5fa;
25
+ --chip: #0e1a32;
26
+ --card: color-mix(in srgb, var(--accent) 6%, transparent);
27
+ }
28
+ }
29
+ html,
30
+ body {
31
+ margin: 0;
32
+ padding: 0;
33
+ background: var(--bg);
34
+ color: var(--fg);
35
+ font:
36
+ 15px/1.6 ui-sans-serif,
37
+ system-ui,
38
+ -apple-system,
39
+ Segoe UI,
40
+ Roboto,
41
+ Helvetica,
42
+ Arial;
43
+ }
44
+ main {
45
+ max-width: 1100px;
46
+ margin: 0 auto;
47
+ padding: 28px 16px 80px;
48
+ }
49
+ h1 {
50
+ font-size: clamp(1.6rem, 2.6vw + 1rem, 2.2rem);
51
+ margin: 0 0 6px;
52
+ }
53
+ header p {
54
+ margin: 0;
55
+ color: var(--muted);
56
+ }
57
+ section {
58
+ margin: 18px 0 22px;
59
+ padding: 14px 14px 16px;
60
+ border: 1px solid color-mix(in srgb, var(--accent) 18%, transparent);
61
+ border-radius: 14px;
62
+ background: var(--card);
63
+ }
64
+ section h2 {
65
+ margin: 0 0 8px;
66
+ font-size: 1.15rem;
67
+ }
68
+ .row {
69
+ display: flex;
70
+ gap: 12px;
71
+ align-items: center;
72
+ flex-wrap: wrap;
73
+ }
74
+ .col {
75
+ display: flex;
76
+ flex-direction: column;
77
+ gap: 10px;
78
+ }
79
+ .btn {
80
+ appearance: none;
81
+ border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
82
+ background: color-mix(in srgb, var(--accent) 8%, transparent);
83
+ color: var(--fg);
84
+ border-radius: 10px;
85
+ padding: 8px 12px;
86
+ font-weight: 700;
87
+ cursor: pointer;
88
+ }
89
+ .mono {
90
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
91
+ }
92
+ .muted {
93
+ color: var(--muted);
94
+ }
95
+ .small {
96
+ font-size: 0.92em;
97
+ }
98
+ .chip {
99
+ display: inline-block;
100
+ padding: 2px 8px;
101
+ border-radius: 999px;
102
+ background: var(--chip);
103
+ font-weight: 600;
104
+ }
105
+ input[type='text'] {
106
+ width: 540px;
107
+ border-radius: 10px;
108
+ border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
109
+ padding: 8px 10px;
110
+ }
111
+ label {
112
+ user-select: none;
113
+ }
114
+ code {
115
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
116
+ padding: 0.1rem 0.35rem;
117
+ border-radius: 0.35rem;
118
+ }
119
+ .ok {
120
+ color: var(--ok);
121
+ }
122
+ .bad {
123
+ color: var(--bad);
124
+ }
125
+ .warn {
126
+ color: var(--warn);
127
+ }
128
+ #answer {
129
+ overflow-x: auto;
130
+ }
131
+ #answer pre {
132
+ white-space: pre !important;
133
+ overflow-x: auto;
134
+ overflow-y: auto;
135
+ max-width: 100%;
136
+ }
137
+ table.tbl {
138
+ border-collapse: collapse;
139
+ width: 100%;
140
+ }
141
+ .tbl th,
142
+ .tbl td {
143
+ padding: 6px 8px;
144
+ border-bottom: 1px dashed color-mix(in srgb, var(--fg) 18%, transparent);
145
+ vertical-align: top;
146
+ text-align: left;
147
+ }
148
+ </style>
149
+ </head>
150
+ <body>
151
+ <main>
152
+ <header class="row">
153
+ <div>
154
+ <h1>Polynomial roots (Durand–Kerner)</h1>
155
+ <p>
156
+ Self‑contained complex root solver with an “explain & check” harness. Enter coefficients; get roots,
157
+ residuals, Vieta checks, and a rebuild test.
158
+ </p>
159
+ </div>
160
+ <div class="row" style="margin-left: auto">
161
+ <label class="muted small"
162
+ >Coefficients (descending powers): <input id="coeffs" type="text" class="mono" value="1, -10, 35, -50, 24"
163
+ /></label>
164
+ <button id="solve" class="btn">Solve</button>
165
+ <button id="loadP1" class="btn">Load P1</button>
166
+ <button id="loadP2" class="btn">Load P2</button>
167
+ <button id="checkBtn" class="btn">Check</button>
168
+ </div>
169
+ </header>
170
+
171
+ <section>
172
+ <h2>What this is?</h2>
173
+ <p>This tool finds all complex roots of a polynomial using the <em>Durand–Kerner</em> (Weierstrass) method.</p>
174
+ <div class="small">
175
+ <p>
176
+ <strong>How it works:</strong> normalize to monic; choose distinct complex seeds; iterate
177
+ <code>x_k ← x_k − P(x_k) / ∏_{j≠k}(x_k − x_j)</code> simultaneously. For simple roots and generic starting
178
+ points, all roots converge quickly.
179
+ </p>
180
+ <p>
181
+ <strong>Why it’s correct (sketch):</strong> roots are fixed points of this map; locally it behaves like a
182
+ simultaneous Newton step. We stop when all updates are &lt; <code>1e‑14</code> or after a hard iteration
183
+ cap.
184
+ </p>
185
+ <p>
186
+ <strong>Proof harness:</strong> after solving we compute tiny residuals <code>|P(r)|</code>, check Vieta’s
187
+ identities on the monic form, and rebuild the polynomial from the roots to compare coefficients.
188
+ </p>
189
+ </div>
190
+ </section>
191
+
192
+ <section id="answer">
193
+ <h2>Answer</h2>
194
+ <div id="chips" class="row small" style="gap: 8px"></div>
195
+ <div id="prettyPoly" class="mono small"></div>
196
+ <div id="roots"></div>
197
+ <div id="vieta" class="mono small"></div>
198
+ <div id="rebuild" class="mono small"></div>
199
+ </section>
200
+
201
+ <section id="reason">
202
+ <h2>Reason why</h2>
203
+ <div class="small">
204
+ <p>
205
+ <strong>Sorting & formatting.</strong> Roots are shown in a deterministic order (by rounded real, then imag
206
+ parts). Near‑integers snap to integers; we hide <code>±0i</code> and print <code>i</code> or
207
+ <code>−i</code> for <code>±1·i</code>.
208
+ </p>
209
+ <p>
210
+ <strong>Limits.</strong> Multiple roots converge slower; here, the two included test polynomials have simple
211
+ roots, so quadratic convergence is typical. Complexity per iteration is O(n²) (each update divides by a
212
+ product).
213
+ </p>
214
+ </div>
215
+ </section>
216
+
217
+ <section id="check">
218
+ <h2>Check (harness)</h2>
219
+ <div id="check-body"></div>
220
+ </section>
221
+ </main>
222
+
223
+ <script>
224
+ (function () {
225
+ 'use strict';
226
+ const $ = (id) => document.getElementById(id);
227
+ const setHTML = (id, html) => {
228
+ const el = $(id);
229
+ if (el) el.innerHTML = html;
230
+ };
231
+ const now = () => performance.now();
232
+
233
+ // -------- Complex numbers (double) --------
234
+ function C(re, im) {
235
+ return { re: +re, im: +im };
236
+ }
237
+ const C0 = C(0, 0),
238
+ C1 = C(1, 0);
239
+ function cAdd(a, b) {
240
+ return C(a.re + b.re, a.im + b.im);
241
+ }
242
+ function cSub(a, b) {
243
+ return C(a.re - b.re, a.im - b.im);
244
+ }
245
+ function cMul(a, b) {
246
+ return C(a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re);
247
+ }
248
+ function cDiv(a, b) {
249
+ const d = b.re * b.re + b.im * b.im;
250
+ return C((a.re * b.re + a.im * b.im) / d, (a.im * b.re - a.re * b.im) / d);
251
+ }
252
+ function cAbs(a) {
253
+ return Math.hypot(a.re, a.im);
254
+ }
255
+ function cEq(a, b, eps = 1e-12) {
256
+ return cAbs(cSub(a, b)) <= eps;
257
+ }
258
+ function cScale(a, s) {
259
+ return C(a.re * s, a.im * s);
260
+ }
261
+ function cHorner(coeffs, x) {
262
+ // coeffs in DESC order
263
+ let v = C0;
264
+ for (const c of coeffs) {
265
+ v = cAdd(cMul(v, x), c);
266
+ }
267
+ return v;
268
+ }
269
+ function cPow(base, k) {
270
+ let r = C1;
271
+ for (let i = 0; i < k; i++) r = cMul(r, base);
272
+ return r;
273
+ }
274
+
275
+ // Parse complex from text like "3", "-2+5i", "7- i", "i", "-i", "(14+33i)"
276
+ function parseComplex(s) {
277
+ s = (s || '').trim();
278
+ if (!s) return C0;
279
+ // strip outer parens
280
+ if (s[0] === '(' && s[s.length - 1] === ')') s = s.slice(1, -1);
281
+ s = s.replace(/\s+/g, '');
282
+ s = s.replace(/·/g, ''); // just in case
283
+ s = s.replace(/\*?i$/, 'i'); // normalize "*i" -> "i" at end
284
+ s = s.replace(/−/g, '-'); // minus symbol
285
+ if (!/i/i.test(s)) {
286
+ const x = Number(s);
287
+ if (!Number.isFinite(x)) throw new Error('Bad coefficient: ' + s);
288
+ return C(x, 0);
289
+ }
290
+ // handle pure imag "i" or "-i"
291
+ if (s === 'i' || s === '+i') return C(0, 1);
292
+ if (s === '-i') return C(0, -1);
293
+ // split real and imag (look for last + or - not at start)
294
+ const body = s.endsWith('i') ? s.slice(0, -1) : s; // drop trailing i
295
+ let split = -1;
296
+ for (let i = body.length - 1; i > 0; i--) {
297
+ const ch = body[i];
298
+ if (ch === '+' || ch === '-') {
299
+ split = i;
300
+ break;
301
+ }
302
+ }
303
+ if (split === -1) {
304
+ // no explicit real part: like "5i" or "-2.5i"
305
+ const b = Number(body);
306
+ if (!Number.isFinite(b)) throw new Error('Bad imag: ' + s);
307
+ return C(0, b);
308
+ } else {
309
+ const ra = body.slice(0, split);
310
+ const ib = body.slice(split); // includes sign
311
+ const a = Number(ra);
312
+ const b = Number(ib);
313
+ if (!Number.isFinite(a) || !Number.isFinite(b)) throw new Error('Bad complex: ' + s);
314
+ return C(a, b);
315
+ }
316
+ }
317
+
318
+ function fmtNearInt(x, eps = 1e-12) {
319
+ const r = Math.round(x);
320
+ return Math.abs(x - r) <= eps ? r : +x;
321
+ }
322
+ function fmtC(z, eps = 1e-12) {
323
+ const a0 = fmtNearInt(z.re, eps),
324
+ b0 = fmtNearInt(z.im, eps);
325
+ const a = +a0,
326
+ b = +b0;
327
+ if (Math.abs(b) <= eps) return String(a);
328
+ if (Math.abs(a) <= eps) {
329
+ if (b === 1) return 'i';
330
+ if (b === -1) return '-i';
331
+ return String(b) + '*i';
332
+ }
333
+ const sb = b >= 0 ? '+' : '-';
334
+ const bb = Math.abs(b);
335
+ const ib = bb === 1 ? 'i' : bb + '*i';
336
+ return a + sb + ib;
337
+ }
338
+
339
+ function prettyPoly(coeffs) {
340
+ const n = coeffs.length - 1;
341
+ const terms = [];
342
+ for (let i = 0; i < coeffs.length; i++) {
343
+ const p = n - i;
344
+ const c = coeffs[i];
345
+ const isZero = cAbs(c) <= 0;
346
+ if (isZero) continue;
347
+ const name = p === 0 ? '' : p === 1 ? 'x' : 'x^' + p;
348
+ const coef = fmtC(c);
349
+ terms.push(
350
+ (name ? (coef === '1' ? '' : coef === '-1' ? '-' : coef + '*') : '') + name + (name ? '' : coef),
351
+ );
352
+ }
353
+ return 'P(x) = ' + (terms.length ? terms.join(' + ').replace(/\+\s-\s/g, ' - ') : '0');
354
+ }
355
+
356
+ // -------- Durand–Kerner --------
357
+ function normalizeMonic(coeffs) {
358
+ const lc = coeffs[0];
359
+ return { lc, monic: coeffs.map((c) => cDiv(c, lc)) };
360
+ }
361
+
362
+ function durandKerner(coeffs, maxIter = 2000, tol = 1e-14) {
363
+ // coeffs: array of complex (DESC powers). Leading coeff can be anything.
364
+ const n = coeffs.length - 1;
365
+ if (n <= 0) return { roots: [], it: 0 };
366
+ const { lc, monic } = normalizeMonic(coeffs);
367
+ const base = C(0.4, 0.9);
368
+ let roots = Array.from({ length: n }, (_, k) => cPow(base, k));
369
+ let it = 0;
370
+ for (it = 1; it <= maxIter; it++) {
371
+ let done = true;
372
+ const next = roots.slice();
373
+ for (let k = 0; k < n; k++) {
374
+ const xk = roots[k];
375
+ const px = cHorner(monic, xk);
376
+ let denom = C1;
377
+ for (let j = 0; j < n; j++) {
378
+ if (j === k) continue;
379
+ let diff = cSub(xk, roots[j]);
380
+ if (cAbs(diff) === 0) {
381
+ diff = cAdd(diff, C(1e-12 * (k + 1), 1e-12 * (j + 1))); // deterministic micro-perturbation
382
+ }
383
+ denom = cMul(denom, diff);
384
+ }
385
+ if (cAbs(denom) === 0) denom = C(1e-18, 0);
386
+ const step = cDiv(px, denom);
387
+ const xnew = cSub(xk, step);
388
+ next[k] = xnew;
389
+ if (cAbs(cSub(xnew, xk)) > tol) done = false;
390
+ }
391
+ roots = next;
392
+ if (done) break;
393
+ }
394
+ return { roots, it };
395
+ }
396
+
397
+ // Symmetric sums e1..en via DP (elementary symmetric polynomials)
398
+ function symmetricSums(roots) {
399
+ const n = roots.length;
400
+ const e = Array(n + 1).fill(C0);
401
+ e[0] = C1;
402
+ for (const r of roots) {
403
+ for (let k = n; k >= 1; k--) {
404
+ e[k] = cAdd(e[k], cMul(r, e[k - 1]));
405
+ }
406
+ }
407
+ // e[1]=sum, e[2]=sum pairs, ..., e[n]=product
408
+ return e.slice(1);
409
+ }
410
+
411
+ // Convolution (descending)
412
+ function convolveDesc(a, b) {
413
+ const da = a.length - 1,
414
+ db = b.length - 1;
415
+ const out = Array(da + db + 1).fill(C0);
416
+ for (let i = 0; i < a.length; i++) {
417
+ for (let j = 0; j < b.length; j++) {
418
+ out[i + j] = cAdd(out[i + j], cMul(a[i], b[j]));
419
+ }
420
+ }
421
+ return out;
422
+ }
423
+ function rebuildFromRoots(roots) {
424
+ let coeffs = [C1];
425
+ for (const r of roots) {
426
+ coeffs = convolveDesc(coeffs, [C1, C(-r.re, -r.im)]); // (x - r)
427
+ }
428
+ return coeffs;
429
+ }
430
+
431
+ // -------- UI actions --------
432
+ function parseCoeffList(text) {
433
+ // Comma-separated complex numbers using "i" notation, descending powers
434
+ const parts = (text || '')
435
+ .split(',')
436
+ .map((s) => s.trim())
437
+ .filter((s) => s.length > 0);
438
+ if (parts.length === 0) throw new Error('Please enter coefficients, e.g. "1, -10, 35, -50, 24"');
439
+ return parts.map(parseComplex);
440
+ }
441
+
442
+ function sortRoots(roots) {
443
+ return roots.slice().sort((a, b) => {
444
+ const ra = Math.round(a.re * 1e12) / 1e12;
445
+ const rb = Math.round(b.re * 1e12) / 1e12;
446
+ if (ra !== rb) return ra - rb;
447
+ const ia = Math.round(a.im * 1e12) / 1e12;
448
+ const ib = Math.round(b.im * 1e12) / 1e12;
449
+ return ia - ib;
450
+ });
451
+ }
452
+
453
+ function magnitude(x) {
454
+ return typeof x === 'number' ? Math.abs(x) : cAbs(x);
455
+ }
456
+
457
+ function solveAndExplain(coeffs) {
458
+ const t0 = now();
459
+ const { roots, it } = durandKerner(coeffs);
460
+ const t1 = now();
461
+ const rootsSorted = sortRoots(roots);
462
+ const residuals = rootsSorted.map((r) => cAbs(cHorner(coeffs, r)));
463
+ const maxRes = Math.max(...residuals);
464
+ const pretty = prettyPoly(coeffs);
465
+ setHTML('prettyPoly', '<pre>' + pretty + '</pre>');
466
+ const chips = [
467
+ ['degree', coeffs.length - 1],
468
+ ['iter', it],
469
+ ['tol', '1e-14'],
470
+ ['max |P(r)|', maxRes.toExponential(3)],
471
+ ['time', (t1 - t0).toFixed(2) + ' ms'],
472
+ ]
473
+ .map(([k, v]) => `<span class="chip">${k}: ${v}</span>`)
474
+ .join(' ');
475
+ $('chips').innerHTML = chips;
476
+
477
+ // Roots table
478
+ const rows = rootsSorted.map(
479
+ (r, i) =>
480
+ `<tr><td>${i + 1}</td><td class="mono">${fmtC(r)}</td><td class="mono muted">${r.re.toFixed(12)} ${r.im >= 0 ? '+' : '−'} ${Math.abs(r.im).toFixed(12)}i</td><td class="mono small">${residuals[i].toExponential(3)}</td></tr>`,
481
+ );
482
+ const table = `<table class="tbl"><thead><tr><th>#</th><th>root</th><th>as complex</th><th>|P(r)|</th></tr></thead><tbody>${rows.join('\n')}</tbody></table>`;
483
+ setHTML('roots', table);
484
+
485
+ // Vieta on monic
486
+ const { lc, monic } = normalizeMonic(coeffs);
487
+ const e = symmetricSums(rootsSorted); // e[0]=sum, ..., e[n-1]=product
488
+ const n = monic.length - 1;
489
+ const targets = [];
490
+ for (let k = 1; k <= n; k++) {
491
+ // expected e_k = (-1)^k * a_{n-k}, where monic = [1, a_{n-1}, ..., a_0]
492
+ const a = monic[k];
493
+ const want = k % 2 === 1 ? C(-a.re, -a.im) : C(a.re, a.im);
494
+ targets.push(want);
495
+ }
496
+ // Build lines
497
+ const lines = [];
498
+ for (let k = 1; k <= n; k++) {
499
+ const got = e[k - 1],
500
+ want = targets[k - 1];
501
+ const err = cAbs(cSub(got, want));
502
+ const tag = k === n ? 'product' : k === 1 ? 'sum' : 'e' + k;
503
+ lines.push(`${tag.padEnd(7)}: ${fmtC(got)} vs ${fmtC(want)} |Δ|=${err.toExponential(3)}`);
504
+ }
505
+ setHTML('vieta', '<pre>Vieta checks (monic):\n' + lines.join('\n') + '</pre>');
506
+
507
+ // Rebuild
508
+ const rebuiltMonic = rebuildFromRoots(rootsSorted);
509
+ // rescale
510
+ const rebuilt = rebuiltMonic.map((c) => cMul(c, normalizeMonic(coeffs).lc));
511
+ // Pad to match length and compare
512
+ const A = coeffs,
513
+ B = rebuilt;
514
+ const m = Math.max(A.length, B.length);
515
+ const diffs = [];
516
+ let maxCoefErr = 0;
517
+ for (let i = 0; i < m; i++) {
518
+ const a = A[i] || C0,
519
+ b = B[i] || C0;
520
+ const d = cAbs(cSub(a, b));
521
+ maxCoefErr = Math.max(maxCoefErr, d);
522
+ diffs.push(
523
+ `a[${i}] ${fmtC(a)}`.padEnd(24) + ` vs b[${i}] ${fmtC(b)}`.padEnd(24) + ` |Δ|=${d.toExponential(3)}`,
524
+ );
525
+ }
526
+ setHTML(
527
+ 'rebuild',
528
+ '<pre>Rebuild from roots (coeff comparison):\n' +
529
+ diffs.join('\n') +
530
+ '\n' +
531
+ `Max coefficient error: ${maxCoefErr.toExponential(3)}` +
532
+ '</pre>',
533
+ );
534
+ }
535
+
536
+ // -------- Check (harness) --------
537
+ function runChecks() {
538
+ const lines = [];
539
+ const ok = (b) => (b ? '✓' : '✗');
540
+ const tol = 1e-9;
541
+
542
+ // P1: x^4 - 10x^3 + 35x^2 - 50x + 24 (roots 1,2,3,4)
543
+ const P1 = [C(1, 0), C(-10, 0), C(35, 0), C(-50, 0), C(24, 0)];
544
+ const r1 = durandKerner(P1);
545
+ const roots1 = sortRoots(r1.roots);
546
+ const want1 = [1, 2, 3, 4].map((x) => C(x, 0));
547
+ const match1 = roots1.length === 4 && want1.every((w, i) => cEq(roots1[i], w, 1e-10));
548
+ const res1 = Math.max(...roots1.map((r) => cAbs(cHorner(P1, r))));
549
+ lines.push(
550
+ `P1 roots correct (1,2,3,4): ${ok(match1)} | max |P(r)| = ${res1.toExponential(3)} | it=${r1.it}`,
551
+ );
552
+
553
+ // P2: (x - i)(x - (1+i))(x - (3+2i))(x - (5+i))
554
+ const P2 = [C(1, 0), C(-9, -5), C(14, 33), C(24, -44), C(-26, 0)];
555
+ const r2 = durandKerner(P2);
556
+ const roots2 = sortRoots(r2.roots);
557
+ const want2 = [C(0, 1), C(1, 1), C(3, 2), C(5, 1)];
558
+ const match2 = roots2.length === 4 && want2.every((w, i) => cEq(roots2[i], w, 1e-9));
559
+ const res2 = Math.max(...roots2.map((r) => cAbs(cHorner(P2, r))));
560
+ lines.push(
561
+ `P2 roots correct (i, 1+i, 3+2i, 5+i): ${ok(match2)} | max |P(r)| = ${res2.toExponential(3)} | it=${r2.it}`,
562
+ );
563
+
564
+ // Vieta checks for both
565
+ function vietaOK(coeffs, roots) {
566
+ const { monic } = normalizeMonic(coeffs);
567
+ const e = symmetricSums(roots);
568
+ const n = monic.length - 1;
569
+ let all = true;
570
+ for (let k = 1; k <= n; k++) {
571
+ const a = monic[k];
572
+ const want = k % 2 === 1 ? C(-a.re, -a.im) : C(a.re, a.im);
573
+ const got = e[k - 1];
574
+ if (cAbs(cSub(got, want)) > tol) {
575
+ all = false;
576
+ break;
577
+ }
578
+ }
579
+ return all;
580
+ }
581
+ lines.push(`P1 Vieta equalities hold: ${ok(vietaOK(P1, roots1))}`);
582
+ lines.push(`P2 Vieta equalities hold: ${ok(vietaOK(P2, roots2))}`);
583
+
584
+ // Rebuild comparisons
585
+ function rebuildErr(coeffs, roots) {
586
+ const rebMon = rebuildFromRoots(roots);
587
+ const reb = rebMon.map((c) => cMul(c, normalizeMonic(coeffs).lc));
588
+ let maxE = 0;
589
+ for (let i = 0; i < coeffs.length; i++) {
590
+ const e = cAbs(cSub(coeffs[i], reb[i]));
591
+ if (e > maxE) maxE = e;
592
+ }
593
+ return maxE;
594
+ }
595
+ const e1 = rebuildErr(P1, roots1);
596
+ const e2 = rebuildErr(P2, roots2);
597
+ lines.push(`P1 rebuild max-coefficient error: ${e1.toExponential(3)} (${ok(e1 < 1e-10)})`);
598
+ lines.push(`P2 rebuild max-coefficient error: ${e2.toExponential(3)} (${ok(e2 < 1e-10)})`);
599
+
600
+ // Determinism (seed & sort): running twice gives same ordered roots
601
+ const r1b = durandKerner(P1);
602
+ const sameOrder = roots1.every((z, i) => cEq(z, sortRoots(r1b.roots)[i], 1e-12));
603
+ lines.push(`Deterministic ordering (repeat run): ${ok(sameOrder)}`);
604
+
605
+ const pre = document.createElement('pre');
606
+ pre.className = 'mono';
607
+ pre.style.whiteSpace = 'pre-wrap';
608
+ pre.textContent = lines.join('\n');
609
+ $('check-body').replaceChildren(pre);
610
+ }
611
+
612
+ // Wire up
613
+ function doSolve() {
614
+ const txt = $('coeffs').value;
615
+ let coeffs;
616
+ try {
617
+ coeffs = parseCoeffList(txt);
618
+ } catch (e) {
619
+ setHTML('prettyPoly', '<span class="bad">' + (e.message || String(e)) + '</span>');
620
+ $('chips').innerHTML = '';
621
+ $('roots').innerHTML = '';
622
+ $('vieta').innerHTML = '';
623
+ $('rebuild').innerHTML = '';
624
+ return;
625
+ }
626
+ solveAndExplain(coeffs);
627
+ }
628
+
629
+ $('solve').addEventListener('click', doSolve);
630
+ $('loadP1').addEventListener('click', () => {
631
+ $('coeffs').value = '1, -10, 35, -50, 24';
632
+ doSolve();
633
+ });
634
+ $('loadP2').addEventListener('click', () => {
635
+ $('coeffs').value = '1, (-9-5i), (14+33i), (24-44i), -26';
636
+ doSolve();
637
+ });
638
+ $('checkBtn').addEventListener('click', runChecks);
639
+
640
+ // Initial
641
+ doSolve(); // with default P1
642
+ runChecks();
643
+ })();
644
+ </script>
645
+ </body>
646
+ </html>