@johnhenry/math-sample 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/series.ts ADDED
@@ -0,0 +1,209 @@
1
+ import { assertCount, assertRange } from "./budget.ts";
2
+ import type { Domain, Samples } from "./sample.ts";
3
+
4
+ /** Evaluate `sum a_k (x - x0)^k` for `k <= order` by Horner's rule. */
5
+ export function evaluateTaylor(
6
+ coefficients: ArrayLike<number>,
7
+ x0: number,
8
+ x: number,
9
+ order = coefficients.length - 1,
10
+ ): number {
11
+ const top = Math.min(order, coefficients.length - 1);
12
+ let acc = 0;
13
+ for (let k = top; k >= 0; k--) acc = acc * (x - x0) + (coefficients[k] as number);
14
+ return acc;
15
+ }
16
+
17
+ export interface PartialSumCurves {
18
+ /** The orders requested, ascending as given. */
19
+ orders: number[];
20
+ /** One `Samples` per order; `y` is `NaN` where the partial sum is not finite (gap rules as in 1D). */
21
+ curves: Samples[];
22
+ }
23
+
24
+ function cleanSamples(x: Float64Array, y: Float64Array): Samples {
25
+ // enforce: gaps not first/last/adjacent
26
+ const xs: number[] = [];
27
+ const ys: number[] = [];
28
+ for (let k = 0; k < x.length; k++) {
29
+ const yy = y[k] as number;
30
+ if (Number.isFinite(yy)) {
31
+ xs.push(x[k] as number);
32
+ ys.push(yy);
33
+ } else if (ys.length > 0 && !Number.isNaN(ys[ys.length - 1] as number)) {
34
+ xs.push(x[k] as number);
35
+ ys.push(Number.NaN);
36
+ }
37
+ }
38
+ while (ys.length > 0 && Number.isNaN(ys[ys.length - 1] as number)) {
39
+ ys.pop();
40
+ xs.pop();
41
+ }
42
+ return { x: Float64Array.from(xs), y: Float64Array.from(ys) };
43
+ }
44
+
45
+ function grid(domain: Domain, n: number): Float64Array {
46
+ assertRange("domain", domain);
47
+ assertCount("samples", n);
48
+ const x = new Float64Array(n);
49
+ for (let k = 0; k < n; k++) x[k] = k === n - 1 ? domain.max : domain.min + ((domain.max - domain.min) * k) / (n - 1);
50
+ return x;
51
+ }
52
+
53
+ /**
54
+ * Taylor partial sums of the given coefficients (`a_k = f^(k)(x0) / k!`) about `x0`, one curve per requested
55
+ * order, over a uniform grid. With `@johnhenry/math`, `taylorCoefficients` in `@johnhenry/math-sample/expr`
56
+ * produces the coefficients from an expression.
57
+ */
58
+ export function sampleTaylorPartialSums(
59
+ coefficients: ArrayLike<number>,
60
+ x0: number,
61
+ domain: Domain,
62
+ orders: number[],
63
+ samples = 400,
64
+ ): PartialSumCurves {
65
+ const x = grid(domain, samples);
66
+ const curves = orders.map((order) => {
67
+ if (!Number.isInteger(order) || order < 0)
68
+ throw new RangeError(`order must be a non-negative integer, got ${order}`);
69
+ const y = new Float64Array(samples);
70
+ for (let k = 0; k < samples; k++) y[k] = evaluateTaylor(coefficients, x0, x[k] as number, order);
71
+ return cleanSamples(x, y);
72
+ });
73
+ return { orders: [...orders], curves };
74
+ }
75
+
76
+ /**
77
+ * Partial sums of a function series `sum_{k=from}^{n} term(k, x)`, one curve per order `n`, computed
78
+ * in a single pass over `k` so each term is evaluated once per sample.
79
+ */
80
+ export function sampleSeriesPartialSums(
81
+ term: (k: number, x: number) => number,
82
+ from: number,
83
+ domain: Domain,
84
+ orders: number[],
85
+ samples = 400,
86
+ ): PartialSumCurves {
87
+ if (!Number.isInteger(from)) throw new RangeError("from must be an integer");
88
+ const x = grid(domain, samples);
89
+ const sorted = [...new Set(orders)].sort((a, b) => a - b);
90
+ for (const o of sorted)
91
+ if (!Number.isInteger(o) || o < from) throw new RangeError(`order ${o} must be an integer >= from (${from})`);
92
+ const sums = new Float64Array(samples);
93
+ const byOrder = new Map<number, Samples>();
94
+ let k = from;
95
+ for (const o of sorted) {
96
+ for (; k <= o; k++) for (let i = 0; i < samples; i++) sums[i] = (sums[i] as number) + term(k, x[i] as number);
97
+ byOrder.set(o, cleanSamples(x, Float64Array.from(sums)));
98
+ }
99
+ return { orders: [...orders], curves: orders.map((o) => byOrder.get(o) as Samples) };
100
+ }
101
+
102
+ export interface NumericSeries {
103
+ /** `partial[n - from]` is the sum of terms `from..n`. */
104
+ partial: Float64Array;
105
+ from: number;
106
+ /** Aitken delta-squared extrapolation of the last three partial sums; `NaN` when it cannot be formed. A hint, not a proof of convergence. */
107
+ limitEstimate: number;
108
+ }
109
+
110
+ /** Partial sums of the numeric series `sum_{k=from}^{to} term(k)`, with an Aitken limit estimate. */
111
+ export function partialSums(term: (k: number) => number, from: number, to: number): NumericSeries {
112
+ if (!Number.isInteger(from) || !Number.isInteger(to) || to < from) throw new RangeError("need integers from <= to");
113
+ const partial = new Float64Array(to - from + 1);
114
+ let s = 0;
115
+ for (let k = from; k <= to; k++) {
116
+ s += term(k);
117
+ partial[k - from] = s;
118
+ }
119
+ let limitEstimate = Number.NaN;
120
+ const n = partial.length;
121
+ if (n >= 3) {
122
+ const a = partial[n - 3] as number;
123
+ const b = partial[n - 2] as number;
124
+ const c = partial[n - 1] as number;
125
+ const den = c - 2 * b + a;
126
+ limitEstimate = den === 0 ? c : c - ((c - b) * (c - b)) / den;
127
+ }
128
+ return { partial, from, limitEstimate };
129
+ }
130
+
131
+ export interface FourierCoefficients {
132
+ /** Half-period `L`: the function has period `2L`. */
133
+ halfPeriod: number;
134
+ /** The centre of the period interval used `[center - L, center + L]`. */
135
+ center: number;
136
+ /** Constant term `a0 / 2` is `a0 / 2`; `a0` is `(1/L) * integral f`. */
137
+ a0: number;
138
+ /** `a[k - 1]` multiplies `cos(k pi (x - center) / L)`. */
139
+ a: Float64Array;
140
+ /** `b[k - 1]` multiplies `sin(k pi (x - center) / L)`. */
141
+ b: Float64Array;
142
+ }
143
+
144
+ /**
145
+ * Fourier coefficients of `f` on `[a, b]` taken as one period, by the midpoint rule on `samples` points
146
+ * (exact for trig polynomials below the Nyquist order, being a full-period rule; for a function with jumps the coefficients carry
147
+ * ordinary O(1/samples) quadrature error). `terms` is the highest harmonic. Trig form:
148
+ * `a0/2 + sum a_k cos(k w x) + b_k sin(k w x)` about the interval centre.
149
+ */
150
+ export function fourierCoefficients(
151
+ f: (x: number) => number,
152
+ period: Domain,
153
+ terms: number,
154
+ samples = 4096,
155
+ ): FourierCoefficients {
156
+ assertRange("period", period);
157
+ assertCount("terms", terms, 1);
158
+ assertCount("samples", samples, 4);
159
+ const L = (period.max - period.min) / 2;
160
+ const center = (period.max + period.min) / 2;
161
+ const a = new Float64Array(terms);
162
+ const b = new Float64Array(terms);
163
+ const vals = new Float64Array(samples);
164
+ for (let i = 0; i < samples; i++) vals[i] = f(period.min + ((period.max - period.min) * (i + 0.5)) / samples); // midpoint
165
+ let a0 = 0;
166
+ for (let i = 0; i < samples; i++) a0 += vals[i] as number;
167
+ a0 = (2 * a0) / samples;
168
+ for (let k = 1; k <= terms; k++) {
169
+ let sa = 0;
170
+ let sb = 0;
171
+ for (let i = 0; i < samples; i++) {
172
+ const t = Math.PI * k * (((i + 0.5) / samples) * 2 - 1);
173
+ sa += (vals[i] as number) * Math.cos(t);
174
+ sb += (vals[i] as number) * Math.sin(t);
175
+ }
176
+ a[k - 1] = (2 * sa) / samples;
177
+ b[k - 1] = (2 * sb) / samples;
178
+ }
179
+ return { halfPeriod: L, center, a0, a, b };
180
+ }
181
+
182
+ /** Value of the Fourier partial sum with harmonics up to `n` at `x`. */
183
+ export function evaluateFourier(c: FourierCoefficients, x: number, n = c.a.length): number {
184
+ const top = Math.min(n, c.a.length);
185
+ const w = Math.PI / c.halfPeriod;
186
+ let s = c.a0 / 2;
187
+ for (let k = 1; k <= top; k++) {
188
+ const t = k * w * (x - c.center);
189
+ s += (c.a[k - 1] as number) * Math.cos(t) + (c.b[k - 1] as number) * Math.sin(t);
190
+ }
191
+ return s;
192
+ }
193
+
194
+ /** One curve per harmonic count in `orders` (0 = just the constant term). */
195
+ export function sampleFourierPartialSums(
196
+ c: FourierCoefficients,
197
+ domain: Domain,
198
+ orders: number[],
199
+ samples = 400,
200
+ ): PartialSumCurves {
201
+ const x = grid(domain, samples);
202
+ const curves = orders.map((n) => {
203
+ if (!Number.isInteger(n) || n < 0) throw new RangeError(`order must be a non-negative integer, got ${n}`);
204
+ const y = new Float64Array(samples);
205
+ for (let i = 0; i < samples; i++) y[i] = evaluateFourier(c, x[i] as number, n);
206
+ return cleanSamples(x, y);
207
+ });
208
+ return { orders: [...orders], curves };
209
+ }