smath 1.15.1 → 2.0.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/dist/smath.js ADDED
@@ -0,0 +1,506 @@
1
+ /**
2
+ * Check if two numbers are approximately equal with a maximum abolute error.
3
+ * @param a Any number
4
+ * @param b Any number
5
+ * @param epsilon Maximum absolute error
6
+ * @returns True if `a` is approximately `b`
7
+ * @example
8
+ * const b1 = SMath.approx(1 / 3, 0.33, 1e-6), // false
9
+ * b2 = SMath.approx(1 / 3, 0.33, 1e-2); // true
10
+ */
11
+ export function approx(a, b, epsilon = 1e-6) {
12
+ return a - b < epsilon && b - a < epsilon;
13
+ }
14
+ /**
15
+ * Clamp a number within a range.
16
+ * @param n The number to clamp
17
+ * @param min The minimum value of the range
18
+ * @param max The maximum value of the range
19
+ * @returns A clamped number
20
+ * @example
21
+ * const n1 = SMath.clamp(5, 0, 10), // 5
22
+ * n2 = SMath.clamp(-2, 0, 10); // 0
23
+ */
24
+ export function clamp(n, min, max) {
25
+ if (n < min) {
26
+ return min;
27
+ }
28
+ if (n > max) {
29
+ return max;
30
+ }
31
+ return n;
32
+ }
33
+ /**
34
+ * Normalize the number `n` from the range `min, max` to the range `0, 1`
35
+ * @param n The number to normalize
36
+ * @param min The minimum value in the range
37
+ * @param max The maximum value in the range
38
+ * @returns A normalized value
39
+ * @example
40
+ * const y = SMath.normalize(18, 9, 99); // 0.1
41
+ */
42
+ export function normalize(n, min, max) {
43
+ if (min === max) {
44
+ return 0;
45
+ }
46
+ return (n - min) / (max - min);
47
+ }
48
+ /**
49
+ * Expand a normalized number `n` to the range `min, max`
50
+ * @param n A normalized number
51
+ * @param min The minimum value in the range
52
+ * @param max The maximum value in the range
53
+ * @returns A value within the number range
54
+ * @example
55
+ * const y = SMath.expand(0.25, 4, 6); // 4.5
56
+ */
57
+ export function expand(n, min, max) {
58
+ return (max - min) * n + min;
59
+ }
60
+ /**
61
+ * Translate a number `n` from the range `min1, max1` to the range `min2, max2`
62
+ * @param n The number to translate
63
+ * @param min1 The minimum value from the initial range
64
+ * @param max1 The maximum value from the initial range
65
+ * @param min2 The minimum value for the final range
66
+ * @param max2 The maximum value for the final range
67
+ * @returns A translated number in the final range
68
+ * @example
69
+ * const C = 20,
70
+ * F = SMath.translate(C, 0, 100, 32, 212); // 68
71
+ */
72
+ export function translate(n, min1, max1, min2, max2) {
73
+ return expand(normalize(n, min1, max1), min2, max2);
74
+ }
75
+ /**
76
+ * Generate an array of linearly spaced numbers.
77
+ * @param min The initial value of the linear space
78
+ * @param max The final value of the linear space
79
+ * @param count The number of values in the space
80
+ * @returns The linear space as an array of numbers
81
+ * @example
82
+ * const space = SMath.linspace(1, 5, 6);
83
+ * // [ 1, 1.8, 2.6, 3.4, 4.2, 5 ]
84
+ */
85
+ export function linspace(min, max, count) {
86
+ const space = [];
87
+ for (let i = 0; i < count; i++) {
88
+ space[i] = translate(i, 0, count - 1, min, max);
89
+ }
90
+ return space;
91
+ }
92
+ /**
93
+ * Generate an array of logarithmically spaced numbers.
94
+ * @param min The initial magnitude of the space
95
+ * @param max The final magnitude of the space
96
+ * @param count The number of values in the space
97
+ * @returns The logarithmic space as an array of numbers
98
+ * @example
99
+ * const space = SMath.logspace(0, 2, 5);
100
+ * // [ 1, 3.2, 10, 31.6, 100 ]
101
+ */
102
+ export function logspace(min, max, count) {
103
+ return linspace(min, max, count).map(n => 10 ** n);
104
+ }
105
+ /**
106
+ * Compute the factorial of `n`.
107
+ * @param n Any positive integer
108
+ * @returns `n!`
109
+ * @example
110
+ * const y = SMath.factorial(5); // 120
111
+ */
112
+ export function factorial(n) {
113
+ if (n < 0 || (n | 0) !== n) {
114
+ throw new Error('Input must be a positive integer.');
115
+ }
116
+ else if (n === 0) {
117
+ return 1;
118
+ }
119
+ else if (n <= 2) {
120
+ return n;
121
+ }
122
+ else {
123
+ return n * factorial(n - 1);
124
+ }
125
+ }
126
+ /**
127
+ * Factorize `n` into its prime factors.
128
+ * @param n Any positive integer
129
+ * @returns The array of prime factors
130
+ * @example
131
+ * const y = SMath.factors(12); // [ 2, 2, 3 ]
132
+ */
133
+ export function factors(n) {
134
+ if (n < 0 || (n | 0) !== n) {
135
+ throw new Error('Input must be a positive integer!');
136
+ }
137
+ if (n <= 3) {
138
+ return [n];
139
+ }
140
+ const f = [];
141
+ let i = 2;
142
+ while (n > 1 && i <= n) {
143
+ if ((n / i) === ((n / i) | 0)) {
144
+ n /= i;
145
+ f.push(i);
146
+ }
147
+ else {
148
+ i++;
149
+ }
150
+ }
151
+ return f;
152
+ }
153
+ /**
154
+ * Round a number to the nearest multiple of an arbitrary
155
+ * base. Does not round when the base is set to zero.
156
+ * @param n Any number to round
157
+ * @param base Any base to round to
158
+ * @returns `n` rounded to the nearest multiple of `base`
159
+ * @example
160
+ * const y = SMath.round2(Math.PI, 0.2); // 3.2
161
+ */
162
+ export function round2(n, base) {
163
+ const rounded = base ? base * Math.round(n / base) : n;
164
+ const precision = 10; // Removes precision errors
165
+ return parseFloat(rounded.toFixed(precision));
166
+ }
167
+ /**
168
+ * Calculate the relative normalized error or deviation from any
169
+ * value to an accepted value. An error of 0 indicates that the
170
+ * two values are identical. An error of -0.1 indicates that the
171
+ * experimental value is 10% smaller than (90% of) the accepted
172
+ * value. An error of 1.0 indicates that the experimental value
173
+ * is 100% greater (or twice the size) of the accepted value.
174
+ * @param experimental The value observed or produced by a test
175
+ * @param actual The accepted or theoretical value
176
+ * @returns The relative (normalized) error
177
+ * @example
178
+ * const e = SMath.error(22.5, 25); // -0.1
179
+ */
180
+ export function error(experimental, actual) {
181
+ if (experimental === 0 && actual === 0) {
182
+ return 0;
183
+ }
184
+ else {
185
+ return (experimental - actual) / actual;
186
+ }
187
+ }
188
+ /**
189
+ * Add up all the inputs.
190
+ * If none are present, returns 0.
191
+ * @param data An array of numeric inputs
192
+ * @returns The sum total
193
+ * @example
194
+ * const y = SMath.sum([1, 2, 3]); // 6
195
+ */
196
+ export function sum(data) {
197
+ return data.reduce((a, b) => a + b, 0);
198
+ }
199
+ /**
200
+ * Multiply all the inputs.
201
+ * If none are present, returns 1.
202
+ * @param data An array of numeric inputs
203
+ * @returns The product
204
+ * @example
205
+ * const y = SMath.prod([2, 2, 3, 5]); // 60
206
+ */
207
+ export function prod(data) {
208
+ return data.reduce((a, b) => a * b, 1);
209
+ }
210
+ /**
211
+ * Compute the average, or mean, of a set of numbers.
212
+ * @param data An array of numeric inputs
213
+ * @returns The average, or mean
214
+ * @example
215
+ * const y = SMath.avg([1, 2, 4, 4]); // 2.75
216
+ */
217
+ export function avg(data) {
218
+ return sum(data) / data.length;
219
+ }
220
+ /**
221
+ * Compute the median of a set of numbers.
222
+ * @param data An array of numeric inputs
223
+ * @returns The median of the dataset
224
+ * @example
225
+ * const y = SMath.median([2, 5, 3, 1]); // 2.5
226
+ */
227
+ export function median(data) {
228
+ data.sort((a, b) => a - b);
229
+ if (data.length % 2) {
230
+ return data[(data.length - 1) / 2];
231
+ }
232
+ return avg([data[data.length / 2 - 1], data[data.length / 2]]);
233
+ }
234
+ /**
235
+ * Compute the variance of a **complete population**.
236
+ * @param data An array of numeric inputs
237
+ * @returns The population variance
238
+ * @example
239
+ * const y = SMath.varp([1, 2, 4, 4]); // 1.6875
240
+ */
241
+ export function varp(data) {
242
+ const mean = avg(data), squares = data.map(x => (x - mean) ** 2);
243
+ return sum(squares) / data.length;
244
+ }
245
+ /**
246
+ * Compute the variance of a **sample**.
247
+ * @param data An array of numeric inputs
248
+ * @returns The sample variance
249
+ * @example
250
+ * const y = SMath.vars([1, 2, 4, 4]); // 2.25
251
+ */
252
+ export function vars(data) {
253
+ const mean = avg(data), squares = data.map(x => (x - mean) ** 2);
254
+ return sum(squares) / (data.length - 1);
255
+ }
256
+ /**
257
+ * Compute the standard deviation of a **complete population**.
258
+ * @param data An array of numeric inputs
259
+ * @returns The population standard deviation
260
+ * @example
261
+ * const y = SMath.stdevp([1, 2, 3, 4]); // 1.118...
262
+ */
263
+ export function stdevp(data) {
264
+ return Math.sqrt(varp(data));
265
+ }
266
+ /**
267
+ * Compute the standard deviation of a **sample**.
268
+ * @param data An array of numeric inputs
269
+ * @returns The sample standard deviation
270
+ * @example
271
+ * const y = SMath.stdevs([1, 2, 3, 4]); // 1.29...
272
+ */
273
+ export function stdevs(data) {
274
+ return Math.sqrt(vars(data));
275
+ }
276
+ /**
277
+ * Generate a uniformly-distributed floating-point number within the range.
278
+ * @param min The minimum bound
279
+ * @param max The maximum bound
280
+ * @returns A random float within the range
281
+ * @example
282
+ * const y = SMath.runif(-2, 2); // 0.376...
283
+ */
284
+ export function runif(min, max) {
285
+ return expand(Math.random(), min, max);
286
+ }
287
+ /**
288
+ * Generate a uniformly-distributed integer within the range.
289
+ * @param min The minimum bound (inclusive)
290
+ * @param max The maximum bound (inclusive)
291
+ * @returns A random integer within the range
292
+ * @example
293
+ * const y = SMath.rint(-4, 3); // -4
294
+ */
295
+ export function rint(min, max) {
296
+ min |= 0;
297
+ max |= 0;
298
+ if (min < 0) {
299
+ min--;
300
+ }
301
+ if (max > 0) {
302
+ max++;
303
+ }
304
+ return clamp(runif(min, max), min, max) | 0; // `| 0` pulls toward 0
305
+ }
306
+ /**
307
+ * Generate a normally-distributed floating-point number.
308
+ * @param mean The mean of the population distribution
309
+ * @param stdev The standard deviation of the population
310
+ * @returns A random float
311
+ * @example
312
+ * const y = SMath.rnorm(2, 3); // 1.627...
313
+ */
314
+ export function rnorm(mean = 0, stdev = 1) {
315
+ return mean + stdev * Math.sqrt(-2 * Math.log(Math.random())) * Math.cos(2 * Math.PI * Math.random());
316
+ }
317
+ /**
318
+ * Generate a population of normally-distributed floating-point numbers.
319
+ * @param count The number of values to generate
320
+ * @param mean The mean of the population distribution
321
+ * @param stdev The standard deviation of the population
322
+ * @returns A population of random floats
323
+ * @example
324
+ * const dataset = SMath.rdist(3); // [ 1.051..., -0.779..., -2.254... ]
325
+ */
326
+ export function rdist(count, mean = 0, stdev = 1) {
327
+ const distribution = [];
328
+ for (let i = 0; i < count; i++) {
329
+ distribution[i] = rnorm(mean, stdev);
330
+ }
331
+ return distribution;
332
+ }
333
+ /**
334
+ * Randomize an array of arbitrary elements.
335
+ * @param stack An array of arbitrary elements
336
+ * @returns The `stack` array in a random order
337
+ * @example
338
+ * const shuffled = SMath.shuffle(['a', 'b', 'c']); // [ 'c', 'a', 'b' ]
339
+ */
340
+ export function shuffle(stack) {
341
+ const rawData = [];
342
+ for (const item of stack) {
343
+ rawData.push({ index: Math.random(), value: item });
344
+ }
345
+ return rawData.sort((a, b) => a.index - b.index).map(a => a.value);
346
+ }
347
+ /**
348
+ * Select a single item from an array at random with uniform weights.
349
+ * @param stack An array of arbirary item
350
+ * @returns A single randomly selected item
351
+ * @example
352
+ * const selected = SMath.selectRandom([10, 20, 30, 40]); // 30
353
+ */
354
+ export function selectRandom(stack) {
355
+ return stack[rint(0, stack.length - 1)];
356
+ }
357
+ /**
358
+ * Select a single index in an array at random with different weights.
359
+ * @param weights The weights for each item
360
+ * @returns The 0-based index of the randomly selected item
361
+ * @example
362
+ * const index = SMath.selectRandomWeighted([3.5, 4, 1]); // 1
363
+ */
364
+ export function selectRandomWeighted(weights) {
365
+ const startWeights = [];
366
+ let accumulation = 0;
367
+ for (const weight of weights) {
368
+ accumulation += clamp(0, weight, Infinity);
369
+ startWeights.push(accumulation);
370
+ }
371
+ const random = runif(0, accumulation);
372
+ return startWeights.findIndex(weight => random < weight);
373
+ }
374
+ /**
375
+ * Take the limit of a function. A return value of `NaN` indicates
376
+ * that no limit exists either due to a discontinuity or imaginary value.
377
+ * @param f Function `f(x)`
378
+ * @param x The x-value where to take the limit
379
+ * @param h The approach distance
380
+ * @param discontinuity_cutoff The discontinuity cutoff
381
+ * @returns `lim(f(x->x))`
382
+ * @example
383
+ * const y = SMath.lim(Math.log, 0); // -Infinity
384
+ */
385
+ export function lim(f, x, h = 1e-6, discontinuity_cutoff = 1e-3) {
386
+ const center = f(x), left1 = f(x - h), left2 = f(x - h / 2), right1 = f(x + h), right2 = f(x + h / 2);
387
+ let left, right;
388
+ if (Number.isFinite(center)) {
389
+ return center;
390
+ }
391
+ // Check the limit approaching from the left
392
+ if (Number.isFinite(left1) && Number.isFinite(left2)) {
393
+ if (approx(left1, left2, discontinuity_cutoff)) {
394
+ left = left2; // Converges
395
+ }
396
+ else if (left1 > 0 && left2 > left1) {
397
+ left = Infinity; // Diverges to +inf
398
+ }
399
+ else if (left1 < 0 && left2 < left1) {
400
+ left = -Infinity; // Diverges to -inf
401
+ }
402
+ else {
403
+ left = NaN; // Diverges
404
+ }
405
+ }
406
+ else {
407
+ left = NaN; // Discontinuous
408
+ }
409
+ // Check the limit approaching from the right
410
+ if (Number.isFinite(right1) && Number.isFinite(right2)) {
411
+ if (approx(right1, right2, discontinuity_cutoff)) {
412
+ right = right2; // Converges
413
+ }
414
+ else if (right1 > 0 && right2 > right1) {
415
+ right = Infinity; // Diverges to +inf
416
+ }
417
+ else if (right1 < 0 && right2 < right1) {
418
+ right = -Infinity; // Diverges to -inf
419
+ }
420
+ else {
421
+ right = NaN; // Diverges
422
+ }
423
+ }
424
+ else {
425
+ right = NaN; // Discontinuous
426
+ }
427
+ // Check if limits match or are close
428
+ if (left === right) { // Handles +/-Infinity case
429
+ return left;
430
+ }
431
+ else if (Number.isNaN(left) && Number.isNaN(right)) {
432
+ return center;
433
+ }
434
+ else if (!Number.isNaN(left) && Number.isNaN(right)) {
435
+ return left;
436
+ }
437
+ else if (Number.isNaN(left) && !Number.isNaN(right)) {
438
+ return right;
439
+ }
440
+ else if (approx(left, right, discontinuity_cutoff)) {
441
+ return avg([left, right]);
442
+ }
443
+ else {
444
+ return NaN;
445
+ }
446
+ }
447
+ /**
448
+ * Take the derivative of a function.
449
+ * @param f Function `f(x)`
450
+ * @param x The x-value where to evaluate the derivative
451
+ * @param epsilon Small step value
452
+ * @returns `f'(x)`
453
+ * @example
454
+ * const y = SMath.differentiate(x => 3 * x ** 2, 2); // 12
455
+ */
456
+ export function differentiate(f, x, epsilon = 1e-6) {
457
+ return lim(h => (f(x + h) - f(x - h)) / (2 * h), 0, epsilon);
458
+ }
459
+ /**
460
+ * Compute the definite integral of a function.
461
+ * @param f Function `f(x)`
462
+ * @param a The miminum integral bound
463
+ * @param b The maximum integral bound
464
+ * @param Ndx The number of rectangles to compute
465
+ * @returns `F(b)-F(a)`
466
+ * @example
467
+ * const y = SMath.integrate(x => 3 * x ** 2, 1, 2); // 7
468
+ */
469
+ export function integrate(f, a, b, Ndx = 1e6) {
470
+ return ((b - a) / Ndx) * sum(linspace(a, b, Ndx).map(x => f(x)));
471
+ }
472
+ /**
473
+ * Convert an arbitrary decimal number into a simplified fraction (or ratio).
474
+ * See `mixed()` for instructions on how to break out the whole number part.
475
+ * @param n The decimal number to convert
476
+ * @param epsilon Maximum absolute error
477
+ * @returns An object containing the fraction's numerator and denominator
478
+ * @example
479
+ * const frac = SMath.rat(0.625); // { num: 5, den: 8 }
480
+ */
481
+ export function rat(n, epsilon = 1e-6) {
482
+ let num = 0, den = 1;
483
+ const sign = n < 0 ? -1 : 1;
484
+ while (!approx(sign * n, num / den, epsilon)) {
485
+ if (sign * n > num / den) {
486
+ num++;
487
+ }
488
+ else {
489
+ den++;
490
+ }
491
+ }
492
+ return { num: sign * num, den: den };
493
+ }
494
+ /**
495
+ * Convert an arbitrary decimal number into a simplified fraction, after
496
+ * breaking out the whole number part first. See `rat()` for keeping the
497
+ * number as a ratio without separating the whole number part.
498
+ * @param n A decimal number to convert
499
+ * @param epsilon Maximum absolute error
500
+ * @returns An object containing the whole part and fraction numerator and denominator
501
+ * @example
502
+ * const frac = SMath.mixed(-8 / 6); // { whole: -1, num: 1, den: 3 }
503
+ */
504
+ export function mixed(n, epsilon = 1e-6) {
505
+ return { whole: n | 0, ...rat(n < -1 ? (n | 0) - n : n - (n | 0), epsilon) };
506
+ }
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "smath",
3
- "version": "1.15.1",
3
+ "version": "2.0.0",
4
4
  "description": "Small math function library",
