@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/README.md
CHANGED
|
@@ -1,3 +1,245 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @johnhenry/math-sample
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sampling for plots (1D curves, 2D implicit/contour/gradient fields, 3D meshes, complex rasters, ODEs, series), as a library with no DOM in it. Give it a `(x) => number`
|
|
4
|
+
(or an `@johnhenry/math` expression) and a viewport; get back `Float64Array`s
|
|
5
|
+
with the poles and jumps already turned into gaps, plus the viewport maths,
|
|
6
|
+
"nice" ticks and SVG path output a plot needs. Pure ESM, zero runtime
|
|
7
|
+
dependencies, no globals beyond the language: it runs unchanged in a Web
|
|
8
|
+
Worker, so a plotter can send it *sources and a viewport*, and transfer the
|
|
9
|
+
arrays back.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @johnhenry/math-sample
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { sampleAdaptive, samplesToSvgPath, axesToSvg, niceTicks } from "@johnhenry/math-sample";
|
|
17
|
+
import { compileExpression } from "@johnhenry/math-sample/expr"; // needs @johnhenry/math
|
|
18
|
+
|
|
19
|
+
const f = compileExpression("a*sin(x)", { params: { a: 2 } });
|
|
20
|
+
const view = { xMin: -10, xMax: 10, yMin: -6, yMax: 6 };
|
|
21
|
+
|
|
22
|
+
const s = sampleAdaptive(f, { min: view.xMin, max: view.xMax }, 400, {
|
|
23
|
+
visibleYRange: { min: view.yMin, max: view.yMax },
|
|
24
|
+
});
|
|
25
|
+
s.x; s.y; // Float64Array, NaN in y = gap
|
|
26
|
+
samplesToSvgPath(s, view, 600, 300); // "M0.00 150.00 L1.50 ..."
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
### Samples
|
|
32
|
+
|
|
33
|
+
`Samples` is `{ x: Float64Array; y: Float64Array }`. A row with `y = NaN` is a
|
|
34
|
+
gap: do not draw between its neighbours. Gaps are never first, last or
|
|
35
|
+
adjacent to another gap, so `runsOf(samples)` (half-open row ranges) is exactly
|
|
36
|
+
the polylines to draw.
|
|
37
|
+
|
|
38
|
+
| Function | Does |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `sampleUniform(f, domain, n, options?)` | `n` evenly spaced evaluations, both ends included |
|
|
41
|
+
| `sampleAdaptive(f, domain, n, options?)` | the same base grid, with cells bisected where the curve bends |
|
|
42
|
+
| `runsOf(samples)` | `[start, end)` row ranges of consecutive data |
|
|
43
|
+
| `findDiscontinuities(samples)` | `{ before, after }` points either side of every gap |
|
|
44
|
+
| `findCrossings(samples, signedDistance)` | interpolated crossings of a boundary (`y => y` is the x-axis); not across gaps |
|
|
45
|
+
| `findRoots(f, domain, { resolution? })` | sign changes on a grid, polished by bisection; poles rejected |
|
|
46
|
+
| `findIntersections(f, g, domain, opts?)` | roots of `f - g`, each with `y = f(x)` |
|
|
47
|
+
| `toPoints(samples)` | `[x, y \| null][]`, JSON-safe |
|
|
48
|
+
|
|
49
|
+
`f` is any `(x: number) => number`. Domains are `{ min, max }` (finite, `min < max`);
|
|
50
|
+
`n` is an integer >= 2. Bad input throws `RangeError`.
|
|
51
|
+
|
|
52
|
+
**Options (all samplers):**
|
|
53
|
+
|
|
54
|
+
- `visibleYRange: { min, max }`: the y-range on screen. A sample far outside it
|
|
55
|
+
(a pole's finite but enormous shoulders) is treated as a gap, and the curve
|
|
56
|
+
is capped with an interpolated point just inside the edge, so it runs up to
|
|
57
|
+
the border of the plot instead of stopping short. Also sets the default
|
|
58
|
+
tolerance and, for `sampleAdaptive`, enables jump detection (`sampleUniform` does not detect jumps).
|
|
59
|
+
- `signal` (`{ aborted: boolean }`, so any `AbortSignal`) and `isCancelled: () => boolean`:
|
|
60
|
+
checked every 256 evaluations; either one makes the sampler throw
|
|
61
|
+
`SampleCancelledError`. `isCancelled: () => generation !== latest` is the
|
|
62
|
+
generation-counter pattern for pan/zoom.
|
|
63
|
+
|
|
64
|
+
**Options (`sampleAdaptive`):**
|
|
65
|
+
|
|
66
|
+
- `maxDepth` (4): bisections per base cell.
|
|
67
|
+
- `tolerance`: a cell is split while its midpoint is further than this, in y
|
|
68
|
+
units, from the straight line through its ends. Default `1e-4` of the visible
|
|
69
|
+
y span, so zooming in refines more and zooming out less; with no
|
|
70
|
+
`visibleYRange`, `1e-3`.
|
|
71
|
+
- `jumpFraction` (0.5 with `visibleYRange`; off without): a cell still this far
|
|
72
|
+
apart (as a fraction of the y span) at `maxDepth`, whose height has not
|
|
73
|
+
shrunk as it was halved, is a jump (a step, a pole) and becomes a gap. A
|
|
74
|
+
steep slope loses height as it is halved, so it stays connected. `Infinity` turns it off.
|
|
75
|
+
|
|
76
|
+
### Viewport
|
|
77
|
+
|
|
78
|
+
`Viewport` is `{ xMin, xMax, yMin, yMax }`. Everything is unit-agnostic: use CSS
|
|
79
|
+
pixels or device pixels, as long as width and height agree.
|
|
80
|
+
|
|
81
|
+
`toScreenX/toScreenY(v, viewport, size)`, `toDataX/toDataY(s, viewport, size)`,
|
|
82
|
+
`panViewport(v, dxPx, dyPx, w, h)` (content follows the pointer),
|
|
83
|
+
`zoomViewport(v, factor, sx, sy, w, h)` (the data under the cursor stays put),
|
|
84
|
+
`viewportFromAnchor(...)`, `wheelZoomFactor(deltaY, step?)`,
|
|
85
|
+
`pinchZoomFactor(startDist, currentDist)`, `fitViewport(x, y, padding?)`, and
|
|
86
|
+
`backingStoreSize(cssW, cssH, dpr)` which returns `{ width, height, scale }` for
|
|
87
|
+
sizing a canvas: set `canvas.width/height` to those, `ctx.scale(scale, scale)`,
|
|
88
|
+
and draw in CSS pixels. A non-finite or non-positive `dpr` counts as 1.
|
|
89
|
+
|
|
90
|
+
### Ticks
|
|
91
|
+
|
|
92
|
+
`niceTicks(min, max, target = 6)` gives round values (1, 2 or 5 times a power of
|
|
93
|
+
ten) with no float drift (`0.3`, not `0.30000000000000004`). `tickStep` is the
|
|
94
|
+
step it chose, and `formatTick(v, step)` labels with matching decimals.
|
|
95
|
+
Degenerate or non-finite ranges give `[]`.
|
|
96
|
+
|
|
97
|
+
### SVG
|
|
98
|
+
|
|
99
|
+
`samplesToSvgPath(samples, viewport, w, h, { digits = 2 })` returns path data,
|
|
100
|
+
one `M ... L ...` subpath per run. `axesToSvg(viewport, w, h, { targetTickCount, stroke, labelFill, fontSize })`
|
|
101
|
+
returns `<line>`/`<text>` elements with inline attributes (axes at zero, or
|
|
102
|
+
hugging the nearest edge with labels flipped inward). `svgDocument(w, h, content)`
|
|
103
|
+
wraps them.
|
|
104
|
+
|
|
105
|
+
### Expressions (`@johnhenry/math-sample/expr`)
|
|
106
|
+
|
|
107
|
+
`compileExpression(exprOrSource, { variable = "x", params = {} })` returns a
|
|
108
|
+
`(x) => number` from an `@johnhenry/math` `Expr` or source string. This is the
|
|
109
|
+
only entry that needs `@johnhenry/math`, which is an **optional peer
|
|
110
|
+
dependency**; the main entry never imports it. In a Worker: post the source
|
|
111
|
+
string and params, compile there, sample, and `postMessage` the arrays with
|
|
112
|
+
`[s.x.buffer, s.y.buffer]` as transferables.
|
|
113
|
+
|
|
114
|
+
## 2D and 3D samplers, ODEs, series (part 2/3)
|
|
115
|
+
|
|
116
|
+
All of these take plain functions, return typed arrays, and never draw spikes: a
|
|
117
|
+
non-finite value is a **hole** (a gap row in curves, a skipped vertex in meshes,
|
|
118
|
+
`NaN` in rasters). The grid and field samplers (`sampleSurface`, `sampleParametricSurface`,
|
|
119
|
+
`sampleSpaceCurve`, `sampleVectorField3`, `sampleComplexRaster`, `findZerosAndPoles`, `sampleSlopeField`,
|
|
120
|
+
`samplePhasePortrait` (field pass), `sampleImplicit`, `sampleContours`, `sampleGradient`) accept a
|
|
121
|
+
**budget** and the cancellation options of part 1 (`signal`, `isCancelled`, which throw
|
|
122
|
+
`SampleCancelledError`). `conformalGrid`, the ODE integrators, and the series/Fourier samplers do not take a budget
|
|
123
|
+
(they are bounded by their own `samplesPerLine`, `maxSteps`, `samples` arguments):
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
{ maxCells?: number; maxMillis?: number; now?: () => number }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
When the budget runs out the sampler stops at a row (or slab) boundary and
|
|
130
|
+
**returns what it has** with `progress: { rows, of, complete, reason? }`
|
|
131
|
+
(`reason` is `"cells"` or `"time"`), so a UI can say "stopped at row 120 of 161:
|
|
132
|
+
the time limit". The time limit is checked per row, so one row can overrun it.
|
|
133
|
+
|
|
134
|
+
### 3D (typed-array meshes)
|
|
135
|
+
|
|
136
|
+
| Function | Returns |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `sampleSurface(f(x, y), xDomain, yDomain, { columns, rows, zRange, ...budget })` | `Mesh` for `z = f(x, y)` |
|
|
139
|
+
| `sampleParametricSurface(f(u, v) => [x, y, z], uDomain, vDomain, opts)` | `Mesh` |
|
|
140
|
+
| `sampleSpaceCurve(f(t) => [x, y, z], tDomain, { samples, maxStep, ...budget })` | `Curve3` |
|
|
141
|
+
| `curve3Runs(curve)` | `[start, end)` row ranges of consecutive data |
|
|
142
|
+
| `sampleVectorField3(f(x, y, z) => [a, b, c], xD, yD, zD, { grid, ...budget })` | `VectorField3` |
|
|
143
|
+
|
|
144
|
+
`Mesh` is `{ positions: Float32Array, normals: Float32Array, indices: Uint32Array, valid: Uint8Array,
|
|
145
|
+
vertexCount, columns, rows, bounds, progress }` (3 floats per vertex; triangles are counter-clockwise
|
|
146
|
+
seen from the side the normals point to, which is `+z` for `sampleSurface`, and `dP/du x dP/dv` for a
|
|
147
|
+
parametric surface). A hole vertex has `NaN` position, zero normal and `valid = 0`, and **no triangle
|
|
148
|
+
references it**, so GPU upload needs no special casing. `zRange` also turns a pole's huge but finite
|
|
149
|
+
shoulders into holes. A quad with one hole corner keeps its other triangle. `bounds` covers valid vertices
|
|
150
|
+
(`NaN`s when there are none). A valid vertex whose own normal degenerates (the poles of a sphere parameterisation) borrows
|
|
151
|
+
a neighbouring vertex's normal. Mind the sign: `dP/du x dP/dv` on the usual sphere parameterisation
|
|
152
|
+
`(sin v cos u, sin v sin u, cos v)` points **inward**; swap the arguments or negate a component for outward normals.
|
|
153
|
+
`Curve3` is `{ positions: Float64Array (3 per row), t, count, progress }` with `NaN` rows as gaps (never first,
|
|
154
|
+
last or adjacent). `VectorField3` is `{ origins, vectors, magnitudes: Float32Array, maxMagnitude, count,
|
|
155
|
+
skipped, progress }`; points where the field is non-finite are dropped and counted in `skipped`.
|
|
156
|
+
|
|
157
|
+
### Complex functions
|
|
158
|
+
|
|
159
|
+
`ComplexFn` is `(re, im) => [re, im]`.
|
|
160
|
+
|
|
161
|
+
- `sampleComplexRaster(f, reDomain, imDomain, { width, height })`: `{ re, im, abs, arg: Float32Array,
|
|
162
|
+
width, height, progress }` sampled at cell centres, row 0 at the smallest imaginary part; `arg` in
|
|
163
|
+
`(-PI, PI]`. Colour it yourself (hue from `arg`, brightness from `abs`).
|
|
164
|
+
- `conformalGrid(f, reDomain, imDomain, { reLines, imLines, samplesPerLine, bound, maxStep })`: the image of a
|
|
165
|
+
rectangular grid of lines as `{ kind: "re" | "im", value, x, y }[]`; `NaN` gaps where `|f| > bound` (a pole)
|
|
166
|
+
or a step exceeds `maxStep` (a branch cut).
|
|
167
|
+
- `findZerosAndPoles(f, reDomain, imDomain, { grid, tolerance })`: argument principle on every grid cell
|
|
168
|
+
(winding number = zeros - poles), then Newton on `f` or `1/f` scaled by the multiplicity. Returns
|
|
169
|
+
`{ items: { re, im, kind, order, refined }[] }`. A zero and a pole in one cell cancel, and two distinct
|
|
170
|
+
roots in one cell are reported as one root of the combined order (Newton's step scaled by that order may fail to converge,
|
|
171
|
+
giving `refined: false` at the cell centre): raise `grid` if you suspect it. A genuine multiple root (`z^3`) reports
|
|
172
|
+
its multiplicity once with `refined: true`. The search grid is offset by an irrational fraction of a cell and
|
|
173
|
+
extended past the rectangle, so roots and poles on round-number domains and on grid vertices are found; results
|
|
174
|
+
outside the rectangle are dropped.
|
|
175
|
+
|
|
176
|
+
### ODEs
|
|
177
|
+
|
|
178
|
+
- `sampleSlopeField(f(x, y), xDomain, yDomain, { columns, rows })`: slopes and unit directions
|
|
179
|
+
(`dirX`, `dirY`; vertical slopes are `(0, +-1)`; `NaN` where undefined).
|
|
180
|
+
- `integrateRk4(f(t, y[]) => number[], t0, y0[], tEnd, { steps })` and `integrateAdaptive(...,
|
|
181
|
+
{ atol, rtol, hMin, hMax })` (Dormand-Prince 5(4)): a `Trajectory { t, y (steps x dim), dim, steps,
|
|
182
|
+
reason }` where `reason` is `"end" | "blowup" | "maxSteps" | "stopped" | "stepTooSmall"`. Either direction in
|
|
183
|
+
`t`. `stop(t, y)` ends the run; `blowup` (default 1e12) catches finite-time singularities cleanly.
|
|
184
|
+
- `sampleSolutionCurve(f(x, y), x0, y0, xDomain, { method: "adaptive" | "rk4" })`: the solution through a
|
|
185
|
+
point both ways as `Samples`, ending (not spiking) at an asymptote.
|
|
186
|
+
- `samplePhasePortrait(F(x, y) => [dx, dy], xDomain, yDomain, { grid, seeds, tMax, fixedPointStarts })`:
|
|
187
|
+
`{ field, trajectories, fixedPoints, progress }`. Fixed points are found by Newton from a grid of starts
|
|
188
|
+
and classified from the Jacobian by `classifyFixedPoint(J)`: `stable/unstable node`, `saddle`,
|
|
189
|
+
`stable/unstable spiral`, `center`, the star/degenerate case, or `non-hyperbolic` (det ~ 0; linearisation
|
|
190
|
+
says nothing). A `center` is a linear classification: the nonlinear system may spiral.
|
|
191
|
+
|
|
192
|
+
### Series
|
|
193
|
+
|
|
194
|
+
- `taylorCoefficients(expr, x0, order)` (in `/expr`, needs `@johnhenry/math`; computed by power-series arithmetic, so order
|
|
195
|
+
12 or more is instant for `+ - * /`, powers, `exp ln sqrt sin cos tan sinh cosh tanh atan asin acos`; any other function
|
|
196
|
+
falls back to repeated symbolic differentiation, keep `order` <= 8 there) then
|
|
197
|
+
`sampleTaylorPartialSums(coefficients, x0, domain, orders, samples)`: one curve per order.
|
|
198
|
+
- `sampleSeriesPartialSums(term(k, x), from, domain, orders, samples)`: function-series partial sums in
|
|
199
|
+
one pass over `k` (each term evaluated once per sample).
|
|
200
|
+
- `partialSums(term(k), from, to)`: numeric partial sums plus an Aitken delta-squared `limitEstimate`
|
|
201
|
+
(a hint, not a convergence proof).
|
|
202
|
+
- `fourierCoefficients(f, { min, max }, terms)` (midpoint rule on 4096 samples by default; the interval is one
|
|
203
|
+
period), `evaluateFourier`, `sampleFourierPartialSums(coefficients, domain, orders)`. Trig form
|
|
204
|
+
`a0/2 + sum a_k cos(k pi (x - c) / L) + b_k sin(...)`. A function with jumps shows the Gibbs overshoot, as it should.
|
|
205
|
+
|
|
206
|
+
### 2D (implicit, contours, gradient)
|
|
207
|
+
|
|
208
|
+
- `sampleImplicit(f(x, y), xDomain, yDomain, { grid, level })`: marching squares, saddle cells settled by the
|
|
209
|
+
centre value. Segments come back as `Samples` rows: two endpoints then a `NaN` row per segment, so
|
|
210
|
+
`runsOf` gives one run per segment. Cells with a non-finite corner are skipped.
|
|
211
|
+
- `sampleContours(f, xDomain, yDomain, { grid, levels | count })`: one pass for all levels; default levels are
|
|
212
|
+
spread strictly between the grid's least and greatest value.
|
|
213
|
+
- `sampleGradient(fx, fy, xDomain, yDomain, { grid })`: arrow tails and components with magnitudes.
|
|
214
|
+
|
|
215
|
+
## What it does not do (yet)
|
|
216
|
+
|
|
217
|
+
- **Implicit multiplication is opt-in.** `2x` fails to parse by default; write
|
|
218
|
+
`2*x`, or pass `compileExpression(src, { implicitMultiplication: true })`
|
|
219
|
+
(needs `@johnhenry/math` 0.1.0+; multi-letter variable names such as `theta`
|
|
220
|
+
then become products, use `x_1`).
|
|
221
|
+
- **No interval-robust implicit mode.** `sampleImplicit` is a plain grid; thin features narrower
|
|
222
|
+
than a cell and isolated points can be missed (the quadtree/interval variant needs
|
|
223
|
+
`Symbolic.evaluateInterval`, johnhenry/math#108). Segments are not joined into long polylines.
|
|
224
|
+
- **Meshes are uniform grids.** No adaptive refinement for surfaces or space curves; normals come from
|
|
225
|
+
finite differences of the vertex grid (one-sided at holes and edges), not from analytic gradients.
|
|
226
|
+
- **Budgets stop at row boundaries**, and the time limit is only checked there. The ODE integrators take
|
|
227
|
+
`maxSteps`, not a budget.
|
|
228
|
+
- **Polar/parametric 2D curves and 3D adaptive sampling** are not here; wrap `sampleSpaceCurve`
|
|
229
|
+
(with `z = 0`) or sample a parametric function through `sampleUniform` on each coordinate.
|
|
230
|
+
- **Gaps from steep or fast-oscillating functions.** Jump detection is a
|
|
231
|
+
heuristic; a function that oscillates faster than the grid resolves can read
|
|
232
|
+
as a jump. Raise the resolution or `maxDepth`, or set `jumpFraction: Infinity`.
|
|
233
|
+
- **Double-root and tangent roots are missed** by `findRoots` (no sign change).
|
|
234
|
+
- **No memoization or debouncing.** Callers own caching; use `isCancelled` to
|
|
235
|
+
drop stale work.
|
|
236
|
+
- **Typed output is `Float64Array` only.** Convert with `Float32Array.from(s.y)`
|
|
237
|
+
if you need less.
|
|
238
|
+
|
|
239
|
+
## Tests
|
|
240
|
+
|
|
241
|
+
`npm test -w @johnhenry/math-sample`. Golden values from NumPy, SciPy
|
|
242
|
+
(`brentq`) and SymPy (pole locations) are committed in `test/fixtures/goldens.json`;
|
|
243
|
+
regenerate with `scripts/generate-goldens.py` (the tests never call Python).
|
|
244
|
+
Behaviour was designed against [mallory](https://github.com/johnhenry/mallory)'s
|
|
245
|
+
samplers (MIT, same author); the code here is a rewrite.
|
package/dist/budget.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type CancelSignal } from "./sample.ts";
|
|
2
|
+
/**
|
|
3
|
+
* A work limit for the 2D/3D samplers. When it runs out the sampler stops at a
|
|
4
|
+
* row boundary and returns what it has, with a {@link Progress} saying where
|
|
5
|
+
* it stopped (it does not throw).
|
|
6
|
+
*/
|
|
7
|
+
export interface Budget {
|
|
8
|
+
/** Most function evaluations ("cells") the call may spend. Checked per row/slab. */
|
|
9
|
+
maxCells?: number;
|
|
10
|
+
/** Wall-clock limit in milliseconds. Checked per row/slab, so a single row can overrun it. */
|
|
11
|
+
maxMillis?: number;
|
|
12
|
+
/** Clock override for tests; default `performance.now` when present, else `Date.now`. */
|
|
13
|
+
now?: () => number;
|
|
14
|
+
}
|
|
15
|
+
export interface Cancellable {
|
|
16
|
+
signal?: CancelSignal;
|
|
17
|
+
isCancelled?: () => boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface Progress {
|
|
20
|
+
/** Rows (or slabs, or steps) completed. */
|
|
21
|
+
rows: number;
|
|
22
|
+
/** Rows the full job needed. */
|
|
23
|
+
of: number;
|
|
24
|
+
complete: boolean;
|
|
25
|
+
/** Why it stopped early. Absent when `complete`. */
|
|
26
|
+
reason?: "cells" | "time";
|
|
27
|
+
}
|
|
28
|
+
/** Tracks a {@link Budget} and cancellation for one call. */
|
|
29
|
+
export declare class Meter {
|
|
30
|
+
private readonly start;
|
|
31
|
+
private spent;
|
|
32
|
+
private readonly now;
|
|
33
|
+
private readonly budget;
|
|
34
|
+
private readonly cancel;
|
|
35
|
+
constructor(budget: Budget | undefined, cancel: Cancellable | undefined);
|
|
36
|
+
/** Throws on cancellation; returns a reason when the budget cannot afford `cost` more evaluations. */
|
|
37
|
+
check(cost: number): "cells" | "time" | undefined;
|
|
38
|
+
spend(n: number): void;
|
|
39
|
+
get cells(): number;
|
|
40
|
+
}
|
|
41
|
+
export declare function progress(rows: number, of: number, reason?: "cells" | "time"): Progress;
|
|
42
|
+
export declare function assertCount(name: string, n: number, min?: number): void;
|
|
43
|
+
export declare function assertRange(name: string, d: {
|
|
44
|
+
min: number;
|
|
45
|
+
max: number;
|
|
46
|
+
}): void;
|
|
47
|
+
//# sourceMappingURL=budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../src/budget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAwB,MAAM,aAAa,CAAC;AAEtE;;;;GAIG;AACH,MAAM,WAAW,MAAM;IACrB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8FAA8F;IAC9F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yFAAyF;IACzF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,QAAQ;IACvB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,CAAC;IAClB,oDAAoD;IACpD,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;CAC3B;AAED,6DAA6D;AAC7D,qBAAa,KAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;gBACrC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,WAAW,GAAG,SAAS;IAMvE,sGAAsG;IACtG,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS;IAOjD,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAGtB,IAAI,KAAK,IAAI,MAAM,CAElB;CACF;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAEtF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,SAAI,GAAG,IAAI,CAElE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAG/E"}
|
package/dist/budget.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { SampleCancelledError } from "./sample.js";
|
|
2
|
+
/** Tracks a {@link Budget} and cancellation for one call. */
|
|
3
|
+
export class Meter {
|
|
4
|
+
start;
|
|
5
|
+
spent = 0;
|
|
6
|
+
now;
|
|
7
|
+
budget;
|
|
8
|
+
cancel;
|
|
9
|
+
constructor(budget, cancel) {
|
|
10
|
+
this.budget = budget;
|
|
11
|
+
this.cancel = cancel;
|
|
12
|
+
this.now = budget?.now ?? (typeof performance !== "undefined" ? () => performance.now() : () => Date.now());
|
|
13
|
+
this.start = this.now();
|
|
14
|
+
}
|
|
15
|
+
/** Throws on cancellation; returns a reason when the budget cannot afford `cost` more evaluations. */
|
|
16
|
+
check(cost) {
|
|
17
|
+
if (this.cancel?.signal?.aborted || this.cancel?.isCancelled?.())
|
|
18
|
+
throw new SampleCancelledError();
|
|
19
|
+
const b = this.budget;
|
|
20
|
+
if (b?.maxCells !== undefined && this.spent + cost > b.maxCells)
|
|
21
|
+
return "cells";
|
|
22
|
+
if (b?.maxMillis !== undefined && this.now() - this.start > b.maxMillis)
|
|
23
|
+
return "time";
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
spend(n) {
|
|
27
|
+
this.spent += n;
|
|
28
|
+
}
|
|
29
|
+
get cells() {
|
|
30
|
+
return this.spent;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function progress(rows, of, reason) {
|
|
34
|
+
return reason === undefined ? { rows, of, complete: rows >= of } : { rows, of, complete: false, reason };
|
|
35
|
+
}
|
|
36
|
+
export function assertCount(name, n, min = 2) {
|
|
37
|
+
if (!Number.isInteger(n) || n < min)
|
|
38
|
+
throw new RangeError(`${name} must be an integer >= ${min}, got ${n}`);
|
|
39
|
+
}
|
|
40
|
+
export function assertRange(name, d) {
|
|
41
|
+
if (!Number.isFinite(d.min) || !Number.isFinite(d.max) || !(d.min < d.max))
|
|
42
|
+
throw new RangeError(`${name} must be finite with min < max`);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.js","sourceRoot":"","sources":["../src/budget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA+BtE,6DAA6D;AAC7D,MAAM,OAAO,KAAK;IACC,KAAK,CAAS;IACvB,KAAK,GAAG,CAAC,CAAC;IACD,GAAG,CAAe;IAClB,MAAM,CAAqB;IAC3B,MAAM,CAA0B;IACjD,YAAY,MAA0B,EAAE,MAA+B;QACrE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,GAAG,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,WAAW,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC5G,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC1B,CAAC;IACD,sGAAsG;IACtG,KAAK,CAAC,IAAY;QAChB,IAAI,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,IAAI,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE;YAAE,MAAM,IAAI,oBAAoB,EAAE,CAAC;QACnG,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;QACtB,IAAI,CAAC,EAAE,QAAQ,KAAK,SAAS,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ;YAAE,OAAO,OAAO,CAAC;QAChF,IAAI,CAAC,EAAE,SAAS,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,SAAS;YAAE,OAAO,MAAM,CAAC;QACvF,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,KAAK,CAAC,CAAS;QACb,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;IAClB,CAAC;IACD,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;CACF;AAED,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,EAAU,EAAE,MAAyB;IAC1E,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC3G,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,CAAS,EAAE,GAAG,GAAG,CAAC;IAC1D,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG;QAAE,MAAM,IAAI,UAAU,CAAC,GAAG,IAAI,0BAA0B,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC;AAC9G,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,CAA+B;IACvE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;QACxE,MAAM,IAAI,UAAU,CAAC,GAAG,IAAI,gCAAgC,CAAC,CAAC;AAClE,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type Budget, type Cancellable, type Progress } from "./budget.ts";
|
|
2
|
+
import type { Domain } from "./sample.ts";
|
|
3
|
+
/** A complex function over plain numbers: `(re, im) -> [re, im]`. */
|
|
4
|
+
export type ComplexFn = (re: number, im: number) => readonly [number, number];
|
|
5
|
+
export interface ComplexRasterOptions extends Budget, Cancellable {
|
|
6
|
+
/** Pixels across (real axis). Default 128. */
|
|
7
|
+
width?: number;
|
|
8
|
+
/** Pixels down (imaginary axis). Default 128. */
|
|
9
|
+
height?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Grid of `f(z)` values for domain colouring. Row 0 is the *smallest* imaginary part (mathematical
|
|
13
|
+
* orientation); flip when drawing to a canvas. Cells where `f` is not finite hold `NaN` in all four arrays
|
|
14
|
+
* (a pole shows as `abs = Infinity` only if `f` returned an infinite modulus with finite parts; otherwise NaN).
|
|
15
|
+
* `arg` is in `(-PI, PI]`.
|
|
16
|
+
*/
|
|
17
|
+
export interface ComplexRaster {
|
|
18
|
+
width: number;
|
|
19
|
+
/** Rows completed (may be fewer than requested when the budget ran out). */
|
|
20
|
+
height: number;
|
|
21
|
+
re: Float32Array;
|
|
22
|
+
im: Float32Array;
|
|
23
|
+
abs: Float32Array;
|
|
24
|
+
arg: Float32Array;
|
|
25
|
+
progress: Progress;
|
|
26
|
+
}
|
|
27
|
+
/** Sample `f` at the centre of each cell of a `width` x `height` grid over the rectangle. */
|
|
28
|
+
export declare function sampleComplexRaster(f: ComplexFn, reDomain: Domain, imDomain: Domain, options?: ComplexRasterOptions): ComplexRaster;
|
|
29
|
+
export interface ConformalLine {
|
|
30
|
+
/** `"re"`: the image of the vertical line `Re z = value`; `"im"`: of the horizontal line `Im z = value`. */
|
|
31
|
+
kind: "re" | "im";
|
|
32
|
+
value: number;
|
|
33
|
+
/** Image points; `NaN` rows are gaps (never first, last or adjacent). */
|
|
34
|
+
x: Float64Array;
|
|
35
|
+
y: Float64Array;
|
|
36
|
+
}
|
|
37
|
+
export interface ConformalGridOptions {
|
|
38
|
+
/** Vertical grid lines (constant real part), evenly spaced including both edges. Default 11. */
|
|
39
|
+
reLines?: number;
|
|
40
|
+
/** Horizontal grid lines (constant imaginary part). Default 11. */
|
|
41
|
+
imLines?: number;
|
|
42
|
+
/** Samples along each line. Default 200. */
|
|
43
|
+
samplesPerLine?: number;
|
|
44
|
+
/** Image points with modulus above this become gaps (poles). Default 1e6. */
|
|
45
|
+
bound?: number;
|
|
46
|
+
/** Segments whose image is longer than this become gaps (branch cuts). Default: no limit. */
|
|
47
|
+
maxStep?: number;
|
|
48
|
+
}
|
|
49
|
+
/** The image under `f` of a rectangular grid of lines: where the grid lines go, drawn as the conformal-map picture. */
|
|
50
|
+
export declare function conformalGrid(f: ComplexFn, reDomain: Domain, imDomain: Domain, options?: ConformalGridOptions): ConformalLine[];
|
|
51
|
+
export interface ZeroOrPole {
|
|
52
|
+
re: number;
|
|
53
|
+
im: number;
|
|
54
|
+
kind: "zero" | "pole";
|
|
55
|
+
/** Multiplicity (zero) or order (pole), from the winding number of the cell it was found in. */
|
|
56
|
+
order: number;
|
|
57
|
+
/** `false` when the local refinement did not converge and `re`/`im` is only the cell centre. */
|
|
58
|
+
refined: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface ZerosPolesOptions extends Budget, Cancellable {
|
|
61
|
+
/** Cells per side of the search grid. Default 24. */
|
|
62
|
+
grid?: number;
|
|
63
|
+
/** Convergence tolerance on the root position. Default 1e-10 (relative to the rectangle size). */
|
|
64
|
+
tolerance?: number;
|
|
65
|
+
}
|
|
66
|
+
export interface ZerosPoles {
|
|
67
|
+
items: ZeroOrPole[];
|
|
68
|
+
progress: Progress;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Zeros and poles of `f` inside a rectangle, by the argument principle: the winding number of `f` around each
|
|
72
|
+
* grid cell is (zeros - poles) inside it; non-zero cells are refined with Newton's method (on `f` for a
|
|
73
|
+
* zero, on `1/f` for a pole, scaled by the multiplicity). A zero and a pole inside one cell cancel and are missed:
|
|
74
|
+
* raise `grid`. The search grid is offset by an irrational fraction of a cell and extended past the
|
|
75
|
+
* rectangle, so round-number domains do not put roots on cell edges; results inside the rectangle (or on its edge) are
|
|
76
|
+
* kept and results within the tolerance are merged. `progress.rows` counts grid rows searched.
|
|
77
|
+
*/
|
|
78
|
+
export declare function findZerosAndPoles(f: ComplexFn, reDomain: Domain, imDomain: Domain, options?: ZerosPolesOptions): ZerosPoles;
|
|
79
|
+
//# sourceMappingURL=complex.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"complex.d.ts","sourceRoot":"","sources":["../src/complex.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,MAAM,EAAE,KAAK,WAAW,EAAS,KAAK,QAAQ,EAAY,MAAM,aAAa,CAAC;AACtH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,qEAAqE;AACrE,MAAM,MAAM,SAAS,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE9E,MAAM,WAAW,oBAAqB,SAAQ,MAAM,EAAE,WAAW;IAC/D,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,YAAY,CAAC;IACjB,EAAE,EAAE,YAAY,CAAC;IACjB,GAAG,EAAE,YAAY,CAAC;IAClB,GAAG,EAAE,YAAY,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,6FAA6F;AAC7F,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,SAAS,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,oBAAyB,GACjC,aAAa,CAyCf;AAED,MAAM,WAAW,aAAa;IAC5B,4GAA4G;IAC5G,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,CAAC,EAAE,YAAY,CAAC;IAChB,CAAC,EAAE,YAAY,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,gGAAgG;IAChG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,uHAAuH;AACvH,wBAAgB,aAAa,CAC3B,CAAC,EAAE,SAAS,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,oBAAyB,GACjC,aAAa,EAAE,CAiDjB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,gGAAgG;IAChG,KAAK,EAAE,MAAM,CAAC;IACd,gGAAgG;IAChG,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAkB,SAAQ,MAAM,EAAE,WAAW;IAC5D,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kGAAkG;IAClG,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAQD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,SAAS,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,iBAAsB,GAC9B,UAAU,CA+HZ"}
|