smath 1.16.0 → 2.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/dist/smath.js ADDED
@@ -0,0 +1,529 @@
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
+ * An optimized algorithm to determine if any number is prime.
155
+ * @param n Any positive integer
156
+ * @returns `true` if `n` is prime
157
+ */
158
+ export function isPrime(n) {
159
+ if (n <= 1 || (n | 0) !== n) {
160
+ return false;
161
+ }
162
+ if (n <= 3) {
163
+ return true;
164
+ }
165
+ if (n % 2 === 0 || n % 3 === 0) {
166
+ return false;
167
+ }
168
+ // Check 6x-1 and 6x+1 (all prime forms)
169
+ for (let i = 5; i * i <= n; i += 6) {
170
+ if (n % i === 0 || n % (i + 2) === 0) {
171
+ return false;
172
+ }
173
+ }
174
+ return true;
175
+ }
176
+ /**
177
+ * Round a number to the nearest multiple of an arbitrary
178
+ * base. Does not round when the base is set to zero.
179
+ * @param n Any number to round
180
+ * @param base Any base to round to
181
+ * @returns `n` rounded to the nearest multiple of `base`
182
+ * @example
183
+ * const y = SMath.round2(Math.PI, 0.2); // 3.2
184
+ */
185
+ export function round2(n, base) {
186
+ const rounded = base ? base * Math.round(n / base) : n;
187
+ const precision = 10; // Removes precision errors
188
+ return parseFloat(rounded.toFixed(precision));
189
+ }
190
+ /**
191
+ * Calculate the relative normalized error or deviation from any
192
+ * value to an accepted value. An error of 0 indicates that the
193
+ * two values are identical. An error of -0.1 indicates that the
194
+ * experimental value is 10% smaller than (90% of) the accepted
195
+ * value. An error of 1.0 indicates that the experimental value
196
+ * is 100% greater (or twice the size) of the accepted value.
197
+ * @param experimental The value observed or produced by a test
198
+ * @param actual The accepted or theoretical value
199
+ * @returns The relative (normalized) error
200
+ * @example
201
+ * const e = SMath.error(22.5, 25); // -0.1
202
+ */
203
+ export function error(experimental, actual) {
204
+ if (experimental === 0 && actual === 0) {
205
+ return 0;
206
+ }
207
+ else {
208
+ return (experimental - actual) / actual;
209
+ }
210
+ }
211
+ /**
212
+ * Add up all the inputs.
213
+ * If none are present, returns 0.
214
+ * @param data An array of numeric inputs
215
+ * @returns The sum total
216
+ * @example
217
+ * const y = SMath.sum([1, 2, 3]); // 6
218
+ */
219
+ export function sum(data) {
220
+ return data.reduce((a, b) => a + b, 0);
221
+ }
222
+ /**
223
+ * Multiply all the inputs.
224
+ * If none are present, returns 1.
225
+ * @param data An array of numeric inputs
226
+ * @returns The product
227
+ * @example
228
+ * const y = SMath.prod([2, 2, 3, 5]); // 60
229
+ */
230
+ export function prod(data) {
231
+ return data.reduce((a, b) => a * b, 1);
232
+ }
233
+ /**
234
+ * Compute the average, or mean, of a set of numbers.
235
+ * @param data An array of numeric inputs
236
+ * @returns The average, or mean
237
+ * @example
238
+ * const y = SMath.avg([1, 2, 4, 4]); // 2.75
239
+ */
240
+ export function avg(data) {
241
+ return sum(data) / data.length;
242
+ }
243
+ /**
244
+ * Compute the median of a set of numbers.
245
+ * @param data An array of numeric inputs
246
+ * @returns The median of the dataset
247
+ * @example
248
+ * const y = SMath.median([2, 5, 3, 1]); // 2.5
249
+ */
250
+ export function median(data) {
251
+ data.sort((a, b) => a - b);
252
+ if (data.length % 2) {
253
+ return data[(data.length - 1) / 2];
254
+ }
255
+ return avg([data[data.length / 2 - 1], data[data.length / 2]]);
256
+ }
257
+ /**
258
+ * Compute the variance of a **complete population**.
259
+ * @param data An array of numeric inputs
260
+ * @returns The population variance
261
+ * @example
262
+ * const y = SMath.varp([1, 2, 4, 4]); // 1.6875
263
+ */
264
+ export function varp(data) {
265
+ const mean = avg(data), squares = data.map(x => (x - mean) ** 2);
266
+ return sum(squares) / data.length;
267
+ }
268
+ /**
269
+ * Compute the variance of a **sample**.
270
+ * @param data An array of numeric inputs
271
+ * @returns The sample variance
272
+ * @example
273
+ * const y = SMath.vars([1, 2, 4, 4]); // 2.25
274
+ */
275
+ export function vars(data) {
276
+ const mean = avg(data), squares = data.map(x => (x - mean) ** 2);
277
+ return sum(squares) / (data.length - 1);
278
+ }
279
+ /**
280
+ * Compute the standard deviation of a **complete population**.
281
+ * @param data An array of numeric inputs
282
+ * @returns The population standard deviation
283
+ * @example
284
+ * const y = SMath.stdevp([1, 2, 3, 4]); // 1.118...
285
+ */
286
+ export function stdevp(data) {
287
+ return Math.sqrt(varp(data));
288
+ }
289
+ /**
290
+ * Compute the standard deviation of a **sample**.
291
+ * @param data An array of numeric inputs
292
+ * @returns The sample standard deviation
293
+ * @example
294
+ * const y = SMath.stdevs([1, 2, 3, 4]); // 1.29...
295
+ */
296
+ export function stdevs(data) {
297
+ return Math.sqrt(vars(data));
298
+ }
299
+ /**
300
+ * Generate a uniformly-distributed floating-point number within the range.
301
+ * @param min The minimum bound
302
+ * @param max The maximum bound
303
+ * @returns A random float within the range
304
+ * @example
305
+ * const y = SMath.runif(-2, 2); // 0.376...
306
+ */
307
+ export function runif(min, max) {
308
+ return expand(Math.random(), min, max);
309
+ }
310
+ /**
311
+ * Generate a uniformly-distributed integer within the range.
312
+ * @param min The minimum bound (inclusive)
313
+ * @param max The maximum bound (inclusive)
314
+ * @returns A random integer within the range
315
+ * @example
316
+ * const y = SMath.rint(-4, 3); // -4
317
+ */
318
+ export function rint(min, max) {
319
+ min |= 0;
320
+ max |= 0;
321
+ if (min < 0) {
322
+ min--;
323
+ }
324
+ if (max > 0) {
325
+ max++;
326
+ }
327
+ return clamp(runif(min, max), min, max) | 0; // `| 0` pulls toward 0
328
+ }
329
+ /**
330
+ * Generate a normally-distributed floating-point number.
331
+ * @param mean The mean of the population distribution
332
+ * @param stdev The standard deviation of the population
333
+ * @returns A random float
334
+ * @example
335
+ * const y = SMath.rnorm(2, 3); // 1.627...
336
+ */
337
+ export function rnorm(mean = 0, stdev = 1) {
338
+ return mean + stdev * Math.sqrt(-2 * Math.log(Math.random())) * Math.cos(2 * Math.PI * Math.random());
339
+ }
340
+ /**
341
+ * Generate a population of normally-distributed floating-point numbers.
342
+ * @param count The number of values to generate
343
+ * @param mean The mean of the population distribution
344
+ * @param stdev The standard deviation of the population
345
+ * @returns A population of random floats
346
+ * @example
347
+ * const dataset = SMath.rdist(3); // [ 1.051..., -0.779..., -2.254... ]
348
+ */
349
+ export function rdist(count, mean = 0, stdev = 1) {
350
+ const distribution = [];
351
+ for (let i = 0; i < count; i++) {
352
+ distribution[i] = rnorm(mean, stdev);
353
+ }
354
+ return distribution;
355
+ }
356
+ /**
357
+ * Randomize an array of arbitrary elements.
358
+ * @param stack An array of arbitrary elements
359
+ * @returns The `stack` array in a random order
360
+ * @example
361
+ * const shuffled = SMath.shuffle(['a', 'b', 'c']); // [ 'c', 'a', 'b' ]
362
+ */
363
+ export function shuffle(stack) {
364
+ const rawData = [];
365
+ for (const item of stack) {
366
+ rawData.push({ index: Math.random(), value: item });
367
+ }
368
+ return rawData.sort((a, b) => a.index - b.index).map(a => a.value);
369
+ }
370
+ /**
371
+ * Select a single item from an array at random with uniform weights.
372
+ * @param stack An array of arbirary item
373
+ * @returns A single randomly selected item
374
+ * @example
375
+ * const selected = SMath.selectRandom([10, 20, 30, 40]); // 30
376
+ */
377
+ export function selectRandom(stack) {
378
+ return stack[rint(0, stack.length - 1)];
379
+ }
380
+ /**
381
+ * Select a single index in an array at random with different weights.
382
+ * @param weights The weights for each item
383
+ * @returns The 0-based index of the randomly selected item
384
+ * @example
385
+ * const index = SMath.selectRandomWeighted([3.5, 4, 1]); // 1
386
+ */
387
+ export function selectRandomWeighted(weights) {
388
+ const startWeights = [];
389
+ let accumulation = 0;
390
+ for (const weight of weights) {
391
+ accumulation += clamp(0, weight, Infinity);
392
+ startWeights.push(accumulation);
393
+ }
394
+ const random = runif(0, accumulation);
395
+ return startWeights.findIndex(weight => random < weight);
396
+ }
397
+ /**
398
+ * Take the limit of a function. A return value of `NaN` indicates
399
+ * that no limit exists either due to a discontinuity or imaginary value.
400
+ * @param f Function `f(x)`
401
+ * @param x The x-value where to take the limit
402
+ * @param h The approach distance
403
+ * @param discontinuity_cutoff The discontinuity cutoff
404
+ * @returns `lim(f(x->x))`
405
+ * @example
406
+ * const y = SMath.lim(Math.log, 0); // -Infinity
407
+ */
408
+ export function lim(f, x, h = 1e-6, discontinuity_cutoff = 1e-3) {
409
+ const center = f(x), left1 = f(x - h), left2 = f(x - h / 2), right1 = f(x + h), right2 = f(x + h / 2);
410
+ let left, right;
411
+ if (Number.isFinite(center)) {
412
+ return center;
413
+ }
414
+ // Check the limit approaching from the left
415
+ if (Number.isFinite(left1) && Number.isFinite(left2)) {
416
+ if (approx(left1, left2, discontinuity_cutoff)) {
417
+ left = left2; // Converges
418
+ }
419
+ else if (left1 > 0 && left2 > left1) {
420
+ left = Infinity; // Diverges to +inf
421
+ }
422
+ else if (left1 < 0 && left2 < left1) {
423
+ left = -Infinity; // Diverges to -inf
424
+ }
425
+ else {
426
+ left = NaN; // Diverges
427
+ }
428
+ }
429
+ else {
430
+ left = NaN; // Discontinuous
431
+ }
432
+ // Check the limit approaching from the right
433
+ if (Number.isFinite(right1) && Number.isFinite(right2)) {
434
+ if (approx(right1, right2, discontinuity_cutoff)) {
435
+ right = right2; // Converges
436
+ }
437
+ else if (right1 > 0 && right2 > right1) {
438
+ right = Infinity; // Diverges to +inf
439
+ }
440
+ else if (right1 < 0 && right2 < right1) {
441
+ right = -Infinity; // Diverges to -inf
442
+ }
443
+ else {
444
+ right = NaN; // Diverges
445
+ }
446
+ }
447
+ else {
448
+ right = NaN; // Discontinuous
449
+ }
450
+ // Check if limits match or are close
451
+ if (left === right) { // Handles +/-Infinity case
452
+ return left;
453
+ }
454
+ else if (Number.isNaN(left) && Number.isNaN(right)) {
455
+ return center;
456
+ }
457
+ else if (!Number.isNaN(left) && Number.isNaN(right)) {
458
+ return left;
459
+ }
460
+ else if (Number.isNaN(left) && !Number.isNaN(right)) {
461
+ return right;
462
+ }
463
+ else if (approx(left, right, discontinuity_cutoff)) {
464
+ return avg([left, right]);
465
+ }
466
+ else {
467
+ return NaN;
468
+ }
469
+ }
470
+ /**
471
+ * Take the derivative of a function.
472
+ * @param f Function `f(x)`
473
+ * @param x The x-value where to evaluate the derivative
474
+ * @param epsilon Small step value
475
+ * @returns `f'(x)`
476
+ * @example
477
+ * const y = SMath.differentiate(x => 3 * x ** 2, 2); // 12
478
+ */
479
+ export function differentiate(f, x, epsilon = 1e-6) {
480
+ return lim(h => (f(x + h) - f(x - h)) / (2 * h), 0, epsilon);
481
+ }
482
+ /**
483
+ * Compute the definite integral of a function.
484
+ * @param f Function `f(x)`
485
+ * @param a The miminum integral bound
486
+ * @param b The maximum integral bound
487
+ * @param Ndx The number of rectangles to compute
488
+ * @returns `F(b)-F(a)`
489
+ * @example
490
+ * const y = SMath.integrate(x => 3 * x ** 2, 1, 2); // 7
491
+ */
492
+ export function integrate(f, a, b, Ndx = 1e6) {
493
+ return ((b - a) / Ndx) * sum(linspace(a, b, Ndx).map(x => f(x)));
494
+ }
495
+ /**
496
+ * Convert an arbitrary decimal number into a simplified fraction (or ratio).
497
+ * See `mixed()` for instructions on how to break out the whole number part.
498
+ * @param n The decimal number to convert
499
+ * @param epsilon Maximum absolute error
500
+ * @returns An object containing the fraction's numerator and denominator
501
+ * @example
502
+ * const frac = SMath.rat(0.625); // { num: 5, den: 8 }
503
+ */
504
+ export function rat(n, epsilon = 1e-6) {
505
+ let num = 0, den = 1;
506
+ const sign = n < 0 ? -1 : 1;
507
+ while (!approx(sign * n, num / den, epsilon)) {
508
+ if (sign * n > num / den) {
509
+ num++;
510
+ }
511
+ else {
512
+ den++;
513
+ }
514
+ }
515
+ return { num: sign * num, den: den };
516
+ }
517
+ /**
518
+ * Convert an arbitrary decimal number into a simplified fraction, after
519
+ * breaking out the whole number part first. See `rat()` for keeping the
520
+ * number as a ratio without separating the whole number part.
521
+ * @param n A decimal number to convert
522
+ * @param epsilon Maximum absolute error
523
+ * @returns An object containing the whole part and fraction numerator and denominator
524
+ * @example
525
+ * const frac = SMath.mixed(-8 / 6); // { whole: -1, num: 1, den: 3 }
526
+ */
527
+ export function mixed(n, epsilon = 1e-6) {
528
+ return { whole: n | 0, ...rat(n < -1 ? (n | 0) - n : n - (n | 0), epsilon) };
529
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smath",
3
- "version": "1.16.0",
3
+ "version": "2.1.0",
4
4
  "description": "Small math function library",
5
5
  "homepage": "https://npm.nicfv.com/",
6
6
  "type": "module",
@@ -39,7 +39,23 @@
39
39
  "extrapolate",
40
40
  "extrapolation",
41
41
  "linspace",
42
- "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"
43
59
  ],
44
60
  "author": {
45
61
  "name": "Nicolas Ventura",
@@ -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
+ }