@johnhenry/math-sample 0.0.1 → 0.1.0
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 +111 -3
- package/dist/budget.d.ts +47 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +44 -0
- package/dist/budget.js.map +1 -0
- package/dist/complex.d.ts +79 -0
- package/dist/complex.d.ts.map +1 -0
- package/dist/complex.js +236 -0
- package/dist/complex.js.map +1 -0
- package/dist/expr.d.ts +12 -0
- package/dist/expr.d.ts.map +1 -1
- package/dist/expr.js +240 -0
- package/dist/expr.js.map +1 -1
- package/dist/field2d.d.ts +58 -0
- package/dist/field2d.d.ts.map +1 -0
- package/dist/field2d.js +199 -0
- package/dist/field2d.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/mesh.d.ts +90 -0
- package/dist/mesh.d.ts.map +1 -0
- package/dist/mesh.js +294 -0
- package/dist/mesh.js.map +1 -0
- package/dist/ode.d.ts +147 -0
- package/dist/ode.d.ts.map +1 -0
- package/dist/ode.js +466 -0
- package/dist/ode.js.map +1 -0
- package/dist/series.d.ts +53 -0
- package/dist/series.d.ts.map +1 -0
- package/dist/series.js +158 -0
- package/dist/series.js.map +1 -0
- package/package.json +3 -3
- package/src/budget.ts +72 -0
- package/src/complex.ts +327 -0
- package/src/expr.ts +244 -0
- package/src/field2d.ts +259 -0
- package/src/index.ts +76 -0
- package/src/mesh.ts +376 -0
- package/src/ode.ts +633 -0
- package/src/series.ts +209 -0
package/dist/series.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { assertCount, assertRange } from "./budget.js";
|
|
2
|
+
/** Evaluate `sum a_k (x - x0)^k` for `k <= order` by Horner's rule. */
|
|
3
|
+
export function evaluateTaylor(coefficients, x0, x, order = coefficients.length - 1) {
|
|
4
|
+
const top = Math.min(order, coefficients.length - 1);
|
|
5
|
+
let acc = 0;
|
|
6
|
+
for (let k = top; k >= 0; k--)
|
|
7
|
+
acc = acc * (x - x0) + coefficients[k];
|
|
8
|
+
return acc;
|
|
9
|
+
}
|
|
10
|
+
function cleanSamples(x, y) {
|
|
11
|
+
// enforce: gaps not first/last/adjacent
|
|
12
|
+
const xs = [];
|
|
13
|
+
const ys = [];
|
|
14
|
+
for (let k = 0; k < x.length; k++) {
|
|
15
|
+
const yy = y[k];
|
|
16
|
+
if (Number.isFinite(yy)) {
|
|
17
|
+
xs.push(x[k]);
|
|
18
|
+
ys.push(yy);
|
|
19
|
+
}
|
|
20
|
+
else if (ys.length > 0 && !Number.isNaN(ys[ys.length - 1])) {
|
|
21
|
+
xs.push(x[k]);
|
|
22
|
+
ys.push(Number.NaN);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
while (ys.length > 0 && Number.isNaN(ys[ys.length - 1])) {
|
|
26
|
+
ys.pop();
|
|
27
|
+
xs.pop();
|
|
28
|
+
}
|
|
29
|
+
return { x: Float64Array.from(xs), y: Float64Array.from(ys) };
|
|
30
|
+
}
|
|
31
|
+
function grid(domain, n) {
|
|
32
|
+
assertRange("domain", domain);
|
|
33
|
+
assertCount("samples", n);
|
|
34
|
+
const x = new Float64Array(n);
|
|
35
|
+
for (let k = 0; k < n; k++)
|
|
36
|
+
x[k] = k === n - 1 ? domain.max : domain.min + ((domain.max - domain.min) * k) / (n - 1);
|
|
37
|
+
return x;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Taylor partial sums of the given coefficients (`a_k = f^(k)(x0) / k!`) about `x0`, one curve per requested
|
|
41
|
+
* order, over a uniform grid. With `@johnhenry/math`, `taylorCoefficients` in `@johnhenry/math-sample/expr`
|
|
42
|
+
* produces the coefficients from an expression.
|
|
43
|
+
*/
|
|
44
|
+
export function sampleTaylorPartialSums(coefficients, x0, domain, orders, samples = 400) {
|
|
45
|
+
const x = grid(domain, samples);
|
|
46
|
+
const curves = orders.map((order) => {
|
|
47
|
+
if (!Number.isInteger(order) || order < 0)
|
|
48
|
+
throw new RangeError(`order must be a non-negative integer, got ${order}`);
|
|
49
|
+
const y = new Float64Array(samples);
|
|
50
|
+
for (let k = 0; k < samples; k++)
|
|
51
|
+
y[k] = evaluateTaylor(coefficients, x0, x[k], order);
|
|
52
|
+
return cleanSamples(x, y);
|
|
53
|
+
});
|
|
54
|
+
return { orders: [...orders], curves };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Partial sums of a function series `sum_{k=from}^{n} term(k, x)`, one curve per order `n`, computed
|
|
58
|
+
* in a single pass over `k` so each term is evaluated once per sample.
|
|
59
|
+
*/
|
|
60
|
+
export function sampleSeriesPartialSums(term, from, domain, orders, samples = 400) {
|
|
61
|
+
if (!Number.isInteger(from))
|
|
62
|
+
throw new RangeError("from must be an integer");
|
|
63
|
+
const x = grid(domain, samples);
|
|
64
|
+
const sorted = [...new Set(orders)].sort((a, b) => a - b);
|
|
65
|
+
for (const o of sorted)
|
|
66
|
+
if (!Number.isInteger(o) || o < from)
|
|
67
|
+
throw new RangeError(`order ${o} must be an integer >= from (${from})`);
|
|
68
|
+
const sums = new Float64Array(samples);
|
|
69
|
+
const byOrder = new Map();
|
|
70
|
+
let k = from;
|
|
71
|
+
for (const o of sorted) {
|
|
72
|
+
for (; k <= o; k++)
|
|
73
|
+
for (let i = 0; i < samples; i++)
|
|
74
|
+
sums[i] = sums[i] + term(k, x[i]);
|
|
75
|
+
byOrder.set(o, cleanSamples(x, Float64Array.from(sums)));
|
|
76
|
+
}
|
|
77
|
+
return { orders: [...orders], curves: orders.map((o) => byOrder.get(o)) };
|
|
78
|
+
}
|
|
79
|
+
/** Partial sums of the numeric series `sum_{k=from}^{to} term(k)`, with an Aitken limit estimate. */
|
|
80
|
+
export function partialSums(term, from, to) {
|
|
81
|
+
if (!Number.isInteger(from) || !Number.isInteger(to) || to < from)
|
|
82
|
+
throw new RangeError("need integers from <= to");
|
|
83
|
+
const partial = new Float64Array(to - from + 1);
|
|
84
|
+
let s = 0;
|
|
85
|
+
for (let k = from; k <= to; k++) {
|
|
86
|
+
s += term(k);
|
|
87
|
+
partial[k - from] = s;
|
|
88
|
+
}
|
|
89
|
+
let limitEstimate = Number.NaN;
|
|
90
|
+
const n = partial.length;
|
|
91
|
+
if (n >= 3) {
|
|
92
|
+
const a = partial[n - 3];
|
|
93
|
+
const b = partial[n - 2];
|
|
94
|
+
const c = partial[n - 1];
|
|
95
|
+
const den = c - 2 * b + a;
|
|
96
|
+
limitEstimate = den === 0 ? c : c - ((c - b) * (c - b)) / den;
|
|
97
|
+
}
|
|
98
|
+
return { partial, from, limitEstimate };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Fourier coefficients of `f` on `[a, b]` taken as one period, by the midpoint rule on `samples` points
|
|
102
|
+
* (exact for trig polynomials below the Nyquist order, being a full-period rule; for a function with jumps the coefficients carry
|
|
103
|
+
* ordinary O(1/samples) quadrature error). `terms` is the highest harmonic. Trig form:
|
|
104
|
+
* `a0/2 + sum a_k cos(k w x) + b_k sin(k w x)` about the interval centre.
|
|
105
|
+
*/
|
|
106
|
+
export function fourierCoefficients(f, period, terms, samples = 4096) {
|
|
107
|
+
assertRange("period", period);
|
|
108
|
+
assertCount("terms", terms, 1);
|
|
109
|
+
assertCount("samples", samples, 4);
|
|
110
|
+
const L = (period.max - period.min) / 2;
|
|
111
|
+
const center = (period.max + period.min) / 2;
|
|
112
|
+
const a = new Float64Array(terms);
|
|
113
|
+
const b = new Float64Array(terms);
|
|
114
|
+
const vals = new Float64Array(samples);
|
|
115
|
+
for (let i = 0; i < samples; i++)
|
|
116
|
+
vals[i] = f(period.min + ((period.max - period.min) * (i + 0.5)) / samples); // midpoint
|
|
117
|
+
let a0 = 0;
|
|
118
|
+
for (let i = 0; i < samples; i++)
|
|
119
|
+
a0 += vals[i];
|
|
120
|
+
a0 = (2 * a0) / samples;
|
|
121
|
+
for (let k = 1; k <= terms; k++) {
|
|
122
|
+
let sa = 0;
|
|
123
|
+
let sb = 0;
|
|
124
|
+
for (let i = 0; i < samples; i++) {
|
|
125
|
+
const t = Math.PI * k * (((i + 0.5) / samples) * 2 - 1);
|
|
126
|
+
sa += vals[i] * Math.cos(t);
|
|
127
|
+
sb += vals[i] * Math.sin(t);
|
|
128
|
+
}
|
|
129
|
+
a[k - 1] = (2 * sa) / samples;
|
|
130
|
+
b[k - 1] = (2 * sb) / samples;
|
|
131
|
+
}
|
|
132
|
+
return { halfPeriod: L, center, a0, a, b };
|
|
133
|
+
}
|
|
134
|
+
/** Value of the Fourier partial sum with harmonics up to `n` at `x`. */
|
|
135
|
+
export function evaluateFourier(c, x, n = c.a.length) {
|
|
136
|
+
const top = Math.min(n, c.a.length);
|
|
137
|
+
const w = Math.PI / c.halfPeriod;
|
|
138
|
+
let s = c.a0 / 2;
|
|
139
|
+
for (let k = 1; k <= top; k++) {
|
|
140
|
+
const t = k * w * (x - c.center);
|
|
141
|
+
s += c.a[k - 1] * Math.cos(t) + c.b[k - 1] * Math.sin(t);
|
|
142
|
+
}
|
|
143
|
+
return s;
|
|
144
|
+
}
|
|
145
|
+
/** One curve per harmonic count in `orders` (0 = just the constant term). */
|
|
146
|
+
export function sampleFourierPartialSums(c, domain, orders, samples = 400) {
|
|
147
|
+
const x = grid(domain, samples);
|
|
148
|
+
const curves = orders.map((n) => {
|
|
149
|
+
if (!Number.isInteger(n) || n < 0)
|
|
150
|
+
throw new RangeError(`order must be a non-negative integer, got ${n}`);
|
|
151
|
+
const y = new Float64Array(samples);
|
|
152
|
+
for (let i = 0; i < samples; i++)
|
|
153
|
+
y[i] = evaluateFourier(c, x[i], n);
|
|
154
|
+
return cleanSamples(x, y);
|
|
155
|
+
});
|
|
156
|
+
return { orders: [...orders], curves };
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=series.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"series.js","sourceRoot":"","sources":["../src/series.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnhenry/math-sample",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Pure-ESM, DOM-free, Web-Worker-safe 1D function sampling for plotting: adaptive sampling with NaN gaps and discontinuity detection, a DPR-aware viewport model, nice ticks, and SVG path output. Typed arrays out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"author": "John Henry",
|
|
55
55
|
"license": "MIT",
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@johnhenry/math": "
|
|
57
|
+
"@johnhenry/math": ">=0.2.0 <1.0.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependenciesMeta": {
|
|
60
60
|
"@johnhenry/math": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@johnhenry/math": "^0.
|
|
65
|
+
"@johnhenry/math": "^0.2.0",
|
|
66
66
|
"@types/node": "^22.10.0",
|
|
67
67
|
"typescript": "^5.9.0"
|
|
68
68
|
},
|
package/src/budget.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type CancelSignal, SampleCancelledError } from "./sample.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A work limit for the 2D/3D samplers. When it runs out the sampler stops at a
|
|
5
|
+
* row boundary and returns what it has, with a {@link Progress} saying where
|
|
6
|
+
* it stopped (it does not throw).
|
|
7
|
+
*/
|
|
8
|
+
export interface Budget {
|
|
9
|
+
/** Most function evaluations ("cells") the call may spend. Checked per row/slab. */
|
|
10
|
+
maxCells?: number;
|
|
11
|
+
/** Wall-clock limit in milliseconds. Checked per row/slab, so a single row can overrun it. */
|
|
12
|
+
maxMillis?: number;
|
|
13
|
+
/** Clock override for tests; default `performance.now` when present, else `Date.now`. */
|
|
14
|
+
now?: () => number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Cancellable {
|
|
18
|
+
signal?: CancelSignal;
|
|
19
|
+
isCancelled?: () => boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Progress {
|
|
23
|
+
/** Rows (or slabs, or steps) completed. */
|
|
24
|
+
rows: number;
|
|
25
|
+
/** Rows the full job needed. */
|
|
26
|
+
of: number;
|
|
27
|
+
complete: boolean;
|
|
28
|
+
/** Why it stopped early. Absent when `complete`. */
|
|
29
|
+
reason?: "cells" | "time";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Tracks a {@link Budget} and cancellation for one call. */
|
|
33
|
+
export class Meter {
|
|
34
|
+
private readonly start: number;
|
|
35
|
+
private spent = 0;
|
|
36
|
+
private readonly now: () => number;
|
|
37
|
+
private readonly budget: Budget | undefined;
|
|
38
|
+
private readonly cancel: Cancellable | undefined;
|
|
39
|
+
constructor(budget: Budget | undefined, cancel: Cancellable | undefined) {
|
|
40
|
+
this.budget = budget;
|
|
41
|
+
this.cancel = cancel;
|
|
42
|
+
this.now = budget?.now ?? (typeof performance !== "undefined" ? () => performance.now() : () => Date.now());
|
|
43
|
+
this.start = this.now();
|
|
44
|
+
}
|
|
45
|
+
/** Throws on cancellation; returns a reason when the budget cannot afford `cost` more evaluations. */
|
|
46
|
+
check(cost: number): "cells" | "time" | undefined {
|
|
47
|
+
if (this.cancel?.signal?.aborted || this.cancel?.isCancelled?.()) throw new SampleCancelledError();
|
|
48
|
+
const b = this.budget;
|
|
49
|
+
if (b?.maxCells !== undefined && this.spent + cost > b.maxCells) return "cells";
|
|
50
|
+
if (b?.maxMillis !== undefined && this.now() - this.start > b.maxMillis) return "time";
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
spend(n: number): void {
|
|
54
|
+
this.spent += n;
|
|
55
|
+
}
|
|
56
|
+
get cells(): number {
|
|
57
|
+
return this.spent;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function progress(rows: number, of: number, reason?: "cells" | "time"): Progress {
|
|
62
|
+
return reason === undefined ? { rows, of, complete: rows >= of } : { rows, of, complete: false, reason };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function assertCount(name: string, n: number, min = 2): void {
|
|
66
|
+
if (!Number.isInteger(n) || n < min) throw new RangeError(`${name} must be an integer >= ${min}, got ${n}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function assertRange(name: string, d: { min: number; max: number }): void {
|
|
70
|
+
if (!Number.isFinite(d.min) || !Number.isFinite(d.max) || !(d.min < d.max))
|
|
71
|
+
throw new RangeError(`${name} must be finite with min < max`);
|
|
72
|
+
}
|
package/src/complex.ts
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { assertCount, assertRange, type Budget, type Cancellable, Meter, type Progress, progress } from "./budget.ts";
|
|
2
|
+
import type { Domain } from "./sample.ts";
|
|
3
|
+
|
|
4
|
+
/** A complex function over plain numbers: `(re, im) -> [re, im]`. */
|
|
5
|
+
export type ComplexFn = (re: number, im: number) => readonly [number, number];
|
|
6
|
+
|
|
7
|
+
export interface ComplexRasterOptions extends Budget, Cancellable {
|
|
8
|
+
/** Pixels across (real axis). Default 128. */
|
|
9
|
+
width?: number;
|
|
10
|
+
/** Pixels down (imaginary axis). Default 128. */
|
|
11
|
+
height?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Grid of `f(z)` values for domain colouring. Row 0 is the *smallest* imaginary part (mathematical
|
|
16
|
+
* orientation); flip when drawing to a canvas. Cells where `f` is not finite hold `NaN` in all four arrays
|
|
17
|
+
* (a pole shows as `abs = Infinity` only if `f` returned an infinite modulus with finite parts; otherwise NaN).
|
|
18
|
+
* `arg` is in `(-PI, PI]`.
|
|
19
|
+
*/
|
|
20
|
+
export interface ComplexRaster {
|
|
21
|
+
width: number;
|
|
22
|
+
/** Rows completed (may be fewer than requested when the budget ran out). */
|
|
23
|
+
height: number;
|
|
24
|
+
re: Float32Array;
|
|
25
|
+
im: Float32Array;
|
|
26
|
+
abs: Float32Array;
|
|
27
|
+
arg: Float32Array;
|
|
28
|
+
progress: Progress;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Sample `f` at the centre of each cell of a `width` x `height` grid over the rectangle. */
|
|
32
|
+
export function sampleComplexRaster(
|
|
33
|
+
f: ComplexFn,
|
|
34
|
+
reDomain: Domain,
|
|
35
|
+
imDomain: Domain,
|
|
36
|
+
options: ComplexRasterOptions = {},
|
|
37
|
+
): ComplexRaster {
|
|
38
|
+
const width = options.width ?? 128;
|
|
39
|
+
const height = options.height ?? 128;
|
|
40
|
+
assertCount("width", width, 1);
|
|
41
|
+
assertCount("height", height, 1);
|
|
42
|
+
assertRange("re domain", reDomain);
|
|
43
|
+
assertRange("im domain", imDomain);
|
|
44
|
+
const meter = new Meter(options, options);
|
|
45
|
+
const re = new Float32Array(width * height).fill(Number.NaN);
|
|
46
|
+
const im = new Float32Array(width * height).fill(Number.NaN);
|
|
47
|
+
const abs = new Float32Array(width * height).fill(Number.NaN);
|
|
48
|
+
const arg = new Float32Array(width * height).fill(Number.NaN);
|
|
49
|
+
let rows = 0;
|
|
50
|
+
let reason: "cells" | "time" | undefined;
|
|
51
|
+
for (let j = 0; j < height; j++) {
|
|
52
|
+
reason = meter.check(width);
|
|
53
|
+
if (reason) break;
|
|
54
|
+
const y = imDomain.min + ((imDomain.max - imDomain.min) * (j + 0.5)) / height;
|
|
55
|
+
for (let i = 0; i < width; i++) {
|
|
56
|
+
const x = reDomain.min + ((reDomain.max - reDomain.min) * (i + 0.5)) / width;
|
|
57
|
+
const w = f(x, y);
|
|
58
|
+
const k = j * width + i;
|
|
59
|
+
if (Number.isFinite(w[0]) && Number.isFinite(w[1])) {
|
|
60
|
+
re[k] = w[0];
|
|
61
|
+
im[k] = w[1];
|
|
62
|
+
abs[k] = Math.hypot(w[0], w[1]);
|
|
63
|
+
arg[k] = Math.atan2(w[1], w[0]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
meter.spend(width);
|
|
67
|
+
rows++;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
width,
|
|
71
|
+
height: rows,
|
|
72
|
+
re: re.slice(0, width * rows),
|
|
73
|
+
im: im.slice(0, width * rows),
|
|
74
|
+
abs: abs.slice(0, width * rows),
|
|
75
|
+
arg: arg.slice(0, width * rows),
|
|
76
|
+
progress: progress(rows, height, reason),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ConformalLine {
|
|
81
|
+
/** `"re"`: the image of the vertical line `Re z = value`; `"im"`: of the horizontal line `Im z = value`. */
|
|
82
|
+
kind: "re" | "im";
|
|
83
|
+
value: number;
|
|
84
|
+
/** Image points; `NaN` rows are gaps (never first, last or adjacent). */
|
|
85
|
+
x: Float64Array;
|
|
86
|
+
y: Float64Array;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ConformalGridOptions {
|
|
90
|
+
/** Vertical grid lines (constant real part), evenly spaced including both edges. Default 11. */
|
|
91
|
+
reLines?: number;
|
|
92
|
+
/** Horizontal grid lines (constant imaginary part). Default 11. */
|
|
93
|
+
imLines?: number;
|
|
94
|
+
/** Samples along each line. Default 200. */
|
|
95
|
+
samplesPerLine?: number;
|
|
96
|
+
/** Image points with modulus above this become gaps (poles). Default 1e6. */
|
|
97
|
+
bound?: number;
|
|
98
|
+
/** Segments whose image is longer than this become gaps (branch cuts). Default: no limit. */
|
|
99
|
+
maxStep?: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The image under `f` of a rectangular grid of lines: where the grid lines go, drawn as the conformal-map picture. */
|
|
103
|
+
export function conformalGrid(
|
|
104
|
+
f: ComplexFn,
|
|
105
|
+
reDomain: Domain,
|
|
106
|
+
imDomain: Domain,
|
|
107
|
+
options: ConformalGridOptions = {},
|
|
108
|
+
): ConformalLine[] {
|
|
109
|
+
const nRe = options.reLines ?? 11;
|
|
110
|
+
const nIm = options.imLines ?? 11;
|
|
111
|
+
const n = options.samplesPerLine ?? 200;
|
|
112
|
+
const bound = options.bound ?? 1e6;
|
|
113
|
+
assertCount("reLines", nRe, 1);
|
|
114
|
+
assertCount("imLines", nIm, 1);
|
|
115
|
+
assertCount("samplesPerLine", n);
|
|
116
|
+
assertRange("re domain", reDomain);
|
|
117
|
+
assertRange("im domain", imDomain);
|
|
118
|
+
const lines: ConformalLine[] = [];
|
|
119
|
+
const trace = (kind: "re" | "im", value: number, d: Domain): ConformalLine => {
|
|
120
|
+
const xs: number[] = [];
|
|
121
|
+
const ys: number[] = [];
|
|
122
|
+
let pending = false;
|
|
123
|
+
let px = Number.NaN;
|
|
124
|
+
let py = Number.NaN;
|
|
125
|
+
for (let k = 0; k < n; k++) {
|
|
126
|
+
const s = k === n - 1 ? d.max : d.min + ((d.max - d.min) * k) / (n - 1);
|
|
127
|
+
const w = kind === "re" ? f(value, s) : f(s, value);
|
|
128
|
+
const good = Number.isFinite(w[0]) && Number.isFinite(w[1]) && Math.hypot(w[0], w[1]) <= bound;
|
|
129
|
+
if (!good) {
|
|
130
|
+
pending = xs.length > 0;
|
|
131
|
+
px = Number.NaN;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const cut =
|
|
135
|
+
options.maxStep !== undefined && !Number.isNaN(px) && Math.hypot(w[0] - px, w[1] - py) > options.maxStep;
|
|
136
|
+
if ((pending || cut) && xs.length > 0) {
|
|
137
|
+
xs.push(Number.NaN);
|
|
138
|
+
ys.push(Number.NaN);
|
|
139
|
+
}
|
|
140
|
+
xs.push(w[0]);
|
|
141
|
+
ys.push(w[1]);
|
|
142
|
+
px = w[0];
|
|
143
|
+
py = w[1];
|
|
144
|
+
pending = false;
|
|
145
|
+
}
|
|
146
|
+
return { kind, value, x: Float64Array.from(xs), y: Float64Array.from(ys) };
|
|
147
|
+
};
|
|
148
|
+
for (let i = 0; i < nRe; i++)
|
|
149
|
+
lines.push(
|
|
150
|
+
trace("re", nRe === 1 ? reDomain.min : reDomain.min + ((reDomain.max - reDomain.min) * i) / (nRe - 1), imDomain),
|
|
151
|
+
);
|
|
152
|
+
for (let j = 0; j < nIm; j++)
|
|
153
|
+
lines.push(
|
|
154
|
+
trace("im", nIm === 1 ? imDomain.min : imDomain.min + ((imDomain.max - imDomain.min) * j) / (nIm - 1), reDomain),
|
|
155
|
+
);
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface ZeroOrPole {
|
|
160
|
+
re: number;
|
|
161
|
+
im: number;
|
|
162
|
+
kind: "zero" | "pole";
|
|
163
|
+
/** Multiplicity (zero) or order (pole), from the winding number of the cell it was found in. */
|
|
164
|
+
order: number;
|
|
165
|
+
/** `false` when the local refinement did not converge and `re`/`im` is only the cell centre. */
|
|
166
|
+
refined: boolean;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface ZerosPolesOptions extends Budget, Cancellable {
|
|
170
|
+
/** Cells per side of the search grid. Default 24. */
|
|
171
|
+
grid?: number;
|
|
172
|
+
/** Convergence tolerance on the root position. Default 1e-10 (relative to the rectangle size). */
|
|
173
|
+
tolerance?: number;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface ZerosPoles {
|
|
177
|
+
items: ZeroOrPole[];
|
|
178
|
+
progress: Progress;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const wrap = (d: number) => {
|
|
182
|
+
while (d > Math.PI) d -= 2 * Math.PI;
|
|
183
|
+
while (d < -Math.PI) d += 2 * Math.PI;
|
|
184
|
+
return d;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Zeros and poles of `f` inside a rectangle, by the argument principle: the winding number of `f` around each
|
|
189
|
+
* grid cell is (zeros - poles) inside it; non-zero cells are refined with Newton's method (on `f` for a
|
|
190
|
+
* zero, on `1/f` for a pole, scaled by the multiplicity). A zero and a pole inside one cell cancel and are missed:
|
|
191
|
+
* raise `grid`. The search grid is offset by an irrational fraction of a cell and extended past the
|
|
192
|
+
* rectangle, so round-number domains do not put roots on cell edges; results inside the rectangle (or on its edge) are
|
|
193
|
+
* kept and results within the tolerance are merged. `progress.rows` counts grid rows searched.
|
|
194
|
+
*/
|
|
195
|
+
export function findZerosAndPoles(
|
|
196
|
+
f: ComplexFn,
|
|
197
|
+
reDomain: Domain,
|
|
198
|
+
imDomain: Domain,
|
|
199
|
+
options: ZerosPolesOptions = {},
|
|
200
|
+
): ZerosPoles {
|
|
201
|
+
const grid = options.grid ?? 24;
|
|
202
|
+
assertCount("grid", grid, 1);
|
|
203
|
+
assertRange("re domain", reDomain);
|
|
204
|
+
assertRange("im domain", imDomain);
|
|
205
|
+
const size = Math.max(reDomain.max - reDomain.min, imDomain.max - imDomain.min);
|
|
206
|
+
const tol = (options.tolerance ?? 1e-10) * size;
|
|
207
|
+
const meter = new Meter(options, options);
|
|
208
|
+
const dx = (reDomain.max - reDomain.min) / grid;
|
|
209
|
+
const dy = (imDomain.max - imDomain.min) / grid;
|
|
210
|
+
const found: ZeroOrPole[] = [];
|
|
211
|
+
|
|
212
|
+
const argAt = (x: number, y: number): number => {
|
|
213
|
+
const w = f(x, y);
|
|
214
|
+
if (w[0] === 0 && w[1] === 0) return Number.NaN; // an exact zero has no argument
|
|
215
|
+
return Math.atan2(w[1], w[0]);
|
|
216
|
+
};
|
|
217
|
+
// winding of f along segment p->q, subdividing until the phase moves < 1 rad per piece
|
|
218
|
+
const segment = (x0: number, y0: number, a0: number, x1: number, y1: number, a1: number, depth: number): number => {
|
|
219
|
+
const d = wrap(a1 - a0);
|
|
220
|
+
// a minimum depth keeps a high-order root from aliasing a whole turn (3 theta moving 2 pi in one piece)
|
|
221
|
+
if ((depth >= 4 && Math.abs(d) < 1) || depth >= 14) return d;
|
|
222
|
+
const xm = (x0 + x1) / 2;
|
|
223
|
+
const ym = (y0 + y1) / 2;
|
|
224
|
+
const am = argAt(xm, ym);
|
|
225
|
+
return segment(x0, y0, a0, xm, ym, am, depth + 1) + segment(xm, ym, am, x1, y1, a1, depth + 1);
|
|
226
|
+
};
|
|
227
|
+
const winding = (x0: number, y0: number, x1: number, y1: number): number => {
|
|
228
|
+
const pts: Array<[number, number]> = [
|
|
229
|
+
[x0, y0],
|
|
230
|
+
[x1, y0],
|
|
231
|
+
[x1, y1],
|
|
232
|
+
[x0, y1],
|
|
233
|
+
[x0, y0],
|
|
234
|
+
];
|
|
235
|
+
const args = pts.map(([x, y]) => argAt(x, y));
|
|
236
|
+
if (args.some((a) => !Number.isFinite(a))) return Number.NaN;
|
|
237
|
+
let total = 0;
|
|
238
|
+
for (let k = 0; k < 4; k++) {
|
|
239
|
+
const p = pts[k] as [number, number];
|
|
240
|
+
const q = pts[k + 1] as [number, number];
|
|
241
|
+
total += segment(p[0], p[1], args[k] as number, q[0], q[1], args[k + 1] as number, 0);
|
|
242
|
+
}
|
|
243
|
+
return Math.round(total / (2 * Math.PI));
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const newton = (
|
|
247
|
+
x: number,
|
|
248
|
+
y: number,
|
|
249
|
+
kind: "zero" | "pole",
|
|
250
|
+
order: number,
|
|
251
|
+
box: [number, number, number, number],
|
|
252
|
+
) => {
|
|
253
|
+
const g = (a: number, b: number): [number, number] => {
|
|
254
|
+
const w = f(a, b);
|
|
255
|
+
if (kind === "zero") return [w[0], w[1]];
|
|
256
|
+
const m = w[0] * w[0] + w[1] * w[1];
|
|
257
|
+
return [w[0] / m, -w[1] / m]; // 1/f
|
|
258
|
+
};
|
|
259
|
+
let zx = x;
|
|
260
|
+
let zy = y;
|
|
261
|
+
for (let it = 0; it < 60; it++) {
|
|
262
|
+
const [gr, gi] = g(zx, zy);
|
|
263
|
+
if (!Number.isFinite(gr) || !Number.isFinite(gi)) return undefined;
|
|
264
|
+
const h = 1e-7 * Math.max(1, Math.abs(zx), Math.abs(zy));
|
|
265
|
+
const [ar, ai] = g(zx + h, zy);
|
|
266
|
+
const dr = (ar - gr) / h; // derivative of an analytic function = d/dx
|
|
267
|
+
const di = (ai - gi) / h;
|
|
268
|
+
const den = dr * dr + di * di;
|
|
269
|
+
if (!(den > 0) || !Number.isFinite(den)) return undefined;
|
|
270
|
+
// step = order * g / g'
|
|
271
|
+
const sr = (order * (gr * dr + gi * di)) / den;
|
|
272
|
+
const si = (order * (gi * dr - gr * di)) / den;
|
|
273
|
+
zx -= sr;
|
|
274
|
+
zy -= si;
|
|
275
|
+
if (Math.hypot(sr, si) < tol) {
|
|
276
|
+
const pad = 0.5 * Math.max(dx, dy);
|
|
277
|
+
if (zx < box[0] - pad || zx > box[1] + pad || zy < box[2] - pad || zy > box[3] + pad) return undefined;
|
|
278
|
+
return [zx, zy] as const;
|
|
279
|
+
}
|
|
280
|
+
if (!Number.isFinite(zx) || !Number.isFinite(zy)) return undefined;
|
|
281
|
+
}
|
|
282
|
+
return undefined;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// The search grid is shifted by an irrational fraction of a cell and extended by one cell on each side, so
|
|
286
|
+
// roots and poles on a round-number domain (z^3 - z on [-2, 2]) sit inside cells, not on their edges or
|
|
287
|
+
// corners. Candidates outside the requested rectangle are discarded at the end.
|
|
288
|
+
const OFFSET = 0.3819660112501051;
|
|
289
|
+
const gx0 = reDomain.min - (1 - OFFSET) * dx;
|
|
290
|
+
const gy0 = imDomain.min - (1 - OFFSET) * dy;
|
|
291
|
+
const cells = grid + 1;
|
|
292
|
+
const slack = 1e-9 * size;
|
|
293
|
+
const insideRect = (x: number, y: number) =>
|
|
294
|
+
x >= reDomain.min - slack && x <= reDomain.max + slack && y >= imDomain.min - slack && y <= imDomain.max + slack;
|
|
295
|
+
let rows = 0;
|
|
296
|
+
let reason: "cells" | "time" | undefined;
|
|
297
|
+
for (let j = 0; j < cells; j++) {
|
|
298
|
+
reason = meter.check(cells * 4);
|
|
299
|
+
if (reason) break;
|
|
300
|
+
for (let i = 0; i < cells; i++) {
|
|
301
|
+
const x0 = gx0 + i * dx;
|
|
302
|
+
const y0 = gy0 + j * dy;
|
|
303
|
+
let w = winding(x0, y0, x0 + dx, y0 + dy);
|
|
304
|
+
let ex = 0;
|
|
305
|
+
if (Number.isNaN(w)) {
|
|
306
|
+
// f is zero or non-finite on this cell's boundary (an exact root/pole on an edge): retry slightly inflated
|
|
307
|
+
ex = 0.0123 * dx;
|
|
308
|
+
w = winding(x0 - ex, y0 - 0.0123 * dy, x0 + dx + ex, y0 + dy + 0.0123 * dy);
|
|
309
|
+
}
|
|
310
|
+
if (!Number.isFinite(w) || w === 0) continue;
|
|
311
|
+
const kind = w > 0 ? "zero" : "pole";
|
|
312
|
+
const order = Math.abs(w);
|
|
313
|
+
const cx = x0 + dx / 2;
|
|
314
|
+
const cy = y0 + dy / 2;
|
|
315
|
+
const refined = newton(cx, cy, kind, order, [x0, x0 + dx, y0, y0 + dy]);
|
|
316
|
+
const [rx, ry] = refined ?? [cx, cy];
|
|
317
|
+
if (!insideRect(rx, ry)) continue;
|
|
318
|
+
if (found.some((p) => p.kind === kind && Math.hypot(p.re - rx, p.im - ry) < Math.max(tol * 1e3, 1e-7 * size)))
|
|
319
|
+
continue;
|
|
320
|
+
found.push({ re: rx, im: ry, kind, order, refined: refined !== undefined });
|
|
321
|
+
}
|
|
322
|
+
meter.spend(cells * 4);
|
|
323
|
+
rows++;
|
|
324
|
+
}
|
|
325
|
+
found.sort((a, b) => a.re - b.re || a.im - b.im);
|
|
326
|
+
return { items: found, progress: progress(rows, cells, reason) };
|
|
327
|
+
}
|