@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.
Files changed (62) hide show
  1. package/README.md +244 -2
  2. package/dist/budget.d.ts +47 -0
  3. package/dist/budget.d.ts.map +1 -0
  4. package/dist/budget.js +44 -0
  5. package/dist/budget.js.map +1 -0
  6. package/dist/complex.d.ts +79 -0
  7. package/dist/complex.d.ts.map +1 -0
  8. package/dist/complex.js +236 -0
  9. package/dist/complex.js.map +1 -0
  10. package/dist/expr.d.ts +38 -0
  11. package/dist/expr.d.ts.map +1 -0
  12. package/dist/expr.js +264 -0
  13. package/dist/expr.js.map +1 -0
  14. package/dist/field2d.d.ts +58 -0
  15. package/dist/field2d.d.ts.map +1 -0
  16. package/dist/field2d.js +199 -0
  17. package/dist/field2d.js.map +1 -0
  18. package/dist/index.d.ts +11 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +10 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/mesh.d.ts +90 -0
  23. package/dist/mesh.d.ts.map +1 -0
  24. package/dist/mesh.js +294 -0
  25. package/dist/mesh.js.map +1 -0
  26. package/dist/ode.d.ts +147 -0
  27. package/dist/ode.d.ts.map +1 -0
  28. package/dist/ode.js +466 -0
  29. package/dist/ode.js.map +1 -0
  30. package/dist/sample.d.ts +114 -0
  31. package/dist/sample.d.ts.map +1 -0
  32. package/dist/sample.js +308 -0
  33. package/dist/sample.js.map +1 -0
  34. package/dist/series.d.ts +53 -0
  35. package/dist/series.d.ts.map +1 -0
  36. package/dist/series.js +158 -0
  37. package/dist/series.js.map +1 -0
  38. package/dist/svg.d.ts +37 -0
  39. package/dist/svg.d.ts.map +1 -0
  40. package/dist/svg.js +82 -0
  41. package/dist/svg.js.map +1 -0
  42. package/dist/ticks.d.ts +17 -0
  43. package/dist/ticks.d.ts.map +1 -0
  44. package/dist/ticks.js +47 -0
  45. package/dist/ticks.js.map +1 -0
  46. package/dist/viewport.d.ts +59 -0
  47. package/dist/viewport.d.ts.map +1 -0
  48. package/dist/viewport.js +111 -0
  49. package/dist/viewport.js.map +1 -0
  50. package/package.json +70 -4
  51. package/src/budget.ts +72 -0
  52. package/src/complex.ts +327 -0
  53. package/src/expr.ts +283 -0
  54. package/src/field2d.ts +259 -0
  55. package/src/index.ts +112 -0
  56. package/src/mesh.ts +376 -0
  57. package/src/ode.ts +633 -0
  58. package/src/sample.ts +391 -0
  59. package/src/series.ts +209 -0
  60. package/src/svg.ts +124 -0
  61. package/src/ticks.ts +46 -0
  62. package/src/viewport.ts +150 -0
package/README.md CHANGED
@@ -1,3 +1,245 @@
1
- # Temporary Holding Version
1
+ # @johnhenry/math-sample
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
@@ -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"}