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.
- package/README.md +3 -3
- package/follows-from/artifacts/ackermann.html +678 -0
- package/follows-from/artifacts/auroracare.html +1297 -0
- package/follows-from/artifacts/bike-trip.html +752 -0
- package/follows-from/artifacts/binomial-theorem.html +631 -0
- package/follows-from/artifacts/bmi.html +511 -0
- package/follows-from/artifacts/building-performance.html +750 -0
- package/follows-from/artifacts/clinical-care.html +726 -0
- package/follows-from/artifacts/collatz.html +403 -0
- package/follows-from/artifacts/complex.html +321 -0
- package/follows-from/artifacts/control-system.html +482 -0
- package/follows-from/artifacts/delfour.html +849 -0
- package/follows-from/artifacts/earthquake-epicenter.html +982 -0
- package/follows-from/artifacts/eco-route.html +662 -0
- package/follows-from/artifacts/euclid-infinitude.html +564 -0
- package/follows-from/artifacts/euler-identity.html +667 -0
- package/follows-from/artifacts/exoplanet-transit.html +1000 -0
- package/follows-from/artifacts/faltings-theorem.html +1046 -0
- package/follows-from/artifacts/fibonacci.html +299 -0
- package/follows-from/artifacts/fundamental-theorem-arithmetic.html +398 -0
- package/follows-from/artifacts/godel-numbering.html +743 -0
- package/follows-from/artifacts/gps-bike.html +759 -0
- package/follows-from/artifacts/gps-clinical-bench.html +792 -0
- package/follows-from/artifacts/graph-french.html +449 -0
- package/follows-from/artifacts/grass-molecular.html +592 -0
- package/follows-from/artifacts/group-theory.html +740 -0
- package/follows-from/artifacts/health-info.html +833 -0
- package/follows-from/artifacts/kaprekar-constant.html +576 -0
- package/follows-from/artifacts/lee.html +805 -0
- package/follows-from/artifacts/linked-lists.html +502 -0
- package/follows-from/artifacts/lldm.html +612 -0
- package/follows-from/artifacts/matrix-multiplication.html +502 -0
- package/follows-from/artifacts/matrix.html +651 -0
- package/follows-from/artifacts/newton-raphson.html +944 -0
- package/follows-from/artifacts/peano-factorial.html +456 -0
- package/follows-from/artifacts/pi.html +363 -0
- package/follows-from/artifacts/polynomial.html +646 -0
- package/follows-from/artifacts/prime.html +366 -0
- package/follows-from/artifacts/pythagorean-theorem.html +468 -0
- package/follows-from/artifacts/rest-path.html +469 -0
- package/follows-from/artifacts/roots-of-unity.html +363 -0
- package/follows-from/artifacts/turing.html +409 -0
- package/follows-from/artifacts/wind-turbines.html +726 -0
- package/follows-from/index.html +549 -0
- package/follows-from/library/index.md +22 -0
- package/follows-from/logo.svg +12 -0
- package/follows-from/manifesto.md +48 -0
- package/follows-from/method/index.md +30 -0
- package/follows-from/path/index.md +20 -0
- 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 < <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>
|