5
5
  "homepage": "https://npm.nicfv.com/",
6
+ "type": "module",
6
7
  "bin": "dist/bin.js",
7
8
  "main": "dist/index.js",
8
9
  "types": "types/index.d.ts",
@@ -38,7 +39,23 @@
38
39
  "extrapolate",
39
40
  "extrapolation",
40
41
  "linspace",
41
- "logspace"
42
+ "logspace",
43
+ "curve",
44
+ "fit",
45
+ "fitting",
46
+ "least",
47
+ "squares",
48
+ "regression",
49
+ "data",
50
+ "dataset",
51
+ "point",
52
+ "parameters",
53
+ "linear",
54
+ "nonlinear",
55
+ "variable",
56
+ "multivariable",
57
+ "multivariant",
58
+ "multivariate"
42
59
  ],
43
60
  "author": {
44
61
  "name": "Nicolas Ventura",
@@ -52,7 +69,7 @@
52
69
  "repository": "github:nicfv/npm",
53
70
  "license": "MIT",
54
71
  "devDependencies": {
55
- "@types/node": "25.0.10",
56
- "t6": "1.2.1"
72
+ "@types/node": "25.6.0",
73
+ "t6": "1.3.0"
57
74
  }
58
75
  }
@@ -0,0 +1,2 @@
1
+ export * from './lib.js';
2
+ export * from './types.js';
@@ -0,0 +1,25 @@
1
+ import { Datum, F, Summary, VariableType } from './types.js';
2
+ /**
3
+ * Minimize the sum of squared errors to fit a set of data
4
+ * points to a curve with a set of unknown parameters.
5
+ * @param f The model function for curve fitting.
6
+ * @param data The entire dataset, as an array of points.
7
+ * @param params_initial The initial guess for function
8
+ * parameters, which defaults to an array filled with zeroes.
9
+ * @param iterations The number of parameter sets to generate.
10
+ * @param maxDeviation The relative standard parameter deviation.
11
+ * This is a number [0.0-1.0] and affects the standard deviation
12
+ * on the first iteration. Every subsequent iteration has a
13
+ * decayed standard deviation until the final iteration.
14
+ * @returns The set of parameters and error for the best fit.
15
+ * @example
16
+ * // Define model function
17
+ * function f(x: number, a2: number = -0.5, a1: number = 3.9, a0: number = -1.2): number {
18
+ * return a2 * x ** 2 + a1 * x + a0;
19
+ * }
20
+ * // Construct a data set
21
+ * const data: Datum<number>[] = [0, 2, 4].map(x => ({ x: x, y: f(x) }));
22
+ * // Compute best-fit summary
23
+ * const summary = fit(f, data);
24
+ */
25
+ export declare function fit<T extends VariableType>(f: F<T>, data: Datum<T>[], params_initial?: number[], iterations?: number, maxDeviation?: number): Summary<T>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Declares whether this is a single- or multi-variable problem.
3
+ */
4
+ export type VariableType = number | number[];
5
+ /**
6
+ * Represents a mathematical function y = f(x) with unknown parameters.
7
+ * @example
8
+ * // Single variable function in Typescript, 2nd degree polynomial:
9
+ * function f(x: number, a2: number, a1: number, a0: number): number {
10
+ * return a2 * x ** 2 + a1 * x + a0;
11
+ * }
12
+ * // Multivariable function in Typescript, general plane equation:
13
+ * function f([x, y]: number[], cx: number, cy: number, cz: number): number {
14
+ * return cx * x + cy * y + cz;
15
+ * }
16
+ */
17
+ export type F<T extends VariableType> = (x: T, ...params: number[]) => number;
18
+ /**
19
+ * Stores a data point. For multivariable points, the `x`
20
+ * coordinate contains an array of all the free variables.
21
+ */
22
+ export interface Datum<T extends VariableType> {
23
+ /**
24
+ * **Input:** X variable(s)
25
+ */
26
+ readonly x: T;
27
+ /**
28
+ * **Output:** Y variable
29
+ */
30
+ readonly y: number;
31
+ }
32
+ /**
33
+ * Includes information about a best-fit for a curve.
34
+ */
35
+ export interface Summary<T extends VariableType> {
36
+ /**
37
+ * The model with best-fit parameters applied.
38
+ */
39
+ readonly f: (x: T) => number;
40
+ /**
41
+ * Contains the set of best-fit parameters for the function `f(x)`
42
+ */
43
+ readonly params: number[];
44
+ /**
45
+ * This is the residual sum of squared errors.
46
+ */
47
+ readonly error: number;
48
+ /**
49
+ * The average absolute error per data point, comparing the given
50
+ * dataset to the model output with the set of best-fit parameters.
51
+ */
52
+ readonly errorAvgAbs: number;
53
+ }