@johnhenry/math-sample 0.0.0-stage → 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 +244 -2
- 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 +38 -0
- package/dist/expr.d.ts.map +1 -0
- package/dist/expr.js +264 -0
- package/dist/expr.js.map +1 -0
- 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 +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- 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/sample.d.ts +114 -0
- package/dist/sample.d.ts.map +1 -0
- package/dist/sample.js +308 -0
- package/dist/sample.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/dist/svg.d.ts +37 -0
- package/dist/svg.d.ts.map +1 -0
- package/dist/svg.js +82 -0
- package/dist/svg.js.map +1 -0
- package/dist/ticks.d.ts +17 -0
- package/dist/ticks.d.ts.map +1 -0
- package/dist/ticks.js +47 -0
- package/dist/ticks.js.map +1 -0
- package/dist/viewport.d.ts +59 -0
- package/dist/viewport.d.ts.map +1 -0
- package/dist/viewport.js +111 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +70 -4
- package/src/budget.ts +72 -0
- package/src/complex.ts +327 -0
- package/src/expr.ts +283 -0
- package/src/field2d.ts +259 -0
- package/src/index.ts +112 -0
- package/src/mesh.ts +376 -0
- package/src/ode.ts +633 -0
- package/src/sample.ts +391 -0
- package/src/series.ts +209 -0
- package/src/svg.ts +124 -0
- package/src/ticks.ts +46 -0
- package/src/viewport.ts +150 -0
package/src/expr.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { type Expr, Symbolic } from "@johnhenry/math";
|
|
2
|
+
import type { Fn1 } from "./sample.ts";
|
|
3
|
+
|
|
4
|
+
export interface CompileOptions {
|
|
5
|
+
/** The free variable that becomes the argument. Default `"x"`. */
|
|
6
|
+
variable?: string;
|
|
7
|
+
/** Fixed values for every other free variable (slider values, say). */
|
|
8
|
+
params?: Record<string, number>;
|
|
9
|
+
/**
|
|
10
|
+
* Read juxtaposition as multiplication in source text (`2x`, `3sin(x)`, `(x+1)(x-1)`), the way people
|
|
11
|
+
* type maths. Default `false`. Needs `@johnhenry/math` 0.1.0 or later; multi-letter variable names are
|
|
12
|
+
* unavailable when it is on (see `ParseOptions` in `@johnhenry/math`). Ignored when `expr` is already a parsed `Expr`.
|
|
13
|
+
*/
|
|
14
|
+
implicitMultiplication?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compile an `@johnhenry/math` expression (source text or a parsed `Expr`)
|
|
19
|
+
* into a plain `(x) => number` the samplers take. The AST is walked once; the
|
|
20
|
+
* closure is cheap to call thousands of times. This is the only module that
|
|
21
|
+
* needs `@johnhenry/math` (an optional peer dependency), so a Worker can
|
|
22
|
+
* receive an expression string plus params, compile it here, and sample.
|
|
23
|
+
*
|
|
24
|
+
* Source text uses `@johnhenry/math`'s grammar: write `2*x`, or pass
|
|
25
|
+
* `implicitMultiplication: true` to accept `2x`.
|
|
26
|
+
*/
|
|
27
|
+
export function compileExpression(expr: Expr | string, options: CompileOptions = {}): Fn1 {
|
|
28
|
+
const variable = options.variable ?? "x";
|
|
29
|
+
const tree =
|
|
30
|
+
typeof expr === "string"
|
|
31
|
+
? Symbolic.parse(expr, { implicitMultiplication: options.implicitMultiplication === true })
|
|
32
|
+
: expr;
|
|
33
|
+
const compiled = Symbolic.compile(tree);
|
|
34
|
+
const env: Record<string, number> = { ...options.params, [variable]: 0 };
|
|
35
|
+
return (x) => {
|
|
36
|
+
env[variable] = x;
|
|
37
|
+
return compiled(env);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Jet = Float64Array;
|
|
42
|
+
|
|
43
|
+
class JetUnsupported extends Error {}
|
|
44
|
+
|
|
45
|
+
function jmul(a: Jet, b: Jet): Jet {
|
|
46
|
+
const n = a.length;
|
|
47
|
+
const c = new Float64Array(n);
|
|
48
|
+
for (let i = 0; i < n; i++) {
|
|
49
|
+
const ai = a[i] as number;
|
|
50
|
+
if (ai === 0) continue;
|
|
51
|
+
for (let j = 0; i + j < n; j++) c[i + j] = (c[i + j] as number) + ai * (b[j] as number);
|
|
52
|
+
}
|
|
53
|
+
return c;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function jdiv(a: Jet, b: Jet): Jet {
|
|
57
|
+
const n = a.length;
|
|
58
|
+
const c = new Float64Array(n);
|
|
59
|
+
for (let k = 0; k < n; k++) {
|
|
60
|
+
let acc = a[k] as number;
|
|
61
|
+
for (let j = 1; j <= k; j++) acc -= (b[j] as number) * (c[k - j] as number);
|
|
62
|
+
c[k] = acc / (b[0] as number);
|
|
63
|
+
}
|
|
64
|
+
return c;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const jconst = (v: number, n: number): Jet => {
|
|
68
|
+
const j = new Float64Array(n);
|
|
69
|
+
j[0] = v;
|
|
70
|
+
return j;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function jexp(a: Jet): Jet {
|
|
74
|
+
const n = a.length;
|
|
75
|
+
const e = new Float64Array(n);
|
|
76
|
+
e[0] = Math.exp(a[0] as number);
|
|
77
|
+
for (let k = 1; k < n; k++) {
|
|
78
|
+
let acc = 0;
|
|
79
|
+
for (let j = 1; j <= k; j++) acc += j * (a[j] as number) * (e[k - j] as number);
|
|
80
|
+
e[k] = acc / k;
|
|
81
|
+
}
|
|
82
|
+
return e;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function jln(a: Jet): Jet {
|
|
86
|
+
const n = a.length;
|
|
87
|
+
const l = new Float64Array(n);
|
|
88
|
+
l[0] = Math.log(a[0] as number);
|
|
89
|
+
for (let k = 1; k < n; k++) {
|
|
90
|
+
let acc = 0;
|
|
91
|
+
for (let j = 1; j < k; j++) acc += j * (l[j] as number) * (a[k - j] as number);
|
|
92
|
+
l[k] = ((a[k] as number) - acc / k) / (a[0] as number);
|
|
93
|
+
}
|
|
94
|
+
return l;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** sin/cos (hyperbolic = true for sinh/cosh) of a jet, together. */
|
|
98
|
+
function jsincos(a: Jet, hyperbolic: boolean): [Jet, Jet] {
|
|
99
|
+
const n = a.length;
|
|
100
|
+
const s = new Float64Array(n);
|
|
101
|
+
const c = new Float64Array(n);
|
|
102
|
+
s[0] = hyperbolic ? Math.sinh(a[0] as number) : Math.sin(a[0] as number);
|
|
103
|
+
c[0] = hyperbolic ? Math.cosh(a[0] as number) : Math.cos(a[0] as number);
|
|
104
|
+
for (let k = 1; k < n; k++) {
|
|
105
|
+
let ss = 0;
|
|
106
|
+
let cc = 0;
|
|
107
|
+
for (let j = 1; j <= k; j++) {
|
|
108
|
+
ss += j * (a[j] as number) * (c[k - j] as number);
|
|
109
|
+
cc += j * (a[j] as number) * (s[k - j] as number);
|
|
110
|
+
}
|
|
111
|
+
s[k] = ss / k;
|
|
112
|
+
c[k] = (hyperbolic ? cc : -cc) / k;
|
|
113
|
+
}
|
|
114
|
+
return [s, c];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function jpowReal(a: Jet, p: number): Jet {
|
|
118
|
+
const n = a.length;
|
|
119
|
+
const f = new Float64Array(n);
|
|
120
|
+
const a0 = a[0] as number;
|
|
121
|
+
f[0] = a0 ** p;
|
|
122
|
+
for (let k = 1; k < n; k++) {
|
|
123
|
+
let acc = 0;
|
|
124
|
+
for (let j = 1; j <= k; j++) acc += (p * j - (k - j)) * (a[j] as number) * (f[k - j] as number);
|
|
125
|
+
f[k] = acc / (k * a0);
|
|
126
|
+
}
|
|
127
|
+
return f;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function jpowInt(a: Jet, m: number): Jet {
|
|
131
|
+
let r = jconst(1, a.length);
|
|
132
|
+
let base = a;
|
|
133
|
+
let e = Math.abs(m);
|
|
134
|
+
while (e > 0) {
|
|
135
|
+
if (e & 1) r = jmul(r, base);
|
|
136
|
+
base = jmul(base, base);
|
|
137
|
+
e = Math.floor(e / 2);
|
|
138
|
+
}
|
|
139
|
+
return m < 0 ? jdiv(jconst(1, a.length), r) : r;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Integrate a jet's derivative: given g = f', return f with f(0) = f0. */
|
|
143
|
+
function jantiderivative(g: Jet, f0: number): Jet {
|
|
144
|
+
const n = g.length;
|
|
145
|
+
const f = new Float64Array(n);
|
|
146
|
+
f[0] = f0;
|
|
147
|
+
for (let k = 1; k < n; k++) f[k] = (g[k - 1] as number) / k;
|
|
148
|
+
return f;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function jderivative(a: Jet): Jet {
|
|
152
|
+
const n = a.length;
|
|
153
|
+
const d = new Float64Array(n);
|
|
154
|
+
for (let k = 0; k + 1 < n; k++) d[k] = (k + 1) * (a[k + 1] as number);
|
|
155
|
+
return d;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function jfunc(name: string, a: Jet): Jet {
|
|
159
|
+
const n = a.length;
|
|
160
|
+
const one = jconst(1, n);
|
|
161
|
+
switch (name) {
|
|
162
|
+
case "exp":
|
|
163
|
+
return jexp(a);
|
|
164
|
+
case "ln":
|
|
165
|
+
return jln(a);
|
|
166
|
+
case "sqrt":
|
|
167
|
+
return jpowReal(a, 0.5);
|
|
168
|
+
case "cbrt":
|
|
169
|
+
return jpowReal(a, 1 / 3);
|
|
170
|
+
case "sin":
|
|
171
|
+
return jsincos(a, false)[0];
|
|
172
|
+
case "cos":
|
|
173
|
+
return jsincos(a, false)[1];
|
|
174
|
+
case "tan": {
|
|
175
|
+
const [s, c] = jsincos(a, false);
|
|
176
|
+
return jdiv(s, c);
|
|
177
|
+
}
|
|
178
|
+
case "sinh":
|
|
179
|
+
return jsincos(a, true)[0];
|
|
180
|
+
case "cosh":
|
|
181
|
+
return jsincos(a, true)[1];
|
|
182
|
+
case "tanh": {
|
|
183
|
+
const [s, c] = jsincos(a, true);
|
|
184
|
+
return jdiv(s, c);
|
|
185
|
+
}
|
|
186
|
+
case "atan": {
|
|
187
|
+
const d = jdiv(jderivative(a), jplus(one, jmul(a, a), 1));
|
|
188
|
+
return jantiderivative(d, Math.atan(a[0] as number));
|
|
189
|
+
}
|
|
190
|
+
case "asin":
|
|
191
|
+
case "acos": {
|
|
192
|
+
const inv = jpowReal(jplus(one, jmul(a, a), -1), -0.5);
|
|
193
|
+
const d = jmul(jderivative(a), inv);
|
|
194
|
+
return name === "asin"
|
|
195
|
+
? jantiderivative(d, Math.asin(a[0] as number))
|
|
196
|
+
: jantiderivative(d.map((v) => -v) as Jet, Math.acos(a[0] as number));
|
|
197
|
+
}
|
|
198
|
+
default:
|
|
199
|
+
throw new JetUnsupported(name);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function jplus(a: Jet, b: Jet, sign: 1 | -1): Jet {
|
|
204
|
+
const c = new Float64Array(a.length);
|
|
205
|
+
for (let k = 0; k < a.length; k++) c[k] = (a[k] as number) + sign * (b[k] as number);
|
|
206
|
+
return c;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function evalJet(e: Expr, variable: string, x0: number, params: Record<string, number>, n: number): Jet {
|
|
210
|
+
const go = (t: Expr): Jet => {
|
|
211
|
+
switch (t.type) {
|
|
212
|
+
case "const":
|
|
213
|
+
return jconst(t.value, n);
|
|
214
|
+
case "var": {
|
|
215
|
+
if (t.name === variable) {
|
|
216
|
+
const j = jconst(x0, n);
|
|
217
|
+
if (n > 1) j[1] = 1;
|
|
218
|
+
return j;
|
|
219
|
+
}
|
|
220
|
+
return jconst(params[t.name] ?? Number.NaN, n);
|
|
221
|
+
}
|
|
222
|
+
case "neg":
|
|
223
|
+
return go(t.arg).map((v) => -v) as Jet;
|
|
224
|
+
case "add":
|
|
225
|
+
return jplus(go(t.left), go(t.right), 1);
|
|
226
|
+
case "sub":
|
|
227
|
+
return jplus(go(t.left), go(t.right), -1);
|
|
228
|
+
case "mul":
|
|
229
|
+
return jmul(go(t.left), go(t.right));
|
|
230
|
+
case "div":
|
|
231
|
+
return jdiv(go(t.left), go(t.right));
|
|
232
|
+
case "func":
|
|
233
|
+
return jfunc(t.name, go(t.arg));
|
|
234
|
+
case "pow": {
|
|
235
|
+
const b = go(t.base);
|
|
236
|
+
const ex = go(t.exp);
|
|
237
|
+
const constExp = ex.every((v, k) => k === 0 || v === 0);
|
|
238
|
+
if (constExp) {
|
|
239
|
+
const p = ex[0] as number;
|
|
240
|
+
if (Number.isInteger(p) && Math.abs(p) <= 256) return jpowInt(b, p);
|
|
241
|
+
return jpowReal(b, p);
|
|
242
|
+
}
|
|
243
|
+
return jexp(jmul(ex, jln(b)));
|
|
244
|
+
}
|
|
245
|
+
default:
|
|
246
|
+
throw new JetUnsupported(t.type);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
return go(e);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Taylor coefficients `a_k = f^(k)(x0) / k!` for `k = 0..order` of an expression. They are computed by truncated
|
|
254
|
+
* power-series ("jet") arithmetic, O(order^2) per node, so order 12 of `sin(x)*exp(x)/(1+x^2)` takes well under a
|
|
255
|
+
* millisecond. Supported: `+ - * /`, powers (integer or real exponent, or a variable exponent), and `exp ln sqrt cbrt sin
|
|
256
|
+
* cos tan sinh cosh tanh atan asin acos`. Any other node falls back to repeated symbolic differentiation with
|
|
257
|
+
* simplification after every derivative, which grows quickly: keep `order` at or below 8 there. A coefficient that is
|
|
258
|
+
* not finite at `x0` (a pole, `ln(0)`) is `NaN` or `Infinity`, and the partial sums built on it are gaps.
|
|
259
|
+
*/
|
|
260
|
+
export function taylorCoefficients(
|
|
261
|
+
expr: Expr | string,
|
|
262
|
+
x0: number,
|
|
263
|
+
order: number,
|
|
264
|
+
options: { variable?: string; params?: Record<string, number> } = {},
|
|
265
|
+
): Float64Array {
|
|
266
|
+
if (!Number.isInteger(order) || order < 0) throw new RangeError("order must be a non-negative integer");
|
|
267
|
+
const variable = options.variable ?? "x";
|
|
268
|
+
const tree: Expr = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
269
|
+
try {
|
|
270
|
+
return evalJet(tree, variable, x0, options.params ?? {}, order + 1);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
if (!(err instanceof JetUnsupported)) throw err;
|
|
273
|
+
}
|
|
274
|
+
let term: Expr = tree;
|
|
275
|
+
const out = new Float64Array(order + 1);
|
|
276
|
+
let factorial = 1;
|
|
277
|
+
for (let k = 0; k <= order; k++) {
|
|
278
|
+
if (k > 0) factorial *= k;
|
|
279
|
+
out[k] = Symbolic.evaluate(term, { ...options.params, [variable]: x0 }) / factorial;
|
|
280
|
+
if (k < order) term = Symbolic.simplify(Symbolic.differentiate(term, variable));
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
}
|
package/src/field2d.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { assertCount, assertRange, type Budget, type Cancellable, Meter, type Progress, progress } from "./budget.ts";
|
|
2
|
+
import type { Domain, Samples } from "./sample.ts";
|
|
3
|
+
|
|
4
|
+
export type Fn2 = (x: number, y: number) => number;
|
|
5
|
+
|
|
6
|
+
export interface Grid2Options extends Budget, Cancellable {
|
|
7
|
+
/** Cells per side of the sampling grid (`grid + 1` nodes per side). Default 128. */
|
|
8
|
+
grid?: number | [number, number];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ImplicitCurve {
|
|
12
|
+
/**
|
|
13
|
+
* Segments as rows: each segment is two rows (its endpoints) followed by one `NaN` separator row, so
|
|
14
|
+
* `Samples` gap rules hold and `runsOf` yields one run per segment (join them if you wish).
|
|
15
|
+
*/
|
|
16
|
+
segments: Samples;
|
|
17
|
+
segmentCount: number;
|
|
18
|
+
progress: Progress;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function dims(g: number | [number, number] | undefined): [number, number] {
|
|
22
|
+
const [nx, ny] = typeof g === "number" ? [g, g] : (g ?? [128, 128]);
|
|
23
|
+
assertCount("grid", nx, 1);
|
|
24
|
+
assertCount("grid", ny, 1);
|
|
25
|
+
return [nx, ny];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function marching(
|
|
29
|
+
f: Fn2,
|
|
30
|
+
xDomain: Domain,
|
|
31
|
+
yDomain: Domain,
|
|
32
|
+
levels: number[],
|
|
33
|
+
options: Grid2Options,
|
|
34
|
+
): { segs: number[][]; progress: Progress } {
|
|
35
|
+
const [nx, ny] = dims(options.grid);
|
|
36
|
+
assertRange("x domain", xDomain);
|
|
37
|
+
assertRange("y domain", yDomain);
|
|
38
|
+
const meter = new Meter(options, options);
|
|
39
|
+
const hx = (xDomain.max - xDomain.min) / nx;
|
|
40
|
+
const hy = (yDomain.max - yDomain.min) / ny;
|
|
41
|
+
const segs: number[][] = levels.map(() => []);
|
|
42
|
+
let prev = new Float64Array(nx + 1);
|
|
43
|
+
let cur = new Float64Array(nx + 1);
|
|
44
|
+
const row = (j: number, out: Float64Array) => {
|
|
45
|
+
const y = yDomain.min + j * hy;
|
|
46
|
+
for (let i = 0; i <= nx; i++) out[i] = f(xDomain.min + i * hx, y);
|
|
47
|
+
};
|
|
48
|
+
let reason: "cells" | "time" | undefined;
|
|
49
|
+
row(0, prev);
|
|
50
|
+
meter.spend(nx + 1);
|
|
51
|
+
let done = 0;
|
|
52
|
+
for (let j = 0; j < ny; j++) {
|
|
53
|
+
reason = meter.check(nx + 1);
|
|
54
|
+
if (reason) break;
|
|
55
|
+
row(j + 1, cur);
|
|
56
|
+
meter.spend(nx + 1);
|
|
57
|
+
const y0 = yDomain.min + j * hy;
|
|
58
|
+
for (let i = 0; i < nx; i++) {
|
|
59
|
+
const x0 = xDomain.min + i * hx;
|
|
60
|
+
const v = [prev[i], prev[i + 1], cur[i + 1], cur[i]] as number[]; // corners ccw from bottom-left
|
|
61
|
+
if (!v.every(Number.isFinite)) continue; // holes: no segments through undefined cells
|
|
62
|
+
const cx = [x0, x0 + hx, x0 + hx, x0];
|
|
63
|
+
const cy = [y0, y0, y0 + hy, y0 + hy];
|
|
64
|
+
levels.forEach((level, li) => {
|
|
65
|
+
const s = v.map((q) => q - level);
|
|
66
|
+
// edge e joins corner e and corner e+1
|
|
67
|
+
const cross = (e: number): [number, number] | undefined => {
|
|
68
|
+
const a = s[e] as number;
|
|
69
|
+
const b = s[(e + 1) % 4] as number;
|
|
70
|
+
if (a < 0 === b < 0) return undefined; // a === 0 counts as >= 0 on the positive side
|
|
71
|
+
const t = a / (a - b);
|
|
72
|
+
const ea = e;
|
|
73
|
+
const eb = (e + 1) % 4;
|
|
74
|
+
return [
|
|
75
|
+
(cx[ea] as number) + t * ((cx[eb] as number) - (cx[ea] as number)),
|
|
76
|
+
(cy[ea] as number) + t * ((cy[eb] as number) - (cy[ea] as number)),
|
|
77
|
+
];
|
|
78
|
+
};
|
|
79
|
+
const pts: Array<[number, number] | undefined> = [0, 1, 2, 3].map(cross);
|
|
80
|
+
const idx = [0, 1, 2, 3].filter((e) => pts[e]);
|
|
81
|
+
const out = segs[li] as number[];
|
|
82
|
+
const emit = (p: [number, number], q: [number, number]) => out.push(p[0], p[1], q[0], q[1]);
|
|
83
|
+
if (idx.length === 2)
|
|
84
|
+
emit(pts[idx[0] as number] as [number, number], pts[idx[1] as number] as [number, number]);
|
|
85
|
+
else if (idx.length === 4) {
|
|
86
|
+
// saddle: the cell centre decides which corners are connected
|
|
87
|
+
const centre = ((s[0] as number) + (s[1] as number) + (s[2] as number) + (s[3] as number)) / 4;
|
|
88
|
+
const p = pts as Array<[number, number]>;
|
|
89
|
+
const bottomLeftPositive = (s[0] as number) >= 0;
|
|
90
|
+
const joinsCorner0 = centre >= 0 === bottomLeftPositive; // centre agrees with corner 0: corners 0 and 2 are linked through the middle, so the curve cuts off corners 1 and 3
|
|
91
|
+
if (joinsCorner0) {
|
|
92
|
+
emit(p[0] as [number, number], p[1] as [number, number]);
|
|
93
|
+
emit(p[2] as [number, number], p[3] as [number, number]);
|
|
94
|
+
} else {
|
|
95
|
+
emit(p[3] as [number, number], p[0] as [number, number]);
|
|
96
|
+
emit(p[1] as [number, number], p[2] as [number, number]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
[prev, cur] = [cur, prev];
|
|
102
|
+
done++;
|
|
103
|
+
}
|
|
104
|
+
return { segs, progress: progress(done, ny, reason) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function toSamples(flat: number[]): Samples {
|
|
108
|
+
const n = flat.length / 4;
|
|
109
|
+
const x = new Float64Array(n === 0 ? 0 : n * 3 - 1);
|
|
110
|
+
const y = new Float64Array(x.length);
|
|
111
|
+
for (let s = 0; s < n; s++) {
|
|
112
|
+
const o = s * 3;
|
|
113
|
+
x[o] = flat[4 * s] as number;
|
|
114
|
+
y[o] = flat[4 * s + 1] as number;
|
|
115
|
+
x[o + 1] = flat[4 * s + 2] as number;
|
|
116
|
+
y[o + 1] = flat[4 * s + 3] as number;
|
|
117
|
+
if (s < n - 1) {
|
|
118
|
+
x[o + 2] = Number.NaN;
|
|
119
|
+
y[o + 2] = Number.NaN;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { x, y };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The curve `f(x, y) = level` (default 0) by marching squares on a regular grid. A saddle cell is joined by its
|
|
127
|
+
* centre value. Cells with a non-finite corner are skipped (a hole, not a spike). With a budget,
|
|
128
|
+
* `progress.rows` counts grid rows finished.
|
|
129
|
+
*/
|
|
130
|
+
export function sampleImplicit(
|
|
131
|
+
f: Fn2,
|
|
132
|
+
xDomain: Domain,
|
|
133
|
+
yDomain: Domain,
|
|
134
|
+
options: Grid2Options & { level?: number } = {},
|
|
135
|
+
): ImplicitCurve {
|
|
136
|
+
const { segs, progress: p } = marching(f, xDomain, yDomain, [options.level ?? 0], options);
|
|
137
|
+
const flat = segs[0] as number[];
|
|
138
|
+
return { segments: toSamples(flat), segmentCount: flat.length / 4, progress: p };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface Contours {
|
|
142
|
+
levels: number[];
|
|
143
|
+
/** One entry per level, in `levels` order. */
|
|
144
|
+
curves: Array<{ level: number; segments: Samples; segmentCount: number }>;
|
|
145
|
+
progress: Progress;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Contour lines of `f` at the given `levels`, or `count` (default 9) levels evenly spread strictly between the
|
|
150
|
+
* grid's least and greatest values. One pass over the grid for all levels.
|
|
151
|
+
*/
|
|
152
|
+
export function sampleContours(
|
|
153
|
+
f: Fn2,
|
|
154
|
+
xDomain: Domain,
|
|
155
|
+
yDomain: Domain,
|
|
156
|
+
options: Grid2Options & { levels?: number[]; count?: number } = {},
|
|
157
|
+
): Contours {
|
|
158
|
+
let levels = options.levels;
|
|
159
|
+
if (!levels) {
|
|
160
|
+
const [nx, ny] = dims(options.grid);
|
|
161
|
+
let lo = Infinity;
|
|
162
|
+
let hi = -Infinity;
|
|
163
|
+
for (let j = 0; j <= ny; j++)
|
|
164
|
+
for (let i = 0; i <= nx; i++) {
|
|
165
|
+
const v = f(
|
|
166
|
+
xDomain.min + ((xDomain.max - xDomain.min) * i) / nx,
|
|
167
|
+
yDomain.min + ((yDomain.max - yDomain.min) * j) / ny,
|
|
168
|
+
);
|
|
169
|
+
if (Number.isFinite(v)) {
|
|
170
|
+
if (v < lo) lo = v;
|
|
171
|
+
if (v > hi) hi = v;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const count = options.count ?? 9;
|
|
175
|
+
levels = lo < hi ? Array.from({ length: count }, (_, k) => lo + ((hi - lo) * (k + 1)) / (count + 1)) : [];
|
|
176
|
+
}
|
|
177
|
+
const { segs, progress: p } = marching(f, xDomain, yDomain, levels, options);
|
|
178
|
+
return {
|
|
179
|
+
levels,
|
|
180
|
+
curves: levels.map((level, k) => ({
|
|
181
|
+
level,
|
|
182
|
+
segments: toSamples(segs[k] as number[]),
|
|
183
|
+
segmentCount: (segs[k] as number[]).length / 4,
|
|
184
|
+
})),
|
|
185
|
+
progress: p,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface GradientField {
|
|
190
|
+
columns: number;
|
|
191
|
+
rows: number;
|
|
192
|
+
/** Arrow tails. */
|
|
193
|
+
x: Float64Array;
|
|
194
|
+
y: Float64Array;
|
|
195
|
+
/** Components; `NaN` where either component is non-finite. */
|
|
196
|
+
u: Float64Array;
|
|
197
|
+
v: Float64Array;
|
|
198
|
+
magnitude: Float64Array;
|
|
199
|
+
maxMagnitude: number;
|
|
200
|
+
progress: Progress;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Sample the vector field `(fx, fy)` (e.g. the two partial derivatives of a scalar field) on a grid including the edges. */
|
|
204
|
+
export function sampleGradient(
|
|
205
|
+
fx: Fn2,
|
|
206
|
+
fy: Fn2,
|
|
207
|
+
xDomain: Domain,
|
|
208
|
+
yDomain: Domain,
|
|
209
|
+
options: Grid2Options = {},
|
|
210
|
+
): GradientField {
|
|
211
|
+
const [cols, rws] = typeof options.grid === "number" ? [options.grid, options.grid] : (options.grid ?? [16, 16]);
|
|
212
|
+
assertCount("grid", cols);
|
|
213
|
+
assertCount("grid", rws);
|
|
214
|
+
assertRange("x domain", xDomain);
|
|
215
|
+
assertRange("y domain", yDomain);
|
|
216
|
+
const meter = new Meter(options, options);
|
|
217
|
+
const n = cols * rws;
|
|
218
|
+
const X = new Float64Array(n);
|
|
219
|
+
const Y = new Float64Array(n);
|
|
220
|
+
const U = new Float64Array(n).fill(Number.NaN);
|
|
221
|
+
const V = new Float64Array(n).fill(Number.NaN);
|
|
222
|
+
const M = new Float64Array(n).fill(Number.NaN);
|
|
223
|
+
let done = 0;
|
|
224
|
+
let reason: "cells" | "time" | undefined;
|
|
225
|
+
let max = 0;
|
|
226
|
+
for (let j = 0; j < rws; j++) {
|
|
227
|
+
reason = meter.check(cols);
|
|
228
|
+
if (reason) break;
|
|
229
|
+
const y = j === rws - 1 ? yDomain.max : yDomain.min + ((yDomain.max - yDomain.min) * j) / (rws - 1);
|
|
230
|
+
for (let i = 0; i < cols; i++) {
|
|
231
|
+
const x = i === cols - 1 ? xDomain.max : xDomain.min + ((xDomain.max - xDomain.min) * i) / (cols - 1);
|
|
232
|
+
const k = j * cols + i;
|
|
233
|
+
X[k] = x;
|
|
234
|
+
Y[k] = y;
|
|
235
|
+
const a = fx(x, y);
|
|
236
|
+
const b = fy(x, y);
|
|
237
|
+
if (Number.isFinite(a) && Number.isFinite(b)) {
|
|
238
|
+
U[k] = a;
|
|
239
|
+
V[k] = b;
|
|
240
|
+
M[k] = Math.hypot(a, b);
|
|
241
|
+
if (M[k] > max) max = M[k] as number;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
meter.spend(cols);
|
|
245
|
+
done++;
|
|
246
|
+
}
|
|
247
|
+
const m = cols * done;
|
|
248
|
+
return {
|
|
249
|
+
columns: cols,
|
|
250
|
+
rows: done,
|
|
251
|
+
x: X.slice(0, m),
|
|
252
|
+
y: Y.slice(0, m),
|
|
253
|
+
u: U.slice(0, m),
|
|
254
|
+
v: V.slice(0, m),
|
|
255
|
+
magnitude: M.slice(0, m),
|
|
256
|
+
maxMagnitude: max,
|
|
257
|
+
progress: progress(done, rws, reason),
|
|
258
|
+
};
|
|
259
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export type { Budget, Cancellable, Progress } from "./budget.ts";
|
|
2
|
+
export {
|
|
3
|
+
type ComplexFn,
|
|
4
|
+
type ComplexRaster,
|
|
5
|
+
type ComplexRasterOptions,
|
|
6
|
+
type ConformalGridOptions,
|
|
7
|
+
type ConformalLine,
|
|
8
|
+
conformalGrid,
|
|
9
|
+
findZerosAndPoles,
|
|
10
|
+
sampleComplexRaster,
|
|
11
|
+
type ZeroOrPole,
|
|
12
|
+
type ZerosPoles,
|
|
13
|
+
type ZerosPolesOptions,
|
|
14
|
+
} from "./complex.ts";
|
|
15
|
+
export {
|
|
16
|
+
type Contours,
|
|
17
|
+
type GradientField,
|
|
18
|
+
type Grid2Options,
|
|
19
|
+
type ImplicitCurve,
|
|
20
|
+
sampleContours,
|
|
21
|
+
sampleGradient,
|
|
22
|
+
sampleImplicit,
|
|
23
|
+
} from "./field2d.ts";
|
|
24
|
+
export {
|
|
25
|
+
type Curve3,
|
|
26
|
+
type CurveFn3,
|
|
27
|
+
type CurveOptions,
|
|
28
|
+
curve3Runs,
|
|
29
|
+
type Fn2,
|
|
30
|
+
type Mesh,
|
|
31
|
+
type MeshOptions,
|
|
32
|
+
type ParametricSurfaceFn,
|
|
33
|
+
sampleParametricSurface,
|
|
34
|
+
sampleSpaceCurve,
|
|
35
|
+
sampleSurface,
|
|
36
|
+
sampleVectorField3,
|
|
37
|
+
type VectorField3,
|
|
38
|
+
type VectorField3Options,
|
|
39
|
+
type VectorFn3,
|
|
40
|
+
} from "./mesh.ts";
|
|
41
|
+
export {
|
|
42
|
+
type AdaptiveOdeOptions,
|
|
43
|
+
classifyFixedPoint,
|
|
44
|
+
type Field2,
|
|
45
|
+
type FixedPoint,
|
|
46
|
+
type FixedPointType,
|
|
47
|
+
integrateAdaptive,
|
|
48
|
+
integrateRk4,
|
|
49
|
+
jacobian2,
|
|
50
|
+
type OdeOptions,
|
|
51
|
+
type PhasePortrait,
|
|
52
|
+
type PhasePortraitOptions,
|
|
53
|
+
type Rk4Options,
|
|
54
|
+
type Slope,
|
|
55
|
+
type SlopeField,
|
|
56
|
+
type SlopeFieldOptions,
|
|
57
|
+
type SolutionCurveOptions,
|
|
58
|
+
type StopReason,
|
|
59
|
+
type System,
|
|
60
|
+
samplePhasePortrait,
|
|
61
|
+
sampleSlopeField,
|
|
62
|
+
sampleSolutionCurve,
|
|
63
|
+
type Trajectory,
|
|
64
|
+
} from "./ode.ts";
|
|
65
|
+
export {
|
|
66
|
+
type AdaptiveOptions,
|
|
67
|
+
type CancelSignal,
|
|
68
|
+
type Domain,
|
|
69
|
+
type Fn1,
|
|
70
|
+
findCrossings,
|
|
71
|
+
findDiscontinuities,
|
|
72
|
+
findIntersections,
|
|
73
|
+
findRoots,
|
|
74
|
+
type Point,
|
|
75
|
+
type RootOptions,
|
|
76
|
+
runsOf,
|
|
77
|
+
SampleCancelledError,
|
|
78
|
+
type SampleOptions,
|
|
79
|
+
type Samples,
|
|
80
|
+
sampleAdaptive,
|
|
81
|
+
sampleUniform,
|
|
82
|
+
toPoints,
|
|
83
|
+
type YRange,
|
|
84
|
+
} from "./sample.ts";
|
|
85
|
+
export {
|
|
86
|
+
evaluateFourier,
|
|
87
|
+
evaluateTaylor,
|
|
88
|
+
type FourierCoefficients,
|
|
89
|
+
fourierCoefficients,
|
|
90
|
+
type NumericSeries,
|
|
91
|
+
type PartialSumCurves,
|
|
92
|
+
partialSums,
|
|
93
|
+
sampleFourierPartialSums,
|
|
94
|
+
sampleSeriesPartialSums,
|
|
95
|
+
sampleTaylorPartialSums,
|
|
96
|
+
} from "./series.ts";
|
|
97
|
+
export { type AxesOptions, axesToSvg, type PathOptions, samplesToSvgPath, svgDocument, type XY } from "./svg.ts";
|
|
98
|
+
export { formatTick, niceTicks, tickStep } from "./ticks.ts";
|
|
99
|
+
export {
|
|
100
|
+
backingStoreSize,
|
|
101
|
+
fitViewport,
|
|
102
|
+
panViewport,
|
|
103
|
+
pinchZoomFactor,
|
|
104
|
+
toDataX,
|
|
105
|
+
toDataY,
|
|
106
|
+
toScreenX,
|
|
107
|
+
toScreenY,
|
|
108
|
+
type Viewport,
|
|
109
|
+
viewportFromAnchor,
|
|
110
|
+
wheelZoomFactor,
|
|
111
|
+
zoomViewport,
|
|
112
|
+
} from "./viewport.ts";
|